Skip to content

Модель данных ​

Всё, что видно на экране, описывают два массива: entities и links. Карта их только читает: ни одно поле не добавляется, не переставляется и не нормализуется на месте.

AssetEntity ​

ts
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. Задайте ему тип, и он дойдёт до слотов:

ts
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' } },
]
ts
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Считается корнем.

Это важно, потому что данные об активах обычно приходят из систем инвентаризации, где висячая ссылка — обычное дело, и карта должна оставаться рабочей даже при таких данных:

ts
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: те же сущности и связи плюс индексы, которые читает всё остальное.

ts
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 хранит эффективного родителя, уже после починки данных, поэтому цикл в иерархии не подвесит обход.

Внутри компонента граф обычно не строят руками — карта строит его сама и отдаёт наружу:

ts
const map = useTemplateRef<AssetMapExposed>('map')
const graph = map.value?.getGraph()

Пересчёт привязан к ссылкам на массивы. Правка поля внутри существующей сущности индексы не трогает — подставьте новый массив, как это делает любой иммутабельный стор.

MIT licensed