Add create action into Receiver panel

This patch adds create action into Receiver panel as
global action. And add "params" attribute for receiver.

Change-Id: Ibb0a5afd90f57a9a76ca15960b496675f41eeb62
This commit is contained in:
Shu Muto 2016-12-27 17:23:23 +09:00
parent ae382d7a78
commit 1ace5bac93
20 changed files with 560 additions and 32 deletions

View File

@ -19,7 +19,7 @@ from senlin_dashboard.api import senlin
from senlin_dashboard.api import utils as api_utils
from senlin_dashboard.cluster.nodes import forms as node_forms
from senlin_dashboard.cluster.profiles import forms
from senlin_dashboard.cluster.receivers import forms as receiver_forms
CLIENT_KEYWORDS = {'marker', 'sort_dir', 'sort_key', 'paginate'}
@ -40,12 +40,37 @@ class Receivers(generic.View):
receivers, has_more_data, has_prev_data = senlin.receiver_list(
request, filters=filters, **kwargs)
receivers_dict = []
for r in receivers:
r = r.to_dict()
r["params"] = api_utils.convert_to_yaml(r["params"])
r["channel"] = api_utils.convert_to_yaml(r["channel"])
receivers_dict.append(r)
return {
'items': [r.to_dict() for r in receivers],
'items': receivers_dict,
'has_more_data': has_more_data,
'has_prev_data': has_prev_data,
}
@rest_utils.ajax(data_required=True)
def post(self, request):
"""Create a new Receiver.
Returns the new Receiver object on success.
"""
request_param = request.DATA
params = receiver_forms._populate_receiver_params(
request_param.get("name"),
request_param.get("type"),
request_param.get("cluster_id"),
request_param.get("action"),
request_param.get("params"))
new_receiver = senlin.receiver_create(request, **params)
return rest_utils.CreatedResponse(
'/api/senlin/receivers/%s' % new_receiver.id,
new_receiver.to_dict())
@urls.register
class Receiver(generic.View):
@ -63,7 +88,10 @@ class Receiver(generic.View):
The result is a receiver object.
"""
return senlin.receiver_get(request, receiver_id).to_dict()
receiver = senlin.receiver_get(request, receiver_id).to_dict()
receiver["params"] = api_utils.convert_to_yaml(receiver["params"])
receiver["channel"] = api_utils.convert_to_yaml(receiver["channel"])
return receiver
@rest_utils.ajax()
def delete(self, request, receiver_id):

View File

@ -75,7 +75,7 @@ class Event(base.APIResourceWrapper):
class Receiver(base.APIResourceWrapper):
_attrs = ['id', 'name', 'type', 'cluster_id', 'action', 'created_at',
'updated_at', 'channel']
'updated_at', 'params', 'channel']
@memoized.memoized
@ -450,7 +450,7 @@ def receiver_list(request, sort_dir='desc', sort_key='created_at',
return [Receiver(r) for r in receivers], has_more_data, has_prev_data
def receiver_create(request, params):
def receiver_create(request, **params):
"""Create receiver"""
receiver = senlinclient(request).create_receiver(**params)
return Receiver(receiver)

View File

@ -30,6 +30,24 @@ from senlin_dashboard.api import senlin
INDEX_URL = "horizon:cluster:receivers:index"
def _populate_receiver_params(name, type_name, cluster_id, action, params):
if not params:
params_dict = {}
else:
try:
params_dict = yaml.load(params)
except Exception as ex:
raise Exception(_('The specified params is not a valid '
'YAML: %s') % six.text_type(ex))
params = {"name": name,
"type": type_name,
"cluster_id": cluster_id,
"action": action,
"params": params_dict}
return params
class CreateReceiverForm(forms.SelfHandlingForm):
name = forms.CharField(max_length=255, label=_("Name"))
cluster_id = forms.ThemableChoiceField(

View File

@ -70,12 +70,19 @@ class ReceiversTest(test.TestCase):
'action': 'CLUSTER_SCALE_IN',
'params': ''
}
formdata = {
'name': 'test-receiver',
'type': 'webhook',
'cluster_id': '123456',
'action': 'CLUSTER_SCALE_IN',
'params': ''
}
api.senlin.cluster_list(
IsA(http.HttpRequest)).AndReturn((clusters, False, False))
api.senlin.receiver_create(
IsA(http.HttpRequest), data).AndReturn(receiver)
IsA(http.HttpRequest), **data).AndReturn(receiver)
self.mox.ReplayAll()
res = self.client.post(CREATE_URL, data)
res = self.client.post(CREATE_URL, formdata)
self.assertNoFormErrors(res)

View File

@ -49,6 +49,7 @@
getEvents: getEvents,
getReceivers: getReceivers,
getReceiver: getReceiver,
createReceiver: createReceiver,
deleteReceiver: deleteReceiver,
getCluster: getCluster,
getClusters: getClusters
@ -216,6 +217,27 @@
});
}
/**
* @name createReceiver
* @description
* Create new Receiver.
*
* @param {Object} params
* JSON object to create new receiver like name, type, cluster_id, action
* and params.
* @param {boolean} suppressError
* If passed in, this will not show the default error handling
* @returns {Object} The result of the API call
*/
function createReceiver(params, suppressError) {
var promise = apiService.post('/api/senlin/receivers/', params);
return suppressError ? promise : promise.error(function() {
var msg = gettext('Unable to create the receiver with name: %(name)s');
toastService.add('error', interpolate(msg, { name: params.name }, true));
});
}
/**
* @name deleteReceiver
* @description

View File

@ -30,16 +30,29 @@
registerReceiverActions.$inject = [
'horizon.framework.conf.resource-type-registry.service',
'horizon.app.core.receivers.actions.delete.service',
'horizon.cluster.receivers.actions.create.service',
'horizon.cluster.receivers.actions.delete.service',
'horizon.app.core.receivers.resourceType'
];
function registerReceiverActions(
registry,
createReceiverService,
deleteReceiverService,
receiverResourceType
) {
var receiverResource = registry.getResourceType(receiverResourceType);
receiverResource.globalActions
.append({
id: 'createReceiverAction',
service: createReceiverService,
template: {
text: gettext('Create Receiver'),
type: 'create'
}
});
receiverResource.itemActions
.append({
id: 'deleteReceiverAction',

View File

@ -0,0 +1,95 @@
/**
* 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 factory
* @name horizon.cluster.receivers.actions.create.service
* @description
* Service for the cluster receiver create modal
*/
angular
.module('horizon.cluster.receivers.actions')
.factory('horizon.cluster.receivers.actions.create.service', createService);
createService.$inject = [
'$location',
'horizon.app.core.openstack-service-api.policy',
'horizon.app.core.openstack-service-api.senlin',
'horizon.app.core.receivers.basePath',
'horizon.app.core.receivers.resourceType',
'horizon.framework.util.actions.action-result.service',
'horizon.framework.util.i18n.gettext',
'horizon.framework.widgets.form.ModalFormService',
'horizon.framework.widgets.toast.service',
'horizon.cluster.receivers.actions.workflow'
];
function createService(
$location, policy, senlin, basePath, resourceType, actionResult, gettext,
modal, toast, workflow
) {
var message = {
success: gettext('Receiver %s was successfully created.')
};
var service = {
initAction: initAction,
perform: perform,
allowed: allowed
};
return service;
//////////////
function initAction() {
}
function perform() {
// modal title, buttons
var title, submitText, submitIcon, helpUrl;
title = gettext('Create Receiver');
submitText = gettext('Create');
submitIcon = 'fa fa-check';
helpUrl = basePath + 'actions/receiver.help.html';
var config = workflow.init('create', title, submitText, submitIcon, helpUrl);
return modal.open(config).then(submit);
}
function allowed() {
return policy.ifAllowed({ rules: [['cluster', 'receivers:create']] });
}
function submit(context) {
delete context.model.id;
return senlin.createReceiver(context.model, false).then(success, true);
}
function success(response) {
toast.add('success', interpolate(message.success, [response.data.id]));
var result = actionResult.getActionResult()
.created(resourceType, response.data.id);
if (result.result.failed.length === 0 && result.result.created.length > 0) {
$location.path("/cluster/receivers");
} else {
return result.result;
}
}
}
})();

View File

@ -0,0 +1,84 @@
/**
* 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('horizon.cluster.receivers.actions.create.service', function() {
var service, $scope, $q, deferred, senlin, basePath;
var workflow = {
init: function (actionType, title, submitText, submitIcon, helpUrl) {
actionType = title = submitText = submitIcon = helpUrl;
return {then: angular.noop, dummy: actionType};
}
};
var modal = {
open: function (config) {
deferred = $q.defer();
deferred.resolve(config.model);
return deferred.promise;
}
};
var selected = {id: 1};
///////////////////
beforeEach(module('horizon.app.core'));
beforeEach(module('horizon.framework'));
beforeEach(module('horizon.cluster.receivers'));
beforeEach(module(function($provide) {
$provide.value('horizon.framework.widgets.form.ModalFormService', modal);
$provide.value('horizon.cluster.receivers.actions.workflow', workflow);
}));
beforeEach(inject(function($injector, _$rootScope_, _$q_) {
$q = _$q_;
$scope = _$rootScope_.$new();
service = $injector.get('horizon.cluster.receivers.actions.create.service');
senlin = $injector.get('horizon.app.core.openstack-service-api.senlin');
basePath = $injector.get('horizon.app.core.receivers.basePath');
deferred = $q.defer();
deferred.resolve({data: {id: 1}});
spyOn(senlin, 'createReceiver').and.returnValue(deferred.promise);
spyOn(workflow, 'init').and.callThrough();
spyOn(modal, 'open').and.callThrough();
}));
it('should check the policy if the user is allowed to create receiver', function() {
var allowed = service.allowed();
expect(allowed).toBeTruthy();
});
it('should initialize workflow', function() {
service.initAction();
service.perform(selected, $scope);
expect(workflow.init).toHaveBeenCalled();
var modalArgs = workflow.init.calls.mostRecent().args;
expect(modalArgs[0]).toEqual('create');
expect(modalArgs[1]).toEqual('Create Receiver');
expect(modalArgs[2]).toEqual('Create');
expect(modalArgs[3]).toEqual('fa fa-check');
expect(modalArgs[4]).toEqual(basePath + 'actions/receiver.help.html');
expect(modal.open).toHaveBeenCalled();
});
});
})();

View File

@ -17,7 +17,7 @@
angular
.module('horizon.cluster.receivers')
.factory('horizon.app.core.receivers.actions.delete.service', deleteReceiverService);
.factory('horizon.cluster.receivers.actions.delete.service', deleteReceiverService);
deleteReceiverService.$inject = [
'$q',
@ -33,7 +33,7 @@
/*
* @ngdoc factory
* @name horizon.app.core.receivers.actions.delete.service
* @name horizon.cluster.receivers.actions.delete.service
*
* @Description
* Brings up the delete receivers confirmation modal dialog.
@ -100,18 +100,17 @@
function createResult(deleteModalResult) {
// To make the result of this action generically useful, reformat the return
// from the deleteModal into a standard form
var actionResult = actionResultService.getActionResult();
var result = actionResultService.getActionResult();
deleteModalResult.pass.forEach(function markDeleted(item) {
actionResult.deleted(receiversResourceType, getEntity(item).id);
result.deleted(receiversResourceType, getEntity(item).id);
});
deleteModalResult.fail.forEach(function markFailed(item) {
actionResult.failed(receiversResourceType, getEntity(item).id);
result.failed(receiversResourceType, getEntity(item).id);
});
if (actionResult.result.failed.length === 0 &&
actionResult.result.deleted.length > 0) {
if (result.result.failed.length === 0 && result.result.deleted.length > 0) {
$location.path('/cluster/receivers');
} else {
return actionResult.result;
return result.result;
}
}

View File

@ -15,7 +15,7 @@
(function() {
'use strict';
describe('horizon.app.core.receivers.actions.delete.service', function() {
describe('horizon.cluster.receivers.actions.delete.service', function() {
var service, $scope, deferredModal;
@ -62,7 +62,7 @@
beforeEach(inject(function($injector, _$rootScope_, $q) {
$scope = _$rootScope_.$new();
service = $injector.get('horizon.app.core.receivers.actions.delete.service');
service = $injector.get('horizon.cluster.receivers.actions.delete.service');
deferredModal = $q.defer();
}));

View File

@ -0,0 +1,12 @@
<dl>
<dt translate>Name</dt>
<dd translate>An arbitrary human-readable name.</dd>
<dt translate>Type</dt>
<dd translate>Type of the receiver to create. Default to webhook</dd>
<dt translate>Cluster</dt>
<dd translate>Targeted cluster for this receiver.</dd>
<dt translate>Action</dt>
<dd translate>Name or ID of the targeted action to be triggered.</dd>
<dt translate>Params</dt>
<dd translate>YAML formatted parameters that will be passed to target action when the receiver is triggered.</dd>
</dl>

View File

@ -0,0 +1,160 @@
/**
* 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 factory
* @name horizon.cluster.receivers.actions.workflow
* @ngController
*
* @description
* Workflow for creating/updating receiver
*/
angular
.module('horizon.cluster.receivers.actions')
.factory('horizon.cluster.receivers.actions.workflow', workflow);
workflow.$inject = [
'horizon.app.core.openstack-service-api.senlin',
'horizon.framework.util.i18n.gettext'
];
function workflow(senlin, gettext) {
var workflow = {
init: init
};
function init(actionType, title, submitText, submitIcon, helpUrl) {
var schema, form, model;
// schema
schema = {
type: 'object',
properties: {
name: {
title: gettext('Name'),
type: 'string'
},
type: {
title: gettext('Type'),
type: 'string',
default: ''
},
cluster_id: {
title: gettext('Cluster'),
type: 'string',
default: ''
},
action: {
title: gettext('Action'),
type: 'string'
},
params: {
title: gettext('Params'),
type: 'string'
}
}
};
// form
form = [
{
type: 'section',
htmlClass: 'row',
items: [
{
type: 'section',
htmlClass: 'col-sm-6',
items: [
{
key: 'name',
placeholder: gettext('Name of the receiver.'),
required: true
},
{
key: 'type',
type: 'select',
titleMap: [
{value: '', name: gettext('Select type for the receiver.')},
{value: 'webhook', name: gettext('Webhook')},
{value: 'message', name: gettext('Message')}
],
required: true
},
{
key: 'cluster_id',
type: 'select',
titleMap: [
{value: '', name: gettext('Select cluster for the receiver.')}
],
required: "model.type === 'webhook' || model.type === ''"
},
{
key: 'action',
type: 'select',
titleMap: [
{value: '', name: gettext('Select action for the receiver.')},
{value: 'CLUSTER_SCALE_IN', name: gettext('Scale In the Cluster')},
{value: 'CLUSTER_SCALE_OUT', name: gettext('Scale Out the Cluster')}
],
required: "model.type === 'webhook' || model.type === ''"
},
{
key: 'params',
type: 'textarea',
placeholder: gettext('Parameters of the receiver in YAML format.')
}
]
},
{
type: 'template',
templateUrl: helpUrl
}
]
}
]; // form
// Get clusters
senlin.getClusters().then(onGetClusters);
function onGetClusters(response) {
angular.forEach(response.data.items, function(item) {
form[0].items[0].items[2].titleMap.push({value: item.id, name: item.name});
});
}
model = {
id: '',
name: '',
type: '',
cluster_id: '',
action: '',
params: ''
};
var config = {
title: title,
submitText: submitText,
schema: schema,
form: form,
model: model
};
return config;
}
return workflow;
}
})();

View File

@ -0,0 +1,58 @@
/**
* 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('horizon.cluster.receivers.actions.workflow', function() {
var workflow, senlin;
function fakePromise() {
return { then: angular.noop };
}
beforeEach(module('horizon.app.core'));
beforeEach(module('horizon.framework'));
beforeEach(module('horizon.cluster.receivers'));
beforeEach(inject(function($injector) {
workflow = $injector.get('horizon.cluster.receivers.actions.workflow');
senlin = $injector.get('horizon.app.core.openstack-service-api.senlin');
spyOn(senlin, 'getProfiles').and.callFake(fakePromise);
spyOn(senlin, 'getClusters').and.callFake(fakePromise);
}));
function testInitWorkflow(actionType, title, submitText) {
var submitIcon, helpUrl;
submitIcon = 'fa fa-check';
helpUrl = 'receiver.help.html';
var config = workflow.init(actionType, title, submitText, submitIcon, helpUrl);
expect(senlin.getClusters).toHaveBeenCalled();
expect(config.title).toEqual(title);
expect(config.submitText).toEqual(submitText);
expect(config.schema).toBeDefined();
expect(config.form).toBeDefined();
return config;
}
it('should be create workflow config for create', function() {
var config = testInitWorkflow('create', 'Create Receiver', 'Create');
expect(config.form[0].items[0].items[1].required).toEqual(true);
});
});
})();

View File

@ -1,6 +1,6 @@
<hz-resource-property-list
resource-type-name="OS::Senlin::Receiver"
item="item"
property-groups="[['id', 'channel'],
property-groups="[['id', 'params', 'channel'],
['created_at', 'updated_at']]">
</hz-resource-property-list>

View File

@ -8,7 +8,7 @@
cls="dl-horizontal"
item="ctrl.receiver"
property-groups="[['id', 'name', 'created_at', 'updated_at'],
['type', 'cluster_id', 'action', 'channel']]">
['type', 'cluster_id', 'action', 'params', 'channel']]">
</hz-resource-property-list>
</div>
</div>

View File

@ -1,4 +1,4 @@
<hz-resource-panel resource-type-name="OS::Senlin::Receiver">
<hz-resource-table resource-type-name="OS::Senlin::Receiver">
<hz-resource-table resource-type-name="OS::Senlin::Receiver" track-by="trackBy">
</hz-resource-table>
</hz-resource-panel>

View File

@ -106,10 +106,11 @@
return {
id: { label: gettext('ID'), filters: ['noValue'] },
name: { label: gettext('Name'), filters: ['noName'] },
type: { label: gettext('Type'), filters: ['noName'] },
type: { label: gettext('Type'), filters: ['noValue'] },
cluster_id: { label: gettext('Cluster ID'), filters: ['noValue'] },
action: { label: gettext('Action'), filters: ['noValue'] },
channel: { label: gettext('Channel'), filters: ['noValue', 'json'] },
params: { label: gettext('Parameters'), filters: ['noValue'] },
channel: { label: gettext('Channel'), filters: ['noValue'] },
created_at: { label: gettext('Created'), filters: ['simpleDate'] },
updated_at: { label: gettext('Updated'), filters: ['simpleDate'] }
};

View File

@ -66,7 +66,17 @@
* receivers. This is used in displaying lists of Receivers.
*/
function getReceiversPromise(params) {
return senlin.getReceivers(params);
return senlin.getReceivers(params).then(modifyResponse);
}
function modifyResponse(response) {
return {data: {items: response.data.items.map(modifyItem)}};
function modifyItem(item) {
var timestamp = item.updated_at ? item.updated_at : item.created_at;
item.trackBy = item.id + timestamp;
return item;
}
}
}
})();

View File

@ -22,12 +22,33 @@ class SenlinRestTestCase(test.TestCase):
# Receiver
#
_receivers = [
{
'id': '1',
'name': 'test-receiver1',
'type': 'webhook',
'cluster_id': 'c1',
'action': 'CLUSTER_SCALE_OUT',
'params': None,
'channel': None
},
{
'id': '2',
'name': 'test-receiver2',
'type': 'message',
'cluster_id': None,
'action': None,
'params': None,
'channel': None
},
]
@mock.patch.object(senlin, 'senlin')
def test_receivers_get(self, client):
request = self.mock_rest_request(**{'GET': {}})
client.receiver_list.return_value = ([
mock.Mock(**{'to_dict.return_value': {'id': 'one'}}),
mock.Mock(**{'to_dict.return_value': {'id': 'two'}}),
mock.Mock(**{'to_dict.return_value': self._receivers[0]}),
mock.Mock(**{'to_dict.return_value': self._receivers[1]}),
], False, True)
response = senlin.Receivers().get(request)
@ -39,15 +60,15 @@ class SenlinRestTestCase(test.TestCase):
@mock.patch.object(senlin, 'senlin')
def test_receiver_get_single(self, client):
request = self.mock_rest_request()
client.receiver_get.return_value.to_dict.return_value = {
'name': 'test-receiver'}
client.receiver_get.return_value.to_dict.return_value = \
self._receivers[0]
response = senlin.Receiver().get(request, '1')
self.assertStatusCode(response, 200)
self.assertEqual('test-receiver', response.json['name'])
self.assertEqual('test-receiver1', response.json['name'])
@mock.patch.object(senlin, 'senlin')
def test_receiver_delete(self, client):
request = self.mock_rest_request()
senlin.Receiver().delete(request, "1")
client.receiver_delete.assert_called_once_with(request, "1")
senlin.Receiver().delete(request, '1')
client.receiver_delete.assert_called_once_with(request, '1')

View File

@ -110,7 +110,7 @@ class SenlinApiTests(test.APITestCase):
senlinclient.receivers.return_value = receivers
ret_val, _more, _prev = api.senlin.receiver_list(self.request)
for receiver in ret_val:
for receiver in ret_val[0]:
self.assertIsInstance(receiver, api.senlin.Receiver)
senlinclient.receivers.assert_called_once_with(**params)