@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,358 @@
1
+ import type { EditorText } from "./en";
2
+
3
+ export const fr: EditorText = {
4
+ toolbar: {
5
+ blocks: { switch: "Switch", router: "Routeur", firewall: "Pare-feu", container: "Conteneur", vm: "Machine virtuelle", mine: "Mes équipements" },
6
+ kindsOf: (block) => `Types : ${block.toLowerCase()}`,
7
+ devices: "Appareils",
8
+ hand: "Main : déplacer le canevas",
9
+ select: "Sélection : sélectionner, déplacer, supprimer",
10
+ lan: "LAN",
11
+ lanTitle: "LAN / VLAN : une zone ; les machines à l'intérieur y reçoivent une adresse",
12
+ switch: "Switch",
13
+ switchTitle: "Switch : des ports dans des VLAN, et un trunk",
14
+ note: "Note",
15
+ noteTitle: "Note : du texte, un cadre ou une ellipse, pour expliquer le lab",
16
+ forget: (device) => `Oublier ${device}`,
17
+ forgetLabel: (device) => `Oublier l'appareil ${device}`,
18
+ undo: "Annuler",
19
+ undoTitle: "Annuler · Ctrl/⌘ Z",
20
+ redo: "Rétablir",
21
+ redoTitle: "Rétablir · Ctrl/⌘ Maj Z",
22
+ },
23
+ hints: {
24
+ lan: "Cliquez sur le canevas pour placer un LAN ; les machines à l'intérieur y reçoivent une adresse. Double-cliquez sur son nom ou sa plage pour les modifier.",
25
+ note: "Cliquez sur le canevas pour placer une note, puis double-cliquez dessus pour écrire. Les notes restent dans l'éditeur, pas dans isoloom.yml.",
26
+ place: (device) => `${device} : cliquez sur le canevas pour l'ajouter. Échap pour annuler.`,
27
+ hand: "Faites glisser pour déplacer le canevas. Appuyez sur V pour revenir à la sélection.",
28
+ cable: "Tirez d'un point d'une machine vers une autre pour les câbler.",
29
+ select: "Faites glisser sur le canevas pour sélectionner plusieurs machines : Suppr, ou Ctrl/⌘ C, V, D. Maintenez Espace pour déplacer le canevas.",
30
+ },
31
+ devices: {
32
+ router: { label: "Routeur", hint: "automatique : ses réseaux communiquent entre eux" },
33
+ "router-frr": { label: "Routeur (FRR)", hint: "conteneur FRRouting que vous configurez" },
34
+ firewall: { label: "Pare-feu", hint: "nftables sur Alpine" },
35
+ container: { label: "Conteneur Linux" },
36
+ web: { label: "Serveur web", hint: "nginx, port 80" },
37
+ database: { label: "Base de données", hint: "PostgreSQL, port 5432" },
38
+ desktop: { label: "Bureau Linux", hint: "Xfce avec VNC : clic droit, Écran ; mot de passe vncpassword" },
39
+ "linux-vm": { label: "VM Linux" },
40
+ "ubuntu-vm": { label: "VM Ubuntu" },
41
+ "kali-vm": { label: "VM Kali" },
42
+ "windows-vm": { label: "VM Windows" },
43
+ workstation: { label: "Poste de travail", hint: "là où arrive l'utilisateur" },
44
+ "cisco-iol": { label: "Routeur Cisco IOL", hint: "IOS sur Linux, votre image vrnetlab" },
45
+ "cisco-iol-l2": { label: "Switch Cisco IOL", hint: "IOL-L2, votre image vrnetlab" },
46
+ },
47
+ myDevice: (what) => `votre appareil : ${what}`,
48
+ myDeviceRouter: "routeur automatique",
49
+ status: {
50
+ loading: "Chargement…",
51
+ toFix: (count) => `${String(count)} à corriger`,
52
+ valid: "Valide",
53
+ },
54
+ run: {
55
+ autoStart: "Lancer ce lab à l’ouverture de l’application",
56
+ autoStartNote: "Sur cet ordinateur, quand l’application s’ouvre sur ce lab et qu’il ne tourne pas déjà.",
57
+ statusTitle: (up, total) => `${String(up)} machines sur ${String(total)} en marche`,
58
+ probed: "ports lus sur les machines",
59
+ inferred: "ports d’après l’état des machines (les VM ne sont pas sondées)",
60
+ run: "Lancer",
61
+ starting: "Démarrage…",
62
+ stop: "Arrêter",
63
+ stopping: "Arrêt…",
64
+ vmsTitle: "Un lab avec des VM tourne avec Vagrant (et VirtualBox ou libvirt) : installez-le depuis vagrantup.com, ou exportez le lab",
65
+ runVmsTitle: "Lancer le lab sur cette machine (VM avec Vagrant)",
66
+ startingVmsLog: "Démarrage des VM avec vagrant up (la première fois télécharge leurs box : quelques minutes)…",
67
+ runTitle: "Lancer le lab sur cette machine (Docker)",
68
+ startingLog: "Démarrage du lab avec docker compose…",
69
+ window: "Lancer sur cette machine",
70
+ running: "Le lab tourne. Sélectionnez un LAN ou un câble pour capturer son trafic.",
71
+ stopped: "Le lab est arrêté.",
72
+ noDocker:
73
+ "Docker ne répond pas. Installez-le et démarrez-le ; sous Linux, ajoutez-vous aussi au groupe docker (sudo usermod -aG docker $USER, puis reconnectez-vous).",
74
+ noLiveCapture: (wireshark, dir) =>
75
+ `${
76
+ wireshark
77
+ ? "Wireshark ne peut pas capturer en direct depuis ce compte : lancez sudo usermod -aG wireshark $USER, puis reconnectez-vous. En attendant, les captures"
78
+ : "Wireshark n'est pas installé : les captures"
79
+ } sont enregistrées en fichiers .pcap dans ${dir}.`,
80
+ },
81
+ capture: {
82
+ capturing: (network) => `Capture de ${network}`,
83
+ writingTo: "Écriture dans",
84
+ live: "En direct dans Wireshark.",
85
+ stop: "Arrêter la capture",
86
+ start: "Capturer le trafic",
87
+ runFirst: "Lancez le lab pour capturer son trafic.",
88
+ },
89
+ link: {
90
+ suspend: "Suspendre le lien",
91
+ resume: "Rétablir le lien",
92
+ suspendAll: "Suspendre tous les liens",
93
+ resumeAll: "Rétablir tous les liens",
94
+ cableSuspended: "Lien suspendu : rien ne passe.",
95
+ lanSuspended: (ports) => (ports === 1 ? "1 lien suspendu." : `${String(ports)} liens suspendus.`),
96
+ runFirst: "Lancez le lab pour suspendre ses liens.",
97
+ },
98
+ export: "Exporter",
99
+ engineDidntLoad: (error) => `Le moteur ne s'est pas chargé : ${error}`,
100
+ engineFailed: (error) => `Le moteur a échoué sur ce lab : ${error}`,
101
+ toasts: {
102
+ brokenLink: "Ce lien de partage est abîmé : le lab n'a pas pu être lu.",
103
+ openedShared: (lab) => `Lab partagé « ${lab} » ouvert. Annuler ramène le vôtre.`,
104
+ linkCopied: "Lien de partage copié : toute personne qui l'ouvre reçoit une copie de ce lab.",
105
+ copyPrompt: "Copiez ce lien pour partager le lab :",
106
+ savedDevice: (device) => `« ${device} » enregistré : il est au bout de la barre d'outils.`,
107
+ gns3Opened: (lab, machines, lans) =>
108
+ `« ${lab} » ouvert depuis GNS3 : ${String(machines)} machine${machines > 1 ? "s" : ""}, ${String(lans)} LAN.`,
109
+ undoBringsBack: "Annuler ramène votre lab.",
110
+ gns3Unreadable: "Ce projet GNS3 n'a pas pu être lu.",
111
+ },
112
+ windows: {
113
+ machine: "Machine",
114
+ lan: (lan) => `LAN ${lan}`,
115
+ cable: (a, b) => `Câble ${a} ↔ ${b}`,
116
+ portOn: (machine) => `Port sur ${machine}`,
117
+ cableNote: "Chaque câble est son propre petit réseau (un /29) ; ses deux extrémités en reçoivent les deux premières adresses.",
118
+ removeCable: "Supprimer le câble",
119
+ conditions: {
120
+ title: "Conditions du lien",
121
+ delay: "Latence (ms, dans chaque sens)",
122
+ jitter: "Gigue (ms)",
123
+ loss: "Perte (%)",
124
+ rate: "Débit (Mbit/s)",
125
+ hint: "Un lien lent ou qui perd des paquets, appliqué avec tc netem à chaque extrémité. Vide : aucune.",
126
+ onLan: "Ce câble place la machine sur le LAN : réglez les conditions sur le LAN.",
127
+ onVlan: "Les conditions se règlent sur un câble ou un LAN simple, pas encore sur le VLAN d'un switch.",
128
+ },
129
+ selected: (count) => `${String(count)} sélectionnés`,
130
+ selection: (items, cables) =>
131
+ `${String(items)} élément${items > 1 ? "s" : ""}, ${String(cables)} câble${cables > 1 ? "s" : ""}. Faites glisser l'un d'eux pour les déplacer ensemble.`,
132
+ deleteSelection: (count) => `Supprimer ${String(count)} (touche Suppr)`,
133
+ close: "Fermer",
134
+ },
135
+ lab: {
136
+ title: "Lab",
137
+ containers: "Conteneurs",
138
+ vms: "VM",
139
+ lans: "LAN",
140
+ cables: "Câbles",
141
+ toFix: "À corriger",
142
+ valid: "Le lab est valide : prêt à exporter.",
143
+ routing:
144
+ "Un routeur a besoin de sa propre configuration de transfert : chaque câble est son propre réseau, et rien ne route entre eux à votre place.",
145
+ },
146
+ menu: {
147
+ menu: "Menu",
148
+ example: "Charger le lab d'exemple",
149
+ vlanExample: "Charger l'exemple VLAN",
150
+ gns3: "Ouvrir un projet GNS3…",
151
+ gns3File: "Projet GNS3",
152
+ share: "Copier un lien de partage",
153
+ fit: "Ajuster à l'écran",
154
+ clear: "Vider le canevas",
155
+ light: "Thème clair",
156
+ dark: "Thème sombre",
157
+ style: "Style",
158
+ styles: { sketch: "Croquis : dessiné à la main", business: "Business : net et sobre" },
159
+ language: "Langue",
160
+ about: "À propos d'Isothrum",
161
+ blog: "Blog",
162
+ },
163
+ welcome: {
164
+ menuHint: "Exemple, thème, langue…",
165
+ toolbarHint: "Choisissez un appareil, puis cliquez sur le canevas",
166
+ pitch: "Dessinez un lab réseau de conteneurs et de VM. Téléchargez-le, prêt à lancer avec Docker, Vagrant ou Terraform.",
167
+ example: "Charger le lab d'exemple",
168
+ how: "Comment ça marche",
169
+ },
170
+ inspector: {
171
+ name: "Nom",
172
+ runsAs: "Exécuté comme",
173
+ automaticRouter: "Routeur automatique",
174
+ container: "Conteneur (Docker)",
175
+ vm: "Machine virtuelle",
176
+ routerNote:
177
+ "Rien à configurer : les réseaux auxquels il est relié (ses LAN, VLAN et câbles) communiquent par le routeur d'Isoloom. Pour configurer le routage vous-même, exécutez-le comme conteneur (FRR) ou comme VM.",
178
+ image: "Image",
179
+ keepRunning: "Le garder allumé",
180
+ applianceNote: "Cisco IOL tourne depuis votre propre image : construisez-la avec",
181
+ ciscoOs: "OS Cisco",
182
+ needsKvm: "Tourne dans QEMU : l’hôte Docker doit avoir /dev/kvm.",
183
+ dynamipsFirmware: (name: string) => `Placez votre image IOS de 7200 dans le téléchargement : images/${name}.bin.`,
184
+ iosConfig: "Configuration IOS",
185
+ iosConfigHint: "Ajoutée à celle qu’Isothrum écrit (nom, admin/admin en SSH, l’adresse de chaque interface). Appliquée au premier démarrage.",
186
+ keepRunningHint: "Pour une image sans service propre (Alpine, Ubuntu, Kali…) : une machine depuis laquelle travailler.",
187
+ os: "Système d'exploitation",
188
+ provision: "Étapes de provisionnement (une par ligne)",
189
+ ports: "Ports de service",
190
+ access: "L'utilisateur arrive sur cette machine",
191
+ cables: "Câbles",
192
+ noCables: "Aucun pour l'instant : tirez depuis l'un de ses points vers une autre machine.",
193
+ saveTitle: "Garder la configuration de cette machine comme appareil dans la barre d'outils, pour la placer à nouveau",
194
+ save: "Enregistrer comme appareil",
195
+ remove: "Supprimer la machine",
196
+ },
197
+ canvas: {
198
+ imageShort: "Image",
199
+ hosts: (n) => (n === 1 ? "1 hôte" : `${String(n)} hôtes`),
200
+ roles: { router: "Routeur", firewall: "Pare-feu", workstation: "Poste de travail", server: "Serveur", desktop: "Bureau", host: "Hôte" },
201
+ switchModel: (ports) => `Switch · ${String(ports)} ports`,
202
+ live: {
203
+ up: "En marche",
204
+ starting: "Démarrage",
205
+ unhealthy: "En mauvaise santé",
206
+ down: "Arrêtée",
207
+ portUp: "Lien actif",
208
+ portDown: "Lien coupé",
209
+ portUnknown: "Inconnu (arrêtée, ou le port appartient à l’OS de l’équipement)",
210
+ lanPorts: (up, total) => `${String(up)} ports sur ${String(total)} actifs sur ce LAN`,
211
+ cable: { up: "Le trafic passe", cut: "Suspendu : rien ne passe", down: "Coupé : une extrémité est arrêtée ou son port est coupé" },
212
+ cableShort: { cut: "suspendu", down: "coupé" },
213
+ },
214
+ editTitle: (what) => `${what} : double-cliquez pour modifier`,
215
+ unnamed: "sans nom",
216
+ machineName: "Nom de la machine",
217
+ vm: "VM",
218
+ automatic: "Automatique",
219
+ container: "Conteneur",
220
+ access: "accès",
221
+ addressTitle: (network, fixed) => `${network} : double-cliquez pour fixer cette adresse${fixed ? " (fixée)" : ""}`,
222
+ fixed: "fixée",
223
+ addressOn: (network) => `Adresse sur ${network}`,
224
+ lanName: "Nom du LAN",
225
+ addressBlock: "Plage d'adresses",
226
+ internetOnTitle: "Accède à internet (cliquez pour le couper)",
227
+ internetOffTitle: "Pas d'internet (cliquez pour l'autoriser)",
228
+ internetOn: "Accès à internet : activé",
229
+ internetOff: "Accès à internet : désactivé",
230
+ showAreaTitle: "Afficher la zone du LAN",
231
+ hideAreaTitle: "Masquer la zone du LAN (le LAN reste : seul son rectangle disparaît)",
232
+ showArea: "Afficher la zone",
233
+ hideArea: "Masquer la zone",
234
+ switchName: "Nom du switch",
235
+ port: (n) => `Port ${String(n)}`,
236
+ trunkPort: "Port trunk",
237
+ trunkCarries: (vlans) => `trunk, transporte ${vlans}`,
238
+ noVlan: "aucun VLAN",
239
+ clickToChange: "(cliquez pour changer)",
240
+ vlanNumber: "Numéro de VLAN",
241
+ vlanBlock: "Plage d'adresses du VLAN",
242
+ remove: (what) => `Supprimer ${what}`,
243
+ routerName: "Nom du routeur",
244
+ portFree: (port) => `${port} : libre`,
245
+ textOnly: "Texte seul",
246
+ box: "Cadre",
247
+ ellipse: "Ellipse",
248
+ colors: { yellow: "jaune", red: "rouge", blue: "bleu", green: "vert", violet: "violet" },
249
+ color: (color) => `Couleur ${color}`,
250
+ write: "Double-cliquez pour écrire",
251
+ note: "Note",
252
+ noteName: "note",
253
+ },
254
+ exportPanel: {
255
+ copy: "Copier",
256
+ copied: "Copié",
257
+ labName: "Nom du lab (celui du zip)",
258
+ fixFirst: "Corrigez le lab avant d'exporter",
259
+ download: (lab) => `Télécharger ${lab}.zip`,
260
+ onlySpec: "Seulement isoloom.yml",
261
+ where: "Où le lancer",
262
+ whereIntro: "Le zip contient un dossier par cible. Décompressez-le, puis lancez celle qui vous convient.",
263
+ mixed:
264
+ "Ce lab mélange conteneurs et VM : il tourne donc avec Vagrant. Passez tout en conteneurs (Docker, Kubernetes, une VM cloud) ou tout en VM (Proxmox, VM cloud) pour débloquer d'autres cibles.",
265
+ install: "Isoloom lance n'importe laquelle en une commande. Installez-le une fois :",
266
+ files: (count) => `${String(count)} fichier${count > 1 ? "s" : ""}`,
267
+ downloadOnly: (target) => `Télécharger seulement ${target} (avec isoloom.yml)`,
268
+ downloadTarget: (target) => `Télécharger ${target}`,
269
+ withIsoloom: "Avec Isoloom (arrêtez-le avec isoloom down)",
270
+ withTool: "Ou directement avec l'outil",
271
+ run: "Lancer",
272
+ specIntro: "Le lab en un seul fichier lisible, à garder dans Git ou à modifier à la main.",
273
+ showSpec: "Afficher le fichier",
274
+ hideSpec: "Masquer le fichier",
275
+ containersAndVms: "conteneurs et VM",
276
+ dockerOnOneVm: "Docker sur une VM",
277
+ oneVmPerMachine: "une VM par machine",
278
+ },
279
+ issues: {
280
+ vlanBlocks: (vlan, block, other, sw) => `${vlan} est en ${block} sur un switch et en ${other} sur ${sw}.`,
281
+ areaBlock: (lan, block, switches) => `La zone ${lan} est en ${block}, ses switchs disent ${switches}.`,
282
+ noName: { machine: "Une machine n'a pas de nom.", lan: "Un LAN n'a pas de nom.", switch: "Un switch n'a pas de nom." },
283
+ sameName: {
284
+ machine: (name) => `Deux machines s'appellent « ${name} ».`,
285
+ lan: (name) => `Deux LAN s'appellent « ${name} ».`,
286
+ switch: (name) => `Deux switchs s'appellent « ${name} ».`,
287
+ },
288
+ badPort: (machine, entry) => `« ${entry} » dans les ports de ${machine} n'est pas un port : un nombre de 1 à 65535, puis /udp, /http, /https ou /tls s'il n'est pas en TCP simple.`,
289
+ },
290
+ gns3: {
291
+ notGns3: "Ce n'est pas un projet GNS3 : ouvrez le fichier .gns3 (dans un .gns3project, c'est celui à la racine).",
292
+ noTopology: "Ce fichier ne contient aucune topologie GNS3.",
293
+ cisco: (node, kind) =>
294
+ `${node} : un routeur Cisco ${kind} est devenu un routeur automatique ; sa configuration IOS n'est pas reprise.`,
295
+ firewall: (node) => `${node} : un pare-feu est devenu un routeur automatique (ses règles ne sont pas reprises).`,
296
+ noEquivalent: (node, type) => `${node} : le nœud « ${type} » de GNS3 n'a pas d'équivalent ; ignoré.`,
297
+ noNodes: "Le projet n'a aucun nœud.",
298
+ },
299
+ account: {
300
+ signIn: "Se connecter avec Cyber Auth",
301
+ signOut: "Se déconnecter",
302
+ signedInAs: (name: string) => `Connecté : ${name}`,
303
+ },
304
+ projects: {
305
+ save: "Enregistrer dans mes projets",
306
+ saveAs: "Enregistrer comme nouveau projet…",
307
+ openList: "Ouvrir un projet…",
308
+ saveAsPrompt: "Nom du nouveau projet :",
309
+ windowTitle: "Mes projets",
310
+ loading: "Chargement de vos projets…",
311
+ none: "Aucun projet pour l'instant : enregistrez ce lab pour en créer un.",
312
+ open: "ouvert",
313
+ openButton: "Ouvrir",
314
+ delete: (name: string) => `Supprimer ${name}`,
315
+ confirmDelete: (name: string) => `Supprimer le projet « ${name} » et son historique ?`,
316
+ failed: (error: string) => `Impossible de charger vos projets : ${error}`,
317
+ history: (name: string) => `Versions précédentes de ${name}`,
318
+ noVersions: "Pas encore de version précédente.",
319
+ restore: "Ouvrir",
320
+ historyHint: "Ouvrir une version la place sur le canevas : Annuler ramène la version actuelle, et le prochain enregistrement la conserve.",
321
+ opened: (name: string) => `« ${name} » ouvert. Annuler ramène le lab qui était là.`,
322
+ status: {
323
+ idle: (name: string) => `Projet « ${name} »`,
324
+ saving: (name: string) => `Enregistrement de « ${name} »…`,
325
+ saved: (name: string) => `Enregistré dans « ${name} »`,
326
+ error: (name: string) => `Échec de l'enregistrement de « ${name} »`,
327
+ },
328
+ },
329
+ machineRun: {
330
+ display: "Écran (VNC)",
331
+ displayTitle: "L’écran de la machine : son serveur VNC, ou l’affichage de la VM",
332
+ displayOpened: (what) => `Ouvert : ${what}`,
333
+ consoleHint: "Double-cliquez sur une machine pour sa console ; clic droit pour son écran et le reste.",
334
+ runFirst: "Lancez le lab pour ouvrir ses consoles.",
335
+ running: "En marche dans le lab.",
336
+ stopped: "Arrêtée (le reste du lab tourne).",
337
+ unknown: "Absente du lab en marche : relancez le lab pour l’ajouter.",
338
+ console: "Console",
339
+ restart: "Redémarrer",
340
+ stop: "Arrêter",
341
+ start: "Démarrer",
342
+ snapshots: "Instantanés",
343
+ takeSnapshot: "En prendre un",
344
+ snapshotHint: "Enregistrer la machine telle qu’elle est (ses fichiers, la configuration enregistrée d’un routeur) ; pas ses volumes",
345
+ snapshotPrompt: "Nom de l’instantané (facultatif) :",
346
+ noSnapshots: "Aucun pour l’instant.",
347
+ restore: "Restaurer",
348
+ inUse: "utilisé",
349
+ ownImage: "Revenir à son image",
350
+ deleteSnapshot: (label: string) => `Supprimer l’instantané ${label}`,
351
+ },
352
+ update: {
353
+ available: (version: string) => `Isothrum ${version} est disponible.`,
354
+ restart: "Redémarrer pour mettre à jour",
355
+ installing: "Mise à jour…",
356
+ later: "Plus tard",
357
+ },
358
+ };
@@ -0,0 +1,21 @@
1
+ import { hasLocale, type Locale } from "@/lib/i18n";
2
+ import { de } from "./de";
3
+ import { en, type EditorText } from "./en";
4
+ import { es } from "./es";
5
+ import { fr } from "./fr";
6
+
7
+ export type { EditorText };
8
+
9
+ /**
10
+ * The editor's own words in each language. They live here, typed, rather than in the site's
11
+ * JSON dictionaries: the editor only renders in the browser, and many of its strings take
12
+ * values (names, counts), which functions say better than templates.
13
+ */
14
+ const TEXTS: Record<Locale, EditorText> = { en, fr, es, de };
15
+
16
+ export function editorText(lang: string): EditorText {
17
+ return hasLocale(lang) ? TEXTS[lang] : en;
18
+ }
19
+
20
+ /** English: the source the other languages follow. */
21
+ export const defaultText = en;
@@ -0,0 +1,33 @@
1
+ import { describe, expect, test } from "vitest";
2
+ import { PRESETS } from "../presets";
3
+ import { de } from "./de";
4
+ import { en } from "./en";
5
+ import { es } from "./es";
6
+ import { fr } from "./fr";
7
+ import { defaultText, editorText } from "./index";
8
+
9
+ const languages = { en, fr, es, de };
10
+
11
+ describe("editorText", () => {
12
+ test("gives each site language its own words", () => {
13
+ expect([editorText("en"), editorText("fr"), editorText("es"), editorText("de")]).toEqual([en, fr, es, de]);
14
+ });
15
+
16
+ test("falls back to English, the source the others follow", () => {
17
+ expect(editorText("it")).toBe(en);
18
+ expect(defaultText).toBe(en);
19
+ });
20
+ });
21
+
22
+ describe.each(Object.entries(languages))("%s", (_, text) => {
23
+ test("names every built-in device of the toolbar", () => {
24
+ for (const p of PRESETS) expect(p.id in text.devices, p.id).toBe(true);
25
+ });
26
+
27
+ test("puts the names it is given in its messages", () => {
28
+ expect(text.issues.sameName.machine("web7")).toContain("web7");
29
+ expect(text.issues.badPort("web7", "80/sctp")).toContain("80/sctp");
30
+ expect(text.gns3.cisco("R1", "IOU")).toContain("R1");
31
+ expect(text.myDevice("alpine:3.20")).toContain("alpine:3.20");
32
+ });
33
+ });
@@ -0,0 +1,108 @@
1
+ import { describe, expect, test } from "vitest";
2
+ import { cable, compile, lan, machine, sw1Networks, switchNode } from "@/test/lab";
3
+ import { toTopology } from "./topology";
4
+
5
+ /** A machine's box, centered in the 400 by 300 LAN at the origin. */
6
+ const inside = { x: 140, y: 92 };
7
+ /** Far from every LAN. */
8
+ const away = { x: 2000, y: 2000 };
9
+
10
+ describe("which networks a machine is on", () => {
11
+ test("the LAN whose area it sits in, with the next free address", () => {
12
+ expect(compile([lan("office"), machine("web", {}, inside)]).nodes.web.networks).toEqual({ office: null });
13
+ });
14
+
15
+ test("its fixed address there, when it has one", () => {
16
+ expect(compile([lan("office"), machine("web", { pins: { office: 20 } }, inside)]).nodes.web.networks).toEqual({ office: 20 });
17
+ });
18
+
19
+ test("a LAN it is cabled to, whichever end the LAN is", () => {
20
+ const nodes = [lan("office"), lan("dmz", {}, { x: 1000 }), machine("web", {}, away)];
21
+ const t = compile(nodes, [cable("c1", "web", "office"), cable("c2", "dmz", "web")]);
22
+ expect(t.nodes.web.networks).toEqual({ office: null, dmz: null });
23
+ });
24
+
25
+ test("the VLAN of the switch's access port it is cabled to, whichever end the switch is", () => {
26
+ const nodes = [switchNode("sw1"), machine("a", {}, away), machine("b", {}, away)];
27
+ const t = compile(nodes, [cable("c1", "a", "sw1", { targetHandle: "p1" }), cable("c2", "sw1", "b", { sourceHandle: "p3" })]);
28
+ expect([t.nodes.a.networks, t.nodes.b.networks]).toEqual([{ vlan10: null }, { vlan20: null }]);
29
+ });
30
+
31
+ test("every VLAN of the switch on its trunk, and none on a free port", () => {
32
+ const nodes = [switchNode("sw1"), machine("r", {}, away), machine("free", {}, away)];
33
+ const t = compile(nodes, [cable("c1", "r", "sw1", { targetHandle: "trunk" }), cable("c2", "free", "sw1", { targetHandle: "p5" })]);
34
+ expect([t.nodes.r.networks, t.nodes.free.networks]).toEqual([{ vlan10: null, vlan20: null }, {}]);
35
+ });
36
+ });
37
+
38
+ describe("what a machine runs", () => {
39
+ test("an automatic router is a router", () => {
40
+ expect(compile([machine("r", { kind: "router", icon: "router" })]).nodes.r.kind).toEqual({ router: {} });
41
+ });
42
+
43
+ test("a container keeps its image trimmed, idle when it is a stock Linux image", () => {
44
+ const t = compile([machine("a", { image: " alpine:3.20 " }), machine("b", { image: "nginx" }), machine("c", { image: "alpine", idle: false })]);
45
+ expect([t.nodes.a.kind, t.nodes.b.kind, t.nodes.c.kind]).toEqual([
46
+ { container: { image: "alpine:3.20", idle: true } },
47
+ { container: { image: "nginx", idle: false } },
48
+ { container: { image: "alpine", idle: false } },
49
+ ]);
50
+ });
51
+
52
+ test("a Cisco appliance carries its OS and its own configuration, when it has one", () => {
53
+ const iol = { image: "vrnetlab/cisco_iol:17.12.01", appliance: "cisco-iol" as const };
54
+ const t = compile([machine("r1", { ...iol, config: "router ospf 1" }), machine("r2", { ...iol, config: " " })]);
55
+ expect(t.nodes.r1.kind).toEqual({ container: { ...iol, config: "router ospf 1" } });
56
+ expect(t.nodes.r2.kind).toEqual({ container: iol });
57
+ });
58
+
59
+ test("a VM keeps its OS and one provisioning step per non-blank line", () => {
60
+ const t = compile([machine("vm", { kind: "vm", os: "debian@12", provision: "a\n b \n\n" })]);
61
+ expect(t.nodes.vm.kind).toEqual({ vm: { os: "debian@12", provision: ["a", "b"] } });
62
+ });
63
+
64
+ test("services are the ports of the list Isoloom takes, with what each speaks", () => {
65
+ expect(compile([machine("web", { ports: "x, 70000, 22, 443/https ,0, 53/udp" })]).nodes.web.services).toEqual([{ port: 22 }, { port: 443, protocol: "https" }, { port: 53, protocol: "udp" }]);
66
+ });
67
+
68
+ test("the access machine says so", () => {
69
+ expect(compile([machine("pc", { access: true })]).nodes.pc.access).toBe(true);
70
+ });
71
+
72
+ test("a machine without a name is left out", () => {
73
+ expect(compile([machine("m", { name: "" })]).nodes).toEqual({});
74
+ });
75
+ });
76
+
77
+ describe("links", () => {
78
+ test("a cable between two named machines is a link, its ports trimmed and its conditions as tc", () => {
79
+ const t = compile([machine("a", {}, away), machine("b", {}, away)], [cable("c", "a", "b", { aPort: " eth2 ", bPort: "eth1", tc: { delay: 10 } })]);
80
+ expect(t.links).toEqual([{ a: { node: "a", port: "eth2" }, b: { node: "b", port: "eth1" }, tc: { delay: "10ms" } }]);
81
+ });
82
+
83
+ test("a cable to a LAN, to a nameless machine or without ends' labels is no link", () => {
84
+ const nodes = [lan("office"), machine("a", {}, away), machine("b", { name: "" }, away)];
85
+ const unlabelled = { ...cable("c3", "a", "a"), data: undefined };
86
+ expect(compile(nodes, [cable("c1", "a", "office"), cable("c2", "a", "b"), unlabelled]).links).toEqual([]);
87
+ });
88
+ });
89
+
90
+ describe("networks", () => {
91
+ test("each named LAN area, its block trimmed, its internet and conditions", () => {
92
+ const t = compile([lan("office", { cidr: " 10.10.1.0/24 ", internet: false, tc: { loss: 1 } }), lan("unnamed", { name: "" })]);
93
+ expect(t.networks).toEqual({ office: { cidr: "10.10.1.0/24", internet: false, tc: { loss: 1 } } });
94
+ });
95
+
96
+ test("a switch's VLANs, nested under the switch's LAN", () => {
97
+ expect(compile([switchNode("sw1")]).networks).toEqual(sw1Networks);
98
+ });
99
+
100
+ test("a LAN area named like a VLAN is that VLAN: its block and internet, without conditions", () => {
101
+ const area = lan("l", { name: "vlan10", cidr: "10.10.10.0/24", internet: false, tc: { delay: 5 } });
102
+ expect(compile([area, switchNode("sw1")]).networks.vlan10).toEqual({ cidr: "10.10.10.0/24", internet: false, switch: "sw1", vlan: 10 });
103
+ });
104
+
105
+ test("the topology is JSON text, named after the lab", () => {
106
+ expect(JSON.parse(toTopology("office-lab", [], []))).toEqual({ name: "office-lab", nodes: {}, networks: {}, links: [] });
107
+ });
108
+ });
@@ -0,0 +1,116 @@
1
+ import { tcOf, type Tc } from "./conditions";
2
+ import { lansAround } from "./geometry";
3
+ import { idleByDefault, isLan, isMachine, isSwitch, type CableEdge, type LabNode, type MachineData } from "./model";
4
+ import { readPorts, type Service } from "./ports";
5
+ import { accessVlan, isTrunkPort, switchLans, vlanName } from "./vlans";
6
+
7
+ /** The lab as the engine's topology JSON (`isothrum_compiler::Topology`). */
8
+
9
+ /** Machine id -> the LANs it is on. */
10
+ type Membership = Map<string, Set<string>>;
11
+
12
+ function join(membership: Membership, machine: string, lan: string): void {
13
+ membership.set(machine, (membership.get(machine) ?? new Set()).add(lan));
14
+ }
15
+
16
+ /** A machine on a switch port joins the port's VLAN; on the trunk, every VLAN of the switch. */
17
+ function joinSwitchPorts(membership: Membership, e: CableEdge, byId: Map<string, LabNode>): void {
18
+ const a = byId.get(e.source);
19
+ const b = byId.get(e.target);
20
+ for (const [m, sw, handle] of [
21
+ [a, b, e.targetHandle],
22
+ [b, a, e.sourceHandle],
23
+ ] as const) {
24
+ if (!m || !sw || !isMachine(m) || !isSwitch(sw)) continue;
25
+ if (isTrunkPort(sw, handle)) {
26
+ for (const v of sw.data.vlans) join(membership, m.id, vlanName(v.id));
27
+ } else {
28
+ const vlan = accessVlan(sw, handle);
29
+ if (vlan !== null) join(membership, m.id, vlanName(vlan));
30
+ }
31
+ }
32
+ }
33
+
34
+ /** A machine is on a LAN when it sits inside its area, or when a cable runs from it to the area. */
35
+ function membershipOf(nodes: LabNode[], edges: CableEdge[], byId: Map<string, LabNode>): Membership {
36
+ const membership: Membership = new Map();
37
+ for (const n of nodes.filter(isMachine)) for (const l of lansAround(n, nodes)) join(membership, n.id, l.data.name);
38
+ for (const e of edges) {
39
+ const a = byId.get(e.source);
40
+ const b = byId.get(e.target);
41
+ if (a && b && isMachine(a) && isLan(b)) join(membership, a.id, b.data.name);
42
+ if (a && b && isLan(a) && isMachine(b)) join(membership, b.id, a.data.name);
43
+ joinSwitchPorts(membership, e, byId);
44
+ }
45
+ return membership;
46
+ }
47
+
48
+ /** What a machine runs, as the engine's `kind`. */
49
+ function kindOf(m: MachineData) {
50
+ if (m.kind === "router") return { router: {} };
51
+ if (m.kind === "container") {
52
+ return {
53
+ container: m.appliance
54
+ ? { image: m.image.trim(), appliance: m.appliance, ...(m.config?.trim() ? { config: m.config } : {}) }
55
+ : { image: m.image.trim(), idle: m.idle ?? idleByDefault(m.image) },
56
+ };
57
+ }
58
+ const provision = m.provision
59
+ .split("\n")
60
+ .map((l) => l.trim())
61
+ .filter(Boolean);
62
+ return { vm: { os: m.os, provision } };
63
+ }
64
+
65
+ /** The ports in a machine's field that Isoloom takes (the others are reported: `canvasIssues`). */
66
+ const servicesOf = (m: MachineData): Service[] => readPorts(m.ports).services;
67
+
68
+ /** Cables between two machines; a cable to a LAN is a membership, not a link. */
69
+ function linksOf(edges: CableEdge[], byId: Map<string, LabNode>) {
70
+ return edges.flatMap((e) => {
71
+ const a = byId.get(e.source);
72
+ const b = byId.get(e.target);
73
+ if (!a || !b || !isMachine(a) || !isMachine(b) || !a.data.name || !b.data.name || !e.data) return [];
74
+ return [{ a: { node: a.data.name, port: e.data.aPort.trim() }, b: { node: b.data.name, port: e.data.bPort.trim() }, tc: tcOf(e.data.tc) }];
75
+ });
76
+ }
77
+
78
+ interface Net {
79
+ cidr: string;
80
+ internet: boolean;
81
+ tc?: Tc;
82
+ switch?: string;
83
+ vlan?: number;
84
+ }
85
+
86
+ /** The LAN areas, and each switch's VLANs, which nest under it in isoloom.yml (`sw1.vlan10`). */
87
+ function networksOf(nodes: LabNode[], edges: CableEdge[]): Record<string, Net> {
88
+ const lans = nodes.filter(isLan).filter((l) => l.data.name);
89
+ const networks: Record<string, Net> = Object.fromEntries(
90
+ lans.map((l) => [l.data.name, { cidr: l.data.cidr.trim(), internet: l.data.internet, tc: tcOf(l.data.tc) }]),
91
+ );
92
+ const nameOfGroup = switchLans(nodes, edges);
93
+ for (const sw of nodes.filter(isSwitch)) {
94
+ for (const v of sw.data.vlans) {
95
+ const net = (networks[vlanName(v.id)] as Net | undefined) ?? { cidr: v.cidr.trim(), internet: true };
96
+ networks[vlanName(v.id)] = { ...net, tc: undefined, switch: nameOfGroup(sw.id), vlan: v.id };
97
+ }
98
+ }
99
+ return networks;
100
+ }
101
+
102
+ export function toTopology(name: string, nodes: LabNode[], edges: CableEdge[]): string {
103
+ const byId = new Map(nodes.map((n) => [n.id, n]));
104
+ const membership = membershipOf(nodes, edges, byId);
105
+ const machines = Object.fromEntries(
106
+ nodes
107
+ .filter(isMachine)
108
+ .filter((n) => n.data.name)
109
+ .map(({ id, data: m }) => {
110
+ const networks = Object.fromEntries([...(membership.get(id) ?? [])].map((lan) => [lan, m.pins?.[lan] ?? null]));
111
+ return [m.name, { kind: kindOf(m), access: m.access, services: servicesOf(m), networks }];
112
+ }),
113
+ );
114
+ const links = linksOf(edges, byId);
115
+ return JSON.stringify({ name, nodes: machines, networks: networksOf(nodes, edges), links });
116
+ }