From 01aa99473a45b5a54e4d23472c8257e8b2e9d7b5 Mon Sep 17 00:00:00 2001 From: Matt Borland Date: Wed, 18 May 2016 08:05:43 -0600 Subject: [PATCH] Adding resource panel and table features This patch adds two basic features: a directive that takes in a resource type name and produces a table of the resource type with actions, links to views, etc., based on information in the resource type object; the other directive provides a resource panel with header based on a resource type name. Change-Id: Idaba844aca5fc6e89e2bd7c65cb836feaba67f67 Partially-Implements: blueprint angular-registry --- .../conf/resource-type-registry.service.js | 21 +-- .../resource-type-registry.service.spec.js | 5 - .../panel/hz-resource-panel.controller.js | 34 +++++ .../hz-resource-panel.controller.spec.js | 57 ++++++++ .../panel/hz-resource-panel.directive.js | 57 ++++++++ .../widgets/panel/hz-resource-panel.html | 4 + .../framework/widgets/panel/panel.module.js | 22 +++ .../table/hz-resource-table.controller.js | 126 ++++++++++++++++++ .../hz-resource-table.controller.spec.js | 122 +++++++++++++++++ .../table/hz-resource-table.directive.js | 59 ++++++++ .../widgets/table/hz-resource-table.html | 10 ++ .../framework/widgets/widgets.module.js | 1 + .../resource-directives-44629f1116545141.yaml | 14 ++ 13 files changed, 510 insertions(+), 22 deletions(-) create mode 100644 horizon/static/framework/widgets/panel/hz-resource-panel.controller.js create mode 100644 horizon/static/framework/widgets/panel/hz-resource-panel.controller.spec.js create mode 100644 horizon/static/framework/widgets/panel/hz-resource-panel.directive.js create mode 100644 horizon/static/framework/widgets/panel/hz-resource-panel.html create mode 100644 horizon/static/framework/widgets/panel/panel.module.js create mode 100644 horizon/static/framework/widgets/table/hz-resource-table.controller.js create mode 100644 horizon/static/framework/widgets/table/hz-resource-table.controller.spec.js create mode 100644 horizon/static/framework/widgets/table/hz-resource-table.directive.js create mode 100644 horizon/static/framework/widgets/table/hz-resource-table.html create mode 100644 releasenotes/notes/resource-directives-44629f1116545141.yaml diff --git a/horizon/static/framework/conf/resource-type-registry.service.js b/horizon/static/framework/conf/resource-type-registry.service.js index 122df0459..f4f282fbd 100644 --- a/horizon/static/framework/conf/resource-type-registry.service.js +++ b/horizon/static/framework/conf/resource-type-registry.service.js @@ -94,7 +94,9 @@ // type, with the data as a result in a promise. For example, Images code // would register a list function that returns a promise that will resolve // to all the Images data in list form. - this.listFunction = angular.noop; + this.listFunction = function def() { + return Promise.resolve({data: {items: []}}); + }; this.setListFunction = setListFunction; // The table columns are an extensible registration of columns of data @@ -506,10 +508,6 @@ } var resourceTypes = {}; - // The slugs are only used to align Django routes with heat - // type names. In a context without Django routing this is - // not needed. - var slugs = {}; var defaultSummaryTemplateUrl = false; var defaultDetailsTemplateUrl = false; var registry = { @@ -519,20 +517,9 @@ setDefaultSummaryTemplateUrl: setDefaultSummaryTemplateUrl, getDefaultSummaryTemplateUrl: getDefaultSummaryTemplateUrl, setDefaultDetailsTemplateUrl: setDefaultDetailsTemplateUrl, - getDefaultDetailsTemplateUrl: getDefaultDetailsTemplateUrl, - setSlug: setSlug, - getTypeNameBySlug: getTypeNameBySlug + getDefaultDetailsTemplateUrl: getDefaultDetailsTemplateUrl }; - function getTypeNameBySlug(slug) { - return slugs[slug]; - } - - function setSlug(slug, typeName) { - slugs[slug] = typeName; - return this; - } - function getDefaultSummaryTemplateUrl() { return defaultSummaryTemplateUrl; } diff --git a/horizon/static/framework/conf/resource-type-registry.service.spec.js b/horizon/static/framework/conf/resource-type-registry.service.spec.js index 6b93420b1..c8131d85b 100644 --- a/horizon/static/framework/conf/resource-type-registry.service.spec.js +++ b/horizon/static/framework/conf/resource-type-registry.service.spec.js @@ -166,11 +166,6 @@ }); }); - it("sets and retrieves slugs", function() { - service.setSlug('image', 'OS::Glance::Image'); - expect(service.getTypeNameBySlug('image')).toBe('OS::Glance::Image'); - }); - describe('getName', function() { it('returns nothing if names not provided', function() { var type = service.getResourceType('something'); diff --git a/horizon/static/framework/widgets/panel/hz-resource-panel.controller.js b/horizon/static/framework/widgets/panel/hz-resource-panel.controller.js new file mode 100644 index 000000000..59ff9c220 --- /dev/null +++ b/horizon/static/framework/widgets/panel/hz-resource-panel.controller.js @@ -0,0 +1,34 @@ +/* + * (c) Copyright 2016 Hewlett Packard Enterprise Development Company LP + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +(function() { + 'use strict'; + + angular + .module('horizon.framework.widgets.panel') + .controller('horizon.framework.widgets.panel.HzResourcePanelController', controller); + + controller.$inject = [ + 'horizon.framework.conf.resource-type-registry.service' + ]; + + function controller(registry) { + var ctrl = this; + + ctrl.resourceType = registry.getResourceType(ctrl.resourceTypeName); + ctrl.pageName = ctrl.resourceType.getName(); + } + +})(); diff --git a/horizon/static/framework/widgets/panel/hz-resource-panel.controller.spec.js b/horizon/static/framework/widgets/panel/hz-resource-panel.controller.spec.js new file mode 100644 index 000000000..52010575e --- /dev/null +++ b/horizon/static/framework/widgets/panel/hz-resource-panel.controller.spec.js @@ -0,0 +1,57 @@ +/* + * (c) Copyright 2015 Hewlett-Packard Development Company, L.P. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +(function() { + 'use strict'; + + describe('hz-resource-panel controller', function() { + var ctrl; + + var resourceType = { + getName: function() { + return 'MyType'; + } + }; + + beforeEach(module('horizon.framework.conf')); + beforeEach(module('horizon.framework.widgets.panel')); + + beforeEach(inject(function($controller) { + var registry = { + getResourceType: angular.noop + }; + + spyOn(registry, 'getResourceType').and.returnValue(resourceType); + + ctrl = $controller('horizon.framework.widgets.panel.HzResourcePanelController', { + 'horizon.framework.conf.resource-type-registry.service': registry, + tableResourceType: 'OS::Test::Example'}); + })); + + it('exists', function() { + expect(ctrl).toBeDefined(); + }); + + it('sets resourceType to the resource type', function() { + expect(ctrl.resourceType).toBe(resourceType); + }); + + it('sets resourceTypeName to the resource type name', function() { + expect(ctrl.pageName).toEqual('MyType'); + }); + }); + +})(); diff --git a/horizon/static/framework/widgets/panel/hz-resource-panel.directive.js b/horizon/static/framework/widgets/panel/hz-resource-panel.directive.js new file mode 100644 index 000000000..c1d2e56dd --- /dev/null +++ b/horizon/static/framework/widgets/panel/hz-resource-panel.directive.js @@ -0,0 +1,57 @@ +/** + * (c) Copyright 2016 Hewlett Packard Enterprise Development Company LP + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +(function() { + 'use strict'; + + angular + .module('horizon.framework.widgets.panel') + .directive('hzResourcePanel', directive); + + directive.$inject = ['horizon.framework.widgets.basePath']; + + /** + * @ngdoc directive + * @name hzResourcePanel + * @description + * This directive takes in a resource type name, e.g. 'OS::Glance::Image' + * as a String and produces the shell of a panel for that given resource + * type. This primarily includes a header and allows content to be + * transcluded. + * + * @example + ``` + +
Here is my content!
+ +
+ ``` + */ + function directive(basePath) { + + var directive = { + restrict: 'E', + scope: { + resourceTypeName: '@' + }, + transclude: true, + bindToController: true, + templateUrl: basePath + 'panel/hz-resource-panel.html', + controller: "horizon.framework.widgets.panel.HzResourcePanelController as ctrl" + }; + + return directive; + } +})(); diff --git a/horizon/static/framework/widgets/panel/hz-resource-panel.html b/horizon/static/framework/widgets/panel/hz-resource-panel.html new file mode 100644 index 000000000..1f650b862 --- /dev/null +++ b/horizon/static/framework/widgets/panel/hz-resource-panel.html @@ -0,0 +1,4 @@ +
+ + +
diff --git a/horizon/static/framework/widgets/panel/panel.module.js b/horizon/static/framework/widgets/panel/panel.module.js new file mode 100644 index 000000000..802e9ef1a --- /dev/null +++ b/horizon/static/framework/widgets/panel/panel.module.js @@ -0,0 +1,22 @@ +/* + * (c) Copyright 2016 Hewlett Packard Enterprise Development Company LP + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +(function() { + 'use strict'; + + angular + .module('horizon.framework.widgets.panel', []); + +})(); diff --git a/horizon/static/framework/widgets/table/hz-resource-table.controller.js b/horizon/static/framework/widgets/table/hz-resource-table.controller.js new file mode 100644 index 000000000..f8b490176 --- /dev/null +++ b/horizon/static/framework/widgets/table/hz-resource-table.controller.js @@ -0,0 +1,126 @@ +/* + * (c) Copyright 2016 Hewlett Packard Enterprise Development Company LP + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +(function() { + 'use strict'; + + angular + .module('horizon.framework.widgets.table') + .controller('horizon.framework.widgets.table.ResourceTableController', controller); + + controller.$inject = [ + '$q', + '$scope', + 'horizon.framework.util.actions.action-result.service', + 'horizon.framework.conf.resource-type-registry.service' + ]; + + function controller($q, $scope, actionResultService, registry) { + var ctrl = this; + + // 'Public' Controller members + + ctrl.resourceType = registry.getResourceType(ctrl.resourceTypeName); + ctrl.items = []; + ctrl.itemsSrc = []; + ctrl.searchFacets = []; + ctrl.config = { + detailsTemplateUrl: ctrl.resourceType.summaryTemplateUrl, + selectAll: true, + expand: true, + trackId: 'id', + searchColumnSpan: 6, + actionColumnSpan: 6, + columns: ctrl.resourceType.getTableColumns() + }; + ctrl.batchActions = ctrl.resourceType.globalActions + .concat(ctrl.resourceType.batchActions); + + ctrl.actionResultHandler = actionResultHandler; + + // Controller Initialization/Loading + + ctrl.resourceType.listFunction().then(onLoad); + registry.initActions(ctrl.resourceType.type, $scope); + + // Local functions + + function onLoad(response) { + ctrl.itemsSrc = response.data.items; + } + + function actionResultHandler(returnValue) { + return $q.when(returnValue, actionSuccessHandler); + } + + function actionSuccessHandler(result) { // eslint-disable-line no-unused-vars + + // The action has completed (for whatever "complete" means to that + // action. Notice the view doesn't really need to know the semantics of the + // particular action because the actions return data in a standard form. + // That return includes the id and type of each created, updated, deleted + // and failed item. + // + // This handler is also careful to check the type of each item. This + // is important because actions which create non-items are launched from + // the items page (like create "volume" from image). + var deletedIds, updatedIds, createdIds, failedIds; + + if ( result ) { + // Reduce the results to just item ids ignoring other types the action + // may have produced + deletedIds = actionResultService.getIdsOfType(result.deleted, ctrl.resourceType.type); + updatedIds = actionResultService.getIdsOfType(result.updated, ctrl.resourceType.type); + createdIds = actionResultService.getIdsOfType(result.created, ctrl.resourceType.type); + failedIds = actionResultService.getIdsOfType(result.failed, ctrl.resourceType.type); + + // Handle deleted items + if (deletedIds.length) { + ctrl.itemsSrc = difference(ctrl.itemsSrc, deletedIds,'id'); + } + + // Handle updated and created items + if ( updatedIds.length || createdIds.length ) { + // Ideally, get each created item individually, but + // this is simple and robust for the common use case. + // TODO: If we want more detailed updates, we could do so here. + ctrl.resourceType.listFunction().then(onLoad); + } + + // Handle failed items + if (failedIds.length) { + // Do nothing for now. Please note, actions may (and probably + // should) provide toast messages when something goes wrong. + } + + } else { + // promise resolved, but no result returned. Because the action didn't + // tell us what happened...reload the displayed items just in case. + ctrl.resourceType.listFunction().then(onLoad); + } + } + + function difference(currentList, otherList, key) { + return currentList.filter(filter); + + function filter(elem) { + return otherList.filter(function filterDeletedItem(deletedItem) { + return deletedItem === elem[key]; + }).length === 0; + } + } + } + +})(); diff --git a/horizon/static/framework/widgets/table/hz-resource-table.controller.spec.js b/horizon/static/framework/widgets/table/hz-resource-table.controller.spec.js new file mode 100644 index 000000000..0dd99d69c --- /dev/null +++ b/horizon/static/framework/widgets/table/hz-resource-table.controller.spec.js @@ -0,0 +1,122 @@ +/* + * (c) Copyright 2015 Hewlett-Packard Development Company, L.P. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +(function() { + 'use strict'; + + describe('hz-generic-table controller', function() { + var ctrl, listFunctionDeferred, $timeout, actionResultDeferred; + + beforeEach(module('horizon.framework.util')); + beforeEach(module('horizon.framework.conf')); + beforeEach(module('horizon.framework.widgets.table')); + + var resourceType = { + type: 'OS::Test::Example', + getTableColumns: angular.noop, + listFunction: angular.noop, + globalActions: [], + batchActions: [] + }; + + beforeEach(inject(function($controller, $q, _$timeout_) { + $timeout = _$timeout_; + var registry = { + getTypeNameBySlug: angular.noop, + getResourceType: angular.noop, + initActions: angular.noop + }; + + listFunctionDeferred = $q.defer(); + actionResultDeferred = $q.defer(); + spyOn(resourceType, 'listFunction').and.returnValue(listFunctionDeferred.promise); + spyOn(registry, 'getResourceType').and.returnValue(resourceType); + + ctrl = $controller('horizon.framework.widgets.table.ResourceTableController', { + $scope: {}, + 'horizon.framework.conf.resource-type-registry.service': registry}, + {resourceTypeName: 'OS::Test::Example'}); + })); + + it('exists', function() { + expect(ctrl).toBeDefined(); + }); + + it('sets itemsSrc to the response data', function() { + listFunctionDeferred.resolve({data: {items: [1,2,3]}}); + $timeout.flush(); + expect(ctrl.itemsSrc).toEqual([1,2,3]); + }); + + describe('actionResultHandler', function() { + beforeEach(function() { + ctrl.itemsSrc = [{type: 'Something', id: -1}, {type: 'OS::Test::Example', id: 1}]; + }); + + it('handles deleted items', function() { + actionResultDeferred.resolve({deleted: [{type: 'ignored', id: 0}, + {type: 'OS::Test::Example', id: 1}]}); + var promise = ctrl.actionResultHandler(actionResultDeferred.promise); + promise.then(function() { + expect(ctrl.itemsSrc).toEqual([{type: 'Something', id: -1}]); + }); + $timeout.flush(); + }); + + it('handles updated items', function() { + actionResultDeferred.resolve({updated: [{type: 'OS::Test::Example', id: 1}]}); + var promise = ctrl.actionResultHandler(actionResultDeferred.promise); + resourceType.listFunction.calls.reset(); + promise.then(function() { + expect(resourceType.listFunction).toHaveBeenCalled(); + }); + $timeout.flush(); + }); + + it('handles created items', function() { + actionResultDeferred.resolve({created: [{type: 'OS::Test::Example', id: 1}]}); + var promise = ctrl.actionResultHandler(actionResultDeferred.promise); + resourceType.listFunction.calls.reset(); + promise.then(function() { + expect(resourceType.listFunction).toHaveBeenCalled(); + }); + $timeout.flush(); + }); + + it('handles failed items', function() { + actionResultDeferred.resolve({failed: [{type: 'OS::Test::Example', id: 1}]}); + var promise = ctrl.actionResultHandler(actionResultDeferred.promise); + resourceType.listFunction.calls.reset(); + promise.then(function() { + expect(resourceType.listFunction).not.toHaveBeenCalled(); + }); + $timeout.flush(); + }); + + it('handles falsy results', function() { + actionResultDeferred.resolve(false); + var promise = ctrl.actionResultHandler(actionResultDeferred.promise); + resourceType.listFunction.calls.reset(); + promise.then(function() { + expect(resourceType.listFunction).toHaveBeenCalled(); + }); + $timeout.flush(); + }); + }); + + }); + +})(); diff --git a/horizon/static/framework/widgets/table/hz-resource-table.directive.js b/horizon/static/framework/widgets/table/hz-resource-table.directive.js new file mode 100644 index 000000000..49c60f6da --- /dev/null +++ b/horizon/static/framework/widgets/table/hz-resource-table.directive.js @@ -0,0 +1,59 @@ +/** + * (c) Copyright 2016 Hewlett Packard Enterprise Development Company LP + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +(function() { + 'use strict'; + + angular + .module('horizon.framework.widgets.table') + .directive('hzResourceTable', directive); + + directive.$inject = ['horizon.framework.widgets.basePath']; + + /** + * @ngdoc directive + * @name hzResourceTable + * @description + * This directive produces a table and accompanying components that describe + * a list of resources of the given type. Based on information in the + * registry, the batch, global, and item-level actions are presented as + * appropriate. Search capabilities are also provided. The table contents + * are responsive to actions' promise resolutions, updating contents when + * they are likely to have changed. This directive allows for the rapid + * development of standard resource tables without having to rewrite + * boilerplate controllers, markup, etc. + * @example + ``` +
Here's some content above the table.
+ +
Here's some content below the table.
+ ``` + */ + + function directive(basePath) { + + var directive = { + restrict: 'E', + scope: { + resourceTypeName: '@' + }, + bindToController: true, + templateUrl: basePath + 'table/hz-resource-table.html', + controller: "horizon.framework.widgets.table.ResourceTableController as ctrl" + }; + + return directive; + } +})(); diff --git a/horizon/static/framework/widgets/table/hz-resource-table.html b/horizon/static/framework/widgets/table/hz-resource-table.html new file mode 100644 index 000000000..2ca4e71e7 --- /dev/null +++ b/horizon/static/framework/widgets/table/hz-resource-table.html @@ -0,0 +1,10 @@ + diff --git a/horizon/static/framework/widgets/widgets.module.js b/horizon/static/framework/widgets/widgets.module.js index 8384fe80a..2c0969a47 100644 --- a/horizon/static/framework/widgets/widgets.module.js +++ b/horizon/static/framework/widgets/widgets.module.js @@ -26,6 +26,7 @@ 'horizon.framework.widgets.table', 'horizon.framework.widgets.modal', 'horizon.framework.widgets.modal-wait-spinner', + 'horizon.framework.widgets.panel', 'horizon.framework.widgets.transfer-table', 'horizon.framework.widgets.charts', 'horizon.framework.widgets.action-list', diff --git a/releasenotes/notes/resource-directives-44629f1116545141.yaml b/releasenotes/notes/resource-directives-44629f1116545141.yaml new file mode 100644 index 000000000..e7b29438f --- /dev/null +++ b/releasenotes/notes/resource-directives-44629f1116545141.yaml @@ -0,0 +1,14 @@ +--- +prelude: > + Angular components now exist to provide simple-to- + configure panels and tables, based off of registry + information about resources (e.g. Instances). +features: + - The hz-resource-table directive takes in a Heat + resource name (e.g. 'OS::Nova::Server') and uses + the Angular registry to provide actions, columns, + and summary views. + - The hz-resource-panel directive takes in a Heat + resource name (e.g. 'OS::Nova::Server') and + displays an appropriate header and allows content + to be transcluded to build the panel page.