chore: rename repository to NODEDC MISSION CORE
This commit is contained in:
@@ -0,0 +1,61 @@
|
||||
import { Button, Icon, StatusBadge } from "@nodedc/ui-react";
|
||||
|
||||
import type { RootDefinition } from "../productModel";
|
||||
import { backendLabel, backendTone, phaseLabel, phaseTone } from "../presentation";
|
||||
import type { BackendStatus } from "../useK1Console";
|
||||
|
||||
export interface LandingStageProps {
|
||||
root: RootDefinition | null;
|
||||
backendStatus: BackendStatus;
|
||||
phase?: string | null;
|
||||
message?: string | null;
|
||||
onOpenObservation: () => void;
|
||||
onOpenDevice: () => void;
|
||||
}
|
||||
|
||||
export function LandingStage({
|
||||
root,
|
||||
backendStatus,
|
||||
phase,
|
||||
message,
|
||||
onOpenObservation,
|
||||
onOpenDevice,
|
||||
}: LandingStageProps) {
|
||||
return (
|
||||
<section className="landing-stage" data-root={root?.id ?? "home"}>
|
||||
<div className="landing-stage__copy">
|
||||
<span className="section-eyebrow">{root?.eyebrow ?? "NODEDC / MISSION CORE"}</span>
|
||||
<h1>{root?.title ?? "Mission Core"}</h1>
|
||||
<p>
|
||||
{root?.statement ??
|
||||
"Наблюдение, планирование и корректировка миссий в одной модульной рабочей области."}
|
||||
</p>
|
||||
<div className="landing-stage__actions">
|
||||
<Button variant="primary" icon={<Icon name="globe" />} onClick={onOpenObservation}>
|
||||
Пространственная сцена
|
||||
</Button>
|
||||
<Button variant="secondary" icon={<Icon name="network" />} onClick={onOpenDevice}>
|
||||
Локальное устройство
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="landing-stage__status">
|
||||
<div>
|
||||
<span className="section-eyebrow">ЛОКАЛЬНЫЙ КОНТУР</span>
|
||||
<StatusBadge tone={backendTone(backendStatus)}>{backendLabel(backendStatus)}</StatusBadge>
|
||||
</div>
|
||||
<div>
|
||||
<span className="section-eyebrow">ИСТОЧНИК</span>
|
||||
<StatusBadge tone={phaseTone(phase)}>{phaseLabel(phase)}</StatusBadge>
|
||||
</div>
|
||||
<p>{message || "Выберите архитектурный блок в верхней навигации."}</p>
|
||||
</div>
|
||||
|
||||
<footer className="landing-stage__footer">
|
||||
<span>{root?.accent ?? "НАБЛЮДЕНИЕ · МИССИИ · ДАННЫЕ"}</span>
|
||||
<span>01 / ЛОКАЛЬНЫЙ СТЕНД</span>
|
||||
</footer>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import { GlassSurface } from "@nodedc/ui-react";
|
||||
|
||||
export interface MetricCardProps {
|
||||
eyebrow: string;
|
||||
value: string;
|
||||
unit?: string;
|
||||
detail: string;
|
||||
featured?: boolean;
|
||||
}
|
||||
|
||||
export function MetricCard({ eyebrow, value, unit, detail, featured = false }: MetricCardProps) {
|
||||
return (
|
||||
<GlassSurface className="metric-card" padding="md" data-featured={featured ? "true" : undefined}>
|
||||
<span className="metric-card__eyebrow">{eyebrow}</span>
|
||||
<div className="metric-card__reading">
|
||||
<strong>{value}</strong>
|
||||
{unit ? <span>{unit}</span> : null}
|
||||
</div>
|
||||
<p>{detail}</p>
|
||||
</GlassSurface>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
|
||||
export type RerunViewportStatus = "idle" | "loading" | "ready" | "error";
|
||||
|
||||
export interface RerunSelection {
|
||||
entityPath: string;
|
||||
viewName?: string;
|
||||
position?: [number, number, number];
|
||||
}
|
||||
|
||||
export interface RerunViewportProps {
|
||||
sourceUrl: string;
|
||||
onStatusChange?: (status: RerunViewportStatus, message?: string) => void;
|
||||
onSelectionChange?: (selection: RerunSelection | null) => void;
|
||||
}
|
||||
|
||||
export function RerunViewport({
|
||||
sourceUrl,
|
||||
onStatusChange,
|
||||
onSelectionChange,
|
||||
}: RerunViewportProps) {
|
||||
const hostRef = useRef<HTMLDivElement>(null);
|
||||
const [status, setStatus] = useState<RerunViewportStatus>(sourceUrl ? "loading" : "idle");
|
||||
|
||||
useEffect(() => {
|
||||
const normalizedSource = sourceUrl.trim();
|
||||
if (!normalizedSource || !hostRef.current) {
|
||||
setStatus("idle");
|
||||
onStatusChange?.("idle");
|
||||
onSelectionChange?.(null);
|
||||
return;
|
||||
}
|
||||
|
||||
let disposed = false;
|
||||
let stopViewer: (() => void) | undefined;
|
||||
const unsubscribers: Array<() => void> = [];
|
||||
setStatus("loading");
|
||||
onStatusChange?.("loading");
|
||||
|
||||
void import("@rerun-io/web-viewer")
|
||||
.then(async ({ WebViewer }) => {
|
||||
const viewer = new WebViewer();
|
||||
stopViewer = () => {
|
||||
// Remove the gRPC receiver explicitly before tearing down WASM. This
|
||||
// closes the browser-side stream promptly so the local SDK server can
|
||||
// release its port before the next device session starts.
|
||||
try {
|
||||
viewer.close(normalizedSource);
|
||||
} catch {
|
||||
// The viewer can already be stopped after a startup failure.
|
||||
}
|
||||
viewer.stop();
|
||||
};
|
||||
|
||||
await viewer.start(
|
||||
normalizedSource,
|
||||
hostRef.current,
|
||||
{
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
theme: "dark",
|
||||
hide_welcome_screen: true,
|
||||
enable_history: false,
|
||||
allow_fullscreen: false,
|
||||
},
|
||||
null,
|
||||
);
|
||||
if (disposed) {
|
||||
stopViewer();
|
||||
return;
|
||||
}
|
||||
|
||||
viewer.override_panel_state("top", "hidden");
|
||||
viewer.override_panel_state("blueprint", "hidden");
|
||||
viewer.override_panel_state("selection", "hidden");
|
||||
viewer.override_panel_state("time", "hidden");
|
||||
|
||||
unsubscribers.push(
|
||||
viewer.on("selection_change", (event) => {
|
||||
const entity = event.items.find((item) => item.type === "entity");
|
||||
if (!entity || entity.type !== "entity") {
|
||||
onSelectionChange?.(null);
|
||||
return;
|
||||
}
|
||||
onSelectionChange?.({
|
||||
entityPath: entity.entity_path,
|
||||
viewName: entity.view_name,
|
||||
position: entity.position,
|
||||
});
|
||||
}),
|
||||
);
|
||||
|
||||
setStatus("ready");
|
||||
onStatusChange?.("ready");
|
||||
})
|
||||
.catch(() => {
|
||||
if (disposed) return;
|
||||
const message = "Не удалось запустить встроенный визуализатор.";
|
||||
setStatus("error");
|
||||
onStatusChange?.("error", message);
|
||||
});
|
||||
|
||||
return () => {
|
||||
disposed = true;
|
||||
unsubscribers.forEach((unsubscribe) => unsubscribe());
|
||||
stopViewer?.();
|
||||
onSelectionChange?.(null);
|
||||
};
|
||||
}, [onSelectionChange, onStatusChange, sourceUrl]);
|
||||
|
||||
return (
|
||||
<div className="rerun-viewport" data-status={status}>
|
||||
<div ref={hostRef} className="rerun-viewport__canvas" />
|
||||
{status === "loading" ? (
|
||||
<div className="rerun-viewport__notice" role="status">
|
||||
<span className="busy-indicator" aria-hidden="true" />
|
||||
<div>
|
||||
<strong>Подключаем визуальный источник</strong>
|
||||
<span>Запуск встроенного визуализатора Rerun.</span>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
{status === "error" ? (
|
||||
<div className="rerun-viewport__notice rerun-viewport__notice--error" role="alert">
|
||||
<div>
|
||||
<strong>Источник не открыт</strong>
|
||||
<span>Проверьте адрес RRD или Rerun gRPC в настройках источника.</span>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user