feat(NetworkMapUnifi): implement edge hover and selection features with detailed connection modal for enhanced user interaction
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 1m58s
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 1m58s
This commit is contained in:
@@ -63,7 +63,9 @@ export default function NetworkMapUnifi({ servers = [], connections = [], pingMa
|
||||
const [size, setSize] = useState({ w: 800, h: 520 });
|
||||
const [positions, setPositions] = useState({});
|
||||
const [drag, setDrag] = useState(null);
|
||||
const [isFullscreen, setIsFullscreen] = useState(false);
|
||||
const [isFullscreen, setIsFullscreen] = useState(false);
|
||||
const [hoveredEdgeKey, setHoveredEdgeKey] = useState(null);
|
||||
const [selectedEdge, setSelectedEdge] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
const el = containerRef.current;
|
||||
@@ -183,12 +185,14 @@ export default function NetworkMapUnifi({ servers = [], connections = [], pingMa
|
||||
if (typeof rev === 'number') parts.push(`← ${rev} ms`);
|
||||
const pingLabel = parts.length ? parts.join(' ') : null;
|
||||
return {
|
||||
edgeKey: `${from}-${to}-${c.tunnelType || ''}`,
|
||||
from,
|
||||
to,
|
||||
p1,
|
||||
p2,
|
||||
pingLabel,
|
||||
tunnelType: c.tunnelType,
|
||||
raw: c,
|
||||
};
|
||||
}).filter((e) => e.p1 && e.p2);
|
||||
}, [connections, positions, pingMap]);
|
||||
@@ -281,14 +285,27 @@ export default function NetworkMapUnifi({ servers = [], connections = [], pingMa
|
||||
const LABEL_W = 64;
|
||||
const LABEL_H = 20;
|
||||
const LABEL_GAP = 4;
|
||||
return edgesWithPing.map((e, idx) => (
|
||||
<g key={`${e.from}-${e.to}-${idx}`}>
|
||||
return edgesWithPing.map((e, idx) => {
|
||||
const dimFactor =
|
||||
hoveredEdgeKey && hoveredEdgeKey !== e.edgeKey ? 0.25 : 1;
|
||||
const strokeWidth = hoveredEdgeKey === e.edgeKey ? 3.2 : 2.5;
|
||||
return (
|
||||
<g
|
||||
key={e.edgeKey || `${e.from}-${e.to}-${idx}`}
|
||||
onMouseEnter={() => setHoveredEdgeKey(e.edgeKey)}
|
||||
onMouseLeave={() =>
|
||||
setHoveredEdgeKey((prev) => (prev === e.edgeKey ? null : prev))
|
||||
}
|
||||
onClick={() => setSelectedEdge(e)}
|
||||
style={{ cursor: 'pointer' }}
|
||||
>
|
||||
<path
|
||||
d={pathD(e)}
|
||||
fill="none"
|
||||
stroke={UNIFI_GREEN}
|
||||
strokeWidth={2.5}
|
||||
strokeWidth={strokeWidth}
|
||||
strokeLinecap="round"
|
||||
opacity={dimFactor}
|
||||
/>
|
||||
{e.pingLabel && (() => {
|
||||
const base = labelPoint(e);
|
||||
@@ -332,7 +349,8 @@ export default function NetworkMapUnifi({ servers = [], connections = [], pingMa
|
||||
);
|
||||
})()}
|
||||
</g>
|
||||
));
|
||||
);
|
||||
});
|
||||
})()}
|
||||
</g>
|
||||
|
||||
@@ -389,6 +407,107 @@ export default function NetworkMapUnifi({ servers = [], connections = [], pingMa
|
||||
);
|
||||
})}
|
||||
</svg>
|
||||
{selectedEdge && (() => {
|
||||
const fromServer = servers.find((s) => String(s.ip) === selectedEdge.from);
|
||||
const toServer = servers.find((s) => String(s.ip) === selectedEdge.to);
|
||||
const fwd = pingMap[`${selectedEdge.from}:${selectedEdge.to}`];
|
||||
const rev = pingMap[`${selectedEdge.to}:${selectedEdge.from}`];
|
||||
return (
|
||||
<div
|
||||
className="network-map-edge-modal-backdrop"
|
||||
style={{
|
||||
position: 'absolute',
|
||||
inset: 0,
|
||||
background: 'rgba(15,23,42,0.55)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
zIndex: 50,
|
||||
}}
|
||||
onClick={() => setSelectedEdge(null)}
|
||||
>
|
||||
<div
|
||||
className="card shadow-lg"
|
||||
style={{
|
||||
minWidth: 360,
|
||||
maxWidth: 520,
|
||||
background: '#0f172a',
|
||||
color: '#e5e7eb',
|
||||
borderRadius: 12,
|
||||
border: `1px solid ${UNIFI_CARD_BORDER}`,
|
||||
}}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<div className="card-header d-flex align-items-center justify-content-between">
|
||||
<div className="fw-semibold">Детали соединения</div>
|
||||
<button
|
||||
type="button"
|
||||
className="btn-close btn-close-white"
|
||||
aria-label="Закрыть"
|
||||
onClick={() => setSelectedEdge(null)}
|
||||
style={{ filter: 'invert(1)' }}
|
||||
/>
|
||||
</div>
|
||||
<div className="card-body" style={{ fontSize: 13 }}>
|
||||
<div className="mb-2">
|
||||
<div className="text-muted text-uppercase mb-1" style={{ fontSize: 11 }}>
|
||||
От
|
||||
</div>
|
||||
<div>
|
||||
<strong>{fromServer?.dns || fromServer?.ip || selectedEdge.from}</strong>{' '}
|
||||
<span className="text-muted">({fromServer?.ip || '—'})</span>{' '}
|
||||
<span className="badge bg-blue-lt text-blue ms-1">
|
||||
{String(fromServer?.type || '').toUpperCase() || 'SERVER'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mb-3">
|
||||
<div className="text-muted text-uppercase mb-1" style={{ fontSize: 11 }}>
|
||||
К
|
||||
</div>
|
||||
<div>
|
||||
<strong>{toServer?.dns || toServer?.ip || selectedEdge.to}</strong>{' '}
|
||||
<span className="text-muted">({toServer?.ip || '—'})</span>{' '}
|
||||
<span className="badge bg-blue-lt text-blue ms-1">
|
||||
{String(toServer?.type || '').toUpperCase() || 'SERVER'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mb-3">
|
||||
<div className="text-muted text-uppercase mb-1" style={{ fontSize: 11 }}>
|
||||
Туннель
|
||||
</div>
|
||||
<div>
|
||||
<span className="badge bg-green-lt text-green me-2">
|
||||
{selectedEdge.tunnelType || 'TUNNEL'}
|
||||
</span>
|
||||
{selectedEdge.raw?.internalFromTo && (
|
||||
<span className="text-muted">
|
||||
{selectedEdge.raw.internalToFrom} ⇄ {selectedEdge.raw.internalFromTo}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="mb-1">
|
||||
<div className="text-muted text-uppercase mb-1" style={{ fontSize: 11 }}>
|
||||
Пинг
|
||||
</div>
|
||||
<div>
|
||||
<div>
|
||||
<span className="text-muted">→ </span>
|
||||
{typeof fwd === 'number' ? `${fwd} ms` : 'нет данных'}
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-muted">← </span>
|
||||
{typeof rev === 'number' ? `${rev} ms` : 'нет данных'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})()}
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
|
||||
Reference in New Issue
Block a user