From 11968c840cc88ef29fcd49b9c8f4f2f66328e35c Mon Sep 17 00:00:00 2001 From: Matt Borland Date: Wed, 23 Mar 2016 10:22:44 -0600 Subject: [PATCH] Generic details display framework This patch provides the ability for the registered detail views for any resource type to be generically presented. This patch does the following: * Adds a directive that displays a set of views (i.e. details sub-views) * Adds a Generic Detail display for routed pages * Adds the concept of a Descriptor which contains a resource type name and an identifier. The identifier can be something as simple as a string, but may also be an object (if the resource type needs more than one value to look up its data, e.g. Pool Members) * Adds the ability for a resource type to have knowledge about how one of its items may be loaded, so any detail page can fetch the information given a basic context * Adds a generic Angular page (since they all just route to ng-views). We will see this used in subsequent patches as well. * Sets up a Django route to a non-navigational panel for the Details Change-Id: Ie116b52ba196f9240fdc6bbc4a12d37beb9b9fcf Partially-Implements: blueprint angular-registry --- .../conf/resource-type-registry.service.js | 194 +++++++++++++++++- .../resource-type-registry.service.spec.js | 83 +++++++- .../widgets/details/details.directive.js | 68 ++++++ .../framework/widgets/details/details.html | 13 ++ .../widgets/details/details.module.js | 29 +++ .../details/routed-details-view.controller.js | 47 +++++ .../routed-details-view.controller.spec.js | 70 +++++++ .../widgets/details/routed-details-view.html | 23 +++ .../framework/widgets/widgets.module.js | 17 ++ .../dashboards/project/ngdetails/__init__.py | 0 .../dashboards/project/ngdetails/panel.py | 25 +++ .../dashboards/project/ngdetails/urls.py | 24 +++ .../dashboards/project/ngdetails/views.py | 19 ++ .../enabled/_1070_project_ng_details_panel.py | 30 +++ .../static/app/core/core.module.js | 9 +- .../app/core/images/images.module.spec.js | 2 - openstack_dashboard/templates/angular.html | 16 ++ .../generic-details-4f78452b14005e5b.yaml | 26 +++ 18 files changed, 688 insertions(+), 7 deletions(-) create mode 100644 horizon/static/framework/widgets/details/details.directive.js create mode 100644 horizon/static/framework/widgets/details/details.html create mode 100644 horizon/static/framework/widgets/details/details.module.js create mode 100644 horizon/static/framework/widgets/details/routed-details-view.controller.js create mode 100644 horizon/static/framework/widgets/details/routed-details-view.controller.spec.js create mode 100644 horizon/static/framework/widgets/details/routed-details-view.html create mode 100644 openstack_dashboard/dashboards/project/ngdetails/__init__.py create mode 100644 openstack_dashboard/dashboards/project/ngdetails/panel.py create mode 100644 openstack_dashboard/dashboards/project/ngdetails/urls.py create mode 100644 openstack_dashboard/dashboards/project/ngdetails/views.py create mode 100644 openstack_dashboard/enabled/_1070_project_ng_details_panel.py create mode 100644 openstack_dashboard/templates/angular.html create mode 100644 releasenotes/notes/generic-details-4f78452b14005e5b.yaml diff --git a/horizon/static/framework/conf/resource-type-registry.service.js b/horizon/static/framework/conf/resource-type-registry.service.js index eb409ce2d8..b6815eff9b 100644 --- a/horizon/static/framework/conf/resource-type-registry.service.js +++ b/horizon/static/framework/conf/resource-type-registry.service.js @@ -56,7 +56,7 @@ */ function registryService(extensibleService) { - function ResourceType() { + function ResourceType(type) { // 'properties' contains information about properties associated with // this resource type. The expectation is that the key is the 'code' // name of the property and the value conforms to the standard @@ -66,6 +66,31 @@ this.getName = getName; this.label = label; this.format = format; + this.type = type; + this.setLoadFunction = setLoadFunction; + this.load = load; + + // These members support the ability of a type to provide a function + // that, given an object in the structure presented by the + // load() function, produces a human-readable name. + this.itemNameFunction = defaultItemNameFunction; + this.setItemNameFunction = setItemNameFunction; + this.itemName = itemName; + + // The purpose of these members is to allow details to be retrieved + // automatically from such a path, or similarly to create a path + // to such a route from any reference. This establishes a two-way + // relationship between the path and the identifier(s) for the item. + // The path could be used as part of a details route, for example: + // + // An identifier of 'abc-defg' would yield '/abc-defg' which + // could be used in a details url, such as: + // '/details/OS::Glance::Image/abc-defg' + this.pathParser = defaultPathParser; + this.setPathParser = setPathParser; + this.parsePath = parsePath; + this.setPathGenerator = setPathGenerator; + this.pathGenerator = defaultPathGenerator; // itemActions is a list of actions that can be executed upon a single // item. The list is made extensible so it can be added to independently. @@ -77,6 +102,14 @@ this.batchActions = []; extensibleService(this.batchActions, this.batchActions); + // detailsViews is a list of views that can be shown on a details view. + // For example, each item added to this list could be represented + // as a tab of a details view. + this.detailsViews = []; + extensibleService(this.detailsViews, this.detailsViews); + + // Function declarations + /** * @ngdoc function * @name setProperty @@ -135,6 +168,141 @@ return this; } + /** + * @ngdoc function + * @name setPathParser + * @description + * Sets a function that is used to parse paths. See parsePath. + * @example + ``` + getResourceType('thing').setPathParser(func); + + function func(path) { + return path.replace('-', ''); + } + + var descriptor = resourceType.parsePath(path); + ``` + */ + function setPathParser(func) { + this.pathParser = func; + return this; + } + + /** + * @ngdoc function + * @name parsePath + * @description + * Given a subpath, produce an object that describes the object + * enough to load it from an API. This is used in details + * routes, which must generate an object that has enough + * fidelity to fetch the object. In many cases this is a simple + * ID, but in others there may be multiple IDs that are required + * to fetch the data. + * @example + ``` + getResourceType('thing').setPathParser(func); + + function func(path) { + return path.replace('-', ''); + } + + var descriptor = resourceType.parsePath(path); + ``` + */ + function parsePath(path) { + return {identifier: this.pathParser(path), resourceTypeCode: this.type}; + } + + /** + * @ngdoc function + * @name setLoadFunction + * @description + * Sets a function that is used to load a single item. See load(). + * @example + ``` + getResourceType('thing').setLoadFunction(func); + + function func(descriptor) { + return someApi.get(descriptor.id); + } + + var loadPromise = resourceType.load({id: 'some-id'}); + ``` + */ + function setLoadFunction(func) { + this.loadFunction = func; + return this; + } + + /** + * @ngdoc function + * @name load + * @description + * Loads a single item + * @example + ``` + getResourceType('thing').setLoadFunction(func); + + function func(descriptor) { + return someApi.get(descriptor.id); + } + + var loadPromise = resourceType.load({id: 'some-id'}); + ``` + */ + function load(descriptor) { + return this.loadFunction(descriptor); + } + + /** + * @ngdoc function + * @name setPathGenerator + * @description + * Sets a function that is used generate paths. Accepts the + * resource-type-specific id/object. + * The subpath returned should NOT have a leading slash. + * @example + ``` + getResourceType('thing').setPathGenerator(func); + + function func(descriptor) { + return 'load-balancer/' + descriptor.balancerId + + '/listener/' + descriptor.id + } + + ``` + */ + function setPathGenerator(func) { + this.pathGenerator = func; + return this; + } + + // Functions relating item names, described above. + function defaultItemNameFunction(item) { + return item.name; + } + + function setItemNameFunction(func) { + this.itemNameFunction = func; + return this; + } + + function itemName(item) { + return this.itemNameFunction(item); + } + + // Functions providing default path parsers and generators + // so most common objects don't have to re-specify the most common + // case, which is that a path component for an identifier is just the ID. + function defaultPathParser(path) { + return path; + } + + function defaultPathGenerator(id) { + return id; + } + /** * @ngdoc function * @name getName @@ -229,11 +397,33 @@ } var resourceTypes = {}; + var defaultDetailsTemplateUrl = false; var registry = { + setDefaultDetailsTemplateUrl: setDefaultDetailsTemplateUrl, + getDefaultDetailsTemplateUrl: getDefaultDetailsTemplateUrl, getResourceType: getResourceType, initActions: initActions }; + function getDefaultDetailsTemplateUrl() { + return defaultDetailsTemplateUrl; + } + + /* + * @ngdoc function + * @name setDefaultDetailsTemplateUrl + * @param {String} url - The URL for the template to be used + * @description + * The idea is that in the case that someone links to a details page for a + * resource and there is no view registered, there can be a default view. + * For example, if there's a generic property viewer, that could display + * the resource. + */ + function setDefaultDetailsTemplateUrl(url) { + defaultDetailsTemplateUrl = url; + return this; + } + /* * @ngdoc function * @name getResourceType @@ -245,7 +435,7 @@ */ function getResourceType(type, config) { if (!resourceTypes.hasOwnProperty(type)) { - resourceTypes[type] = new ResourceType(); + resourceTypes[type] = new ResourceType(type); } if (angular.isDefined(config)) { angular.extend(resourceTypes[type], config); 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 e90ae9135b..fc85fe33a1 100644 --- a/horizon/static/framework/conf/resource-type-registry.service.spec.js +++ b/horizon/static/framework/conf/resource-type-registry.service.spec.js @@ -34,6 +34,10 @@ expect(service).toBeDefined(); }); + it('establishes detailsViews on a resourceType object', function() { + expect(service.getResourceType('something').detailsViews).toBeDefined(); + }); + it('init calls initScope on item and batch actions', function() { var action = { service: { initScope: angular.noop } }; spyOn(action.service, 'initScope'); @@ -79,6 +83,11 @@ }); }); + it('get/setDefaultDetailsTemplateUrl sets/retrieves a URL', function() { + service.setDefaultDetailsTemplateUrl('/my/path.html'); + expect(service.getDefaultDetailsTemplateUrl()).toBe('/my/path.html'); + }); + describe('label', function() { var label; beforeEach(function() { @@ -175,6 +184,78 @@ }); }); - }); + describe('functions the resourceType object', function() { + var type; + beforeEach(function() { + type = service.getResourceType('something'); + }); + it('itemName defaults to returning the name of an item', function() { + var item = {name: 'MegaMan'}; + expect(type.itemName(item)).toBe('MegaMan'); + }); + + it('setItemNameFunction supplies a function for interpreting names', function() { + var item = {name: 'MegaMan'}; + var func = function(x) { return 'Mr. ' + x.name; }; + type.setItemNameFunction(func); + expect(type.itemName(item)).toBe('Mr. MegaMan'); + }); + + it("pathParser return has resourceTypeCode embedded", function() { + expect(type.parsePath('abcd').resourceTypeCode).toBe('something'); + }); + + it("pathParser defaults to using the full path as the id", function() { + expect(type.parsePath('abcd').identifier).toBe('abcd'); + }); + + it("setPathParser sets the function for parsing the path", function() { + var func = function(x) { + var y = x.split('/'); + return {poolId: y[0], memberId: y[1]}; + }; + var expected = { + identifier: {poolId: '12', memberId: '42'}, + resourceTypeCode: 'something' + }; + type.setPathParser(func); + expect(type.parsePath('12/42')).toEqual(expected); + }); + + it("pathParser defaults to using the full path as the id", function() { + expect(type.parsePath('abcd').identifier).toBe('abcd'); + }); + + it("setPathParser sets the function for parsing the path", function() { + var func = function(x) { + var y = x.split('/'); + return {poolId: y[0], memberId: y[1]}; + }; + var expected = { + identifier: {poolId: '12', memberId: '42'}, + resourceTypeCode: 'something' + }; + type.setPathParser(func); + expect(type.parsePath('12/42')).toEqual(expected); + }); + + it('setPathGenerator sets the path identifier generator', function() { + var func = function(x) { + return x.poolId + '/' + x.memberId; + }; + type.setPathGenerator(func); + var identifier = {poolId: '12', memberId: '42'}; + expect(type.pathGenerator(identifier)).toBe('12/42'); + }); + + it('setLoadFunction sets the function used by "load"', function() { + var api = { + loadMe: function() { return {an: 'object'}; } + }; + type.setLoadFunction(api.loadMe); + expect(type.load()).toEqual({an: 'object'}); + }); + }); + }); })(); diff --git a/horizon/static/framework/widgets/details/details.directive.js b/horizon/static/framework/widgets/details/details.directive.js new file mode 100644 index 0000000000..0d6b87da09 --- /dev/null +++ b/horizon/static/framework/widgets/details/details.directive.js @@ -0,0 +1,68 @@ +/* + * (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.details') + .directive('hzDetails', hzDetails); + + hzDetails.$inject = ['$window']; + + /** + * @ngdoc directive + * @name horizon.framework.widgets.details:hzDetails + * @description + * Given a list of details views, provides a tab for each if more than one; + * show a single view without tabs if only one; and if none then display + * the default details view. + * + * The 'context' is an object that is provided by the resource type + * features, consisting of an 'identifier' member and a 'loadPromise' + * that are used in conveying basic information about the subject of the + * details views. + * @example + * + * ``` + * js: + * ctrl.context = { + * identifier: 'some-id', + * loadPromise: imageResourceType.load('some-id') + * }; + * ctrl.defaultTemplateUrl = '/full/path/to/some/fallthough/template.html' + * + * markup: + * + * ``` + * + */ + function hzDetails($window) { + var directive = { + restrict: 'E', + scope: { + views: '=', + context: '=', + defaultTemplateUrl: '=' + }, + templateUrl: $window.STATIC_URL + 'framework/widgets/details/details.html' + }; + return directive; + } +})(); diff --git a/horizon/static/framework/widgets/details/details.html b/horizon/static/framework/widgets/details/details.html new file mode 100644 index 0000000000..093f4810b1 --- /dev/null +++ b/horizon/static/framework/widgets/details/details.html @@ -0,0 +1,13 @@ +
+ + + + + +
+
+ +
+
+ +
diff --git a/horizon/static/framework/widgets/details/details.module.js b/horizon/static/framework/widgets/details/details.module.js new file mode 100644 index 0000000000..22444080a6 --- /dev/null +++ b/horizon/static/framework/widgets/details/details.module.js @@ -0,0 +1,29 @@ +/** + * (c) Copyright 2016 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'; + + /** + * @ngdoc overview + * @ngname horizon.framework.widgets.details + * + * @description + * Provides all of the common features for details. + */ + angular.module('horizon.framework.widgets.details', []); + +})(); diff --git a/horizon/static/framework/widgets/details/routed-details-view.controller.js b/horizon/static/framework/widgets/details/routed-details-view.controller.js new file mode 100644 index 0000000000..2e0e28ca64 --- /dev/null +++ b/horizon/static/framework/widgets/details/routed-details-view.controller.js @@ -0,0 +1,47 @@ +/* + * (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.details') + .controller('RoutedDetailsViewController', controller); + + controller.$inject = [ + 'horizon.framework.conf.resource-type-registry.service', + '$routeParams', + '$rootScope' + ]; + + function controller( + registry, + $routeParams, + $rootScope + ) { + var ctrl = this; + + ctrl.resourceType = registry.getResourceType($routeParams.type); + ctrl.context = ctrl.resourceType.parsePath($routeParams.path); + ctrl.context.loadPromise = ctrl.resourceType.load(ctrl.context.identifier); + ctrl.context.loadPromise.then(function loadData(response) { + registry.initActions($routeParams.type, $rootScope.$new()); + ctrl.itemData = response.data; + ctrl.itemName = ctrl.resourceType.itemName(response.data); + }); + ctrl.defaultTemplateUrl = registry.getDefaultDetailsTemplateUrl(); + } + +})(); diff --git a/horizon/static/framework/widgets/details/routed-details-view.controller.spec.js b/horizon/static/framework/widgets/details/routed-details-view.controller.spec.js new file mode 100644 index 0000000000..65ccda55de --- /dev/null +++ b/horizon/static/framework/widgets/details/routed-details-view.controller.spec.js @@ -0,0 +1,70 @@ +/** + * (c) Copyright 2016 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('RoutedDetailsViewController', function() { + var ctrl, deferred, $timeout; + + beforeEach(module('horizon.framework.widgets.details')); + beforeEach(inject(function($injector, $controller, $q, _$timeout_) { + deferred = $q.defer(); + $timeout = _$timeout_; + + var service = { + getResourceType: function() { return { + load: function() { return deferred.promise; }, + parsePath: function() { return {a: 'my-context'}; }, + itemName: function() { return 'A name'; } + }; }, + getDefaultDetailsTemplateUrl: angular.noop, + initActions: angular.noop + }; + + ctrl = $controller("RoutedDetailsViewController", { + 'horizon.framework.conf.resource-type-registry.service': service, + '$routeParams': { + type: 'OS::Glance::Image', + path: '1234' + } + }); + })); + + it('sets resourceType', function() { + expect(ctrl.resourceType).toBeDefined(); + }); + + it('sets context', function() { + expect(ctrl.context.a).toEqual('my-context'); + }); + + it('sets itemData when item loads', function() { + deferred.resolve({data: {some: 'data'}}); + expect(ctrl.itemData).toBeUndefined(); + $timeout.flush(); + expect(ctrl.itemData).toEqual({some: 'data'}); + }); + + it('sets itemName when item loads', function() { + deferred.resolve({data: {some: 'data'}}); + expect(ctrl.itemData).toBeUndefined(); + $timeout.flush(); + expect(ctrl.itemName).toEqual('A name'); + }); + }); + +})(); diff --git a/horizon/static/framework/widgets/details/routed-details-view.html b/horizon/static/framework/widgets/details/routed-details-view.html new file mode 100644 index 0000000000..05b754bc59 --- /dev/null +++ b/horizon/static/framework/widgets/details/routed-details-view.html @@ -0,0 +1,23 @@ +
+ + + +
+ diff --git a/horizon/static/framework/widgets/widgets.module.js b/horizon/static/framework/widgets/widgets.module.js index 283a5fe449..8384fe80a9 100644 --- a/horizon/static/framework/widgets/widgets.module.js +++ b/horizon/static/framework/widgets/widgets.module.js @@ -1,9 +1,26 @@ +/* + * (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', [ 'horizon.framework.widgets.headers', + 'horizon.framework.widgets.details', 'horizon.framework.widgets.help-panel', 'horizon.framework.widgets.wizard', 'horizon.framework.widgets.table', diff --git a/openstack_dashboard/dashboards/project/ngdetails/__init__.py b/openstack_dashboard/dashboards/project/ngdetails/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/openstack_dashboard/dashboards/project/ngdetails/panel.py b/openstack_dashboard/dashboards/project/ngdetails/panel.py new file mode 100644 index 0000000000..ca983032d2 --- /dev/null +++ b/openstack_dashboard/dashboards/project/ngdetails/panel.py @@ -0,0 +1,25 @@ +# (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. + +from django.utils.translation import ugettext_lazy as _ + +import horizon + + +class NGDetails(horizon.Panel): + name = _("Details") + slug = 'ngdetails' + + def nav(self, context): + return False diff --git a/openstack_dashboard/dashboards/project/ngdetails/urls.py b/openstack_dashboard/dashboards/project/ngdetails/urls.py new file mode 100644 index 0000000000..aade418503 --- /dev/null +++ b/openstack_dashboard/dashboards/project/ngdetails/urls.py @@ -0,0 +1,24 @@ +# (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. + +from django.conf.urls import patterns +from django.conf.urls import url + +from openstack_dashboard.dashboards.project.ngdetails import views + + +urlpatterns = patterns( + 'openstack_dashboard.dashboards.project.ngdetails.views', + url('', views.IndexView.as_view(), name='index'), +) diff --git a/openstack_dashboard/dashboards/project/ngdetails/views.py b/openstack_dashboard/dashboards/project/ngdetails/views.py new file mode 100644 index 0000000000..e072aeab92 --- /dev/null +++ b/openstack_dashboard/dashboards/project/ngdetails/views.py @@ -0,0 +1,19 @@ +# (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. + +from django.views import generic + + +class IndexView(generic.TemplateView): + template_name = 'angular.html' diff --git a/openstack_dashboard/enabled/_1070_project_ng_details_panel.py b/openstack_dashboard/enabled/_1070_project_ng_details_panel.py new file mode 100644 index 0000000000..ed4fad8eaa --- /dev/null +++ b/openstack_dashboard/enabled/_1070_project_ng_details_panel.py @@ -0,0 +1,30 @@ +# (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. + +# The slug of the dashboard the PANEL associated with. Required. +PANEL_DASHBOARD = 'project' + +# The slug of the panel group the PANEL is associated with. +# If you want the panel to show up without a panel group, +# use the panel group "default". +PANEL_GROUP = 'compute' + +# The slug of the panel to be added to HORIZON_CONFIG. Required. +PANEL = 'ngdetails' + +# If set to True, this settings file will not be added to the settings. +DISABLED = False + +# Python panel class of the PANEL to be added. +ADD_PANEL = 'openstack_dashboard.dashboards.project.ngdetails.panel.NGDetails' diff --git a/openstack_dashboard/static/app/core/core.module.js b/openstack_dashboard/static/app/core/core.module.js index 3233019abc..f1e93c4ac9 100644 --- a/openstack_dashboard/static/app/core/core.module.js +++ b/openstack_dashboard/static/app/core/core.module.js @@ -49,11 +49,16 @@ performRegistrations ]); - config.$inject = ['$provide', '$windowProvider']; + config.$inject = ['$provide', '$windowProvider', '$routeProvider']; - function config($provide, $windowProvider) { + function config($provide, $windowProvider, $routeProvider) { var path = $windowProvider.$get().STATIC_URL + 'app/core/'; $provide.constant('horizon.app.core.basePath', path); + $routeProvider + .when('/project/ngdetails/:type/:path', { + templateUrl: $windowProvider.$get().STATIC_URL + + 'framework/widgets/details/routed-details-view.html' + }); } function performRegistrations(registry) { diff --git a/openstack_dashboard/static/app/core/images/images.module.spec.js b/openstack_dashboard/static/app/core/images/images.module.spec.js index efedb892c2..861c310368 100644 --- a/openstack_dashboard/static/app/core/images/images.module.spec.js +++ b/openstack_dashboard/static/app/core/images/images.module.spec.js @@ -77,7 +77,6 @@ }); it('should set table and detail path', function() { - expect($routeProvider.when.calls.count()).toEqual(2); var imagesRouteCallArgs = $routeProvider.when.calls.argsFor(0); expect(imagesRouteCallArgs).toEqual([ '/project/ngimages/', {templateUrl: staticUrl + 'app/core/images/table/images-table.html'} @@ -102,5 +101,4 @@ expect(Object.keys(imageFormats).length).toEqual(11); }); }); - })(); diff --git a/openstack_dashboard/templates/angular.html b/openstack_dashboard/templates/angular.html new file mode 100644 index 0000000000..857b41fa15 --- /dev/null +++ b/openstack_dashboard/templates/angular.html @@ -0,0 +1,16 @@ +{% extends 'base.html' %} +{% load i18n %} +{% block title %}{% trans "Horizon" %}{% endblock %} + +{% block breadcrumb_nav %}{% endblock %} + +{% block page_header %} +{% endblock %} + +{% block ng_route_base %} + +{% endblock %} + +{% block main %} +
+{% endblock %} diff --git a/releasenotes/notes/generic-details-4f78452b14005e5b.yaml b/releasenotes/notes/generic-details-4f78452b14005e5b.yaml new file mode 100644 index 0000000000..ea85230a5c --- /dev/null +++ b/releasenotes/notes/generic-details-4f78452b14005e5b.yaml @@ -0,0 +1,26 @@ +--- +prelude: > + A Details page for a resource type (e.g. Images) + may now use the Angular application-level registry + to register views so developers may easily create + or extend details views. In this implementation + these views are presented as tabs within the + Details page. +features: + - A directive (hz-details) provides the ability to + intelligently display a set of views (typically for + a Details context). + - A generic Details display parses the location to + determine the resource type, and displays relevant + details views for that type. + - A Descriptor concept allows convenient passing of + information that can globally identify an object, + for use in generic views and actions. + - Horizon now has a (non-navigational) route in Django + so generic details pages are deep-linked. + - A shared Django template is now available for use by + any Angular page. +upgrade: + - (optional) Use the common Angular template as the + basis of any Angular pages to minimize boilerplate code + and to ensure that we use similar features/framing.