Skip to content

Карта сервисов в реальном времени ​

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

Что здесь используется:

Чистый JavaScript ​

html
<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>
css
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.

ts
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>.

vue
<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 мс, вместо того чтобы получать его готовым.