feat(editor): F4.5a the device sheet for PCs, notebooks, switches and modems
Measured first: the new oracle SheetRun types 77 texts into FILIUS' own settings panels and records what was marked red, what the device took and what the field showed afterwards. Java's two judgements disagree (red text taken, black text refused, any int as a modem port, a refused text dropped without a word); here one rule decides both (packages/doc fields.ts), a refused text stays marked in the field, and only what Java's model accepts reaches the document. - doc: settings.ts (name, label policy, addresses, IP forwarding, DHCP stopping the own DHCP server, LAN/WLAN with the wireless connection, switch SSID dropping its wireless hosts, retention time, cloud, modem port as its pair), sheet.ts (what the sheet shows) - fls: modemPair/modemPort in one place - editor: DeviceSheet beside the surface or below it on a phone, opened with the selection; every change one undo step - not adopted from Java: the name overwritten by the MAC address, the access point search depending on unrelated devices Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,198 @@
|
||||
// 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.
|
||||
//
|
||||
// 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
|
||||
// wireless card and a switch's SSID move the wireless connections as Java
|
||||
// moves them: a host joins the first switch whose SSID it names, a switch
|
||||
// that changes its SSID loses its wireless hosts.
|
||||
//
|
||||
// Not adopted from Java: a host that shows its MAC address as its name takes
|
||||
// that address as its name as soon as another field is taken (SheetRun's
|
||||
// flow useMacAsNameThenOff); here the name stays what it was. And Java's
|
||||
// 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.
|
||||
|
||||
import * as Y from 'yjs';
|
||||
import { DEFAULT_REMOTE_LINK_LATENCY_MILLISECONDS, defaultSwitchConfiguration, forwardsIPv4ByDefault, type Id, 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';
|
||||
import { inOrder, ROOTS } from './projectDoc.js';
|
||||
import { toY } from './values.js';
|
||||
|
||||
const nodes = (doc: Y.Doc) => doc.getMap<Y.Map<unknown>>(ROOTS.nodes);
|
||||
const links = (doc: Y.Doc) => doc.getMap<Y.Map<unknown>>(ROOTS.links);
|
||||
const settingsOf = (node: Y.Map<unknown>) => node.get('settings') as Y.Map<unknown>;
|
||||
const kindOf = (node: Y.Map<unknown>) => node.get('kind') as NodeKind;
|
||||
|
||||
/** A setting of a device as a map of its fields, made with these fields when the device has none. */
|
||||
function settingMap(node: Y.Map<unknown>, key: string, fields: Readonly<Record<string, string | number | boolean>>): Y.Map<unknown> {
|
||||
const settings = settingsOf(node);
|
||||
let map = settings.get(key) as Y.Map<unknown> | undefined;
|
||||
if (map === undefined) {
|
||||
map = toY(fields) as Y.Map<unknown>;
|
||||
settings.set(key, map);
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
/** Changes a device in one transaction when the text passes the rule; says what it came to. */
|
||||
function setChecked(doc: Y.Doc, id: Id, checked: CheckedField, change: (node: Y.Map<unknown>, value: string) => void, origin: unknown): CheckedField {
|
||||
const node = nodes(doc).get(id);
|
||||
if (node === undefined || 'invalid' in checked) return checked;
|
||||
doc.transact(() => change(node, checked.value), origin);
|
||||
return checked;
|
||||
}
|
||||
|
||||
export function renameDevice(doc: Y.Doc, id: Id, name: string, origin: unknown): void {
|
||||
const node = nodes(doc).get(id);
|
||||
if (node !== undefined) doc.transact(() => node.set('name', name), origin);
|
||||
}
|
||||
|
||||
/** What a PC or notebook shows as its name (Host.useIPAsName, useMACAsName): its address, its MAC address, both or its name. */
|
||||
export function setHostLabels(doc: Y.Doc, id: Id, labels: { readonly ipAddress: boolean; readonly macAddress: boolean }, origin: unknown): void {
|
||||
const node = nodes(doc).get(id);
|
||||
if (node === undefined) return;
|
||||
const policy = labels.ipAddress && labels.macAddress ? 'ipAndMAC' : labels.ipAddress ? 'ipAddress' : labels.macAddress ? 'macAddress' : 'manual';
|
||||
doc.transact(() => node.set('labelPolicy', policy), origin);
|
||||
}
|
||||
|
||||
export type HostAddressField = 'ipAddress' | 'subnetMask' | 'defaultGateway' | 'dnsServer';
|
||||
|
||||
const ADDRESS_RULES = { ipAddress: 'ipAddress', subnetMask: 'subnetMask', defaultGateway: 'ipAddressOrEmpty', dnsServer: 'ipAddressOrEmpty' } as const;
|
||||
|
||||
/** A PC's or notebook's address, mask, gateway or DNS server (Betriebssystem.setzeIPAdresse …). */
|
||||
export function setHostAddress(doc: Y.Doc, id: Id, field: HostAddressField, text: string, origin: unknown): CheckedField {
|
||||
return setChecked(
|
||||
doc,
|
||||
id,
|
||||
checkField(ADDRESS_RULES[field], text),
|
||||
(node, value) => settingMap(node, 'device', { ipAddress: '192.168.0.10', subnetMask: '255.255.255.0', defaultGateway: '', dnsServer: '' }).set(field, value),
|
||||
origin,
|
||||
);
|
||||
}
|
||||
|
||||
/** IP forwarding, kept only where it differs from the kind's (as the project keeps it). */
|
||||
export function setIpForwarding(doc: Y.Doc, id: Id, on: boolean, origin: unknown): void {
|
||||
const node = nodes(doc).get(id);
|
||||
if (node === undefined) return;
|
||||
doc.transact(() => {
|
||||
if (on === forwardsIPv4ByDefault(kindOf(node))) settingsOf(node).delete('ipForwarding');
|
||||
else settingsOf(node).set('ipForwarding', on);
|
||||
}, origin);
|
||||
}
|
||||
|
||||
/** DHCP for the configuration; switched on, it stops the device's own DHCP server (JHostKonfiguration.aenderungenAnnehmen). */
|
||||
export function setDhcpClient(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).delete('dhcpClient');
|
||||
return;
|
||||
}
|
||||
settingsOf(node).set('dhcpClient', toY({ isEnabled: true }));
|
||||
(settingsOf(node).get('dhcpServer') as Y.Map<unknown> | undefined)?.set('isActive', false);
|
||||
}, origin);
|
||||
}
|
||||
|
||||
const wirelessOf = (node: Y.Map<unknown>) => (settingsOf(node).get('wireless') as Y.Map<unknown> | undefined)?.get('isEnabled') === true;
|
||||
|
||||
/** The cables of a device to a switch: a wireless host's wireless connections. */
|
||||
function linksToSwitches(doc: Y.Doc, id: Id): string[] {
|
||||
return [...links(doc).entries()]
|
||||
.filter(([, link]) => {
|
||||
const ends = [link.get('sourceNode') as Id, link.get('targetNode') as Id];
|
||||
const other = ends[0] === id ? ends[1]! : ends[1] === id ? ends[0]! : undefined;
|
||||
const node = other === undefined ? undefined : nodes(doc).get(other);
|
||||
return node !== undefined && kindOf(node) === 'networkSwitch';
|
||||
})
|
||||
.map(([linkId]) => linkId);
|
||||
}
|
||||
|
||||
/** Why a host's card cannot become wireless (Java disables the choice): its cable is plugged. */
|
||||
export type WirelessError = 'cablePlugged' | ConnectError;
|
||||
|
||||
/**
|
||||
* A PC's or notebook's card by cable or wireless, and the SSID of the network
|
||||
* it joins ('' for none). Java (JHostKonfiguration.updateWifiConnection):
|
||||
* when either changes, the host leaves its wireless network, then joins the
|
||||
* first switch with that SSID, when it is wireless and names one. A card
|
||||
* whose cable is plugged cannot become wireless. The SSID stays when the
|
||||
* card goes back to the cable, as Java keeps it.
|
||||
*/
|
||||
export function setHostWireless(doc: Y.Doc, id: Id, wireless: boolean, ssid: string, linkId: Id, origin: unknown): { readonly link?: Id } | { readonly error: WirelessError } {
|
||||
const node = nodes(doc).get(id);
|
||||
if (node === undefined) return { error: 'unknownDevice' };
|
||||
const before = settingsOf(node).get('wireless') as Y.Map<unknown> | undefined;
|
||||
const wasWireless = wirelessOf(node);
|
||||
const plugged = [...links(doc).values()].some((link) => link.get('sourceNode') === id || link.get('targetNode') === id);
|
||||
if (wireless && !wasWireless && plugged) return { error: 'cablePlugged' };
|
||||
if (wasWireless === wireless && ((before?.get('ssid') as string | undefined) ?? '') === ssid) return {};
|
||||
let result: { readonly link?: Id } | { readonly error: WirelessError } = {};
|
||||
doc.transact(() => {
|
||||
if (wasWireless) for (const each of linksToSwitches(doc, id)) links(doc).delete(each);
|
||||
if (!wireless && ssid === '') settingsOf(node).delete('wireless');
|
||||
else settingsOf(node).set('wireless', toY({ isEnabled: wireless, ssid }));
|
||||
if (!wireless || ssid === '') return;
|
||||
const accessPoint = inOrder(nodes(doc)).find(([, candidate]) => kindOf(candidate) === 'networkSwitch' && (settingsOf(candidate).get('switch') as Y.Map<unknown> | undefined)?.get('ssid') === ssid);
|
||||
if (accessPoint !== undefined) result = connect(doc, { node: id }, { node: accessPoint[0] }, linkId, origin);
|
||||
}, origin);
|
||||
return result;
|
||||
}
|
||||
|
||||
const SWITCH_DEFAULTS = (id: Id) => ({ ...defaultSwitchConfiguration(id) });
|
||||
|
||||
/** A switch's SSID; a new one drops its wireless hosts (JSwitchKonfiguration.aenderungenAnnehmen). */
|
||||
export function setSwitchSsid(doc: Y.Doc, id: Id, text: string, origin: unknown): CheckedField {
|
||||
return setChecked(
|
||||
doc,
|
||||
id,
|
||||
checkField('ssid', text),
|
||||
(node, value) => {
|
||||
const setting = settingMap(node, 'switch', SWITCH_DEFAULTS(id));
|
||||
if (setting.get('ssid') === value) return;
|
||||
setting.set('ssid', value);
|
||||
for (const linkId of [...links(doc).keys()]) {
|
||||
const link = links(doc).get(linkId)!;
|
||||
const ends = [link.get('sourceNode') as Id, link.get('targetNode') as Id];
|
||||
const host = ends[0] === id ? ends[1]! : ends[1] === id ? ends[0]! : undefined;
|
||||
const other = host === undefined ? undefined : nodes(doc).get(host);
|
||||
if (other !== undefined && (kindOf(other) === 'pc' || kindOf(other) === 'notebook') && wirelessOf(other)) links(doc).delete(linkId);
|
||||
}
|
||||
},
|
||||
origin,
|
||||
);
|
||||
}
|
||||
|
||||
/** How long a switch keeps a learned MAC address, in seconds as the panel asks for it. */
|
||||
export function setSwitchRetention(doc: Y.Doc, id: Id, text: string, origin: unknown): CheckedField {
|
||||
return setChecked(doc, id, checkField('seconds', text), (node, value) => settingMap(node, 'switch', SWITCH_DEFAULTS(id)).set('retentionTimeMilliseconds', Number(value) * 1000), origin);
|
||||
}
|
||||
|
||||
/** A switch drawn as a cloud (Switch.cloud), kept only when true. */
|
||||
export function setSwitchCloud(doc: Y.Doc, id: Id, cloud: boolean, origin: unknown): void {
|
||||
const node = nodes(doc).get(id);
|
||||
if (node === undefined) return;
|
||||
doc.transact(() => {
|
||||
const setting = settingMap(node, 'switch', SWITCH_DEFAULTS(id));
|
||||
if (cloud) setting.set('showsAsCloud', true);
|
||||
else setting.delete('showsAsCloud');
|
||||
}, origin);
|
||||
}
|
||||
|
||||
/** A modem's port: the pair it belongs to (two modems on one port are a pair, as the export writes them). */
|
||||
export function setModemPort(doc: Y.Doc, id: Id, text: string, origin: unknown): CheckedField {
|
||||
return setChecked(
|
||||
doc,
|
||||
id,
|
||||
checkField('port', text),
|
||||
(node, value) =>
|
||||
settingMap(node, 'remoteLink', { pairIdentifier: '', latencyMilliseconds: DEFAULT_REMOTE_LINK_LATENCY_MILLISECONDS, isEnabled: true }).set('pairIdentifier', modemPair(Number(value))),
|
||||
origin,
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user