fix(navigation): return repeated section selections to their entry view

This commit is contained in:
DCCONSTRUCTIONS
2026-09-25 19:14:21 +03:00
parent a993eda6b8
commit db3b1d7b67
6 changed files with 35 additions and 10 deletions
+16 -3
View File
@@ -152,6 +152,7 @@ export default function App() {
contentExpanded: true,
});
const [navigationRevision, setNavigationRevision] = useState(0);
const [activeRoot, setActiveRoot] = useState<RootId | null>(
polygonDatasetRoute.active ? "data" : null,
);
@@ -457,6 +458,17 @@ export default function App() {
workspace.openView(viewId);
};
// Explicit menu navigation always returns to that workspace's entry screen.
const openNavigationView = (viewId: string) => {
if (!workspaceById(viewId)) return;
setNavigationRevision(value => value + 1);
sessionOverview.close();
setSourceWindowOpen(false);
setDisplayWindowOpen(false);
setLayerInspectorOpen(false);
openView(viewId);
};
const openSource = () => {
if (!sceneWorkspaceActive) return;
setSourceDraft(sourceUrl);
@@ -729,6 +741,7 @@ export default function App() {
<SystemNavigationPanel
title={currentRoot.title}
onAdd={computeContourSettings.openCreate}
onNavigateRoot={() => openNavigationView("modules")}
onClose={workspace.closeNavigation}
/>
) : currentRoot ? (
@@ -755,7 +768,7 @@ export default function App() {
icon: <Icon name={item.icon} />,
}))}
activeId={workspace.activeView ?? undefined}
onItemChange={openView}
onItemChange={openNavigationView}
footer={
<>
<span className="nodedc-admin-panel__nav-icon" aria-hidden="true">
@@ -831,7 +844,7 @@ export default function App() {
) : activeDefinition.root === "system" ? (
<SystemWorkspaceSelector
value={activeDefinition.id}
onChange={openView}
onChange={openNavigationView}
/>
) : (
<StatusBadge tone="warning">Интерфейс готов</StatusBadge>
@@ -840,7 +853,7 @@ export default function App() {
utilityActions={contentActions}
onClose={workspace.closeView}
>
<PlanningCaptureGuard
<PlanningCaptureGuard key={`${activeDefinition.id}:${navigationRevision}`}
enabled={launchProfile === 'direct' && (activeDefinition.kind === 'device' || activeDefinition.kind === 'spatial')}
onResume={() => openView('local-device', 'planning')}
>
@@ -0,0 +1,8 @@
import {createPortal} from 'react-dom';
import {Icon,IconButton} from '@nodedc/ui-react';
/** A nested workspace returns through the shared panel header. */
export function WorkspaceBackButton({label,onClick,host}:{label:string;onClick:()=>void;host?:HTMLElement|null}) {
const button=<IconButton label={label} onClick={onClick}><Icon name="chevron-left"/></IconButton>;
return host?createPortal(button,host):button;
}
@@ -9,12 +9,14 @@ import { useComputeContours } from "../../core/system/ComputeContourContext";
interface SystemNavigationPanelProps {
title: string;
onAdd: () => void;
onNavigateRoot: () => void;
onClose: () => void;
}
export function SystemNavigationPanel({
title,
onAdd,
onNavigateRoot,
onClose,
}: SystemNavigationPanelProps) {
const { contours, selectedContour, selectContour } = useComputeContours();
@@ -40,7 +42,7 @@ export function SystemNavigationPanel({
description: contour.expected_node_id,
icon: <Icon name="apps" />,
active: selectedContour?.contour_id === contour.contour_id,
onSelect: () => selectContour(contour.contour_id),
onSelect: () => { selectContour(contour.contour_id); onNavigateRoot(); },
}))}
items={[]}
onItemChange={() => undefined}
@@ -37,5 +37,5 @@ export function useSessionOverviewMode(enabled: boolean) {
window.addEventListener("keydown", close, true);
return () => window.removeEventListener("keydown", close, true);
}, [open]);
return { open: enabled && open, toggle: useCallback(() => setOpen(v => !v), []) };
return { open: enabled && open, close: useCallback(() => setOpen(false), []), toggle: useCallback(() => setOpen(v => !v), []) };
}
@@ -1,5 +1,6 @@
import {WorkspaceBackButton} from '../../components/WorkspaceBackButton';
import {useState} from 'react';
import {ActivityIndicator,Button,Icon,ResourceList,ResourceRow,Select,SettingsCard,StatusBadge,TextField,ToastStack} from '@nodedc/ui-react';
import {ActivityIndicator,Button,ResourceList,ResourceRow,Select,SettingsCard,StatusBadge,TextField,ToastStack} from '@nodedc/ui-react';
import {useBoardMonitor,monitorSeries,monitorValue} from '../../core/fleet/boardMonitor';
import {TelemetrySeries} from '../../components/system/TelemetrySeries';
import '../../styles/system-telemetry.css';
@@ -7,7 +8,7 @@ import './board-monitor.css';
const eventLabels:Record<string,string>={'ui-error':'Ошибка интерфейса','ui-rejection':'Ошибка асинхронной операции','ui-render-error':'Ошибка представления','system-oom':'Нехватка памяти','gpu-reset':'Сброс GPU','disk-error':'Ошибка диска','usb-disconnected':'Отключение USB','usb-error':'Ошибка USB','service-failed':'Сбой службы','ui-process-exit':'Завершение процесса окна','ui-load-failed':'Ошибка загрузки окна'};
const time=(value:number)=>new Date(value*1000).toLocaleString('ru-RU');
export function BoardMonitorView({vehicle,name,back}:{vehicle:string;name:string;back:()=>void}) {
export function BoardMonitorView({vehicle,name,back,headerToolsHost}:{vehicle:string;name:string;back:()=>void;headerToolsHost?:HTMLElement|null}) {
const [metric,setMetric]=useState('cpu.usage'),[group,setGroup]=useState('CPU'),[window,setWindow]=useState(900),[end,setEnd]=useState<number|null>(null),[mode,setMode]=useState<'min'|'max'|'mean'>('max');
const {value,error,dismissError}=useBoardMonitor(vehicle,metric,window,end);
const definitions=value?.definitions??{},definition=definitions[metric];
@@ -15,7 +16,7 @@ export function BoardMonitorView({vehicle,name,back}:{vehicle:string;name:string
const metrics=Object.entries(definitions).filter(([,item])=>item.group===group);
const latest=value?.latest;
return <div className="board-monitor">
<div><Button onClick={back}><Icon name="chevron-left"/>К аппарату</Button></div>
<WorkspaceBackButton label="К аппарату" onClick={back} host={headerToolsHost}/>
<SettingsCard title="Мониторинг системы БК" description={name} actions={<StatusBadge tone={value?.fresh&&!error?'success':'neutral'}>{value?.fresh&&!error?'Данные поступают':'Нет свежих данных'}</StatusBadge>}>
{latest&&<p>Измерено на БК: {time(latest.at)}. {value?.storage==='ready'?'Локальная история записывается.':'Запись локальной истории недоступна.'} {value?.backlog?'История догружается в Mission Core.':''}</p>}
{value?.database_bytes!=null&&<p>Архив на БК: {monitorValue(value.database_bytes,'bytes')} из {monitorValue(value.database_budget_bytes,'bytes')}. {value.archive_since?`Копия в Mission Core с ${time(value.archive_since)}.`:''}</p>}
@@ -1,3 +1,4 @@
import {WorkspaceBackButton} from '../../components/WorkspaceBackButton';
import {BoardObservationCenter} from './observation/BoardObservationCenter';
import {BoardMonitorView} from './BoardMonitorView';
import { useEffect, useRef, useState } from "react";
@@ -44,12 +45,12 @@ export function VehiclesWorkspace({ createRequest = 0, headerToolsHost }: { crea
}
const detail = fleet.items?.find(item => item.id === selected);
if(detail&&observationOpen)return <BoardObservationCenter key={detail.id} vehicleID={detail.id} name={detail.name} enabled={!fleet.error&&detail.enrollment==="paired"&&detail.connectivity==="online"} back={()=>{setObservationOpen(false);setSelected(null);}} configure={()=>setObservationOpen(false)} headerToolsHost={headerToolsHost}/>;
if(detail&&monitorOpen)return <BoardMonitorView vehicle={detail.id} name={detail.name} back={()=>setMonitorOpen(false)}/>;
if(detail&&monitorOpen)return <BoardMonitorView vehicle={detail.id} name={detail.name} back={()=>setMonitorOpen(false)} headerToolsHost={headerToolsHost}/>;
return <div className="fleet-workspace">
{fleet.error && <p role="alert">{fleet.error}</p>}
{!adding && error && <p role="alert">{error}</p>}
{detail ? <>
<div><Button onClick={() => {setSelected(null);}}>К списку аппаратов</Button></div>
<WorkspaceBackButton label="К списку аппаратов" onClick={()=>setSelected(null)} host={headerToolsHost}/>
<VehicleSensors vehicleID={detail.id} enabled={!fleet.error && detail.enrollment === "paired" && detail.connectivity === "online"} description={detail.name} computer={<SettingsCard title={detail.name} description={`${platformLabel(detail.platform)} · с бортовым компьютером`} actions={<StatusBadge tone={!fleet.error && detail.enrollment === "paired" && detail.connectivity === "online" ? "success" : "neutral"}>{fleet.error ? "Нет свежих данных" : statusLabel(detail)}</StatusBadge>}>
{detail.notice && <p role="status">{detail.notice}</p>}
<dl className="fleet-facts"><div><dt>Бортовой компьютер</dt><dd>{detail.node_id}</dd></div>