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