- 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>
24 KiB
Test-X6: Детальная схема связей устройства — Архитектура
Обзор проекта
Веб-приложение для визуализации и редактирования детальных схем связей сетевых устройств. Основано на библиотеке AntV X6 для рендеринга интерактивных графов. Позволяет отображать сайты, устройства, карты, порты и линии связи с полной поддержкой вложенности, автоматической раскладки и многопользовательского редактирования.
Технологический стек
| Слой | Технологии |
|---|---|
| Frontend | React 18, TypeScript, AntD 5, AntV X6 2.x, Vite |
| Backend | NestJS, TypeORM, PostgreSQL |
| Инфраструктура | Docker, k3s, Drone CI, Harbor |
| Линтинг | ESLint, Prettier, tsc strict mode |
Структура монорепозитория
test-x6/
├── frontend/ # React-приложение
│ ├── src/
│ │ ├── api/ # API-клиент, типы запросов/ответов
│ │ ├── components/ # Общие UI-компоненты
│ │ │ ├── Layout/ # Общий layout приложения
│ │ │ ├── Toolbar/ # Панель инструментов
│ │ │ ├── SidePanel/ # Левая и правая боковые панели
│ │ │ └── ConnectionsPanel/# Нижняя панель соединений
│ │ ├── features/
│ │ │ └── schema/ # Основной модуль схемы
│ │ │ ├── graph/ # Инициализация и конфигурация X6 Graph
│ │ │ ├── nodes/ # Custom Nodes (Site, Cross, Splice, Device, Card)
│ │ │ ├── ports/ # Логика портов (контейнеры, сортировка, drag)
│ │ │ ├── edges/ # Линии (стили, группировка, перемычки)
│ │ │ ├── layout/ # Автолейаут (8 слоёв)
│ │ │ ├── context-menu/# Контекстные меню
│ │ │ ├── selection/ # Выделение (rectangle, lasso)
│ │ │ ├── locking/ # Блокировка / concurrency
│ │ │ └── export/ # Экспорт PNG, Excel
│ │ ├── hooks/ # React-хуки
│ │ ├── store/ # Zustand store
│ │ ├── types/ # TypeScript типы и интерфейсы
│ │ ├── utils/ # Утилиты
│ │ ├── constants/ # Константы (цвета, размеры, слои)
│ │ ├── App.tsx
│ │ └── main.tsx
│ ├── index.html
│ ├── vite.config.ts
│ ├── tsconfig.json
│ ├── .eslintrc.cjs
│ └── package.json
├── backend/ # NestJS API
│ ├── src/
│ │ ├── modules/
│ │ │ ├── sites/ # CRUD сайтов
│ │ │ ├── devices/ # CRUD устройств (активных и пассивных)
│ │ │ ├── cards/ # CRUD карт
│ │ │ ├── ports/ # CRUD портов
│ │ │ ├── lines/ # CRUD линий связи
│ │ │ ├── schema/ # Сборка и отдача схемы, сохранение layout
│ │ │ ├── locks/ # Блокировки редактирования
│ │ │ └── export/ # Экспорт (PNG-метаданные, Excel)
│ │ ├── common/ # Guards, interceptors, filters, pipes
│ │ ├── database/ # TypeORM конфигурация, миграции
│ │ ├── app.module.ts
│ │ └── main.ts
│ ├── tsconfig.json
│ ├── .eslintrc.cjs
│ └── package.json
├── CLAUDE.md
├── ARCHITECTURE.md
└── docker-compose.yml
Архитектура Frontend
Граф (X6 Graph)
Центральный компонент — SchemaCanvas, обёртка над @antv/x6 Graph. Инициализация:
SchemaCanvas
├── useGraphInit() — создание экземпляра Graph, плагины (Selection, Snapline, MiniMap, Keyboard)
├── useGraphData() — загрузка данных, маппинг в ноды/эджи X6
├── useGraphInteraction() — обработчики событий (click, dblclick, contextmenu, drag)
└── useGraphLayout() — автоматическая раскладка (8 слоёв)
Custom Nodes
Каждый тип ноды — отдельный класс, наследующий Shape.Rect (или Shape.HTML для сложных случаев):
| Нода | Класс | Особенности |
|---|---|---|
| SiteNode | Shape.Rect + custom markup |
Контейнер с шапкой (чёрный фон, 4 строки), вложенность (embedding), динамический размер |
| CrossDeviceNode | Shape.Rect + custom attrs |
Асимметричные скругления (rx/ry по углам через <path>), пропорция 1:3, L/S порты |
| SpliceClosureNode | Shape.Rect |
Квадрат 98×98, border-radius 6px |
| OtherDeviceNode | Shape.Rect |
Адаптивный размер под количество портов |
| CardNode | Shape.Rect (embedded) |
Вложенный в устройство, асимметричный контур (верх/низ 1.5px, бока 5px), условное отображение |
Система портов
// Двухконтейнерная архитектура портов
interface PortContainerConfig {
left: PortGroup; // Левый контейнер (ЛК)
right: PortGroup; // Правый контейнер (ПК)
}
Логика распределения:
- При загрузке — определение стороны на основе относительного положения связанных устройств
- При перемещении устройства —
node:moveсобытие, пересчёт пересечения границ, автопереброс портов - Ручной drag — пользователь перетаскивает порт между ЛК и ПК
Линии (Edges)
// Конфигурация линии
interface EdgeConfig {
style: EdgeLineStyle; // solid, dashed, dotted (Appendix B)
color: string; // по статусу (Appendix A)
medium: 'optical' | 'copper' | 'wireless' | 'unknown';
router: 'orth' | 'normal'; // ломаная / прямая
grouped: boolean; // входит ли в группу
}
Группировка: при >1 линии между парой устройств — рендеринг одной линии с badge-счётчиком. Тултип по hover — разбивка по статусам.
Автолейаут (8 слоёв)
// Маппинг типов устройств на слои
const LAYER_MAPPING: Record<number, DeviceType[]> = {
1: ['cross_optical', 'rrl', 'wireless', 'satellite'],
2: ['tspu', 'dwdm'],
3: ['men', 'sdh', 'multiservice_platform'],
4: ['ip', 'optical_modem', 'optical_mux', 'lan_wlan'],
5: ['ran_controller', 'mgn', 'mgx', 'server', 'sorm', 'mob', 'fix'],
6: ['voip', 'xdsl', 'pdh'],
7: ['ran_base_station', 'unknown', 'video_surveillance'],
8: [], // overflow from layer 1 (>6 cross) + copper cross
};
Алгоритм:
- Определить тип каждого устройства → маппинг в слой
- Проверить overflow (слой 1, >6 кроссов → перенос в слой 8)
- Рассчитать Y-координаты слоёв (пустые — схлопнуть)
- Внутри слоя — расположить устройства в ряд с отступами
- Применить позиции
State Management (Zustand)
SchemaStore
├── graph: Graph | null — экземпляр X6 Graph
├── data: SchemaData — текущие данные схемы
├── mode: 'view' | 'edit' — режим работы
├── selectedElements: string[] — выделенные элементы
├── clipboard: ClipboardData | null — буфер обмена
├── displaySettings: DisplaySettings — настройки отображения
├── locks: LockInfo[] — информация о блокировках
├── portSettings: Record<string, PortSettings> — пользовательские настройки портов
└── layoutPositions: Record<string, Position> — сохранённые позиции
Архитектура Backend
API Endpoints
# Схема
GET /api/schema/:deviceId — получить детальную схему устройства
PUT /api/schema/:deviceId/layout — сохранить layout (позиции, порты, карты)
# Сайты
GET /api/sites — список сайтов (с фильтрацией)
GET /api/sites/:id — карточка сайта
POST /api/sites — создать сайт
PUT /api/sites/:id — обновить сайт
DELETE /api/sites/:id — удалить сайт
# Устройства
GET /api/devices — список устройств (фильтры: тип, сайт)
GET /api/devices/:id — карточка устройства
POST /api/devices — создать устройство
PUT /api/devices/:id — обновить устройство
DELETE /api/devices/:id — удалить устройство
POST /api/devices/:id/copy — копирование (body: { withConnections: boolean })
PUT /api/devices/:id/move-site — перенос на другой сайт
# Карты
GET /api/devices/:deviceId/cards — карты устройства
PUT /api/cards/:id/visibility — показать/скрыть карту
# Порты
GET /api/devices/:deviceId/ports — порты устройства
PUT /api/ports/order — сохранить порядок портов
PUT /api/ports/:id/settings — настройки порта (цвет, сторона)
# Линии
GET /api/lines — линии (фильтры: device, site)
GET /api/lines/:id — карточка линии
POST /api/lines — создать линию
PUT /api/lines/:id — обновить линию
DELETE /api/lines/:id — удалить линию
POST /api/lines/:id/break — разрыв линии
POST /api/lines/:id/copy — копирование линии
# Блокировки
POST /api/locks — заблокировать элементы
DELETE /api/locks/:id — разблокировать
GET /api/locks/schema/:deviceId — текущие блокировки на схеме
# Экспорт
GET /api/export/excel/:deviceId — экспорт в Excel
Модель данных (TypeORM Entities)
┌──────────────────────┐
│ Site │
├──────────────────────┤
│ id: UUID │
│ name: string │
│ address: string │
│ erpCode: string │
│ code1C: string │
│ status: SiteStatus │
│ parentSiteId: UUID? │
│ ──────────────────── │
│ devices: Device[] │
│ childSites: Site[] │
└──────────────────────┘
│ 1:N
▼
┌──────────────────────┐
│ Device │
├──────────────────────┤
│ id: UUID │
│ name: string │
│ networkName: string │
│ ipAddress: string │
│ marking: string │
│ id1: string │
│ id2: string │
│ type: DeviceType │
│ group: DeviceGroup │ // 'active' | 'passive' | 'cross' | 'splice'
│ category: string │ // для маппинга в слой
│ status: DeviceStatus │
│ siteId: UUID │
│ ──────────────────── │
│ cards: Card[] │
│ ports: Port[] │
│ site: Site │
└──────────────────────┘
│ 1:N
▼
┌──────────────────────┐
│ Card │
├──────────────────────┤
│ id: UUID │
│ slotName: string │
│ networkName: string │
│ status: CardStatus │
│ visible: boolean │
│ deviceId: UUID │
│ ──────────────────── │
│ ports: Port[] │
│ device: Device │
└──────────────────────┘
│ 1:N
▼
┌──────────────────────┐
│ Port │
├──────────────────────┤
│ id: UUID │
│ name: string │
│ slotName: string │
│ side: 'left' | 'right'│
│ sortOrder: number │
│ labelColor: string │
│ deviceId: UUID │
│ cardId: UUID? │
│ ──────────────────── │
│ device: Device │
│ card: Card? │
│ linesA: Line[] │
│ linesZ: Line[] │
└──────────────────────┘
┌──────────────────────┐
│ Line │
├──────────────────────┤
│ id: UUID │
│ name: string │
│ templateName: string │
│ status: LineStatus │
│ type: LineType │ // simple | complex
│ medium: Medium │ // optical | copper | wireless | unknown
│ lineStyle: LineStyle │ // solid | dashed | dotted
│ portAId: UUID │
│ portZId: UUID │
│ ──────────────────── │
│ portA: Port │
│ portZ: Port │
│ fibers: Fiber[] │ // для сложных линий
└──────────────────────┘
┌──────────────────────┐
│ Fiber │
├──────────────────────┤
│ id: UUID │
│ name: string │
│ status: FiberStatus │
│ lineId: UUID │
│ portAId: UUID │
│ portZId: UUID │
└──────────────────────┘
┌──────────────────────┐
│ SchemaLayout │
├──────────────────────┤
│ id: UUID │
│ userId: UUID │
│ deviceId: UUID │ // центральное устройство схемы
│ positions: jsonb │ // { nodeId: {x, y, w, h} }
│ portOrders: jsonb │ // { deviceId: { portId: sortOrder } }
│ cardVisibility: jsonb │ // { cardId: boolean }
│ settings: jsonb │ // настройки отображения
│ updatedAt: Date │
└──────────────────────┘
┌──────────────────────┐
│ EditLock │
├──────────────────────┤
│ id: UUID │
│ schemaDeviceId: UUID │ // схема которую заблокировали
│ userId: UUID │
│ userName: string │
│ lockedAt: Date │
│ expiresAt: Date │ // автоистечение через N минут
└──────────────────────┘
Enums и цветовая карта
// Статусы (для цветовой карты — Appendix A)
enum EntityStatus {
Active = 'active',
Planned = 'planned',
UnderConstruction = 'under_construction',
Reserved = 'reserved',
Faulty = 'faulty',
Decommissioned = 'decommissioned',
Unknown = 'unknown',
}
// Типы устройств (для маппинга в слои)
enum DeviceCategory {
CrossOptical = 'cross_optical',
CrossCopper = 'cross_copper',
RRL = 'rrl',
Wireless = 'wireless',
Satellite = 'satellite',
TSPU = 'tspu',
DWDM = 'dwdm',
MEN = 'men',
SDH = 'sdh',
MultiservicePlatform = 'multiservice_platform',
IP = 'ip',
OpticalModem = 'optical_modem',
OpticalMux = 'optical_mux',
LanWlan = 'lan_wlan',
RanController = 'ran_controller',
MGN = 'mgn',
MGX = 'mgx',
Server = 'server',
SORM = 'sorm',
MOB = 'mob',
FIX = 'fix',
VOIP = 'voip',
xDSL = 'xdsl',
PDH = 'pdh',
RanBaseStation = 'ran_base_station',
Unknown = 'unknown',
VideoSurveillance = 'video_surveillance',
}
// Группы устройств
enum DeviceGroup {
Active = 'active',
Passive = 'passive',
}
// Среда передачи
enum Medium {
Optical = 'optical',
Copper = 'copper',
Wireless = 'wireless',
Unknown = 'unknown',
}
Ключевые архитектурные решения
1. Рендеринг нод: SVG vs HTML
- Сайт (SiteNode): SVG
Shape.Rectс кастомным markup. Шапка —<foreignObject>для текстового переноса. - Кросс: SVG
<path>для асимметричного скругления. - Карты и порты: SVG элементы, вложенные через X6 embedding (parent-child).
2. Вложенность (Embedding)
X6 нативно поддерживает embedding:
graph.options.embedding = {
enabled: true,
findParent: 'bbox', // или кастомная функция
};
- Сайт → устройства:
device.setParent(site) - Устройство → карты:
card.setParent(device) - Карта → порты:
port.setParent(card)(через X6 ports API)
3. Группировка линий
Реализуется на уровне рендеринга:
- При загрузке данных — группировка линий по парам
(deviceA, deviceZ) - Если count > 1 → рендеринг одной «групповой» линии с label-счётчиком
- По клику «Разгруппировать» → замена на N отдельных линий с разбивкой по статусам
4. Автолейаут
Кастомный алгоритм (не dagre/elk):
- Парсинг устройств сайта → распределение по 8 слоям
- Расчёт геометрии слоёв (Y offset, высота по максимальному элементу)
- Размещение устройств внутри слоя (X offset, горизонтальный ряд)
- Схлопывание пустых слоёв
- Позиционирование дочерних сайтов по нижней границе родителя
5. Блокировка / Concurrency
- При переходе в режим редактирования →
POST /api/locks(pessimistic lock) - Блокировка с TTL (auto-expire через 30 минут неактивности)
- Heartbeat каждые 60 секунд для продления блокировки
- Другие пользователи видят значок блокировки (overlay на нодах)
- При сохранении →
DELETE /api/locks/:id
6. Сохранение состояния
- Позиции, порядок портов, видимость карт — JSON в таблице
schema_layout - Привязка к паре
(userId, deviceId)— у каждого пользователя свой layout - При отсутствии сохранённого layout — применяется автолейаут
Зависимости Frontend
| Пакет | Назначение |
|---|---|
@antv/x6 |
Основная библиотека графов |
@antv/x6-plugin-selection |
Выделение (rectangle, rubberband) |
@antv/x6-plugin-snapline |
Привязка к сетке |
@antv/x6-plugin-minimap |
Мини-карта |
@antv/x6-plugin-keyboard |
Горячие клавиши |
@antv/x6-plugin-clipboard |
Буфер обмена |
@antv/x6-plugin-history |
Undo/Redo |
@antv/x6-plugin-export |
Экспорт PNG |
antd |
UI-компоненты (меню, модалки, таблицы, формы) |
zustand |
State management |
axios |
HTTP-клиент |
xlsx |
Экспорт Excel |
Фазы реализации
Фаза 1: Каркас и базовые ноды
- Инициализация проекта (frontend + backend)
- Настройка X6 Graph с базовыми плагинами
- Custom Nodes: SiteNode, OtherDeviceNode (простые)
- Базовые Entity и CRUD (sites, devices)
- Загрузка и отображение статической схемы
Фаза 2: Порты и линии
- Система портов (двухконтейнерная, сортировка)
- CardNode с вложенными портами
- Линии с привязкой к портам
- Стили линий по статусу и среде передачи
- CrossDeviceNode, SpliceClosureNode
Фаза 3: Интерактивность
- Контекстные меню (6 типов)
- Drag-and-drop устройств
- Создание линий (4 способа)
- Выделение (rectangle, lasso)
- Автолейаут (8 слоёв)
Фаза 4: Панели и UI
- Toolbar с настройками
- Левая панель (дерево навигации)
- Правая панель (карточки объектов)
- Нижняя панель соединений
Фаза 5: Режимы работы и persistence
- Режим просмотра / редактирования
- Блокировки (concurrency)
- Сохранение layout (позиции, порты, карты)
- Копирование/вставка со связями
- Разрыв линии
Фаза 6: Группировка, экспорт, полировка
- Группировка линий (счётчик, тултип, разгруппировка)
- Экспорт PNG и Excel
- Перемычки (self-loops)
- Snap-to-grid, MiniMap
- Тестирование, оптимизация производительности