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>