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
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 1m56s
This commit is contained in:
@@ -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} />
|
||||||
|
|||||||
Reference in New Issue
Block a user