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
This commit is contained in:
Gal Margalit 2016-02-08 09:47:21 +00:00
parent 57e26272a5
commit 10a31bdb7a
10 changed files with 356 additions and 25 deletions

View File

@ -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 <YYYY-MM-DD HH:MM>
:param count: Number of wanted executions <integer>
"""
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
)

View File

@ -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

View File

@ -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.

View File

@ -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,)

View File

@ -0,0 +1,45 @@
{% extends "horizon/common/_modal_form.html" %}
{% load i18n %}
{% block form_attrs %}enctype="multipart/form-data"{% endblock %}
{% block modal-body-right %}
<h3>{% trans "Description" %}:</h3>
<p>
{% blocktrans %}
Cron Trigger is an object allowing to run workflow on a schedule.
{% endblocktrans %}
</p>
<p>
{% 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 %}
</p>
<p>
{% trans "For more info" %}:
<ul class="list">
<li>
<a href="http://docs.openstack.org/developer/mistral/developer/webapi/v2.html#cron-triggers"
title="{% trans "Mistral Cron Trigger" %}" target="_blank">
{% trans "Mistral Cron Trigger" %}
</a>
</li>
<li>
<a href="http://en.wikipedia.org/wiki/Cron"
title="Wikipedia" target="_blank">
http://en.wikipedia.org/wiki/Cron
</a>
</li>
</ul>
</p>
<hr />
<p>
<strong class="block">
{% trans "Please note" %}:
</strong>
<br/>
{% blocktrans %}
Name, Workflow ID and Pattern are mandatory fields.
{% endblocktrans %}
</p>
{% endblock %}

View File

@ -0,0 +1,7 @@
{% extends 'base.html' %}
{% load i18n %}
{% load static %}
{% block main %}
{% include 'mistral/cron_triggers/_create.html' %}
{% endblock %}

View File

@ -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'),
)

View File

@ -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'

View File

@ -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

View File

@ -0,0 +1,3 @@
.list{
list-style: inherit;
}