feat: миграция frontend React 18 → Angular 19
All checks were successful
continuous-integration/drone/push Build is passing
All checks were successful
continuous-integration/drone/push Build is passing
Полная миграция фронтенда на Angular 19 + PrimeNG + NgRx SignalStore. - React 18 + AntD 5 + Zustand + Vite → Angular 19 + PrimeNG 19 + NgRx SignalStore + Angular CLI - Ноды X6: React-компоненты → чистые DOM-функции через Shape.HTML.register с effect: ['data'] - Все 5 типов нод (site, device, cross-device, splice, card) переписаны как рендереры - Zustand store → NgRx signalStore (schema.store.ts) - AntD компоненты → PrimeNG (p-tree, p-table, p-menu, p-toggleSwitch, p-slider, p-button) - 13 файлов чистого TypeScript переиспользованы as-is (типы, константы, утилиты, мок, layout, ports, edges) - Структура файлов переименована в kebab-case Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
316
frontend/src/app/features/schema/helpers/data-mapper.ts
Normal file
316
frontend/src/app/features/schema/helpers/data-mapper.ts
Normal file
@ -0,0 +1,316 @@
|
||||
import type { SchemaData } from '../../../types/index';
|
||||
import { DeviceCategory } from '../../../types/index';
|
||||
import type { GraphNodeConfig, GraphEdgeConfig, GraphBuildResult } from '../../../types/graph';
|
||||
import { createPortItem } from '../ports/port-config';
|
||||
import { createEdgeConfig } from '../edges/edge-config';
|
||||
import { autoLayout, type LayoutResult } from '../layout/auto-layout';
|
||||
import { resolvePortSides } from './port-side-resolver';
|
||||
import {
|
||||
CROSS_WIDTH,
|
||||
CROSS_HEIGHT,
|
||||
SPLICE_SIZE,
|
||||
DEVICE_MIN_WIDTH,
|
||||
DEVICE_MIN_HEIGHT,
|
||||
DEVICE_HEADER_HEIGHT,
|
||||
CARD_WIDTH,
|
||||
CARD_HEIGHT,
|
||||
} from '../../../constants/sizes';
|
||||
|
||||
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 = cardCount > 0 && portCount > 0
|
||||
? cardsHeight + portHeight
|
||||
: 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 hasCards = deviceCards.length > 0;
|
||||
const cardsEndY = hasCards
|
||||
? DEVICE_HEADER_HEIGHT + deviceCards.length * (CARD_HEIGHT + 6) + 8
|
||||
: 0;
|
||||
|
||||
const leftDevicePorts = devicePorts.filter(
|
||||
(p) => (portSideMap.get(p.id) ?? p.side) === 'left',
|
||||
);
|
||||
const rightDevicePorts = devicePorts.filter(
|
||||
(p) => (portSideMap.get(p.id) ?? p.side) === 'right',
|
||||
);
|
||||
|
||||
const portItems = devicePorts.map((port) => {
|
||||
const resolvedSide = portSideMap.get(port.id) ?? port.side;
|
||||
const label = port.slotName ? `${port.slotName}:${port.name}` : port.name;
|
||||
|
||||
let portArgs: { x: number; y: number } | undefined;
|
||||
if (hasCards) {
|
||||
const sameSidePorts = resolvedSide === 'left' ? leftDevicePorts : rightDevicePorts;
|
||||
const indexInSide = sameSidePorts.indexOf(port);
|
||||
const portSpacing = 22;
|
||||
portArgs = {
|
||||
x: resolvedSide === 'left' ? 0 : size.width,
|
||||
y: cardsEndY + portSpacing * (indexInSide + 0.5),
|
||||
};
|
||||
}
|
||||
|
||||
return createPortItem(port.id, resolvedSide, label, port.labelColor || undefined, portArgs);
|
||||
});
|
||||
|
||||
const leftPosition = hasCards ? 'absolute' : 'left';
|
||||
const rightPosition = hasCards ? 'absolute' : 'right';
|
||||
|
||||
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: leftPosition,
|
||||
attrs: {
|
||||
circle: {
|
||||
r: 6,
|
||||
magnet: true,
|
||||
stroke: '#8c8c8c',
|
||||
strokeWidth: 1,
|
||||
fill: '#fff',
|
||||
},
|
||||
},
|
||||
label: {
|
||||
position: { name: 'top', args: { y: -8 } },
|
||||
},
|
||||
},
|
||||
right: {
|
||||
position: rightPosition,
|
||||
attrs: {
|
||||
circle: {
|
||||
r: 6,
|
||||
magnet: true,
|
||||
stroke: '#8c8c8c',
|
||||
strokeWidth: 1,
|
||||
fill: '#fff',
|
||||
},
|
||||
},
|
||||
label: {
|
||||
position: { name: 'top', args: { y: -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: 'top', args: { y: -6 } },
|
||||
},
|
||||
},
|
||||
right: {
|
||||
position: 'right',
|
||||
attrs: {
|
||||
circle: {
|
||||
r: 5,
|
||||
magnet: true,
|
||||
stroke: '#8c8c8c',
|
||||
strokeWidth: 1,
|
||||
fill: '#fff',
|
||||
},
|
||||
},
|
||||
label: {
|
||||
position: { name: 'top', args: { y: -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;
|
||||
|
||||
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 };
|
||||
}
|
||||
Reference in New Issue
Block a user