pkg/skoop/ui/html/index.html (337 lines of code) (raw):

<!DOCTYPE html> <html> <head> <title>Skoop: {{.DiagnoseInfo}} </title> <script> function getElementLeft(element) { return element.getBoundingClientRect().left } function getElementBottom(element) { return element.getBoundingClientRect().top + element.getBoundingClientRect().height } function init() { let side = document.getElementById("side") let mask = side.querySelector(".side-panel-mask") mask.onclick = e => { hideSlidePanel() } } let cluster_info = JSON.parse('{{.Cluster}}') let node_info = JSON.parse('{{.Nodes}}') let edge_info = JSON.parse('{{.Edges}}') function hideSlidePanel() { let side = document.getElementById("side") let mask = side.querySelector(".side-panel-mask") let panel = side.querySelector(".side-panel") mask.style.opacity = 0 panel.style.right = "-400px" setTimeout(() => { side.style.display = "none" }, 200) } function showSlidePanel(title, info, suspicions, show_suspicions) { let side = document.getElementById("side") let header = side.querySelector(".side-panel-header") header.innerHTML = title let panel_info = side.querySelector(".side-panel-info") if (info.length !== 0) { let info_inner = "" info.forEach(i => { info_inner += `<div class="side-panel-info-item"> <div class="side-panel-info-item-name">${i[0]}</div> <div class="side-panel-info-item-value">${i[1]}</div> </div>` }); panel_info.innerHTML = info_inner } else { panel_info.style.display = "none" } let panel_suspicion = side.querySelector(".side-panel-suspicion") if (show_suspicions) { let suspicion_items = panel_suspicion.querySelector(".side-panel-suspicion-items") if (suspicions) { let items_inner = "" suspicions.forEach(s => { items_inner += `<div class="side-panel-suspicion-item"> <span class="side-panel-suspicion-item-level color-${s.level.toLowerCase()}">${s.level}</span> <span class="side-panel-suspicion-item-message">${s.message}</span> </div>` suspicion_items.innerHTML = items_inner }) } else { suspicion_items.innerHTML = 'There is no suspicion on this node.' } panel_suspicion.style.display = "block" } else { panel_suspicion.style.display = "none" } let mask = side.querySelector(".side-panel-mask") let panel = side.querySelector(".side-panel") side.style.display = "block" setTimeout(() => { mask.style.opacity = 0.5 panel.style.right = "0" }, 0) } function patchNodeInfo(elem, n) { console.log(`patching ${n.id}`) let text = elem.querySelector("text") console.log(`text: ${text}`) node_name = text.innerHTML // calculate x, y and width let ellipse = elem.querySelector("ellipse") let d = Math.SQRT2 * ellipse.rx.baseVal.value let x = ellipse.cx.baseVal.value - 0.5 * d; let y = ellipse.cy.baseVal.value - 0.5 * d; let object = `<foreignObject class="node-display" width="${d}" height="${d}" x="${x}" y="${y}"> <img style="width: 40%" src="./svg/${n.type}.svg" /> <p class="node-name">${node_name}</p> <div class="node-suspicion-counter color-${n.suspicion_level.toLowerCase()}">${n.suspicions ? n.suspicions.length : 0}</div> </foreignObject>` text.remove(); elem.innerHTML += object } function applyCluster() { cluster_suspicion = document.getElementById("cluster-suspicions") cluster_suspicion.onclick = ev => { showSlidePanel(`Cluster`, [], cluster_info.suspicions, true) } counter = `<span style="display: inline-block; text-align: center;" class="node-suspicion-counter color-${cluster_info.suspicion_level.toLowerCase()}"> ${cluster_info.suspicions ? cluster_info.suspicions.length : 0} </span>` cluster_suspicion.innerHTML += counter } function applyNodes() { node_info.forEach(n => { let node = document.getElementsByClassName(`node ${n.id}`)[0] patchNodeInfo(node, n) node.classList.add("node-" + n.suspicion_level.toLowerCase()) node.onclick = e => { showSlidePanel(`Node ${n.id}`, [["Node ID", n.id], ["Type", n.type]], n.suspicions, true) e.cancelBubble = true } }); } function applyEdges() { edge_info.forEach(e => { let edge = document.getElementsByClassName(`edge ${e.id}`)[0] edge.onclick = ev => { showSlidePanel(`Edge ${e.id}`, [ ["Edge ID", e.id], ["Type", e.type], ["Action", e.action], ["Output Interface (Sender Node)", e.oif], ["Input Interface (Receiver Node)", e.iif], ["Packet Source", e.packet.src], ["Packet Destination", e.packet.dst], ["Packet Destination Port", e.packet.dport], ["Packet Protocol", e.packet.protocol] ], null, false) ev.cancelBubble = true } }) } document.addEventListener("DOMContentLoaded", _ => { init() applyCluster() applyNodes() applyEdges() }) </script> <style> body { margin: 0; } .skoop-header { display: flex; align-items: center; background-color: #444CCB; color: white; top: 0; height: 44px; min-height: 44px; padding-left: 10px; } #graph { display: flex; justify-content: center; max-width: 1800px; width: 100%; margin: auto; } svg { width: 90%; height: auto; } .node, .edge { pointer-events: fill; cursor: pointer; } .edge text { user-select: none; } .node ellipse { stroke-width: 1px; stroke: #666666; transition: 100ms; } .node:hover ellipse { stroke-width: 2px !important; transition: 200ms; } .edge:hover path { stroke-width: 2px !important; transition: 200ms; } .node-name { margin: 0; font-size: 12px; line-break: anywhere; text-align: center; } .node-display { text-align: center; } .node-suspicion-counter { margin: auto; font-size: 12px; height: 16px; width: 16px; border: 1px gray; border-radius: 3px; } .color-info { background-color: #30BD61; } .color-warning { background-color: #FFB369; } .color-fatal, .color-critical { background-color: #F76D76; } .side { display: none; } .side-panel { position: fixed; top: 44px; right: -400px; width: 400px; height: 100%; border-left: 1px solid #fafafa; background-color: #ffffff; transition: right 0.2s; } .side-panel-header { padding: 13px 16px; border-bottom: 1px solid #cccccc; } .side-panel-mask { position: fixed; top: 44px; right: 0; width: 100%; height: 100%; background-color: black; opacity: 0; transition: all .2s; } .side-panel-info-item { margin: 15px 20px; font-size: 13px; } .side-panel-info-item-name { color: gray; } #suspicions { display: none; width: 400px; background-color: white; position: absolute; min-height: 100px; border: 1px solid gray; } #suspicions-list p { margin: 0 3px 3px 3px; border: 1px solid gray; } #suspicions-header { background-color: #444CCB; color: white; height: 30px; align-items: center; margin: 0; padding-left: 5px; white-space: nowrap; } #diagnose-result { display: flex; align-items: center; justify-content: center; min-width: 1000px; min-height: 1000px; } .side-panel-suspicion { border-top: 1px solid #cccccc; } .side-panel-suspicion-header { margin: 15px 15px; font-size: 15px; } .side-panel-suspicion-items { margin: 15px 30px 15px 26px; } .side-panel-suspicion-item { margin-top: 20px; } .side-panel-suspicion-item-level { user-select: none; color: white; padding: 1px 5px; border-radius: 5px; font-size: 13px; } .side-panel-suspicion-item-message { line-break: loose; font-size: 15px; } #cluster-suspicions { cursor: pointer; margin-left: 10px; padding: 3px; border: 1px solid #fafafa; border-radius: 7px; } .node-display .node-suspicion-counter { margin-top: 3px; } </style> </head> <body> <header class="skoop-header"> <div id="title">Skoop {{.DiagnoseInfo}}</div> <div id="cluster-suspicions"> <span>Cluster Suspicions</span> </div> </header> <div id="diagnose-result"> {{.GraphSvg}} </div> <div class="side" id="side"> <div class="side-panel-mask"></div> <div class="side-panel"> <div class="side-panel-header">Diagnose Result:</div> <div class="side-panel-info"> </div> <div class="side-panel-suspicion"> <div class="side-panel-suspicion-header">Suspicions:</div> <div class="side-panel-suspicion-items"> </div> </div> </div> </div> </body> </html>