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>
188 lines
8.5 KiB
TypeScript
188 lines
8.5 KiB
TypeScript
// What the editor draws, read straight from the document: the devices with
|
|
// their labels, the cables, the documentation items. Lighter than
|
|
// readProject (no file systems, no settings beyond the few a drawing needs),
|
|
// so that it can run on every change while someone drags a device.
|
|
|
|
import * as Y from 'yjs';
|
|
import { compareIds, stableGeneratedMacAddress, type DocumentationItemKind, type HostLabelPolicy, type InstallableProgram, type NodeKind } from '@mdg-filius/core';
|
|
import { inOrder, ROOTS } from './projectDoc.js';
|
|
|
|
export interface NodeView {
|
|
readonly id: string;
|
|
readonly kind: NodeKind;
|
|
readonly name: string;
|
|
/** What Java writes under the icon (Host.holeAnzeigeName): the name, or the first card's address. */
|
|
readonly label: string;
|
|
/** Java's position of the device's button: its top left corner. */
|
|
readonly x: number;
|
|
readonly y: number;
|
|
/** A switch drawn as a cloud. */
|
|
readonly cloud: boolean;
|
|
readonly ports: readonly { readonly id: string; readonly label: string }[];
|
|
}
|
|
|
|
export interface LinkView {
|
|
readonly id: string;
|
|
readonly sourceNode: string;
|
|
readonly sourcePort: string;
|
|
readonly targetNode: string;
|
|
readonly targetPort: string;
|
|
/** A host's wireless card joining a switch: Java draws it dashed and grey. */
|
|
readonly wireless: boolean;
|
|
}
|
|
|
|
export interface DocumentationView {
|
|
readonly id: string;
|
|
readonly kind: DocumentationItemKind;
|
|
readonly x: number;
|
|
readonly y: number;
|
|
readonly width: number;
|
|
readonly height: number;
|
|
readonly text: string;
|
|
/** Components 0 to 1. */
|
|
readonly color: { readonly red: number; readonly green: number; readonly blue: number; readonly alpha: number };
|
|
readonly font: string;
|
|
readonly size: number;
|
|
readonly bold: boolean;
|
|
readonly order: number;
|
|
}
|
|
|
|
export interface TopologyView {
|
|
readonly nodes: readonly NodeView[];
|
|
readonly links: readonly LinkView[];
|
|
readonly docs: readonly DocumentationView[];
|
|
}
|
|
|
|
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; 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) ?? '';
|
|
const port = (node.get('ports') as Y.Array<Y.Map<unknown>>).get(0);
|
|
const mac = port === undefined ? '' : ((port.get('mac') as string | undefined) ?? stableGeneratedMacAddress(port.get('id') as string));
|
|
if (policy === 'ipAddress') return ip;
|
|
if (policy === 'macAddress') return mac;
|
|
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>();
|
|
const wirelessHosts = new Set<string>();
|
|
const nodes = inOrder(doc.getMap<Y.Map<unknown>>(ROOTS.nodes)).map(([id, node]): NodeView => {
|
|
const kind = node.get('kind') as NodeKind;
|
|
const name = node.get('name') as string;
|
|
kinds.set(id, kind);
|
|
if (settingsOf(node)?.get('wireless')?.get('isEnabled') === true && (kind === 'pc' || kind === 'notebook')) wirelessHosts.add(id);
|
|
return {
|
|
id,
|
|
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,
|
|
ports: (node.get('ports') as Y.Array<Y.Map<unknown>>).toArray().map((port) => ({ id: port.get('id') as string, label: port.get('label') as string })),
|
|
};
|
|
});
|
|
// exportProject's joinsWirelessly: a host with a wireless card and a switch.
|
|
const wireless = (first: string, second: string) => (wirelessHosts.has(first) && kinds.get(second) === 'networkSwitch') || (wirelessHosts.has(second) && kinds.get(first) === 'networkSwitch');
|
|
const links = inOrder(doc.getMap<Y.Map<unknown>>(ROOTS.links)).map(([id, link]): LinkView => {
|
|
const sourceNode = link.get('sourceNode') as string;
|
|
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,
|
|
}));
|
|
// 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));
|
|
return { nodes, links, docs };
|
|
}
|
|
|
|
/** What a computer's desktop needs of the document (Java's GUIDesktopWindow, GUIDesktopPanel). */
|
|
export interface DesktopView {
|
|
readonly kind: 'pc' | 'notebook';
|
|
/** Its label (Host.holeAnzeigeName). */
|
|
readonly label: string;
|
|
/** Whether the label is its address: Java's title then leaves the address out (isUseIPAsName). */
|
|
readonly addressAsName: boolean;
|
|
readonly programs: readonly InstallableProgram[];
|
|
}
|
|
|
|
/** A PC's or notebook's desktop; undefined for any other device. */
|
|
export function desktopView(doc: Y.Doc, id: string): DesktopView | undefined {
|
|
const node = doc.getMap<Y.Map<unknown>>(ROOTS.nodes).get(id);
|
|
const kind = node?.get('kind') as NodeKind | undefined;
|
|
if (node === undefined || (kind !== 'pc' && kind !== 'notebook')) return undefined;
|
|
const policy = node.get('labelPolicy') as HostLabelPolicy;
|
|
return {
|
|
kind,
|
|
label: nodeLabel(node),
|
|
addressAsName: policy === 'ipAddress' || policy === 'ipAndMAC',
|
|
programs: [...(node.get('programs') as Y.Map<unknown>).keys()] as InstallableProgram[],
|
|
};
|
|
}
|
|
|