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, contentExpanded: true,
}); });
const [navigationRevision, setNavigationRevision] = useState(0);
const [activeRoot, setActiveRoot] = useState<RootId | null>( const [activeRoot, setActiveRoot] = useState<RootId | null>(
polygonDatasetRoute.active ? "data" : null, polygonDatasetRoute.active ? "data" : null,
); );
@@ -457,6 +458,17 @@ export default function App() {
workspace.openView(viewId); 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 = () => { const openSource = () => {
if (!sceneWorkspaceActive) return; if (!sceneWorkspaceActive) return;
setSourceDraft(sourceUrl); setSourceDraft(sourceUrl);
@@ -729,6 +741,7 @@ export default function App() {
<SystemNavigationPanel <SystemNavigationPanel
title={currentRoot.title} title={currentRoot.title}
onAdd={computeContourSettings.openCreate} onAdd={computeContourSettings.openCreate}
onNavigateRoot={() => openNavigationView("modules")}
onClose={workspace.closeNavigation} onClose={workspace.closeNavigation}
/> />
) : currentRoot ? ( ) : currentRoot ? (
@@ -755,7 +768,7 @@ export default function App() {
icon: <Icon name={item.icon} />, icon: <Icon name={item.icon} />,
}))} }))}
activeId={workspace.activeView ?? undefined} activeId={workspace.activeView ?? undefined}
onItemChange={openView} onItemChange={openNavigationView}
footer={ footer={
<> <>
<span className="nodedc-admin-panel__nav-icon" aria-hidden="true"> <span className="nodedc-admin-panel__nav-icon" aria-hidden="true">
@@ -831,7 +844,7 @@ export default function App() {
) : activeDefinition.root === "system" ? ( ) : activeDefinition.root === "system" ? (
<SystemWorkspaceSelector <SystemWorkspaceSelector
value={activeDefinition.id} value={activeDefinition.id}
onChange={openView} onChange={openNavigationView}
/> />
) : ( ) : (
<StatusBadge tone="warning">Интерфейс готов</StatusBadge> <StatusBadge tone="warning">Интерфейс готов</StatusBadge>
@@ -840,7 +853,7 @@ export default function App() {
utilityActions={contentActions} utilityActions={contentActions}
onClose={workspace.closeView} onClose={workspace.closeView}
> >
<PlanningCaptureGuard <PlanningCaptureGuard key={`${activeDefinition.id}:${navigationRevision}`}
enabled={launchProfile === 'direct' && (activeDefinition.kind === 'device' || activeDefinition.kind === 'spatial')} enabled={launchProfile === 'direct' && (activeDefinition.kind === 'device' || activeDefinition.kind === 'spatial')}
onResume={() => openView('local-device', 'planning')} 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 { interface SystemNavigationPanelProps {
title: string; title: string;
onAdd: () => void; onAdd: () => void;
onNavigateRoot: () => void;
onClose: () => void; onClose: () => void;
} }
export function SystemNavigationPanel({ export function SystemNavigationPanel({
title, title,
onAdd, onAdd,
onNavigateRoot,
onClose, onClose,
}: SystemNavigationPanelProps) { }: SystemNavigationPanelProps) {
const { contours, selectedContour, selectContour } = useComputeContours(); const { contours, selectedContour, selectContour } = useComputeContours();
@@ -40,7 +42,7 @@ export function SystemNavigationPanel({
description: contour.expected_node_id, description: contour.expected_node_id,
icon: <Icon name="apps" />, icon: <Icon name="apps" />,
active: selectedContour?.contour_id === contour.contour_id, active: selectedContour?.contour_id === contour.contour_id,
onSelect: () => selectContour(contour.contour_id), onSelect: () => { selectContour(contour.contour_id); onNavigateRoot(); },
}))} }))}
items={[]} items={[]}
onItemChange={() => undefined} onItemChange={() => undefined}
@@ -37,5 +37,5 @@ export function useSessionOverviewMode(enabled: boolean) {
window.addEventListener("keydown", close, true); window.addEventListener("keydown", close, true);
return () => window.removeEventListener("keydown", close, true); return () => window.removeEventListener("keydown", close, true);
}, [open]); }, [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 {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 {useBoardMonitor,monitorSeries,monitorValue} from '../../core/fleet/boardMonitor';
import {TelemetrySeries} from '../../components/system/TelemetrySeries'; import {TelemetrySeries} from '../../components/system/TelemetrySeries';
import '../../styles/system-telemetry.css'; 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 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'); 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 [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 {value,error,dismissError}=useBoardMonitor(vehicle,metric,window,end);
const definitions=value?.definitions??{},definition=definitions[metric]; 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 metrics=Object.entries(definitions).filter(([,item])=>item.group===group);
const latest=value?.latest; const latest=value?.latest;
return <div className="board-monitor"> 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>}> <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>} {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>} {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 {BoardObservationCenter} from './observation/BoardObservationCenter';
import {BoardMonitorView} from './BoardMonitorView'; import {BoardMonitorView} from './BoardMonitorView';
import { useEffect, useRef, useState } from "react"; 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); 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&&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"> return <div className="fleet-workspace">
{fleet.error && <p role="alert">{fleet.error}</p>} {fleet.error && <p role="alert">{fleet.error}</p>}
{!adding && error && <p role="alert">{error}</p>} {!adding && error && <p role="alert">{error}</p>}
{detail ? <> {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>}> <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>} {detail.notice && <p role="status">{detail.notice}</p>}
<dl className="fleet-facts"><div><dt>Бортовой компьютер</dt><dd>{detail.node_id}</dd></div> <dl className="fleet-facts"><div><dt>Бортовой компьютер</dt><dd>{detail.node_id}</dd></div>