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-05-22 18:10:46 +02:00
import 'chartjs-adapter-moment' ; // https://www.chartjs.org/docs/latest/axes/cartesian/time.html#date-adapters
2025-05-21 18:35:55 +02:00
const systemModel = SystemModel . create ( ) ;
function trKeyFromPeriod ( period ) {
if ( period === 0 ) return 'app.graphs.period.live' ;
2025-05-22 18:10:46 +02:00
if ( period === 1 ) return 'app.graphs.period.1h' ;
2025-05-21 18:35:55 +02:00
if ( period === 6 ) return 'app.graphs.period.6h' ;
if ( period === 12 ) return 'app.graphs.period.12h' ;
if ( period === 24 ) return 'app.graphs.period.24h' ;
if ( period === 24 * 7 ) return 'app.graphs.period.7d' ;
if ( period === 24 * 30 ) return 'app.graphs.period.30d' ;
return '' ;
}
const periods = [
{ id : 0 , label : t ( trKeyFromPeriod ( 0 ) ) } ,
2025-05-22 18:10:46 +02:00
{ id : 1 , label : t ( trKeyFromPeriod ( 1 ) ) } ,
2025-05-21 18:35:55 +02:00
{ id : 6 , label : t ( trKeyFromPeriod ( 6 ) ) } ,
{ id : 12 , label : t ( trKeyFromPeriod ( 12 ) ) } ,
{ id : 24 , label : t ( trKeyFromPeriod ( 24 ) ) } ,
{ id : 24 * 7 , label : t ( trKeyFromPeriod ( 24 * 7 ) ) } ,
{ id : 24 * 30 , label : t ( trKeyFromPeriod ( 24 * 30 ) ) } ,
] ;
const busy = ref ( false ) ;
2025-05-22 18:17:42 +02:00
const period = ref ( 6 ) ;
2025-05-21 18:35:55 +02:00
const cpuGraphNode = useTemplateRef ( 'cpuGraphNode' ) ;
const memoryGraphNode = useTemplateRef ( 'memoryGraphNode' ) ;
let systemMemory = { } ;
let cpuGraph = null ;
let memoryGraph = null ;
let metricStream = null ;
async function liveRefresh ( ) {
metricStream = await systemModel . getMetricStream ( ) ;
metricStream . onerror = ( error ) => console . log ( 'event stream error:' , error ) ;
metricStream . onmessage = ( message ) => {
const data = JSON . parse ( message . data ) ;
// value can be null if no previous value
if ( data . cpu [ 0 ] ) {
cpuGraph . data . labels . push ( moment ( data . cpu [ 1 ] * 1000 ) . format ( 'hh:mm' ) ) ;
cpuGraph . data . datasets [ 0 ] . data . push ( data . cpu [ 0 ] ) ;
cpuGraph . update ( 'none' ) ;
}
2025-05-22 10:21:21 +02:00
memoryGraph . data . labels . push ( moment ( data . memory [ 1 ] * 1000 ) . format ( 'hh:mm' ) ) ;
memoryGraph . data . datasets [ 0 ] . data . push ( ( data . memory [ 0 ] / 1024 / 1024 / 1024 ) . toFixed ( 2 ) ) ;
memoryGraph . data . datasets [ 1 ] . data . push ( ( data . swap [ 0 ] / 1024 / 1024 / 1024 ) . toFixed ( 2 ) + 2 ) ;
memoryGraph . update ( 'none' ) ;
2025-05-21 18:35:55 +02:00
} ;
}
async function refresh ( ) {
busy . value = true ;
const [ error , result ] = await systemModel . getMetrics ( { fromSecs : ( period . value || 0.1 ) * 60 * 60 , intervalSecs : 300 } ) ;
if ( error ) return console . error ( error ) ;
2025-05-22 18:10:46 +02:00
const now = Date . now ( ) ;
const cpuLabels = result . cpu . map ( v => v [ 1 ] * 1000 ) ; // convert to msecs
2025-05-21 18:35:55 +02:00
const cpuData = result . cpu . map ( v => v [ 0 ] ) ; // already scaled to cpu*100
const cpuGraphData = {
labels : cpuLabels ,
datasets : [ {
label : 'CPU' ,
data : cpuData ,
pointRadius : 0 ,
// https://www.chartjs.org/docs/latest/charts/line.html#line-styling
borderWidth : 1 ,
tension : 0.4 ,
showLine : true ,
fill : true
} ]
} ;
const cpuGraphOptions = {
2025-05-22 19:51:41 +02:00
maintainAspectRatio : false ,
2025-05-21 18:35:55 +02:00
plugins : {
legend : false
} ,
scales : {
x : {
2025-05-22 18:10:46 +02:00
type : 'time' ,
bounds : 'ticks' , // otherwise data bound. https://www.chartjs.org/docs/latest/axes/cartesian/time.html#changing-the-scale-type-from-time-scale-to-logarithmic-linear-scale
min : now - period . value * 60 * 60 * 1000 ,
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
maxTicksLimit : 15 , // max tick labels to show
2025-05-22 18:17:42 +02:00
} ,
grid : {
drawOnChartArea : false ,
} ,
2025-05-21 18:35:55 +02:00
} ,
y : {
2025-05-22 18:10:46 +02:00
type : 'linear' ,
min : 0 ,
max : result . cpuCount * 100 ,
2025-05-21 18:35:55 +02:00
ticks : {
2025-05-22 10:21:21 +02:00
callback : ( value ) => ` ${ value } % ` ,
2025-05-21 18:35:55 +02:00
maxTicksLimit : 6 // max tick labels to show
} ,
beginAtZero : true ,
2025-05-22 16:14:43 +02:00
} ,
2025-05-21 18:35:55 +02:00
} ,
interaction : {
intersect : false ,
mode : 'nearest' ,
axis : 'x'
}
} ;
if ( cpuGraph ) cpuGraph . destroy ( ) ;
cpuGraph = new Chart ( cpuGraphNode . value , { type : 'line' , data : cpuGraphData , options : cpuGraphOptions } ) ;
2025-05-22 18:10:46 +02:00
const memoryLabels = result . memory . map ( v => v [ 1 ] * 1000 ) ; // convert to msecs
const memoryData = result . memory . map ( v => ( v [ 0 ] / 1024 / 1024 / 1024 ) . toFixed ( 2 ) ) ;
// assume that there is 1:1 timeline for swap and memory data
const swapData = result . swap . map ( v => ( v [ 0 ] / 1024 / 1024 / 1024 ) . toFixed ( 2 ) ) ;
2025-05-22 10:21:21 +02:00
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 = {
labels : memoryLabels ,
datasets : [ {
2025-05-22 10:21:21 +02:00
label : 'RAM' ,
2025-05-21 18:35:55 +02:00
data : memoryData ,
2025-05-22 10:21:21 +02:00
stack : 'memory+swap' ,
2025-05-21 18:35:55 +02:00
pointRadius : 0 ,
// https://www.chartjs.org/docs/latest/charts/line.html#line-styling
borderWidth : 1 ,
tension : 0.4 ,
showLine : true ,
2025-05-22 10:21:21 +02:00
fill : true ,
color : '#9ad0f5'
} , {
label : 'Swap' ,
data : swapData ,
stack : 'memory+swap' ,
pointRadius : 0 ,
// https://www.chartjs.org/docs/latest/charts/line.html#line-styling
borderWidth : 1 ,
tension : 0.4 ,
showLine : true ,
fill : true ,
color : '#ffb1c1'
2025-05-21 18:35:55 +02:00
} ]
} ;
const memoryGraphOptions = {
2025-05-22 19:51:41 +02:00
maintainAspectRatio : false ,
2025-05-21 18:35:55 +02:00
plugins : {
2025-05-22 19:51:41 +02:00
legend : false ,
2025-05-21 18:35:55 +02:00
} ,
scales : {
x : {
2025-05-22 18:10:46 +02:00
type : 'time' ,
bounds : 'ticks' , // otherwise data bound. https://www.chartjs.org/docs/latest/axes/cartesian/time.html#changing-the-scale-type-from-time-scale-to-logarithmic-linear-scale
min : now - period . value * 60 * 60 * 1000 ,
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
maxTicksLimit : 15 , // max tick labels to show
2025-05-22 18:17:42 +02:00
} ,
grid : {
drawOnChartArea : false ,
} ,
2025-05-21 18:35:55 +02:00
} ,
y : {
2025-05-22 18:10:46 +02:00
type : 'linear' ,
min : 0 ,
max : ( roundedMemory + roundedSwap ) / giB ,
2025-05-21 18:35:55 +02:00
ticks : {
2025-05-22 18:10:46 +02:00
stepSize : 1 ,
autoSkip : true , // skip tick labels as needed
autoSkipPadding : 20 , // padding between ticks
2025-05-22 10:21:21 +02:00
callback : ( value ) => ` ${ value } GiB ` ,
2025-05-22 18:10:46 +02:00
maxTicksLimit : 8 // max tick labels to show
2025-05-21 18:35:55 +02:00
} ,
beginAtZero : true ,
2025-05-22 10:21:21 +02:00
stacked : true ,
2025-05-21 18:35:55 +02:00
}
} ,
interaction : {
intersect : false ,
mode : 'nearest' ,
axis : 'x'
}
} ;
if ( memoryGraph ) memoryGraph . destroy ( ) ;
memoryGraph = new Chart ( memoryGraphNode . value , { type : 'line' , data : memoryGraphData , options : memoryGraphOptions } ) ;
busy . value = false ;
if ( metricStream ) {
metricStream . close ( ) ;
metricStream = null ;
}
if ( period . value === 0 ) liveRefresh ( ) ;
}
onMounted ( async ( ) => {
const [ error , result ] = await systemModel . memory ( ) ;
if ( error ) return console . error ( error ) ;
systemMemory = result ;
await refresh ( ) ;
} ) ;
2025-05-22 12:26:30 +02:00
onUnmounted ( async ( ) => {
if ( metricStream ) metricStream . close ( ) ;
} ) ;
2025-05-21 18:35:55 +02:00
< / script >
< template >
< Section :title = "$t('system.graphs.title')" >
< template # header -buttons >
< SingleSelect @select ="refresh()" v-model = "period" :options="periods" option-key="id" option-label="label" />
< / template >
2025-05-22 19:51:41 +02:00
2025-05-21 18:35:55 +02:00
< div class = "graphs" >
2025-05-22 19:51:41 +02:00
< label > { { $t ( 'system.cpuUsage.title' ) } } < / label >
< 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-05-22 19:51:41 +02:00
< label > { { $t ( 'system.systemMemory.title' ) } } < / label >
< 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 >
< / 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 >