@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,42 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
import type { Engine, Plan } from "@/lib/tool/engine";
|
|
3
|
+
|
|
4
|
+
/** Saves `blob` as `filename`, through a link the browser follows. */
|
|
5
|
+
function download(blob: Blob, filename: string) {
|
|
6
|
+
const url = URL.createObjectURL(blob);
|
|
7
|
+
const a = document.createElement("a");
|
|
8
|
+
a.href = url;
|
|
9
|
+
a.download = filename;
|
|
10
|
+
a.click();
|
|
11
|
+
URL.revokeObjectURL(url);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const zip = (bytes: Uint8Array) => new Blob([bytes as BlobPart], { type: "application/zip" });
|
|
15
|
+
|
|
16
|
+
/** The Export window's downloads: the whole lab, one target, or isoloom.yml alone. */
|
|
17
|
+
export function useDownloads(engine: Engine | null, plan: Plan | null, topology: string, labName: string) {
|
|
18
|
+
const [downloadError, setDownloadError] = useState<string | null>(null);
|
|
19
|
+
/** Builds a zip with the engine and saves it, or says why it couldn't. */
|
|
20
|
+
function save(build: (engine: Engine) => Uint8Array, filename: string) {
|
|
21
|
+
if (!engine) return;
|
|
22
|
+
setDownloadError(null);
|
|
23
|
+
try {
|
|
24
|
+
download(zip(build(engine)), filename);
|
|
25
|
+
} catch (e) {
|
|
26
|
+
setDownloadError(String(e));
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return {
|
|
30
|
+
downloadError,
|
|
31
|
+
downloadAll: () => {
|
|
32
|
+
save((e) => e.bundle(topology), `${labName || "lab"}.zip`);
|
|
33
|
+
},
|
|
34
|
+
/** One target's zip: isoloom.yml and that target's folder. */
|
|
35
|
+
downloadTarget: (dir: string) => {
|
|
36
|
+
save((e) => e.bundle_target(topology, dir), `${labName || "lab"}-${dir.replace(/\//g, "-")}.zip`);
|
|
37
|
+
},
|
|
38
|
+
downloadSpec: () => {
|
|
39
|
+
if (plan?.isoloom_yml) download(new Blob([plan.isoloom_yml], { type: "text/yaml" }), "isoloom.yml");
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { useMemo, useState, type RefObject } from "react";
|
|
2
|
+
import { useReactFlow } from "@xyflow/react";
|
|
3
|
+
import { useTheme } from "next-themes";
|
|
4
|
+
import { toolLabel } from "@/lib/tool/blocks";
|
|
5
|
+
import { runsOnVms } from "@/lib/tool/desktop";
|
|
6
|
+
import { facingSides, shownCables } from "@/lib/tool/facing";
|
|
7
|
+
import { liveViewOf } from "@/lib/tool/live";
|
|
8
|
+
import { isMachine, type CableEdge, type LabNode } from "@/lib/tool/model";
|
|
9
|
+
import { routerPortsOf, type Addresses } from "@/lib/tool/router-ports";
|
|
10
|
+
import { labIssues, selectionOf } from "@/lib/tool/selection";
|
|
11
|
+
import { toTopology } from "@/lib/tool/topology";
|
|
12
|
+
import type { Plan } from "@/lib/tool/engine";
|
|
13
|
+
import { useText } from "../text";
|
|
14
|
+
import { useCabling, useLabIndex } from "./use-cabling";
|
|
15
|
+
import { useCaptures } from "./use-captures";
|
|
16
|
+
import { useClipboard } from "./use-clipboard";
|
|
17
|
+
import { useContextMenus } from "./use-context-menus";
|
|
18
|
+
import { useAutoStart, useDesktopStartup, useDesktopState, useLiveLab, useRunPanel } from "./use-desktop";
|
|
19
|
+
import { useDesign } from "./use-design";
|
|
20
|
+
import { useDevices } from "./use-devices";
|
|
21
|
+
import { useDownloads } from "./use-downloads";
|
|
22
|
+
import { useEngine, usePlan } from "./use-engine";
|
|
23
|
+
import { useHistory } from "./use-history";
|
|
24
|
+
import { useDraftSaving, useLab } from "./use-lab";
|
|
25
|
+
import { useFitWhenDrawn, useLabActions } from "./use-lab-actions";
|
|
26
|
+
import { useLabRun } from "./use-lab-run";
|
|
27
|
+
import { useLanCarry } from "./use-lan-carry";
|
|
28
|
+
import { useMachineActions } from "./use-machine-actions";
|
|
29
|
+
import { usePlacement } from "./use-placement";
|
|
30
|
+
import { useProjects } from "./use-projects";
|
|
31
|
+
import { useSharedLink } from "./use-shared-link";
|
|
32
|
+
import { useToast } from "./use-toast";
|
|
33
|
+
import { useToolKeys, useTools } from "./use-tools";
|
|
34
|
+
|
|
35
|
+
/** The desktop app's side: running the lab, its machines, captures (all inert in a browser). */
|
|
36
|
+
function useDesktopSide(labName: string, initialName: string, setToast: (message: string) => void) {
|
|
37
|
+
const t = useText();
|
|
38
|
+
const state = useDesktopState();
|
|
39
|
+
const live = useLiveLab(state.invoke, labName, state.run);
|
|
40
|
+
const actions = useMachineActions({ invoke: state.invoke, labName, live, setToast, t });
|
|
41
|
+
const menus = useContextMenus();
|
|
42
|
+
const panel = useRunPanel();
|
|
43
|
+
const autoStart = useAutoStart(state.invoke, labName);
|
|
44
|
+
const captures = useCaptures(state.invoke, labName, (error) => {
|
|
45
|
+
panel.setRunLog(error);
|
|
46
|
+
panel.setRunOpen(true);
|
|
47
|
+
});
|
|
48
|
+
useDesktopStartup(state, initialName, autoStart.setAutoStartPending);
|
|
49
|
+
return { state, live, actions, menus, panel, autoStart, captures };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** While the lab has an error the engine gives no addresses; the canvas keeps showing the last good ones, so the address in conflict can still be edited where it is. */
|
|
53
|
+
function useAddresses(plan: Plan | null): Addresses {
|
|
54
|
+
const [last, setLast] = useState<Addresses>({});
|
|
55
|
+
const good = plan?.ok && Object.keys(plan.addresses).length ? plan.addresses : null;
|
|
56
|
+
if (good && good !== last) setLast(good);
|
|
57
|
+
return plan?.ok ? plan.addresses : last;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** What the canvas draws from the lab: port sides, cables as attached now, router ports. */
|
|
61
|
+
function useCanvasViews(nodes: LabNode[], edges: CableEdge[], lans: ReturnType<typeof useLabIndex>["lans"], addresses: Addresses) {
|
|
62
|
+
const sides = useMemo(() => facingSides(nodes, edges, lans), [nodes, edges, lans]);
|
|
63
|
+
const shownEdges = useMemo(() => shownCables(nodes, edges, lans), [nodes, edges, lans]);
|
|
64
|
+
const routerPorts = useMemo(() => routerPortsOf(nodes, edges, lans, addresses), [nodes, edges, lans, addresses]);
|
|
65
|
+
return { sides, shownEdges, routerPorts };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** The lab editor's state and behaviour: what the canvas, the toolbar and the windows show and do. */
|
|
69
|
+
export function useEditor(lang: string, accounts: boolean, canvasRef: RefObject<HTMLDivElement | null>) {
|
|
70
|
+
const t = useText();
|
|
71
|
+
const { engine, error: engineError } = useEngine();
|
|
72
|
+
const { resolvedTheme } = useTheme();
|
|
73
|
+
const flow = useReactFlow<LabNode, CableEdge>();
|
|
74
|
+
const lab = useLab();
|
|
75
|
+
const [exportOpen, setExportOpen] = useState(false);
|
|
76
|
+
const [statusOpen, setStatusOpen] = useState(false);
|
|
77
|
+
const tools = useTools();
|
|
78
|
+
const [toast, setToast] = useToast();
|
|
79
|
+
const desk = useDesktopSide(lab.labName, lab.initial.name, setToast);
|
|
80
|
+
const history = useHistory(lab);
|
|
81
|
+
const design = useDesign();
|
|
82
|
+
const fitWhenDrawn = useFitWhenDrawn(flow.fitView);
|
|
83
|
+
useSharedLink({ lab, setDesign: design.setDesign, setToast, fitWhenDrawn, t });
|
|
84
|
+
useClipboard(lab, flow.deleteElements);
|
|
85
|
+
useDraftSaving(lab);
|
|
86
|
+
const topology = useMemo(() => toTopology(lab.labName, lab.nodes, lab.edges), [lab.labName, lab.nodes, lab.edges]);
|
|
87
|
+
const plan = usePlan(engine, topology, t);
|
|
88
|
+
const projects = useProjects({ accounts, lab, design: design.design, setDesign: design.setDesign, plan, setToast, fitWhenDrawn, t });
|
|
89
|
+
const addresses = useAddresses(plan);
|
|
90
|
+
const index = useLabIndex(lab.nodes);
|
|
91
|
+
const views = useCanvasViews(lab.nodes, lab.edges, index.lans, addresses);
|
|
92
|
+
const carry = useLanCarry(lab);
|
|
93
|
+
const cabling = useCabling(lab, index);
|
|
94
|
+
const devices = useDevices(tools, setToast, t);
|
|
95
|
+
useToolKeys(tools);
|
|
96
|
+
const actions = useLabActions(lab, fitWhenDrawn, setToast, t);
|
|
97
|
+
const machines = lab.nodes.filter(isMachine);
|
|
98
|
+
const hasVms = runsOnVms(machines.some((n) => n.data.kind === "vm"), plan);
|
|
99
|
+
const run = useLabRun({ ...desk, state: desk.state, lab: { labName: lab.labName, topology, plan, engine, hasVms }, t });
|
|
100
|
+
const { invoke, run: runState } = desk.state;
|
|
101
|
+
const { live, links } = desk.live;
|
|
102
|
+
const liveView = useMemo(() => (runState === "running" && live ? liveViewOf(live, links, lab.nodes, lab.edges) : null), [runState, live, links, lab.nodes, lab.edges]);
|
|
103
|
+
const placement = usePlacement({ lab, lans: index.lans, tools, presets: devices.presets, canvasRef, screenToFlowPosition: flow.screenToFlowPosition, closeMenus: desk.menus.closeMenus, importGns3: (f) => void actions.importGns3(f) });
|
|
104
|
+
const downloads = useDownloads(engine, plan, topology, lab.labName);
|
|
105
|
+
const tool = toolLabel(tools.activeTool, devices.presets, t);
|
|
106
|
+
const issues = labIssues(lab.nodes, plan, t.issues);
|
|
107
|
+
const selection = selectionOf(lab.nodes, lab.edges);
|
|
108
|
+
return {
|
|
109
|
+
lang, accounts, t, engine, engineError, plan, resolvedTheme, flow, lab, tools, toast, setToast, desk, history, design, projects, addresses, index, views, carry, cabling, devices, actions, run, liveView, placement, downloads, tool, issues, selection, invoke,
|
|
110
|
+
exportOpen, setExportOpen, statusOpen, setStatusOpen, machines, hasVms, mixed: hasVms && machines.some((n) => n.data.kind === "container"), empty: lab.nodes.length === 0, fitWhenDrawn,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export type EditorModel = ReturnType<typeof useEditor>;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { useEffect, useMemo, useState } from "react";
|
|
2
|
+
import { loadEngine, planOf, type Engine, type Plan } from "@/lib/tool/engine";
|
|
3
|
+
import type { EditorText } from "@/lib/tool/text";
|
|
4
|
+
|
|
5
|
+
/** The engine, once loaded (or why it didn't load). */
|
|
6
|
+
export function useEngine(): { engine: Engine | null; error: string | null } {
|
|
7
|
+
const [engine, setEngine] = useState<Engine | null>(null);
|
|
8
|
+
const [error, setError] = useState<string | null>(null);
|
|
9
|
+
useEffect(() => {
|
|
10
|
+
let alive = true;
|
|
11
|
+
loadEngine().then(
|
|
12
|
+
(m) => {
|
|
13
|
+
if (alive) setEngine(m);
|
|
14
|
+
},
|
|
15
|
+
(e: unknown) => {
|
|
16
|
+
if (alive) setError(String(e));
|
|
17
|
+
},
|
|
18
|
+
);
|
|
19
|
+
return () => {
|
|
20
|
+
alive = false;
|
|
21
|
+
};
|
|
22
|
+
}, []);
|
|
23
|
+
return { engine, error };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** The engine's plan for the lab (null until the engine has loaded). An engine crash must not take the canvas down with it. */
|
|
27
|
+
export function usePlan(engine: Engine | null, topology: string, t: EditorText): Plan | null {
|
|
28
|
+
return useMemo(() => (engine ? planOf(engine, topology, t.engineFailed) : null), [engine, topology, t]);
|
|
29
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
2
|
+
import type { CableEdge, LabNode } from "@/lib/tool/model";
|
|
3
|
+
import type { Lab } from "./use-lab";
|
|
4
|
+
|
|
5
|
+
/** A lab as the history keeps it: nothing selected or being dragged. */
|
|
6
|
+
interface Snapshot {
|
|
7
|
+
nodes: LabNode[];
|
|
8
|
+
edges: CableEdge[];
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function snapshot(nodes: LabNode[], edges: CableEdge[]): Snapshot {
|
|
12
|
+
return {
|
|
13
|
+
nodes: nodes.map((n) => ({ ...n, selected: false, dragging: false })),
|
|
14
|
+
edges: edges.map((e) => ({ ...e, selected: false })),
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** What a change to the lab is (not selection, not the sizes React Flow measures). */
|
|
19
|
+
function signature(nodes: LabNode[], edges: CableEdge[]): string {
|
|
20
|
+
return JSON.stringify([
|
|
21
|
+
nodes.map((n) => [n.id, n.type, n.position.x, n.position.y, n.width, n.height, n.data]),
|
|
22
|
+
edges.map((e) => [e.id, e.source, e.sourceHandle, e.target, e.targetHandle, e.data?.aPort, e.data?.bPort]),
|
|
23
|
+
]);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface History {
|
|
27
|
+
past: Snapshot[];
|
|
28
|
+
future: Snapshot[];
|
|
29
|
+
current: Snapshot;
|
|
30
|
+
sig: string;
|
|
31
|
+
last: number;
|
|
32
|
+
applying: boolean;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Records a change: changes in quick succession (a drag, typing a name) count as one. */
|
|
36
|
+
function record(h: History, nodes: LabNode[], edges: CableEdge[], sig: string): void {
|
|
37
|
+
const now = Date.now();
|
|
38
|
+
if (!h.applying) {
|
|
39
|
+
if (now - h.last > 700) {
|
|
40
|
+
h.past.push(h.current);
|
|
41
|
+
if (h.past.length > 200) h.past.shift();
|
|
42
|
+
}
|
|
43
|
+
h.future = [];
|
|
44
|
+
h.last = now;
|
|
45
|
+
}
|
|
46
|
+
h.applying = false;
|
|
47
|
+
h.current = snapshot(nodes, edges);
|
|
48
|
+
h.sig = sig;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Undo and redo. Every change to the lab goes into the history. */
|
|
52
|
+
export function useHistory(lab: Lab) {
|
|
53
|
+
const { initial, nodes, edges, setNodes, setEdges } = lab;
|
|
54
|
+
const history = useRef<History>({ past: [], future: [], current: snapshot(initial.nodes, initial.edges), sig: signature(initial.nodes, initial.edges), last: 0, applying: false });
|
|
55
|
+
const [canUndo, setCanUndo] = useState(false);
|
|
56
|
+
const [canRedo, setCanRedo] = useState(false);
|
|
57
|
+
useEffect(() => {
|
|
58
|
+
const h = history.current;
|
|
59
|
+
const sig = signature(nodes, edges);
|
|
60
|
+
if (sig === h.sig) return;
|
|
61
|
+
record(h, nodes, edges, sig);
|
|
62
|
+
queueMicrotask(() => {
|
|
63
|
+
setCanUndo(h.past.length > 0);
|
|
64
|
+
setCanRedo(h.future.length > 0);
|
|
65
|
+
});
|
|
66
|
+
}, [nodes, edges]);
|
|
67
|
+
const step = useCallback(
|
|
68
|
+
(back: boolean) => {
|
|
69
|
+
const h = history.current;
|
|
70
|
+
const target = (back ? h.past : h.future).pop();
|
|
71
|
+
if (!target) return;
|
|
72
|
+
(back ? h.future : h.past).push(h.current);
|
|
73
|
+
h.applying = true;
|
|
74
|
+
h.last = 0;
|
|
75
|
+
setNodes(target.nodes);
|
|
76
|
+
setEdges(target.edges);
|
|
77
|
+
},
|
|
78
|
+
[setNodes, setEdges],
|
|
79
|
+
);
|
|
80
|
+
useUndoKeys(step);
|
|
81
|
+
return { step, canUndo, canRedo };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Whether a key event happens in a text field (where the keys edit the text, as everywhere else). */
|
|
85
|
+
export function inTextField(e: KeyboardEvent): boolean {
|
|
86
|
+
return e.target instanceof Element && e.target.closest("input, textarea, select, [contenteditable]") !== null;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Ctrl/⌘ Z undoes, Ctrl/⌘ Shift Z or Y redoes. */
|
|
90
|
+
function useUndoKeys(step: (back: boolean) => void): void {
|
|
91
|
+
useEffect(() => {
|
|
92
|
+
function onKey(e: KeyboardEvent) {
|
|
93
|
+
// In a text field, the keys undo the typing, as everywhere else.
|
|
94
|
+
if (inTextField(e)) return;
|
|
95
|
+
if (!(e.metaKey || e.ctrlKey)) return;
|
|
96
|
+
const key = e.key.toLowerCase();
|
|
97
|
+
if (key === "z" && !e.shiftKey) step(true);
|
|
98
|
+
else if ((key === "z" && e.shiftKey) || key === "y") step(false);
|
|
99
|
+
else return;
|
|
100
|
+
e.preventDefault();
|
|
101
|
+
}
|
|
102
|
+
window.addEventListener("keydown", onKey);
|
|
103
|
+
return () => {
|
|
104
|
+
window.removeEventListener("keydown", onKey);
|
|
105
|
+
};
|
|
106
|
+
}, [step]);
|
|
107
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { useCallback } from "react";
|
|
2
|
+
import { migrateEdges } from "@/lib/tool/cables";
|
|
3
|
+
import { exampleLab } from "@/lib/tool/examples";
|
|
4
|
+
import { fromGns3 } from "@/lib/tool/gns3-import";
|
|
5
|
+
import { isLan, isMachine, type CableData, type Draft, type MachineData } from "@/lib/tool/model";
|
|
6
|
+
import type { EditorText } from "@/lib/tool/text";
|
|
7
|
+
import { replaceLab, type Lab } from "./use-lab";
|
|
8
|
+
|
|
9
|
+
type FitView = (options: { padding: number; maxZoom: number }) => unknown;
|
|
10
|
+
|
|
11
|
+
/** After replacing the whole lab: fits once React Flow has measured the new nodes. */
|
|
12
|
+
export function useFitWhenDrawn(fitView: FitView) {
|
|
13
|
+
return useCallback(
|
|
14
|
+
(options = { padding: 0.4, maxZoom: 1 }) => {
|
|
15
|
+
setTimeout(() => void fitView(options), 120);
|
|
16
|
+
},
|
|
17
|
+
[fitView],
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Editing the lab from its windows and menus: a machine's or a cable's fields, examples, a GNS3 project. */
|
|
22
|
+
export function useLabActions(lab: Lab, fitWhenDrawn: (options?: { padding: number; maxZoom: number }) => void, setToast: (message: string) => void, t: EditorText) {
|
|
23
|
+
const { nodes, setNodes, setEdges } = lab;
|
|
24
|
+
return {
|
|
25
|
+
updateMachine: (id: string, patch: Partial<MachineData>) => {
|
|
26
|
+
const next = nodes.map((n) => (n.id === id && isMachine(n) ? { ...n, data: { ...n.data, ...patch } } : n));
|
|
27
|
+
setNodes(next);
|
|
28
|
+
// Turning a router into a real one (or back) changes its connection points: its cables move
|
|
29
|
+
// onto free ports, or back onto its single port.
|
|
30
|
+
if ("kind" in patch) setEdges((es) => migrateEdges(es, next));
|
|
31
|
+
},
|
|
32
|
+
updateCable: (id: string, patch: Partial<CableData>) => {
|
|
33
|
+
setEdges((es) => es.map((e) => (e.id === id && e.data ? { ...e, data: { ...e.data, ...patch } } : e)));
|
|
34
|
+
},
|
|
35
|
+
loadExample: (make: () => Draft = exampleLab) => {
|
|
36
|
+
replaceLab(lab, make());
|
|
37
|
+
fitWhenDrawn();
|
|
38
|
+
},
|
|
39
|
+
clearLab: () => {
|
|
40
|
+
setNodes([]);
|
|
41
|
+
setEdges([]);
|
|
42
|
+
},
|
|
43
|
+
/** A GNS3 project opens as a lab (Undo brings back the one that was there). */
|
|
44
|
+
importGns3: async (file: File) => {
|
|
45
|
+
try {
|
|
46
|
+
const { lab: opened, warnings } = fromGns3(await file.text(), t.gns3);
|
|
47
|
+
replaceLab(lab, opened);
|
|
48
|
+
fitWhenDrawn({ padding: 0.3, maxZoom: 1 });
|
|
49
|
+
const machines = opened.nodes.filter(isMachine).length;
|
|
50
|
+
const lans = opened.nodes.filter(isLan).length;
|
|
51
|
+
setToast(`${t.toasts.gns3Opened(opened.name, machines, lans)}${warnings.length ? ` ${warnings.join(" ")}` : ""} ${t.toasts.undoBringsBack}`);
|
|
52
|
+
} catch (err) {
|
|
53
|
+
setToast(err instanceof Error ? err.message : t.toasts.gns3Unreadable);
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { useEffect, useEffectEvent } from "react";
|
|
2
|
+
import { ownFiles, type Engine, type Plan } from "@/lib/tool/engine";
|
|
3
|
+
import type { EditorText } from "@/lib/tool/text";
|
|
4
|
+
import type { DesktopState, LiveLab, RunState, useAutoStart, useRunPanel } from "./use-desktop";
|
|
5
|
+
import type { Captures } from "./use-captures";
|
|
6
|
+
|
|
7
|
+
interface Deps {
|
|
8
|
+
state: DesktopState;
|
|
9
|
+
panel: ReturnType<typeof useRunPanel>;
|
|
10
|
+
autoStart: ReturnType<typeof useAutoStart>;
|
|
11
|
+
live: LiveLab;
|
|
12
|
+
captures: Captures;
|
|
13
|
+
lab: { labName: string; topology: string; plan: Plan | null; engine: Engine | null; hasVms: boolean };
|
|
14
|
+
t: EditorText;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Running the lab on this machine (desktop): `lab_up` from the engine's spec, and `lab_down`. */
|
|
18
|
+
export function useLabRun({ state, panel, autoStart, live, captures, lab, t }: Deps) {
|
|
19
|
+
const { invoke, run, setRun } = state;
|
|
20
|
+
async function runLab() {
|
|
21
|
+
if (!invoke || !lab.plan?.isoloom_yml) return;
|
|
22
|
+
setRun("starting");
|
|
23
|
+
panel.setRunOpen(true);
|
|
24
|
+
panel.setRunLog(lab.hasVms ? t.run.startingVmsLog : t.run.startingLog);
|
|
25
|
+
try {
|
|
26
|
+
const files = lab.engine ? ownFiles(lab.engine, lab.topology) : [];
|
|
27
|
+
panel.setRunLog(await invoke<string>("lab_up", { name: lab.labName, yaml: lab.plan.isoloom_yml, files }));
|
|
28
|
+
setRun("running");
|
|
29
|
+
} catch (e) {
|
|
30
|
+
panel.setRunLog(String(e));
|
|
31
|
+
setRun("stopped");
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
async function stopLab() {
|
|
35
|
+
if (!invoke) return;
|
|
36
|
+
setRun("stopping");
|
|
37
|
+
try {
|
|
38
|
+
panel.setRunLog(await invoke<string>("lab_down", { name: lab.labName }));
|
|
39
|
+
} catch (e) {
|
|
40
|
+
panel.setRunLog(String(e));
|
|
41
|
+
}
|
|
42
|
+
captures.clearCaptures();
|
|
43
|
+
live.setLinks([]);
|
|
44
|
+
live.setLive(null);
|
|
45
|
+
setRun("stopped");
|
|
46
|
+
}
|
|
47
|
+
// The auto-start waits for the engine: the lab runs from the spec it makes.
|
|
48
|
+
const spec = lab.plan?.isoloom_yml;
|
|
49
|
+
const { autoStartPending, setAutoStartPending } = autoStart;
|
|
50
|
+
const startPending = useEffectEvent((current: RunState) => {
|
|
51
|
+
setAutoStartPending(false);
|
|
52
|
+
if (current === "stopped") void runLab();
|
|
53
|
+
});
|
|
54
|
+
useEffect(() => {
|
|
55
|
+
if (!autoStartPending || !spec) return;
|
|
56
|
+
queueMicrotask(() => {
|
|
57
|
+
startPending(run);
|
|
58
|
+
});
|
|
59
|
+
}, [autoStartPending, spec, run]);
|
|
60
|
+
return { runLab, stopLab };
|
|
61
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { useEffect, useState } from "react";
|
|
2
|
+
import { useEdgesState, useNodesState } from "@xyflow/react";
|
|
3
|
+
import type { CableEdge, Draft, LabNode } from "@/lib/tool/model";
|
|
4
|
+
import { loadDraft, saveDraft } from "@/lib/tool/storage";
|
|
5
|
+
|
|
6
|
+
/** The lab on the canvas: its name, nodes and cables, kept in this browser as it changes. */
|
|
7
|
+
export function useLab() {
|
|
8
|
+
// The last lab drawn in this browser, or a blank canvas (the example is one click away). The
|
|
9
|
+
// editor renders on the client only, so reading storage here can't mismatch the server's HTML.
|
|
10
|
+
const [initial] = useState<Draft>(() => loadDraft() ?? { name: "my-lab", nodes: [], edges: [] });
|
|
11
|
+
const [labName, setLabName] = useState(initial.name);
|
|
12
|
+
const [nodes, setNodes, onNodesChange] = useNodesState<LabNode>(initial.nodes);
|
|
13
|
+
const [edges, setEdges, onEdgesChange] = useEdgesState<CableEdge>(initial.edges);
|
|
14
|
+
return { initial, labName, setLabName, nodes, setNodes, onNodesChange, edges, setEdges, onEdgesChange };
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export type Lab = ReturnType<typeof useLab>;
|
|
18
|
+
|
|
19
|
+
/** Saves the lab as it changes. */
|
|
20
|
+
export function useDraftSaving(lab: Lab): void {
|
|
21
|
+
const { labName, nodes, edges } = lab;
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
saveDraft({ name: labName, nodes, edges });
|
|
24
|
+
}, [labName, nodes, edges]);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Replaces the whole lab (an example, a shared link, a project, a GNS3 import). */
|
|
28
|
+
export function replaceLab(lab: Lab, next: Draft): void {
|
|
29
|
+
lab.setLabName(next.name);
|
|
30
|
+
lab.setNodes(next.nodes);
|
|
31
|
+
lab.setEdges(next.edges);
|
|
32
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { useRef } from "react";
|
|
2
|
+
import type { XYPosition } from "@xyflow/react";
|
|
3
|
+
import { lansAround, within } from "@/lib/tool/geometry";
|
|
4
|
+
import { SWITCH_H, SWITCH_W, isLan, isMachine, isNote, isSwitch, type LabNode, type LanNode } from "@/lib/tool/model";
|
|
5
|
+
import type { Lab } from "./use-lab";
|
|
6
|
+
|
|
7
|
+
/** What moves with a LAN: machines inside (or on the edge, routers), switches on its edge or inside it, notes in it. */
|
|
8
|
+
function lanMembers(lan: LanNode, nodes: LabNode[]): Map<string, XYPosition> {
|
|
9
|
+
const r = { x: lan.position.x, y: lan.position.y, w: lan.width ?? 420, h: lan.height ?? 300 };
|
|
10
|
+
const inside = (cx: number, cy: number) => within(r, { x: cx, y: cy }, 0.5);
|
|
11
|
+
const carried = (n: LabNode) =>
|
|
12
|
+
(isMachine(n) && lansAround(n, nodes).some((l) => l.id === lan.id)) ||
|
|
13
|
+
(isSwitch(n) && inside(n.position.x + SWITCH_W / 2, n.position.y + SWITCH_H / 2)) ||
|
|
14
|
+
(isNote(n) && inside(n.position.x + (n.width ?? 200) / 2, n.position.y + (n.height ?? 90) / 2));
|
|
15
|
+
return new Map(nodes.filter(carried).map((m) => [m.id, { ...m.position }]));
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Moving a LAN carries the machines inside it, as Excalidraw frames do. */
|
|
19
|
+
export function useLanCarry(lab: Lab) {
|
|
20
|
+
const { nodes, setNodes } = lab;
|
|
21
|
+
const carried = useRef<{ lan: string; start: XYPosition; members: Map<string, XYPosition> } | null>(null);
|
|
22
|
+
return {
|
|
23
|
+
onNodeDragStart: (_: unknown, node: LabNode) => {
|
|
24
|
+
if (!isLan(node)) return;
|
|
25
|
+
carried.current = { lan: node.id, start: { ...node.position }, members: lanMembers(node, nodes) };
|
|
26
|
+
},
|
|
27
|
+
onNodeDrag: (_: unknown, node: LabNode) => {
|
|
28
|
+
const c = carried.current;
|
|
29
|
+
if (!c || c.lan !== node.id) return;
|
|
30
|
+
const dx = node.position.x - c.start.x;
|
|
31
|
+
const dy = node.position.y - c.start.y;
|
|
32
|
+
setNodes((ns) =>
|
|
33
|
+
ns.map((n) => {
|
|
34
|
+
const from = c.members.get(n.id);
|
|
35
|
+
return from ? { ...n, position: { x: from.x + dx, y: from.y + dy } } : n;
|
|
36
|
+
}),
|
|
37
|
+
);
|
|
38
|
+
},
|
|
39
|
+
onNodeDragStop: () => {
|
|
40
|
+
carried.current = null;
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
import type { Invoke, SuspendedLink } from "@/lib/tool/desktop";
|
|
3
|
+
import type { EditorText } from "@/lib/tool/text";
|
|
4
|
+
import type { LiveLab } from "./use-desktop";
|
|
5
|
+
|
|
6
|
+
export type MachineAction = "start" | "stop" | "restart";
|
|
7
|
+
export type LinkAction = "suspend" | "resume";
|
|
8
|
+
|
|
9
|
+
interface Deps {
|
|
10
|
+
invoke: Invoke | null;
|
|
11
|
+
labName: string;
|
|
12
|
+
live: LiveLab;
|
|
13
|
+
setToast: (message: string) => void;
|
|
14
|
+
t: EditorText;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* One machine of the running lab (desktop): a console, its screen, starting or stopping it, its
|
|
19
|
+
* snapshots, and cutting or reconnecting its links. A failure shows as a toast.
|
|
20
|
+
*/
|
|
21
|
+
export function useMachineActions({ invoke, labName, live, setToast, t }: Deps) {
|
|
22
|
+
const [busy, setBusy] = useState(false);
|
|
23
|
+
/** Runs a command for this lab while the machine controls wait, then reads the lab again. */
|
|
24
|
+
async function act(run: (invoke: Invoke) => Promise<unknown>) {
|
|
25
|
+
if (!invoke) return;
|
|
26
|
+
setBusy(true);
|
|
27
|
+
try {
|
|
28
|
+
await run(invoke);
|
|
29
|
+
} catch (e) {
|
|
30
|
+
setToast(String(e));
|
|
31
|
+
}
|
|
32
|
+
setBusy(false);
|
|
33
|
+
void live.refreshStates();
|
|
34
|
+
}
|
|
35
|
+
/** Opens a console on the machine, or its screen (GNS3's VNC console: its VNC server, or a VM's display). */
|
|
36
|
+
async function open(command: "machine_console" | "machine_display", machine: string) {
|
|
37
|
+
if (!invoke) return;
|
|
38
|
+
try {
|
|
39
|
+
const shown = await invoke<string>(command, { name: labName, machine });
|
|
40
|
+
if (command === "machine_display") setToast(t.machineRun.displayOpened(shown));
|
|
41
|
+
} catch (e) {
|
|
42
|
+
setToast(String(e));
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return {
|
|
46
|
+
busy,
|
|
47
|
+
snapshotDo: (command: string, args: Record<string, unknown>) => act((run) => run(command, { name: labName, ...args })),
|
|
48
|
+
machineAct: (machine: string, action: MachineAction) => act((run) => run("machine_action", { name: labName, machine, action })),
|
|
49
|
+
/** Cuts or reconnects the ports of `machines` on `network` (every machine's, when not named). */
|
|
50
|
+
linkAct: (network: string, machines: string[] | undefined, action: LinkAction) =>
|
|
51
|
+
act(async (run) => {
|
|
52
|
+
live.setLinks(await run<SuspendedLink[]>("link_action", { name: labName, network, machines, action }));
|
|
53
|
+
}),
|
|
54
|
+
machineConsole: (machine: string) => open("machine_console", machine),
|
|
55
|
+
machineDisplay: (machine: string) => open("machine_display", machine),
|
|
56
|
+
};
|
|
57
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import type { DragEvent, MouseEvent, RefObject } from "react";
|
|
2
|
+
import type { XYPosition } from "@xyflow/react";
|
|
3
|
+
import { LAN_TOOL, NOTE_TOOL, SWITCH_TOOL } from "@/lib/tool/blocks";
|
|
4
|
+
import { newLan, newNote } from "@/lib/tool/create";
|
|
5
|
+
import { isLan, unselected, type LabNode, type LanNode } from "@/lib/tool/model";
|
|
6
|
+
import { paletteSpot, placeAppliance, placeMachine, placeSwitch } from "@/lib/tool/placement";
|
|
7
|
+
import type { Preset } from "@/lib/tool/presets";
|
|
8
|
+
import type { Lab } from "./use-lab";
|
|
9
|
+
import type { Tools } from "./use-tools";
|
|
10
|
+
|
|
11
|
+
/** The drag-and-drop type of a device dragged from the toolbar. */
|
|
12
|
+
export const PRESET_MIME = "application/x-isothrum-preset";
|
|
13
|
+
|
|
14
|
+
interface Deps {
|
|
15
|
+
lab: Lab;
|
|
16
|
+
lans: LanNode[];
|
|
17
|
+
tools: Tools;
|
|
18
|
+
presets: Preset[];
|
|
19
|
+
canvasRef: RefObject<HTMLDivElement | null>;
|
|
20
|
+
screenToFlowPosition: (p: XYPosition) => XYPosition;
|
|
21
|
+
/** Closes the right-click menus (a click on the canvas does). */
|
|
22
|
+
closeMenus: () => void;
|
|
23
|
+
/** Opens a dropped GNS3 project. */
|
|
24
|
+
importGns3: (file: File) => void;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Placing devices, LANs, switches and notes: by a click on the canvas with a tool, or a drop. */
|
|
28
|
+
export function usePlacement({ lab, lans, tools, presets, canvasRef, screenToFlowPosition, closeMenus, importGns3 }: Deps) {
|
|
29
|
+
const { setNodes } = lab;
|
|
30
|
+
function addPreset(presetId: string, at?: XYPosition) {
|
|
31
|
+
const preset = presets.find((p) => p.id === presetId);
|
|
32
|
+
if (!preset) return;
|
|
33
|
+
// A click in the palette drops the machine in the middle of the view.
|
|
34
|
+
const viewCenter = () => {
|
|
35
|
+
const r = canvasRef.current?.getBoundingClientRect();
|
|
36
|
+
return r ? screenToFlowPosition({ x: r.left + r.width / 2, y: r.top + r.height / 2 }) : { x: 0, y: 0 };
|
|
37
|
+
};
|
|
38
|
+
const position = at ?? paletteSpot(viewCenter(), lab.nodes);
|
|
39
|
+
if (preset.data.icon === "router" || preset.data.icon === "shield") setNodes((ns) => placeAppliance(preset, position, !!at, ns, lans));
|
|
40
|
+
else setNodes((ns) => placeMachine(preset, position, ns));
|
|
41
|
+
}
|
|
42
|
+
/** Places the active tool, centered on the pointer; then back to selecting, as Excalidraw does after a shape. */
|
|
43
|
+
function place(tool: string, e: MouseEvent) {
|
|
44
|
+
const at = () => screenToFlowPosition({ x: e.clientX, y: e.clientY });
|
|
45
|
+
// LANs go first in the list, so they are drawn under the machines; notes on top of everything, so they can sit over a LAN.
|
|
46
|
+
if (tool === LAN_TOOL) {
|
|
47
|
+
const center = at();
|
|
48
|
+
setNodes((ns) => [{ ...newLan(center, ns), selected: true }, ...unselected(ns)]);
|
|
49
|
+
} else if (tool === NOTE_TOOL) {
|
|
50
|
+
const { x, y } = at();
|
|
51
|
+
setNodes((ns) => [...unselected(ns), newNote({ x: x - 100, y: y - 45 })]);
|
|
52
|
+
} else if (tool === SWITCH_TOOL) {
|
|
53
|
+
const center = at();
|
|
54
|
+
setNodes((ns) => placeSwitch(center, ns, lans));
|
|
55
|
+
} else addPreset(tool, screenToFlowPosition({ x: e.clientX - 60, y: e.clientY - 58 }));
|
|
56
|
+
}
|
|
57
|
+
function onPaneClick(e: MouseEvent) {
|
|
58
|
+
closeMenus();
|
|
59
|
+
if (!tools.activeTool) return;
|
|
60
|
+
place(tools.activeTool, e);
|
|
61
|
+
tools.setActiveTool(null);
|
|
62
|
+
}
|
|
63
|
+
return {
|
|
64
|
+
onPaneClick,
|
|
65
|
+
// Inside a LAN the click lands on the area, not the canvas: placing there is the common case.
|
|
66
|
+
onNodeClick: (e: MouseEvent, node: LabNode) => {
|
|
67
|
+
if (tools.activeTool && isLan(node)) onPaneClick(e);
|
|
68
|
+
},
|
|
69
|
+
onDrop: (e: DragEvent) => {
|
|
70
|
+
e.preventDefault();
|
|
71
|
+
const file = [...e.dataTransfer.files].find((f) => f.name.endsWith(".gns3"));
|
|
72
|
+
if (file) {
|
|
73
|
+
importGns3(file);
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
const id = e.dataTransfer.getData(PRESET_MIME);
|
|
77
|
+
if (id) addPreset(id, screenToFlowPosition({ x: e.clientX - 60, y: e.clientY - 58 }));
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
}
|