Merge "Reproduce navigations on refreshing ngdetails view"

This commit is contained in:
Zuul 2018-04-06 10:02:39 +00:00 committed by Gerrit Code Review
commit febb96507b
17 changed files with 387 additions and 3 deletions

View File

@ -100,7 +100,10 @@ class AngularDetailsView(generic.TemplateView):
title = _("Horizon") title = _("Horizon")
context["title"] = title context["title"] = title
context["page_title"] = title context["page_title"] = title
# set default dashboard and panel
dashboard = horizon.get_default_dashboard() dashboard = horizon.get_default_dashboard()
self.request.horizon['dashboard'] = dashboard self.request.horizon['dashboard'] = dashboard
self.request.horizon['panel'] = dashboard.get_panels()[0] self.request.horizon['panel'] = dashboard.get_panels()[0]
# set flag that means routed by django
context['routed_by_django'] = True
return context return context

View File

@ -139,6 +139,10 @@
self.summaryTemplateUrl = false; self.summaryTemplateUrl = false;
self.setSummaryTemplateUrl = setSummaryTemplateUrl; self.setSummaryTemplateUrl = setSummaryTemplateUrl;
self.defaultIndexUrl = false;
self.setDefaultIndexUrl = setDefaultIndexUrl;
self.getDefaultIndexUrl = getDefaultIndexUrl;
// Function declarations // Function declarations
/* /*
@ -503,6 +507,35 @@
return self; 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 * @ngdoc function
* @name setItemNameFunction * @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.filters',
'horizon.framework.util.http', 'horizon.framework.util.http',
'horizon.framework.util.i18n', 'horizon.framework.util.i18n',
'horizon.framework.util.navigations',
'horizon.framework.util.promise-toggle', 'horizon.framework.util.promise-toggle',
'horizon.framework.util.q', 'horizon.framework.util.q',
'horizon.framework.util.tech-debt', 'horizon.framework.util.tech-debt',

View File

@ -23,7 +23,9 @@
controller.$inject = [ controller.$inject = [
'horizon.framework.conf.resource-type-registry.service', 'horizon.framework.conf.resource-type-registry.service',
'horizon.framework.util.actions.action-result.service', 'horizon.framework.util.actions.action-result.service',
'horizon.framework.util.navigations.service',
'horizon.framework.widgets.modal-wait-spinner.service', 'horizon.framework.widgets.modal-wait-spinner.service',
'$location',
'$q', '$q',
'$routeParams' '$routeParams'
]; ];
@ -31,7 +33,9 @@
function controller( function controller(
registry, registry,
resultService, resultService,
navigationsService,
spinnerService, spinnerService,
$location,
$q, $q,
$routeParams $routeParams
) { ) {
@ -45,6 +49,34 @@
ctrl.defaultTemplateUrl = registry.getDefaultDetailsTemplateUrl(); ctrl.defaultTemplateUrl = registry.getDefaultDetailsTemplateUrl();
ctrl.resultHandler = actionResultHandler; 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) { function actionResultHandler(returnValue) {
return $q.when(returnValue, actionSuccessHandler); return $q.when(returnValue, actionSuccessHandler);
} }

View File

@ -18,7 +18,7 @@
'use strict'; 'use strict';
describe('RoutedDetailsViewController', function() { describe('RoutedDetailsViewController', function() {
var ctrl, deferred, $timeout, $q, actionResultService; var ctrl, deferred, $timeout, $q, actionResultService, navigationsService;
beforeEach(module('horizon.framework.widgets.details')); beforeEach(module('horizon.framework.widgets.details'));
beforeEach(inject(function($injector, $controller, _$q_, _$timeout_) { beforeEach(inject(function($injector, $controller, _$q_, _$timeout_) {
@ -32,7 +32,8 @@
load: function() { return deferred.promise; }, load: function() { return deferred.promise; },
parsePath: function() { return 'my-context'; }, parsePath: function() { return 'my-context'; },
itemName: function() { return 'A name'; }, itemName: function() { return 'A name'; },
initActions: angular.noop initActions: angular.noop,
getDefaultIndexUrl: function() { return '/project/images/'; }
}; };
}, },
getDefaultDetailsTemplateUrl: angular.noop getDefaultDetailsTemplateUrl: angular.noop
@ -42,9 +43,15 @@
getIdsOfType: function() { return []; } getIdsOfType: function() { return []; }
}; };
navigationsService = {
expandNavigationByUrl: function() { return ['Project', 'Compute', 'Images']; },
setBreadcrumb: angular.noop
};
ctrl = $controller("RoutedDetailsViewController", { ctrl = $controller("RoutedDetailsViewController", {
'horizon.framework.conf.resource-type-registry.service': service, 'horizon.framework.conf.resource-type-registry.service': service,
'horizon.framework.util.actions.action-result.service': actionResultService, 'horizon.framework.util.actions.action-result.service': actionResultService,
'horizon.framework.util.navigations.service': navigationsService,
'horizon.framework.widgets.modal-wait-spinner.service': { 'horizon.framework.widgets.modal-wait-spinner.service': {
showModalSpinner: angular.noop, showModalSpinner: angular.noop,
hideModalSpinner: angular.noop hideModalSpinner: angular.noop

View File

@ -45,6 +45,7 @@
registry.getResourceType(domainResourceType) registry.getResourceType(domainResourceType)
.setNames(gettext('Domain'), gettext('Domains')) .setNames(gettext('Domain'), gettext('Domains'))
.setSummaryTemplateUrl(basePath + 'details/drawer.html') .setSummaryTemplateUrl(basePath + 'details/drawer.html')
.setDefaultIndexUrl('/identity/domains/')
.setProperties(domainProperties()) .setProperties(domainProperties())
.setListFunction(domainService.listDomains) .setListFunction(domainService.listDomains)
.tableColumns .tableColumns

View File

@ -48,6 +48,7 @@
registry.getResourceType(userResourceType) registry.getResourceType(userResourceType)
.setNames(gettext('User'), gettext('Users')) .setNames(gettext('User'), gettext('Users'))
.setSummaryTemplateUrl(basePath + 'details/drawer.html') .setSummaryTemplateUrl(basePath + 'details/drawer.html')
.setDefaultIndexUrl('/identity/users/')
.setProperties(userProperties()) .setProperties(userProperties())
.setListFunction(usersService.getUsersPromise) .setListFunction(usersService.getUsersPromise)
.setNeedsFilterFirstFunction(usersService.getFilterFirstSettingPromise) .setNeedsFilterFirstFunction(usersService.getFilterFirstSettingPromise)

View File

@ -73,6 +73,7 @@
registry.getResourceType(imageResourceType) registry.getResourceType(imageResourceType)
.setNames(gettext('Image'), gettext('Images')) .setNames(gettext('Image'), gettext('Images'))
.setSummaryTemplateUrl(basePath + 'details/drawer.html') .setSummaryTemplateUrl(basePath + 'details/drawer.html')
.setDefaultIndexUrl('/project/images/')
.setItemInTransitionFunction(imagesService.isInTransition) .setItemInTransitionFunction(imagesService.isInTransition)
.setProperties(imageProperties(imagesService, statuses)) .setProperties(imageProperties(imagesService, statuses))
.setListFunction(imagesService.getImagesPromise) .setListFunction(imagesService.getImagesPromise)

View File

@ -66,7 +66,11 @@
* view. * view.
*/ */
function getDetailsPath(item) { 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')) .setNames(gettext('Key Pair'), gettext('Key Pairs'))
// for detail summary view on table row. // for detail summary view on table row.
.setSummaryTemplateUrl(basePath + 'details/drawer.html') .setSummaryTemplateUrl(basePath + 'details/drawer.html')
.setDefaultIndexUrl('/project/key_pairs/')
.setProperties(keypairProperties()) .setProperties(keypairProperties())
.setListFunction(keypairsService.getKeypairsPromise) .setListFunction(keypairsService.getKeypairsPromise)
.tableColumns .tableColumns

View File

@ -46,6 +46,7 @@
registry.getResourceType(qosResourceType) registry.getResourceType(qosResourceType)
.setNames(gettext('QoS Policy'), gettext('QoS Policies')) .setNames(gettext('QoS Policy'), gettext('QoS Policies'))
.setSummaryTemplateUrl(basePath + 'details/drawer.html') .setSummaryTemplateUrl(basePath + 'details/drawer.html')
.setDefaultIndexUrl('/project/network_qos/')
.setProperties(qosProperties(qosService)) .setProperties(qosProperties(qosService))
.setListFunction(qosService.getPoliciesPromise) .setListFunction(qosService.getPoliciesPromise)
.tableColumns .tableColumns

View File

@ -54,6 +54,7 @@
registry.getResourceType(trunkResourceType) registry.getResourceType(trunkResourceType)
.setNames(gettext('Trunk'), gettext('Trunks')) .setNames(gettext('Trunk'), gettext('Trunks'))
.setSummaryTemplateUrl(basePath + 'summary.html') .setSummaryTemplateUrl(basePath + 'summary.html')
.setDefaultIndexUrl('/project/trunks/')
.setProperties(trunkProperties()) .setProperties(trunkProperties())
.setListFunction(trunksService.getTrunksPromise) .setListFunction(trunksService.getTrunksPromise)
.tableColumns .tableColumns

View File

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