fix(navigation): return repeated section selections to their entry view
This commit is contained in:
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user