feat(NetworkMapUnifi): add short speed formatting and country flag display to network edges
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 1m56s

This commit is contained in:
2026-02-18 13:36:26 +07:00
parent 283ab53b80
commit fdc39060a1
+45 -13
View File
@@ -1,5 +1,6 @@
import { useMemo, useCallback, useState, useRef, useEffect } from 'react'; import { useMemo, useCallback, useState, useRef, useEffect } from 'react';
import { IconRefresh, IconMaximize, IconMinimize } from '@tabler/icons-react'; import { IconRefresh, IconMaximize, IconMinimize } from '@tabler/icons-react';
import { countryToFlag } from './utils/serverUtils.js';
const UNIFI_BG = '#0f172a'; const UNIFI_BG = '#0f172a';
const UNIFI_CARD_BG = '#1e293b'; const UNIFI_CARD_BG = '#1e293b';
@@ -64,6 +65,22 @@ function formatMbps(bps) {
return `${mbps.toFixed(1)} Мбит/с`; return `${mbps.toFixed(1)} Мбит/с`;
} }
/** Короткий формат скорости для подписи на ребре: "↓287 ↑170 Мбит/с" */
function formatSpeedShort(downloadBps, uploadBps) {
const fmt = (v) => {
if (v == null || Number.isNaN(v)) return '—';
const mbps = v / 1_000_000;
if (!Number.isFinite(mbps)) return '—';
return mbps >= 10 ? Math.round(mbps) : mbps.toFixed(1);
};
const down = fmt(downloadBps);
const up = fmt(uploadBps);
if (down === '—' && up === '—') return null;
if (down === '—') return `↑${up} Мбит/с`;
if (up === '—') return `↓${down} Мбит/с`;
return `↓${down} ↑${up} Мбит/с`;
}
function speedKey(key1, key2) { function speedKey(key1, key2) {
return [String(key1), String(key2)].sort().join(':'); return [String(key1), String(key2)].sort().join(':');
} }
@@ -197,7 +214,7 @@ export default function NetworkMapUnifi({ servers = [], connections = [], pingMa
const speed = speedMap[sk]; const speed = speedMap[sk];
const speedLabel = const speedLabel =
speed && (speed.tcpDownloadBps != null || speed.tcpUploadBps != null) speed && (speed.tcpDownloadBps != null || speed.tcpUploadBps != null)
? `↓ ${formatMbps(speed.tcpDownloadBps)} ↑ ${formatMbps(speed.tcpUploadBps)}` ? formatSpeedShort(speed.tcpDownloadBps, speed.tcpUploadBps)
: null; : null;
return { return {
edgeKey: `${from}-${to}-${c.tunnelType || ''}`, edgeKey: `${from}-${to}-${c.tunnelType || ''}`,
@@ -299,9 +316,9 @@ export default function NetworkMapUnifi({ servers = [], connections = [], pingMa
<g> <g>
{(() => { {(() => {
const used = []; const used = [];
const LABEL_W = 64; const LABEL_W = 100;
const LABEL_H = 20; const LABEL_H = 18;
const LABEL_GAP = 4; const LABEL_GAP = 6;
return edgesWithPing.map((e, idx) => { return edgesWithPing.map((e, idx) => {
const dimFactor = const dimFactor =
hoveredEdgeKey && hoveredEdgeKey !== e.edgeKey ? 0.25 : 1; hoveredEdgeKey && hoveredEdgeKey !== e.edgeKey ? 0.25 : 1;
@@ -331,39 +348,40 @@ export default function NetworkMapUnifi({ servers = [], connections = [], pingMa
const labels = [e.pingLabel, e.speedLabel].filter(Boolean); const labels = [e.pingLabel, e.speedLabel].filter(Boolean);
if (labels.length === 0) return null; if (labels.length === 0) return null;
let level = 0; let level = 0;
const totalH = labels.length * LABEL_H + (labels.length - 1) * LABEL_GAP;
while ( while (
level < 10 && level < 10 &&
used.some( used.some(
(p) => (p) =>
Math.abs(p.x - x) < LABEL_W && Math.abs(p.x - x) < LABEL_W &&
Math.abs(p.y - (y + level * (LABEL_H + LABEL_GAP))) < (labels.length * (LABEL_H + LABEL_GAP)) Math.abs(p.y - (y + level * (totalH + 8))) < totalH + 16
) )
) { ) {
level += 1; level += 1;
} }
y = y + level * (LABEL_H + LABEL_GAP); y = y + level * (totalH + 8);
used.push({ x, y }); used.push({ x, y });
const pad = 6; const pad = 8;
return ( return (
<g> <g>
{labels.map((text, i) => { {labels.map((text, i) => {
const yy = y + i * (LABEL_H + LABEL_GAP); const yy = y + i * (LABEL_H + LABEL_GAP) + LABEL_H / 2;
const w = Math.min(text.length * 5.5 + pad * 2, 140); const w = Math.max(LABEL_W, Math.min(text.length * 6 + pad * 2, 160));
const h = 18; const h = LABEL_H + 4;
return ( return (
<g key={i}> <g key={i}>
<rect x={x - w / 2} y={yy - h / 2} width={w} height={h} rx={4} fill="rgba(15,23,42,0.9)" stroke={UNIFI_GREEN} strokeWidth={1} /> <rect x={x - w / 2} y={yy - h / 2} width={w} height={h} rx={4} fill="rgba(15,23,42,0.92)" stroke={UNIFI_GREEN} strokeWidth={1} />
<text <text
x={x} x={x}
y={yy} y={yy}
textAnchor="middle" textAnchor="middle"
dominantBaseline="middle" dominantBaseline="middle"
fill={UNIFI_GREEN} fill={UNIFI_GREEN}
fontSize={i === 0 ? 11 : 10} fontSize={i === 0 ? 12 : 11}
fontWeight={600} fontWeight={600}
> >
{text.length > 24 ? text.slice(0, 22) + '…' : text} {text}
</text> </text>
</g> </g>
); );
@@ -400,6 +418,20 @@ export default function NetworkMapUnifi({ servers = [], connections = [], pingMa
strokeWidth={1} strokeWidth={1}
/> />
<g transform={`translate(${NODE_WIDTH / 2}, ${NODE_HEIGHT / 2})`}> <g transform={`translate(${NODE_WIDTH / 2}, ${NODE_HEIGHT / 2})`}>
{/* Флаг страны в левом верхнем углу карточки */}
{s.country && (
<text
x={-NODE_WIDTH / 2 + 14}
y={-NODE_HEIGHT / 2 + 14}
textAnchor="middle"
dominantBaseline="middle"
fontSize={14}
style={{ pointerEvents: 'none' }}
title={s.country}
>
{countryToFlag(s.country)}
</text>
)}
{/* Иконка сервера в стиле UNIFI: прямоугольник с «портами» */} {/* Иконка сервера в стиле UNIFI: прямоугольник с «портами» */}
<g transform="translate(-10, -16)"> <g transform="translate(-10, -16)">
<rect x={2} y={0} width={16} height={12} rx={2} fill="none" stroke={UNIFI_GREEN} strokeWidth={1.5} /> <rect x={2} y={0} width={16} height={12} rx={2} fill="none" stroke={UNIFI_GREEN} strokeWidth={1.5} />