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' +
+ ' ' +
+ ' ' +
+ ' ' +
+ ' ' +
+ '
' +
+ '
' +
+ ' - Project
' +
+ ' - Compute
' +
+ ' - Images
' +
+ '
' +
+ '
' +
+ '
');
+ 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' +
+ ' ' +
+ ' ' +
+ ' ' +
+ ' ' +
+ '
' +
+ '
' +
+ ' - Project
' +
+ ' - Compute
' +
+ ' - Images
' +
+ '
' +
+ '
' +
+ '
');
+
+ 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.