Модель данных
Всё, что видно на экране, описывают два массива: entities и links. Карта их только читает: ни одно поле не добавляется, не переставляется и не нормализуется на месте.
AssetEntity
interface AssetEntity<M = unknown> {
id: EntityId
type: string
label: string
sublabels?: string[]
status?: string
badges?: Badge[]
parentId?: EntityId | null
position?: XY
meta?: M
}| Поле | Зачем |
|---|---|
id | Уникален в наборе. Всё остальное ссылается на сущность по нему. |
type | Выбирает иконку и компонент отрисовки. Открытый строковый союз — годится любое значение. |
label | Название на плашке под кругом. |
sublabels | Строки под названием: IP, FQDN, домен. Первая рисуется приглушённым цветом, вторая — цветом оверлея. |
status | Через themeConfig.statusColors превращается в цвет. Пакет не знает ни одного статуса заранее. |
badges | Небольшие метки под плашкой. |
parentId | Иерархия для группировки и свёртки. null или отсутствие — корень. |
position | Закреплённая позиция в пикселях холста. Сильнее алгоритма раскладки. |
meta | Пользовательские данные. Внутри помечаются markRaw и реактивными не становятся. |
meta — это то, что рисует панель подробностей и что читают группы фильтров через valuesOf. Задайте ему тип, и он дойдёт до слотов:
interface AssetMeta {
owner?: string
subnet?: 'internet' | 'vpn' | 'docker'
severity?: 'critical' | 'high' | 'medium' | 'low'
}
const entities: AssetEntity<AssetMeta>[] = [
{ id: 'ws-1', type: 'workstation', label: 'Рабочая станция', meta: { owner: 'Дмитрий', severity: 'low' } },
]AssetLink
interface AssetLink<M = unknown> {
id: string
source: EntityId
target: EntityId
layers: string[]
type?: string
label?: string
directed?: boolean
sourceHandle?: string
targetHandle?: string
meta?: M
}| Поле | Зачем |
|---|---|
layers | В какие представления попадает связь. См. Представления. |
type | Выбирает стиль ребра через CSS-класс и свой компонент ребра, если он зарегистрирован. |
directed | Рисует стрелку и ограничивает обходы, которые считаются по направлению, например blastRadius. |
label | Рисуется на линии. У агрегированных рёбер вместо неё счётчик. |
sourceHandle / targetHandle | К какой ручке на каждом конце крепится линия. Только для узлов, рисующих больше одной, — см. Связи. |
meta | Ваша нагрузка, так же как у сущности. |
Параллельные связи сохраняются: две связи между той же парой с разными id — это два ребра, и кратность доживает до агрегации при свёртке группы.
Обработка некорректных данных
buildGraph никогда не бросает исключение на плохих данных. Он сообщает через onIssue и продолжает:
| Ситуация | Что происходит |
|---|---|
| Дубль id сущности или связи | Побеждает первая запись. |
| Связь в несуществующую сущность | Связь отбрасывается. |
parentId в несуществующую сущность | Узел становится корневым. |
| Цикл в иерархии | Связь с родителем разрывается у узла, замкнувшего цикл. |
parentId, равный своему же id | Считается корнем. |
Это важно, потому что данные об активах обычно приходят из систем инвентаризации, где висячая ссылка — обычное дело, и карта должна оставаться рабочей даже при таких данных:
import { buildGraph } from 'vue-asset-map/core'
const graph = buildGraph(entities, links, {
onIssue: (issue) => console.warn(`[asset-map] ${issue.code}: ${issue.id}`, issue.detail),
})Индексированный граф
buildGraph возвращает AssetGraph: те же сущности и связи плюс индексы, которые читает всё остальное.
interface AssetGraph<E = unknown, L = unknown> {
entities: ReadonlyMap<EntityId, AssetEntity<E>>
links: ReadonlyMap<string, AssetLink<L>>
adjacency: ReadonlyMap<EntityId, readonly string[]>
childrenOf: ReadonlyMap<EntityId, readonly EntityId[]>
roots: readonly EntityId[]
parentOf: ReadonlyMap<EntityId, EntityId | null>
outgoing: ReadonlyMap<EntityId, readonly string[]>
incoming: ReadonlyMap<EntityId, readonly string[]>
}Две детали важны для понимания структуры. Петля попадает в adjacency один раз, а не два, поэтому петля, проведённая дважды, остаётся отличимой от двух параллельных связей. parentOf хранит эффективного родителя, уже после починки данных, поэтому цикл в иерархии не подвесит обход.
Внутри компонента граф обычно не строят руками — карта строит его сама и отдаёт наружу:
const map = useTemplateRef<AssetMapExposed>('map')
const graph = map.value?.getGraph()Пересчёт привязан к ссылкам на массивы. Правка поля внутри существующей сущности индексы не трогает — подставьте новый массив, как это делает любой иммутабельный стор.