diff --git a/dashboard/package-lock.json b/dashboard/package-lock.json index 5fbb4cde6..65415f33d 100644 --- a/dashboard/package-lock.json +++ b/dashboard/package-lock.json @@ -19,11 +19,13 @@ "async": "^3.2.6", "bootstrap-sass": "^3.4.3", "chart.js": "^4.4.9", + "chartjs-adapter-moment": "^1.0.1", "eslint": "^9.27.0", "eslint-plugin-vue": "^10.1.0", "filesize": "^10.1.6", "jquery": "^3.7.1", "marked": "^15.0.12", + "moment": "^2.30.1", "moment-timezone": "^0.5.48", "pankow": "^3.0.8", "sass": "^1.89.0", @@ -1582,6 +1584,16 @@ "pnpm": ">=8" } }, + "node_modules/chartjs-adapter-moment": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/chartjs-adapter-moment/-/chartjs-adapter-moment-1.0.1.tgz", + "integrity": "sha512-Uz+nTX/GxocuqXpGylxK19YG4R3OSVf8326D+HwSTsNw1LgzyIGRo+Qujwro1wy6X+soNSnfj5t2vZ+r6EaDmA==", + "license": "MIT", + "peerDependencies": { + "chart.js": ">=3.0.0", + "moment": "^2.10.2" + } + }, "node_modules/chokidar": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.1.tgz", @@ -2250,6 +2262,7 @@ "version": "2.30.1", "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==", + "license": "MIT", "engines": { "node": "*" } @@ -3710,6 +3723,12 @@ "@kurkle/color": "^0.3.0" } }, + "chartjs-adapter-moment": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/chartjs-adapter-moment/-/chartjs-adapter-moment-1.0.1.tgz", + "integrity": "sha512-Uz+nTX/GxocuqXpGylxK19YG4R3OSVf8326D+HwSTsNw1LgzyIGRo+Qujwro1wy6X+soNSnfj5t2vZ+r6EaDmA==", + "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 5a73ce0d6..880ec14e7 100644 --- a/dashboard/package.json +++ b/dashboard/package.json @@ -20,11 +20,13 @@ "async": "^3.2.6", "bootstrap-sass": "^3.4.3", "chart.js": "^4.4.9", + "chartjs-adapter-moment": "^1.0.1", "eslint": "^9.27.0", "eslint-plugin-vue": "^10.1.0", "filesize": "^10.1.6", "jquery": "^3.7.1", "marked": "^15.0.12", + "moment": "^2.30.1", "moment-timezone": "^0.5.48", "pankow": "^3.0.8", "sass": "^1.89.0", diff --git a/dashboard/src/components/SystemMetrics.vue b/dashboard/src/components/SystemMetrics.vue index 73b06bf06..49236f942 100644 --- a/dashboard/src/components/SystemMetrics.vue +++ b/dashboard/src/components/SystemMetrics.vue @@ -11,10 +11,13 @@ import { SingleSelect, Spinner } from 'pankow'; import Section from './Section.vue'; import SystemModel from '../models/SystemModel.js'; +import 'chartjs-adapter-moment'; // https://www.chartjs.org/docs/latest/axes/cartesian/time.html#date-adapters + const systemModel = SystemModel.create(); function trKeyFromPeriod(period) { if (period === 0) return 'app.graphs.period.live'; + if (period === 1) return 'app.graphs.period.1h'; if (period === 6) return 'app.graphs.period.6h'; if (period === 12) return 'app.graphs.period.12h'; if (period === 24) return 'app.graphs.period.24h'; @@ -26,6 +29,7 @@ function trKeyFromPeriod(period) { const periods = [ { id: 0, label: t(trKeyFromPeriod(0)) }, + { id: 1, label: t(trKeyFromPeriod(1)) }, { id: 6, label: t(trKeyFromPeriod(6)) }, { id: 12, label: t(trKeyFromPeriod(12)) }, { id: 24, label: t(trKeyFromPeriod(24)) }, @@ -34,7 +38,7 @@ const periods = [ ]; const busy = ref(false); -const period = ref(0); +const period = ref(24*7); const cpuGraphNode = useTemplateRef('cpuGraphNode'); const memoryGraphNode = useTemplateRef('memoryGraphNode'); @@ -70,11 +74,8 @@ async function refresh() { const [error, result] = await systemModel.getMetrics({ fromSecs: (period.value || 0.1) * 60 * 60, intervalSecs: 300 }); if (error) return console.error(error); - // cpu - const cpuLabels = result.cpu.map(v => { - return moment(v[1]*1000).format('hh:mm'); - }); - + const now = Date.now(); + const cpuLabels = result.cpu.map(v => v[1]*1000); // convert to msecs const cpuData = result.cpu.map(v => v[0]); // already scaled to cpu*100 const cpuGraphData = { @@ -97,23 +98,25 @@ async function refresh() { }, scales: { x: { + 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, 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 - }, - grid: { - drawOnChartArea: false, - }, + } }, y: { + type: 'linear', + min: 0, + max: result.cpuCount * 100, ticks: { callback: (value) => `${value}%`, maxTicksLimit: 6 // max tick labels to show }, - min: 0, - max: result.cpuCount * 100, beginAtZero: true, grid: { drawOnChartArea: false, @@ -130,22 +133,14 @@ async function refresh() { if (cpuGraph) cpuGraph.destroy(); cpuGraph = new Chart(cpuGraphNode.value, { type: 'line', data: cpuGraphData, options: cpuGraphOptions }); - const memoryLabels = result.memory.map(v => { - return moment(v[1]*1000).format('hh:mm'); - }); - - const memoryData = result.memory.map(v => { - return (v[0] / 1024 / 1024 / 1024).toFixed(2); - }); - - const swapData = result.swap.map(v => { // assume that there is 1:1 timeline for swap and memory - return (v[0] / 1024 / 1024 / 1024).toFixed(2); - }); + 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)); const giB = 1024 * 1024 * 1024; - const quarterGiB = 0.25 * giB; - const roundedMemory = Math.round(systemMemory.memory / quarterGiB) * quarterGiB; - const roundedSwap = Math.round(systemMemory.swap / quarterGiB) * quarterGiB; + 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; const memoryGraphData = { labels: memoryLabels, @@ -184,27 +179,28 @@ async function refresh() { }, scales: { x: { + 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, 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 - }, - grid: { - drawOnChartArea: false, - }, + } }, y: { - ticks: { - callback: (value) => `${value} GiB`, - color: (value /* ,index, ticks */) => { - const tickValue = parseFloat(value['tick']['value']); - return ((tickValue * 1024 * 1024 * 1024) > roundedMemory) ? '#ff7d98' : '#46a9ec'; - }, - maxTicksLimit: 6 // max tick labels to show - }, + type: 'linear', min: 0, - max: ((roundedMemory + roundedSwap)/ giB).toFixed(2), // string + 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 + }, beginAtZero: true, stacked: true, grid: {