feat(editor): F4.5e a router's connections in the device sheet
SheetRun now clicks through Java's JConnectionsDialog with a router, a computer and a switch cabled in the GUIContainer: a click marks a card, the same click unmarks it, another card swaps the cables or moves the one; "+" adds 192.168.0.10/24 up to eight cards, "-" removes the last card with its cable down to none, and a router without a card keeps no gateway. The document adds, removes and swaps a router's cards (the same cables, the same devices), Java's click logic is one function for the test and the page, and the sheet names the address at a cable's far end. The page "Verbindungen verwalten" shows Java's RJ45 buttons in Java's colours with each card's far end below. Not adopted: the last card stays. A card tab or a mark whose card is undone away falls back. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,92 @@
|
||||
// A router's connections as a page of the device sheet, after Java's dialog
|
||||
// (JConnectionsDialog): each card with its RJ45 button left of its name and,
|
||||
// below, what its cable joins; a card's button marks it, another card's
|
||||
// button then swaps the two cards' cables (or moves the one), as Java's
|
||||
// buttons do; "+" adds a card after the last, "−" removes the last with its
|
||||
// cable — down to one card, not to none as in Java. Java's colours on the
|
||||
// buttons: green plugged, red free, yellow marked. Java draws its cables
|
||||
// between two columns; a narrow sheet has room for one.
|
||||
|
||||
import { useState } from 'react';
|
||||
import { addRouterInterface, connectionsClick, removeRouterInterface, ROUTER_INTERFACES, swapRouterCables, type RouterSheet } from '@mdg-filius/doc';
|
||||
import { RJ45_URL } from '../canvas/icons.js';
|
||||
import { useTranslate } from '../i18n/i18n.js';
|
||||
import type { Change } from './NameField.js';
|
||||
import { SheetNote } from './SheetField.js';
|
||||
|
||||
export function ConnectionsPanel({ sheet, change }: { readonly sheet: RouterSheet; readonly change: Change }) {
|
||||
const t = useTranslate();
|
||||
const [chosen, setChosen] = useState<number | null>(null);
|
||||
const cards = sheet.interfaces;
|
||||
// A card another person removed is no longer marked.
|
||||
const marked = chosen !== null && chosen < cards.length ? chosen : null;
|
||||
const click = (index: number) => {
|
||||
const next = connectionsClick(marked, index);
|
||||
if (next.swap !== undefined) {
|
||||
const [first, second] = next.swap;
|
||||
change.run(() => swapRouterCables(change.doc, change.id, cards[first]!.portId, cards[second]!.portId, change.origin));
|
||||
}
|
||||
setChosen(next.marked);
|
||||
};
|
||||
return (
|
||||
<>
|
||||
<h3 className="sheet-subtitle">{t('connections.local')}</h3>
|
||||
<ol className="connections">
|
||||
{cards.map((card, index) => {
|
||||
const name = t('connections.nic', { number: index + 1, address: card.ipAddress });
|
||||
const joined = card.connected;
|
||||
return (
|
||||
<li key={card.portId} data-state={index === marked ? 'marked' : joined === null ? 'free' : 'plugged'}>
|
||||
<button
|
||||
type="button"
|
||||
className="connection-port"
|
||||
aria-pressed={index === marked}
|
||||
aria-label={joined === null ? `${name}, ${t('sheet.notConnected')}` : name}
|
||||
onClick={() => click(index)}
|
||||
>
|
||||
<img src={RJ45_URL} alt="" />
|
||||
</button>
|
||||
<span className="connection-local">{name}</span>
|
||||
{joined === null ? null : <span className="connection-remote">{joined.address === null ? joined.device : `${joined.device} (${joined.address})`}</span>}
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ol>
|
||||
<div className="sheet-buttons">
|
||||
<button
|
||||
type="button"
|
||||
name="addInterface"
|
||||
aria-label={t('connections.add')}
|
||||
title={t('connections.add')}
|
||||
disabled={cards.length >= ROUTER_INTERFACES.maximum}
|
||||
onClick={() => {
|
||||
change.run(() => addRouterInterface(change.doc, change.id, crypto.randomUUID(), change.origin));
|
||||
setChosen(null);
|
||||
}}
|
||||
>
|
||||
+
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
name="removeInterface"
|
||||
aria-label={t('connections.remove')}
|
||||
title={t('connections.remove')}
|
||||
disabled={cards.length <= ROUTER_INTERFACES.fewest}
|
||||
onClick={() => {
|
||||
change.run(() => removeRouterInterface(change.doc, change.id, change.origin));
|
||||
setChosen(null);
|
||||
}}
|
||||
>
|
||||
−
|
||||
</button>
|
||||
</div>
|
||||
{cards.length >= ROUTER_INTERFACES.maximum ? (
|
||||
<p className="sheet-note">{t('connections.most')}</p>
|
||||
) : cards.length <= ROUTER_INTERFACES.fewest ? (
|
||||
<p className="sheet-note">{t('connections.fewest')}</p>
|
||||
) : null}
|
||||
{/* Below the cards, as in Java's dialog. */}
|
||||
<SheetNote text={t('connections.hint')} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -30,6 +30,7 @@ import { ICON_URLS } from '../canvas/icons.js';
|
||||
import { useTranslate } from '../i18n/i18n.js';
|
||||
import { NameField, type Change } from './NameField.js';
|
||||
import { HomeRouterFields, RouterFields } from './RouterSheets.js';
|
||||
import { ConnectionsPanel } from './ConnectionsPanel.js';
|
||||
import { FirewallPanel } from './FirewallPanel.js';
|
||||
import { DhcpServerPanel, PortForwardingPanel } from './ServicePanels.js';
|
||||
import { SheetCheck, SheetField } from './SheetField.js';
|
||||
@@ -62,7 +63,9 @@ export interface DeviceSheetProps {
|
||||
}
|
||||
|
||||
/** A page of the sheet that Java opens as a dialog. */
|
||||
export type SheetPage = 'dhcp' | 'forwarding' | 'firewall';
|
||||
export type SheetPage = 'dhcp' | 'forwarding' | 'firewall' | 'connections';
|
||||
|
||||
const PAGE_TITLES: Readonly<Record<SheetPage, string>> = { dhcp: 'sheet.dhcpServer', forwarding: 'sheet.portForwarding', firewall: 'sheet.firewall', connections: 'sheet.connections' };
|
||||
|
||||
export function DeviceSheet({ doc, id, label, origin, onChanged, onMessage, onClose, onHighlight }: DeviceSheetProps) {
|
||||
const t = useTranslate();
|
||||
@@ -92,7 +95,7 @@ export function DeviceSheet({ doc, id, label, origin, onChanged, onMessage, onCl
|
||||
‹
|
||||
</button>
|
||||
)}
|
||||
<h2>{open === 'dhcp' ? t('sheet.dhcpServer') : open === 'forwarding' ? t('sheet.portForwarding') : open === 'firewall' ? t('sheet.firewall') : title}</h2>
|
||||
<h2>{open === null ? title : t(PAGE_TITLES[open])}</h2>
|
||||
<button type="button" className="sheet-close" onClick={onClose} aria-label={t('sheet.close')} title={t('sheet.close')}>
|
||||
×
|
||||
</button>
|
||||
@@ -121,6 +124,10 @@ export function DeviceSheet({ doc, id, label, origin, onChanged, onMessage, onCl
|
||||
<div key={`${id}-firewall`}>
|
||||
<FirewallPanel firewall={sheet.firewall} change={change} />
|
||||
</div>
|
||||
) : open === 'connections' && sheet.kind === 'router' ? (
|
||||
<div key={`${id}-connections`}>
|
||||
<ConnectionsPanel sheet={sheet} change={change} />
|
||||
</div>
|
||||
) : open === 'forwarding' && sheet.kind === 'gateway' ? (
|
||||
<div key={`${id}-forwarding`}>
|
||||
<PortForwardingPanel sheet={sheet} change={change} />
|
||||
|
||||
@@ -22,7 +22,7 @@ import {
|
||||
import type { FirewallAction, FirewallProtocol, FirewallRule } from '@mdg-filius/core';
|
||||
import { useTranslate } from '../i18n/i18n.js';
|
||||
import type { Change } from './NameField.js';
|
||||
import { SheetCheck, SheetField } from './SheetField.js';
|
||||
import { SheetCheck, SheetField, SheetNote } from './SheetField.js';
|
||||
import { SheetTabs } from './SheetTabs.js';
|
||||
|
||||
/** Java's protocol names in the rule table (ruleAsVector): "*" for all. */
|
||||
@@ -32,19 +32,6 @@ const OFFERED: readonly FirewallProtocol[] = ['all', 'tcp', 'udp'];
|
||||
|
||||
const cellText = (rule: FirewallRule, column: (typeof FIREWALL_RULE_COLUMNS)[number]) => (column === 'port' ? (rule.port < 0 ? '' : String(rule.port)) : rule[column]);
|
||||
|
||||
/** A text with Java's line breaks as paragraphs. */
|
||||
function Note({ text }: { readonly text: string }) {
|
||||
return (
|
||||
<>
|
||||
{text.split('\n').map((line, index) => (
|
||||
<p key={index} className="sheet-note">
|
||||
{line}
|
||||
</p>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export function FirewallPanel({ firewall, change }: { readonly firewall: FirewallSheet; readonly change: Change }) {
|
||||
const t = useTranslate();
|
||||
const [tab, setTab] = useState('general');
|
||||
@@ -68,15 +55,15 @@ export function FirewallPanel({ firewall, change }: { readonly firewall: Firewal
|
||||
{tab === 'general' ? (
|
||||
<>
|
||||
<SheetCheck name="firewallActive" label={t('firewall.active')} checked={firewall.isActive} onChange={flag('isActive')} />
|
||||
<Note text={t('firewall.activeHint')} />
|
||||
<SheetNote text={t('firewall.activeHint')} />
|
||||
<SheetCheck name="dropIcmp" label={t('firewall.dropIcmp')} checked={firewall.dropIcmp} onChange={flag('dropIcmp')} />
|
||||
<Note text={t('firewall.dropIcmpHint')} />
|
||||
<SheetNote text={t('firewall.dropIcmpHint')} />
|
||||
<SheetCheck name="filterSyn" label={t('firewall.filterSyn')} checked={firewall.filterSynSegmentsOnly} onChange={flag('filterSynSegmentsOnly')} />
|
||||
<Note text={t('firewall.filterSynHint')} />
|
||||
<SheetNote text={t('firewall.filterSynHint')} />
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Note text={t('firewall.intro')} />
|
||||
<SheetNote text={t('firewall.intro')} />
|
||||
<label className="sheet-field">
|
||||
<span>{t('firewall.defaultPolicy')}</span>
|
||||
<select
|
||||
|
||||
@@ -3,8 +3,9 @@
|
||||
// general settings, each card and — on a router — the forwarding table
|
||||
// (JWeiterleitungsTabelle), greyed out while automatic routing (RIP) runs.
|
||||
// The card whose tab is open has its cable drawn active, as Java draws it.
|
||||
// The firewall, the port forwarding, the DHCP server and the change of the
|
||||
// cards follow with their dialogs (F4.5c).
|
||||
// The firewall, the port forwarding, the DHCP server and the router's
|
||||
// connections open as pages of the sheet (ServicePanels, FirewallPanel,
|
||||
// ConnectionsPanel).
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import {
|
||||
@@ -74,12 +75,13 @@ export function RouterFields({
|
||||
readonly sheet: RouterSheet;
|
||||
readonly change: Change;
|
||||
readonly onHighlight: (link: string | null) => void;
|
||||
readonly onOpen: (page: 'firewall') => void;
|
||||
readonly onOpen: (page: 'firewall' | 'connections') => void;
|
||||
}) {
|
||||
const t = useTranslate();
|
||||
const [tab, setTab] = useState('general');
|
||||
// Java greys the table out while RIP runs; an open table goes back to the general tab.
|
||||
const shown = tab === 'table' && sheet.rip ? 'general' : tab;
|
||||
// Java greys the table out while RIP runs; an open table goes back to the general tab, as does the tab of a card removed.
|
||||
const gone = tab.startsWith('card-') && Number(tab.slice(5)) >= sheet.interfaces.length;
|
||||
const shown = (tab === 'table' && sheet.rip) || gone ? 'general' : tab;
|
||||
const card = shown.startsWith('card-') ? sheet.interfaces[Number(shown.slice(5))] : undefined;
|
||||
useHighlight(card?.connected?.link ?? null, onHighlight);
|
||||
const tabs: Tab[] = [
|
||||
@@ -103,6 +105,9 @@ export function RouterFields({
|
||||
<button type="button" onClick={() => onOpen('firewall')}>
|
||||
{t('sheet.firewall')}
|
||||
</button>
|
||||
<button type="button" onClick={() => onOpen('connections')}>
|
||||
{t('sheet.connections')}
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
) : card !== undefined ? (
|
||||
|
||||
@@ -110,3 +110,16 @@ export function SheetCheck({
|
||||
</label>
|
||||
);
|
||||
}
|
||||
|
||||
/** A text with Java's line breaks as paragraphs. */
|
||||
export function SheetNote({ text }: { readonly text: string }) {
|
||||
return (
|
||||
<>
|
||||
{text.split('\n').map((line, index) => (
|
||||
<p key={index} className="sheet-note">
|
||||
{line}
|
||||
</p>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -252,3 +252,67 @@
|
||||
font-size: 13px;
|
||||
color: var(--danger);
|
||||
}
|
||||
|
||||
.sheet-subtitle {
|
||||
margin: 12px 0 4px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* A router's cards: Java's RJ45 button left of the card's name, what its
|
||||
* cable joins below, joined by a line like Java's cable. */
|
||||
.connections {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.connections li {
|
||||
display: grid;
|
||||
grid-template-columns: 52px 1fr;
|
||||
column-gap: 10px;
|
||||
align-items: center;
|
||||
padding: 4px 0;
|
||||
}
|
||||
|
||||
.connection-port {
|
||||
grid-row: span 2;
|
||||
width: 52px;
|
||||
height: 44px;
|
||||
padding: 0;
|
||||
border: 1px solid var(--border-strong);
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.connections li[data-state='plugged'] .connection-port {
|
||||
background: var(--connection-plugged);
|
||||
}
|
||||
|
||||
.connections li[data-state='free'] .connection-port {
|
||||
background: var(--connection-free);
|
||||
}
|
||||
|
||||
.connections li[data-state='marked'] .connection-port {
|
||||
background: var(--connection-marked);
|
||||
outline: 2px solid var(--border-focus);
|
||||
outline-offset: 1px;
|
||||
}
|
||||
|
||||
.connection-port img {
|
||||
display: block;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.connection-local {
|
||||
font-size: 14px;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.connection-remote {
|
||||
padding-left: 10px;
|
||||
border-left: 2px solid var(--canvas-cable);
|
||||
font-size: 13px;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user