Added stats section to web

This commit is contained in:
Quentin Renard
2025-01-29 15:55:27 +01:00
parent 6780010376
commit 3cecbe1cf4
3 changed files with 477 additions and 248 deletions

View File

@@ -23,60 +23,58 @@ header {
background-color: #333333;
border-bottom: solid 1px #000000;
color: #ffffff;
display: flex;
height: auto;
padding: 10px;
width: 100%;
}
header #left {
width: 74%
}
header #left #top {
header #top {
display: flex;
flex-direction: row;
}
header #left #search {
border: none;
border-radius: 5px;
font-family: Roboto;
font-size: 12px;
padding: 7px 10px;
width: 300px;
}
header #left #top i {
header #top i {
cursor: pointer;
font-size: 20px;
margin-left: 10px;
display: none;
padding: 4px;
}
header #left #tags>div {
body.nodes header #top #show-stats,
body.nodes header #top #zoom-out,
body.nodes header #top #zoom-auto,
body.nodes header #top #zoom-in,
body.nodes header #top #toggle-settings {
display: block;
}
body.stats header #top #show-nodes,
body.stats header #top #toggle-settings {
display: block;
}
header #settings {
display: none;
}
header #settings #tags>div {
background-color: #555555;
border: solid 1px #777777;
border-radius: 5px;
display: flex;
float: left;
padding: 5px 8px;
margin: 10px 10px 0 0;
padding: 5px 8px;
}
header #left #tags>div:last-child {
margin-right: 0;
}
header #left #tags>div:first-child {
header #settings #tags>div:first-child {
cursor: pointer;
}
header #left #tags .action:hover span {
header #settings #tags .action:hover span {
text-decoration: underline;
}
header #left #tags .fa {
header #settings #tags .fa {
cursor: pointer;
margin-top: auto;
margin-bottom: auto;
@@ -84,29 +82,19 @@ header #left #tags .fa {
vertical-align: middle;
}
header #left #tags .fa.active,
header #tags .fa:hover {
header #settings #tags .fa.active,
header #settings #tags .fa:hover {
opacity: 1;
}
header #left #tags>div>.fa:first-child {
header #settings #tags>div>.fa:first-child {
margin-right: 8px;
}
header #left #tags>div>.fa:last-child {
header #settings #tags>div>.fa:last-child {
margin-left: 8px;
}
header #right {
padding-left: 10px;
width: 26%;
}
header #right>div {
display: grid;
grid-template-columns: repeat(4, 1fr);
}
section {
-ms-overflow-style: none;
/* IE and Edge */
@@ -123,6 +111,18 @@ section::-webkit-scrollbar {
display: none;
}
section>div {
display: none;
}
body.nodes section #nodes {
display: block;
}
body.stats section #stats {
display: flex;
}
section #nodes {
margin: 1em 0;
position: relative;
@@ -141,7 +141,7 @@ section #nodes svg line {
stroke: #000000;
}
section .node {
section #nodes .node {
align-items: center;
border: solid 0.1em transparent;
border-radius: 0.3em;
@@ -156,45 +156,74 @@ section #nodes>div:last-child>div {
margin-bottom: 0;
}
section .node.paused {
section #nodes .node.paused {
color: #856404;
background-color: #fff3cd;
border-color: #ffeeba;
}
section .node.running {
section #nodes .node.running {
color: #155724;
background-color: #d4edda;
border-color: #c3e6cb;
}
section .node.stopped {
section #nodes .node.stopped {
color: #721c24;
background-color: #f8d7da;
border-color: #f5c6cb;
}
section .node .label {
section #nodes .node .label {
text-decoration: underline;
}
section .node .name {
section #nodes .node .name {
font-style: italic;
}
section .node .stats {
section #nodes .node .stats {
margin-top: 1em;
}
section .node .stats td {
section #nodes .node .stats td {
padding: 0;
}
section .node .stats td:first-child {
section #nodes .node .stats td:first-child {
padding-right: 0.3em;
white-space: nowrap;
}
section #stats {
gap: 10px;
padding: 10px;
}
section #stats .table .title {
font-weight: bold;
margin-bottom: 8px;
text-align: center;
text-decoration: underline;
}
section #stats .table table {
border-collapse: collapse;
}
section #stats .table table tr,
section #stats .table table th,
section #stats .table table td {
padding: 0;
}
section #stats .table table th,
section #stats .table table td {
border: solid 1px #000;
padding: 4px;
white-space: nowrap;
}
footer {
background-color: #333333;
border-top: solid 1px #000000;
@@ -254,5 +283,6 @@ footer.recording-loaded>#recording-time {
}
footer>div:last-child input[type="range"] {
cursor: pointer;
width: 100%;
}

View File

@@ -4,49 +4,30 @@
<head>
<meta charset="UTF-8">
<title>Astiencoder</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.13.0/css/all.min.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.7.2/css/all.min.css">
<link rel="stylesheet" href="index.css">
</head>
<body>
<header>
<div id="left">
<div id="top">
<input id="search" type="text" placeholder="Search" oninput="astiencoder.onSearch(event)"
autocomplete="off">
<i class="fa fa-search-minus" onclick="astiencoder.zoomOut()"></i>
<i class="fa fa-expand-arrows-alt" onclick="astiencoder.zoomAuto()"></i>
<i class="fa fa-search-plus" onclick="astiencoder.zoomIn()"></i>
</div>
<div id="top">
<i id="show-stats" class="fa fa-table" onclick="astiencoder.setSection('stats')"></i>
<i id="show-nodes" class="fa fa-share-nodes" onclick="astiencoder.setSection('nodes')"></i>
<i id="zoom-out" class="fa fa-search-minus" onclick="astiencoder.zoomOut()"></i>
<i id="zoom-auto" class="fa fa-expand-arrows-alt" onclick="astiencoder.zoomAuto()"></i>
<i id="zoom-in" class="fa fa-search-plus" onclick="astiencoder.zoomIn()"></i>
<i id="toggle-settings" class="fa fa-gear" onclick="astiencoder.toggleSettings()"></i>
</div>
<div id="settings">
<div id="tags">
<div class="action" onclick="astiencoder.onResetAllTags()">
<span>Reset all</span>
</div>
</div>
</div>
<div id="right">
<table>
<tr>
<td>Process memory</td>
<td><span id="memory-resident"></span>/<span id="memory-virtual"></span>GB</td>
</tr>
<tr>
<td>Total memory</td>
<td><span id="memory-used"></span>/<span id="memory-total"></span>GB</td>
</tr>
<tr>
<td>Process CPU</td>
<td><span id="cpu-process"></span>%</td>
</tr>
<tr>
<td>Total CPU</td>
<td><span id="cpu-total"></span>%</td>
</tr>
</table>
<div id="cpus"></div>
</div>
</header>
<section>
<div id="stats"></div>
<div id="nodes"></div>
</section>
<footer>
@@ -62,7 +43,7 @@
<div id="recording-time"></div>
<div id="recording-progress">
<input min="0" max="100" type="range" onchange="astiencoder.onRecordingSeek(event)"
onkeydown="event.preventDefault()" />
oninput="astiencoder.onRecordingPreviewTime(event)" onkeydown="event.preventDefault()" />
</div>
</footer>

View File

@@ -7,8 +7,8 @@ var astiencoder = {
// Get params
const params = new URLSearchParams(window.location.search)
// Init nodes position refresher
this.initNodesPositionRefresher()
// Show default section
this.setSection('nodes')
// Get urls
this.websocketUrl = params.get("websocket_url")
@@ -45,8 +45,8 @@ var astiencoder = {
// No workflow or no nodes
if (!data.workflow || data.workflow.nodes.length === 0) {
// Refresh nodes position
this.refreshNodesPosition()
// Refresh section
this.refreshSection()
return
}
@@ -59,8 +59,8 @@ var astiencoder = {
this.apply(item.name, item)
}.bind(this))
// Refresh nodes position
this.refreshNodesPosition()
// Refresh section
this.refreshSection()
}.bind(this)
})
},
@@ -163,22 +163,22 @@ var astiencoder = {
// Apply
this.apply(payload.parent, { children: [payload.child] })
// Refresh nodes position
if (!recording) this.refreshNodesPosition()
// Refresh section
if (!recording) this.refreshSection()
break
case 'astiencoder.node.child.removed':
// Apply
this.apply(payload.parent, { childrenRemoved: [payload.child] })
// Refresh nodes position
if (!recording) this.refreshNodesPosition()
// Refresh section
if (!recording) this.refreshSection()
break
case 'astiencoder.node.closed':
// Apply
this.apply(payload, { closed: true })
// Refresh nodes position
if (!recording) this.refreshNodesPosition()
// Refresh section
if (!recording) this.refreshSection()
break
case 'astiencoder.node.continued':
this.apply(payload, { status: 'running' })
@@ -190,15 +190,15 @@ var astiencoder = {
// Apply
this.apply(payload.name, payload)
// Refresh nodes position
if (!recording) this.refreshNodesPosition()
// Refresh section
if (!recording) this.refreshSection()
break
case 'astiencoder.node.stopped':
// Apply
this.apply(payload, { status: 'stopped' })
// Refresh nodes position
if (!recording) this.refreshNodesPosition()
// Refresh section
if (!recording) this.refreshSection()
break
case 'astiencoder.stats':
// Apply
@@ -206,8 +206,8 @@ var astiencoder = {
this.apply(stat.target, { stat: stat })
})
// Refresh nodes size
if (!recording) this.refreshNodesSize()
// Refresh section
if (!recording) this.refreshSection()
break
}
return rollbacks
@@ -271,6 +271,17 @@ var astiencoder = {
return false
},
/* section */
setSection(name) {
document.querySelector('body').className = name
this.sectionName = name
this.refreshSection()
},
refreshSection() {
if (this.sectionName === 'nodes') this.refreshNodesPosition()
else if (this.sectionName === 'stats') this.refreshStats()
},
/* workflow */
workflow: {
name: '',
@@ -282,13 +293,8 @@ var astiencoder = {
// Switch on prop
switch (prop) {
case 'astiencoder.host.usage':
document.getElementById("memory-virtual").innerText = ''
document.getElementById("memory-resident").innerText = ''
document.getElementById("memory-used").innerText = ''
document.getElementById("memory-total").innerText = ''
document.getElementById("cpu-process").innerText = ''
document.getElementById("cpu-total").innerText = ''
document.getElementById("cpus").innerText = ''
// Refresh stats
astiencoder.refreshStats()
break
}
@@ -302,19 +308,8 @@ var astiencoder = {
// Switch on prop
switch (prop) {
case 'astiencoder.host.usage':
if (value.value.memory.resident) document.getElementById("memory-resident").innerText = (value.value.memory.resident / Math.pow(1024, 3)).toFixed(2)
if (value.value.memory.virtual) document.getElementById("memory-virtual").innerText = (value.value.memory.virtual / Math.pow(1024, 3)).toFixed(2)
if (value.value.memory.used) document.getElementById("memory-used").innerText = (value.value.memory.used / Math.pow(1024, 3)).toFixed(2)
if (value.value.memory.total) document.getElementById("memory-total").innerText = (value.value.memory.total / Math.pow(1024, 3)).toFixed(2)
if (value.value.cpu.process) document.getElementById("cpu-process").innerText = value.value.cpu.process.toFixed(2)
if (value.value.cpu.total) document.getElementById("cpu-total").innerText = value.value.cpu.total.toFixed(2)
if (value.value.cpu.individual) {
var e = document.getElementById("cpus")
e.innerHTML = ""
for (var idx = 0; idx < value.value.cpu.individual.length; idx++) {
e.innerHTML += "<div>#" + (idx + 1) + ": " + value.value.cpu.individual[idx].toFixed(2) + "%</div>"
}
}
// Refresh stats
astiencoder.refreshStats()
break
}
@@ -404,7 +399,7 @@ var astiencoder = {
updateTime(t) {
this.currentTime = t
document.querySelector('#recording-progress > input').value = ((t.getTime() - this.from.getTime()) / this.duration) * 100
document.querySelector('#recording-time').innerText = t.getHours().toString().padStart(2, '0') + ':' + t.getMinutes().toString().padStart(2, '0') + ':' + t.getSeconds().toString().padStart(2, '0')
astiencoder.updateRecordingTime(t)
},
next() {
// No nexts
@@ -462,6 +457,9 @@ var astiencoder = {
return true
}
}),
updateRecordingTime(t) {
document.querySelector('#recording-time').innerText = t.getHours().toString().padStart(2, '0') + ':' + t.getMinutes().toString().padStart(2, '0') + ':' + t.getSeconds().toString().padStart(2, '0')
},
onRecordingLoadClick() {
document.querySelector('#recording-load input').click()
},
@@ -584,8 +582,8 @@ var astiencoder = {
// Reset
this.reset()
// Refresh nodes position
this.refreshNodesPosition()
// Refresh section
this.refreshSection()
// On open
if (this.websocketUrl && this.welcomeUrl) this.onopen()
@@ -597,8 +595,8 @@ var astiencoder = {
// Apply next
this.recording.applyNext()
// Refresh nodes position
this.refreshNodesPosition()
// Refresh section
this.refreshSection()
},
onRecordingPreviousClick() {
// No recording
@@ -607,8 +605,8 @@ var astiencoder = {
// Apply previous
this.recording.applyPrevious()
// Refresh nodes position
this.refreshNodesPosition()
// Refresh section
this.refreshSection()
},
onRecordingSeek(e) {
// No recording
@@ -644,8 +642,18 @@ var astiencoder = {
}
}
// Refresh nodes position
this.refreshNodesPosition()
// Refresh section
this.refreshSection()
},
onRecordingPreviewTime(e) {
// No recording
if (!this.recording.loaded) return
// Get seek time
const t = new Date(e.target.value / 100 * this.recording.duration + this.recording.from.getTime())
// Update recording time
this.updateRecordingTime(t)
},
/* tags */
@@ -741,8 +749,8 @@ var astiencoder = {
this.refreshTagsForNode(name)
}
// Refresh nodes position
this.refreshNodesPosition()
// Refresh section
this.refreshSection()
},
refreshTagsForNode(name) {
// Index tags
@@ -783,7 +791,6 @@ var astiencoder = {
var n = {
_key: value.label,
dom: {},
notInSearch: false,
notInTags: false
}
@@ -889,10 +896,10 @@ var astiencoder = {
case 'label':
_c1.innerText = value + ':'
break
case 'unit':
case 'funit':
_u.innerText = value
break
case 'value':
case 'fvalue':
_v.innerText = value
break
}
@@ -927,7 +934,7 @@ var astiencoder = {
// Methods
n.displayed = function () {
return !this.notInTags && !this.notInSearch
return !this.notInTags
}
// Store node
@@ -1062,35 +1069,45 @@ var astiencoder = {
if (payload.stat.unit) this.nodes[name].stats[payload.stat.label].unit = payload.stat.unit
// Value
if (typeof payload.stat.value !== 'undefined') this.nodes[name].stats[payload.stat.label].value = payload.stat.value
// Formatted
if (typeof payload.stat.value !== 'undefined') {
// Update formatted unit and value
this.nodes[name].stats[payload.stat.label].funit = payload.stat.unit
this.nodes[name].stats[payload.stat.label].fvalue = payload.stat.value
// Get value
var v = payload.stat.value
// Parse value
var f = parseFloat(v)
if (!isNaN(f)) {
switch (this.nodes[name].stats[payload.stat.label].unit) {
case 'Bps':
f *= 8
this.nodes[name].stats[payload.stat.label].unit = 'bps'
this.nodes[name].stats[payload.stat.label].funit = 'bps'
if (f > 1e9) {
f /= 1e9
this.nodes[name].stats[payload.stat.label].unit = 'Gbps'
this.nodes[name].stats[payload.stat.label].funit = 'Gbps'
} else if (f > 1e6) {
f /= 1e6
this.nodes[name].stats[payload.stat.label].unit = 'Mbps'
this.nodes[name].stats[payload.stat.label].funit = 'Mbps'
} else if (f > 1e3) {
f /= 1e3
this.nodes[name].stats[payload.stat.label].unit = 'kbps'
this.nodes[name].stats[payload.stat.label].funit = 'kbps'
}
break
case 'ns':
if (f > 1e9 || f < -1e9) {
f /= 1e9
this.nodes[name].stats[payload.stat.label].unit = 's'
this.nodes[name].stats[payload.stat.label].funit = 's'
} else if (f > 1e6 || f < -1e6) {
f /= 1e6
this.nodes[name].stats[payload.stat.label].unit = 'ms'
this.nodes[name].stats[payload.stat.label].funit = 'ms'
} else if (f > 1e3 || f < -1e3) {
f /= 1e3
this.nodes[name].stats[payload.stat.label].unit = 'µs'
this.nodes[name].stats[payload.stat.label].funit = 'µs'
}
break
}
@@ -1102,7 +1119,7 @@ var astiencoder = {
else if (f <= -1000) f = '-∞'
}
}
this.nodes[name].stats[payload.stat.label].value = f
this.nodes[name].stats[payload.stat.label].fvalue = f
}
}
@@ -1119,105 +1136,103 @@ var astiencoder = {
},
/* nodes position refresher */
initNodesPositionRefresher() {
this.nodesPositionRefresher = {
a: new Uint8Array(1),
f: function () {
// Reset
document.getElementById('nodes').innerHTML = ''
nodesPositionRefresher: {
a: new Uint8Array(1),
f: function () {
// Reset
document.getElementById('nodes').innerHTML = ''
// Create svg
const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg")
document.getElementById('nodes').appendChild(svg)
// Create svg
const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg")
document.getElementById('nodes').appendChild(svg)
// Get levels
var processedNodes = {}, total = Object.keys(this.nodes).length, levels = []
while (Object.keys(processedNodes).length < total) {
// Loop through nodes
var level = [], tempLevel = {}
for (var name in this.nodes) {
// Already processed
if (processedNodes[name]) continue
// Get levels
var processedNodes = {}, total = Object.keys(astiencoder.nodes).length, levels = []
while (Object.keys(processedNodes).length < total) {
// Loop through nodes
var level = [], tempLevel = {}
for (var name in astiencoder.nodes) {
// Already processed
if (processedNodes[name]) continue
// There are no levels yet, we check nodes with no parents
if (levels.length === 0) {
if (Object.keys(this.nodes[name].parents).length === 0) level.push(this.nodes[name])
continue
}
// Loop through previous level nodes
levels[levels.length - 1].forEach(item => {
if (this.nodes[name].parents[item.name]) tempLevel[name] = this.nodes[name]
})
// There are no levels yet, we check nodes with no parents
if (levels.length === 0) {
if (Object.keys(astiencoder.nodes[name].parents).length === 0) level.push(astiencoder.nodes[name])
continue
}
// Move children in the same zone as their parent
if (levels.length > 0 && Object.keys(tempLevel).length > 0) {
// Loop through previous level nodes
levels[levels.length - 1].forEach(item => {
// Loop through children
for (var k in item.children) {
const c = tempLevel[k]
if (c) {
level.push(c)
delete tempLevel[k]
}
}
})
}
// No nodes in level
// This shouldn't happen but we want to avoid infinite loops
if (level.length === 0) break
// Append level
levels.push(level)
level.forEach(node => processedNodes[node.name] = true)
// Loop through previous level nodes
levels[levels.length - 1].forEach(item => {
if (astiencoder.nodes[name].parents[item.name]) tempLevel[name] = astiencoder.nodes[name]
})
}
// Loop through levels
levels.forEach(level => {
// Create wrapper
const lw = document.createElement('div')
// Loop through level items
level.forEach(item => {
// Node is not displayed
if (!item.displayed()) return
// Append wrapper
lw.appendChild(item.dom.w)
// Move children in the same zone as their parent
if (levels.length > 0 && Object.keys(tempLevel).length > 0) {
// Loop through previous level nodes
levels[levels.length - 1].forEach(item => {
// Loop through children
for (var c in item.children) {
// Node is not displayed
const n = this.nodes[c]
if (!n || !n.displayed()) continue
// Append arrow
svg.appendChild(item.children[c].arrow.line)
svg.appendChild(item.children[c].arrow.head.line1)
svg.appendChild(item.children[c].arrow.head.line2)
for (var k in item.children) {
const c = tempLevel[k]
if (c) {
level.push(c)
delete tempLevel[k]
}
}
})
// Append wrapper
document.getElementById('nodes').appendChild(lw)
})
// Refresh size
this.refreshNodesSize()
}.bind(this),
i: setInterval(function () {
// Nothing to do
if (Atomics.compareExchange(this.nodesPositionRefresher.a, 0, 1, 0) === 0) {
return
}
// Refresh nodes position
this.nodesPositionRefresher.f()
}.bind(this), 50)
}
// No nodes in level
// This shouldn't happen but we want to avoid infinite loops
if (level.length === 0) break
// Append level
levels.push(level)
level.forEach(node => processedNodes[node.name] = true)
}
// Loop through levels
levels.forEach(level => {
// Create wrapper
const lw = document.createElement('div')
// Loop through level items
level.forEach(item => {
// Node is not displayed
if (!item.displayed()) return
// Append wrapper
lw.appendChild(item.dom.w)
// Loop through children
for (var c in item.children) {
// Node is not displayed
const n = astiencoder.nodes[c]
if (!n || !n.displayed()) continue
// Append arrow
svg.appendChild(item.children[c].arrow.line)
svg.appendChild(item.children[c].arrow.head.line1)
svg.appendChild(item.children[c].arrow.head.line2)
}
})
// Append wrapper
document.getElementById('nodes').appendChild(lw)
})
// Refresh size
astiencoder.refreshNodesSize()
},
i: setInterval(function () {
// Nothing to do
if (Atomics.compareExchange(astiencoder.nodesPositionRefresher.a, 0, 1, 0) === 0) {
return
}
// Refresh nodes position
astiencoder.nodesPositionRefresher.f()
}, 50)
},
refreshNodesPosition() {
Atomics.store(this.nodesPositionRefresher.a, 0, 1)
@@ -1426,19 +1441,6 @@ var astiencoder = {
}
},
/* search */
onSearch(event) {
// Loop through nodes
for (var name in this.nodes) {
this.nodes[name].notInSearch = event.target.value !== ''
&& this.nodes[name].label.toLowerCase().search(event.target.value.toLowerCase()) === -1
&& this.nodes[name].name.toLowerCase().search(event.target.value.toLowerCase()) === -1
}
// Refresh nodes position
this.refreshNodesPosition()
},
/* zoom */
zoom: {
auto: true,
@@ -1452,8 +1454,8 @@ var astiencoder = {
this.zoom.auto = false
this.zoom.value += 10
// Refresh nodes size
this.refreshNodesSize()
// Refresh section
this.refreshSection()
},
zoomOut() {
// No nodes
@@ -1466,8 +1468,8 @@ var astiencoder = {
this.zoom.auto = false
this.zoom.value -= 10
// Refresh nodes size
this.refreshNodesSize()
// Refresh section
this.refreshSection()
},
zoomAuto() {
// No nodes
@@ -1476,8 +1478,224 @@ var astiencoder = {
// Update zoom
this.zoom.auto = true
// Refresh nodes size
this.refreshNodesSize()
// Refresh section
this.refreshSection()
},
/* stats */
refreshStats() {
// Workflow stats
let tables = []
for (let name in this.workflow.stats) {
const v = this.workflow.stats[name]
switch (name) {
case 'astiencoder.host.usage':
// Create table
let table = {
label: 'Host',
rows: [],
}
// Add process memory
if (v.value.memory.resident && v.value.memory.virtual) {
table.rows.push([
{ text: 'Process memory' },
{ text: (v.value.memory.resident / Math.pow(1024, 3)).toFixed(2) + '/' + (v.value.memory.virtual / Math.pow(1024, 3)).toFixed(2) + 'GB' }
])
}
// Add total memory
if (v.value.memory.used && v.value.memory.total) {
table.rows.push([
{ text: 'Total memory' },
{ text: (v.value.memory.used / Math.pow(1024, 3)).toFixed(2) + '/' + (v.value.memory.total / Math.pow(1024, 3)).toFixed(2) + 'GB' }
])
}
// Process CPU
if (v.value.cpu.process) {
table.rows.push([
{ text: 'Process CPU' },
{ text: v.value.cpu.process.toFixed(2) + '%' }
])
}
// Total CPU
if (v.value.cpu.total) {
table.rows.push([
{ text: 'Total CPU' },
{ text: v.value.cpu.total.toFixed(2) + '%' }
])
}
// Individual CPUs
if (v.value.cpu.individual) {
for (var idx = 0; idx < v.value.cpu.individual.length; idx++) {
table.rows.push([
{ text: 'CPU #' + (idx + 1) },
{ text: v.value.cpu.individual[idx].toFixed(2) + '%' }
])
}
}
// Append table
tables.push(table)
break
}
}
// Loop through nodes
const nodeStats = []
for (let nodeName in this.nodes) {
// Get node
const n = this.nodes[nodeName]
// Node not in tags
if (n.notInTags) continue
// Loop through stats
for (let statLabel in n.stats) {
// Invalid status
if (n.status !== 'running') continue
// Get stat
const s = n.stats[statLabel]
// Get node stat
let nodeStat = nodeStats.find(s => s.label === statLabel)
if (!nodeStat) {
nodeStat = {
label: statLabel,
nodes: [],
}
nodeStats.push(nodeStat)
}
// Append node value
nodeStat.nodes.push({
funit: s.funit,
fvalue: s.fvalue,
label: n.label,
name: n.name,
value: s.value,
})
}
}
// Order node stats
for (let a = 0; a < nodeStats.length; a++) {
nodeStats[a].nodes.sort((a, b) => {
if (a.value < b.value) return 1
if (a.value === b.value) return 0
return -1
})
}
// Loop through node stats
nodeStats.forEach(s => {
// Create table
let table = {
label: s.label,
rows: [],
}
// Loop through nodes
s.nodes.forEach(n => {
// Append row
table.rows.push([
{
text: n.name,
tooltip: n.label,
},
{ text: n.fvalue + n.funit },
])
})
// Append table
tables.push(table)
})
// Order tables
tables.sort((a, b) => {
if (a.label < b.label) return -1
if (a.label === b.label) return 0
return 1
})
// Get html node
const n = document.querySelector('section #stats')
// Reset html
n.innerHTML = ''
// Loop through tables
tables.forEach(t => {
// Create container
const container = document.createElement('div')
container.className = 'table'
// Create title
const title = document.createElement('div')
title.className = 'title'
title.innerText = t.label
container.appendChild(title)
// Create table
const table = document.createElement('table')
container.appendChild(table)
// Process headers
if (t.headers) {
// Create header
const header = document.createElement('thead')
table.appendChild(header)
// Create row
const row = document.createElement('tr')
header.appendChild(row)
// Loop throughheaders
t.headers.forEach(h => {
// Create column
const column = document.createElement('th')
row.appendChild(column)
// Update text
column.innerText = h
})
}
// Create body
const body = document.createElement('tbody')
table.appendChild(body)
// Loop through rows
t.rows.forEach(r => {
// Create row
const row = document.createElement('tr')
body.appendChild(row)
// Loop through columns
r.forEach(c => {
// Create column
const column = document.createElement('td')
row.appendChild(column)
// Update column
column.innerText = c.text
if (c.tooltip) column.setAttribute('title', c.tooltip)
})
})
// Add container to node
n.appendChild(container)
})
},
/* settings */
toggleSettings() {
document.querySelector('header #settings').style.display = this.settingsDisplayed ? 'none' : 'flex'
this.settingsDisplayed = !this.settingsDisplayed
},
/* helpers */