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,
|
||||
});
|
||||
|
||||
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>
|
||||
|
||||
Reference in New Issue
Block a user