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 FLAG_CDN = 'https://flagcdn.com';
|
||||||
|
|
||||||
const UNIFI_BG = '#0f172a';
|
const UNIFI_BG = '#0f172a';
|
||||||
|
const UNIFI_BG_DEEP = '#0b1220';
|
||||||
const UNIFI_CARD_BG = '#1e293b';
|
const UNIFI_CARD_BG = '#1e293b';
|
||||||
const UNIFI_CARD_BORDER = '#334155';
|
const UNIFI_CARD_BORDER = '#334155';
|
||||||
const UNIFI_GREEN = '#22c55e';
|
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 = '#f1f5f9';
|
||||||
const UNIFI_TEXT_MUTED = '#94a3b8';
|
const UNIFI_TEXT_MUTED = '#94a3b8';
|
||||||
const NODE_WIDTH = 88;
|
const NODE_WIDTH = 124;
|
||||||
const NODE_HEIGHT = 52;
|
const NODE_HEIGHT = 64;
|
||||||
const STORAGE_KEY = 'network-map-unifi-positions';
|
const STORAGE_KEY = 'network-map-unifi-positions';
|
||||||
|
|
||||||
function computeInitialLayout(servers, size) {
|
function computeInitialLayout(servers, size) {
|
||||||
@@ -86,6 +90,21 @@ function speedKey(key1, key2) {
|
|||||||
return [String(key1), String(key2)].sort().join(':');
|
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({
|
export default function NetworkMapUnifi({
|
||||||
servers = [],
|
servers = [],
|
||||||
connections = [],
|
connections = [],
|
||||||
@@ -231,6 +250,8 @@ export default function NetworkMapUnifi({
|
|||||||
speed && (speed.tcpDownloadBps != null || speed.tcpUploadBps != null)
|
speed && (speed.tcpDownloadBps != null || speed.tcpUploadBps != null)
|
||||||
? formatSpeedShort(speed.tcpDownloadBps, speed.tcpUploadBps)
|
? formatSpeedShort(speed.tcpDownloadBps, speed.tcpUploadBps)
|
||||||
: null;
|
: null;
|
||||||
|
const edgeLabel =
|
||||||
|
pingLabel && speedLabel ? `${pingLabel} • ${speedLabel}` : (pingLabel || speedLabel);
|
||||||
return {
|
return {
|
||||||
edgeKey: `${from}-${to}-${c.tunnelType || ''}`,
|
edgeKey: `${from}-${to}-${c.tunnelType || ''}`,
|
||||||
from,
|
from,
|
||||||
@@ -239,8 +260,10 @@ export default function NetworkMapUnifi({
|
|||||||
p2,
|
p2,
|
||||||
pingLabel,
|
pingLabel,
|
||||||
speedLabel,
|
speedLabel,
|
||||||
|
edgeLabel,
|
||||||
pingStale,
|
pingStale,
|
||||||
speedStale,
|
speedStale,
|
||||||
|
stale: pingStale || speedStale,
|
||||||
speed,
|
speed,
|
||||||
tunnelType: c.tunnelType,
|
tunnelType: c.tunnelType,
|
||||||
raw: c,
|
raw: c,
|
||||||
@@ -334,17 +357,34 @@ export default function NetworkMapUnifi({
|
|||||||
preserveAspectRatio="xMidYMid meet"
|
preserveAspectRatio="xMidYMid meet"
|
||||||
style={{ display: 'block' }}
|
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>
|
<g>
|
||||||
{(() => {
|
{(() => {
|
||||||
const used = [];
|
const used = [];
|
||||||
const LABEL_W = 100;
|
const LABEL_W = 118;
|
||||||
const LABEL_H = 18;
|
const LABEL_H = 22;
|
||||||
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;
|
||||||
const strokeWidth = hoveredEdgeKey === e.edgeKey ? 3.2 : 2.5;
|
const strokeWidth = hoveredEdgeKey === e.edgeKey ? 3 : 2;
|
||||||
|
const edgeColor = getTunnelColor(e.tunnelType);
|
||||||
return (
|
return (
|
||||||
<g
|
<g
|
||||||
key={e.edgeKey || `${e.from}-${e.to}-${idx}`}
|
key={e.edgeKey || `${e.from}-${e.to}-${idx}`}
|
||||||
@@ -358,18 +398,14 @@ export default function NetworkMapUnifi({
|
|||||||
<path
|
<path
|
||||||
d={pathD(e)}
|
d={pathD(e)}
|
||||||
fill="none"
|
fill="none"
|
||||||
stroke={UNIFI_GREEN}
|
stroke={edgeColor}
|
||||||
strokeWidth={strokeWidth}
|
strokeWidth={strokeWidth}
|
||||||
strokeLinecap="round"
|
strokeLinecap="round"
|
||||||
opacity={dimFactor}
|
opacity={dimFactor}
|
||||||
|
filter="url(#edgeGlow)"
|
||||||
/>
|
/>
|
||||||
{(() => {
|
{(() => {
|
||||||
const labels = [
|
if (!e.edgeLabel) return null;
|
||||||
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;
|
|
||||||
const minDist = 70;
|
const minDist = 70;
|
||||||
let level = 0;
|
let level = 0;
|
||||||
let x; let y;
|
let x; let y;
|
||||||
@@ -385,54 +421,47 @@ export default function NetworkMapUnifi({
|
|||||||
}
|
}
|
||||||
used.push({ x, y });
|
used.push({ x, y });
|
||||||
|
|
||||||
const pad = 8;
|
const pad = 10;
|
||||||
const staleColor = '#f59e0b';
|
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 (
|
return (
|
||||||
<g>
|
<g>
|
||||||
{labels.map((label, i) => {
|
<rect
|
||||||
const yy = y + i * (LABEL_H + LABEL_GAP) + LABEL_H / 2;
|
x={x - w / 2}
|
||||||
const isStale = label.stale === true;
|
y={y - h / 2}
|
||||||
const textWidth = label.text.length * 6;
|
width={w}
|
||||||
const iconWidth = isStale ? 14 : 0;
|
height={h}
|
||||||
const w = Math.max(LABEL_W, Math.min(textWidth + pad * 2 + iconWidth, 180));
|
rx={7}
|
||||||
const h = LABEL_H + 4;
|
fill="rgba(2,6,23,0.9)"
|
||||||
const textX = x - (iconWidth / 2);
|
stroke={isStale ? staleColor : edgeColor}
|
||||||
return (
|
strokeWidth={1}
|
||||||
<g key={i}>
|
strokeDasharray={isStale ? '2,2' : 'none'}
|
||||||
<rect
|
/>
|
||||||
x={x - w / 2}
|
{isStale && (
|
||||||
y={yy - h / 2}
|
<circle
|
||||||
width={w}
|
cx={x - w / 2 + 10}
|
||||||
height={h}
|
cy={y}
|
||||||
rx={4}
|
r={4.4}
|
||||||
fill="rgba(15,23,42,0.92)"
|
fill={staleColor}
|
||||||
stroke={isStale ? staleColor : UNIFI_GREEN}
|
opacity={0.95}
|
||||||
strokeWidth={1}
|
/>
|
||||||
strokeDasharray={isStale ? '2,2' : 'none'}
|
)}
|
||||||
/>
|
<text
|
||||||
{isStale && (
|
x={textX}
|
||||||
<circle
|
y={y}
|
||||||
cx={x - w / 2 + 10}
|
textAnchor="middle"
|
||||||
cy={yy}
|
dominantBaseline="middle"
|
||||||
r={5}
|
fill={isStale ? staleColor : edgeColor}
|
||||||
fill={staleColor}
|
fontSize={11}
|
||||||
opacity={0.9}
|
fontWeight={600}
|
||||||
/>
|
>
|
||||||
)}
|
{e.edgeLabel}
|
||||||
<text
|
</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>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</g>
|
</g>
|
||||||
);
|
);
|
||||||
})()}
|
})()}
|
||||||
@@ -447,6 +476,7 @@ export default function NetworkMapUnifi({
|
|||||||
const pos = positions[String(s.ip)];
|
const pos = positions[String(s.ip)];
|
||||||
if (!pos) return null;
|
if (!pos) return null;
|
||||||
const label = s.dns?.split('.')[0] || s.ip || '?';
|
const label = s.dns?.split('.')[0] || s.ip || '?';
|
||||||
|
const typeMeta = getNodeTypeMeta(s.type);
|
||||||
const isDragging = drag?.nodeId === s.ip;
|
const isDragging = drag?.nodeId === s.ip;
|
||||||
return (
|
return (
|
||||||
<g
|
<g
|
||||||
@@ -463,6 +493,7 @@ export default function NetworkMapUnifi({
|
|||||||
fill={UNIFI_CARD_BG}
|
fill={UNIFI_CARD_BG}
|
||||||
stroke={UNIFI_CARD_BORDER}
|
stroke={UNIFI_CARD_BORDER}
|
||||||
strokeWidth={1}
|
strokeWidth={1}
|
||||||
|
filter="url(#nodeShadow)"
|
||||||
/>
|
/>
|
||||||
<g transform={`translate(${NODE_WIDTH / 2}, ${NODE_HEIGHT / 2})`}>
|
<g transform={`translate(${NODE_WIDTH / 2}, ${NODE_HEIGHT / 2})`}>
|
||||||
{/* Флаг страны в левом верхнем углу карточки (картинка, т.к. эмодзи в SVG часто отображаются как буквы) */}
|
{/* Флаг страны в левом верхнем углу карточки (картинка, т.к. эмодзи в SVG часто отображаются как буквы) */}
|
||||||
@@ -488,29 +519,50 @@ export default function NetworkMapUnifi({
|
|||||||
})()}
|
})()}
|
||||||
{/* Иконка сервера в стиле 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={typeMeta.color} strokeWidth={1.5} />
|
||||||
<line x1={5} y1={4} x2={15} y2={4} stroke={UNIFI_GREEN} strokeWidth={1} opacity={0.8} />
|
<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={UNIFI_GREEN} strokeWidth={1} opacity={0.8} />
|
<line x1={5} y1={8} x2={15} y2={8} stroke={typeMeta.color} strokeWidth={1} opacity={0.8} />
|
||||||
</g>
|
</g>
|
||||||
<text
|
<text
|
||||||
x={0}
|
x={0}
|
||||||
y={8}
|
y={8}
|
||||||
textAnchor="middle"
|
textAnchor="middle"
|
||||||
fill={UNIFI_TEXT}
|
fill={UNIFI_TEXT}
|
||||||
fontSize={12}
|
fontSize={11.5}
|
||||||
fontWeight={600}
|
fontWeight={600}
|
||||||
>
|
>
|
||||||
{label.length > 12 ? label.slice(0, 11) + '…' : label}
|
{label.length > 14 ? `${label.slice(0, 13)}…` : label}
|
||||||
</text>
|
</text>
|
||||||
<text
|
<text
|
||||||
x={0}
|
x={0}
|
||||||
y={22}
|
y={22}
|
||||||
textAnchor="middle"
|
textAnchor="middle"
|
||||||
fill={UNIFI_TEXT_MUTED}
|
fill={UNIFI_TEXT_MUTED}
|
||||||
fontSize={10}
|
fontSize={9.5}
|
||||||
>
|
>
|
||||||
{s.ip}
|
{s.ip}
|
||||||
</text>
|
</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>
|
||||||
</g>
|
</g>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user