feat(network-map): enhance node representation and edge styling with new color schemes and layout adjustments
This commit is contained in:
@@ -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>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user