Mercurial > hg > thermostat-ng > web-client
changeset 131:59e8facc27cb
Add experimental "multicharts" feature
Reviewed-by: almac
Review-thread: http://icedtea.classpath.org/pipermail/thermostat/2017-July/024046.html
line wrap: on
line diff
--- a/src/app/app.module.js Tue Jul 18 09:37:28 2017 -0400 +++ b/src/app/app.module.js Tue Jul 18 10:14:56 2017 -0400 @@ -28,12 +28,14 @@ import 'angular-patternfly'; import '@uirouter/angularjs'; import 'oclazyload'; +import 'bootstrap'; import 'bootstrap-switch'; import configModule from 'shared/config/config.module.js'; import {default as authModule, config as authModBootstrap} from './components/auth/auth.module.js'; import filters from 'shared/filters/filters.module.js'; import services from 'shared/services/services.module.js'; +import directives from 'shared/directives/directives.module.js'; import appRouting from './app.routing.js'; import authInterceptorFactory from './auth-interceptor.factory.js'; import AppController from './app.controller.js'; @@ -54,6 +56,7 @@ // non-core modules services, filters, + directives, appRouting, authInterceptorFactory, AppController
--- a/src/app/components/jvm-info/jvm-memory/jvm-memory.controller.js Tue Jul 18 09:37:28 2017 -0400 +++ b/src/app/components/jvm-info/jvm-memory/jvm-memory.controller.js Tue Jul 18 10:14:56 2017 -0400 @@ -32,7 +32,7 @@ class JvmMemoryController { constructor (jvmId, $scope, $interval, jvmMemoryService, metricToBigIntFilter, - bigIntToStringFilter, stringToNumberFilter, scaleBytesService) { + bigIntToStringFilter, stringToNumberFilter, scaleBytesService, sanitizeService) { 'ngInject'; this.jvmId = jvmId; @@ -44,6 +44,7 @@ this.bigIntToString = bigIntToStringFilter; this.stringToNumber = stringToNumberFilter; this.scaleBytes = scaleBytesService; + this.scope.sanitize = sanitizeService.sanitize; this.scope.refreshRate = '2000'; @@ -82,6 +83,14 @@ } } + multichartFn () { + return new Promise(resolve => + this.jvmMemoryService.getJvmMemory(this.scope.systemId).then(resp => + resolve(this.convertMemStat(resp.data.response[0].metaspaceUsed)) + ) + ); + } + update () { this.jvmMemoryService.getJvmMemory(this.jvmId).then(resp => { let data = resp.data.response[0];
--- a/src/app/components/jvm-info/jvm-memory/jvm-memory.controller.spec.js Tue Jul 18 09:37:28 2017 -0400 +++ b/src/app/components/jvm-info/jvm-memory/jvm-memory.controller.spec.js Tue Jul 18 10:14:56 2017 -0400 @@ -243,4 +243,31 @@ }); }); + describe('multichartFn', () => { + it('should return a promise', () => { + let res = ctrl.multichartFn(); + res.should.be.a.Promise(); + }); + + it('should resolve jvm-memory stat', done => { + promise.then.should.be.calledOnce(); + let res = ctrl.multichartFn(); + res.then(v => { + v.should.equal(9001); + done(); + }); + promise.then.should.be.calledTwice(); + let prom = promise.then.secondCall.args[0]; + prom({ + data: { + response: [ + { + metaspaceUsed: { $numberLong: '9001' } + } + ] + } + }); + }); + }); + });
--- a/src/app/components/jvm-info/jvm-memory/jvm-memory.html Tue Jul 18 09:37:28 2017 -0400 +++ b/src/app/components/jvm-info/jvm-memory/jvm-memory.html Tue Jul 18 10:14:56 2017 -0400 @@ -39,6 +39,7 @@ <div class="card-pf card-pf-view"> <div class="card-pf-heading"> <label class="card-pf-title">{{generation.name}} ({{generation.collector}})</label> + <mc-add class="pull-right" svc-name="{{ctrl.jvmId}}-{{sanitize(generation.name)}}-metaspace" get-fn="ctrl.multichartFn()"></mc-add> </div> <div ng-repeat="space in generation.spaces"> <div class="card-pf-body text-center">
--- a/src/app/components/jvm-info/jvm-memory/jvm-memory.module.js Tue Jul 18 09:37:28 2017 -0400 +++ b/src/app/components/jvm-info/jvm-memory/jvm-memory.module.js Tue Jul 18 10:14:56 2017 -0400 @@ -27,10 +27,12 @@ import JvmMemoryController from './jvm-memory.controller.js'; import service from './jvm-memory.service.js'; +import directives from 'shared/directives/directives.module.js'; export default angular .module('jvmMemory', [ JvmMemoryController, - service + service, + directives ]) .name;
--- a/src/app/components/jvm-list/jvm-list.controller.js Tue Jul 18 09:37:28 2017 -0400 +++ b/src/app/components/jvm-list/jvm-list.controller.js Tue Jul 18 10:14:56 2017 -0400 @@ -27,7 +27,7 @@ import filters from 'shared/filters/filters.module.js'; import service from './jvm-list.service.js'; -import dismissibleErrorMessage from 'shared/directives/dismissible-error-message/dismissible-error-message.directive.js'; +import directives from 'shared/directives/directives.module.js'; class JvmListController { constructor (jvmListService, $scope, $location, $timeout, $anchorScroll) { @@ -99,9 +99,9 @@ export default angular .module('jvmList.controller', [ - 'directives.dismissible-error-message', 'patternfly', filters, + directives, service ]) .controller('JvmListController', JvmListController)
--- a/src/app/components/landing/landing.html Tue Jul 18 09:37:28 2017 -0400 +++ b/src/app/components/landing/landing.html Tue Jul 18 10:14:56 2017 -0400 @@ -13,22 +13,28 @@ <div class="container-fluid container-cards-pf"> <div class="row row-cards-pf"> - <div class="col-md-4"> - <div class="card-pf card-pf-utilization"> + <div class="col-xs-12 col-md-4"> + <div class="card-pf"> + <div class="card-pf-heading"> - <h2 class="card-pf-title"> - Some Metric - </h2> - </div> - <div class="card-pf-body"> - <div class="row"> - <div class="col-xs-12 col-sm-4 col-md-4"> - <h3 class="card-pf-subtitle"> - <a ui-sref="jvmList">JVM Listing</a> - </h3> - </div> + <div class="card-pf-title"> + Navigation </div> </div> + + <div class="card-pf-body"> + <div class="card-pf-subtitle"> + <ul> + <li> + <a ui-sref="jvmList">JVM Listing</a> + </li> + <li> + <a ui-sref="multichart">Multicharts</a> + </li> + </ul> + </div> + </div> + </div> </div> </div><!-- /row -->
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/app/components/multichart/chart.controller.js Tue Jul 18 10:14:56 2017 -0400 @@ -0,0 +1,159 @@ +/** + * 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 services from 'shared/services/services.module.js'; +import filters from 'shared/filters/filters.module.js'; + +class MultiChartController { + constructor (multichartService, $scope, $interval, dateFilter, DATE_FORMAT) { + this.svc = multichartService; + this.scope = $scope; + this.interval = $interval; + this.dateFilter = dateFilter; + this.dateFormat = DATE_FORMAT; + this.chart = $scope.$parent.chart; + + this.initializeChartData(); + + this.scope.$on('$destroy', () => this.stop()); + + this.scope.refreshRate = '2000'; + this.scope.dataAgeLimit = '60000'; + + this.scope.$watch('refreshRate', (cur, prev) => this.setRefreshRate(cur)); + this.scope.$watch('dataAgeLimit', () => this.trimData()); + + this.refresh = $interval(() => this.update(), parseInt(this.scope.refreshRate)); + this.update(); + } + + update () { + this.svc.getData(this.chart).then(data => { + let keys = Object.keys(data); + if (keys.length === 0) { + return; + } + + this.chartData.xData.push(Date.now()); + keys.forEach(prop => { + if (this.chartData.hasOwnProperty(prop)) { + this.chartData[prop].push(data[prop][1]); + } else { + this.chartData[prop] = data[prop]; + } + }); + this.chartConfig.data.axes = this.svc.getAxesForChart(this.chart); + + this.trimData(); + }, angular.noop); + } + + stop () { + 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) { + if (this.chartData.xData.length <= 2) { + break; + } + let oldest = this.chartData.xData[1]; + if (oldest < oldestLimit) { + Object.keys(this.chartData).forEach(key => { + this.chartData[key].splice(1, 1); + }); + } else { + break; + } + } + } + + initializeChartData () { + let self = this; + this.chartConfig = { + chartId: 'chart-' + this.chart, + axis: { + x: { + label: 'timestamp', + type: 'timeseries', + localtime: false, + tick: { + format: timestamp => this.dateFilter(timestamp, this.dateFormat.time.medium), + count: 5 + } + }, + y: { + tick: { + format: d => d + } + }, + y2: { + get show () { + return self.svc.countServicesForChart(self.chart) > 1; + } + } + }, + tooltip: { + format: { + title: x => 'Time: ' + x, + value: y => y + } + } + }; + this.chartData = { + xData: ['timestamp'] + }; + } + + removeChart () { + this.svc.removeChart(this.chart); + } + + setRefreshRate (val) { + this.stop(); + if (val > 0) { + this.refresh = this.interval(() => this.update(), val); + } + } +} + +export default angular + .module('multichartChartController', [ + 'patternfly', + 'patternfly.charts', + services, + filters + ]) + .controller('MultiChartChartController', MultiChartController) + .name;
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/app/components/multichart/chart.controller.spec.js Tue Jul 18 10:14:56 2017 -0400 @@ -0,0 +1,338 @@ +/** + * 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 './chart.controller.js'; + +describe('multichart chartController', () => { + + let ctrl, svc, scope, interval, dateFilter; + beforeEach(() => { + angular.mock.module(controllerModule); + angular.mock.inject($controller => { + 'ngInject'; + + let getDataPromise = sinon.spy(); + let getData = sinon.stub().returns({ + then: getDataPromise + }); + svc = { + getData: getData, + getDataPromise: getDataPromise, + getAxesForChart: sinon.stub().returns(['y']), + countServicesForChart: sinon.stub().returns(2), + removeChart: sinon.spy() + }; + scope = { + $parent: { + chart: 'foo-chart' + }, + $on: sinon.spy(), + $watch: sinon.spy() + }; + interval = sinon.stub().returns('interval-sentinel'); + interval.cancel = sinon.spy(); + dateFilter = sinon.stub().returns('dateFilter-sentinel'); + + ctrl = $controller('MultiChartChartController', { + multichartService: svc, + $scope: scope, + $interval: interval, + dateFilter: dateFilter, + DATE_FORMAT: { time: { medium: 'medium-time' } } + }); + ctrl.chartConfig.data = {}; + }); + }); + + it('should exist', () => { + should.exist(ctrl); + }); + + it('should stop on destroy', () => { + scope.$on.should.be.calledWith('$destroy'); + scope.$on.callCount.should.equal(1); + let fn = scope.$on.args[0][1]; + fn.should.be.a.Function(); + + interval.cancel.should.not.be.called(); + fn(); + interval.cancel.should.be.calledOnce(); + interval.cancel.should.be.calledWith('interval-sentinel'); + }); + + it('should do nothing if stopped when already stopped', () => { + interval.cancel.should.not.be.called(); + ctrl.stop(); + interval.cancel.should.be.calledOnce(); + ctrl.stop(); + interval.cancel.should.be.calledOnce(); + }); + + it('should initialize refreshRate', () => { + scope.should.have.ownProperty('refreshRate'); + scope.refreshRate.should.equal('2000'); + }); + + it('should initialize dataAgeLimit', () => { + scope.should.have.ownProperty('dataAgeLimit'); + scope.dataAgeLimit.should.equal('60000'); + }); + + it('should begin updating at refreshRate period', () => { + svc.getData.should.be.calledOnce(); + interval.should.be.calledOnce(); + interval.should.be.calledWithMatch(sinon.match.func, sinon.match(2000)); + let fn = interval.args[0][0]; + fn(); + svc.getData.should.be.calledTwice(); + }); + + it('should watch refreshRate changes', () => { + interval.cancel.should.not.be.called(); + scope.$watch.should.be.calledWithMatch(sinon.match('refreshRate'), sinon.match.func); + let fn = scope.$watch.withArgs('refreshRate').args[0][1]; + fn.should.be.a.Function(); + interval.returns('foo-sentinel'); + fn(5); + interval.cancel.should.be.calledOnce(); + ctrl.refresh.should.equal('foo-sentinel'); + interval.should.be.calledWithMatch(sinon.match.func, sinon.match(5)); + }); + + it('should watch dataAgeLimit changes', () => { + let spy = sinon.spy(ctrl, 'trimData'); + scope.$watch.should.be.calledWithMatch(sinon.match('dataAgeLimit'), sinon.match.func); + let fn = scope.$watch.withArgs('dataAgeLimit').args[0][1]; + fn.should.be.a.Function(); + fn(); + spy.should.be.calledOnce(); + spy.restore(); + }); + + describe('update', () => { + let fn; + beforeEach(() => { + fn = svc.getDataPromise.args[0][0]; + fn.should.be.a.Function(); + }); + + it('should pass chart ID', () => { + ctrl.update(); + svc.getData.should.be.calledWith(scope.$parent.chart); + }); + + it('should do nothing if data is empty', () => { + ctrl.chartData.xData.length.should.equal(1); + fn({}); + ctrl.chartData.xData.length.should.equal(1); + }); + + it('should append new data', () => { + ctrl.chartData.xData.length.should.equal(1); + ctrl.should.not.have.ownProperty('yData'); + fn({ + yData: ['someMetric', 100] + }); + ctrl.chartData.xData.length.should.equal(2); + ctrl.chartData.should.have.ownProperty('yData'); + ctrl.chartData.yData.length.should.equal(2); + }); + + it('should append data for existing metric', () => { + ctrl.chartData.xData.length.should.equal(1); + ctrl.should.not.have.ownProperty('yData'); + fn({ + yData: ['someMetric', 100] + }); + ctrl.chartData.xData.length.should.equal(2); + ctrl.chartData.should.have.ownProperty('yData'); + ctrl.chartData.yData.length.should.equal(2); + fn({ + yData: ['someMetric', 200] + }); + ctrl.chartData.xData.length.should.equal(3); + ctrl.chartData.yData.length.should.equal(3); + }); + }); + + describe('trimData', () => { + let dateNowStub; + beforeEach(() => { + dateNowStub = sinon.stub(Date, 'now'); + }); + + afterEach(() => { + Date.now.restore(); + }); + + it('should do nothing if no samples', () => { + ctrl.trimData(); + ctrl.chartData.should.eql({ + xData: ['timestamp'] + }); + }); + + it('should not trim data if only one sample', () => { + scope.dataAgeLimit = 10; + let updateFn = svc.getDataPromise.args[0][0]; + + dateNowStub.returns(100); + updateFn({ + yData: ['foo', 500], + yData1: ['bar', 5000] + }); + + dateNowStub.returns(200); + ctrl.trimData(); + + ctrl.chartData.should.eql({ + xData: ['timestamp', 100], + yData: ['foo', 500], + yData1: ['bar', 5000] + }); + }); + + it('should trim old data', () => { + scope.dataAgeLimit = 200; + let updateFn = svc.getDataPromise.args[0][0]; + + dateNowStub.returns(100); + updateFn({ + yData: ['foo', 500], + yData1: ['bar', 5000] + }); + + dateNowStub.returns(200); + updateFn({ + yData: ['foo', 600], + yData1: ['bar', 6000] + }); + + dateNowStub.returns(300); + updateFn({ + yData: ['foo', 700], + yData1: ['bar', 7000] + }); + + dateNowStub.returns(350); + + ctrl.chartData.should.eql({ + xData: ['timestamp', 100, 200, 300], + yData: ['foo', 500, 600, 700], + yData1: ['bar', 5000, 6000, 7000] + }); + + ctrl.trimData(); + + ctrl.chartData.should.eql({ + xData: ['timestamp', 200, 300], + yData: ['foo', 600, 700], + yData1: ['bar', 6000, 7000] + }); + }); + }); + + describe('initializeChartData', () => { + it('should set chartId', () => { + ctrl.chartConfig.chartId.should.equal('chart-foo-chart'); + }); + + it('should format x-axis ticks using dateFilter', () => { + dateFilter.should.not.be.called(); + let fmtFn = ctrl.chartConfig.axis.x.tick.format; + fmtFn(100).should.equal('dateFilter-sentinel'); + dateFilter.should.be.calledOnce(); + dateFilter.should.be.calledWith(100, 'medium-time'); + }); + + it('should format y-axis ticks with identity function', () => { + let fmtFn = ctrl.chartConfig.axis.y.tick.format; + fmtFn(1).should.equal(1); + fmtFn('foo').should.equal('foo'); + }); + + it('should show y2 axis if suggested by service', () => { + svc.countServicesForChart.should.not.be.called(); + let res = ctrl.chartConfig.axis.y2.show; + res.should.equal(true); + svc.countServicesForChart.should.be.calledOnce(); + svc.countServicesForChart.should.be.calledWith('foo-chart'); + }); + + it('should format tooltips', () => { + let titleFmt = ctrl.chartConfig.tooltip.format.title; + let valueFmt = ctrl.chartConfig.tooltip.format.value; + + titleFmt('foo').should.equal('Time: foo'); + titleFmt(100).should.equal('Time: 100'); + + valueFmt('foo').should.equal('foo'); + valueFmt(100).should.equal(100); + }); + }); + + describe('removeChart', () => { + it('should delegate to service', () => { + svc.removeChart.should.not.be.called(); + ctrl.removeChart(); + svc.removeChart.should.be.calledOnce(); + svc.removeChart.should.be.calledWith('foo-chart'); + }); + }); + + describe('setRefreshRate', () => { + it('should stop previous refreshes', () => { + interval.cancel.should.not.be.called(); + ctrl.setRefreshRate(5); + interval.cancel.should.be.calledOnce(); + }); + + it('should set the new update interval', () => { + interval.should.be.calledOnce(); + ctrl.setRefreshRate(5); + interval.should.be.calledTwice(); + interval.secondCall.should.be.calledWith(sinon.match.func, 5); + }); + + it('should perform updates at each interval', () => { + ctrl.setRefreshRate(5); + svc.getData.should.be.calledOnce(); + let fn = interval.secondCall.args[0]; + fn(); + svc.getData.should.be.calledTwice(); + }); + + it('should cancel if given non-positive value', () => { + interval.cancel.should.not.be.called(); + svc.getData.should.be.calledOnce(); + ctrl.setRefreshRate(-1); + interval.cancel.should.be.calledOnce(); + svc.getData.should.be.calledOnce(); + }); + }); + +});
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/app/components/multichart/multichart.controller.js Tue Jul 18 10:14:56 2017 -0400 @@ -0,0 +1,69 @@ +/** + * 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 services from 'shared/services/services.module.js'; +import directives from 'shared/directives/directives.module.js'; + +class MultiChartController { + constructor (multichartService) { + this.svc = multichartService; + this.showErr = false; + } + + createChart (chartName) { + if (!chartName) { + return false; + } + chartName = chartName.trim(); + if (!this.isValid(chartName)) { + this.showErr = true; + return; + } + this.showErr = false; + this.svc.addChart(chartName); + } + + isValid (chartName) { + if (!chartName) { + return false; + } + return chartName.search(/^[\w-]+$/) > -1; + } + + get chartNames () { + return this.svc.chartNames; + } +} + +export default angular + .module('multichartController', [ + 'patternfly', + services, + directives + ]) + .controller('MultichartController', MultiChartController) + .name;
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/app/components/multichart/multichart.controller.spec.js Tue Jul 18 10:14:56 2017 -0400 @@ -0,0 +1,137 @@ +/** + * 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 './multichart.controller.js'; + +describe('MultiChartController', () => { + + let svc, ctrl; + beforeEach(() => { + angular.mock.module(controllerModule); + angular.mock.inject($controller => { + 'ngInject'; + svc = { + addChart: sinon.spy(), + chartNames: ['foo', 'bar'] + }; + ctrl = $controller('MultichartController', { + multichartService: svc + }); + }); + }); + + it('should exist', () => { + should(ctrl).not.be.undefined(); + }); + + it('should initialize showErr to false', () => { + ctrl.showErr.should.be.false(); + }); + + it('should return chartNames from service', () => { + ctrl.chartNames.should.deepEqual(['foo', 'bar']); + }); + + describe('createChart', () => { + it('should do nothing if chartName is undefined', () => { + svc.addChart.should.not.be.called(); + ctrl.createChart(); + svc.addChart.should.not.be.called(); + ctrl.showErr.should.be.false(); + }); + + it('should call to service on success', () => { + svc.addChart.should.not.be.called(); + ctrl.createChart('foo'); + svc.addChart.should.be.calledOnce(); + svc.addChart.should.be.calledWith('foo'); + }); + + it('should trim spaces from chart names', () => { + ctrl.createChart(' foo '); + svc.addChart.should.be.calledWith('foo'); + }); + + it('should set showErr to false on success', () => { + ctrl.createChart('foo'); + ctrl.showErr.should.be.false(); + }); + + it('should set showErr to true on error', () => { + ctrl.createChart('<script>alert();</script>'); + ctrl.showErr.should.be.true(); + svc.addChart.should.not.be.called(); + }); + }); + + describe('isValid', () => { + it('should reject undefined', () => { + ctrl.isValid(undefined).should.be.false(); + }); + + it('should reject null', () => { + ctrl.isValid(null).should.be.false(); + }); + + it('should reject empty string', () => { + ctrl.isValid('').should.be.false(); + }); + + it('should reject HTML-formatted string', () => { + ctrl.isValid('<b>chart</b>').should.be.false(); + }); + + it('should reject HTML script tag', () => { + ctrl.isValid('<script>alert("foo");</script>').should.be.false(); + }); + + it('should reject string with spaces', () => { + ctrl.isValid('foo ').should.be.false(); + }); + + it('should accept plain single word', () => { + ctrl.isValid('foo').should.be.true(); + }); + + it('should accept words separated by hyphens', () => { + ctrl.isValid('foo-system').should.be.true(); + }); + + it('should accept a name comprised of hyphens and underscores', () => { + ctrl.isValid('-_--_-').should.be.true(); + }); + + it('should accept words separated by underscores', () => { + ctrl.isValid('foo_system'); + }); + + it('should accept numbers', () => { + ctrl.isValid('1').should.be.true(); + }); + }); + +});
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/app/components/multichart/multichart.html Tue Jul 18 10:14:56 2017 -0400 @@ -0,0 +1,80 @@ +<div class="container-fluid container-cards-pf"> + <ol class="breadcrumb" style="margin-bottom: 0px;"> + <li><a ui-sref="landing">Thermostat</a></li> + <li><a ui-serf="multichart">Multichart</a></li> + </ol> + + <dismissible-error-message ng-show="ctrl.showErr" err-title="'Invalid chart name.'" + err-message="'Chart names may contain alphanumeric characters, underscores, and hyphens.'"/> + + <div class="row"> + <div class="col-xs-12 col-md-4"> + <div class="input-group"> + <span class="input-group-addon">Name</span> + <input type="text" class="form-control" placeholder="Enter new chart name" ng-model="newChartName"/> + <button class="input-group-button" ng-click="ctrl.createChart(newChartName)">Create Chart</button> + </div> + </div> + </div> + + <div class="row row-cards-pf"> + <div class="container-fluid container-cards-pf"> + + <div ng-repeat="chart in ctrl.chartNames"> + <div class="col-xs-12 col-md-10"> + <div ng-controller="MultiChartChartController as chartCtrl" class="card-pf card-pf-view"> + <div class="card-pf-heading"> + <label class="card-pf-title">{{chart}}</label> + <button class="btn btn-default pull-right" ng-click="chartCtrl.removeChart(chart)"><span class="pficon pficon-close"></span></button> + </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">Refresh Rate</label> + <select name="refreshCombo" class="combobox form-control" ng-model="refreshRate"> + <option value="-1">Disabled</option> + <option value="1000">1 Second</option> + <option value="2000" selected>2 Seconds (Default)</option> + <option value="5000">5 Seconds</option> + <option value="10000">10 Seconds</option> + <option value="30000">30 Seconds</option> + <option value="60000">1 Minute</option> + <option value="300000">5 Minutes</option> + <option value="600000">10 Minutes</option> + </select> + </div> + <!-- Metric Controls: Max Data Age --> + <div class="col-xs-12 col-md-3"> + <label for="dataAgeCombo" class="label label-info">Max Data Age</label> + <select name="dataAgeCombo" class="combobox form-control" ng-model="dataAgeLimit"> + <option value="10000">10 Seconds</option> + <option value="30000">30 Seconds</option> + <option value="60000" selected>1 Minute (Default)</option> + <option value="300000">5 Minutes</option> + <option value="600000">10 Minutes</option> + <option value="900000">15 Minutes</option> + <option value="1800000">30 Minutes</option> + <option value="3600000">1 Hour</option> + </select> + </div> + </div> + + <div class="card-pf-body"> + <div pf-line-chart id="chart-{{chart}}" config="chartCtrl.chartConfig" + chart-data="chartCtrl.chartData" + show-x-axis="true" + show-y-axis="true" + ></div> + + + </div> + + </div> + </div> + </div> + + </div> + </div> + +</div>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/app/components/multichart/multichart.module.js Tue Jul 18 10:14:56 2017 -0400 @@ -0,0 +1,36 @@ +/** + * 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 './multichart.controller.js'; +import chartController from './chart.controller.js'; + +export default angular + .module('multichartModule', [ + controller, + chartController + ]) + .name;
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/app/components/multichart/multichart.routing.js Tue Jul 18 10:14:56 2017 -0400 @@ -0,0 +1,64 @@ +/** + * 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. + */ + +function config ($stateProvider) { + 'ngInject'; + + $stateProvider.state('multichart', { + url: '/multichart', + templateProvider: $q => { + 'ngInject'; + return $q(resolve => + require.ensure([], () => resolve(require('./multichart.html')) + ) + ); + }, + controller: 'MultichartController as ctrl', + resolve: { + loadMultichart: ($q, $ocLazyLoad) => { + 'ngInject'; + return $q(resolve => { + require.ensure(['./multichart.module.js'], () => { + let module = require('./multichart.module.js'); + $ocLazyLoad.load({ name: module.default }); + resolve(module); + }); + }); + } + } + }); +} + +export { config }; + +export default angular + .module('multichartRouter', [ + 'ui.router', + 'oc.lazyLoad' + ]) + .config(config) + .name;
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/app/components/multichart/multichart.routing.spec.js Tue Jul 18 10:14:56 2017 -0400 @@ -0,0 +1,92 @@ +/** + * 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('MultiChartRouting', () => { + + let module = require('./multichart.routing.js'); + + let stateProvider, args, q, ocLazyLoad; + beforeEach(() => { + stateProvider = { + state: sinon.spy() + }; + module.config(stateProvider); + args = stateProvider.state.args[0]; + q = sinon.spy(); + ocLazyLoad = { + load: sinon.spy() + }; + }); + + describe('stateProvider', () => { + it('should call $stateProvider.state', () => { + stateProvider.state.should.be.calledOnce(); + }); + + it('should define a \'multichart\' state', () => { + args[0].should.equal('multichart'); + }); + + it('should map to /multichart', () => { + args[1].url.should.equal('/multichart'); + }); + + it('template provider should return multichart.html', done => { + let providerFn = args[1].templateProvider[1]; + providerFn.should.be.a.Function(); + providerFn(q); + q.should.be.calledOnce(); + + let deferred = q.args[0][0]; + deferred.should.be.a.Function(); + + let resolve = sinon.stub().callsFake(val => { + val.should.equal(require('./multichart.html')); + done(); + }); + deferred(resolve); + }); + + it('resolve should load multichart module', done => { + let resolveFn = args[1].resolve.loadMultichart[2]; + resolveFn.should.be.a.Function(); + resolveFn(q, ocLazyLoad); + q.should.be.calledOnce(); + + let deferred = q.args[0][0]; + deferred.should.be.a.Function(); + + let resolve = sinon.stub().callsFake(val => { + ocLazyLoad.load.should.be.calledWith({ name: require('./multichart.module.js').default}); + val.should.equal(require('./multichart.module.js')); + done(); + }); + deferred(resolve); + }); + }); + +});
--- a/src/app/components/system-info/system-cpu.controller.js Tue Jul 18 09:37:28 2017 -0400 +++ b/src/app/components/system-info/system-cpu.controller.js Tue Jul 18 10:14:56 2017 -0400 @@ -65,6 +65,14 @@ }; }); } + + multichartFn () { + return new Promise(resolve => + this.svc.getCpuInfo(this.scope.systemId).then(resp => + resolve(_.floor(_.mean(resp.data.response[0].perProcessorUsage))) + ) + ); + } } export default angular
--- a/src/app/components/system-info/system-cpu.controller.spec.js Tue Jul 18 09:37:28 2017 -0400 +++ b/src/app/components/system-info/system-cpu.controller.spec.js Tue Jul 18 10:14:56 2017 -0400 @@ -140,4 +140,31 @@ }); }); + describe('multichartFn', () => { + it('should return a promise', () => { + let res = controller.multichartFn(); + res.should.be.a.Promise(); + }); + + it('should resolve system-cpu stat', done => { + service.cpuPromise.should.be.calledOnce(); + let res = controller.multichartFn(); + res.then(v => { + v.should.equal(90); + done(); + }); + service.cpuPromise.should.be.calledTwice(); + let prom = service.cpuPromise.secondCall.args[0]; + prom({ + data: { + response: [ + { + perProcessorUsage: [90] + } + ] + } + }); + }); + }); + });
--- a/src/app/components/system-info/system-info.html Tue Jul 18 09:37:28 2017 -0400 +++ b/src/app/components/system-info/system-info.html Tue Jul 18 10:14:56 2017 -0400 @@ -50,6 +50,7 @@ <div class="card-pf card-pf-view"> <div class="card-pf-heading"> <label class="card-pf-title">CPU Usage</label> + <mc-add class="pull-right" svc-name="{{systemId}}-cpu" get-fn="ctrl.multichartFn()"></mc-add> </div> <div class="card-pf-body"> <div pf-donut-pct-chart id="cpuChart" config="ctrl.config" data="ctrl.data"></div> @@ -62,6 +63,7 @@ <div class="card-pf card-pf-view"> <div class="card-pf-heading"> <label class="card-pf-title">Memory Usage</label> + <mc-add class="pull-right" svc-name="{{systemId}}-memory" get-fn="ctrl.multichartFn()"></mc-add> </div> <div class="card-pf-body"> <div pf-donut-pct-chart id="systemMemoryDonutChart" config="ctrl.donutConfig" data="ctrl.donutData"></div>
--- a/src/app/components/system-info/system-info.module.js Tue Jul 18 09:37:28 2017 -0400 +++ b/src/app/components/system-info/system-info.module.js Tue Jul 18 10:14:56 2017 -0400 @@ -29,12 +29,14 @@ import SystemCpuController from './system-cpu.controller.js'; import SystemMemoryController from './system-memory.controller.js'; import service from './system-info.service.js'; +import directives from 'shared/directives/directives.module.js'; export default angular .module('systemInfo', [ SystemInfocontroller, SystemCpuController, SystemMemoryController, - service + service, + directives ]) .name;
--- a/src/app/components/system-info/system-memory.controller.js Tue Jul 18 09:37:28 2017 -0400 +++ b/src/app/components/system-info/system-memory.controller.js Tue Jul 18 10:14:56 2017 -0400 @@ -184,6 +184,20 @@ } } } + + 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
--- a/src/app/components/system-info/system-memory.controller.spec.js Tue Jul 18 09:37:28 2017 -0400 +++ b/src/app/components/system-info/system-memory.controller.spec.js Tue Jul 18 10:14:56 2017 -0400 @@ -152,6 +152,37 @@ 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];
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/app/shared/directives/directives.module.js Tue Jul 18 10:14:56 2017 -0400 @@ -0,0 +1,39 @@ +/** + * 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. + */ + +let mods = []; +let req = require.context('./', true, /\.module\.js/); +req.keys().map(v => { + let name = req(v).default; + if (name) { + mods.push(name); + } +}); + +export default angular + .module('app.directives', mods) + .name;
--- a/src/app/shared/directives/dismissible-error-message/dismissible-error-message.directive.js Tue Jul 18 09:37:28 2017 -0400 +++ b/src/app/shared/directives/dismissible-error-message/dismissible-error-message.directive.js Tue Jul 18 10:14:56 2017 -0400 @@ -39,5 +39,6 @@ }; export default angular - .module('directives.dismissible-error-message', []) - .directive('dismissibleErrorMessage', dismissibleErrorMessageFunc); + .module('dismissibleErrorMessage.directive', []) + .directive('dismissibleErrorMessage', dismissibleErrorMessageFunc) + .name;
--- a/src/app/shared/directives/dismissible-error-message/dismissible-error-message.directive.spec.js Tue Jul 18 09:37:28 2017 -0400 +++ b/src/app/shared/directives/dismissible-error-message/dismissible-error-message.directive.spec.js Tue Jul 18 10:14:56 2017 -0400 @@ -25,11 +25,12 @@ * exception statement from your version. */ +import directiveModule from './dismissible-error-message.directive.js'; import {dismissibleErrorMessageFunc} from './dismissible-error-message.directive.js'; describe('dismissibleErrorMessage Directive', () => { let compiledDirectiveElement; - beforeEach(angular.mock.module('directives.dismissible-error-message')); + beforeEach(angular.mock.module(directiveModule)); let initDummyModule = () => { let compile, rootScope;
--- a/src/app/shared/directives/dismissible-error-message/dismissible-error-message.html Tue Jul 18 09:37:28 2017 -0400 +++ b/src/app/shared/directives/dismissible-error-message/dismissible-error-message.html Tue Jul 18 10:14:56 2017 -0400 @@ -4,6 +4,5 @@ </button> <span class="pficon pficon-warning-triangle-o"></span> - <strong>{{errTitle}}</strong> - {{errMessage}} + <strong>{{errTitle}}</strong> {{errMessage}} </div>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/app/shared/directives/dismissible-error-message/dismissible-error-message.module.js Tue Jul 18 10:14:56 2017 -0400 @@ -0,0 +1,32 @@ +/** + * 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 directive from './dismissible-error-message.directive.js'; + +export default angular + .module('dismissibleErrorMessage', [directive]) + .name;
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/app/shared/directives/multichart-add/multichart-add.controller.js Tue Jul 18 10:14:56 2017 -0400 @@ -0,0 +1,74 @@ +/** + * 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 servicesModule from 'shared/services/services.module.js'; + +class MultichartAddController { + constructor (multichartService, $scope, $timeout) { + 'ngInject'; + this.svc = multichartService; + this.scope = $scope; + this.scope.multicharts = multichartService.chartNames; + + this.scope.$watch('multicharts', cur => { + $timeout(() => { + cur.forEach(chart => { + let el = angular.element('#' + chart + '-' + this.scope.svcName); + el.bootstrapSwitch(); + el.on('switchChange.bootstrapSwitch', event => { + this.toggleChart(event.currentTarget.getAttribute('data-chart')); + }); + }); + }); + }); + } + + toggleChart (chartName) { + if (this.isInChart(chartName)) { + this.removeFromChart(chartName); + } else { + this.addToChart(chartName); + } + } + + removeFromChart (chartName) { + this.svc.removeService(chartName, this.scope.svcName); + } + + addToChart (chartName) { + this.svc.addService(chartName, this.scope.svcName, this.scope.getFn); + } + + isInChart (chartName) { + return this.svc.hasServiceForChart(chartName, this.scope.svcName); + } +} + +export default angular + .module('multichartAddControllerModule', [servicesModule]) + .controller('MultichartAddController', MultichartAddController) + .name;
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/app/shared/directives/multichart-add/multichart-add.controller.spec.js Tue Jul 18 10:14:56 2017 -0400 @@ -0,0 +1,172 @@ +/** + * 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 './multichart-add.controller.js'; + +describe('MultichartAddController', () => { + + let svc, scope, timeout, ctrl; + + beforeEach(() => { + angular.mock.module(controllerModule); + angular.mock.inject($controller => { + 'ngInject'; + + svc = { + chartNames: ['foo', 'bar'], + addService: sinon.spy(), + removeService: sinon.spy(), + hasServiceForChart: sinon.stub().returns(true) + }; + + scope = { + svcName: 'foo-svc', + getFn: sinon.spy(), + $watch: sinon.spy() + }; + + timeout = sinon.spy(); + + ctrl = $controller('MultichartAddController', { + multichartService: svc, + $scope: scope, + $timeout: timeout + }); + }); + }); + + it('should exist', () => { + should.exist(ctrl); + }); + + it('should assign multichart names from service', () => { + scope.multicharts.should.deepEqual(svc.chartNames); + }); + + describe('bootstrapSwitch', () => { + let mockElem; + beforeEach(() => { + mockElem = { + bootstrapSwitch: sinon.spy(), + on: sinon.spy() + }; + sinon.stub(angular, 'element').returns(mockElem); + }); + + afterEach(() => { + angular.element.restore(); + }); + + it('should watch on multicharts', () => { + scope.$watch.should.be.calledWith('multicharts', sinon.match.func); + }); + + it('should use $timeout to wait for $scope $digest to complete', () => { + timeout.should.not.be.called(); + let fn = scope.$watch.firstCall.args[1]; + fn(); + timeout.should.be.calledOnce(); + timeout.should.be.calledWith(sinon.match.func); + }); + + it('should set up bootstrapSwitch functionality for each switch', () => { + svc.removeService.should.not.be.called(); + let fn = scope.$watch.firstCall.args[1]; + fn(['foo']); + timeout.yield(); + + angular.element.should.be.calledOnce(); + angular.element.should.be.calledWith('#foo-foo-svc'); + mockElem.bootstrapSwitch.should.be.calledOnce(); + mockElem.on.should.be.calledOnce(); + mockElem.on.should.be.calledWith('switchChange.bootstrapSwitch', sinon.match.func); + let evtHandler = mockElem.on.firstCall.args[1]; + evtHandler({ + currentTarget: { + getAttribute: () => 'foo-chart' + } + }); + svc.removeService.should.be.calledOnce(); + svc.removeService.should.be.calledWith('foo-chart', 'foo-svc'); + }); + }); + + describe('isInChart', () => { + it('should delegate to service', () => { + svc.hasServiceForChart.should.not.be.called(); + ctrl.isInChart('foo').should.be.true(); + svc.hasServiceForChart.should.be.calledOnce(); + svc.hasServiceForChart.should.be.calledWith('foo', scope.svcName); + }); + }); + + describe('addToChart', () => { + it('should delegate to service', () => { + svc.addService.should.not.be.called(); + ctrl.addToChart('foo'); + svc.addService.should.be.calledOnce(); + svc.addService.should.be.calledWith('foo', scope.svcName, scope.getFn); + }); + }); + + describe('removeFromChart', () => { + it('should delegate to service', () => { + svc.removeService.should.not.be.called(); + ctrl.removeFromChart('foo'); + svc.removeService.should.be.calledOnce(); + svc.removeService.should.be.calledWith('foo', scope.svcName); + }); + }); + + describe('toggleChart', () => { + it('should remove if already added', () => { + svc.hasServiceForChart.should.not.be.called(); + svc.addService.should.not.be.called(); + svc.removeService.should.not.be.called(); + ctrl.toggleChart('foo'); + svc.hasServiceForChart.should.be.calledOnce(); + svc.hasServiceForChart.should.be.calledWith('foo', scope.svcName); + svc.addService.should.not.be.called(); + svc.removeService.should.be.calledOnce(); + svc.removeService.should.be.calledWith('foo', scope.svcName); + }); + + it('should add if not present', () => { + svc.hasServiceForChart.returns(false); + svc.hasServiceForChart.should.not.be.called(); + svc.addService.should.not.be.called(); + svc.removeService.should.not.be.called(); + ctrl.toggleChart('foo'); + svc.hasServiceForChart.should.be.calledOnce(); + svc.hasServiceForChart.should.be.calledWith('foo', scope.svcName); + svc.addService.should.be.calledOnce(); + svc.addService.should.be.calledWith('foo', scope.svcName); + svc.removeService.should.not.be.called(); + }); + }); + +});
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/app/shared/directives/multichart-add/multichart-add.directive.js Tue Jul 18 10:14:56 2017 -0400 @@ -0,0 +1,48 @@ +/** + * 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 './multichart-add.controller.js'; + +function configFactory () { + return { + restrict: 'EA', + controller: 'MultichartAddController', + controllerAs: 'ctrl', + template: require('./multichart-add.html'), + scope: { + svcName: '@', + getFn: '&' + } + }; +} + +export { configFactory }; + +export default angular + .module('multichartAddDirective', [controller]) + .directive('mcAdd', configFactory) + .name;
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/app/shared/directives/multichart-add/multichart-add.directive.spec.js Tue Jul 18 10:14:56 2017 -0400 @@ -0,0 +1,62 @@ +/** + * 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 { configFactory } from './multichart-add.directive.js'; + +describe('MultichartAddDirective', () => { + let cfg; + beforeEach(() => { + cfg = configFactory(); + }); + + it('should be restricted to an element or attribute', () => { + cfg.restrict.should.have.length(2); + cfg.restrict.should.containEql('E'); + cfg.restrict.should.containEql('A'); + }); + + it('should expect a svcName string in scope', () => { + cfg.scope.should.have.ownProperty('svcName'); + cfg.scope.svcName.should.equal('@'); + }); + + it('should expect a getFn expression in scope', () => { + cfg.scope.should.have.ownProperty('getFn'); + cfg.scope.getFn.should.equal('&'); + }); + + it('should use correct template', () => { + cfg.template.should.equal(require('./multichart-add.html')); + }); + + it('should attach multichartAddController', () => { + cfg.should.have.ownProperty('controller'); + cfg.controller.should.equal('MultichartAddController'); + cfg.should.have.ownProperty('controllerAs'); + cfg.controllerAs.should.equal('ctrl'); + }); +});
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/app/shared/directives/multichart-add/multichart-add.html Tue Jul 18 10:14:56 2017 -0400 @@ -0,0 +1,24 @@ +<div ng-if="multicharts.length > 0"> + <div class="dropdown dropdown-kebab-pf"> + <button class="btn btn-link dropdown-toggle" type="button" id="kebab" data-toggle="dropdown" aria-haspopup="true" aria-expanded="true"> + <span class="fa fa-ellipsis-v"></span> + </button> + <ul class="dropdown-menu dropdown-menu-right" aria-labelledby="kebab"> + <li>Multicharts</li> + <li role="separator" class="divider"></li> + <li ng-repeat="chart in multicharts"> + <div class="text-right"> + <div class="form-group"> + <label for="switch-{{chart}}-{{svcName}}" class="label label-info pull-left">{{chart}}</label> + <input class="bootstrap-switch pull-right" id="{{chart}}-{{svcName}}" name="switch-{{chart}}-{{svcName}}" data-chart="{{chart}}" + data-size="mini" + type="checkbox" + ng-checked="ctrl.isInChart(chart)" + /> + </div> + </div> + </li> + <li> + </ul> + </div> +</div>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/app/shared/directives/multichart-add/multichart-add.module.js Tue Jul 18 10:14:56 2017 -0400 @@ -0,0 +1,36 @@ +/** + * 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 directive from './multichart-add.directive.js'; +import controller from './multichart-add.controller.js'; + +export default angular + .module('multichartAddModule', [ + directive, + controller + ]) + .name;
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/app/shared/services/multichart.service.js Tue Jul 18 10:14:56 2017 -0400 @@ -0,0 +1,155 @@ +/** + * 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 servicesModule from 'shared/services/services.module.js'; +import _ from 'lodash'; + +class MultiChartService { + constructor () { + this.charts = new Map(); + } + + get chartNames () { + let res = []; + for (let key of this.charts.keys()) { + res.push(key); + } + res.sort(); + return res; + } + + countServicesForChart (chartName) { + if (!this.charts.has(chartName)) { + return 0; + } + return this.charts.get(chartName).length; + } + + getAxesForChart (chartName) { + //TODO: make this more intelligent once we start getting extended info + //back from gateway about what metrics are, the units and context. + //then we can group similar metrics onto the same axis. + if (!this.hasChart(chartName)) { + return {}; + } + let charts = this.charts.get(chartName); + let ret = {}; + + for (let i = 0; i < this.countServicesForChart(chartName); i++) { + let axis; + if (i === 0) { + axis = 'y'; + } else { + axis = 'y2'; + } + ret[charts[i].svcName] = axis; + } + return ret; + } + + addChart (chartName) { + if (this.charts.has(chartName)) { + return; + } + this.charts.set(chartName, new Array()); + } + + hasChart (chartName) { + return this.charts.has(chartName); + } + + removeChart (chartName) { + if (!this.hasChart(chartName)) { + return; + } + this.charts.delete(chartName); + } + + addService (chartName, svcName, getFn) { + if (!this.hasChart(chartName) || this.hasServiceForChart(chartName, svcName)) { + return; + } + let svcs = this.charts.get(chartName); + svcs.push({ + svcName: svcName, + getFn: getFn + }); + } + + hasServiceForChart (chartName, svcName) { + if (!this.hasChart(chartName)) { + return false; + } + let svcs = this.charts.get(chartName); + for (let i = 0; i < svcs.length; i++) { + if (svcs[i].svcName === svcName) { + return true; + } + } + return false; + } + + removeService (chartName, svcName) { + if (!this.hasChart(chartName)) { + return; + } + _.remove(this.charts.get(chartName), svc => svc.svcName === svcName); + } + + getData (chartName) { + if (!this.charts.has(chartName)) { + return new Promise((resolve, reject) => reject(new Error('No such multichart ' + chartName))); + } + + let svcs = this.charts.get(chartName); + let promises = []; + let res = {}; + for (let i = 0; i < svcs.length; i++) { + let svc = svcs[i]; + let key; + if (i === 0) { + key = 'yData'; + } else { + key = 'yData' + i; + } + res[key] = [svc.svcName]; + let promise = svc.getFn(); + promises.push(promise); + promise.then(data => { + res[key].push(data); + }); + } + + return new Promise(resolve => { + Promise.all(promises).then(() => resolve(res)); + }); + } +} + +angular + .module(servicesModule) + .service('multichartService', MultiChartService);
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/app/shared/services/multichart.service.spec.js Tue Jul 18 10:14:56 2017 -0400 @@ -0,0 +1,298 @@ +/** + * 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 servicesModule from 'shared/services/services.module.js'; + +describe('MultichartService', () => { + + let svc; + beforeEach(() => { + angular.mock.module(servicesModule); + angular.mock.inject(multichartService => { + 'ngInject'; + svc = multichartService; + }); + }); + + it('should exist', () => { + should.exist(svc); + }); + + it('should initialize an empty charts map', () => { + svc.charts.should.be.an.instanceof(Map); + svc.charts.should.eql(new Map()); + }); + + describe('chartNames', () => { + it('should be initially an empty array', () => { + let res = svc.chartNames; + res.should.be.an.Array(); + res.should.deepEqual([]); + }); + + it('should reflect added charts', () => { + svc.addChart('foo'); + svc.addChart('bar'); + svc.chartNames.should.deepEqual(['bar', 'foo']); + }); + + it('should reflect removed charts', () => { + svc.addChart('foo'); + svc.addChart('bar'); + svc.chartNames.should.deepEqual(['bar', 'foo']); + svc.removeChart('foo'); + svc.chartNames.should.deepEqual(['bar']); + }); + + it('should not contain duplicates', () => { + svc.addChart('foo'); + svc.addChart('foo'); + svc.chartNames.should.deepEqual(['foo']); + }); + + it('should be sorted alphabetically', () => { + svc.addChart('b'); + svc.addChart('a'); + svc.addChart('d'); + svc.addChart('c'); + svc.chartNames.should.deepEqual(['a', 'b', 'c', 'd']); + }); + }); + + describe('countServicesForChart', () => { + it('should return 0 for nonexistent chart', () => { + svc.countServicesForChart('foo').should.equal(0); + }); + + it('should return 0 for chart with no services', () => { + svc.addChart('foo'); + svc.countServicesForChart('foo').should.equal(0); + }); + + it('should return the service count', () => { + svc.addChart('foo'); + svc.addService('foo', 'foo-svc-A', angular.noop); + svc.addService('foo', 'foo-svc-B', angular.noop); + svc.countServicesForChart('foo').should.equal(2); + }); + + it('should reflect removed services', () => { + svc.addChart('foo'); + svc.addService('foo', 'foo-svc-A', angular.noop); + svc.addService('foo', 'foo-svc-B', angular.noop); + svc.countServicesForChart('foo').should.equal(2); + svc.removeService('foo', 'foo-svc-B'); + svc.countServicesForChart('foo').should.equal(1); + }); + + it('should not count duplicate services', () => { + svc.addChart('foo'); + svc.addService('foo', 'foo-svc-A', angular.noop); + svc.addService('foo', 'foo-svc-A', angular.noop); + svc.countServicesForChart('foo').should.equal(1); + }); + }); + + describe('getAxesForChart', () => { + it('should return an empty object for nonexistent chart', () => { + svc.getAxesForChart('foo').should.deepEqual({}); + }); + + it('should return a single-axis config for a chart with one service', () => { + svc.addChart('foo'); + svc.addService('foo', 'foo-svc-A', angular.noop); + svc.getAxesForChart('foo').should.deepEqual({'foo-svc-A': 'y'}); + }); + + it('should return a two-axis config for a chart with two services', () => { + svc.addChart('foo'); + svc.addService('foo', 'foo-svc-A', angular.noop); + svc.addService('foo', 'foo-svc-B', angular.noop); + svc.getAxesForChart('foo').should.deepEqual( + { + 'foo-svc-A': 'y', + 'foo-svc-B': 'y2' + } + ); + }); + + it('should return a two-axis config for a chart with three services', () => { + svc.addChart('foo'); + svc.addService('foo', 'foo-svc-A', angular.noop); + svc.addService('foo', 'foo-svc-B', angular.noop); + svc.addService('foo', 'foo-svc-C', angular.noop); + svc.getAxesForChart('foo').should.deepEqual( + { + 'foo-svc-A': 'y', + 'foo-svc-B': 'y2', + 'foo-svc-C': 'y2' + } + ); + }); + }); + + describe('addChart', () => { + it('should add new charts', () => { + svc.hasChart('foo').should.be.false(); + svc.addChart('foo'); + svc.hasChart('foo').should.be.true(); + }); + + it('should not add duplicates', () => { + svc.addChart('foo'); + svc.addChart('foo'); + let map = new Map(); + map.set('foo', new Array()); + svc.charts.should.eql(map); + svc.hasChart('foo').should.be.true(); + }); + + it('should not clobber services for repeat additions', () => { + svc.addChart('foo'); + svc.addService('foo', 'foo-svc-A', angular.noop); + svc.addChart('foo'); + let map = new Map(); + map.set('foo', [{svcName: 'foo-svc-A', getFn: angular.noop}]); + svc.charts.should.eql(map); + }); + }); + + describe('removeChart', () => { + it('should do nothing for nonexistent charts', () => { + svc.hasChart('foo').should.be.false(); + svc.removeChart('foo'); + svc.hasChart('foo').should.be.false(); + svc.charts.should.deepEqual(new Map()); + }); + + it('should remove added charts', () => { + svc.addChart('foo'); + svc.hasChart('foo').should.be.true(); + svc.removeChart('foo'); + svc.hasChart('foo').should.be.false(); + }); + }); + + describe('addService', () => { + it('should do nothing for nonexistent chart', () => { + svc.hasServiceForChart('foo', 'foo-svc-A').should.be.false(); + svc.addService('foo', 'foo-svc-A'); + svc.hasServiceForChart('foo', 'foo-svc-A').should.be.false(); + svc.countServicesForChart('foo').should.equal(0); + }); + + it('should add service to chart', () => { + svc.hasServiceForChart('foo', 'foo-svc-A').should.be.false(); + svc.addChart('foo'); + svc.addService('foo', 'foo-svc-A'); + svc.hasServiceForChart('foo', 'foo-svc-A').should.be.true(); + svc.countServicesForChart('foo').should.equal(1); + }); + + it('should do nothing if service already added', () => { + svc.hasServiceForChart('foo', 'foo-svc-A').should.be.false(); + svc.addChart('foo'); + svc.addService('foo', 'foo-svc-A'); + svc.hasServiceForChart('foo', 'foo-svc-A').should.be.true(); + svc.addService('foo', 'foo-svc-A'); + svc.hasServiceForChart('foo', 'foo-svc-A').should.be.true(); + svc.countServicesForChart('foo').should.equal(1); + }); + }); + + describe('hasServiceForChart', () => { + it('should return false for nonexistent chart', () => { + svc.hasServiceForChart('foo', 'foo-svc-A').should.be.false(); + }); + + it('should return false for nonexistent service', () => { + svc.addChart('foo'); + svc.addService('foo', 'foo-svc-B'); + svc.hasServiceForChart('foo', 'foo-svc-A').should.be.false(); + }); + + it('should return true for existing service', () => { + svc.addChart('foo'); + svc.addService('foo', 'foo-svc-A'); + svc.hasServiceForChart('foo', 'foo-svc-A').should.be.true(); + }); + }); + + describe('removeService', () => { + it('should do nothing for nonexistent chart', () => { + svc.hasServiceForChart('foo', 'foo-svc-A').should.be.false(); + svc.hasChart('foo').should.be.false(); + svc.removeService('foo', 'foo-svc-A'); + svc.hasServiceForChart('foo', 'foo-svc-A').should.be.false(); + svc.hasChart('foo').should.be.false(); + }); + + it('should do nothing for nonexistent service', () => { + svc.addChart('foo'); + svc.hasServiceForChart('foo', 'foo-svc-A').should.be.false(); + svc.hasChart('foo').should.be.true(); + svc.removeService('foo', 'foo-svc-A'); + svc.hasServiceForChart('foo', 'foo-svc-A').should.be.false(); + svc.hasChart('foo').should.be.true(); + }); + + it('should remove services', () => { + svc.addChart('foo'); + svc.addService('foo', 'foo-svc-A', angular.noop); + svc.hasServiceForChart('foo', 'foo-svc-A').should.be.true(); + svc.removeService('foo', 'foo-svc-A'); + svc.hasServiceForChart('foo', 'foo-svc-A').should.be.false(); + }); + }); + + describe('getData', () => { + it('should reject for nonexistent chart', () => { + return svc.getData('foo').should.be.rejectedWith(Error); + }); + + it('should resolve data from single service getter functions', () => { + svc.addChart('foo'); + svc.addService('foo', 'foo-svc-A', () => new Promise(resolve => resolve(500))); + return svc.getData('foo').should.be.fulfilledWith({ yData: ['foo-svc-A', 500] }); + }); + + it('should resolve data from multiple service getter functions', () => { + svc.addChart('foo'); + svc.addService('foo', 'foo-svc-A', () => new Promise(resolve => resolve(500))); + svc.addService('foo', 'foo-svc-B', () => new Promise(resolve => resolve(600))); + svc.addService('foo', 'foo-svc-C', () => new Promise(resolve => resolve(700))); + svc.addChart('bar'); + svc.addService('bar', 'bar-svc-A', () => new Promise(resolve => resolve(500))); + return svc.getData('foo').should.be.fulfilledWith({ + yData: ['foo-svc-A', 500], + yData1: ['foo-svc-B', 600], + yData2: ['foo-svc-C', 700] + }); + }); + }); + +});
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/app/shared/services/sanitize.service.js Tue Jul 18 10:14:56 2017 -0400 @@ -0,0 +1,46 @@ +/** + * 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 servicesModule from 'shared/services/services.module.js'; + +class SanitizeService { + sanitize (str) { + if (!str) { + return; + } + if (typeof str !== 'string') { + return str; + } + let res = str; + res = res.replace(/\s+/g, '-'); + return res; + } +} + +angular + .module(servicesModule) + .service('sanitizeService', SanitizeService);
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/app/shared/services/sanitize.service.spec.js Tue Jul 18 10:14:56 2017 -0400 @@ -0,0 +1,83 @@ +/** + * 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 servicesModule from 'shared/services/services.module.js'; + +describe('SanitizeService', () => { + + let svc; + beforeEach(() => { + angular.mock.module(servicesModule); + angular.mock.inject(sanitizeService => { + 'ngInject'; + svc = sanitizeService; + }); + }); + + it('should exist', () => { + should.exist(svc); + }); + + describe('non-string inputs', () => { + it('should do nothing to undefined', () => { + should(svc.sanitize(undefined)).be.undefined(); + }); + + it('should do nothing to null', () => { + should(svc.sanitize(null)).be.undefined(); + }); + + it('should return the argument if number', () => { + svc.sanitize(5).should.equal(5); + }); + + it('should return the argument if object', () => { + svc.sanitize({}).should.eql({}); + }); + + it('should return the argument if array', () => { + svc.sanitize([]).should.eql([]); + }); + }); + + it('should do nothing to sane strings', () => { + svc.sanitize('foo').should.equal('foo'); + }); + + it('should sanitize string with a space', () => { + svc.sanitize('foo bar').should.equal('foo-bar'); + }); + + it('should sanitize string with multiple spaces', () => { + svc.sanitize('foo and bar').should.equal('foo-and-bar'); + }); + + it('should sanitize string with long spaces', () => { + svc.sanitize('foo bar').should.equal('foo-bar'); + }); + +});
--- a/webpack.config.js Tue Jul 18 09:37:28 2017 -0400 +++ b/webpack.config.js Tue Jul 18 10:14:56 2017 -0400 @@ -21,6 +21,7 @@ 'angular': 'angular-patternfly/node_modules/angular', 'd3': 'angular-patternfly/node_modules/patternfly/node_modules/d3', 'c3': 'angular-patternfly/node_modules/patternfly/node_modules/c3', + 'bootstrap': 'angular-patternfly/node_modules/patternfly/node_modules/bootstrap/dist/js/bootstrap.js', 'bootstrap-switch': 'angular-patternfly/node_modules/patternfly/node_modules/bootstrap-switch', 'assets': path.resolve(__dirname, 'src', 'assets'),