Netpulse_SasS/web/src/components/TrafficEdge.tsx
byrsapty edc6209c98 Вигляд мапи, редаговані вбудовані шаблони, мобільні картки
Мапа: підпис ребра більше не друкує «? → ?» і «— з 10.0 Гбіт/с» —
невідоме краще не обіцяти. Вузол став карткою з градієнтом, кольоровою
смугою стану зліва й крапкою стану праворуч; смуга несе стан навіть
тоді, коли рамку перефарбували під майданчик. Пульсація лише за
обривом, порти — на наведення. Фон: радіальна підсвітка й дві сітки.

Вбудовані шаблони редагуються: правка створює власну копію тенанта з
тим самим ключем, вона замінює оригінал у списку, оригінал лишається
недоторканим для решти. Плюс окрема кнопка «Копія» з суфіксом у ключі.
Ключі елементів і графіків тепер виводяться на сервері — прямий виклик
API без key давав 500 від core.slug.

Вибір шаблонів у формі хоста став картками з описом і лічильником
метрик; обрані спливають угору.

Мобільна сітка: min-w-0 на картці. Елемент грід-сітки типово має
min-width:auto й не стискається нижче ширини вмісту — картка на 410 px
обрізалась у 375 px екрані.

Межа помилок навколо полотна: цикл рендеру в React Flow одного разу вже
поклав увесь застосунок, тепер падатиме лише мапа.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-24 23:53:58 +03:00

140 lines
4.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import {
BaseEdge,
EdgeLabelRenderer,
getBezierPath,
type Edge,
type EdgeProps,
} from '@xyflow/react'
import type { DeviceStatus } from '../types'
export type TrafficEdgeData = {
label?: string
sourcePort?: string
targetPort?: string
linkStatus?: DeviceStatus
utilPct?: number
capacityBps?: number
warnPct: number
critPct: number
animated: boolean
showMetrics: boolean
}
export type TrafficEdgeType = Edge<TrafficEdgeData, 'traffic'>
/**
* Швидкість анімації від завантаження каналу.
*
* Обернена залежність, а не пряма: точка має бігти швидше, коли трафіку
* більше. Стеля 0.6 с — далі рух зливається в смугу й перестає читатись;
* підлога 6 с — щоб ледь завантажений канал не виглядав мертвим.
*/
function animationDuration(utilPct: number): number {
const u = Math.max(0, Math.min(100, utilPct))
return 6 - (u / 100) * 5.4
}
function strokeColor(d: TrafficEdgeData): string {
if (d.linkStatus === 'down') return '#ef4444'
if (d.linkStatus === 'unknown' || d.linkStatus === undefined) return '#475569'
const u = d.utilPct ?? 0
if (u >= d.critPct) return '#ef4444'
if (u >= d.warnPct) return '#f59e0b'
return '#10b981'
}
function formatBps(bps: number): string {
if (bps >= 1e9) return `${(bps / 1e9).toFixed(1)} Гбіт/с`
if (bps >= 1e6) return `${(bps / 1e6).toFixed(0)} Мбіт/с`
if (bps >= 1e3) return `${(bps / 1e3).toFixed(0)} кбіт/с`
return `${bps.toFixed(0)} біт/с`
}
export function TrafficEdge({
id,
sourceX,
sourceY,
targetX,
targetY,
sourcePosition,
targetPosition,
data,
selected,
}: EdgeProps<TrafficEdgeType>) {
const d = data as TrafficEdgeData
const [path, labelX, labelY] = getBezierPath({
sourceX,
sourceY,
targetX,
targetY,
sourcePosition,
targetPosition,
})
const color = strokeColor(d)
const util = d.utilPct ?? 0
// Анімуємо лише коли трафік справді є: рухома точка на порожньому
// каналі означала б рух, якого немає.
const moving = d.animated && d.linkStatus !== 'down' && util > 0
// Невідомий порт не друкується взагалі.
//
// «? → ?» на кожній лінії виглядає як зламаний інтерфейс, хоча
// насправді означає лише те, що автовиявлення ще не зіставило порти.
// Порожнеча чесніша: вона нічого не обіцяє.
const ports =
d.sourcePort && d.targetPort
? `${d.sourcePort}${d.targetPort}`
: (d.sourcePort ?? d.targetPort ?? '')
// Так само зі швидкістю: «— з 10.0 Гбіт/с» читається як помилка.
// Показуємо рядок лише тоді, коли трафік справді виміряний.
const rate =
d.capacityBps !== undefined && util > 0
? `${util < 0.01 ? '<0.01' : util.toFixed(2)}% з ${formatBps(d.capacityBps)}`
: ''
const label = d.showMetrics && (ports || rate)
return (
<>
<BaseEdge
id={id}
path={path}
style={{
stroke: color,
strokeWidth: selected ? 3.5 : 2.2,
opacity: d.linkStatus === 'down' ? 0.9 : 0.85,
strokeDasharray: d.linkStatus === 'down' ? '6 4' : undefined,
}}
/>
{moving && (
<circle r="3.5" fill={color}>
<animateMotion dur={`${animationDuration(util)}s`} repeatCount="indefinite">
<mpath href={`#${id}`} />
</animateMotion>
</circle>
)}
{label && (
<EdgeLabelRenderer>
<div
className="pointer-events-none absolute rounded-md border border-slate-700/60
bg-slate-900/90 px-2 py-1 text-[10px] leading-tight text-slate-300
shadow-lg backdrop-blur-sm"
style={{ transform: `translate(-50%, -50%) translate(${labelX}px, ${labelY}px)` }}
>
{ports && <div className="whitespace-nowrap font-medium">{ports}</div>}
{rate && (
<div className="whitespace-nowrap tabular-nums" style={{ color }}>
{rate}
</div>
)}
</div>
</EdgeLabelRenderer>
)}
</>
)
}