From 10a31bdb7a698420c6c91b7c76c9d403ac9320b7 Mon Sep 17 00:00:00 2001 From: Gal Margalit Date: Mon, 8 Feb 2016 09:47:21 +0000 Subject: [PATCH] UI: Cron trigger create modal * Added cron trigger create modal Screenshots: http://pasteboard.co/1q5HRneL.png Partially implements blueprint: mistral-dashboard-cron-trigger-screen Change-Id: I80449534b5e1ce35b9f60b1d3160550933297345 --- mistraldashboard/api.py | 56 ++++- mistraldashboard/cron_triggers/forms.py | 207 ++++++++++++++++++ mistraldashboard/cron_triggers/panel.py | 4 +- mistraldashboard/cron_triggers/tables.py | 18 +- .../templates/cron_triggers/_create.html | 45 ++++ .../templates/cron_triggers/create.html | 7 + mistraldashboard/cron_triggers/urls.py | 7 +- mistraldashboard/cron_triggers/views.py | 22 +- mistraldashboard/default/utils.py | 12 + .../static/mistraldashboard/css/style.css | 3 + 10 files changed, 356 insertions(+), 25 deletions(-) create mode 100644 mistraldashboard/cron_triggers/forms.py create mode 100644 mistraldashboard/cron_triggers/templates/cron_triggers/_create.html create mode 100644 mistraldashboard/cron_triggers/templates/cron_triggers/create.html diff --git a/mistraldashboard/api.py b/mistraldashboard/api.py index 91716a7..5ba964e 100644 --- a/mistraldashboard/api.py +++ b/mistraldashboard/api.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- -# # Copyright 2014 - StackStorm, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -155,7 +153,7 @@ def task_get(request, task_id=None): return mistralclient(request).tasks.get(task_id) -@handle_errors(_("Unable to retrieve workflows."), []) +@handle_errors(_("Unable to retrieve workflows"), []) def workflow_list(request): """Returns all workflows.""" @@ -293,6 +291,21 @@ def action_update(request, action_definition): return mistralclient(request).actions.update(action_definition) +def action_run(request, action_name, input, params): + """Run specific action execution. + + :param action_name: Action name + :param input: input + :param params: params + """ + + return mistralclient(request).action_executions.create( + action_name, + input, + **params + ) + + def action_delete(request, action_name): """Delete action. @@ -334,16 +347,35 @@ def cron_trigger_delete(request, cron_trigger_name): return mistralclient(request).cron_triggers.delete(cron_trigger_name) -def action_run(request, action_name, input, params): - """Run specific action execution. +def cron_trigger_create( + request, + cron_trigger_name, + workflow_ID, + workflow_input, + workflow_params, + pattern, + first_time, + count +): + """Create Cron Trigger. - :param action_name: Action name - :param input: input - :param params: params + :param request: Request data + :param cron_trigger_name: Cron Trigger name + :param workflow_ID: Workflow ID + :param workflow_input: Workflow input + :param workflow_params: Workflow params <* * * * *> + :param pattern: <* * * * *> + :param first_time: + Date and time of the first execution + :param count: Number of wanted executions """ - return mistralclient(request).action_executions.create( - action_name, - input, - **params + return mistralclient(request).cron_triggers.create( + cron_trigger_name, + workflow_ID, + workflow_input, + workflow_params, + pattern, + first_time, + count ) diff --git a/mistraldashboard/cron_triggers/forms.py b/mistraldashboard/cron_triggers/forms.py new file mode 100644 index 0000000..a28c26b --- /dev/null +++ b/mistraldashboard/cron_triggers/forms.py @@ -0,0 +1,207 @@ +# Copyright 2016 - Nokia. +# +# 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. + +import json + +from django.utils.translation import ugettext_lazy as _ + +from horizon import forms +from horizon import messages + +from mistraldashboard import api +from mistraldashboard.default.utils import convert_empty_string_to_none +from mistraldashboard.handle_errors import handle_errors + + +class CreateForm(forms.SelfHandlingForm): + name = forms.CharField( + max_length=255, + label=_("Name"), + help_text=_('Cron Trigger name.'), + required=True + ) + + workflow_id = forms.ChoiceField( + label=_('Workflow ID'), + help_text=_('Select Workflow ID.'), + widget=forms.Select( + attrs={'class': 'switchable', + 'data-slug': 'workflow_select'} + ) + ) + + input_source = forms.ChoiceField( + label=_('Input'), + help_text=_('JSON of input values defined in the workflow. ' + 'Select either file or raw content.'), + choices=[('file', _('File')), + ('raw', _('Direct Input'))], + widget=forms.Select( + attrs={'class': 'switchable', + 'data-slug': 'inputsource'} + ) + ) + input_upload = forms.FileField( + label=_('Input File'), + help_text=_('A local input to upload.'), + widget=forms.FileInput( + attrs={'class': 'switched', + 'data-switch-on': 'inputsource', + 'data-inputsource-file': _('Input File')} + ), + required=False + ) + input_data = forms.CharField( + label=_('Input Data'), + help_text=_('The raw contents of the input.'), + widget=forms.widgets.Textarea( + attrs={'class': 'switched', + 'data-switch-on': 'inputsource', + 'data-inputsource-raw': _('Input Data'), + 'rows': 4} + ), + required=False + ) + + params_source = forms.ChoiceField( + label=_('Params'), + help_text=_('JSON of params values defined in the workflow. ' + 'Select either file or raw content.'), + choices=[('file', _('File')), + ('raw', _('Direct Input'))], + widget=forms.Select( + attrs={'class': 'switchable', + 'data-slug': 'paramssource'} + ) + ) + params_upload = forms.FileField( + label=_('Params File'), + help_text=_('A local input to upload.'), + widget=forms.FileInput( + attrs={'class': 'switched', + 'data-switch-on': 'paramssource', + 'data-paramssource-file': _('Params File')} + ), + required=False + ) + params_data = forms.CharField( + label=_('Params Data'), + help_text=_('The raw contents of the params.'), + widget=forms.widgets.Textarea( + attrs={'class': 'switched', + 'data-switch-on': 'paramssource', + 'data-paramssource-raw': _('Params Data'), + 'rows': 4} + ), + required=False + ) + first_time = forms.CharField( + label=_('First Time (YYYY-MM-DD HH:MM)'), + help_text=_('Date and time of the first execution.'), + widget=forms.widgets.TextInput(), + required=False + ) + schedule_count = forms.CharField( + label=_('Count'), + help_text=_('Number of desired executions.'), + widget=forms.widgets.TextInput(), + required=False + ) + schedule_pattern = forms.CharField( + label=_('Pattern (* * * * *)'), + help_text=_('Cron Trigger pattern, mind the space between each char.'), + widget=forms.widgets.TextInput(), + required=False + ) + + def __init__(self, request, *args, **kwargs): + super(CreateForm, self).__init__(request, *args, **kwargs) + workflow_list = api.workflow_list(request) + workflow_id_list = [] + for wf in workflow_list: + workflow_id_list.append( + (wf.id, "{id} ({name})".format(id=wf.id, name=wf.name)) + ) + + self.fields['workflow_id'].choices = workflow_id_list + + def clean(self): + cleaned_data = super(CreateForm, self).clean() + cleaned_data['input'] = "" + cleaned_data['params'] = "" + + if cleaned_data.get('input_upload'): + files = self.request.FILES + cleaned_data['input'] = files['input_upload'].read() + elif cleaned_data.get('input_data'): + cleaned_data['input'] = cleaned_data['input_data'] + + del(cleaned_data['input_upload']) + del(cleaned_data['input_data']) + + if len(cleaned_data['input']) > 0: + try: + cleaned_data['input'] = json.loads(cleaned_data['input']) + except Exception as e: + msg = _('Input is invalid JSON: %s') % str(e) + raise forms.ValidationError(msg) + + if cleaned_data.get('params_upload'): + files = self.request.FILES + cleaned_data['params'] = files['params_upload'].read() + elif cleaned_data.get('params_data'): + cleaned_data['params'] = cleaned_data['params_data'] + + del(cleaned_data['params_upload']) + del(cleaned_data['params_data']) + + if len(cleaned_data['params']) > 0: + try: + cleaned_data['params'] = json.loads(cleaned_data['params']) + except Exception as e: + msg = _('Params is invalid JSON: %s') % str(e) + raise forms.ValidationError(msg) + + return cleaned_data + + @handle_errors(_("Unable to create Cron Trigger"), []) + def handle(self, request, data): + data['input'] = convert_empty_string_to_none(data['input']) + data['params'] = convert_empty_string_to_none(data['params']) + data['schedule_pattern'] = convert_empty_string_to_none( + data['schedule_pattern'] + ) + data['first_time'] = convert_empty_string_to_none(data['first_time']) + data['schedule_count'] = convert_empty_string_to_none( + data['schedule_count'] + ) + + try: + api.cron_trigger_create( + request, + data['name'], + data['workflow_id'], + data['input'], + data['params'], + data['schedule_pattern'], + data['first_time'], + data['schedule_count'], + ) + msg = _('Successfully created Cron Trigger.') + messages.success(request, msg) + + return True + + finally: + pass diff --git a/mistraldashboard/cron_triggers/panel.py b/mistraldashboard/cron_triggers/panel.py index 76479ed..8a9ddbb 100644 --- a/mistraldashboard/cron_triggers/panel.py +++ b/mistraldashboard/cron_triggers/panel.py @@ -1,6 +1,4 @@ -# -*- coding: utf-8 -*- -# -# Copyright 2016 - Alcatel-Lucent. +# Copyright 2016 - Nokia. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/mistraldashboard/cron_triggers/tables.py b/mistraldashboard/cron_triggers/tables.py index 9c38359..b538e02 100644 --- a/mistraldashboard/cron_triggers/tables.py +++ b/mistraldashboard/cron_triggers/tables.py @@ -1,6 +1,4 @@ -# -*- coding: utf-8 -*- -# -# Copyright 2016 - Alcatel-Lucent. +# Copyright 2016 - Nokia. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -25,6 +23,14 @@ from mistraldashboard import api from mistraldashboard.default.utils import humantime +class CreateCronTrigger(tables.LinkAction): + name = "create" + verbose_name = _("Create Cron Trigger") + url = "horizon:mistral:cron_triggers:create" + classes = ("ajax-modal",) + icon = "plus" + + class DeleteCronTrigger(tables.DeleteAction): @staticmethod def action_present(count): @@ -101,5 +107,9 @@ class CronTriggersTable(tables.DataTable): class Meta(object): name = "cron trigger" verbose_name = _("Cron Trigger") - table_actions = (tables.FilterAction, DeleteCronTrigger) + table_actions = ( + tables.FilterAction, + CreateCronTrigger, + DeleteCronTrigger + ) row_actions = (DeleteCronTrigger,) diff --git a/mistraldashboard/cron_triggers/templates/cron_triggers/_create.html b/mistraldashboard/cron_triggers/templates/cron_triggers/_create.html new file mode 100644 index 0000000..50426db --- /dev/null +++ b/mistraldashboard/cron_triggers/templates/cron_triggers/_create.html @@ -0,0 +1,45 @@ +{% extends "horizon/common/_modal_form.html" %} +{% load i18n %} +{% block form_attrs %}enctype="multipart/form-data"{% endblock %} +{% block modal-body-right %} +

{% trans "Description" %}:

+

+ {% blocktrans %} + Cron Trigger is an object allowing to run workflow on a schedule. + {% endblocktrans %} +

+

+ {% blocktrans %} + Using Cron Triggers it is possible to run workflows according to + specific rules: periodically setting a pattern + or on external events like Ceilometer alarm. + {% endblocktrans %} +

+

+ {% trans "For more info" %}: +

+

+
+

+ + {% trans "Please note" %}: + +
+ {% blocktrans %} + Name, Workflow ID and Pattern are mandatory fields. + {% endblocktrans %} +

+{% endblock %} diff --git a/mistraldashboard/cron_triggers/templates/cron_triggers/create.html b/mistraldashboard/cron_triggers/templates/cron_triggers/create.html new file mode 100644 index 0000000..89d9303 --- /dev/null +++ b/mistraldashboard/cron_triggers/templates/cron_triggers/create.html @@ -0,0 +1,7 @@ +{% extends 'base.html' %} +{% load i18n %} +{% load static %} + +{% block main %} + {% include 'mistral/cron_triggers/_create.html' %} +{% endblock %} diff --git a/mistraldashboard/cron_triggers/urls.py b/mistraldashboard/cron_triggers/urls.py index 88f9da2..24fc89d 100644 --- a/mistraldashboard/cron_triggers/urls.py +++ b/mistraldashboard/cron_triggers/urls.py @@ -1,6 +1,4 @@ -# -*- coding: utf-8 -*- -# -# Copyright 2016 - Alcatel-Lucent. +# Copyright 2016 - Nokia. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -25,4 +23,7 @@ urlpatterns = patterns( '', url(r'^$', views.IndexView.as_view(), name='index'), url(CRON_TRIGGERS % 'detail', views.OverviewView.as_view(), name='detail'), + url(r'^create$', + views.CreateView.as_view(), + name='create'), ) diff --git a/mistraldashboard/cron_triggers/views.py b/mistraldashboard/cron_triggers/views.py index f12546d..49a520d 100644 --- a/mistraldashboard/cron_triggers/views.py +++ b/mistraldashboard/cron_triggers/views.py @@ -1,6 +1,4 @@ -# -*- coding: utf-8 -*- -# -# Copyright 2016 - Alcatel-Lucent. +# Copyright 2016 - Nokia. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -20,9 +18,11 @@ from django.core.urlresolvers import reverse_lazy from django.utils.translation import ugettext_lazy as _ from django.views import generic +from horizon import forms from horizon import tables from mistraldashboard import api +from mistraldashboard.cron_triggers import forms as mistral_forms from mistraldashboard.cron_triggers.tables import CronTriggersTable @@ -49,6 +49,22 @@ class OverviewView(generic.TemplateView): return context +class CreateView(forms.ModalFormView): + template_name = 'mistral/cron_triggers/create.html' + modal_header = _("Create Cron Trigger") + form_id = "create_cron_trigger" + form_class = mistral_forms.CreateForm + submit_label = _("Create Cron Trigger") + submit_url = reverse_lazy("horizon:mistral:cron_triggers:create") + success_url = reverse_lazy('horizon:mistral:cron_triggers:index') + page_title = _("Create Cron Trigger") + + def get_form_kwargs(self): + kwargs = super(CreateView, self).get_form_kwargs() + + return kwargs + + class IndexView(tables.DataTableView): table_class = CronTriggersTable template_name = 'mistral/cron_triggers/index.html' diff --git a/mistraldashboard/default/utils.py b/mistraldashboard/default/utils.py index 3e7ca69..970a186 100644 --- a/mistraldashboard/default/utils.py +++ b/mistraldashboard/default/utils.py @@ -49,3 +49,15 @@ def prettyprint(x): return render_to_string("mistral/default/_prettyprint.html", {"full": full, "short": short}) + + +def convert_empty_string_to_none(str): + """Returns None if given string is empty. + + Empty string is default for Django form empty HTML input. + python-mistral-client does not handle empty strings, only "None" type. + + :param str: string variable + """ + + return str if len(str) != 0 else None diff --git a/mistraldashboard/static/mistraldashboard/css/style.css b/mistraldashboard/static/mistraldashboard/css/style.css index e69de29..37a5e7c 100644 --- a/mistraldashboard/static/mistraldashboard/css/style.css +++ b/mistraldashboard/static/mistraldashboard/css/style.css @@ -0,0 +1,3 @@ +.list{ + list-style: inherit; +} \ No newline at end of file