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>
This commit is contained in:
Alina
2026-02-17 22:02:25 +03:00
commit ef816cdcf4
48 changed files with 8738 additions and 0 deletions

531
ARCHITECTURE.md Normal file
View File

@ -0,0 +1,531 @@
# 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), условное отображение |
### Система портов
```typescript
// Двухконтейнерная архитектура портов
interface PortContainerConfig {
left: PortGroup; // Левый контейнер (ЛК)
right: PortGroup; // Правый контейнер (ПК)
}
```
Логика распределения:
1. При загрузке — определение стороны на основе относительного положения связанных устройств
2. При перемещении устройства — `node:move` событие, пересчёт пересечения границ, автопереброс портов
3. Ручной drag — пользователь перетаскивает порт между ЛК и ПК
### Линии (Edges)
```typescript
// Конфигурация линии
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 слоёв)
```typescript
// Маппинг типов устройств на слои
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
};
```
Алгоритм:
1. Определить тип каждого устройства → маппинг в слой
2. Проверить overflow (слой 1, >6 кроссов → перенос в слой 8)
3. Рассчитать Y-координаты слоёв (пустые — схлопнуть)
4. Внутри слоя — расположить устройства в ряд с отступами
5. Применить позиции
### 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 и цветовая карта
```typescript
// Статусы (для цветовой карты — 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`:
```typescript
graph.options.embedding = {
enabled: true,
findParent: 'bbox', // или кастомная функция
};
```
- Сайт → устройства: `device.setParent(site)`
- Устройство → карты: `card.setParent(device)`
- Карта → порты: `port.setParent(card)` (через X6 ports API)
### 3. Группировка линий
Реализуется на уровне рендеринга:
1. При загрузке данных — группировка линий по парам `(deviceA, deviceZ)`
2. Если count > 1 → рендеринг одной «групповой» линии с label-счётчиком
3. По клику «Разгруппировать» → замена на N отдельных линий с разбивкой по статусам
### 4. Автолейаут
Кастомный алгоритм (не dagre/elk):
1. Парсинг устройств сайта → распределение по 8 слоям
2. Расчёт геометрии слоёв (Y offset, высота по максимальному элементу)
3. Размещение устройств внутри слоя (X offset, горизонтальный ряд)
4. Схлопывание пустых слоёв
5. Позиционирование дочерних сайтов по нижней границе родителя
### 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
- Тестирование, оптимизация производительности