feat(network-map): enhance node representation and edge styling with new color schemes and layout adjustments

This commit is contained in:
2026-02-24 22:54:41 +07:00
parent 80de7160b9
commit dcf1853efb
+118 -66
View File
@@ -4,13 +4,17 @@ import { IconRefresh, IconMaximize, IconMinimize, IconClock } from '@tabler/icon
const FLAG_CDN = 'https://flagcdn.com';
const UNIFI_BG = '#0f172a';
const UNIFI_BG_DEEP = '#0b1220';
const UNIFI_CARD_BG = '#1e293b';
const UNIFI_CARD_BORDER = '#334155';
const UNIFI_GREEN = '#22c55e';
const UNIFI_GREEN_SOFT = '#34d399';
const UNIFI_BLUE_SOFT = '#38bdf8';
const UNIFI_WARNING = '#f59e0b';
const UNIFI_TEXT = '#f1f5f9';
const UNIFI_TEXT_MUTED = '#94a3b8';
const NODE_WIDTH = 88;
const NODE_HEIGHT = 52;
const NODE_WIDTH = 124;
const NODE_HEIGHT = 64;
const STORAGE_KEY = 'network-map-unifi-positions';
function computeInitialLayout(servers, size) {
@@ -86,6 +90,21 @@ function speedKey(key1, key2) {
return [String(key1), String(key2)].sort().join(':');
}
function getNodeTypeMeta(type) {
const t = String(type || '').toLowerCase();
if (t === 'home') return { label: 'HOME', color: '#60a5fa' };
if (t === 'jumphost') return { label: 'JUMP', color: '#34d399' };
if (t === 'exit') return { label: 'EXIT', color: '#f59e0b' };
return { label: 'NODE', color: '#94a3b8' };
}
function getTunnelColor(tunnelType) {
const t = String(tunnelType || '').toLowerCase();
if (t.includes('wireguard') || t === 'wg') return UNIFI_BLUE_SOFT;
if (t.includes('ipsec')) return UNIFI_GREEN;
return UNIFI_GREEN_SOFT;
}
export default function NetworkMapUnifi({
servers = [],
connections = [],
@@ -231,6 +250,8 @@ export default function NetworkMapUnifi({
speed && (speed.tcpDownloadBps != null || speed.tcpUploadBps != null)
? formatSpeedShort(speed.tcpDownloadBps, speed.tcpUploadBps)
: null;
const edgeLabel =
pingLabel && speedLabel ? `${pingLabel}${speedLabel}` : (pingLabel || speedLabel);
return {
edgeKey: `${from}-${to}-${c.tunnelType || ''}`,
from,
@@ -239,8 +260,10 @@ export default function NetworkMapUnifi({
p2,
pingLabel,
speedLabel,
edgeLabel,
pingStale,
speedStale,
stale: pingStale || speedStale,
speed,
tunnelType: c.tunnelType,
raw: c,
@@ -334,17 +357,34 @@ export default function NetworkMapUnifi({
preserveAspectRatio="xMidYMid meet"
style={{ display: 'block' }}
>
{/* Рёбра: зелёные кривые (стиль UNIFI — проводные соединения) */}
<defs>
<linearGradient id="unifiBg" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stopColor={UNIFI_BG} />
<stop offset="100%" stopColor={UNIFI_BG_DEEP} />
</linearGradient>
<filter id="edgeGlow" x="-50%" y="-50%" width="200%" height="200%">
<feGaussianBlur stdDeviation="2.4" result="blur" />
<feMerge>
<feMergeNode in="blur" />
<feMergeNode in="SourceGraphic" />
</feMerge>
</filter>
<filter id="nodeShadow" x="-40%" y="-40%" width="180%" height="180%">
<feDropShadow dx="0" dy="1" stdDeviation="2" floodColor="#020617" floodOpacity="0.55" />
</filter>
</defs>
<rect x="0" y="0" width={size.w} height={size.h} fill="url(#unifiBg)" />
{/* Рёбра: сглаженные линии с цветовой дифференциацией туннеля */}
<g>
{(() => {
const used = [];
const LABEL_W = 100;
const LABEL_H = 18;
const LABEL_GAP = 6;
const LABEL_W = 118;
const LABEL_H = 22;
return edgesWithPing.map((e, idx) => {
const dimFactor =
hoveredEdgeKey && hoveredEdgeKey !== e.edgeKey ? 0.25 : 1;
const strokeWidth = hoveredEdgeKey === e.edgeKey ? 3.2 : 2.5;
const strokeWidth = hoveredEdgeKey === e.edgeKey ? 3 : 2;
const edgeColor = getTunnelColor(e.tunnelType);
return (
<g
key={e.edgeKey || `${e.from}-${e.to}-${idx}`}
@@ -358,18 +398,14 @@ export default function NetworkMapUnifi({
<path
d={pathD(e)}
fill="none"
stroke={UNIFI_GREEN}
stroke={edgeColor}
strokeWidth={strokeWidth}
strokeLinecap="round"
opacity={dimFactor}
filter="url(#edgeGlow)"
/>
{(() => {
const labels = [
e.pingLabel ? { text: e.pingLabel, stale: e.pingStale } : null,
e.speedLabel ? { text: e.speedLabel, stale: e.speedStale } : null,
].filter(Boolean);
if (labels.length === 0) return null;
const totalH = labels.length * LABEL_H + (labels.length - 1) * LABEL_GAP;
if (!e.edgeLabel) return null;
const minDist = 70;
let level = 0;
let x; let y;
@@ -385,54 +421,47 @@ export default function NetworkMapUnifi({
}
used.push({ x, y });
const pad = 8;
const staleColor = '#f59e0b';
const pad = 10;
const isStale = e.stale === true;
const staleColor = UNIFI_WARNING;
const textWidth = e.edgeLabel.length * 6.2;
const iconWidth = isStale ? 14 : 0;
const w = Math.max(LABEL_W, Math.min(textWidth + pad * 2 + iconWidth, 210));
const h = LABEL_H;
const textX = x - (iconWidth / 2);
return (
<g>
{labels.map((label, i) => {
const yy = y + i * (LABEL_H + LABEL_GAP) + LABEL_H / 2;
const isStale = label.stale === true;
const textWidth = label.text.length * 6;
const iconWidth = isStale ? 14 : 0;
const w = Math.max(LABEL_W, Math.min(textWidth + pad * 2 + iconWidth, 180));
const h = LABEL_H + 4;
const textX = x - (iconWidth / 2);
return (
<g key={i}>
<rect
x={x - w / 2}
y={yy - h / 2}
width={w}
height={h}
rx={4}
fill="rgba(15,23,42,0.92)"
stroke={isStale ? staleColor : UNIFI_GREEN}
strokeWidth={1}
strokeDasharray={isStale ? '2,2' : 'none'}
/>
{isStale && (
<circle
cx={x - w / 2 + 10}
cy={yy}
r={5}
fill={staleColor}
opacity={0.9}
/>
)}
<text
x={textX}
y={yy}
textAnchor="middle"
dominantBaseline="middle"
fill={isStale ? staleColor : UNIFI_GREEN}
fontSize={i === 0 ? 12 : 11}
fontWeight={600}
>
{label.text}
</text>
</g>
);
})}
<rect
x={x - w / 2}
y={y - h / 2}
width={w}
height={h}
rx={7}
fill="rgba(2,6,23,0.9)"
stroke={isStale ? staleColor : edgeColor}
strokeWidth={1}
strokeDasharray={isStale ? '2,2' : 'none'}
/>
{isStale && (
<circle
cx={x - w / 2 + 10}
cy={y}
r={4.4}
fill={staleColor}
opacity={0.95}
/>
)}
<text
x={textX}
y={y}
textAnchor="middle"
dominantBaseline="middle"
fill={isStale ? staleColor : edgeColor}
fontSize={11}
fontWeight={600}
>
{e.edgeLabel}
</text>
</g>
);
})()}
@@ -447,6 +476,7 @@ export default function NetworkMapUnifi({
const pos = positions[String(s.ip)];
if (!pos) return null;
const label = s.dns?.split('.')[0] || s.ip || '?';
const typeMeta = getNodeTypeMeta(s.type);
const isDragging = drag?.nodeId === s.ip;
return (
<g
@@ -463,6 +493,7 @@ export default function NetworkMapUnifi({
fill={UNIFI_CARD_BG}
stroke={UNIFI_CARD_BORDER}
strokeWidth={1}
filter="url(#nodeShadow)"
/>
<g transform={`translate(${NODE_WIDTH / 2}, ${NODE_HEIGHT / 2})`}>
{/* Флаг страны в левом верхнем углу карточки (картинка, т.к. эмодзи в SVG часто отображаются как буквы) */}
@@ -488,29 +519,50 @@ export default function NetworkMapUnifi({
})()}
{/* Иконка сервера в стиле UNIFI: прямоугольник с «портами» */}
<g transform="translate(-10, -16)">
<rect x={2} y={0} width={16} height={12} rx={2} fill="none" stroke={UNIFI_GREEN} strokeWidth={1.5} />
<line x1={5} y1={4} x2={15} y2={4} stroke={UNIFI_GREEN} strokeWidth={1} opacity={0.8} />
<line x1={5} y1={8} x2={15} y2={8} stroke={UNIFI_GREEN} strokeWidth={1} opacity={0.8} />
<rect x={2} y={0} width={16} height={12} rx={2} fill="none" stroke={typeMeta.color} strokeWidth={1.5} />
<line x1={5} y1={4} x2={15} y2={4} stroke={typeMeta.color} strokeWidth={1} opacity={0.8} />
<line x1={5} y1={8} x2={15} y2={8} stroke={typeMeta.color} strokeWidth={1} opacity={0.8} />
</g>
<text
x={0}
y={8}
textAnchor="middle"
fill={UNIFI_TEXT}
fontSize={12}
fontSize={11.5}
fontWeight={600}
>
{label.length > 12 ? label.slice(0, 11) + '…' : label}
{label.length > 14 ? `${label.slice(0, 13)}` : label}
</text>
<text
x={0}
y={22}
textAnchor="middle"
fill={UNIFI_TEXT_MUTED}
fontSize={10}
fontSize={9.5}
>
{s.ip}
</text>
<rect
x={NODE_WIDTH / 2 - 42}
y={-NODE_HEIGHT / 2 + 5}
width={36}
height={14}
rx={7}
fill="rgba(15,23,42,0.9)"
stroke={typeMeta.color}
strokeWidth={1}
/>
<text
x={NODE_WIDTH / 2 - 24}
y={-NODE_HEIGHT / 2 + 15}
textAnchor="middle"
fill={typeMeta.color}
fontSize={8}
fontWeight={700}
letterSpacing={0.5}
>
{typeMeta.label}
</text>
</g>
</g>
);