Merge "Reproduce navigations on refreshing ngdetails view"
This commit is contained in:
commit
febb96507b
@ -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