Adding resource panel and table features
This patch adds two basic features: a directive that takes in a resource type name and produces a table of the resource type with actions, links to views, etc., based on information in the resource type object; the other directive provides a resource panel with header based on a resource type name. Change-Id: Idaba844aca5fc6e89e2bd7c65cb836feaba67f67 Partially-Implements: blueprint angular-registry
This commit is contained in:
parent
947b9f5b42
commit
01aa99473a
@ -94,7 +94,9 @@
|
||||
// type, with the data as a result in a promise. For example, Images code
|
||||
// would register a list function that returns a promise that will resolve
|
||||
// to all the Images data in list form.
|
||||
this.listFunction = angular.noop;
|
||||
this.listFunction = function def() {
|
||||
return Promise.resolve({data: {items: []}});
|
||||
};
|
||||
this.setListFunction = setListFunction;
|
||||
|
||||
// The table columns are an extensible registration of columns of data
|
||||
@ -506,10 +508,6 @@
|
||||
}
|
||||
|
||||
var resourceTypes = {};
|
||||
// The slugs are only used to align Django routes with heat
|
||||
// type names. In a context without Django routing this is
|
||||
// not needed.
|
||||
var slugs = {};
|
||||
var defaultSummaryTemplateUrl = false;
|
||||
var defaultDetailsTemplateUrl = false;
|
||||
var registry = {
|
||||
@ -519,20 +517,9 @@
|
||||
setDefaultSummaryTemplateUrl: setDefaultSummaryTemplateUrl,
|
||||
getDefaultSummaryTemplateUrl: getDefaultSummaryTemplateUrl,
|
||||
setDefaultDetailsTemplateUrl: setDefaultDetailsTemplateUrl,
|
||||
getDefaultDetailsTemplateUrl: getDefaultDetailsTemplateUrl,
|
||||
setSlug: setSlug,
|
||||
getTypeNameBySlug: getTypeNameBySlug
|
||||
getDefaultDetailsTemplateUrl: getDefaultDetailsTemplateUrl
|
||||
};
|
||||
|
||||
function getTypeNameBySlug(slug) {
|
||||
return slugs[slug];
|
||||
}
|
||||
|
||||
function setSlug(slug, typeName) {
|
||||
slugs[slug] = typeName;
|
||||
return this;
|
||||
}
|
||||
|
||||
function getDefaultSummaryTemplateUrl() {
|
||||
return defaultSummaryTemplateUrl;
|
||||
}
|
||||
|
@ -166,11 +166,6 @@
|
||||
});
|
||||
});
|
||||
|
||||
it("sets and retrieves slugs", function() {
|
||||
service.setSlug('image', 'OS::Glance::Image');
|
||||
expect(service.getTypeNameBySlug('image')).toBe('OS::Glance::Image');
|
||||
});
|
||||
|
||||
describe('getName', function() {
|
||||
it('returns nothing if names not provided', function() {
|
||||
var type = service.getResourceType('something');
|
||||
|
@ -0,0 +1,34 @@
|
||||
/*
|
||||
* (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.panel')
|
||||
.controller('horizon.framework.widgets.panel.HzResourcePanelController', controller);
|
||||
|
||||
controller.$inject = [
|
||||
'horizon.framework.conf.resource-type-registry.service'
|
||||
];
|
||||
|
||||
function controller(registry) {
|
||||
var ctrl = this;
|
||||
|
||||
ctrl.resourceType = registry.getResourceType(ctrl.resourceTypeName);
|
||||
ctrl.pageName = ctrl.resourceType.getName();
|
||||
}
|
||||
|
||||
})();
|
@ -0,0 +1,57 @@
|
||||
/*
|
||||
* (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.
|
||||
*/
|
||||
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
describe('hz-resource-panel controller', function() {
|
||||
var ctrl;
|
||||
|
||||
var resourceType = {
|
||||
getName: function() {
|
||||
return 'MyType';
|
||||
}
|
||||
};
|
||||
|
||||
beforeEach(module('horizon.framework.conf'));
|
||||
beforeEach(module('horizon.framework.widgets.panel'));
|
||||
|
||||
beforeEach(inject(function($controller) {
|
||||
var registry = {
|
||||
getResourceType: angular.noop
|
||||
};
|
||||
|
||||
spyOn(registry, 'getResourceType').and.returnValue(resourceType);
|
||||
|
||||
ctrl = $controller('horizon.framework.widgets.panel.HzResourcePanelController', {
|
||||
'horizon.framework.conf.resource-type-registry.service': registry,
|
||||
tableResourceType: 'OS::Test::Example'});
|
||||
}));
|
||||
|
||||
it('exists', function() {
|
||||
expect(ctrl).toBeDefined();
|
||||
});
|
||||
|
||||
it('sets resourceType to the resource type', function() {
|
||||
expect(ctrl.resourceType).toBe(resourceType);
|
||||
});
|
||||
|
||||
it('sets resourceTypeName to the resource type name', function() {
|
||||
expect(ctrl.pageName).toEqual('MyType');
|
||||
});
|
||||
});
|
||||
|
||||
})();
|
@ -0,0 +1,57 @@
|
||||
/**
|
||||
* (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.panel')
|
||||
.directive('hzResourcePanel', directive);
|
||||
|
||||
directive.$inject = ['horizon.framework.widgets.basePath'];
|
||||
|
||||
/**
|
||||
* @ngdoc directive
|
||||
* @name hzResourcePanel
|
||||
* @description
|
||||
* This directive takes in a resource type name, e.g. 'OS::Glance::Image'
|
||||
* as a String and produces the shell of a panel for that given resource
|
||||
* type. This primarily includes a header and allows content to be
|
||||
* transcluded.
|
||||
*
|
||||
* @example
|
||||
```
|
||||
<hz-resource-panel resource-type-name="OS::Nova::Server">
|
||||
<div>Here is my content!</div>
|
||||
<hz-resource-table resource-type-name="OS::Nova::Server"></hz-resource-table>
|
||||
</hz-resource-panel>
|
||||
```
|
||||
*/
|
||||
function directive(basePath) {
|
||||
|
||||
var directive = {
|
||||
restrict: 'E',
|
||||
scope: {
|
||||
resourceTypeName: '@'
|
||||
},
|
||||
transclude: true,
|
||||
bindToController: true,
|
||||
templateUrl: basePath + 'panel/hz-resource-panel.html',
|
||||
controller: "horizon.framework.widgets.panel.HzResourcePanelController as ctrl"
|
||||
};
|
||||
|
||||
return directive;
|
||||
}
|
||||
})();
|
@ -0,0 +1,4 @@
|
||||
<div>
|
||||
<hz-page-header header="{$ ctrl.pageName $}"></hz-page-header>
|
||||
<ng-transclude></ng-transclude>
|
||||
</div>
|
22
horizon/static/framework/widgets/panel/panel.module.js
Normal file
22
horizon/static/framework/widgets/panel/panel.module.js
Normal file
@ -0,0 +1,22 @@
|
||||
/*
|
||||
* (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.panel', []);
|
||||
|
||||
})();
|
@ -0,0 +1,126 @@
|
||||
/*
|
||||
* (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.table')
|
||||
.controller('horizon.framework.widgets.table.ResourceTableController', controller);
|
||||
|
||||
controller.$inject = [
|
||||
'$q',
|
||||
'$scope',
|
||||
'horizon.framework.util.actions.action-result.service',
|
||||
'horizon.framework.conf.resource-type-registry.service'
|
||||
];
|
||||
|
||||
function controller($q, $scope, actionResultService, registry) {
|
||||
var ctrl = this;
|
||||
|
||||
// 'Public' Controller members
|
||||
|
||||
ctrl.resourceType = registry.getResourceType(ctrl.resourceTypeName);
|
||||
ctrl.items = [];
|
||||
ctrl.itemsSrc = [];
|
||||
ctrl.searchFacets = [];
|
||||
ctrl.config = {
|
||||
detailsTemplateUrl: ctrl.resourceType.summaryTemplateUrl,
|
||||
selectAll: true,
|
||||
expand: true,
|
||||
trackId: 'id',
|
||||
searchColumnSpan: 6,
|
||||
actionColumnSpan: 6,
|
||||
columns: ctrl.resourceType.getTableColumns()
|
||||
};
|
||||
ctrl.batchActions = ctrl.resourceType.globalActions
|
||||
.concat(ctrl.resourceType.batchActions);
|
||||
|
||||
ctrl.actionResultHandler = actionResultHandler;
|
||||
|
||||
// Controller Initialization/Loading
|
||||
|
||||
ctrl.resourceType.listFunction().then(onLoad);
|
||||
registry.initActions(ctrl.resourceType.type, $scope);
|
||||
|
||||
// Local functions
|
||||
|
||||
function onLoad(response) {
|
||||
ctrl.itemsSrc = response.data.items;
|
||||
}
|
||||
|
||||
function actionResultHandler(returnValue) {
|
||||
return $q.when(returnValue, actionSuccessHandler);
|
||||
}
|
||||
|
||||
function actionSuccessHandler(result) { // eslint-disable-line no-unused-vars
|
||||
|
||||
// The action has completed (for whatever "complete" means to that
|
||||
// action. Notice the view doesn't really need to know the semantics of the
|
||||
// particular action because the actions return data in a standard form.
|
||||
// That return includes the id and type of each created, updated, deleted
|
||||
// and failed item.
|
||||
//
|
||||
// This handler is also careful to check the type of each item. This
|
||||
// is important because actions which create non-items are launched from
|
||||
// the items page (like create "volume" from image).
|
||||
var deletedIds, updatedIds, createdIds, failedIds;
|
||||
|
||||
if ( result ) {
|
||||
// Reduce the results to just item ids ignoring other types the action
|
||||
// may have produced
|
||||
deletedIds = actionResultService.getIdsOfType(result.deleted, ctrl.resourceType.type);
|
||||
updatedIds = actionResultService.getIdsOfType(result.updated, ctrl.resourceType.type);
|
||||
createdIds = actionResultService.getIdsOfType(result.created, ctrl.resourceType.type);
|
||||
failedIds = actionResultService.getIdsOfType(result.failed, ctrl.resourceType.type);
|
||||
|
||||
// Handle deleted items
|
||||
if (deletedIds.length) {
|
||||
ctrl.itemsSrc = difference(ctrl.itemsSrc, deletedIds,'id');
|
||||
}
|
||||
|
||||
// Handle updated and created items
|
||||
if ( updatedIds.length || createdIds.length ) {
|
||||
// Ideally, get each created item individually, but
|
||||
// this is simple and robust for the common use case.
|
||||
// TODO: If we want more detailed updates, we could do so here.
|
||||
ctrl.resourceType.listFunction().then(onLoad);
|
||||
}
|
||||
|
||||
// Handle failed items
|
||||
if (failedIds.length) {
|
||||
// Do nothing for now. Please note, actions may (and probably
|
||||
// should) provide toast messages when something goes wrong.
|
||||
}
|
||||
|
||||
} else {
|
||||
// promise resolved, but no result returned. Because the action didn't
|
||||
// tell us what happened...reload the displayed items just in case.
|
||||
ctrl.resourceType.listFunction().then(onLoad);
|
||||
}
|
||||
}
|
||||
|
||||
function difference(currentList, otherList, key) {
|
||||
return currentList.filter(filter);
|
||||
|
||||
function filter(elem) {
|
||||
return otherList.filter(function filterDeletedItem(deletedItem) {
|
||||
return deletedItem === elem[key];
|
||||
}).length === 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
})();
|
@ -0,0 +1,122 @@
|
||||
/*
|
||||
* (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.
|
||||
*/
|
||||
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
describe('hz-generic-table controller', function() {
|
||||
var ctrl, listFunctionDeferred, $timeout, actionResultDeferred;
|
||||
|
||||
beforeEach(module('horizon.framework.util'));
|
||||
beforeEach(module('horizon.framework.conf'));
|
||||
beforeEach(module('horizon.framework.widgets.table'));
|
||||
|
||||
var resourceType = {
|
||||
type: 'OS::Test::Example',
|
||||
getTableColumns: angular.noop,
|
||||
listFunction: angular.noop,
|
||||
globalActions: [],
|
||||
batchActions: []
|
||||
};
|
||||
|
||||
beforeEach(inject(function($controller, $q, _$timeout_) {
|
||||
$timeout = _$timeout_;
|
||||
var registry = {
|
||||
getTypeNameBySlug: angular.noop,
|
||||
getResourceType: angular.noop,
|
||||
initActions: angular.noop
|
||||
};
|
||||
|
||||
listFunctionDeferred = $q.defer();
|
||||
actionResultDeferred = $q.defer();
|
||||
spyOn(resourceType, 'listFunction').and.returnValue(listFunctionDeferred.promise);
|
||||
spyOn(registry, 'getResourceType').and.returnValue(resourceType);
|
||||
|
||||
ctrl = $controller('horizon.framework.widgets.table.ResourceTableController', {
|
||||
$scope: {},
|
||||
'horizon.framework.conf.resource-type-registry.service': registry},
|
||||
{resourceTypeName: 'OS::Test::Example'});
|
||||
}));
|
||||
|
||||
it('exists', function() {
|
||||
expect(ctrl).toBeDefined();
|
||||
});
|
||||
|
||||
it('sets itemsSrc to the response data', function() {
|
||||
listFunctionDeferred.resolve({data: {items: [1,2,3]}});
|
||||
$timeout.flush();
|
||||
expect(ctrl.itemsSrc).toEqual([1,2,3]);
|
||||
});
|
||||
|
||||
describe('actionResultHandler', function() {
|
||||
beforeEach(function() {
|
||||
ctrl.itemsSrc = [{type: 'Something', id: -1}, {type: 'OS::Test::Example', id: 1}];
|
||||
});
|
||||
|
||||
it('handles deleted items', function() {
|
||||
actionResultDeferred.resolve({deleted: [{type: 'ignored', id: 0},
|
||||
{type: 'OS::Test::Example', id: 1}]});
|
||||
var promise = ctrl.actionResultHandler(actionResultDeferred.promise);
|
||||
promise.then(function() {
|
||||
expect(ctrl.itemsSrc).toEqual([{type: 'Something', id: -1}]);
|
||||
});
|
||||
$timeout.flush();
|
||||
});
|
||||
|
||||
it('handles updated items', function() {
|
||||
actionResultDeferred.resolve({updated: [{type: 'OS::Test::Example', id: 1}]});
|
||||
var promise = ctrl.actionResultHandler(actionResultDeferred.promise);
|
||||
resourceType.listFunction.calls.reset();
|
||||
promise.then(function() {
|
||||
expect(resourceType.listFunction).toHaveBeenCalled();
|
||||
});
|
||||
$timeout.flush();
|
||||
});
|
||||
|
||||
it('handles created items', function() {
|
||||
actionResultDeferred.resolve({created: [{type: 'OS::Test::Example', id: 1}]});
|
||||
var promise = ctrl.actionResultHandler(actionResultDeferred.promise);
|
||||
resourceType.listFunction.calls.reset();
|
||||
promise.then(function() {
|
||||
expect(resourceType.listFunction).toHaveBeenCalled();
|
||||
});
|
||||
$timeout.flush();
|
||||
});
|
||||
|
||||
it('handles failed items', function() {
|
||||
actionResultDeferred.resolve({failed: [{type: 'OS::Test::Example', id: 1}]});
|
||||
var promise = ctrl.actionResultHandler(actionResultDeferred.promise);
|
||||
resourceType.listFunction.calls.reset();
|
||||
promise.then(function() {
|
||||
expect(resourceType.listFunction).not.toHaveBeenCalled();
|
||||
});
|
||||
$timeout.flush();
|
||||
});
|
||||
|
||||
it('handles falsy results', function() {
|
||||
actionResultDeferred.resolve(false);
|
||||
var promise = ctrl.actionResultHandler(actionResultDeferred.promise);
|
||||
resourceType.listFunction.calls.reset();
|
||||
promise.then(function() {
|
||||
expect(resourceType.listFunction).toHaveBeenCalled();
|
||||
});
|
||||
$timeout.flush();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
})();
|
@ -0,0 +1,59 @@
|
||||
/**
|
||||
* (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.table')
|
||||
.directive('hzResourceTable', directive);
|
||||
|
||||
directive.$inject = ['horizon.framework.widgets.basePath'];
|
||||
|
||||
/**
|
||||
* @ngdoc directive
|
||||
* @name hzResourceTable
|
||||
* @description
|
||||
* This directive produces a table and accompanying components that describe
|
||||
* a list of resources of the given type. Based on information in the
|
||||
* registry, the batch, global, and item-level actions are presented as
|
||||
* appropriate. Search capabilities are also provided. The table contents
|
||||
* are responsive to actions' promise resolutions, updating contents when
|
||||
* they are likely to have changed. This directive allows for the rapid
|
||||
* development of standard resource tables without having to rewrite
|
||||
* boilerplate controllers, markup, etc.
|
||||
* @example
|
||||
```
|
||||
<div>Here's some content above the table.</div>
|
||||
<hz-resource-table resource-type-name="OS::Cinder::Volume"></hz-resource-table>
|
||||
<div>Here's some content below the table.</div>
|
||||
```
|
||||
*/
|
||||
|
||||
function directive(basePath) {
|
||||
|
||||
var directive = {
|
||||
restrict: 'E',
|
||||
scope: {
|
||||
resourceTypeName: '@'
|
||||
},
|
||||
bindToController: true,
|
||||
templateUrl: basePath + 'table/hz-resource-table.html',
|
||||
controller: "horizon.framework.widgets.table.ResourceTableController as ctrl"
|
||||
};
|
||||
|
||||
return directive;
|
||||
}
|
||||
})();
|
@ -0,0 +1,10 @@
|
||||
<hz-dynamic-table
|
||||
table="ctrl"
|
||||
config="ctrl.config"
|
||||
items="ctrl.itemsSrc"
|
||||
item-actions="ctrl.resourceType.itemActions"
|
||||
client-full-text-search="true"
|
||||
batch-actions="ctrl.batchActions"
|
||||
filter-facets="ctrl.searchFacets"
|
||||
result-handler="ctrl.actionResultHandler"
|
||||
></hz-dynamic-table>
|
@ -26,6 +26,7 @@
|
||||
'horizon.framework.widgets.table',
|
||||
'horizon.framework.widgets.modal',
|
||||
'horizon.framework.widgets.modal-wait-spinner',
|
||||
'horizon.framework.widgets.panel',
|
||||
'horizon.framework.widgets.transfer-table',
|
||||
'horizon.framework.widgets.charts',
|
||||
'horizon.framework.widgets.action-list',
|
||||
|
14
releasenotes/notes/resource-directives-44629f1116545141.yaml
Normal file
14
releasenotes/notes/resource-directives-44629f1116545141.yaml
Normal file
@ -0,0 +1,14 @@
|
||||
---
|
||||
prelude: >
|
||||
Angular components now exist to provide simple-to-
|
||||
configure panels and tables, based off of registry
|
||||
information about resources (e.g. Instances).
|
||||
features:
|
||||
- The hz-resource-table directive takes in a Heat
|
||||
resource name (e.g. 'OS::Nova::Server') and uses
|
||||
the Angular registry to provide actions, columns,
|
||||
and summary views.
|
||||
- The hz-resource-panel directive takes in a Heat
|
||||
resource name (e.g. 'OS::Nova::Server') and
|
||||
displays an appropriate header and allows content
|
||||
to be transcluded to build the panel page.
|
Loading…
Reference in New Issue
Block a user