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:
Binary file not shown.
|
After Width: | Height: | Size: 714 B |
@@ -7,7 +7,11 @@ import laptop from '../assets/hardware/laptop.png';
|
||||
import modem from '../assets/hardware/modem.png';
|
||||
import router from '../assets/hardware/router.png';
|
||||
import server from '../assets/hardware/server.png';
|
||||
import rj45 from '../assets/hardware/rj45.png';
|
||||
import networkSwitch from '../assets/hardware/switch.png';
|
||||
import type { IconName } from './layout.js';
|
||||
|
||||
export const ICON_URLS: Readonly<Record<IconName, string>> = { cloud, gateway, laptop, modem, router, server, switch: networkSwitch };
|
||||
|
||||
/** A card's socket, on the buttons of a router's connections (JConnectionsDialog). */
|
||||
export const RJ45_URL = rj45;
|
||||
|
||||
@@ -11,6 +11,13 @@
|
||||
"cable.wanOccupied": "WAN-Anschluss (extern) bereits belegt!",
|
||||
"cable.wirelessNotPossible": "Drahtlose Verbindung nicht möglich",
|
||||
"canvas.label": "Netzwerk",
|
||||
"connections.add": "Schnittstelle hinzufügen",
|
||||
"connections.fewest": "Eine Schnittstelle bleibt immer.",
|
||||
"connections.hint": "Hinweis: Zum Umstecken von Verbindungen:\n1. Verbindung auswählen durch Klick auf Schaltfläche links neben der Schnittstellenbezeichnung\n2. Auswählen der neuen Schnittstelle durch Klick auf die entsprechende Schaltfläche\nSollte die Zielschnittstelle bereits belegt sein, so werden die Verbindungen vertauscht.",
|
||||
"connections.local": "Lokale Schnittstellen",
|
||||
"connections.most": "Maximal 8 Schnittstellen erlaubt!",
|
||||
"connections.nic": "NIC {number}: {address}",
|
||||
"connections.remove": "Schnittstelle entfernen",
|
||||
"design.actions": "Aktionen",
|
||||
"design.palette": "Komponenten",
|
||||
"design.redo": "Wiederholen",
|
||||
@@ -88,6 +95,7 @@
|
||||
"sheet.cloud": "als Wolke darstellen",
|
||||
"sheet.connectedTo": "Verbunden mit {device}",
|
||||
"sheet.connection": "Anschluss",
|
||||
"sheet.connections": "Verbindungen verwalten",
|
||||
"sheet.defaultGateway": "Gateway",
|
||||
"sheet.deleteEntry": "Eintrag löschen",
|
||||
"sheet.dhcpClient": "DHCP zur Konfiguration verwenden",
|
||||
|
||||
@@ -11,6 +11,13 @@
|
||||
"cable.wanOccupied": "WAN port (external) already occupied!",
|
||||
"cable.wirelessNotPossible": "Wireless connection not possible",
|
||||
"canvas.label": "Network",
|
||||
"connections.add": "Add interface",
|
||||
"connections.fewest": "One interface always stays.",
|
||||
"connections.hint": "Note: For reorganising cable connections:\n1. Choose cable connection by clicking the button next to the interface name\n2. Choose new interface by clicking according button\nIf the target interface is already occupied by another connection, then those cables will be swapped.",
|
||||
"connections.local": "Local interfaces",
|
||||
"connections.most": "At most 8 interfaces supported!",
|
||||
"connections.nic": "NIC {number}: {address}",
|
||||
"connections.remove": "Remove interface",
|
||||
"design.actions": "Actions",
|
||||
"design.palette": "Components",
|
||||
"design.redo": "Redo",
|
||||
@@ -88,6 +95,7 @@
|
||||
"sheet.cloud": "Display as Cloud",
|
||||
"sheet.connectedTo": "Connected to {device}",
|
||||
"sheet.connection": "Connection",
|
||||
"sheet.connections": "Manage connections",
|
||||
"sheet.defaultGateway": "Gateway",
|
||||
"sheet.deleteEntry": "Delete entry",
|
||||
"sheet.dhcpClient": "Use DHCP for configuration",
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -33,6 +33,10 @@
|
||||
/* Java's active cable (JCablePanel.farbeBlinken): a frame on its way, or the card whose tab is open. */
|
||||
--canvas-cable-active: rgb(0 255 64);
|
||||
--canvas-label: #000000;
|
||||
/* Java's buttons of a router's connections (JConnectionsDialog): a card plugged, free, marked. */
|
||||
--connection-plugged: rgb(0 255 0);
|
||||
--connection-free: rgb(255 0 0);
|
||||
--connection-marked: rgb(255 255 0);
|
||||
}
|
||||
|
||||
:root[data-theme='dark'] {
|
||||
@@ -64,6 +68,9 @@
|
||||
--canvas-cable-wireless: rgb(150 150 150);
|
||||
--canvas-cable-active: rgb(0 200 50);
|
||||
--canvas-label: #000000;
|
||||
--connection-plugged: rgb(0 190 0);
|
||||
--connection-free: rgb(210 40 40);
|
||||
--connection-marked: rgb(235 215 0);
|
||||
}
|
||||
|
||||
html,
|
||||
|
||||
Reference in New Issue
Block a user