From 1c0181297dc0b19d091ff43c2ea38918a3968456 Mon Sep 17 00:00:00 2001 From: DCCONSTRUCTIONS Date: Tue, 28 Jul 2026 00:54:49 +0300 Subject: [PATCH] feat(control-station): add compute contour workspace --- apps/control-station/src/App.tsx | 72 +++-- .../system/ComputeContourSettingsWindow.tsx | 289 ++++++++++++++++++ .../system/SystemNavigationPanel.tsx | 57 ++++ .../system/SystemWorkspaceSelector.tsx | 33 ++ .../system/useComputeContourSettings.tsx | 50 +++ .../components/useApplicationPanelActions.ts | 47 +++ .../src/core/system/ComputeContourContext.tsx | 134 ++++++++ .../src/core/system/computeContours.ts | 148 +++++++++ .../src/core/system/useWorkerTelemetry.ts | 22 +- .../src/core/system/workerTelemetry.ts | 1 + apps/control-station/src/main.tsx | 5 +- apps/control-station/src/productModel.ts | 24 +- .../src/styles/system-telemetry.css | 76 ++++- .../system/ComputeModulesWorkspace.tsx | 37 ++- .../workspaces/system/NetworkWorkspace.tsx | 240 ++++----------- .../test/systemTelemetry.test.mjs | 14 + 16 files changed, 987 insertions(+), 262 deletions(-) create mode 100644 apps/control-station/src/components/system/ComputeContourSettingsWindow.tsx create mode 100644 apps/control-station/src/components/system/SystemNavigationPanel.tsx create mode 100644 apps/control-station/src/components/system/SystemWorkspaceSelector.tsx create mode 100644 apps/control-station/src/components/system/useComputeContourSettings.tsx create mode 100644 apps/control-station/src/components/useApplicationPanelActions.ts create mode 100644 apps/control-station/src/core/system/ComputeContourContext.tsx create mode 100644 apps/control-station/src/core/system/computeContours.ts diff --git a/apps/control-station/src/App.tsx b/apps/control-station/src/App.tsx index 0cfc3d6..8c73acc 100644 --- a/apps/control-station/src/App.tsx +++ b/apps/control-station/src/App.tsx @@ -21,12 +21,15 @@ import { Window, WindowFooterActions, useApplicationWorkspace, - type ApplicationPanelUtilityAction, } from "@nodedc/ui-react"; import { LandingStage } from "./components/LandingStage"; import { EnvironmentSettingsWindow } from "./components/EnvironmentSettingsWindow"; import { ObservationSessionSelect } from "./components/ObservationSessionSelect"; +import { SystemNavigationPanel } from "./components/system/SystemNavigationPanel"; +import { SystemWorkspaceSelector } from "./components/system/SystemWorkspaceSelector"; +import { useComputeContourSettings } from "./components/system/useComputeContourSettings"; +import { useApplicationPanelActions } from "./components/useApplicationPanelActions"; import { useEnvironmentSettings } from "./core/environment/useEnvironmentSettings"; import { useDevicePluginHost } from "./core/device-plugins/DevicePluginHost"; import { useMissionRuntime } from "./core/runtime/MissionRuntimeContext"; @@ -151,6 +154,7 @@ function mergeViewerSettings( export default function App() { const runtime = useMissionRuntime(); const environment = useEnvironmentSettings(); + const computeContourSettings = useComputeContourSettings(); const { selection } = useDevicePluginHost(); const polygonDatasetRoute = useMemo( () => resolvePolygonRunRoute(typeof window === "undefined" ? "" : window.location.search), @@ -439,8 +443,14 @@ export default function App() { const selectRoot = (rootId: RootId) => { setActiveRoot(rootId); - workspace.closeView(); workspace.openNavigation(); + if (rootId === "system") { + workspace.openView( + activeDefinition?.root === "system" ? activeDefinition.id : "modules", + ); + } else { + workspace.closeView(); + } }; const openView = (viewId: string) => { @@ -604,35 +614,13 @@ export default function App() { return () => window.clearTimeout(timer); }, [layoutSaveNotice]); - const contentActions = useMemo(() => { - const actions: ApplicationPanelUtilityAction[] = []; - - if (activeDefinition?.kind === "device") { - actions.push({ - label: "Обновить состояние локального контура", - icon: "refresh", - onClick: () => void runtime.refresh(), - }); - } - - if ( - activeDefinition - && ["spatial", "recordings"].includes(activeDefinition.kind) - ) { - actions.push( - { - label: workspaceLayoutProfile.state === "saving" - ? "Сохраняем компоновку" - : "Сохранить компоновку", - icon: "save", - disabled: workspaceLayoutProfile.state === "saving", - onClick: () => void saveWorkspaceLayout(), - }, - ); - } - - return actions; - }, [activeDefinition?.kind, runtime, saveWorkspaceLayout, workspaceLayoutProfile.state]); + const contentActions = useApplicationPanelActions({ + definition: activeDefinition, + refreshRuntime: runtime.refresh, + saveWorkspaceLayout, + workspaceLayoutSaving: workspaceLayoutProfile.state === "saving", + systemUtilityAction: computeContourSettings.utilityAction, + }); const header = ( } - navigation={currentRoot ? ( + navigation={currentRoot && activeRoot === "system" ? ( + + ) : currentRoot ? ( , - active: runtime.backendStatus !== "offline" && runtime.backendStatus !== "unconfigured", + active: + runtime.backendStatus !== "offline" + && runtime.backendStatus !== "unconfigured", }, ]} items={rootWorkspaces.map((item) => ({ @@ -795,10 +791,11 @@ export default function App() { Offline evaluation ) : activeDefinition.kind === "lab-archive" ? ( null - ) : activeDefinition.kind === "compute-modules" ? ( - Живая телеметрия - ) : activeDefinition.kind === "network-monitor" ? ( - Живая сеть + ) : activeDefinition.root === "system" ? ( + ) : ( Интерфейс готов ) @@ -864,6 +861,7 @@ export default function App() { onSave={environment.save} onUpload={environment.upload} /> + {computeContourSettings.window} void; + onCreate: (draft: ComputeContourDraft) => Promise; + onUpdate: ( + contour: ComputeContour, + draft: ComputeContourDraft, + ) => Promise; +} + +const platformOptions = [ + { value: "windows", label: "Windows", description: "Worker с Windows service" }, + { value: "linux", label: "Linux", description: "Worker с systemd service" }, + { value: "unknown", label: "Не определено", description: "Платформа будет уточнена" }, +] satisfies Array<{ + value: ComputeContourPlatform; + label: string; + description: string; +}>; + +function emptyDraft(): ComputeContourDraft { + return { + display_name: "", + expected_node_id: "", + platform: "unknown", + telemetry_mode: "agent-mqtt", + address: "", + ssh_port: 22, + mqtt_host: "127.0.0.1", + mqtt_port: 1883, + }; +} + +function draftFromContour(contour: ComputeContour | null): ComputeContourDraft { + if (!contour) return emptyDraft(); + return { + display_name: contour.display_name, + expected_node_id: contour.expected_node_id, + platform: contour.platform, + telemetry_mode: contour.telemetry_mode, + address: contour.address, + ssh_port: contour.ssh_port, + mqtt_host: contour.mqtt_host, + mqtt_port: contour.mqtt_port, + }; +} + +export function ComputeContourSettingsWindow({ + open, + contour, + mode, + onClose, + onCreate, + onUpdate, +}: ComputeContourSettingsWindowProps) { + const [draft, setDraft] = useState(() => draftFromContour(contour)); + const [activeSection, setActiveSection] = useState<"connection" | "agent">("connection"); + const [install, setInstall] = useState(null); + const [busy, setBusy] = useState(false); + const [error, setError] = useState(null); + + useEffect(() => { + if (!open) return; + setDraft(draftFromContour(mode === "edit" ? contour : null)); + setActiveSection("connection"); + setInstall(null); + setError(null); + }, [contour, mode, open]); + + useEffect(() => { + if (!open || mode !== "edit" || !contour) return; + const controller = new AbortController(); + void fetchComputeContourAgentInstall(contour.contour_id, controller.signal) + .then((document) => { + if (!controller.signal.aborted) setInstall(document); + }) + .catch((reason: unknown) => { + if (!controller.signal.aborted) { + setError(reason instanceof Error ? reason.message : "Инструкция агента недоступна."); + } + }); + return () => controller.abort(); + }, [contour, mode, open]); + + const valid = useMemo(() => ( + Boolean(draft.display_name.trim()) + && Boolean(draft.expected_node_id.trim()) + && Number.isInteger(draft.ssh_port) + && draft.ssh_port > 0 + && Number.isInteger(draft.mqtt_port) + && draft.mqtt_port > 0 + ), [draft]); + + const save = async () => { + if (!valid || busy) return; + setBusy(true); + setError(null); + try { + if (mode === "edit" && contour) { + await onUpdate(contour, draft); + } else { + await onCreate(draft); + } + onClose(); + } catch (reason) { + setError(reason instanceof Error ? reason.message : "Контур не сохранён."); + } finally { + setBusy(false); + } + }; + + return ( + + + + + )} + > +
+ {activeSection === "connection" ? ( + +
+ setDraft((current) => ({ + ...current, + display_name: event.currentTarget.value, + }))} + /> + setDraft((current) => ({ + ...current, + expected_node_id: event.currentTarget.value, + }))} + /> +