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
This commit is contained in:
parent
c32b5c1f2e
commit
8275d67949
@ -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
|
||||
|
@ -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
|
||||
|
@ -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', []);
|
||||
|
||||
})();
|
109
horizon/static/framework/util/navigations/navigations.service.js
Normal file
109
horizon/static/framework/util/navigations/navigations.service.js
Normal file
@ -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("<li>").addClass("breadcrumb-item-truncate");
|
||||
if (array.length - 1 === index) {
|
||||
newItem.addClass("active");
|
||||
}
|
||||
newItem.text(item);
|
||||
breadcrumb.append(newItem);
|
||||
});
|
||||
}
|
||||
}
|
||||
})();
|
@ -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(
|
||||
'<div>' +
|
||||
' <!-- navigation side bar -->' +
|
||||
' <li class="openstack-dashboard">' +
|
||||
' <a class="" aria-expanded="true">' +
|
||||
' Project' +
|
||||
' </a>' +
|
||||
' <ul class="in" style="">' +
|
||||
' <li class="openstack-panel-group">' +
|
||||
' <a class="" area-expanded="true">' +
|
||||
' Compute' +
|
||||
' </a>' +
|
||||
' <div class="in" style="">' +
|
||||
' <a class="openstack-panel active" href="/project/images/">' +
|
||||
' Images' +
|
||||
' </a>' +
|
||||
' </div>' +
|
||||
' </li>' +
|
||||
' </ul>' +
|
||||
' </li>' +
|
||||
' <!-- breadcrumb -->' +
|
||||
' <div class="page-breadcrumb">' +
|
||||
' <ol class="breadcrumb">' +
|
||||
' <li>Project</li>' +
|
||||
' <li>Compute</li>' +
|
||||
' <li class="active">Images</li>' +
|
||||
' </ol>' +
|
||||
' </div>' +
|
||||
'</div>');
|
||||
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(
|
||||
'<div>' +
|
||||
' <!-- navigation side bar -->' +
|
||||
' <li class="openstack-dashboard">' +
|
||||
' <a class="" aria-expanded="true">' +
|
||||
' Project' +
|
||||
' </a>' +
|
||||
' <ul class="in" style="">' +
|
||||
' <div class="in" style="">' +
|
||||
' <a class="openstack-panel active" href="/project/images/">' +
|
||||
' Images' +
|
||||
' </a>' +
|
||||
' </div>' +
|
||||
' </ul>' +
|
||||
' </li>' +
|
||||
' <!-- breadcrumb -->' +
|
||||
' <div class="page-breadcrumb">' +
|
||||
' <ol class="breadcrumb">' +
|
||||
' <li>Project</li>' +
|
||||
' <li>Compute</li>' +
|
||||
' <li class="active">Images</li>' +
|
||||
' </ol>' +
|
||||
' </div>' +
|
||||
'</div>');
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
})();
|
@ -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',
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
/*
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -11,4 +11,5 @@
|
||||
|
||||
{% block main %}
|
||||
<div ng-view></div>
|
||||
<ngdetails routed-by-django="{{ routed_by_django }}">
|
||||
{% endblock %}
|
||||
|
6
releasenotes/notes/bug-1746706-8d2f982c514f22b1.yaml
Normal file
6
releasenotes/notes/bug-1746706-8d2f982c514f22b1.yaml
Normal file
@ -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.
|
Loading…
Reference in New Issue
Block a user