@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,106 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import type { ReactNode } from "react";
|
|
4
|
+
import { Hand, MousePointer2, Network, StickyNote } from "lucide-react";
|
|
5
|
+
import { LAN_TOOL, NOTE_TOOL, hintFor } from "@/lib/tool/blocks";
|
|
6
|
+
import { cn } from "@/lib/utils";
|
|
7
|
+
import { DeviceBlock } from "./device-block";
|
|
8
|
+
import type { EditorModel } from "./hooks/use-editor";
|
|
9
|
+
import type { Mode } from "./hooks/use-tools";
|
|
10
|
+
|
|
11
|
+
const KEY = "absolute bottom-0 left-0.5 font-mono text-[9px] text-muted-foreground";
|
|
12
|
+
const TOOL = "relative grid size-10 shrink-0 place-items-center rounded-lg transition-colors hover:bg-muted";
|
|
13
|
+
const PICKED = "bg-brand-100 ring-1 ring-signal hover:bg-brand-100 dark:bg-brand-950";
|
|
14
|
+
|
|
15
|
+
const Separator = () => <span className="mx-1 h-6 w-px shrink-0 bg-border" aria-hidden />;
|
|
16
|
+
|
|
17
|
+
/** Hand and selection, Excalidraw's two modes. */
|
|
18
|
+
function ModeButtons({ e }: { e: EditorModel }) {
|
|
19
|
+
const t = e.t.toolbar;
|
|
20
|
+
const { tools } = e;
|
|
21
|
+
const modes: [Mode, typeof Hand, string, string][] = [
|
|
22
|
+
["hand", Hand, t.hand, "H"],
|
|
23
|
+
["select", MousePointer2, t.select, "V"],
|
|
24
|
+
];
|
|
25
|
+
return modes.map(([m, Icon, label, key]) => {
|
|
26
|
+
const on = tools.mode === m && !tools.activeTool;
|
|
27
|
+
return (
|
|
28
|
+
<button
|
|
29
|
+
key={m}
|
|
30
|
+
type="button"
|
|
31
|
+
onClick={() => { tools.setModeOnly(m); }}
|
|
32
|
+
aria-pressed={on}
|
|
33
|
+
aria-label={label}
|
|
34
|
+
title={`${label} · ${key}`}
|
|
35
|
+
className={cn(TOOL, on && "bg-brand-100 text-brand-700 hover:bg-brand-100 dark:bg-brand-950 dark:text-brand-300")}
|
|
36
|
+
>
|
|
37
|
+
<Icon className="size-[18px]" aria-hidden />
|
|
38
|
+
<span className={KEY}>{key}</span>
|
|
39
|
+
</button>
|
|
40
|
+
);
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** A tool of its own (a LAN, a note): picked, it is the next thing a click places. */
|
|
45
|
+
function ToolButton({ e, tool, label, title, letter, children }: { e: EditorModel; tool: string; label: string; title: string; letter: string; children: ReactNode }) {
|
|
46
|
+
const { tools } = e;
|
|
47
|
+
return (
|
|
48
|
+
<button
|
|
49
|
+
type="button"
|
|
50
|
+
onClick={() => { tools.pick(tool); }}
|
|
51
|
+
aria-pressed={tools.activeTool === tool}
|
|
52
|
+
aria-label={label}
|
|
53
|
+
title={`${title} · ${letter}`}
|
|
54
|
+
className={cn(TOOL, tools.activeTool === tool && PICKED)}
|
|
55
|
+
>
|
|
56
|
+
{children}
|
|
57
|
+
<span className={KEY}>{letter}</span>
|
|
58
|
+
</button>
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** The blocks from `from` to `to` (the network ones, then the machines). */
|
|
63
|
+
function Blocks({ e, from, to }: { e: EditorModel; from: number; to?: number }) {
|
|
64
|
+
const { devices, tools } = e;
|
|
65
|
+
return devices.blocks.slice(from, to).map((b) => (
|
|
66
|
+
<DeviceBlock key={b.id} block={b} choice={devices.blockChoice[b.id]} active={tools.activeTool} onPick={(tool) => { devices.pickFromBlock(b.id, tool); }} onForget={devices.forgetDevice} />
|
|
67
|
+
));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Top center: the devices, grouped as network, containers, VMs; the line under it says what a click does. */
|
|
71
|
+
export function Toolbar({ e }: { e: EditorModel }) {
|
|
72
|
+
const t = e.t;
|
|
73
|
+
const { tools, lab } = e;
|
|
74
|
+
const hint = hintFor(tools.activeTool, e.tool?.label, { hand: tools.mode === "hand", nodes: lab.nodes.length, edges: lab.edges.length }, t);
|
|
75
|
+
return (
|
|
76
|
+
<div className="pointer-events-none absolute inset-x-0 top-3 z-10 flex flex-col items-center gap-2 px-16 max-sm:items-start max-sm:pl-16">
|
|
77
|
+
<div role="toolbar" aria-label={t.toolbar.devices} className="pointer-events-auto flex max-w-full items-center gap-0.5 overflow-x-auto rounded-xl border bg-popover p-1 shadow-[0_4px_20px_-6px_rgb(0_0_0/0.15)]">
|
|
78
|
+
<ModeButtons e={e} />
|
|
79
|
+
<Separator />
|
|
80
|
+
{/* The devices as elementary blocks: a LAN, a switch, a router, a firewall, a
|
|
81
|
+
container, a VM (and the devices saved from the canvas). A block places the kind
|
|
82
|
+
picked last; its arrow lists the kinds. */}
|
|
83
|
+
<div className="flex items-center gap-0.5">
|
|
84
|
+
<ToolButton e={e} tool={LAN_TOOL} label={t.toolbar.lan} title={t.toolbar.lanTitle} letter="L">
|
|
85
|
+
<span className="grid size-8 place-items-center rounded-md border border-dashed border-foreground/40 bg-pastel-blue">
|
|
86
|
+
<Network className="size-4" aria-hidden />
|
|
87
|
+
</span>
|
|
88
|
+
</ToolButton>
|
|
89
|
+
<Blocks e={e} from={0} to={3} />
|
|
90
|
+
</div>
|
|
91
|
+
<Separator />
|
|
92
|
+
<div className="flex items-center gap-0.5">
|
|
93
|
+
<Blocks e={e} from={3} />
|
|
94
|
+
</div>
|
|
95
|
+
<div className="flex items-center gap-0.5">
|
|
96
|
+
<Separator />
|
|
97
|
+
<ToolButton e={e} tool={NOTE_TOOL} label={t.toolbar.note} title={t.toolbar.noteTitle} letter="T">
|
|
98
|
+
<StickyNote className="size-5" />
|
|
99
|
+
</ToolButton>
|
|
100
|
+
</div>
|
|
101
|
+
</div>
|
|
102
|
+
{/* On the empty canvas the welcome's handwritten hints say it instead. */}
|
|
103
|
+
{(!e.empty || tools.activeTool) && <p className="pointer-events-none hidden text-center text-xs text-muted-foreground sm:block">{hint}</p>}
|
|
104
|
+
</div>
|
|
105
|
+
);
|
|
106
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { CircleAlert, CircleCheck, Download, Play, Square } from "lucide-react";
|
|
4
|
+
import { vlanExample } from "@/lib/tool/examples";
|
|
5
|
+
import { cn } from "@/lib/utils";
|
|
6
|
+
import { Button } from "@/components/ui/button";
|
|
7
|
+
import { EditorMenu } from "./editor-menu";
|
|
8
|
+
import { copyShareLink } from "./hooks/use-shared-link";
|
|
9
|
+
import type { EditorModel } from "./hooks/use-editor";
|
|
10
|
+
|
|
11
|
+
/** Top left: the menu, and where the lab is saved. */
|
|
12
|
+
export function TopLeft({ e, onImportGns3 }: { e: EditorModel; onImportGns3: () => void }) {
|
|
13
|
+
const { projects, actions, lab, design, t } = e;
|
|
14
|
+
const { proj } = projects;
|
|
15
|
+
const share = () => copyShareLink({ name: lab.labName, nodes: lab.nodes, edges: lab.edges, design: design.design }, e.lang, e.setToast, t);
|
|
16
|
+
const projectActions = projects.signedIn ? { onSave: () => void proj.save(), onSaveAs: () => void projects.saveAs(), onOpen: () => { projects.setProjectsOpen(true); } } : null;
|
|
17
|
+
return (
|
|
18
|
+
<div className="absolute top-3 left-3 z-10 flex items-center gap-2">
|
|
19
|
+
<EditorMenu
|
|
20
|
+
lang={e.lang}
|
|
21
|
+
onExample={() => { actions.loadExample(); }}
|
|
22
|
+
onVlanExample={() => { actions.loadExample(vlanExample); }}
|
|
23
|
+
onShare={() => void share()}
|
|
24
|
+
onImportGns3={onImportGns3}
|
|
25
|
+
onClear={actions.clearLab}
|
|
26
|
+
onFit={() => void e.flow.fitView({ padding: 0.4, duration: 300 })}
|
|
27
|
+
canClear={!e.empty}
|
|
28
|
+
design={design.design}
|
|
29
|
+
onDesign={design.chooseDesign}
|
|
30
|
+
accounts={e.accounts}
|
|
31
|
+
projects={projectActions}
|
|
32
|
+
/>
|
|
33
|
+
{projects.signedIn && proj.project && (
|
|
34
|
+
<span role="status" className={cn("pointer-events-none max-w-56 truncate rounded-md px-1.5 text-xs text-muted-foreground max-sm:hidden", proj.state === "error" && "text-destructive")}>
|
|
35
|
+
{t.projects.status[proj.state](proj.project.name)}
|
|
36
|
+
</span>
|
|
37
|
+
)}
|
|
38
|
+
</div>
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** The lab's status: valid, or how many things to fix (the engine still loading). */
|
|
43
|
+
function StatusButton({ e }: { e: EditorModel }) {
|
|
44
|
+
const { issues, t } = e;
|
|
45
|
+
const label = !e.engine ? t.status.loading : issues.length ? t.status.toFix(issues.length) : t.status.valid;
|
|
46
|
+
return (
|
|
47
|
+
<button type="button" onClick={() => { e.setStatusOpen((o) => !o); }} className={cn("flex h-9 items-center gap-1.5 rounded-lg border bg-popover px-3 text-xs font-medium shadow-sm", issues.length ? "text-warning" : "text-success")}>
|
|
48
|
+
{issues.length ? <CircleAlert className="size-4" /> : <CircleCheck className="size-4" />}
|
|
49
|
+
<span className="max-sm:sr-only">{label}</span>
|
|
50
|
+
</button>
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Desktop app: run the lab on this machine, or stop it. */
|
|
55
|
+
function RunButton({ e }: { e: EditorModel }) {
|
|
56
|
+
const { t, hasVms } = e;
|
|
57
|
+
const { run, info } = e.desk.state;
|
|
58
|
+
if (run === "running" || run === "stopping") {
|
|
59
|
+
return (
|
|
60
|
+
<Button variant="outline" onClick={() => void e.run.stopLab()} disabled={run === "stopping"} className="h-9 bg-popover shadow-sm">
|
|
61
|
+
<Square data-icon="inline-start" /> {run === "stopping" ? t.run.stopping : t.run.stop}
|
|
62
|
+
</Button>
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
const vagrant = !!info?.vagrant;
|
|
66
|
+
return (
|
|
67
|
+
<Button
|
|
68
|
+
variant="outline"
|
|
69
|
+
onClick={() => void e.run.runLab()}
|
|
70
|
+
disabled={run === "starting" || !e.plan?.ok || e.issues.length > 0 || e.empty || (hasVms && !vagrant)}
|
|
71
|
+
title={hasVms ? (vagrant ? t.run.runVmsTitle : t.run.vmsTitle) : t.run.runTitle}
|
|
72
|
+
className="h-9 bg-popover shadow-sm"
|
|
73
|
+
>
|
|
74
|
+
<Play data-icon="inline-start" /> {run === "starting" ? t.run.starting : t.run.run}
|
|
75
|
+
</Button>
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Top right: the lab's status, Run (desktop app), and Export. */
|
|
80
|
+
export function TopRight({ e }: { e: EditorModel }) {
|
|
81
|
+
return (
|
|
82
|
+
<div className="absolute top-3 right-3 z-10 flex items-center gap-2">
|
|
83
|
+
{!e.empty && <StatusButton e={e} />}
|
|
84
|
+
{e.invoke && <RunButton e={e} />}
|
|
85
|
+
<Button onClick={() => { e.setExportOpen((o) => !o); }} disabled={!e.engine || e.empty} className="h-9 shadow-sm">
|
|
86
|
+
<Download data-icon="inline-start" /> {e.t.export}
|
|
87
|
+
</Button>
|
|
88
|
+
</div>
|
|
89
|
+
);
|
|
90
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { withCondition } from "@/lib/tool/conditions";
|
|
4
|
+
import type { LinkConditions } from "@/lib/tool/model";
|
|
5
|
+
import { cn } from "@/lib/utils";
|
|
6
|
+
import { useText } from "../text";
|
|
7
|
+
import { field } from "./fields";
|
|
8
|
+
|
|
9
|
+
/** How a cable or a LAN behaves: four optional numbers (empty: none). */
|
|
10
|
+
export function ConditionsFields({ value, onChange }: { value: LinkConditions | undefined; onChange: (tc: LinkConditions | undefined) => void }) {
|
|
11
|
+
const t = useText().windows.conditions;
|
|
12
|
+
const fields: [keyof LinkConditions, string, number][] = [
|
|
13
|
+
["delay", t.delay, 1],
|
|
14
|
+
["jitter", t.jitter, 1],
|
|
15
|
+
["loss", t.loss, 0.1],
|
|
16
|
+
["rate", t.rate, 1],
|
|
17
|
+
];
|
|
18
|
+
return (
|
|
19
|
+
<fieldset className="flex flex-col gap-2">
|
|
20
|
+
<legend className="mb-1 text-muted-foreground">{t.title}</legend>
|
|
21
|
+
<div className="grid grid-cols-2 gap-2">
|
|
22
|
+
{fields.map(([key, label, step]) => (
|
|
23
|
+
<label key={key} className="flex flex-col gap-1">
|
|
24
|
+
<span className="text-xs text-muted-foreground">{label}</span>
|
|
25
|
+
<input
|
|
26
|
+
type="number"
|
|
27
|
+
min={0}
|
|
28
|
+
step={step}
|
|
29
|
+
inputMode="decimal"
|
|
30
|
+
className={cn(field, "font-mono")}
|
|
31
|
+
value={value?.[key] ?? ""}
|
|
32
|
+
disabled={key === "jitter" && !value?.delay}
|
|
33
|
+
onChange={(e) => {
|
|
34
|
+
onChange(withCondition(value, key, e.target.value));
|
|
35
|
+
}}
|
|
36
|
+
/>
|
|
37
|
+
</label>
|
|
38
|
+
))}
|
|
39
|
+
</div>
|
|
40
|
+
<p className="text-xs text-muted-foreground">{t.hint}</p>
|
|
41
|
+
</fieldset>
|
|
42
|
+
);
|
|
43
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { cleanName } from "@/lib/tool/model";
|
|
4
|
+
import { ExportPanel } from "../export/export-panel";
|
|
5
|
+
import { FloatingWindow } from "../floating-window";
|
|
6
|
+
import type { EditorModel } from "../hooks/use-editor";
|
|
7
|
+
import { ProjectsList } from "../projects-list";
|
|
8
|
+
import { RunWindow, SelectionWindow, StatusWindow } from "./lab-windows";
|
|
9
|
+
import { CableWindow, LanWindow, MachineWindow } from "./selected-windows";
|
|
10
|
+
|
|
11
|
+
/** The user's projects (signed in): open one, or one of its earlier versions. */
|
|
12
|
+
function ProjectsWindow({ e }: { e: EditorModel }) {
|
|
13
|
+
const { projects } = e;
|
|
14
|
+
return (
|
|
15
|
+
<FloatingWindow title={e.t.projects.windowTitle} side="left" width={340} onClose={() => { projects.setProjectsOpen(false); }}>
|
|
16
|
+
<div className="p-4">
|
|
17
|
+
<ProjectsList
|
|
18
|
+
current={projects.proj.project}
|
|
19
|
+
onOpen={(p, d) => {
|
|
20
|
+
projects.openProject(p, d);
|
|
21
|
+
projects.setProjectsOpen(false);
|
|
22
|
+
}}
|
|
23
|
+
onOpenVersion={(d) => { projects.openDraft(d); }}
|
|
24
|
+
onDeleted={(id) => {
|
|
25
|
+
if (projects.proj.project?.id === id) projects.proj.detach();
|
|
26
|
+
}}
|
|
27
|
+
/>
|
|
28
|
+
</div>
|
|
29
|
+
</FloatingWindow>
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Export: the lab's name, its zip and isoloom.yml, and where it runs. */
|
|
34
|
+
function ExportWindow({ e }: { e: EditorModel }) {
|
|
35
|
+
const { plan, downloads, lab } = e;
|
|
36
|
+
return (
|
|
37
|
+
<FloatingWindow title={e.t.export} side="right" width={420} onClose={() => { e.setExportOpen(false); }}>
|
|
38
|
+
{downloads.downloadError && (
|
|
39
|
+
<p role="alert" className="px-4 pt-4 text-sm text-destructive">
|
|
40
|
+
{downloads.downloadError}
|
|
41
|
+
</p>
|
|
42
|
+
)}
|
|
43
|
+
<ExportPanel
|
|
44
|
+
labName={lab.labName}
|
|
45
|
+
onLabNameChange={(n) => { lab.setLabName(cleanName(n)); }}
|
|
46
|
+
mixed={e.mixed}
|
|
47
|
+
spec={plan?.isoloom_yml ?? null}
|
|
48
|
+
files={plan?.ok ? plan.files : []}
|
|
49
|
+
issues={e.issues}
|
|
50
|
+
onDownloadAll={downloads.downloadAll}
|
|
51
|
+
onDownloadSpec={downloads.downloadSpec}
|
|
52
|
+
onDownloadTarget={downloads.downloadTarget}
|
|
53
|
+
/>
|
|
54
|
+
</FloatingWindow>
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** The windows floating over the canvas: what is selected, projects, the lab's status, Run, Export. */
|
|
59
|
+
export function EditorWindows({ e }: { e: EditorModel }) {
|
|
60
|
+
const { selection, empty } = e;
|
|
61
|
+
return (
|
|
62
|
+
<>
|
|
63
|
+
{selection.machine && <MachineWindow key={`m-${selection.machine.id}`} e={e} node={selection.machine} />}
|
|
64
|
+
{selection.lan && <LanWindow key={`l-${selection.lan.id}`} e={e} lan={selection.lan} />}
|
|
65
|
+
{selection.edge?.data && <CableWindow key={`c-${selection.edge.id}`} e={e} edge={selection.edge} data={selection.edge.data} />}
|
|
66
|
+
{e.projects.projectsOpen && <ProjectsWindow e={e} />}
|
|
67
|
+
{selection.count > 1 && <SelectionWindow e={e} />}
|
|
68
|
+
{e.statusOpen && !empty && <StatusWindow e={e} />}
|
|
69
|
+
{e.invoke && e.desk.panel.runOpen && <RunWindow e={e} />}
|
|
70
|
+
{e.exportOpen && !empty && <ExportWindow e={e} />}
|
|
71
|
+
</>
|
|
72
|
+
);
|
|
73
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/** A text field in the editor's windows. */
|
|
2
|
+
export const field =
|
|
3
|
+
"h-8 w-full rounded-md border border-input bg-background px-2 text-sm outline-none focus-visible:ring-2 focus-visible:ring-ring";
|
|
4
|
+
|
|
5
|
+
/** A multi-line field (provisioning steps, an appliance's configuration). */
|
|
6
|
+
export const area = "w-full rounded-md border border-input bg-background px-2 py-1.5 font-mono text-xs outline-none focus-visible:ring-2 focus-visible:ring-ring";
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { machineStatus, type LabLive, type MachineLive, type Status, type SuspendedLink } from "@/lib/tool/desktop";
|
|
4
|
+
import { cn } from "@/lib/utils";
|
|
5
|
+
import { useText } from "../text";
|
|
6
|
+
|
|
7
|
+
const DOT: Record<Status, string> = { up: "bg-success", starting: "bg-warning", unhealthy: "bg-destructive", down: "bg-muted-foreground/50" };
|
|
8
|
+
|
|
9
|
+
type Port = LabLive["ports"][number];
|
|
10
|
+
|
|
11
|
+
/** A port of a running machine: its network (and interface), green when up, struck through when down. */
|
|
12
|
+
function PortChip({ p, cut }: { p: Port; cut: boolean }) {
|
|
13
|
+
const t = useText().canvas.live;
|
|
14
|
+
const title = p.up === null ? t.portUnknown : p.up ? t.portUp : cut ? t.cable.cut : t.portDown;
|
|
15
|
+
return (
|
|
16
|
+
<span
|
|
17
|
+
title={title}
|
|
18
|
+
className={cn("rounded-sm border px-1 font-mono text-[10px]", p.up === true && "border-success/40 text-success", p.up === false && "border-destructive/40 text-destructive line-through", p.up === null && "text-muted-foreground")}
|
|
19
|
+
>
|
|
20
|
+
{p.network}
|
|
21
|
+
{p.interface ? ` ${p.interface}` : ""}
|
|
22
|
+
</span>
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** One machine now: its state (and health), then its ports. */
|
|
27
|
+
function MachineLine({ name, l, live, cut }: { name: string; l: MachineLive; live: LabLive; cut: SuspendedLink[] }) {
|
|
28
|
+
const t = useText();
|
|
29
|
+
const status = machineStatus(l);
|
|
30
|
+
return (
|
|
31
|
+
<li className="flex flex-col gap-0.5">
|
|
32
|
+
<span className="flex items-center gap-1.5">
|
|
33
|
+
<span className={cn("size-2 shrink-0 rounded-full", DOT[status])} aria-hidden />
|
|
34
|
+
<span className="font-mono font-medium">{name}</span>
|
|
35
|
+
<span className="text-muted-foreground">
|
|
36
|
+
{t.canvas.live[status]}
|
|
37
|
+
{l.health && status !== "unhealthy" ? ` · ${l.health}` : ""}
|
|
38
|
+
</span>
|
|
39
|
+
</span>
|
|
40
|
+
<span className="flex flex-wrap gap-1 pl-3.5">
|
|
41
|
+
{live.ports
|
|
42
|
+
.filter((p) => p.machine === name)
|
|
43
|
+
.map((p) => (
|
|
44
|
+
<PortChip key={p.network} p={p} cut={cut.some((c) => c.machine === name && c.network === p.network)} />
|
|
45
|
+
))}
|
|
46
|
+
</span>
|
|
47
|
+
</li>
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Desktop only: the running lab now, machine by machine, with each one's ports. */
|
|
52
|
+
export function LabStatus({ live, cut }: { live: LabLive; cut: SuspendedLink[] }) {
|
|
53
|
+
const t = useText();
|
|
54
|
+
const machines = Object.entries(live.machines);
|
|
55
|
+
const up = machines.filter(([, l]) => machineStatus(l) === "up").length;
|
|
56
|
+
return (
|
|
57
|
+
<section className="flex flex-col gap-2">
|
|
58
|
+
<h3 className="text-xs font-medium text-muted-foreground">
|
|
59
|
+
{t.run.statusTitle(up, machines.length)} · {live.probed ? t.run.probed : t.run.inferred}
|
|
60
|
+
</h3>
|
|
61
|
+
<ul className="flex max-h-64 flex-col gap-1 overflow-auto rounded-md border bg-surface p-2 text-xs">
|
|
62
|
+
{machines.map(([name, l]) => (
|
|
63
|
+
<MachineLine key={name} name={name} l={l} live={live} cut={cut} />
|
|
64
|
+
))}
|
|
65
|
+
</ul>
|
|
66
|
+
</section>
|
|
67
|
+
);
|
|
68
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { Trash2 } from "lucide-react";
|
|
4
|
+
import { isLan, isNote, isSwitch, nodeName, unselected } from "@/lib/tool/model";
|
|
5
|
+
import { setAutoStarts } from "@/lib/tool/storage";
|
|
6
|
+
import { Button } from "@/components/ui/button";
|
|
7
|
+
import { FloatingWindow } from "../floating-window";
|
|
8
|
+
import type { EditorModel } from "../hooks/use-editor";
|
|
9
|
+
import { LabStatus } from "./lab-status";
|
|
10
|
+
|
|
11
|
+
/** Several things selected: what they are, and deleting them all. */
|
|
12
|
+
export function SelectionWindow({ e }: { e: EditorModel }) {
|
|
13
|
+
const { t, lab } = e;
|
|
14
|
+
const { selectedNodes, selectedEdges, count } = e.selection;
|
|
15
|
+
return (
|
|
16
|
+
<FloatingWindow
|
|
17
|
+
title={t.windows.selected(count)}
|
|
18
|
+
side="left"
|
|
19
|
+
width={280}
|
|
20
|
+
onClose={() => {
|
|
21
|
+
lab.setNodes((ns) => unselected(ns));
|
|
22
|
+
lab.setEdges((es) => unselected(es));
|
|
23
|
+
}}
|
|
24
|
+
>
|
|
25
|
+
<div className="flex flex-col gap-3 p-4 text-sm">
|
|
26
|
+
<p className="text-muted-foreground">{t.windows.selection(selectedNodes.length, selectedEdges.length)}</p>
|
|
27
|
+
{selectedNodes.length > 0 && (
|
|
28
|
+
<ul className="flex flex-wrap gap-1.5 font-mono text-xs">
|
|
29
|
+
{selectedNodes.map((n) => (
|
|
30
|
+
<li key={n.id} className="rounded-md border px-1.5 py-0.5">
|
|
31
|
+
{isNote(n) ? t.canvas.noteName : nodeName(n)}
|
|
32
|
+
</li>
|
|
33
|
+
))}
|
|
34
|
+
</ul>
|
|
35
|
+
)}
|
|
36
|
+
<Button variant="outline" size="sm" onClick={() => void e.flow.deleteElements({ nodes: selectedNodes.map((n) => ({ id: n.id })), edges: selectedEdges.map((c) => ({ id: c.id })) })}>
|
|
37
|
+
<Trash2 data-icon="inline-start" /> {t.windows.deleteSelection(count)}
|
|
38
|
+
</Button>
|
|
39
|
+
</div>
|
|
40
|
+
</FloatingWindow>
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** The lab at a glance: how many containers, VMs, LANs and cables, and what to fix. */
|
|
45
|
+
export function StatusWindow({ e }: { e: EditorModel }) {
|
|
46
|
+
const { t, lab, machines, issues } = e;
|
|
47
|
+
const vlans = new Set(lab.nodes.filter(isSwitch).flatMap((sw) => sw.data.vlans.map((v) => v.id)));
|
|
48
|
+
const counts = [
|
|
49
|
+
[t.lab.containers, machines.filter((n) => n.data.kind === "container").length],
|
|
50
|
+
[t.lab.vms, machines.filter((n) => n.data.kind === "vm").length],
|
|
51
|
+
[t.lab.lans, lab.nodes.filter(isLan).length + vlans.size],
|
|
52
|
+
[t.lab.cables, lab.edges.length],
|
|
53
|
+
] as const;
|
|
54
|
+
return (
|
|
55
|
+
<FloatingWindow title={t.lab.title} side="right" width={320} onClose={() => { e.setStatusOpen(false); }}>
|
|
56
|
+
<div className="flex flex-col gap-4 p-4 text-sm">
|
|
57
|
+
{/* Equal columns, unless a label (Contenedores) needs more: then it widens, unbroken. */}
|
|
58
|
+
<div className="grid grid-cols-[repeat(4,minmax(min-content,1fr))] gap-2 text-center">
|
|
59
|
+
{counts.map(([label, count]) => (
|
|
60
|
+
<div key={label} className="rounded-lg border p-2">
|
|
61
|
+
<div className="font-heading text-lg font-semibold">{count}</div>
|
|
62
|
+
<div className="text-xs text-muted-foreground">{label}</div>
|
|
63
|
+
</div>
|
|
64
|
+
))}
|
|
65
|
+
</div>
|
|
66
|
+
{issues.length > 0 ? (
|
|
67
|
+
<div>
|
|
68
|
+
<h3 className="mb-2 font-medium">{t.lab.toFix}</h3>
|
|
69
|
+
<ul className="list-disc space-y-1 pl-5 text-muted-foreground">
|
|
70
|
+
{issues.map((p, i) => (
|
|
71
|
+
<li key={i}>{p}</li>
|
|
72
|
+
))}
|
|
73
|
+
</ul>
|
|
74
|
+
</div>
|
|
75
|
+
) : (
|
|
76
|
+
<p className="text-success">{t.lab.valid}</p>
|
|
77
|
+
)}
|
|
78
|
+
<p className="text-xs text-muted-foreground">{t.lab.routing}</p>
|
|
79
|
+
</div>
|
|
80
|
+
</FloatingWindow>
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** What the Run window says the lab is doing. */
|
|
85
|
+
function runLine(e: EditorModel): string {
|
|
86
|
+
const t = e.t;
|
|
87
|
+
switch (e.desk.state.run) {
|
|
88
|
+
case "running":
|
|
89
|
+
return `${t.run.running} ${t.machineRun.consoleHint}`;
|
|
90
|
+
case "starting":
|
|
91
|
+
return t.run.starting;
|
|
92
|
+
case "stopping":
|
|
93
|
+
return t.run.stopping;
|
|
94
|
+
case "stopped":
|
|
95
|
+
return t.run.stopped;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Desktop app: the lab running on this machine, its log, and starting it with the app. */
|
|
100
|
+
export function RunWindow({ e }: { e: EditorModel }) {
|
|
101
|
+
const { t, desk, lab } = e;
|
|
102
|
+
const { info, run } = desk.state;
|
|
103
|
+
const { autoStart, setAutoStart } = desk.autoStart;
|
|
104
|
+
return (
|
|
105
|
+
<FloatingWindow title={t.run.window} side="right" width={420} onClose={() => { desk.panel.setRunOpen(false); }}>
|
|
106
|
+
<div className="flex flex-col gap-3 p-4 text-sm">
|
|
107
|
+
<p>{runLine(e)}</p>
|
|
108
|
+
{info && !info.docker && <p className="rounded-md bg-pastel-yellow p-2.5 text-xs">{t.run.noDocker}</p>}
|
|
109
|
+
{info && !info.live_capture && <p className="text-xs text-muted-foreground">{t.run.noLiveCapture(info.wireshark, info.labs.replace(/labs$/, "captures"))}</p>}
|
|
110
|
+
<label className="flex items-start gap-2 text-xs">
|
|
111
|
+
<input
|
|
112
|
+
type="checkbox"
|
|
113
|
+
className="mt-0.5"
|
|
114
|
+
checked={autoStart}
|
|
115
|
+
onChange={(ev) => {
|
|
116
|
+
setAutoStarts(lab.labName, ev.target.checked);
|
|
117
|
+
setAutoStart(ev.target.checked);
|
|
118
|
+
}}
|
|
119
|
+
/>
|
|
120
|
+
<span>
|
|
121
|
+
{t.run.autoStart}
|
|
122
|
+
<span className="block text-muted-foreground">{t.run.autoStartNote}</span>
|
|
123
|
+
</span>
|
|
124
|
+
</label>
|
|
125
|
+
{run === "running" && desk.live.live && <LabStatus live={desk.live.live} cut={desk.live.links} />}
|
|
126
|
+
{desk.panel.runLog && <pre className="max-h-72 overflow-auto rounded-md border bg-surface p-3 font-mono text-xs whitespace-pre-wrap">{desk.panel.runLog}</pre>}
|
|
127
|
+
</div>
|
|
128
|
+
</FloatingWindow>
|
|
129
|
+
);
|
|
130
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { Plug, Radio, Square, Unplug } from "lucide-react";
|
|
4
|
+
import { Button } from "@/components/ui/button";
|
|
5
|
+
import { useText } from "../text";
|
|
6
|
+
|
|
7
|
+
/** Desktop only: capture a LAN's or a cable's traffic while the lab runs. */
|
|
8
|
+
export function CaptureControl({ network, running, file, capturing, onStart, onStop }: { network: string; running: boolean; file: string | null | undefined; capturing: boolean; onStart: () => void; onStop: () => void }) {
|
|
9
|
+
const t = useText().capture;
|
|
10
|
+
return (
|
|
11
|
+
<div className="flex flex-col gap-1.5 rounded-lg bg-pastel-violet p-3">
|
|
12
|
+
{capturing ? (
|
|
13
|
+
<>
|
|
14
|
+
<p className="flex items-center gap-1.5 font-medium">
|
|
15
|
+
<span className="size-2 animate-pulse rounded-full bg-destructive" aria-hidden /> {t.capturing(network)}
|
|
16
|
+
</p>
|
|
17
|
+
<p className="text-xs text-muted-foreground">
|
|
18
|
+
{file ? (
|
|
19
|
+
<>
|
|
20
|
+
{t.writingTo} <span className="font-mono break-all">{file}</span>
|
|
21
|
+
</>
|
|
22
|
+
) : (
|
|
23
|
+
t.live
|
|
24
|
+
)}
|
|
25
|
+
</p>
|
|
26
|
+
<Button size="sm" variant="outline" onClick={onStop}>
|
|
27
|
+
<Square data-icon="inline-start" /> {t.stop}
|
|
28
|
+
</Button>
|
|
29
|
+
</>
|
|
30
|
+
) : (
|
|
31
|
+
<>
|
|
32
|
+
<Button size="sm" onClick={onStart} disabled={!running}>
|
|
33
|
+
<Radio data-icon="inline-start" /> {t.start}
|
|
34
|
+
</Button>
|
|
35
|
+
{!running && <p className="text-xs text-muted-foreground">{t.runFirst}</p>}
|
|
36
|
+
</>
|
|
37
|
+
)}
|
|
38
|
+
</div>
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Desktop only: suspend a link while the lab runs, as GNS3 does: a cable's ends (their ports,
|
|
44
|
+
* down) or, with `all`, every link of a LAN. `suspended` says so when it is.
|
|
45
|
+
*/
|
|
46
|
+
export function LinkControl({ running, busy, all = false, suspended, onSuspend, onResume }: { running: boolean; busy: boolean; all?: boolean; suspended: string | null; onSuspend: () => void; onResume: () => void }) {
|
|
47
|
+
const t = useText().link;
|
|
48
|
+
return (
|
|
49
|
+
<div className="flex flex-col gap-1.5 rounded-lg bg-pastel-violet p-3">
|
|
50
|
+
{suspended ? (
|
|
51
|
+
<>
|
|
52
|
+
<p className="flex items-center gap-1.5 font-medium">
|
|
53
|
+
<Unplug className="size-4" aria-hidden /> {suspended}
|
|
54
|
+
</p>
|
|
55
|
+
<Button size="sm" variant="outline" onClick={onResume} disabled={busy}>
|
|
56
|
+
<Plug data-icon="inline-start" /> {all ? t.resumeAll : t.resume}
|
|
57
|
+
</Button>
|
|
58
|
+
</>
|
|
59
|
+
) : (
|
|
60
|
+
<>
|
|
61
|
+
<Button size="sm" variant="outline" onClick={onSuspend} disabled={!running || busy}>
|
|
62
|
+
<Unplug data-icon="inline-start" /> {all ? t.suspendAll : t.suspend}
|
|
63
|
+
</Button>
|
|
64
|
+
{!running && <p className="text-xs text-muted-foreground">{t.runFirst}</p>}
|
|
65
|
+
</>
|
|
66
|
+
)}
|
|
67
|
+
</div>
|
|
68
|
+
);
|
|
69
|
+
}
|