diff --git a/dashboard/package-lock.json b/dashboard/package-lock.json index ab6bd7bca..97e5fd090 100644 --- a/dashboard/package-lock.json +++ b/dashboard/package-lock.json @@ -19,6 +19,7 @@ "async": "^3.2.6", "bootstrap-sass": "^3.4.3", "chart.js": "^4.5.0", + "chartjs-plugin-annotation": "^3.1.0", "eslint": "^9.30.1", "eslint-plugin-vue": "^10.3.0", "filesize": "^10.1.6", @@ -1592,6 +1593,15 @@ "pnpm": ">=8" } }, + "node_modules/chartjs-plugin-annotation": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/chartjs-plugin-annotation/-/chartjs-plugin-annotation-3.1.0.tgz", + "integrity": "sha512-EkAed6/ycXD/7n0ShrlT1T2Hm3acnbFhgkIEJLa0X+M6S16x0zwj1Fv4suv/2bwayCT3jGPdAtI9uLcAMToaQQ==", + "license": "MIT", + "peerDependencies": { + "chart.js": ">=4.0.0" + } + }, "node_modules/chokidar": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.1.tgz", @@ -3735,6 +3745,12 @@ "@kurkle/color": "^0.3.0" } }, + "chartjs-plugin-annotation": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/chartjs-plugin-annotation/-/chartjs-plugin-annotation-3.1.0.tgz", + "integrity": "sha512-EkAed6/ycXD/7n0ShrlT1T2Hm3acnbFhgkIEJLa0X+M6S16x0zwj1Fv4suv/2bwayCT3jGPdAtI9uLcAMToaQQ==", + "requires": {} + }, "chokidar": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.1.tgz", diff --git a/dashboard/package.json b/dashboard/package.json index 5a3c810f0..714911350 100644 --- a/dashboard/package.json +++ b/dashboard/package.json @@ -20,6 +20,7 @@ "async": "^3.2.6", "bootstrap-sass": "^3.4.3", "chart.js": "^4.5.0", + "chartjs-plugin-annotation": "^3.1.0", "eslint": "^9.30.1", "eslint-plugin-vue": "^10.3.0", "filesize": "^10.1.6", diff --git a/dashboard/src/components/GraphItem.vue b/dashboard/src/components/GraphItem.vue index 247f266ce..42f78c3d9 100644 --- a/dashboard/src/components/GraphItem.vue +++ b/dashboard/src/components/GraphItem.vue @@ -5,6 +5,9 @@ import moment from 'moment-timezone'; import { onMounted, onUnmounted, useTemplateRef, watch } from 'vue'; import Chart from 'chart.js/auto'; import { prettyDecimalSize } from 'pankow/utils'; +import annotationPlugin from 'chartjs-plugin-annotation'; + +Chart.register(annotationPlugin); const LIVE_REFRESH_INTERVAL_MSECS = 500; const LIVE_REFRESH_HISTORY_MSECS = 5*60*1000; // last 5 mins @@ -28,11 +31,12 @@ const props = defineProps({ validator: (val) => Array.isArray(val) && val.every(item => typeof item === 'string') }, yscale: String, // cpu, memory - memory: Object, - cpu: Object, + memory: Number, + cpuCount: Number, + highMark: Number, }); -function createGraphOptions({ yscale, period, displayLegend }) { +function createGraphOptions({ yscale, period, displayLegend, highMark }) { const now = Date.now(); return { @@ -50,6 +54,18 @@ function createGraphOptions({ yscale, period, displayLegend }) { return `${datasetLabel}: ${yscale.ticks.callback(tooltipItem.raw.y)}`; } } + }, + annotation: { + annotations: { + high: { + display: highMark !== null, + type: 'line', + yMin: highMark, + yMax: highMark, + borderColor: 'rgb(75, 192, 192)', + borderWidth: 0.5 + } + } } }, scales: { @@ -87,7 +103,7 @@ function transformData(data) { const x = data[1]*1000; // convert to msecs let y; if (props.yscale === 'memory') { - y = yscaleUnit === 'GiB' ? (data[0] / 1024 / 1024 / 1024).toFixed(2) : (data[0] / 1024 / 1024).toFixed(2); + y = yscaleUnit === 'GiB' ? data[0] / 1024 / 1024 / 1024 : data[0] / 1024 / 1024; } else { // in non-memory case, for relative values like cpu, if null make it 0 y = data[0] || 0; } @@ -146,19 +162,20 @@ function onPeriodChanged() { // CPU and Memory graph have known min/max set and auto-scaling gets disabled // Disk and Network graphs auto-scale the y values. - let yscale = null; + let yscale = null, highMark = null; if (props.yscale === 'cpu') { yscale = { type: 'linear', min: 0, - max: 4 * 100, + max: props.cpuCount * 100, ticks: { - callback: (value) => `${value.toFixed(2)}%`, + callback: (value) => `${value.toFixed(2).replace('.00', '')}%`, maxTicksLimit: 6 // max tick labels to show }, beginAtZero: true, }; + if (props.highMark) highMark = props.highMark * 100; } else if (props.yscale === 'memory') { const giB = 1024 * 1024 * 1024; const roundedMemoryGiB = Math.ceil(props.memory / giB); @@ -179,6 +196,8 @@ function onPeriodChanged() { beginAtZero: true, stacked: true, }; + + if (props.highMark) highMark = yscaleUnit === 'GiB' ? props.highMark/giB : props.highMark/(1024*1024); } else if (props.yscale === 'disk') { yscale = { type: 'linear', @@ -206,7 +225,7 @@ function onPeriodChanged() { } // this sets a min 'x' based on current timestamp. so it has to re-created every time the period changes - const graphOptions = createGraphOptions({ yscale, period: props.period, displayLegend: props.datasetLabels.length > 1 }); + const graphOptions = createGraphOptions({ yscale, period: props.period, displayLegend: props.datasetLabels.length > 1, highMark }); if (!graph) { graph = new Chart(graphNode.value, { type: 'line', data, options: graphOptions }); diff --git a/dashboard/src/components/SystemMetrics.vue b/dashboard/src/components/SystemMetrics.vue index be372b368..5c13065f9 100644 --- a/dashboard/src/components/SystemMetrics.vue +++ b/dashboard/src/components/SystemMetrics.vue @@ -120,7 +120,7 @@ onUnmounted(async () => { yscale="cpu" :dataset-labels="['CPU']" :dataset-colors="['#9ad0f5']" - :cpu="systemCpus" + :cpu-count="systemCpus.length" > diff --git a/dashboard/src/components/app/Graphs.vue b/dashboard/src/components/app/Graphs.vue index c1d01a5fa..8968a52cb 100644 --- a/dashboard/src/components/app/Graphs.vue +++ b/dashboard/src/components/app/Graphs.vue @@ -112,7 +112,8 @@ onUnmounted(async () => { yscale="cpu" :dataset-labels="['CPU']" :dataset-colors="['#9ad0f5']" - :cpu="systemCpus" + :cpu-count="systemCpus.length" + :high-mark="systemCpus.length * app.cpuQuota/100" > @@ -124,6 +125,7 @@ onUnmounted(async () => { :dataset-labels="['Memory']" :dataset-colors="['#9ad0f5']" :memory="appMemory" + :high-mark="appMemory" >