diff --git a/src/theme.scss b/src/theme.scss index 855e426b9..50c208042 100644 --- a/src/theme.scss +++ b/src/theme.scss @@ -1347,6 +1347,20 @@ footer { } } +// ---------------------------- +// Graphs and system info classes +// ---------------------------- + +.graphs-toolbar { + display: flex; + justify-content: space-between; +} + +.graphs-toolbar-actions { + display: flex; + justify-content: end; +} + // ---------------------------- // System classes // ---------------------------- diff --git a/src/views/system.html b/src/views/system.html index 1507a1f1a..9b69368f1 100644 --- a/src/views/system.html +++ b/src/views/system.html @@ -1,39 +1,66 @@ -
+
-
-

- {{ 'system.title' | tr }} - {{ 'main.action.logs' | tr }} - -

+
+
+

+ {{ 'system.title' | tr }} + {{ 'main.action.logs' | tr }} + +

+
- - -
- - +
+
+ +

+ Graphs + +

+ +
+ + +
+ + +
{{ 'system.systemMemory.graphSubtext' | tr }}
- +
- -
- - -
-
+
+

+ {{ 'system.diskUsage.title' | tr }} +
+ +
+

- -
-
+ +
\ No newline at end of file diff --git a/src/views/system.js b/src/views/system.js index b248ea779..4a6347ca7 100644 --- a/src/views/system.js +++ b/src/views/system.js @@ -9,13 +9,7 @@ angular.module('Application').controller('SystemController', ['$scope', '$locati $scope.config = Client.getConfig(); $scope.memory = null; - $scope.busy = true; - $scope.activeTab = 0; $scope.volumesById = {}; - $scope.disks = []; - $scope.period = 6; - $scope.memoryChart = null; - $scope.diskChart = null; // http://stackoverflow.com/questions/1484506/random-color-generator-in-javascript function getRandomColor() { @@ -34,15 +28,17 @@ angular.module('Application').controller('SystemController', ['$scope', '$locati return getRandomColor(); } - $scope.refresh = function () { - $scope.busy = true; + $scope.disks = { + busy: true, + disks: [], - Client.getSystemGraphs($scope.period * 60, function (error, result) { - if (error) return console.error('Failed to fetch system graphs:', error); + refresh: function () { + $scope.disks.busy = true; - $scope.disks = result.disks; // [ { filesystem, type, size, used, available, capacity, mountpoint }] + var result; + $scope.disks.disks = result.disks; // [ { filesystem, type, size, used, available, capacity, mountpoint }] - $scope.disks.forEach(function (disk) { + $scope.disks.disks.forEach(function (disk) { var usageOther = disk.occupied; colorIndex = 0; @@ -57,7 +53,7 @@ angular.module('Application').controller('SystemController', ['$scope', '$locati disk.contains.sort(function (x, y) { return y.usage - x.usage; }); // sort by usage - if ($scope.disks[0] === disk) { // the root mount point is the first disk. keep this 'contains' in the end + if ($scope.disks.disks[0] === disk) { // the root mount point is the first disk. keep this 'contains' in the end disk.contains.push({ type: 'standard', label: 'Everything else (Ubuntu, Swap, etc)', @@ -76,120 +72,139 @@ angular.module('Application').controller('SystemController', ['$scope', '$locati } }); - // in minutes - var timePeriod = $scope.period * 60; - - // keep in sync with graphs.js - var timeBucketSizeMinutes = timePeriod > (24 * 60) ? (6*60) : 5; - var steps = Math.floor(timePeriod/timeBucketSizeMinutes); - - var labels = new Array(steps).fill(0); - labels = labels.map(function (v, index) { - var dateTime = new Date(Date.now() - ((timePeriod - (index * timeBucketSizeMinutes)) * 60 * 1000)); - - if ($scope.period > 24) { - return dateTime.toLocaleDateString(); - } else { - return dateTime.toLocaleTimeString(); - } - }); - - var borderColors = [ '#2196F3', '#FF6384' ]; - var backgroundColors = [ '#82C4F844', '#FF63844F' ]; - - function fillGraph(canvasId, contents, chartPropertyName, divisor, max) { - if (!contents || !contents[0]) return; // no data available yet - - var datasets = []; - - contents.forEach(function (content, index) { - - // fill holes with previous value - var cur = 0; - content.data.datapoints.forEach(function (d) { - if (d[0] === null) d[0] = cur; - else cur = d[0]; - }); - - var datapoints = Array(steps).map(function () { return '0'; }); - - // walk backwards and fill up the datapoints - content.data.datapoints.reverse().forEach(function (d, index) { - datapoints[datapoints.length-1-index] = (d[0] / divisor).toFixed(2); - // return parseInt((d[0] / divisor).toFixed(2)); - }); - - datasets.push({ - label: content.label, - backgroundColor: backgroundColors[index], - borderColor: borderColors[index%2], // FIXME give real distinct colors - borderWidth: 1, - radius: 0, - data: datapoints, - cubicInterpolationMode: 'monotone', - tension: 0.4 - }); - }); - - var graphData = { - labels: labels, - datasets: datasets - }; - - var options = { - responsive: true, - maintainAspectRatio: true, - aspectRatio: 2.5, - animation: false, - plugins: { - legend: { - display: false - } - }, - interaction: { - intersect: false, - mode: 'index', - }, - scales: { - x: { - ticks: { autoSkipPadding: 50, maxRotation: 0 } - }, - y: { - min: 0, - beginAtZero: true - } - } - }; - - if (max) options.scales.y.max = max; - - var ctx = $(canvasId).get(0).getContext('2d'); - - if ($scope[chartPropertyName]) $scope[chartPropertyName].destroy(); - $scope[chartPropertyName] = new Chart(ctx, { type: 'line', data: graphData, options: options }); - } - - var threshold = 1024 * 1024 * 1024; - var appsWithHighMemory = Object.keys(result.apps).map(function (appId) { - result.apps[appId].id = appId; - return result.apps[appId]; - }).filter(function (app) { - if (!app.memory) return false; // not sure why we get empty objects - return app.memory.datapoints.some(function (d) { return d[0] > threshold; }); - }).map(function (app) { - return { data: app.memory, label: app.id }; - }); - - fillGraph('#graphsCPUChart', [{ data: result.cpu, label: 'CPU' }], 'cpuChart', 100, 1); - fillGraph('#graphsSystemMemoryChart', [{ data: result.memory, label: 'Memory' }].concat(appsWithHighMemory), 'memoryChart', 1024 * 1024, Number.parseInt($scope.memory.memory / 1024 / 1024)); - - $scope.busy = false; - }); + $scope.disks.busy = false; + } }; - $scope.setPeriod = function (hours) { - $scope.period = hours; - $scope.refresh(); + $scope.graphs = { + busy: false, + period: 6, + memoryChart: null, + diskChart: null, + + setPeriod: function (hours) { + $scope.graphs.period = hours; + $scope.graphs.refresh(); + }, + + refresh: function () { + $scope.graphs.busy = true; + + Client.getSystemGraphs($scope.graphs.period * 60, function (error, result) { + if (error) return console.error('Failed to fetch system graphs:', error); + + // in minutes + var timePeriod = $scope.graphs.period * 60; + + // keep in sync with graphs.js + var timeBucketSizeMinutes = timePeriod > (24 * 60) ? (6*60) : 5; + var steps = Math.floor(timePeriod/timeBucketSizeMinutes); + + var labels = new Array(steps).fill(0); + labels = labels.map(function (v, index) { + var dateTime = new Date(Date.now() - ((timePeriod - (index * timeBucketSizeMinutes)) * 60 * 1000)); + + if ($scope.graphs.period > 24) { + return dateTime.toLocaleDateString(); + } else { + return dateTime.toLocaleTimeString(); + } + }); + + var colors = [ '#2196F3', '#FF6384' ]; + + function fillGraph(canvasId, contents, chartPropertyName, divisor, max) { + if (!contents || !contents[0]) return; // no data available yet + + var datasets = []; + + contents.forEach(function (content, index) { + + // fill holes with previous value + var cur = 0; + content.data.datapoints.forEach(function (d) { + if (d[0] === null) d[0] = cur; + else cur = d[0]; + }); + + var datapoints = Array(steps).map(function () { return '0'; }); + + // walk backwards and fill up the datapoints + content.data.datapoints.reverse().forEach(function (d, index) { + datapoints[datapoints.length-1-index] = (d[0] / divisor).toFixed(2); + // return parseInt((d[0] / divisor).toFixed(2)); + }); + + var color = index > 2 ? getRandomColor() : colors[index]; + datasets.push({ + label: content.label, + backgroundColor: color + '4F', + borderColor: color, // FIXME give real distinct colors + borderWidth: 1, + radius: 0, + data: datapoints, + cubicInterpolationMode: 'monotone', + tension: 0.4 + }); + }); + + var graphData = { + labels: labels, + datasets: datasets + }; + + var options = { + responsive: true, + maintainAspectRatio: true, + aspectRatio: 2.5, + animation: false, + plugins: { + legend: { + display: false + } + }, + interaction: { + intersect: false, + mode: 'index', + }, + scales: { + x: { + ticks: { autoSkipPadding: 50, maxRotation: 0 } + }, + y: { + min: 0, + beginAtZero: true + } + } + }; + + if (max) options.scales.y.max = max; + + var ctx = $(canvasId).get(0).getContext('2d'); + + if ($scope.graphs[chartPropertyName]) $scope.graphs[chartPropertyName].destroy(); + $scope.graphs[chartPropertyName] = new Chart(ctx, { type: 'line', data: graphData, options: options }); + } + + // var threshold = 1024 * 1024; + // var appsWithHighMemory = Object.keys(result.apps).map(function (appId) { + // result.apps[appId].id = appId; + // return result.apps[appId]; + // }).filter(function (app) { + // if (!app.memory) return false; // not sure why we get empty objects + // return app.memory.datapoints.some(function (d) { return d[0] > threshold; }); + // }).map(function (app) { + // return { data: app.memory, label: app.id }; + // }); + + var appsWithHighMemory = []; + + fillGraph('#graphsCPUChart', [{ data: result.cpu, label: 'CPU' }], 'cpuChart', 100, 1); + fillGraph('#graphsSystemMemoryChart', [{ data: result.memory, label: 'Memory' }].concat(appsWithHighMemory), 'memoryChart', 1024 * 1024, Number.parseInt($scope.memory.memory / 1024 / 1024)); + + $scope.graphs.busy = false; + }); + } }; Client.onReady(function () { @@ -204,7 +219,8 @@ angular.module('Application').controller('SystemController', ['$scope', '$locati $scope.volumesById = {}; volumes.forEach(function (v) { $scope.volumesById[v.id] = v; }); - $scope.refresh(); + $scope.graphs.refresh(); + // $scope.disks.refresh(); }); }); });