diff --git a/CHANGES.md b/CHANGES.md index 9a3b0c6..5333830 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -127,6 +127,7 @@ Swift-Pfade relativ zu `filius-ipad/ios/FiliusPad/TopologyEditor/` (Stand | `src/settings.ts`, `src/sheet.ts` (Router, Heimrouter) | Java `JVermittlungsrechnerKonfiguration`, `JGatewayConfiguration`, `JWeiterleitungsTabelle`, `Weiterleitungstabelle.holeTabelle/validate/addManuellenEintrag` | erweitert (F4.5b): Adresse und Maske je Karte (die erste gespiegelt in den Einstellungen des Geräts, wie das Projekt sie hält), Standard-Gateway, RIP, die manuellen Zeilen der Weiterleitungstabelle (neue Zeile erst mit vier gültigen Zellen, Zelle, Löschen), DHCP am Heimrouter (lässt seinen DHCP-Server laufen, wie Java); das Blatt liest Karten mit dem Gerät am anderen Kabelende und die ganze Tabelle über `routeTableRows` des Kerns (gegen fünf von Java gemessene Tabellen). Abweichend von Java: Eine Kartenadresse, die das Muster, aber nicht `IP.ipCheck` besteht („.0.0.1“), wird nicht `null` (Javas Router-Panel wirft danach); eine abgelehnte Tabellenzelle wird nicht als `null` gespeichert; ein leeres Gateway ist nicht rot (Javas Router-Panel prüft es mit `musterIpAdresse`, übernimmt es aber) | | `src/settings.ts`, `src/sheet.ts`, `src/fields.ts` (DHCP-Server, Portweiterleitung) | Java `JDHCPKonfiguration`, `DHCPServer.determineGatewayip/determineDnsserverip/addStaticAssignment`, `JPortForwardingDialog`, `GatewayFirmware.validateData`, `EingabenUeberpruefung.musterMacAddress` | erweitert (F4.5c): Grenzen, Gateway und DNS des DHCP-Servers (wie Javas Dialog nie leer), „Manuelle Einstellungen“ und „DHCP aktivieren“, feste Zuordnungen (MAC und Adresse, eine MAC nur einmal, ohne Rücksicht auf Groß-/Kleinschreibung), die Zeilen der Portweiterleitung (TCP oder UDP, Ports nach `musterPort`, LAN-Adresse); das Blatt zeigt, was der Server ausgibt, wie Java es bestimmt (eigene Einstellung, sonst Gateway und DNS des Geräts oder 0.0.0.0; beim Heimrouter die LAN-Adresse) und welche Zeile Javas Firmware weiterleitet (`parsePortForwardingRow` des Kerns, jetzt exportiert). Abweichend von Java: Grenzen in `IP.ipCheck`-Form statt wie getippt („.0.0.1“ nimmt Java), MAC-Adressen groß und nur aus Hexziffern (Javas Klasse `[0-9a-fA-f]` lässt „_“ durch), eine zweite Zuordnung derselben MAC wird mit Meldung abgelehnt statt still verworfen, ungültige Portweiterleitungen werden nicht gespeichert (Java speichert jede, leitet aber nur gültige weiter) | | `src/settings.ts`, `src/sheet.ts`, `src/fields.ts` (Firewall) | Java `JFirewallDialog`, `GatewayFirewallConfigTable.closeEditor`, `Firewall.addRule/moveUp/moveDown/deleteRule`, `FirewallRule.validateExistingAttributes`, `EingabenUeberpruefung.isValidSubnetmask` | erweitert (F4.5d): Firewall an/aus, ICMP filtern, nur SYN verwerfen, Standardaktion, Regeln (neu wie Javas: leer, TCP, alle Ports, verwerfen; Zelle, Protokoll, Aktion, rauf/runter, löschen); Quelle leer, Adresse oder Javas Marke „999.999.999.999“, Ziel leer oder Adresse, Masken leer oder durchgehende Einsen, Port leer (alle) oder `musterPort`. Abweichend von Java: Javas Tabelle schreibt eine Zelle vor der Prüfung in die Regel, sodass die Firewall „abc“, Maske „255.0.255.0“ oder Port 0 hält, während die Tabelle den alten Wert zeigt — hier wird nichts übernommen, was die Prüfung ablehnt; ein Port, der keine Zahl ist, wird nicht still zu „alle Ports“; „.0.0.1“ und „080“ gelten nicht | +| `src/settings.ts`, `src/sheet.ts`, `src/commands.ts` (Verbindungen) | Java `JConnectionsDialog` (`swapConnection`, `removeConnection`, Knöpfe „+“/„-“), `Vermittlungsrechner.hinzuAnschluss/removeNic`, `NetzwerkInterface()`, `InternetKnotenBetriebssystem.setStandardGateway` | erweitert (F4.5e): eine Karte nach der letzten anfügen (192.168.0.10/24, eigene MAC aus der Port-Kennung, bis acht), die letzte samt Kabel entfernen, die Kabel zweier Karten tauschen oder eines umstecken (dieselben Kabel, dieselben Geräte; zwei freie Karten: nichts), Javas Klicklogik (`connectionsClick`: markieren, abwählen, tauschen); das Blatt nennt die Adresse der Gegenstelle (Rechner, Router; Switch und Modem ohne). Abweichend von Java: Java entfernt auch die letzte Karte — ein Router ohne Karte behält kein Gateway (Java legt es in die Karten, gemessen), und keine Datei könnte es halten; hier bleibt eine Karte (`ROUTER_INTERFACES.fewest`) | | `src/view.ts` (`nodeLabel`) | Java `Knoten.holeAnzeigeName` | herausgelöst (F4.5b): die Beschriftung eines Geräts, auch für „Verbunden mit …“ | | `test/sheet.test.ts`, `test/sheet.cases.txt`, `test/sheet.java.json` | — | neu (F4.5a): 77 in Javas Felder getippte Texte, jede Regel dagegen samt der benannten Abweichungen; die Befehle, die Funkverbindungen, Rückgängig je Änderung, alles über eine gespeicherte `.fls` | | `test/devices.test.ts`, `test/fixtures/new-devices.fls` | — | neu (F4.4a): neue Geräte jeder Art gegen Javas gespeicherte (Zufall maskiert), Kabelregeln, ein selbst gebautes Netz pingt im Kern, Rückgängig nur der eigenen Änderungen über zwei Replikate | @@ -153,7 +154,7 @@ Swift-Pfade relativ zu `filius-ipad/ios/FiliusPad/TopologyEditor/` (Stand | Datei | Quelle | Art der Übertragung | | --- | --- | --- | -| `src/assets/hardware/*.png` | Java-Filius `src/main/resources/gfx/hardware/` (GPL) | unverändert kopiert: Javas Standardsymbole (`IconMode.DEFAULT`), damit Arbeitsblätter passen | +| `src/assets/hardware/*.png` | Java-Filius `src/main/resources/gfx/hardware/` (GPL) | unverändert kopiert: Javas Standardsymbole (`IconMode.DEFAULT`), damit Arbeitsblätter passen; seit F4.5e auch `rj45.png` für die Knöpfe der Verbindungen | | `src/canvas/viewport.ts` | Dashboard `frontend/src/apps/kanban/zoom.js` (Rechte bei uns, Plan §7.1) | kopiert und umgebaut (F4.3): Grenzen, Stufen, Einpassen, Fingerrechnung; statt Rollposition ein Ausschnitt aus Maßstab und Versatz | | `src/canvas/layout.ts` | Java `JSidebarButton.getWidth/getHeight`, `JCablePanel.updateBounds/paintComponent`, `IconMode`, `Host.holeAnzeigeName` | neu nach Java (F4.3): Knopfgröße, Symbol oben mittig, Kabel als Javas quadratische Kurve zwischen den Knopfmitten (ganzzahlig wie Java); Beschriftungsschrift nach den Maßen der Dateien ab Java 2.5 | | `src/canvas/Canvas.tsx`, `src/canvas/ZoomBar.tsx`, `src/canvas/icons.ts`, `src/Editor.tsx`, `src/main.tsx` | Plan §7.1, §7.2 | neu (F4.3): die Fläche (SVG) mit Geräten, Kabeln (Funk gestrichelt), Doku-Elementen; Verschieben (Zeiger erst bei Bewegung gefangen), Strg+Rad und Trackpad-Pinch, zwei Finger, Zoomleiste mit 44-px-Zielen; Entwicklungsseite mit Beispielen | @@ -166,8 +167,9 @@ Swift-Pfade relativ zu `filius-ipad/ios/FiliusPad/TopologyEditor/` (Stand | `src/Editor.tsx`, `src/canvas/Canvas.tsx` (Werkzeuge) | Java `GUIEvents` (Kabel ziehen, Heimrouter-Hälften, Meldungen `guievents_msg1/12/22–26`) | neu (F4.4b): Auswählen, Gerät ziehen, Langdruck, Kabel ziehen oder zweimal antippen mit Gummiband und WAN/LAN-Hinweis, Javas Meldungen, Entf-Taste, Rückgängig/Wiederholen per Knopf und Strg+Z/Strg+Y; jede Änderung mit dem eigenen Ursprung des Editors | | `browser/design.spec.ts`, `browser/editorPage.ts` | — | neu (F4.4b): die Werkzeuge in Chromium mit Maus und Berührung (CDP) | | `src/sheet/DeviceSheet.tsx`, `src/sheet/SheetField.tsx`, `src/sheet/sheet.css` | Java `JKonfiguration` und die Panels je Art (Reihenfolge und Wörter `jhostkonfiguration_msg1`–`16`, `jswitchkonfiguration_msg1`–`5`, `jmodemkonfiguration_msg4`) | neu (F4.5a): das Geräte-Blatt — öffnet mit dem gewählten Gerät wie Javas Panel, neben der Fläche (340 px) oder auf dem Handy darunter; ein Feld wird beim Verlassen oder mit Enter übernommen, abgelehnter Text bleibt rot stehen mit dem, was weiter gilt (Java setzt still den alten Wert zurück), Escape verwirft ihn; Adressfelder bei DHCP gesperrt; jede Änderung ein Schritt im Rückgängig. Vermittlungsrechner und Heimrouter vorerst nur mit Namen | -| `browser/sheet.spec.ts` | — | neu (F4.5a): das Blatt in Chromium — Felder wie Java, Markieren und Behalten, Gerätewechsel, Rückgängig, WLAN, Handy mit Berührung, Englisch; seit F4.5b Router-Reiter, aktives Kabel, Weiterleitungstabelle, RIP, Heimrouter; seit F4.5c DHCP-Server und Portfreigaben; seit F4.5d Firewall | +| `browser/sheet.spec.ts` | — | neu (F4.5a): das Blatt in Chromium — Felder wie Java, Markieren und Behalten, Gerätewechsel, Rückgängig, WLAN, Handy mit Berührung, Englisch; seit F4.5b Router-Reiter, aktives Kabel, Weiterleitungstabelle, RIP, Heimrouter; seit F4.5c DHCP-Server und Portfreigaben; seit F4.5d Firewall; seit F4.5e Verbindungen (Tauschen, Umstecken, Karten bis acht und bis eine, Markierung nach Rückgängig) | | `src/sheet/FirewallPanel.tsx` | Java `JFirewallDialog` (`jfirewalldialog_msg21`–`43`, `jvermittlungsrechnerkonfiguration_msg4`) | neu (F4.5d): die Firewall als Seite des Blatts (bei Vermittlungsrechner und Heimrouter unter „Allgemein“): Reiter Allgemein mit den drei Schaltern und Javas Erklärungen, Reiter Firewall-Regeln mit Javas Einleitung, Standardaktion, Regeltabelle, rauf/runter (an den Enden gesperrt), Neue Regel, Regel löschen (die Nachfolgerin bleibt gewählt, wie bei Java). Javas erster Reiter heißt „Netzwerkschnittstellen“, enthält aber die allgemeinen Schalter — hier „Allgemein“; Javas „Aktion?“ ohne Fragezeichen, „über die Standard-Aktion“ groß | +| `src/sheet/ConnectionsPanel.tsx`, `src/sheet/SheetField.tsx` (`SheetNote`), `src/theme/tokens.css` (`--connection-*`) | Java `JConnectionsDialog` (`jvermittlungsrechnerkonfiguration_msg19`, `22`–`25`) | neu (F4.5e): „Verbindungen verwalten“ als Seite des Vermittlungsrechners (unter „Allgemein“ nach „Firewall einrichten“, wie bei Java): je Karte Javas RJ45-Knopf links von „NIC n: Adresse“ in Javas Farben (grün belegt, rot frei, gelb markiert), darunter die Gegenstelle wie bei Java („A (10.0.0.2)“, ein Switch nur mit Namen); antippen markiert, dieselbe abwählen, eine andere tauscht; „+“ und „−“ mit Javas „Schnittstelle hinzufügen“ und „Maximal 8 Schnittstellen erlaubt!“; Javas Hinweis darunter wie im Dialog. Java zeichnet die Kabel zwischen zwei Spalten — im schmalen Blatt steht die Gegenstelle unter der Karte; der Reiter einer entfernten Karte fällt auf „Allgemein“ zurück | | `src/sheet/ServicePanels.tsx`, `src/sheet/RowsEditor.tsx`, `src/sheet/SheetTabs.tsx`, `src/sheet/DeviceSheet.tsx` (Seiten) | Java `JDHCPKonfiguration` (`jdhcpkonfiguration_msg1`–`15`), `JPortForwardingDialog` (`jportforwarding_msg4`–`7`, `jgatewayconfiguration_msg19`) | neu (F4.5c): Javas Dialoge als Seiten des Blatts mit „Zurück“ — DHCP-Server (Grundeinstellungen, statische Adresszuweisung; beim Rechner, gesperrt während DHCP die Karte einrichtet, und auf dem LAN-Reiter des Heimrouters), Portfreigaben (Heimrouter); jedes Feld beim Verlassen übernommen statt mit Javas OK-Knopf. Die Zeilentabellen (Weiterleitung, Zuordnungen, Portfreigaben) teilen sich `RowsEditor`: eine neue Zeile, sobald alle Felder gültig sind, Zeile wählen, bearbeiten, löschen. Javas Tooltip zu „Manuelle Einstellungen“ als Text unter dem Kästchen (ein Finger hat kein Überfahren) | | `src/sheet/RouterSheets.tsx`, `src/sheet/NameField.tsx`, `src/canvas/Canvas.tsx` (`highlightedLink`), `src/theme/tokens.css` (`--canvas-cable-active`) | Java `JVermittlungsrechnerKonfiguration`, `JGatewayConfiguration`, `JWeiterleitungsTabelle` (Wörter `jvermittlungsrechnerkonfiguration_msg6`–`26`, `jweiterleitungstabelle_msg3`–`6`), `JCablePanel.farbeBlinken`, `Kabel.setAktiv` | neu (F4.5b): Blatt des Vermittlungsrechners — Reiter Allgemein (Name, Gateway, Automatisches Routing, IP-Weiterleitung nur angezeigt), je Karte (Reiter mit ihrer Adresse, „Verbunden mit …“, Adresse, Maske, MAC) und Weiterleitungstabelle (alle Einträge oder nur die manuellen, Javas Zeilen grau, eine neue Zeile, die mit vier gültigen Feldern übernommen wird, Zelle bearbeiten, Eintrag löschen; bei RIP gesperrt); Blatt des Heimrouters mit Allgemein, LAN, WAN (DHCP sperrt die WAN-Adresse). Das Kabel der offenen Karte leuchtet grün wie bei Java. Javas „Eintrag Löschen“ klein geschrieben | diff --git a/packages/doc/src/commands.ts b/packages/doc/src/commands.ts index e73142b..0a08bd3 100644 --- a/packages/doc/src/commands.ts +++ b/packages/doc/src/commands.ts @@ -33,8 +33,12 @@ import { emptyProject, inOrder, nodeEntry, ROOTS } from './projectDoc.js'; /** The kinds of the palette, in Java's order (GUIDesignSidebar.addComponentItemsToSidebar; the cable comes first, a tool). */ export const PALETTE: readonly Exclude[] = ['pc', 'notebook', 'networkSwitch', 'gateway', 'router', 'remoteLink']; -/** A router's interfaces, as Java's dialog offers them (2 to 8, 2 when it is closed). */ -export const ROUTER_INTERFACES = { minimum: 2, maximum: 8, standard: 2 } as const; +/** + * A router's interfaces, as Java's dialog offers them (2 to 8, 2 when it is + * closed); its connections dialog removes them down to `fewest` here (Java: + * none — see settings.ts). + */ +export const ROUTER_INTERFACES = { minimum: 2, maximum: 8, standard: 2, fewest: 1 } as const; export interface NewDevice { readonly kind: Exclude; diff --git a/packages/doc/src/settings.ts b/packages/doc/src/settings.ts index d72a4ab..bbd532c 100644 --- a/packages/doc/src/settings.ts +++ b/packages/doc/src/settings.ts @@ -27,7 +27,10 @@ // taken, and the sheet says why. Nor that Java's firewall table writes a // cell into the rule before it checks it, so that a rule holds "abc" or a // mask of 255.0.255.0 while the table shows the old value, and reads a port -// that is no number as "all ports". +// that is no number as "all ports". Nor that Java's connections dialog +// removes a router's last card too: a router without one keeps no gateway +// (setStandardGateway writes it into the cards; SheetRun's "connections"), +// nor could a file hold it; here one card stays. import * as Y from 'yjs'; import { @@ -47,7 +50,7 @@ import { type NodeKind, } from '@mdg-filius/core'; import { modemPair } from '@mdg-filius/fls'; -import { connect, type ConnectError } from './commands.js'; +import { connect, ROUTER_INTERFACES, type ConnectError } from './commands.js'; import { checkField, type CheckedField, type FieldRule } from './fields.js'; import { inOrder, ROOTS } from './projectDoc.js'; import { fromY, toY, type PlainValue } from './values.js'; @@ -295,6 +298,87 @@ export function setRip(doc: Y.Doc, id: Id, on: boolean, origin: unknown): void { }, origin); } +/** A router's ports, or none for another device. */ +function routerPorts(doc: Y.Doc, id: Id): Y.Array> | undefined { + const node = nodes(doc).get(id); + return node === undefined || kindOf(node) !== 'router' ? undefined : portsOf(node); +} + +/** + * A card added to a router after its last (JConnectionsDialog's "+": + * hinzuAnschluss, a new NetzwerkInterface with 192.168.0.10/24 and its own + * MAC address, here from the port's id), up to eight; says whether it was. + */ +export function addRouterInterface(doc: Y.Doc, id: Id, portId: Id, origin: unknown): boolean { + const ports = routerPorts(doc, id); + if (ports === undefined || ports.length >= ROUTER_INTERFACES.maximum) return false; + doc.transact(() => { + const port = new Y.Map(); + port.set('id', portId); + port.set('label', `rt${ports.length + 1}`); + port.set('occupied', false); + port.set('config', toY({ ipAddress: '192.168.0.10', subnetMask: '255.255.255.0' })); + ports.push([port]); + }, origin); + return true; +} + +type PluggedEnd = { readonly linkId: Id; readonly link: Y.Map; readonly end: 'sourcePort' | 'targetPort' }; + +/** The cables at a device's port, with the end that plugs into it. */ +function cablesAt(doc: Y.Doc, id: Id, portId: Id): PluggedEnd[] { + const ends: PluggedEnd[] = []; + for (const [linkId, link] of links(doc).entries()) { + if (link.get('sourceNode') === id && link.get('sourcePort') === portId) ends.push({ linkId, link, end: 'sourcePort' }); + else if (link.get('targetNode') === id && link.get('targetPort') === portId) ends.push({ linkId, link, end: 'targetPort' }); + } + return ends; +} + +/** A router's last card removed with its cable (JConnectionsDialog's "-"), down to one; says whether it was. */ +export function removeRouterInterface(doc: Y.Doc, id: Id, origin: unknown): boolean { + const ports = routerPorts(doc, id); + if (ports === undefined || ports.length <= ROUTER_INTERFACES.fewest) return false; + const last = ports.get(ports.length - 1).get('id') as Id; + doc.transact(() => { + for (const { linkId } of cablesAt(doc, id, last)) links(doc).delete(linkId); + ports.delete(ports.length - 1); + }, origin); + return true; +} + +/** + * Two of a router's cards trade their cables (JConnectionsDialog.swapConnection): + * both plugged, the cables swap; one plugged, its cable moves to the other + * card; neither, nothing changes. The cables stay the same cables, joining the + * same devices. Says whether a cable moved. + */ +export function swapRouterCables(doc: Y.Doc, id: Id, first: Id, second: Id, origin: unknown): boolean { + const ports = routerPorts(doc, id); + const has = (portId: Id) => ports?.toArray().some((port) => port.get('id') === portId) === true; + if (first === second || !has(first) || !has(second)) return false; + const atFirst = cablesAt(doc, id, first); + const atSecond = cablesAt(doc, id, second); + if (atFirst.length === 0 && atSecond.length === 0) return false; + doc.transact(() => { + for (const { link, end } of atFirst) link.set(end, second); + for (const { link, end } of atSecond) link.set(end, first); + }, origin); + return true; +} + +/** + * A click on a card's button in Java's connections dialog, with the card + * marked before (null for none): the first click marks a card, a click on it + * again unmarks it, a click on another card swaps the two cards' cables + * (swapRouterCables) and leaves none marked. + */ +export function connectionsClick(marked: number | null, clicked: number): { readonly marked: number | null; readonly swap?: readonly [number, number] } { + if (marked === null) return { marked: clicked }; + if (marked === clicked) return { marked: null }; + return { marked: null, swap: [marked, clicked] }; +} + /** A column of the forwarding table's manual rows. */ export type RouteColumn = keyof ManualRoute; diff --git a/packages/doc/src/sheet.ts b/packages/doc/src/sheet.ts index 84c312a..da49e94 100644 --- a/packages/doc/src/sheet.ts +++ b/packages/doc/src/sheet.ts @@ -88,8 +88,13 @@ export interface InterfaceSheet { readonly ipAddress: string; readonly subnetMask: string; readonly macAddress: string; - /** The device at the other end of its cable (its label, as Java names it) and the cable; null without one. */ - readonly connected: { readonly device: string; readonly link: string } | null; + /** + * The device at the other end of its cable (its label, as Java names it), + * the address of the card the cable plugs into there (a computer's, a + * router's; null for a switch or a modem, which have none) and the cable; + * null without one. + */ + readonly connected: { readonly device: string; readonly address: string | null; readonly link: string } | null; } /** A router's or home router's firewall as Java's dialog shows it (JFirewallDialog). */ @@ -142,6 +147,22 @@ const setting = (node: Y.Map, key: string) => settingsOf(node).get(key) const text = (map: Y.Map | undefined, key: string) => (map?.get(key) as string | undefined) ?? ''; +/** The address of a device's card at a port, as Java's NetzwerkInterface holds it; null for a device without cards (InternetKnoten). */ +function cardAddress(node: Y.Map, portId: string): string | null { + switch (node.get('kind') as NodeKind) { + case 'pc': + case 'notebook': + return text(setting(node, 'device'), 'ipAddress'); + case 'router': + case 'gateway': { + const port = (node.get('ports') as Y.Array>).toArray().find((each) => each.get('id') === portId); + return text(port?.get('config') as Y.Map | undefined, 'ipAddress'); + } + default: + return null; + } +} + /** A router's or home router's cards, each with what its cable joins. */ function interfacesOf(doc: Y.Doc, id: string, node: Y.Map): InterfaceSheet[] { const all = doc.getMap>(ROOTS.nodes); @@ -150,14 +171,14 @@ function interfacesOf(doc: Y.Doc, id: string, node: Y.Map): InterfaceSh const portId = port.get('id') as string; const config = port.get('config') as Y.Map | undefined; const cable = links.find(([, link]) => (link.get('sourceNode') === id && link.get('sourcePort') === portId) || (link.get('targetNode') === id && link.get('targetPort') === portId)); - const other = - cable === undefined ? undefined : all.get((cable[1].get('sourceNode') === id && cable[1].get('sourcePort') === portId ? cable[1].get('targetNode') : cable[1].get('sourceNode')) as string); + const near = cable !== undefined && cable[1].get('sourceNode') === id && cable[1].get('sourcePort') === portId; + const other = cable === undefined ? undefined : all.get(cable[1].get(near ? 'targetNode' : 'sourceNode') as string); return { portId, ipAddress: text(config, 'ipAddress'), subnetMask: text(config, 'subnetMask'), macAddress: (port.get('mac') as string | undefined) ?? stableGeneratedMacAddress(portId), - connected: cable === undefined || other === undefined ? null : { device: nodeLabel(other), link: cable[0] }, + connected: cable === undefined || other === undefined ? null : { device: nodeLabel(other), address: cardAddress(other, cable[1].get(near ? 'targetPort' : 'sourcePort') as string), link: cable[0] }, }; }); } diff --git a/packages/doc/test/sheet.java.json b/packages/doc/test/sheet.java.json index 5371b48..8678ef1 100644 --- a/packages/doc/test/sheet.java.json +++ b/packages/doc/test/sheet.java.json @@ -1209,7 +1209,7 @@ } ], "flows": { - "useMacAsNameThenOff": "5E:C4:F7:30:10:4A", + "useMacAsNameThenOff": "42:F9:7A:9A:D9:3D", "useIpAsNameThenOff": "PC-A", "dhcpClientStopsServer": true, "dhcpClient": true, @@ -2283,6 +2283,1947 @@ ] ] }, + "connections": { + "steps": [ + { + "click": "start", + "local": [ + "NIC 1: 10.0.0.1", + "NIC 2: 10.1.0.1", + "NIC 3: 10.2.0.1", + "", + "", + "", + "", + "" + ], + "remote": [ + "A (10.0.0.2)", + "S", + "", + "", + "", + "", + "", + "" + ], + "buttons": [ + "plugged", + "plugged", + "free", + "off", + "off", + "off", + "off", + "off" + ], + "add": true, + "remove": true, + "cards": [ + [ + "10.0.0.1", + "255.255.255.0", + "A" + ], + [ + "10.1.0.1", + "255.255.255.0", + "S" + ], + [ + "10.2.0.1", + "255.255.255.0", + "" + ] + ], + "cables": 2 + }, + { + "click": "c0", + "local": [ + "NIC 1: 10.0.0.1", + "NIC 2: 10.1.0.1", + "NIC 3: 10.2.0.1", + "", + "", + "", + "", + "" + ], + "remote": [ + "A (10.0.0.2)", + "S", + "", + "", + "", + "", + "", + "" + ], + "buttons": [ + "marked", + "plugged", + "free", + "off", + "off", + "off", + "off", + "off" + ], + "add": true, + "remove": true, + "cards": [ + [ + "10.0.0.1", + "255.255.255.0", + "A" + ], + [ + "10.1.0.1", + "255.255.255.0", + "S" + ], + [ + "10.2.0.1", + "255.255.255.0", + "" + ] + ], + "cables": 2 + }, + { + "click": "c0", + "local": [ + "NIC 1: 10.0.0.1", + "NIC 2: 10.1.0.1", + "NIC 3: 10.2.0.1", + "", + "", + "", + "", + "" + ], + "remote": [ + "A (10.0.0.2)", + "S", + "", + "", + "", + "", + "", + "" + ], + "buttons": [ + "plugged", + "plugged", + "free", + "off", + "off", + "off", + "off", + "off" + ], + "add": true, + "remove": true, + "cards": [ + [ + "10.0.0.1", + "255.255.255.0", + "A" + ], + [ + "10.1.0.1", + "255.255.255.0", + "S" + ], + [ + "10.2.0.1", + "255.255.255.0", + "" + ] + ], + "cables": 2 + }, + { + "click": "c0", + "local": [ + "NIC 1: 10.0.0.1", + "NIC 2: 10.1.0.1", + "NIC 3: 10.2.0.1", + "", + "", + "", + "", + "" + ], + "remote": [ + "A (10.0.0.2)", + "S", + "", + "", + "", + "", + "", + "" + ], + "buttons": [ + "marked", + "plugged", + "free", + "off", + "off", + "off", + "off", + "off" + ], + "add": true, + "remove": true, + "cards": [ + [ + "10.0.0.1", + "255.255.255.0", + "A" + ], + [ + "10.1.0.1", + "255.255.255.0", + "S" + ], + [ + "10.2.0.1", + "255.255.255.0", + "" + ] + ], + "cables": 2 + }, + { + "click": "c2", + "local": [ + "NIC 1: 10.0.0.1", + "NIC 2: 10.1.0.1", + "NIC 3: 10.2.0.1", + "", + "", + "", + "", + "" + ], + "remote": [ + "", + "S", + "A (10.0.0.2)", + "", + "", + "", + "", + "" + ], + "buttons": [ + "free", + "plugged", + "plugged", + "off", + "off", + "off", + "off", + "off" + ], + "add": true, + "remove": true, + "cards": [ + [ + "10.0.0.1", + "255.255.255.0", + "" + ], + [ + "10.1.0.1", + "255.255.255.0", + "S" + ], + [ + "10.2.0.1", + "255.255.255.0", + "A" + ] + ], + "cables": 2 + }, + { + "click": "c2", + "local": [ + "NIC 1: 10.0.0.1", + "NIC 2: 10.1.0.1", + "NIC 3: 10.2.0.1", + "", + "", + "", + "", + "" + ], + "remote": [ + "", + "S", + "A (10.0.0.2)", + "", + "", + "", + "", + "" + ], + "buttons": [ + "free", + "plugged", + "marked", + "off", + "off", + "off", + "off", + "off" + ], + "add": true, + "remove": true, + "cards": [ + [ + "10.0.0.1", + "255.255.255.0", + "" + ], + [ + "10.1.0.1", + "255.255.255.0", + "S" + ], + [ + "10.2.0.1", + "255.255.255.0", + "A" + ] + ], + "cables": 2 + }, + { + "click": "c1", + "local": [ + "NIC 1: 10.0.0.1", + "NIC 2: 10.1.0.1", + "NIC 3: 10.2.0.1", + "", + "", + "", + "", + "" + ], + "remote": [ + "", + "A (10.0.0.2)", + "S", + "", + "", + "", + "", + "" + ], + "buttons": [ + "free", + "plugged", + "plugged", + "off", + "off", + "off", + "off", + "off" + ], + "add": true, + "remove": true, + "cards": [ + [ + "10.0.0.1", + "255.255.255.0", + "" + ], + [ + "10.1.0.1", + "255.255.255.0", + "A" + ], + [ + "10.2.0.1", + "255.255.255.0", + "S" + ] + ], + "cables": 2 + }, + { + "click": "c1", + "local": [ + "NIC 1: 10.0.0.1", + "NIC 2: 10.1.0.1", + "NIC 3: 10.2.0.1", + "", + "", + "", + "", + "" + ], + "remote": [ + "", + "A (10.0.0.2)", + "S", + "", + "", + "", + "", + "" + ], + "buttons": [ + "free", + "marked", + "plugged", + "off", + "off", + "off", + "off", + "off" + ], + "add": true, + "remove": true, + "cards": [ + [ + "10.0.0.1", + "255.255.255.0", + "" + ], + [ + "10.1.0.1", + "255.255.255.0", + "A" + ], + [ + "10.2.0.1", + "255.255.255.0", + "S" + ] + ], + "cables": 2 + }, + { + "click": "c1", + "local": [ + "NIC 1: 10.0.0.1", + "NIC 2: 10.1.0.1", + "NIC 3: 10.2.0.1", + "", + "", + "", + "", + "" + ], + "remote": [ + "", + "A (10.0.0.2)", + "S", + "", + "", + "", + "", + "" + ], + "buttons": [ + "free", + "plugged", + "plugged", + "off", + "off", + "off", + "off", + "off" + ], + "add": true, + "remove": true, + "cards": [ + [ + "10.0.0.1", + "255.255.255.0", + "" + ], + [ + "10.1.0.1", + "255.255.255.0", + "A" + ], + [ + "10.2.0.1", + "255.255.255.0", + "S" + ] + ], + "cables": 2 + }, + { + "click": "c0", + "local": [ + "NIC 1: 10.0.0.1", + "NIC 2: 10.1.0.1", + "NIC 3: 10.2.0.1", + "", + "", + "", + "", + "" + ], + "remote": [ + "", + "A (10.0.0.2)", + "S", + "", + "", + "", + "", + "" + ], + "buttons": [ + "marked", + "plugged", + "plugged", + "off", + "off", + "off", + "off", + "off" + ], + "add": true, + "remove": true, + "cards": [ + [ + "10.0.0.1", + "255.255.255.0", + "" + ], + [ + "10.1.0.1", + "255.255.255.0", + "A" + ], + [ + "10.2.0.1", + "255.255.255.0", + "S" + ] + ], + "cables": 2 + }, + { + "click": "+", + "local": [ + "NIC 1: 10.0.0.1", + "NIC 2: 10.1.0.1", + "NIC 3: 10.2.0.1", + "NIC 4: 192.168.0.10", + "", + "", + "", + "" + ], + "remote": [ + "", + "A (10.0.0.2)", + "S", + "", + "", + "", + "", + "" + ], + "buttons": [ + "free", + "plugged", + "plugged", + "free", + "off", + "off", + "off", + "off" + ], + "add": true, + "remove": true, + "cards": [ + [ + "10.0.0.1", + "255.255.255.0", + "" + ], + [ + "10.1.0.1", + "255.255.255.0", + "A" + ], + [ + "10.2.0.1", + "255.255.255.0", + "S" + ], + [ + "192.168.0.10", + "255.255.255.0", + "" + ] + ], + "cables": 2 + }, + { + "click": "c2", + "local": [ + "NIC 1: 10.0.0.1", + "NIC 2: 10.1.0.1", + "NIC 3: 10.2.0.1", + "NIC 4: 192.168.0.10", + "", + "", + "", + "" + ], + "remote": [ + "", + "A (10.0.0.2)", + "S", + "", + "", + "", + "", + "" + ], + "buttons": [ + "free", + "plugged", + "marked", + "free", + "off", + "off", + "off", + "off" + ], + "add": true, + "remove": true, + "cards": [ + [ + "10.0.0.1", + "255.255.255.0", + "" + ], + [ + "10.1.0.1", + "255.255.255.0", + "A" + ], + [ + "10.2.0.1", + "255.255.255.0", + "S" + ], + [ + "192.168.0.10", + "255.255.255.0", + "" + ] + ], + "cables": 2 + }, + { + "click": "c3", + "local": [ + "NIC 1: 10.0.0.1", + "NIC 2: 10.1.0.1", + "NIC 3: 10.2.0.1", + "NIC 4: 192.168.0.10", + "", + "", + "", + "" + ], + "remote": [ + "", + "A (10.0.0.2)", + "", + "S", + "", + "", + "", + "" + ], + "buttons": [ + "free", + "plugged", + "free", + "plugged", + "off", + "off", + "off", + "off" + ], + "add": true, + "remove": true, + "cards": [ + [ + "10.0.0.1", + "255.255.255.0", + "" + ], + [ + "10.1.0.1", + "255.255.255.0", + "A" + ], + [ + "10.2.0.1", + "255.255.255.0", + "" + ], + [ + "192.168.0.10", + "255.255.255.0", + "S" + ] + ], + "cables": 2 + }, + { + "click": "-", + "local": [ + "NIC 1: 10.0.0.1", + "NIC 2: 10.1.0.1", + "NIC 3: 10.2.0.1", + "", + "", + "", + "", + "" + ], + "remote": [ + "", + "A (10.0.0.2)", + "", + "", + "", + "", + "", + "" + ], + "buttons": [ + "free", + "plugged", + "free", + "off", + "off", + "off", + "off", + "off" + ], + "add": true, + "remove": true, + "cards": [ + [ + "10.0.0.1", + "255.255.255.0", + "" + ], + [ + "10.1.0.1", + "255.255.255.0", + "A" + ], + [ + "10.2.0.1", + "255.255.255.0", + "" + ] + ], + "cables": 1 + }, + { + "click": "-", + "local": [ + "NIC 1: 10.0.0.1", + "NIC 2: 10.1.0.1", + "", + "", + "", + "", + "", + "" + ], + "remote": [ + "", + "A (10.0.0.2)", + "", + "", + "", + "", + "", + "" + ], + "buttons": [ + "free", + "plugged", + "off", + "off", + "off", + "off", + "off", + "off" + ], + "add": true, + "remove": true, + "cards": [ + [ + "10.0.0.1", + "255.255.255.0", + "" + ], + [ + "10.1.0.1", + "255.255.255.0", + "A" + ] + ], + "cables": 1 + }, + { + "click": "+", + "local": [ + "NIC 1: 10.0.0.1", + "NIC 2: 10.1.0.1", + "NIC 3: 192.168.0.10", + "", + "", + "", + "", + "" + ], + "remote": [ + "", + "A (10.0.0.2)", + "", + "", + "", + "", + "", + "" + ], + "buttons": [ + "free", + "plugged", + "free", + "off", + "off", + "off", + "off", + "off" + ], + "add": true, + "remove": true, + "cards": [ + [ + "10.0.0.1", + "255.255.255.0", + "" + ], + [ + "10.1.0.1", + "255.255.255.0", + "A" + ], + [ + "192.168.0.10", + "255.255.255.0", + "" + ] + ], + "cables": 1 + }, + { + "click": "c0", + "local": [ + "NIC 1: 10.0.0.1", + "NIC 2: 10.1.0.1", + "NIC 3: 192.168.0.10", + "", + "", + "", + "", + "" + ], + "remote": [ + "", + "A (10.0.0.2)", + "", + "", + "", + "", + "", + "" + ], + "buttons": [ + "marked", + "plugged", + "free", + "off", + "off", + "off", + "off", + "off" + ], + "add": true, + "remove": true, + "cards": [ + [ + "10.0.0.1", + "255.255.255.0", + "" + ], + [ + "10.1.0.1", + "255.255.255.0", + "A" + ], + [ + "192.168.0.10", + "255.255.255.0", + "" + ] + ], + "cables": 1 + }, + { + "click": "c2", + "local": [ + "NIC 1: 10.0.0.1", + "NIC 2: 10.1.0.1", + "NIC 3: 192.168.0.10", + "", + "", + "", + "", + "" + ], + "remote": [ + "", + "A (10.0.0.2)", + "", + "", + "", + "", + "", + "" + ], + "buttons": [ + "free", + "plugged", + "free", + "off", + "off", + "off", + "off", + "off" + ], + "add": true, + "remove": true, + "cards": [ + [ + "10.0.0.1", + "255.255.255.0", + "" + ], + [ + "10.1.0.1", + "255.255.255.0", + "A" + ], + [ + "192.168.0.10", + "255.255.255.0", + "" + ] + ], + "cables": 1 + }, + { + "click": "c0", + "local": [ + "NIC 1: 10.0.0.1", + "NIC 2: 10.1.0.1", + "NIC 3: 192.168.0.10", + "", + "", + "", + "", + "" + ], + "remote": [ + "", + "A (10.0.0.2)", + "", + "", + "", + "", + "", + "" + ], + "buttons": [ + "marked", + "plugged", + "free", + "off", + "off", + "off", + "off", + "off" + ], + "add": true, + "remove": true, + "cards": [ + [ + "10.0.0.1", + "255.255.255.0", + "" + ], + [ + "10.1.0.1", + "255.255.255.0", + "A" + ], + [ + "192.168.0.10", + "255.255.255.0", + "" + ] + ], + "cables": 1 + }, + { + "click": "c1", + "local": [ + "NIC 1: 10.0.0.1", + "NIC 2: 10.1.0.1", + "NIC 3: 192.168.0.10", + "", + "", + "", + "", + "" + ], + "remote": [ + "A (10.0.0.2)", + "", + "", + "", + "", + "", + "", + "" + ], + "buttons": [ + "plugged", + "free", + "free", + "off", + "off", + "off", + "off", + "off" + ], + "add": true, + "remove": true, + "cards": [ + [ + "10.0.0.1", + "255.255.255.0", + "A" + ], + [ + "10.1.0.1", + "255.255.255.0", + "" + ], + [ + "192.168.0.10", + "255.255.255.0", + "" + ] + ], + "cables": 1 + }, + { + "click": "+", + "local": [ + "NIC 1: 10.0.0.1", + "NIC 2: 10.1.0.1", + "NIC 3: 192.168.0.10", + "NIC 4: 192.168.0.10", + "", + "", + "", + "" + ], + "remote": [ + "A (10.0.0.2)", + "", + "", + "", + "", + "", + "", + "" + ], + "buttons": [ + "plugged", + "free", + "free", + "free", + "off", + "off", + "off", + "off" + ], + "add": true, + "remove": true, + "cards": [ + [ + "10.0.0.1", + "255.255.255.0", + "A" + ], + [ + "10.1.0.1", + "255.255.255.0", + "" + ], + [ + "192.168.0.10", + "255.255.255.0", + "" + ], + [ + "192.168.0.10", + "255.255.255.0", + "" + ] + ], + "cables": 1 + }, + { + "click": "+", + "local": [ + "NIC 1: 10.0.0.1", + "NIC 2: 10.1.0.1", + "NIC 3: 192.168.0.10", + "NIC 4: 192.168.0.10", + "NIC 5: 192.168.0.10", + "", + "", + "" + ], + "remote": [ + "A (10.0.0.2)", + "", + "", + "", + "", + "", + "", + "" + ], + "buttons": [ + "plugged", + "free", + "free", + "free", + "free", + "off", + "off", + "off" + ], + "add": true, + "remove": true, + "cards": [ + [ + "10.0.0.1", + "255.255.255.0", + "A" + ], + [ + "10.1.0.1", + "255.255.255.0", + "" + ], + [ + "192.168.0.10", + "255.255.255.0", + "" + ], + [ + "192.168.0.10", + "255.255.255.0", + "" + ], + [ + "192.168.0.10", + "255.255.255.0", + "" + ] + ], + "cables": 1 + }, + { + "click": "+", + "local": [ + "NIC 1: 10.0.0.1", + "NIC 2: 10.1.0.1", + "NIC 3: 192.168.0.10", + "NIC 4: 192.168.0.10", + "NIC 5: 192.168.0.10", + "NIC 6: 192.168.0.10", + "", + "" + ], + "remote": [ + "A (10.0.0.2)", + "", + "", + "", + "", + "", + "", + "" + ], + "buttons": [ + "plugged", + "free", + "free", + "free", + "free", + "free", + "off", + "off" + ], + "add": true, + "remove": true, + "cards": [ + [ + "10.0.0.1", + "255.255.255.0", + "A" + ], + [ + "10.1.0.1", + "255.255.255.0", + "" + ], + [ + "192.168.0.10", + "255.255.255.0", + "" + ], + [ + "192.168.0.10", + "255.255.255.0", + "" + ], + [ + "192.168.0.10", + "255.255.255.0", + "" + ], + [ + "192.168.0.10", + "255.255.255.0", + "" + ] + ], + "cables": 1 + }, + { + "click": "+", + "local": [ + "NIC 1: 10.0.0.1", + "NIC 2: 10.1.0.1", + "NIC 3: 192.168.0.10", + "NIC 4: 192.168.0.10", + "NIC 5: 192.168.0.10", + "NIC 6: 192.168.0.10", + "NIC 7: 192.168.0.10", + "" + ], + "remote": [ + "A (10.0.0.2)", + "", + "", + "", + "", + "", + "", + "" + ], + "buttons": [ + "plugged", + "free", + "free", + "free", + "free", + "free", + "free", + "off" + ], + "add": true, + "remove": true, + "cards": [ + [ + "10.0.0.1", + "255.255.255.0", + "A" + ], + [ + "10.1.0.1", + "255.255.255.0", + "" + ], + [ + "192.168.0.10", + "255.255.255.0", + "" + ], + [ + "192.168.0.10", + "255.255.255.0", + "" + ], + [ + "192.168.0.10", + "255.255.255.0", + "" + ], + [ + "192.168.0.10", + "255.255.255.0", + "" + ], + [ + "192.168.0.10", + "255.255.255.0", + "" + ] + ], + "cables": 1 + }, + { + "click": "+", + "local": [ + "NIC 1: 10.0.0.1", + "NIC 2: 10.1.0.1", + "NIC 3: 192.168.0.10", + "NIC 4: 192.168.0.10", + "NIC 5: 192.168.0.10", + "NIC 6: 192.168.0.10", + "NIC 7: 192.168.0.10", + "NIC 8: 192.168.0.10" + ], + "remote": [ + "A (10.0.0.2)", + "", + "", + "", + "", + "", + "", + "" + ], + "buttons": [ + "plugged", + "free", + "free", + "free", + "free", + "free", + "free", + "free" + ], + "add": false, + "remove": true, + "cards": [ + [ + "10.0.0.1", + "255.255.255.0", + "A" + ], + [ + "10.1.0.1", + "255.255.255.0", + "" + ], + [ + "192.168.0.10", + "255.255.255.0", + "" + ], + [ + "192.168.0.10", + "255.255.255.0", + "" + ], + [ + "192.168.0.10", + "255.255.255.0", + "" + ], + [ + "192.168.0.10", + "255.255.255.0", + "" + ], + [ + "192.168.0.10", + "255.255.255.0", + "" + ], + [ + "192.168.0.10", + "255.255.255.0", + "" + ] + ], + "cables": 1 + }, + { + "click": "+", + "local": [ + "NIC 1: 10.0.0.1", + "NIC 2: 10.1.0.1", + "NIC 3: 192.168.0.10", + "NIC 4: 192.168.0.10", + "NIC 5: 192.168.0.10", + "NIC 6: 192.168.0.10", + "NIC 7: 192.168.0.10", + "NIC 8: 192.168.0.10" + ], + "remote": [ + "A (10.0.0.2)", + "", + "", + "", + "", + "", + "", + "" + ], + "buttons": [ + "plugged", + "free", + "free", + "free", + "free", + "free", + "free", + "free" + ], + "add": false, + "remove": true, + "cards": [ + [ + "10.0.0.1", + "255.255.255.0", + "A" + ], + [ + "10.1.0.1", + "255.255.255.0", + "" + ], + [ + "192.168.0.10", + "255.255.255.0", + "" + ], + [ + "192.168.0.10", + "255.255.255.0", + "" + ], + [ + "192.168.0.10", + "255.255.255.0", + "" + ], + [ + "192.168.0.10", + "255.255.255.0", + "" + ], + [ + "192.168.0.10", + "255.255.255.0", + "" + ], + [ + "192.168.0.10", + "255.255.255.0", + "" + ] + ], + "cables": 1 + }, + { + "click": "-", + "local": [ + "NIC 1: 10.0.0.1", + "NIC 2: 10.1.0.1", + "NIC 3: 192.168.0.10", + "NIC 4: 192.168.0.10", + "NIC 5: 192.168.0.10", + "NIC 6: 192.168.0.10", + "NIC 7: 192.168.0.10", + "" + ], + "remote": [ + "A (10.0.0.2)", + "", + "", + "", + "", + "", + "", + "" + ], + "buttons": [ + "plugged", + "free", + "free", + "free", + "free", + "free", + "free", + "off" + ], + "add": true, + "remove": true, + "cards": [ + [ + "10.0.0.1", + "255.255.255.0", + "A" + ], + [ + "10.1.0.1", + "255.255.255.0", + "" + ], + [ + "192.168.0.10", + "255.255.255.0", + "" + ], + [ + "192.168.0.10", + "255.255.255.0", + "" + ], + [ + "192.168.0.10", + "255.255.255.0", + "" + ], + [ + "192.168.0.10", + "255.255.255.0", + "" + ], + [ + "192.168.0.10", + "255.255.255.0", + "" + ] + ], + "cables": 1 + }, + { + "click": "-", + "local": [ + "NIC 1: 10.0.0.1", + "NIC 2: 10.1.0.1", + "NIC 3: 192.168.0.10", + "NIC 4: 192.168.0.10", + "NIC 5: 192.168.0.10", + "NIC 6: 192.168.0.10", + "", + "" + ], + "remote": [ + "A (10.0.0.2)", + "", + "", + "", + "", + "", + "", + "" + ], + "buttons": [ + "plugged", + "free", + "free", + "free", + "free", + "free", + "off", + "off" + ], + "add": true, + "remove": true, + "cards": [ + [ + "10.0.0.1", + "255.255.255.0", + "A" + ], + [ + "10.1.0.1", + "255.255.255.0", + "" + ], + [ + "192.168.0.10", + "255.255.255.0", + "" + ], + [ + "192.168.0.10", + "255.255.255.0", + "" + ], + [ + "192.168.0.10", + "255.255.255.0", + "" + ], + [ + "192.168.0.10", + "255.255.255.0", + "" + ] + ], + "cables": 1 + }, + { + "click": "-", + "local": [ + "NIC 1: 10.0.0.1", + "NIC 2: 10.1.0.1", + "NIC 3: 192.168.0.10", + "NIC 4: 192.168.0.10", + "NIC 5: 192.168.0.10", + "", + "", + "" + ], + "remote": [ + "A (10.0.0.2)", + "", + "", + "", + "", + "", + "", + "" + ], + "buttons": [ + "plugged", + "free", + "free", + "free", + "free", + "off", + "off", + "off" + ], + "add": true, + "remove": true, + "cards": [ + [ + "10.0.0.1", + "255.255.255.0", + "A" + ], + [ + "10.1.0.1", + "255.255.255.0", + "" + ], + [ + "192.168.0.10", + "255.255.255.0", + "" + ], + [ + "192.168.0.10", + "255.255.255.0", + "" + ], + [ + "192.168.0.10", + "255.255.255.0", + "" + ] + ], + "cables": 1 + }, + { + "click": "-", + "local": [ + "NIC 1: 10.0.0.1", + "NIC 2: 10.1.0.1", + "NIC 3: 192.168.0.10", + "NIC 4: 192.168.0.10", + "", + "", + "", + "" + ], + "remote": [ + "A (10.0.0.2)", + "", + "", + "", + "", + "", + "", + "" + ], + "buttons": [ + "plugged", + "free", + "free", + "free", + "off", + "off", + "off", + "off" + ], + "add": true, + "remove": true, + "cards": [ + [ + "10.0.0.1", + "255.255.255.0", + "A" + ], + [ + "10.1.0.1", + "255.255.255.0", + "" + ], + [ + "192.168.0.10", + "255.255.255.0", + "" + ], + [ + "192.168.0.10", + "255.255.255.0", + "" + ] + ], + "cables": 1 + }, + { + "click": "-", + "local": [ + "NIC 1: 10.0.0.1", + "NIC 2: 10.1.0.1", + "NIC 3: 192.168.0.10", + "", + "", + "", + "", + "" + ], + "remote": [ + "A (10.0.0.2)", + "", + "", + "", + "", + "", + "", + "" + ], + "buttons": [ + "plugged", + "free", + "free", + "off", + "off", + "off", + "off", + "off" + ], + "add": true, + "remove": true, + "cards": [ + [ + "10.0.0.1", + "255.255.255.0", + "A" + ], + [ + "10.1.0.1", + "255.255.255.0", + "" + ], + [ + "192.168.0.10", + "255.255.255.0", + "" + ] + ], + "cables": 1 + }, + { + "click": "-", + "local": [ + "NIC 1: 10.0.0.1", + "NIC 2: 10.1.0.1", + "", + "", + "", + "", + "", + "" + ], + "remote": [ + "A (10.0.0.2)", + "", + "", + "", + "", + "", + "", + "" + ], + "buttons": [ + "plugged", + "free", + "off", + "off", + "off", + "off", + "off", + "off" + ], + "add": true, + "remove": true, + "cards": [ + [ + "10.0.0.1", + "255.255.255.0", + "A" + ], + [ + "10.1.0.1", + "255.255.255.0", + "" + ] + ], + "cables": 1 + }, + { + "click": "-", + "local": [ + "NIC 1: 10.0.0.1", + "", + "", + "", + "", + "", + "", + "" + ], + "remote": [ + "A (10.0.0.2)", + "", + "", + "", + "", + "", + "", + "" + ], + "buttons": [ + "plugged", + "off", + "off", + "off", + "off", + "off", + "off", + "off" + ], + "add": true, + "remove": true, + "cards": [ + [ + "10.0.0.1", + "255.255.255.0", + "A" + ] + ], + "cables": 1, + "panelError": null, + "gateway": "10.0.0.254" + }, + { + "click": "-", + "local": [ + "", + "", + "", + "", + "", + "", + "", + "" + ], + "remote": [ + "", + "", + "", + "", + "", + "", + "", + "" + ], + "buttons": [ + "off", + "off", + "off", + "off", + "off", + "off", + "off", + "off" + ], + "add": true, + "remove": false, + "cards": [], + "cables": 0, + "panelError": null, + "gateway": null + } + ], + "hostPortFree": true, + "switchPortsFree": true + }, "edits": { "invalidCell": { "manual": [ diff --git a/packages/doc/test/sheet.test.ts b/packages/doc/test/sheet.test.ts index bcb95eb..c8b66d9 100644 --- a/packages/doc/test/sheet.test.ts +++ b/packages/doc/test/sheet.test.ts @@ -31,6 +31,10 @@ import { setSwitchRetention, setSwitchSsid, addRoute, + addRouterInterface, + connectionsClick, + removeRouterInterface, + swapRouterCables, addFirewallRule, moveFirewallRule, removeFirewallRule, @@ -104,8 +108,28 @@ const java = JSON.parse(readFileSync(join(dirname(fileURLToPath(import.meta.url) readonly cells: readonly { readonly column: number; readonly input: string; readonly rule: JavaFirewallRule; readonly valid: boolean }[]; readonly moves: readonly (readonly [string, number, boolean | null, readonly string[]])[]; }; + readonly connections: { + readonly steps: readonly JavaConnectionsStep[]; + readonly hostPortFree: boolean; + readonly switchPortsFree: boolean; + }; }; +/** A click in Java's connections dialog ("c" a card's button, "+", "-") and what the dialog and the router show then. */ +interface JavaConnectionsStep { + readonly click: string; + readonly local: readonly string[]; + readonly remote: readonly string[]; + readonly buttons: readonly ('off' | 'marked' | 'plugged' | 'free')[]; + readonly add: boolean; + readonly remove: boolean; + /** Address, mask and the device its cable joins ("" for none), per card. */ + readonly cards: readonly (readonly [string, string, string])[]; + readonly cables: number; + readonly panelError?: string | null; + readonly gateway?: string | null; +} + interface JavaFirewallRule { readonly srcIP: string; readonly srcMask: string; @@ -654,7 +678,7 @@ describe("a router's sheet", () => { const pc = add(doc, 'pc', 'PC-A'); setHostLabels(doc, pc, { ipAddress: true, macAddress: false }, 'me'); connect(doc, { node: pc }, { node: id }, 'cable', 'me'); - expect(router(doc, id).interfaces.map((card) => card.connected)).toEqual([{ device: '192.168.0.10', link: 'cable' }, null]); + expect(router(doc, id).interfaces.map((card) => card.connected)).toEqual([{ device: '192.168.0.10', address: '192.168.0.10', link: 'cable' }, null]); expect(router(doc, id).interfaces[0]!.macAddress).toMatch(/^([0-9A-F]{2}:){5}[0-9A-F]{2}$/); }); }); @@ -1089,3 +1113,196 @@ describe("a router's firewall", () => { } }); }); + +describe("a router's connections", () => { + /** SheetRun's network: router R with three cards, PC A plugged into the first, switch S into the second. */ + function network() { + const doc = new Y.Doc(); + const id = add(doc, 'router', 'R', 3); + router(doc, id).interfaces.forEach((card, index) => setInterfaceAddress(doc, id, card.portId, 'ipAddress', `10.${index}.0.1`, 'me')); + const pc = add(doc, 'pc', 'A'); + setHostAddress(doc, pc, 'ipAddress', '10.0.0.2', 'me'); + const sw = add(doc, 'networkSwitch', 'S'); + expect(connect(doc, { node: pc }, { node: id }, 'cable-a', 'me')).toEqual({ link: 'cable-a' }); + expect(connect(doc, { node: id, port: router(doc, id).interfaces[1]!.portId }, { node: sw }, 'cable-s', 'me')).toEqual({ link: 'cable-s' }); + return { doc, id, pc, sw }; + } + + /** What Java's dialog would show of the router, with the card marked. */ + function shown(doc: Y.Doc, id: string, marked: number | null) { + const cards = router(doc, id).interfaces; + const pad = (each: T[], blank: T) => [...each, ...Array(8 - each.length).fill(blank)]; + return { + local: pad(cards.map((card, index) => `NIC ${index + 1}: ${card.ipAddress}`), ''), + remote: pad(cards.map((card) => (card.connected === null ? '' : card.connected.address === null ? card.connected.device : `${card.connected.device} (${card.connected.address})`)), ''), + buttons: pad(cards.map((card, index) => (index === marked ? 'marked' : card.connected === null ? 'free' : 'plugged')), 'off'), + add: cards.length < 8, + remove: cards.length > 1, + cards: cards.map((card) => [card.ipAddress, card.subnetMask, card.connected?.device ?? '']), + cables: doc.getMap(ROOTS.links).size, + }; + } + + it('swap, add and remove cards as Java’s dialog does, click by click, down to the last card', () => { + const { doc, id } = network(); + let marked: number | null = null; + const steps = java.connections.steps; + const last = steps.findIndex((step) => step.cards.length === 1); + for (const [index, step] of steps.slice(0, last + 1).entries()) { + if (step.click.startsWith('c')) { + const next = connectionsClick(marked, Number(step.click.slice(1))); + if (next.swap !== undefined) { + const ports = router(doc, id).interfaces.map((card) => card.portId); + swapRouterCables(doc, id, ports[next.swap[0]]!, ports[next.swap[1]]!, 'me'); + } + marked = next.marked; + } else if (step.click === '+') { + // Java's "+" is greyed out at eight cards; its click does nothing then. + expect(addRouterInterface(doc, id, newId(), 'me'), `step ${index}`).toBe(steps[index - 1]!.add); + marked = null; + } else if (step.click === '-') { + expect(removeRouterInterface(doc, id, 'me'), `step ${index}`).toBe(true); + marked = null; + } + const { local, remote, buttons, add: addable, remove, cards, cables } = step; + // Java's "-" stays open at one card; here the last card stays (below). + expect(shown(doc, id, marked), `step ${index} ${step.click}`).toEqual({ local, remote, buttons, add: addable, remove: index === last ? false : remove, cards, cables }); + } + expect(steps[last]!.remove).toBe(true); + expect(steps.at(-1)!.cards).toEqual([]); + // Java's router without a card keeps no gateway; the one with a card does. + expect([steps[last]!.gateway, steps.at(-1)!.gateway]).toEqual(['10.0.0.254', null]); + expect(steps[last]!.panelError).toBeNull(); + }); + + it('keep the last card, with its cable, and its gateway', () => { + const doc = new Y.Doc(); + const id = add(doc, 'router', 'R', 2); + const pc = add(doc, 'pc', 'A'); + connect(doc, { node: pc }, { node: id }, 'cable', 'me'); + setRouterGateway(doc, id, '192.168.0.254', 'me'); + expect(removeRouterInterface(doc, id, 'me')).toBe(true); + expect(removeRouterInterface(doc, id, 'me')).toBe(false); + expect(router(doc, id).interfaces).toHaveLength(1); + expect(router(doc, id).interfaces[0]!.connected).toMatchObject({ device: 'A', link: 'cable' }); + expect(router(doc, id).gateway).toBe('192.168.0.254'); + }); + + it('remove the last card’s cable with it, and free the other device’s port', () => { + const { doc, id, sw } = network(); + const ports = router(doc, id).interfaces.map((card) => card.portId); + expect(swapRouterCables(doc, id, ports[1]!, ports[2]!, 'me')).toBe(true); + expect(removeRouterInterface(doc, id, 'me')).toBe(true); + expect([...doc.getMap>(ROOTS.links).keys()]).toEqual(['cable-a']); + expect(java.connections.switchPortsFree).toBe(true); + // The switch's port takes another cable. + const pc = add(doc, 'pc', 'B'); + expect(connect(doc, { node: pc }, { node: sw }, 'cable-b', 'me')).toEqual({ link: 'cable-b' }); + expect(java.connections.hostPortFree).toBe(true); + }); + + it('give a new card Java’s address and mask, its own MAC address, the next label and no cable', () => { + const { doc, id } = network(); + expect(addRouterInterface(doc, id, 'port-4', 'me')).toBe(true); + const cards = router(doc, id).interfaces; + expect(cards[3]).toMatchObject({ portId: 'port-4', ipAddress: '192.168.0.10', subnetMask: '255.255.255.0', connected: null }); + expect(new Set(cards.map((card) => card.macAddress)).size).toBe(4); + const ports = (doc.getMap>(ROOTS.nodes).get(id)!.get('ports') as Y.Array>).toArray(); + expect(ports.map((port) => port.get('label'))).toEqual(['rt1', 'rt2', 'rt3', 'rt4']); + expect(ports[3]!.get('occupied')).toBe(false); + }); + + it('keep each cable’s identity and devices when cards swap, and move nothing between free cards', () => { + const { doc, id, pc, sw } = network(); + const ports = router(doc, id).interfaces.map((card) => card.portId); + const ends = () => + [...doc.getMap>(ROOTS.links).entries()].map(([linkId, link]) => [linkId, link.get('sourceNode'), link.get('sourcePort'), link.get('targetNode'), link.get('targetPort')]); + const before = ends(); + expect(swapRouterCables(doc, id, ports[0]!, ports[1]!, 'me')).toBe(true); + // The PC plugged first (the router is the cable's target), the router into the switch (its source). + expect(ends()).toEqual([ + ['cable-a', pc, before[0]![2], id, ports[1]], + ['cable-s', id, ports[0], sw, before[1]![4]], + ]); + addRouterInterface(doc, id, 'port-4', 'me'); + const now = ends(); + expect(swapRouterCables(doc, id, ports[2]!, 'port-4', 'me')).toBe(false); + expect(ends()).toEqual(now); + }); + + it('refuse a card twice, a card not the router’s, and devices that are no router', () => { + const { doc, id, pc } = network(); + const ports = router(doc, id).interfaces.map((card) => card.portId); + const updates: unknown[] = []; + doc.on('update', (update) => updates.push(update)); + expect(swapRouterCables(doc, id, ports[0]!, ports[0]!, 'me')).toBe(false); + expect(swapRouterCables(doc, id, ports[0]!, 'no-port', 'me')).toBe(false); + expect(swapRouterCables(doc, id, 'no-port', ports[0]!, 'me')).toBe(false); + const pcPort = (doc.getMap>(ROOTS.nodes).get(pc)!.get('ports') as Y.Array>).get(0).get('id') as string; + expect(swapRouterCables(doc, id, ports[2]!, pcPort, 'me')).toBe(false); + expect(swapRouterCables(doc, 'nobody', ports[0]!, ports[1]!, 'me')).toBe(false); + const homeId = add(doc, 'gateway'); + const [wan, lan] = [home(doc, homeId).wan.portId, home(doc, homeId).lan.portId]; + expect(swapRouterCables(doc, homeId, wan, lan, 'me')).toBe(false); + expect(addRouterInterface(doc, homeId, 'port-x', 'me')).toBe(false); + expect(removeRouterInterface(doc, homeId, 'me')).toBe(false); + expect(addRouterInterface(doc, pc, 'port-x', 'me')).toBe(false); + expect(removeRouterInterface(doc, 'nobody', 'me')).toBe(false); + const before = updates.length; + expect(before).toBeGreaterThan(0); + for (let more = 3; more < 8; more++) expect(addRouterInterface(doc, id, `port-${more}`, 'me')).toBe(true); + expect(addRouterInterface(doc, id, 'port-9', 'me')).toBe(false); + expect(router(doc, id).interfaces).toHaveLength(8); + expect(updates).toHaveLength(before + 5); + }); + + it('click as Java’s dialog does: mark, unmark, swap', () => { + expect(connectionsClick(null, 2)).toEqual({ marked: 2 }); + expect(connectionsClick(2, 2)).toEqual({ marked: null }); + expect(connectionsClick(2, 0)).toEqual({ marked: null, swap: [2, 0] }); + }); + + it('are each one step of the person’s undo, and go into the file', () => { + const { doc, id } = network(); + const undo = createUndoManager(doc, 'me'); + const ports = router(doc, id).interfaces.map((card) => card.portId); + undo.stopCapturing(); + addRouterInterface(doc, id, 'port-4', 'me'); + undo.stopCapturing(); + swapRouterCables(doc, id, ports[0]!, 'port-4', 'me'); + undo.stopCapturing(); + removeRouterInterface(doc, id, 'me'); + expect(router(doc, id).interfaces.map((card) => card.connected?.device ?? '')).toEqual(['', 'S', '']); + undo.undo(); + expect(router(doc, id).interfaces.map((card) => card.connected?.device ?? '')).toEqual(['', 'S', '', 'A']); + undo.undo(); + expect(router(doc, id).interfaces.map((card) => card.connected?.device ?? '')).toEqual(['A', 'S', '', '']); + swapRouterCables(doc, id, ports[0]!, 'port-4', 'me'); + let counter = 0; + const reopened = openProjectArchive(saveProjectArchive(readProject(doc)).archive, () => `70000000-0000-0000-0000-${(++counter).toString(16).padStart(12, '0')}`); + const again = reopened.graph.nodes.find((node) => node.displayName === 'R')!; + expect(again.ports.map((port) => port.label)).toEqual(['rt1', 'rt2', 'rt3', 'rt4']); + const plugged = reopened.graph.links.flatMap((link) => [link.sourcePortId, link.targetPortId]); + expect(again.ports.map((port) => plugged.includes(port.id))).toEqual([false, true, false, true]); + }); + + it('name the address of the card a cable plugs into: a computer’s, a router’s, none for a switch', () => { + const { doc, id } = network(); + const other = add(doc, 'router', 'R2'); + setInterfaceAddress(doc, other, router(doc, other).interfaces[1]!.portId, 'ipAddress', '10.2.0.2', 'me'); + connect(doc, { node: id, port: router(doc, id).interfaces[2]!.portId }, { node: other, port: router(doc, other).interfaces[1]!.portId }, 'cable-r', 'me'); + const homeId = add(doc, 'gateway', 'H'); + const modem = add(doc, 'remoteLink', 'M'); + addRouterInterface(doc, id, 'port-4', 'me'); + addRouterInterface(doc, id, 'port-5', 'me'); + connect(doc, { node: homeId, port: home(doc, homeId).lan.portId }, { node: id, port: 'port-4' }, 'cable-h', 'me'); + connect(doc, { node: modem }, { node: id, port: 'port-5' }, 'cable-m', 'me'); + expect(router(doc, id).interfaces.map((card) => card.connected && [card.connected.device, card.connected.address])).toEqual([ + ['A', '10.0.0.2'], + ['S', null], + ['R2', '10.2.0.2'], + ['H', '192.168.0.10'], + ['M', null], + ]); + }); +}); diff --git a/packages/editor/browser/sheet.spec.ts b/packages/editor/browser/sheet.spec.ts index 09133a2..c717378 100644 --- a/packages/editor/browser/sheet.spec.ts +++ b/packages/editor/browser/sheet.spec.ts @@ -400,6 +400,102 @@ test("sets up a router's firewall on its own page: the switches, the default, ru await expect(sheet(page).getByRole('heading')).toHaveText('Vermittlungsrechner'); }); +test("manages a router's connections on its own page: cables swapped with two taps, cards added and removed, the last kept", async ({ page }) => { + await openEditor(page, server.base, 'zwei_rechnernetze_komplett.fls'); + const id = (await drawing(page)).nodes.find((node) => node.kind === 'router')!.id; + await choose(page, id); + const joined = async () => ((await sheetOf(page, id)) as RouterSheet).interfaces.map((card) => card.connected?.device ?? ''); + const values = (await sheetOf(page, id)) as RouterSheet; + const [one, two] = await joined(); + const cables = (await drawing(page)).links.length; + await sheet(page).getByRole('button', { name: 'Verbindungen verwalten' }).click(); + await expect(sheet(page).getByRole('heading', { level: 2 })).toHaveText('Verbindungen verwalten'); + const items = sheet(page).locator('ol.connections li'); + const port = (index: number) => items.nth(index).getByRole('button'); + await expect(items.locator('.connection-local')).toHaveText(values.interfaces.map((card, index) => `NIC ${index + 1}: ${card.ipAddress}`)); + await expect(items.locator('.connection-remote')).toHaveText([one!, two!]); + // Java's colours: green for a plugged card, yellow for the marked one, red for a free one. + await expect(port(0)).toHaveCSS('background-color', 'rgb(0, 255, 0)'); + await port(0).click(); + await expect(port(0)).toHaveAttribute('aria-pressed', 'true'); + await expect(port(0)).toHaveCSS('background-color', 'rgb(255, 255, 0)'); + await port(0).click(); + await expect(port(0)).toHaveAttribute('aria-pressed', 'false'); + await port(0).click(); + await port(1).click(); + await expect.poll(joined).toEqual([two, one]); + await expect(items.nth(0)).toHaveAttribute('data-state', 'plugged'); + + await sheet(page).getByRole('button', { name: 'Schnittstelle hinzufügen' }).click(); + await expect(items).toHaveCount(3); + await expect(items.nth(2).locator('.connection-local')).toHaveText('NIC 3: 192.168.0.10'); + await expect(port(2)).toHaveCSS('background-color', 'rgb(255, 0, 0)'); + await expect(port(2)).toHaveAccessibleName('NIC 3: 192.168.0.10, Keine Verbindung'); + // A free card takes the marked card's cable. + await port(0).click(); + await port(2).click(); + await expect.poll(joined).toEqual(['', one, two]); + await sheet(page).getByRole('button', { name: 'Schnittstelle entfernen' }).click(); + await expect.poll(joined).toEqual(['', one]); + expect((await drawing(page)).links).toHaveLength(cables - 1); + + for (let count = 2; count < 8; count++) await sheet(page).getByRole('button', { name: 'Schnittstelle hinzufügen' }).click(); + await expect(items).toHaveCount(8); + await expect(sheet(page).getByRole('button', { name: 'Schnittstelle hinzufügen' })).toBeDisabled(); + await expect(sheet(page).getByText('Maximal 8 Schnittstellen erlaubt!')).toBeVisible(); + for (let count = 8; count > 1; count--) await sheet(page).getByRole('button', { name: 'Schnittstelle entfernen' }).click(); + await expect(items).toHaveCount(1); + await expect(sheet(page).getByRole('button', { name: 'Schnittstelle entfernen' })).toBeDisabled(); + await expect(sheet(page).getByText('Eine Schnittstelle bleibt immer.')).toBeVisible(); + + await sheet(page).getByRole('button', { name: 'Zurück' }).click(); + const tabs = sheet(page).getByRole('tab'); + await expect(tabs).toHaveText(['Allgemein', values.interfaces[0]!.ipAddress, 'Weiterleitungstabelle']); + // Undone, the second card comes back with its cable; a card's tab whose card goes again shows the general tab. + await page.keyboard.press('Control+z'); + await expect(tabs).toHaveCount(4); + await tabs.nth(2).click(); + await expect(sheet(page).locator('.sheet-connected')).toHaveText(`Verbunden mit ${one}`); + await page.keyboard.press('Control+Shift+z'); + await expect(tabs).toHaveCount(3); + await expect(tabs.first()).toHaveAttribute('aria-selected', 'true'); + await expect(field(page, 'name')).toHaveValue(values.name); +}); + +test("names a router's computer by its address, and lets go of a marked card when cards change", async ({ page }) => { + await openEditor(page, server.base, 'internetwork_komplett.fls'); + const routers = (await drawing(page)).nodes.filter((node) => node.kind === 'router'); + let id = ''; + for (const each of routers) if (((await sheetOf(page, each.id)) as RouterSheet).interfaces.some((card) => card.connected?.address)) id = each.id; + await choose(page, id); + const values = (await sheetOf(page, id)) as RouterSheet; + await sheet(page).getByRole('button', { name: 'Verbindungen verwalten' }).click(); + const items = sheet(page).locator('ol.connections li'); + const port = (index: number) => items.nth(index).getByRole('button'); + // Java: a computer's or a router's name with the address of its card, a switch's name alone. + await expect(items.locator('.connection-remote')).toHaveText( + values.interfaces.flatMap((card) => (card.connected === null ? [] : [card.connected.address === null ? card.connected.device : `${card.connected.device} (${card.connected.address})`])), + ); + expect(values.interfaces.some((card) => card.connected?.address !== null && card.connected?.address !== undefined)).toBe(true); + const pressed = () => sheet(page).locator('.connection-port[aria-pressed="true"]'); + await port(0).click(); + await sheet(page).getByRole('button', { name: 'Schnittstelle hinzufügen' }).click(); + await expect(pressed()).toHaveCount(0); + await port(0).click(); + await sheet(page).getByRole('button', { name: 'Schnittstelle entfernen' }).click(); + await expect(pressed()).toHaveCount(0); + // A marked card undone away is marked no more: the next tap marks, it swaps nothing. + await sheet(page).getByRole('button', { name: 'Schnittstelle hinzufügen' }).click(); + const count = values.interfaces.length + 1; + await port(count - 1).click(); + await page.keyboard.press('Control+z'); + await expect(items).toHaveCount(count - 1); + const before = ((await sheetOf(page, id)) as RouterSheet).interfaces.map((card) => card.connected?.link ?? null); + await port(0).click(); + await expect(port(0)).toHaveAttribute('aria-pressed', 'true'); + expect(((await sheetOf(page, id)) as RouterSheet).interfaces.map((card) => card.connected?.link ?? null)).toEqual(before); +}); + test("opens a home router's firewall from its general tab", async ({ page }) => { await openEditor(page, server.base, 'new'); await page.locator('[data-palette="gateway"]').click(); diff --git a/packages/editor/src/assets/hardware/rj45.png b/packages/editor/src/assets/hardware/rj45.png new file mode 100644 index 0000000..aa7e3d3 Binary files /dev/null and b/packages/editor/src/assets/hardware/rj45.png differ diff --git a/packages/editor/src/canvas/icons.ts b/packages/editor/src/canvas/icons.ts index a03a47c..fcefa2e 100644 --- a/packages/editor/src/canvas/icons.ts +++ b/packages/editor/src/canvas/icons.ts @@ -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> = { 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; diff --git a/packages/editor/src/i18n/de.json b/packages/editor/src/i18n/de.json index 2639c1e..86a53af 100644 --- a/packages/editor/src/i18n/de.json +++ b/packages/editor/src/i18n/de.json @@ -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", diff --git a/packages/editor/src/i18n/en.json b/packages/editor/src/i18n/en.json index d4e71da..1079ff7 100644 --- a/packages/editor/src/i18n/en.json +++ b/packages/editor/src/i18n/en.json @@ -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", diff --git a/packages/editor/src/sheet/ConnectionsPanel.tsx b/packages/editor/src/sheet/ConnectionsPanel.tsx new file mode 100644 index 0000000..3249f1b --- /dev/null +++ b/packages/editor/src/sheet/ConnectionsPanel.tsx @@ -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(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 ( + <> +

{t('connections.local')}

+
    + {cards.map((card, index) => { + const name = t('connections.nic', { number: index + 1, address: card.ipAddress }); + const joined = card.connected; + return ( +
  1. + + {name} + {joined === null ? null : {joined.address === null ? joined.device : `${joined.device} (${joined.address})`}} +
  2. + ); + })} +
+
+ + +
+ {cards.length >= ROUTER_INTERFACES.maximum ? ( +

{t('connections.most')}

+ ) : cards.length <= ROUTER_INTERFACES.fewest ? ( +

{t('connections.fewest')}

+ ) : null} + {/* Below the cards, as in Java's dialog. */} + + + ); +} diff --git a/packages/editor/src/sheet/DeviceSheet.tsx b/packages/editor/src/sheet/DeviceSheet.tsx index f495175..f7fd28e 100644 --- a/packages/editor/src/sheet/DeviceSheet.tsx +++ b/packages/editor/src/sheet/DeviceSheet.tsx @@ -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> = { 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 ‹ )} -

{open === 'dhcp' ? t('sheet.dhcpServer') : open === 'forwarding' ? t('sheet.portForwarding') : open === 'firewall' ? t('sheet.firewall') : title}

+

{open === null ? title : t(PAGE_TITLES[open])}

@@ -121,6 +124,10 @@ export function DeviceSheet({ doc, id, label, origin, onChanged, onMessage, onCl
+ ) : open === 'connections' && sheet.kind === 'router' ? ( +
+ +
) : open === 'forwarding' && sheet.kind === 'gateway' ? (
diff --git a/packages/editor/src/sheet/FirewallPanel.tsx b/packages/editor/src/sheet/FirewallPanel.tsx index b05c9bf..d6931b6 100644 --- a/packages/editor/src/sheet/FirewallPanel.tsx +++ b/packages/editor/src/sheet/FirewallPanel.tsx @@ -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) => ( -

- {line} -

- ))} - - ); -} - 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' ? ( <> - + - + - + ) : ( <> - +