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:
Shu Muto 2017-05-25 18:39:15 +09:00
parent c32b5c1f2e
commit 8275d67949
17 changed files with 387 additions and 3 deletions

View File

@ -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

View File

@ -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

View File

@ -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', []);
})();

View 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);
});
}
}
})();

View File

@ -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);
});
});
});
})();

View File

@ -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',

View File

@ -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);
}

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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)

View File

@ -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;
}
/*

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -11,4 +11,5 @@
{% block main %}
<div ng-view></div>
<ngdetails routed-by-django="{{ routed_by_django }}">
{% endblock %}

View 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.