fix(ui): align telemetry and laboratory navigation
This commit is contained in:
@@ -619,7 +619,7 @@ export default function App() {
|
|||||||
refreshRuntime: runtime.refresh,
|
refreshRuntime: runtime.refresh,
|
||||||
saveWorkspaceLayout,
|
saveWorkspaceLayout,
|
||||||
workspaceLayoutSaving: workspaceLayoutProfile.state === "saving",
|
workspaceLayoutSaving: workspaceLayoutProfile.state === "saving",
|
||||||
systemUtilityAction: computeContourSettings.utilityAction,
|
systemUtilityActions: computeContourSettings.utilityActions,
|
||||||
});
|
});
|
||||||
|
|
||||||
const header = (
|
const header = (
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ interface TelemetrySeriesProps {
|
|||||||
label: string;
|
label: string;
|
||||||
values: Array<number | null>;
|
values: Array<number | null>;
|
||||||
value: string;
|
value: string;
|
||||||
|
resource?: string | null;
|
||||||
ceiling?: number;
|
ceiling?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -24,13 +25,17 @@ export function TelemetrySeries({
|
|||||||
label,
|
label,
|
||||||
values,
|
values,
|
||||||
value,
|
value,
|
||||||
|
resource,
|
||||||
ceiling,
|
ceiling,
|
||||||
}: TelemetrySeriesProps) {
|
}: TelemetrySeriesProps) {
|
||||||
const points = linePoints(values, ceiling);
|
const points = linePoints(values, ceiling);
|
||||||
return (
|
return (
|
||||||
<div className="system-telemetry-series">
|
<div className="system-telemetry-series">
|
||||||
<div>
|
<div>
|
||||||
<span>{label}</span>
|
<span className="system-telemetry-series__label">
|
||||||
|
<span>{label}</span>
|
||||||
|
{resource ? <small>{resource}</small> : null}
|
||||||
|
</span>
|
||||||
<strong>{value}</strong>
|
<strong>{value}</strong>
|
||||||
</div>
|
</div>
|
||||||
<svg
|
<svg
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ interface ComputeContourSettingsController {
|
|||||||
canEdit: boolean;
|
canEdit: boolean;
|
||||||
openCreate: () => void;
|
openCreate: () => void;
|
||||||
openEdit: () => void;
|
openEdit: () => void;
|
||||||
utilityAction: ApplicationPanelUtilityAction;
|
utilityActions: readonly ApplicationPanelUtilityAction[];
|
||||||
window: ReactNode;
|
window: ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -24,18 +24,30 @@ export function useComputeContourSettings(): ComputeContourSettingsController {
|
|||||||
setMode("edit");
|
setMode("edit");
|
||||||
setOpen(true);
|
setOpen(true);
|
||||||
}, []);
|
}, []);
|
||||||
const utilityAction = useMemo<ApplicationPanelUtilityAction>(() => ({
|
const utilityActions = useMemo<readonly ApplicationPanelUtilityAction[]>(() => [
|
||||||
label: "Настроить выбранный вычислительный контур",
|
{
|
||||||
icon: "settings",
|
label: "Обновить телеметрию выбранного контура",
|
||||||
disabled: contours.selectedContour === null,
|
icon: "refresh",
|
||||||
onClick: openEdit,
|
disabled: contours.selectedContour?.contour_id !== "worker-006",
|
||||||
}), [contours.selectedContour, openEdit]);
|
onClick: contours.refreshTelemetry,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Настроить выбранный вычислительный контур",
|
||||||
|
icon: "settings",
|
||||||
|
disabled: contours.selectedContour === null,
|
||||||
|
onClick: openEdit,
|
||||||
|
},
|
||||||
|
], [
|
||||||
|
contours.refreshTelemetry,
|
||||||
|
contours.selectedContour,
|
||||||
|
openEdit,
|
||||||
|
]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
canEdit: contours.selectedContour !== null,
|
canEdit: contours.selectedContour !== null,
|
||||||
openCreate,
|
openCreate,
|
||||||
openEdit,
|
openEdit,
|
||||||
utilityAction,
|
utilityActions,
|
||||||
window: (
|
window: (
|
||||||
<ComputeContourSettingsWindow
|
<ComputeContourSettingsWindow
|
||||||
open={open}
|
open={open}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ interface ApplicationPanelActionsOptions {
|
|||||||
refreshRuntime: () => void;
|
refreshRuntime: () => void;
|
||||||
saveWorkspaceLayout: () => Promise<void>;
|
saveWorkspaceLayout: () => Promise<void>;
|
||||||
workspaceLayoutSaving: boolean;
|
workspaceLayoutSaving: boolean;
|
||||||
systemUtilityAction: ApplicationPanelUtilityAction;
|
systemUtilityActions: readonly ApplicationPanelUtilityAction[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useApplicationPanelActions({
|
export function useApplicationPanelActions({
|
||||||
@@ -16,7 +16,7 @@ export function useApplicationPanelActions({
|
|||||||
refreshRuntime,
|
refreshRuntime,
|
||||||
saveWorkspaceLayout,
|
saveWorkspaceLayout,
|
||||||
workspaceLayoutSaving,
|
workspaceLayoutSaving,
|
||||||
systemUtilityAction,
|
systemUtilityActions,
|
||||||
}: ApplicationPanelActionsOptions): ApplicationPanelUtilityAction[] {
|
}: ApplicationPanelActionsOptions): ApplicationPanelUtilityAction[] {
|
||||||
return useMemo(() => {
|
return useMemo(() => {
|
||||||
const actions: ApplicationPanelUtilityAction[] = [];
|
const actions: ApplicationPanelUtilityAction[] = [];
|
||||||
@@ -35,13 +35,13 @@ export function useApplicationPanelActions({
|
|||||||
onClick: () => void saveWorkspaceLayout(),
|
onClick: () => void saveWorkspaceLayout(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (definition?.root === "system") actions.push(systemUtilityAction);
|
if (definition?.root === "system") actions.push(...systemUtilityActions);
|
||||||
return actions;
|
return actions;
|
||||||
}, [
|
}, [
|
||||||
definition,
|
definition,
|
||||||
refreshRuntime,
|
refreshRuntime,
|
||||||
saveWorkspaceLayout,
|
saveWorkspaceLayout,
|
||||||
systemUtilityAction,
|
systemUtilityActions,
|
||||||
workspaceLayoutSaving,
|
workspaceLayoutSaving,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ interface ComputeContourContextValue {
|
|||||||
selectedContour: ComputeContour | null;
|
selectedContour: ComputeContour | null;
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
error: string | null;
|
error: string | null;
|
||||||
|
telemetryRefreshGeneration: number;
|
||||||
selectContour: (contourId: string) => void;
|
selectContour: (contourId: string) => void;
|
||||||
createContour: (draft: ComputeContourDraft) => Promise<ComputeContour>;
|
createContour: (draft: ComputeContourDraft) => Promise<ComputeContour>;
|
||||||
updateContour: (
|
updateContour: (
|
||||||
@@ -30,6 +31,7 @@ interface ComputeContourContextValue {
|
|||||||
draft: ComputeContourDraft,
|
draft: ComputeContourDraft,
|
||||||
) => Promise<ComputeContour>;
|
) => Promise<ComputeContour>;
|
||||||
refresh: () => void;
|
refresh: () => void;
|
||||||
|
refreshTelemetry: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ComputeContourContext = createContext<ComputeContourContextValue | null>(null);
|
const ComputeContourContext = createContext<ComputeContourContextValue | null>(null);
|
||||||
@@ -42,7 +44,12 @@ export function ComputeContourProvider({ children }: { children: ReactNode }) {
|
|||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [generation, setGeneration] = useState(0);
|
const [generation, setGeneration] = useState(0);
|
||||||
|
const [telemetryRefreshGeneration, setTelemetryRefreshGeneration] = useState(0);
|
||||||
const refresh = useCallback(() => setGeneration((value) => value + 1), []);
|
const refresh = useCallback(() => setGeneration((value) => value + 1), []);
|
||||||
|
const refreshTelemetry = useCallback(
|
||||||
|
() => setTelemetryRefreshGeneration((value) => value + 1),
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const controller = new AbortController();
|
const controller = new AbortController();
|
||||||
@@ -103,19 +110,23 @@ export function ComputeContourProvider({ children }: { children: ReactNode }) {
|
|||||||
selectedContour,
|
selectedContour,
|
||||||
loading,
|
loading,
|
||||||
error,
|
error,
|
||||||
|
telemetryRefreshGeneration,
|
||||||
selectContour,
|
selectContour,
|
||||||
createContour,
|
createContour,
|
||||||
updateContour,
|
updateContour,
|
||||||
refresh,
|
refresh,
|
||||||
|
refreshTelemetry,
|
||||||
}), [
|
}), [
|
||||||
contours,
|
contours,
|
||||||
selectedContour,
|
selectedContour,
|
||||||
loading,
|
loading,
|
||||||
error,
|
error,
|
||||||
|
telemetryRefreshGeneration,
|
||||||
selectContour,
|
selectContour,
|
||||||
createContour,
|
createContour,
|
||||||
updateContour,
|
updateContour,
|
||||||
refresh,
|
refresh,
|
||||||
|
refreshTelemetry,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ export interface WorkerTelemetryState {
|
|||||||
export function useWorkerTelemetry(
|
export function useWorkerTelemetry(
|
||||||
pollMilliseconds = DEFAULT_WORKER_TELEMETRY_POLL_MILLISECONDS,
|
pollMilliseconds = DEFAULT_WORKER_TELEMETRY_POLL_MILLISECONDS,
|
||||||
enabled = true,
|
enabled = true,
|
||||||
|
externalRefreshGeneration = 0,
|
||||||
): WorkerTelemetryState {
|
): WorkerTelemetryState {
|
||||||
const normalizedPollMilliseconds = normalizeWorkerTelemetryPollMilliseconds(
|
const normalizedPollMilliseconds = normalizeWorkerTelemetryPollMilliseconds(
|
||||||
pollMilliseconds,
|
pollMilliseconds,
|
||||||
@@ -51,7 +52,7 @@ export function useWorkerTelemetry(
|
|||||||
if (!controller.signal.aborted) setLoading(false);
|
if (!controller.signal.aborted) setLoading(false);
|
||||||
});
|
});
|
||||||
return () => controller.abort();
|
return () => controller.abort();
|
||||||
}, [enabled, generation]);
|
}, [enabled, externalRefreshGeneration, generation]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!enabled || loading) return;
|
if (!enabled || loading) return;
|
||||||
|
|||||||
@@ -338,10 +338,10 @@
|
|||||||
.scene-operation-status-stack {
|
.scene-operation-status-stack {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
z-index: 11;
|
z-index: 11;
|
||||||
right: 0.85rem;
|
left: 0.85rem;
|
||||||
bottom: 7.15rem;
|
bottom: 7.15rem;
|
||||||
display: grid;
|
display: grid;
|
||||||
justify-items: end;
|
justify-items: start;
|
||||||
gap: 0.3rem;
|
gap: 0.3rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -142,7 +142,7 @@
|
|||||||
|
|
||||||
.system-telemetry-series > div {
|
.system-telemetry-series > div {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: baseline;
|
align-items: flex-start;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
gap: 0.75rem;
|
gap: 0.75rem;
|
||||||
}
|
}
|
||||||
@@ -153,6 +153,22 @@
|
|||||||
font-size: 0.62rem;
|
font-size: 0.62rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.system-telemetry-series__label {
|
||||||
|
display: grid;
|
||||||
|
min-width: 0;
|
||||||
|
gap: 0.16rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.system-telemetry-series__label small {
|
||||||
|
overflow: hidden;
|
||||||
|
max-width: 11rem;
|
||||||
|
color: var(--nodedc-text-secondary);
|
||||||
|
font-size: 0.54rem;
|
||||||
|
line-height: 1.15;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
.system-telemetry-series strong,
|
.system-telemetry-series strong,
|
||||||
.network-stat-card strong {
|
.network-stat-card strong {
|
||||||
color: var(--nodedc-text-primary);
|
color: var(--nodedc-text-primary);
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import {
|
import {
|
||||||
useEffect,
|
useEffect,
|
||||||
useMemo,
|
useMemo,
|
||||||
|
useRef,
|
||||||
useState,
|
useState,
|
||||||
type ComponentType,
|
type ComponentType,
|
||||||
} from "react";
|
} from "react";
|
||||||
@@ -71,6 +72,11 @@ const EMPTY_ADVANCED_RESULTS: AdvancedLaboratoryResults = {
|
|||||||
e37: null,
|
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 {
|
function digestFromContentId(value: string | null | undefined): string | null {
|
||||||
const digest = value?.split("-").at(-1) ?? "";
|
const digest = value?.split("-").at(-1) ?? "";
|
||||||
return /^[a-f0-9]{64}$/.test(digest) ? digest : null;
|
return /^[a-f0-9]{64}$/.test(digest) ? digest : null;
|
||||||
@@ -559,6 +565,7 @@ function PublishedLaboratoryResult({
|
|||||||
export function LaboratoryArchiveWorkspace(props: LaboratoryWorkspaceProps) {
|
export function LaboratoryArchiveWorkspace(props: LaboratoryWorkspaceProps) {
|
||||||
const [profileId, setProfileId] = useState<LaboratoryProfileId>("sensor-fusion");
|
const [profileId, setProfileId] = useState<LaboratoryProfileId>("sensor-fusion");
|
||||||
const [workId, setWorkId] = useState<LaboratoryWorkId>("e28-local-surface");
|
const [workId, setWorkId] = useState<LaboratoryWorkId>("e28-local-surface");
|
||||||
|
const initialWorkSelectedRef = useRef(false);
|
||||||
const [e28Model, setE28Model] = useState<LidarLocalSurfaceModel | null>(null);
|
const [e28Model, setE28Model] = useState<LidarLocalSurfaceModel | null>(null);
|
||||||
const [e29Result, setE29Result] = useState<E29EvidenceResult | null>(null);
|
const [e29Result, setE29Result] = useState<E29EvidenceResult | null>(null);
|
||||||
const [e30Result, setE30Result] = useState<E30ReviewResult | null>(null);
|
const [e30Result, setE30Result] = useState<E30ReviewResult | null>(null);
|
||||||
@@ -580,7 +587,12 @@ export function LaboratoryArchiveWorkspace(props: LaboratoryWorkspaceProps) {
|
|||||||
&& session.status === "ready"
|
&& session.status === "ready"
|
||||||
&& session.replayable
|
&& session.replayable
|
||||||
&& session.modalities.includes("point-cloud")
|
&& 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],
|
[sessions.items],
|
||||||
);
|
);
|
||||||
const sourceSessions = useMemo(
|
const sourceSessions = useMemo(
|
||||||
@@ -661,7 +673,9 @@ export function LaboratoryArchiveWorkspace(props: LaboratoryWorkspaceProps) {
|
|||||||
advancedResults,
|
advancedResults,
|
||||||
sourceSessions,
|
sourceSessions,
|
||||||
));
|
));
|
||||||
return items;
|
return items.sort(
|
||||||
|
(left, right) => laboratoryWorkOrdinal(right.label) - laboratoryWorkOrdinal(left.label),
|
||||||
|
);
|
||||||
}, [
|
}, [
|
||||||
advancedResults,
|
advancedResults,
|
||||||
e28Model,
|
e28Model,
|
||||||
@@ -717,13 +731,26 @@ export function LaboratoryArchiveWorkspace(props: LaboratoryWorkspaceProps) {
|
|||||||
setProfileId(firstProfile.id);
|
setProfileId(firstProfile.id);
|
||||||
if (firstProfile.id === "sensor-fusion") {
|
if (firstProfile.id === "sensor-fusion") {
|
||||||
const firstWork = sensorWorks[0];
|
const firstWork = sensorWorks[0];
|
||||||
if (firstWork) setWorkId(firstWork.id);
|
if (firstWork) {
|
||||||
|
setWorkId(firstWork.id);
|
||||||
|
initialWorkSelectedRef.current = true;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
const first = publishedWorks[0];
|
const first = publishedWorks[0];
|
||||||
if (first) setWorkId(`session:${first.id}`);
|
if (first) {
|
||||||
|
setWorkId(`session:${first.id}`);
|
||||||
|
initialWorkSelectedRef.current = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return;
|
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)) {
|
if (!workOptions.some((work) => work.id === workId)) {
|
||||||
const firstWork = workOptions[0];
|
const firstWork = workOptions[0];
|
||||||
if (firstWork) setWorkId(firstWork.id);
|
if (firstWork) setWorkId(firstWork.id);
|
||||||
@@ -741,6 +768,7 @@ export function LaboratoryArchiveWorkspace(props: LaboratoryWorkspaceProps) {
|
|||||||
|
|
||||||
const selectProfile = (next: LaboratoryProfileId) => {
|
const selectProfile = (next: LaboratoryProfileId) => {
|
||||||
setProfileId(next);
|
setProfileId(next);
|
||||||
|
initialWorkSelectedRef.current = true;
|
||||||
if (next === "sensor-fusion") {
|
if (next === "sensor-fusion") {
|
||||||
const first = sensorWorks[0];
|
const first = sensorWorks[0];
|
||||||
if (first) setWorkId(first.id);
|
if (first) setWorkId(first.id);
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
import {
|
import {
|
||||||
Button,
|
|
||||||
GlassSurface,
|
GlassSurface,
|
||||||
Icon,
|
|
||||||
StatusBadge,
|
StatusBadge,
|
||||||
} from "@nodedc/ui-react";
|
} from "@nodedc/ui-react";
|
||||||
|
|
||||||
@@ -24,14 +22,53 @@ function pipelineStateLabel(state: string): string {
|
|||||||
return "Нет live-состояния";
|
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() {
|
export function ComputeModulesWorkspace() {
|
||||||
const { selectedContour } = useComputeContours();
|
const {
|
||||||
|
selectedContour,
|
||||||
|
telemetryRefreshGeneration,
|
||||||
|
} = useComputeContours();
|
||||||
const legacyDiagnostic = selectedContour?.contour_id === "worker-006"
|
const legacyDiagnostic = selectedContour?.contour_id === "worker-006"
|
||||||
&& selectedContour.telemetry_mode === "legacy-ssh";
|
&& selectedContour.telemetry_mode === "legacy-ssh";
|
||||||
const supportsLiveTelemetry = selectedContour?.contour_id === "worker-006";
|
const supportsLiveTelemetry = selectedContour?.contour_id === "worker-006";
|
||||||
const { telemetry, error, refresh } = useWorkerTelemetry(
|
const { telemetry, error } = useWorkerTelemetry(
|
||||||
(selectedContour?.telemetry_poll_interval_seconds ?? 3) * 1_000,
|
(selectedContour?.telemetry_poll_interval_seconds ?? 3) * 1_000,
|
||||||
supportsLiveTelemetry,
|
supportsLiveTelemetry,
|
||||||
|
telemetryRefreshGeneration,
|
||||||
);
|
);
|
||||||
const node = telemetry?.node ?? null;
|
const node = telemetry?.node ?? null;
|
||||||
const missionCoreRuntimes = telemetry?.runtimes.filter((runtime) => !runtime.external) ?? [];
|
const missionCoreRuntimes = telemetry?.runtimes.filter((runtime) => !runtime.external) ?? [];
|
||||||
@@ -45,6 +82,12 @@ export function ComputeModulesWorkspace() {
|
|||||||
const memoryPercent = node?.memory.used_percent;
|
const memoryPercent = node?.memory.used_percent;
|
||||||
const gpuPercent = node?.gpu?.utilization_percent;
|
const gpuPercent = node?.gpu?.utilization_percent;
|
||||||
const gpuMemoryPercent = node?.gpu?.memory_used_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 (
|
return (
|
||||||
<div className="system-workspace compute-modules-workspace">
|
<div className="system-workspace compute-modules-workspace">
|
||||||
@@ -63,15 +106,6 @@ export function ComputeModulesWorkspace() {
|
|||||||
? agentTelemetry ? "Агент доступен" : "SSH-диагностика"
|
? agentTelemetry ? "Агент доступен" : "SSH-диагностика"
|
||||||
: "Нет свежих данных"}
|
: "Нет свежих данных"}
|
||||||
</StatusBadge>
|
</StatusBadge>
|
||||||
<Button
|
|
||||||
size="compact"
|
|
||||||
variant="secondary"
|
|
||||||
disabled={!supportsLiveTelemetry}
|
|
||||||
onClick={refresh}
|
|
||||||
icon={<Icon name="refresh" size={14} />}
|
|
||||||
>
|
|
||||||
Обновить
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
@@ -92,24 +126,37 @@ export function ComputeModulesWorkspace() {
|
|||||||
<TelemetrySeries
|
<TelemetrySeries
|
||||||
label="CPU"
|
label="CPU"
|
||||||
value={formatOptionalPercent(cpuPercent)}
|
value={formatOptionalPercent(cpuPercent)}
|
||||||
|
resource={formatCpuCapacity(
|
||||||
|
cpuPercent,
|
||||||
|
node?.cpu.logical_processors,
|
||||||
|
)}
|
||||||
ceiling={100}
|
ceiling={100}
|
||||||
values={history.map((item) => item.cpu_percent)}
|
values={history.map((item) => item.cpu_percent)}
|
||||||
/>
|
/>
|
||||||
<TelemetrySeries
|
<TelemetrySeries
|
||||||
label="RAM"
|
label="RAM"
|
||||||
value={formatOptionalPercent(memoryPercent)}
|
value={formatOptionalPercent(memoryPercent)}
|
||||||
|
resource={formatResourcePair(
|
||||||
|
node?.memory.used_bytes,
|
||||||
|
node?.memory.total_bytes,
|
||||||
|
)}
|
||||||
ceiling={100}
|
ceiling={100}
|
||||||
values={history.map((item) => item.memory_percent)}
|
values={history.map((item) => item.memory_percent)}
|
||||||
/>
|
/>
|
||||||
<TelemetrySeries
|
<TelemetrySeries
|
||||||
label="GPU"
|
label="GPU"
|
||||||
value={formatOptionalPercent(gpuPercent)}
|
value={formatOptionalPercent(gpuPercent)}
|
||||||
|
resource={node?.gpu?.name ?? null}
|
||||||
ceiling={100}
|
ceiling={100}
|
||||||
values={history.map((item) => item.gpu_percent)}
|
values={history.map((item) => item.gpu_percent)}
|
||||||
/>
|
/>
|
||||||
<TelemetrySeries
|
<TelemetrySeries
|
||||||
label="VRAM"
|
label="VRAM"
|
||||||
value={formatOptionalPercent(gpuMemoryPercent)}
|
value={formatOptionalPercent(gpuMemoryPercent)}
|
||||||
|
resource={formatResourcePair(
|
||||||
|
gpuMemoryUsedBytes,
|
||||||
|
gpuMemoryTotalBytes,
|
||||||
|
)}
|
||||||
ceiling={100}
|
ceiling={100}
|
||||||
values={history.map((item) => item.gpu_memory_percent)}
|
values={history.map((item) => item.gpu_memory_percent)}
|
||||||
/>
|
/>
|
||||||
@@ -135,11 +182,7 @@ export function ComputeModulesWorkspace() {
|
|||||||
</dl>
|
</dl>
|
||||||
<dl>
|
<dl>
|
||||||
<div><dt>GPU</dt><dd>{node?.gpu?.name ?? "—"}</dd></div>
|
<div><dt>GPU</dt><dd>{node?.gpu?.name ?? "—"}</dd></div>
|
||||||
<div><dt>VRAM занята</dt><dd>{formatBytes(
|
<div><dt>VRAM занята</dt><dd>{formatBytes(gpuMemoryUsedBytes)}</dd></div>
|
||||||
typeof node?.gpu?.memory_used_mib === "number"
|
|
||||||
? node.gpu.memory_used_mib * 1024 * 1024
|
|
||||||
: null,
|
|
||||||
)}</dd></div>
|
|
||||||
<div><dt>Температура</dt><dd>{
|
<div><dt>Температура</dt><dd>{
|
||||||
typeof node?.gpu?.temperature_celsius === "number"
|
typeof node?.gpu?.temperature_celsius === "number"
|
||||||
? `${node.gpu.temperature_celsius} °C`
|
? `${node.gpu.temperature_celsius} °C`
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
import {
|
import {
|
||||||
Button,
|
|
||||||
GlassSurface,
|
GlassSurface,
|
||||||
Icon,
|
|
||||||
StatusBadge,
|
StatusBadge,
|
||||||
} from "@nodedc/ui-react";
|
} from "@nodedc/ui-react";
|
||||||
|
|
||||||
@@ -18,13 +16,17 @@ import {
|
|||||||
} from "../../core/system/useWorkerTelemetry";
|
} from "../../core/system/useWorkerTelemetry";
|
||||||
|
|
||||||
export function NetworkWorkspace() {
|
export function NetworkWorkspace() {
|
||||||
const { selectedContour } = useComputeContours();
|
const {
|
||||||
|
selectedContour,
|
||||||
|
telemetryRefreshGeneration,
|
||||||
|
} = useComputeContours();
|
||||||
const legacyDiagnostic = selectedContour?.contour_id === "worker-006"
|
const legacyDiagnostic = selectedContour?.contour_id === "worker-006"
|
||||||
&& selectedContour.telemetry_mode === "legacy-ssh";
|
&& selectedContour.telemetry_mode === "legacy-ssh";
|
||||||
const supportsLiveTelemetry = selectedContour?.contour_id === "worker-006";
|
const supportsLiveTelemetry = selectedContour?.contour_id === "worker-006";
|
||||||
const { telemetry, error, refresh } = useWorkerTelemetry(
|
const { telemetry, error } = useWorkerTelemetry(
|
||||||
(selectedContour?.telemetry_poll_interval_seconds ?? 3) * 1_000,
|
(selectedContour?.telemetry_poll_interval_seconds ?? 3) * 1_000,
|
||||||
supportsLiveTelemetry,
|
supportsLiveTelemetry,
|
||||||
|
telemetryRefreshGeneration,
|
||||||
);
|
);
|
||||||
const aggregate = telemetry?.network.aggregate ?? null;
|
const aggregate = telemetry?.network.aggregate ?? null;
|
||||||
const connected = Boolean(
|
const connected = Boolean(
|
||||||
@@ -50,15 +52,6 @@ export function NetworkWorkspace() {
|
|||||||
<StatusBadge tone={connected ? "success" : "danger"}>
|
<StatusBadge tone={connected ? "success" : "danger"}>
|
||||||
{connected ? "Маршрут доступен" : "Нет свежих данных"}
|
{connected ? "Маршрут доступен" : "Нет свежих данных"}
|
||||||
</StatusBadge>
|
</StatusBadge>
|
||||||
<Button
|
|
||||||
size="compact"
|
|
||||||
variant="secondary"
|
|
||||||
disabled={!supportsLiveTelemetry}
|
|
||||||
onClick={refresh}
|
|
||||||
icon={<Icon name="refresh" size={14} />}
|
|
||||||
>
|
|
||||||
Обновить
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
|||||||
@@ -202,6 +202,23 @@ test("data recordings keep the compact session dropdown and laboratory results s
|
|||||||
assert.match(laboratorySource, /ЛАБОРАТОРНАЯ РАБОТА/);
|
assert.match(laboratorySource, /ЛАБОРАТОРНАЯ РАБОТА/);
|
||||||
assert.match(laboratorySource, /e28-local-surface/);
|
assert.match(laboratorySource, /e28-local-surface/);
|
||||||
assert.match(laboratorySource, /e29-camera-geometry/);
|
assert.match(laboratorySource, /e29-camera-geometry/);
|
||||||
|
assert.match(laboratorySource, /laboratoryWorkOrdinal\(right\.label\)/);
|
||||||
|
assert.match(laboratorySource, /initialWorkSelectedRef/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("recording preparation statuses share the viewer's left alignment", async () => {
|
||||||
|
const spatialStyles = await readFile(
|
||||||
|
new URL("../src/styles/spatial.css", import.meta.url),
|
||||||
|
"utf8",
|
||||||
|
);
|
||||||
|
const statusStack = spatialStyles.slice(
|
||||||
|
spatialStyles.indexOf(".scene-operation-status-stack"),
|
||||||
|
spatialStyles.indexOf(".scene-operation-status {"),
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.match(statusStack, /left:\s*0\.85rem/);
|
||||||
|
assert.match(statusStack, /justify-items:\s*start/);
|
||||||
|
assert.doesNotMatch(statusStack, /right:/);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("source and laboratory catalogs are requested as disjoint backend projections", async () => {
|
test("source and laboratory catalogs are requested as disjoint backend projections", async () => {
|
||||||
|
|||||||
@@ -18,6 +18,9 @@ test("Worker 006 telemetry remains a bounded system feature slice", async () =>
|
|||||||
pipelineStages,
|
pipelineStages,
|
||||||
networkWorkspace,
|
networkWorkspace,
|
||||||
telemetryPolling,
|
telemetryPolling,
|
||||||
|
telemetryContext,
|
||||||
|
panelActions,
|
||||||
|
contourSettingsHook,
|
||||||
contourSettings,
|
contourSettings,
|
||||||
contourContract,
|
contourContract,
|
||||||
pollIntervalContract,
|
pollIntervalContract,
|
||||||
@@ -31,6 +34,9 @@ test("Worker 006 telemetry remains a bounded system feature slice", async () =>
|
|||||||
read("components/system/WorkerPipelineStages.tsx"),
|
read("components/system/WorkerPipelineStages.tsx"),
|
||||||
read("workspaces/system/NetworkWorkspace.tsx"),
|
read("workspaces/system/NetworkWorkspace.tsx"),
|
||||||
read("core/system/useWorkerTelemetry.ts"),
|
read("core/system/useWorkerTelemetry.ts"),
|
||||||
|
read("core/system/ComputeContourContext.tsx"),
|
||||||
|
read("components/useApplicationPanelActions.ts"),
|
||||||
|
read("components/system/useComputeContourSettings.tsx"),
|
||||||
read("components/system/ComputeContourSettingsWindow.tsx"),
|
read("components/system/ComputeContourSettingsWindow.tsx"),
|
||||||
read("core/system/computeContours.ts"),
|
read("core/system/computeContours.ts"),
|
||||||
read("core/system/telemetryPollInterval.ts"),
|
read("core/system/telemetryPollInterval.ts"),
|
||||||
@@ -49,7 +55,9 @@ test("Worker 006 telemetry remains a bounded system feature slice", async () =>
|
|||||||
assert.doesNotMatch(core, /@nodedc\/ui-react/);
|
assert.doesNotMatch(core, /@nodedc\/ui-react/);
|
||||||
assert.match(computeWorkspace, /useWorkerTelemetry/);
|
assert.match(computeWorkspace, /useWorkerTelemetry/);
|
||||||
assert.match(computeWorkspace, /telemetry_poll_interval_seconds/);
|
assert.match(computeWorkspace, /telemetry_poll_interval_seconds/);
|
||||||
assert.match(computeWorkspace, />\s*Обновить\s*</);
|
assert.doesNotMatch(computeWorkspace, />\s*Обновить\s*</);
|
||||||
|
assert.match(computeWorkspace, /resource=\{formatCpuCapacity/);
|
||||||
|
assert.match(computeWorkspace, /resource=\{formatResourcePair/);
|
||||||
assert.doesNotMatch(computeWorkspace, /loading\s*\?\s*"Читаем"/);
|
assert.doesNotMatch(computeWorkspace, /loading\s*\?\s*"Читаем"/);
|
||||||
assert.match(computeWorkspace, /WorkerPipelineStages/);
|
assert.match(computeWorkspace, /WorkerPipelineStages/);
|
||||||
assert.match(pipelineStages, /измеренного времени/);
|
assert.match(pipelineStages, /измеренного времени/);
|
||||||
@@ -69,9 +77,15 @@ test("Worker 006 telemetry remains a bounded system feature slice", async () =>
|
|||||||
);
|
);
|
||||||
assert.match(networkWorkspace, /127\.0\.0\.1:8000/);
|
assert.match(networkWorkspace, /127\.0\.0\.1:8000/);
|
||||||
assert.match(networkWorkspace, /telemetry_poll_interval_seconds/);
|
assert.match(networkWorkspace, /telemetry_poll_interval_seconds/);
|
||||||
assert.match(networkWorkspace, />\s*Обновить\s*</);
|
assert.doesNotMatch(networkWorkspace, />\s*Обновить\s*</);
|
||||||
assert.doesNotMatch(networkWorkspace, /loading\s*\?\s*"Читаем"/);
|
assert.doesNotMatch(networkWorkspace, /loading\s*\?\s*"Читаем"/);
|
||||||
assert.doesNotMatch(networkWorkspace, /8765/);
|
assert.doesNotMatch(networkWorkspace, /8765/);
|
||||||
|
assert.match(telemetryContext, /telemetryRefreshGeneration/);
|
||||||
|
assert.match(telemetryContext, /refreshTelemetry/);
|
||||||
|
assert.match(telemetryPolling, /externalRefreshGeneration/);
|
||||||
|
assert.match(panelActions, /actions\.push\(\.\.\.systemUtilityActions\)/);
|
||||||
|
assert.match(contourSettingsHook, /icon: "refresh"/);
|
||||||
|
assert.match(contourSettingsHook, /icon: "settings"/);
|
||||||
assert.match(contourSettings, /FieldFrame label="Операционная система"/);
|
assert.match(contourSettings, /FieldFrame label="Операционная система"/);
|
||||||
assert.match(contourSettings, /label="Интервал MQTT"/);
|
assert.match(contourSettings, /label="Интервал MQTT"/);
|
||||||
assert.match(contourSettings, /value=\{telemetryPollIntervalDraft\}/);
|
assert.match(contourSettings, /value=\{telemetryPollIntervalDraft\}/);
|
||||||
|
|||||||
Reference in New Issue
Block a user