@isothrum/editor 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -0
- package/NOTICE +8 -0
- package/PUBLISHING.md +93 -0
- package/README.md +106 -2
- package/desktop-editor/editor.css +11 -0
- package/desktop-editor/index.html +14 -0
- package/desktop-editor/main.tsx +26 -0
- package/dist/desktop/editor.css +1 -0
- package/dist/desktop/editor.js +181 -0
- package/dist/desktop/fonts/geist-cyrillic-ext-wght-normal-RO72NYA2.woff2 +0 -0
- package/dist/desktop/fonts/geist-cyrillic-wght-normal-CK2XY27K.woff2 +0 -0
- package/dist/desktop/fonts/geist-latin-ext-wght-normal-XEQ56JUF.woff2 +0 -0
- package/dist/desktop/fonts/geist-latin-wght-normal-T72VVCUW.woff2 +0 -0
- package/dist/desktop/fonts/geist-mono-cyrillic-ext-wght-normal-QXKVIKFD.woff2 +0 -0
- package/dist/desktop/fonts/geist-mono-cyrillic-wght-normal-GOIAQAGU.woff2 +0 -0
- package/dist/desktop/fonts/geist-mono-latin-ext-wght-normal-HU3ZAWN6.woff2 +0 -0
- package/dist/desktop/fonts/geist-mono-latin-wght-normal-CJNQKXUG.woff2 +0 -0
- package/dist/desktop/fonts/geist-mono-symbols2-wght-normal-WZJJVLQO.woff2 +0 -0
- package/dist/desktop/fonts/geist-mono-vietnamese-wght-normal-KW5NM6YV.woff2 +0 -0
- package/dist/desktop/fonts/geist-vietnamese-wght-normal-Y2R7JZKV.woff2 +0 -0
- package/dist/desktop/fonts/shantell-sans-cyrillic-ext-wght-normal-ILPQWRJJ.woff2 +0 -0
- package/dist/desktop/fonts/shantell-sans-cyrillic-wght-normal-RKC6NDPM.woff2 +0 -0
- package/dist/desktop/fonts/shantell-sans-latin-ext-wght-normal-SNKHG3LM.woff2 +0 -0
- package/dist/desktop/fonts/shantell-sans-latin-wght-normal-7CHHGC3N.woff2 +0 -0
- package/dist/desktop/fonts/shantell-sans-vietnamese-wght-normal-IPFAQSBA.woff2 +0 -0
- package/dist/desktop/index.html +14 -0
- package/dist/desktop/styles.css +2 -0
- package/dist/desktop/wasm/isothrum_wasm.d.ts +60 -0
- package/dist/desktop/wasm/isothrum_wasm.js +312 -0
- package/dist/desktop/wasm/isothrum_wasm_bg.wasm +0 -0
- package/dist/index.js +7059 -0
- package/dist/index.js.map +7 -0
- package/dist/model.js +745 -0
- package/dist/model.js.map +7 -0
- package/dist/types/components/tool/canvas/cable-edge.d.ts +4 -0
- package/dist/types/components/tool/canvas/contexts.d.ts +12 -0
- package/dist/types/components/tool/canvas/device-icon.d.ts +17 -0
- package/dist/types/components/tool/canvas/inline-text.d.ts +15 -0
- package/dist/types/components/tool/canvas/lan-node.d.ts +8 -0
- package/dist/types/components/tool/canvas/machine-addresses.d.ts +14 -0
- package/dist/types/components/tool/canvas/machine-node.d.ts +4 -0
- package/dist/types/components/tool/canvas/note-node.d.ts +7 -0
- package/dist/types/components/tool/canvas/port-led.d.ts +10 -0
- package/dist/types/components/tool/canvas/router-node.d.ts +8 -0
- package/dist/types/components/tool/canvas/sketch.d.ts +50 -0
- package/dist/types/components/tool/canvas/status-dot.d.ts +5 -0
- package/dist/types/components/tool/canvas/switch-node.d.ts +8 -0
- package/dist/types/components/tool/canvas/use-measured-ports.d.ts +7 -0
- package/dist/types/components/tool/context-menus.d.ts +5 -0
- package/dist/types/components/tool/device-block.d.ts +12 -0
- package/dist/types/components/tool/editor-menu.d.ts +28 -0
- package/dist/types/components/tool/export/copy-button.d.ts +9 -0
- package/dist/types/components/tool/export/export-panel.d.ts +17 -0
- package/dist/types/components/tool/export/target-list.d.ts +15 -0
- package/dist/types/components/tool/floating-window.d.ts +13 -0
- package/dist/types/components/tool/flow-canvas.d.ts +6 -0
- package/dist/types/components/tool/hooks/use-cabling.d.ts +23 -0
- package/dist/types/components/tool/hooks/use-captures.d.ts +9 -0
- package/dist/types/components/tool/hooks/use-clipboard.d.ts +12 -0
- package/dist/types/components/tool/hooks/use-context-menus.d.ts +25 -0
- package/dist/types/components/tool/hooks/use-design.d.ts +10 -0
- package/dist/types/components/tool/hooks/use-desktop.d.ts +50 -0
- package/dist/types/components/tool/hooks/use-devices.d.ts +17 -0
- package/dist/types/components/tool/hooks/use-downloads.d.ts +9 -0
- package/dist/types/components/tool/hooks/use-editor.d.ts +609 -0
- package/dist/types/components/tool/hooks/use-engine.d.ts +9 -0
- package/dist/types/components/tool/hooks/use-history.d.ts +9 -0
- package/dist/types/components/tool/hooks/use-lab-actions.d.ts +25 -0
- package/dist/types/components/tool/hooks/use-lab-run.d.ts +25 -0
- package/dist/types/components/tool/hooks/use-lab.d.ts +18 -0
- package/dist/types/components/tool/hooks/use-lan-carry.d.ts +8 -0
- package/dist/types/components/tool/hooks/use-machine-actions.d.ts +26 -0
- package/dist/types/components/tool/hooks/use-placement.d.ts +27 -0
- package/dist/types/components/tool/hooks/use-project.d.ts +25 -0
- package/dist/types/components/tool/hooks/use-projects.d.ts +34 -0
- package/dist/types/components/tool/hooks/use-shared-link.d.ts +23 -0
- package/dist/types/components/tool/hooks/use-toast.d.ts +2 -0
- package/dist/types/components/tool/hooks/use-tools.d.ts +17 -0
- package/dist/types/components/tool/lab-canvas.d.ts +18 -0
- package/dist/types/components/tool/overlays.d.ts +5 -0
- package/dist/types/components/tool/projects-list.d.ts +9 -0
- package/dist/types/components/tool/text.d.ts +418 -0
- package/dist/types/components/tool/toolbar.d.ts +5 -0
- package/dist/types/components/tool/top-bars.d.ts +10 -0
- package/dist/types/components/tool/windows/conditions-fields.d.ts +6 -0
- package/dist/types/components/tool/windows/editor-windows.d.ts +5 -0
- package/dist/types/components/tool/windows/fields.d.ts +4 -0
- package/dist/types/components/tool/windows/lab-status.d.ts +6 -0
- package/dist/types/components/tool/windows/lab-windows.d.ts +13 -0
- package/dist/types/components/tool/windows/link-controls.d.ts +21 -0
- package/dist/types/components/tool/windows/machine-controls.d.ts +16 -0
- package/dist/types/components/tool/windows/machine-inspector.d.ts +11 -0
- package/dist/types/components/tool/windows/runs-fields.d.ts +18 -0
- package/dist/types/components/tool/windows/selected-windows.d.ts +21 -0
- package/dist/types/components/ui/button.d.ts +8 -0
- package/dist/types/components/ui/dropdown-menu.d.ts +21 -0
- package/dist/types/index.d.ts +7 -0
- package/dist/types/lib/auth.d.ts +38 -0
- package/dist/types/lib/brand.d.ts +19 -0
- package/dist/types/lib/i18n.d.ts +19 -0
- package/dist/types/lib/tool/blocks.d.ts +35 -0
- package/dist/types/lib/tool/cables.d.ts +48 -0
- package/dist/types/lib/tool/clone.d.ts +12 -0
- package/dist/types/lib/tool/conditions.d.ts +31 -0
- package/dist/types/lib/tool/create.d.ts +22 -0
- package/dist/types/lib/tool/desktop.d.ts +63 -0
- package/dist/types/lib/tool/draft.d.ts +20 -0
- package/dist/types/lib/tool/engine.d.ts +33 -0
- package/dist/types/lib/tool/examples.d.ts +11 -0
- package/dist/types/lib/tool/export-targets.d.ts +21 -0
- package/dist/types/lib/tool/facing.d.ts +9 -0
- package/dist/types/lib/tool/geometry.d.ts +36 -0
- package/dist/types/lib/tool/gns3-cables.d.ts +17 -0
- package/dist/types/lib/tool/gns3-import.d.ts +15 -0
- package/dist/types/lib/tool/gns3-parts.d.ts +25 -0
- package/dist/types/lib/tool/issues.d.ts +12 -0
- package/dist/types/lib/tool/layout.d.ts +52 -0
- package/dist/types/lib/tool/live.d.ts +21 -0
- package/dist/types/lib/tool/logos.d.ts +7 -0
- package/dist/types/lib/tool/model.d.ts +173 -0
- package/dist/types/lib/tool/os.d.ts +20 -0
- package/dist/types/lib/tool/pins.d.ts +5 -0
- package/dist/types/lib/tool/placement.d.ts +21 -0
- package/dist/types/lib/tool/ports.d.ts +21 -0
- package/dist/types/lib/tool/presets.d.ts +21 -0
- package/dist/types/lib/tool/projects-api.d.ts +37 -0
- package/dist/types/lib/tool/router-ports.d.ts +22 -0
- package/dist/types/lib/tool/selection.d.ts +19 -0
- package/dist/types/lib/tool/storage.d.ts +25 -0
- package/dist/types/lib/tool/switch-edit.d.ts +17 -0
- package/dist/types/lib/tool/text/de.d.ts +2 -0
- package/dist/types/lib/tool/text/en.d.ts +429 -0
- package/dist/types/lib/tool/text/es.d.ts +2 -0
- package/dist/types/lib/tool/text/fr.d.ts +2 -0
- package/dist/types/lib/tool/text/index.d.ts +419 -0
- package/dist/types/lib/tool/topology.d.ts +2 -0
- package/dist/types/lib/tool/vlans.d.ts +24 -0
- package/dist/types/lib/utils.d.ts +2 -0
- package/dist/types/model.d.ts +13 -0
- package/package.json +88 -4
- package/scripts/build-desktop-editor.mjs +43 -0
- package/scripts/build-lib.mjs +38 -0
- package/src/components/tool/canvas/cable-edge.tsx +112 -0
- package/src/components/tool/canvas/contexts.ts +18 -0
- package/src/components/tool/canvas/device-icon.tsx +105 -0
- package/src/components/tool/canvas/inline-text.tsx +69 -0
- package/src/components/tool/canvas/lan-node.tsx +148 -0
- package/src/components/tool/canvas/machine-addresses.tsx +117 -0
- package/src/components/tool/canvas/machine-node.tsx +114 -0
- package/src/components/tool/canvas/note-node.tsx +109 -0
- package/src/components/tool/canvas/port-led.tsx +21 -0
- package/src/components/tool/canvas/router-node.tsx +114 -0
- package/src/components/tool/canvas/sketch.tsx +130 -0
- package/src/components/tool/canvas/status-dot.tsx +29 -0
- package/src/components/tool/canvas/switch-node.tsx +143 -0
- package/src/components/tool/canvas/use-measured-ports.ts +25 -0
- package/src/components/tool/context-menus.tsx +142 -0
- package/src/components/tool/device-block.tsx +107 -0
- package/src/components/tool/editor-menu.tsx +191 -0
- package/src/components/tool/export/copy-button.tsx +45 -0
- package/src/components/tool/export/export-panel.tsx +127 -0
- package/src/components/tool/export/target-list.tsx +87 -0
- package/src/components/tool/floating-window.tsx +69 -0
- package/src/components/tool/flow-canvas.tsx +138 -0
- package/src/components/tool/hooks/use-cabling.ts +66 -0
- package/src/components/tool/hooks/use-captures.ts +24 -0
- package/src/components/tool/hooks/use-clipboard.ts +54 -0
- package/src/components/tool/hooks/use-context-menus.ts +37 -0
- package/src/components/tool/hooks/use-design.ts +24 -0
- package/src/components/tool/hooks/use-desktop.ts +100 -0
- package/src/components/tool/hooks/use-devices.ts +58 -0
- package/src/components/tool/hooks/use-downloads.ts +42 -0
- package/src/components/tool/hooks/use-editor.ts +114 -0
- package/src/components/tool/hooks/use-engine.ts +29 -0
- package/src/components/tool/hooks/use-history.ts +107 -0
- package/src/components/tool/hooks/use-lab-actions.ts +57 -0
- package/src/components/tool/hooks/use-lab-run.ts +61 -0
- package/src/components/tool/hooks/use-lab.ts +32 -0
- package/src/components/tool/hooks/use-lan-carry.ts +43 -0
- package/src/components/tool/hooks/use-machine-actions.ts +57 -0
- package/src/components/tool/hooks/use-placement.ts +80 -0
- package/src/components/tool/hooks/use-project.ts +112 -0
- package/src/components/tool/hooks/use-projects.ts +85 -0
- package/src/components/tool/hooks/use-shared-link.ts +64 -0
- package/src/components/tool/hooks/use-toast.ts +16 -0
- package/src/components/tool/hooks/use-tools.ts +60 -0
- package/src/components/tool/lab-canvas.tsx +96 -0
- package/src/components/tool/overlays.tsx +104 -0
- package/src/components/tool/projects-list.tsx +96 -0
- package/src/components/tool/text.ts +9 -0
- package/src/components/tool/toolbar.tsx +106 -0
- package/src/components/tool/top-bars.tsx +90 -0
- package/src/components/tool/windows/conditions-fields.tsx +43 -0
- package/src/components/tool/windows/editor-windows.tsx +73 -0
- package/src/components/tool/windows/fields.ts +6 -0
- package/src/components/tool/windows/lab-status.tsx +68 -0
- package/src/components/tool/windows/lab-windows.tsx +130 -0
- package/src/components/tool/windows/link-controls.tsx +69 -0
- package/src/components/tool/windows/machine-controls.tsx +116 -0
- package/src/components/tool/windows/machine-inspector.tsx +83 -0
- package/src/components/tool/windows/runs-fields.tsx +127 -0
- package/src/components/tool/windows/selected-windows.tsx +147 -0
- package/src/components/ui/button.tsx +59 -0
- package/src/components/ui/dropdown-menu.tsx +185 -0
- package/src/index.ts +21 -0
- package/src/lib/auth.tsx +43 -0
- package/src/lib/brand.tsx +46 -0
- package/src/lib/i18n.ts +53 -0
- package/src/lib/tool/blocks.test.ts +64 -0
- package/src/lib/tool/blocks.ts +78 -0
- package/src/lib/tool/cable-wiring.test.ts +74 -0
- package/src/lib/tool/cables.test.ts +75 -0
- package/src/lib/tool/cables.ts +215 -0
- package/src/lib/tool/clone.test.ts +54 -0
- package/src/lib/tool/clone.ts +48 -0
- package/src/lib/tool/conditions.test.ts +170 -0
- package/src/lib/tool/conditions.ts +132 -0
- package/src/lib/tool/create.test.ts +67 -0
- package/src/lib/tool/create.ts +83 -0
- package/src/lib/tool/desktop.test.ts +59 -0
- package/src/lib/tool/desktop.ts +99 -0
- package/src/lib/tool/draft.test.ts +150 -0
- package/src/lib/tool/draft.ts +109 -0
- package/src/lib/tool/engine.test.ts +39 -0
- package/src/lib/tool/engine.ts +58 -0
- package/src/lib/tool/examples.test.ts +35 -0
- package/src/lib/tool/examples.ts +71 -0
- package/src/lib/tool/export-targets.test.ts +47 -0
- package/src/lib/tool/export-targets.ts +84 -0
- package/src/lib/tool/facing.test.ts +72 -0
- package/src/lib/tool/facing.ts +100 -0
- package/src/lib/tool/geometry.test.ts +80 -0
- package/src/lib/tool/geometry.ts +109 -0
- package/src/lib/tool/gns3-cables.test.ts +47 -0
- package/src/lib/tool/gns3-cables.ts +81 -0
- package/src/lib/tool/gns3-import.test.ts +102 -0
- package/src/lib/tool/gns3-import.ts +198 -0
- package/src/lib/tool/gns3-layout.test.ts +62 -0
- package/src/lib/tool/gns3-parts.test.ts +47 -0
- package/src/lib/tool/gns3-parts.ts +68 -0
- package/src/lib/tool/issues.test.ts +35 -0
- package/src/lib/tool/issues.ts +60 -0
- package/src/lib/tool/layout.test.ts +128 -0
- package/src/lib/tool/layout.ts +229 -0
- package/src/lib/tool/live.test.ts +46 -0
- package/src/lib/tool/live.ts +49 -0
- package/src/lib/tool/logos.test.ts +30 -0
- package/src/lib/tool/logos.ts +99 -0
- package/src/lib/tool/model.test.ts +128 -0
- package/src/lib/tool/model.ts +238 -0
- package/src/lib/tool/os.test.ts +45 -0
- package/src/lib/tool/os.ts +57 -0
- package/src/lib/tool/parity.test.ts +145 -0
- package/src/lib/tool/pins.test.ts +24 -0
- package/src/lib/tool/pins.ts +11 -0
- package/src/lib/tool/placement.test.ts +79 -0
- package/src/lib/tool/placement.ts +60 -0
- package/src/lib/tool/ports.test.ts +39 -0
- package/src/lib/tool/ports.ts +48 -0
- package/src/lib/tool/presets.test.ts +50 -0
- package/src/lib/tool/presets.ts +77 -0
- package/src/lib/tool/projects-api.test.ts +59 -0
- package/src/lib/tool/projects-api.ts +47 -0
- package/src/lib/tool/router-ports.test.ts +63 -0
- package/src/lib/tool/router-ports.ts +88 -0
- package/src/lib/tool/selection.test.ts +44 -0
- package/src/lib/tool/selection.ts +34 -0
- package/src/lib/tool/storage.test.ts +123 -0
- package/src/lib/tool/storage.ts +127 -0
- package/src/lib/tool/switch-edit.test.ts +49 -0
- package/src/lib/tool/switch-edit.ts +38 -0
- package/src/lib/tool/text/de.ts +358 -0
- package/src/lib/tool/text/en.ts +371 -0
- package/src/lib/tool/text/es.ts +358 -0
- package/src/lib/tool/text/fr.ts +358 -0
- package/src/lib/tool/text/index.ts +21 -0
- package/src/lib/tool/text/text.test.ts +33 -0
- package/src/lib/tool/topology.test.ts +108 -0
- package/src/lib/tool/topology.ts +116 -0
- package/src/lib/tool/vlans.test.ts +64 -0
- package/src/lib/tool/vlans.ts +79 -0
- package/src/lib/utils.ts +6 -0
- package/src/model.ts +13 -0
- package/src/test/arbitrary.ts +41 -0
- package/src/test/engine.ts +48 -0
- package/src/test/lab.ts +101 -0
- package/styles/globals.css +258 -0
- package/styles/theme.css +192 -0
- package/tsconfig.build.json +12 -0
- package/tsconfig.json +22 -0
- package/wasm/isothrum_wasm.d.ts +60 -0
- package/wasm/isothrum_wasm.js +312 -0
- package/wasm/isothrum_wasm_bg.wasm +0 -0
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { describe, expect, test } from "vitest";
|
|
2
|
+
import { draftFromTopology, type Topology } from "./layout";
|
|
3
|
+
import { nodeName, type LabNode } from "./model";
|
|
4
|
+
import { byId, compile } from "@/test/lab";
|
|
5
|
+
|
|
6
|
+
/** Two LANs, a server and a workstation in the first, an automatic router between them, two machines on none. */
|
|
7
|
+
const topology: Topology = {
|
|
8
|
+
name: "Lab One",
|
|
9
|
+
nodes: {
|
|
10
|
+
web: { kind: { container: { image: "nginx" } }, services: [{ port: 80 }, { port: 443, protocol: "https" }], networks: { lan1: 5, lan2: null } },
|
|
11
|
+
r1: { kind: { router: {} }, networks: { lan1: null, lan2: null } },
|
|
12
|
+
pc: { kind: { vm: { os: "debian@12", provision: ["a", "b"] } }, access: true, networks: { lan1: null } },
|
|
13
|
+
loose: { kind: { container: { image: "alpine", idle: true } } },
|
|
14
|
+
r2: { kind: { router: {} } },
|
|
15
|
+
},
|
|
16
|
+
networks: {
|
|
17
|
+
lan1: { cidr: "10.10.1.0/24", tc: { delay: "40ms", jitter: "5ms", loss: 1, rate: "10mbit" } },
|
|
18
|
+
lan2: { cidr: "10.10.2.0/24", internet: false },
|
|
19
|
+
},
|
|
20
|
+
links: [
|
|
21
|
+
{ a: { node: "web", port: "" }, b: { node: "pc", port: "eth5" }, tc: { rate: "1gbit", loss: 2 } },
|
|
22
|
+
{ a: { node: "ghost", port: "" }, b: { node: "pc", port: "" } },
|
|
23
|
+
],
|
|
24
|
+
};
|
|
25
|
+
const { draft } = draftFromTopology(topology);
|
|
26
|
+
/** The node named `name`. */
|
|
27
|
+
const named = (name: string): LabNode => {
|
|
28
|
+
const n = draft.nodes.find((x) => x.type !== "note" && x.data.name === name);
|
|
29
|
+
if (!n) throw new Error(`no node ${name}`);
|
|
30
|
+
return n;
|
|
31
|
+
};
|
|
32
|
+
/** The cables from the node named `from` to LANs, as [handle, the LAN's name, port]. */
|
|
33
|
+
const cablesFrom = (from: string) =>
|
|
34
|
+
draft.edges.filter((e) => e.source === named(from).id && byId(draft.nodes, e.target).type === "lan").map((e) => [e.sourceHandle, nodeName(byId(draft.nodes, e.target)), e.data?.aPort]);
|
|
35
|
+
|
|
36
|
+
describe("draftFromTopology", () => {
|
|
37
|
+
test("names the lab after the topology, cleaned, or 'lab'", () => {
|
|
38
|
+
expect(draft.name).toBe("labone");
|
|
39
|
+
expect(draftFromTopology({ name: "", nodes: {} }).draft).toEqual({ name: "lab", nodes: [], edges: [] });
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test("lays the LANs out side by side, each sized for the machines inside", () => {
|
|
43
|
+
const lans = draft.nodes.filter((n) => n.type === "lan");
|
|
44
|
+
expect(lans.map((l) => [l.data.name, l.position.x, l.width, l.height])).toEqual([
|
|
45
|
+
["lan1", 0, 420, 265],
|
|
46
|
+
["lan2", 580, 340, 265],
|
|
47
|
+
]);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test("keeps each LAN's block, its internet (on by default) and its conditions", () => {
|
|
51
|
+
expect(named("lan1").data).toEqual({ name: "lan1", cidr: "10.10.1.0/24", internet: true, tc: { delay: 40, jitter: 5, loss: 1, rate: 10 } });
|
|
52
|
+
expect(named("lan2").data).toMatchObject({ internet: false, tc: undefined });
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test("places machines in a grid inside their first LAN", () => {
|
|
56
|
+
expect([named("web").position, named("pc").position]).toEqual([
|
|
57
|
+
{ x: 40, y: 70 },
|
|
58
|
+
{ x: 210, y: 70 },
|
|
59
|
+
]);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test("puts an automatic router under the LANs it joins, and machines on no LAN in a row below", () => {
|
|
63
|
+
expect([named("r1").position, named("loose").position, named("r2").position]).toEqual([
|
|
64
|
+
{ x: 420, y: 375 },
|
|
65
|
+
{ x: 0, y: 610 },
|
|
66
|
+
{ x: 170, y: 610 },
|
|
67
|
+
]);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test("turns each node into a machine: its kind, icon, image or OS, services and fixed addresses", () => {
|
|
71
|
+
expect(named("web").data).toMatchObject({ kind: "container", icon: "server", image: "nginx", ports: "80, 443/https", pins: { lan1: 5 } });
|
|
72
|
+
expect(named("pc").data).toMatchObject({ kind: "vm", icon: "laptop", os: "debian@12", provision: "a\nb", access: true });
|
|
73
|
+
expect(named("loose").data).toMatchObject({ kind: "container", icon: "container", idle: true });
|
|
74
|
+
expect(named("r1").data).toMatchObject({ kind: "router", icon: "router" });
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test("cables a router to each LAN it joins, and a machine to the LANs it doesn't sit in", () => {
|
|
78
|
+
expect(cablesFrom("r1")).toEqual([
|
|
79
|
+
["port", "lan1", "eth0"],
|
|
80
|
+
["port", "lan2", "eth1"],
|
|
81
|
+
]);
|
|
82
|
+
expect(cablesFrom("web")).toEqual([["bottom", "lan2", "eth0"]]);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test("cables the links between known machines, a port left empty taking the next free one", () => {
|
|
86
|
+
const link = draft.edges.find((e) => e.target === named("pc").id);
|
|
87
|
+
expect(link?.data).toEqual({ aPort: "eth1", bPort: "eth5", tc: { loss: 2, rate: 1000 } });
|
|
88
|
+
expect(draft.edges).toHaveLength(4);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test("reads back as the same machines on the same networks", () => {
|
|
92
|
+
const t = compile(draft.nodes, draft.edges);
|
|
93
|
+
expect(Object.fromEntries(Object.entries(t.nodes).map(([n, m]) => [n, m.networks]))).toEqual({
|
|
94
|
+
web: { lan1: 5, lan2: null },
|
|
95
|
+
pc: { lan1: null },
|
|
96
|
+
r1: { lan1: null, lan2: null },
|
|
97
|
+
loose: {},
|
|
98
|
+
r2: {},
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
describe("draftFromTopology's link conditions", () => {
|
|
104
|
+
test("keeps a delay written in microseconds or seconds", () => {
|
|
105
|
+
const slow: Topology = { name: "slow", nodes: {}, networks: { far: { cidr: "10.10.9.0/24", tc: { delay: "1s", jitter: "500us" } } } };
|
|
106
|
+
expect(draftFromTopology(slow).draft.nodes[0].data).toMatchObject({ tc: { delay: 1000, jitter: 0.5 } });
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test("reports each condition it can't read, where it is, and draws the rest", () => {
|
|
110
|
+
const odd: Topology = {
|
|
111
|
+
name: "odd",
|
|
112
|
+
nodes: { a: { kind: { container: { image: "alpine" } } }, b: { kind: { container: { image: "alpine" } } } },
|
|
113
|
+
networks: { far: { cidr: "10.10.9.0/24", tc: { delay: "fast", loss: 1 } } },
|
|
114
|
+
links: [{ a: { node: "a", port: "eth0" }, b: { node: "b", port: "eth0" }, tc: { rate: "lots" } }],
|
|
115
|
+
};
|
|
116
|
+
const { draft: laid, problems } = draftFromTopology(odd);
|
|
117
|
+
expect(problems).toEqual([
|
|
118
|
+
"networks.far.tc.delay: `fast` isn't a time like 40ms, 500us or 1s; the drawing leaves it out",
|
|
119
|
+
"links[0].tc.rate: `lots` isn't a rate like 10mbit or 512kbit; the drawing leaves it out",
|
|
120
|
+
]);
|
|
121
|
+
expect(laid.nodes.find((n) => n.type === "lan")?.data).toMatchObject({ tc: { loss: 1 } });
|
|
122
|
+
expect(laid.edges[0].data?.tc).toBeUndefined();
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
test("has no problems with a topology whose conditions it all reads", () => {
|
|
126
|
+
expect(draftFromTopology(topology).problems).toEqual([]);
|
|
127
|
+
});
|
|
128
|
+
});
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A lab given as the engine's topology (what an assistant writes through the MCP server), laid
|
|
3
|
+
* out on the canvas: LANs side by side with their machines in a grid inside, automatic routers
|
|
4
|
+
* under the LANs they join (cabled to each), the machines on no LAN in a row below, and the
|
|
5
|
+
* cables between machines. The inverse of `toTopology`, for labs nobody drew.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { migrateEdges, nextPort } from "./cables";
|
|
9
|
+
import { conditionsOf, tcProblemText, type Tc } from "./conditions";
|
|
10
|
+
import { portsText, type Service } from "./ports";
|
|
11
|
+
import {
|
|
12
|
+
LAN_BEHAVIOR,
|
|
13
|
+
MACHINE_W,
|
|
14
|
+
centerOf,
|
|
15
|
+
cleanName,
|
|
16
|
+
uid,
|
|
17
|
+
type CableEdge,
|
|
18
|
+
type Draft,
|
|
19
|
+
type IconId,
|
|
20
|
+
type LabNode,
|
|
21
|
+
type LanNode,
|
|
22
|
+
type LinkConditions,
|
|
23
|
+
type MachineData,
|
|
24
|
+
type MachineNode,
|
|
25
|
+
type Side,
|
|
26
|
+
} from "./model";
|
|
27
|
+
|
|
28
|
+
type TopologyNode = {
|
|
29
|
+
kind: { container?: { image: string; idle?: boolean }; vm?: { os: string; provision?: string[] }; router?: object };
|
|
30
|
+
access?: boolean;
|
|
31
|
+
services?: Service[];
|
|
32
|
+
networks?: Record<string, number | null>;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export interface Topology {
|
|
36
|
+
name: string;
|
|
37
|
+
nodes: Record<string, TopologyNode>;
|
|
38
|
+
networks?: Record<string, { cidr: string; internet?: boolean; tc?: Tc }>;
|
|
39
|
+
links?: { a: { node: string; port: string }; b: { node: string; port: string }; tc?: Tc }[];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const COLS = 3;
|
|
43
|
+
const CELL_W = 170;
|
|
44
|
+
const CELL_H = 175;
|
|
45
|
+
const GAP = 160;
|
|
46
|
+
|
|
47
|
+
function icon(kind: TopologyNode["kind"], access: boolean): IconId {
|
|
48
|
+
if (kind.router) return "router";
|
|
49
|
+
if (kind.vm) return access ? "laptop" : "monitor";
|
|
50
|
+
const image = kind.container?.image ?? "";
|
|
51
|
+
return /nginx|httpd|apache|caddy|traefik|postgres|mysql|mariadb|redis|mongo|bind|dnsmasq|vsftpd|postfix/.test(image) ? "server" : "container";
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** What the layout builds: the nodes and cables so far, the LANs and machines by topology name,
|
|
55
|
+
* and what in the topology the drawing can't keep. */
|
|
56
|
+
interface Layout {
|
|
57
|
+
topo: Topology;
|
|
58
|
+
nodes: LabNode[];
|
|
59
|
+
edges: CableEdge[];
|
|
60
|
+
lans: Map<string, LanNode>;
|
|
61
|
+
machines: Map<string, MachineNode>;
|
|
62
|
+
problems: string[];
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** A topology laid out: the draft, and each value in the topology the drawing leaves out. */
|
|
66
|
+
export interface LaidOut {
|
|
67
|
+
draft: Draft;
|
|
68
|
+
problems: string[];
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** The editor's numbers for the `tc` at path `at`, each value it can't read kept as a problem. */
|
|
72
|
+
function conditionsAt(layout: Layout, at: string, tc: Tc | undefined): LinkConditions | undefined {
|
|
73
|
+
const { conditions, problems } = conditionsOf(tc);
|
|
74
|
+
layout.problems.push(...problems.map((p) => tcProblemText(at, p)));
|
|
75
|
+
return conditions;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** The LANs among a machine's networks. */
|
|
79
|
+
const lansOf = (layout: Layout, n: TopologyNode) => Object.keys(n.networks ?? {}).filter((name) => layout.lans.has(name));
|
|
80
|
+
|
|
81
|
+
/** Each LAN's machines: those that sit in it (their first LAN); routers join theirs by cables. */
|
|
82
|
+
function residents(topo: Topology): Map<string, string[]> {
|
|
83
|
+
const lanNames = Object.keys(topo.networks ?? {});
|
|
84
|
+
const inside = new Map<string, string[]>(lanNames.map((l) => [l, []]));
|
|
85
|
+
for (const [name, n] of Object.entries(topo.nodes)) {
|
|
86
|
+
const first = Object.keys(n.networks ?? {}).find((l) => lanNames.includes(l));
|
|
87
|
+
if (!n.kind.router && first) inside.get(first)?.push(name);
|
|
88
|
+
}
|
|
89
|
+
return inside;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** The LANs, in a row, each sized for the grid of machines inside it. */
|
|
93
|
+
function placeLans(layout: Layout, inside: Map<string, string[]>): void {
|
|
94
|
+
let x = 0;
|
|
95
|
+
for (const [name, net] of Object.entries(layout.topo.networks ?? {})) {
|
|
96
|
+
const count = Math.max(1, inside.get(name)?.length ?? 0);
|
|
97
|
+
const w = Math.max(340, Math.min(COLS, count) * CELL_W + 80);
|
|
98
|
+
const h = Math.max(240, Math.ceil(count / COLS) * CELL_H + 90);
|
|
99
|
+
const lan: LanNode = {
|
|
100
|
+
...LAN_BEHAVIOR,
|
|
101
|
+
id: uid("l"),
|
|
102
|
+
type: "lan",
|
|
103
|
+
position: { x, y: 0 },
|
|
104
|
+
width: w,
|
|
105
|
+
height: h,
|
|
106
|
+
data: { name: cleanName(name), cidr: net.cidr, internet: net.internet ?? true, tc: conditionsAt(layout, `networks.${name}.tc`, net.tc) },
|
|
107
|
+
};
|
|
108
|
+
layout.lans.set(name, lan);
|
|
109
|
+
layout.nodes.push(lan);
|
|
110
|
+
x += w + GAP;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** A machine from its topology entry, at `position`. */
|
|
115
|
+
function addMachine(layout: Layout, name: string, n: TopologyNode, position: { x: number; y: number }): void {
|
|
116
|
+
const access = !!n.access;
|
|
117
|
+
const data: MachineData = {
|
|
118
|
+
name: cleanName(name),
|
|
119
|
+
kind: n.kind.router ? "router" : n.kind.vm ? "vm" : "container",
|
|
120
|
+
icon: icon(n.kind, access),
|
|
121
|
+
image: n.kind.container?.image ?? "",
|
|
122
|
+
os: n.kind.vm?.os ?? "",
|
|
123
|
+
provision: (n.kind.vm?.provision ?? []).join("\n"),
|
|
124
|
+
ports: portsText(n.services ?? []),
|
|
125
|
+
access,
|
|
126
|
+
...(n.kind.container ? { idle: n.kind.container.idle } : {}),
|
|
127
|
+
};
|
|
128
|
+
const pins = Object.fromEntries(Object.entries(n.networks ?? {}).filter((e): e is [string, number] => typeof e[1] === "number"));
|
|
129
|
+
if (Object.keys(pins).length) data.pins = pins;
|
|
130
|
+
const m: MachineNode = { id: uid("m"), type: "machine", position, data };
|
|
131
|
+
layout.machines.set(name, m);
|
|
132
|
+
layout.nodes.push(m);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** Machines inside their first LAN, in a grid; routers under the LANs they join; the rest below. */
|
|
136
|
+
function placeMachines(layout: Layout, inside: Map<string, string[]>): void {
|
|
137
|
+
for (const [lanName, names] of inside) {
|
|
138
|
+
const lan = layout.lans.get(lanName);
|
|
139
|
+
if (!lan) continue;
|
|
140
|
+
names.forEach((name, i) => {
|
|
141
|
+
const at = { x: lan.position.x + 40 + (i % COLS) * CELL_W, y: lan.position.y + 70 + Math.floor(i / COLS) * CELL_H };
|
|
142
|
+
addMachine(layout, name, layout.topo.nodes[name], at);
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
const lanBottom = Math.max(0, ...[...layout.lans.values()].map((l) => l.height ?? 0));
|
|
146
|
+
let routerX = 0;
|
|
147
|
+
let looseX = 0;
|
|
148
|
+
for (const [name, n] of Object.entries(layout.topo.nodes)) {
|
|
149
|
+
if (layout.machines.has(name)) continue;
|
|
150
|
+
const joined = lansOf(layout, n).flatMap((l) => layout.lans.get(l) ?? []);
|
|
151
|
+
if (n.kind.router && joined.length) {
|
|
152
|
+
const center = joined.reduce((s, l) => s + l.position.x + (l.width ?? 0) / 2, 0) / joined.length;
|
|
153
|
+
routerX = Math.max(routerX, center - MACHINE_W / 2);
|
|
154
|
+
addMachine(layout, name, n, { x: routerX, y: lanBottom + 110 });
|
|
155
|
+
routerX += MACHINE_W + 60;
|
|
156
|
+
} else {
|
|
157
|
+
addMachine(layout, name, n, { x: looseX, y: lanBottom + 110 + CELL_H + 60 });
|
|
158
|
+
looseX += CELL_W;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** The side of a LAN that faces a machine, relative to the LAN's own proportions. */
|
|
164
|
+
function side(lan: LanNode, m: MachineNode): Side {
|
|
165
|
+
const d = { x: centerOf(m).x - centerOf(lan).x, y: centerOf(m).y - centerOf(lan).y };
|
|
166
|
+
if (Math.abs(d.x) / (lan.width ?? 1) > Math.abs(d.y) / (lan.height ?? 1)) return d.x < 0 ? "left" : "right";
|
|
167
|
+
return d.y < 0 ? "top" : "bottom";
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** Where a cable leaves a machine: an appliance's port, or the top or bottom facing the other end. */
|
|
171
|
+
function handle(m: MachineNode, other: LabNode): string {
|
|
172
|
+
if (m.data.icon === "router" || m.data.icon === "shield") return "port";
|
|
173
|
+
return centerOf(other).y < centerOf(m).y ? "top" : "bottom";
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** A cable from each machine to each LAN it joins besides the one it sits in. */
|
|
177
|
+
function cableToLans(layout: Layout): void {
|
|
178
|
+
for (const [name, n] of Object.entries(layout.topo.nodes)) {
|
|
179
|
+
const m = layout.machines.get(name);
|
|
180
|
+
if (!m) continue;
|
|
181
|
+
const skip = n.kind.router ? undefined : lansOf(layout, n)[0];
|
|
182
|
+
for (const l of lansOf(layout, n).filter((x) => x !== skip)) {
|
|
183
|
+
const lan = layout.lans.get(l);
|
|
184
|
+
if (!lan) continue;
|
|
185
|
+
layout.edges.push({
|
|
186
|
+
id: uid("c"),
|
|
187
|
+
type: "cable",
|
|
188
|
+
source: m.id,
|
|
189
|
+
sourceHandle: handle(m, lan),
|
|
190
|
+
target: lan.id,
|
|
191
|
+
targetHandle: side(lan, m),
|
|
192
|
+
data: { aPort: nextPort(m.id, layout.edges), bPort: "" },
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/** The links between machines, each with its ports and conditions. */
|
|
199
|
+
function cableLinks(layout: Layout): void {
|
|
200
|
+
for (const [i, link] of (layout.topo.links ?? []).entries()) {
|
|
201
|
+
const a = layout.machines.get(link.a.node);
|
|
202
|
+
const b = layout.machines.get(link.b.node);
|
|
203
|
+
if (!a || !b) continue;
|
|
204
|
+
layout.edges.push({
|
|
205
|
+
id: uid("c"),
|
|
206
|
+
type: "cable",
|
|
207
|
+
source: a.id,
|
|
208
|
+
sourceHandle: handle(a, b),
|
|
209
|
+
target: b.id,
|
|
210
|
+
targetHandle: handle(b, a),
|
|
211
|
+
data: {
|
|
212
|
+
aPort: link.a.port || nextPort(a.id, layout.edges),
|
|
213
|
+
bPort: link.b.port || nextPort(b.id, layout.edges),
|
|
214
|
+
tc: conditionsAt(layout, `links[${String(i)}].tc`, link.tc),
|
|
215
|
+
},
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
export function draftFromTopology(topo: Topology): LaidOut {
|
|
221
|
+
const layout: Layout = { topo, nodes: [], edges: [], lans: new Map(), machines: new Map(), problems: [] };
|
|
222
|
+
const inside = residents(topo);
|
|
223
|
+
placeLans(layout, inside);
|
|
224
|
+
placeMachines(layout, inside);
|
|
225
|
+
cableToLans(layout);
|
|
226
|
+
cableLinks(layout);
|
|
227
|
+
const draft = { name: cleanName(topo.name) || "lab", nodes: layout.nodes, edges: migrateEdges(layout.edges, layout.nodes) };
|
|
228
|
+
return { draft, problems: layout.problems };
|
|
229
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { describe, expect, test } from "vitest";
|
|
2
|
+
import type { LabLive } from "./desktop";
|
|
3
|
+
import { liveViewOf } from "./live";
|
|
4
|
+
import { cable, lan, machine } from "@/test/lab";
|
|
5
|
+
|
|
6
|
+
const away = { x: 2000, y: 2000 };
|
|
7
|
+
const nodes = [lan("office"), machine("web", {}, away), machine("db", {}, away), machine("cache", {}, away)];
|
|
8
|
+
const edges = [cable("webLan", "web", "office"), cable("dbLan", "db", "office"), cable("webCache", "web", "cache")];
|
|
9
|
+
const live: LabLive = {
|
|
10
|
+
machines: { web: { state: "running", health: "" }, db: { state: "running", health: "unhealthy" }, cache: { state: "running", health: "" } },
|
|
11
|
+
ports: [
|
|
12
|
+
{ machine: "web", network: "office", up: true, interface: "eth0" },
|
|
13
|
+
{ machine: "db", network: "office", up: false, interface: null },
|
|
14
|
+
{ machine: "web", network: "link-0", up: null, interface: null },
|
|
15
|
+
],
|
|
16
|
+
probed: true,
|
|
17
|
+
};
|
|
18
|
+
const view = liveViewOf(live, [], nodes, edges);
|
|
19
|
+
|
|
20
|
+
describe("liveViewOf", () => {
|
|
21
|
+
test("shows each machine's status, and its state with its health", () => {
|
|
22
|
+
expect(view.machines.db).toEqual({ status: "unhealthy", detail: "running, unhealthy" });
|
|
23
|
+
expect(view.machines.web).toEqual({ status: "up", detail: "running" });
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test("keys each port by machine and network", () => {
|
|
27
|
+
expect(view.ports).toEqual({ "web/office": true, "db/office": false, "web/link-0": null });
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test("a cable is up when its ports are, down when one is, unknown when a port isn't known", () => {
|
|
31
|
+
expect(view.cables).toEqual({ webLan: "up", dbLan: "down", webCache: "unknown" });
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test("a cable to a machine that is down is down", () => {
|
|
35
|
+
const stopped = { ...live, machines: { ...live.machines, web: { state: "exited", health: "" } } };
|
|
36
|
+
expect(liveViewOf(stopped, [], nodes, edges).cables.webLan).toBe("down");
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test("a suspended link cuts its cable", () => {
|
|
40
|
+
expect(liveViewOf(live, [{ network: "office", machine: "web" }], nodes, edges).cables.webLan).toBe("cut");
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test("counts each network's ports up, out of all", () => {
|
|
44
|
+
expect(view.lans).toEqual({ office: { up: 1, total: 2 }, "link-0": { up: 0, total: 1 } });
|
|
45
|
+
});
|
|
46
|
+
});
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { machineEnds, networkOfCable } from "./cables";
|
|
2
|
+
import { machineStatus, type LabLive, type Status, type SuspendedLink } from "./desktop";
|
|
3
|
+
import type { CableEdge, LabNode } from "./model";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The running lab as the canvas shows it (desktop app; null: not running, or in a browser).
|
|
7
|
+
* Machines by name; ports by `machine/network` (up, down, or null: unknown); cables by edge id;
|
|
8
|
+
* LANs by name, with how many of their ports are up.
|
|
9
|
+
*/
|
|
10
|
+
export type CableStatus = "up" | "cut" | "down" | "unknown";
|
|
11
|
+
export interface LiveView {
|
|
12
|
+
machines: Partial<Record<string, { status: Status; detail: string }>>;
|
|
13
|
+
ports: Partial<Record<string, boolean | null>>;
|
|
14
|
+
cables: Partial<Record<string, CableStatus>>;
|
|
15
|
+
lans: Partial<Record<string, { up: number; total: number }>>;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** A cable now: cut (suspended), down (a port or a machine is), up (both ports are), or unknown. */
|
|
19
|
+
function cableStatus(view: Pick<LiveView, "machines" | "ports">, network: string, ends: string[], links: SuspendedLink[]): CableStatus {
|
|
20
|
+
if (links.some((l) => l.network === network && ends.includes(l.machine))) return "cut";
|
|
21
|
+
const states = ends.map((m) => view.ports[`${m}/${network}`]);
|
|
22
|
+
if (states.some((s) => s === false) || ends.some((m) => view.machines[m]?.status === "down")) return "down";
|
|
23
|
+
return states.length && states.every((s) => s === true) ? "up" : "unknown";
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Each LAN's ports up, out of all. */
|
|
27
|
+
function lansLive(live: LabLive): LiveView["lans"] {
|
|
28
|
+
const lans: Record<string, { up: number; total: number }> = {};
|
|
29
|
+
for (const p of live.ports) {
|
|
30
|
+
const l = (lans[p.network] ??= { up: 0, total: 0 });
|
|
31
|
+
l.total += 1;
|
|
32
|
+
if (p.up === true) l.up += 1;
|
|
33
|
+
}
|
|
34
|
+
return lans;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function liveViewOf(live: LabLive, links: SuspendedLink[], nodes: LabNode[], edges: CableEdge[]): LiveView {
|
|
38
|
+
const ports: Record<string, boolean | null> = {};
|
|
39
|
+
for (const p of live.ports) ports[`${p.machine}/${p.network}`] = p.up;
|
|
40
|
+
const machines = Object.fromEntries(
|
|
41
|
+
Object.entries(live.machines).map(([m, l]) => [m, { status: machineStatus(l), detail: l.health ? `${l.state}, ${l.health}` : l.state }]),
|
|
42
|
+
);
|
|
43
|
+
const cables: Record<string, CableStatus> = {};
|
|
44
|
+
for (const e of edges) {
|
|
45
|
+
const network = networkOfCable(e.id, nodes, edges);
|
|
46
|
+
if (network) cables[e.id] = cableStatus({ machines, ports }, network, machineEnds(e, nodes), links);
|
|
47
|
+
}
|
|
48
|
+
return { machines, ports, cables, lans: lansLive(live) };
|
|
49
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { siAlpinelinux, siDocker, siKalilinux, siLinux, siNginx, siPostgresql } from "simple-icons";
|
|
2
|
+
import { describe, expect, test } from "vitest";
|
|
3
|
+
import { dockerLogo, isDark, logoFor } from "./logos";
|
|
4
|
+
|
|
5
|
+
describe("logoFor", () => {
|
|
6
|
+
test("a VM takes its OS's logo, the Linux one when its distribution has none", () => {
|
|
7
|
+
expect(logoFor({ kind: "vm", os: "kali@rolling", image: "" })).toBe(siKalilinux);
|
|
8
|
+
expect(logoFor({ kind: "vm", os: "unknown", image: "" })).toBe(siLinux);
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
test("a Windows VM has no logo", () => {
|
|
12
|
+
expect(logoFor({ kind: "vm", os: "windows@11", image: "" })).toBeNull();
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
test("a container takes its image's logo, by the image's name", () => {
|
|
16
|
+
expect(logoFor({ kind: "container", os: "", image: "library/nginx:1" })).toBe(siNginx);
|
|
17
|
+
expect(logoFor({ kind: "container", os: "", image: " Postgres:16 " })).toBe(siPostgresql);
|
|
18
|
+
expect(logoFor({ kind: "container", os: "", image: "alpine" })).toBe(siAlpinelinux);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test("an image it doesn't know has none, and falls back to Docker's whale", () => {
|
|
22
|
+
expect(logoFor({ kind: "container", os: "", image: "foo" })).toBeNull();
|
|
23
|
+
expect(dockerLogo).toBe(siDocker);
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test("isDark tells the brand colors too dark for a dark background", () => {
|
|
28
|
+
expect(["000000", "1C1C1C"].map(isDark)).toEqual([true, true]);
|
|
29
|
+
expect(["FFFFFF", "F97316", "0D597F"].map(isDark)).toEqual([false, false, false]);
|
|
30
|
+
});
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import {
|
|
2
|
+
siAlmalinux,
|
|
3
|
+
siAlpinelinux,
|
|
4
|
+
siApache,
|
|
5
|
+
siCentos,
|
|
6
|
+
siDebian,
|
|
7
|
+
siDocker,
|
|
8
|
+
siElasticsearch,
|
|
9
|
+
siFedora,
|
|
10
|
+
siGitlab,
|
|
11
|
+
siGrafana,
|
|
12
|
+
siJenkins,
|
|
13
|
+
siKalilinux,
|
|
14
|
+
siKibana,
|
|
15
|
+
siLinux,
|
|
16
|
+
siMariadb,
|
|
17
|
+
siMongodb,
|
|
18
|
+
siMysql,
|
|
19
|
+
siNginx,
|
|
20
|
+
siNodedotjs,
|
|
21
|
+
siPostgresql,
|
|
22
|
+
siPrometheus,
|
|
23
|
+
siPython,
|
|
24
|
+
siRedis,
|
|
25
|
+
siRockylinux,
|
|
26
|
+
siTraefikproxy,
|
|
27
|
+
siUbuntu,
|
|
28
|
+
siWireshark,
|
|
29
|
+
type SimpleIcon,
|
|
30
|
+
} from "simple-icons";
|
|
31
|
+
import { imageName, type MachineData } from "./model";
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* A machine's logo, from what it runs: the VM's OS, or the container's image. Logos are from
|
|
35
|
+
* Simple Icons (there is none for Windows: Microsoft had theirs removed), so a Windows VM, a
|
|
36
|
+
* router or an image we don't recognise keeps its generic icon.
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
const BY_OS: [prefix: string, icon: SimpleIcon][] = [
|
|
40
|
+
["debian", siDebian],
|
|
41
|
+
["ubuntu", siUbuntu],
|
|
42
|
+
["kali", siKalilinux],
|
|
43
|
+
["rocky", siRockylinux],
|
|
44
|
+
["alma", siAlmalinux],
|
|
45
|
+
["centos", siCentos],
|
|
46
|
+
["fedora", siFedora],
|
|
47
|
+
];
|
|
48
|
+
|
|
49
|
+
/** Image name (its last path segment, without the tag) → logo. */
|
|
50
|
+
const BY_IMAGE: [name: string, icon: SimpleIcon][] = [
|
|
51
|
+
["nginx", siNginx],
|
|
52
|
+
["alpine", siAlpinelinux],
|
|
53
|
+
["postgres", siPostgresql],
|
|
54
|
+
["mysql", siMysql],
|
|
55
|
+
["mariadb", siMariadb],
|
|
56
|
+
["redis", siRedis],
|
|
57
|
+
["mongo", siMongodb],
|
|
58
|
+
["httpd", siApache],
|
|
59
|
+
["node", siNodedotjs],
|
|
60
|
+
["python", siPython],
|
|
61
|
+
["grafana", siGrafana],
|
|
62
|
+
["prometheus", siPrometheus],
|
|
63
|
+
["elasticsearch", siElasticsearch],
|
|
64
|
+
["kibana", siKibana],
|
|
65
|
+
["jenkins", siJenkins],
|
|
66
|
+
["gitlab-ce", siGitlab],
|
|
67
|
+
["gitlab-ee", siGitlab],
|
|
68
|
+
["traefik", siTraefikproxy],
|
|
69
|
+
["wireshark", siWireshark],
|
|
70
|
+
["kali-rolling", siKalilinux],
|
|
71
|
+
["debian", siDebian],
|
|
72
|
+
["ubuntu", siUbuntu],
|
|
73
|
+
["rockylinux", siRockylinux],
|
|
74
|
+
["alma", siAlmalinux],
|
|
75
|
+
["centos", siCentos],
|
|
76
|
+
["fedora", siFedora],
|
|
77
|
+
];
|
|
78
|
+
|
|
79
|
+
export function logoFor(m: Pick<MachineData, "kind" | "image" | "os">): SimpleIcon | null {
|
|
80
|
+
if (m.kind === "vm") {
|
|
81
|
+
if (m.os.startsWith("windows")) return null;
|
|
82
|
+
return BY_OS.find(([prefix]) => m.os.startsWith(prefix))?.[1] ?? siLinux;
|
|
83
|
+
}
|
|
84
|
+
const name = imageName(m.image);
|
|
85
|
+
return BY_IMAGE.find(([n]) => name === n)?.[1] ?? null;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** The fallback for a container whose image we don't know: Docker's whale. */
|
|
89
|
+
export const dockerLogo = siDocker;
|
|
90
|
+
|
|
91
|
+
/** Brand colors too dark to read on a dark background (AlmaLinux and others use black). */
|
|
92
|
+
export function isDark(hex: string): boolean {
|
|
93
|
+
const n = parseInt(hex, 16);
|
|
94
|
+
const [r, g, b] = [(n >> 16) & 255, (n >> 8) & 255, n & 255].map((c) => {
|
|
95
|
+
const s = c / 255;
|
|
96
|
+
return s <= 0.03928 ? s / 12.92 : ((s + 0.055) / 1.055) ** 2.4;
|
|
97
|
+
});
|
|
98
|
+
return 0.2126 * r + 0.7152 * g + 0.0722 * b < 0.05;
|
|
99
|
+
}
|