app proto flow determination improvement
This commit is contained in:
@@ -47,6 +47,7 @@ type TopologyLink = {
|
||||
source: string;
|
||||
target: string;
|
||||
value: number;
|
||||
packetCount: number;
|
||||
label: string;
|
||||
};
|
||||
|
||||
@@ -124,6 +125,14 @@ function protocolColor(protocol: string) {
|
||||
return palette[hash % palette.length];
|
||||
}
|
||||
|
||||
function clamp(value: number, min: number, max: number) {
|
||||
return Math.min(max, Math.max(min, value));
|
||||
}
|
||||
|
||||
function sankeyVisualWeight(packetCount: number) {
|
||||
return Math.max(1, Math.sqrt(Math.max(0, packetCount)));
|
||||
}
|
||||
|
||||
function buildTopologyData(interfaces: InterfaceProtocolAttachment[]): TopologyData {
|
||||
const nodes = new Map<string, TopologyNode>();
|
||||
const links = new Map<string, TopologyLink>();
|
||||
@@ -157,7 +166,8 @@ function buildTopologyData(interfaces: InterfaceProtocolAttachment[]): TopologyD
|
||||
links.set(interfaceHostLinkId, {
|
||||
source: interfaceNodeId,
|
||||
target: hostId,
|
||||
value: host.packet_count,
|
||||
value: sankeyVisualWeight(host.packet_count),
|
||||
packetCount: host.packet_count,
|
||||
label: `${entry.interface} -> ${host.ip_address ?? host.mac_address ?? 'host'} (${host.packet_count})`,
|
||||
});
|
||||
|
||||
@@ -187,7 +197,8 @@ function buildTopologyData(interfaces: InterfaceProtocolAttachment[]): TopologyD
|
||||
links.set(hostProtocolLinkId, {
|
||||
source: hostId,
|
||||
target: protocolId,
|
||||
value: protocol.packet_count,
|
||||
value: sankeyVisualWeight(protocol.packet_count),
|
||||
packetCount: protocol.packet_count,
|
||||
label: `${host.ip_address ?? host.mac_address ?? 'host'} -> ${protocol.protocol} (${protocol.packet_count})`,
|
||||
});
|
||||
|
||||
@@ -229,7 +240,13 @@ function SankeyTopology({ data }: { data: TopologyData }) {
|
||||
if (!svgRef.current) return;
|
||||
|
||||
const width = 1160;
|
||||
const height = Math.max(420, data.nodes.length * 26);
|
||||
const nodesPerLayer = {
|
||||
interface: data.nodes.filter((node) => node.kind === 'interface').length,
|
||||
host: data.nodes.filter((node) => node.kind === 'host').length,
|
||||
protocol: data.nodes.filter((node) => node.kind === 'protocol').length,
|
||||
};
|
||||
const maxNodesInLayer = Math.max(nodesPerLayer.interface, nodesPerLayer.host, nodesPerLayer.protocol, 1);
|
||||
const height = clamp(maxNodesInLayer * 56 + 120, 260, 760);
|
||||
const svg = d3.select(svgRef.current);
|
||||
svg.selectAll('*').remove();
|
||||
svg.attr('viewBox', `0 0 ${width} ${height}`);
|
||||
@@ -246,7 +263,7 @@ function SankeyTopology({ data }: { data: TopologyData }) {
|
||||
const sankeyLayout = d3Sankey<TopologyNode, TopologyLink>()
|
||||
.nodeId((node) => node.id)
|
||||
.nodeWidth(18)
|
||||
.nodePadding(16)
|
||||
.nodePadding(maxNodesInLayer <= 4 ? 28 : maxNodesInLayer <= 8 ? 22 : 16)
|
||||
.extent([
|
||||
[24, 28],
|
||||
[width - 24, height - 28],
|
||||
@@ -254,22 +271,7 @@ function SankeyTopology({ data }: { data: TopologyData }) {
|
||||
|
||||
const layout = sankeyLayout(graph);
|
||||
|
||||
const defs = svg.append('defs');
|
||||
defs
|
||||
.append('marker')
|
||||
.attr('id', 'sankey-arrow')
|
||||
.attr('markerWidth', 10)
|
||||
.attr('markerHeight', 10)
|
||||
.attr('refX', 8)
|
||||
.attr('refY', 5)
|
||||
.attr('orient', 'auto')
|
||||
.append('path')
|
||||
.attr('d', 'M0,0 L10,5 L0,10 z')
|
||||
.attr('fill', '#8394a7');
|
||||
|
||||
svg.append('rect').attr('x', 0).attr('y', 0).attr('width', width).attr('height', height).attr('rx', 18).attr('fill', '#f8fbff');
|
||||
|
||||
const linkLayer = svg.append('g').attr('fill', 'none').attr('stroke-opacity', 0.35);
|
||||
const linkLayer = svg.append('g').attr('fill', 'none').attr('stroke-opacity', 0.4);
|
||||
linkLayer
|
||||
.selectAll('path')
|
||||
.data(layout.links as SankeyLinkDatum[])
|
||||
@@ -280,12 +282,11 @@ function SankeyTopology({ data }: { data: TopologyData }) {
|
||||
if (target.kind === 'protocol' && target.protocol) {
|
||||
return protocolColor(target.protocol);
|
||||
}
|
||||
return '#7f8ea3';
|
||||
return '#9aa7b5';
|
||||
})
|
||||
.attr('stroke-width', (link) => Math.max(1, link.width || 1))
|
||||
.attr('marker-end', 'url(#sankey-arrow)')
|
||||
.append('title')
|
||||
.text((link) => `${link.label}\nPackets: ${link.value}`);
|
||||
.text((link) => `${link.label}\nPackets: ${link.packetCount}`);
|
||||
|
||||
const nodeLayer = svg.append('g');
|
||||
const node = nodeLayer
|
||||
@@ -299,7 +300,6 @@ function SankeyTopology({ data }: { data: TopologyData }) {
|
||||
.attr('y', (d) => d.y0 ?? 0)
|
||||
.attr('width', (d) => (d.x1 ?? 0) - (d.x0 ?? 0))
|
||||
.attr('height', (d) => Math.max(8, (d.y1 ?? 0) - (d.y0 ?? 0)))
|
||||
.attr('rx', 8)
|
||||
.attr('fill', (d) => {
|
||||
if (d.kind === 'interface') return '#20405d';
|
||||
if (d.kind === 'host') return '#d7e7f5';
|
||||
@@ -307,7 +307,7 @@ function SankeyTopology({ data }: { data: TopologyData }) {
|
||||
})
|
||||
.attr('stroke', (d) => (d.kind === 'host' ? '#9bb8d6' : 'none'))
|
||||
.append('title')
|
||||
.text((d) => `${d.label}\nPackets: ${d.value ?? d.packetCount}`);
|
||||
.text((d) => `${d.label}\nPackets: ${d.packetCount}`);
|
||||
|
||||
node
|
||||
.append('text')
|
||||
@@ -338,8 +338,8 @@ function ForceTopology({ data }: { data: TopologyData }) {
|
||||
|
||||
useEffect(() => {
|
||||
if (!svgRef.current) return;
|
||||
const width = 1160;
|
||||
const height = 720;
|
||||
const width = 1320;
|
||||
const height = 860;
|
||||
const svg = d3.select(svgRef.current);
|
||||
svg.selectAll('*').remove();
|
||||
svg.attr('viewBox', `0 0 ${width} ${height}`);
|
||||
@@ -350,6 +350,32 @@ function ForceTopology({ data }: { data: TopologyData }) {
|
||||
|
||||
const nodes: ForceNode[] = data.nodes.map((node) => ({ ...node }));
|
||||
const links: ForceLink[] = data.links.map((link) => ({ ...link }));
|
||||
const groupedNodes = {
|
||||
interface: nodes.filter((node) => node.kind === 'interface').sort((left, right) => left.label.localeCompare(right.label)),
|
||||
host: nodes.filter((node) => node.kind === 'host').sort((left, right) => (left.ipAddress ?? left.macAddress ?? left.label).localeCompare(right.ipAddress ?? right.macAddress ?? right.label)),
|
||||
protocol: nodes.filter((node) => node.kind === 'protocol').sort((left, right) => left.label.localeCompare(right.label)),
|
||||
};
|
||||
|
||||
const distributedY = (group: ForceNode[], top: number, bottom: number) => {
|
||||
const targets = new Map<string, number>();
|
||||
if (group.length === 0) {
|
||||
return targets;
|
||||
}
|
||||
if (group.length === 1) {
|
||||
targets.set(group[0].id, (top + bottom) / 2);
|
||||
return targets;
|
||||
}
|
||||
const step = (bottom - top) / (group.length - 1);
|
||||
group.forEach((node, index) => {
|
||||
targets.set(node.id, top + step * index);
|
||||
});
|
||||
return targets;
|
||||
};
|
||||
|
||||
const interfaceY = distributedY(groupedNodes.interface, 120, height - 120);
|
||||
const hostY = distributedY(groupedNodes.host, 90, height - 90);
|
||||
const protocolY = distributedY(groupedNodes.protocol, 120, height - 120);
|
||||
const targetY = (node: ForceNode) => interfaceY.get(node.id) ?? hostY.get(node.id) ?? protocolY.get(node.id) ?? height / 2;
|
||||
|
||||
const simulation = d3
|
||||
.forceSimulation<ForceNode>(nodes)
|
||||
@@ -360,20 +386,29 @@ function ForceTopology({ data }: { data: TopologyData }) {
|
||||
.id((node) => node.id)
|
||||
.distance((link) => {
|
||||
const source = link.source as ForceNode;
|
||||
return source.kind === 'interface' ? 140 : 120;
|
||||
return source.kind === 'interface' ? 250 : 200;
|
||||
})
|
||||
.strength((link) => {
|
||||
const source = link.source as ForceNode;
|
||||
return source.kind === 'interface' ? 0.45 : 0.35;
|
||||
}),
|
||||
)
|
||||
.force('charge', d3.forceManyBody().strength(-280))
|
||||
.force('collision', d3.forceCollide<ForceNode>().radius((node) => (node.kind === 'host' ? 38 : 28)))
|
||||
.force('charge', d3.forceManyBody().strength(-720))
|
||||
.force('collision', d3.forceCollide<ForceNode>().radius((node) => {
|
||||
if (node.kind === 'interface') return 52;
|
||||
if (node.kind === 'host') return 44;
|
||||
return 34;
|
||||
}))
|
||||
.force(
|
||||
'x',
|
||||
d3.forceX<ForceNode>().x((node) => {
|
||||
if (node.kind === 'interface') return 150;
|
||||
if (node.kind === 'interface') return 180;
|
||||
if (node.kind === 'host') return width / 2;
|
||||
return width - 180;
|
||||
}).strength(0.32),
|
||||
return width - 200;
|
||||
}).strength(0.42),
|
||||
)
|
||||
.force('y', d3.forceY<ForceNode>(height / 2).strength(0.08));
|
||||
.force('y', d3.forceY<ForceNode>().y((node) => targetY(node)).strength(0.22))
|
||||
.force('center', d3.forceCenter(width / 2, height / 2).strength(0.06));
|
||||
|
||||
svg.append('rect').attr('x', 0).attr('y', 0).attr('width', width).attr('height', height).attr('rx', 18).attr('fill', '#fbfcfe');
|
||||
|
||||
@@ -445,7 +480,7 @@ function ForceTopology({ data }: { data: TopologyData }) {
|
||||
return <Empty description="No graph data available yet" />;
|
||||
}
|
||||
|
||||
return <svg ref={svgRef} style={{ width: '100%', minWidth: 960 }} />;
|
||||
return <svg ref={svgRef} style={{ width: '100%', minWidth: 1120 }} />;
|
||||
}
|
||||
|
||||
function ProtocolHeatmap({ data }: { data: TopologyData }) {
|
||||
|
||||
Reference in New Issue
Block a user