From 8275d67949915e9b600559787cdb05bb23fdf337 Mon Sep 17 00:00:00 2001 From: Shu Muto Date: Thu, 25 May 2017 18:39:15 +0900 Subject: [PATCH] Reproduce navigations on refreshing ngdetails view To setup proper navigation to side bar and breadcrumb, this patch adds new 'defaultIndexUrl' parameter and its getter/setter into resource-type-service. The 'defaultIndexUrl' parameter makes details view enable to set navigations properly in Angular-side. Each panel module should set URL for default index view using 'defaultIndexUrl' parameter. So, this patch adds the `defaultIndexUrl` parameter into existing panel modules that have Angularized details view. Also, if query string has 'nav' parameter, the navigation setting will be overwitten with it. This URL overwriting may be used by panels that has multiple index panels, like images panel. Change-Id: I2edd44e55eb10114e5282cec1762e9635881f733 Closes-Bug: #1746706 --- horizon/browsers/views.py | 3 + .../conf/resource-type-registry.service.js | 33 ++++ .../util/navigations/navigations.module.js | 19 ++ .../util/navigations/navigations.service.js | 109 ++++++++++++ .../navigations/navigations.service.spec.js | 163 ++++++++++++++++++ horizon/static/framework/util/util.module.js | 1 + .../details/routed-details-view.controller.js | 32 ++++ .../routed-details-view.controller.spec.js | 11 +- .../identity/domains/domains.module.js | 1 + .../dashboard/identity/users/users.module.js | 1 + .../static/app/core/images/images.module.js | 1 + .../static/app/core/images/images.service.js | 6 +- .../app/core/keypairs/keypairs.module.js | 1 + .../static/app/core/network_qos/qos.module.js | 1 + .../static/app/core/trunks/trunks.module.js | 1 + openstack_dashboard/templates/angular.html | 1 + .../notes/bug-1746706-8d2f982c514f22b1.yaml | 6 + 17 files changed, 387 insertions(+), 3 deletions(-) create mode 100644 horizon/static/framework/util/navigations/navigations.module.js create mode 100644 horizon/static/framework/util/navigations/navigations.service.js create mode 100644 horizon/static/framework/util/navigations/navigations.service.spec.js create mode 100644 releasenotes/notes/bug-1746706-8d2f982c514f22b1.yaml diff --git a/horizon/browsers/views.py b/horizon/browsers/views.py index 81e0c5cff6..4d89a97989 100644 --- a/horizon/browsers/views.py +++ b/horizon/browsers/views.py @@ -100,7 +100,10 @@ class AngularDetailsView(generic.TemplateView): title = _("Horizon") context["title"] = title context["page_title"] = title + # set default dashboard and panel dashboard = horizon.get_default_dashboard() self.request.horizon['dashboard'] = dashboard self.request.horizon['panel'] = dashboard.get_panels()[0] + # set flag that means routed by django + context['routed_by_django'] = True return context diff --git a/horizon/static/framework/conf/resource-type-registry.service.js b/horizon/static/framework/conf/resource-type-registry.service.js index 8074df411f..a4f0550cdf 100644 --- a/horizon/static/framework/conf/resource-type-registry.service.js +++ b/horizon/static/framework/conf/resource-type-registry.service.js @@ -139,6 +139,10 @@ self.summaryTemplateUrl = false; self.setSummaryTemplateUrl = setSummaryTemplateUrl; + self.defaultIndexUrl = false; + self.setDefaultIndexUrl = setDefaultIndexUrl; + self.getDefaultIndexUrl = getDefaultIndexUrl; + // Function declarations /* @@ -503,6 +507,35 @@ return self; } + /** + * @ngdoc function + * @name setDefaultIndexUrl + * @param url + * @description + * This sets the defaultIndexUrl property on the resourceType. + * + * That URL points to a index view that shows table view for the + * resource type. The defaultIndexUrl will be used when details view + * should redirect to index view (e.g. after deletion of the resource + * itself) or should reset navigations (e.g. after refreshing details + * view by browser). + */ + function setDefaultIndexUrl(url) { + self.defaultIndexUrl = url; + return self; + } + + /** + * @ngdoc function + * @name setDefaultIndexUrl + * @param url + * @description + * This returns the defaultIndexUrl property on the resourceType. + */ + function getDefaultIndexUrl() { + return self.defaultIndexUrl; + } + /** * @ngdoc function * @name setItemNameFunction diff --git a/horizon/static/framework/util/navigations/navigations.module.js b/horizon/static/framework/util/navigations/navigations.module.js new file mode 100644 index 0000000000..8a93c69653 --- /dev/null +++ b/horizon/static/framework/util/navigations/navigations.module.js @@ -0,0 +1,19 @@ +/* + * 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.util.navigations', []); + +})(); diff --git a/horizon/static/framework/util/navigations/navigations.service.js b/horizon/static/framework/util/navigations/navigations.service.js new file mode 100644 index 0000000000..6f289eb5d1 --- /dev/null +++ b/horizon/static/framework/util/navigations/navigations.service.js @@ -0,0 +1,109 @@ +/* + * 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.util.navigations') + .factory('horizon.framework.util.navigations.service', navigationsService); + + function navigationsService() { + + return { + getActivePanelUrl: getActivePanelUrl, + collapseAllNavigation: collapseAllNavigation, + expandNavigationByUrl: expandNavigationByUrl, + setBreadcrumb: setBreadcrumb + }; + + /* get URL for active panel on navigation side bar */ + function getActivePanelUrl() { + return angular.element('a.openstack-panel.active').attr('href'); + } + + /* collapse all nodes on navigation side bar */ + function collapseAllNavigation() { + // collapse all dashboards + var dashboards = angular.element(".openstack-dashboard").children("a"); + dashboards.addClass("collapsed").attr("aria-expanded", false); + dashboards.siblings("ul").removeClass("in").attr("style", "height: 0px"); + + // collapse all panelgroups + var panelgroups = angular.element(".openstack-panel-group").children("a"); + panelgroups.addClass("collapsed").attr("aria-expanded", false); + panelgroups.siblings("div").removeClass("in").attr("style", "height: 0px"); + + // remove active from all panels + angular.element("a.openstack-panel").removeClass("active"); + } + + /* expand specified node on navigation side bar */ + function expandNavigationByUrl(url) { + // collapse all navigation + collapseAllNavigation(); + + var labels = []; + + // get panel on nav_bar + var panel = angular.element("a.openstack-panel[href='" + url + "']"); + + // get panelgroup on nav_bar + var panelgroup = panel.parents(".openstack-panel-group").children("a"); + + // get dashboard on nav_bar + var dashboard = panel.parents(".openstack-dashboard").children("a"); + + // open dashboard nav + dashboard.removeClass("collapsed").attr("aria-expanded", true); + dashboard.siblings("ul").addClass("in").attr("style", null); + // get dashboard label + labels.push(dashboard.text().trim()); + + // open panelgroup on nav_bar if exists + if (panelgroup.length) { + panelgroup.removeClass("collapsed").attr("aria-expanded", true); + // get panelgroup label + labels.push(panelgroup.text().trim()); + } + + // open container for panels + panel.parent().addClass("in").attr("style", null); + + // set panel active + panel.addClass("active"); + // get panel label + labels.push(panel.text().trim()); + + return labels; + } + + /* set breadcrumb items by array. The last item will be set as active */ + function setBreadcrumb(items) { + var breadcrumb = angular.element("div.page-breadcrumb ol.breadcrumb"); + + // remove all items + breadcrumb.empty(); + + // add items + items.forEach(function (item, index, array) { + var newItem = angular.element("
  • ").addClass("breadcrumb-item-truncate"); + if (array.length - 1 === index) { + newItem.addClass("active"); + } + newItem.text(item); + breadcrumb.append(newItem); + }); + } + } +})(); diff --git a/horizon/static/framework/util/navigations/navigations.service.spec.js b/horizon/static/framework/util/navigations/navigations.service.spec.js new file mode 100644 index 0000000000..d80fd05cfb --- /dev/null +++ b/horizon/static/framework/util/navigations/navigations.service.spec.js @@ -0,0 +1,163 @@ +/* + * (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'; + + describe('horizon.framework.util.navigations.service', function() { + var service, navigations, spyElement; + var imagesUrl = '/project/images/'; + var breadcrumb = ['Project', 'Compute', 'Images']; + var breadcrumbWithoutGroup = ['Project', 'Images']; + + function getNavsElement (selector) { + try { + // for searching element + return navigations.find(selector); + } catch (e) { + // for creating element + return $(selector); + } + } + + beforeEach(module('horizon.framework.util.navigations')); + + beforeEach(inject(function($injector) { + service = $injector.get('horizon.framework.util.navigations.service'); + navigations = angular.element( + '
    ' + + ' ' + + '
  • ' + + ' ' + + ' Project' + + ' ' + + ' ' + + '
  • ' + + ' ' + + ' ' + + ''); + spyElement = spyOn(angular, 'element').and.callFake(getNavsElement); + })); + + afterEach(function() { + spyElement.and.callThrough(); + }); + + describe('getActivePanelUrl', function() { + it('returns an empty array if no items', function() { + var activeUrl = service.getActivePanelUrl(); + + expect(activeUrl).toBe(imagesUrl); + }); + }); + + describe('collapseAllNavigation', function() { + it('collapse all nodes on navigation side bar', function() { + service.collapseAllNavigation(); + + var hasIn = navigations.find('.in'); + expect(hasIn.length).toBe(0); + var collapsed = navigations.find('a.collapsed[aria-expanded=false]'); + expect(collapsed.length).toBe(2); + var hasActive = navigations.find('a.openstack-panel.active'); + expect(hasActive.length).toBe(0); + }); + }); + + describe('expandNavigationByUrl', function() { + it('expands navigation side bar and return their label of selected nodes', function() { + spyOn(service, 'collapseAllNavigation').and.callThrough(); + var list = service.expandNavigationByUrl(imagesUrl); + + expect(list).toEqual(breadcrumb); + + var hasIn = navigations.find('.in'); + expect(hasIn.length).toBe(2); + var expanded = navigations.find('a[aria-expanded=true]'); + expect(expanded.length).toBe(2); + var hasActive = navigations.find('a.openstack-panel.active'); + expect(hasActive.length).toBe(1); + }); + }); + + describe('expandNavigationByUrl', function() { + it('expands navigation side bar without panelgroup' + + 'and return their label of selected nodes', function() { + navigations = angular.element( + '
    ' + + ' ' + + '
  • ' + + ' ' + + ' Project' + + ' ' + + ' ' + + '
  • ' + + ' ' + + ' ' + + '
    '); + + spyOn(service, 'collapseAllNavigation').and.callThrough(); + var list = service.expandNavigationByUrl(imagesUrl); + + expect(list).toEqual(breadcrumbWithoutGroup); + + var hasIn = navigations.find('.in'); + expect(hasIn.length).toBe(2); + var expanded = navigations.find('a[aria-expanded=true]'); + expect(expanded.length).toBe(1); + var hasActive = navigations.find('a.openstack-panel.active'); + expect(hasActive.length).toBe(1); + }); + }); + + describe('setBreadcrumb', function() { + it('sets breadcrumb items from specified array', function() { + service.setBreadcrumb(breadcrumb); + }); + }); + + }); + +})(); diff --git a/horizon/static/framework/util/util.module.js b/horizon/static/framework/util/util.module.js index bfe9d807f2..38695d220b 100644 --- a/horizon/static/framework/util/util.module.js +++ b/horizon/static/framework/util/util.module.js @@ -23,6 +23,7 @@ 'horizon.framework.util.filters', 'horizon.framework.util.http', 'horizon.framework.util.i18n', + 'horizon.framework.util.navigations', 'horizon.framework.util.promise-toggle', 'horizon.framework.util.q', 'horizon.framework.util.tech-debt', diff --git a/horizon/static/framework/widgets/details/routed-details-view.controller.js b/horizon/static/framework/widgets/details/routed-details-view.controller.js index 0479775f80..d2a1ee60d5 100644 --- a/horizon/static/framework/widgets/details/routed-details-view.controller.js +++ b/horizon/static/framework/widgets/details/routed-details-view.controller.js @@ -23,7 +23,9 @@ controller.$inject = [ 'horizon.framework.conf.resource-type-registry.service', 'horizon.framework.util.actions.action-result.service', + 'horizon.framework.util.navigations.service', 'horizon.framework.widgets.modal-wait-spinner.service', + '$location', '$q', '$routeParams' ]; @@ -31,7 +33,9 @@ function controller( registry, resultService, + navigationsService, spinnerService, + $location, $q, $routeParams ) { @@ -45,6 +49,34 @@ ctrl.defaultTemplateUrl = registry.getDefaultDetailsTemplateUrl(); ctrl.resultHandler = actionResultHandler; + checkRoutedByDjango(ctrl.resourceType); + + function checkRoutedByDjango(resourceType) { + // get flag that means routed once by django. + var routedByDjango = angular.element("ngdetails").attr("routed-by-django"); + if (routedByDjango === "True") { + // If django routed to ngdetails view, navigations (i.e. side bar and + // breadcrumbs) are set as default dashboard and panel by django side + // AngularDetailsView. + // So reset navigations properly using defaultIndexUrl parameter for + // resource-type-service. + + // get defaultIndexUrl + var url = resourceType.getDefaultIndexUrl(); + // if querystring has 'nav' parameter, overwrite the url + var query = $location.search(); + if (query.hasOwnProperty("nav")) { + url = query.nav; + } + // set navigations (side bar and breadcrumb) + var labels = navigationsService.expandNavigationByUrl(url); + navigationsService.setBreadcrumb(labels); + + // clear flag + angular.element("ngdetails").removeAttr("routed-by-django"); + } + } + function actionResultHandler(returnValue) { return $q.when(returnValue, actionSuccessHandler); } 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 index 010109abe2..27ff02522c 100644 --- a/horizon/static/framework/widgets/details/routed-details-view.controller.spec.js +++ b/horizon/static/framework/widgets/details/routed-details-view.controller.spec.js @@ -18,7 +18,7 @@ 'use strict'; describe('RoutedDetailsViewController', function() { - var ctrl, deferred, $timeout, $q, actionResultService; + var ctrl, deferred, $timeout, $q, actionResultService, navigationsService; beforeEach(module('horizon.framework.widgets.details')); beforeEach(inject(function($injector, $controller, _$q_, _$timeout_) { @@ -32,7 +32,8 @@ load: function() { return deferred.promise; }, parsePath: function() { return 'my-context'; }, itemName: function() { return 'A name'; }, - initActions: angular.noop + initActions: angular.noop, + getDefaultIndexUrl: function() { return '/project/images/'; } }; }, getDefaultDetailsTemplateUrl: angular.noop @@ -42,9 +43,15 @@ getIdsOfType: function() { return []; } }; + navigationsService = { + expandNavigationByUrl: function() { return ['Project', 'Compute', 'Images']; }, + setBreadcrumb: angular.noop + }; + ctrl = $controller("RoutedDetailsViewController", { 'horizon.framework.conf.resource-type-registry.service': service, 'horizon.framework.util.actions.action-result.service': actionResultService, + 'horizon.framework.util.navigations.service': navigationsService, 'horizon.framework.widgets.modal-wait-spinner.service': { showModalSpinner: angular.noop, hideModalSpinner: angular.noop diff --git a/openstack_dashboard/dashboards/identity/static/dashboard/identity/domains/domains.module.js b/openstack_dashboard/dashboards/identity/static/dashboard/identity/domains/domains.module.js index 34ed76363e..b5e918df76 100644 --- a/openstack_dashboard/dashboards/identity/static/dashboard/identity/domains/domains.module.js +++ b/openstack_dashboard/dashboards/identity/static/dashboard/identity/domains/domains.module.js @@ -45,6 +45,7 @@ registry.getResourceType(domainResourceType) .setNames(gettext('Domain'), gettext('Domains')) .setSummaryTemplateUrl(basePath + 'details/drawer.html') + .setDefaultIndexUrl('/identity/domains/') .setProperties(domainProperties()) .setListFunction(domainService.listDomains) .tableColumns diff --git a/openstack_dashboard/dashboards/identity/static/dashboard/identity/users/users.module.js b/openstack_dashboard/dashboards/identity/static/dashboard/identity/users/users.module.js index 3726909b8e..01d5d06fe1 100644 --- a/openstack_dashboard/dashboards/identity/static/dashboard/identity/users/users.module.js +++ b/openstack_dashboard/dashboards/identity/static/dashboard/identity/users/users.module.js @@ -48,6 +48,7 @@ registry.getResourceType(userResourceType) .setNames(gettext('User'), gettext('Users')) .setSummaryTemplateUrl(basePath + 'details/drawer.html') + .setDefaultIndexUrl('/identity/users/') .setProperties(userProperties()) .setListFunction(usersService.getUsersPromise) .setNeedsFilterFirstFunction(usersService.getFilterFirstSettingPromise) diff --git a/openstack_dashboard/static/app/core/images/images.module.js b/openstack_dashboard/static/app/core/images/images.module.js index e133dfdc86..83cd62eef1 100644 --- a/openstack_dashboard/static/app/core/images/images.module.js +++ b/openstack_dashboard/static/app/core/images/images.module.js @@ -73,6 +73,7 @@ registry.getResourceType(imageResourceType) .setNames(gettext('Image'), gettext('Images')) .setSummaryTemplateUrl(basePath + 'details/drawer.html') + .setDefaultIndexUrl('/project/images/') .setItemInTransitionFunction(imagesService.isInTransition) .setProperties(imageProperties(imagesService, statuses)) .setListFunction(imagesService.getImagesPromise) diff --git a/openstack_dashboard/static/app/core/images/images.service.js b/openstack_dashboard/static/app/core/images/images.service.js index 496479604e..a039a8519c 100644 --- a/openstack_dashboard/static/app/core/images/images.service.js +++ b/openstack_dashboard/static/app/core/images/images.service.js @@ -66,7 +66,11 @@ * view. */ function getDetailsPath(item) { - return detailRoute + 'OS::Glance::Image/' + item.id; + var detailsPath = detailRoute + 'OS::Glance::Image/' + item.id; + if ($location.url() === '/admin/images') { + detailsPath = detailsPath + "?nav=/admin/images/"; + } + return detailsPath; } /* diff --git a/openstack_dashboard/static/app/core/keypairs/keypairs.module.js b/openstack_dashboard/static/app/core/keypairs/keypairs.module.js index f75c783087..6f59bce63b 100644 --- a/openstack_dashboard/static/app/core/keypairs/keypairs.module.js +++ b/openstack_dashboard/static/app/core/keypairs/keypairs.module.js @@ -48,6 +48,7 @@ .setNames(gettext('Key Pair'), gettext('Key Pairs')) // for detail summary view on table row. .setSummaryTemplateUrl(basePath + 'details/drawer.html') + .setDefaultIndexUrl('/project/key_pairs/') .setProperties(keypairProperties()) .setListFunction(keypairsService.getKeypairsPromise) .tableColumns diff --git a/openstack_dashboard/static/app/core/network_qos/qos.module.js b/openstack_dashboard/static/app/core/network_qos/qos.module.js index 4fa1d96380..f2f9898c24 100644 --- a/openstack_dashboard/static/app/core/network_qos/qos.module.js +++ b/openstack_dashboard/static/app/core/network_qos/qos.module.js @@ -46,6 +46,7 @@ registry.getResourceType(qosResourceType) .setNames(gettext('QoS Policy'), gettext('QoS Policies')) .setSummaryTemplateUrl(basePath + 'details/drawer.html') + .setDefaultIndexUrl('/project/network_qos/') .setProperties(qosProperties(qosService)) .setListFunction(qosService.getPoliciesPromise) .tableColumns diff --git a/openstack_dashboard/static/app/core/trunks/trunks.module.js b/openstack_dashboard/static/app/core/trunks/trunks.module.js index 4b0656715c..fc5a0de377 100644 --- a/openstack_dashboard/static/app/core/trunks/trunks.module.js +++ b/openstack_dashboard/static/app/core/trunks/trunks.module.js @@ -54,6 +54,7 @@ registry.getResourceType(trunkResourceType) .setNames(gettext('Trunk'), gettext('Trunks')) .setSummaryTemplateUrl(basePath + 'summary.html') + .setDefaultIndexUrl('/project/trunks/') .setProperties(trunkProperties()) .setListFunction(trunksService.getTrunksPromise) .tableColumns diff --git a/openstack_dashboard/templates/angular.html b/openstack_dashboard/templates/angular.html index 13f5b5fee7..332f42e85e 100644 --- a/openstack_dashboard/templates/angular.html +++ b/openstack_dashboard/templates/angular.html @@ -11,4 +11,5 @@ {% block main %}
    + {% endblock %} diff --git a/releasenotes/notes/bug-1746706-8d2f982c514f22b1.yaml b/releasenotes/notes/bug-1746706-8d2f982c514f22b1.yaml new file mode 100644 index 0000000000..669b07da42 --- /dev/null +++ b/releasenotes/notes/bug-1746706-8d2f982c514f22b1.yaml @@ -0,0 +1,6 @@ +--- +fixes: + - | + [:bug:`1746706`] Fixed a bug the navigation menu and breadcrumb list + are not reproduced properly when reloading or opening Angular-based + detail page directly.