Mercurial > hg > thermostat-ng > web-client
changeset 200:42e34f861a44
Extract system-memory into component
Reviewed-by: jkang
Review-thread: http://icedtea.classpath.org/pipermail/thermostat/2017-September/024928.html
line wrap: on
line diff
--- a/src/app/components/system-info/en.locale.yaml Mon Sep 11 11:26:31 2017 -0400 +++ b/src/app/components/system-info/en.locale.yaml Wed Sep 13 16:27:00 2017 -0400 @@ -18,30 +18,3 @@ networkTable: HEADER: Network Interfaces - - refresh: - DISABLED: Disabled - SECONDS: '{SECONDS, plural, =0{0 Seconds} one{1 Second} other{# Seconds}}{DEFAULT, select, true{ (Default)} other{}}' - - dataAge: - SECONDS: '{SECONDS, plural, =0{0 Seconds} one{1 Second} other{# Seconds}}{DEFAULT, select, true{ (Default)} other{}}' - MINUTES: '{MINUTES, plural, =0{0 Minutes} one{1 Minute} other{# Minutes}}{DEFAULT, select, true{ (Default)} other{}}' - - systemMemory: - DONUT_CHART_LABEL: Memory Usage - LINE_CHART_LABEL: Memory - REFRESH_RATE_LABEL: Refresh Rate - MAX_DATA_AGE_LABEL: Max Data Age - - X_AXIS_LABEL: Time - Y_AXIS_LABEL: Size (MiB) - TOOLTIP_FMT: '{{size}} MiB' - - xAxisTypes: - TIMESTAMP: timestamp - TOTAL: Total Memory - FREE: Free Memory - USED: Used Memory - SWAP_TOTAL: Total Swap - SWAP_FREE: Free Swap - BUFFERS: Buffers
--- a/src/app/components/system-info/system-info.html Mon Sep 11 11:26:31 2017 -0400 +++ b/src/app/components/system-info/system-info.html Wed Sep 13 16:27:00 2017 -0400 @@ -67,60 +67,8 @@ <div class="row row-cards-pf"> <div class="container container-cards-pf"> <system-cpu system-id="ctrl.systemId"></system-cpu> - <div class="system-memory-charts" ng-controller="SystemMemoryController as ctrl"> - <!-- System-Memory Donut Chart --> - <div class="col-xs-12 col-sm-6 col-md-6"> - <div class="card-pf card-pf-view"> - <div class="card-pf-heading"> - <label class="card-pf-title" translate>systemInfo.systemMemory.DONUT_CHART_LABEL</label> - <mc-add class="pull-right" svc-name="{{systemId}}-memory" get-fn="ctrl.multichartFn()"></mc-add> - </div> - <div class="card-pf-body"> - <pf-donut-pct-chart id="systemMemoryDonutChart" config="ctrl.donutConfig" data="ctrl.donutData"></pf-donut-pct-chart> - </div> - </div> - </div> - <!-- System-Memory Line Chart --> - <div class="col-xs-12 col-md-12"> - <div class="card-pf card-pf-view"> - <div class="card-pf-heading"> - <label class="card-pf-title" translate>systemInfo.systemMemory.LINE_CHART_LABEL</label> - </div> - <!-- Metric Controls: Refresh Rate --> - <div class="row" style="margin-top:2vh"> - <div class="col-xs-12 col-md-3"> - <label for="refreshCombo" class="label label-info" translate>systemInfo.systemMemory.REFRESH_RATE_LABEL</label> - <select name="refreshCombo" class="combobox form-control" ng-model="refreshRate"> - <option value="-1" translate>systemInfo.refresh.DISABLED</option> - <option value="1000" selected translate="systemInfo.refresh.SECONDS" translate-values="{ SECONDS: 1, DEFAULT: true }" translate-interpolation="messageformat"></option> - <option value="2000" translate="systemInfo.refresh.SECONDS" translate-values="{ SECONDS: 2 }" translate-interpolation="messageformat"></option> - <option value="5000" translate="systemInfo.refresh.SECONDS" translate-values="{ SECONDS: 5 }" translate-interpolation="messageformat"></option> - <option value="10000" translate="systemInfo.refresh.SECONDS" translate-values="{ SECONDS: 10 }" translate-interpolation="messageformat"></option> - <option value="30000" translate="systemInfo.refresh.SECONDS" translate-values="{ SECONDS: 30 }" translate-interpolation="messageformat"></option> - </select> - </div> - <!-- Metric Controls: Max Data Age --> - <div class="col-xs-12 col-md-3"> - <label for="dataAgeCombo" class="label label-info" translate>systemInfo.systemMemory.MAX_DATA_AGE_LABEL</label> - <select name="dataAgeCombo" class="combobox form-control" ng-model="dataAgeLimit"> - <option value="10000" translate="systemInfo.dataAge.SECONDS" translate-values="{ SECONDS: 10 }" translate-interpolation="messageformat"></option> - <option value="30000" selected translate="systemInfo.dataAge.SECONDS" translate-values="{ SECONDS: 30, DEFAULT: true }" translate-interpolation="messageformat"></option> - <option value="60000" translate="systemInfo.dataAge.MINUTES" translate-values="{ MINUTES: 1 }" translate-interpolation="messageformat"></option> - <option value="300000" translate="systemInfo.dataAge.MINUTES" translate-values="{ MINUTES: 5 }" translate-interpolation="messageformat"></option> - <option value="900000" translate="systemInfo.dataAge.MINUTES" translate-values="{ MINUTES: 15 }" translate-interpolation="messageformat"></option> - </select> - </div> - </div> - <!-- Line Chart --> - <div class="card-pf-body"> - <pf-line-chart id="systemMemoryLineChart" config="ctrl.lineConfig" chart-data="ctrl.lineData" set-area-chart="false" show-x-axis="true" show-y-axis="true"></pf-line-chart> - </div> - </div> - </div> - </div> - </div> + <system-memory system-id="ctrl.systemId"></system-memory> </div> - </div> </div>
--- a/src/app/components/system-info/system-info.module.js Mon Sep 11 11:26:31 2017 -0400 +++ b/src/app/components/system-info/system-info.module.js Wed Sep 13 16:27:00 2017 -0400 @@ -27,7 +27,7 @@ import SystemInfocontroller from './system-info.controller.js'; import systemCpu from './system-cpu/system-cpu.component.js'; -import SystemMemoryController from './system-memory.controller.js'; +import systemMemory from './system-memory/system-memory.component.js'; import systemNetwork from './system-network/system-network.component.js'; import service from './system-info.service.js'; import components from 'shared/components/components.module.js'; @@ -36,7 +36,7 @@ .module('systemInfo', [ SystemInfocontroller, systemCpu, - SystemMemoryController, + systemMemory, systemNetwork, service, components
--- a/src/app/components/system-info/system-info.service.js Mon Sep 11 11:26:31 2017 -0400 +++ b/src/app/components/system-info/system-info.service.js Wed Sep 13 16:27:00 2017 -0400 @@ -45,13 +45,6 @@ }); } - getMemoryInfo (systemId) { - return this.http.get(urlJoin(this.gatewayUrl, 'system-memory', '0.0.1', 'systems', systemId), { - params: { - sort: '-timeStamp' - } - }); - } } export default angular
--- a/src/app/components/system-info/system-info.service.spec.js Mon Sep 11 11:26:31 2017 -0400 +++ b/src/app/components/system-info/system-info.service.spec.js Wed Sep 13 16:27:00 2017 -0400 @@ -72,22 +72,4 @@ }); }); - describe('getMemoryInfo(systemId)', () => { - it('should resolve mock data', done => { - let expected = { - total: 16384, - used: 9001 - }; - httpBackend.when('GET', 'http://example.com:1234/system-memory/0.0.1/systems/foo-systemId?sort=-timeStamp') - .respond(expected); - svc.getMemoryInfo('foo-systemId').then(res => { - res.data.should.deepEqual(expected); - done(); - }); - httpBackend.expectGET('http://example.com:1234/system-memory/0.0.1/systems/foo-systemId?sort=-timeStamp'); - httpBackend.flush(); - scope.$apply(); - }); - }); - });
--- a/src/app/components/system-info/system-memory.controller.js Mon Sep 11 11:26:31 2017 -0400 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,231 +0,0 @@ -/** - * Copyright 2012-2017 Red Hat, Inc. - * - * Thermostat is distributed under the GNU General Public License, - * version 2 or any later version (with a special exception described - * below, commonly known as the "Classpath Exception"). - * - * A copy of GNU General Public License (GPL) is included in this - * distribution, in the file COPYING. - * - * Linking Thermostat code with other modules is making a combined work - * based on Thermostat. Thus, the terms and conditions of the GPL - * cover the whole combination. - * - * As a special exception, the copyright holders of Thermostat give you - * permission to link this code with independent modules to produce an - * executable, regardless of the license terms of these independent - * modules, and to copy and distribute the resulting executable under - * terms of your choice, provided that you also meet, for each linked - * independent module, the terms and conditions of the license of that - * module. An independent module is a module which is not derived from - * or based on Thermostat code. If you modify Thermostat, you may - * extend this exception to your version of the software, but you are - * not obligated to do so. If you do not wish to do so, delete this - * exception statement from your version. - */ - -import 'c3'; -import filters from 'shared/filters/filters.module.js'; -import service from './system-info.service.js'; - -class SystemMemoryController { - constructor (systemInfoService, $scope, $interval, pfUtils, - dateFilter, DATE_FORMAT, $q, $translate) { - 'ngInject'; - this.svc = systemInfoService; - this.scope = $scope; - this.interval = $interval; - this.dateFilter = dateFilter; - this.dateFormat = DATE_FORMAT; - this.q = $q; - this.translate = $translate; - - this.scope.refreshRate = '1000'; - this.scope.dataAgeLimit = '30000'; - - this.scope.$watch('refreshRate', newRefreshRate => this.setRefreshRate(newRefreshRate)); - this.scope.$watch('dataAgeLimit', () => this.trimData()); - this.scope.$on('$destroy', () => this.stopUpdating()); - - this.setupDonutChart(); - this.setupLineChart(pfUtils); - - this.update(); - } - - setupDonutChart () { - this.donutConfig = { - chartId: 'systemMemoryDonutChart', - units: '%' - }; - - this.donutData = { - used: 0, - total: 100 - }; - } - - setupLineChart (pfUtils) { - this.translate([ - 'systemInfo.systemMemory.X_AXIS_LABEL', - 'systemInfo.systemMemory.Y_AXIS_LABEL' - ]).then(translations => { - this.lineConfig = { - chartId: 'systemMemoryLineChart', - color: { - pattern: [ - pfUtils.colorPalette.red, // total memory - pfUtils.colorPalette.blue, // free memory - pfUtils.colorPalette.orange, // used memory - pfUtils.colorPalette.gold, // total swap - pfUtils.colorPalette.purple, // free swap - pfUtils.colorPalette.green // buffers - ] - }, - grid: { y: {show: true} }, - point: { r: 2 }, - legend : { 'show': true }, - tooltip: { - format: { - // TODO: this should be localized too, but c3 doesn't allow for the tooltip - // formatter to be a promise, only a function, and angular-translate only - // returns promises - value: memoryValue => { return memoryValue + ' MiB'; } - } - }, - transition: { duration: 50 }, - axis: { - x: { - type: 'timeseries', - label: { - text: translations['systemInfo.systemMemory.X_AXIS_LABEL'], - position: 'outer-center' - }, - tick : { - format: timestamp => this.dateFilter(timestamp, this.dateFormat.time.medium), - count: 5, - fit: false - } - }, - y: { - min: 0, - padding: 0, - tick: 10, - label: { - text: translations['systemInfo.systemMemory.Y_AXIS_LABEL'], - position: 'outer-middle' - } - } - } - }; - }); - - this.translate([ - 'systemInfo.systemMemory.xAxisTypes.TIMESTAMP', - 'systemInfo.systemMemory.xAxisTypes.TOTAL', - 'systemInfo.systemMemory.xAxisTypes.FREE', - 'systemInfo.systemMemory.xAxisTypes.USED', - 'systemInfo.systemMemory.xAxisTypes.SWAP_TOTAL', - 'systemInfo.systemMemory.xAxisTypes.SWAP_FREE', - 'systemInfo.systemMemory.xAxisTypes.BUFFERS' - ]).then(translations => { - this.lineData = { - xData: [translations['systemInfo.systemMemory.xAxisTypes.TIMESTAMP']], - yData0: [translations['systemInfo.systemMemory.xAxisTypes.TOTAL']], - yData1: [translations['systemInfo.systemMemory.xAxisTypes.FREE']], - yData2: [translations['systemInfo.systemMemory.xAxisTypes.USED']], - yData3: [translations['systemInfo.systemMemory.xAxisTypes.SWAP_TOTAL']], - yData4: [translations['systemInfo.systemMemory.xAxisTypes.SWAP_FREE']], - yData5: [translations['systemInfo.systemMemory.xAxisTypes.BUFFERS']] - }; - }); - } - - processData (resp) { - for (let i = resp.data.response.length - 1; i >= 0; i--) { - let data = resp.data.response[i]; - let free = data.free; - let total = data.total; - let used = total - free; - let usage = Math.round((used) / total * 100); - - // update the memory time series chart - this.lineConfig.axis.y.max = total; - this.lineData.xData.push(data.timeStamp); - this.lineData.yData0.push(total); - this.lineData.yData1.push(free); - this.lineData.yData2.push(used); - this.lineData.yData3.push(data.swapTotal); - this.lineData.yData4.push(data.swapFree); - this.lineData.yData5.push(data.buffers); - this.trimData(); - - // update the memory donut chart - this.donutData.used = usage; - } - } - - update () { - this.svc.getMemoryInfo(this.scope.systemId) - .then(response => this.processData(response), angular.noop); - } - - setRefreshRate (refreshRate) { - this.stopUpdating(); - if (refreshRate > 0) { - this.refresh = this.interval(() => this.update(), refreshRate); - this.update(); - } - } - - stopUpdating () { - if (angular.isDefined(this.refresh)) { - this.interval.cancel(this.refresh); - delete this.refresh; - } - } - - trimData () { - let now = Date.now(); - let oldestLimit = now - parseInt(this.scope.dataAgeLimit); - while (true) { - let oldest = this.lineData.xData[1]; - if (angular.isDefined(oldest) && oldest < oldestLimit) { - this.lineData.xData.splice(1, 1); - this.lineData.yData0.splice(1, 1); - this.lineData.yData1.splice(1, 1); - this.lineData.yData2.splice(1, 1); - this.lineData.yData3.splice(1, 1); - this.lineData.yData4.splice(1, 1); - this.lineData.yData5.splice(1, 1); - } else { - break; - } - } - } - - multichartFn () { - return new Promise(resolve => - this.svc.getMemoryInfo(this.scope.systemId).then(resp => { - let data = resp.data.response[0]; - let free = data.free; - let total = data.total; - let used = total - free; - let usage = Math.round(used / total * 100); - resolve(usage); - }) - ); - } - -} - -export default angular - .module('systemMemory.controller', [ - 'patternfly', - 'patternfly.charts', - filters, - service - ]) - .controller('SystemMemoryController', SystemMemoryController) - .name;
--- a/src/app/components/system-info/system-memory.controller.spec.js Mon Sep 11 11:26:31 2017 -0400 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,453 +0,0 @@ -/** - * Copyright 2012-2017 Red Hat, Inc. - * - * Thermostat is distributed under the GNU General Public License, - * version 2 or any later version (with a special exception described - * below, commonly known as the "Classpath Exception"). - * - * A copy of GNU General Public License (GPL) is included in this - * distribution, in the file COPYING. - * - * Linking Thermostat code with other modules is making a combined work - * based on Thermostat. Thus, the terms and conditions of the GPL - * cover the whole combination. - * - * As a special exception, the copyright holders of Thermostat give you - * permission to link this code with independent modules to produce an - * executable, regardless of the license terms of these independent - * modules, and to copy and distribute the resulting executable under - * terms of your choice, provided that you also meet, for each linked - * independent module, the terms and conditions of the license of that - * module. An independent module is a module which is not derived from - * or based on Thermostat code. If you modify Thermostat, you may - * extend this exception to your version of the software, but you are - * not obligated to do so. If you do not wish to do so, delete this - * exception statement from your version. - */ - -describe('SystemMemoryController', () => { - - beforeEach(angular.mock.module('systemMemory.controller')); - - let service, scope, interval, memoryPromise, controller, - dateFilterStub, dateFormatSpy, translate; - - beforeEach(inject($controller => { - 'ngInject'; - - let systemPromise = sinon.spy(); - let cpuPromise = sinon.spy(); - memoryPromise = { - then: sinon.spy() - }; - service = { - systemPromise: systemPromise, - cpuPromise: cpuPromise, - memoryPromise: memoryPromise, - getSystemInfo: sinon.stub().returns({ then: systemPromise }), - getCpuInfo: sinon.stub().returns({ then: cpuPromise }), - getMemoryInfo: sinon.stub().returns(memoryPromise) - }; - - dateFilterStub = sinon.stub().returns('mockDate'); - dateFormatSpy = { - time: { - medium: sinon.spy() - } - }; - scope = { - $on: sinon.spy(), - $watch: sinon.spy() - }; - - interval = sinon.stub().returns('interval-sentinel'); - interval.cancel = sinon.stub().returns(interval.sentinel); - - translate = sinon.stub().returns({ - then: sinon.stub().yields({ - 'systemInfo.systemMemory.X_AXIS_LABEL': 'Time', - 'systemInfo.systemMemory.Y_AXIS_LABEL': 'Size (MiB)', - 'systemInfo.systemMemory.xAxisTypes.TIMESTAMP': 'timestamp', - 'systemInfo.systemMemory.xAxisTypes.TOTAL': 'Total Memory', - 'systemInfo.systemMemory.xAxisTypes.FREE': 'Free Memory', - 'systemInfo.systemMemory.xAxisTypes.USED': 'Used Memory', - 'systemInfo.systemMemory.xAxisTypes.SWAP_TOTAL': 'Total Swap', - 'systemInfo.systemMemory.xAxisTypes.SWAP_FREE': 'Free Swap', - 'systemInfo.systemMemory.xAxisTypes.BUFFERS': 'Buffers', - }) - }); - - controller = $controller('SystemMemoryController', { - systemId: 'foo-systemId', - systemInfoService: service, - $scope: scope, - $interval: interval, - dateFilter: dateFilterStub, - DATE_FORMAT: dateFormatSpy, - $translate: translate - }); - - })); - - it('should exist', () => { - should.exist(controller); - should.exist(service); - }); - - it('should update on initialization', () => { - service.getMemoryInfo.should.be.called(); - }); - - it('should call to service on update', () => { - controller.update(); - service.getMemoryInfo.should.be.called(); - memoryPromise.then.should.be.calledWith(sinon.match.func); - let successHandler = memoryPromise.then.args[1][0]; - successHandler({ - data: { - response: { - systemId: 'foo-systemId', - agentId: 'mock-agentId', - timeStamp: Date.now(), - total: 16384, - free: 0, - buffers: 1, - cached: 2, - swapTotal: 3, - swapFree: 4, - commitLimit: 0 - } - } - }); - let errorHandler = memoryPromise.then.args[1][1]; - errorHandler.should.equal(angular.noop); - errorHandler(); - }); - - it('should set initial data objects', () => { - controller.should.have.ownProperty('donutData'); - controller.donutData.should.deepEqual({ - used: 0, - total: 100 - }); - controller.should.have.ownProperty('lineData'); - controller.lineData.should.deepEqual({ - xData: ['timestamp'], - yData0: ['Total Memory'], - yData1: ['Free Memory'], - yData2: ['Used Memory'], - yData3: ['Total Swap'], - yData4: ['Free Swap'], - yData5: ['Buffers'] - }); - }); - - it('should set interval on setting refresh rate', () => { - interval.should.not.be.called(); - interval.cancel.should.not.be.called(); - controller.setRefreshRate(1); - interval.should.be.called(); - interval.cancel.should.not.be.called(); - }); - - it('should disable when setRefreshRate is called with a non-positive value', () => { - interval.cancel.should.not.be.called(); - controller.setRefreshRate.should.not.be.called(); - controller.update.should.not.be.called(); - - controller.setRefreshRate(1); - - interval.cancel.should.not.be.called(); - controller.should.have.ownProperty('refresh'); - - controller.setRefreshRate(-1); - - interval.cancel.should.be.calledOnce(); - controller.should.not.have.ownProperty('refresh'); - }); - - describe('multichartFn', () => { - it('should return a promise', () => { - let res = controller.multichartFn(); - res.should.be.a.Promise(); - }); - - [[50, 45, 10], [100, 20, 80], [500, 50, 90]].forEach(tup => { - it('should resolve system-memory stat (' + tup + ')', done => { - service.memoryPromise.then.should.be.calledOnce(); - let res = controller.multichartFn(); - res.then(v => { - v.should.equal(tup[2]); - done(); - }); - service.memoryPromise.then.should.be.calledTwice(); - let prom = service.memoryPromise.then.secondCall.args[0]; - prom({ - data: { - response: [ - { - total: tup[0], - free: tup[1] - } - ] - } - }); - }); - }); - - }); - - it('should call update() on refresh', () => { - scope.$watch.should.be.calledWith(sinon.match('refreshRate'), sinon.match.func); - let refreshFn = scope.$watch.args[0][1]; - refreshFn.should.be.a.Function(); - refreshFn(1); - let intervalFn = interval.args[0][0]; - let callCount = service.getMemoryInfo.callCount; - intervalFn(); - service.getMemoryInfo.callCount.should.equal(callCount + 1); - - }); - - it ('should call trimData() on dataAgeLimit change', () => { - scope.$watch.should.be.calledWith(sinon.match('dataAgeLimit')); - scope.$watch.args[1][0].should.equal('dataAgeLimit'); - let watchFn = scope.$watch.args[1][1]; - watchFn.should.be.a.Function(); - controller.trimData = sinon.spy(); - let callCount = controller.trimData.callCount; - watchFn(); - controller.trimData.callCount.should.equal(callCount + 1); - }); - - describe('chart configs', () => { - it('should set an initial config object', () => { - controller.should.have.ownProperty('donutConfig'); - controller.should.have.ownProperty('lineConfig'); - }); - - it('should use dateFilter with DATE_FORMAT.time.medium to format x ticks', () => { - let fn = controller.lineConfig.axis.x.tick.format; - fn.should.be.a.Function(); - fn('fooTimestamp').should.equal('mockDate'); - dateFilterStub.should.be.calledWith('fooTimestamp', dateFormatSpy.time.medium); - }); - - - it('line chart should set a custom tooltip', () => { - let tooltipFormat = controller.lineConfig.tooltip.format; - tooltipFormat.should.have.ownProperty('value'); - tooltipFormat.value.should.be.a.Function(); - tooltipFormat.value(100).should.equal('100 MiB'); - }); - }); - - describe('processData', () => { - it('should process singleton service results', () => { - controller.donutData.should.deepEqual({ - used: 0, - total: 100 - }); - controller.lineData.should.deepEqual({ - xData: ['timestamp'], - yData0: ['Total Memory'], - yData1: ['Free Memory'], - yData2: ['Used Memory'], - yData3: ['Total Swap'], - yData4: ['Free Swap'], - yData5: ['Buffers'] - }); - let timestamp = Date.now(); - controller.processData({ - data: { - response: [ - { - systemId: 'foo-systemId', - agentId: 'mock-agentId', - timeStamp: timestamp, - total: 16384, - free: 0, - buffers: 1, - cached: 2, - swapTotal: 3, - swapFree: 4, - commitLimit: 0 - } - ] - } - }); - controller.donutData.should.deepEqual({ - used: 100, - total: 100 - }); - controller.lineData.should.deepEqual({ - xData: ['timestamp', timestamp], - yData0: ['Total Memory', 16384], - yData1: ['Free Memory', 0], - yData2: ['Used Memory', 16384], - yData3: ['Total Swap', 3], - yData4: ['Free Swap', 4], - yData5: ['Buffers', 1] - }); - }); - - it('should process multiple service results', () => { - controller.donutData.should.deepEqual({ - used: 0, - total: 100 - }); - controller.lineData.should.deepEqual({ - xData: ['timestamp'], - yData0: ['Total Memory'], - yData1: ['Free Memory'], - yData2: ['Used Memory'], - yData3: ['Total Swap'], - yData4: ['Free Swap'], - yData5: ['Buffers'] - }); - let timestampA = Date.now(); - let timestampB = Date.now() - 1000; - controller.processData({ - data: { - response: [ - { - systemId: 'foo-systemId', - agentId: 'mock-agentId', - timeStamp: timestampA, - total: 16384, - free: 0, - buffers: 0, - cached: 0, - swapTotal: 0, - swapFree: 0, - commitLimit: 0 - }, - { - systemId: 'foo-systemId', - agentId: 'mock-agentId', - timeStamp: timestampB, - total: 16384, - free: 0, - buffers: 0, - cached: 0, - swapTotal: 0, - swapFree: 0, - commitLimit: 0 - } - ] - } - }); - controller.lineData.xData.length.should.equal(3); - controller.lineData.xData[1].should.equal(timestampB); - controller.lineData.xData[2].should.equal(timestampA); - }); - - it('should append new data to line chart data object', () => { - let timestampA = Date.now(); - let timestampB = Date.now() + 1000; - controller.processData({ - data: { - response: [ - { - systemId: 'foo-systemId', - agentId: 'mock-agentId', - timeStamp: timestampA, - total: 16384, - free: 0, - buffers: 0, - cached: 0, - swapTotal: 0, - swapFree: 0, - commitLimit: 0 - } - ] - } - }); - controller.processData({ - data: { - response: [ - { - systemId: 'foo-systemId', - agentId: 'mock-agentId', - timeStamp: timestampB, - total: 16384, - free: 0, - buffers: 0, - cached: 0, - swapTotal: 0, - swapFree: 0, - commitLimit: 0 - } - ] - } - }); - controller.lineData.xData.length.should.equal(3); - controller.lineData.xData[1].should.equal(timestampA); - controller.lineData.xData[2].should.equal(timestampB); - }); - - it('should remove data that is older than dataAgeLimit', () => { - controller.dataAgeLimit = 30000; - let timestampA = Date.now() - 30001; - let timestampB = Date.now; - controller.processData({ - data: { - response: [ - { - systemId: 'foo-systemId', - agentId: 'mock-agentId', - timeStamp: timestampA, - total: 16384, - free: 0, - buffers: 0, - cached: 0, - swapTotal: 0, - swapFree: 0, - commitLimit: 0 - } - ] - } - }); - controller.processData({ - data: { - response: [ - { - systemId: 'foo-systemId', - agentId: 'mock-agentId', - timeStamp: timestampB, - total: 16384, - free: 0, - buffers: 0, - cached: 0, - swapTotal: 0, - swapFree: 0, - commitLimit: 0 - } - ] - } - }); - controller.lineData.xData.length.should.equal(2); - controller.lineData.xData[1].should.equal(timestampB); - }); - }); - - describe('on destroy', () => { - it('should set an ondestroy handler', () => { - scope.$on.should.be.calledWith('$destroy', sinon.match.func); - }); - - it('should cancel refresh', () => { - controller.refresh = 'interval-sentinel'; - let refreshFn = scope.$on.args[0][1]; - refreshFn(); - interval.cancel.should.be.calledWith('interval-sentinel'); - }); - - it('should do nothing if refresh undefined', () => { - controller.refresh = undefined; - let refreshFn = scope.$on.args[0][1]; - refreshFn(); - interval.cancel.should.not.be.called(); - }); - }); -});
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/app/components/system-info/system-memory/en.locale.yaml Wed Sep 13 16:27:00 2017 -0400 @@ -0,0 +1,27 @@ +systemMemory: + + refresh: + DISABLED: Disabled + SECONDS: '{SECONDS, plural, =0{0 Seconds} one{1 Second} other{# Seconds}}{DEFAULT, select, true{ (Default)} other{}}' + + dataAge: + SECONDS: '{SECONDS, plural, =0{0 Seconds} one{1 Second} other{# Seconds}}{DEFAULT, select, true{ (Default)} other{}}' + MINUTES: '{MINUTES, plural, =0{0 Minutes} one{1 Minute} other{# Minutes}}{DEFAULT, select, true{ (Default)} other{}}' + + DONUT_CHART_LABEL: Memory Usage + LINE_CHART_LABEL: Memory + REFRESH_RATE_LABEL: Refresh Rate + MAX_DATA_AGE_LABEL: Max Data Age + + X_AXIS_LABEL: Time + Y_AXIS_LABEL: Size (MiB) + TOOLTIP_FMT: '{{size}} MiB' + + xAxisTypes: + TIMESTAMP: timestamp + TOTAL: Total Memory + FREE: Free Memory + USED: Used Memory + SWAP_TOTAL: Total Swap + SWAP_FREE: Free Swap + BUFFERS: Buffers
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/app/components/system-info/system-memory/system-memory.component.js Wed Sep 13 16:27:00 2017 -0400 @@ -0,0 +1,43 @@ +/** + * Copyright 2012-2017 Red Hat, Inc. + * + * Thermostat is distributed under the GNU General Public License, + * version 2 or any later version (with a special exception described + * below, commonly known as the "Classpath Exception"). + * + * A copy of GNU General Public License (GPL) is included in this + * distribution, in the file COPYING. + * + * Linking Thermostat code with other modules is making a combined work + * based on Thermostat. Thus, the terms and conditions of the GPL + * cover the whole combination. + * + * As a special exception, the copyright holders of Thermostat give you + * permission to link this code with independent modules to produce an + * executable, regardless of the license terms of these independent + * modules, and to copy and distribute the resulting executable under + * terms of your choice, provided that you also meet, for each linked + * independent module, the terms and conditions of the license of that + * module. An independent module is a module which is not derived from + * or based on Thermostat code. If you modify Thermostat, you may + * extend this exception to your version of the software, but you are + * not obligated to do so. If you do not wish to do so, delete this + * exception statement from your version. + */ + +import controller from './system-memory.controller.js'; +import service from './system-memory.service.js'; + +export default angular + .module('systemMemory.component', [ + controller, + service + ]) + .component('systemMemory', { + bindings: { + systemId: '<' + }, + controller: 'SystemMemoryController', + template: require('./system-memory.html') + }) + .name;
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/app/components/system-info/system-memory/system-memory.controller.js Wed Sep 13 16:27:00 2017 -0400 @@ -0,0 +1,250 @@ +/** + * Copyright 2012-2017 Red Hat, Inc. + * + * Thermostat is distributed under the GNU General Public License, + * version 2 or any later version (with a special exception described + * below, commonly known as the "Classpath Exception"). + * + * A copy of GNU General Public License (GPL) is included in this + * distribution, in the file COPYING. + * + * Linking Thermostat code with other modules is making a combined work + * based on Thermostat. Thus, the terms and conditions of the GPL + * cover the whole combination. + * + * As a special exception, the copyright holders of Thermostat give you + * permission to link this code with independent modules to produce an + * executable, regardless of the license terms of these independent + * modules, and to copy and distribute the resulting executable under + * terms of your choice, provided that you also meet, for each linked + * independent module, the terms and conditions of the license of that + * module. An independent module is a module which is not derived from + * or based on Thermostat code. If you modify Thermostat, you may + * extend this exception to your version of the software, but you are + * not obligated to do so. If you do not wish to do so, delete this + * exception statement from your version. + */ + +import 'c3'; +import filters from 'shared/filters/filters.module.js'; +import service from './system-memory.service.js'; + +class SystemMemoryController { + constructor (systemMemoryService, $interval, pfUtils, + dateFilter, DATE_FORMAT, $translate) { + 'ngInject'; + this._svc = systemMemoryService; + this._interval = $interval; + this._dateFilter = dateFilter; + this._dateFormat = DATE_FORMAT; + this._translate = $translate; + + this._refreshRate = 1000; + this._dataAgeLimit = 30000; + + this._setupDonutChart(); + this._setupLineChart(pfUtils); + } + + $onInit () { + this._start(); + } + + $onDestroy () { + this._stop(); + } + + _setupDonutChart () { + this.donutConfig = { + chartId: 'systemMemoryDonutChart', + units: '%' + }; + + this.donutData = { + used: 0, + total: 100 + }; + } + + _setupLineChart (pfUtils) { + this._translate([ + 'systemMemory.X_AXIS_LABEL', + 'systemMemory.Y_AXIS_LABEL' + ]).then(translations => { + this.lineConfig = { + chartId: 'systemMemoryLineChart', + color: { + pattern: [ + pfUtils.colorPalette.red, // total memory + pfUtils.colorPalette.blue, // free memory + pfUtils.colorPalette.orange, // used memory + pfUtils.colorPalette.gold, // total swap + pfUtils.colorPalette.purple, // free swap + pfUtils.colorPalette.green // buffers + ] + }, + grid: { y: {show: true} }, + point: { r: 2 }, + legend : { 'show': true }, + tooltip: { + format: { + // TODO: this should be localized too, but c3 doesn't allow for the tooltip + // formatter to be a promise, only a function, and angular-translate only + // returns promises + value: memoryValue => { return memoryValue + ' MiB'; } + } + }, + transition: { duration: 50 }, + axis: { + x: { + type: 'timeseries', + label: { + text: translations['systemMemory.X_AXIS_LABEL'], + position: 'outer-center' + }, + tick : { + format: timestamp => this._dateFilter(timestamp, this._dateFormat.time.medium), + count: 5, + fit: false + } + }, + y: { + min: 0, + padding: 0, + tick: 10, + label: { + text: translations['systemMemory.Y_AXIS_LABEL'], + position: 'outer-middle' + } + } + } + }; + }); + + this._translate([ + 'systemMemory.xAxisTypes.TIMESTAMP', + 'systemMemory.xAxisTypes.TOTAL', + 'systemMemory.xAxisTypes.FREE', + 'systemMemory.xAxisTypes.USED', + 'systemMemory.xAxisTypes.SWAP_TOTAL', + 'systemMemory.xAxisTypes.SWAP_FREE', + 'systemMemory.xAxisTypes.BUFFERS' + ]).then(translations => { + this.lineData = { + xData: [translations['systemMemory.xAxisTypes.TIMESTAMP']], + yData0: [translations['systemMemory.xAxisTypes.TOTAL']], + yData1: [translations['systemMemory.xAxisTypes.FREE']], + yData2: [translations['systemMemory.xAxisTypes.USED']], + yData3: [translations['systemMemory.xAxisTypes.SWAP_TOTAL']], + yData4: [translations['systemMemory.xAxisTypes.SWAP_FREE']], + yData5: [translations['systemMemory.xAxisTypes.BUFFERS']] + }; + }); + } + + _processData (resp) { + for (let i = resp.data.response.length - 1; i >= 0; i--) { + let data = resp.data.response[i]; + let free = data.free; + let total = data.total; + let used = total - free; + let usage = Math.round((used) / total * 100); + + // update the memory time series chart + this.lineConfig.axis.y.max = total; + this.lineData.xData.push(data.timeStamp); + this.lineData.yData0.push(total); + this.lineData.yData1.push(free); + this.lineData.yData2.push(used); + this.lineData.yData3.push(data.swapTotal); + this.lineData.yData4.push(data.swapFree); + this.lineData.yData5.push(data.buffers); + this._trimData(); + + // update the memory donut chart + this.donutData.used = usage; + } + } + + _start () { + this._stop(); + this._update(); + this._refresh = this._interval(() => this._update(), this._refreshRate); + } + + _update () { + this._svc.getMemoryInfo(this.systemId) + .then(response => this._processData(response), angular.noop); + } + + set refreshRate (refreshRate) { + this._stop(); + this._refreshRate = parseInt(refreshRate); + if (refreshRate > 0) { + this._start(); + } + } + + get refreshRate () { + return this._refreshRate.toString(); + } + + set dataAgeLimit (val) { + this._dataAgeLimit = val; + this._trimData(); + } + + get dataAgeLimit () { + return this._dataAgeLimit.toString(); + } + + _stop () { + if (angular.isDefined(this._refresh)) { + this._interval.cancel(this._refresh); + delete this._refresh; + } + } + + _trimData () { + let now = Date.now(); + let oldestLimit = now - this._dataAgeLimit; + while (true) { + let oldest = this.lineData.xData[1]; + if (angular.isDefined(oldest) && oldest < oldestLimit) { + this.lineData.xData.splice(1, 1); + this.lineData.yData0.splice(1, 1); + this.lineData.yData1.splice(1, 1); + this.lineData.yData2.splice(1, 1); + this.lineData.yData3.splice(1, 1); + this.lineData.yData4.splice(1, 1); + this.lineData.yData5.splice(1, 1); + } else { + break; + } + } + } + + multichartFn () { + return new Promise(resolve => + this._svc.getMemoryInfo(this.systemId).then(resp => { + let data = resp.data.response[0]; + let free = data.free; + let total = data.total; + let used = total - free; + let usage = Math.round(used / total * 100); + resolve(usage); + }) + ); + } + +} + +export default angular + .module('systemMemory.controller', [ + 'patternfly', + 'patternfly.charts', + filters, + service + ]) + .controller('SystemMemoryController', SystemMemoryController) + .name;
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/app/components/system-info/system-memory/system-memory.controller.spec.js Wed Sep 13 16:27:00 2017 -0400 @@ -0,0 +1,429 @@ +/** + * Copyright 2012-2017 Red Hat, Inc. + * + * Thermostat is distributed under the GNU General Public License, + * version 2 or any later version (with a special exception described + * below, commonly known as the "Classpath Exception"). + * + * A copy of GNU General Public License (GPL) is included in this + * distribution, in the file COPYING. + * + * Linking Thermostat code with other modules is making a combined work + * based on Thermostat. Thus, the terms and conditions of the GPL + * cover the whole combination. + * + * As a special exception, the copyright holders of Thermostat give you + * permission to link this code with independent modules to produce an + * executable, regardless of the license terms of these independent + * modules, and to copy and distribute the resulting executable under + * terms of your choice, provided that you also meet, for each linked + * independent module, the terms and conditions of the license of that + * module. An independent module is a module which is not derived from + * or based on Thermostat code. If you modify Thermostat, you may + * extend this exception to your version of the software, but you are + * not obligated to do so. If you do not wish to do so, delete this + * exception statement from your version. + */ + +import controllerModule from './system-memory.controller.js'; + +describe('SystemMemoryController', () => { + + beforeEach(angular.mock.module(controllerModule)); + + let service, interval, memoryPromise, controller, + dateFilterStub, dateFormatSpy, translate; + + beforeEach(inject($controller => { + 'ngInject'; + + memoryPromise = { + then: sinon.spy() + }; + service = { + memoryPromise: memoryPromise, + getMemoryInfo: sinon.stub().returns(memoryPromise) + }; + + dateFilterStub = sinon.stub().returns('mockDate'); + dateFormatSpy = { + time: { + medium: sinon.spy() + } + }; + + interval = sinon.stub().returns('interval-sentinel'); + interval.cancel = sinon.stub().returns(interval.sentinel); + + translate = sinon.stub().returns({ + then: sinon.stub().yields({ + 'systemMemory.X_AXIS_LABEL': 'Time', + 'systemMemory.Y_AXIS_LABEL': 'Size (MiB)', + 'systemMemory.xAxisTypes.TIMESTAMP': 'timestamp', + 'systemMemory.xAxisTypes.TOTAL': 'Total Memory', + 'systemMemory.xAxisTypes.FREE': 'Free Memory', + 'systemMemory.xAxisTypes.USED': 'Used Memory', + 'systemMemory.xAxisTypes.SWAP_TOTAL': 'Total Swap', + 'systemMemory.xAxisTypes.SWAP_FREE': 'Free Swap', + 'systemMemory.xAxisTypes.BUFFERS': 'Buffers', + }) + }); + + controller = $controller('SystemMemoryController', { + systemMemoryService: service, + $interval: interval, + dateFilter: dateFilterStub, + DATE_FORMAT: dateFormatSpy, + $translate: translate + }); + controller.systemId = 'foo-systemId'; + controller.$onInit(); + })); + + it('should exist', () => { + should.exist(controller); + should.exist(service); + }); + + it('should start on initialization', () => { + interval.should.be.calledOnce(); + }); + + it('should call to service on update', () => { + controller._update(); + service.getMemoryInfo.should.be.called(); + memoryPromise.then.should.be.calledWith(sinon.match.func); + let successHandler = memoryPromise.then.args[0][0]; + successHandler({ + data: { + response: { + systemId: 'foo-systemId', + agentId: 'mock-agentId', + timeStamp: Date.now(), + total: 16384, + free: 0, + buffers: 1, + cached: 2, + swapTotal: 3, + swapFree: 4, + commitLimit: 0 + } + } + }); + let errorHandler = memoryPromise.then.args[0][1]; + errorHandler.should.equal(angular.noop); + }); + + it('should set initial data objects', () => { + controller.should.have.ownProperty('donutData'); + controller.donutData.should.deepEqual({ + used: 0, + total: 100 + }); + controller.should.have.ownProperty('lineData'); + controller.lineData.should.deepEqual({ + xData: ['timestamp'], + yData0: ['Total Memory'], + yData1: ['Free Memory'], + yData2: ['Used Memory'], + yData3: ['Total Swap'], + yData4: ['Free Swap'], + yData5: ['Buffers'] + }); + }); + + it('should set interval on setting refresh rate', () => { + interval.should.be.calledOnce(); + interval.cancel.should.not.be.called(); + controller.refreshRate = 1; + interval.should.be.calledTwice(); + interval.cancel.should.be.calledOnce(); + }); + + it('should disable when refresh rate is set to non-positive value', () => { + interval.should.be.calledOnce(); + interval.cancel.should.not.be.called(); + + controller.refreshRate = 1; + + interval.should.be.calledTwice(); + interval.cancel.should.be.calledOnce(); + + controller.refreshRate = -1; + + interval.should.be.calledTwice(); + interval.cancel.should.be.calledTwice(); + }); + + describe('multichartFn', () => { + it('should return a promise', () => { + let res = controller.multichartFn(); + res.should.be.a.Promise(); + }); + + [[50, 45, 10], [100, 20, 80], [500, 50, 90]].forEach(tup => { + it('should resolve system-memory stat (' + tup + ')', done => { + service.memoryPromise.then.should.be.calledOnce(); + let res = controller.multichartFn(); + res.then(v => { + v.should.equal(tup[2]); + done(); + }); + service.memoryPromise.then.should.be.calledTwice(); + let prom = service.memoryPromise.then.args[1][0]; + prom({ + data: { + response: [ + { + total: tup[0], + free: tup[1] + } + ] + } + }); + }); + }); + + }); + + it('should call _update() on refresh', () => { + controller.refreshRate = 1; + controller.refreshRate.should.equal('1'); + let intervalFn = interval.args[0][0]; + let callCount = service.getMemoryInfo.callCount; + intervalFn(); + service.getMemoryInfo.callCount.should.equal(callCount + 1); + }); + + it ('should call trimData() on dataAgeLimit change', () => { + sinon.spy(controller, '_trimData'); + controller._trimData.should.not.be.called(); + controller.dataAgeLimit = 30000; + controller.dataAgeLimit.should.equal('30000'); + controller._trimData.should.be.calledOnce(); + }); + + describe('chart configs', () => { + it('should set an initial config object', () => { + controller.should.have.ownProperty('donutConfig'); + controller.should.have.ownProperty('lineConfig'); + }); + + it('should use dateFilter with DATE_FORMAT.time.medium to format x ticks', () => { + let fn = controller.lineConfig.axis.x.tick.format; + fn.should.be.a.Function(); + fn('fooTimestamp').should.equal('mockDate'); + dateFilterStub.should.be.calledWith('fooTimestamp', dateFormatSpy.time.medium); + }); + + + it('line chart should set a custom tooltip', () => { + let tooltipFormat = controller.lineConfig.tooltip.format; + tooltipFormat.should.have.ownProperty('value'); + tooltipFormat.value.should.be.a.Function(); + tooltipFormat.value(100).should.equal('100 MiB'); + }); + }); + + describe('_processData', () => { + it('should process singleton service results', () => { + controller.donutData.should.deepEqual({ + used: 0, + total: 100 + }); + controller.lineData.should.deepEqual({ + xData: ['timestamp'], + yData0: ['Total Memory'], + yData1: ['Free Memory'], + yData2: ['Used Memory'], + yData3: ['Total Swap'], + yData4: ['Free Swap'], + yData5: ['Buffers'] + }); + let timestamp = Date.now(); + controller._processData({ + data: { + response: [ + { + systemId: 'foo-systemId', + agentId: 'mock-agentId', + timeStamp: timestamp, + total: 16384, + free: 0, + buffers: 1, + cached: 2, + swapTotal: 3, + swapFree: 4, + commitLimit: 0 + } + ] + } + }); + controller.donutData.should.deepEqual({ + used: 100, + total: 100 + }); + controller.lineData.should.deepEqual({ + xData: ['timestamp', timestamp], + yData0: ['Total Memory', 16384], + yData1: ['Free Memory', 0], + yData2: ['Used Memory', 16384], + yData3: ['Total Swap', 3], + yData4: ['Free Swap', 4], + yData5: ['Buffers', 1] + }); + }); + + it('should process multiple service results', () => { + controller.donutData.should.deepEqual({ + used: 0, + total: 100 + }); + controller.lineData.should.deepEqual({ + xData: ['timestamp'], + yData0: ['Total Memory'], + yData1: ['Free Memory'], + yData2: ['Used Memory'], + yData3: ['Total Swap'], + yData4: ['Free Swap'], + yData5: ['Buffers'] + }); + let timestampA = Date.now(); + let timestampB = Date.now() - 1000; + controller._processData({ + data: { + response: [ + { + systemId: 'foo-systemId', + agentId: 'mock-agentId', + timeStamp: timestampA, + total: 16384, + free: 0, + buffers: 0, + cached: 0, + swapTotal: 0, + swapFree: 0, + commitLimit: 0 + }, + { + systemId: 'foo-systemId', + agentId: 'mock-agentId', + timeStamp: timestampB, + total: 16384, + free: 0, + buffers: 0, + cached: 0, + swapTotal: 0, + swapFree: 0, + commitLimit: 0 + } + ] + } + }); + controller.lineData.xData.length.should.equal(3); + controller.lineData.xData[1].should.equal(timestampB); + controller.lineData.xData[2].should.equal(timestampA); + }); + + it('should append new data to line chart data object', () => { + let timestampA = Date.now(); + let timestampB = Date.now() + 1000; + controller._processData({ + data: { + response: [ + { + systemId: 'foo-systemId', + agentId: 'mock-agentId', + timeStamp: timestampA, + total: 16384, + free: 0, + buffers: 0, + cached: 0, + swapTotal: 0, + swapFree: 0, + commitLimit: 0 + } + ] + } + }); + controller._processData({ + data: { + response: [ + { + systemId: 'foo-systemId', + agentId: 'mock-agentId', + timeStamp: timestampB, + total: 16384, + free: 0, + buffers: 0, + cached: 0, + swapTotal: 0, + swapFree: 0, + commitLimit: 0 + } + ] + } + }); + controller.lineData.xData.length.should.equal(3); + controller.lineData.xData[1].should.equal(timestampA); + controller.lineData.xData[2].should.equal(timestampB); + }); + + it('should remove data that is older than dataAgeLimit', () => { + controller.dataAgeLimit = 30000; + let timestampA = Date.now() - 30001; + let timestampB = Date.now; + controller._processData({ + data: { + response: [ + { + systemId: 'foo-systemId', + agentId: 'mock-agentId', + timeStamp: timestampA, + total: 16384, + free: 0, + buffers: 0, + cached: 0, + swapTotal: 0, + swapFree: 0, + commitLimit: 0 + } + ] + } + }); + controller._processData({ + data: { + response: [ + { + systemId: 'foo-systemId', + agentId: 'mock-agentId', + timeStamp: timestampB, + total: 16384, + free: 0, + buffers: 0, + cached: 0, + swapTotal: 0, + swapFree: 0, + commitLimit: 0 + } + ] + } + }); + controller.lineData.xData.length.should.equal(2); + controller.lineData.xData[1].should.equal(timestampB); + }); + }); + + describe('on destroy', () => { + it('should cancel refresh', () => { + controller.$onDestroy(); + interval.cancel.should.be.calledWith('interval-sentinel'); + }); + + it('should do nothing if refresh undefined', () => { + delete controller._refresh; + controller.$onDestroy(); + interval.cancel.should.not.be.called(); + }); + }); +});
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/app/components/system-info/system-memory/system-memory.html Wed Sep 13 16:27:00 2017 -0400 @@ -0,0 +1,58 @@ +<div class="system-memory-charts"> + <div class="col-xs-12 col-sm-6 col-md-6"> + <div class="card-pf card-pf-view"> + + <div class="card-pf-heading"> + <label class="card-pf-title" translate>systemMemory.DONUT_CHART_LABEL</label> + <mc-add class="pull-right" svc-name="{{$ctrl.systemId}}-memory" get-fn="$ctrl.multichartFn()"></mc-add> + </div> + + <div class="card-pf-body"> + <pf-donut-pct-chart id="systemMemoryDonutChart" config="$ctrl.donutConfig" data="$ctrl.donutData"></pf-donut-pct-chart> + </div> + </div> + </div> + + <div class="col-xs-12 col-md-12"> + <div class="card-pf card-pf-view"> + + <div class="card-pf-heading"> + <label class="card-pf-title" translate>systemMemory.LINE_CHART_LABEL</label> + </div> + + <div class="row" style="margin-top:2vh"> + <!-- Metric Controls: Refresh Rate --> + <div class="col-xs-12 col-md-3"> + <label for="refreshCombo" class="label label-info" translate>systemMemory.REFRESH_RATE_LABEL</label> + <select name="refreshCombo" class="combobox form-control" ng-model="$ctrl.refreshRate"> + <option value="-1" translate>systemMemory.refresh.DISABLED</option> + <option value="1000" selected translate="systemMemory.refresh.SECONDS" translate-values="{ SECONDS: 1, DEFAULT: true }" translate-interpolation="messageformat"></option> + <option value="2000" translate="systemMemory.refresh.SECONDS" translate-values="{ SECONDS: 2 }" translate-interpolation="messageformat"></option> + <option value="5000" translate="systemMemory.refresh.SECONDS" translate-values="{ SECONDS: 5 }" translate-interpolation="messageformat"></option> + <option value="10000" translate="systemMemory.refresh.SECONDS" translate-values="{ SECONDS: 10 }" translate-interpolation="messageformat"></option> + <option value="30000" translate="systemMemory.refresh.SECONDS" translate-values="{ SECONDS: 30 }" translate-interpolation="messageformat"></option> + </select> + </div> + + <!-- Metric Controls: Max Data Age --> + <div class="col-xs-12 col-md-3"> + <label for="dataAgeCombo" class="label label-info" translate>systemMemory.MAX_DATA_AGE_LABEL</label> + <select name="dataAgeCombo" class="combobox form-control" ng-model="$ctrl.dataAgeLimit"> + <option value="10000" translate="systemMemory.dataAge.SECONDS" translate-values="{ SECONDS: 10 }" translate-interpolation="messageformat"></option> + <option value="30000" selected translate="systemMemory.dataAge.SECONDS" translate-values="{ SECONDS: 30, DEFAULT: true }" translate-interpolation="messageformat"></option> + <option value="60000" translate="systemMemory.dataAge.MINUTES" translate-values="{ MINUTES: 1 }" translate-interpolation="messageformat"></option> + <option value="300000" translate="systemMemory.dataAge.MINUTES" translate-values="{ MINUTES: 5 }" translate-interpolation="messageformat"></option> + <option value="900000" translate="systemMemory.dataAge.MINUTES" translate-values="{ MINUTES: 15 }" translate-interpolation="messageformat"></option> + </select> + </div> + </div> + + <!-- Line Chart --> + <div class="card-pf-body"> + <pf-line-chart id="systemMemoryLineChart" config="$ctrl.lineConfig" chart-data="$ctrl.lineData" set-area-chart="false" show-x-axis="true" show-y-axis="true"></pf-line-chart> + </div> + + </div> + </div> + +</div>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/app/components/system-info/system-memory/system-memory.service.js Wed Sep 13 16:27:00 2017 -0400 @@ -0,0 +1,52 @@ +/** + * Copyright 2012-2017 Red Hat, Inc. + * + * Thermostat is distributed under the GNU General Public License, + * version 2 or any later version (with a special exception described + * below, commonly known as the "Classpath Exception"). + * + * A copy of GNU General Public License (GPL) is included in this + * distribution, in the file COPYING. + * + * Linking Thermostat code with other modules is making a combined work + * based on Thermostat. Thus, the terms and conditions of the GPL + * cover the whole combination. + * + * As a special exception, the copyright holders of Thermostat give you + * permission to link this code with independent modules to produce an + * executable, regardless of the license terms of these independent + * modules, and to copy and distribute the resulting executable under + * terms of your choice, provided that you also meet, for each linked + * independent module, the terms and conditions of the license of that + * module. An independent module is a module which is not derived from + * or based on Thermostat code. If you modify Thermostat, you may + * extend this exception to your version of the software, but you are + * not obligated to do so. If you do not wish to do so, delete this + * exception statement from your version. + */ + +import config from 'shared/config/config.module.js'; +import urlJoin from 'url-join'; + +class SystemMemoryService { + constructor ($q, $http, gatewayUrl) { + 'ngInject'; + this.q = $q; + this.http = $http; + this.gatewayUrl = gatewayUrl; + } + + getMemoryInfo (systemId) { + return this.http.get(urlJoin(this.gatewayUrl, 'system-memory', '0.0.1', 'systems', systemId), { + params: { + sort: '-timeStamp' + } + }); + } +} + +export default angular + .module('systemMemory.service', [config]) + .service('systemMemoryService', SystemMemoryService) + .name; +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/app/components/system-info/system-memory/system-memory.service.spec.js Wed Sep 13 16:27:00 2017 -0400 @@ -0,0 +1,77 @@ +/** + * Copyright 2012-2017 Red Hat, Inc. + * + * Thermostat is distributed under the GNU General Public License, + * version 2 or any later version (with a special exception described + * below, commonly known as the "Classpath Exception"). + * + * A copy of GNU General Public License (GPL) is included in this + * distribution, in the file COPYING. + * + * Linking Thermostat code with other modules is making a combined work + * based on Thermostat. Thus, the terms and conditions of the GPL + * cover the whole combination. + * + * As a special exception, the copyright holders of Thermostat give you + * permission to link this code with independent modules to produce an + * executable, regardless of the license terms of these independent + * modules, and to copy and distribute the resulting executable under + * terms of your choice, provided that you also meet, for each linked + * independent module, the terms and conditions of the license of that + * module. An independent module is a module which is not derived from + * or based on Thermostat code. If you modify Thermostat, you may + * extend this exception to your version of the software, but you are + * not obligated to do so. If you do not wish to do so, delete this + * exception statement from your version. + */ + +import configModule from 'shared/config/config.module.js'; +import serviceModule from './system-memory.service.js'; + +describe('SystemMemoryService', () => { + + beforeEach(() => { + angular.mock.module(configModule, $provide => { + 'ngInject'; + $provide.constant('gatewayUrl', 'http://example.com:1234'); + }); + + angular.mock.module(serviceModule); + }); + + let httpBackend, scope, svc; + beforeEach(inject(($httpBackend, $rootScope, systemMemoryService) => { + 'ngInject'; + httpBackend = $httpBackend; + + scope = $rootScope; + svc = systemMemoryService; + })); + + afterEach(() => { + httpBackend.verifyNoOutstandingExpectation(); + httpBackend.verifyNoOutstandingRequest(); + }); + + it('should exist', () => { + should.exist(svc); + }); + + describe('getMemoryInfo(systemId)', () => { + it('should resolve mock data', done => { + let expected = { + total: 16384, + used: 9001 + }; + httpBackend.when('GET', 'http://example.com:1234/system-memory/0.0.1/systems/foo-systemId?sort=-timeStamp') + .respond(expected); + svc.getMemoryInfo('foo-systemId').then(res => { + res.data.should.deepEqual(expected); + done(); + }); + httpBackend.expectGET('http://example.com:1234/system-memory/0.0.1/systems/foo-systemId?sort=-timeStamp'); + httpBackend.flush(); + scope.$apply(); + }); + }); +});