feat(editor): F4.5b the device sheet for routers and home routers
Measured first: SheetRun now types into FILIUS' router and home router
panels, records five forwarding tables (holeTabelle) and steps in the
panel's table. Java turns a card address that passes the pattern but not
IP.ipCheck (".0.0.1") into null and the router panel throws; an empty
gateway is marked red yet taken; a refused table cell is stored as null;
a home router's DHCP client leaves its DHCP server running. None of the
nulls is adopted. The core's routeTableRows matches all five tables.
- doc: card addresses (the first card mirrored in the device's settings),
router gateway, RIP, manual routes (a new row once all four cells pass,
a cell, removal); the sheet reads cards with what their cable joins and
the whole table
- editor: Java's tabs (general, each card titled by its address, the
forwarding table greyed out under RIP; general, LAN, WAN on a home
router); the open card's cable drawn active in Java's green
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,7 +1,9 @@
|
||||
// What the device sheet does to the document: a device's settings changed as
|
||||
// FILIUS' settings panels change them (JHostKonfiguration,
|
||||
// JSwitchKonfiguration, JModemKonfiguration; measured with SheetRun), each a
|
||||
// transaction with the person's origin, like the design tools' commands.
|
||||
// JSwitchKonfiguration, JModemKonfiguration, JVermittlungsrechnerKonfiguration
|
||||
// with its JWeiterleitungsTabelle, JGatewayConfiguration; measured with
|
||||
// SheetRun), each a transaction with the person's origin, like the design
|
||||
// tools' commands.
|
||||
//
|
||||
// A text field's setter takes the text only when the field's rule does
|
||||
// (fields.ts) and says so; the device keeps its value otherwise. A host's
|
||||
@@ -15,9 +17,12 @@
|
||||
// search for the access point (updateWifiConnection) keeps the last switch
|
||||
// with the SSID it passes until a device that is neither comes after the
|
||||
// host — which switch depends on unrelated devices; here it is the first.
|
||||
// Nor: an interface address that passes the panel's pattern but not
|
||||
// IP.ipCheck (".0.0.1") becomes null in Java, and a router's panel then
|
||||
// throws; a table cell Java refuses is stored as null. Here neither is taken.
|
||||
|
||||
import * as Y from 'yjs';
|
||||
import { DEFAULT_REMOTE_LINK_LATENCY_MILLISECONDS, defaultSwitchConfiguration, forwardsIPv4ByDefault, type Id, type NodeKind } from '@mdg-filius/core';
|
||||
import { DEFAULT_REMOTE_LINK_LATENCY_MILLISECONDS, defaultSwitchConfiguration, forwardsIPv4ByDefault, type Id, type ManualRoute, type NodeKind } from '@mdg-filius/core';
|
||||
import { modemPair } from '@mdg-filius/fls';
|
||||
import { connect, type ConnectError } from './commands.js';
|
||||
import { checkField, type CheckedField } from './fields.js';
|
||||
@@ -86,7 +91,11 @@ export function setIpForwarding(doc: Y.Doc, id: Id, on: boolean, origin: unknown
|
||||
}, origin);
|
||||
}
|
||||
|
||||
/** DHCP for the configuration; switched on, it stops the device's own DHCP server (JHostKonfiguration.aenderungenAnnehmen). */
|
||||
/**
|
||||
* DHCP for the configuration. On a PC or notebook it stops the device's own
|
||||
* DHCP server (JHostKonfiguration.aenderungenAnnehmen); a home router's
|
||||
* panel (its WAN card) leaves its server running, as Java does.
|
||||
*/
|
||||
export function setDhcpClient(doc: Y.Doc, id: Id, on: boolean, origin: unknown): void {
|
||||
const node = nodes(doc).get(id);
|
||||
if (node === undefined) return;
|
||||
@@ -96,7 +105,7 @@ export function setDhcpClient(doc: Y.Doc, id: Id, on: boolean, origin: unknown):
|
||||
return;
|
||||
}
|
||||
settingsOf(node).set('dhcpClient', toY({ isEnabled: true }));
|
||||
(settingsOf(node).get('dhcpServer') as Y.Map<unknown> | undefined)?.set('isActive', false);
|
||||
if (kindOf(node) === 'pc' || kindOf(node) === 'notebook') (settingsOf(node).get('dhcpServer') as Y.Map<unknown> | undefined)?.set('isActive', false);
|
||||
}, origin);
|
||||
}
|
||||
|
||||
@@ -196,3 +205,117 @@ export function setModemPort(doc: Y.Doc, id: Id, text: string, origin: unknown):
|
||||
origin,
|
||||
);
|
||||
}
|
||||
|
||||
const portsOf = (node: Y.Map<unknown>) => node.get('ports') as Y.Array<Y.Map<unknown>>;
|
||||
|
||||
/**
|
||||
* A router's or home router's interface: its address or mask
|
||||
* (NetzwerkInterface.setIp, setSubnetzMaske). The device's own settings
|
||||
* mirror its first interface, as the project keeps them (the home router's
|
||||
* WAN card).
|
||||
*/
|
||||
export function setInterfaceAddress(doc: Y.Doc, id: Id, portId: Id, field: 'ipAddress' | 'subnetMask', text: string, origin: unknown): CheckedField {
|
||||
const node = nodes(doc).get(id);
|
||||
const ports = node === undefined ? [] : portsOf(node).toArray();
|
||||
const index = ports.findIndex((port) => port.get('id') === portId);
|
||||
if (index < 0) return checkField(field, text);
|
||||
return setChecked(
|
||||
doc,
|
||||
id,
|
||||
checkField(field, text),
|
||||
(device, value) => {
|
||||
const port = ports[index]!;
|
||||
let config = port.get('config') as Y.Map<unknown> | undefined;
|
||||
if (config === undefined) {
|
||||
config = toY({ ipAddress: '192.168.0.10', subnetMask: '255.255.255.0' }) as Y.Map<unknown>;
|
||||
port.set('config', config);
|
||||
}
|
||||
config.set(field, value);
|
||||
if (index === 0) (settingsOf(device).get('device') as Y.Map<unknown> | undefined)?.set(field, value);
|
||||
},
|
||||
origin,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* A router's or home router's standard gateway (setStandardGateway: the
|
||||
* gateway of every card), empty for none. A router keeps its own settings
|
||||
* only while it has a gateway, as the project keeps them.
|
||||
*/
|
||||
export function setRouterGateway(doc: Y.Doc, id: Id, text: string, origin: unknown): CheckedField {
|
||||
return setChecked(
|
||||
doc,
|
||||
id,
|
||||
checkField('ipAddressOrEmpty', text),
|
||||
(node, value) => {
|
||||
const first = portsOf(node).get(0)?.get('config') as Y.Map<unknown> | undefined;
|
||||
const device = settingMap(node, 'device', {
|
||||
ipAddress: (first?.get('ipAddress') as string | undefined) ?? '',
|
||||
subnetMask: (first?.get('subnetMask') as string | undefined) ?? '',
|
||||
defaultGateway: '',
|
||||
dnsServer: '',
|
||||
});
|
||||
device.set('defaultGateway', value);
|
||||
if (kindOf(node) === 'router' && value === '' && device.get('dnsServer') === '') settingsOf(node).delete('device');
|
||||
},
|
||||
origin,
|
||||
);
|
||||
}
|
||||
|
||||
/** RIP on a router, kept only when on (as the project keeps it). */
|
||||
export function setRip(doc: Y.Doc, id: Id, on: boolean, origin: unknown): void {
|
||||
const node = nodes(doc).get(id);
|
||||
if (node === undefined) return;
|
||||
doc.transact(() => {
|
||||
if (on) settingsOf(node).set('rip', true);
|
||||
else settingsOf(node).delete('rip');
|
||||
}, origin);
|
||||
}
|
||||
|
||||
/** A column of the forwarding table's manual rows. */
|
||||
export type RouteColumn = keyof ManualRoute;
|
||||
|
||||
export const ROUTE_COLUMNS: readonly RouteColumn[] = ['destinationNetwork', 'subnetMask', 'gateway', 'interfaceIPAddress'];
|
||||
|
||||
/** Each column's rule (JWeiterleitungsTabelle: musterIpAdresse, musterSubNetz for the mask). */
|
||||
const routeRule = (column: RouteColumn) => (column === 'subnetMask' ? 'subnetMask' : 'ipAddress');
|
||||
|
||||
function routesOf(node: Y.Map<unknown>): Y.Array<Y.Map<unknown>> {
|
||||
let routes = settingsOf(node).get('routes') as Y.Array<Y.Map<unknown>> | undefined;
|
||||
if (routes === undefined) {
|
||||
routes = new Y.Array<Y.Map<unknown>>();
|
||||
settingsOf(node).set('routes', routes);
|
||||
}
|
||||
return routes;
|
||||
}
|
||||
|
||||
/**
|
||||
* A new manual row, taken once all four cells pass (Java keeps a new row in
|
||||
* the table until Weiterleitungstabelle.validate takes it); the columns
|
||||
* that do not pass otherwise.
|
||||
*/
|
||||
export function addRoute(doc: Y.Doc, id: Id, cells: Readonly<Record<RouteColumn, string>>, origin: unknown): { readonly route: ManualRoute } | { readonly invalid: readonly RouteColumn[] } {
|
||||
const checked = ROUTE_COLUMNS.map((column) => [column, checkField(routeRule(column), cells[column])] as const);
|
||||
const invalid = checked.filter(([, each]) => 'invalid' in each).map(([column]) => column);
|
||||
const node = nodes(doc).get(id);
|
||||
if (invalid.length > 0 || node === undefined) return { invalid };
|
||||
const route = Object.fromEntries(checked.map(([column, each]) => [column, (each as { value: string }).value])) as unknown as ManualRoute;
|
||||
doc.transact(() => routesOf(node).push([toY({ ...route }) as Y.Map<unknown>]), origin);
|
||||
return { route };
|
||||
}
|
||||
|
||||
/** A cell of a manual row (by its place among the manual rows). */
|
||||
export function setRouteCell(doc: Y.Doc, id: Id, index: number, column: RouteColumn, text: string, origin: unknown): CheckedField {
|
||||
return setChecked(doc, id, checkField(routeRule(column), text), (node, value) => (settingsOf(node).get('routes') as Y.Array<Y.Map<unknown>> | undefined)?.get(index)?.set(column, value), origin);
|
||||
}
|
||||
|
||||
/** Removes a manual row (by its place among the manual rows); the list goes with its last row, as the project keeps it. */
|
||||
export function removeRoute(doc: Y.Doc, id: Id, index: number, origin: unknown): void {
|
||||
const node = nodes(doc).get(id);
|
||||
const routes = node === undefined ? undefined : (settingsOf(node).get('routes') as Y.Array<Y.Map<unknown>> | undefined);
|
||||
if (node === undefined || routes === undefined || index < 0 || index >= routes.length) return;
|
||||
doc.transact(() => {
|
||||
routes.delete(index, 1);
|
||||
if (routes.length === 0) settingsOf(node).delete('routes');
|
||||
}, origin);
|
||||
}
|
||||
|
||||
+102
-6
@@ -1,12 +1,24 @@
|
||||
// What the device sheet shows of a device, read straight from the document:
|
||||
// the fields of FILIUS' settings panel of its kind (JHostKonfiguration,
|
||||
// JSwitchKonfiguration, JModemKonfiguration). A router's and a home router's
|
||||
// sheet show their name only, until their own panels follow.
|
||||
// JSwitchKonfiguration, JModemKonfiguration, JVermittlungsrechnerKonfiguration
|
||||
// with its forwarding table, JGatewayConfiguration).
|
||||
|
||||
import * as Y from 'yjs';
|
||||
import { defaultSwitchConfiguration, DEFAULT_SWITCH_RETENTION_MILLISECONDS, forwardsIPv4ByDefault, stableGeneratedMacAddress, type HostLabelPolicy, type NodeKind } from '@mdg-filius/core';
|
||||
import {
|
||||
defaultSwitchConfiguration,
|
||||
DEFAULT_SWITCH_RETENTION_MILLISECONDS,
|
||||
forwardsIPv4ByDefault,
|
||||
routeTableRows,
|
||||
stableGeneratedMacAddress,
|
||||
type HostLabelPolicy,
|
||||
type ManualRoute,
|
||||
type NodeKind,
|
||||
type RouteRow,
|
||||
} from '@mdg-filius/core';
|
||||
import { modemPort } from '@mdg-filius/fls';
|
||||
import { inOrder, ROOTS } from './projectDoc.js';
|
||||
import { fromY } from './values.js';
|
||||
import { nodeLabel } from './view.js';
|
||||
|
||||
export interface HostSheet {
|
||||
readonly kind: 'pc' | 'notebook';
|
||||
@@ -44,16 +56,73 @@ export interface ModemSheet {
|
||||
readonly port: number;
|
||||
}
|
||||
|
||||
/** A router's or home router's card. */
|
||||
export interface InterfaceSheet {
|
||||
readonly portId: string;
|
||||
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;
|
||||
}
|
||||
|
||||
export interface RouterSheet {
|
||||
readonly kind: 'router';
|
||||
readonly name: string;
|
||||
readonly gateway: string;
|
||||
readonly rip: boolean;
|
||||
/** Shown, not changed: Java's panel greys it out. */
|
||||
readonly ipForwarding: boolean;
|
||||
readonly interfaces: readonly InterfaceSheet[];
|
||||
/** The manual rows, in their order. */
|
||||
readonly routes: readonly ManualRoute[];
|
||||
/** The whole table as Java shows it (Weiterleitungstabelle.holeTabelle): own addresses and networks, localhost, the manual rows, the gateway. */
|
||||
readonly table: readonly RouteRow[];
|
||||
}
|
||||
|
||||
export interface HomeRouterSheet {
|
||||
readonly kind: 'gateway';
|
||||
readonly name: string;
|
||||
readonly gateway: string;
|
||||
readonly ipForwarding: boolean;
|
||||
/** DHCP for the WAN card. */
|
||||
readonly dhcpClient: boolean;
|
||||
readonly wan: InterfaceSheet;
|
||||
readonly lan: InterfaceSheet;
|
||||
}
|
||||
|
||||
export interface NameSheet {
|
||||
readonly kind: Exclude<NodeKind, 'pc' | 'notebook' | 'networkSwitch' | 'remoteLink'>;
|
||||
readonly kind: 'unsupported';
|
||||
readonly name: string;
|
||||
}
|
||||
|
||||
export type DeviceSheet = HostSheet | SwitchSheet | ModemSheet | NameSheet;
|
||||
export type DeviceSheet = HostSheet | SwitchSheet | ModemSheet | RouterSheet | HomeRouterSheet | NameSheet;
|
||||
|
||||
const settingsOf = (node: Y.Map<unknown>) => node.get('settings') as Y.Map<unknown>;
|
||||
const setting = (node: Y.Map<unknown>, key: string) => settingsOf(node).get(key) as Y.Map<unknown> | undefined;
|
||||
|
||||
const text = (map: Y.Map<unknown> | undefined, key: string) => (map?.get(key) as string | undefined) ?? '';
|
||||
|
||||
/** A router's or home router's cards, each with what its cable joins. */
|
||||
function interfacesOf(doc: Y.Doc, id: string, node: Y.Map<unknown>): InterfaceSheet[] {
|
||||
const all = doc.getMap<Y.Map<unknown>>(ROOTS.nodes);
|
||||
const links = inOrder(doc.getMap<Y.Map<unknown>>(ROOTS.links));
|
||||
return (node.get('ports') as Y.Array<Y.Map<unknown>>).toArray().map((port) => {
|
||||
const portId = port.get('id') as string;
|
||||
const config = port.get('config') as Y.Map<unknown> | 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);
|
||||
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] },
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/** The sheet of a device, or undefined when the document has no such device. */
|
||||
export function deviceSheet(doc: Y.Doc, id: string): DeviceSheet | undefined {
|
||||
const all = doc.getMap<Y.Map<unknown>>(ROOTS.nodes);
|
||||
@@ -106,7 +175,34 @@ export function deviceSheet(doc: Y.Doc, id: string): DeviceSheet | undefined {
|
||||
}
|
||||
case 'remoteLink':
|
||||
return { kind, name, port: modemPort(setting(node, 'remoteLink')?.get('pairIdentifier') as string | undefined) };
|
||||
default:
|
||||
case 'router': {
|
||||
const interfaces = interfacesOf(doc, id, node);
|
||||
const gateway = text(setting(node, 'device'), 'defaultGateway');
|
||||
const routes = (fromY(settingsOf(node).get('routes')) ?? []) as unknown as ManualRoute[];
|
||||
return {
|
||||
kind,
|
||||
name,
|
||||
gateway,
|
||||
rip: settingsOf(node).get('rip') === true,
|
||||
ipForwarding: (settingsOf(node).get('ipForwarding') as boolean | undefined) ?? forwardsIPv4ByDefault(kind),
|
||||
interfaces,
|
||||
routes,
|
||||
table: routeTableRows(interfaces, routes, gateway),
|
||||
};
|
||||
}
|
||||
case 'gateway': {
|
||||
const [wan, lan] = interfacesOf(doc, id, node);
|
||||
return {
|
||||
kind,
|
||||
name,
|
||||
gateway: text(setting(node, 'device'), 'defaultGateway'),
|
||||
ipForwarding: (settingsOf(node).get('ipForwarding') as boolean | undefined) ?? forwardsIPv4ByDefault(kind),
|
||||
dhcpClient: setting(node, 'dhcpClient')?.get('isEnabled') === true,
|
||||
wan: wan!,
|
||||
lan: lan!,
|
||||
};
|
||||
}
|
||||
case 'unsupported':
|
||||
return { kind, name };
|
||||
}
|
||||
}
|
||||
|
||||
+19
-19
@@ -55,8 +55,10 @@ export interface TopologyView {
|
||||
|
||||
const settingsOf = (node: Y.Map<unknown>) => node.get('settings') as Y.Map<Y.Map<unknown>> | undefined;
|
||||
|
||||
/** Host.holeAnzeigeName: a PC's or notebook's address and MAC address, as its label policy asks. */
|
||||
function labelOf(node: Y.Map<unknown>, kind: NodeKind, name: string): string {
|
||||
/** Host.holeAnzeigeName: a PC's or notebook's address and MAC address, as its label policy asks; any other device's name. */
|
||||
export function nodeLabel(node: Y.Map<unknown>): string {
|
||||
const kind = node.get('kind') as NodeKind;
|
||||
const name = node.get('name') as string;
|
||||
const policy = node.get('labelPolicy') as HostLabelPolicy;
|
||||
if ((kind !== 'pc' && kind !== 'notebook') || policy === 'manual') return name;
|
||||
const ip = (settingsOf(node)?.get('device')?.get('ipAddress') as string | undefined) ?? '';
|
||||
@@ -80,7 +82,7 @@ export function topologyView(doc: Y.Doc): TopologyView {
|
||||
id,
|
||||
kind,
|
||||
name,
|
||||
label: labelOf(node, kind, name),
|
||||
label: nodeLabel(node),
|
||||
x: node.get('x') as number,
|
||||
y: node.get('y') as number,
|
||||
cloud: kind === 'networkSwitch' && settingsOf(node)?.get('switch')?.get('showsAsCloud') === true,
|
||||
@@ -94,22 +96,20 @@ export function topologyView(doc: Y.Doc): TopologyView {
|
||||
const targetNode = link.get('targetNode') as string;
|
||||
return { id, sourceNode, sourcePort: link.get('sourcePort') as string, targetNode, targetPort: link.get('targetPort') as string, wireless: wireless(sourceNode, targetNode) };
|
||||
});
|
||||
const docs = [...doc.getMap<Y.Map<unknown>>(ROOTS.docs).entries()].map(
|
||||
([id, item]): DocumentationView => ({
|
||||
id,
|
||||
kind: item.get('kind') as DocumentationItemKind,
|
||||
x: item.get('x') as number,
|
||||
y: item.get('y') as number,
|
||||
width: item.get('width') as number,
|
||||
height: item.get('height') as number,
|
||||
text: (item.get('text') as Y.Text).toString(),
|
||||
color: { red: item.get('red') as number, green: item.get('green') as number, blue: item.get('blue') as number, alpha: item.get('alpha') as number },
|
||||
font: item.get('font') as string,
|
||||
size: item.get('size') as number,
|
||||
bold: item.get('bold') as boolean,
|
||||
order: item.get('order') as number,
|
||||
}),
|
||||
);
|
||||
const docs = [...doc.getMap<Y.Map<unknown>>(ROOTS.docs).entries()].map(([id, item]): DocumentationView => ({
|
||||
id,
|
||||
kind: item.get('kind') as DocumentationItemKind,
|
||||
x: item.get('x') as number,
|
||||
y: item.get('y') as number,
|
||||
width: item.get('width') as number,
|
||||
height: item.get('height') as number,
|
||||
text: (item.get('text') as Y.Text).toString(),
|
||||
color: { red: item.get('red') as number, green: item.get('green') as number, blue: item.get('blue') as number, alpha: item.get('alpha') as number },
|
||||
font: item.get('font') as string,
|
||||
size: item.get('size') as number,
|
||||
bold: item.get('bold') as boolean,
|
||||
order: item.get('order') as number,
|
||||
}));
|
||||
// The model's render order (inDeterministicRenderOrder): rectangles below texts, then by order, then by id.
|
||||
const layer = (item: DocumentationView) => (item.kind === 'rectangle' ? 0 : 1);
|
||||
docs.sort((left, right) => layer(left) - layer(right) || left.order - right.order || compareIds(left.id, right.id));
|
||||
|
||||
@@ -78,3 +78,41 @@ modem port
|
||||
modem port 080
|
||||
modem port \u002080
|
||||
modem port 80
|
||||
router name R1
|
||||
router name
|
||||
router ip 10.0.0.1
|
||||
router ip 010.000.000.001
|
||||
router ip 10..0.0.1
|
||||
router ip .0.0.1
|
||||
router ip +10.0.0.1
|
||||
router ip 256.0.0.1
|
||||
router ip 10.0.0.1
|
||||
router ip 10.0.0.1
|
||||
router ip
|
||||
router ip 10.0.0.1.
|
||||
router mask 255.255.0.0
|
||||
router mask 255.0.255.0
|
||||
router mask 256.0.0.0
|
||||
router mask 255..255.0.0
|
||||
router mask
|
||||
router gateway
|
||||
router gateway 10.0.0.254
|
||||
router gateway
|
||||
router gateway abc
|
||||
router gateway 192.168..0.1
|
||||
router gateway 10.0.0.256
|
||||
homeRouter name Heim
|
||||
homeRouter gateway
|
||||
homeRouter gateway 42.0.0.1
|
||||
homeRouter gateway 42.0.0
|
||||
homeRouter wanIp 42.1.2.3
|
||||
homeRouter wanIp .1.2.3
|
||||
homeRouter wanIp 42..1.2
|
||||
homeRouter wanIp 042.1.2.3
|
||||
homeRouter wanMask 255.255.0.0
|
||||
homeRouter wanMask 255.255
|
||||
homeRouter lanIp 192.168.1.1
|
||||
homeRouter lanIp 192.168.1.256
|
||||
homeRouter lanIp 192.168.1.1
|
||||
homeRouter lanMask 255.255.255.128
|
||||
homeRouter lanMask abc
|
||||
|
||||
@@ -692,12 +692,804 @@
|
||||
"marked": false,
|
||||
"stored": "80",
|
||||
"shown": "80"
|
||||
},
|
||||
{
|
||||
"device": "router",
|
||||
"field": "name",
|
||||
"input": "R1",
|
||||
"start": "Vermittlungsrechner",
|
||||
"marked": false,
|
||||
"stored": "R1",
|
||||
"shown": "R1"
|
||||
},
|
||||
{
|
||||
"device": "router",
|
||||
"field": "name",
|
||||
"input": "",
|
||||
"start": "Vermittlungsrechner",
|
||||
"marked": false,
|
||||
"stored": "",
|
||||
"shown": ""
|
||||
},
|
||||
{
|
||||
"device": "router",
|
||||
"field": "ip",
|
||||
"input": "10.0.0.1",
|
||||
"start": "192.168.0.10",
|
||||
"marked": false,
|
||||
"stored": "10.0.0.1",
|
||||
"shown": "10.0.0.1"
|
||||
},
|
||||
{
|
||||
"device": "router",
|
||||
"field": "ip",
|
||||
"input": "010.000.000.001",
|
||||
"start": "192.168.0.10",
|
||||
"marked": false,
|
||||
"stored": "10.0.0.1",
|
||||
"shown": "10.0.0.1"
|
||||
},
|
||||
{
|
||||
"device": "router",
|
||||
"field": "ip",
|
||||
"input": "10..0.0.1",
|
||||
"start": "192.168.0.10",
|
||||
"marked": true,
|
||||
"stored": "192.168.0.10",
|
||||
"shown": "192.168.0.10"
|
||||
},
|
||||
{
|
||||
"device": "router",
|
||||
"field": "ip",
|
||||
"input": ".0.0.1",
|
||||
"start": "192.168.0.10",
|
||||
"marked": false,
|
||||
"stored": null,
|
||||
"shown": "",
|
||||
"error": "NullPointerException"
|
||||
},
|
||||
{
|
||||
"device": "router",
|
||||
"field": "ip",
|
||||
"input": "+10.0.0.1",
|
||||
"start": "192.168.0.10",
|
||||
"marked": true,
|
||||
"stored": "192.168.0.10",
|
||||
"shown": "192.168.0.10"
|
||||
},
|
||||
{
|
||||
"device": "router",
|
||||
"field": "ip",
|
||||
"input": "256.0.0.1",
|
||||
"start": "192.168.0.10",
|
||||
"marked": true,
|
||||
"stored": "192.168.0.10",
|
||||
"shown": "192.168.0.10"
|
||||
},
|
||||
{
|
||||
"device": "router",
|
||||
"field": "ip",
|
||||
"input": " 10.0.0.1",
|
||||
"start": "192.168.0.10",
|
||||
"marked": true,
|
||||
"stored": "192.168.0.10",
|
||||
"shown": "192.168.0.10"
|
||||
},
|
||||
{
|
||||
"device": "router",
|
||||
"field": "ip",
|
||||
"input": "10.0.0.1 ",
|
||||
"start": "192.168.0.10",
|
||||
"marked": true,
|
||||
"stored": "192.168.0.10",
|
||||
"shown": "192.168.0.10"
|
||||
},
|
||||
{
|
||||
"device": "router",
|
||||
"field": "ip",
|
||||
"input": "",
|
||||
"start": "192.168.0.10",
|
||||
"marked": true,
|
||||
"stored": "192.168.0.10",
|
||||
"shown": "192.168.0.10"
|
||||
},
|
||||
{
|
||||
"device": "router",
|
||||
"field": "ip",
|
||||
"input": "10.0.0.1.",
|
||||
"start": "192.168.0.10",
|
||||
"marked": true,
|
||||
"stored": "192.168.0.10",
|
||||
"shown": "192.168.0.10"
|
||||
},
|
||||
{
|
||||
"device": "router",
|
||||
"field": "mask",
|
||||
"input": "255.255.0.0",
|
||||
"start": "255.255.255.0",
|
||||
"marked": false,
|
||||
"stored": "255.255.0.0",
|
||||
"shown": "255.255.0.0"
|
||||
},
|
||||
{
|
||||
"device": "router",
|
||||
"field": "mask",
|
||||
"input": "255.0.255.0",
|
||||
"start": "255.255.255.0",
|
||||
"marked": false,
|
||||
"stored": "255.0.255.0",
|
||||
"shown": "255.0.255.0"
|
||||
},
|
||||
{
|
||||
"device": "router",
|
||||
"field": "mask",
|
||||
"input": "256.0.0.0",
|
||||
"start": "255.255.255.0",
|
||||
"marked": true,
|
||||
"stored": "255.255.255.0",
|
||||
"shown": "255.255.255.0"
|
||||
},
|
||||
{
|
||||
"device": "router",
|
||||
"field": "mask",
|
||||
"input": "255..255.0.0",
|
||||
"start": "255.255.255.0",
|
||||
"marked": true,
|
||||
"stored": "255.255.255.0",
|
||||
"shown": "255.255.255.0"
|
||||
},
|
||||
{
|
||||
"device": "router",
|
||||
"field": "mask",
|
||||
"input": "",
|
||||
"start": "255.255.255.0",
|
||||
"marked": true,
|
||||
"stored": "255.255.255.0",
|
||||
"shown": "255.255.255.0"
|
||||
},
|
||||
{
|
||||
"device": "router",
|
||||
"field": "gateway",
|
||||
"input": "",
|
||||
"start": "192.168.0.254",
|
||||
"marked": true,
|
||||
"stored": "",
|
||||
"shown": ""
|
||||
},
|
||||
{
|
||||
"device": "router",
|
||||
"field": "gateway",
|
||||
"input": "10.0.0.254",
|
||||
"start": "192.168.0.254",
|
||||
"marked": false,
|
||||
"stored": "10.0.0.254",
|
||||
"shown": "10.0.0.254"
|
||||
},
|
||||
{
|
||||
"device": "router",
|
||||
"field": "gateway",
|
||||
"input": " ",
|
||||
"start": "192.168.0.254",
|
||||
"marked": true,
|
||||
"stored": "",
|
||||
"shown": ""
|
||||
},
|
||||
{
|
||||
"device": "router",
|
||||
"field": "gateway",
|
||||
"input": "abc",
|
||||
"start": "192.168.0.254",
|
||||
"marked": true,
|
||||
"stored": "192.168.0.254",
|
||||
"shown": "192.168.0.254"
|
||||
},
|
||||
{
|
||||
"device": "router",
|
||||
"field": "gateway",
|
||||
"input": "192.168..0.1",
|
||||
"start": "192.168.0.254",
|
||||
"marked": true,
|
||||
"stored": "192.168.0.1",
|
||||
"shown": "192.168.0.1"
|
||||
},
|
||||
{
|
||||
"device": "router",
|
||||
"field": "gateway",
|
||||
"input": "10.0.0.256",
|
||||
"start": "192.168.0.254",
|
||||
"marked": true,
|
||||
"stored": "192.168.0.254",
|
||||
"shown": "192.168.0.254"
|
||||
},
|
||||
{
|
||||
"device": "homeRouter",
|
||||
"field": "name",
|
||||
"input": "Heim",
|
||||
"start": "Heimrouter",
|
||||
"marked": false,
|
||||
"stored": "Heim",
|
||||
"shown": "Heim"
|
||||
},
|
||||
{
|
||||
"device": "homeRouter",
|
||||
"field": "gateway",
|
||||
"input": "",
|
||||
"start": "192.168.0.254",
|
||||
"marked": true,
|
||||
"stored": "",
|
||||
"shown": ""
|
||||
},
|
||||
{
|
||||
"device": "homeRouter",
|
||||
"field": "gateway",
|
||||
"input": "42.0.0.1",
|
||||
"start": "192.168.0.254",
|
||||
"marked": false,
|
||||
"stored": "42.0.0.1",
|
||||
"shown": "42.0.0.1"
|
||||
},
|
||||
{
|
||||
"device": "homeRouter",
|
||||
"field": "gateway",
|
||||
"input": "42.0.0",
|
||||
"start": "192.168.0.254",
|
||||
"marked": true,
|
||||
"stored": "192.168.0.254",
|
||||
"shown": "192.168.0.254"
|
||||
},
|
||||
{
|
||||
"device": "homeRouter",
|
||||
"field": "wanIp",
|
||||
"input": "42.1.2.3",
|
||||
"start": "42.0.0.10",
|
||||
"marked": false,
|
||||
"stored": "42.1.2.3",
|
||||
"shown": "42.1.2.3"
|
||||
},
|
||||
{
|
||||
"device": "homeRouter",
|
||||
"field": "wanIp",
|
||||
"input": ".1.2.3",
|
||||
"start": "42.0.0.10",
|
||||
"marked": false,
|
||||
"stored": null,
|
||||
"shown": ""
|
||||
},
|
||||
{
|
||||
"device": "homeRouter",
|
||||
"field": "wanIp",
|
||||
"input": "42..1.2",
|
||||
"start": "42.0.0.10",
|
||||
"marked": true,
|
||||
"stored": "42.0.0.10",
|
||||
"shown": "42.0.0.10"
|
||||
},
|
||||
{
|
||||
"device": "homeRouter",
|
||||
"field": "wanIp",
|
||||
"input": "042.1.2.3",
|
||||
"start": "42.0.0.10",
|
||||
"marked": false,
|
||||
"stored": "42.1.2.3",
|
||||
"shown": "42.1.2.3"
|
||||
},
|
||||
{
|
||||
"device": "homeRouter",
|
||||
"field": "wanMask",
|
||||
"input": "255.255.0.0",
|
||||
"start": "255.0.0.0",
|
||||
"marked": false,
|
||||
"stored": "255.255.0.0",
|
||||
"shown": "255.255.0.0"
|
||||
},
|
||||
{
|
||||
"device": "homeRouter",
|
||||
"field": "wanMask",
|
||||
"input": "255.255",
|
||||
"start": "255.0.0.0",
|
||||
"marked": true,
|
||||
"stored": "255.0.0.0",
|
||||
"shown": "255.0.0.0"
|
||||
},
|
||||
{
|
||||
"device": "homeRouter",
|
||||
"field": "lanIp",
|
||||
"input": "192.168.1.1",
|
||||
"start": "192.168.0.10",
|
||||
"marked": false,
|
||||
"stored": "192.168.1.1",
|
||||
"shown": "192.168.1.1"
|
||||
},
|
||||
{
|
||||
"device": "homeRouter",
|
||||
"field": "lanIp",
|
||||
"input": "192.168.1.256",
|
||||
"start": "192.168.0.10",
|
||||
"marked": true,
|
||||
"stored": "192.168.0.10",
|
||||
"shown": "192.168.0.10"
|
||||
},
|
||||
{
|
||||
"device": "homeRouter",
|
||||
"field": "lanIp",
|
||||
"input": " 192.168.1.1",
|
||||
"start": "192.168.0.10",
|
||||
"marked": true,
|
||||
"stored": "192.168.0.10",
|
||||
"shown": "192.168.0.10"
|
||||
},
|
||||
{
|
||||
"device": "homeRouter",
|
||||
"field": "lanMask",
|
||||
"input": "255.255.255.128",
|
||||
"start": "255.255.255.0",
|
||||
"marked": false,
|
||||
"stored": "255.255.255.128",
|
||||
"shown": "255.255.255.128"
|
||||
},
|
||||
{
|
||||
"device": "homeRouter",
|
||||
"field": "lanMask",
|
||||
"input": "abc",
|
||||
"start": "255.255.255.0",
|
||||
"marked": true,
|
||||
"stored": "255.255.255.0",
|
||||
"shown": "255.255.255.0"
|
||||
}
|
||||
],
|
||||
"flows": {
|
||||
"useMacAsNameThenOff": "0E:EE:AA:A5:58:21",
|
||||
"useMacAsNameThenOff": "CE:0D:5A:24:8A:C6",
|
||||
"useIpAsNameThenOff": "PC-A",
|
||||
"dhcpClientStopsServer": true,
|
||||
"dhcpClient": true
|
||||
"dhcpClient": true,
|
||||
"homeRouterDhcpClientStopsServer": false,
|
||||
"homeRouterDhcpClient": true
|
||||
},
|
||||
"tables": {
|
||||
"gatewayInSecondNetwork": [
|
||||
{
|
||||
"cells": [
|
||||
"192.168.1.1",
|
||||
"255.255.255.255",
|
||||
"127.0.0.1",
|
||||
"127.0.0.1"
|
||||
],
|
||||
"manual": false
|
||||
},
|
||||
{
|
||||
"cells": [
|
||||
"10.0.0.1",
|
||||
"255.255.255.255",
|
||||
"127.0.0.1",
|
||||
"127.0.0.1"
|
||||
],
|
||||
"manual": false
|
||||
},
|
||||
{
|
||||
"cells": [
|
||||
"192.168.1.0",
|
||||
"255.255.255.0",
|
||||
"192.168.1.1",
|
||||
"192.168.1.1"
|
||||
],
|
||||
"manual": false
|
||||
},
|
||||
{
|
||||
"cells": [
|
||||
"10.0.0.0",
|
||||
"255.0.0.0",
|
||||
"10.0.0.1",
|
||||
"10.0.0.1"
|
||||
],
|
||||
"manual": false
|
||||
},
|
||||
{
|
||||
"cells": [
|
||||
"127.0.0.0",
|
||||
"255.0.0.0",
|
||||
"127.0.0.1",
|
||||
"127.0.0.1"
|
||||
],
|
||||
"manual": false
|
||||
},
|
||||
{
|
||||
"cells": [
|
||||
"172.16.0.0",
|
||||
"255.255.0.0",
|
||||
"192.168.1.2",
|
||||
"192.168.1.1"
|
||||
],
|
||||
"manual": true
|
||||
},
|
||||
{
|
||||
"cells": [
|
||||
"10.20.0.0",
|
||||
"255.255.0.0",
|
||||
"10.0.0.2",
|
||||
"10.0.0.1"
|
||||
],
|
||||
"manual": true
|
||||
},
|
||||
{
|
||||
"cells": [
|
||||
"0.0.0.0",
|
||||
"0.0.0.0",
|
||||
"192.168.1.254",
|
||||
"192.168.1.1"
|
||||
],
|
||||
"manual": false
|
||||
}
|
||||
],
|
||||
"gatewayInNoNetwork": [
|
||||
{
|
||||
"cells": [
|
||||
"192.168.1.1",
|
||||
"255.255.255.255",
|
||||
"127.0.0.1",
|
||||
"127.0.0.1"
|
||||
],
|
||||
"manual": false
|
||||
},
|
||||
{
|
||||
"cells": [
|
||||
"10.0.0.1",
|
||||
"255.255.255.255",
|
||||
"127.0.0.1",
|
||||
"127.0.0.1"
|
||||
],
|
||||
"manual": false
|
||||
},
|
||||
{
|
||||
"cells": [
|
||||
"192.168.1.0",
|
||||
"255.255.255.0",
|
||||
"192.168.1.1",
|
||||
"192.168.1.1"
|
||||
],
|
||||
"manual": false
|
||||
},
|
||||
{
|
||||
"cells": [
|
||||
"10.0.0.0",
|
||||
"255.0.0.0",
|
||||
"10.0.0.1",
|
||||
"10.0.0.1"
|
||||
],
|
||||
"manual": false
|
||||
},
|
||||
{
|
||||
"cells": [
|
||||
"127.0.0.0",
|
||||
"255.0.0.0",
|
||||
"127.0.0.1",
|
||||
"127.0.0.1"
|
||||
],
|
||||
"manual": false
|
||||
},
|
||||
{
|
||||
"cells": [
|
||||
"172.16.0.0",
|
||||
"255.255.0.0",
|
||||
"192.168.1.2",
|
||||
"192.168.1.1"
|
||||
],
|
||||
"manual": true
|
||||
},
|
||||
{
|
||||
"cells": [
|
||||
"10.20.0.0",
|
||||
"255.255.0.0",
|
||||
"10.0.0.2",
|
||||
"10.0.0.1"
|
||||
],
|
||||
"manual": true
|
||||
},
|
||||
{
|
||||
"cells": [
|
||||
"0.0.0.0",
|
||||
"0.0.0.0",
|
||||
"172.30.0.1",
|
||||
"10.0.0.1"
|
||||
],
|
||||
"manual": false
|
||||
}
|
||||
],
|
||||
"gatewayInBothNetworks": [
|
||||
{
|
||||
"cells": [
|
||||
"10.1.0.1",
|
||||
"255.255.255.255",
|
||||
"127.0.0.1",
|
||||
"127.0.0.1"
|
||||
],
|
||||
"manual": false
|
||||
},
|
||||
{
|
||||
"cells": [
|
||||
"10.0.0.1",
|
||||
"255.255.255.255",
|
||||
"127.0.0.1",
|
||||
"127.0.0.1"
|
||||
],
|
||||
"manual": false
|
||||
},
|
||||
{
|
||||
"cells": [
|
||||
"10.1.0.0",
|
||||
"255.255.0.0",
|
||||
"10.1.0.1",
|
||||
"10.1.0.1"
|
||||
],
|
||||
"manual": false
|
||||
},
|
||||
{
|
||||
"cells": [
|
||||
"10.0.0.0",
|
||||
"255.0.0.0",
|
||||
"10.0.0.1",
|
||||
"10.0.0.1"
|
||||
],
|
||||
"manual": false
|
||||
},
|
||||
{
|
||||
"cells": [
|
||||
"127.0.0.0",
|
||||
"255.0.0.0",
|
||||
"127.0.0.1",
|
||||
"127.0.0.1"
|
||||
],
|
||||
"manual": false
|
||||
},
|
||||
{
|
||||
"cells": [
|
||||
"0.0.0.0",
|
||||
"0.0.0.0",
|
||||
"10.1.0.254",
|
||||
"10.1.0.1"
|
||||
],
|
||||
"manual": false
|
||||
}
|
||||
],
|
||||
"noGateway": [
|
||||
{
|
||||
"cells": [
|
||||
"192.168.1.1",
|
||||
"255.255.255.255",
|
||||
"127.0.0.1",
|
||||
"127.0.0.1"
|
||||
],
|
||||
"manual": false
|
||||
},
|
||||
{
|
||||
"cells": [
|
||||
"10.0.0.1",
|
||||
"255.255.255.255",
|
||||
"127.0.0.1",
|
||||
"127.0.0.1"
|
||||
],
|
||||
"manual": false
|
||||
},
|
||||
{
|
||||
"cells": [
|
||||
"192.168.1.0",
|
||||
"255.255.255.0",
|
||||
"192.168.1.1",
|
||||
"192.168.1.1"
|
||||
],
|
||||
"manual": false
|
||||
},
|
||||
{
|
||||
"cells": [
|
||||
"10.0.0.0",
|
||||
"255.0.0.0",
|
||||
"10.0.0.1",
|
||||
"10.0.0.1"
|
||||
],
|
||||
"manual": false
|
||||
},
|
||||
{
|
||||
"cells": [
|
||||
"127.0.0.0",
|
||||
"255.0.0.0",
|
||||
"127.0.0.1",
|
||||
"127.0.0.1"
|
||||
],
|
||||
"manual": false
|
||||
},
|
||||
{
|
||||
"cells": [
|
||||
"172.16.0.0",
|
||||
"255.255.0.0",
|
||||
"192.168.1.2",
|
||||
"192.168.1.1"
|
||||
],
|
||||
"manual": true
|
||||
},
|
||||
{
|
||||
"cells": [
|
||||
"10.20.0.0",
|
||||
"255.255.0.0",
|
||||
"10.0.0.2",
|
||||
"10.0.0.1"
|
||||
],
|
||||
"manual": true
|
||||
}
|
||||
],
|
||||
"threeInterfaces": [
|
||||
{
|
||||
"cells": [
|
||||
"172.16.5.1",
|
||||
"255.255.255.255",
|
||||
"127.0.0.1",
|
||||
"127.0.0.1"
|
||||
],
|
||||
"manual": false
|
||||
},
|
||||
{
|
||||
"cells": [
|
||||
"192.168.1.1",
|
||||
"255.255.255.255",
|
||||
"127.0.0.1",
|
||||
"127.0.0.1"
|
||||
],
|
||||
"manual": false
|
||||
},
|
||||
{
|
||||
"cells": [
|
||||
"10.0.0.1",
|
||||
"255.255.255.255",
|
||||
"127.0.0.1",
|
||||
"127.0.0.1"
|
||||
],
|
||||
"manual": false
|
||||
},
|
||||
{
|
||||
"cells": [
|
||||
"172.16.0.0",
|
||||
"255.255.0.0",
|
||||
"172.16.5.1",
|
||||
"172.16.5.1"
|
||||
],
|
||||
"manual": false
|
||||
},
|
||||
{
|
||||
"cells": [
|
||||
"192.168.1.0",
|
||||
"255.255.255.0",
|
||||
"192.168.1.1",
|
||||
"192.168.1.1"
|
||||
],
|
||||
"manual": false
|
||||
},
|
||||
{
|
||||
"cells": [
|
||||
"10.0.0.0",
|
||||
"255.0.0.0",
|
||||
"10.0.0.1",
|
||||
"10.0.0.1"
|
||||
],
|
||||
"manual": false
|
||||
},
|
||||
{
|
||||
"cells": [
|
||||
"127.0.0.0",
|
||||
"255.0.0.0",
|
||||
"127.0.0.1",
|
||||
"127.0.0.1"
|
||||
],
|
||||
"manual": false
|
||||
},
|
||||
{
|
||||
"cells": [
|
||||
"0.0.0.0",
|
||||
"0.0.0.0",
|
||||
"10.0.0.254",
|
||||
"10.0.0.1"
|
||||
],
|
||||
"manual": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"edits": {
|
||||
"invalidCell": {
|
||||
"manual": [
|
||||
[
|
||||
null,
|
||||
"255.255.0.0",
|
||||
"192.168.0.2",
|
||||
"192.168.0.10"
|
||||
]
|
||||
]
|
||||
},
|
||||
"emptyCell": {
|
||||
"manual": [
|
||||
[
|
||||
null,
|
||||
"255.255.0.0",
|
||||
"192.168.0.2",
|
||||
"192.168.0.10"
|
||||
]
|
||||
]
|
||||
},
|
||||
"leadingZeros": {
|
||||
"manual": [
|
||||
[
|
||||
"10.0.0.0",
|
||||
"255.255.0.0",
|
||||
"192.168.0.2",
|
||||
"192.168.0.10"
|
||||
]
|
||||
]
|
||||
},
|
||||
"dottedTwice": {
|
||||
"manual": [
|
||||
[
|
||||
null,
|
||||
"255.255.0.0",
|
||||
"192.168.0.2",
|
||||
"192.168.0.10"
|
||||
]
|
||||
]
|
||||
},
|
||||
"maskCell": {
|
||||
"manual": [
|
||||
[
|
||||
"172.16.0.0",
|
||||
"255.0.255.0",
|
||||
"192.168.0.2",
|
||||
"192.168.0.10"
|
||||
]
|
||||
]
|
||||
},
|
||||
"newRowThreeCells": {
|
||||
"manual": [
|
||||
[
|
||||
"172.16.0.0",
|
||||
"255.255.0.0",
|
||||
"192.168.0.2",
|
||||
"192.168.0.10"
|
||||
]
|
||||
]
|
||||
},
|
||||
"newRowFourCells": {
|
||||
"manual": [
|
||||
[
|
||||
"172.16.0.0",
|
||||
"255.255.0.0",
|
||||
"192.168.0.2",
|
||||
"192.168.0.10"
|
||||
],
|
||||
[
|
||||
"10.9.0.0",
|
||||
"255.255.0.0",
|
||||
"10.0.0.9",
|
||||
"10.0.0.1"
|
||||
]
|
||||
]
|
||||
},
|
||||
"newRowInvalidCell": {
|
||||
"manual": [
|
||||
[
|
||||
"172.16.0.0",
|
||||
"255.255.0.0",
|
||||
"192.168.0.2",
|
||||
"192.168.0.10"
|
||||
]
|
||||
]
|
||||
},
|
||||
"newRowTwice": {
|
||||
"manual": [
|
||||
[
|
||||
"172.16.0.0",
|
||||
"255.255.0.0",
|
||||
"192.168.0.2",
|
||||
"192.168.0.10"
|
||||
]
|
||||
],
|
||||
"rowsShown": 2
|
||||
},
|
||||
"removeRow": {
|
||||
"manual": []
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+322
-29
@@ -28,25 +28,50 @@ import {
|
||||
setSwitchCloud,
|
||||
setSwitchRetention,
|
||||
setSwitchSsid,
|
||||
addRoute,
|
||||
removeRoute,
|
||||
setInterfaceAddress,
|
||||
setRip,
|
||||
setRouteCell,
|
||||
setRouterGateway,
|
||||
topologyView,
|
||||
type FieldRule,
|
||||
type HomeRouterSheet,
|
||||
type HostSheet,
|
||||
type NewDevice,
|
||||
type RouterSheet,
|
||||
} from '../src/index.js';
|
||||
|
||||
interface JavaCase {
|
||||
readonly device: 'host' | 'switch' | 'modem';
|
||||
readonly device: 'host' | 'switch' | 'modem' | 'router' | 'homeRouter';
|
||||
readonly field: string;
|
||||
readonly input: string;
|
||||
readonly start: string;
|
||||
readonly marked: boolean;
|
||||
readonly stored: string;
|
||||
/** null: Java's card lost its address. */
|
||||
readonly stored: string | null;
|
||||
readonly shown: string;
|
||||
/** What Java's panel threw, if it did. */
|
||||
readonly error?: string;
|
||||
}
|
||||
|
||||
interface JavaRow {
|
||||
readonly cells: readonly (string | null)[];
|
||||
readonly manual: boolean;
|
||||
}
|
||||
|
||||
const java = JSON.parse(readFileSync(join(dirname(fileURLToPath(import.meta.url)), 'sheet.java.json'), 'utf8')) as {
|
||||
readonly cases: readonly JavaCase[];
|
||||
readonly flows: { readonly useMacAsNameThenOff: string; readonly useIpAsNameThenOff: string; readonly dhcpClientStopsServer: boolean; readonly dhcpClient: boolean };
|
||||
readonly flows: {
|
||||
readonly useMacAsNameThenOff: string;
|
||||
readonly useIpAsNameThenOff: string;
|
||||
readonly dhcpClientStopsServer: boolean;
|
||||
readonly dhcpClient: boolean;
|
||||
readonly homeRouterDhcpClientStopsServer: boolean;
|
||||
readonly homeRouterDhcpClient: boolean;
|
||||
};
|
||||
readonly tables: Readonly<Record<string, readonly JavaRow[]>>;
|
||||
readonly edits: Readonly<Record<string, { readonly manual: readonly (readonly (string | null)[])[]; readonly rowsShown?: number }>>;
|
||||
};
|
||||
|
||||
const RULES: Readonly<Record<string, FieldRule>> = {
|
||||
@@ -60,10 +85,27 @@ const RULES: Readonly<Record<string, FieldRule>> = {
|
||||
'switch retention': 'seconds',
|
||||
'modem name': 'name',
|
||||
'modem port': 'port',
|
||||
'router name': 'name',
|
||||
'router ip': 'ipAddress',
|
||||
'router mask': 'subnetMask',
|
||||
'router gateway': 'ipAddressOrEmpty',
|
||||
'homeRouter name': 'name',
|
||||
'homeRouter gateway': 'ipAddressOrEmpty',
|
||||
'homeRouter wanIp': 'ipAddress',
|
||||
'homeRouter wanMask': 'subnetMask',
|
||||
'homeRouter lanIp': 'ipAddress',
|
||||
'homeRouter lanMask': 'subnetMask',
|
||||
};
|
||||
|
||||
/** Fields whose text is judged without the spaces around it. */
|
||||
const TRIMMED = new Set(['host ip', 'host mask', 'host gateway', 'host dns', 'switch retention', 'modem port']);
|
||||
const TRIMMED = new Set(
|
||||
Object.entries(RULES)
|
||||
.filter(([key, rule]) => rule !== 'name' && rule !== 'ssid')
|
||||
.map(([key]) => key),
|
||||
);
|
||||
|
||||
/** Whether Java's device took the text: its value changed (to an address: null is a card that lost its own), or was the text already. */
|
||||
const tookIt = (each: JavaCase) => each.stored !== null && (each.stored !== each.start || each.input === each.start);
|
||||
|
||||
/** What Java's model holds in the units of the sheet's value: seconds for the retention time. */
|
||||
const javaValue = (each: JavaCase) => (each.field === 'retention' ? String(Number(each.stored) / 1000) : each.stored);
|
||||
@@ -88,9 +130,12 @@ describe("a field's rule, against FILIUS' settings panels", () => {
|
||||
const key = `${each.device} ${each.field}`;
|
||||
const verdict = judged(each);
|
||||
// Taken: the value changed, or the text was the value already.
|
||||
const took = verdict.stored !== verdict.start || verdict.input === verdict.start;
|
||||
const took = tookIt(verdict);
|
||||
// An empty gateway Java's setter takes as "none", though a router's panel marks it (its key listener checks musterIpAdresse).
|
||||
const emptyTaken = RULES[key] === 'ipAddressOrEmpty' && verdict.input.trim() === '' && took;
|
||||
// A name is taken as typed. A modem's port Java never marks: it takes any int; here Java's own rule for ports.
|
||||
const expected = RULES[key] === 'name' ? true : key === 'modem port' ? took && /^([1-9][0-9]{0,4})$/.test(verdict.input) && Number(verdict.input) <= 65_535 : !verdict.marked && took;
|
||||
const expected =
|
||||
RULES[key] === 'name' ? true : key === 'modem port' ? took && /^([1-9][0-9]{0,4})$/.test(verdict.input) && Number(verdict.input) <= 65_535 : emptyTaken || (!verdict.marked && took);
|
||||
const checked = checkField(RULES[key]!, each.input);
|
||||
expect('value' in checked, `${key} ${JSON.stringify(each.input)}`).toBe(expected);
|
||||
if ('value' in checked) expect(checked.value, `${key} ${JSON.stringify(each.input)}`).toBe(RULES[key] === 'name' ? each.input : javaValue(verdict));
|
||||
@@ -98,39 +143,46 @@ describe("a field's rule, against FILIUS' settings panels", () => {
|
||||
});
|
||||
|
||||
it("names where Java's two judgements disagree, which here are one", () => {
|
||||
const disagreeing = java.cases.filter((each) => RULES[`${each.device} ${each.field}`] !== 'name' && each.field !== 'port' && each.marked === (each.stored !== each.start));
|
||||
// Marked red, yet taken; or black, yet refused (the gateway's spaces Java trims to empty).
|
||||
expect(disagreeing.map((each) => `${each.field} ${JSON.stringify(each.input)} ${each.marked ? 'marked, taken' : 'refused'}`)).toEqual([
|
||||
'ip "10..0.0.1" marked, taken',
|
||||
'ip ".0.0.1" refused',
|
||||
'ip "+10.0.0.1" marked, taken',
|
||||
'ip "10.0.0.1." marked, taken',
|
||||
'ip "١٠.0.0.1" marked, taken',
|
||||
'ip "-0.0.0.1" marked, taken',
|
||||
'mask "255..255.0.0" marked, taken',
|
||||
'gateway " " marked, taken',
|
||||
'gateway "192.168..1.1" marked, taken',
|
||||
'dns " " marked, taken',
|
||||
'retention "" refused',
|
||||
'retention "-5" marked, taken',
|
||||
'retention "99999999999999999999" refused',
|
||||
'retention "٣" marked, taken',
|
||||
const disagreeing = java.cases.filter((each) => RULES[`${each.device} ${each.field}`] !== 'name' && each.field !== 'port' && each.marked === tookIt(each));
|
||||
// Marked red, yet taken; or black, yet refused (the gateway's spaces Java trims to empty) — or lost: a card without its address.
|
||||
const verdictOf = (each: JavaCase) => (each.marked ? 'marked, taken' : each.stored === null ? `lost${each.error === undefined ? '' : `, ${each.error}`}` : 'refused');
|
||||
expect(disagreeing.map((each) => `${each.device} ${each.field} ${JSON.stringify(each.input)} ${verdictOf(each)}`)).toEqual([
|
||||
'host ip "10..0.0.1" marked, taken',
|
||||
'host ip ".0.0.1" refused',
|
||||
'host ip "+10.0.0.1" marked, taken',
|
||||
'host ip "10.0.0.1." marked, taken',
|
||||
'host ip "١٠.0.0.1" marked, taken',
|
||||
'host ip "-0.0.0.1" marked, taken',
|
||||
'host mask "255..255.0.0" marked, taken',
|
||||
'host gateway " " marked, taken',
|
||||
'host gateway "192.168..1.1" marked, taken',
|
||||
'host dns " " marked, taken',
|
||||
'switch retention "" refused',
|
||||
'switch retention "-5" marked, taken',
|
||||
'switch retention "99999999999999999999" refused',
|
||||
'switch retention "٣" marked, taken',
|
||||
'router ip ".0.0.1" lost, NullPointerException',
|
||||
'router gateway "" marked, taken',
|
||||
'router gateway " " marked, taken',
|
||||
'router gateway "192.168..0.1" marked, taken',
|
||||
'homeRouter gateway "" marked, taken',
|
||||
'homeRouter wanIp ".1.2.3" lost',
|
||||
]);
|
||||
const ports = java.cases.filter((each) => each.field === 'port' && each.stored !== each.start);
|
||||
expect(ports.map((each) => each.input)).toEqual(['0', '65535', '65536', '-1', '080', '80']);
|
||||
});
|
||||
|
||||
it("shows what the device holds once a field is left, as Java's field does", () => {
|
||||
for (const each of java.cases) if (each.field !== 'retention') expect(each.shown, each.input).toBe(each.stored);
|
||||
for (const each of java.cases) if (each.field !== 'retention') expect(each.shown, each.input).toBe(each.stored ?? '');
|
||||
});
|
||||
});
|
||||
|
||||
let ids = 0;
|
||||
const newId = () => `20000000-0000-0000-0000-${(++ids).toString(16).padStart(12, '0')}`;
|
||||
|
||||
function add(doc: Y.Doc, kind: NewDevice['kind'], name: string = kind): string {
|
||||
function add(doc: Y.Doc, kind: NewDevice['kind'], name: string = kind, interfaces?: number): string {
|
||||
const id = newId();
|
||||
addDevice(doc, { kind, id, name, x: 100, y: 100, newId }, 'me');
|
||||
addDevice(doc, { kind, id, name, x: 100, y: 100, newId, ...(interfaces === undefined ? {} : { interfaces }) }, 'me');
|
||||
return id;
|
||||
}
|
||||
|
||||
@@ -345,7 +397,7 @@ describe("a switch's and a modem's sheet", () => {
|
||||
expect(deviceSheet(doc, hub)).toMatchObject({ retentionSeconds: 1 });
|
||||
});
|
||||
|
||||
it("pairs a modem by its port, and names the other kinds' sheets", () => {
|
||||
it('pairs a modem by its port, and renames a router', () => {
|
||||
const doc = new Y.Doc();
|
||||
const modem = add(doc, 'remoteLink', 'Modem');
|
||||
expect(deviceSheet(doc, modem)).toEqual({ kind: 'remoteLink', name: 'Modem', port: 12_345 });
|
||||
@@ -355,8 +407,8 @@ describe("a switch's and a modem's sheet", () => {
|
||||
expect(deviceSheet(doc, modem)).toMatchObject({ port: 80 });
|
||||
const router = add(doc, 'router', 'R1');
|
||||
renameDevice(doc, router, 'R2', 'me');
|
||||
expect(deviceSheet(doc, router)).toEqual({ kind: 'router', name: 'R2' });
|
||||
expect(deviceSheet(doc, add(doc, 'gateway', 'Heim'))).toEqual({ kind: 'gateway', name: 'Heim' });
|
||||
expect(deviceSheet(doc, router)).toMatchObject({ kind: 'router', name: 'R2' });
|
||||
expect(deviceSheet(doc, add(doc, 'gateway', 'Heim'))).toMatchObject({ kind: 'gateway', name: 'Heim' });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -413,3 +465,244 @@ describe("the sheet's changes", () => {
|
||||
expect(reopened.remoteLinkConfigurations.get(modemAgain!.id)?.pairIdentifier).toBe('java-modem-port-4242');
|
||||
});
|
||||
});
|
||||
|
||||
const router = (doc: Y.Doc, id: string) => deviceSheet(doc, id) as RouterSheet;
|
||||
const home = (doc: Y.Doc, id: string) => deviceSheet(doc, id) as HomeRouterSheet;
|
||||
|
||||
/** Java's manual rows of the measured tables, as SheetRun added them. */
|
||||
const JAVA_MANUAL = [
|
||||
{ destinationNetwork: '172.16.0.0', subnetMask: '255.255.0.0', gateway: '192.168.1.2', interfaceIPAddress: '192.168.1.1' },
|
||||
{ destinationNetwork: '010.020.0.0', subnetMask: '255.255.0.0', gateway: '10.0.0.2', interfaceIPAddress: '10.0.0.1' },
|
||||
];
|
||||
|
||||
/** The measured tables' routers: their cards (address, mask), gateway and manual rows. */
|
||||
const TABLES: Readonly<Record<string, { readonly cards: readonly (readonly [string, string])[]; readonly gateway: string; readonly manual: boolean }>> = {
|
||||
gatewayInSecondNetwork: {
|
||||
cards: [
|
||||
['10.0.0.1', '255.0.0.0'],
|
||||
['192.168.1.1', '255.255.255.0'],
|
||||
],
|
||||
gateway: '192.168.1.254',
|
||||
manual: true,
|
||||
},
|
||||
gatewayInNoNetwork: {
|
||||
cards: [
|
||||
['10.0.0.1', '255.0.0.0'],
|
||||
['192.168.1.1', '255.255.255.0'],
|
||||
],
|
||||
gateway: '172.30.0.1',
|
||||
manual: true,
|
||||
},
|
||||
gatewayInBothNetworks: {
|
||||
cards: [
|
||||
['10.0.0.1', '255.0.0.0'],
|
||||
['10.1.0.1', '255.255.0.0'],
|
||||
],
|
||||
gateway: '10.1.0.254',
|
||||
manual: false,
|
||||
},
|
||||
noGateway: {
|
||||
cards: [
|
||||
['10.0.0.1', '255.0.0.0'],
|
||||
['192.168.1.1', '255.255.255.0'],
|
||||
],
|
||||
gateway: '',
|
||||
manual: true,
|
||||
},
|
||||
threeInterfaces: {
|
||||
cards: [
|
||||
['10.0.0.1', '255.0.0.0'],
|
||||
['192.168.1.1', '255.255.255.0'],
|
||||
['172.16.5.1', '255.255.0.0'],
|
||||
],
|
||||
gateway: '10.0.0.254',
|
||||
manual: false,
|
||||
},
|
||||
};
|
||||
|
||||
describe("a router's sheet", () => {
|
||||
it("shows the forwarding table Java shows, in Java's order, its manual rows editable", () => {
|
||||
expect(Object.keys(java.tables).sort()).toEqual(Object.keys(TABLES).sort());
|
||||
for (const [name, setup] of Object.entries(TABLES)) {
|
||||
const doc = new Y.Doc();
|
||||
const id = add(doc, 'router', 'R', setup.cards.length);
|
||||
router(doc, id).interfaces.forEach((card, index) => {
|
||||
expect(setInterfaceAddress(doc, id, card.portId, 'ipAddress', setup.cards[index]![0], 'me')).toEqual({ value: setup.cards[index]![0] });
|
||||
expect(setInterfaceAddress(doc, id, card.portId, 'subnetMask', setup.cards[index]![1], 'me')).toEqual({ value: setup.cards[index]![1] });
|
||||
});
|
||||
setRouterGateway(doc, id, setup.gateway, 'me');
|
||||
if (setup.manual) for (const row of JAVA_MANUAL) expect('route' in addRoute(doc, id, row, 'me')).toBe(true);
|
||||
const table = router(doc, id).table.map((row) => ({ cells: [row.destinationNetwork, row.subnetMask, row.nextHop, row.interfaceIPAddress], manual: row.isEditable }));
|
||||
expect(table, name).toEqual(java.tables[name]);
|
||||
}
|
||||
});
|
||||
|
||||
it('takes a card’s address as Java keeps it and keeps the old one for a refused text, the first card mirrored in the device', () => {
|
||||
const doc = new Y.Doc();
|
||||
const id = add(doc, 'router');
|
||||
const [first, second] = router(doc, id).interfaces;
|
||||
expect(setInterfaceAddress(doc, id, first!.portId, 'ipAddress', ' 010.0.0.1 ', 'me')).toEqual({ value: '10.0.0.1' });
|
||||
expect(setInterfaceAddress(doc, id, first!.portId, 'ipAddress', '.0.0.1', 'me')).toEqual({ invalid: true });
|
||||
expect(setInterfaceAddress(doc, id, second!.portId, 'subnetMask', '255.255', 'me')).toEqual({ invalid: true });
|
||||
expect(setInterfaceAddress(doc, id, second!.portId, 'subnetMask', '255.0.255.0', 'me')).toEqual({ value: '255.0.255.0' });
|
||||
expect(setInterfaceAddress(doc, id, 'no-port', 'ipAddress', '10.0.0.1', 'me')).toEqual({ value: '10.0.0.1' });
|
||||
expect(router(doc, id).interfaces.map((card) => [card.ipAddress, card.subnetMask])).toEqual([
|
||||
['10.0.0.1', '255.255.255.0'],
|
||||
['192.168.0.10', '255.0.255.0'],
|
||||
]);
|
||||
// A router keeps its own settings only with a gateway; then they follow its first card.
|
||||
expect(readProject(doc).deviceConfigurations.has(id)).toBe(false);
|
||||
expect(setRouterGateway(doc, id, '10.0.0.254', 'me')).toEqual({ value: '10.0.0.254' });
|
||||
expect(readProject(doc).deviceConfigurations.get(id)).toEqual({ ipAddress: '10.0.0.1', subnetMask: '255.255.255.0', defaultGateway: '10.0.0.254', dnsServer: '' });
|
||||
setInterfaceAddress(doc, id, first!.portId, 'ipAddress', '10.0.0.2', 'me');
|
||||
setInterfaceAddress(doc, id, first!.portId, 'subnetMask', '255.0.0.0', 'me');
|
||||
expect(readProject(doc).deviceConfigurations.get(id)).toMatchObject({ ipAddress: '10.0.0.2', subnetMask: '255.0.0.0' });
|
||||
expect(setRouterGateway(doc, id, 'abc', 'me')).toEqual({ invalid: true });
|
||||
expect(router(doc, id).gateway).toBe('10.0.0.254');
|
||||
expect(setRouterGateway(doc, id, ' ', 'me')).toEqual({ value: '' });
|
||||
expect(readProject(doc).deviceConfigurations.has(id)).toBe(false);
|
||||
});
|
||||
|
||||
it('switches RIP, and shows IP forwarding on', () => {
|
||||
const doc = new Y.Doc();
|
||||
const id = add(doc, 'router');
|
||||
expect(router(doc, id)).toMatchObject({ rip: false, ipForwarding: true, gateway: '' });
|
||||
setRip(doc, id, true, 'me');
|
||||
expect(router(doc, id).rip).toBe(true);
|
||||
expect(readProject(doc).ripEnabledByNodeId.get(id)).toBe(true);
|
||||
setRip(doc, id, false, 'me');
|
||||
expect(readProject(doc).ripEnabledByNodeId.has(id)).toBe(false);
|
||||
});
|
||||
|
||||
it('names what each card is plugged into, by its label', () => {
|
||||
const doc = new Y.Doc();
|
||||
const id = add(doc, 'router');
|
||||
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[0]!.macAddress).toMatch(/^([0-9A-F]{2}:){5}[0-9A-F]{2}$/);
|
||||
});
|
||||
});
|
||||
|
||||
describe("a router's manual rows", () => {
|
||||
/** A router with Java's one manual row of SheetRun's edits. */
|
||||
function edited() {
|
||||
const doc = new Y.Doc();
|
||||
const id = add(doc, 'router');
|
||||
addRoute(doc, id, { destinationNetwork: '172.16.0.0', subnetMask: '255.255.0.0', gateway: '192.168.0.2', interfaceIPAddress: '192.168.0.10' }, 'me');
|
||||
const rows = () =>
|
||||
readProject(doc)
|
||||
.manualRoutesByNodeId.get(id)
|
||||
?.map((row) => [row.destinationNetwork, row.subnetMask, row.gateway, row.interfaceIPAddress]) ?? [];
|
||||
return { doc, id, rows };
|
||||
}
|
||||
|
||||
it('take a cell as Java does where Java takes it, and keep it where Java stores null', () => {
|
||||
for (const [name, column, text] of [
|
||||
['leadingZeros', 'destinationNetwork', '010.0.0.0'],
|
||||
['maskCell', 'subnetMask', '255.0.255.0'],
|
||||
] as const) {
|
||||
const { doc, id, rows } = edited();
|
||||
expect('value' in setRouteCell(doc, id, 0, column, text, 'me'), name).toBe(true);
|
||||
expect(rows(), name).toEqual(java.edits[name]!.manual);
|
||||
}
|
||||
for (const [name, text] of [
|
||||
['invalidCell', 'abc'],
|
||||
['emptyCell', ''],
|
||||
['dottedTwice', '10..0.0'],
|
||||
] as const) {
|
||||
// Java: the cell becomes null.
|
||||
expect(java.edits[name]!.manual[0]![0], name).toBeNull();
|
||||
const { doc, id, rows } = edited();
|
||||
expect(setRouteCell(doc, id, 0, 'destinationNetwork', text, 'me'), name).toEqual({ invalid: true });
|
||||
expect(rows()[0]![0], name).toBe('172.16.0.0');
|
||||
}
|
||||
});
|
||||
|
||||
it('edit and lose the row they are asked for', () => {
|
||||
const { doc, id, rows } = edited();
|
||||
addRoute(doc, id, JAVA_MANUAL[0]!, 'me');
|
||||
expect(setRouteCell(doc, id, 1, 'gateway', '192.168.1.3', 'me')).toEqual({ value: '192.168.1.3' });
|
||||
expect(rows()).toEqual([
|
||||
['172.16.0.0', '255.255.0.0', '192.168.0.2', '192.168.0.10'],
|
||||
['172.16.0.0', '255.255.0.0', '192.168.1.3', '192.168.1.1'],
|
||||
]);
|
||||
removeRoute(doc, id, 1, 'me');
|
||||
expect(rows()).toEqual([['172.16.0.0', '255.255.0.0', '192.168.0.2', '192.168.0.10']]);
|
||||
});
|
||||
|
||||
it('take a new row once all four cells pass, as Java does, and lose a row', () => {
|
||||
const cells = { destinationNetwork: '10.9.0.0', subnetMask: '255.255.0.0', gateway: '10.0.0.9', interfaceIPAddress: '' };
|
||||
let { doc, id, rows } = edited();
|
||||
expect(addRoute(doc, id, cells, 'me')).toEqual({ invalid: ['interfaceIPAddress'] });
|
||||
expect(rows()).toEqual(java.edits.newRowThreeCells!.manual);
|
||||
expect(addRoute(doc, id, { ...cells, interfaceIPAddress: '10..0.0.1' }, 'me')).toEqual({ invalid: ['interfaceIPAddress'] });
|
||||
expect(rows()).toEqual(java.edits.newRowInvalidCell!.manual);
|
||||
expect(addRoute(doc, id, { ...cells, interfaceIPAddress: '10.0.0.1' }, 'me')).toEqual({ route: { ...cells, interfaceIPAddress: '10.0.0.1' } });
|
||||
expect(rows()).toEqual(java.edits.newRowFourCells!.manual);
|
||||
expect(addRoute(doc, 'nobody', { ...cells, interfaceIPAddress: '10.0.0.1' }, 'me')).toEqual({ invalid: [] });
|
||||
({ doc, id, rows } = edited());
|
||||
removeRoute(doc, id, 5, 'me');
|
||||
removeRoute(doc, id, 0, 'me');
|
||||
expect(rows()).toEqual(java.edits.removeRow!.manual);
|
||||
expect((doc.getMap<Y.Map<unknown>>(ROOTS.nodes).get(id)!.get('settings') as Y.Map<unknown>).has('routes')).toBe(false);
|
||||
// Java keeps one new row at a time; the sheet's draft is one row too.
|
||||
expect(java.edits.newRowTwice!.rowsShown).toBe(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe("a home router's sheet", () => {
|
||||
it("shows its WAN and LAN cards, keeps the WAN card in the device's settings, and leaves its DHCP server running", () => {
|
||||
expect(java.flows).toMatchObject({ homeRouterDhcpClient: true, homeRouterDhcpClientStopsServer: false });
|
||||
const doc = new Y.Doc();
|
||||
const id = add(doc, 'gateway', 'Heim');
|
||||
const sheet = home(doc, id);
|
||||
expect([sheet.wan.ipAddress, sheet.wan.subnetMask, sheet.lan.ipAddress, sheet.lan.subnetMask, sheet.gateway, sheet.ipForwarding, sheet.dhcpClient]).toEqual([
|
||||
'42.0.0.10',
|
||||
'255.0.0.0',
|
||||
'192.168.0.10',
|
||||
'255.255.255.0',
|
||||
'',
|
||||
true,
|
||||
false,
|
||||
]);
|
||||
expect(setInterfaceAddress(doc, id, sheet.wan.portId, 'ipAddress', '042.1.2.3', 'me')).toEqual({ value: '42.1.2.3' });
|
||||
expect(setInterfaceAddress(doc, id, sheet.lan.portId, 'subnetMask', '255.255.255.128', 'me')).toEqual({ value: '255.255.255.128' });
|
||||
expect(setRouterGateway(doc, id, '42.0.0.1', 'me')).toEqual({ value: '42.0.0.1' });
|
||||
expect(readProject(doc).deviceConfigurations.get(id)).toEqual({ ipAddress: '42.1.2.3', subnetMask: '255.0.0.0', defaultGateway: '42.0.0.1', dnsServer: '' });
|
||||
// Unlike a router, a home router keeps its own settings without a gateway.
|
||||
setRouterGateway(doc, id, '', 'me');
|
||||
expect(readProject(doc).deviceConfigurations.get(id)).toMatchObject({ defaultGateway: '' });
|
||||
const settings = doc.getMap<Y.Map<unknown>>(ROOTS.nodes).get(id)!.get('settings') as Y.Map<unknown>;
|
||||
const server = new Y.Map<unknown>();
|
||||
server.set('isActive', true);
|
||||
settings.set('dhcpServer', server);
|
||||
setDhcpClient(doc, id, true, 'me');
|
||||
expect(home(doc, id).dhcpClient).toBe(true);
|
||||
expect(server.get('isActive')).toBe(true);
|
||||
});
|
||||
|
||||
it('keeps everything through a saved file, with the router’s rows and RIP', () => {
|
||||
const doc = new Y.Doc();
|
||||
const id = add(doc, 'router', 'R', 3);
|
||||
const homeId = add(doc, 'gateway', 'Heim');
|
||||
const cards = router(doc, id).interfaces;
|
||||
setInterfaceAddress(doc, id, cards[2]!.portId, 'ipAddress', '172.16.5.1', 'me');
|
||||
setRouterGateway(doc, id, '192.168.0.254', 'me');
|
||||
setRip(doc, id, true, 'me');
|
||||
addRoute(doc, id, JAVA_MANUAL[1]!, 'me');
|
||||
setInterfaceAddress(doc, homeId, home(doc, homeId).lan.portId, 'ipAddress', '192.168.7.1', 'me');
|
||||
setRouterGateway(doc, homeId, '42.0.0.1', 'me');
|
||||
let counter = 0;
|
||||
const reopened = openProjectArchive(saveProjectArchive(readProject(doc)).archive, () => `40000000-0000-0000-0000-${(++counter).toString(16).padStart(12, '0')}`);
|
||||
const [routerAgain, homeAgain] = reopened.graph.nodes;
|
||||
const address = (node: typeof routerAgain, index: number) => reopened.interfaceConfigurations.get(`${node!.id}/${node!.ports[index]!.id}`)?.ipAddress;
|
||||
expect(address(routerAgain, 2)).toBe('172.16.5.1');
|
||||
expect(reopened.deviceConfigurations.get(routerAgain!.id)?.defaultGateway).toBe('192.168.0.254');
|
||||
expect(reopened.ripEnabledByNodeId.get(routerAgain!.id)).toBe(true);
|
||||
expect(reopened.manualRoutesByNodeId.get(routerAgain!.id)).toEqual([{ destinationNetwork: '10.20.0.0', subnetMask: '255.255.0.0', gateway: '10.0.0.2', interfaceIPAddress: '10.0.0.1' }]);
|
||||
expect(address(homeAgain, 1)).toBe('192.168.7.1');
|
||||
expect(reopened.deviceConfigurations.get(homeAgain!.id)?.defaultGateway).toBe('42.0.0.1');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
// beside the surface on a wide screen, below it on a phone.
|
||||
|
||||
import { expect, test, type Page } from '@playwright/test';
|
||||
import type { HostSheet } from '@mdg-filius/doc';
|
||||
import type { HostSheet, RouterSheet } from '@mdg-filius/doc';
|
||||
import { drawing, onDevice, openEditor, serveEditor, sheetOf } from './editorPage.js';
|
||||
|
||||
let server: { readonly base: string; readonly close: () => Promise<void> };
|
||||
@@ -156,6 +156,100 @@ test("joins a notebook to a switch's wireless network, drawn as wireless", async
|
||||
await expect.poll(async () => (await drawing(page)).links).toEqual([]);
|
||||
});
|
||||
|
||||
test("shows a router's cards in Java's tabs, draws the open card's cable active, and renames a tab by its address", 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 values = (await sheetOf(page, id)) as RouterSheet;
|
||||
const tabs = sheet(page).getByRole('tab');
|
||||
await expect(tabs).toHaveText(['Allgemein', ...values.interfaces.map((card) => card.ipAddress), 'Weiterleitungstabelle']);
|
||||
await expect(field(page, 'name')).toHaveValue(values.name);
|
||||
await expect(sheet(page).getByLabel('IP-Weiterleitung aktivieren')).toBeDisabled();
|
||||
await expect(sheet(page).getByLabel('IP-Weiterleitung aktivieren')).toBeChecked();
|
||||
const card = values.interfaces[0]!;
|
||||
await tabs.nth(1).click();
|
||||
await expect(sheet(page).locator('.sheet-connected')).toHaveText(`Verbunden mit ${card.connected!.device}`);
|
||||
await expect(page.locator(`path[data-link="${card.connected!.link}"]`)).toHaveAttribute('data-active', 'true');
|
||||
// Java's active cable is green (JCablePanel.farbeBlinken).
|
||||
await expect(page.locator(`path[data-link="${card.connected!.link}"]`)).toHaveCSS('stroke', 'rgb(0, 255, 64)');
|
||||
await field(page, 'ipAddress').fill('192.168.0.1');
|
||||
await field(page, 'ipAddress').press('Enter');
|
||||
await expect(tabs.nth(1)).toHaveText('192.168.0.1');
|
||||
await tabs.first().click();
|
||||
await expect(page.locator('path[data-active]')).toHaveCount(0);
|
||||
await tabs.nth(1).click();
|
||||
await sheet(page).getByRole('button', { name: 'Schließen' }).click();
|
||||
await expect(page.locator('path[data-active]')).toHaveCount(0);
|
||||
});
|
||||
|
||||
test("edits a router's forwarding table: a new row once its four cells pass, a cell, the row removed; RIP greys it out", async ({ page }) => {
|
||||
await openEditor(page, server.base, 'new');
|
||||
await page.locator('[data-palette="router"]').click();
|
||||
await page.getByRole('dialog').getByRole('button', { name: '2', exact: true }).click();
|
||||
const id = (await drawing(page)).nodes[0]!.id;
|
||||
const tabs = sheet(page).getByRole('tab');
|
||||
await tabs.last().click();
|
||||
const rows = sheet(page).locator('.route-table tbody tr');
|
||||
// Java's own rows: two addresses, two networks (the same twice: both cards are new), localhost.
|
||||
await expect(rows).toHaveCount(5);
|
||||
await sheet(page).getByRole('button', { name: 'Neuer Eintrag' }).click();
|
||||
await expect(sheet(page).getByText('Die Zeile wird übernommen, sobald alle vier Felder gültig sind.')).toBeVisible();
|
||||
await expect(field(page, 'route-destinationNetwork')).toHaveAttribute('aria-invalid', 'false');
|
||||
for (const [column, text] of [
|
||||
['destinationNetwork', '10.0.0.0'],
|
||||
['subnetMask', '255.0.0.0'],
|
||||
['gateway', '192.168.0.1'],
|
||||
] as const) {
|
||||
await field(page, `route-${column}`).fill(text);
|
||||
await field(page, `route-${column}`).press('Enter');
|
||||
}
|
||||
expect(((await sheetOf(page, id)) as RouterSheet).routes).toEqual([]);
|
||||
await field(page, 'route-interfaceIPAddress').fill('192.168.0.10');
|
||||
await field(page, 'route-interfaceIPAddress').press('Enter');
|
||||
await expect.poll(async () => ((await sheetOf(page, id)) as RouterSheet).routes).toEqual([{ destinationNetwork: '10.0.0.0', subnetMask: '255.0.0.0', gateway: '192.168.0.1', interfaceIPAddress: '192.168.0.10' }]);
|
||||
await expect(sheet(page).locator('tr[data-manual="0"]')).toHaveAttribute('aria-selected', 'true');
|
||||
await field(page, 'route-gateway').fill('192.168.0.2');
|
||||
await field(page, 'route-gateway').press('Enter');
|
||||
await expect(sheet(page).locator('tr[data-manual="0"] td').nth(2)).toHaveText('192.168.0.2');
|
||||
await sheet(page).getByLabel('Alle Einträge anzeigen').uncheck();
|
||||
await expect(rows).toHaveCount(1);
|
||||
await sheet(page).getByLabel('Alle Einträge anzeigen').check();
|
||||
await sheet(page).locator('tr[data-manual="0"]').click();
|
||||
await sheet(page).getByRole('button', { name: 'Eintrag löschen' }).click();
|
||||
await expect.poll(async () => ((await sheetOf(page, id)) as RouterSheet).routes).toEqual([]);
|
||||
await expect(rows).toHaveCount(5);
|
||||
await tabs.first().click();
|
||||
await sheet(page).getByLabel('Automatisches Routing').check();
|
||||
await expect(tabs.last()).toBeDisabled();
|
||||
await sheet(page).getByLabel('Automatisches Routing').uncheck();
|
||||
await expect(tabs.last()).toBeEnabled();
|
||||
// RIP switched on while the table is open (here: taken back by undo) closes it.
|
||||
await tabs.last().click();
|
||||
await expect(sheet(page).locator('.route-table')).toBeVisible();
|
||||
await page.getByRole('button', { name: 'Rückgängig' }).click();
|
||||
await expect(tabs.last()).toBeDisabled();
|
||||
await expect(tabs.first()).toHaveAttribute('aria-selected', 'true');
|
||||
await expect(sheet(page).getByLabel('Automatisches Routing')).toBeChecked();
|
||||
});
|
||||
|
||||
test("shows a home router's LAN and WAN tabs, DHCP closing the WAN card's address", async ({ page }) => {
|
||||
await openEditor(page, server.base, 'new');
|
||||
await page.locator('[data-palette="gateway"]').click();
|
||||
const tabs = sheet(page).getByRole('tab');
|
||||
await expect(tabs).toHaveText(['Allgemein', 'LAN: 192.168.0.10', 'WAN: 42.0.0.10']);
|
||||
await expect(sheet(page).locator('.sheet-connected')).toHaveCount(0);
|
||||
await tabs.nth(2).click();
|
||||
await expect(sheet(page).locator('.sheet-connected')).toHaveText('Keine Verbindung');
|
||||
await sheet(page).getByLabel('DHCP zur Konfiguration verwenden').check();
|
||||
await expect(field(page, 'ipAddress')).toBeDisabled();
|
||||
await expect(field(page, 'subnetMask')).toBeDisabled();
|
||||
await tabs.nth(1).click();
|
||||
await expect(field(page, 'ipAddress')).toBeEnabled();
|
||||
await field(page, 'ipAddress').fill('192.168.7.1');
|
||||
await field(page, 'ipAddress').press('Enter');
|
||||
await expect(tabs.nth(1)).toHaveText('LAN: 192.168.7.1');
|
||||
});
|
||||
|
||||
test.describe('on a phone', () => {
|
||||
test.use({ viewport: { width: 390, height: 844 }, hasTouch: true });
|
||||
|
||||
|
||||
@@ -78,6 +78,8 @@ export function Editor({ doc }: { readonly doc: Y.Doc }) {
|
||||
const [menu, setMenu] = useState<{ readonly target: Selection; readonly at: Point } | null>(null);
|
||||
const [routerAt, setRouterAt] = useState<Point | null>(null);
|
||||
const [message, setMessage] = useState<string | null>(null);
|
||||
// The cable of the card whose tab is open in the sheet (Java's Kabel.setAktiv).
|
||||
const [highlightedLink, setHighlightedLink] = useState<string | null>(null);
|
||||
|
||||
// A message goes after a while.
|
||||
useEffect(() => {
|
||||
@@ -203,6 +205,7 @@ export function Editor({ doc }: { readonly doc: Y.Doc }) {
|
||||
onMoveEnd={() => manager?.stopCapturing()}
|
||||
onCableStart={setCableFrom}
|
||||
onCable={onCable}
|
||||
highlightedLink={highlightedLink}
|
||||
onMenu={(target, at) => {
|
||||
setSelection(target);
|
||||
setMenu({ target, at });
|
||||
@@ -247,7 +250,16 @@ export function Editor({ doc }: { readonly doc: Y.Doc }) {
|
||||
</div>
|
||||
{/* Java shows the settings of the device that is chosen; closing them lets go of it, as a click beside it does. */}
|
||||
{selected === undefined ? null : (
|
||||
<DeviceSheet doc={doc} id={selected.id} label={selected.label} origin={origin} onChanged={() => manager?.stopCapturing()} onMessage={setMessage} onClose={() => setSelection(null)} />
|
||||
<DeviceSheet
|
||||
doc={doc}
|
||||
id={selected.id}
|
||||
label={selected.label}
|
||||
origin={origin}
|
||||
onChanged={() => manager?.stopCapturing()}
|
||||
onMessage={setMessage}
|
||||
onHighlight={setHighlightedLink}
|
||||
onClose={() => setSelection(null)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -54,6 +54,8 @@ export interface CanvasProps {
|
||||
readonly onCableStart?: (from: { readonly id: string; readonly side: Side } | null) => void;
|
||||
readonly onCable?: (from: { readonly id: string; readonly side: Side }, to: { readonly id: string; readonly side: Side }) => void;
|
||||
readonly onMenu?: (target: Selection, at: Point) => void;
|
||||
/** A cable drawn active, as Java draws the cable of the card whose tab is open (Kabel.setAktiv). */
|
||||
readonly highlightedLink?: string | null;
|
||||
}
|
||||
|
||||
/** The label's width in Java's font, measured once per text. */
|
||||
@@ -74,8 +76,7 @@ function useTextWidth(): (text: string) => number {
|
||||
);
|
||||
}
|
||||
|
||||
const rgba = (color: DocumentationView['color']) =>
|
||||
`rgba(${Math.round(color.red * 255)}, ${Math.round(color.green * 255)}, ${Math.round(color.blue * 255)}, ${color.alpha})`;
|
||||
const rgba = (color: DocumentationView['color']) => `rgba(${Math.round(color.red * 255)}, ${Math.round(color.green * 255)}, ${Math.round(color.blue * 255)}, ${color.alpha})`;
|
||||
|
||||
function Documentation({ item }: { readonly item: DocumentationView }) {
|
||||
if (item.kind === 'rectangle') {
|
||||
@@ -140,7 +141,7 @@ type Gesture =
|
||||
| { readonly kind: 'link'; readonly id: string; readonly start: Point; moved: boolean; longPressed: boolean };
|
||||
|
||||
export const Canvas = forwardRef<CanvasHandle, CanvasProps>(function Canvas(
|
||||
{ view, tool = 'select', selection = null, cableFrom = null, onSelect, onMoveDevice, onMoveEnd, onCableStart, onCable, onMenu },
|
||||
{ view, tool = 'select', selection = null, cableFrom = null, onSelect, onMoveDevice, onMoveEnd, onCableStart, onCable, onMenu, highlightedLink = null },
|
||||
handle,
|
||||
) {
|
||||
const t = useTranslate();
|
||||
@@ -420,14 +421,16 @@ export const Canvas = forwardRef<CanvasHandle, CanvasProps>(function Canvas(
|
||||
if (source === undefined || target === undefined) return null;
|
||||
const d = cablePath(source.center, target.center);
|
||||
const selected = selection?.kind === 'link' && selection.id === link.id;
|
||||
const active = highlightedLink === link.id;
|
||||
return (
|
||||
<g key={link.id}>
|
||||
<path
|
||||
data-link={link.id}
|
||||
data-selected={selected ? 'true' : undefined}
|
||||
data-active={active ? 'true' : undefined}
|
||||
d={d}
|
||||
strokeWidth={selected ? 4 : 2}
|
||||
stroke={selected ? 'var(--accent)' : link.wireless ? 'var(--canvas-cable-wireless)' : 'var(--canvas-cable)'}
|
||||
stroke={selected ? 'var(--accent)' : active ? 'var(--canvas-cable-active)' : link.wireless ? 'var(--canvas-cable-wireless)' : 'var(--canvas-cable)'}
|
||||
strokeDasharray={link.wireless ? '10' : undefined}
|
||||
/>
|
||||
{/* A wide, invisible line to tap: 2 px is no target for a finger. */}
|
||||
|
||||
@@ -30,19 +30,28 @@
|
||||
"menu.delete": "Löschen",
|
||||
"menu.removeAllCables": "Alle Verbindungen entfernen",
|
||||
"menu.removeCable": "Verbindung entfernen",
|
||||
"route.destinationNetwork": "Ziel",
|
||||
"route.gateway": "Nächstes Gateway",
|
||||
"route.interfaceIPAddress": "Über Schnittstelle",
|
||||
"route.subnetMask": "Netzmaske",
|
||||
"router.cancel": "Abbrechen",
|
||||
"router.prompt": "Wählen Sie die Anzahl der Schnittstellen des Vermittlungsrechners aus!",
|
||||
"router.title": "Anzahl Schnittstellen",
|
||||
"sheet.cable": "Kabelgebunden (LAN)",
|
||||
"sheet.cablePlugged": "Das Kabel ist noch eingesteckt.",
|
||||
"sheet.cards": "Netzwerkkarten",
|
||||
"sheet.chooseNetwork": "Bitte auswählen",
|
||||
"sheet.close": "Schließen",
|
||||
"sheet.cloud": "als Wolke darstellen",
|
||||
"sheet.connectedTo": "Verbunden mit {device}",
|
||||
"sheet.connection": "Anschluss",
|
||||
"sheet.defaultGateway": "Gateway",
|
||||
"sheet.deleteEntry": "Eintrag löschen",
|
||||
"sheet.dhcpClient": "DHCP zur Konfiguration verwenden",
|
||||
"sheet.dnsServer": "Domain Name Server",
|
||||
"sheet.empty": "(leer)",
|
||||
"sheet.forwardingTable": "Weiterleitungstabelle",
|
||||
"sheet.general": "Allgemein",
|
||||
"sheet.invalidValue": "Dieser Wert ist ungültig.",
|
||||
"sheet.ipAddress": "IP-Adresse",
|
||||
"sheet.ipAsName": "IP-Adresse als Name verwenden",
|
||||
@@ -51,9 +60,15 @@
|
||||
"sheet.macAsName": "MAC-Adresse als Name verwenden",
|
||||
"sheet.name": "Name",
|
||||
"sheet.network": "Drahtloses Netzwerk (SSID)",
|
||||
"sheet.newEntry": "Neuer Eintrag",
|
||||
"sheet.newEntryHint": "Die Zeile wird übernommen, sobald alle vier Felder gültig sind.",
|
||||
"sheet.nic": "Netzwerkkarte {number}",
|
||||
"sheet.notConnected": "Keine Verbindung",
|
||||
"sheet.notTaken": "Nicht übernommen – es gilt weiterhin {value}.",
|
||||
"sheet.port": "Port",
|
||||
"sheet.retention": "Gültigkeitsdauer SAT-Einträge (in Sek.)",
|
||||
"sheet.rip": "Automatisches Routing",
|
||||
"sheet.showAllEntries": "Alle Einträge anzeigen",
|
||||
"sheet.subnetMask": "Netzmaske",
|
||||
"sheet.switchSsid": "Name WLAN (SSID)",
|
||||
"sheet.title": "Einstellungen",
|
||||
|
||||
@@ -30,19 +30,28 @@
|
||||
"menu.delete": "Delete",
|
||||
"menu.removeAllCables": "Remove all cables",
|
||||
"menu.removeCable": "Remove cable",
|
||||
"route.destinationNetwork": "Destination",
|
||||
"route.gateway": "Next gateway",
|
||||
"route.interfaceIPAddress": "NIC",
|
||||
"route.subnetMask": "Netmask",
|
||||
"router.cancel": "Cancel",
|
||||
"router.prompt": "Choose number of NIC of the router!",
|
||||
"router.title": "Number of NIC",
|
||||
"sheet.cable": "Cable (LAN)",
|
||||
"sheet.cablePlugged": "The cable is still plugged in.",
|
||||
"sheet.cards": "Network cards",
|
||||
"sheet.chooseNetwork": "Please select",
|
||||
"sheet.close": "Close",
|
||||
"sheet.cloud": "Display as Cloud",
|
||||
"sheet.connectedTo": "Connected to {device}",
|
||||
"sheet.connection": "Connection",
|
||||
"sheet.defaultGateway": "Gateway",
|
||||
"sheet.deleteEntry": "Delete entry",
|
||||
"sheet.dhcpClient": "Use DHCP for configuration",
|
||||
"sheet.dnsServer": "Domain Name Server",
|
||||
"sheet.empty": "(empty)",
|
||||
"sheet.forwardingTable": "Forwarding table",
|
||||
"sheet.general": "General",
|
||||
"sheet.invalidValue": "This value is invalid.",
|
||||
"sheet.ipAddress": "IP Address",
|
||||
"sheet.ipAsName": "Use IP address as name",
|
||||
@@ -51,9 +60,15 @@
|
||||
"sheet.macAsName": "Use MAC address as name",
|
||||
"sheet.name": "Name",
|
||||
"sheet.network": "Wireless Network (SSID)",
|
||||
"sheet.newEntry": "New entry",
|
||||
"sheet.newEntryHint": "The row is taken once all four fields are valid.",
|
||||
"sheet.nic": "NIC {number}",
|
||||
"sheet.notConnected": "No Connection",
|
||||
"sheet.notTaken": "Not taken – still {value}.",
|
||||
"sheet.port": "Port",
|
||||
"sheet.retention": "Retention Time SAT Entries (Seconds)",
|
||||
"sheet.rip": "Automatic Routing",
|
||||
"sheet.showAllEntries": "Show all entries",
|
||||
"sheet.subnetMask": "Netmask",
|
||||
"sheet.switchSsid": "Wi-Fi Name (SSID)",
|
||||
"sheet.title": "Settings",
|
||||
|
||||
@@ -1,16 +1,14 @@
|
||||
// The device sheet (plan §7.1): the settings of the selected device, the
|
||||
// fields of FILIUS' settings panel of its kind (JHostKonfiguration,
|
||||
// JSwitchKonfiguration, JModemKonfiguration) in the panels' order. Java
|
||||
// shows the panel when a device is chosen; so does the sheet, beside the
|
||||
// surface or below it on a phone. Each change is one step of the person's
|
||||
// undo. A router's and a home router's sheet name the device only, until
|
||||
// their own panels follow (F4.5b).
|
||||
// JSwitchKonfiguration, JModemKonfiguration; a router's and a home router's
|
||||
// in RouterSheets.tsx) in the panels' order. Java shows the panel when a
|
||||
// device is chosen; so does the sheet, beside the surface or below it on a
|
||||
// phone. Each change is one step of the person's undo.
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import type * as Y from 'yjs';
|
||||
import {
|
||||
deviceSheet,
|
||||
renameDevice,
|
||||
setDhcpClient,
|
||||
setHostAddress,
|
||||
setHostLabels,
|
||||
@@ -30,6 +28,8 @@ import {
|
||||
import { iconOf } from '../canvas/layout.js';
|
||||
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 { SheetCheck, SheetField } from './SheetField.js';
|
||||
import './sheet.css';
|
||||
|
||||
@@ -55,17 +55,11 @@ export interface DeviceSheetProps {
|
||||
readonly onChanged: () => void;
|
||||
readonly onMessage: (message: string) => void;
|
||||
readonly onClose: () => void;
|
||||
/** The cable to draw active (a router's card whose tab is open), or none. */
|
||||
readonly onHighlight: (link: string | null) => void;
|
||||
}
|
||||
|
||||
interface Change {
|
||||
readonly doc: Y.Doc;
|
||||
readonly id: string;
|
||||
readonly origin: object;
|
||||
/** Runs a change and ends the undo step. */
|
||||
readonly run: <T>(change: () => T) => T;
|
||||
}
|
||||
|
||||
export function DeviceSheet({ doc, id, label, origin, onChanged, onMessage, onClose }: DeviceSheetProps) {
|
||||
export function DeviceSheet({ doc, id, label, origin, onChanged, onMessage, onClose, onHighlight }: DeviceSheetProps) {
|
||||
const t = useTranslate();
|
||||
const sheet = useDeviceSheet(doc, id);
|
||||
if (sheet === undefined) return null;
|
||||
@@ -97,6 +91,10 @@ export function DeviceSheet({ doc, id, label, origin, onChanged, onMessage, onCl
|
||||
<SwitchFields sheet={sheet} change={change} />
|
||||
) : sheet.kind === 'remoteLink' ? (
|
||||
<ModemFields sheet={sheet} change={change} />
|
||||
) : sheet.kind === 'router' ? (
|
||||
<RouterFields sheet={sheet} change={change} onHighlight={onHighlight} />
|
||||
) : sheet.kind === 'gateway' ? (
|
||||
<HomeRouterFields sheet={sheet} change={change} onHighlight={onHighlight} />
|
||||
) : (
|
||||
<NameField name={sheet.name} change={change} />
|
||||
)}
|
||||
@@ -105,20 +103,6 @@ export function DeviceSheet({ doc, id, label, origin, onChanged, onMessage, onCl
|
||||
);
|
||||
}
|
||||
|
||||
function NameField({ name, change, shown, disabled = false }: { readonly name: string; readonly change: Change; readonly shown?: string; readonly disabled?: boolean }) {
|
||||
const t = useTranslate();
|
||||
return (
|
||||
<SheetField
|
||||
name="name"
|
||||
label={t('sheet.name')}
|
||||
value={shown ?? name}
|
||||
rule="name"
|
||||
disabled={disabled}
|
||||
onCommit={(text) => change.run(() => (renameDevice(change.doc, change.id, text, change.origin), { value: text }))}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function HostFields({ sheet, label, change, onMessage }: { readonly sheet: HostSheet; readonly label: string; readonly change: Change; readonly onMessage: (message: string) => void }) {
|
||||
const t = useTranslate();
|
||||
const address =
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
// What every part of the device sheet shares: the change of one device by
|
||||
// one person, and the device's name.
|
||||
|
||||
import type * as Y from 'yjs';
|
||||
import { renameDevice } from '@mdg-filius/doc';
|
||||
import { useTranslate } from '../i18n/i18n.js';
|
||||
import { SheetField } from './SheetField.js';
|
||||
|
||||
export interface Change {
|
||||
readonly doc: Y.Doc;
|
||||
readonly id: string;
|
||||
readonly origin: object;
|
||||
/** Runs a change and ends the undo step. */
|
||||
readonly run: <T>(change: () => T) => T;
|
||||
}
|
||||
|
||||
export function NameField({ name, change, shown, disabled = false }: { readonly name: string; readonly change: Change; readonly shown?: string; readonly disabled?: boolean }) {
|
||||
const t = useTranslate();
|
||||
return (
|
||||
<SheetField
|
||||
name="name"
|
||||
label={t('sheet.name')}
|
||||
value={shown ?? name}
|
||||
rule="name"
|
||||
disabled={disabled}
|
||||
onCommit={(text) => change.run(() => (renameDevice(change.doc, change.id, text, change.origin), { value: text }))}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,266 @@
|
||||
// The sheets of a router and a home router, after Java's panels
|
||||
// (JVermittlungsrechnerKonfiguration, JGatewayConfiguration): tabs for the
|
||||
// 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).
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import {
|
||||
addRoute,
|
||||
checkField,
|
||||
ROUTE_COLUMNS,
|
||||
removeRoute,
|
||||
setDhcpClient,
|
||||
setInterfaceAddress,
|
||||
setRip,
|
||||
setRouteCell,
|
||||
setRouterGateway,
|
||||
type CheckedField,
|
||||
type HomeRouterSheet,
|
||||
type InterfaceSheet,
|
||||
type RouteColumn,
|
||||
type RouterSheet,
|
||||
} from '@mdg-filius/doc';
|
||||
import { useTranslate } from '../i18n/i18n.js';
|
||||
import { NameField, type Change } from './NameField.js';
|
||||
import { SheetCheck, SheetField } from './SheetField.js';
|
||||
|
||||
interface Tab {
|
||||
readonly key: string;
|
||||
readonly label: string;
|
||||
readonly disabled?: boolean;
|
||||
}
|
||||
|
||||
function SheetTabs({ tabs, selected, onSelect }: { readonly tabs: readonly Tab[]; readonly selected: string; readonly onSelect: (key: string) => void }) {
|
||||
return (
|
||||
<div role="tablist" className="sheet-tabs">
|
||||
{tabs.map((tab) => (
|
||||
<button key={tab.key} type="button" role="tab" data-tab={tab.key} aria-selected={tab.key === selected} disabled={tab.disabled === true} onClick={() => onSelect(tab.key)}>
|
||||
{tab.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/** Reports the cable of the card whose tab is open, and none once the sheet goes. */
|
||||
function useHighlight(link: string | null, onHighlight: (link: string | null) => void): void {
|
||||
useEffect(() => {
|
||||
onHighlight(link);
|
||||
}, [link, onHighlight]);
|
||||
useEffect(() => () => onHighlight(null), [onHighlight]);
|
||||
}
|
||||
|
||||
/** A card: what its cable joins, its address and mask, its MAC address. */
|
||||
function InterfaceFields({ card, change, closed = false }: { readonly card: InterfaceSheet; readonly change: Change; readonly closed?: boolean }) {
|
||||
const t = useTranslate();
|
||||
const set = (field: 'ipAddress' | 'subnetMask') => (text: string) => change.run(() => setInterfaceAddress(change.doc, change.id, card.portId, field, text, change.origin));
|
||||
return (
|
||||
<>
|
||||
<p className="sheet-connected" data-connected={card.connected === null ? 'false' : 'true'}>
|
||||
{card.connected === null ? t('sheet.notConnected') : t('sheet.connectedTo', { device: card.connected.device })}
|
||||
</p>
|
||||
<SheetField name="ipAddress" label={t('sheet.ipAddress')} value={card.ipAddress} rule="ipAddress" inputMode="decimal" disabled={closed} onCommit={set('ipAddress')} />
|
||||
<SheetField name="subnetMask" label={t('sheet.subnetMask')} value={card.subnetMask} rule="subnetMask" inputMode="decimal" disabled={closed} onCommit={set('subnetMask')} />
|
||||
<SheetField name="macAddress" label={t('sheet.macAddress')} value={card.macAddress} rule="name" readOnly onCommit={() => ({ invalid: true })} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function GatewayField({ value, change }: { readonly value: string; readonly change: Change }) {
|
||||
const t = useTranslate();
|
||||
return (
|
||||
<SheetField
|
||||
name="gateway"
|
||||
label={t('sheet.defaultGateway')}
|
||||
value={value}
|
||||
rule="ipAddressOrEmpty"
|
||||
inputMode="decimal"
|
||||
onCommit={(text) => change.run(() => setRouterGateway(change.doc, change.id, text, change.origin))}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function RouterFields({ sheet, change, onHighlight }: { readonly sheet: RouterSheet; readonly change: Change; readonly onHighlight: (link: string | null) => 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;
|
||||
const card = shown.startsWith('card-') ? sheet.interfaces[Number(shown.slice(5))] : undefined;
|
||||
useHighlight(card?.connected?.link ?? null, onHighlight);
|
||||
const tabs: Tab[] = [
|
||||
{ key: 'general', label: t('sheet.general') },
|
||||
// Java names a card's tab by its address (updateAttribute), "Netzwerkkarte n" without one.
|
||||
...sheet.interfaces.map((each, index) => ({ key: `card-${index}`, label: each.ipAddress === '' ? t('sheet.nic', { number: index + 1 }) : each.ipAddress })),
|
||||
{ key: 'table', label: t('sheet.forwardingTable'), disabled: sheet.rip },
|
||||
];
|
||||
return (
|
||||
<>
|
||||
<SheetTabs tabs={tabs} selected={shown} onSelect={setTab} />
|
||||
{/* A panel per tab: a text typed on one card is not offered to the next. */}
|
||||
<div role="tabpanel" key={shown}>
|
||||
{shown === 'general' ? (
|
||||
<>
|
||||
<NameField name={sheet.name} change={change} />
|
||||
<GatewayField value={sheet.gateway} change={change} />
|
||||
<SheetCheck name="rip" label={t('sheet.rip')} checked={sheet.rip} onChange={(on) => change.run(() => setRip(change.doc, change.id, on, change.origin))} />
|
||||
<SheetCheck name="ipForwarding" label={t('sheet.ipForwarding')} checked={sheet.ipForwarding} disabled onChange={() => undefined} />
|
||||
</>
|
||||
) : card !== undefined ? (
|
||||
<InterfaceFields card={card} change={change} />
|
||||
) : (
|
||||
<RouteTable sheet={sheet} change={change} />
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export function HomeRouterFields({ sheet, change, onHighlight }: { readonly sheet: HomeRouterSheet; readonly change: Change; readonly onHighlight: (link: string | null) => void }) {
|
||||
const t = useTranslate();
|
||||
const [tab, setTab] = useState('general');
|
||||
const card = tab === 'lan' ? sheet.lan : tab === 'wan' ? sheet.wan : undefined;
|
||||
useHighlight(card?.connected?.link ?? null, onHighlight);
|
||||
// Java's order: general, LAN, WAN; a card's tab says which and its address.
|
||||
const tabs: Tab[] = [
|
||||
{ key: 'general', label: t('sheet.general') },
|
||||
{ key: 'lan', label: `LAN: ${sheet.lan.ipAddress}` },
|
||||
{ key: 'wan', label: `WAN: ${sheet.wan.ipAddress}` },
|
||||
];
|
||||
return (
|
||||
<>
|
||||
<SheetTabs tabs={tabs} selected={tab} onSelect={setTab} />
|
||||
<div role="tabpanel" key={tab}>
|
||||
{tab === 'general' ? (
|
||||
<>
|
||||
<NameField name={sheet.name} change={change} />
|
||||
<GatewayField value={sheet.gateway} change={change} />
|
||||
<SheetCheck name="ipForwarding" label={t('sheet.ipForwarding')} checked={sheet.ipForwarding} disabled onChange={() => undefined} />
|
||||
</>
|
||||
) : tab === 'lan' ? (
|
||||
<InterfaceFields card={sheet.lan} change={change} />
|
||||
) : (
|
||||
<>
|
||||
{/* DHCP configures the WAN card; its address and mask are closed meanwhile, as in Java. */}
|
||||
<InterfaceFields card={sheet.wan} change={change} closed={sheet.dhcpClient} />
|
||||
<SheetCheck name="dhcpClient" label={t('sheet.dhcpClient')} checked={sheet.dhcpClient} onChange={(on) => change.run(() => setDhcpClient(change.doc, change.id, on, change.origin))} />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const routeRule = (column: RouteColumn) => (column === 'subnetMask' ? 'subnetMask' : 'ipAddress');
|
||||
const EMPTY_ROW: Readonly<Record<RouteColumn, string>> = { destinationNetwork: '', subnetMask: '', gateway: '', interfaceIPAddress: '' };
|
||||
|
||||
/**
|
||||
* The forwarding table: all rows or the manual ones only (Java's "show all
|
||||
* entries", on at first), a manual row chosen to edit its cells or remove
|
||||
* it, one new row at a time, taken once all four cells pass (as Java keeps
|
||||
* its new row until Weiterleitungstabelle.validate takes it).
|
||||
*/
|
||||
function RouteTable({ sheet, change }: { readonly sheet: RouterSheet; readonly change: Change }) {
|
||||
const t = useTranslate();
|
||||
const [showAll, setShowAll] = useState(true);
|
||||
const [chosen, setChosen] = useState<number | 'new' | null>(null);
|
||||
const [draft, setDraft] = useState<Record<RouteColumn, string>>(EMPTY_ROW);
|
||||
let manual = -1;
|
||||
const rows = sheet.table.map((row) => ({ row, manual: row.isEditable ? ++manual : null })).filter((each) => showAll || each.manual !== null);
|
||||
const editing = typeof chosen === 'number' ? sheet.routes[chosen] : undefined;
|
||||
|
||||
const draftCell =
|
||||
(column: RouteColumn) =>
|
||||
(text: string): CheckedField => {
|
||||
const checked = checkField(routeRule(column), text);
|
||||
if ('invalid' in checked) return checked;
|
||||
const next = { ...draft, [column]: checked.value };
|
||||
setDraft(next);
|
||||
if (ROUTE_COLUMNS.every((each) => 'value' in checkField(routeRule(each), next[each]))) {
|
||||
const added = change.run(() => addRoute(change.doc, change.id, next, change.origin));
|
||||
if ('route' in added) {
|
||||
setDraft(EMPTY_ROW);
|
||||
setChosen(sheet.routes.length);
|
||||
}
|
||||
}
|
||||
return checked;
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<SheetCheck name="showAll" label={t('sheet.showAllEntries')} checked={showAll} onChange={setShowAll} />
|
||||
<div className="route-table-frame">
|
||||
<table className="route-table">
|
||||
<thead>
|
||||
<tr>
|
||||
{ROUTE_COLUMNS.map((column) => (
|
||||
<th key={column} scope="col">
|
||||
{t(`route.${column}`)}
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{rows.map(({ row, manual: index }, place) => (
|
||||
<tr key={place} data-manual={index === null ? undefined : String(index)} aria-selected={index !== null && chosen === index} onClick={index === null ? undefined : () => setChosen(index)}>
|
||||
{[row.destinationNetwork, row.subnetMask, row.nextHop, row.interfaceIPAddress].map((cell, column) => (
|
||||
// A manual cell the rule refuses (from a file) is marked, not mended.
|
||||
<td key={column} data-invalid={index !== null && 'invalid' in checkField(routeRule(ROUTE_COLUMNS[column]!), cell) ? 'true' : undefined}>
|
||||
{cell}
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
{chosen === 'new' ? (
|
||||
<tr aria-selected data-new>
|
||||
{ROUTE_COLUMNS.map((column) => (
|
||||
<td key={column}>{draft[column]}</td>
|
||||
))}
|
||||
</tr>
|
||||
) : null}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div className="sheet-buttons">
|
||||
<button type="button" onClick={() => setChosen('new')}>
|
||||
{t('sheet.newEntry')}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
disabled={chosen === null}
|
||||
onClick={() => {
|
||||
if (chosen === 'new') setDraft(EMPTY_ROW);
|
||||
else if (chosen !== null) change.run(() => removeRoute(change.doc, change.id, chosen, change.origin));
|
||||
setChosen(null);
|
||||
}}
|
||||
>
|
||||
{t('sheet.deleteEntry')}
|
||||
</button>
|
||||
</div>
|
||||
{chosen === 'new' ? (
|
||||
<fieldset className="sheet-group" key="new">
|
||||
<p className="sheet-note">{t('sheet.newEntryHint')}</p>
|
||||
{ROUTE_COLUMNS.map((column) => (
|
||||
<SheetField key={column} name={`route-${column}`} label={t(`route.${column}`)} value={draft[column]} rule={routeRule(column)} inputMode="decimal" unfilled onCommit={draftCell(column)} />
|
||||
))}
|
||||
</fieldset>
|
||||
) : editing !== undefined ? (
|
||||
<fieldset className="sheet-group" key={`row-${String(chosen)}`}>
|
||||
{ROUTE_COLUMNS.map((column) => (
|
||||
<SheetField
|
||||
key={column}
|
||||
name={`route-${column}`}
|
||||
label={t(`route.${column}`)}
|
||||
value={editing[column]}
|
||||
rule={routeRule(column)}
|
||||
inputMode="decimal"
|
||||
onCommit={(text) => change.run(() => setRouteCell(change.doc, change.id, chosen as number, column, text, change.origin))}
|
||||
/>
|
||||
))}
|
||||
</fieldset>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -22,16 +22,18 @@ export interface SheetFieldProps {
|
||||
/** A name for the browser tests and the label's input mode. */
|
||||
readonly name: string;
|
||||
readonly inputMode?: 'text' | 'decimal' | 'numeric';
|
||||
/** A field still to be filled (a new row's cell): empty and untouched, it is not marked. */
|
||||
readonly unfilled?: boolean;
|
||||
}
|
||||
|
||||
export function SheetField({ label, value, rule, onCommit, disabled = false, readOnly = false, name, inputMode = 'text' }: SheetFieldProps) {
|
||||
export function SheetField({ label, value, rule, onCommit, disabled = false, readOnly = false, name, inputMode = 'text', unfilled = false }: SheetFieldProps) {
|
||||
const t = useTranslate();
|
||||
const id = useId();
|
||||
// What is typed and not yet taken; null shows the device's value.
|
||||
const [draft, setDraft] = useState<string | null>(null);
|
||||
const [refused, setRefused] = useState(false);
|
||||
const shown = draft ?? value;
|
||||
const marked = !readOnly && 'invalid' in checkField(rule, shown);
|
||||
const marked = !readOnly && !(unfilled && draft === null && shown === '') && 'invalid' in checkField(rule, shown);
|
||||
|
||||
const commit = () => {
|
||||
if (draft === null || readOnly) return;
|
||||
|
||||
@@ -148,3 +148,101 @@
|
||||
padding: 0;
|
||||
border: none;
|
||||
}
|
||||
|
||||
/* Java's tabs (JTabbedPane), as chips that wrap in a narrow sheet. */
|
||||
.sheet-tabs {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
margin: 4px 0 8px;
|
||||
}
|
||||
|
||||
.sheet-tabs button,
|
||||
.sheet-buttons button {
|
||||
min-height: 44px;
|
||||
padding: 0 12px;
|
||||
border: 1px solid var(--border-default);
|
||||
border-radius: 8px;
|
||||
background: var(--surface-raised);
|
||||
color: var(--text-primary);
|
||||
font: inherit;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.sheet-tabs button[aria-selected='true'] {
|
||||
border-color: var(--accent);
|
||||
background: var(--accent-soft);
|
||||
color: var(--accent-on-soft);
|
||||
}
|
||||
|
||||
.sheet-tabs button:disabled,
|
||||
.sheet-buttons button:disabled {
|
||||
color: var(--text-muted);
|
||||
background: var(--surface-sunken);
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.sheet-buttons {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
margin: 8px 0;
|
||||
}
|
||||
|
||||
.sheet-connected,
|
||||
.sheet-note {
|
||||
margin: 8px 0;
|
||||
font-size: 14px;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.route-table-frame {
|
||||
overflow-x: auto;
|
||||
border: 1px solid var(--border-default);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.route-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 12px;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.route-table th,
|
||||
.route-table td {
|
||||
padding: 6px;
|
||||
text-align: left;
|
||||
white-space: nowrap;
|
||||
border-bottom: 1px solid var(--border-subtle);
|
||||
}
|
||||
|
||||
.route-table th {
|
||||
font-weight: 600;
|
||||
color: var(--text-secondary);
|
||||
background: var(--surface-sunken);
|
||||
}
|
||||
|
||||
/* Java greys the rows it makes and lets the manual ones be edited. */
|
||||
.route-table tbody tr:not([data-manual]):not([data-new]) {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.route-table tbody tr[data-manual] {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.route-table tbody tr[aria-selected='true'] {
|
||||
background: var(--surface-selected);
|
||||
}
|
||||
|
||||
.route-table td[data-invalid='true'] {
|
||||
color: var(--danger);
|
||||
}
|
||||
|
||||
/* A manual row is a finger's target. */
|
||||
.route-table tbody tr[data-manual] td {
|
||||
padding-top: 13px;
|
||||
padding-bottom: 13px;
|
||||
}
|
||||
|
||||
@@ -30,6 +30,8 @@
|
||||
--canvas-bg: #ffffff;
|
||||
--canvas-cable: rgb(64 64 64);
|
||||
--canvas-cable-wireless: rgb(192 192 192);
|
||||
/* 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;
|
||||
}
|
||||
|
||||
@@ -60,6 +62,7 @@
|
||||
--canvas-bg: #e9ecf1;
|
||||
--canvas-cable: rgb(64 64 64);
|
||||
--canvas-cable-wireless: rgb(150 150 150);
|
||||
--canvas-cable-active: rgb(0 200 50);
|
||||
--canvas-label: #000000;
|
||||
}
|
||||
|
||||
|
||||
@@ -56,6 +56,16 @@ const FROM_JAVA: Readonly<Record<string, string>> = {
|
||||
'sheet.switchSsid': 'jswitchkonfiguration_msg4',
|
||||
'sheet.retention': 'jswitchkonfiguration_msg5',
|
||||
'sheet.port': 'jmodemkonfiguration_msg4',
|
||||
'sheet.general': 'jvermittlungsrechnerkonfiguration_msg17',
|
||||
'sheet.notConnected': 'jvermittlungsrechnerkonfiguration_msg16',
|
||||
'sheet.rip': 'jvermittlungsrechnerkonfiguration_msg26',
|
||||
'sheet.forwardingTable': 'jvermittlungsrechnerkonfiguration_msg15',
|
||||
'sheet.showAllEntries': 'jvermittlungsrechnerkonfiguration_msg11',
|
||||
'sheet.newEntry': 'jvermittlungsrechnerkonfiguration_msg12',
|
||||
'route.destinationNetwork': 'jweiterleitungstabelle_msg3',
|
||||
'route.subnetMask': 'jweiterleitungstabelle_msg4',
|
||||
'route.gateway': 'jweiterleitungstabelle_msg5',
|
||||
'route.interfaceIPAddress': 'jweiterleitungstabelle_msg6',
|
||||
};
|
||||
|
||||
/** Java's words, mended: the prompt says "Vermittlungsrechnerns" (guicontainer_msg1). */
|
||||
@@ -63,6 +73,11 @@ const FROM_JAVA_MENDED: Readonly<Record<string, readonly [string, (java: string)
|
||||
'router.prompt': ['guicontainer_msg1', (java) => java.replace(' \\n ', ' ').replace('Vermittlungsrechnerns', 'Vermittlungsrechners')],
|
||||
// A label above its list, without the colon of Java's label beside it.
|
||||
'sheet.network': ['jhostkonfiguration_msg15', (java) => java.replace(/:$/, '')],
|
||||
// Java writes "Eintrag Löschen".
|
||||
'sheet.deleteEntry': ['jvermittlungsrechnerkonfiguration_msg13', (java) => java.replace('Löschen', 'löschen')],
|
||||
// Java's word, then what it joins or which card.
|
||||
'sheet.connectedTo': ['jvermittlungsrechnerkonfiguration_msg6', (java) => `${java} {device}`],
|
||||
'sheet.nic': ['jvermittlungsrechnerkonfiguration_msg10', (java) => `${java} {number}`],
|
||||
};
|
||||
|
||||
describe('the catalogs', () => {
|
||||
|
||||
Reference in New Issue
Block a user