@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.
Files changed (293) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +8 -0
  3. package/PUBLISHING.md +93 -0
  4. package/README.md +106 -2
  5. package/desktop-editor/editor.css +11 -0
  6. package/desktop-editor/index.html +14 -0
  7. package/desktop-editor/main.tsx +26 -0
  8. package/dist/desktop/editor.css +1 -0
  9. package/dist/desktop/editor.js +181 -0
  10. package/dist/desktop/fonts/geist-cyrillic-ext-wght-normal-RO72NYA2.woff2 +0 -0
  11. package/dist/desktop/fonts/geist-cyrillic-wght-normal-CK2XY27K.woff2 +0 -0
  12. package/dist/desktop/fonts/geist-latin-ext-wght-normal-XEQ56JUF.woff2 +0 -0
  13. package/dist/desktop/fonts/geist-latin-wght-normal-T72VVCUW.woff2 +0 -0
  14. package/dist/desktop/fonts/geist-mono-cyrillic-ext-wght-normal-QXKVIKFD.woff2 +0 -0
  15. package/dist/desktop/fonts/geist-mono-cyrillic-wght-normal-GOIAQAGU.woff2 +0 -0
  16. package/dist/desktop/fonts/geist-mono-latin-ext-wght-normal-HU3ZAWN6.woff2 +0 -0
  17. package/dist/desktop/fonts/geist-mono-latin-wght-normal-CJNQKXUG.woff2 +0 -0
  18. package/dist/desktop/fonts/geist-mono-symbols2-wght-normal-WZJJVLQO.woff2 +0 -0
  19. package/dist/desktop/fonts/geist-mono-vietnamese-wght-normal-KW5NM6YV.woff2 +0 -0
  20. package/dist/desktop/fonts/geist-vietnamese-wght-normal-Y2R7JZKV.woff2 +0 -0
  21. package/dist/desktop/fonts/shantell-sans-cyrillic-ext-wght-normal-ILPQWRJJ.woff2 +0 -0
  22. package/dist/desktop/fonts/shantell-sans-cyrillic-wght-normal-RKC6NDPM.woff2 +0 -0
  23. package/dist/desktop/fonts/shantell-sans-latin-ext-wght-normal-SNKHG3LM.woff2 +0 -0
  24. package/dist/desktop/fonts/shantell-sans-latin-wght-normal-7CHHGC3N.woff2 +0 -0
  25. package/dist/desktop/fonts/shantell-sans-vietnamese-wght-normal-IPFAQSBA.woff2 +0 -0
  26. package/dist/desktop/index.html +14 -0
  27. package/dist/desktop/styles.css +2 -0
  28. package/dist/desktop/wasm/isothrum_wasm.d.ts +60 -0
  29. package/dist/desktop/wasm/isothrum_wasm.js +312 -0
  30. package/dist/desktop/wasm/isothrum_wasm_bg.wasm +0 -0
  31. package/dist/index.js +7059 -0
  32. package/dist/index.js.map +7 -0
  33. package/dist/model.js +745 -0
  34. package/dist/model.js.map +7 -0
  35. package/dist/types/components/tool/canvas/cable-edge.d.ts +4 -0
  36. package/dist/types/components/tool/canvas/contexts.d.ts +12 -0
  37. package/dist/types/components/tool/canvas/device-icon.d.ts +17 -0
  38. package/dist/types/components/tool/canvas/inline-text.d.ts +15 -0
  39. package/dist/types/components/tool/canvas/lan-node.d.ts +8 -0
  40. package/dist/types/components/tool/canvas/machine-addresses.d.ts +14 -0
  41. package/dist/types/components/tool/canvas/machine-node.d.ts +4 -0
  42. package/dist/types/components/tool/canvas/note-node.d.ts +7 -0
  43. package/dist/types/components/tool/canvas/port-led.d.ts +10 -0
  44. package/dist/types/components/tool/canvas/router-node.d.ts +8 -0
  45. package/dist/types/components/tool/canvas/sketch.d.ts +50 -0
  46. package/dist/types/components/tool/canvas/status-dot.d.ts +5 -0
  47. package/dist/types/components/tool/canvas/switch-node.d.ts +8 -0
  48. package/dist/types/components/tool/canvas/use-measured-ports.d.ts +7 -0
  49. package/dist/types/components/tool/context-menus.d.ts +5 -0
  50. package/dist/types/components/tool/device-block.d.ts +12 -0
  51. package/dist/types/components/tool/editor-menu.d.ts +28 -0
  52. package/dist/types/components/tool/export/copy-button.d.ts +9 -0
  53. package/dist/types/components/tool/export/export-panel.d.ts +17 -0
  54. package/dist/types/components/tool/export/target-list.d.ts +15 -0
  55. package/dist/types/components/tool/floating-window.d.ts +13 -0
  56. package/dist/types/components/tool/flow-canvas.d.ts +6 -0
  57. package/dist/types/components/tool/hooks/use-cabling.d.ts +23 -0
  58. package/dist/types/components/tool/hooks/use-captures.d.ts +9 -0
  59. package/dist/types/components/tool/hooks/use-clipboard.d.ts +12 -0
  60. package/dist/types/components/tool/hooks/use-context-menus.d.ts +25 -0
  61. package/dist/types/components/tool/hooks/use-design.d.ts +10 -0
  62. package/dist/types/components/tool/hooks/use-desktop.d.ts +50 -0
  63. package/dist/types/components/tool/hooks/use-devices.d.ts +17 -0
  64. package/dist/types/components/tool/hooks/use-downloads.d.ts +9 -0
  65. package/dist/types/components/tool/hooks/use-editor.d.ts +609 -0
  66. package/dist/types/components/tool/hooks/use-engine.d.ts +9 -0
  67. package/dist/types/components/tool/hooks/use-history.d.ts +9 -0
  68. package/dist/types/components/tool/hooks/use-lab-actions.d.ts +25 -0
  69. package/dist/types/components/tool/hooks/use-lab-run.d.ts +25 -0
  70. package/dist/types/components/tool/hooks/use-lab.d.ts +18 -0
  71. package/dist/types/components/tool/hooks/use-lan-carry.d.ts +8 -0
  72. package/dist/types/components/tool/hooks/use-machine-actions.d.ts +26 -0
  73. package/dist/types/components/tool/hooks/use-placement.d.ts +27 -0
  74. package/dist/types/components/tool/hooks/use-project.d.ts +25 -0
  75. package/dist/types/components/tool/hooks/use-projects.d.ts +34 -0
  76. package/dist/types/components/tool/hooks/use-shared-link.d.ts +23 -0
  77. package/dist/types/components/tool/hooks/use-toast.d.ts +2 -0
  78. package/dist/types/components/tool/hooks/use-tools.d.ts +17 -0
  79. package/dist/types/components/tool/lab-canvas.d.ts +18 -0
  80. package/dist/types/components/tool/overlays.d.ts +5 -0
  81. package/dist/types/components/tool/projects-list.d.ts +9 -0
  82. package/dist/types/components/tool/text.d.ts +418 -0
  83. package/dist/types/components/tool/toolbar.d.ts +5 -0
  84. package/dist/types/components/tool/top-bars.d.ts +10 -0
  85. package/dist/types/components/tool/windows/conditions-fields.d.ts +6 -0
  86. package/dist/types/components/tool/windows/editor-windows.d.ts +5 -0
  87. package/dist/types/components/tool/windows/fields.d.ts +4 -0
  88. package/dist/types/components/tool/windows/lab-status.d.ts +6 -0
  89. package/dist/types/components/tool/windows/lab-windows.d.ts +13 -0
  90. package/dist/types/components/tool/windows/link-controls.d.ts +21 -0
  91. package/dist/types/components/tool/windows/machine-controls.d.ts +16 -0
  92. package/dist/types/components/tool/windows/machine-inspector.d.ts +11 -0
  93. package/dist/types/components/tool/windows/runs-fields.d.ts +18 -0
  94. package/dist/types/components/tool/windows/selected-windows.d.ts +21 -0
  95. package/dist/types/components/ui/button.d.ts +8 -0
  96. package/dist/types/components/ui/dropdown-menu.d.ts +21 -0
  97. package/dist/types/index.d.ts +7 -0
  98. package/dist/types/lib/auth.d.ts +38 -0
  99. package/dist/types/lib/brand.d.ts +19 -0
  100. package/dist/types/lib/i18n.d.ts +19 -0
  101. package/dist/types/lib/tool/blocks.d.ts +35 -0
  102. package/dist/types/lib/tool/cables.d.ts +48 -0
  103. package/dist/types/lib/tool/clone.d.ts +12 -0
  104. package/dist/types/lib/tool/conditions.d.ts +31 -0
  105. package/dist/types/lib/tool/create.d.ts +22 -0
  106. package/dist/types/lib/tool/desktop.d.ts +63 -0
  107. package/dist/types/lib/tool/draft.d.ts +20 -0
  108. package/dist/types/lib/tool/engine.d.ts +33 -0
  109. package/dist/types/lib/tool/examples.d.ts +11 -0
  110. package/dist/types/lib/tool/export-targets.d.ts +21 -0
  111. package/dist/types/lib/tool/facing.d.ts +9 -0
  112. package/dist/types/lib/tool/geometry.d.ts +36 -0
  113. package/dist/types/lib/tool/gns3-cables.d.ts +17 -0
  114. package/dist/types/lib/tool/gns3-import.d.ts +15 -0
  115. package/dist/types/lib/tool/gns3-parts.d.ts +25 -0
  116. package/dist/types/lib/tool/issues.d.ts +12 -0
  117. package/dist/types/lib/tool/layout.d.ts +52 -0
  118. package/dist/types/lib/tool/live.d.ts +21 -0
  119. package/dist/types/lib/tool/logos.d.ts +7 -0
  120. package/dist/types/lib/tool/model.d.ts +173 -0
  121. package/dist/types/lib/tool/os.d.ts +20 -0
  122. package/dist/types/lib/tool/pins.d.ts +5 -0
  123. package/dist/types/lib/tool/placement.d.ts +21 -0
  124. package/dist/types/lib/tool/ports.d.ts +21 -0
  125. package/dist/types/lib/tool/presets.d.ts +21 -0
  126. package/dist/types/lib/tool/projects-api.d.ts +37 -0
  127. package/dist/types/lib/tool/router-ports.d.ts +22 -0
  128. package/dist/types/lib/tool/selection.d.ts +19 -0
  129. package/dist/types/lib/tool/storage.d.ts +25 -0
  130. package/dist/types/lib/tool/switch-edit.d.ts +17 -0
  131. package/dist/types/lib/tool/text/de.d.ts +2 -0
  132. package/dist/types/lib/tool/text/en.d.ts +429 -0
  133. package/dist/types/lib/tool/text/es.d.ts +2 -0
  134. package/dist/types/lib/tool/text/fr.d.ts +2 -0
  135. package/dist/types/lib/tool/text/index.d.ts +419 -0
  136. package/dist/types/lib/tool/topology.d.ts +2 -0
  137. package/dist/types/lib/tool/vlans.d.ts +24 -0
  138. package/dist/types/lib/utils.d.ts +2 -0
  139. package/dist/types/model.d.ts +13 -0
  140. package/package.json +88 -4
  141. package/scripts/build-desktop-editor.mjs +43 -0
  142. package/scripts/build-lib.mjs +38 -0
  143. package/src/components/tool/canvas/cable-edge.tsx +112 -0
  144. package/src/components/tool/canvas/contexts.ts +18 -0
  145. package/src/components/tool/canvas/device-icon.tsx +105 -0
  146. package/src/components/tool/canvas/inline-text.tsx +69 -0
  147. package/src/components/tool/canvas/lan-node.tsx +148 -0
  148. package/src/components/tool/canvas/machine-addresses.tsx +117 -0
  149. package/src/components/tool/canvas/machine-node.tsx +114 -0
  150. package/src/components/tool/canvas/note-node.tsx +109 -0
  151. package/src/components/tool/canvas/port-led.tsx +21 -0
  152. package/src/components/tool/canvas/router-node.tsx +114 -0
  153. package/src/components/tool/canvas/sketch.tsx +130 -0
  154. package/src/components/tool/canvas/status-dot.tsx +29 -0
  155. package/src/components/tool/canvas/switch-node.tsx +143 -0
  156. package/src/components/tool/canvas/use-measured-ports.ts +25 -0
  157. package/src/components/tool/context-menus.tsx +142 -0
  158. package/src/components/tool/device-block.tsx +107 -0
  159. package/src/components/tool/editor-menu.tsx +191 -0
  160. package/src/components/tool/export/copy-button.tsx +45 -0
  161. package/src/components/tool/export/export-panel.tsx +127 -0
  162. package/src/components/tool/export/target-list.tsx +87 -0
  163. package/src/components/tool/floating-window.tsx +69 -0
  164. package/src/components/tool/flow-canvas.tsx +138 -0
  165. package/src/components/tool/hooks/use-cabling.ts +66 -0
  166. package/src/components/tool/hooks/use-captures.ts +24 -0
  167. package/src/components/tool/hooks/use-clipboard.ts +54 -0
  168. package/src/components/tool/hooks/use-context-menus.ts +37 -0
  169. package/src/components/tool/hooks/use-design.ts +24 -0
  170. package/src/components/tool/hooks/use-desktop.ts +100 -0
  171. package/src/components/tool/hooks/use-devices.ts +58 -0
  172. package/src/components/tool/hooks/use-downloads.ts +42 -0
  173. package/src/components/tool/hooks/use-editor.ts +114 -0
  174. package/src/components/tool/hooks/use-engine.ts +29 -0
  175. package/src/components/tool/hooks/use-history.ts +107 -0
  176. package/src/components/tool/hooks/use-lab-actions.ts +57 -0
  177. package/src/components/tool/hooks/use-lab-run.ts +61 -0
  178. package/src/components/tool/hooks/use-lab.ts +32 -0
  179. package/src/components/tool/hooks/use-lan-carry.ts +43 -0
  180. package/src/components/tool/hooks/use-machine-actions.ts +57 -0
  181. package/src/components/tool/hooks/use-placement.ts +80 -0
  182. package/src/components/tool/hooks/use-project.ts +112 -0
  183. package/src/components/tool/hooks/use-projects.ts +85 -0
  184. package/src/components/tool/hooks/use-shared-link.ts +64 -0
  185. package/src/components/tool/hooks/use-toast.ts +16 -0
  186. package/src/components/tool/hooks/use-tools.ts +60 -0
  187. package/src/components/tool/lab-canvas.tsx +96 -0
  188. package/src/components/tool/overlays.tsx +104 -0
  189. package/src/components/tool/projects-list.tsx +96 -0
  190. package/src/components/tool/text.ts +9 -0
  191. package/src/components/tool/toolbar.tsx +106 -0
  192. package/src/components/tool/top-bars.tsx +90 -0
  193. package/src/components/tool/windows/conditions-fields.tsx +43 -0
  194. package/src/components/tool/windows/editor-windows.tsx +73 -0
  195. package/src/components/tool/windows/fields.ts +6 -0
  196. package/src/components/tool/windows/lab-status.tsx +68 -0
  197. package/src/components/tool/windows/lab-windows.tsx +130 -0
  198. package/src/components/tool/windows/link-controls.tsx +69 -0
  199. package/src/components/tool/windows/machine-controls.tsx +116 -0
  200. package/src/components/tool/windows/machine-inspector.tsx +83 -0
  201. package/src/components/tool/windows/runs-fields.tsx +127 -0
  202. package/src/components/tool/windows/selected-windows.tsx +147 -0
  203. package/src/components/ui/button.tsx +59 -0
  204. package/src/components/ui/dropdown-menu.tsx +185 -0
  205. package/src/index.ts +21 -0
  206. package/src/lib/auth.tsx +43 -0
  207. package/src/lib/brand.tsx +46 -0
  208. package/src/lib/i18n.ts +53 -0
  209. package/src/lib/tool/blocks.test.ts +64 -0
  210. package/src/lib/tool/blocks.ts +78 -0
  211. package/src/lib/tool/cable-wiring.test.ts +74 -0
  212. package/src/lib/tool/cables.test.ts +75 -0
  213. package/src/lib/tool/cables.ts +215 -0
  214. package/src/lib/tool/clone.test.ts +54 -0
  215. package/src/lib/tool/clone.ts +48 -0
  216. package/src/lib/tool/conditions.test.ts +170 -0
  217. package/src/lib/tool/conditions.ts +132 -0
  218. package/src/lib/tool/create.test.ts +67 -0
  219. package/src/lib/tool/create.ts +83 -0
  220. package/src/lib/tool/desktop.test.ts +59 -0
  221. package/src/lib/tool/desktop.ts +99 -0
  222. package/src/lib/tool/draft.test.ts +150 -0
  223. package/src/lib/tool/draft.ts +109 -0
  224. package/src/lib/tool/engine.test.ts +39 -0
  225. package/src/lib/tool/engine.ts +58 -0
  226. package/src/lib/tool/examples.test.ts +35 -0
  227. package/src/lib/tool/examples.ts +71 -0
  228. package/src/lib/tool/export-targets.test.ts +47 -0
  229. package/src/lib/tool/export-targets.ts +84 -0
  230. package/src/lib/tool/facing.test.ts +72 -0
  231. package/src/lib/tool/facing.ts +100 -0
  232. package/src/lib/tool/geometry.test.ts +80 -0
  233. package/src/lib/tool/geometry.ts +109 -0
  234. package/src/lib/tool/gns3-cables.test.ts +47 -0
  235. package/src/lib/tool/gns3-cables.ts +81 -0
  236. package/src/lib/tool/gns3-import.test.ts +102 -0
  237. package/src/lib/tool/gns3-import.ts +198 -0
  238. package/src/lib/tool/gns3-layout.test.ts +62 -0
  239. package/src/lib/tool/gns3-parts.test.ts +47 -0
  240. package/src/lib/tool/gns3-parts.ts +68 -0
  241. package/src/lib/tool/issues.test.ts +35 -0
  242. package/src/lib/tool/issues.ts +60 -0
  243. package/src/lib/tool/layout.test.ts +128 -0
  244. package/src/lib/tool/layout.ts +229 -0
  245. package/src/lib/tool/live.test.ts +46 -0
  246. package/src/lib/tool/live.ts +49 -0
  247. package/src/lib/tool/logos.test.ts +30 -0
  248. package/src/lib/tool/logos.ts +99 -0
  249. package/src/lib/tool/model.test.ts +128 -0
  250. package/src/lib/tool/model.ts +238 -0
  251. package/src/lib/tool/os.test.ts +45 -0
  252. package/src/lib/tool/os.ts +57 -0
  253. package/src/lib/tool/parity.test.ts +145 -0
  254. package/src/lib/tool/pins.test.ts +24 -0
  255. package/src/lib/tool/pins.ts +11 -0
  256. package/src/lib/tool/placement.test.ts +79 -0
  257. package/src/lib/tool/placement.ts +60 -0
  258. package/src/lib/tool/ports.test.ts +39 -0
  259. package/src/lib/tool/ports.ts +48 -0
  260. package/src/lib/tool/presets.test.ts +50 -0
  261. package/src/lib/tool/presets.ts +77 -0
  262. package/src/lib/tool/projects-api.test.ts +59 -0
  263. package/src/lib/tool/projects-api.ts +47 -0
  264. package/src/lib/tool/router-ports.test.ts +63 -0
  265. package/src/lib/tool/router-ports.ts +88 -0
  266. package/src/lib/tool/selection.test.ts +44 -0
  267. package/src/lib/tool/selection.ts +34 -0
  268. package/src/lib/tool/storage.test.ts +123 -0
  269. package/src/lib/tool/storage.ts +127 -0
  270. package/src/lib/tool/switch-edit.test.ts +49 -0
  271. package/src/lib/tool/switch-edit.ts +38 -0
  272. package/src/lib/tool/text/de.ts +358 -0
  273. package/src/lib/tool/text/en.ts +371 -0
  274. package/src/lib/tool/text/es.ts +358 -0
  275. package/src/lib/tool/text/fr.ts +358 -0
  276. package/src/lib/tool/text/index.ts +21 -0
  277. package/src/lib/tool/text/text.test.ts +33 -0
  278. package/src/lib/tool/topology.test.ts +108 -0
  279. package/src/lib/tool/topology.ts +116 -0
  280. package/src/lib/tool/vlans.test.ts +64 -0
  281. package/src/lib/tool/vlans.ts +79 -0
  282. package/src/lib/utils.ts +6 -0
  283. package/src/model.ts +13 -0
  284. package/src/test/arbitrary.ts +41 -0
  285. package/src/test/engine.ts +48 -0
  286. package/src/test/lab.ts +101 -0
  287. package/styles/globals.css +258 -0
  288. package/styles/theme.css +192 -0
  289. package/tsconfig.build.json +12 -0
  290. package/tsconfig.json +22 -0
  291. package/wasm/isothrum_wasm.d.ts +60 -0
  292. package/wasm/isothrum_wasm.js +312 -0
  293. package/wasm/isothrum_wasm_bg.wasm +0 -0
@@ -0,0 +1,44 @@
1
+ import { describe, expect, test } from "vitest";
2
+ import type { Plan } from "./engine";
3
+ import { labIssues, nameById, selectionOf } from "./selection";
4
+ import { cable, issueText, lan, machine, note } from "@/test/lab";
5
+
6
+ const selected = <T extends object>(x: T): T => ({ ...x, selected: true });
7
+
8
+ describe("selectionOf", () => {
9
+ test("one machine, LAN or cable selected opens its window", () => {
10
+ expect(selectionOf([selected(machine("m")), lan("l")], []).machine?.id).toBe("m");
11
+ expect(selectionOf([machine("m"), selected(lan("l"))], []).lan?.id).toBe("l");
12
+ expect(selectionOf([machine("m")], [selected(cable("c", "m", "l"))]).edge?.id).toBe("c");
13
+ });
14
+
15
+ test("several selected open none of them, and are counted", () => {
16
+ const s = selectionOf([selected(machine("m")), selected(lan("l"))], [selected(cable("c", "m", "l"))]);
17
+ expect(s).toMatchObject({ count: 3, machine: undefined, lan: undefined, edge: undefined });
18
+ });
19
+
20
+ test("a selected note is counted but opens no window", () => {
21
+ expect(selectionOf([selected(note("n"))], [])).toMatchObject({ count: 1, machine: undefined, lan: undefined });
22
+ });
23
+ });
24
+
25
+ describe("labIssues", () => {
26
+ const plan = (p: Partial<Plan>): Plan => ({ ok: true, error: null, isoloom_yml: null, problems: [], files: [], skipped: [], addresses: {}, ...p });
27
+
28
+ test("an empty canvas has nothing to fix yet", () => {
29
+ expect(labIssues([], plan({ ok: false, error: "empty" }), issueText)).toEqual([]);
30
+ });
31
+
32
+ test("lists the canvas's findings, then the engine's error and problems", () => {
33
+ const nodes = [machine("a", { name: "" })];
34
+ expect(labIssues(nodes, plan({ ok: false, error: "bad lab", problems: ["p1"] }), issueText)).toEqual(["unnamed machine", "bad lab", "p1"]);
35
+ });
36
+
37
+ test("lists the canvas's findings alone before the engine has run", () => {
38
+ expect(labIssues([machine("a", { name: "" })], null, issueText)).toEqual(["unnamed machine"]);
39
+ });
40
+ });
41
+
42
+ test("nameById shows a node's name, or ? for one that is gone", () => {
43
+ expect([nameById([machine("m", { name: "web" })], "m"), nameById([], "m")]).toEqual(["web", "?"]);
44
+ });
@@ -0,0 +1,34 @@
1
+ import { canvasIssues, type IssueText } from "./issues";
2
+ import type { Plan } from "./engine";
3
+ import { isLan, isMachine, nodeName, type CableEdge, type LabNode } from "./model";
4
+
5
+ /** What is selected: one thing opens its window; several open the "selection" window instead. */
6
+ export function selectionOf(nodes: LabNode[], edges: CableEdge[]) {
7
+ const selectedNodes = nodes.filter((n) => n.selected);
8
+ const selectedEdges = edges.filter((e) => e.selected);
9
+ const count = selectedNodes.length + selectedEdges.length;
10
+ const only = count === 1 ? selectedNodes.at(0) : undefined;
11
+ return {
12
+ selectedNodes,
13
+ selectedEdges,
14
+ count,
15
+ machine: only && isMachine(only) ? only : undefined,
16
+ lan: only && isLan(only) ? only : undefined,
17
+ edge: count === 1 ? selectedEdges.at(0) : undefined,
18
+ };
19
+ }
20
+
21
+ /**
22
+ * What to fix before the lab runs: the canvas's own findings, then the engine's. An empty canvas
23
+ * isn't a lab with problems yet: nothing until there is something to check.
24
+ */
25
+ export function labIssues(nodes: LabNode[], plan: Plan | null, text: IssueText): string[] {
26
+ if (nodes.length === 0) return [];
27
+ return [...canvasIssues(nodes, text), ...(plan && !plan.ok && plan.error ? [plan.error] : []), ...(plan?.problems ?? [])];
28
+ }
29
+
30
+ /** A node's name by its id, as the windows show it ("?" for one that is gone). */
31
+ export function nameById(nodes: LabNode[], id: string): string {
32
+ const n = nodes.find((x) => x.id === id);
33
+ return n ? nodeName(n) : "?";
34
+ }
@@ -0,0 +1,123 @@
1
+ import { describe, expect, test, vi } from "vitest";
2
+ import { LAN_BEHAVIOR } from "./model";
3
+ import { PRESETS } from "./presets";
4
+ import {
5
+ autoStarts,
6
+ loadBlockChoices,
7
+ loadDesign,
8
+ loadDevices,
9
+ loadDraft,
10
+ loadProject,
11
+ saveBlockChoices,
12
+ saveDesign,
13
+ saveDevices,
14
+ saveDraft,
15
+ saveProject,
16
+ setAutoStarts,
17
+ } from "./storage";
18
+ import { cable, lan, machine } from "@/test/lab";
19
+
20
+ /** This browser's storage, faked: what it holds, by key. */
21
+ function useStorage(entries: Record<string, string> = {}): Map<string, string> {
22
+ const held = new Map(Object.entries(entries));
23
+ vi.stubGlobal("localStorage", {
24
+ getItem: (k: string) => held.get(k) ?? null,
25
+ setItem: (k: string, v: string) => held.set(k, v),
26
+ removeItem: (k: string) => held.delete(k),
27
+ });
28
+ return held;
29
+ }
30
+
31
+ /** Storage a private window blocks: every call throws. */
32
+ function useBlockedStorage(): void {
33
+ const blocked = () => {
34
+ throw new Error("blocked");
35
+ };
36
+ vi.stubGlobal("localStorage", { getItem: blocked, setItem: blocked, removeItem: blocked });
37
+ }
38
+
39
+ describe("the draft", () => {
40
+ test("keeps the lab without its UI state, and reads it back current", () => {
41
+ const held = useStorage();
42
+ saveDraft({ name: "lab", nodes: [{ ...lan("office"), selected: true }, machine("web")], edges: [cable("c", "web", "office")], design: "business" });
43
+ expect(JSON.parse(held.get("isothrum-canvas-v1") ?? "")).not.toHaveProperty("design");
44
+ expect(loadDraft()?.nodes[0]).toMatchObject({ ...LAN_BEHAVIOR, id: "office" });
45
+ expect(loadDraft()?.nodes[0]).not.toHaveProperty("selected");
46
+ });
47
+
48
+ test("is nothing when there is none, it isn't JSON or it hasn't a lab's shape", () => {
49
+ useStorage();
50
+ expect(loadDraft()).toBeNull();
51
+ useStorage({ "isothrum-canvas-v1": "{" });
52
+ expect(loadDraft()).toBeNull();
53
+ useStorage({ "isothrum-canvas-v1": JSON.stringify({ nodes: [null], edges: [] }) });
54
+ expect(loadDraft()).toBeNull();
55
+ });
56
+ });
57
+
58
+ describe("saved devices", () => {
59
+ test("come back as they were saved", () => {
60
+ useStorage();
61
+ saveDevices([PRESETS[0]]);
62
+ expect(loadDevices()).toEqual([PRESETS[0]]);
63
+ });
64
+
65
+ test("come back with their OS as Isoloom names it now", () => {
66
+ const old = { ...PRESETS[0], data: { ...PRESETS[0].data, kind: "vm", os: "windows-server-2022" } };
67
+ useStorage({ "isothrum-devices-v1": JSON.stringify([old]) });
68
+ expect(loadDevices()[0].data.os).toBe("windows-server@2022");
69
+ });
70
+
71
+ test("leave out what can't be placed, and anything that isn't a list", () => {
72
+ useStorage({ "isothrum-devices-v1": JSON.stringify([PRESETS[1], { id: "x" }, null]) });
73
+ expect(loadDevices()).toEqual([PRESETS[1]]);
74
+ useStorage({ "isothrum-devices-v1": "{}" });
75
+ expect(loadDevices()).toEqual([]);
76
+ });
77
+ });
78
+
79
+ describe("the look and the toolbar", () => {
80
+ test("keep the canvas's look, and only a known one", () => {
81
+ useStorage();
82
+ saveDesign("business");
83
+ expect(loadDesign()).toBe("business");
84
+ useStorage({ "isothrum-design": "neon" });
85
+ expect(loadDesign()).toBeNull();
86
+ });
87
+
88
+ test("keep the kind each toolbar block places", () => {
89
+ useStorage();
90
+ expect(loadBlockChoices()).toEqual({});
91
+ saveBlockChoices({ router: "router-frr" });
92
+ expect(loadBlockChoices()).toEqual({ router: "router-frr" });
93
+ });
94
+ });
95
+
96
+ describe("auto-start and the attached project", () => {
97
+ test("a lab starts with the app once it is set to, until it is unset", () => {
98
+ useStorage();
99
+ setAutoStarts("lab-a", true);
100
+ setAutoStarts("lab-a", true);
101
+ expect(autoStarts("lab-a")).toBe(true);
102
+ setAutoStarts("lab-a", false);
103
+ expect(autoStarts("lab-a")).toBe(false);
104
+ });
105
+
106
+ test("keeps the attached project, and forgets it when detached", () => {
107
+ const held = useStorage();
108
+ saveProject({ id: "p1", name: "office" });
109
+ expect(loadProject()).toEqual({ id: "p1", name: "office" });
110
+ saveProject(null);
111
+ expect(held.has("isothrum-project")).toBe(false);
112
+ });
113
+ });
114
+
115
+ test("blocked storage keeps nothing and reads nothing, without failing", () => {
116
+ useBlockedStorage();
117
+ expect(() => {
118
+ saveDraft({ name: "lab", nodes: [], edges: [] });
119
+ saveDesign("sketch");
120
+ setAutoStarts("lab", true);
121
+ }).not.toThrow();
122
+ expect([loadDraft(), loadDesign(), loadBlockChoices(), loadProject(), loadDevices()]).toEqual([null, null, null, null, []]);
123
+ });
@@ -0,0 +1,127 @@
1
+ import { isDesign, normalizeDraft, storedEdges, storedNodes } from "./draft";
2
+ import type { Design, Draft } from "./model";
3
+ import { currentMachine } from "./os";
4
+ import type { Preset } from "./presets";
5
+
6
+ /**
7
+ * What the editor keeps in this browser (localStorage). Storage can be blocked (a private
8
+ * window) or full: then reading gives nothing and writing keeps nothing, and the editor works on.
9
+ */
10
+
11
+ const DRAFT_KEY = "isothrum-canvas-v1";
12
+ const DEVICES_KEY = "isothrum-devices-v1";
13
+ const DESIGN_KEY = "isothrum-design";
14
+ const BLOCKS_KEY = "isothrum-toolbar-blocks";
15
+ const AUTO_START_KEY = "isothrum-auto-start";
16
+ const PROJECT_KEY = "isothrum-project";
17
+
18
+ /** The stored value at `key`, parsed; null when there is none or it can't be read. */
19
+ function read(key: string): unknown {
20
+ try {
21
+ const raw = localStorage.getItem(key);
22
+ return raw === null ? null : (JSON.parse(raw) as unknown);
23
+ } catch {
24
+ return null;
25
+ }
26
+ }
27
+
28
+ /** Stores `value` at `key` as JSON, or removes it when null. */
29
+ function write(key: string, value: unknown): void {
30
+ try {
31
+ if (value === null) localStorage.removeItem(key);
32
+ else localStorage.setItem(key, JSON.stringify(value));
33
+ } catch {
34
+ // Not kept: it lasts as long as the page.
35
+ }
36
+ }
37
+
38
+ /** The last lab drawn in this browser; none (a blank canvas) when what is kept hasn't a lab's shape. */
39
+ export function loadDraft(): Draft | null {
40
+ return normalizeDraft(read(DRAFT_KEY));
41
+ }
42
+
43
+ /** Keeps the lab, without its UI state (selection is not part of the lab). */
44
+ export function saveDraft(draft: Draft): void {
45
+ write(DRAFT_KEY, { name: draft.name, nodes: storedNodes(draft.nodes), edges: storedEdges(draft.edges) });
46
+ }
47
+
48
+ /** Whether a stored device has what placing it needs: an id, its machine, a name prefix. */
49
+ function isPreset(p: unknown): p is Preset {
50
+ if (typeof p !== "object" || p === null) return false;
51
+ const { id, data, prefix } = p as Partial<Record<keyof Preset, unknown>>;
52
+ return Boolean(id) && Boolean(data) && Boolean(prefix);
53
+ }
54
+
55
+ /** Devices saved from the canvas (GNS3's appliances), placed from the toolbar like the built-in ones. */
56
+ export function loadDevices(): Preset[] {
57
+ const list = read(DEVICES_KEY);
58
+ // Saved before Isoloom's new format: OS names as it writes them now.
59
+ return Array.isArray(list) ? list.filter(isPreset).map((p) => ({ ...p, data: currentMachine(p.data) })) : [];
60
+ }
61
+
62
+ export function saveDevices(list: Preset[]): void {
63
+ write(DEVICES_KEY, list);
64
+ }
65
+
66
+ /** The canvas's look picked in this browser, if any. */
67
+ export function loadDesign(): Design | null {
68
+ try {
69
+ const saved = localStorage.getItem(DESIGN_KEY);
70
+ return isDesign(saved) ? saved : null;
71
+ } catch {
72
+ return null;
73
+ }
74
+ }
75
+
76
+ export function saveDesign(design: Design): void {
77
+ try {
78
+ localStorage.setItem(DESIGN_KEY, design);
79
+ } catch {
80
+ // The choice lasts as long as the page.
81
+ }
82
+ }
83
+
84
+ /** The kind each toolbar block places, by block id, as picked last. */
85
+ export function loadBlockChoices(): Record<string, string> | null {
86
+ try {
87
+ const saved: unknown = JSON.parse(localStorage.getItem(BLOCKS_KEY) ?? "{}");
88
+ return saved && typeof saved === "object" ? (saved as Record<string, string>) : null;
89
+ } catch {
90
+ // The first kind of each block.
91
+ return null;
92
+ }
93
+ }
94
+
95
+ export function saveBlockChoices(choices: Record<string, string>): void {
96
+ write(BLOCKS_KEY, choices);
97
+ }
98
+
99
+ /** The labs that start when the desktop app opens on them (GNS3's auto-start). */
100
+ function autoStartLabs(): string[] {
101
+ const list = read(AUTO_START_KEY);
102
+ return Array.isArray(list) ? list.filter((x): x is string => typeof x === "string") : [];
103
+ }
104
+
105
+ /** Whether `lab` starts when the app opens on it. */
106
+ export function autoStarts(lab: string): boolean {
107
+ return autoStartLabs().includes(lab);
108
+ }
109
+
110
+ export function setAutoStarts(lab: string, on: boolean): void {
111
+ const others = autoStartLabs().filter((l) => l !== lab);
112
+ write(AUTO_START_KEY, on ? [...others, lab] : others);
113
+ }
114
+
115
+ /** The saved project the editor is attached to. */
116
+ export interface ProjectRef {
117
+ id: string;
118
+ name: string;
119
+ }
120
+
121
+ export function loadProject(): ProjectRef | null {
122
+ return read(PROJECT_KEY) as ProjectRef | null;
123
+ }
124
+
125
+ export function saveProject(p: ProjectRef | null): void {
126
+ write(PROJECT_KEY, p);
127
+ }
@@ -0,0 +1,49 @@
1
+ import { describe, expect, test } from "vitest";
2
+ import { addVlan, changeVlan, cyclePort, removeVlan, vlanNumber } from "./switch-edit";
3
+ import { switchNode } from "@/test/lab";
4
+
5
+ /** vlan10 on ports 1 and 2, vlan20 on port 3, port 4 a trunk, the rest unassigned. */
6
+ const data = switchNode("sw1").data;
7
+
8
+ test("cyclePort steps a port through each VLAN, then trunk, then unassigned", () => {
9
+ expect([0, 2, 3, 4].map((i) => cyclePort(data, i).ports[i])).toEqual([20, "trunk", null, 10]);
10
+ });
11
+
12
+ describe("VLANs", () => {
13
+ test("a renumbered VLAN moves its ports along", () => {
14
+ expect(changeVlan(data, 10, { id: 30 })).toEqual({
15
+ vlans: [
16
+ { id: 30, cidr: "10.10.10.0/24" },
17
+ { id: 20, cidr: "10.10.20.0/24" },
18
+ ],
19
+ ports: [30, 30, 20, "trunk", null, null, null, null],
20
+ });
21
+ });
22
+
23
+ test("a VLAN's new block leaves its ports", () => {
24
+ expect(changeVlan(data, 20, { cidr: "10.10.99.0/24" }).ports).toBe(data.ports);
25
+ });
26
+
27
+ test("a removed VLAN leaves its ports unassigned", () => {
28
+ expect(removeVlan(data, 10)).toEqual({ vlans: [{ id: 20, cidr: "10.10.20.0/24" }], ports: [null, null, 20, "trunk", null, null, null, null] });
29
+ });
30
+
31
+ test("a new VLAN is numbered 10 after the highest, with its own block", () => {
32
+ expect(addVlan(data).vlans.at(-1)).toEqual({ id: 30, cidr: "10.10.30.0/24" });
33
+ expect(addVlan({ ...data, vlans: [] }).vlans).toEqual([{ id: 10, cidr: "10.10.10.0/24" }]);
34
+ });
35
+
36
+ test("a VLAN numbered past 255 gets a block outside 10.10", () => {
37
+ expect(addVlan({ ...data, vlans: [{ id: 250, cidr: "x" }] }).vlans.at(-1)).toEqual({ id: 260, cidr: "10.11.0.0/24" });
38
+ });
39
+ });
40
+
41
+ describe("vlanNumber", () => {
42
+ test("takes a number from 1 to 4094", () => {
43
+ expect(["1", "30", "4094"].map((t) => vlanNumber(data, t))).toEqual([1, 30, 4094]);
44
+ });
45
+
46
+ test("refuses one out of range, not a number, or one the switch has", () => {
47
+ expect(["0", "4095", "abc", "10"].map((t) => vlanNumber(data, t))).toEqual([null, null, null, null]);
48
+ });
49
+ });
@@ -0,0 +1,38 @@
1
+ import { TRUNK, type PortMode, type SwitchNode } from "./model";
2
+
3
+ /** Editing a switch on the canvas: each change returns the switch's fields that change. */
4
+
5
+ type SwitchData = SwitchNode["data"];
6
+
7
+ /** A click steps a port through each VLAN, then trunk, then unassigned. */
8
+ export function cyclePort(data: SwitchData, i: number): Pick<SwitchData, "ports"> {
9
+ const order: PortMode[] = [...data.vlans.map((v) => v.id), TRUNK, null];
10
+ const next = order[(order.indexOf(data.ports[i]) + 1) % order.length];
11
+ return { ports: data.ports.map((p, j) => (j === i ? next : p)) };
12
+ }
13
+
14
+ /** A VLAN's number or block changed; a new number moves its ports along. */
15
+ export function changeVlan(data: SwitchData, old: number, patch: { id?: number; cidr?: string }): Pick<SwitchData, "vlans" | "ports"> {
16
+ const renumbered = patch.id;
17
+ return {
18
+ vlans: data.vlans.map((v) => (v.id === old ? { ...v, ...patch } : v)),
19
+ ports: renumbered === undefined ? data.ports : data.ports.map((p) => (p === old ? renumbered : p)),
20
+ };
21
+ }
22
+
23
+ /** A VLAN removed: its ports are left unassigned. */
24
+ export function removeVlan(data: SwitchData, vid: number): Pick<SwitchData, "vlans" | "ports"> {
25
+ return { vlans: data.vlans.filter((v) => v.id !== vid), ports: data.ports.map((p) => (p === vid ? null : p)) };
26
+ }
27
+
28
+ /** A new VLAN, numbered 10 after the highest, with its own block. */
29
+ export function addVlan(data: SwitchData): Pick<SwitchData, "vlans"> {
30
+ const next = Math.max(0, ...data.vlans.map((v) => v.id)) + 10;
31
+ return { vlans: [...data.vlans, { id: next, cidr: next <= 255 ? `10.10.${String(next)}.0/24` : "10.11.0.0/24" }] };
32
+ }
33
+
34
+ /** A typed VLAN number, if it can be one: 1–4094, and not one the switch has already. */
35
+ export function vlanNumber(data: SwitchData, typed: string): number | null {
36
+ const n = Number(typed);
37
+ return n > 0 && n < 4095 && !data.vlans.some((v) => v.id === n) ? n : null;
38
+ }