summaryrefslogtreecommitdiff
path: root/horizon/static
diff options
context:
space:
mode:
authorTyr Johanson <tyr@hp.com>2016-07-22 16:53:49 -0600
committerTyr Johanson <tyr@hp.com>2016-08-03 12:07:20 -0600
commit7339641fac40787dee8474be5cd6939291d55b0d (patch)
treee0d16df3b2c7c7c21c7e411491c02f66ce2e2b50 /horizon/static
parentf9ba1ecc2f47a2b3fd2816dfba62f8ab9eaf343f (diff)
downloadhorizon-7339641fac40787dee8474be5cd6939291d55b0d.tar.gz
Allow listFunction extra params
--- Includes numerous bug fixes to hz-dynamic-table and friends to be responsive to dynamic table configurations or trackBy expressions. --- Some resource types (like subnets) can only be listed for a given parent container (like networks). This patch modifies the hz-resource-table to allow a user to pass in extra paramters that will be passed to the list function for a given resource type. For example usage: See DNSaaS https://review.openstack.org/#/c/341182/9/designatedashboard/static/designatedashboard/resources/os-designate-recordset/details/zone-recordsets.html For example: This allows hz-resource-table to be used to show a list of subnets as one of the details views for a network. The details view controller passes in the current network ID as an extra paramter to the hz-resource-table, which in turn, supplies that value to the subnets list function. The subnets list function can use the parameters it is given to build the appropriate API call. This is very similar to what already exists to pass server search parameters from magic search to the API in the angular images table. Change-Id: I90c851aef0a452e4e8ef39e938ac3ca67a93cfac Partially-Implements: blueprint angular-registry Closes-Bug: 1608462
Diffstat (limited to 'horizon/static')
-rw-r--r--horizon/static/framework/widgets/magic-search/hz-magic-search-context.directive.js17
-rw-r--r--horizon/static/framework/widgets/table/hz-detail-row.directive.js27
-rw-r--r--horizon/static/framework/widgets/table/hz-dynamic-table.controller.js90
-rw-r--r--horizon/static/framework/widgets/table/hz-dynamic-table.directive.js56
-rw-r--r--horizon/static/framework/widgets/table/hz-dynamic-table.html6
-rw-r--r--horizon/static/framework/widgets/table/hz-resource-table.controller.js94
-rw-r--r--horizon/static/framework/widgets/table/hz-resource-table.controller.spec.js147
-rw-r--r--horizon/static/framework/widgets/table/hz-resource-table.directive.js21
-rw-r--r--horizon/static/framework/widgets/table/hz-table.directive.js11
9 files changed, 340 insertions, 129 deletions
diff --git a/horizon/static/framework/widgets/magic-search/hz-magic-search-context.directive.js b/horizon/static/framework/widgets/magic-search/hz-magic-search-context.directive.js
index 6fe361e3d..cf4630116 100644
--- a/horizon/static/framework/widgets/magic-search/hz-magic-search-context.directive.js
+++ b/horizon/static/framework/widgets/magic-search/hz-magic-search-context.directive.js
@@ -92,10 +92,8 @@
function link(scope, element, attrs) {
var filterStrings = $parse(attrs.filterStrings)(scope);
- var filterFacets = $parse(attrs.filterFacets)(scope);
var clientFullTextSearch = $parse(attrs.clientFullTextSearch)(scope);
var searchSettingsCallback = $parse(attrs.searchSettingsCallback)(scope);
- scope.filterFacets = filterFacets;
scope.searchSettingsCallback = searchSettingsCallback;
scope.clientFullTextSearch = angular.isDefined(clientFullTextSearch)
@@ -124,6 +122,21 @@
scope.$on(magicSearchEvents.FACETS_CHANGED, resend);
scope.$on(magicSearchEvents.SERVER_SEARCH_UPDATED, resend);
+ // This directive doesn't use an isolate scope because it is used to wrap magic-search which
+ // doesn't take all data as attributes (yet). Until it does, in order to make changes
+ // to the 'filter-facets' of this directive visible to a child magic-search, we
+ // explicitly watch whatever value the parent set as the 'filter-facets' attribute on this
+ // directive, and set that to 'scope.filterFacets' for any children to use.
+ //
+ // For example, if the parent sets filter-facets='ctrl.myFilters', then this watch
+ // is equivalent to:
+ // scope.filterFacets = scope.ctrl.myFilters
+ if (angular.isUndefined(scope.filterFacets)) {
+ scope.$watch(attrs.filterFacets, function (newValue) {
+ scope.filterFacets = newValue;
+ });
+ }
+
function resend(event, data) {
scope.$broadcast(event.name + '-ms-context', data);
}
diff --git a/horizon/static/framework/widgets/table/hz-detail-row.directive.js b/horizon/static/framework/widgets/table/hz-detail-row.directive.js
index 7ae9f1283..4fbb1151b 100644
--- a/horizon/static/framework/widgets/table/hz-detail-row.directive.js
+++ b/horizon/static/framework/widgets/table/hz-detail-row.directive.js
@@ -23,7 +23,6 @@
hzDetailRow.$inject = ['horizon.framework.widgets.basePath',
'$http',
'$compile',
- '$parse',
'$templateCache'];
/**
@@ -33,7 +32,7 @@
* The `hzDetailRow` directive is the detail drawer per each row triggered by
* the hzExpandDetail. Use this option for customization and complete control over what
* is rendered. If a custom template is not provided, it will use the template
- * found at hz-detail-row.html. 'config.columns' and 'item' must be provided for the
+ * found at hz-detail-row.html. 'config.columns' 'table' and 'item' must be provided for the
* default template to work. See example below.
*
* It should ideally be used within the context of the `hz-dynamic-table` directive.
@@ -72,7 +71,7 @@
* ```
*
*/
- function hzDetailRow(basePath, $http, $compile, $parse, $templateCache) {
+ function hzDetailRow(basePath, $http, $compile, $templateCache) {
var directive = {
restrict: 'E',
@@ -83,15 +82,19 @@
return directive;
function link(scope, element, attrs) {
- var templateUrl = $parse(attrs.templateUrl)(scope);
- if (!templateUrl) {
- templateUrl = basePath + 'table/hz-detail-row.html';
- }
- $http.get(templateUrl, { cache: $templateCache })
- .then(function(response) {
- var template = response.data;
- element.append($compile(template)(scope));
- });
+ // Watch for changes to the template URL to allow the template to change
+ // at run-time, or to support late binding where we display part of the
+ // table before knowing the type of each row
+ scope.$watch(attrs.templateUrl, function(templateUrl) {
+ if (!templateUrl) {
+ templateUrl = basePath + 'table/hz-detail-row.html';
+ }
+ $http.get(templateUrl, { cache: $templateCache })
+ .then(function(response) {
+ var template = response.data;
+ element.append($compile(template)(scope));
+ });
+ });
}
}
})();
diff --git a/horizon/static/framework/widgets/table/hz-dynamic-table.controller.js b/horizon/static/framework/widgets/table/hz-dynamic-table.controller.js
new file mode 100644
index 000000000..a50e261fc
--- /dev/null
+++ b/horizon/static/framework/widgets/table/hz-dynamic-table.controller.js
@@ -0,0 +1,90 @@
+/*
+ * (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.HzDynamicTableController', controller);
+
+ controller.$inject = [
+ '$scope',
+ 'horizon.framework.conf.permissions.service'
+ ];
+
+ function controller($scope, permissionsService) {
+ // For now, NOT using controller as syntax. See directive definition
+ $scope.items = [];
+ $scope.columnAllowed = columnAllowed;
+
+ var allowedColumns = {};
+
+ // Deep watch for changes to the table config
+ $scope.$watch(
+ "config",
+ onConfigChange,
+ true
+ );
+
+ // Local functions
+
+ /**
+ * Handle changes to the table config.
+ *
+ * @param newValue {string}
+ * new resource type name
+ */
+ function onConfigChange (newConfig) {
+ if (angular.isDefined(newConfig)) {
+
+ // if selectAll and expand are not set in the config, default set to true
+ if (angular.isUndefined(newConfig.selectAll)) {
+ newConfig.selectAll = true;
+ }
+ if (angular.isUndefined(newConfig.expand)) {
+ newConfig.expand = true;
+ }
+
+ // Check permissions on the columns
+ allowedColumns = {};
+ angular.forEach(newConfig.columns, checkPermissions);
+ }
+ }
+
+ function checkPermissions(column) {
+ permissionsService.checkAll(column).then(allow, disallow);
+
+ function allow() {
+ allowedColumns[column.id] = true;
+ }
+
+ function disallow() {
+ allowedColumns[column.id] = false;
+ }
+ }
+
+ /**
+ * Returns true if a column is allowed to be shown. Default is false.
+ *
+ * @param column - The column to check
+ * @returns {*|boolean}
+ */
+ function columnAllowed(column) {
+ return allowedColumns[column.id] || false;
+ }
+ }
+
+})();
diff --git a/horizon/static/framework/widgets/table/hz-dynamic-table.directive.js b/horizon/static/framework/widgets/table/hz-dynamic-table.directive.js
index 3de351819..757d418a3 100644
--- a/horizon/static/framework/widgets/table/hz-dynamic-table.directive.js
+++ b/horizon/static/framework/widgets/table/hz-dynamic-table.directive.js
@@ -21,8 +21,7 @@
.directive('hzDynamicTable', hzDynamicTable);
hzDynamicTable.$inject = [
- 'horizon.framework.widgets.basePath',
- 'horizon.framework.conf.permissions.service'
+ 'horizon.framework.widgets.basePath'
];
/**
@@ -102,13 +101,17 @@
* ```
*
*/
- function hzDynamicTable(basePath, permissionsService) {
+ function hzDynamicTable(basePath) {
// <r1chardj0n3s>: there are some configuration items which are on the directive,
// and some on the "config" attribute of the directive. Those latter configuration
// items will be effectively "static" for the lifespan of the directive whereas
// angular will watch directive attributes for changes. This should be revisited
// at some point to make sure the split we've actually got here makes sense.
+
+ // TODO (tyr) In Ocata, convert to "controller as" syntax.
+ // This was not done in Mitaka to avoid breaking any hz-detail-row templates that
+ // assume table attributes are available directly on inherited scope.
var directive = {
restrict: 'E',
scope: {
@@ -120,53 +123,10 @@
filterFacets: '=?',
resultHandler: '=?'
},
- templateUrl: basePath + 'table/hz-dynamic-table.html',
- link: {
- pre: preLink,
- post: postLink
- }
+ controller: 'horizon.framework.widgets.table.HzDynamicTableController',
+ templateUrl: basePath + 'table/hz-dynamic-table.html'
};
return directive;
-
- function preLink(scope) {
- //Isolate config changes we do here from propagating out.
- scope.config = angular.copy(scope.config);
- scope.items = [];
- }
-
- function postLink(scope) {
- // if selectAll and expand are not set in the config, default set to true
- if (angular.isUndefined(scope.config.selectAll)) {
- scope.config.selectAll = true;
- }
- if (angular.isUndefined(scope.config.expand)) {
- scope.config.expand = true;
- }
-
- setColumnPermitted(scope.config.columns);
- }
-
- function setColumnPermitted(columns) {
-
- angular.forEach(columns, checkPermissions);
-
- function checkPermissions(column) {
- if (column.permitted === true || column.permitted === false) {
- // No need to check again
- return;
- } else {
- permissionsService.checkAll(column).then(allow, disallow);
- }
-
- function allow() {
- column.permitted = true;
- }
-
- function disallow() {
- column.permitted = false;
- }
- }
- }
}
})();
diff --git a/horizon/static/framework/widgets/table/hz-dynamic-table.html b/horizon/static/framework/widgets/table/hz-dynamic-table.html
index 7a7a2333d..a90b8a715 100644
--- a/horizon/static/framework/widgets/table/hz-dynamic-table.html
+++ b/horizon/static/framework/widgets/table/hz-dynamic-table.html
@@ -3,7 +3,7 @@
-->
<hz-magic-search-context filter-facets="filterFacets">
<div hz-table
- track-rows-by="{$ config.trackId $}"
+ track-rows-by="config.trackId"
ng-cloak
st-magic-search
st-table="items"
@@ -39,7 +39,7 @@
st-sort="{$ column.id $}"
ng-attr-st-sort-default="{$ column.sortDefault $}"
translate
- ng-if="column.permitted">
+ ng-if="columnAllowed(column)">
{$ column.title $}
</th>
<th ng-if="itemActions"></th>
@@ -71,7 +71,7 @@
</td>
<td ng-repeat="column in config.columns"
class="rsp-p{$ column.priority $}"
- ng-if="column.permitted">
+ ng-if="columnAllowed(column)">
<hz-cell table="table" column="column" item="item"></hz-cell>
</td>
<td ng-if="itemActions" class="actions_column">
diff --git a/horizon/static/framework/widgets/table/hz-resource-table.controller.js b/horizon/static/framework/widgets/table/hz-resource-table.controller.js
index b93209f16..136466920 100644
--- a/horizon/static/framework/widgets/table/hz-resource-table.controller.js
+++ b/horizon/static/framework/widgets/table/hz-resource-table.controller.js
@@ -31,38 +31,90 @@
function controller($q, $scope, events, searchService, actionResultService, registry) {
var ctrl = this;
+ var lastSearchQuery = {};
// 'Public' Controller members
-
- ctrl.resourceType = registry.getResourceType(ctrl.resourceTypeName);
+ ctrl.actionResultHandler = actionResultHandler;
+ ctrl.searchFacets = [];
+ ctrl.config = {};
+ ctrl.batchActions = [];
ctrl.items = [];
ctrl.itemsSrc = [];
- ctrl.searchFacets = ctrl.resourceType.filterFacets;
- ctrl.config = {
- detailsTemplateUrl: ctrl.resourceType.summaryTemplateUrl,
- selectAll: true,
- expand: ctrl.resourceType.summaryTemplateUrl,
- trackId: ctrl.trackBy || 'id',
- columns: ctrl.resourceType.getTableColumns()
- };
- ctrl.batchActions = ctrl.resourceType.globalActions
- .concat(ctrl.resourceType.batchActions);
- ctrl.actionResultHandler = actionResultHandler;
+ // Watch for changes to search bar
+ $scope.$on(events.SERVER_SEARCH_UPDATED, handleServerSearch);
- // Controller Initialization/Loading
+ // Watch for changes to resourceTypeName
+ $scope.$watch(
+ "ctrl.resourceTypeName",
+ onResourceTypeNameChange
+ );
- ctrl.resourceType.list().then(onLoad);
- ctrl.resourceType.initActions($scope);
- $scope.$on(events.SERVER_SEARCH_UPDATED, handleServerSearch);
+ // Watch for changes to listFunctionExtraParams
+ $scope.$watch(
+ "ctrl.listFunctionExtraParams",
+ onListFunctionExtraParamsChange
+ );
// Local functions
+ /**
+ * Handle changes to resource type name
+ *
+ * @param newValue {string}
+ * new resource type name
+ */
+ function onResourceTypeNameChange (newValue) {
+ if (angular.isDefined(newValue)) {
+ ctrl.resourceType = registry.getResourceType(newValue);
+ ctrl.resourceType.initActions($scope);
+ ctrl.searchFacets = ctrl.resourceType.filterFacets;
+ ctrl.config = {
+ detailsTemplateUrl: ctrl.resourceType.summaryTemplateUrl,
+ selectAll: true,
+ expand: ctrl.resourceType.summaryTemplateUrl,
+ trackId: ctrl.trackBy || 'id',
+ columns: ctrl.resourceType.getTableColumns()
+ };
+ ctrl.batchActions = ctrl.resourceType.globalActions
+ .concat(ctrl.resourceType.batchActions);
+ listResources();
+ }
+ }
+
+ /**
+ * Handle changes to list function extra params
+ *
+ * @param newValue {object}
+ * new list function extra params
+ */
+ function onListFunctionExtraParamsChange (newValue) {
+ if (angular.isDefined(newValue)) {
+ listResources();
+ }
+ }
+
+ /**
+ * If a resource type has been set, list all resources for this resource type.
+ * In the call to the list function, include the current search terms (if any)
+ * and any extra list function params supplied by the parent (if any).
+ */
+ function listResources() {
+ if (ctrl.resourceType) {
+ ctrl.resourceType
+ .list(angular.extend({}, lastSearchQuery, ctrl.listFunctionExtraParams))
+ .then(onLoad);
+ }
+ }
+
function handleServerSearch(evt, magicSearchQueryObj) {
- var params = searchService
+ // Save the current search. We will use this if an action requires we re-list
+ // resources, but still respect the current search terms.
+ lastSearchQuery = searchService
.getSearchTermsFromQueryString(magicSearchQueryObj.magicSearchQuery)
.reduce(queryToObject, {});
- ctrl.resourceType.list(params).then(onLoad);
+
+ listResources();
function queryToObject(orig, curr) {
var fields = searchService.getSearchTermObject(curr);
@@ -110,7 +162,7 @@
// 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.list().then(onLoad);
+ listResources();
}
// Handle failed items
@@ -122,7 +174,7 @@
} 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.list().then(onLoad);
+ listResources();
}
}
diff --git a/horizon/static/framework/widgets/table/hz-resource-table.controller.spec.js b/horizon/static/framework/widgets/table/hz-resource-table.controller.spec.js
index fef83cbcd..51da3b530 100644
--- a/horizon/static/framework/widgets/table/hz-resource-table.controller.spec.js
+++ b/horizon/static/framework/widgets/table/hz-resource-table.controller.spec.js
@@ -18,7 +18,7 @@
'use strict';
describe('hz-generic-table controller', function() {
- var ctrl, listFunctionDeferred, $timeout, actionResultDeferred, $scope;
+ var ctrl, listFunctionDeferred, actionResultDeferred, $scope;
beforeEach(module('horizon.framework.util'));
beforeEach(module('horizon.framework.conf'));
@@ -34,12 +34,8 @@
batchActions: []
};
- $scope = {
- $on: angular.noop
- };
-
- beforeEach(inject(function($controller, $q, _$timeout_) {
- $timeout = _$timeout_;
+ beforeEach(inject(function($rootScope, $controller, $q) {
+ $scope = $rootScope.$new();
var registry = {
getTypeNameBySlug: angular.noop,
getResourceType: angular.noop
@@ -49,12 +45,13 @@
actionResultDeferred = $q.defer();
spyOn(resourceType, 'list').and.returnValue(listFunctionDeferred.promise);
spyOn(registry, 'getResourceType').and.returnValue(resourceType);
- spyOn($scope, '$on');
ctrl = $controller('horizon.framework.widgets.table.ResourceTableController', {
$scope: $scope,
'horizon.framework.conf.resource-type-registry.service': registry},
{resourceTypeName: 'OS::Test::Example'});
+ $scope.ctrl = ctrl;
+ $scope.$apply();
}));
it('exists', function() {
@@ -63,23 +60,107 @@
it('sets itemsSrc to the response data', function() {
listFunctionDeferred.resolve({data: {items: [1,2,3]}});
- $timeout.flush();
+ $scope.$apply();
expect(ctrl.itemsSrc).toEqual([1,2,3]);
});
describe('server search handler', function() {
- it('returns the correct value from its function', function() {
- var func = $scope.$on.calls.argsFor(0)[1];
+ var events;
+ beforeEach(inject(function($injector) {
+ events = $injector.get('horizon.framework.widgets.magic-search.events');
+ }));
+
+ it('passes search parameters to the list function', function() {
var input = {
magicSearchQuery: "name=happy&age=100&height=72"
};
- func('', input);
- expect(ctrl.resourceType.list)
+ resourceType.list.calls.reset();
+ $scope.$broadcast(events.SERVER_SEARCH_UPDATED, input);
+ expect(resourceType.list)
.toHaveBeenCalledWith({name: 'happy', age: '100', height: '72'});
});
});
+ describe('data watchers', function() {
+
+ var events;
+ beforeEach(inject(function($injector) {
+ events = $injector.get('horizon.framework.widgets.magic-search.events');
+ }));
+
+ it('lists resources on resourceTypeName change', function() {
+ delete ctrl.resourceType;
+ ctrl.resourceTypeName = 'Test::FooBar';
+ $scope.$apply();
+ expect(ctrl.resourceType).toEqual(resourceType);
+ });
+
+ it('does not list resources on resourceTypeName undefined', function() {
+ ctrl.resourceTypeName = 'Test::FooBar';
+ $scope.$apply();
+ ctrl.resourceType.list.calls.reset();
+ ctrl.resourceTypeName = undefined;
+ $scope.$apply();
+ expect(resourceType.list.calls.count()).toBe(0);
+ });
+
+ it('lists resources on listFunctionExtraParams change', function() {
+ ctrl.listFunctionExtraParams = {data: 'foobar'};
+ resourceType.list.calls.reset();
+ $scope.$apply();
+ expect(resourceType.list).toHaveBeenCalled();
+ });
+
+ it('does not list resources on listFunctionExtraParams undefined', function() {
+ ctrl.listFunctionExtraParams = {data: 'foobar'};
+ $scope.$apply();
+ ctrl.listFunctionExtraParams = undefined;
+ resourceType.list.calls.reset();
+ $scope.$apply();
+ expect(resourceType.list.calls.count()).toBe(0);
+ });
+
+ it('does not list resources on listFunctionExtraParams change ' +
+ 'if resourceType undefined', function() {
+ delete ctrl.resourceType;
+ ctrl.listFunctionExtraParams = {data: 'foobar'};
+ resourceType.list.calls.reset();
+ $scope.$apply();
+ expect(resourceType.list.calls.count()).toBe(0);
+ });
+
+ it('passes listFunctionExtraParams to list function', function() {
+ ctrl.listFunctionExtraParams = {data: 'foobar'};
+ resourceType.list.calls.reset();
+ $scope.$apply();
+ expect(resourceType.list).toHaveBeenCalledWith({data: 'foobar'});
+ });
+
+ it('merges listfunctionExtraParams with new search query', function() {
+ ctrl.listFunctionExtraParams = {data: 'foobar'};
+ var input = {
+ magicSearchQuery: "name=happy&age=100&height=72"
+ };
+ resourceType.list.calls.reset();
+ $scope.$broadcast(events.SERVER_SEARCH_UPDATED, input);
+ expect(resourceType.list)
+ .toHaveBeenCalledWith({name: 'happy', age: '100', height: '72', data: 'foobar'});
+ });
+
+ it('merges listFunctionExtraParams with prior search query', function() {
+ var input = {
+ magicSearchQuery: "name=happy&age=100&height=72"
+ };
+ $scope.$broadcast(events.SERVER_SEARCH_UPDATED, input);
+ resourceType.list.calls.reset();
+ ctrl.listFunctionExtraParams = {data: 'foobar'};
+ $scope.$apply();
+ expect(resourceType.list)
+ .toHaveBeenCalledWith({name: 'happy', age: '100', height: '72', data: 'foobar'});
+ });
+ });
+
describe('actionResultHandler', function() {
beforeEach(function() {
ctrl.itemsSrc = [{type: 'Something', id: -1}, {type: 'OS::Test::Example', id: 1}];
@@ -88,51 +169,41 @@
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();
+ ctrl.actionResultHandler(actionResultDeferred.promise);
+ $scope.$apply();
+ expect(ctrl.itemsSrc).toEqual([{type: 'Something', id: -1}]);
});
it('handles updated items', function() {
actionResultDeferred.resolve({updated: [{type: 'OS::Test::Example', id: 1}]});
- var promise = ctrl.actionResultHandler(actionResultDeferred.promise);
+ ctrl.actionResultHandler(actionResultDeferred.promise);
resourceType.list.calls.reset();
- promise.then(function() {
- expect(resourceType.list).toHaveBeenCalled();
- });
- $timeout.flush();
+ $scope.$apply();
+ expect(resourceType.list).toHaveBeenCalled();
});
it('handles created items', function() {
actionResultDeferred.resolve({created: [{type: 'OS::Test::Example', id: 1}]});
- var promise = ctrl.actionResultHandler(actionResultDeferred.promise);
+ ctrl.actionResultHandler(actionResultDeferred.promise);
resourceType.list.calls.reset();
- promise.then(function() {
- expect(resourceType.list).toHaveBeenCalled();
- });
- $timeout.flush();
+ $scope.$apply();
+ expect(resourceType.list).toHaveBeenCalled();
});
it('handles failed items', function() {
actionResultDeferred.resolve({failed: [{type: 'OS::Test::Example', id: 1}]});
- var promise = ctrl.actionResultHandler(actionResultDeferred.promise);
+ ctrl.actionResultHandler(actionResultDeferred.promise);
resourceType.list.calls.reset();
- promise.then(function() {
- expect(resourceType.list).not.toHaveBeenCalled();
- });
- $timeout.flush();
+ $scope.$apply();
+ expect(resourceType.list).not.toHaveBeenCalled();
});
it('handles falsy results', function() {
actionResultDeferred.resolve(false);
- var promise = ctrl.actionResultHandler(actionResultDeferred.promise);
+ ctrl.actionResultHandler(actionResultDeferred.promise);
resourceType.list.calls.reset();
- promise.then(function() {
- expect(resourceType.list).toHaveBeenCalled();
- });
- $timeout.flush();
+ $scope.$apply();
+ expect(resourceType.list).toHaveBeenCalled();
});
});
diff --git a/horizon/static/framework/widgets/table/hz-resource-table.directive.js b/horizon/static/framework/widgets/table/hz-resource-table.directive.js
index 8be48463d..e726c02c2 100644
--- a/horizon/static/framework/widgets/table/hz-resource-table.directive.js
+++ b/horizon/static/framework/widgets/table/hz-resource-table.directive.js
@@ -24,6 +24,7 @@
/**
* @ngdoc directive
+ * @scope
* @name hzResourceTable
* @description
* This directive produces a table and accompanying components that describe
@@ -34,10 +35,25 @@
* 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.
+ *
+ * @property resource-type-name {string}
+ * The resource name in the registry
+ *
+ * @property track-by {string} (optional)
+ * The track-by string to pass to the hz-generic-table
+ *
+ * @property list-function-extra-params {object} (optional)
+ * Extra parameters required by this resource type's list function.
+ * For example, if the list function requires a parent container ID.
+ *
* @example
```
<div>Here's some content above the table.</div>
- <hz-resource-table resource-type-name="OS::Cinder::Volume"></hz-resource-table>
+ <hz-resource-table
+ resource-type-name="OS::Cinder::Volume"
+ track-by="updated_at"
+ list-function-extra-params="{region: 1234}">
+ </hz-resource-table>
<div>Here's some content below the table.</div>
```
*/
@@ -48,7 +64,8 @@
restrict: 'E',
scope: {
resourceTypeName: '@',
- trackBy: '@?'
+ trackBy: '@?',
+ listFunctionExtraParams: '=?'
},
bindToController: true,
templateUrl: basePath + 'table/hz-resource-table.html',
diff --git a/horizon/static/framework/widgets/table/hz-table.directive.js b/horizon/static/framework/widgets/table/hz-table.directive.js
index e49db0cd6..22c4a2c73 100644
--- a/horizon/static/framework/widgets/table/hz-table.directive.js
+++ b/horizon/static/framework/widgets/table/hz-table.directive.js
@@ -78,9 +78,14 @@
///////////////////
function link(scope, element, attrs) {
- if (attrs.trackRowsBy) {
- scope.tCtrl.trackId = attrs.trackRowsBy;
- }
+ // By default, TableController sets trackId to 'id'. Watch the
+ // track-rows-by attribute in case the parent wants to track items
+ // using a different key either at binding or at run-time.
+ scope.$watch(attrs.trackRowsBy, function(newValue) {
+ if (angular.isDefined(newValue)) {
+ scope.tCtrl.trackId = newValue;
+ }
+ });
}
}
})();