master
html 706 lines 24.3 KB
Raw
1 <!DOCTYPE html>
2 <html lang="en">
3 <head>
4 <meta charset="UTF-8">
5 <meta name="viewport" content="width=device-width, initial-scale=1.0">
6 <title>Network Viewer</title>
7 <style>
8 /* Styles to make the canvas full width and height */
9 body, html {
10 margin: 0;
11 padding: 0;
12 height: 100%;
13 width: 100%;
14 overflow: hidden;
15 }
16
17 #d3-canvas {
18 height: 100%;
19 width: 100%;
20 }
21 </style>
22 <link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:wght@400&display=swap" rel="stylesheet">
23 <!-- Include D3.js -->
24 <script src="https://d3js.org/d3.v7.min.js"></script>
25 </head>
26 <body>
27 <div id="d3-canvas"></div> <!-- Div for D3 rendering -->
28
29 <script>
30
31 let config = {
32 listen: true,
33 }
34
35 function transformData(dataPayload) {
36 // console.log("dataPayload", dataPayload);
37 const desiredColumns = ["Direction", "Protocol", "Namespace", "Process", "LocalIP", "LocalPort", "RemoteIP", "RemotePort", "LocalAddressSpace", "RemoteAddressSpace"];
38
39 const transformedData = [];
40 const appMap = new Map();
41
42 console.log(dataPayload);
43 dataPayload.data.forEach(row => {
44 const rowData = {};
45 desiredColumns.forEach(columnName => {
46 const columnIndex = dataPayload.columns[columnName].index;
47 rowData[columnName] = row[columnIndex];
48 });
49
50 const appName = rowData['Process'];
51 if (!appMap.has(appName)) {
52 appMap.set(appName, {
53 counts: {
54 listen: 0,
55 inbound: 0,
56 outbound: 0,
57 local: 0,
58 private: 0,
59 public: 0,
60 total: 0
61 }
62 });
63 }
64
65 const appData = appMap.get(appName);
66
67 if(config.listen || rowData['Direction'] !== 'listen')
68 appData.counts.total++;
69
70 if (rowData['Direction'] === 'listen')
71 appData.counts.listen++;
72 else if (rowData['Direction'] === 'local')
73 appData.counts.local++;
74 else if (rowData['Direction'] === 'inbound')
75 appData.counts.inbound++;
76 else if (rowData['Direction'] === 'outbound')
77 appData.counts.outbound++;
78
79 if (rowData['RemoteAddressSpace'] === 'public')
80 appData.counts.public++;
81 else if (rowData['RemoteAddressSpace'] === 'private')
82 appData.counts.private++;
83 });
84
85 // Convert the map to an array format
86 for (let [appName, appData] of appMap) {
87 transformedData.push({
88 name: appName,
89 ...appData
90 });
91 }
92
93 if(!config.listen)
94 return transformedData.filter(function(d) {
95 return d.counts.total > 0;
96 });
97 else
98 return transformedData;
99 }
100
101 function normalizeData(data, w, h, borderPadding) {
102 const cw = w / 2 - borderPadding;
103 const ch = h / 2 - borderPadding;
104
105 const minSize = 13;
106 const maxSize = Math.min(
107 (cw * 2) / 3 - borderPadding,
108 (ch * 2) / 3 - borderPadding,
109 Math.max(5, Math.min(w, h) / data.length) + minSize
110 )
111
112 const max = {
113 total: d3.max(data, d => d.counts.total),
114 local: d3.max(data, d => d.counts.local),
115 listen: d3.max(data, d => d.counts.listen),
116 private: d3.max(data, d => d.counts.private),
117 public: d3.max(data, d => d.counts.public),
118 inbound: d3.max(data, d => d.counts.inbound),
119 outbound: d3.max(data, d => d.counts.outbound),
120 }
121
122 const circleSize = d3.scaleLog()
123 .domain([1, max.total])
124 .range([minSize, maxSize])
125 .clamp(true); // Clamps the output so that it stays within the range
126
127 let listenerDelta = 50;
128 let listenersAdded = 0;
129 let listenersAddedRight = 0;
130 let listenersAddedLeft = 0;
131 let listenersH = h - borderPadding;
132
133 data.forEach((d, i) => {
134 const logScaleRight = d3.scaleLog().domain([1, d.counts.public + 1]).range([0, cw]);
135 const logScaleLeft = d3.scaleLog().domain([1, d.counts.private + 1]).range([0, cw]);
136 const logScaleTop = d3.scaleLog().domain([1, d.counts.outbound + 1]).range([0, ch]);
137 const logScaleBottom = d3.scaleLog().domain([1, (d.counts.listen + d.counts.inbound) / 2 + 1]).range([0, ch]);
138
139 d.forces = {
140 total: d.counts.total / max.total,
141 local: d.counts.local / max.local,
142 listen: d.counts.listen / max.listen,
143 private: d.counts.private / max.private,
144 public: d.counts.public / max.public,
145 inbound: d.counts.inbound / max.inbound,
146 outbound: d.counts.outbound / max.outbound,
147 }
148
149 d.pos = {
150 // we add 1 to avoid log(0)
151 right: logScaleRight(d.counts.public + 1),
152 left: logScaleLeft(d.counts.private + 1),
153 top: logScaleTop(d.counts.outbound + 1),
154 bottom: logScaleBottom(((config.listen ? d.counts.listen : 0) + d.counts.inbound) / (config.listen ? 2 : 1) + 1),
155 };
156
157 let x = borderPadding + cw + d.pos.right - d.pos.left;
158 let y = borderPadding + ch + d.pos.bottom - d.pos.top;
159 let size = circleSize(d.counts.total);
160
161 if(d.counts.listen === d.counts.total) {
162 // a listener
163 size = circleSize(1);
164
165 if(listenersAdded * listenerDelta > cw * 2 / 3) {
166 // too many listeners on this row
167 // let's start adding on another row above
168 listenersAdded = 0;
169 listenersAddedLeft = 0;
170 listenersAddedRight = 0;
171 listenersH -= 30;
172 }
173
174 if(!listenersAdded) {
175 x = cw;
176 y = listenersH - size;
177 }
178 else {
179 if(listenersAddedLeft >= listenersAddedRight) {
180 listenersAddedRight++;
181 x = cw + listenersAddedRight * listenerDelta;
182 y = listenersH - size - (listenersAddedRight % 2 === 0 ? 0 : 30);
183 }
184 else {
185 listenersAddedLeft++;
186 x = cw - listenersAddedLeft * listenerDelta;
187 y = listenersH - size - (listenersAddedLeft % 2 === 0 ? 0 : 30);
188 }
189 }
190
191 listenersAdded++;
192 }
193
194 const others = d.counts.total - (d.counts.public + d.counts.private + (config.listen ? d.counts.listen : 0) + d.counts.inbound + d.counts.outbound);
195 d.d3 = {
196 x: x,
197 y: y,
198 size: size,
199 pie: [
200 { value: d.counts.public },
201 { value: d.counts.private },
202 { value: (config.listen ? d.counts.listen : 0) + d.counts.inbound },
203 { value: d.counts.outbound },
204 { value: others > 0 ? others : 0 },
205 ]
206 }
207
208 if(d.d3.x - d.d3.size / 2 < borderPadding)
209 d.d3.x = borderPadding + d.d3.size * 2;
210
211 if(d.d3.x + d.d3.size / 2 > w)
212 d.d3.x = w - d.d3.size * 2;
213
214 if(d.d3.y - d.d3.size / 2 < borderPadding)
215 d.d3.y = borderPadding + d.d3.size * 2;
216
217 if(d.d3.y + d.d3.size / 2 > h)
218 d.d3.y = h - d.d3.size * 2;
219
220 if (d.name === 'upsd')
221 console.log("object", d, "cw", cw, "ch", ch);
222 });
223
224 return data;
225 }
226
227 const themes = {
228 dark: {
229 publicColor: "#bb9900",
230 privateColor: "#323299",
231 serverColor: "#008800",
232 clientColor: "#994433",
233 otherColor: "#454545",
234 backgroundColor: "black",
235 appFontColor: "#bbbbbb",
236 appFontFamily: 'IBM Plex Sans',
237 appFontSize: "12px",
238 appFontWeight: "regular",
239 borderFontColor: "#aaaaaa",
240 borderFontFamily: 'IBM Plex Sans',
241 borderFontSize: "14px",
242 borderFontWeight: "bold",
243 },
244 light: {
245 publicColor: "#bbaa00",
246 privateColor: "#5555ff",
247 serverColor: "#009900",
248 clientColor: "#990000",
249 otherColor: "#666666",
250 backgroundColor: "white",
251 appFontColor: "black",
252 appFontFamily: 'IBM Plex Sans',
253 appFontSize: "12px",
254 appFontWeight: "bold",
255 borderFontColor: "white",
256 borderFontFamily: 'IBM Plex Sans',
257 borderFontSize: "14px",
258 borderFontWeight: "bold",
259 }
260 }
261
262 function hexToHalfOpacityRGBA(hex) {
263 if (hex.length !== 7 || hex[0] !== '#')
264 throw new Error('Invalid hex color format');
265
266 const r = parseInt(hex.slice(1, 3), 16);
267 const g = parseInt(hex.slice(3, 5), 16);
268 const b = parseInt(hex.slice(5, 7), 16);
269 return `rgba(${r}, ${g}, ${b}, 0.5)`;
270 }
271
272 function getRgbColor(hex, opacity = 1) {
273 if (hex.length !== 7 || hex[0] !== "#") throw new Error("Invalid hex color format");
274
275 // Parse the hex color components (red, green, blue)
276 const r = parseInt(hex.slice(1, 3), 16);
277 const g = parseInt(hex.slice(3, 5), 16);
278 const b = parseInt(hex.slice(5, 7), 16);
279
280 // Ensure opacity is within the valid range (0 to 1)
281 const validOpacity = Math.min(1, Math.max(0, opacity));
282
283 // Return the RGBA color
284 return `rgba(${r}, ${g}, ${b}, ${validOpacity})`;
285 }
286
287 function drawInitialChart(svg, data, w, h, borderPadding, theme) {
288 const cw = w / 2;
289 const ch = h / 2;
290
291 document.body.style.backgroundColor = theme.backgroundColor;
292
293 const clientsGradient = svg.append("defs")
294 .append("linearGradient")
295 .attr("id", "clientsGradient")
296 .attr("x1", "0%")
297 .attr("y1", "0%")
298 .attr("x2", "0%")
299 .attr("y2", "100%");
300
301 clientsGradient.append("stop")
302 .attr("offset", "0%")
303 .style("stop-color", getRgbColor(theme.clientColor, 1));
304
305 clientsGradient.append("stop")
306 .attr("offset", "100%")
307 .style("stop-color", getRgbColor(theme.clientColor, 0));
308
309 svg.append("rect")
310 .attr("id", "clientsGradientRect")
311 .attr("x", 0)
312 .attr("y", 0)
313 .attr("width", "100%")
314 .attr("height", borderPadding / 2)
315 .style("fill", "url(#clientsGradient)");
316
317 svg.append("text")
318 .attr("id", "clientsText")
319 .text("Clients")
320 .attr("x", "50%")
321 .attr("y", borderPadding / 2 - 4)
322 .attr("text-anchor", "middle")
323 .style("font-family", theme.borderFontFamily)
324 .style("font-size", theme.borderFontSize)
325 .style("font-weight", theme.borderFontWeight)
326 .style("fill", theme.borderFontColor);
327
328 const serversGradient = svg.append("defs")
329 .append("linearGradient")
330 .attr("id", "serversGradient")
331 .attr("x1", "0%")
332 .attr("y1", "100%") // Start from the bottom
333 .attr("x2", "0%")
334 .attr("y2", "0%") // End at the top
335
336 serversGradient.append("stop")
337 .attr("offset", "0%")
338 .style("stop-color", getRgbColor(theme.serverColor, 1));
339
340 serversGradient.append("stop")
341 .attr("offset", "100%")
342 .style("stop-color", getRgbColor(theme.serverColor, 0));
343
344 svg.append("rect")
345 .attr("id", "serversGradientRect")
346 .attr("x", 0)
347 .attr("y", h - borderPadding / 2)
348 .attr("width", "100%")
349 .attr("height", borderPadding / 2)
350 .style("fill", "url(#serversGradient)"); // Use the reversed gradient fill
351
352 svg.append("text")
353 .attr("id", "serversText")
354 .text("Servers")
355 .attr("x", "50%")
356 .attr("y", h - borderPadding / 2 + 16)
357 .attr("text-anchor", "middle")
358 .style("font-family", theme.borderFontFamily)
359 .style("font-size", theme.borderFontSize)
360 .style("font-weight", theme.borderFontWeight)
361 .style("fill", theme.borderFontColor);
362
363 const publicGradient = svg.append("defs")
364 .append("linearGradient")
365 .attr("id", "publicGradient")
366 .attr("x1", "100%") // Start from the right
367 .attr("y1", "0%")
368 .attr("x2", "0%") // End at the left
369 .attr("y2", "0%");
370
371 publicGradient.append("stop")
372 .attr("offset", "0%")
373 .style("stop-color", getRgbColor(theme.publicColor, 1));
374
375 publicGradient.append("stop")
376 .attr("offset", "100%")
377 .style("stop-color", getRgbColor(theme.publicColor, 0));
378
379 svg.append("rect")
380 .attr("id", "publicGradientRect")
381 .attr("x", w - borderPadding / 2)
382 .attr("y", 0)
383 .attr("width", borderPadding / 2)
384 .attr("height", "100%")
385 .style("fill", "url(#publicGradient)");
386
387 svg.append("text")
388 .attr("id", "publicText")
389 .text("Public")
390 .attr("x", w - (borderPadding / 2))
391 .attr("y", ch - 10)
392 .attr("text-anchor", "middle")
393 .attr("dominant-baseline", "middle")
394 .attr("transform", `rotate(90, ${w - (borderPadding / 2)}, ${ch})`)
395 .style("font-family", theme.borderFontFamily)
396 .style("font-size", theme.borderFontSize)
397 .style("font-weight", theme.borderFontWeight)
398 .style("fill", theme.borderFontColor);
399
400 const privateGradient = svg.append("defs")
401 .append("linearGradient")
402 .attr("id", "privateGradient")
403 .attr("x1", "0%") // Start from the left
404 .attr("y1", "0%")
405 .attr("x2", "100%") // End at the right
406 .attr("y2", "0%");
407
408 privateGradient.append("stop")
409 .attr("offset", "0%")
410 .style("stop-color", getRgbColor(theme.privateColor, 1));
411
412 privateGradient.append("stop")
413 .attr("offset", "100%")
414 .style("stop-color", getRgbColor(theme.privateColor, 0));
415
416 svg.append("rect")
417 .attr("id", "privateGradientRect")
418 .attr("x", 0)
419 .attr("y", 0)
420 .attr("width", borderPadding / 2)
421 .attr("height", "100%")
422 .style("fill", "url(#privateGradient)");
423
424 svg.append("text")
425 .attr("id", "privateText")
426 .text("Private")
427 .attr("x", borderPadding / 2)
428 .attr("y", ch)
429 .attr("text-anchor", "middle")
430 .attr("dominant-baseline", "middle")
431 .attr("transform", `rotate(-90, ${borderPadding / 2 - 10}, ${ch})`)
432 .style("font-family", theme.borderFontFamily)
433 .style("font-size", theme.borderFontSize)
434 .style("font-weight", theme.borderFontWeight)
435 .style("fill", theme.borderFontColor);
436 }
437
438 function updateGradientBoxes(svg, w, h, borderPadding, theme) {
439 const cw = w / 2;
440 const ch = h / 2;
441
442 // Update clientsGradient (top gradient) - mainly adjusting width
443 svg.select("rect#clientsGradientRect")
444 .attr("width", w); // Stretch across the new width
445
446 // Update text position if necessary
447 svg.select("text#clientsText")
448 .attr("x", w / 2); // Center text
449
450 // Update serversGradient (bottom gradient) - adjust Y position and width
451 svg.select("rect#serversGradientRect")
452 .attr("y", h - borderPadding / 2) // Move to the new bottom position
453 .attr("width", w) // Stretch across the new width
454 .attr("fill", "red");
455
456 // Update text position if necessary
457 svg.select("text#serversText")
458 .attr("x", w / 2) // Center text
459 .attr("y", h - borderPadding / 2 + 16); // Adjust based on your specific offset
460
461 // Update publicGradient (right gradient) - adjust X position and height
462 svg.select("rect#publicGradientRect")
463 .attr("x", w - borderPadding / 2) // Move to the new right side position
464 .attr("height", h); // Stretch across the new height
465
466 // Update text position if necessary, adjusting for rotation
467 svg.select("text#publicText")
468 .attr("x", w - (borderPadding / 2))
469 .attr("y", ch - 10)
470 .attr("transform", `rotate(90, ${w - (borderPadding / 2)}, ${ch})`);
471
472 // Update privateGradient (left gradient) - height adjustment only as it's already at x = 0
473 svg.select("rect#privateGradientRect")
474 .attr("height", h); // Stretch across the new height
475
476 // Update text position if necessary, adjusting for rotation
477 svg.select("text#privateText")
478 .attr("x", borderPadding / 2)
479 .attr("y", ch)
480 .attr("transform", `rotate(-90, ${borderPadding / 2 - 10}, ${ch})`);
481
482 }
483
484 let positionsMap = new Map();
485 function saveCurrentPositions(svg) {
486 svg.selectAll('.app').each(function(d) {
487 if (d) {
488 positionsMap.set(d.name, { x: d.x, y: d.y });
489 }
490 });
491 }
492
493 function updateApps(svg, data, w, h, borderPadding, theme) {
494 const cw = w / 2;
495 const ch = h / 2;
496
497 saveCurrentPositions(svg);
498 //svg.selectAll('.app').remove();
499
500 const pieColors = d3.scaleOrdinal()
501 .domain(["public", "private", "listenInbound", "outbound", "others"])
502 .range([theme.publicColor, theme.privateColor, theme.serverColor, theme.clientColor, theme.otherColor]);
503
504 const pie = d3.pie().value(d => d.value);
505 const arc = d3.arc();
506
507 // Binding data with key function
508 const app = svg.selectAll('.app')
509 .data(data, d => d.name);
510
511 // Remove any elements that no longer have data associated with them
512 app.exit()
513 .transition()
514 .style("opacity", 0)
515 .remove();
516
517 // Enter selection for new data points
518 const appEnter = app.enter()
519 .append('g')
520 .attr('class', 'app')
521 .attr('transform', `translate(${cw}, ${ch})`); // Start from center
522
523 // Initialize new elements
524 appEnter.each(function (d) {
525 const group = d3.select(this);
526 const pieData = pie(d.d3.pie);
527
528 group.selectAll('path')
529 .data(pieData)
530 .enter().append('path')
531 .transition()
532 .attr('fill', (d, i) => pieColors(i));
533
534 group.append('text')
535 .text(d => d.name)
536 .attr('text-anchor', 'middle')
537 .style('font-family', theme.appFontFamily)
538 .style('font-size', theme.appFontSize)
539 .style('font-weight', theme.appFontWeight)
540 .style('fill', theme.appFontColor);
541 });
542
543 // Transition for new elements
544 appEnter.transition()
545 .attr('transform', d => `translate(${d.d3.x}, ${d.d3.y})`);
546
547 // Merge the enter and update selections
548 const mergedApp = appEnter.merge(app);
549
550 // Update saved positions
551 mergedApp.each(function (d) {
552 const group = d3.select(this);
553 const oldPos = positionsMap.get(d.name) || { x: cw, y: ch };
554
555 d.x = oldPos.x;
556 d.y = oldPos.y;
557
558 group.selectAll('path')
559 .data(pie(d.d3.pie))
560 .transition()
561 .attr('d', arc.innerRadius(0).outerRadius(d.d3.size));
562
563 group.select('text')
564 .transition()
565 .attr('y', d.d3.size + 10);
566 });
567
568 // Apply the drag behavior to the merged selection
569 mergedApp.call(d3.drag()
570 .on('start', dragstarted)
571 .on('drag', dragged)
572 .on('end', dragended));
573
574 return mergedApp;
575 }
576
577 let forceInit = true;
578 let initial = true;
579 let simulation;
580
581 function dragstarted(event, d) {
582 if (!event.active) simulation.alphaTarget(1).restart();
583 d.fx = d.x;
584 d.fy = d.y;
585 }
586
587 function dragged(event, d) {
588 d.fx = event.x;
589 d.fy = event.y;
590 }
591
592 function dragended(event, d) {
593 if (!event.active) simulation.alphaTarget(0);
594 d.fx = null;
595 d.fy = null;
596 }
597
598 // Function to draw the circles and labels for each application with border forces
599 function drawApplications(data, w, h, borderPadding, theme) {
600 let svg = d3.select('#d3-canvas').select('svg');
601
602 if(svg.empty()) {
603 svg = d3.select('#d3-canvas').select('svg');
604 svg = d3.select('#d3-canvas').append('svg')
605 .attr('width', '100%')
606 .attr('height', '100%');
607
608 drawInitialChart(svg, data, w, h, borderPadding, theme);
609 forceInit = false;
610 }
611
612 updateGradientBoxes(svg, w, h, borderPadding, theme);
613
614 const app = updateApps(svg, data, w, h, borderPadding, theme);
615
616 app.transition().duration(5000)
617
618 simulation = d3.forceSimulation(data)
619 //.force('center', d3.forceCenter(cw, ch).strength(1))
620 .force("x", d3.forceX(d => d.d3.x).strength(d => {
621 if(d.counts.listen === d.counts.total)
622 return 0.5
623 else
624 return 0.05
625 }))
626 .force("y", d3.forceY(d => d.d3.y).strength(d => {
627 if(d.counts.listen === d.counts.total)
628 return 0.5
629 else
630 return 0.05
631 }))
632 //.force("charge", d3.forceManyBody().strength(-0.05))
633 .force("collide", d3.forceCollide(d => d.d3.size * 1.1 + 15).strength(1))
634 .on('tick', ticked);
635
636 function ticked() {
637 data.forEach(d => {
638 if(d.x > w - d.d3.size)
639 d.x = w - d.d3.size;
640 else if(d.x < 0)
641 d.x = 0;
642
643 if(d.y > h - d.d3.size)
644 d.y = h - d.d3.size;
645 else if(d.y < 0)
646 d.y = 0;
647 });
648
649 app.attr('transform', d => `translate(${d.x}, ${d.y})`);
650 }
651
652 initial = false;
653 }
654
655 let lastPayload = {};
656 function redrawChart() {
657 const transformed = transformData(lastPayload);
658 console.log(transformed);
659
660 const w = window.innerWidth;
661 const h = window.innerHeight;
662 const borderPadding = 40;
663
664 const normalized = normalizeData(transformed, w, h, borderPadding);
665 drawApplications(normalized, w, h, borderPadding, themes.dark);
666
667 // Update SVG dimensions
668 const svg = d3.select('#d3-canvas').select('svg')
669 .attr('width', w)
670 .attr('height', h);
671 }
672
673 // Debounce function to optimize performance
674 function debounce(func, timeout = 50) {
675 let timer;
676 return (...args) => {
677 clearTimeout(timer);
678 timer = setTimeout(() => { func.apply(this, args); }, timeout);
679 };
680 }
681
682 // Attach the event listener to the window resize event
683 window.addEventListener('resize', debounce(() => {
684 forceInit = true;
685 redrawChart();
686 }));
687
688 // Modify your fetchData function to call drawApplications after data transformation
689 function fetchDataAndUpdateChart() {
690 fetch('http://localhost:19999/api/v1/function?function=network-connections')
691 .then(response => response.json())
692 .then(data => {
693 lastPayload = data;
694 redrawChart();
695 })
696 .catch(error => console.error('Error fetching data:', error));
697 }
698
699 // Initial load
700 window.onload = () => {
701 fetchDataAndUpdateChart();
702 setInterval(fetchDataAndUpdateChart, 2000); // You may need to adjust this part
703 };
704 </script>
705 </body>
706 </html>