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.