Files
test-x6/frontend/src/features/schema/helpers/dataMapper.ts
Alina fd6373bcb0
Some checks failed
continuous-integration/drone/push Build is failing
feat: редизайн UI — приглушённые цвета, шапки устройств, компактные контейнеры
- Цвета статусов: приглушённые тона для печати, красный акцент для неисправных
- DeviceNode: название в цветной шапке, карты ниже без наложений
- Перенос слов вместо обрезки во всех нодах
- Лейаут: убран лишний gap после последнего слоя, SITE_MIN_WIDTH 250,
  корректный расчёт startY от шапки сайта
- Белый фон графа, чёрные линии по умолчанию

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 22:52:26 +03:00

289 lines
7.9 KiB
TypeScript

import type { SchemaData } from '../../../types/index.ts';
import { DeviceCategory } from '../../../types/index.ts';
import type { GraphNodeConfig, GraphEdgeConfig, GraphBuildResult } from '../../../types/graph.ts';
import { createPortItem } from '../ports/portConfig.ts';
import { createEdgeConfig } from '../edges/edgeConfig.ts';
import { autoLayout, type LayoutResult } from '../layout/autoLayout.ts';
import { resolvePortSides } from './portSideResolver.ts';
import {
CROSS_WIDTH,
CROSS_HEIGHT,
SPLICE_SIZE,
DEVICE_MIN_WIDTH,
DEVICE_MIN_HEIGHT,
DEVICE_HEADER_HEIGHT,
CARD_WIDTH,
CARD_HEIGHT,
} from '../../../constants/sizes.ts';
function getDeviceShape(category: DeviceCategory, deviceName: string, marking: string): string {
if (
category === DeviceCategory.CrossOptical ||
category === DeviceCategory.CrossCopper
) {
return 'cross-device-node';
}
if (
deviceName.toLowerCase().includes('муфта') ||
marking.toLowerCase().includes('мток')
) {
return 'splice-node';
}
return 'device-node';
}
function getDeviceSize(
category: DeviceCategory,
deviceName: string,
marking: string,
portCount: number,
cardCount: number,
): { width: number; height: number } {
if (
category === DeviceCategory.CrossOptical ||
category === DeviceCategory.CrossCopper
) {
return { width: CROSS_WIDTH, height: CROSS_HEIGHT };
}
if (
deviceName.toLowerCase().includes('муфта') ||
marking.toLowerCase().includes('мток')
) {
return { width: SPLICE_SIZE, height: SPLICE_SIZE };
}
const portHeight = Math.max(portCount * 22, 60);
const cardsHeight = cardCount > 0 ? cardCount * (CARD_HEIGHT + 6) + 8 : 0;
const bodyHeight = Math.max(portHeight, cardsHeight);
return {
width: DEVICE_MIN_WIDTH,
height: Math.max(DEVICE_MIN_HEIGHT, DEVICE_HEADER_HEIGHT + bodyHeight + 10),
};
}
export function buildGraphData(
data: SchemaData,
routerType: 'manhattan' | 'normal' = 'manhattan',
): GraphBuildResult {
const layout: LayoutResult = autoLayout(data);
const nodes: GraphNodeConfig[] = [];
const edges: GraphEdgeConfig[] = [];
// Resolve port sides based on device positions
const devicePositions = new Map<string, { x: number; y: number }>();
for (const [id, pos] of layout.nodePositions.entries()) {
devicePositions.set(id, { x: pos.x, y: pos.y });
}
const portSideMap = resolvePortSides(data, devicePositions);
// 1. Create site nodes
for (const site of data.sites) {
const sitePos = layout.sitePositions.get(site.id);
if (!sitePos) continue;
nodes.push({
id: site.id,
shape: 'site-node',
x: sitePos.x,
y: sitePos.y,
width: sitePos.width,
height: sitePos.height,
data: {
name: site.name,
address: site.address,
erpCode: site.erpCode,
code1C: site.code1C,
status: site.status,
entityType: 'site',
entityId: site.id,
},
zIndex: 1,
});
}
// 2. Create device nodes with ports
for (const device of data.devices) {
const pos = layout.nodePositions.get(device.id);
if (!pos) continue;
const devicePorts = data.ports.filter(
(p) => p.deviceId === device.id && !p.cardId,
);
const shape = getDeviceShape(device.category, device.name, device.marking);
const deviceCards = data.cards.filter((c) => c.deviceId === device.id && c.visible);
const size = getDeviceSize(device.category, device.name, device.marking, devicePorts.length, deviceCards.length);
const portItems = devicePorts.map((port) => {
const resolvedSide = portSideMap.get(port.id) ?? port.side;
const label = port.slotName ? `${port.slotName}:${port.name}` : port.name;
return createPortItem(port.id, resolvedSide, label, port.labelColor || undefined);
});
const node: GraphNodeConfig = {
id: device.id,
shape,
x: pos.x,
y: pos.y,
width: size.width,
height: size.height,
data: {
name: device.name,
networkName: device.networkName,
ipAddress: device.ipAddress,
marking: device.marking,
id1: device.id1,
id2: device.id2,
group: device.group,
category: device.category,
status: device.status,
entityType: 'device',
entityId: device.id,
},
ports: {
groups: {
left: {
position: 'left',
attrs: {
circle: {
r: 6,
magnet: true,
stroke: '#8c8c8c',
strokeWidth: 1,
fill: '#fff',
},
},
label: {
position: { name: 'left', args: { x: -8 } },
},
},
right: {
position: 'right',
attrs: {
circle: {
r: 6,
magnet: true,
stroke: '#8c8c8c',
strokeWidth: 1,
fill: '#fff',
},
},
label: {
position: { name: 'right', args: { x: 8 } },
},
},
},
items: portItems,
},
parent: device.siteId,
zIndex: 2,
};
nodes.push(node);
}
// 3. Create card nodes (embedded inside devices)
for (const card of data.cards) {
if (!card.visible) continue;
const parentDevice = data.devices.find((d) => d.id === card.deviceId);
if (!parentDevice) continue;
const parentPos = layout.nodePositions.get(card.deviceId);
if (!parentPos) continue;
const cardPorts = data.ports.filter((p) => p.cardId === card.id);
const cardIndex = data.cards.filter(
(c) => c.deviceId === card.deviceId && c.visible,
).indexOf(card);
const cardX = parentPos.x + 10;
const cardY = parentPos.y + DEVICE_HEADER_HEIGHT + 8 + cardIndex * (CARD_HEIGHT + 6);
const portItems = cardPorts.map((port) => {
const resolvedSide = portSideMap.get(port.id) ?? port.side;
const label = `${port.slotName}:${port.name}`;
return createPortItem(port.id, resolvedSide, label, port.labelColor || undefined);
});
nodes.push({
id: card.id,
shape: 'card-node',
x: cardX,
y: cardY,
width: CARD_WIDTH,
height: CARD_HEIGHT,
data: {
slotName: card.slotName,
networkName: card.networkName,
status: card.status,
entityType: 'card',
entityId: card.id,
},
ports: {
groups: {
left: {
position: 'left',
attrs: {
circle: {
r: 5,
magnet: true,
stroke: '#8c8c8c',
strokeWidth: 1,
fill: '#fff',
},
},
label: {
position: { name: 'left', args: { x: -6 } },
},
},
right: {
position: 'right',
attrs: {
circle: {
r: 5,
magnet: true,
stroke: '#8c8c8c',
strokeWidth: 1,
fill: '#fff',
},
},
label: {
position: { name: 'right', args: { x: 6 } },
},
},
},
items: portItems,
},
parent: card.deviceId,
zIndex: 3,
});
}
// 4. Create edges from lines
for (const line of data.lines) {
const portA = data.ports.find((p) => p.id === line.portAId);
const portZ = data.ports.find((p) => p.id === line.portZId);
if (!portA || !portZ) continue;
// Determine source and target cells
const sourceCellId = portA.cardId ?? portA.deviceId;
const targetCellId = portZ.cardId ?? portZ.deviceId;
edges.push(
createEdgeConfig(
line.id,
sourceCellId,
line.portAId,
targetCellId,
line.portZId,
line.status,
line.medium,
line.lineStyle,
line.name,
routerType,
),
);
}
return { nodes, edges };
}