feat: F4.7 the data exchange window, measured against Java's

One row per frame (its highest layer's), Java's detail tree, the table as
Java's text file, "Tabellen löschen" for every card (clearCapture in the
room), a tab per card with Java's title, Java's layer colours as tokens,
the chosen frame and autoscroll per tab. New oracle ExchangeWindowRun.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-04 09:12:38 +00:00
co-authored by Claude Opus 5.5
parent 6809a71300
commit 9252b942c4
26 changed files with 7692 additions and 33 deletions
+1 -17
View File
@@ -24,7 +24,7 @@ import {
import { modemPort } from '@mdg-filius/fls';
import { inOrder, ROOTS } from './projectDoc.js';
import { fromY } from './values.js';
import { nodeLabel } from './view.js';
import { cardAddress, nodeLabel } from './view.js';
/** A device's DHCP server as Java's dialog shows it (JDHCPKonfiguration). */
export interface DhcpServerSheet {
@@ -147,22 +147,6 @@ const setting = (node: Y.Map<unknown>, key: string) => settingsOf(node).get(key)
const text = (map: Y.Map<unknown> | undefined, key: string) => (map?.get(key) as string | undefined) ?? '';
/** The address of a device's card at a port, as Java's NetzwerkInterface holds it; null for a device without cards (InternetKnoten). */
function cardAddress(node: Y.Map<unknown>, portId: string): string | null {
switch (node.get('kind') as NodeKind) {
case 'pc':
case 'notebook':
return text(setting(node, 'device'), 'ipAddress');
case 'router':
case 'gateway': {
const port = (node.get('ports') as Y.Array<Y.Map<unknown>>).toArray().find((each) => each.get('id') === portId);
return text(port?.get('config') as Y.Map<unknown> | undefined, 'ipAddress');
}
default:
return null;
}
}
/** A router's or home router's cards, each with what its cable joins. */
function interfacesOf(doc: Y.Doc, id: string, node: Y.Map<unknown>): InterfaceSheet[] {
const all = doc.getMap<Y.Map<unknown>>(ROOTS.nodes);
+45
View File
@@ -69,6 +69,51 @@ export function nodeLabel(node: Y.Map<unknown>): string {
return `${ip} (${mac})`;
}
/** The address of a device's card at a port, as Java's NetzwerkInterface holds it; null for a device without cards (InternetKnoten). */
export function cardAddress(node: Y.Map<unknown>, portId: string): string | null {
const text = (map: Y.Map<unknown> | undefined) => (map?.get('ipAddress') as string | undefined) ?? '';
switch (node.get('kind') as NodeKind) {
case 'pc':
case 'notebook':
return text(settingsOf(node)?.get('device'));
case 'router':
case 'gateway': {
const port = (node.get('ports') as Y.Array<Y.Map<unknown>>).toArray().find((each) => each.get('id') === portId);
return text(port?.get('config') as Y.Map<unknown> | undefined);
}
default:
return null;
}
}
/** A card whose frames the data exchange window shows: its port, address and the title of its tab. */
export interface CaptureCard {
readonly portId: string;
readonly ipAddress: string;
readonly title: string;
}
/**
* A device's cards in the order of its ports, as Java's menu offers their
* data exchange (GUIEvents.kontextMenueAktionsmodus); empty for a device
* without cards. A tab's title is the card's address where the label is the
* address (Host.isUseIPAsName), else the label, " - " and the address
* (AggregatedExchangePanel.updateTabTitle).
*/
export function captureCards(doc: Y.Doc, id: string): CaptureCard[] {
const node = doc.getMap<Y.Map<unknown>>(ROOTS.nodes).get(id);
if (node === undefined) return [];
const policy = node.get('labelPolicy') as HostLabelPolicy;
const kind = node.get('kind') as NodeKind;
const addressAsName = (kind === 'pc' || kind === 'notebook') && (policy === 'ipAddress' || policy === 'ipAndMAC');
return (node.get('ports') as Y.Array<Y.Map<unknown>>).toArray().flatMap((port) => {
const portId = port.get('id') as string;
const ipAddress = cardAddress(node, portId);
if (ipAddress === null) return [];
return [{ portId, ipAddress, title: addressAsName ? ipAddress : `${nodeLabel(node)} - ${ipAddress}` }];
});
}
/** The drawing of the document, in the order of its devices and cables. */
export function topologyView(doc: Y.Doc): TopologyView {
const kinds = new Map<string, NodeKind>();
+37 -2
View File
@@ -10,7 +10,7 @@ import { describe, expect, it } from 'vitest';
import * as Y from 'yjs';
import { stableGeneratedMacAddress } from '@mdg-filius/core';
import { exportProject, openProjectArchive, type ImportedProject } from '@mdg-filius/fls';
import { addDevice, desktopView, readProject, setHostLabels, topologyView, writeProject } from '../src/index.js';
import { addDevice, captureCards, desktopView, readProject, renameDevice, setHostAddress, setHostLabels, setInterfaceAddress, topologyView, writeProject } from '../src/index.js';
const corpus = join(dirname(fileURLToPath(import.meta.url)), '../../../corpus');
const files = ['java-examples', 'java-tests', 'ipad-fixtures'].flatMap((folder) =>
@@ -103,5 +103,40 @@ describe("a computer's desktop", () => {
setHostLabels(doc, 'pc', { ipAddress: true, macAddress: false }, 'me');
expect(desktopView(doc, 'pc')).toMatchObject({ label: '192.168.0.10', addressAsName: true });
});
});
it("offers each card's data exchange with the title of Java's tab: the address where the label is it, else the label and the address", () => {
// Java's titles (packages/core/test/exchangeWindow.java.json): "Anna - 192.168.0.20", a computer named so; "192.168.0.30", one labelled by its address.
const java = JSON.parse(readFileSync(join(dirname(fileURLToPath(import.meta.url)), '../../core/test/exchangeWindow.java.json'), 'utf8')) as { steps: { step: string; titles: string[] }[] };
const doc = new Y.Doc();
let counter = 0;
const newId = () => `port-${++counter}`;
addDevice(doc, { kind: 'pc', id: 'anna', name: 'Anna', x: 0, y: 0, newId }, 'me');
setHostAddress(doc, 'anna', 'ipAddress', '192.168.0.20', 'me');
addDevice(doc, { kind: 'notebook', id: 'server', name: 'Server', x: 0, y: 0, newId }, 'me');
setHostAddress(doc, 'server', 'ipAddress', '192.168.0.30', 'me');
setHostLabels(doc, 'server', { ipAddress: true, macAddress: false }, 'me');
expect([...captureCards(doc, 'anna'), ...captureCards(doc, 'server')].map((card) => card.title)).toEqual(java.steps.find((step) => step.step === 'the server\'s card beside it')!.titles);
expect(captureCards(doc, 'anna')).toEqual([{ portId: 'port-1', ipAddress: '192.168.0.20', title: 'Anna - 192.168.0.20' }]);
// Address and MAC address: the address; the MAC address alone: the label, which is it.
setHostLabels(doc, 'server', { ipAddress: true, macAddress: true }, 'me');
expect(captureCards(doc, 'server')[0]!.title).toBe('192.168.0.30');
setHostLabels(doc, 'server', { ipAddress: false, macAddress: true }, 'me');
expect(captureCards(doc, 'server')[0]!.title).toBe(`${stableGeneratedMacAddress('port-2')} - 192.168.0.30`);
// A router's cards in the order of its ports, each with its own address; a home router's WAN card first.
addDevice(doc, { kind: 'router', id: 'router', name: 'Vermittlungsrechner', x: 0, y: 0, interfaces: 2, newId }, 'me');
const [first, second] = topologyView(doc).nodes.find((node) => node.id === 'router')!.ports;
setInterfaceAddress(doc, 'router', second!.id, 'ipAddress', '10.0.0.1', 'me');
renameDevice(doc, 'router', 'R1', 'me');
expect(captureCards(doc, 'router').map((card) => [card.portId, card.title])).toEqual([
[first!.id, `R1 - ${captureCards(doc, 'router')[0]!.ipAddress}`],
[second!.id, 'R1 - 10.0.0.1'],
]);
addDevice(doc, { kind: 'gateway', id: 'gateway', name: 'Heimrouter', x: 0, y: 0, newId }, 'me');
expect(captureCards(doc, 'gateway').map((card) => card.portId)).toEqual(topologyView(doc).nodes.find((node) => node.id === 'gateway')!.ports.map((port) => port.id));
// No cards: a switch, a modem; no device.
addDevice(doc, { kind: 'networkSwitch', id: 'switch', name: 'Switch', x: 0, y: 0, newId }, 'me');
addDevice(doc, { kind: 'remoteLink', id: 'modem', name: 'Modem', x: 0, y: 0, newId }, 'me');
expect([captureCards(doc, 'switch'), captureCards(doc, 'modem'), captureCards(doc, 'nobody')]).toEqual([[], [], []]);
});
});