Skip to content

Обходы графа ​

Функции по графу живут в vue-asset-map/core, который не импортирует ни Vue, ни Vue Flow. Они одинаково работают в браузере, в node-скрипте и на сервере.

ts
import { buildGraph, neighbors, findPaths, blastRadius } from 'vue-asset-map/core'

const graph = buildGraph(entities, links)

neighbors ​

Узлы на расстоянии до depth рёбер:

ts
neighbors(graph, 'keenetic-1', 1)
neighbors(graph, 'keenetic-1', 2, ['physical'])

Сам узел в результат не входит — даже если он достижим из себя по циклу: нужна окрестность, а не её замыкание.

findPaths ​

Простые пути между двумя узлами:

ts
const paths = findPaths(graph, 'ws-artjom', 'ws-vlad', {
  maxDepth: 6,
  maxPaths: 10,
  layers: ['logical'],
})

Пути простые — узел не повторяется, поэтому обход завершается на любом цикле. Порядок результатов детерминирован: рёбра перебираются в порядке adjacency, то есть в порядке входного массива.

maxDepth считается в рёбрах и по умолчанию равен 6, maxPaths — 10.

blastRadius ​

Что затронет отказ узла: сам узел плюс всё, до чего распространяется влияние.

ts
blastRadius(graph, 'vps-tailscale', { direction: 'downstream', maxDepth: 2, layers: ['logical'] })

direction ограничивает только ориентированные связи. Неориентированная связь проходится в обе стороны при любом direction, потому что она не утверждает, кто от кого зависит. Направление зависимости, выведенное из неориентированного ребра, было бы недостоверным.

Подсветка пути на карте ​

Автоматически, по выделению ​

autoPath — это вся функция целиком: когда выделено ровно два узла, карта сама находит маршрут между ними и подсвечивает его, а результат уходит событием path-change. Ничего не нужно вызывать и вешать на кнопку.

vue
<AssetMap
  v-model:selection="selection"
  :entities="entities"
  :links="links"
  auto-path
  @path-change="(hops) => (path = hops)"
/>

Пара набирается Shift+кликом. Клик с модификатором только набирает выделение и окно подробностей не открывает: иначе выбор второго узла завалил бы окнами тот самый путь, который вы попросили под ними нарисовать. Обычный клик окно по-прежнему открывает.

Shift — надёжный модификатор: он добавляет к выделению на любой платформе. Ctrl+клик на macOS до обработчика клика узла не доходит, потому что там это системный жест правого клика, и браузер шлёт вместо click событие contextmenu. Ctrl и Cmd по-прежнему работают для тех, чья привычка настроена на них.

Любое количество выделенных узлов, кроме двух, означает, что соединять нечего, и подсветка снимается — в том числе в момент, когда к выделению добавляется третий узел. Выделение снаружи работает так же: два id в v-model:selection строят путь без единого клика.

path-change срабатывает только когда маршрут действительно изменился, поэтому на него можно вешать работу. Событие несёт id исходных сущностей по порядку или пустой массив, если подсветку сняли.

По умолчанию autoPath выключен: карта, которая сама строит пути, спорила бы с приложением, которое подсвечивает по выделению что-то своё.

Вручную ​

highlightPath делает то же самое по требованию — для пары, которая пришла не из выделения, а, скажем, из списка активов или результата поиска:

ts
const map = useTemplateRef<AssetMapExposed>('map')

const hops = map.value?.highlightPath('ws-artjom', 'ws-vlad') ?? []
map.value?.clearHighlight()

Он тоже эмитит path-change, так что оба пути к функции отчитываются одним каналом.

pathOptions ограничивает поиск для обоих — { maxDepth, layers }, по умолчанию 12 шагов и слои текущего представления. Третий аргумент highlightPath перекрывает их для одного вызова. Глубина ограничена потому, что поиск идёт в глубину: на плотной карте неограниченный обход — это то, что вешает вкладку, а маршрут длиннее этого читатель на экране всё равно не проследит.

Поиск идёт по исходному графу с учётом слоёв текущего представления, а подсветка рисуется по свёрнутому графу: если участок пути спрятан в группе, загорается групповой узел и ведущие к нему рёбра. Иначе путь визуально обрывался бы. Поэтому же трассировка пересчитывается, когда группа сворачивается или меняется вид: маршрут зависит от того, какие связи сейчас на экране.

Показать исходник
<script setup lang="ts">
import { ref } from 'vue'
import { AssetMap } from 'vue-asset-map'
import { useSiteTheme } from '../theme/useSiteTheme'
import { useSiteLocale } from '../theme/useSiteLocale'
import { entities, links, views, statusColors, kindIcons } from './fixture'

const theme = useSiteTheme()
const locale = useSiteLocale()
const selection = ref<string[]>(['ws-artjom', 'ws-vlad'])
const traced = ref<string[]>([])

// `autoPath` traces the pair itself, so there is no button to press and nothing
// to call on mount — the pre-selected pair is already highlighted on the first
// frame. The route arrives through `path-change` in original entity ids; it is
// searched on the original graph and drawn on the collapsed one, so a stretch
// hidden inside a group still lights up.
function onPathChange(path: string[]): void {
  traced.value = path
}

// Clearing the selection is all it takes: the trace follows it.
function clear(): void {
  selection.value = []
}
</script>

<template>
  <AssetMap
    v-model:selection="selection"
    class="demo-map"
    view="logical"
    :entities="entities"
    :links="links"
    :views="views"
    :theme="theme"
    :locale="locale"
    :theme-config="{ statusColors, kindIcons }"
    :layout="{ engine: 'radial', options: { rootId: 'vps-tailscale' } }"
    auto-path
    auto-fit-view
    @path-change="onPathChange"
  >
    <template #toolbar-right>
      <span class="demo-toolbar-button demo-toolbar-button--hint">
        Shift-click two nodes — the path traces itself
      </span>
      <button v-if="traced.length" type="button" class="demo-toolbar-button" @click="clear">
        Clear · {{ traced.length }} hops
      </button>
    </template>
  </AssetMap>
</template>

<style scoped>
.demo-toolbar-button--hint {
  cursor: default;
  color: var(--vp-c-text-2);
}

.demo-toolbar-button--hint:hover {
  border-color: var(--vp-c-divider);
}
</style>

Режим «заражения» ​

blastRadius стоит и за режимом заражения: помечаете сущности, а карта показывает всё, что они захватывают.

vue
<AssetMap
  :entities="entities"
  :links="links"
  :infected="['vps-tailscale']"
  :infection-options="{ direction: 'downstream', maxDepth: 2 }"
/>

Ребро считается заражённым, только если заражены оба конца. Один заражённый конец означает, что распространение по этому ребру не прошло: этому помешали направление или слой, и закрашивать такое ребро было бы неверно.

Групповой узел считается заражённым, если захвачен хоть один участник, иначе свёртка прятала бы факт заражения.

Цвета берутся из --vam-infected-bg, --vam-infected-border и --vam-infected-fg, а помеченный руками источник рисуется толще, чем захваченные им узлы.

Маркеры принадлежности ​

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

ts
import { connectionMarkers, topAnchors } from 'vue-asset-map/core'

const markers = connectionMarkers([
  { id: 'keenetic-1', label: 'Офис A' },
  { id: 'vps-tailscale', label: 'VPN' },
])

У рабочей станции, подключённой к роутеру физически и к координатору VPN логически, окажется два кружка: принадлежность к одному не отменяет другой. Слои по умолчанию не ограничены намеренно: кружки существуют, чтобы видеть принадлежность, не переключая представление, а подсчёт по текущему представлению скрыл бы ровно ту связь, ради которой на них смотрят.

Если центры заранее не известны, возьмите их по степени связности:

ts
const markers = connectionMarkers(topAnchors(graph, 3))
Показать исходник
<script setup lang="ts">
import { ref } from 'vue'
import { AssetMap } from 'vue-asset-map'
import { connectionMarkers } from 'vue-asset-map/core'
import { useSiteTheme } from '../theme/useSiteTheme'
import { useSiteLocale } from '../theme/useSiteLocale'
import { entities, links, views, statusColors, kindIcons } from './fixture'

const theme = useSiteTheme()
const locale = useSiteLocale()

// One marker per hub; a node carries the dot of every hub it is connected to,
// so a workstation wired to a router and joined to the VPN gets two. Markers
// are not restricted to a layer, so switching the view here does not change
// which dots a node shows — that persistence is the point of the demo.
const markers = connectionMarkers([
  { id: 'keenetic-1', label: 'Keenetic 1' },
  { id: 'keenetic-2', label: 'Keenetic 2' },
  { id: 'vps-tailscale', label: 'Tailscale VPS' },
])

const view = ref('physical')
</script>

<template>
  <AssetMap
    v-model:view="view"
    class="demo-map"
    :entities="entities"
    :links="links"
    :views="views"
    :markers="markers"
    :marker-hover-delay="300"
    :theme="theme"
    :locale="locale"
    :theme-config="{ statusColors, kindIcons }"
    :layout="{ engine: 'radial', options: { rootId: view === 'physical' ? 'internet' : 'vps-tailscale' } }"
    auto-fit-view
  />
</template>

Наведение на кружок подсвечивает всех, у кого он есть, и гасит остальных, после задержки markerHoverDelay (500 мс по умолчанию). Задержка не даёт курсору, проходящему над кружками по пути к чему-то другому, мигать всей картой. Фокус с клавиатуры подсвечивает сразу, потому что это уже осознанное действие.

Помощники по иерархии ​

ts
import { ancestorsOf, descendantsOf, incidentLinks, otherEnd } from 'vue-asset-map/core'

ancestorsOf(graph, 'ct-web-1')
descendantsOf(graph, 'pve-beelink')
incidentLinks(graph, 'keenetic-1', ['physical'])

Оба обхода циклобезопасны, descendantsOf возвращает порядок обхода в глубину и не включает сам узел.

MIT licensed