Карта сервисов в реальном времени
Диаграмма, которая показывает состояние системы по мере его изменения: каждая связь между сервисами окрашивается, штрихуется и анимируется в зависимости от своего здоровья и подписывается задержкой. Приходят новые данные, линии меняются, а диаграмму вручную никто не пересобирает.
Что здесь используется:
- Стилизация соединений —
styleсоединения несёт его цвет, штрихи и прозрачность. - Анимированный поток — бегущий узор, скорость которого отражает состояние.
- Подписи соединений — задержка в подписи, которую рисует библиотека.
- Темы —
darkThemeдля тёмной панели.
Чистый JavaScript
<div id="diagram">
<div class="service" id="gateway" style="left: 0; top: 110px">Gateway</div>
<div class="service" id="api" style="left: 260px; top: 110px">API</div>
<div class="service" id="db" style="left: 540px; top: 30px">Database</div>
<div class="service" id="cache" style="left: 540px; top: 190px">Cache</div>
</div>body {
margin: 0;
padding: 16px;
font:
14px system-ui,
sans-serif;
background: #0f172a;
color: #e2e8f0;
}
#diagram {
position: relative;
height: 300px;
}
.service {
position: absolute;
width: 120px;
padding: 12px 14px;
background: #1e293b;
border: 1px solid #475569;
border-radius: 8px;
text-align: center;
}Данные лежат в status. Одна функция превращает их в соединения, а таймер заменяет ваш настоящий источник, которым может быть опрос или WebSocket.
import {
createVisualLinker,
darkTheme,
type ConnectionDescriptor,
} from '@macrulez/visual-linker-core'
type Status = 'ok' | 'slow' | 'down'
const links = [
{ id: 'gateway>api', from: 'gateway', to: 'api' },
{ id: 'api>db', from: 'api', to: 'db' },
{ id: 'api>cache', from: 'api', to: 'cache' },
]
const status: Record<string, { status: Status; latency: number }> = {
'gateway>api': { status: 'ok', latency: 12 },
'api>db': { status: 'ok', latency: 48 },
'api>cache': { status: 'ok', latency: 3 },
}
const look = {
ok: { color: '#4ade80', dashed: false, animated: { shape: 'dots', speed: 50 } },
slow: { color: '#fbbf24', dashed: false, animated: { shape: 'dashes', speed: 20 } },
down: { color: '#f87171', dashed: true, animated: false, opacity: 0.7 },
} as const
function describe(): ConnectionDescriptor[] {
return links.map(({ id, from, to }) => {
const { status: s, latency } = status[id]
return {
id,
from: { blockId: from },
to: { blockId: to },
style: { ...look[s], markers: { end: { shape: 'arrow' } } },
labels: [
{ id: 'latency', position: 'middle', text: s === 'down' ? 'down' : `${latency} ms` },
],
}
})
}
const diagram = document.querySelector<HTMLElement>('#diagram')!
const linker = createVisualLinker(diagram, {
theme: darkTheme,
lines: { curve: 'smoothstep', width: 2, highlight: { width: 3 } },
interaction: { highlight: true },
})
linker.setBlocks(
['gateway', 'api', 'db', 'cache'].map((id) => ({
id,
el: diagram.querySelector<HTMLElement>(`#${id}`)!,
})),
)
linker.setConnections(describe())
setInterval(() => {
const link = links[Math.floor(Math.random() * links.length)]
const next: Status[] = ['ok', 'slow', 'down']
status[link.id] = {
status: next[Math.floor(Math.random() * 3)],
latency: Math.round(Math.random() * 400),
}
linker.setConnections(describe())
}, 2000)Vue
Данные о здоровье — reactive, а соединения — computed от них, поэтому любое изменение данных перерисовывает диаграмму. Стили те же, что выше, в блоке <style scoped>.
<script setup lang="ts">
import { computed, onBeforeUnmount, onMounted, reactive } from 'vue'
import { VisualLinker, darkTheme, vVlBlock } from '@macrulez/visual-linker-vue'
import type { ConnectionDescriptor, VisualLinkerConfig } from '@macrulez/visual-linker-vue'
type Status = 'ok' | 'slow' | 'down'
const services = [
{ id: 'gateway', title: 'Gateway', x: 0, y: 110 },
{ id: 'api', title: 'API', x: 260, y: 110 },
{ id: 'db', title: 'Database', x: 540, y: 30 },
{ id: 'cache', title: 'Cache', x: 540, y: 190 },
]
const links = [
{ id: 'gateway>api', from: 'gateway', to: 'api' },
{ id: 'api>db', from: 'api', to: 'db' },
{ id: 'api>cache', from: 'api', to: 'cache' },
]
const health = reactive<Record<string, { status: Status; latency: number }>>({
'gateway>api': { status: 'ok', latency: 12 },
'api>db': { status: 'ok', latency: 48 },
'api>cache': { status: 'ok', latency: 3 },
})
const look = {
ok: { color: '#4ade80', dashed: false, animated: { shape: 'dots', speed: 50 } },
slow: { color: '#fbbf24', dashed: false, animated: { shape: 'dashes', speed: 20 } },
down: { color: '#f87171', dashed: true, animated: false, opacity: 0.7 },
} as const
const connections = computed<ConnectionDescriptor[]>(() =>
links.map(({ id, from, to }) => {
const { status, latency } = health[id]
return {
id,
from: { blockId: from },
to: { blockId: to },
style: { ...look[status], markers: { end: { shape: 'arrow' } } },
labels: [
{ id: 'latency', position: 'middle', text: status === 'down' ? 'down' : `${latency} ms` },
],
}
}),
)
const config: VisualLinkerConfig = {
theme: darkTheme,
lines: { curve: 'smoothstep', width: 2, highlight: { width: 3 } },
interaction: { highlight: true },
}
let timer: ReturnType<typeof setInterval>
onMounted(() => {
timer = setInterval(() => {
const link = links[Math.floor(Math.random() * links.length)]
const statuses: Status[] = ['ok', 'slow', 'down']
health[link.id] = {
status: statuses[Math.floor(Math.random() * 3)],
latency: Math.round(Math.random() * 400),
}
}, 2000)
})
onBeforeUnmount(() => clearInterval(timer))
</script>
<template>
<VisualLinker class="diagram" :connections="connections" :config="config">
<div
v-for="service in services"
:key="service.id"
v-vl-block="service.id"
class="service"
:style="{ left: `${service.x}px`, top: `${service.y}px` }"
>
{{ service.title }}
</div>
</VisualLinker>
</template>Nuxt
Компонент работает без изменений. Таймер запускается в onMounted, который сервер не выполняет, поэтому при серверном рендеринге ничего не опрашивается, а первый рендер на клиенте совпадает с серверным. Строку import для VisualLinker и vVlBlock можно убрать.
Как это работает
- Одна функция от данных к соединениям.
describe()возвращает полный список для текущих данных: концы,styleдля статуса и подпись с задержкой. Передача нового списка вsetConnectionsперерисовывает диаграмму. - Статус как стиль.
ok— зелёная линия с быстрыми точками,slow— янтарная с медленными штрихами,down— красная пунктирная без анимации и приглушённая черезopacity. Скорость анимации сообщает состояние без чтения подписи. - Во Vue вызовы не нужны.
computedотслеживаетhealth, поэтому присвоить новый статус — это и есть всё обновление. - Тема.
darkThemeзадаёт цвета портов и подписей для тёмной страницы. Цвета линий берутся из статуса. - Подсветка.
interaction: { highlight: true }включает её, аhighlight: { width: 3 }утолщает линии сервиса, пока над ним курсор.
Варианты
- Настоящие данные. Замените таймер на WebSocket или опрос, которые записывают новые
statusиlatencyв ту же структуру. - Скорость от нагрузки. Вычисляйте
animated.speedпо пропускной способности, чтобы более загруженная связь заметно быстрее текла. - Пороги. Выводите статус из задержки, например
slowвыше 200 мс, вместо того чтобы получать его готовым.