Guided job execution page for Sahara

Adding a guide for job executions in
data processing.  This is meant to weave
the existing panels and forms together in
a way that can be understood by all users.

Change-Id: Ib6e1c86ae18d18a1e2dbac6a172e70d61c79fff4
Partial-Implements: blueprint data-processing-rework-ui
This commit is contained in:
Chad Roberts 2015-01-16 11:44:42 -05:00
parent 22bdf25e23
commit 6ce3206a77
18 changed files with 369 additions and 17 deletions

View File

@ -36,6 +36,12 @@ class ClustersFilterAction(tables.FilterAction):
('status', _("Status"), True))
class ClusterGuide(tables.LinkAction):
name = "cluster_guide"
verbose_name = _("Cluster Creation Guide")
url = "horizon:project:data_processing.wizard:cluster_guide"
class CreateCluster(tables.LinkAction):
name = "create"
verbose_name = _("Launch Cluster")
@ -156,7 +162,8 @@ class ClustersTable(tables.DataTable):
row_class = UpdateRow
cell_class = RichErrorCell
status_columns = ["status"]
table_actions = (CreateCluster,
table_actions = (ClusterGuide,
CreateCluster,
ConfigureCluster,
DeleteCluster,
ClustersFilterAction)

View File

@ -20,6 +20,8 @@ from horizon import forms
from horizon import workflows
from openstack_dashboard.api import sahara as saharaclient
from openstack_dashboard.dashboards.project.data_processing \
.utils import helpers
LOG = logging.getLogger(__name__)
@ -106,6 +108,13 @@ class CreateDataSource(workflows.Workflow):
context["source_url"],
context.get("general_data_source_credential_user", None),
context.get("general_data_source_credential_pass", None))
hlps = helpers.Helpers(request)
if hlps.is_from_guide():
request.session["guide_datasource_id"] = self.object.id
request.session["guide_datasource_name"] = self.object.name
self.success_url = (
"horizon:project:data_processing.wizard:jobex_guide")
return True
except Exception:
exceptions.handle(request)

View File

@ -16,7 +16,7 @@
{% include "horizon/common/_form_fields.html" %}
</fieldset>
</div>
<div class="form-help-block">
<div class="form-help-block right">
{{ form.get_help_text }}
</div>
{% endblock %}

View File

@ -25,6 +25,8 @@ from horizon import tables
from horizon import tabs
from openstack_dashboard.api import sahara as saharaclient
from openstack_dashboard.dashboards.project.data_processing.utils \
import helpers
import openstack_dashboard.dashboards.project.data_processing. \
job_binaries.forms as job_binary_forms
@ -60,6 +62,13 @@ class CreateJobBinaryView(forms.ModalFormView):
template_name = "project/data_processing.job_binaries/create.html"
page_title = _("Create Job Binary")
def get_success_url(self):
hlps = helpers.Helpers(self.request)
if hlps.is_from_guide():
self.success_url = reverse_lazy(
"horizon:project:data_processing.wizard:jobex_guide")
return self.success_url
class JobBinaryDetailsView(tabs.TabView):
tab_group_class = _tabs.JobBinaryDetailsTabs

View File

@ -40,6 +40,12 @@ class JobExecutionsFilterAction(tables.FilterAction):
('status', _("Status"), True))
class JobExecutionGuide(tables.LinkAction):
name = "jobex_guide"
verbose_name = _("Job Execution Guide")
url = "horizon:project:data_processing.wizard:jobex_guide"
class DeleteJobExecution(tables.DeleteAction):
@staticmethod
def action_present(count):
@ -186,7 +192,8 @@ class JobExecutionsTable(tables.DataTable):
row_class = UpdateRow
status_columns = ["status"]
verbose_name = _("Job Executions")
table_actions = [DeleteJobExecution,
table_actions = [JobExecutionGuide,
DeleteJobExecution,
JobExecutionsFilterAction]
row_actions = [DeleteJobExecution,
ReLaunchJobExistingCluster,

View File

@ -185,7 +185,7 @@
var job_type;
$.ajax({
url: "launch-job",
url: "/project/data_processing/jobs/launch-job",
data: {"job_id": $("#id_job").val(),
"json": true },
success: function (data, textStatus, jqXHR) {

View File

@ -27,6 +27,10 @@ urlpatterns = patterns('',
url(r'^create-job$',
views.CreateJobView.as_view(),
name='create-job'),
url(r'^create-job/'
'(?P<guide_job_type>[^/]+)/$',
views.CreateJobView.as_view(),
name='create-job'),
url(r'^launch-job$',
views.LaunchJobView.as_view(),
name='launch-job'),

View File

@ -14,6 +14,7 @@
import json
import logging
from django.core import urlresolvers
from django.utils.translation import ugettext_lazy as _
from horizon import exceptions
@ -21,7 +22,8 @@ from horizon import forms
from horizon.forms import fields
from horizon import workflows
from openstack_dashboard.dashboards.project.data_processing \
.utils import helpers
from openstack_dashboard.api import sahara as saharaclient
@ -85,6 +87,14 @@ class GeneralConfigAction(workflows.Action):
required=False,
widget=forms.Textarea(attrs={'rows': 4}))
def __init__(self, request, context, *args, **kwargs):
super(GeneralConfigAction,
self).__init__(request, context, *args, **kwargs)
resolver_match = urlresolvers.resolve(request.path)
if "guide_job_type" in resolver_match.kwargs:
self.fields["job_type"].initial = (
resolver_match.kwargs["guide_job_type"].lower())
def populate_job_type_choices(self, request, context):
choices = [("pig", _("Pig")), ("hive", _("Hive")),
("spark", _("Spark")),
@ -119,19 +129,11 @@ class GeneralConfigAction(workflows.Action):
class GeneralConfig(workflows.Step):
action_class = GeneralConfigAction
contributes = ("job_name", "job_type", "job_description", "main_binary")
# Map needed because switchable fields need lower case
# and our server is expecting upper case
JOB_TYPE_MAP = {"pig": "Pig",
"hive": "Hive",
"spark": "Spark",
"mapreduce": "MapReduce",
"mapreduce.streaming": "MapReduce.Streaming",
"java": "Java"}
def contribute(self, data, context):
for k, v in data.items():
if k == "job_type":
context[k] = self.JOB_TYPE_MAP[v]
context[k] = helpers.JOB_TYPE_MAP[v][1]
else:
context[k] = v
return context
@ -169,13 +171,21 @@ class CreateJob(workflows.Workflow):
main_locations.append(context["main_binary"])
try:
saharaclient.job_create(
job = saharaclient.job_create(
request,
context["job_name"],
context["job_type"],
main_locations,
lib_locations,
context["job_description"])
hlps = helpers.Helpers(request)
if hlps.is_from_guide():
request.session["guide_job_id"] = job.id
request.session["guide_job_type"] = context["job_type"]
request.session["guide_job_name"] = context["job_name"]
self.success_url = (
"horizon:project:data_processing.wizard:jobex_guide")
return True
except Exception:
exceptions.handle(request)

View File

@ -11,6 +11,8 @@
# See the License for the specific language governing permissions and
# limitations under the License.
from django.utils.translation import ugettext_lazy as _
import openstack_dashboard.dashboards.project.data_processing. \
utils.workflow_helpers as work_helpers
@ -104,3 +106,27 @@ class Helpers(object):
except Exception:
return False
return True
def reset_job_guide(self):
try:
self.request.session.update(
{"guide_job_type": None,
"guide_job_name": None,
"guide_job_id": None,
"guide_datasource_id": None,
"guide_datasource_name": None, })
except Exception:
return False
return True
# Map needed because switchable fields need lower case
# and our server is expecting upper case. We will be
# using the 0 index as the display name and the 1 index
# as the value to pass to the server.
JOB_TYPE_MAP = {"pig": [_("Pig"), "Pig"],
"hive": [_("Hive"), "Hive"],
"spark": [_("Spark"), "Spark"],
"mapreduce": [_("MapReduce"), "MapReduce"],
"mapreduce.streaming": [_("Streaming MapReduce"),
"MapReduce.Streaming"],
"java": [_("Java"), "Java"]}

View File

@ -84,3 +84,38 @@ class ChoosePluginForm(forms.SelfHandlingForm):
class Meta(object):
name = _("Choose plugin type and version")
class ChooseJobTypeForm(forms.SelfHandlingForm):
guide_job_type = forms.ChoiceField(
label=_("Job Type"),
widget=forms.Select())
def __init__(self, request, *args, **kwargs):
super(ChooseJobTypeForm, self).__init__(request, *args, **kwargs)
self.help_text_template = ("project/data_processing.wizard/"
"_job_type_select_help.html")
self.fields["guide_job_type"].choices = \
self.populate_guide_job_type_choices()
def populate_guide_job_type_choices(self):
choices = [(x, helpers.JOB_TYPE_MAP[x][0])
for x in helpers.JOB_TYPE_MAP]
return choices
def handle(self, request, context):
try:
hlps = helpers.Helpers(request)
job_type = context["guide_job_type"]
if force_text(request.session.get("guide_job_type")) != (
force_text(helpers.JOB_TYPE_MAP[job_type][0])):
hlps.reset_job_guide()
request.session["guide_job_type"] = (
helpers.JOB_TYPE_MAP[job_type][0])
messages.success(request, "Job type chosen")
return True
except Exception:
exceptions.handle(request,
_("Unable to set job type"))
return False

View File

@ -0,0 +1,31 @@
{% extends "horizon/common/_modal_form.html" %}
{% load url from future %}
{% load i18n %}
{% block form_id %}create-job-binary{% endblock %}
{% block form_action %}
{% url 'horizon:project:data_processing.wizard:job_type_select' %}
{% endblock %}
{% block form_attrs %}enctype="multipart/form-data"{% endblock %}
{% block modal-header %}{% trans "Choose job type" %}{% endblock %}
{% block modal-body %}
<div class="left">
<fieldset>
{% include "horizon/common/_form_fields.html" %}
</fieldset>
</div>
<div class="form-help-block right">
{{ form.get_help_text }}
</div>
{% endblock %}
{% block modal-footer %}
<input class="btn btn-primary pull-right" id="plugin_select_btn"
type="submit" value="{% trans "Select" %}"/>
<a href="{% url 'horizon:project:data_processing.wizard:jobex_guide' %}"
class="btn btn-default secondary cancel close">{% trans "Cancel" %}</a>
{% endblock %}

View File

@ -0,0 +1,7 @@
{% load i18n horizon %}
<p class="well">
{% blocktrans %}Select which type of job that you want to run.
This choice will dictate which steps are required to successfully
execute your job.
{% endblocktrans %}
</p>

View File

@ -0,0 +1,11 @@
{% extends 'base.html' %}
{% load i18n %}
{% block title %}{% trans "Choose job type" %}{% endblock %}
{% block page_header %}
{% include "horizon/common/_page_header.html" with title=_("Choose job type") %}
{% endblock page_header %}
{% block main %}
{% include 'project/data_processing.wizard/_job_type_select.html' %}
{% endblock %}

View File

@ -0,0 +1,119 @@
{% extends 'base.html' %}
{% load i18n %}
{% load url from future %}
{% block title %}{% trans "Data Processing" %}{% endblock %}
{% block page_header %}
{% include "horizon/common/_page_header.html" with title=_("Guided Job Execution") %}
{% endblock page_header %}
{% block main %}
<div class="guide">
<ol>
<li>
<div>
<div>
{% blocktrans %}First, select which type of job that
you want to run. This choice will determine which
other steps are required
{% endblocktrans %}
</div>
<div>
<a class="btn btn-default btn-small btn-create btn-inline ajax-modal"
href="{% url 'horizon:project:data_processing.wizard:job_type_select' %}">
<span class="fa fa-plus"></span> {% trans "Select type" %}
</a>
</div>
<div>{% trans "Current type:" %}
{% if request.session.guide_job_type %}
<span class="text-success">
{{ request.session.guide_job_type}}
</span>
{% else %}
<span class="text-danger">
{% trans "No type chosen" %}
</span>
{% endif %}
</div>
</div>
</li>
<hr/>
{% if request.session.guide_job_type %}
{% if view.show_data_sources %}
<li>
<div>
<div>{% blocktrans %}Data Sources are what your
job uses for input and output. Depending on the type
of job you will be running, you may need to define one
or more data sources. You can create multiple data
sources by repeating this step.
{% endblocktrans %}
</div>
<div>
<a class="btn btn-default btn-small btn-create btn-inline ajax-modal"
href="{% url 'horizon:project:data_processing.data_sources:create-data-source' %}">
<span class="fa fa-plus"></span> {% trans "Create a data source" %}
</a>
</div>
</div>
</li>
<hr/>
{% endif %}
<li>
<div>
<div>{% blocktrans %}Define your Job Template.
This is where you choose the type of job that you
want to run (Pig, Java Action, Spark, etc) and choose
or upload the files necessary to run it. The inputs
and outputs will be defined later.
{% endblocktrans %}
</div>
<div>
<a class="btn btn-default btn-small btn-create btn-inline ajax-modal{% if not request.session.guide_job_type %} disabled{% endif %}"
href="{% url 'horizon:project:data_processing.jobs:create-job' request.session.guide_job_type %}">
<span class="fa fa-plus"></span> {% trans "Create a job template" %}</a>
</div>
<div>{% trans "Job template:" %}
{% if request.session.guide_job_name %}
<span class="text-success">
{{ request.session.guide_job_name }}
</span>
{% else %}
<span class="text-danger">
{% trans "No job template created" %}
</span>
{% endif %}
</div>
</div>
</li>
<hr/>
<li>
<div>
<div>{% blocktrans %}Launch your job. When
launching, you may need to choose your input and
output data sources. This is where you would also
add any special configuration values, parameters,
or arguments that you need to pass along
to your job.
{% endblocktrans %}
</div>
<div>
<a class="btn btn-default btn-small btn-create btn-inline ajax-modal {% if not request.session.guide_job_id %} disabled{% endif %}"
href="{% url 'horizon:project:data_processing.jobs:launch-job' %}?job_id={{ request.session.guide_job_id }}">{% trans "Launch job" %}</a>
</div>
</div>
</li>
{% endif %}
</ol>
<div class="guide">
<a id="reset_cluster_guide_btn"
class=" btn btn-default btn-small btn-create btn-inline btn-danger"
title="{% trans "Reset Job Execution Guide" %}"
href="{% url 'horizon:project:data_processing.wizard:reset_jobex_guide' True %}">
{% trans "Reset Job Execution Guide" %}
</a>
</div>
</div>
{% endblock %}

View File

@ -33,6 +33,29 @@
</div>
</div>
</li>
</ul>
<li>
<div>
<div>{% blocktrans %}
In order to run a Data Processing job, you need to make
the files for your program available to the
Data Processing system, define where the input and output
need to go and create a Job Template that describes
how to run your job. Each of those steps can be done
manually or you can follow this guide to help take you
through the steps to run a job on an existing cluster.
{% endblocktrans %}
</div>
<div>
<a id="jobex_guide_btn"
class=" btn btn-default btn-small btn-create btn-inline"
title="{% trans "Job Execution Guide" %}"
href="/project/data_processing/wizard/jobex_guide">
{% trans "Job Execution Guide" %}
</a>
</div>
</div>
</li>
</ul>
</div>
{% endblock %}
{% endblock %}

View File

@ -22,6 +22,11 @@ CLUSTER_GUIDE_URL = reverse(
CLUSTER_GUIDE_RESET_URL = reverse(
'horizon:project:data_processing.wizard:reset_cluster_guide',
kwargs={"reset_cluster_guide": "true"})
JOB_GUIDE_URL = reverse(
'horizon:project:data_processing.wizard:jobex_guide')
JOB_GUIDE_RESET_URL = reverse(
'horizon:project:data_processing.wizard:reset_jobex_guide',
kwargs={"reset_jobex_guide": "true"})
class DataProcessingClusterGuideTests(test.TestCase):
@ -42,3 +47,13 @@ class DataProcessingClusterGuideTests(test.TestCase):
def test_cluster_guide_reset(self):
res = self.client.get(CLUSTER_GUIDE_RESET_URL)
self.assertRedirectsNoFollow(res, CLUSTER_GUIDE_URL)
def test_jobex_guide(self):
res = self.client.get(JOB_GUIDE_URL)
self.assertTemplateUsed(
res, 'project/data_processing.wizard/jobex_guide.html')
self.assertContains(res, 'Guided Job Execution')
def test_jobex_guide_reset(self):
res = self.client.get(JOB_GUIDE_RESET_URL)
self.assertRedirectsNoFollow(res, JOB_GUIDE_URL)

View File

@ -26,7 +26,16 @@ urlpatterns = patterns('',
url(r'^cluster_guide/(?P<reset_cluster_guide>[^/]+)/$',
views.ResetClusterGuideView.as_view(),
name='reset_cluster_guide'),
url(r'^jobex_guide$',
views.JobExecutionGuideView.as_view(),
name='jobex_guide'),
url(r'^jobex_guide/(?P<reset_jobex_guide>[^/]+)/$',
views.ResetJobExGuideView.as_view(),
name='reset_jobex_guide'),
url(r'^plugin_select$',
views.PluginSelectView.as_view(),
name='plugin_select'),
url(r'^job_type_select$',
views.JobTypeSelectView.as_view(),
name='job_type_select'),
)

View File

@ -56,9 +56,39 @@ class ResetClusterGuideView(generic.RedirectView):
return http.HttpResponseRedirect(reverse_lazy(self.pattern_name))
class JobExecutionGuideView(horizon_views.APIView):
template_name = 'project/data_processing.wizard/jobex_guide.html'
def show_data_sources(self):
try:
if self.request.session["guide_job_type"] in ["Spark", "Java"]:
return False
return True
except Exception:
return True
class ResetJobExGuideView(generic.RedirectView):
pattern_name = 'horizon:project:data_processing.wizard:jobex_guide'
def get(self, request, *args, **kwargs):
if kwargs["reset_jobex_guide"]:
hlps = helpers.Helpers(request)
hlps.reset_job_guide()
return http.HttpResponseRedirect(reverse_lazy(self.pattern_name))
class PluginSelectView(forms.ModalFormView):
form_class = wizforms.ChoosePluginForm
success_url = reverse_lazy(
'horizon:project:data_processing.wizard:cluster_guide')
classes = ("ajax-modal")
template_name = "project/data_processing.wizard/plugin_select.html"
class JobTypeSelectView(forms.ModalFormView):
form_class = wizforms.ChooseJobTypeForm
success_url = reverse_lazy(
'horizon:project:data_processing.wizard:jobex_guide')
classes = ("ajax-modal")
template_name = "project/data_processing.wizard/job_type_select.html"