Обходы графа
Функции по графу живут в vue-asset-map/core, который не импортирует ни Vue, ни Vue Flow. Они одинаково работают в браузере, в node-скрипте и на сервере.
import { buildGraph, neighbors, findPaths, blastRadius } from 'vue-asset-map/core'
const graph = buildGraph(entities, links)neighbors
Узлы на расстоянии до depth рёбер:
neighbors(graph, 'keenetic-1', 1)
neighbors(graph, 'keenetic-1', 2, ['physical'])Сам узел в результат не входит — даже если он достижим из себя по циклу: нужна окрестность, а не её замыкание.
findPaths
Простые пути между двумя узлами:
const paths = findPaths(graph, 'ws-artjom', 'ws-vlad', {
maxDepth: 6,
maxPaths: 10,
layers: ['logical'],
})Пути простые — узел не повторяется, поэтому обход завершается на любом цикле. Порядок результатов детерминирован: рёбра перебираются в порядке adjacency, то есть в порядке входного массива.
maxDepth считается в рёбрах и по умолчанию равен 6, maxPaths — 10.
blastRadius
Что затронет отказ узла: сам узел плюс всё, до чего распространяется влияние.
blastRadius(graph, 'vps-tailscale', { direction: 'downstream', maxDepth: 2, layers: ['logical'] })direction ограничивает только ориентированные связи. Неориентированная связь проходится в обе стороны при любом direction, потому что она не утверждает, кто от кого зависит. Направление зависимости, выведенное из неориентированного ребра, было бы недостоверным.
Подсветка пути на карте
Автоматически, по выделению
autoPath — это вся функция целиком: когда выделено ровно два узла, карта сама находит маршрут между ними и подсвечивает его, а результат уходит событием path-change. Ничего не нужно вызывать и вешать на кнопку.
<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 делает то же самое по требованию — для пары, которая пришла не из выделения, а, скажем, из списка активов или результата поиска:
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 стоит и за режимом заражения: помечаете сущности, а карта показывает всё, что они захватывают.
<AssetMap
:entities="entities"
:links="links"
:infected="['vps-tailscale']"
:infection-options="{ direction: 'downstream', maxDepth: 2 }"
/>Ребро считается заражённым, только если заражены оба конца. Один заражённый конец означает, что распространение по этому ребру не прошло: этому помешали направление или слой, и закрашивать такое ребро было бы неверно.
Групповой узел считается заражённым, если захвачен хоть один участник, иначе свёртка прятала бы факт заражения.
Цвета берутся из --vam-infected-bg, --vam-infected-border и --vam-infected-fg, а помеченный руками источник рисуется толще, чем захваченные им узлы.
Маркеры принадлежности
Маркеры — та же идея, но читаемая взглядом: кружок под подписью на каждый центр, с которым узел связан.
import { connectionMarkers, topAnchors } from 'vue-asset-map/core'
const markers = connectionMarkers([
{ id: 'keenetic-1', label: 'Офис A' },
{ id: 'vps-tailscale', label: 'VPN' },
])У рабочей станции, подключённой к роутеру физически и к координатору VPN логически, окажется два кружка: принадлежность к одному не отменяет другой. Слои по умолчанию не ограничены намеренно: кружки существуют, чтобы видеть принадлежность, не переключая представление, а подсчёт по текущему представлению скрыл бы ровно ту связь, ради которой на них смотрят.
Если центры заранее не известны, возьмите их по степени связности:
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 мс по умолчанию). Задержка не даёт курсору, проходящему над кружками по пути к чему-то другому, мигать всей картой. Фокус с клавиатуры подсвечивает сразу, потому что это уже осознанное действие.
Помощники по иерархии
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 возвращает порядок обхода в глубину и не включает сам узел.