File transfer
This commit is contained in:
103
resources/scripts/addons/advanced-admin/network/FlowGraph.tsx
Normal file
103
resources/scripts/addons/advanced-admin/network/FlowGraph.tsx
Normal file
@@ -0,0 +1,103 @@
|
||||
/**
|
||||
* Flow Graph — container-to-container traffic visualization
|
||||
* Uses a simple SVG force-directed layout (D3-free for bundle size)
|
||||
* Ball Studios <https://git.balls.studio>
|
||||
*/
|
||||
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
|
||||
interface Flow {
|
||||
src_server_id: number | null;
|
||||
dst_server_id: number | null;
|
||||
bytes: number;
|
||||
packets: number;
|
||||
}
|
||||
|
||||
interface Props { flows: Flow[]; }
|
||||
|
||||
export default function FlowGraph({ flows }: Props) {
|
||||
const svgRef = useRef<SVGSVGElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!svgRef.current || flows.length === 0) return;
|
||||
|
||||
const svg = svgRef.current;
|
||||
const W = svg.clientWidth || 600;
|
||||
const H = 280;
|
||||
svg.setAttribute('viewBox', `0 0 ${W} ${H}`);
|
||||
|
||||
// Collect unique server IDs + "Internet" (null src)
|
||||
const serverIds = new Set<string>();
|
||||
flows.forEach(f => {
|
||||
serverIds.add(f.src_server_id ? `s${f.src_server_id}` : 'internet');
|
||||
serverIds.add(f.dst_server_id ? `s${f.dst_server_id}` : 'internet');
|
||||
});
|
||||
const nodes = Array.from(serverIds);
|
||||
|
||||
// Layout: evenly spaced horizontally
|
||||
const positions: Record<string, { x: number; y: number }> = {};
|
||||
nodes.forEach((id, i) => {
|
||||
positions[id] = { x: (W / (nodes.length + 1)) * (i + 1), y: H / 2 };
|
||||
});
|
||||
|
||||
const maxBytes = Math.max(...flows.map(f => f.bytes), 1);
|
||||
|
||||
// Clear
|
||||
while (svg.firstChild) svg.removeChild(svg.firstChild);
|
||||
|
||||
// Draw edges
|
||||
flows.forEach(f => {
|
||||
const srcId = f.src_server_id ? `s${f.src_server_id}` : 'internet';
|
||||
const dstId = f.dst_server_id ? `s${f.dst_server_id}` : 'internet';
|
||||
if (!positions[srcId] || !positions[dstId]) return;
|
||||
const src = positions[srcId];
|
||||
const dst = positions[dstId];
|
||||
const strokeW = 1 + (f.bytes / maxBytes) * 5;
|
||||
|
||||
const line = document.createElementNS('http://www.w3.org/2000/svg', 'line');
|
||||
line.setAttribute('x1', String(src.x)); line.setAttribute('y1', String(src.y));
|
||||
line.setAttribute('x2', String(dst.x)); line.setAttribute('y2', String(dst.y));
|
||||
line.setAttribute('stroke', '#6366f1');
|
||||
line.setAttribute('stroke-width', String(strokeW));
|
||||
line.setAttribute('stroke-opacity', '0.6');
|
||||
svg.appendChild(line);
|
||||
|
||||
// Arrow marker
|
||||
const arrow = document.createElementNS('http://www.w3.org/2000/svg', 'polygon');
|
||||
const midX = (src.x + dst.x) / 2;
|
||||
const midY = (src.y + dst.y) / 2;
|
||||
arrow.setAttribute('points', `${midX},${midY - 4} ${midX + 8},${midY} ${midX},${midY + 4}`);
|
||||
arrow.setAttribute('fill', '#818cf8');
|
||||
svg.appendChild(arrow);
|
||||
});
|
||||
|
||||
// Draw nodes
|
||||
nodes.forEach(id => {
|
||||
const pos = positions[id];
|
||||
const isNet = id === 'internet';
|
||||
const label = isNet ? '🌐 Internet' : `Server ${id.slice(1)}`;
|
||||
|
||||
const circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
|
||||
circle.setAttribute('cx', String(pos.x)); circle.setAttribute('cy', String(pos.y));
|
||||
circle.setAttribute('r', '28');
|
||||
circle.setAttribute('fill', isNet ? '#1e3a5f' : '#1e1b4b');
|
||||
circle.setAttribute('stroke', isNet ? '#60a5fa' : '#818cf8');
|
||||
circle.setAttribute('stroke-width', '2');
|
||||
svg.appendChild(circle);
|
||||
|
||||
const text = document.createElementNS('http://www.w3.org/2000/svg', 'text');
|
||||
text.setAttribute('x', String(pos.x)); text.setAttribute('y', String(pos.y + 5));
|
||||
text.setAttribute('text-anchor', 'middle');
|
||||
text.setAttribute('fill', '#e5e7eb');
|
||||
text.setAttribute('font-size', '11');
|
||||
text.textContent = label;
|
||||
svg.appendChild(text);
|
||||
});
|
||||
}, [flows]);
|
||||
|
||||
if (flows.length === 0) {
|
||||
return <div className='addon-empty'>No inter-container flows detected in the last 24h.</div>;
|
||||
}
|
||||
|
||||
return <svg ref={svgRef} className='addon-flow-graph' width='100%' height='280' />;
|
||||
}
|
||||
Reference in New Issue
Block a user