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:
@@ -64,6 +64,8 @@ export default function NetworkMapUnifi({ servers = [], connections = [], pingMa
|
|||||||
const [positions, setPositions] = useState({});
|
const [positions, setPositions] = useState({});
|
||||||
const [drag, setDrag] = useState(null);
|
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(() => {
|
useEffect(() => {
|
||||||
const el = containerRef.current;
|
const el = containerRef.current;
|
||||||
@@ -183,12 +185,14 @@ export default function NetworkMapUnifi({ servers = [], connections = [], pingMa
|
|||||||
if (typeof rev === 'number') parts.push(`← ${rev} ms`);
|
if (typeof rev === 'number') parts.push(`← ${rev} ms`);
|
||||||
const pingLabel = parts.length ? parts.join(' ') : null;
|
const pingLabel = parts.length ? parts.join(' ') : null;
|
||||||
return {
|
return {
|
||||||
|
edgeKey: `${from}-${to}-${c.tunnelType || ''}`,
|
||||||
from,
|
from,
|
||||||
to,
|
to,
|
||||||
p1,
|
p1,
|
||||||
p2,
|
p2,
|
||||||
pingLabel,
|
pingLabel,
|
||||||
tunnelType: c.tunnelType,
|
tunnelType: c.tunnelType,
|
||||||
|
raw: c,
|
||||||
};
|
};
|
||||||
}).filter((e) => e.p1 && e.p2);
|
}).filter((e) => e.p1 && e.p2);
|
||||||
}, [connections, positions, pingMap]);
|
}, [connections, positions, pingMap]);
|
||||||
@@ -281,14 +285,27 @@ export default function NetworkMapUnifi({ servers = [], connections = [], pingMa
|
|||||||
const LABEL_W = 64;
|
const LABEL_W = 64;
|
||||||
const LABEL_H = 20;
|
const LABEL_H = 20;
|
||||||
const LABEL_GAP = 4;
|
const LABEL_GAP = 4;
|
||||||
return edgesWithPing.map((e, idx) => (
|
return edgesWithPing.map((e, idx) => {
|
||||||
<g key={`${e.from}-${e.to}-${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
|
<path
|
||||||
d={pathD(e)}
|
d={pathD(e)}
|
||||||
fill="none"
|
fill="none"
|
||||||
stroke={UNIFI_GREEN}
|
stroke={UNIFI_GREEN}
|
||||||
strokeWidth={2.5}
|
strokeWidth={strokeWidth}
|
||||||
strokeLinecap="round"
|
strokeLinecap="round"
|
||||||
|
opacity={dimFactor}
|
||||||
/>
|
/>
|
||||||
{e.pingLabel && (() => {
|
{e.pingLabel && (() => {
|
||||||
const base = labelPoint(e);
|
const base = labelPoint(e);
|
||||||
@@ -332,7 +349,8 @@ export default function NetworkMapUnifi({ servers = [], connections = [], pingMa
|
|||||||
);
|
);
|
||||||
})()}
|
})()}
|
||||||
</g>
|
</g>
|
||||||
));
|
);
|
||||||
|
});
|
||||||
})()}
|
})()}
|
||||||
</g>
|
</g>
|
||||||
|
|
||||||
@@ -389,6 +407,107 @@ export default function NetworkMapUnifi({ servers = [], connections = [], pingMa
|
|||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</svg>
|
</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
|
<div
|
||||||
style={{
|
style={{
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
|
|||||||
Reference in New Issue
Block a user