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
@ -56,7 +56,7 @@
|
||||
*/
|
||||
function registryService(extensibleService) {
|
||||
|
||||
function ResourceType() {
|
||||
function ResourceType(type) {
|
||||
// 'properties' contains information about properties associated with
|
||||
// this resource type. The expectation is that the key is the 'code'
|
||||
// name of the property and the value conforms to the standard
|
||||
@ -66,6 +66,31 @@
|
||||
this.getName = getName;
|
||||
this.label = label;
|
||||
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
|
||||
// item. The list is made extensible so it can be added to independently.
|
||||
@ -77,6 +102,14 @@
|
||||
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
|
||||
* @name setProperty
|
||||
@ -135,6 +168,141 @@
|
||||
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
|
||||
* @name getName
|
||||
@ -229,11 +397,33 @@
|
||||
}
|
||||
|
||||
var resourceTypes = {};
|
||||
var defaultDetailsTemplateUrl = false;
|
||||
var registry = {
|
||||
setDefaultDetailsTemplateUrl: setDefaultDetailsTemplateUrl,
|
||||
getDefaultDetailsTemplateUrl: getDefaultDetailsTemplateUrl,
|
||||
getResourceType: getResourceType,
|
||||
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
|
||||
* @name getResourceType
|
||||
@ -245,7 +435,7 @@
|
||||
*/
|
||||
function getResourceType(type, config) {
|
||||
if (!resourceTypes.hasOwnProperty(type)) {
|
||||
resourceTypes[type] = new ResourceType();
|
||||
resourceTypes[type] = new ResourceType(type);
|
||||
}
|
||||
if (angular.isDefined(config)) {
|
||||
angular.extend(resourceTypes[type], config);
|
||||
|
@ -34,6 +34,10 @@
|
||||
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() {
|
||||
var action = { service: { initScope: angular.noop } };
|
||||
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() {
|
||||
var label;
|
||||
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 () {
|
||||
'use strict';
|
||||
|
||||
angular
|
||||
.module('horizon.framework.widgets', [
|
||||
'horizon.framework.widgets.headers',
|
||||
'horizon.framework.widgets.details',
|
||||
'horizon.framework.widgets.help-panel',
|
||||
'horizon.framework.widgets.wizard',
|
||||
'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
|
||||
]);
|
||||
|
||||
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/';
|
||||
$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) {
|
||||
|
@ -77,7 +77,6 @@
|
||||
});
|
||||
|
||||
it('should set table and detail path', function() {
|
||||
expect($routeProvider.when.calls.count()).toEqual(2);
|
||||
var imagesRouteCallArgs = $routeProvider.when.calls.argsFor(0);
|
||||
expect(imagesRouteCallArgs).toEqual([
|
||||
'/project/ngimages/', {templateUrl: staticUrl + 'app/core/images/table/images-table.html'}
|
||||
@ -102,5 +101,4 @@
|
||||
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…
Reference in New Issue
Block a user