feat(map): open bounded telemetry card from entities

This commit is contained in:
Codex
2026-07-22 11:47:41 +03:00
parent b8fc4c5ba1
commit dd2febe7cf
8 changed files with 452 additions and 4 deletions
+93 -3
View File
@@ -31,6 +31,7 @@ import {
findCameraSurveyPreset,
type CameraSurveySelection,
} from "./mapCameraPresets.js";
import { buildMapSubjectCardModel } from "./mapSubjectCard.mjs";
const CesiumMapRenderer = lazy(() => import("./CesiumMapRenderer.js").then((module) => ({ default: module.CesiumMapRenderer })));
type PreviewFeatures = { inspector?: boolean; toolbar?: boolean; assistant?: boolean };
@@ -255,6 +256,13 @@ const defaultLayersWindowRect: WorkspaceWindowRect = {
height: 500,
};
const defaultSubjectCardRect: WorkspaceWindowRect = {
x: 940,
y: 52,
width: 390,
height: 560,
};
function initialSubjectState(bindings: MapDataProductBinding[], saved: MapSubjectState[] | undefined) {
const savedByBinding = new Map((saved ?? []).map((state) => [state.bindingId, state]));
return Object.fromEntries(bindings.map((binding, index) => {
@@ -307,6 +315,11 @@ export const MapFixturePreview = forwardRef<MapFixturePreviewHandle, {
}>(function MapFixturePreview({ features = { inspector: true, toolbar: true, assistant: false }, expanded = false, initialLayout = null, applicationId, pageId }, ref) {
const workspaceRef = useRef<HTMLDivElement>(null);
const [selectedId, setSelectedId] = useState<string>();
const [subjectCardOpen, setSubjectCardOpen] = useState(false);
const [subjectCardRect, setSubjectCardRect] = useState<WorkspaceWindowRect>(defaultSubjectCardRect);
const [subjectCardMaximized, setSubjectCardMaximized] = useState(false);
const [subjectCardZIndex, setSubjectCardZIndex] = useState(140);
const [subjectCardActive, setSubjectCardActive] = useState(false);
const [inspectorOpen, setInspectorOpen] = useState(false);
const [layersOpen, setLayersOpen] = useState(false);
const [layersWindowRect, setLayersWindowRect] = useState<WorkspaceWindowRect>(defaultLayersWindowRect);
@@ -381,6 +394,9 @@ export const MapFixturePreview = forwardRef<MapFixturePreviewHandle, {
title: mapRuntimeDisplayLabel(fact, profile),
kind: fact.semanticType,
status: presentationClass?.label ?? fact.presentationStatus,
bindingId: binding.bindingId,
dataProductId: binding.dataProductId,
fact,
};
});
})
@@ -458,6 +474,14 @@ export const MapFixturePreview = forwardRef<MapFixturePreviewHandle, {
}] : []),
], [spiralPresetId]);
const selected = selectable.find((entity) => entity.id === selectedId) ?? selectable[0];
const selectedSubjectCard = useMemo(() => {
const entity = selectable.find((candidate) => candidate.id === selectedId);
return entity ? buildMapSubjectCardModel(entity.fact, {
title: entity.title,
bindingId: entity.bindingId,
dataProductId: entity.dataProductId,
}) : null;
}, [selectable, selectedId]);
const presentation = useMemo<MapPresentation>(
() => ({ ...mapSettings, cacheRefresh }),
[cacheRefresh, mapSettings],
@@ -670,12 +694,13 @@ export const MapFixturePreview = forwardRef<MapFixturePreviewHandle, {
};
const openSubjectWindow = (bindingId: string) => {
const nextZIndex = Math.max(20, layersWindowZIndex, ...Object.values(subjectStates).map((state) => state.window.zIndex)) + 1;
const nextZIndex = Math.max(20, layersWindowZIndex, subjectCardZIndex, ...Object.values(subjectStates).map((state) => state.window.zIndex)) + 1;
updateSubjectState(bindingId, (state) => ({
...state,
window: { ...state.window, open: true, zIndex: nextZIndex },
}));
setLayersWindowActive(false);
setSubjectCardActive(false);
setActiveSubjectBindingId(bindingId);
};
@@ -685,9 +710,10 @@ export const MapFixturePreview = forwardRef<MapFixturePreviewHandle, {
};
const activateLayersWindow = () => {
const nextZIndex = Math.max(20, layersWindowZIndex, ...Object.values(subjectStates).map((state) => state.window.zIndex)) + 1;
const nextZIndex = Math.max(20, layersWindowZIndex, subjectCardZIndex, ...Object.values(subjectStates).map((state) => state.window.zIndex)) + 1;
setLayersWindowZIndex(nextZIndex);
setLayersWindowActive(true);
setSubjectCardActive(false);
setActiveSubjectBindingId(undefined);
};
@@ -718,7 +744,12 @@ export const MapFixturePreview = forwardRef<MapFixturePreviewHandle, {
const handleSelect = useCallback((entityId: string) => {
if (!selectable.some((entity) => entity.id === entityId)) return;
setSelectedId(entityId);
}, [selectable]);
setSubjectCardOpen(true);
setSubjectCardActive(true);
setLayersWindowActive(false);
setActiveSubjectBindingId(undefined);
setSubjectCardZIndex((current) => Math.max(current, layersWindowZIndex, ...Object.values(subjectStates).map((state) => state.window.zIndex)) + 1);
}, [layersWindowZIndex, selectable, subjectStates]);
const rememberGatewayHealth = useCallback((health: MapGatewayHealth) => {
gatewayHealthRef.current = health;
@@ -1312,6 +1343,65 @@ export const MapFixturePreview = forwardRef<MapFixturePreviewHandle, {
);
})}
{subjectCardOpen && selectedSubjectCard ? (
<WorkspaceWindow
boundsRef={workspaceRef}
rect={subjectCardRect}
onRectChange={setSubjectCardRect}
maximized={subjectCardMaximized}
onMaximizedChange={setSubjectCardMaximized}
onActivate={() => {
const nextZIndex = Math.max(subjectCardZIndex, layersWindowZIndex, ...Object.values(subjectStates).map((state) => state.window.zIndex)) + 1;
setSubjectCardZIndex(nextZIndex);
setSubjectCardActive(true);
setLayersWindowActive(false);
setActiveSubjectBindingId(undefined);
}}
onClose={() => {
setSubjectCardOpen(false);
setSubjectCardActive(false);
}}
title={selectedSubjectCard.title}
subtitle={selectedSubjectCard.sourceId}
active={subjectCardActive}
zIndex={subjectCardZIndex}
minWidth={320}
minHeight={320}
className="catalog-map-fixture__subject-card catalog-map-fixture__map-glass-window"
aria-label={`Телеметрия: ${selectedSubjectCard.title}`}
>
<div className="catalog-map-subject-card">
{selectedSubjectCard.sections.map((section) => (
<section key={section.id} className="catalog-map-subject-card__section" aria-labelledby={`subject-card-${section.id}`}>
<h3 id={`subject-card-${section.id}`}>{section.label}</h3>
<dl>
{section.rows.map((row) => (
<div key={row.key} className="catalog-map-subject-card__row">
<dt>{row.label}</dt>
<dd>{row.value}</dd>
</div>
))}
</dl>
</section>
))}
<section className="catalog-map-subject-card__section" aria-labelledby="subject-card-readings">
<h3 id="subject-card-readings">Датчики</h3>
{selectedSubjectCard.readings.length ? (
<div className="catalog-map-subject-card__readings">
{selectedSubjectCard.readings.map((reading) => (
<div className="catalog-map-subject-card__reading" key={reading.id}>
<span>{reading.label}</span>
<strong>{reading.value}</strong>
{reading.observedAt ? <time dateTime={reading.observedAt}>{new Date(reading.observedAt).toLocaleString("ru-RU")}</time> : null}
</div>
))}
</div>
) : <small>Gelios не передал разрешённых значений датчиков для этого объекта.</small>}
</section>
</div>
</WorkspaceWindow>
) : null}
{assistantOpen ? <div className="catalog-map-fixture__assistant"><strong>NODE.DC Assistant</strong><span>Контекст выбранной сущности готов к передаче.</span></div> : null}
<button type="button" className="catalog-map-fixture__resize" aria-label="Изменить высоту карты" onPointerDown={startResize}><span /></button>