@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,116 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { Camera, Monitor, Play, RotateCw, Square, SquareTerminal, Trash2 } from "lucide-react";
|
|
4
|
+
import type { Snapshot } from "@/lib/tool/desktop";
|
|
5
|
+
import { Button } from "@/components/ui/button";
|
|
6
|
+
import type { MachineAction } from "../hooks/use-machine-actions";
|
|
7
|
+
import { useText } from "../text";
|
|
8
|
+
|
|
9
|
+
interface Props {
|
|
10
|
+
state: string | undefined;
|
|
11
|
+
busy: boolean;
|
|
12
|
+
onConsole: () => void;
|
|
13
|
+
onDisplay: () => void;
|
|
14
|
+
onAction: (action: MachineAction) => void;
|
|
15
|
+
snapshots: Snapshot[];
|
|
16
|
+
onSnapshot: (label: string) => void;
|
|
17
|
+
onUse: (id: string | null) => void;
|
|
18
|
+
onDeleteSnapshot: (id: string) => void;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** A console, its screen, and starting, restarting or stopping it. */
|
|
22
|
+
function RunButtons({ state, busy, onConsole, onDisplay, onAction }: Pick<Props, "state" | "busy" | "onConsole" | "onDisplay" | "onAction">) {
|
|
23
|
+
const t = useText().machineRun;
|
|
24
|
+
const running = state === "running";
|
|
25
|
+
return (
|
|
26
|
+
<div className="flex flex-wrap gap-2">
|
|
27
|
+
<Button size="sm" variant="outline" disabled={!running} onClick={onConsole}>
|
|
28
|
+
<SquareTerminal data-icon="inline-start" /> {t.console}
|
|
29
|
+
</Button>
|
|
30
|
+
<Button size="sm" variant="outline" disabled={!running} onClick={onDisplay} title={t.displayTitle}>
|
|
31
|
+
<Monitor data-icon="inline-start" /> {t.display}
|
|
32
|
+
</Button>
|
|
33
|
+
{running ? (
|
|
34
|
+
<>
|
|
35
|
+
<Button size="sm" variant="outline" disabled={busy} onClick={() => { onAction("restart"); }}>
|
|
36
|
+
<RotateCw data-icon="inline-start" /> {t.restart}
|
|
37
|
+
</Button>
|
|
38
|
+
<Button size="sm" variant="outline" disabled={busy} onClick={() => { onAction("stop"); }}>
|
|
39
|
+
<Square data-icon="inline-start" /> {t.stop}
|
|
40
|
+
</Button>
|
|
41
|
+
</>
|
|
42
|
+
) : (
|
|
43
|
+
<Button size="sm" variant="outline" disabled={busy || !state} onClick={() => { onAction("start"); }}>
|
|
44
|
+
<Play data-icon="inline-start" /> {t.start}
|
|
45
|
+
</Button>
|
|
46
|
+
)}
|
|
47
|
+
</div>
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const when = (created: number) => new Date(created * 1000).toLocaleString(undefined, { dateStyle: "short", timeStyle: "short" });
|
|
52
|
+
|
|
53
|
+
/** The machine's snapshots, newest first: restore one, delete one, or go back to its own image. */
|
|
54
|
+
function SnapshotList({ snapshots, busy, onUse, onDeleteSnapshot }: Pick<Props, "snapshots" | "busy" | "onUse" | "onDeleteSnapshot">) {
|
|
55
|
+
const t = useText().machineRun;
|
|
56
|
+
if (snapshots.length === 0) return <p className="text-xs text-muted-foreground">{t.noSnapshots}</p>;
|
|
57
|
+
return (
|
|
58
|
+
<ul className="flex flex-col gap-1 text-xs">
|
|
59
|
+
{snapshots
|
|
60
|
+
.slice()
|
|
61
|
+
.reverse()
|
|
62
|
+
.map((x) => (
|
|
63
|
+
<li key={x.id} className="flex items-center gap-1.5">
|
|
64
|
+
<span className="min-w-0 flex-1 truncate" title={x.image}>
|
|
65
|
+
{x.label} <span className="text-muted-foreground">· {when(x.created)}</span>
|
|
66
|
+
</span>
|
|
67
|
+
{x.in_use ? (
|
|
68
|
+
<span className="text-signal">{t.inUse}</span>
|
|
69
|
+
) : (
|
|
70
|
+
<Button size="xs" variant="ghost" disabled={busy} onClick={() => { onUse(x.id); }}>
|
|
71
|
+
{t.restore}
|
|
72
|
+
</Button>
|
|
73
|
+
)}
|
|
74
|
+
<Button size="icon-xs" variant="ghost" disabled={busy || x.in_use} aria-label={t.deleteSnapshot(x.label)} title={t.deleteSnapshot(x.label)} onClick={() => { onDeleteSnapshot(x.id); }}>
|
|
75
|
+
<Trash2 />
|
|
76
|
+
</Button>
|
|
77
|
+
</li>
|
|
78
|
+
))}
|
|
79
|
+
</ul>
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** In the desktop app, for a machine of the running lab: its state, its controls, its snapshots. */
|
|
84
|
+
export function MachineControls(p: Props) {
|
|
85
|
+
const t = useText().machineRun;
|
|
86
|
+
const running = p.state === "running";
|
|
87
|
+
return (
|
|
88
|
+
<div className="mt-4 flex flex-col gap-2 border-t pt-3 text-sm">
|
|
89
|
+
<p className="text-xs text-muted-foreground">{running ? t.running : p.state ? t.stopped : t.unknown}</p>
|
|
90
|
+
<RunButtons state={p.state} busy={p.busy} onConsole={p.onConsole} onDisplay={p.onDisplay} onAction={p.onAction} />
|
|
91
|
+
<div className="mt-2 flex flex-col gap-1.5">
|
|
92
|
+
<div className="flex items-center justify-between gap-2">
|
|
93
|
+
<span className="text-muted-foreground">{t.snapshots}</span>
|
|
94
|
+
<Button
|
|
95
|
+
size="xs"
|
|
96
|
+
variant="outline"
|
|
97
|
+
disabled={p.busy || !running}
|
|
98
|
+
title={t.snapshotHint}
|
|
99
|
+
onClick={() => {
|
|
100
|
+
const label = window.prompt(t.snapshotPrompt, "");
|
|
101
|
+
if (label !== null) p.onSnapshot(label);
|
|
102
|
+
}}
|
|
103
|
+
>
|
|
104
|
+
<Camera data-icon="inline-start" /> {t.takeSnapshot}
|
|
105
|
+
</Button>
|
|
106
|
+
</div>
|
|
107
|
+
<SnapshotList snapshots={p.snapshots} busy={p.busy} onUse={p.onUse} onDeleteSnapshot={p.onDeleteSnapshot} />
|
|
108
|
+
{p.snapshots.some((x) => x.in_use) && (
|
|
109
|
+
<Button size="xs" variant="ghost" className="self-start" disabled={p.busy} onClick={() => { p.onUse(null); }}>
|
|
110
|
+
{t.ownImage}
|
|
111
|
+
</Button>
|
|
112
|
+
)}
|
|
113
|
+
</div>
|
|
114
|
+
</div>
|
|
115
|
+
);
|
|
116
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { BookmarkPlus, Trash2 } from "lucide-react";
|
|
4
|
+
import { cleanName, type MachineData, type MachineNode } from "@/lib/tool/model";
|
|
5
|
+
import { runAs } from "@/lib/tool/presets";
|
|
6
|
+
import { Button } from "@/components/ui/button";
|
|
7
|
+
import { useText } from "../text";
|
|
8
|
+
import { field } from "./fields";
|
|
9
|
+
import { ContainerFields, ServiceFields, VmFields } from "./runs-fields";
|
|
10
|
+
|
|
11
|
+
type Change = (patch: Partial<MachineData>) => void;
|
|
12
|
+
|
|
13
|
+
/** Its name, and what it runs as: an automatic router (a router's only), a container or a VM. */
|
|
14
|
+
function NameAndKind({ m, onChange }: { m: MachineData; onChange: Change }) {
|
|
15
|
+
const t = useText().inspector;
|
|
16
|
+
return (
|
|
17
|
+
<>
|
|
18
|
+
<label className="flex flex-col gap-1">
|
|
19
|
+
<span className="text-muted-foreground">{t.name}</span>
|
|
20
|
+
<input className={field} value={m.name} onChange={(e) => { onChange({ name: cleanName(e.target.value) }); }} />
|
|
21
|
+
</label>
|
|
22
|
+
<label className="flex flex-col gap-1">
|
|
23
|
+
<span className="text-muted-foreground">{t.runsAs}</span>
|
|
24
|
+
<select
|
|
25
|
+
className={field}
|
|
26
|
+
value={m.kind}
|
|
27
|
+
onChange={(e) => {
|
|
28
|
+
const patch = runAs(m, e.target.value);
|
|
29
|
+
if (patch) onChange(patch);
|
|
30
|
+
}}
|
|
31
|
+
>
|
|
32
|
+
{m.icon === "router" && <option value="router">{t.automaticRouter}</option>}
|
|
33
|
+
<option value="container">{t.container}</option>
|
|
34
|
+
<option value="vm">{t.vm}</option>
|
|
35
|
+
</select>
|
|
36
|
+
</label>
|
|
37
|
+
</>
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** The machine's cables, as `its port → the other end's name and port`. */
|
|
42
|
+
function CableList({ cables }: { cables: string[] }) {
|
|
43
|
+
const t = useText().inspector;
|
|
44
|
+
return (
|
|
45
|
+
<div>
|
|
46
|
+
<span className="text-muted-foreground">{t.cables}</span>
|
|
47
|
+
{cables.length ? (
|
|
48
|
+
<ul className="mt-1 space-y-0.5 font-mono text-xs">
|
|
49
|
+
{cables.map((c, i) => (
|
|
50
|
+
<li key={i}>{c}</li>
|
|
51
|
+
))}
|
|
52
|
+
</ul>
|
|
53
|
+
) : (
|
|
54
|
+
<p className="mt-1 text-xs text-muted-foreground">{t.noCables}</p>
|
|
55
|
+
)}
|
|
56
|
+
</div>
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** A machine's window: its name and kind, what it runs, its services, its cables. */
|
|
61
|
+
export function MachineInspector({ node, cables, onChange, onDelete, onSaveDevice }: { node: MachineNode; cables: string[]; onChange: Change; onDelete: () => void; onSaveDevice: () => void }) {
|
|
62
|
+
const t = useText().inspector;
|
|
63
|
+
const m = node.data;
|
|
64
|
+
return (
|
|
65
|
+
<div className="flex flex-col gap-4 text-sm">
|
|
66
|
+
<NameAndKind m={m} onChange={onChange} />
|
|
67
|
+
{m.kind === "router" && <p className="text-xs text-muted-foreground">{t.routerNote}</p>}
|
|
68
|
+
{m.kind === "container" && <ContainerFields m={m} onChange={onChange} />}
|
|
69
|
+
{m.kind !== "router" && m.kind !== "container" && <VmFields m={m} onChange={onChange} />}
|
|
70
|
+
{/* An automatic router is not a machine, nor is an appliance one to land on: no services. */}
|
|
71
|
+
{m.kind !== "router" && !m.appliance && <ServiceFields m={m} onChange={onChange} />}
|
|
72
|
+
<CableList cables={cables} />
|
|
73
|
+
<div className="flex flex-wrap gap-2">
|
|
74
|
+
<Button variant="outline" size="sm" onClick={onSaveDevice} title={t.saveTitle}>
|
|
75
|
+
<BookmarkPlus data-icon="inline-start" /> {t.save}
|
|
76
|
+
</Button>
|
|
77
|
+
<Button variant="outline" size="sm" onClick={onDelete}>
|
|
78
|
+
<Trash2 data-icon="inline-start" /> {t.remove}
|
|
79
|
+
</Button>
|
|
80
|
+
</div>
|
|
81
|
+
</div>
|
|
82
|
+
);
|
|
83
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { APPLIANCES, idleByDefault, type MachineData } from "@/lib/tool/model";
|
|
4
|
+
import { OS_CHOICES } from "@/lib/tool/os";
|
|
5
|
+
import { cn } from "@/lib/utils";
|
|
6
|
+
import { useText } from "../text";
|
|
7
|
+
import { area, field } from "./fields";
|
|
8
|
+
|
|
9
|
+
type Change = (patch: Partial<MachineData>) => void;
|
|
10
|
+
|
|
11
|
+
/** A Cisco OS in its container: which one, what it needs, and its own IOS configuration. */
|
|
12
|
+
function ApplianceFields({ m, onChange }: { m: MachineData; onChange: Change }) {
|
|
13
|
+
const t = useText().inspector;
|
|
14
|
+
return (
|
|
15
|
+
<>
|
|
16
|
+
<label className="flex flex-col gap-1">
|
|
17
|
+
<span className="text-muted-foreground">{t.ciscoOs}</span>
|
|
18
|
+
<select
|
|
19
|
+
className={field}
|
|
20
|
+
value={m.appliance}
|
|
21
|
+
onChange={(e) => {
|
|
22
|
+
const a = APPLIANCES.find((x) => x.id === e.target.value);
|
|
23
|
+
if (a) onChange({ appliance: a.id, image: a.image });
|
|
24
|
+
}}
|
|
25
|
+
>
|
|
26
|
+
{APPLIANCES.map((a) => (
|
|
27
|
+
<option key={a.id} value={a.id}>
|
|
28
|
+
{a.label}
|
|
29
|
+
</option>
|
|
30
|
+
))}
|
|
31
|
+
</select>
|
|
32
|
+
{APPLIANCES.find((a) => a.id === m.appliance)?.kvm && <span className="text-xs text-muted-foreground">{t.needsKvm}</span>}
|
|
33
|
+
{m.appliance === "cisco-dynamips" && <span className="text-xs text-muted-foreground">{t.dynamipsFirmware(m.name)}</span>}
|
|
34
|
+
</label>
|
|
35
|
+
<p className="text-xs text-muted-foreground">
|
|
36
|
+
{t.applianceNote}{" "}
|
|
37
|
+
<a className="underline" href="https://github.com/srl-labs/vrnetlab/tree/master/cisco/iol" target="_blank" rel="noreferrer">
|
|
38
|
+
vrnetlab
|
|
39
|
+
</a>
|
|
40
|
+
</p>
|
|
41
|
+
<label className="flex flex-col gap-1">
|
|
42
|
+
<span className="text-muted-foreground">{t.iosConfig}</span>
|
|
43
|
+
<textarea
|
|
44
|
+
className={`min-h-28 ${area}`}
|
|
45
|
+
placeholder={"router ospf 1\n network 10.0.0.0 0.255.255.255 area 0"}
|
|
46
|
+
value={m.config ?? ""}
|
|
47
|
+
onChange={(e) => {
|
|
48
|
+
onChange({ config: e.target.value });
|
|
49
|
+
}}
|
|
50
|
+
spellCheck={false}
|
|
51
|
+
/>
|
|
52
|
+
<span className="text-xs text-muted-foreground">{t.iosConfigHint}</span>
|
|
53
|
+
</label>
|
|
54
|
+
</>
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** A container: its image (none to type for Dynamips), and a Cisco OS's fields or whether it idles. */
|
|
59
|
+
export function ContainerFields({ m, onChange }: { m: MachineData; onChange: Change }) {
|
|
60
|
+
const t = useText().inspector;
|
|
61
|
+
return (
|
|
62
|
+
<>
|
|
63
|
+
{m.appliance !== "cisco-dynamips" && (
|
|
64
|
+
<label className="flex flex-col gap-1">
|
|
65
|
+
<span className="text-muted-foreground">{t.image}</span>
|
|
66
|
+
<input className={cn(field, "font-mono")} value={m.image} onChange={(e) => { onChange({ image: e.target.value }); }} />
|
|
67
|
+
</label>
|
|
68
|
+
)}
|
|
69
|
+
{m.appliance ? (
|
|
70
|
+
<ApplianceFields m={m} onChange={onChange} />
|
|
71
|
+
) : (
|
|
72
|
+
<label className="flex items-start gap-2">
|
|
73
|
+
<input type="checkbox" className="mt-0.5" checked={m.idle ?? idleByDefault(m.image)} onChange={(e) => { onChange({ idle: e.target.checked }); }} />
|
|
74
|
+
<span>
|
|
75
|
+
{t.keepRunning}
|
|
76
|
+
<span className="block text-xs text-muted-foreground">{t.keepRunningHint}</span>
|
|
77
|
+
</span>
|
|
78
|
+
</label>
|
|
79
|
+
)}
|
|
80
|
+
</>
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** A VM: its OS and its provisioning steps. */
|
|
85
|
+
export function VmFields({ m, onChange }: { m: MachineData; onChange: Change }) {
|
|
86
|
+
const t = useText().inspector;
|
|
87
|
+
return (
|
|
88
|
+
<>
|
|
89
|
+
<label className="flex flex-col gap-1">
|
|
90
|
+
<span className="text-muted-foreground">{t.os}</span>
|
|
91
|
+
<select className={field} value={m.os} onChange={(e) => { onChange({ os: e.target.value }); }}>
|
|
92
|
+
{OS_CHOICES.map((os) => (
|
|
93
|
+
<option key={os} value={os}>
|
|
94
|
+
{os}
|
|
95
|
+
</option>
|
|
96
|
+
))}
|
|
97
|
+
</select>
|
|
98
|
+
</label>
|
|
99
|
+
<label className="flex flex-col gap-1">
|
|
100
|
+
<span className="text-muted-foreground">{t.provision}</span>
|
|
101
|
+
<textarea
|
|
102
|
+
className={`min-h-16 ${area}`}
|
|
103
|
+
placeholder={m.os.startsWith("windows") ? `provision/${m.name}.ps1` : `provision/${m.name}.sh`}
|
|
104
|
+
value={m.provision}
|
|
105
|
+
onChange={(e) => { onChange({ provision: e.target.value }); }}
|
|
106
|
+
/>
|
|
107
|
+
</label>
|
|
108
|
+
</>
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** A machine's services and whether the user lands on it (not an automatic router, nor an appliance). */
|
|
113
|
+
export function ServiceFields({ m, onChange }: { m: MachineData; onChange: Change }) {
|
|
114
|
+
const t = useText().inspector;
|
|
115
|
+
return (
|
|
116
|
+
<>
|
|
117
|
+
<label className="flex flex-col gap-1">
|
|
118
|
+
<span className="text-muted-foreground">{t.ports}</span>
|
|
119
|
+
<input className={field} placeholder="80, 443/https, 53/udp" value={m.ports} onChange={(e) => { onChange({ ports: e.target.value }); }} />
|
|
120
|
+
</label>
|
|
121
|
+
<label className="flex items-center gap-2">
|
|
122
|
+
<input type="checkbox" checked={m.access} onChange={(e) => { onChange({ access: e.target.checked }); }} />
|
|
123
|
+
{t.access}
|
|
124
|
+
</label>
|
|
125
|
+
</>
|
|
126
|
+
);
|
|
127
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { Trash2 } from "lucide-react";
|
|
4
|
+
import { machineEnds, networkOfCable } from "@/lib/tool/cables";
|
|
5
|
+
import { isSwitch, unselected, type CableEdge, type LanNode, type MachineNode } from "@/lib/tool/model";
|
|
6
|
+
import { nameById } from "@/lib/tool/selection";
|
|
7
|
+
import { vlanName } from "@/lib/tool/vlans";
|
|
8
|
+
import { cn } from "@/lib/utils";
|
|
9
|
+
import { Button } from "@/components/ui/button";
|
|
10
|
+
import { FloatingWindow } from "../floating-window";
|
|
11
|
+
import type { EditorModel } from "../hooks/use-editor";
|
|
12
|
+
import { ConditionsFields } from "./conditions-fields";
|
|
13
|
+
import { field } from "./fields";
|
|
14
|
+
import { CaptureControl, LinkControl } from "./link-controls";
|
|
15
|
+
import { MachineControls } from "./machine-controls";
|
|
16
|
+
import { MachineInspector } from "./machine-inspector";
|
|
17
|
+
|
|
18
|
+
/** The cables of a machine, as its window lists them: `its port → the other end's name and port`. */
|
|
19
|
+
function cablesOf(e: EditorModel, id: string): string[] {
|
|
20
|
+
const nameOf = (other: string) => nameById(e.lab.nodes, other);
|
|
21
|
+
return e.lab.edges
|
|
22
|
+
.filter((c) => c.source === id || c.target === id)
|
|
23
|
+
.map((c) => (c.source === id ? `${String(c.data?.aPort)} → ${nameOf(c.target)} ${String(c.data?.bPort)}` : `${String(c.data?.bPort)} → ${nameOf(c.source)} ${String(c.data?.aPort)}`));
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** A machine's window: the inspector, and (desktop app, lab running) its controls. */
|
|
27
|
+
export function MachineWindow({ e, node }: { e: EditorModel; node: MachineNode }) {
|
|
28
|
+
const { lab, desk, actions } = e;
|
|
29
|
+
const name = node.data.name;
|
|
30
|
+
const act = desk.actions;
|
|
31
|
+
return (
|
|
32
|
+
<FloatingWindow title={name || e.t.windows.machine} side="left" width={280} onClose={() => { lab.setNodes((ns) => unselected(ns)); }}>
|
|
33
|
+
<div className="p-4">
|
|
34
|
+
<MachineInspector
|
|
35
|
+
node={node}
|
|
36
|
+
cables={cablesOf(e, node.id)}
|
|
37
|
+
onChange={(patch) => { actions.updateMachine(node.id, patch); }}
|
|
38
|
+
onDelete={() => void e.flow.deleteElements({ nodes: [{ id: node.id }] })}
|
|
39
|
+
onSaveDevice={() => { e.devices.saveDevice(node.data); }}
|
|
40
|
+
/>
|
|
41
|
+
{e.invoke && desk.state.run === "running" && (
|
|
42
|
+
<MachineControls
|
|
43
|
+
state={desk.live.machineStates[name]}
|
|
44
|
+
busy={act.busy}
|
|
45
|
+
onConsole={() => void act.machineConsole(name)}
|
|
46
|
+
onDisplay={() => void act.machineDisplay(name)}
|
|
47
|
+
onAction={(action) => void act.machineAct(name, action)}
|
|
48
|
+
snapshots={desk.live.snapshots.filter((x) => x.machine === name)}
|
|
49
|
+
onSnapshot={(label) => void act.snapshotDo("snapshot_take", { machine: name, label })}
|
|
50
|
+
onUse={(id) => void act.snapshotDo("snapshot_use", { machine: name, id })}
|
|
51
|
+
onDeleteSnapshot={(id) => void act.snapshotDo("snapshot_delete", { id })}
|
|
52
|
+
/>
|
|
53
|
+
)}
|
|
54
|
+
</div>
|
|
55
|
+
</FloatingWindow>
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* A LAN's window: how it behaves (its name and block are edited on the canvas itself), and in the
|
|
61
|
+
* desktop app capturing its traffic and suspending its links.
|
|
62
|
+
*/
|
|
63
|
+
export function LanWindow({ e, lan }: { e: EditorModel; lan: LanNode }) {
|
|
64
|
+
const { lab, desk } = e;
|
|
65
|
+
const name = lan.data.name;
|
|
66
|
+
const running = desk.state.run === "running";
|
|
67
|
+
const { captures, capture, stopCapture } = desk.captures;
|
|
68
|
+
const cut = desk.live.links.filter((l) => l.network === name).length;
|
|
69
|
+
const onVlan = lab.nodes.some((n) => isSwitch(n) && n.data.vlans.some((v) => vlanName(v.id) === name));
|
|
70
|
+
return (
|
|
71
|
+
<FloatingWindow title={e.t.windows.lan(name)} side="left" width={280} onClose={() => { lab.setNodes((ns) => unselected(ns)); }}>
|
|
72
|
+
<div className="flex flex-col gap-4 p-4 text-sm">
|
|
73
|
+
{onVlan ? <p className="text-xs text-muted-foreground">{e.t.windows.conditions.onVlan}</p> : <ConditionsFields value={lan.data.tc} onChange={(tc) => { e.flow.updateNodeData(lan.id, { tc }); }} />}
|
|
74
|
+
{e.invoke && (
|
|
75
|
+
<>
|
|
76
|
+
<CaptureControl network={name} running={running} capturing={name in captures} file={captures[name]} onStart={() => void capture(name)} onStop={() => void stopCapture(name)} />
|
|
77
|
+
<LinkControl
|
|
78
|
+
running={running}
|
|
79
|
+
busy={desk.actions.busy}
|
|
80
|
+
all
|
|
81
|
+
suspended={cut ? e.t.link.lanSuspended(cut) : null}
|
|
82
|
+
onSuspend={() => void desk.actions.linkAct(name, undefined, "suspend")}
|
|
83
|
+
onResume={() => void desk.actions.linkAct(name, undefined, "resume")}
|
|
84
|
+
/>
|
|
85
|
+
</>
|
|
86
|
+
)}
|
|
87
|
+
</div>
|
|
88
|
+
</FloatingWindow>
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Desktop app: capturing a cable's traffic, and suspending it (when it has a machine at an end). */
|
|
93
|
+
function CableRunControls({ e, edge }: { e: EditorModel; edge: CableEdge }) {
|
|
94
|
+
const { desk, lab } = e;
|
|
95
|
+
const network = networkOfCable(edge.id, lab.nodes, lab.edges);
|
|
96
|
+
if (!network) return null;
|
|
97
|
+
const running = desk.state.run === "running";
|
|
98
|
+
const { captures, capture, stopCapture } = desk.captures;
|
|
99
|
+
const ends = machineEnds(edge, lab.nodes);
|
|
100
|
+
const cut = desk.live.links.some((l) => l.network === network && ends.includes(l.machine));
|
|
101
|
+
return (
|
|
102
|
+
<>
|
|
103
|
+
<CaptureControl network={network} running={running} capturing={network in captures} file={captures[network]} onStart={() => void capture(network)} onStop={() => void stopCapture(network)} />
|
|
104
|
+
{ends.length > 0 && (
|
|
105
|
+
<LinkControl
|
|
106
|
+
running={running}
|
|
107
|
+
busy={desk.actions.busy}
|
|
108
|
+
suspended={cut ? e.t.link.cableSuspended : null}
|
|
109
|
+
onSuspend={() => void desk.actions.linkAct(network, ends, "suspend")}
|
|
110
|
+
onResume={() => void desk.actions.linkAct(network, ends, "resume")}
|
|
111
|
+
/>
|
|
112
|
+
)}
|
|
113
|
+
</>
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** A cable's window: the port at each end, how it behaves, and removing it. */
|
|
118
|
+
export function CableWindow({ e, edge, data }: { e: EditorModel; edge: CableEdge; data: NonNullable<CableEdge["data"]> }) {
|
|
119
|
+
const { lab, index, t } = e;
|
|
120
|
+
const nameOf = (id: string) => nameById(lab.nodes, id);
|
|
121
|
+
const ends = [
|
|
122
|
+
["aPort", edge.source],
|
|
123
|
+
["bPort", edge.target],
|
|
124
|
+
] as const;
|
|
125
|
+
return (
|
|
126
|
+
<FloatingWindow title={t.windows.cable(nameOf(edge.source), nameOf(edge.target))} side="left" width={280} onClose={() => { lab.setEdges((es) => unselected(es)); }}>
|
|
127
|
+
<div className="flex flex-col gap-4 p-4 text-sm">
|
|
128
|
+
{ends.map(([key, nodeId]) => (
|
|
129
|
+
<label key={key} className="flex flex-col gap-1">
|
|
130
|
+
<span className="text-muted-foreground">{t.windows.portOn(nameOf(nodeId))}</span>
|
|
131
|
+
<input className={cn(field, "font-mono")} value={data[key]} onChange={(ev) => { e.actions.updateCable(edge.id, { [key]: ev.target.value }); }} />
|
|
132
|
+
</label>
|
|
133
|
+
))}
|
|
134
|
+
<p className="text-xs text-muted-foreground">{t.windows.cableNote}</p>
|
|
135
|
+
{[edge.source, edge.target].some((id) => index.lanIds.has(id) || index.switchIds.has(id)) ? (
|
|
136
|
+
<p className="text-xs text-muted-foreground">{t.windows.conditions.onLan}</p>
|
|
137
|
+
) : (
|
|
138
|
+
<ConditionsFields value={data.tc} onChange={(tc) => { e.actions.updateCable(edge.id, { tc }); }} />
|
|
139
|
+
)}
|
|
140
|
+
{e.invoke && <CableRunControls e={e} edge={edge} />}
|
|
141
|
+
<Button variant="outline" size="sm" onClick={() => void e.flow.deleteElements({ edges: [{ id: edge.id }] })}>
|
|
142
|
+
<Trash2 data-icon="inline-start" /> {t.windows.removeCable}
|
|
143
|
+
</Button>
|
|
144
|
+
</div>
|
|
145
|
+
</FloatingWindow>
|
|
146
|
+
);
|
|
147
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { Button as ButtonPrimitive } from "@base-ui/react/button"
|
|
2
|
+
import { cva, type VariantProps } from "class-variance-authority"
|
|
3
|
+
|
|
4
|
+
import { cn } from "@/lib/utils"
|
|
5
|
+
|
|
6
|
+
const buttonVariants = cva(
|
|
7
|
+
"group/button inline-flex shrink-0 items-center justify-center rounded-lg border border-transparent bg-clip-padding text-sm font-medium whitespace-nowrap transition-all outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 active:not-aria-[haspopup]:translate-y-px disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
|
8
|
+
{
|
|
9
|
+
variants: {
|
|
10
|
+
variant: {
|
|
11
|
+
default:
|
|
12
|
+
"bg-primary text-primary-foreground shadow-xs hover:bg-primary/90",
|
|
13
|
+
outline:
|
|
14
|
+
"border-border bg-background hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50",
|
|
15
|
+
secondary:
|
|
16
|
+
"bg-secondary text-secondary-foreground hover:bg-secondary/80 aria-expanded:bg-secondary aria-expanded:text-secondary-foreground",
|
|
17
|
+
ghost:
|
|
18
|
+
"hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:hover:bg-muted/50",
|
|
19
|
+
destructive:
|
|
20
|
+
"bg-destructive/10 text-destructive hover:bg-destructive/20 focus-visible:border-destructive/40 focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:hover:bg-destructive/30 dark:focus-visible:ring-destructive/40",
|
|
21
|
+
link: "text-link underline-offset-4 hover:underline",
|
|
22
|
+
},
|
|
23
|
+
size: {
|
|
24
|
+
default:
|
|
25
|
+
"h-8 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
|
|
26
|
+
xs: "h-6 gap-1 rounded-[min(var(--radius-md),10px)] px-2 text-xs in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3",
|
|
27
|
+
sm: "h-7 gap-1 rounded-[min(var(--radius-md),12px)] px-2.5 text-[0.8rem] in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3.5",
|
|
28
|
+
lg: "h-10 gap-2 px-4 has-data-[icon=inline-end]:pr-3 has-data-[icon=inline-start]:pl-3",
|
|
29
|
+
icon: "size-8",
|
|
30
|
+
"icon-xs":
|
|
31
|
+
"size-6 rounded-[min(var(--radius-md),10px)] in-data-[slot=button-group]:rounded-lg [&_svg:not([class*='size-'])]:size-3",
|
|
32
|
+
"icon-sm":
|
|
33
|
+
"size-7 rounded-[min(var(--radius-md),12px)] in-data-[slot=button-group]:rounded-lg",
|
|
34
|
+
"icon-lg": "size-9",
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
defaultVariants: {
|
|
38
|
+
variant: "default",
|
|
39
|
+
size: "default",
|
|
40
|
+
},
|
|
41
|
+
}
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
function Button({
|
|
45
|
+
className,
|
|
46
|
+
variant = "default",
|
|
47
|
+
size = "default",
|
|
48
|
+
...props
|
|
49
|
+
}: ButtonPrimitive.Props & VariantProps<typeof buttonVariants>) {
|
|
50
|
+
return (
|
|
51
|
+
<ButtonPrimitive
|
|
52
|
+
data-slot="button"
|
|
53
|
+
className={cn(buttonVariants({ variant, size, className }))}
|
|
54
|
+
{...props}
|
|
55
|
+
/>
|
|
56
|
+
)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export { Button, buttonVariants }
|