瀏覽代碼

HUE-865 [jobsub] Bulk operations

Introduced knockout on the list designs page
Enrico Berti 13 年之前
父節點
當前提交
cb312c2

+ 60 - 95
apps/jobsub/src/jobsub/templates/list_designs.mako

@@ -17,9 +17,6 @@
 <%!
 import cgi
 import urllib
-import time as py_time
-from django.template.defaultfilters import date, time
-from desktop.lib.django_util import extract_field_data
 from desktop.views import commonheader, commonfooter
 from django.utils.translation import ugettext as _
 %>
@@ -30,17 +27,28 @@ ${commonheader(_('Job Designer'), "jobsub", user, "100px")}
 ${layout.menubar(section='designs')}
 
 <script src="/static/ext/js/datatables-paging-0.1.js" type="text/javascript" charset="utf-8"></script>
+<script src="/static/ext/js/knockout-2.0.0.js" type="text/javascript" charset="utf-8"></script>
+<script src="/static/ext/js/moment.min.js" type="text/javascript" charset="utf-8"></script>
+<script src="static/js/jobsub.ko.js" type="text/javascript" charset="utf-8"></script>
 
 <div class="container-fluid">
     <h1>${_('Job Designs')}</h1>
 
     <%actionbar:render>
+        <%def name="actions()">
+            <button class="btn" title="${_('Submit')}" data-bind="click: submitDesign, enable: selectedDesigns().length == 1 && selectedDesigns()[0].canSubmit"><i class="icon-play"></i> ${_('Submit')}</button>
+            <button class="btn" title="${_('Edit')}" data-bind="click: editDesign, enable: selectedDesigns().length == 1 && selectedDesigns()[0].canSubmit"><i class="icon-pencil"></i> ${_('Edit')}</button>
+            <button class="btn" title="${_('Delete')}" data-bind="click: deleteDesign, enable: selectedDesigns().length == 1 && selectedDesigns()[0].canDelete"><i class="icon-trash"></i> ${_('Delete')}</button>
+            <button class="btn" title="${_('Clone')}" data-bind="click: cloneDesign, enable: selectedDesigns().length == 1"><i class="icon-share"></i> ${_('Clone')}</button>
+        </%def>
         <%def name="creation()">
-            <a href="${ url('jobsub.views.new_design', action_type='mapreduce') }" class="btn">${_('Create Mapreduce Design')}</a>
-            <a href="${ url('jobsub.views.new_design', action_type='streaming') }" class="btn">${_('Create Streaming Design')}</a>
-            <a href="${ url('jobsub.views.new_design', action_type='java') }" class="btn">${_('Create Java Design')}</a>
+            <span class="btn-group">
+                <a href="${ url('jobsub.views.new_design', action_type='mapreduce') }" class="btn" title="${_('Create Mapreduce Design')}" rel="tooltip"><i class="icon-plus-sign"></i> ${_('Mapreduce')}</a>
+                <a href="${ url('jobsub.views.new_design', action_type='streaming') }" class="btn" title="${_('Create Streaming Design')}" rel="tooltip"><i class="icon-plus-sign"></i> ${_('Streaming')}</a>
+                <a href="${ url('jobsub.views.new_design', action_type='java') }" class="btn"title="${_('Create Java Design')}" rel="tooltip"><i class="icon-plus-sign"></i> ${_('Java')}</a>
+            </span>
             %if show_install_examples:
-            <a id="installSamplesLink" href="javascript:void(0)" data-confirmation-url="${url('jobsub.views.setup')}" class="btn" >${_('Install Samples')}</a>
+            &nbsp; <a id="installSamplesLink" href="javascript:void(0)" data-confirmation-url="${url('jobsub.views.setup')}" class="btn"><i class="icon-download-alt"></i> ${_('Install Samples')}</a>
             %endif
         </%def>
     </%actionbar:render>
@@ -48,50 +56,37 @@ ${layout.menubar(section='designs')}
     <table id="designTable" class="table table-condensed datatables">
         <thead>
         <tr>
+            <th width="1%"><div data-bind="click: selectAll, css: {hueCheckbox: true, 'icon-ok': allSelected}"></div></th>
             <th>${_('Owner')}</th>
             <th>${_('Name')}</th>
             <th>${_('Type')}</th>
             <th>${_('Description')}</th>
             <th>${_('Last Modified')}</th>
-            <th nowrap="nowrap">&nbsp;</th>
         </tr>
         </thead>
-        <tbody>
-                %for design in designs:
-                <tr>
-                    <td>${design.owner.username}</td>
-                    <td>${design.name}</td>
-                    <td>${design.root_action.action_type}</td>
-                    <td>${design.description}</td>
-                    <td nowrap="nowrap" data-sort-value="${py_time.mktime(design.last_modified.timetuple())}">${date(design.last_modified)} ${time(design.last_modified).replace("p.m.","PM").replace("a.m.","AM")}</td>
-                <td nowrap="nowrap" class="right">
-                %if currentuser.username == design.owner.username:
-                    <a title="${_('Submit %(name)s to the cluster') % dict(name=design.name)}" class="btn small submitConfirmation"
-                       alt="${_('Submit %(name)s to the cluster') % dict(name=design.name)}"
-                       href="javascript:void(0)"
-                       data-param-url="${ url('jobsub.views.get_design_params', design_id=design.id) }"
-                       data-submit-url="${ url('jobsub.views.submit_design', design_id=design.id) }">${_('Submit')}</a>
-                    <a title="${_('Edit %(name)s') % dict(name=design.name)}" class="btn small"
-                       href="${ url('jobsub.views.edit_design', design_id=design.id) }" data-row-selector="true">${_('Edit')}</a>
-                %endif
-                %if currentuser.is_superuser or currentuser.username == design.owner.username:
-                    <a title="${_('Delete %(name)s') % dict(name=design.name)}" class="btn small deleteConfirmation"
-                       alt="${_('Are you sure you want to delete %(name)s?') % dict(name=design.name)}"
-                       href="javascript:void(0)"
-                       data-confirmation-url="${ url('jobsub.views.delete_design', design_id=design.id) }">${_('Delete')}</a>
-                %endif
-                    <a title="${_('Clone %(name)s') % dict(name=design.name)}" class="btn small" href="${ url('jobsub.views.clone_design', design_id=design.id) }">${_('Clone')}</a>
-                </td>
-                </tr>
-                %endfor
+        <tbody id="designs" data-bind="template: {name: 'designTemplate', foreach: designs}">
+
         </tbody>
     </table>
 
 </div>
 
+<script id="designTemplate" type="text/html">
+    <tr style="cursor: pointer">
+        <td class="center" data-bind="click: handleSelect" style="cursor: default">
+            <div data-bind="visible: name != '..', css: {hueCheckbox: name != '..', 'icon-ok': selected}"></div>
+        </td>
+        <td data-bind="click: $root.editDesign, text: owner"></td>
+        <td data-bind="click: $root.editDesign, text: name"></td>
+        <td data-bind="click: $root.editDesign, text: type"></td>
+        <td data-bind="click: $root.editDesign, text: description"></td>
+        <td data-bind="click: $root.editDesign, text: lastModified, attr: { 'data-sort-value': lastModifiedMillis }" style="white-space: nowrap;"></td>
+    </tr>
+</script>
+
 
 <div id="submitWf" class="modal hide fade">
-    <form id="submitWfForm" action="" method="POST">
+    <form id="submitWfForm" action="" method="POST" style="margin:0">
         <div class="modal-header">
             <a href="#" class="close" data-dismiss="modal">&times;</a>
             <h3 id="submitWfMessage">${_('Submit this design?')}</h3>
@@ -104,26 +99,26 @@ ${layout.menubar(section='designs')}
         </div>
         <div class="modal-footer">
             <input id="submitBtn" type="submit" class="btn primary" value="${_('Submit')}"/>
-            <a href="#" class="btn secondary hideModal">${_('Cancel')}</a>
+            <a href="#" class="btn secondary" data-dismiss="modal">${_('Cancel')}</a>
         </div>
     </form>
 </div>
 
 <div id="deleteWf" class="modal hide fade">
-    <form id="deleteWfForm" action="" method="POST">
+    <form id="deleteWfForm" action="" method="POST" style="margin:0">
         <div class="modal-header">
             <a href="#" class="close" data-dismiss="modal">&times;</a>
             <h3 id="deleteWfMessage">${_('Delete this design?')}</h3>
         </div>
         <div class="modal-footer">
             <input type="submit" class="btn primary" value="${_('Yes')}"/>
-            <a href="#" class="btn secondary hideModal">${_('No')}</a>
+            <a href="#" class="btn secondary" data-dismiss="modal">${_('No')}</a>
         </div>
     </form>
 </div>
 
 <div id="installSamples" class="modal hide fade">
-    <form id="installSamplesForm" action="${url('jobsub.views.setup')}" method="POST">
+    <form id="installSamplesForm" action="${url('jobsub.views.setup')}" method="POST" style="margin:0">
         <div class="modal-header">
             <a href="#" class="close" data-dismiss="modal">&times;</a>
             <h3>${_('Install sample job designs?')}</h3>
@@ -140,67 +135,44 @@ ${layout.menubar(section='designs')}
 
 
 <script type="text/javascript" charset="utf-8">
+
+    var deleteMessage = "${_('Are you sure you want to delete %(name)s?') % dict(name='##PLACEHOLDER##')}";
+    var submitMessage = "${_('Submit %(name)s to the cluster') % dict(name='##PLACEHOLDER##')}";
+
     $(document).ready(function() {
 
-        $(".deleteConfirmation").click(function(){
-            var _this = $(this);
-            var _action = _this.attr("data-confirmation-url");
-            $("#deleteWfForm").attr("action", _action);
-            $("#deleteWfMessage").text(_this.attr("alt"));
-            $("#deleteWf").modal("show");
-        });
-        $("#deleteWf .hideModal").click(function(){
-            $("#deleteWf").modal("hide");
-        });
+        var designTable, viewModel;
 
-        $(".submitConfirmation").click(function(){
-            var _this = $(this);
-            var _action = _this.attr("data-submit-url");
-            $("#submitWfForm").attr("action", _action);
-            $("#submitWfMessage").text(_this.attr("alt"));
-            // We will show the model form, but disable the submit button
-            // until we've finish loading the parameters via ajax.
-            $("#submitBtn").attr("disabled", "disabled");
-            $("#submitWf").modal("show");
-
-            $.get(_this.attr("data-param-url"), function(data) {
-                var params = data["params"]
-                var container = $("#param-container");
-                container.empty();
-                for (key in params) {
-                    if (!params.hasOwnProperty(key)) {
-                        continue;
-                    }
-                    container.append(
-                            $("<div/>").addClass("clearfix")
-                                    .append($("<label/>").text(params[key]))
-                                    .append(
-                                    $("<div/>").addClass("input")
-                                            .append($("<input/>").attr("name", key).attr("type", "text"))
-                            )
-                    )
-                }
-                // Good. We can submit now.
-                $("#submitBtn").removeAttr("disabled");
-            }, "json");
+        $("#filterInput").keyup(function() {
+            if (designTable != null){
+                designTable.fnFilter($(this).val());
+            }
         });
-        $("#submitWf .hideModal").click(function(){
-            $("#submitWf").modal("hide");
+
+        $("#installSamplesLink").click(function(){
+            $("#installSamples").modal("show");
         });
 
-        var oTable = $('#designTable').dataTable( {
+        viewModel = new JobSubModel(${designs});
+        ko.applyBindings(viewModel);
+        designTable = $('#designTable').dataTable( {
             "sPaginationType": "bootstrap",
             "bLengthChange": false,
             "sDom": "<'row'r>t<'row'<'span8'i><''p>>",
             "aoColumns": [
+                { "bSortable": false },
                 null,
                 null,
                 null,
                 null,
-                { "sSortDataType": "dom-sort-value", "sType": "numeric" },
-                { "bSortable": false }
+                { "sSortDataType": "dom-sort-value", "sType": "numeric" }
             ],
-            "aaSorting": [[ 4, "desc" ]],
+            "aaSorting": [[ 5, "desc" ]],
+            "fnPreDrawCallback": function( oSettings ) {
+                if (viewModel.allSelected()){
+                    viewModel.selectAll();
+                }
+            },
             "oLanguage": {
                 "sEmptyTable":     "${_('No data available in table')}",
                 "sInfo":           "${_('Showing _START_ to _END_ of _TOTAL_ entries')}",
@@ -216,14 +188,7 @@ ${layout.menubar(section='designs')}
             }
         });
 
-        $("#filterInput").keyup(function() {
-            oTable.fnFilter($(this).val());
-        });
-
-        $("#installSamplesLink").click(function(){
-            $("#installSamples").modal("show");
-        });
-
+        $(".btn[rel='tooltip']").tooltip({placement:'bottom'});
         $("a[data-row-selector='true']").jHueRowSelector();
     });
 </script>

+ 6 - 6
apps/jobsub/src/jobsub/tests.py

@@ -134,29 +134,29 @@ def test_job_design_cycle():
   assert_false('This field is required' in response)
 
   # Now check list
-  response = c.get('/jobsub/')
+  response = c.get('/jobsub')
   for design in OozieDesign.objects.all():
     assert_true(design.name in response.content, response.content)
 
   # With some filters
-  response = c.get('/jobsub/', dict(name='name-1'))
+  response = c.get('/jobsub', dict(name='name-1'))
   assert_true('name-1' in response.content, response.content)
   assert_false('name-2' in response.content, response.content)
 
-  response = c.get('/jobsub/', dict(owner='doesnotexist'))
+  response = c.get('/jobsub', dict(owner='doesnotexist'))
   assert_false('doesnotexist' in response.content)
 
-  response = c.get('/jobsub/', dict(owner='test', name='name-1'))
+  response = c.get('/jobsub', dict(owner='test', name='name-1'))
   assert_true('name-1' in response.content, response.content)
   assert_false('name-2' in response.content, response.content)
 
-  response = c.get('/jobsub/', dict(name="name"))
+  response = c.get('/jobsub', dict(name="name"))
   assert_true('name-1' in response.content, response.content)
   assert_true('name-2' in response.content, response.content)
   assert_false('doesnotexist' in response.content, response.content)
 
   # Combined filters
-  response = c.get('/jobsub/', dict(owner="test", name="name-2"))
+  response = c.get('/jobsub', dict(owner="test", name="name-2"))
   assert_false('name-1' in response.content, response.content)
   assert_true('name-2' in response.content, response.content)
   assert_false('doesnotexist' in response.content, response.content)

+ 26 - 7
apps/jobsub/src/jobsub/views.py

@@ -29,7 +29,7 @@ try:
 except ImportError:
   import simplejson as json
 import logging
-
+import time as py_time
 
 from django.core import urlresolvers
 from django.shortcuts import redirect
@@ -48,8 +48,8 @@ import jobsub.forms
 
 from django.utils.translation import ugettext as _
 
-LOG = logging.getLogger(__name__)
 
+LOG = logging.getLogger(__name__)
 
 def oozie_job(request, jobid):
   """View the details about this job."""
@@ -144,25 +144,44 @@ def list_designs(request):
     name        - Substring filter by design name field
   '''
   data = models.OozieDesign.objects
-  owner = request.GET.get("owner", '')
+  owner = request.GET.get('owner', '')
   name = request.GET.get('name', '')
   if owner:
-    data = data.filter(owner__username__icontains=owner)
+      data = data.filter(owner__username__icontains=owner)
   if name:
-    data = data.filter(name__icontains=name)
+      data = data.filter(name__icontains=name)
   data = data.order_by('-last_modified')
+
   show_install_examples = \
       request.user.is_superuser and not jobsub_setup.Command().has_been_setup()
 
+  designs = []
+  for design in data:
+      ko_design = {
+          'id': design.id,
+          'owner': design.owner.username,
+          'name': design.name,
+          'description': design.description,
+          'type': design.root_action.action_type,
+          'last_modified': py_time.mktime(design.last_modified.timetuple()),
+          'url_params': urlresolvers.reverse(jobsub.views.get_design_params, kwargs={'design_id': design.id}),
+          'url_submit': urlresolvers.reverse(jobsub.views.submit_design, kwargs={'design_id': design.id}),
+          'url_edit': urlresolvers.reverse(jobsub.views.edit_design, kwargs={'design_id': design.id}),
+          'url_delete': urlresolvers.reverse(jobsub.views.delete_design, kwargs={'design_id': design.id}),
+          'url_clone': urlresolvers.reverse(jobsub.views.clone_design, kwargs={'design_id': design.id}),
+          'can_submit': request.user.username == design.owner.username,
+          'can_delete': request.user.is_superuser or request.user.username == design.owner.username
+      }
+      designs.append(ko_design)
+
   return render("list_designs.mako", request, {
-    'designs': list(data),
     'currentuser':request.user,
     'owner': owner,
     'name': name,
+    'designs': json.dumps(designs),
     'show_install_examples': show_install_examples,
   })
 
-
 def _get_design(design_id):
   """Raise PopupException if design doesn't exist"""
   try:

+ 120 - 0
apps/jobsub/static/js/jobsub.ko.js

@@ -0,0 +1,120 @@
+// Licensed to Cloudera, Inc. under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  Cloudera, Inc. licenses this file
+// to you 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.
+
+
+var Design = function (design) {
+    return {
+        id:design.id,
+        owner:design.owner,
+        name:design.name,
+        description:design.description,
+        type:design.type,
+        lastModifiedMillis:design.last_modified,
+        lastModified:moment.unix(design.last_modified).format("MMMM DD, YYYY hh:mm a"),
+        paramsUrl:design.url_params,
+        submitUrl:design.url_submit,
+        editUrl:design.url_edit,
+        deleteUrl:design.url_delete,
+        cloneUrl:design.url_clone,
+        canSubmit:design.can_submit,
+        canDelete:design.can_delete,
+        selected:ko.observable(false),
+        handleSelect:function (row, e) {
+            this.selected(!this.selected());
+        }
+    }
+}
+
+
+var JobSubModel = function (designs) {
+
+    var self = this;
+
+    self.designs = ko.observableArray(ko.utils.arrayMap(designs, function (design) {
+        return new Design(design);
+    }));
+
+    self.isLoading = ko.observable(true);
+
+    self.allSelected = ko.observable(false);
+
+    self.selectedDesigns = ko.computed(function () {
+        return ko.utils.arrayFilter(self.designs(), function (design) {
+            return design.selected();
+        });
+    }, self);
+
+    self.selectedDesign = ko.computed(function () {
+        return self.selectedDesigns()[0];
+    }, self);
+
+    self.selectAll = function () {
+        self.allSelected(!self.allSelected());
+        ko.utils.arrayForEach(self.designs(), function (design) {
+            design.selected(self.allSelected());
+        });
+        return true;
+    };
+
+    self.cloneDesign = function () {
+        location.href = self.selectedDesign().cloneUrl;
+    };
+
+    self.editDesign = function (design) {
+        if (design.editUrl == null) {
+            design = self.selectedDesign();
+        }
+        if (design != null && design.canSubmit) {
+            location.href = design.editUrl;
+        }
+    };
+
+    self.deleteDesign = function () {
+        $("#deleteWfForm").attr("action", self.selectedDesign().deleteUrl);
+        $("#deleteWfMessage").text(deleteMessage.replace("##PLACEHOLDER##", self.selectedDesign().name));
+        $("#deleteWf").modal("show");
+    };
+
+    self.submitDesign = function () {
+        $("#submitWfForm").attr("action", self.selectedDesign().submitUrl);
+        $("#submitWfMessage").text(submitMessage.replace("##PLACEHOLDER##", self.selectedDesign().name));
+        // We will show the model form, but disable the submit button
+        // until we've finish loading the parameters via ajax.
+        $("#submitBtn").attr("disabled", "disabled");
+        $("#submitWf").modal("show");
+
+        $.get(self.selectedDesign().paramsUrl, function (data) {
+            var params = data["params"]
+            var container = $("#param-container");
+            container.empty();
+            for (key in params) {
+                if (!params.hasOwnProperty(key)) {
+                    continue;
+                }
+                container.append(
+                    $("<div/>").addClass("clearfix")
+                        .append($("<label/>").text(params[key]))
+                        .append(
+                        $("<div/>").addClass("input")
+                            .append($("<input/>").attr("name", key).attr("type", "text"))
+                    )
+                )
+            }
+            // Good. We can submit now.
+            $("#submitBtn").removeAttr("disabled");
+        }, "json");
+    };
+};