63 lines
6.7 KiB
TypeScript
63 lines
6.7 KiB
TypeScript
import { useEffect, useState } from "react";
|
|
import { createRoot } from "react-dom/client";
|
|
import { ActivityIndicator, AdminNavigationPanel, AppHeader, ApplicationPanel, ApplicationShell, Button, HeaderNavigation, HeaderProfile, Icon, SettingsCard, ToastStack, UserProfileMenu, useApplicationWorkspace } from "@nodedc/ui-react";
|
|
import "@nodedc/tokens/tokens.css";
|
|
import "@nodedc/tokens/themes.css";
|
|
import "@nodedc/ui-core/styles.css";
|
|
import { desktopLogin } from "./api";
|
|
import { useNode } from "./useNode";
|
|
import { roots, views, type RootId, type ViewId } from "./nodeModel";
|
|
import { NodeOverview } from "./NodeOverview";
|
|
import { USBView, DiagnosticsView, NetworkView } from "./InventoryViews";
|
|
import { SystemAccess } from "./SystemAccess";
|
|
import { TailnetAccess } from "./TailnetAccess";
|
|
import { useEnvironment } from "./useEnvironment";
|
|
import { EnvironmentView } from "./EnvironmentView";
|
|
import { CoreConnectionView } from "./CoreConnectionView";
|
|
import "./node.css";
|
|
import { NodeSensors } from "./NodeSensors";
|
|
|
|
function App() {
|
|
const node = useNode();
|
|
const { value, pending, locked, refresh, failure } = node;
|
|
const environment = useEnvironment(!!value, failure);
|
|
const refreshAll = () => {if(!environment.running) {void refresh();void environment.refresh();}};
|
|
const [root, setRoot] = useState<RootId>("system");
|
|
const workspace = useApplicationWorkspace<ViewId>({ activeView: "environment" });
|
|
const [adding, setAdding] = useState(false);
|
|
const [theme, setTheme] = useState(() => localStorage.getItem("node-theme") === "light" ? "light" : "dark");
|
|
// Theme applies to body portals as well as the application shell.
|
|
useEffect(() => { document.documentElement.dataset.nodedcTheme = theme; }, [theme]);
|
|
const currentRoot = roots.find(item => item.id === root)!;
|
|
const currentView = views.find(item => item.id === workspace.activeView);
|
|
function openView(id: ViewId) { if(environment.running) return; setAdding(false); setRoot(views.find(item => item.id === id)!.root); workspace.openView(id); }
|
|
function selectRoot(id: RootId) { const first = roots.find(item => item.id === id)!.first; if (first) openView(first); }
|
|
const content = !value ? null : workspace.activeView === "environment" ? <EnvironmentView environment={environment} failure={failure} success={node.success} /> : workspace.activeView === "overview" ? <NodeOverview value={value} refresh={refresh} failure={failure} openView={openView} />
|
|
: workspace.activeView === "sensors" ? <NodeSensors />
|
|
: workspace.activeView === "network" ? <NetworkView value={value} />
|
|
: workspace.activeView === "usb" ? <USBView value={value} />
|
|
: workspace.activeView === "core" ? <CoreConnectionView failure={failure} />
|
|
: workspace.activeView === "diagnostics" ? <DiagnosticsView value={value} />
|
|
: workspace.activeView === "tailscale" ? <TailnetAccess failure={failure} revision={value.host.collected_at} />
|
|
: workspace.activeView === "ssh" ? <SystemAccess revision={value.host.collected_at} failure={failure} success={node.success} adding={adding} closeAdd={() => setAdding(false)} /> : null;
|
|
return <>
|
|
<ApplicationShell data-nodedc-ui className="node-app" header={<AppHeader brandMonochrome brand={<img src="/nodedc-logo.svg" alt="NODE.DC" />} brandLabel="Mission Core Node"
|
|
center={<HeaderNavigation label="Разделы бортового компьютера" value={root} items={roots.map(item => ({ value: item.id, label: item.label, disabled: !value || !item.first || environment.running }))} onChange={selectRoot} />}
|
|
right={<HeaderProfile><UserProfileMenu displayName="Node" subtitle={value?.name ?? "Mission Core Node"} triggerLabel={null} actions={[{ id: "refresh", label: "Обновить сведения", icon: "refresh", onSelect: refreshAll }, { id: "theme", label: theme === "dark" ? "Светлая тема" : "Тёмная тема", icon: "eye", onSelect: () => { const next = theme === "dark" ? "light" : "dark"; setTheme(next); localStorage.setItem("node-theme", next); } }]} /></HeaderProfile>} />}
|
|
navigationOpen={!!value && workspace.navigationOpen} contentOpen={!!value && workspace.contentOpen} contentExpanded={workspace.contentExpanded}
|
|
navigation={<AdminNavigationPanel eyebrow="MISSION CORE NODE" title={currentRoot.label} onClose={workspace.closeNavigation} closeLabel="Закрыть навигацию" navigationLabel="Разделы выбранной вкладки"
|
|
contexts={value ? [{ id: "board", label: value.name, description: value.host.hostname, icon: <Icon name="activity" />, onSelect: () => openView("overview") }] : []}
|
|
items={views.filter(item => item.root === root).map(item => ({ id: item.id, label: item.label, icon: <Icon name={item.icon} /> }))} activeId={workspace.activeView ?? undefined} onItemChange={id => openView(id as ViewId)} footer={<span>Mission Core Node · {value?.version}</span>} />}
|
|
content={currentView && <ApplicationPanel title={currentView.label} eyebrow={currentRoot.label} expanded={workspace.contentExpanded} onExpandedChange={workspace.setContentExpanded} onClose={workspace.closeView}
|
|
utilityActions={[...(workspace.activeView === "ssh" ? [{ label: "Добавить доверенное устройство", icon: "plus" as const, onClick: () => setAdding(true) }] : []), { label: "Обновить сведения", icon: "refresh", disabled: pending || environment.running, onClick: refreshAll }]}>{content}</ApplicationPanel>}
|
|
stage={<div className="node-stage" aria-busy={pending}>
|
|
{value ? <SettingsCard title={value.name} eyebrow="MISSION CORE NODE" description={`Бортовой компьютер · ${value.host.architecture}`}><div className="node-home-actions">{roots.map(item => <Button key={item.id} disabled={!item.first} onClick={() => selectRoot(item.id)}>{item.label}</Button>)}</div></SettingsCard> : <SettingsCard className="node-entry" title={pending ? "Подключаемся к ноде" : locked ? "Вход в Mission Core Node" : "Нода недоступна"}>
|
|
{pending ? <ActivityIndicator label="Получение сведений о ноде" /> : <p className="node-note">{locked ? "Подтвердите доступ в системном окне." : "Не удалось связаться со службой. Повторите подключение."}</p>}
|
|
<Button disabled={pending} onClick={() => { if (locked) { if (!desktopLogin()) failure(new Error("Откройте установленное приложение Mission Core Node из меню приложений.")); } else void refresh(); }}>{locked ? "Войти" : "Повторить подключение"}</Button>
|
|
</SettingsCard>}
|
|
</div>} />
|
|
<ToastStack items={node.toasts} onDismiss={node.dismiss} />
|
|
</>;
|
|
}
|
|
createRoot(document.getElementById("root")!).render(<App />);
|