Files
test-x6/frontend/src/components/ConnectionsPanel/ConnectionsPanel.tsx
Alina ef816cdcf4 feat: frontend MVP — детальная схема связей устройств (AntV X6)
- React 18 + TypeScript strict + AntV X6 2.x + AntD 5 + Zustand
- Custom nodes: SiteNode, CrossDeviceNode, SpliceNode, DeviceNode, CardNode
- 8-слойный автолейаут, порты (left/right), линии с цветами по статусу
- Toolbar, дерево навигации, карточка объекта, таблица соединений
- Контекстные меню, легенда, drag линий/нод, создание линий из портов
- Моковые данные: 3 сайта, 10 устройств, 15 линий

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

171 lines
5.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { useState } from 'react';
import { Table, Input, Button, Tag, Space } from 'antd';
import type { ColumnsType } from 'antd/es/table';
import { CloseOutlined, FullscreenOutlined, FullscreenExitOutlined } from '@ant-design/icons';
import { useSchemaStore } from '../../store/schemaStore.ts';
import { STATUS_COLORS, STATUS_LABELS } from '../../constants/statusColors.ts';
import { mockData } from '../../mock/schemaData.ts';
import type { EntityStatus, Line } from '../../types/index.ts';
const { Search } = Input;
interface ConnectionRow {
key: string;
lineName: string;
lineStatus: EntityStatus;
deviceAName: string;
portAName: string;
deviceZName: string;
portZName: string;
}
export function ConnectionsPanel() {
const visible = useSchemaStore((s) => s.connectionsPanelVisible);
const setVisible = useSchemaStore((s) => s.setConnectionsPanelVisible);
const panelData = useSchemaStore((s) => s.connectionsPanelData);
const [searchValue, setSearchValue] = useState('');
const [expanded, setExpanded] = useState(false);
if (!visible || !panelData) return null;
// Build rows from panel data
const rows: ConnectionRow[] = [];
if (panelData.line) {
// Single line mode
const line = panelData.line as Line;
const portA = panelData.portA as { name: string } | null;
const portZ = panelData.portZ as { name: string } | null;
const devA = panelData.deviceA as { name: string } | null;
const devZ = panelData.deviceZ as { name: string } | null;
rows.push({
key: line.id,
lineName: line.name,
lineStatus: line.status,
deviceAName: devA?.name ?? '—',
portAName: portA?.name ?? '—',
deviceZName: devZ?.name ?? '—',
portZName: portZ?.name ?? '—',
});
} else if (panelData.lines) {
// Multiple lines mode
const lines = panelData.lines as Line[];
for (const line of lines) {
const portA = mockData.ports.find((p) => p.id === line.portAId);
const portZ = mockData.ports.find((p) => p.id === line.portZId);
const devA = portA ? mockData.devices.find((d) => d.id === portA.deviceId) : null;
const devZ = portZ ? mockData.devices.find((d) => d.id === portZ.deviceId) : null;
rows.push({
key: line.id,
lineName: line.name,
lineStatus: line.status,
deviceAName: devA?.name ?? '—',
portAName: portA?.name ?? '—',
deviceZName: devZ?.name ?? '—',
portZName: portZ?.name ?? '—',
});
}
}
const filtered = searchValue
? rows.filter(
(r) =>
r.lineName.toLowerCase().includes(searchValue.toLowerCase()) ||
r.deviceAName.toLowerCase().includes(searchValue.toLowerCase()) ||
r.deviceZName.toLowerCase().includes(searchValue.toLowerCase()),
)
: rows;
const columns: ColumnsType<ConnectionRow> = [
{
title: 'Линия',
dataIndex: 'lineName',
key: 'lineName',
render: (name: string, record: ConnectionRow) => {
const colors = STATUS_COLORS[record.lineStatus];
return (
<Space size={4}>
<Tag color={colors.border} style={{ color: colors.text, fontSize: 10 }}>
{STATUS_LABELS[record.lineStatus]}
</Tag>
<span>{name}</span>
</Space>
);
},
},
{
title: 'Устройство A',
key: 'deviceA',
render: (_: unknown, record: ConnectionRow) => (
<span>
{record.deviceAName} ({record.portAName})
</span>
),
},
{
title: 'Устройство Z',
key: 'deviceZ',
render: (_: unknown, record: ConnectionRow) => (
<span>
{record.deviceZName} ({record.portZName})
</span>
),
},
];
return (
<div
style={{
height: expanded ? '60%' : '30%',
minHeight: 150,
borderTop: '1px solid #f0f0f0',
background: '#fff',
display: 'flex',
flexDirection: 'column',
flexShrink: 0,
}}
>
<div
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
padding: '6px 12px',
borderBottom: '1px solid #f0f0f0',
}}
>
<span style={{ fontWeight: 600, fontSize: 13 }}>Соединения</span>
<Space size={4}>
<Search
placeholder="Поиск..."
size="small"
style={{ width: 180 }}
value={searchValue}
onChange={(e) => setSearchValue(e.target.value)}
allowClear
/>
<Button
size="small"
icon={expanded ? <FullscreenExitOutlined /> : <FullscreenOutlined />}
onClick={() => setExpanded(!expanded)}
/>
<Button
size="small"
icon={<CloseOutlined />}
onClick={() => setVisible(false)}
/>
</Space>
</div>
<div style={{ flex: 1, overflow: 'auto' }}>
<Table
dataSource={filtered}
columns={columns}
size="small"
pagination={false}
style={{ fontSize: 11 }}
/>
</div>
</div>
);
}