2025-05-21 18:35:55 +02:00
< script setup >
import { useI18n } from 'vue-i18n' ;
const i18n = useI18n ( ) ;
const t = i18n . t ;
2025-05-22 12:26:30 +02:00
import { ref , onMounted , onUnmounted , useTemplateRef } from 'vue' ;
2025-05-21 18:35:55 +02:00
import Chart from 'chart.js/auto' ;
import moment from 'moment-timezone' ;
import { SingleSelect , Spinner } from 'pankow' ;
import Section from './Section.vue' ;
import SystemModel from '../models/SystemModel.js' ;
2025-07-01 22:32:59 +02:00
import { prettyDecimalSize } from 'pankow/utils' ;
2025-05-21 18:35:55 +02:00
const systemModel = SystemModel . create ( ) ;
const periods = [
2025-05-23 15:08:01 +02:00
{ id : 0 , label : t ( 'app.graphs.period.live' ) , format : 'hh:mm A' , tooltipFormat : 'hh:mm A' } ,
{ id : 1 , label : t ( 'app.graphs.period.1h' ) , format : 'hh:mm A' , tooltipFormat : 'hh:mm A' } ,
{ id : 6 , label : t ( 'app.graphs.period.6h' ) , format : 'hh:mm A' , tooltipFormat : 'hh:mm A' } ,
{ id : 12 , label : t ( 'app.graphs.period.12h' ) , format : 'hh:mm A' , tooltipFormat : 'hh:mm A' } ,
{ id : 24 , label : t ( 'app.graphs.period.24h' ) , format : 'hh:mm A' , tooltipFormat : 'hh:mm A' } ,
{ id : 24 * 7 , label : t ( 'app.graphs.period.7d' ) , format : 'DD MMM' , tooltipFormat : 'DD MMM hh:mm A' } ,
{ id : 24 * 30 , label : t ( 'app.graphs.period.30d' ) , format : 'DD MMM' , tooltipFormat : 'DD MMM hh:mm A' } ,
2025-05-21 18:35:55 +02:00
] ;
const busy = ref ( false ) ;
2025-05-23 11:40:25 +02:00
const period = ref ( 0 ) ;
2025-05-21 18:35:55 +02:00
const cpuGraphNode = useTemplateRef ( 'cpuGraphNode' ) ;
const memoryGraphNode = useTemplateRef ( 'memoryGraphNode' ) ;
2025-07-01 22:32:59 +02:00
const networkGraphNode = useTemplateRef ( 'networkGraphNode' ) ;
const diskGraphNode = useTemplateRef ( 'diskGraphNode' ) ;
const networkReadTotal = ref ( 0 ) ;
const networkWriteTotal = ref ( 0 ) ;
const blockReadTotal = ref ( 0 ) ;
const blockWriteTotal = ref ( 0 ) ;
2025-05-21 18:35:55 +02:00
let systemMemory = { } ;
2025-05-22 18:45:05 +02:00
let systemCpus = { } ;
2025-05-21 18:35:55 +02:00
let cpuGraph = null ;
let memoryGraph = null ;
2025-07-01 22:32:59 +02:00
let diskGraph = null ;
let networkGraph = null ;
2025-05-21 18:35:55 +02:00
let metricStream = null ;
2025-05-23 11:40:25 +02:00
const LIVE _REFRESH _INTERVAL _MSECS = 500 ;
const LIVE _REFRESH _HISTORY _MSECS = 5 * 60 * 1000 ; // last 5 mins
2025-05-23 16:11:48 +02:00
function pruneGraphData ( dataset , options ) {
while ( dataset . data . length && ( dataset . data [ 0 ] . x < options . scales . x . min ) ) { // remove elements beyond our tme window
dataset . data . shift ( ) ;
}
}
2025-05-21 18:35:55 +02:00
async function liveRefresh ( ) {
2025-05-23 11:40:25 +02:00
metricStream = await systemModel . getMetricStream ( LIVE _REFRESH _INTERVAL _MSECS ) ;
2025-05-21 18:35:55 +02:00
metricStream . onerror = ( error ) => console . log ( 'event stream error:' , error ) ;
metricStream . onmessage = ( message ) => {
const data = JSON . parse ( message . data ) ;
2025-07-01 22:32:59 +02:00
///////////// CPU Graph
2025-05-23 16:11:48 +02:00
if ( data . cpu [ 0 ] ) { // since cpu% is relative, value can be null if no previous value
2025-05-23 11:40:25 +02:00
cpuGraph . data . datasets [ 0 ] . data . push ( {
2025-05-23 13:24:45 +02:00
x : data . cpu [ 1 ] * 1000 , // cpuGraph.options.scales.x.max can be used for window edge, if we don't trust server timestamps . but using server timestamps handles network lags better
2025-05-23 11:40:25 +02:00
y : data . cpu [ 0 ]
} ) ;
2025-05-23 15:08:01 +02:00
2025-05-23 16:11:48 +02:00
pruneGraphData ( cpuGraph . data . datasets [ 0 ] , cpuGraph . options ) ;
2025-05-21 18:35:55 +02:00
cpuGraph . update ( 'none' ) ;
}
2025-07-01 22:32:59 +02:00
///////////// Memory Graph
2025-05-23 16:11:48 +02:00
memoryGraph . data . datasets [ 0 ] . data . push ( {
x : data . memory [ 1 ] * 1000 ,
y : ( data . memory [ 0 ] / 1024 / 1024 / 1024 ) . toFixed ( 2 )
} ) ;
pruneGraphData ( memoryGraph . data . datasets [ 0 ] , memoryGraph . options ) ;
memoryGraph . data . datasets [ 1 ] . data . push ( {
x : data . swap [ 1 ] * 1000 ,
y : ( data . swap [ 0 ] / 1024 / 1024 / 1024 ) . toFixed ( 2 )
} ) ;
pruneGraphData ( memoryGraph . data . datasets [ 1 ] , memoryGraph . options ) ;
2025-05-22 10:21:21 +02:00
memoryGraph . update ( 'none' ) ;
2025-07-01 22:32:59 +02:00
///////////// Disk Graph
diskGraph . data . datasets [ 0 ] . data . push ( {
x : data . blockReadRate [ 1 ] * 1000 ,
y : data . blockReadRate [ 0 ]
} ) ;
pruneGraphData ( memoryGraph . data . datasets [ 0 ] , memoryGraph . options ) ;
diskGraph . data . datasets [ 1 ] . data . push ( {
x : data . blockWriteRate [ 1 ] * 1000 ,
y : data . blockWriteRate [ 0 ]
} ) ;
pruneGraphData ( diskGraph . data . datasets [ 1 ] , diskGraph . options ) ;
diskGraph . update ( 'none' ) ;
blockReadTotal . value = prettyDecimalSize ( data . blockReadTotal ) ;
blockWriteTotal . value = prettyDecimalSize ( data . blockWriteTotal ) ;
///////////// Network Graph
networkGraph . data . datasets [ 0 ] . data . push ( {
x : data . networkReadRate [ 1 ] * 1000 ,
y : data . networkReadRate [ 0 ]
} ) ;
pruneGraphData ( memoryGraph . data . datasets [ 0 ] , memoryGraph . options ) ;
networkGraph . data . datasets [ 1 ] . data . push ( {
x : data . networkWriteRate [ 1 ] * 1000 ,
y : data . networkWriteRate [ 0 ]
} ) ;
pruneGraphData ( networkGraph . data . datasets [ 1 ] , networkGraph . options ) ;
networkGraph . update ( 'none' ) ;
networkReadTotal . value = prettyDecimalSize ( data . networkReadTotal ) ;
networkWriteTotal . value = prettyDecimalSize ( data . networkWriteTotal ) ;
2025-05-21 18:35:55 +02:00
} ;
2025-05-23 15:08:01 +02:00
// advances the time window by 500ms. this is independent of incoming data
2025-05-23 11:40:25 +02:00
metricStream . intervalId = setInterval ( function ( ) {
2025-07-01 22:32:59 +02:00
for ( const graph of [ cpuGraph , memoryGraph , diskGraph , networkGraph ] ) {
graph . options . scales . x . min += LIVE _REFRESH _INTERVAL _MSECS ;
graph . options . scales . x . max += LIVE _REFRESH _INTERVAL _MSECS ;
graph . update ( 'none' ) ;
}
2025-05-23 11:40:25 +02:00
} , LIVE _REFRESH _INTERVAL _MSECS ) ;
2025-05-21 18:35:55 +02:00
}
2025-05-23 11:40:25 +02:00
async function getMetrics ( hours ) {
2025-07-01 22:32:59 +02:00
const metrics = {
cpu : [ ] ,
memory : [ ] ,
swap : [ ] ,
blockReadRate : [ ] ,
blockWriteRate : [ ] ,
networkReadRate : [ ] ,
networkWriteRate : [ ] ,
// these are just scalars and not timeseries
blockReadTotal : 0 ,
blockWriteTotal : 0 ,
networkReadTotal : 0 ,
networkWriteTotal : 0
} ;
if ( hours === 0 ) return metrics ; // empty result. values will come from stream and not graphite
2025-05-21 18:35:55 +02:00
2025-05-23 11:40:25 +02:00
const [ error , result ] = await systemModel . getMetrics ( { fromSecs : hours * 60 * 60 , intervalSecs : 300 } ) ;
2025-05-21 18:35:55 +02:00
if ( error ) return console . error ( error ) ;
2025-05-22 20:54:42 +02:00
// time is converted to msecs . cpu is already scaled to cpu*100
2025-07-01 22:32:59 +02:00
metrics . cpu = result . cpu . map ( v => { return { x : v [ 1 ] * 1000 , y : v [ 0 ] } ; } ) ;
2025-05-21 18:35:55 +02:00
2025-07-01 22:32:59 +02:00
metrics . memory = result . memory . map ( v => {
2025-05-22 20:54:42 +02:00
return {
x : v [ 1 ] * 1000 ,
y : ( v [ 0 ] / 1024 / 1024 / 1024 ) . toFixed ( 2 )
} ;
} ) ;
2025-05-23 16:11:48 +02:00
2025-07-01 22:32:59 +02:00
metrics . swap = result . swap . map ( v => {
2025-05-22 20:54:42 +02:00
return {
x : v [ 1 ] * 1000 ,
y : ( v [ 0 ] / 1024 / 1024 / 1024 ) . toFixed ( 2 )
} ;
} ) ;
2025-07-01 22:32:59 +02:00
metrics . blockReadRate = result . blockReadRate . map ( v => {
return {
x : v [ 1 ] * 1000 ,
y : v [ 0 ]
} ;
} ) ;
metrics . blockWriteRate = result . blockWriteRate . map ( v => {
return {
x : v [ 1 ] * 1000 ,
y : v [ 0 ]
} ;
} ) ;
metrics . networkReadRate = result . networkReadRate . map ( v => {
return {
x : v [ 1 ] * 1000 ,
y : v [ 0 ]
} ;
} ) ;
metrics . networkWriteRate = result . networkWriteRate . map ( v => {
return {
x : v [ 1 ] * 1000 ,
y : v [ 0 ]
} ;
} ) ;
metrics . networkReadTotal = result . networkReadTotal ;
metrics . networkWriteTotal = result . networkWriteTotal ;
metrics . blockReadTotal = result . blockReadTotal ;
metrics . blockWriteTotal = result . blockWriteTota ;
return metrics ;
2025-05-22 18:45:05 +02:00
}
2025-07-01 22:15:13 +02:00
function createGraphOptions ( { yscale , realtime } ) {
2025-05-22 18:45:05 +02:00
const now = Date . now ( ) ;
2025-07-01 22:15:13 +02:00
return {
2025-05-22 19:51:41 +02:00
maintainAspectRatio : false ,
2025-05-21 18:35:55 +02:00
plugins : {
2025-05-23 15:08:01 +02:00
legend : {
display : false
} ,
tooltip : {
callbacks : {
2025-07-02 10:21:05 +02:00
title : ( tooltipItem ) => moment ( tooltipItem [ 0 ] . raw . x ) . format ( periods . find ( ( p ) => p . id === period . value ) . tooltipFormat ) ,
label : ( tooltipItem ) => yscale . ticks . callback ( tooltipItem . raw . y )
2025-05-23 15:08:01 +02:00
}
}
2025-05-21 18:35:55 +02:00
} ,
scales : {
x : {
2025-05-23 15:08:01 +02:00
// we used to use 'time' type but it relies on the data to generate ticks. we may not have data for our time periods
type : 'linear' ,
2025-05-23 11:40:25 +02:00
min : now - ( period . value === 0 ? LIVE _REFRESH _HISTORY _MSECS : period . value * 60 * 60 * 1000 ) ,
2025-05-22 18:10:46 +02:00
max : now ,
2025-05-21 18:35:55 +02:00
ticks : {
autoSkip : true , // skip tick labels as needed
autoSkipPadding : 20 , // padding between ticks
maxRotation : 0 , // don't rotate the labels
2025-05-23 15:08:01 +02:00
count : 7 , // tick labels to show. anything more than 7 will not work for "7 days"
callback : function ( value ) {
if ( period . value === 0 ) return ` ${ 5 - ( value - this . min ) / 60000 } min ` ;
return moment ( value ) . format ( periods . find ( ( p ) => p . id === period . value ) . format ) ;
2025-07-01 22:15:13 +02:00
} ,
stepSize : realtime ? 60 * 1000 : null // // for realtime graph, generate steps of 1min and appropriate tick text
2025-05-22 18:17:42 +02:00
} ,
grid : {
drawOnChartArea : false ,
} ,
2025-05-21 18:35:55 +02:00
} ,
2025-07-01 22:15:13 +02:00
y : yscale ,
2025-05-21 18:35:55 +02:00
} ,
interaction : {
intersect : false ,
mode : 'nearest' ,
axis : 'x'
}
} ;
2025-07-01 22:15:13 +02:00
}
2025-05-21 18:35:55 +02:00
2025-07-02 10:21:05 +02:00
// CPU and Memory graph have known min/max set and auto-scaling gets disabled
// Disk and Network graphs auto-scale the y values.
2025-07-01 22:32:59 +02:00
async function onPeriodChange ( ) {
const metrics = await getMetrics ( period . value ) ;
2025-07-01 22:15:13 +02:00
2025-07-01 22:32:59 +02:00
///////////// CPU Graph
2025-07-01 22:15:13 +02:00
const cpuGraphData = {
datasets : [ {
label : 'CPU' ,
2025-07-01 22:32:59 +02:00
data : metrics . cpu ,
2025-07-01 22:15:13 +02:00
pointRadius : 0 ,
2025-07-01 22:32:59 +02:00
borderWidth : 1 , // https://www.chartjs.org/docs/latest/charts/line.html#line-styling
2025-07-01 22:15:13 +02:00
tension : 0.4 ,
showLine : true ,
fill : true
} ]
} ;
const cpuYscale = {
type : 'linear' ,
min : 0 ,
max : systemCpus . length * 100 ,
ticks : {
callback : ( value ) => ` ${ value } % ` ,
maxTicksLimit : 6 // max tick labels to show
} ,
beginAtZero : true ,
} ;
const cpuGraphOptions = createGraphOptions ( { yscale : cpuYscale , realtime : period . value === 0 } ) ;
2025-05-23 11:40:25 +02:00
2025-05-23 13:24:45 +02:00
if ( ! cpuGraph ) {
cpuGraph = new Chart ( cpuGraphNode . value , { type : 'line' , data : cpuGraphData , options : cpuGraphOptions } ) ;
} else {
cpuGraph . data = cpuGraphData ;
cpuGraph . options = cpuGraphOptions ;
cpuGraph . update ( 'none' ) ;
}
2025-05-21 18:35:55 +02:00
2025-07-01 22:32:59 +02:00
///////////// Memory Graph
2025-05-21 18:35:55 +02:00
const giB = 1024 * 1024 * 1024 ;
2025-05-22 18:10:46 +02:00
const roundedMemory = Math . ceil ( systemMemory . memory / giB ) * giB ; // we have to scale up so that the graph can show the data!
const roundedSwap = Math . ceil ( systemMemory . swap / giB ) * giB ;
2025-05-21 18:35:55 +02:00
const memoryGraphData = {
datasets : [ {
2025-05-22 10:21:21 +02:00
label : 'RAM' ,
2025-07-01 22:32:59 +02:00
data : metrics . memory ,
2025-05-22 10:21:21 +02:00
stack : 'memory+swap' ,
2025-05-21 18:35:55 +02:00
pointRadius : 0 ,
2025-07-01 22:32:59 +02:00
borderWidth : 1 , // https://www.chartjs.org/docs/latest/charts/line.html#line-styling
2025-05-21 18:35:55 +02:00
tension : 0.4 ,
showLine : true ,
2025-05-22 10:21:21 +02:00
fill : true ,
color : '#9ad0f5'
} , {
label : 'Swap' ,
2025-07-01 22:32:59 +02:00
data : metrics . swap ,
2025-05-22 10:21:21 +02:00
stack : 'memory+swap' ,
pointRadius : 0 ,
2025-07-01 22:32:59 +02:00
borderWidth : 1 , // https://www.chartjs.org/docs/latest/charts/line.html#line-styling
2025-05-22 10:21:21 +02:00
tension : 0.4 ,
showLine : true ,
fill : true ,
color : '#ffb1c1'
2025-05-21 18:35:55 +02:00
} ]
} ;
2025-07-01 22:15:13 +02:00
const memoryYscale = {
type : 'linear' ,
min : 0 ,
max : ( roundedMemory + roundedSwap ) / giB ,
ticks : {
stepSize : 1 ,
autoSkip : true , // skip tick labels as needed
autoSkipPadding : 20 , // padding between ticks
callback : ( value ) => ` ${ value } GiB ` ,
maxTicksLimit : 8 // max tick labels to show
2025-05-21 18:35:55 +02:00
} ,
2025-07-01 22:15:13 +02:00
beginAtZero : true ,
stacked : true ,
2025-05-21 18:35:55 +02:00
} ;
2025-07-01 22:15:13 +02:00
const memoryGraphOptions = createGraphOptions ( { yscale : memoryYscale , realtime : period . value === 0 } ) ;
2025-05-23 16:11:48 +02:00
if ( ! memoryGraph ) {
memoryGraph = new Chart ( memoryGraphNode . value , { type : 'line' , data : memoryGraphData , options : memoryGraphOptions } ) ;
} else {
memoryGraph . data = memoryGraphData ;
memoryGraph . options = memoryGraphOptions ;
memoryGraph . update ( 'none' ) ;
}
2025-05-21 18:35:55 +02:00
2025-07-01 22:32:59 +02:00
///////////// Disk Graph
const diskGraphData = {
datasets : [ {
label : 'Block Read' ,
data : metrics . blockReadRate ,
stack : 'blockread' ,
pointRadius : 0 ,
borderWidth : 1 , // https://www.chartjs.org/docs/latest/charts/line.html#line-styling
tension : 0.4 ,
showLine : true ,
fill : true ,
color : '#9ad0f5'
} , {
label : 'Block Write' ,
data : metrics . blockWriteRate ,
stack : 'blockwrite' ,
pointRadius : 0 ,
borderWidth : 1 , // https://www.chartjs.org/docs/latest/charts/line.html#line-styling
tension : 0.4 ,
showLine : true ,
fill : true ,
color : '#ffb1c1'
} ]
} ;
const diskYscale = {
type : 'linear' ,
min : 0 ,
2025-07-02 10:21:05 +02:00
grace : 100 * 1000 , // add 100kBps. otherwise, the yaxis auto-scales to data and the values appear too dramatic
2025-07-01 22:32:59 +02:00
ticks : {
2025-07-02 10:21:05 +02:00
callback : ( value ) => ` ${ prettyDecimalSize ( value ) } ps ` ,
2025-07-01 22:32:59 +02:00
maxTicksLimit : 6 // max tick labels to show
} ,
beginAtZero : true ,
stacked : false ,
} ;
const diskGraphOptions = createGraphOptions ( { yscale : diskYscale , realtime : period . value === 0 } ) ;
if ( ! diskGraph ) {
diskGraph = new Chart ( diskGraphNode . value , { type : 'line' , data : diskGraphData , options : diskGraphOptions } ) ;
} else {
diskGraph . data = diskGraphData ;
diskGraph . options = diskGraphOptions ;
diskGraph . update ( 'none' ) ;
}
///////////// Network Graph
const networkGraphData = {
datasets : [ {
label : 'RX' ,
data : metrics . networkReadRate ,
stack : 'networkread' ,
pointRadius : 0 ,
borderWidth : 1 , // https://www.chartjs.org/docs/latest/charts/line.html#line-styling
tension : 0.4 ,
showLine : true ,
fill : true ,
color : '#9ad0f5'
} , {
label : 'TX' ,
data : metrics . networkWriteRate ,
stack : 'networkwrite' ,
pointRadius : 0 ,
borderWidth : 1 , // https://www.chartjs.org/docs/latest/charts/line.html#line-styling
tension : 0.4 ,
showLine : true ,
fill : true ,
color : '#ffb1c1'
} ]
} ;
const networkYscale = {
type : 'linear' ,
min : 0 ,
2025-07-02 10:21:05 +02:00
grace : 50 * 1000 , // add 50kBps. otherwise, the yaxis auto-scales to data and the values appear too dramatic
2025-07-01 22:32:59 +02:00
ticks : {
2025-07-02 10:21:05 +02:00
callback : ( value ) => ` ${ prettyDecimalSize ( value ) } ps ` ,
2025-07-01 22:32:59 +02:00
maxTicksLimit : 6 // max tick labels to show
} ,
beginAtZero : true ,
stacked : false ,
} ;
const networkGraphOptions = createGraphOptions ( { yscale : networkYscale , realtime : period . value === 0 } ) ;
if ( ! networkGraph ) {
networkGraph = new Chart ( networkGraphNode . value , { type : 'line' , data : networkGraphData , options : networkGraphOptions } ) ;
} else {
networkGraph . data = networkGraphData ;
networkGraph . options = networkGraphOptions ;
networkGraph . update ( 'none' ) ;
}
///////////// Scalars
networkReadTotal . value = prettyDecimalSize ( metrics . networkReadTotal ) ;
networkWriteTotal . value = prettyDecimalSize ( metrics . networkWriteTotal ) ;
blockReadTotal . value = prettyDecimalSize ( metrics . blockReadTotal ) ;
blockWriteTotal . value = prettyDecimalSize ( metrics . blockWriteTotal ) ;
2025-05-21 18:35:55 +02:00
if ( metricStream ) {
2025-05-23 11:40:25 +02:00
clearInterval ( metricStream . intervalId ) ;
2025-05-21 18:35:55 +02:00
metricStream . close ( ) ;
metricStream = null ;
}
if ( period . value === 0 ) liveRefresh ( ) ;
}
onMounted ( async ( ) => {
2025-05-22 18:45:05 +02:00
let error , result ;
[ error , result ] = await systemModel . memory ( ) ;
2025-05-21 18:35:55 +02:00
if ( error ) return console . error ( error ) ;
systemMemory = result ;
2025-05-22 18:45:05 +02:00
[ error , result ] = await systemModel . cpus ( ) ;
if ( error ) return console . error ( error ) ;
systemCpus = result ;
2025-07-01 22:32:59 +02:00
await onPeriodChange ( ) ;
2025-05-21 18:35:55 +02:00
} ) ;
2025-05-22 12:26:30 +02:00
onUnmounted ( async ( ) => {
2025-05-23 11:40:25 +02:00
if ( metricStream ) {
clearInterval ( metricStream . intervalId ) ;
metricStream . close ( ) ;
}
2025-05-22 12:26:30 +02:00
} ) ;
2025-05-21 18:35:55 +02:00
< / script >
< template >
< Section :title = "$t('system.graphs.title')" >
< template # header -buttons >
2025-07-01 22:32:59 +02:00
< SingleSelect @select ="onPeriodChange()" v-model = "period" :options="periods" option-key="id" option-label="label" />
2025-05-21 18:35:55 +02:00
< / template >
2025-05-22 19:51:41 +02:00
2025-05-21 18:35:55 +02:00
< div class = "graphs" >
2025-07-02 12:19:36 +02:00
< label > { { $t ( 'system.cpuUsage.title' ) } } < span class = "pull-right text-small" > { { systemCpus . length ? ` ${ systemCpus . length } Core " ${ systemCpus [ 0 ] . model } " ` : '' } } < / span > < / label >
2025-05-22 19:51:41 +02:00
< div style = "text-align: center" v-if = "busy" > < Spinner / > < / div >
< div class = "graph" >
2025-05-21 18:35:55 +02:00
< canvas v-show = "!busy" ref="cpuGraphNode" > < / canvas >
< / div >
2025-07-02 12:19:36 +02:00
< label style = "margin-top: 10px; display: block;" > { { $t ( 'system.systemMemory.title' ) } } < span class = "pull-right text-small" > RAM : { { prettyDecimalSize ( systemMemory . memory ) } } Swap : { { prettyDecimalSize ( systemMemory . swap ) } } < / span > < / label >
2025-05-22 19:51:41 +02:00
< div style = "text-align: center" v-if = "busy" > < Spinner / > < / div >
< div class = "graph" >
2025-05-21 18:35:55 +02:00
< canvas v-show = "!busy" ref="memoryGraphNode" > < / canvas >
< / div >
2025-07-01 22:32:59 +02:00
< label style = "margin-top: 10px; display: block;" > Disk I / O < span class = "pull-right text-small" > { { $t ( 'app.graphs.diskIOTotal' , { read : blockReadTotal , write : blockWriteTotal } ) } } < / span > < / label >
< div style = "text-align: center" v-if = "busy" > < Spinner / > < / div >
< div class = "graph" >
< canvas v-show = "!busy" ref="diskGraphNode" > < / canvas >
< / div >
< label style = "margin-top: 10px; display: block;" > Network I / O < span class = "pull-right text-small" > { { $t ( 'app.graphs.networkIOTotal' , { inbound : networkReadTotal , outbound : networkWriteTotal } ) } } < / span > < / label >
< div style = "text-align: center" v-if = "busy" > < Spinner / > < / div >
< div class = "graph" >
< canvas v-show = "!busy" ref="networkGraphNode" > < / canvas >
< / div >
2025-05-21 18:35:55 +02:00
< / div >
< / Section >
< / template >
< style scoped >
2025-05-22 19:51:41 +02:00
. graphs label {
margin : 16 px 0 ;
}
. graph {
position : relative ;
width : 100 % ;
height : 160 px ;
2025-05-21 18:35:55 +02:00
}
< / style >