Basic node action hooks.

This patch adds action handling for power and provision state
changes in the node list view, via the NodeActionController. Or,
in simpler terms, if you select a node and an action, a modal will
now pop up to tell you that particular action is not yet supported.
Actions will be added in subsequent patches.

The NodeActionController itself has been greatly simplified,
because it needs to serve as both a single-node and a multi-node
action handler; in other words, the previous opinionated
implementation of one-handler-per-node has been genericized.

Change-Id: I77b441d84a1b92d359879ea99b38804decaae9e9
This commit is contained in:
Michael Krotscheck 2016-03-31 15:19:26 -07:00
parent 8eff5be4cd
commit ef21904d32
6 changed files with 252 additions and 63 deletions

View File

@ -0,0 +1,30 @@
/*
* Copyright (c) 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.
*/
/**
* This controller handles the popup that informs the user that we either do not know, or cannot
* perform, the node action they've requested.
*/
angular.module('ironic').controller('UnknownActionModalController',
function($scope, $uibModalInstance, actionName) {
'use strict';
var vm = this;
vm.actionName = actionName;
vm.close = function() {
$uibModalInstance.dismiss();
};
});

View File

@ -18,35 +18,46 @@
* Controller which handles manipulation of individual nodes.
*/
angular.module('ironic').controller('NodeActionController',
function($uibModal, $q) {
function($uibModal) {
'use strict';
var vm = this;
// Set up controller parameters
vm.errorMessage = null;
vm.node = null;
/**
* Initialize this controller with a specific node.
* Generic unknown action handler, here as a placeholder until we get actual actions built out.
*
* @param {IronicNode} node The node to initialize this controller with.
* @return {void}
* @param {String} actionName The name of the action to perform.
* @param {[{}]} nodes An array of nodes on which to perform this action.
* @returns {Promise} A promise that resolves when the user performs the action.
*/
vm.init = function(node) {
vm.node = node;
};
function unknownActionHandler (actionName, nodes) {
return $uibModal.open({
templateUrl: 'view/ironic/action/unknown.html',
controller: 'UnknownActionModalController as ctrl',
resolve: {
actionName: function() {
return actionName;
},
nodes: function() {
return nodes;
}
}
}).result;
}
vm.powerAction = unknownActionHandler;
vm.provisionAction = unknownActionHandler;
/**
* Delete the node in this controller.
*
* @param {IronicNode} node The node instance to remove.
* @return {Promise} A promise that will resolve true if the modal closed with some deletions.
*/
vm.remove = function() {
if (vm.node === null) {
// init() not called, or called with invalid value.
return $q.reject();
}
vm.remove = function(node) {
// Return the result of the modal.
return $uibModal.open({
@ -55,7 +66,7 @@ angular.module('ironic').controller('NodeActionController',
backdrop: 'static',
resolve: {
nodes: function() {
return [vm.node];
return [node];
}
}
}).result;

View File

@ -0,0 +1,16 @@
<div class="modal-header">
<button type="button"
class="close"
ng-click="ctrl.close()">&times;</button>
<h3 class="panel-title">Unsupported Action</h3>
</div>
<div class="modal-body">
<div class="row">
<div class="col-xs-12">
<p>The action "{{ctrl.actionName | capitalize}}" is not yet supported.</p>
</div>
</div>
</div>
<div class="modal-footer ng-scope">
<button class="btn btn-default" ng-click="ctrl.close()">Cancel</button>
</div>

View File

@ -1,9 +1,11 @@
<div class="row padding-top">
<div class="col-xs-12">
<div class="col-xs-12"
ng-controller="NodeActionController as actionCtrl">
<div class="btn-group" uib-dropdown>
<button type="button"
class="btn btn-default"
ng-disabled="nodeListCtrl.selectedNodes.length == 0">
ng-disabled="nodeListCtrl.selectedNodes.length == 0"
ng-click="actionCtrl.powerAction(nodeListCtrl.powerTransitions[0], nodeListCtrl.selectedNodes)">
{{nodeListCtrl.powerTransitions[0] | capitalize}}
</button>
<button type="button"
@ -15,6 +17,7 @@
<ul uib-dropdown-menu>
<li ng-repeat="event in nodeListCtrl.powerTransitions">
<a ng-disabled="nodeListCtrl.selectedNodes.length == 0"
ng-click="actionCtrl.powerAction(event, nodeListCtrl.selectedNodes)"
href="">
{{event | capitalize}}
</a>
@ -22,6 +25,7 @@
<li role="separator" class="divider"></li>
<li ng-repeat="event in nodeListCtrl.provisionTransitions">
<a ng-disabled="nodeListCtrl.selectedNodes.length == 0"
ng-click="actionCtrl.provisionAction(event, nodeListCtrl.selectedNodes)"
href="">
{{event | capitalize}}
</a>
@ -29,7 +33,8 @@
</ul>
</div>
<button type="button" class="btn btn-default"
ng-disabled="nodeListCtrl.selectedNodes.length == 0">
ng-disabled="nodeListCtrl.selectedNodes.length == 0"
ng-click="actionCtrl.powerAction('reboot', nodeListCtrl.selectedNodes)">
Reboot
</button>
<button type="button" class="btn btn-default"
@ -58,20 +63,18 @@
</tr>
</thead>
<tbody ng-if="nodeListCtrl.nodes.$resolved && nodeListCtrl.nodes.length > 0">
<tr ng-repeat="node in nodeListCtrl.nodes"
ng-controller="NodeActionController as nodeCtrl"
ng-init="nodeCtrl.init(node)">
<tr ng-repeat="node in nodeListCtrl.nodes">
<td>
<input type="checkbox"
checklist-model="nodeListCtrl.selectedNodes"
checklist-value="node.uuid"/>
</td>
<td><a href="#/ironic/nodes/{{nodeCtrl.node.uuid}}/node">{{nodeCtrl.node.uuid}}</a></td>
<td>{{nodeCtrl.node.name || 'Unnamed'}}</td>
<td>{{nodeCtrl.node.instance_uuid || 'None'}}</td>
<td>{{nodeCtrl.node.provision_state | capitalize}}</td>
<td>{{nodeCtrl.node.maintenance ? 'True' : 'False'}}</td>
<td>{{nodeCtrl.node.power_state || 'Unknown'}}</td>
<td><a href="#/ironic/nodes/{{node.uuid}}/node">{{node.uuid}}</a></td>
<td>{{node.name || 'Unnamed'}}</td>
<td>{{node.instance_uuid || 'None'}}</td>
<td>{{node.provision_state | capitalize}}</td>
<td>{{node.maintenance ? 'True' : 'False'}}</td>
<td>{{node.power_state || 'Unknown'}}</td>
</tr>
</tbody>
<tbody ng-if="nodeListCtrl.nodes.$resolved && nodeListCtrl.nodes.length == 0">

View File

@ -0,0 +1,73 @@
/*
* Copyright (c) 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.
*/
/**
* Unit tests for the unknown action modal controller.
*/
describe('Unit: Ironic-webclient UnknownActionModalController',
function() {
'use strict';
var $controller;
var mockInjectionProperties = {
$scope: {},
$uibModalInstance: {
close: function() {
},
dismiss: function() {
}
},
actionName: 'test'
};
beforeEach(function() {
module('template.mock');
module('ironic');
});
beforeEach(inject(function(_$controller_) {
$controller = _$controller_;
}));
describe('Controller Properties', function() {
it('does not pollute the $scope',
function() {
$controller('UnknownActionModalController', mockInjectionProperties);
expect(mockInjectionProperties.$scope).toEqual({});
});
});
describe('Controller Initialization', function() {
it('passes the actionName to the controller scope',
function() {
var controller = $controller('UnknownActionModalController', mockInjectionProperties);
expect(controller.actionName).toEqual('test');
});
});
describe('$scope.close', function() {
it('calls dismiss when close() is called.',
function() {
var spy = spyOn(mockInjectionProperties.$uibModalInstance, 'dismiss');
var controller = $controller('UnknownActionModalController', mockInjectionProperties);
controller.close();
expect(spy).toHaveBeenCalled();
expect(spy.calls.count()).toEqual(1);
});
});
});

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2015 Hewlett-Packard Development Company, L.P.
* Copyright (c) 2016 Hewlett Packard Enterprise 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
@ -60,52 +60,17 @@ describe('Unit: Ironic-webclient NodeActionController',
expect(controller.errorMessage).toBeDefined();
expect(controller.errorMessage).toBeFalsy();
});
it('starts without a node object',
function() {
var controller = $controller('NodeActionController', mockInjectionProperties);
expect(controller.node).toBeDefined();
expect(controller.node).toBeFalsy();
});
});
describe('Controller Initialization', function() {
it('should assign the node property via init()',
function() {
var controller = $controller('NodeActionController', mockInjectionProperties);
expect(controller.node).toBeDefined();
expect(controller.node).toBeFalsy();
var mockNode = {};
controller.init(mockNode);
expect(controller.node).toBeDefined();
expect(controller.node).toEqual(mockNode);
});
});
describe('remove', function() {
it('should do nothing if no node is found.',
function() {
var controller = $controller('NodeActionController', mockInjectionProperties);
expect(controller.node).toBeDefined();
expect(controller.node).toBeFalsy();
var promise = controller.remove();
expect(promise.$$state.status).toEqual(2);
});
it('open a modal if called with a node.',
inject(function($q, $uibModal) {
var controller = $controller('NodeActionController', mockInjectionProperties);
var mockNode = {};
var spy = spyOn($uibModal, 'open').and.callThrough();
$httpBackend.expectGET('view/ironic/action/remove_node.html').respond(200, '');
controller.init(mockNode);
var promise = controller.remove();
var promise = controller.remove(mockNode);
expect(promise.$$state.status).toEqual(0); // Unresolved promise.
expect(spy.calls.count()).toBe(1);
@ -131,4 +96,95 @@ describe('Unit: Ironic-webclient NodeActionController',
expect(spy.calls.count()).toBe(1);
}));
});
describe('provisionAction()', function() {
it('should open a modal',
inject(function($q, $uibModal) {
var spy = spyOn($uibModal, 'open').and.callThrough();
$httpBackend.expectGET('view/ironic/action/unknown.html').respond(200, '');
var testNode = {provision_state: 'enroll'};
var controller = $controller('NodeActionController', mockInjectionProperties);
controller.provisionAction('manage', [testNode]);
expect(spy.calls.count()).toBe(1);
var lastArgs = spy.calls.mostRecent().args[0];
expect(lastArgs.controller).toBe('UnknownActionModalController as ctrl');
$httpBackend.flush();
}));
it('should open an unsupported modal for unknown actions',
inject(function($q, $uibModal) {
var unknownActions = [
'foo', 'bar',
// The following are not yet implemented.
'manage', 'rebuild', 'delete', 'deploy', 'fail', 'abort', 'clean', 'inspect',
'provide'
];
var spy = spyOn($uibModal, 'open').and.callThrough();
$httpBackend.expectGET('view/ironic/action/unknown.html').respond(200, '');
angular.forEach(unknownActions, function(actionName) {
var testNode = {provision_state: 'enroll'};
var controller = $controller('NodeActionController', mockInjectionProperties);
controller.provisionAction(actionName, [testNode]);
expect(spy.calls.count()).toBe(1);
var lastArgs = spy.calls.mostRecent().args[0];
expect(lastArgs.controller).toBe('UnknownActionModalController as ctrl');
spy.calls.reset();
});
$httpBackend.flush();
}));
});
describe('powerAction()', function() {
it('should open a modal',
inject(function($q, $uibModal) {
var spy = spyOn($uibModal, 'open').and.callThrough();
$httpBackend.expectGET('view/ironic/action/unknown.html').respond(200, '');
var testNode = {power_state: 'power off'};
var controller = $controller('NodeActionController', mockInjectionProperties);
controller.powerAction('power on', [testNode]);
expect(spy.calls.count()).toBe(1);
var lastArgs = spy.calls.mostRecent().args[0];
expect(lastArgs.controller).toBe('UnknownActionModalController as ctrl');
$httpBackend.flush();
}));
it('should open an unsupported modal for unknown actions',
inject(function($q, $uibModal) {
var unknownActions = [
'foo', 'bar',
// The following are not yet implemented.
'power on', 'power off', 'reboot'
];
var spy = spyOn($uibModal, 'open').and.callThrough();
$httpBackend.expectGET('view/ironic/action/unknown.html').respond(200, '');
angular.forEach(unknownActions, function(actionName) {
var testNode = {power_state: 'power off'};
var controller = $controller('NodeActionController', mockInjectionProperties);
controller.powerAction(actionName, [testNode]);
expect(spy.calls.count()).toBe(1);
var lastArgs = spy.calls.mostRecent().args[0];
expect(lastArgs.controller).toBe('UnknownActionModalController as ctrl');
spy.calls.reset();
});
$httpBackend.flush();
}));
});
});