Generic details display framework
This patch provides the ability for the registered detail views for any resource type to be generically presented. This patch does the following: * Adds a directive that displays a set of views (i.e. details sub-views) * Adds a Generic Detail display for routed pages * Adds the concept of a Descriptor which contains a resource type name and an identifier. The identifier can be something as simple as a string, but may also be an object (if the resource type needs more than one value to look up its data, e.g. Pool Members) * Adds the ability for a resource type to have knowledge about how one of its items may be loaded, so any detail page can fetch the information given a basic context * Adds a generic Angular page (since they all just route to ng-views). We will see this used in subsequent patches as well. * Sets up a Django route to a non-navigational panel for the Details Change-Id: Ie116b52ba196f9240fdc6bbc4a12d37beb9b9fcf Partially-Implements: blueprint angular-registry
This commit is contained in:
parent
ea8e7a504a
commit
11968c840c
horizon/static/framework
conf
widgets
openstack_dashboard
dashboards/project/ngdetails
enabled
static/app/core
templates
releasenotes/notes
@ -56,7 +56,7 @@
|
|||||||
*/
|
*/
|
||||||
function registryService(extensibleService) {
|
function registryService(extensibleService) {
|
||||||
|
|
||||||
function ResourceType() {
|
function ResourceType(type) {
|
||||||
// 'properties' contains information about properties associated with
|
// 'properties' contains information about properties associated with
|
||||||
// this resource type. The expectation is that the key is the 'code'
|
// this resource type. The expectation is that the key is the 'code'
|
||||||
// name of the property and the value conforms to the standard
|
// name of the property and the value conforms to the standard
|
||||||
@ -66,6 +66,31 @@
|
|||||||
this.getName = getName;
|
this.getName = getName;
|
||||||
this.label = label;
|
this.label = label;
|
||||||
this.format = format;
|
this.format = format;
|
||||||
|
this.type = type;
|
||||||
|
this.setLoadFunction = setLoadFunction;
|
||||||
|
this.load = load;
|
||||||
|
|
||||||
|
// These members support the ability of a type to provide a function
|
||||||
|
// that, given an object in the structure presented by the
|
||||||
|
// load() function, produces a human-readable name.
|
||||||
|
this.itemNameFunction = defaultItemNameFunction;
|
||||||
|
this.setItemNameFunction = setItemNameFunction;
|
||||||
|
this.itemName = itemName;
|
||||||
|
|
||||||
|
// The purpose of these members is to allow details to be retrieved
|
||||||
|
// automatically from such a path, or similarly to create a path
|
||||||
|
// to such a route from any reference. This establishes a two-way
|
||||||
|
// relationship between the path and the identifier(s) for the item.
|
||||||
|
// The path could be used as part of a details route, for example:
|
||||||
|
//
|
||||||
|
// An identifier of 'abc-defg' would yield '/abc-defg' which
|
||||||
|
// could be used in a details url, such as:
|
||||||
|
// '/details/OS::Glance::Image/abc-defg'
|
||||||
|
this.pathParser = defaultPathParser;
|
||||||
|
this.setPathParser = setPathParser;
|
||||||
|
this.parsePath = parsePath;
|
||||||
|
this.setPathGenerator = setPathGenerator;
|
||||||
|
this.pathGenerator = defaultPathGenerator;
|
||||||
|
|
||||||
// itemActions is a list of actions that can be executed upon a single
|
// itemActions is a list of actions that can be executed upon a single
|
||||||
// item. The list is made extensible so it can be added to independently.
|
// item. The list is made extensible so it can be added to independently.
|
||||||
@ -77,6 +102,14 @@
|
|||||||
this.batchActions = [];
|
this.batchActions = [];
|
||||||
extensibleService(this.batchActions, this.batchActions);
|
extensibleService(this.batchActions, this.batchActions);
|
||||||
|
|
||||||
|
// detailsViews is a list of views that can be shown on a details view.
|
||||||
|
// For example, each item added to this list could be represented
|
||||||
|
// as a tab of a details view.
|
||||||
|
this.detailsViews = [];
|
||||||
|
extensibleService(this.detailsViews, this.detailsViews);
|
||||||
|
|
||||||
|
// Function declarations
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @ngdoc function
|
* @ngdoc function
|
||||||
* @name setProperty
|
* @name setProperty
|
||||||
@ -135,6 +168,141 @@
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @ngdoc function
|
||||||
|
* @name setPathParser
|
||||||
|
* @description
|
||||||
|
* Sets a function that is used to parse paths. See parsePath.
|
||||||
|
* @example
|
||||||
|
```
|
||||||
|
getResourceType('thing').setPathParser(func);
|
||||||
|
|
||||||
|
function func(path) {
|
||||||
|
return path.replace('-', '');
|
||||||
|
}
|
||||||
|
|
||||||
|
var descriptor = resourceType.parsePath(path);
|
||||||
|
```
|
||||||
|
*/
|
||||||
|
function setPathParser(func) {
|
||||||
|
this.pathParser = func;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @ngdoc function
|
||||||
|
* @name parsePath
|
||||||
|
* @description
|
||||||
|
* Given a subpath, produce an object that describes the object
|
||||||
|
* enough to load it from an API. This is used in details
|
||||||
|
* routes, which must generate an object that has enough
|
||||||
|
* fidelity to fetch the object. In many cases this is a simple
|
||||||
|
* ID, but in others there may be multiple IDs that are required
|
||||||
|
* to fetch the data.
|
||||||
|
* @example
|
||||||
|
```
|
||||||
|
getResourceType('thing').setPathParser(func);
|
||||||
|
|
||||||
|
function func(path) {
|
||||||
|
return path.replace('-', '');
|
||||||
|
}
|
||||||
|
|
||||||
|
var descriptor = resourceType.parsePath(path);
|
||||||
|
```
|
||||||
|
*/
|
||||||
|
function parsePath(path) {
|
||||||
|
return {identifier: this.pathParser(path), resourceTypeCode: this.type};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @ngdoc function
|
||||||
|
* @name setLoadFunction
|
||||||
|
* @description
|
||||||
|
* Sets a function that is used to load a single item. See load().
|
||||||
|
* @example
|
||||||
|
```
|
||||||
|
getResourceType('thing').setLoadFunction(func);
|
||||||
|
|
||||||
|
function func(descriptor) {
|
||||||
|
return someApi.get(descriptor.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
var loadPromise = resourceType.load({id: 'some-id'});
|
||||||
|
```
|
||||||
|
*/
|
||||||
|
function setLoadFunction(func) {
|
||||||
|
this.loadFunction = func;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @ngdoc function
|
||||||
|
* @name load
|
||||||
|
* @description
|
||||||
|
* Loads a single item
|
||||||
|
* @example
|
||||||
|
```
|
||||||
|
getResourceType('thing').setLoadFunction(func);
|
||||||
|
|
||||||
|
function func(descriptor) {
|
||||||
|
return someApi.get(descriptor.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
var loadPromise = resourceType.load({id: 'some-id'});
|
||||||
|
```
|
||||||
|
*/
|
||||||
|
function load(descriptor) {
|
||||||
|
return this.loadFunction(descriptor);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @ngdoc function
|
||||||
|
* @name setPathGenerator
|
||||||
|
* @description
|
||||||
|
* Sets a function that is used generate paths. Accepts the
|
||||||
|
* resource-type-specific id/object.
|
||||||
|
* The subpath returned should NOT have a leading slash.
|
||||||
|
* @example
|
||||||
|
```
|
||||||
|
getResourceType('thing').setPathGenerator(func);
|
||||||
|
|
||||||
|
function func(descriptor) {
|
||||||
|
return 'load-balancer/' + descriptor.balancerId
|
||||||
|
+ '/listener/' + descriptor.id
|
||||||
|
}
|
||||||
|
|
||||||
|
```
|
||||||
|
*/
|
||||||
|
function setPathGenerator(func) {
|
||||||
|
this.pathGenerator = func;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Functions relating item names, described above.
|
||||||
|
function defaultItemNameFunction(item) {
|
||||||
|
return item.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setItemNameFunction(func) {
|
||||||
|
this.itemNameFunction = func;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
function itemName(item) {
|
||||||
|
return this.itemNameFunction(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Functions providing default path parsers and generators
|
||||||
|
// so most common objects don't have to re-specify the most common
|
||||||
|
// case, which is that a path component for an identifier is just the ID.
|
||||||
|
function defaultPathParser(path) {
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
|
||||||
|
function defaultPathGenerator(id) {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @ngdoc function
|
* @ngdoc function
|
||||||
* @name getName
|
* @name getName
|
||||||
@ -229,11 +397,33 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
var resourceTypes = {};
|
var resourceTypes = {};
|
||||||
|
var defaultDetailsTemplateUrl = false;
|
||||||
var registry = {
|
var registry = {
|
||||||
|
setDefaultDetailsTemplateUrl: setDefaultDetailsTemplateUrl,
|
||||||
|
getDefaultDetailsTemplateUrl: getDefaultDetailsTemplateUrl,
|
||||||
getResourceType: getResourceType,
|
getResourceType: getResourceType,
|
||||||
initActions: initActions
|
initActions: initActions
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function getDefaultDetailsTemplateUrl() {
|
||||||
|
return defaultDetailsTemplateUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* @ngdoc function
|
||||||
|
* @name setDefaultDetailsTemplateUrl
|
||||||
|
* @param {String} url - The URL for the template to be used
|
||||||
|
* @description
|
||||||
|
* The idea is that in the case that someone links to a details page for a
|
||||||
|
* resource and there is no view registered, there can be a default view.
|
||||||
|
* For example, if there's a generic property viewer, that could display
|
||||||
|
* the resource.
|
||||||
|
*/
|
||||||
|
function setDefaultDetailsTemplateUrl(url) {
|
||||||
|
defaultDetailsTemplateUrl = url;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* @ngdoc function
|
* @ngdoc function
|
||||||
* @name getResourceType
|
* @name getResourceType
|
||||||
@ -245,7 +435,7 @@
|
|||||||
*/
|
*/
|
||||||
function getResourceType(type, config) {
|
function getResourceType(type, config) {
|
||||||
if (!resourceTypes.hasOwnProperty(type)) {
|
if (!resourceTypes.hasOwnProperty(type)) {
|
||||||
resourceTypes[type] = new ResourceType();
|
resourceTypes[type] = new ResourceType(type);
|
||||||
}
|
}
|
||||||
if (angular.isDefined(config)) {
|
if (angular.isDefined(config)) {
|
||||||
angular.extend(resourceTypes[type], config);
|
angular.extend(resourceTypes[type], config);
|
||||||
|
@ -34,6 +34,10 @@
|
|||||||
expect(service).toBeDefined();
|
expect(service).toBeDefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('establishes detailsViews on a resourceType object', function() {
|
||||||
|
expect(service.getResourceType('something').detailsViews).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
it('init calls initScope on item and batch actions', function() {
|
it('init calls initScope on item and batch actions', function() {
|
||||||
var action = { service: { initScope: angular.noop } };
|
var action = { service: { initScope: angular.noop } };
|
||||||
spyOn(action.service, 'initScope');
|
spyOn(action.service, 'initScope');
|
||||||
@ -79,6 +83,11 @@
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('get/setDefaultDetailsTemplateUrl sets/retrieves a URL', function() {
|
||||||
|
service.setDefaultDetailsTemplateUrl('/my/path.html');
|
||||||
|
expect(service.getDefaultDetailsTemplateUrl()).toBe('/my/path.html');
|
||||||
|
});
|
||||||
|
|
||||||
describe('label', function() {
|
describe('label', function() {
|
||||||
var label;
|
var label;
|
||||||
beforeEach(function() {
|
beforeEach(function() {
|
||||||
@ -175,6 +184,78 @@
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('functions the resourceType object', function() {
|
||||||
|
var type;
|
||||||
|
beforeEach(function() {
|
||||||
|
type = service.getResourceType('something');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('itemName defaults to returning the name of an item', function() {
|
||||||
|
var item = {name: 'MegaMan'};
|
||||||
|
expect(type.itemName(item)).toBe('MegaMan');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('setItemNameFunction supplies a function for interpreting names', function() {
|
||||||
|
var item = {name: 'MegaMan'};
|
||||||
|
var func = function(x) { return 'Mr. ' + x.name; };
|
||||||
|
type.setItemNameFunction(func);
|
||||||
|
expect(type.itemName(item)).toBe('Mr. MegaMan');
|
||||||
|
});
|
||||||
|
|
||||||
|
it("pathParser return has resourceTypeCode embedded", function() {
|
||||||
|
expect(type.parsePath('abcd').resourceTypeCode).toBe('something');
|
||||||
|
});
|
||||||
|
|
||||||
|
it("pathParser defaults to using the full path as the id", function() {
|
||||||
|
expect(type.parsePath('abcd').identifier).toBe('abcd');
|
||||||
|
});
|
||||||
|
|
||||||
|
it("setPathParser sets the function for parsing the path", function() {
|
||||||
|
var func = function(x) {
|
||||||
|
var y = x.split('/');
|
||||||
|
return {poolId: y[0], memberId: y[1]};
|
||||||
|
};
|
||||||
|
var expected = {
|
||||||
|
identifier: {poolId: '12', memberId: '42'},
|
||||||
|
resourceTypeCode: 'something'
|
||||||
|
};
|
||||||
|
type.setPathParser(func);
|
||||||
|
expect(type.parsePath('12/42')).toEqual(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("pathParser defaults to using the full path as the id", function() {
|
||||||
|
expect(type.parsePath('abcd').identifier).toBe('abcd');
|
||||||
|
});
|
||||||
|
|
||||||
|
it("setPathParser sets the function for parsing the path", function() {
|
||||||
|
var func = function(x) {
|
||||||
|
var y = x.split('/');
|
||||||
|
return {poolId: y[0], memberId: y[1]};
|
||||||
|
};
|
||||||
|
var expected = {
|
||||||
|
identifier: {poolId: '12', memberId: '42'},
|
||||||
|
resourceTypeCode: 'something'
|
||||||
|
};
|
||||||
|
type.setPathParser(func);
|
||||||
|
expect(type.parsePath('12/42')).toEqual(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('setPathGenerator sets the path identifier generator', function() {
|
||||||
|
var func = function(x) {
|
||||||
|
return x.poolId + '/' + x.memberId;
|
||||||
|
};
|
||||||
|
type.setPathGenerator(func);
|
||||||
|
var identifier = {poolId: '12', memberId: '42'};
|
||||||
|
expect(type.pathGenerator(identifier)).toBe('12/42');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('setLoadFunction sets the function used by "load"', function() {
|
||||||
|
var api = {
|
||||||
|
loadMe: function() { return {an: 'object'}; }
|
||||||
|
};
|
||||||
|
type.setLoadFunction(api.loadMe);
|
||||||
|
expect(type.load()).toEqual({an: 'object'});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
})();
|
})();
|
||||||
|
@ -0,0 +1,68 @@
|
|||||||
|
/*
|
||||||
|
* (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';
|
||||||
|
|
||||||
|
angular
|
||||||
|
.module('horizon.framework.widgets.details')
|
||||||
|
.directive('hzDetails', hzDetails);
|
||||||
|
|
||||||
|
hzDetails.$inject = ['$window'];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @ngdoc directive
|
||||||
|
* @name horizon.framework.widgets.details:hzDetails
|
||||||
|
* @description
|
||||||
|
* Given a list of details views, provides a tab for each if more than one;
|
||||||
|
* show a single view without tabs if only one; and if none then display
|
||||||
|
* the default details view.
|
||||||
|
*
|
||||||
|
* The 'context' is an object that is provided by the resource type
|
||||||
|
* features, consisting of an 'identifier' member and a 'loadPromise'
|
||||||
|
* that are used in conveying basic information about the subject of the
|
||||||
|
* details views.
|
||||||
|
* @example
|
||||||
|
*
|
||||||
|
* ```
|
||||||
|
* js:
|
||||||
|
* ctrl.context = {
|
||||||
|
* identifier: 'some-id',
|
||||||
|
* loadPromise: imageResourceType.load('some-id')
|
||||||
|
* };
|
||||||
|
* ctrl.defaultTemplateUrl = '/full/path/to/some/fallthough/template.html'
|
||||||
|
*
|
||||||
|
* markup:
|
||||||
|
* <hz-details
|
||||||
|
* views="ctrl.resourceType.detailsViews"
|
||||||
|
* context="ctrl.context"
|
||||||
|
* default-template-url="ctrl.defaultTemplateUrl"
|
||||||
|
* ></hz-details>
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
function hzDetails($window) {
|
||||||
|
var directive = {
|
||||||
|
restrict: 'E',
|
||||||
|
scope: {
|
||||||
|
views: '=',
|
||||||
|
context: '=',
|
||||||
|
defaultTemplateUrl: '='
|
||||||
|
},
|
||||||
|
templateUrl: $window.STATIC_URL + 'framework/widgets/details/details.html'
|
||||||
|
};
|
||||||
|
return directive;
|
||||||
|
}
|
||||||
|
})();
|
13
horizon/static/framework/widgets/details/details.html
Normal file
13
horizon/static/framework/widgets/details/details.html
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
<div ng-if="views.length > 1">
|
||||||
|
<tabset class="tabset-details">
|
||||||
|
<tab class="tab-details" ng-repeat="view in views" heading="{$ view.name $}">
|
||||||
|
<ng-include src="view.template"></ng-include>
|
||||||
|
</tab>
|
||||||
|
</tabset>
|
||||||
|
</div>
|
||||||
|
<div ng-if="views.length === 1">
|
||||||
|
<ng-include src="views[0].template"></ng-include>
|
||||||
|
</div>
|
||||||
|
<div ng-if="views.length === 0">
|
||||||
|
<ng-include src="defaultTemplateUrl"></ng-include>
|
||||||
|
</div>
|
29
horizon/static/framework/widgets/details/details.module.js
Normal file
29
horizon/static/framework/widgets/details/details.module.js
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
/**
|
||||||
|
* (c) Copyright 2016 Hewlett-Packard Development Company, L.P.
|
||||||
|
*
|
||||||
|
* 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';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @ngdoc overview
|
||||||
|
* @ngname horizon.framework.widgets.details
|
||||||
|
*
|
||||||
|
* @description
|
||||||
|
* Provides all of the common features for details.
|
||||||
|
*/
|
||||||
|
angular.module('horizon.framework.widgets.details', []);
|
||||||
|
|
||||||
|
})();
|
@ -0,0 +1,47 @@
|
|||||||
|
/*
|
||||||
|
* (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";
|
||||||
|
|
||||||
|
angular
|
||||||
|
.module('horizon.framework.widgets.details')
|
||||||
|
.controller('RoutedDetailsViewController', controller);
|
||||||
|
|
||||||
|
controller.$inject = [
|
||||||
|
'horizon.framework.conf.resource-type-registry.service',
|
||||||
|
'$routeParams',
|
||||||
|
'$rootScope'
|
||||||
|
];
|
||||||
|
|
||||||
|
function controller(
|
||||||
|
registry,
|
||||||
|
$routeParams,
|
||||||
|
$rootScope
|
||||||
|
) {
|
||||||
|
var ctrl = this;
|
||||||
|
|
||||||
|
ctrl.resourceType = registry.getResourceType($routeParams.type);
|
||||||
|
ctrl.context = ctrl.resourceType.parsePath($routeParams.path);
|
||||||
|
ctrl.context.loadPromise = ctrl.resourceType.load(ctrl.context.identifier);
|
||||||
|
ctrl.context.loadPromise.then(function loadData(response) {
|
||||||
|
registry.initActions($routeParams.type, $rootScope.$new());
|
||||||
|
ctrl.itemData = response.data;
|
||||||
|
ctrl.itemName = ctrl.resourceType.itemName(response.data);
|
||||||
|
});
|
||||||
|
ctrl.defaultTemplateUrl = registry.getDefaultDetailsTemplateUrl();
|
||||||
|
}
|
||||||
|
|
||||||
|
})();
|
@ -0,0 +1,70 @@
|
|||||||
|
/**
|
||||||
|
* (c) Copyright 2016 Hewlett-Packard Development Company, L.P.
|
||||||
|
*
|
||||||
|
* 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('RoutedDetailsViewController', function() {
|
||||||
|
var ctrl, deferred, $timeout;
|
||||||
|
|
||||||
|
beforeEach(module('horizon.framework.widgets.details'));
|
||||||
|
beforeEach(inject(function($injector, $controller, $q, _$timeout_) {
|
||||||
|
deferred = $q.defer();
|
||||||
|
$timeout = _$timeout_;
|
||||||
|
|
||||||
|
var service = {
|
||||||
|
getResourceType: function() { return {
|
||||||
|
load: function() { return deferred.promise; },
|
||||||
|
parsePath: function() { return {a: 'my-context'}; },
|
||||||
|
itemName: function() { return 'A name'; }
|
||||||
|
}; },
|
||||||
|
getDefaultDetailsTemplateUrl: angular.noop,
|
||||||
|
initActions: angular.noop
|
||||||
|
};
|
||||||
|
|
||||||
|
ctrl = $controller("RoutedDetailsViewController", {
|
||||||
|
'horizon.framework.conf.resource-type-registry.service': service,
|
||||||
|
'$routeParams': {
|
||||||
|
type: 'OS::Glance::Image',
|
||||||
|
path: '1234'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('sets resourceType', function() {
|
||||||
|
expect(ctrl.resourceType).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sets context', function() {
|
||||||
|
expect(ctrl.context.a).toEqual('my-context');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sets itemData when item loads', function() {
|
||||||
|
deferred.resolve({data: {some: 'data'}});
|
||||||
|
expect(ctrl.itemData).toBeUndefined();
|
||||||
|
$timeout.flush();
|
||||||
|
expect(ctrl.itemData).toEqual({some: 'data'});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sets itemName when item loads', function() {
|
||||||
|
deferred.resolve({data: {some: 'data'}});
|
||||||
|
expect(ctrl.itemData).toBeUndefined();
|
||||||
|
$timeout.flush();
|
||||||
|
expect(ctrl.itemName).toEqual('A name');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
})();
|
@ -0,0 +1,23 @@
|
|||||||
|
<div ng-controller="RoutedDetailsViewController as ctrl">
|
||||||
|
|
||||||
|
<div class="page-header">
|
||||||
|
<ol class="breadcrumb">
|
||||||
|
<li><a href="javascript:history.back()">Back</a></li>
|
||||||
|
</ol>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-xs-12 col-sm-9 text-left">
|
||||||
|
<span class="h1">{$ ctrl.itemName $}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-xs-12 col-sm-3 text-right details-item-actions" ng-if="ctrl.itemData">
|
||||||
|
<actions allowed="ctrl.resourceType.itemActions" type="row" item="ctrl.itemData"></actions>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<hz-details
|
||||||
|
views="ctrl.resourceType.detailsViews"
|
||||||
|
context="ctrl.context"
|
||||||
|
default-template-url="ctrl.defaultTemplateUrl"
|
||||||
|
></hz-details>
|
||||||
|
</div>
|
||||||
|
|
@ -1,9 +1,26 @@
|
|||||||
|
/*
|
||||||
|
* (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 () {
|
(function () {
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
angular
|
angular
|
||||||
.module('horizon.framework.widgets', [
|
.module('horizon.framework.widgets', [
|
||||||
'horizon.framework.widgets.headers',
|
'horizon.framework.widgets.headers',
|
||||||
|
'horizon.framework.widgets.details',
|
||||||
'horizon.framework.widgets.help-panel',
|
'horizon.framework.widgets.help-panel',
|
||||||
'horizon.framework.widgets.wizard',
|
'horizon.framework.widgets.wizard',
|
||||||
'horizon.framework.widgets.table',
|
'horizon.framework.widgets.table',
|
||||||
|
25
openstack_dashboard/dashboards/project/ngdetails/panel.py
Normal file
25
openstack_dashboard/dashboards/project/ngdetails/panel.py
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
# (c) Copyright 2015 Hewlett-Packard Development Company, L.P.
|
||||||
|
#
|
||||||
|
# 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.
|
||||||
|
|
||||||
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
|
import horizon
|
||||||
|
|
||||||
|
|
||||||
|
class NGDetails(horizon.Panel):
|
||||||
|
name = _("Details")
|
||||||
|
slug = 'ngdetails'
|
||||||
|
|
||||||
|
def nav(self, context):
|
||||||
|
return False
|
24
openstack_dashboard/dashboards/project/ngdetails/urls.py
Normal file
24
openstack_dashboard/dashboards/project/ngdetails/urls.py
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
# (c) Copyright 2015 Hewlett-Packard Development Company, L.P.
|
||||||
|
#
|
||||||
|
# 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.
|
||||||
|
|
||||||
|
from django.conf.urls import patterns
|
||||||
|
from django.conf.urls import url
|
||||||
|
|
||||||
|
from openstack_dashboard.dashboards.project.ngdetails import views
|
||||||
|
|
||||||
|
|
||||||
|
urlpatterns = patterns(
|
||||||
|
'openstack_dashboard.dashboards.project.ngdetails.views',
|
||||||
|
url('', views.IndexView.as_view(), name='index'),
|
||||||
|
)
|
19
openstack_dashboard/dashboards/project/ngdetails/views.py
Normal file
19
openstack_dashboard/dashboards/project/ngdetails/views.py
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
# (c) Copyright 2015 Hewlett-Packard Development Company, L.P.
|
||||||
|
#
|
||||||
|
# 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.
|
||||||
|
|
||||||
|
from django.views import generic
|
||||||
|
|
||||||
|
|
||||||
|
class IndexView(generic.TemplateView):
|
||||||
|
template_name = 'angular.html'
|
@ -0,0 +1,30 @@
|
|||||||
|
# (c) Copyright 2015 Hewlett-Packard Development Company, L.P.
|
||||||
|
#
|
||||||
|
# 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.
|
||||||
|
|
||||||
|
# The slug of the dashboard the PANEL associated with. Required.
|
||||||
|
PANEL_DASHBOARD = 'project'
|
||||||
|
|
||||||
|
# The slug of the panel group the PANEL is associated with.
|
||||||
|
# If you want the panel to show up without a panel group,
|
||||||
|
# use the panel group "default".
|
||||||
|
PANEL_GROUP = 'compute'
|
||||||
|
|
||||||
|
# The slug of the panel to be added to HORIZON_CONFIG. Required.
|
||||||
|
PANEL = 'ngdetails'
|
||||||
|
|
||||||
|
# If set to True, this settings file will not be added to the settings.
|
||||||
|
DISABLED = False
|
||||||
|
|
||||||
|
# Python panel class of the PANEL to be added.
|
||||||
|
ADD_PANEL = 'openstack_dashboard.dashboards.project.ngdetails.panel.NGDetails'
|
@ -49,11 +49,16 @@
|
|||||||
performRegistrations
|
performRegistrations
|
||||||
]);
|
]);
|
||||||
|
|
||||||
config.$inject = ['$provide', '$windowProvider'];
|
config.$inject = ['$provide', '$windowProvider', '$routeProvider'];
|
||||||
|
|
||||||
function config($provide, $windowProvider) {
|
function config($provide, $windowProvider, $routeProvider) {
|
||||||
var path = $windowProvider.$get().STATIC_URL + 'app/core/';
|
var path = $windowProvider.$get().STATIC_URL + 'app/core/';
|
||||||
$provide.constant('horizon.app.core.basePath', path);
|
$provide.constant('horizon.app.core.basePath', path);
|
||||||
|
$routeProvider
|
||||||
|
.when('/project/ngdetails/:type/:path', {
|
||||||
|
templateUrl: $windowProvider.$get().STATIC_URL +
|
||||||
|
'framework/widgets/details/routed-details-view.html'
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function performRegistrations(registry) {
|
function performRegistrations(registry) {
|
||||||
|
@ -77,7 +77,6 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should set table and detail path', function() {
|
it('should set table and detail path', function() {
|
||||||
expect($routeProvider.when.calls.count()).toEqual(2);
|
|
||||||
var imagesRouteCallArgs = $routeProvider.when.calls.argsFor(0);
|
var imagesRouteCallArgs = $routeProvider.when.calls.argsFor(0);
|
||||||
expect(imagesRouteCallArgs).toEqual([
|
expect(imagesRouteCallArgs).toEqual([
|
||||||
'/project/ngimages/', {templateUrl: staticUrl + 'app/core/images/table/images-table.html'}
|
'/project/ngimages/', {templateUrl: staticUrl + 'app/core/images/table/images-table.html'}
|
||||||
@ -102,5 +101,4 @@
|
|||||||
expect(Object.keys(imageFormats).length).toEqual(11);
|
expect(Object.keys(imageFormats).length).toEqual(11);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
})();
|
})();
|
||||||
|
16
openstack_dashboard/templates/angular.html
Normal file
16
openstack_dashboard/templates/angular.html
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
{% load i18n %}
|
||||||
|
{% block title %}{% trans "Horizon" %}{% endblock %}
|
||||||
|
|
||||||
|
{% block breadcrumb_nav %}{% endblock %}
|
||||||
|
|
||||||
|
{% block page_header %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block ng_route_base %}
|
||||||
|
<base href="{{ WEBROOT }}">
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block main %}
|
||||||
|
<div ng-view></div>
|
||||||
|
{% endblock %}
|
26
releasenotes/notes/generic-details-4f78452b14005e5b.yaml
Normal file
26
releasenotes/notes/generic-details-4f78452b14005e5b.yaml
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
---
|
||||||
|
prelude: >
|
||||||
|
A Details page for a resource type (e.g. Images)
|
||||||
|
may now use the Angular application-level registry
|
||||||
|
to register views so developers may easily create
|
||||||
|
or extend details views. In this implementation
|
||||||
|
these views are presented as tabs within the
|
||||||
|
Details page.
|
||||||
|
features:
|
||||||
|
- A directive (hz-details) provides the ability to
|
||||||
|
intelligently display a set of views (typically for
|
||||||
|
a Details context).
|
||||||
|
- A generic Details display parses the location to
|
||||||
|
determine the resource type, and displays relevant
|
||||||
|
details views for that type.
|
||||||
|
- A Descriptor concept allows convenient passing of
|
||||||
|
information that can globally identify an object,
|
||||||
|
for use in generic views and actions.
|
||||||
|
- Horizon now has a (non-navigational) route in Django
|
||||||
|
so generic details pages are deep-linked.
|
||||||
|
- A shared Django template is now available for use by
|
||||||
|
any Angular page.
|
||||||
|
upgrade:
|
||||||
|
- (optional) Use the common Angular template as the
|
||||||
|
basis of any Angular pages to minimize boilerplate code
|
||||||
|
and to ensure that we use similar features/framing.
|
Loading…
x
Reference in New Issue
Block a user