Mercurial > hg > thermostat-ng > web-client
changeset 164:a5f9d8c66e1b
Use pfListView & pfPagination for jvm-list
This patch introduces the PatternFly listview (& toolbar) and pagination directives to jvm-list. Additionally, there are changes to the jvms mock-endpoint so the toolbar actions (sorting & searching) can be tested in a development setting. The dependency for angular-patternfly also had to be updated from 3.23.3 to 4.4.1.
Reviewed-by: aazores
Review-thread: http://icedtea.classpath.org/pipermail/thermostat/2017-August/024633.html
author | Alex Macdonald <almacdon@redhat.com> |
---|---|
date | Tue, 22 Aug 2017 16:30:30 -0400 |
parents | c029b9611981 |
children | d805dbe67564 |
files | mock-api/endpoints/jvms.endpoint.js mock-api/endpoints/system-info.endpoint.js package.json src/app/components/jvm-info/jvm-gc/jvm-gc.html src/app/components/jvm-info/jvm-memory/jvm-memory.html src/app/components/jvm-list/en.locale.yaml src/app/components/jvm-list/jvm-list.controller.js src/app/components/jvm-list/jvm-list.controller.spec.js src/app/components/jvm-list/jvm-list.html src/app/components/jvm-list/jvm-list.module.js src/app/components/multichart/multichart.html src/app/components/system-info/system-info.html src/assets/scss/main.scss webpack.config.js |
diffstat | 14 files changed, 580 insertions(+), 183 deletions(-) [+] |
line wrap: on
line diff
--- a/mock-api/endpoints/jvms.endpoint.js Thu Aug 17 09:12:58 2017 -0400 +++ b/mock-api/endpoints/jvms.endpoint.js Tue Aug 22 16:30:30 2017 -0400 @@ -5,46 +5,33 @@ server.logRequest('jvm-list', req); res.setHeader('Content-Type', 'application/json'); - var limit = 4; + var systemLimit = 4; var aliveOnly = req.query.aliveOnly === 'true'; var resp = []; if (req.query.limit) { - limit = parseInt(req.query.limit); + systemLimit = parseInt(req.query.limit); // 0 means no limit, so we'll default to 4 - if (limit === 0) { - limit = 4; + if (systemLimit === 0) { + systemLimit = 4; } } - for (var i = 0; i < limit; i++) { - var jvms = [ - { - 'mainClass': 'c.r.t.A', + for (var i = 0; i < systemLimit; i++) { + var jvms = []; + for (var j = 0; j < systemLimit - i; j++) { + jvms.push({ + 'mainClass': 'c.r.t.' + j, 'startTime': { $numberLong: (Date.now() - 10000000).toString() }, 'stopTime': { $numberLong: '-1' }, - 'jvmId': 'vm-0', - 'isAlive': true - }, - { - 'mainClass': 'c.r.t.B', - 'startTime': { $numberLong: (Date.now() - 1500000).toString() }, - 'stopTime': { $numberLong: '-1' }, - 'jvmId': 'vm-1', + 'jvmId': 'vm-' + j, 'isAlive': true - }, - { - 'mainClass': 'c.r.t.C', - 'startTime': { $numberLong: (Date.now() - 25000000).toString() }, - 'stopTime': { $numberLong: '-1' }, - 'jvmId': 'vm-2', - 'isAlive': true - } - ]; + }); + } if (!aliveOnly) { jvms.push({ - 'mainClass': 'c.r.t.D', + 'mainClass': 'c.r.t.DeadVM', 'startTime': { $numberLong: (Date.now() - 350000000).toString() }, 'stopTime': { $numberLong: Date.now().toString() }, - 'jvmId': 'vm-3', + 'jvmId': 'vm-dead', 'isAlive': false }); }
--- a/mock-api/endpoints/system-info.endpoint.js Thu Aug 17 09:12:58 2017 -0400 +++ b/mock-api/endpoints/system-info.endpoint.js Tue Aug 22 16:30:30 2017 -0400 @@ -12,7 +12,8 @@ osKernel: '4.10.11-200.fc25.x86_64', cpuCount: 4, cpuModel: 'GenuineIntel', - totalMemory: 16 * 1024 * 1024 * 1024 + totalMemory: 16 * 1024 * 1024 * 1024, + timeCreated: { $numberLong: (Date.now() - (Math.floor(Math.random() * 100000000))).toString() } }] } ));
--- a/package.json Thu Aug 17 09:12:58 2017 -0400 +++ b/package.json Tue Aug 22 16:30:30 2017 -0400 @@ -12,7 +12,7 @@ "devDependencies": { "@uirouter/angularjs": "^1.0.0", "angular-mocks": "1.5.*", - "angular-patternfly": "^3.23.3", + "angular-patternfly": "^4.4.1", "angular-translate": "^2.15.2", "angular-translate-interpolation-messageformat": "^2.15.2", "babel-core": "^6.24.0",
--- a/src/app/components/jvm-info/jvm-gc/jvm-gc.html Thu Aug 17 09:12:58 2017 -0400 +++ b/src/app/components/jvm-info/jvm-gc/jvm-gc.html Tue Aug 22 16:30:30 2017 -0400 @@ -28,7 +28,7 @@ <div class="row row-cards-pf"> <div class="container-fluid container-cards-pf"> - <div class="col"> + <div class="col-md-12"> <div class="card-pf card-pf-view"> <div class="card-pf-heading"> <label class="card-pf-title" translate>jvmGc.CARD_TITLE</label> @@ -36,11 +36,11 @@ <div ng-repeat="collector in ctrl.collectors"> <div class="card-pf-body text-center"> <mc-add class="pull-right" svc-name="{{ctrl.jvmId}}-{{sanitize(collector)}}-gc" get-fn="ctrl.multichartFn(collector)"></mc-add> - <div pf-line-chart id="chart-{{collector}}" config="ctrl.chartConfigs[collector]" + <pf-line-chart id="chart-{{collector}}" config="ctrl.chartConfigs[collector]" chart-data="ctrl.chartData[collector]" show-x-axis="true" show-y-axis="true" - ></div> + ></pf-line-chart> </div> </div> </div>
--- a/src/app/components/jvm-info/jvm-memory/jvm-memory.html Thu Aug 17 09:12:58 2017 -0400 +++ b/src/app/components/jvm-info/jvm-memory/jvm-memory.html Tue Aug 22 16:30:30 2017 -0400 @@ -24,7 +24,7 @@ </div> <div class="card-pf-body"> <mc-add class="pull-right" svc-name="{{ctrl.jvmId}}-metaspace" get-fn="ctrl.multichartMetaspace()"></mc-add> - <div pf-donut-pct-chart id="metaspaceChart" config="ctrl.metaspaceConfig" data="ctrl.metaspaceData"></div> + <pf-donut-pct-chart id="metaspaceChart" config="ctrl.metaspaceConfig" data="ctrl.metaspaceData"></pf-donut-pct-chart> </div> </div> </div> @@ -46,8 +46,8 @@ <label translate="jvmMemory.SPACE" translate-values="{ index: space.index }"></label> <mc-add class="pull-right" svc-name="{{ctrl.jvmId}}-{{sanitize(generation.name)}}-space{{space.index}}" get-fn="ctrl.multichartSpace(index, space.index)"></mc-add> - <div pf-donut-pct-chart id="gen-{{gen.index}}-space-{{space.index}}" - config="ctrl.spaceConfigs['gen-' + generation.index + '-space-' + space.index]" data="space"></div> + <pf-donut-pct-chart id="gen-{{gen.index}}-space-{{space.index}}" + config="ctrl.spaceConfigs['gen-' + generation.index + '-space-' + space.index]" data="space"></pf-donut-pct-chart> </div> </div> </div>
--- a/src/app/components/jvm-list/en.locale.yaml Thu Aug 17 09:12:58 2017 -0400 +++ b/src/app/components/jvm-list/en.locale.yaml Tue Aug 22 16:30:30 2017 -0400 @@ -1,8 +1,20 @@ jvmList: BREADCRUMB: '@:landing.nav.JVM_LISTING' - ALIVE_ONLY: Alive Only - SYSTEM_CARD_LABEL: Monitored System - CREATED_ON: <strong>Created On</strong> {{date}} + ALIVE_ONLY: Alive JVMs Only + SYSTEM_INFO_LABEL: System Information + START_TIME_LIST: Start Time: {{date}} + START_TIME_CARD: <strong>Start Time:</strong>{{date}} ERR_TITLE: Unable to retrieve data. ERR_MESSAGE: Error while retrieving Thermostat JVM Listing. + + HOSTNAME_TITLE: 'Hostname' + + filterConfig: + HOSTNAME_PLACEHOLDER: Filter by System Hostname... + JVM_NAME_TITLE: JVM MainClass Name + JVM_NAME_PLACEHOLDER: Filter by JVM Class Name... + + sortConfig: + TIME_CREATED_TITLE: 'Host: Time Created' + NUM_JVMS_TITLE: 'Host: Number of JVMs' \ No newline at end of file
--- a/src/app/components/jvm-list/jvm-list.controller.js Thu Aug 17 09:12:58 2017 -0400 +++ b/src/app/components/jvm-list/jvm-list.controller.js Tue Aug 22 16:30:30 2017 -0400 @@ -26,21 +26,42 @@ */ import filters from 'shared/filters/filters.module.js'; -import service from './jvm-list.service.js'; import directives from 'shared/directives/directives.module.js'; +import jvmListService from './jvm-list.service.js'; +import systemInfoService from 'components/system-info/system-info.service.js'; class JvmListController { - constructor (jvmListService, $scope, $location, $timeout, $anchorScroll, $translate) { + constructor (jvmListService, systemInfoService, $scope, $location, $timeout, $translate) { 'ngInject'; + this.scope = $scope; this.jvmListService = jvmListService; + this.systemInfoService = systemInfoService; this.scope = $scope; this.location = $location; this.timeout = $timeout; - this.anchorScroll = $anchorScroll; + this.translate = $translate; + this.systemsOpen = {}; + + $scope.sortConfig = {}; + $scope.filterConfig = {}; + $scope.listConfig = { + showSelectBox: false, + useExpandingRows: true, + onClick: item => $location.hash(this.changeLocationHash(item)) + }; - $translate('jvmList.ERR_TITLE').then(s => $scope.errTitle = s); - $translate('jvmList.ERR_MESSAGE').then(s => $scope.errMessage = s); + // Settings for pfPagination + $scope.pageNumber = 1; + $scope.pageSize = 4; + $scope.pageSizeIncrements = [4, 8, 12, 16, 20]; + $scope.emptyStateConfig = { + icon: 'pficon-warning-triangle-o', + }; + $translate('jvmList.ERR_TITLE').then(s => $scope.emptyStateConfig.title = s); + $translate('jvmList.ERR_MESSAGE').then(s => $scope.emptyStateConfig.info = s); + + $location.hash(''); this.aliveOnly = true; let aliveOnlySwitch = angular.element('#aliveOnlyState'); aliveOnlySwitch.bootstrapSwitch(); @@ -49,42 +70,197 @@ this.loadData(); }); - this.title = 'JVM Listing'; - this.showErr = false; - this.systemsOpen = {}; + this.loadData(); + this.constructToolbarSettings(); + } - this.loadData(); + /** + * Given an item, it will append or remove the item from the location + * hash depending on the result of (systemsOpen[item.systemId]) + * E.g., if systemsOpen[item.systemId] is true, then the item is + * currently expanded in the pf-list-view, and the URL hash should be + * appended with the systemId of the item. Subsequently calling + * changeLocationHash with the same item will toggle it's boolean + * state in systemsOpen, and the location hash will be rebuilt to + * include only systems that are currently open. + * @param {Object} item + * @param {String || Number} hash + */ + changeLocationHash (item, hash = this.location.hash()) { + this.systemsOpen[item.systemId] = !this.systemsOpen[item.systemId]; + if (this.systemsOpen[item.systemId]) { + if (hash === '') { + hash = item.hostname; + } else { + hash += '+' + item.hostname; + } + } else { // rebuild the location hash string + hash = ''; + for (let index in this.systemsOpen) { + if (hash === '' && this.systemsOpen[index]) { + hash = index; + } else if (this.systemsOpen[index]) { + hash += '+' + index; + } + } + } + return hash; } loadData () { + this.scope.allItems = []; + this.scope.items = this.scope.allItems; this.jvmListService.getSystems(this.aliveOnly).then( resp => { this.showErr = false; this.systems = resp.data.response; - for (var i = 0; i < this.systems.length; i++) { + for (let i = 0; i < this.systems.length; i++) { let system = this.systems[i]; this.systemsOpen[system.systemId] = false; + this.systemInfoService.getSystemInfo(system.systemId).then( + resp => { + this.scope.allItems.push({ + systemId: system.systemId, + hostname: resp.data.response[0].hostname, + jvms: system.jvms, + timeCreated: resp.data.response[0].timeCreated + }); + } + ); } if (this.systems.length === 1) { this.systemsOpen[this.systems[0].systemId] = true; } - - let hash = this.location.hash(); - if (hash) { - this.systemsOpen[hash] = true; - } - this.onload(); + this.scope.listConfig.itemsAvailable = true; + this.scope.toolbarConfig.filterConfig.resultsCount = this.systems.length; }, () => { + this.scope.listConfig.itemsAvailable = false; this.showErr = true; } ); } - onload () { - this.timeout(this.anchorScroll); + constructToolbarSettings () { + this.scope.filterConfig = { + fields: [ + { + id: 'hostname', + filterType: 'text' + }, + { + id: 'jvmName', + filterType: 'text' + } + ], + resultsCount: this.scope.items.length, + totalCount: this.scope.allItems.length, + appliedFilters: [], + onFilterChange: filters => { + this.applyFilters(filters); + this.scope.toolbarConfig.filterConfig.resultsCount = this.scope.items.length; + } + }; + this.translate('jvmList.HOSTNAME_TITLE').then(s => this.scope.filterConfig.fields[0].title = s); + this.translate('jvmList.filterConfig.HOSTNAME_PLACEHOLDER').then(s => this.scope.filterConfig.fields[0].placeholder = s); + this.translate('jvmList.filterConfig.JVM_NAME_TITLE').then(s => this.scope.filterConfig.fields[1].title = s); + this.translate('jvmList.filterConfig.JVM_NAME_PLACEHOLDER').then(s => this.scope.filterConfig.fields[1].placeholder = s); + + this.scope.sortConfig = { + fields: [ + { + id: 'name', + sortType: 'alpha' + }, + { + id: 'timeCreated', + sortType: 'numeric' + }, + { + id: 'numJvms', + sortType: 'numeric' + } + ], + onSortChange: () => { + this.sortItems(); + } + }; + this.translate('jvmList.HOSTNAME_TITLE').then(s => this.scope.sortConfig.fields[0].title = s); + this.translate('jvmList.sortConfig.TIME_CREATED_TITLE').then(s => this.scope.sortConfig.fields[1].title = s); + this.translate('jvmList.sortConfig.NUM_JVMS_TITLE').then(s => this.scope.sortConfig.fields[2].title = s); + + this.scope.toolbarConfig = { + filterConfig: this.scope.filterConfig, + sortConfig: this.scope.sortConfig + }; + } + + /** + * Starter code for matchesFilter, matchesFilters, applyFilters, and compareFn borrowed + * from the Angular-PatternFly API at: + * http://www.patternfly.org/angular-patternfly/#/api/patternfly.toolbars.componenet:pfToolbar + */ + matchesFilter (item, filter) { + let match = false; + let re = new RegExp(filter.value, 'i'); + if (filter.id === 'hostname') { + match = item.hostname.match(re) !== null; + } else if (filter.id === 'jvmName') { + for (let i = 0; i < item.jvms.length; i++) { + match = (item.jvms[i].mainClass).match(re) !== null; + if (match) { + break; + } + } + } + return match; + } + + matchesFilters (item, filters) { + let matches = true; + filters.forEach(filter => { + if (!this.matchesFilter(item, filter)) { + matches = false; + return false; + } else { + return true; + } + }); + return matches; + } + + applyFilters (filters) { + this.scope.items = []; + if (filters && filters.length > 0) { + this.scope.allItems.forEach(item => { + if (this.matchesFilters(item, filters)) { + this.scope.items.push(item); + } + }); + } else { + this.scope.items = this.scope.allItems; + } + } + + compareFn (item1, item2) { + let compValue = 0; + if (this.scope.sortConfig.currentField.id === 'name') { + compValue = item1.hostname.localeCompare(item2.systemId); + } else if (this.scope.sortConfig.currentField.id === 'timeCreated') { + compValue = item1.timeCreated.$numberLong > item2.timeCreated.$numberLong ? 1 : -1; + } else if (this.scope.sortConfig.currentField.id === 'numJvms') { + compValue = item1.jvms.length > item2.jvms.length ? 1 : -1; + } + if (!this.scope.sortConfig.isAscending) { + compValue = compValue * -1; + } + return compValue; + } + + sortItems () { + this.scope.items.sort((item1, item2) => this.compareFn(item1, item2)); } } @@ -92,9 +268,11 @@ export default angular .module('jvmList.controller', [ 'patternfly', + 'patternfly.toolbars', filters, directives, - service + jvmListService, + systemInfoService ]) .controller('JvmListController', JvmListController) .name;
--- a/src/app/components/jvm-list/jvm-list.controller.spec.js Thu Aug 17 09:12:58 2017 -0400 +++ b/src/app/components/jvm-list/jvm-list.controller.spec.js Tue Aug 22 16:30:30 2017 -0400 @@ -29,7 +29,53 @@ beforeEach(angular.mock.module('jvmList.controller')); - let ctrl, svc, scope, promise, location, timeout, anchorScroll, translate; + let controller, jvmListSvc, systemInfoSvc, scope, promise, location, timeout, translate; + + let fooItem = { + hostname: 'foo', + systemId: 'foo', + timeCreated: { $numberLong: '1500000000000' }, + jvms: [{ + mainClass: 'fooClass' + }] + }; + + let barbazItem = { + hostname: 'barbaz', + systemId: 'barbaz', + timeCreated: { $numberLong: '1400000000000' }, + jvms: [ + { + mainClass: 'barClass' + }, + { + mainClass: 'bazClass' + } + ] + }; + + let filters = [ + { + id: 'hostname', + title: 'Hostname', + value: 'foo' + }, + { + id: 'jvmName', + title: 'JVM MainClass Name', + value: 'fooClass' + } + ]; + + let generateSortConfig = (ascending, id) => { + return { + isAscending: ascending, + currentField: { + id: id + } + }; + }; + beforeEach(inject(($q, $rootScope, $controller) => { 'ngInject'; sinon.stub(angular, 'element').withArgs('#aliveOnlyState').returns({ @@ -39,39 +85,107 @@ scope = $rootScope; promise = $q.defer(); location = { - hash: () => '' + hash: sinon.stub().returns('') }; timeout = sinon.spy(); - anchorScroll = sinon.spy(); translate = sinon.stub().returns({ then: sinon.stub().yields() }); - svc = { + jvmListSvc = { getSystems: sinon.stub().returns(promise.promise) }; - ctrl = $controller('JvmListController', { - jvmListService: svc, + systemInfoSvc = { + getSystemInfo: sinon.stub().returns(promise.promise) + }; + controller = $controller('JvmListController', { + jvmListService: jvmListSvc, + systemInfoService: systemInfoSvc, + $scope: scope, $location: location, - $scope: scope, $timeout: timeout, - $anchorScroll: anchorScroll, $translate: translate }); - sinon.spy(ctrl, 'onload'); + sinon.spy(controller, 'applyFilters'); + sinon.spy(controller, 'changeLocationHash'); + sinon.spy(controller, 'compareFn'); + sinon.spy(controller, 'matchesFilter'); + sinon.spy(controller, 'matchesFilters'); + sinon.spy(controller, 'sortItems'); + sinon.spy(scope.items, 'sort'); })); afterEach(() => { - ctrl.onload.restore(); angular.element.restore(); }); it('should exist', () => { - should.exist(ctrl); + should.exist(controller); + }); + + describe('listConfig', () => { + it('should use a fn (changeLocationHash) for onClick attribute', () => { + let fn = scope.listConfig.onClick; + fn.should.be.a.Function(); + fn(''); + controller.changeLocationHash.should.be.calledOnce(); + }); }); - it('should set a title', () => { - ctrl.title.should.equal('JVM Listing'); + describe('changeLocationHash', () => { + it('should add system hostname to url when opened', () => { + let prevLocationHash = ''; + controller.systemsOpen[fooItem.systemId] = false; + let result = controller.changeLocationHash(fooItem, prevLocationHash); + result.should.equal(fooItem.hostname); + }); + + it('should append system hostname if more than one open', () => { + let prevLocationHash = 'foo'; + controller.systemsOpen[fooItem.systemId] = true; + let result = controller.changeLocationHash(barbazItem, prevLocationHash); + result.should.equal(fooItem.hostname + '+' + barbazItem.hostname); + }); + + it('should rebuild location hashes when list-view rows are closed', () => { + let prevLocationHash = 'foo+bar+baz'; + controller.systemsOpen.foo = true; + controller.systemsOpen.bar = true; + controller.systemsOpen.baz = true; + let result = controller.changeLocationHash(fooItem, prevLocationHash); + result.should.equal('bar+baz'); + }); + + }); + + describe('aliveOnly', () => { + it('should default to true', () => { + controller.should.have.ownProperty('aliveOnly'); + controller.aliveOnly.should.equal(true); + }); + + it('should be bound to aliveOnlyState', () => { + angular.element.should.be.calledWith('#aliveOnlyState'); + }); + + it('should set up bootstrap switch', () => { + angular.element('#aliveOnlyState').bootstrapSwitch.should.be.calledOnce(); + }); + + it('should update state on switch event', () => { + let stateWidget = angular.element('#aliveOnlyState'); + stateWidget.on.should.be.calledOnce(); + stateWidget.on.should.be.calledWith('switchChange.bootstrapSwitch', sinon.match.func); + + jvmListSvc.getSystems.should.be.calledOnce(); + jvmListSvc.getSystems.firstCall.should.be.calledWith(true); + let fn = stateWidget.on.args[0][1]; + fn(null, false); + controller.aliveOnly.should.equal(false); + jvmListSvc.getSystems.should.be.calledTwice(); + jvmListSvc.getSystems.secondCall.should.be.calledWith(false); + promise.resolve(); + }); }); describe('loadData', () => { @@ -88,36 +202,14 @@ }; promise.resolve({ data: data }); scope.$apply(); - ctrl.should.have.ownProperty('systems'); - ctrl.systems.should.deepEqual(data.response); - ctrl.showErr.should.equal(false); - ctrl.systemsOpen.should.deepEqual({ + controller.should.have.ownProperty('systems'); + controller.systems.should.deepEqual(data.response); + controller.showErr.should.equal(false); + controller.systemsOpen.should.deepEqual({ foo: false, bar: false }); - ctrl.onload.should.be.calledOnce(); - done(); - }); - - it('should set systemsOpen to true and anchorScroll if corresponding hash provided', done => { - let data = { - response: [ - { - systemId: 'foo' - }, - { - systemId: 'bar' - } - ] - }; - location.hash = () => 'foo'; - promise.resolve({ data: data }); - scope.$apply(); - ctrl.systemsOpen.should.deepEqual({ - foo: true, - bar: false - }); - ctrl.onload.should.be.calledOnce(); + scope.listConfig.itemsAvailable = true; done(); }); @@ -134,11 +226,11 @@ }; promise.resolve({ data: data }); scope.$apply(); - ctrl.systemsOpen.should.deepEqual({ + controller.systemsOpen.should.deepEqual({ foo: false, bar: false }); - ctrl.onload.should.be.calledOnce(); + scope.listConfig.itemsAvailable = true; done(); }); @@ -152,57 +244,157 @@ }; promise.resolve({ data: data }); scope.$apply(); - ctrl.systemsOpen.should.deepEqual({ + controller.systemsOpen.should.deepEqual({ foo: true }); - ctrl.onload.should.be.calledOnce(); + scope.listConfig.itemsAvailable = true; done(); }); it('should set error flag when service rejects', done => { promise.reject(); scope.$apply(); - ctrl.should.have.ownProperty('showErr'); - ctrl.showErr.should.equal(true); + controller.should.have.ownProperty('showErr'); + controller.showErr.should.equal(true); + scope.listConfig.itemsAvailable = false; done(); }); }); - describe('onload', () => { - it('should call timeout with argument of anchorScroll', () => { - timeout.reset(); - ctrl.onload(); - timeout.should.be.calledWith(anchorScroll); + describe('constructToolbarSettings', () => { + it('should use a function for filterConfig.onFilterChange', () => { + scope.items.length = 2; + let fn = scope.filterConfig.onFilterChange; + fn.should.be.a.Function(); + let filter = { + id: 'hostname', + title: 'Hostname', + value: 'foobar' + }; + fn(filter); + controller.applyFilters.should.be.calledOnce(); + scope.toolbarConfig.filterConfig.resultsCount.should.equal(2); + }); + + it('should use fn sortItems() for sortConfig.onSortChange', () => { + let fn = scope.sortConfig.onSortChange; + fn.should.be.a.Function(); + fn(); + controller.sortItems.should.be.calledOnce(); }); }); - describe('aliveOnly', () => { - it('should default to true', () => { - ctrl.should.have.ownProperty('aliveOnly'); - ctrl.aliveOnly.should.equal(true); + describe('Filter and Sorting helper functions', () => { + it('matchesFilter should match hostnames', () => { + let filter = { + id: 'hostname', + title: 'Hostname', + value: 'foo' + }; + + controller.matchesFilter(fooItem, filter).should.equal(true); + + filter.value = 'bar'; + controller.matchesFilter(fooItem, filter).should.equal(false); + + filter = { + id: 'jvmName', + title: 'JVM MainClass Name', + value: 'fooClass' + }; + controller.matchesFilter(fooItem, filter).should.equal(true); + + filter.value = 'barClass'; + controller.matchesFilter(fooItem, filter).should.equal(false); }); - it('should be bound to aliveOnlyState', () => { - angular.element.should.be.calledWith('#aliveOnlyState'); + it('matchesFilter should match JVM mainclass names', () => { + let filter = { + id: 'jvmName', + title: 'JVM MainClass Name', + value: 'fooClass' + }; + controller.matchesFilter(fooItem, filter).should.equal(true); + + filter.value = 'barClass'; + controller.matchesFilter(fooItem, filter).should.equal(false); }); - it('should set up bootstrap switch', () => { - angular.element('#aliveOnlyState').bootstrapSwitch.should.be.calledOnce(); + it('matchesFilter should return false if an invalid filter is supplied', () => { + let filter = { + id: 'fakeId', + title: 'fakeTitle', + value: 'fakeValue' + }; + controller.matchesFilter(fooItem, filter).should.equal(false); + }); + + it('matchesFilters should delegate matching to matchesFilter', () => { + controller.matchesFilters(fooItem, filters).should.equal(true); + controller.matchesFilter.should.be.calledTwice(); + }); + + it('matchesFilters should be false if at least one filter returns false', () => { + controller.matchesFilters(barbazItem, filters).should.equal(false); }); - it('should update state on switch event', () => { - let stateWidget = angular.element('#aliveOnlyState'); - stateWidget.on.should.be.calledOnce(); - stateWidget.on.should.be.calledWith('switchChange.bootstrapSwitch', sinon.match.func); + it('applyFilters should delegate filtering to matchesFilters', () => { + scope.allItems = [fooItem, barbazItem]; + controller.applyFilters(filters); + controller.matchesFilters.should.be.calledTwice(); + scope.items[0].should.equal(scope.allItems[0]); + scope.items.length.should.equal(1); + }); + + it('sortItems should sort the array using the compareFn', () => { + controller.scope.sortConfig = generateSortConfig(true, 'hostname'); + scope.items = [fooItem, barbazItem]; + controller.sortItems(); + controller.compareFn.should.be.calledOnce(); + }); + + describe('compareFn', () => { + it('compareFn should compare hostnames', () => { + controller.scope.sortConfig = generateSortConfig(true, 'name'); + let result = controller.compareFn(fooItem, barbazItem); + result.should.equal(1); + }); + + it('compareFn should compare by timeCreated', () => { + controller.scope.sortConfig = generateSortConfig(true, 'timeCreated'); + let result = controller.compareFn(fooItem, barbazItem); + result.should.equal(1); - svc.getSystems.should.be.calledOnce(); - svc.getSystems.firstCall.should.be.calledWith(true); - let fn = stateWidget.on.args[0][1]; - fn(null, false); - ctrl.aliveOnly.should.equal(false); - svc.getSystems.should.be.calledTwice(); - svc.getSystems.secondCall.should.be.calledWith(false); - promise.resolve(); + result = controller.compareFn(barbazItem, fooItem); + result.should.equal(-1); + }); + + it('compareFn should compare by numJvms', () => { + controller.scope.sortConfig = generateSortConfig(true, 'numJvms'); + + let result = controller.compareFn(fooItem, barbazItem); + result.should.equal(-1); + + result = controller.compareFn(barbazItem, fooItem); + result.should.equal(1); + }); + + it('compareFn should allow for descending order', () => { + controller.scope.sortConfig = generateSortConfig(false, 'numJvms'); + scope.sortConfig.currentField.id = 'numJvms'; + let result = controller.compareFn(fooItem, barbazItem); + result.should.equal(1); + + result = controller.compareFn(barbazItem, fooItem); + result.should.equal(-1); + }); + + it('compareFn should return 0 if an invalid field id is supplied', () => { + controller.scope.sortConfig = generateSortConfig(true, 'numJvms'); + scope.sortConfig.currentField.id = 'fakeId'; + let result = controller.compareFn(fooItem, barbazItem); + result.should.equal(0); + }); }); });
--- a/src/app/components/jvm-list/jvm-list.html Thu Aug 17 09:12:58 2017 -0400 +++ b/src/app/components/jvm-list/jvm-list.html Tue Aug 22 16:30:30 2017 -0400 @@ -3,63 +3,70 @@ <li><a ui-sref="landing">Thermostat</a></li> <li><a ui-sref="jvmList" translate>jvmList.BREADCRUMB</a></li> </ol> - - <customizable-error-message ng-show="ctrl.showErr" dismissible="true" err-message="errMessage" err-title="errTitle"></customizable-error-message> - - <div ng-show="!ctrl.showErr" > + <div class="jvmList-container" ng-controller="JvmListController as controller"> <div class="text-right"> <label for="aliveOnlySwitch" class="label label-default" translate>jvmList.ALIVE_ONLY</label> <input class="bootstrap-switch pull-right" id="aliveOnlyState" name="aliveOnlySwitch" data-size="mini" type="checkbox" checked/> - <button type="button" class="btn btn-default" id="refreshButton" ng-click="ctrl.loadData()"><span class="fa fa-refresh"></span></button> + <button type="button" class="btn btn-default" id="refreshButton" ng-click="controller.loadData()"><span class="fa fa-refresh"></span></button> + </div> + <div class="pfToolbar col-md-12"> + <pf-toolbar id="jvmListToolbar" config="toolbarConfig"></pf-toolbar> </div> - - <uib-accordion close-others="true"> - - <uib-accordion-group class="cards-pf" ng-repeat="system in ctrl.systems" id="{{system.systemId}}" is-open="ctrl.systemsOpen[system.systemId]"> - - <uib-accordion-heading><a ui-sref="{'#': system.systemId}">{{system.systemId}}</a></uib-accordion-heading> - <div class="container-fluid container-cards-pf"> - <div class="row row-cards-pf"> - - <div class="col-xs-12 col-md-4 col-lg-3"> - <div ui-sref="systemInfo({ systemId: system.systemId })" class="card-pf card-pf-view card-pf-view-select card-pf-view-single-select"> + <div class="pfListView col-md-12"> + <pf-list-view config="listConfig" + items="items" + empty-state-config="emptyStateConfig"> + <div class="list-view-pf-left"> + <span class="pficon pficon-screen list-view-pf-icon-med"></span> + </div> + <div class="list-view-pf-description"> + <div class="list-group-item-heading"> + {{item.hostname}} + </div> + <div class="list-view-pf-additional-info"> + <div class="list-view-pf-additional-info-item"> + <span class="pficon pficon-virtual-machine"></span> + <strong>{{item.jvms.length}}</strong> JVMs + </div> + <div class="list-view-pf-additional-info-item" ng-click="$event.stopPropagation()"> + <span class="pficon pficon-server"></span> + <a ui-sref="systemInfo({ systemId: item.systemId })" translate>jvmList.SYSTEM_INFO_LABEL</a> + </div> + <div class="list-view-pf-additional-info-item"> + <span class="fa fa-clock-o"></span> + <span translate="jvmList.START_TIME_LIST" translate-values="{ date: '{{item.timeCreated | timestampToDate}}' }"></span> + </div> + </div> + </div> + <list-expanded-content> + <div class="jvmPagination col-md-12"> + <pf-pagination + page-size="pageSize" + page-number="pageNumber" + page-size-increments="pageSizeIncrements" + num-total-items="$parent.item.jvms.length"> + </pf-pagination> + </div> + <div ng-repeat="jvm in $parent.item.jvms | startFrom:(pageNumber - 1) * pageSize | limitTo:pageSize"> + <div class="col-xs-12 col-sm-6 col-md-3 col-lg-3"> + <div ui-sref="jvmInfo({ systemId: $parent.$parent.item.systemId, jvmId: jvm.jvmId })" class="card-pf card-pf-view card-pf-view-select"> <div class="card-pf-top-element"> - <span class="pficon pficon-server card-pf-icon-circle"></span> + <span class="pficon pficon-virtual-machine card-pf-icon-circle"></span> </div> <div class="card-pf-body"> <h2 class="card-pf-title text-center ellipsis-word-wrap"> - {{system.systemId}} + {{jvm.mainClass | extractClass:true}} </h2> - <p class="card-pf-info text-center" translate>jvmList.SYSTEM_CARD_LABEL</p> + <div class="card-pf-info text-center" translate> + <span class="pull-right pficon" ng-class="jvm.isAlive ? 'pficon-ok' : 'pficon-error-circle-o'"></span> + <span translate="jvmList.START_TIME_CARD" translate-values="{ date: '{{jvm.startTime | timestampToDate}}' }"></span> + </div> </div> </div> </div> - - <div ng-repeat="jvm in system.jvms"> - - <div class="col-xs-12 col-md-4 col-lg-3"> - <div ui-sref="jvmInfo({ systemId: system.systemId, jvmId: jvm.jvmId })" class="card-pf card-pf-view card-pf-view-select card-pf-view-single-select"> - <div class="card-pf-top-element"> - <span class="pficon pficon-virtual-machine card-pf-icon-circle"></span> - </div> - <div class="card-pf-body"> - <h2 class="card-pf-title text-center ellipsis-word-wrap"> - {{jvm.mainClass | extractClass:true}} - </h2> - <div class="card-pf-info text-center" translate> - <span translate="jvmList.CREATED_ON" translate-values="{ date: '{{jvm.startTime | timestampToDate}}' }"></span> - <span class="pull-right pficon" ng-class="jvm.isAlive ? 'pficon-ok' : 'pficon-error-circle-o'"></span> - </div> - </div> - </div> - </div> - - </div> - </div><!-- /row --> - </div> - </uib-accordion-group> - - </uib-accordion> + </div> + </list-expanded-content> + </pf-list-view> + </div> </div> - </div><!-- /container -->
--- a/src/app/components/jvm-list/jvm-list.module.js Thu Aug 17 09:12:58 2017 -0400 +++ b/src/app/components/jvm-list/jvm-list.module.js Tue Aug 22 16:30:30 2017 -0400 @@ -26,11 +26,13 @@ */ import JvmListController from './jvm-list.controller.js'; -import service from './jvm-list.service.js'; +import jvmListService from './jvm-list.service.js'; +import systemInfoService from 'components/system-info/system-info.service.js'; export default angular .module('jvmList', [ JvmListController, - service + jvmListService, + systemInfoService, ]) .name;
--- a/src/app/components/multichart/multichart.html Thu Aug 17 09:12:58 2017 -0400 +++ b/src/app/components/multichart/multichart.html Tue Aug 22 16:30:30 2017 -0400 @@ -81,11 +81,11 @@ </div> <div class="card-pf-body" ng-if="chartCtrl.chartConfig"> - <div pf-line-chart id="chart-{{chart}}" config="chartCtrl.chartConfig" + <pf-line-chart id="chart-{{chart}}" config="chartCtrl.chartConfig" chart-data="chartCtrl.chartData" show-x-axis="true" show-y-axis="true" - ></div> + ></pf-line-chart> </div>
--- a/src/app/components/system-info/system-info.html Thu Aug 17 09:12:58 2017 -0400 +++ b/src/app/components/system-info/system-info.html Tue Aug 22 16:30:30 2017 -0400 @@ -84,27 +84,27 @@ <div class="row row-cards-pf"> <div class="container container-cards-pf"> <!-- System-CPU Donut Chart --> - <div class="col-xs-12 col-md-6" ng-controller="SystemCpuController as ctrl"> + <div class="col-xs-12 col-sm-6 col-md-6" ng-controller="SystemCpuController as ctrl"> <div class="card-pf card-pf-view"> <div class="card-pf-heading"> <label class="card-pf-title" translate>systemInfo.systemCpu.CHART_LABEL</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> + <pf-donut-pct-chart id="cpuChart" config="ctrl.config" data="ctrl.data"></pf-donut-pct-chart> </div> </div> </div> <div class="system-memory-charts" ng-controller="SystemMemoryController as ctrl"> <!-- System-Memory Donut Chart --> - <div class="col-xs-12 col-md-6"> + <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"> - <div pf-donut-pct-chart id="systemMemoryDonutChart" config="ctrl.donutConfig" data="ctrl.donutData"></div> + <pf-donut-pct-chart id="systemMemoryDonutChart" config="ctrl.donutConfig" data="ctrl.donutData"></pf-donut-pct-chart> </div> </div> </div> @@ -141,7 +141,7 @@ </div> <!-- Line Chart --> <div class="card-pf-body"> - <div 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 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>
--- a/src/assets/scss/main.scss Thu Aug 17 09:12:58 2017 -0400 +++ b/src/assets/scss/main.scss Tue Aug 22 16:30:30 2017 -0400 @@ -46,3 +46,20 @@ white-space: nowrap; text-overflow: ellipsis; } + +// list-view selected row background colour +.list-group-item-container { + background: $color-pf-black-100; +} + +// makes pf-pagination line up with the vm cards +.jvmPagination { + padding-left: 10px; + padding-right: 10px; + padding-bottom: 10px; +} + +// aligns the 'alive vms only' component with the pf-list-view +.text-right { + padding-right: 20px; +}
--- a/webpack.config.js Thu Aug 17 09:12:58 2017 -0400 +++ b/webpack.config.js Tue Aug 22 16:30:30 2017 -0400 @@ -54,6 +54,7 @@ 'bootstrap-switch': 'angular-patternfly/node_modules/patternfly/node_modules/bootstrap-switch', 'assets': path.resolve(__dirname, 'src', 'assets'), + 'components': path.resolve(__dirname, 'src', 'app', 'components'), 'images': 'assets/images', 'scss': 'assets/scss', 'shared': path.resolve(__dirname, 'src', 'app', 'shared'),