summaryrefslogtreecommitdiffstats
path: root/src/collectors/network-viewer.plugin/network-connections-chart.html
diff options
context:
space:
mode:
Diffstat (limited to 'src/collectors/network-viewer.plugin/network-connections-chart.html')
-rw-r--r--src/collectors/network-viewer.plugin/network-connections-chart.html706
1 files changed, 706 insertions, 0 deletions
diff --git a/src/collectors/network-viewer.plugin/network-connections-chart.html b/src/collectors/network-viewer.plugin/network-connections-chart.html
new file mode 100644
index 000000000..4471b2add
--- /dev/null
+++ b/src/collectors/network-viewer.plugin/network-connections-chart.html
@@ -0,0 +1,706 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="UTF-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <title>Network Viewer</title>
+ <style>
+ /* Styles to make the canvas full width and height */
+ body, html {
+ margin: 0;
+ padding: 0;
+ height: 100%;
+ width: 100%;
+ overflow: hidden;
+ }
+
+ #d3-canvas {
+ height: 100%;
+ width: 100%;
+ }
+ </style>
+ <link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:wght@400&display=swap" rel="stylesheet">
+ <!-- Include D3.js -->
+ <script src="https://d3js.org/d3.v7.min.js"></script>
+</head>
+<body>
+<div id="d3-canvas"></div> <!-- Div for D3 rendering -->
+
+<script>
+
+ let config = {
+ listen: true,
+ }
+
+ function transformData(dataPayload) {
+ // console.log("dataPayload", dataPayload);
+ const desiredColumns = ["Direction", "Protocol", "Namespace", "Process", "LocalIP", "LocalPort", "RemoteIP", "RemotePort", "LocalAddressSpace", "RemoteAddressSpace"];
+
+ const transformedData = [];
+ const appMap = new Map();
+
+ console.log(dataPayload);
+ dataPayload.data.forEach(row => {
+ const rowData = {};
+ desiredColumns.forEach(columnName => {
+ const columnIndex = dataPayload.columns[columnName].index;
+ rowData[columnName] = row[columnIndex];
+ });
+
+ const appName = rowData['Process'];
+ if (!appMap.has(appName)) {
+ appMap.set(appName, {
+ counts: {
+ listen: 0,
+ inbound: 0,
+ outbound: 0,
+ local: 0,
+ private: 0,
+ public: 0,
+ total: 0
+ }
+ });
+ }
+
+ const appData = appMap.get(appName);
+
+ if(config.listen || rowData['Direction'] !== 'listen')
+ appData.counts.total++;
+
+ if (rowData['Direction'] === 'listen')
+ appData.counts.listen++;
+ else if (rowData['Direction'] === 'local')
+ appData.counts.local++;
+ else if (rowData['Direction'] === 'inbound')
+ appData.counts.inbound++;
+ else if (rowData['Direction'] === 'outbound')
+ appData.counts.outbound++;
+
+ if (rowData['RemoteAddressSpace'] === 'public')
+ appData.counts.public++;
+ else if (rowData['RemoteAddressSpace'] === 'private')
+ appData.counts.private++;
+ });
+
+ // Convert the map to an array format
+ for (let [appName, appData] of appMap) {
+ transformedData.push({
+ name: appName,
+ ...appData
+ });
+ }
+
+ if(!config.listen)
+ return transformedData.filter(function(d) {
+ return d.counts.total > 0;
+ });
+ else
+ return transformedData;
+ }
+
+ function normalizeData(data, w, h, borderPadding) {
+ const cw = w / 2 - borderPadding;
+ const ch = h / 2 - borderPadding;
+
+ const minSize = 13;
+ const maxSize = Math.min(
+ (cw * 2) / 3 - borderPadding,
+ (ch * 2) / 3 - borderPadding,
+ Math.max(5, Math.min(w, h) / data.length) + minSize
+ )
+
+ const max = {
+ total: d3.max(data, d => d.counts.total),
+ local: d3.max(data, d => d.counts.local),
+ listen: d3.max(data, d => d.counts.listen),
+ private: d3.max(data, d => d.counts.private),
+ public: d3.max(data, d => d.counts.public),
+ inbound: d3.max(data, d => d.counts.inbound),
+ outbound: d3.max(data, d => d.counts.outbound),
+ }
+
+ const circleSize = d3.scaleLog()
+ .domain([1, max.total])
+ .range([minSize, maxSize])
+ .clamp(true); // Clamps the output so that it stays within the range
+
+ let listenerDelta = 50;
+ let listenersAdded = 0;
+ let listenersAddedRight = 0;
+ let listenersAddedLeft = 0;
+ let listenersH = h - borderPadding;
+
+ data.forEach((d, i) => {
+ const logScaleRight = d3.scaleLog().domain([1, d.counts.public + 1]).range([0, cw]);
+ const logScaleLeft = d3.scaleLog().domain([1, d.counts.private + 1]).range([0, cw]);
+ const logScaleTop = d3.scaleLog().domain([1, d.counts.outbound + 1]).range([0, ch]);
+ const logScaleBottom = d3.scaleLog().domain([1, (d.counts.listen + d.counts.inbound) / 2 + 1]).range([0, ch]);
+
+ d.forces = {
+ total: d.counts.total / max.total,
+ local: d.counts.local / max.local,
+ listen: d.counts.listen / max.listen,
+ private: d.counts.private / max.private,
+ public: d.counts.public / max.public,
+ inbound: d.counts.inbound / max.inbound,
+ outbound: d.counts.outbound / max.outbound,
+ }
+
+ d.pos = {
+ // we add 1 to avoid log(0)
+ right: logScaleRight(d.counts.public + 1),
+ left: logScaleLeft(d.counts.private + 1),
+ top: logScaleTop(d.counts.outbound + 1),
+ bottom: logScaleBottom(((config.listen ? d.counts.listen : 0) + d.counts.inbound) / (config.listen ? 2 : 1) + 1),
+ };
+
+ let x = borderPadding + cw + d.pos.right - d.pos.left;
+ let y = borderPadding + ch + d.pos.bottom - d.pos.top;
+ let size = circleSize(d.counts.total);
+
+ if(d.counts.listen === d.counts.total) {
+ // a listener
+ size = circleSize(1);
+
+ if(listenersAdded * listenerDelta > cw * 2 / 3) {
+ // too many listeners on this row
+ // let's start adding on another row above
+ listenersAdded = 0;
+ listenersAddedLeft = 0;
+ listenersAddedRight = 0;
+ listenersH -= 30;
+ }
+
+ if(!listenersAdded) {
+ x = cw;
+ y = listenersH - size;
+ }
+ else {
+ if(listenersAddedLeft >= listenersAddedRight) {
+ listenersAddedRight++;
+ x = cw + listenersAddedRight * listenerDelta;
+ y = listenersH - size - (listenersAddedRight % 2 === 0 ? 0 : 30);
+ }
+ else {
+ listenersAddedLeft++;
+ x = cw - listenersAddedLeft * listenerDelta;
+ y = listenersH - size - (listenersAddedLeft % 2 === 0 ? 0 : 30);
+ }
+ }
+
+ listenersAdded++;
+ }
+
+ const others = d.counts.total - (d.counts.public + d.counts.private + (config.listen ? d.counts.listen : 0) + d.counts.inbound + d.counts.outbound);
+ d.d3 = {
+ x: x,
+ y: y,
+ size: size,
+ pie: [
+ { value: d.counts.public },
+ { value: d.counts.private },
+ { value: (config.listen ? d.counts.listen : 0) + d.counts.inbound },
+ { value: d.counts.outbound },
+ { value: others > 0 ? others : 0 },
+ ]
+ }
+
+ if(d.d3.x - d.d3.size / 2 < borderPadding)
+ d.d3.x = borderPadding + d.d3.size * 2;
+
+ if(d.d3.x + d.d3.size / 2 > w)
+ d.d3.x = w - d.d3.size * 2;
+
+ if(d.d3.y - d.d3.size / 2 < borderPadding)
+ d.d3.y = borderPadding + d.d3.size * 2;
+
+ if(d.d3.y + d.d3.size / 2 > h)
+ d.d3.y = h - d.d3.size * 2;
+
+ if (d.name === 'upsd')
+ console.log("object", d, "cw", cw, "ch", ch);
+ });
+
+ return data;
+ }
+
+ const themes = {
+ dark: {
+ publicColor: "#bb9900",
+ privateColor: "#323299",
+ serverColor: "#008800",
+ clientColor: "#994433",
+ otherColor: "#454545",
+ backgroundColor: "black",
+ appFontColor: "#bbbbbb",
+ appFontFamily: 'IBM Plex Sans',
+ appFontSize: "12px",
+ appFontWeight: "regular",
+ borderFontColor: "#aaaaaa",
+ borderFontFamily: 'IBM Plex Sans',
+ borderFontSize: "14px",
+ borderFontWeight: "bold",
+ },
+ light: {
+ publicColor: "#bbaa00",
+ privateColor: "#5555ff",
+ serverColor: "#009900",
+ clientColor: "#990000",
+ otherColor: "#666666",
+ backgroundColor: "white",
+ appFontColor: "black",
+ appFontFamily: 'IBM Plex Sans',
+ appFontSize: "12px",
+ appFontWeight: "bold",
+ borderFontColor: "white",
+ borderFontFamily: 'IBM Plex Sans',
+ borderFontSize: "14px",
+ borderFontWeight: "bold",
+ }
+ }
+
+ function hexToHalfOpacityRGBA(hex) {
+ if (hex.length !== 7 || hex[0] !== '#')
+ throw new Error('Invalid hex color format');
+
+ const r = parseInt(hex.slice(1, 3), 16);
+ const g = parseInt(hex.slice(3, 5), 16);
+ const b = parseInt(hex.slice(5, 7), 16);
+ return `rgba(${r}, ${g}, ${b}, 0.5)`;
+ }
+
+ function getRgbColor(hex, opacity = 1) {
+ if (hex.length !== 7 || hex[0] !== "#") throw new Error("Invalid hex color format");
+
+ // Parse the hex color components (red, green, blue)
+ const r = parseInt(hex.slice(1, 3), 16);
+ const g = parseInt(hex.slice(3, 5), 16);
+ const b = parseInt(hex.slice(5, 7), 16);
+
+ // Ensure opacity is within the valid range (0 to 1)
+ const validOpacity = Math.min(1, Math.max(0, opacity));
+
+ // Return the RGBA color
+ return `rgba(${r}, ${g}, ${b}, ${validOpacity})`;
+ }
+
+ function drawInitialChart(svg, data, w, h, borderPadding, theme) {
+ const cw = w / 2;
+ const ch = h / 2;
+
+ document.body.style.backgroundColor = theme.backgroundColor;
+
+ const clientsGradient = svg.append("defs")
+ .append("linearGradient")
+ .attr("id", "clientsGradient")
+ .attr("x1", "0%")
+ .attr("y1", "0%")
+ .attr("x2", "0%")
+ .attr("y2", "100%");
+
+ clientsGradient.append("stop")
+ .attr("offset", "0%")
+ .style("stop-color", getRgbColor(theme.clientColor, 1));
+
+ clientsGradient.append("stop")
+ .attr("offset", "100%")
+ .style("stop-color", getRgbColor(theme.clientColor, 0));
+
+ svg.append("rect")
+ .attr("id", "clientsGradientRect")
+ .attr("x", 0)
+ .attr("y", 0)
+ .attr("width", "100%")
+ .attr("height", borderPadding / 2)
+ .style("fill", "url(#clientsGradient)");
+
+ svg.append("text")
+ .attr("id", "clientsText")
+ .text("Clients")
+ .attr("x", "50%")
+ .attr("y", borderPadding / 2 - 4)
+ .attr("text-anchor", "middle")
+ .style("font-family", theme.borderFontFamily)
+ .style("font-size", theme.borderFontSize)
+ .style("font-weight", theme.borderFontWeight)
+ .style("fill", theme.borderFontColor);
+
+ const serversGradient = svg.append("defs")
+ .append("linearGradient")
+ .attr("id", "serversGradient")
+ .attr("x1", "0%")
+ .attr("y1", "100%") // Start from the bottom
+ .attr("x2", "0%")
+ .attr("y2", "0%") // End at the top
+
+ serversGradient.append("stop")
+ .attr("offset", "0%")
+ .style("stop-color", getRgbColor(theme.serverColor, 1));
+
+ serversGradient.append("stop")
+ .attr("offset", "100%")
+ .style("stop-color", getRgbColor(theme.serverColor, 0));
+
+ svg.append("rect")
+ .attr("id", "serversGradientRect")
+ .attr("x", 0)
+ .attr("y", h - borderPadding / 2)
+ .attr("width", "100%")
+ .attr("height", borderPadding / 2)
+ .style("fill", "url(#serversGradient)"); // Use the reversed gradient fill
+
+ svg.append("text")
+ .attr("id", "serversText")
+ .text("Servers")
+ .attr("x", "50%")
+ .attr("y", h - borderPadding / 2 + 16)
+ .attr("text-anchor", "middle")
+ .style("font-family", theme.borderFontFamily)
+ .style("font-size", theme.borderFontSize)
+ .style("font-weight", theme.borderFontWeight)
+ .style("fill", theme.borderFontColor);
+
+ const publicGradient = svg.append("defs")
+ .append("linearGradient")
+ .attr("id", "publicGradient")
+ .attr("x1", "100%") // Start from the right
+ .attr("y1", "0%")
+ .attr("x2", "0%") // End at the left
+ .attr("y2", "0%");
+
+ publicGradient.append("stop")
+ .attr("offset", "0%")
+ .style("stop-color", getRgbColor(theme.publicColor, 1));
+
+ publicGradient.append("stop")
+ .attr("offset", "100%")
+ .style("stop-color", getRgbColor(theme.publicColor, 0));
+
+ svg.append("rect")
+ .attr("id", "publicGradientRect")
+ .attr("x", w - borderPadding / 2)
+ .attr("y", 0)
+ .attr("width", borderPadding / 2)
+ .attr("height", "100%")
+ .style("fill", "url(#publicGradient)");
+
+ svg.append("text")
+ .attr("id", "publicText")
+ .text("Public")
+ .attr("x", w - (borderPadding / 2))
+ .attr("y", ch - 10)
+ .attr("text-anchor", "middle")
+ .attr("dominant-baseline", "middle")
+ .attr("transform", `rotate(90, ${w - (borderPadding / 2)}, ${ch})`)
+ .style("font-family", theme.borderFontFamily)
+ .style("font-size", theme.borderFontSize)
+ .style("font-weight", theme.borderFontWeight)
+ .style("fill", theme.borderFontColor);
+
+ const privateGradient = svg.append("defs")
+ .append("linearGradient")
+ .attr("id", "privateGradient")
+ .attr("x1", "0%") // Start from the left
+ .attr("y1", "0%")
+ .attr("x2", "100%") // End at the right
+ .attr("y2", "0%");
+
+ privateGradient.append("stop")
+ .attr("offset", "0%")
+ .style("stop-color", getRgbColor(theme.privateColor, 1));
+
+ privateGradient.append("stop")
+ .attr("offset", "100%")
+ .style("stop-color", getRgbColor(theme.privateColor, 0));
+
+ svg.append("rect")
+ .attr("id", "privateGradientRect")
+ .attr("x", 0)
+ .attr("y", 0)
+ .attr("width", borderPadding / 2)
+ .attr("height", "100%")
+ .style("fill", "url(#privateGradient)");
+
+ svg.append("text")
+ .attr("id", "privateText")
+ .text("Private")
+ .attr("x", borderPadding / 2)
+ .attr("y", ch)
+ .attr("text-anchor", "middle")
+ .attr("dominant-baseline", "middle")
+ .attr("transform", `rotate(-90, ${borderPadding / 2 - 10}, ${ch})`)
+ .style("font-family", theme.borderFontFamily)
+ .style("font-size", theme.borderFontSize)
+ .style("font-weight", theme.borderFontWeight)
+ .style("fill", theme.borderFontColor);
+ }
+
+ function updateGradientBoxes(svg, w, h, borderPadding, theme) {
+ const cw = w / 2;
+ const ch = h / 2;
+
+ // Update clientsGradient (top gradient) - mainly adjusting width
+ svg.select("rect#clientsGradientRect")
+ .attr("width", w); // Stretch across the new width
+
+ // Update text position if necessary
+ svg.select("text#clientsText")
+ .attr("x", w / 2); // Center text
+
+ // Update serversGradient (bottom gradient) - adjust Y position and width
+ svg.select("rect#serversGradientRect")
+ .attr("y", h - borderPadding / 2) // Move to the new bottom position
+ .attr("width", w) // Stretch across the new width
+ .attr("fill", "red");
+
+ // Update text position if necessary
+ svg.select("text#serversText")
+ .attr("x", w / 2) // Center text
+ .attr("y", h - borderPadding / 2 + 16); // Adjust based on your specific offset
+
+ // Update publicGradient (right gradient) - adjust X position and height
+ svg.select("rect#publicGradientRect")
+ .attr("x", w - borderPadding / 2) // Move to the new right side position
+ .attr("height", h); // Stretch across the new height
+
+ // Update text position if necessary, adjusting for rotation
+ svg.select("text#publicText")
+ .attr("x", w - (borderPadding / 2))
+ .attr("y", ch - 10)
+ .attr("transform", `rotate(90, ${w - (borderPadding / 2)}, ${ch})`);
+
+ // Update privateGradient (left gradient) - height adjustment only as it's already at x = 0
+ svg.select("rect#privateGradientRect")
+ .attr("height", h); // Stretch across the new height
+
+ // Update text position if necessary, adjusting for rotation
+ svg.select("text#privateText")
+ .attr("x", borderPadding / 2)
+ .attr("y", ch)
+ .attr("transform", `rotate(-90, ${borderPadding / 2 - 10}, ${ch})`);
+
+ }
+
+ let positionsMap = new Map();
+ function saveCurrentPositions(svg) {
+ svg.selectAll('.app').each(function(d) {
+ if (d) {
+ positionsMap.set(d.name, { x: d.x, y: d.y });
+ }
+ });
+ }
+
+ function updateApps(svg, data, w, h, borderPadding, theme) {
+ const cw = w / 2;
+ const ch = h / 2;
+
+ saveCurrentPositions(svg);
+ //svg.selectAll('.app').remove();
+
+ const pieColors = d3.scaleOrdinal()
+ .domain(["public", "private", "listenInbound", "outbound", "others"])
+ .range([theme.publicColor, theme.privateColor, theme.serverColor, theme.clientColor, theme.otherColor]);
+
+ const pie = d3.pie().value(d => d.value);
+ const arc = d3.arc();
+
+ // Binding data with key function
+ const app = svg.selectAll('.app')
+ .data(data, d => d.name);
+
+ // Remove any elements that no longer have data associated with them
+ app.exit()
+ .transition()
+ .style("opacity", 0)
+ .remove();
+
+ // Enter selection for new data points
+ const appEnter = app.enter()
+ .append('g')
+ .attr('class', 'app')
+ .attr('transform', `translate(${cw}, ${ch})`); // Start from center
+
+ // Initialize new elements
+ appEnter.each(function (d) {
+ const group = d3.select(this);
+ const pieData = pie(d.d3.pie);
+
+ group.selectAll('path')
+ .data(pieData)
+ .enter().append('path')
+ .transition()
+ .attr('fill', (d, i) => pieColors(i));
+
+ group.append('text')
+ .text(d => d.name)
+ .attr('text-anchor', 'middle')
+ .style('font-family', theme.appFontFamily)
+ .style('font-size', theme.appFontSize)
+ .style('font-weight', theme.appFontWeight)
+ .style('fill', theme.appFontColor);
+ });
+
+ // Transition for new elements
+ appEnter.transition()
+ .attr('transform', d => `translate(${d.d3.x}, ${d.d3.y})`);
+
+ // Merge the enter and update selections
+ const mergedApp = appEnter.merge(app);
+
+ // Update saved positions
+ mergedApp.each(function (d) {
+ const group = d3.select(this);
+ const oldPos = positionsMap.get(d.name) || { x: cw, y: ch };
+
+ d.x = oldPos.x;
+ d.y = oldPos.y;
+
+ group.selectAll('path')
+ .data(pie(d.d3.pie))
+ .transition()
+ .attr('d', arc.innerRadius(0).outerRadius(d.d3.size));
+
+ group.select('text')
+ .transition()
+ .attr('y', d.d3.size + 10);
+ });
+
+ // Apply the drag behavior to the merged selection
+ mergedApp.call(d3.drag()
+ .on('start', dragstarted)
+ .on('drag', dragged)
+ .on('end', dragended));
+
+ return mergedApp;
+ }
+
+ let forceInit = true;
+ let initial = true;
+ let simulation;
+
+ function dragstarted(event, d) {
+ if (!event.active) simulation.alphaTarget(1).restart();
+ d.fx = d.x;
+ d.fy = d.y;
+ }
+
+ function dragged(event, d) {
+ d.fx = event.x;
+ d.fy = event.y;
+ }
+
+ function dragended(event, d) {
+ if (!event.active) simulation.alphaTarget(0);
+ d.fx = null;
+ d.fy = null;
+ }
+
+ // Function to draw the circles and labels for each application with border forces
+ function drawApplications(data, w, h, borderPadding, theme) {
+ let svg = d3.select('#d3-canvas').select('svg');
+
+ if(svg.empty()) {
+ svg = d3.select('#d3-canvas').select('svg');
+ svg = d3.select('#d3-canvas').append('svg')
+ .attr('width', '100%')
+ .attr('height', '100%');
+
+ drawInitialChart(svg, data, w, h, borderPadding, theme);
+ forceInit = false;
+ }
+
+ updateGradientBoxes(svg, w, h, borderPadding, theme);
+
+ const app = updateApps(svg, data, w, h, borderPadding, theme);
+
+ app.transition().duration(5000)
+
+ simulation = d3.forceSimulation(data)
+ //.force('center', d3.forceCenter(cw, ch).strength(1))
+ .force("x", d3.forceX(d => d.d3.x).strength(d => {
+ if(d.counts.listen === d.counts.total)
+ return 0.5
+ else
+ return 0.05
+ }))
+ .force("y", d3.forceY(d => d.d3.y).strength(d => {
+ if(d.counts.listen === d.counts.total)
+ return 0.5
+ else
+ return 0.05
+ }))
+ //.force("charge", d3.forceManyBody().strength(-0.05))
+ .force("collide", d3.forceCollide(d => d.d3.size * 1.1 + 15).strength(1))
+ .on('tick', ticked);
+
+ function ticked() {
+ data.forEach(d => {
+ if(d.x > w - d.d3.size)
+ d.x = w - d.d3.size;
+ else if(d.x < 0)
+ d.x = 0;
+
+ if(d.y > h - d.d3.size)
+ d.y = h - d.d3.size;
+ else if(d.y < 0)
+ d.y = 0;
+ });
+
+ app.attr('transform', d => `translate(${d.x}, ${d.y})`);
+ }
+
+ initial = false;
+ }
+
+ let lastPayload = {};
+ function redrawChart() {
+ const transformed = transformData(lastPayload);
+ console.log(transformed);
+
+ const w = window.innerWidth;
+ const h = window.innerHeight;
+ const borderPadding = 40;
+
+ const normalized = normalizeData(transformed, w, h, borderPadding);
+ drawApplications(normalized, w, h, borderPadding, themes.dark);
+
+ // Update SVG dimensions
+ const svg = d3.select('#d3-canvas').select('svg')
+ .attr('width', w)
+ .attr('height', h);
+ }
+
+ // Debounce function to optimize performance
+ function debounce(func, timeout = 50) {
+ let timer;
+ return (...args) => {
+ clearTimeout(timer);
+ timer = setTimeout(() => { func.apply(this, args); }, timeout);
+ };
+ }
+
+ // Attach the event listener to the window resize event
+ window.addEventListener('resize', debounce(() => {
+ forceInit = true;
+ redrawChart();
+ }));
+
+ // Modify your fetchData function to call drawApplications after data transformation
+ function fetchDataAndUpdateChart() {
+ fetch('http://localhost:19999/api/v1/function?function=network-connections')
+ .then(response => response.json())
+ .then(data => {
+ lastPayload = data;
+ redrawChart();
+ })
+ .catch(error => console.error('Error fetching data:', error));
+ }
+
+ // Initial load
+ window.onload = () => {
+ fetchDataAndUpdateChart();
+ setInterval(fetchDataAndUpdateChart, 2000); // You may need to adjust this part
+ };
+</script>
+</body>
+</html>