feat(lab): publish E31 through E33 results
This commit is contained in:
@@ -0,0 +1,108 @@
|
||||
import type { ReactNode } from "react";
|
||||
import { StatusBadge } from "@nodedc/ui-react";
|
||||
|
||||
import {
|
||||
LaboratoryEvidence,
|
||||
LaboratorySummary,
|
||||
LaboratoryWorkTemplate,
|
||||
} from "../../components/laboratory/LaboratoryPresentation";
|
||||
import type { E31LaboratoryResult } from "../../core/laboratory/advancedResults";
|
||||
import { formatNumber } from "../../presentation";
|
||||
|
||||
export function E31Result({
|
||||
rigLabel,
|
||||
result,
|
||||
evidence,
|
||||
}: {
|
||||
rigLabel: string;
|
||||
result: E31LaboratoryResult;
|
||||
evidence: ReactNode;
|
||||
}) {
|
||||
const metrics = result.metrics;
|
||||
return (
|
||||
<LaboratoryWorkTemplate
|
||||
summary={(
|
||||
<LaboratorySummary
|
||||
title="LAB E31 · квалификация source binding"
|
||||
description="Проверена привязка camera, LiDAR и pose к неизменяемой записи RAVNOVES00. Профиль принят только для этого источника: время основано на host-arrival, а не на аппаратном firing time."
|
||||
status="Допущено к E32"
|
||||
statusTone="success"
|
||||
facts={[
|
||||
{ label: "Конфигурация", value: `${rigLabel} · camera + LiDAR + pose` },
|
||||
{ label: "Источник", value: result.sourceSessionId },
|
||||
{
|
||||
label: "Привязано",
|
||||
value: `${formatNumber(metrics.availableBindingCount, 0)} / ${formatNumber(metrics.frameCount, 0)} кадров`,
|
||||
},
|
||||
{ label: "Контур", value: "Source-scoped · read-only" },
|
||||
]}
|
||||
method={{
|
||||
completeness: "complete",
|
||||
executionClass: "deterministic",
|
||||
pipelineId: result.profileId,
|
||||
components: [
|
||||
{
|
||||
kind: "source",
|
||||
name: result.sourceSessionId,
|
||||
version: "immutable RAVNOVES00 replay",
|
||||
role: "camera, LiDAR and pose evidence",
|
||||
identitySha256: null,
|
||||
},
|
||||
{
|
||||
kind: "algorithm",
|
||||
name: "Source-time and calibration qualification",
|
||||
version: result.profileId,
|
||||
role: "timing, offset sensitivity and self-mask checks",
|
||||
identitySha256: result.producerSha256,
|
||||
},
|
||||
],
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
evidence={(
|
||||
<LaboratoryEvidence
|
||||
eyebrow="ИСХОДНОЕ ДОКАЗАТЕЛЬСТВО"
|
||||
title="Запись, на которой квалифицирована привязка"
|
||||
kind="recorded-replay"
|
||||
resizable
|
||||
>
|
||||
{evidence}
|
||||
</LaboratoryEvidence>
|
||||
)}
|
||||
result={(
|
||||
<section className="laboratory-result-summary">
|
||||
<header>
|
||||
<div>
|
||||
<span className="section-eyebrow">РЕЗУЛЬТАТ И ВЫВОД</span>
|
||||
<h2>Привязка воспроизводима в границах этого источника</h2>
|
||||
</div>
|
||||
<StatusBadge tone="success">Source profile принят</StatusBadge>
|
||||
</header>
|
||||
<div className="laboratory-result-metrics">
|
||||
<div>
|
||||
<span>Camera ↔ LiDAR p95</span>
|
||||
<strong>{metrics.lidarCameraP95Ms.toLocaleString("ru-RU", { maximumFractionDigits: 2 })} мс</strong>
|
||||
<small>host-arrival binding</small>
|
||||
</div>
|
||||
<div>
|
||||
<span>Pose ↔ point p95</span>
|
||||
<strong>{metrics.posePointP95Ms.toLocaleString("ru-RU", { maximumFractionDigits: 2 })} мс</strong>
|
||||
<small>по доступным кадрам</small>
|
||||
</div>
|
||||
<div>
|
||||
<span>Поддержка соответствий</span>
|
||||
<strong>{(metrics.supportedFraction * 100).toLocaleString("ru-RU", { maximumFractionDigits: 1 })}%</strong>
|
||||
<small>{formatNumber(metrics.correspondenceCount, 0)} соответствий</small>
|
||||
</div>
|
||||
<div>
|
||||
<span>Self-mask</span>
|
||||
<strong>{formatNumber(metrics.semanticSelfSampleCount, 0)} образцов</strong>
|
||||
<small>{formatNumber(metrics.semanticSelfCollateralCount, 0)} collateral</small>
|
||||
</div>
|
||||
</div>
|
||||
<p>Результат диагностический: он не переносится на другой монтаж сенсоров и не выдаёт командных полномочий.</p>
|
||||
</section>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
import type { ReactNode } from "react";
|
||||
import { StatusBadge } from "@nodedc/ui-react";
|
||||
|
||||
import {
|
||||
LaboratoryEvidence,
|
||||
LaboratorySummary,
|
||||
LaboratoryWorkTemplate,
|
||||
} from "../../components/laboratory/LaboratoryPresentation";
|
||||
import type { E32LaboratoryResult } from "../../core/laboratory/advancedResults";
|
||||
import { formatNumber } from "../../presentation";
|
||||
|
||||
export function E32Result({
|
||||
rigLabel,
|
||||
result,
|
||||
evidence,
|
||||
}: {
|
||||
rigLabel: string;
|
||||
result: E32LaboratoryResult;
|
||||
evidence: ReactNode;
|
||||
}) {
|
||||
const metrics = result.metrics;
|
||||
return (
|
||||
<LaboratoryWorkTemplate
|
||||
summary={(
|
||||
<LaboratorySummary
|
||||
title="LAB E32 · TrackGeometry v1 replay"
|
||||
description="Полная запись E29 пересчитана в детерминированный покадровый TrackGeometry. Перекрывающиеся LiDAR-точки получили единственного владельца, а диапазоны без квалифицированного источника не опубликованы."
|
||||
status="Replay принят"
|
||||
statusTone="success"
|
||||
facts={[
|
||||
{ label: "Конфигурация", value: `${rigLabel} · TrackGeometry v1` },
|
||||
{ label: "Источник", value: result.sourceSessionId },
|
||||
{ label: "Кадры", value: formatNumber(metrics.framesTotal, 0) },
|
||||
{ label: "Контур", value: "Diagnostic replay · read-only" },
|
||||
]}
|
||||
method={{
|
||||
completeness: "complete",
|
||||
executionClass: "deterministic",
|
||||
pipelineId: result.profileId,
|
||||
components: [
|
||||
{
|
||||
kind: "source",
|
||||
name: result.e31ResultId,
|
||||
version: "accepted E31 source profile",
|
||||
role: "qualified timing and calibration binding",
|
||||
identitySha256: result.e31ResultId.split("-").at(-1) ?? null,
|
||||
},
|
||||
{
|
||||
kind: "algorithm",
|
||||
name: "Exact E29 → TrackGeometry",
|
||||
version: result.profileId,
|
||||
role: "point ownership, masks and deterministic storage",
|
||||
identitySha256: result.producerSha256,
|
||||
},
|
||||
],
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
evidence={(
|
||||
<LaboratoryEvidence
|
||||
eyebrow="ИСХОДНОЕ ДОКАЗАТЕЛЬСТВО"
|
||||
title="Полная запись, пересчитанная в TrackGeometry"
|
||||
kind="recorded-replay"
|
||||
resizable
|
||||
>
|
||||
{evidence}
|
||||
</LaboratoryEvidence>
|
||||
)}
|
||||
result={(
|
||||
<section className="laboratory-result-summary">
|
||||
<header>
|
||||
<div>
|
||||
<span className="section-eyebrow">РЕЗУЛЬТАТ И ВЫВОД</span>
|
||||
<h2>TrackGeometry построен без повторной настройки порогов</h2>
|
||||
</div>
|
||||
<StatusBadge tone="success">Детерминированный replay</StatusBadge>
|
||||
</header>
|
||||
<div className="laboratory-result-metrics">
|
||||
<div>
|
||||
<span>Опубликовано точек</span>
|
||||
<strong>{formatNumber(metrics.qualifiedPointsPublished, 0)}</strong>
|
||||
<small>{formatNumber(metrics.qualifiedPointsWithheld, 0)} withheld</small>
|
||||
</div>
|
||||
<div>
|
||||
<span>Арбитраж наблюдений</span>
|
||||
<strong>{formatNumber(metrics.observationsArbitrated, 0)}</strong>
|
||||
<small>{formatNumber(metrics.overlappingClaimsRemoved, 0)} overlap снято</small>
|
||||
</div>
|
||||
<div>
|
||||
<span>Semantic</span>
|
||||
<strong>{formatNumber(metrics.semanticPublished, 0)}</strong>
|
||||
<small>{formatNumber(metrics.semanticMasked, 0)} self-mask</small>
|
||||
</div>
|
||||
<div>
|
||||
<span>Frame processing p95</span>
|
||||
<strong>{metrics.frameProcessingP95Ms.toLocaleString("ru-RU", { maximumFractionDigits: 3 })} мс</strong>
|
||||
<small>{(metrics.buildElapsedMs / 1000).toLocaleString("ru-RU", { maximumFractionDigits: 2 })} с build</small>
|
||||
</div>
|
||||
</div>
|
||||
<p>Отсутствие LiDAR-точек по-прежнему не трактуется как свободное пространство; результат не является навигационным ground truth.</p>
|
||||
</section>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
import type { ReactNode } from "react";
|
||||
import { StatusBadge } from "@nodedc/ui-react";
|
||||
|
||||
import {
|
||||
LaboratoryEvidence,
|
||||
LaboratorySummary,
|
||||
LaboratoryWorkTemplate,
|
||||
} from "../../components/laboratory/LaboratoryPresentation";
|
||||
import type { E33LaboratoryResult } from "../../core/laboratory/advancedResults";
|
||||
import { formatNumber } from "../../presentation";
|
||||
|
||||
export function E33Result({
|
||||
rigLabel,
|
||||
result,
|
||||
evidence,
|
||||
}: {
|
||||
rigLabel: string;
|
||||
result: E33LaboratoryResult;
|
||||
evidence: ReactNode;
|
||||
}) {
|
||||
const metrics = result.metrics;
|
||||
return (
|
||||
<LaboratoryWorkTemplate
|
||||
summary={(
|
||||
<LaboratorySummary
|
||||
title="LAB E33 · worker shadow 1×"
|
||||
description="Полная запись TrackGeometry выполнена отдельным bounded worker в темпе исходника 1×. Проверены очереди, доставка каждого кадра, задержка результата и ресурсы без подключения командного канала."
|
||||
status="Worker gate пройден"
|
||||
statusTone="success"
|
||||
facts={[
|
||||
{ label: "Конфигурация", value: `${rigLabel} · worker shadow` },
|
||||
{ label: "Worker", value: result.worker.node },
|
||||
{ label: "Доставка", value: `${formatNumber(metrics.deliveredFrames, 0)} / ${formatNumber(metrics.sourceFrames, 0)} кадров` },
|
||||
{ label: "Контур", value: "Recorded-source-paced · 1×" },
|
||||
]}
|
||||
method={{
|
||||
completeness: "complete",
|
||||
executionClass: "deterministic",
|
||||
pipelineId: result.pipelineId,
|
||||
components: [
|
||||
{
|
||||
kind: "source",
|
||||
name: result.e32ResultId,
|
||||
version: "accepted TrackGeometry replay",
|
||||
role: "immutable frame stream",
|
||||
identitySha256: result.e32ResultId.split("-").at(-1) ?? null,
|
||||
},
|
||||
{
|
||||
kind: "runtime",
|
||||
name: result.worker.node,
|
||||
version: `${result.worker.python} · NumPy ${result.worker.numpy}`,
|
||||
role: "bounded worker, work/result queues 2/2",
|
||||
identitySha256: null,
|
||||
},
|
||||
],
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
evidence={(
|
||||
<LaboratoryEvidence
|
||||
eyebrow="ИСХОДНОЕ ДОКАЗАТЕЛЬСТВО"
|
||||
title="Запись, доставленная worker без потерь"
|
||||
kind="recorded-replay"
|
||||
resizable
|
||||
>
|
||||
{evidence}
|
||||
</LaboratoryEvidence>
|
||||
)}
|
||||
result={(
|
||||
<section className="laboratory-result-summary">
|
||||
<header>
|
||||
<div>
|
||||
<span className="section-eyebrow">РЕЗУЛЬТАТ И ВЫВОД</span>
|
||||
<h2>Полный replay выдержан в темпе источника</h2>
|
||||
</div>
|
||||
<StatusBadge tone="success">
|
||||
{formatNumber(metrics.deliveredFrames, 0)} / {formatNumber(metrics.sourceFrames, 0)}
|
||||
</StatusBadge>
|
||||
</header>
|
||||
<div className="laboratory-result-metrics">
|
||||
<div>
|
||||
<span>Эффективная частота</span>
|
||||
<strong>{metrics.effectiveDeliveryFps.toLocaleString("ru-RU", { maximumFractionDigits: 3 })} FPS</strong>
|
||||
<small>скорость источника 1×</small>
|
||||
</div>
|
||||
<div>
|
||||
<span>Result age p95</span>
|
||||
<strong>{metrics.resultAgeP95Ms.toLocaleString("ru-RU", { maximumFractionDigits: 3 })} мс</strong>
|
||||
<small>{(metrics.deadlineMissFraction * 100).toLocaleString("ru-RU", { maximumFractionDigits: 2 })}% deadline miss</small>
|
||||
</div>
|
||||
<div>
|
||||
<span>Processing p95</span>
|
||||
<strong>{metrics.processingP95Ms.toLocaleString("ru-RU", { maximumFractionDigits: 3 })} мс</strong>
|
||||
<small>{metrics.releaseLagP95Ms.toLocaleString("ru-RU", { maximumFractionDigits: 3 })} мс release lag</small>
|
||||
</div>
|
||||
<div>
|
||||
<span>Ресурсы p95</span>
|
||||
<strong>{metrics.processRssP95Mib.toLocaleString("ru-RU", { maximumFractionDigits: 1 })} MiB</strong>
|
||||
<small>GPU {metrics.gpuUtilizationP95Percent.toLocaleString("ru-RU", { maximumFractionDigits: 1 })}%</small>
|
||||
</div>
|
||||
</div>
|
||||
<p>Очереди остались bounded, пропусков нет. Результат подтверждает только worker shadow и не даёт навигационных или safety-полномочий.</p>
|
||||
</section>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -27,6 +27,12 @@ import {
|
||||
fetchE30ReviewCatalog,
|
||||
type E30ReviewResult,
|
||||
} from "../../core/laboratory/e30Review";
|
||||
import {
|
||||
fetchAdvancedLaboratoryResults,
|
||||
type E31LaboratoryResult,
|
||||
type E32LaboratoryResult,
|
||||
type E33LaboratoryResult,
|
||||
} from "../../core/laboratory/advancedResults";
|
||||
import {
|
||||
fetchLidarLocalSurfaces,
|
||||
type LidarLocalSurfaceModel,
|
||||
@@ -35,6 +41,10 @@ import { formatNumber } from "../../presentation";
|
||||
import { E30ReviewWorkspace } from "../E30ReviewWorkspace";
|
||||
import { LidarQualityWorkspace } from "../LidarQualityWorkspace";
|
||||
import type { WorkspaceRendererProps } from "../contracts";
|
||||
import { E31Result } from "./E31Result";
|
||||
import { E32Result } from "./E32Result";
|
||||
import { E33Result } from "./E33Result";
|
||||
import { RecordedReplayEvidence } from "./RecordedReplayEvidence";
|
||||
|
||||
type LaboratoryWorkspaceProps = WorkspaceRendererProps & {
|
||||
SpatialView: ComponentType<WorkspaceRendererProps>;
|
||||
@@ -45,6 +55,9 @@ type LaboratoryWorkId =
|
||||
| "e28-local-surface"
|
||||
| "e29-camera-geometry"
|
||||
| "e30-evidence-review"
|
||||
| "e31-source-binding"
|
||||
| "e32-track-geometry"
|
||||
| "e33-worker-shadow"
|
||||
| `session:${string}`;
|
||||
|
||||
function digestFromContentId(value: string | null | undefined): string | null {
|
||||
@@ -535,6 +548,9 @@ export function LaboratoryArchiveWorkspace(props: LaboratoryWorkspaceProps) {
|
||||
const [e28Model, setE28Model] = useState<LidarLocalSurfaceModel | null>(null);
|
||||
const [e29Result, setE29Result] = useState<E29EvidenceResult | null>(null);
|
||||
const [e30Result, setE30Result] = useState<E30ReviewResult | null>(null);
|
||||
const [e31Result, setE31Result] = useState<E31LaboratoryResult | null>(null);
|
||||
const [e32Result, setE32Result] = useState<E32LaboratoryResult | null>(null);
|
||||
const [e33Result, setE33Result] = useState<E33LaboratoryResult | null>(null);
|
||||
const [evidenceLoading, setEvidenceLoading] = useState(true);
|
||||
const [evidenceError, setEvidenceError] = useState<string | null>(null);
|
||||
const sessions = useObservationSessions({
|
||||
@@ -566,18 +582,24 @@ export function LaboratoryArchiveWorkspace(props: LaboratoryWorkspaceProps) {
|
||||
fetchLidarLocalSurfaces({ signal: controller.signal }),
|
||||
fetchE29EvidenceCatalog({ signal: controller.signal }),
|
||||
fetchE30ReviewCatalog({ signal: controller.signal }),
|
||||
]).then(([e28, e29, e30]) => {
|
||||
fetchAdvancedLaboratoryResults({ signal: controller.signal }),
|
||||
]).then(([e28, e29, e30, advanced]) => {
|
||||
if (controller.signal.aborted) return;
|
||||
const nextE28 = e28.status === "fulfilled" ? e28.value.items[0] ?? null : null;
|
||||
const nextE29 = e29.status === "fulfilled" ? e29.value.items[0] ?? null : null;
|
||||
const nextE30 = e30.status === "fulfilled" ? e30.value.items[0] ?? null : null;
|
||||
const nextAdvanced = advanced.status === "fulfilled" ? advanced.value : null;
|
||||
setE28Model(nextE28);
|
||||
setE29Result(nextE29);
|
||||
setE30Result(nextE30);
|
||||
setE31Result(nextAdvanced?.e31 ?? null);
|
||||
setE32Result(nextAdvanced?.e32 ?? null);
|
||||
setE33Result(nextAdvanced?.e33 ?? null);
|
||||
const failures = [
|
||||
e28.status === "rejected" ? "E28" : null,
|
||||
e29.status === "rejected" ? "E29" : null,
|
||||
e30.status === "rejected" ? "E30" : null,
|
||||
advanced.status === "rejected" ? "E31–E33" : null,
|
||||
].filter(Boolean);
|
||||
setEvidenceError(
|
||||
failures.length
|
||||
@@ -623,8 +645,34 @@ export function LaboratoryArchiveWorkspace(props: LaboratoryWorkspaceProps) {
|
||||
label: "LAB E30 · evidence review A2",
|
||||
});
|
||||
}
|
||||
if (e31Result && sourceSessions.has(e31Result.sourceSessionId)) {
|
||||
items.push({
|
||||
id: "e31-source-binding",
|
||||
label: "LAB E31 · source binding",
|
||||
});
|
||||
}
|
||||
if (e32Result && sourceSessions.has(e32Result.sourceSessionId)) {
|
||||
items.push({
|
||||
id: "e32-track-geometry",
|
||||
label: "LAB E32 · TrackGeometry v1",
|
||||
});
|
||||
}
|
||||
if (e33Result && sourceSessions.has(e33Result.sourceSessionId)) {
|
||||
items.push({
|
||||
id: "e33-worker-shadow",
|
||||
label: "LAB E33 · worker shadow 1×",
|
||||
});
|
||||
}
|
||||
return items;
|
||||
}, [e28Model, e29Result, e30Result, sourceSessions]);
|
||||
}, [
|
||||
e28Model,
|
||||
e29Result,
|
||||
e30Result,
|
||||
e31Result,
|
||||
e32Result,
|
||||
e33Result,
|
||||
sourceSessions,
|
||||
]);
|
||||
const profiles = useMemo(() => {
|
||||
const items: LaboratoryOption<LaboratoryProfileId>[] = [];
|
||||
if (sensorWorks.length) {
|
||||
@@ -660,6 +708,15 @@ export function LaboratoryArchiveWorkspace(props: LaboratoryWorkspaceProps) {
|
||||
const e30SourceSession = e30Result
|
||||
? sourceSessions.get(e30Result.sourceSessionId) ?? null
|
||||
: null;
|
||||
const e31SourceSession = e31Result
|
||||
? sourceSessions.get(e31Result.sourceSessionId) ?? null
|
||||
: null;
|
||||
const e32SourceSession = e32Result
|
||||
? sourceSessions.get(e32Result.sourceSessionId) ?? null
|
||||
: null;
|
||||
const e33SourceSession = e33Result
|
||||
? sourceSessions.get(e33Result.sourceSessionId) ?? null
|
||||
: null;
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
@@ -721,6 +778,17 @@ export function LaboratoryArchiveWorkspace(props: LaboratoryWorkspaceProps) {
|
||||
}
|
||||
if (next === "e30-evidence-review" && e30SourceSession) {
|
||||
void sessions.replay(e30SourceSession.id);
|
||||
return;
|
||||
}
|
||||
const advancedSession = next === "e31-source-binding"
|
||||
? e31SourceSession
|
||||
: next === "e32-track-geometry"
|
||||
? e32SourceSession
|
||||
: next === "e33-worker-shadow"
|
||||
? e33SourceSession
|
||||
: null;
|
||||
if (advancedSession) {
|
||||
void sessions.replay(advancedSession.id);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -862,6 +930,45 @@ export function LaboratoryArchiveWorkspace(props: LaboratoryWorkspaceProps) {
|
||||
result={e30Result}
|
||||
sourceSession={e30SourceSession}
|
||||
/>
|
||||
) : workId === "e31-source-binding" && e31Result && e31SourceSession ? (
|
||||
<E31Result
|
||||
rigLabel={rigLabel}
|
||||
result={e31Result}
|
||||
evidence={(
|
||||
<RecordedReplayEvidence
|
||||
props={props}
|
||||
sourceSession={e31SourceSession}
|
||||
loading={sessions.replayingSessionId === e31SourceSession.id}
|
||||
error={sessions.failedSessionId === e31SourceSession.id ? sessions.error : null}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
) : workId === "e32-track-geometry" && e32Result && e32SourceSession ? (
|
||||
<E32Result
|
||||
rigLabel={rigLabel}
|
||||
result={e32Result}
|
||||
evidence={(
|
||||
<RecordedReplayEvidence
|
||||
props={props}
|
||||
sourceSession={e32SourceSession}
|
||||
loading={sessions.replayingSessionId === e32SourceSession.id}
|
||||
error={sessions.failedSessionId === e32SourceSession.id ? sessions.error : null}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
) : workId === "e33-worker-shadow" && e33Result && e33SourceSession ? (
|
||||
<E33Result
|
||||
rigLabel={rigLabel}
|
||||
result={e33Result}
|
||||
evidence={(
|
||||
<RecordedReplayEvidence
|
||||
props={props}
|
||||
sourceSession={e33SourceSession}
|
||||
loading={sessions.replayingSessionId === e33SourceSession.id}
|
||||
error={sessions.failedSessionId === e33SourceSession.id ? sessions.error : null}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
) : selectedSession ? (
|
||||
<PublishedLaboratoryResult
|
||||
props={props}
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
import type { ComponentType } from "react";
|
||||
import { Icon } from "@nodedc/ui-react";
|
||||
|
||||
import type { ObservationSessionSummary } from "../../core/observation/sessionArchive";
|
||||
import type { WorkspaceRendererProps } from "../contracts";
|
||||
|
||||
type ReplayEvidenceProps = WorkspaceRendererProps & {
|
||||
SpatialView: ComponentType<WorkspaceRendererProps>;
|
||||
};
|
||||
|
||||
export function RecordedReplayEvidence({
|
||||
props,
|
||||
sourceSession,
|
||||
loading,
|
||||
error,
|
||||
}: {
|
||||
props: ReplayEvidenceProps;
|
||||
sourceSession: ObservationSessionSummary;
|
||||
loading: boolean;
|
||||
error: string | null;
|
||||
}) {
|
||||
if (props.recordedReplay?.sessionId === sourceSession.id) {
|
||||
return <props.SpatialView {...props} />;
|
||||
}
|
||||
return (
|
||||
<div className="laboratory-result-pending" role="status">
|
||||
{loading ? <span className="busy-indicator" aria-hidden="true" /> : <Icon name="database" size={20} />}
|
||||
<strong>{loading ? "Проверяем и открываем запись" : "Исходная запись не открыта"}</strong>
|
||||
<p>
|
||||
{error ?? (loading
|
||||
? "Viewer появится после серверной проверки неизменяемого RRD."
|
||||
: "Выберите работу повторно, чтобы открыть связанный источник.")}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user