fix(ui): align telemetry and laboratory navigation
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import {
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
type ComponentType,
|
||||
} from "react";
|
||||
@@ -71,6 +72,11 @@ const EMPTY_ADVANCED_RESULTS: AdvancedLaboratoryResults = {
|
||||
e37: null,
|
||||
};
|
||||
|
||||
function laboratoryWorkOrdinal(value: string): number {
|
||||
const match = value.match(/\bE(\d+)\b/i);
|
||||
return match ? Number(match[1]) : -1;
|
||||
}
|
||||
|
||||
function digestFromContentId(value: string | null | undefined): string | null {
|
||||
const digest = value?.split("-").at(-1) ?? "";
|
||||
return /^[a-f0-9]{64}$/.test(digest) ? digest : null;
|
||||
@@ -559,6 +565,7 @@ function PublishedLaboratoryResult({
|
||||
export function LaboratoryArchiveWorkspace(props: LaboratoryWorkspaceProps) {
|
||||
const [profileId, setProfileId] = useState<LaboratoryProfileId>("sensor-fusion");
|
||||
const [workId, setWorkId] = useState<LaboratoryWorkId>("e28-local-surface");
|
||||
const initialWorkSelectedRef = useRef(false);
|
||||
const [e28Model, setE28Model] = useState<LidarLocalSurfaceModel | null>(null);
|
||||
const [e29Result, setE29Result] = useState<E29EvidenceResult | null>(null);
|
||||
const [e30Result, setE30Result] = useState<E30ReviewResult | null>(null);
|
||||
@@ -580,7 +587,12 @@ export function LaboratoryArchiveWorkspace(props: LaboratoryWorkspaceProps) {
|
||||
&& session.status === "ready"
|
||||
&& session.replayable
|
||||
&& session.modalities.includes("point-cloud")
|
||||
)),
|
||||
)).sort((left, right) => {
|
||||
const ordinalDelta = laboratoryWorkOrdinal(right.lab?.labId ?? "")
|
||||
- laboratoryWorkOrdinal(left.lab?.labId ?? "");
|
||||
if (ordinalDelta !== 0) return ordinalDelta;
|
||||
return (right.startedAtUtc ?? "").localeCompare(left.startedAtUtc ?? "");
|
||||
}),
|
||||
[sessions.items],
|
||||
);
|
||||
const sourceSessions = useMemo(
|
||||
@@ -661,7 +673,9 @@ export function LaboratoryArchiveWorkspace(props: LaboratoryWorkspaceProps) {
|
||||
advancedResults,
|
||||
sourceSessions,
|
||||
));
|
||||
return items;
|
||||
return items.sort(
|
||||
(left, right) => laboratoryWorkOrdinal(right.label) - laboratoryWorkOrdinal(left.label),
|
||||
);
|
||||
}, [
|
||||
advancedResults,
|
||||
e28Model,
|
||||
@@ -717,13 +731,26 @@ export function LaboratoryArchiveWorkspace(props: LaboratoryWorkspaceProps) {
|
||||
setProfileId(firstProfile.id);
|
||||
if (firstProfile.id === "sensor-fusion") {
|
||||
const firstWork = sensorWorks[0];
|
||||
if (firstWork) setWorkId(firstWork.id);
|
||||
if (firstWork) {
|
||||
setWorkId(firstWork.id);
|
||||
initialWorkSelectedRef.current = true;
|
||||
}
|
||||
} else {
|
||||
const first = publishedWorks[0];
|
||||
if (first) setWorkId(`session:${first.id}`);
|
||||
if (first) {
|
||||
setWorkId(`session:${first.id}`);
|
||||
initialWorkSelectedRef.current = true;
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (!initialWorkSelectedRef.current) {
|
||||
const freshestWork = workOptions[0];
|
||||
if (!freshestWork) return;
|
||||
setWorkId(freshestWork.id);
|
||||
initialWorkSelectedRef.current = true;
|
||||
return;
|
||||
}
|
||||
if (!workOptions.some((work) => work.id === workId)) {
|
||||
const firstWork = workOptions[0];
|
||||
if (firstWork) setWorkId(firstWork.id);
|
||||
@@ -741,6 +768,7 @@ export function LaboratoryArchiveWorkspace(props: LaboratoryWorkspaceProps) {
|
||||
|
||||
const selectProfile = (next: LaboratoryProfileId) => {
|
||||
setProfileId(next);
|
||||
initialWorkSelectedRef.current = true;
|
||||
if (next === "sensor-fusion") {
|
||||
const first = sensorWorks[0];
|
||||
if (first) setWorkId(first.id);
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import {
|
||||
Button,
|
||||
GlassSurface,
|
||||
Icon,
|
||||
StatusBadge,
|
||||
} from "@nodedc/ui-react";
|
||||
|
||||
@@ -24,14 +22,53 @@ function pipelineStateLabel(state: string): string {
|
||||
return "Нет live-состояния";
|
||||
}
|
||||
|
||||
function formatResourcePair(
|
||||
used: number | null | undefined,
|
||||
total: number | null | undefined,
|
||||
): string | null {
|
||||
if (
|
||||
typeof used !== "number"
|
||||
|| !Number.isFinite(used)
|
||||
|| typeof total !== "number"
|
||||
|| !Number.isFinite(total)
|
||||
|| total <= 0
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
return `${formatBytes(used)} / ${formatBytes(total)}`;
|
||||
}
|
||||
|
||||
function formatCpuCapacity(
|
||||
percent: number | null | undefined,
|
||||
logicalProcessors: number | null | undefined,
|
||||
): string | null {
|
||||
if (
|
||||
typeof percent !== "number"
|
||||
|| !Number.isFinite(percent)
|
||||
|| typeof logicalProcessors !== "number"
|
||||
|| !Number.isFinite(logicalProcessors)
|
||||
|| logicalProcessors <= 0
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
const used = new Intl.NumberFormat("ru-RU", {
|
||||
maximumFractionDigits: 1,
|
||||
}).format(percent / 100 * logicalProcessors);
|
||||
return `≈ ${used} / ${logicalProcessors} лог. ядра`;
|
||||
}
|
||||
|
||||
export function ComputeModulesWorkspace() {
|
||||
const { selectedContour } = useComputeContours();
|
||||
const {
|
||||
selectedContour,
|
||||
telemetryRefreshGeneration,
|
||||
} = useComputeContours();
|
||||
const legacyDiagnostic = selectedContour?.contour_id === "worker-006"
|
||||
&& selectedContour.telemetry_mode === "legacy-ssh";
|
||||
const supportsLiveTelemetry = selectedContour?.contour_id === "worker-006";
|
||||
const { telemetry, error, refresh } = useWorkerTelemetry(
|
||||
const { telemetry, error } = useWorkerTelemetry(
|
||||
(selectedContour?.telemetry_poll_interval_seconds ?? 3) * 1_000,
|
||||
supportsLiveTelemetry,
|
||||
telemetryRefreshGeneration,
|
||||
);
|
||||
const node = telemetry?.node ?? null;
|
||||
const missionCoreRuntimes = telemetry?.runtimes.filter((runtime) => !runtime.external) ?? [];
|
||||
@@ -45,6 +82,12 @@ export function ComputeModulesWorkspace() {
|
||||
const memoryPercent = node?.memory.used_percent;
|
||||
const gpuPercent = node?.gpu?.utilization_percent;
|
||||
const gpuMemoryPercent = node?.gpu?.memory_used_percent;
|
||||
const gpuMemoryUsedBytes = typeof node?.gpu?.memory_used_mib === "number"
|
||||
? node.gpu.memory_used_mib * 1024 * 1024
|
||||
: null;
|
||||
const gpuMemoryTotalBytes = typeof node?.gpu?.memory_total_mib === "number"
|
||||
? node.gpu.memory_total_mib * 1024 * 1024
|
||||
: null;
|
||||
|
||||
return (
|
||||
<div className="system-workspace compute-modules-workspace">
|
||||
@@ -63,15 +106,6 @@ export function ComputeModulesWorkspace() {
|
||||
? agentTelemetry ? "Агент доступен" : "SSH-диагностика"
|
||||
: "Нет свежих данных"}
|
||||
</StatusBadge>
|
||||
<Button
|
||||
size="compact"
|
||||
variant="secondary"
|
||||
disabled={!supportsLiveTelemetry}
|
||||
onClick={refresh}
|
||||
icon={<Icon name="refresh" size={14} />}
|
||||
>
|
||||
Обновить
|
||||
</Button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -92,24 +126,37 @@ export function ComputeModulesWorkspace() {
|
||||
<TelemetrySeries
|
||||
label="CPU"
|
||||
value={formatOptionalPercent(cpuPercent)}
|
||||
resource={formatCpuCapacity(
|
||||
cpuPercent,
|
||||
node?.cpu.logical_processors,
|
||||
)}
|
||||
ceiling={100}
|
||||
values={history.map((item) => item.cpu_percent)}
|
||||
/>
|
||||
<TelemetrySeries
|
||||
label="RAM"
|
||||
value={formatOptionalPercent(memoryPercent)}
|
||||
resource={formatResourcePair(
|
||||
node?.memory.used_bytes,
|
||||
node?.memory.total_bytes,
|
||||
)}
|
||||
ceiling={100}
|
||||
values={history.map((item) => item.memory_percent)}
|
||||
/>
|
||||
<TelemetrySeries
|
||||
label="GPU"
|
||||
value={formatOptionalPercent(gpuPercent)}
|
||||
resource={node?.gpu?.name ?? null}
|
||||
ceiling={100}
|
||||
values={history.map((item) => item.gpu_percent)}
|
||||
/>
|
||||
<TelemetrySeries
|
||||
label="VRAM"
|
||||
value={formatOptionalPercent(gpuMemoryPercent)}
|
||||
resource={formatResourcePair(
|
||||
gpuMemoryUsedBytes,
|
||||
gpuMemoryTotalBytes,
|
||||
)}
|
||||
ceiling={100}
|
||||
values={history.map((item) => item.gpu_memory_percent)}
|
||||
/>
|
||||
@@ -135,11 +182,7 @@ export function ComputeModulesWorkspace() {
|
||||
</dl>
|
||||
<dl>
|
||||
<div><dt>GPU</dt><dd>{node?.gpu?.name ?? "—"}</dd></div>
|
||||
<div><dt>VRAM занята</dt><dd>{formatBytes(
|
||||
typeof node?.gpu?.memory_used_mib === "number"
|
||||
? node.gpu.memory_used_mib * 1024 * 1024
|
||||
: null,
|
||||
)}</dd></div>
|
||||
<div><dt>VRAM занята</dt><dd>{formatBytes(gpuMemoryUsedBytes)}</dd></div>
|
||||
<div><dt>Температура</dt><dd>{
|
||||
typeof node?.gpu?.temperature_celsius === "number"
|
||||
? `${node.gpu.temperature_celsius} °C`
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import {
|
||||
Button,
|
||||
GlassSurface,
|
||||
Icon,
|
||||
StatusBadge,
|
||||
} from "@nodedc/ui-react";
|
||||
|
||||
@@ -18,13 +16,17 @@ import {
|
||||
} from "../../core/system/useWorkerTelemetry";
|
||||
|
||||
export function NetworkWorkspace() {
|
||||
const { selectedContour } = useComputeContours();
|
||||
const {
|
||||
selectedContour,
|
||||
telemetryRefreshGeneration,
|
||||
} = useComputeContours();
|
||||
const legacyDiagnostic = selectedContour?.contour_id === "worker-006"
|
||||
&& selectedContour.telemetry_mode === "legacy-ssh";
|
||||
const supportsLiveTelemetry = selectedContour?.contour_id === "worker-006";
|
||||
const { telemetry, error, refresh } = useWorkerTelemetry(
|
||||
const { telemetry, error } = useWorkerTelemetry(
|
||||
(selectedContour?.telemetry_poll_interval_seconds ?? 3) * 1_000,
|
||||
supportsLiveTelemetry,
|
||||
telemetryRefreshGeneration,
|
||||
);
|
||||
const aggregate = telemetry?.network.aggregate ?? null;
|
||||
const connected = Boolean(
|
||||
@@ -50,15 +52,6 @@ export function NetworkWorkspace() {
|
||||
<StatusBadge tone={connected ? "success" : "danger"}>
|
||||
{connected ? "Маршрут доступен" : "Нет свежих данных"}
|
||||
</StatusBadge>
|
||||
<Button
|
||||
size="compact"
|
||||
variant="secondary"
|
||||
disabled={!supportsLiveTelemetry}
|
||||
onClick={refresh}
|
||||
icon={<Icon name="refresh" size={14} />}
|
||||
>
|
||||
Обновить
|
||||
</Button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user