Bläddra i källkod

HUE-1812 [spark] UX improvements

General style fixes
Fix error when no uploaded apps
Load app name from the Application page
Bubble up error when opening a non existing job
Romain Rigaux 12 år sedan
förälder
incheckning
7d84893

+ 3 - 3
apps/beeswax/src/beeswax/templates/layout.mako

@@ -41,13 +41,13 @@ def is_selected(section, matcher):
                 ${ _('Spark Igniter') }
               </a>
             </li>
-            <li class="${is_selected(section, 'query')}"><a href="${ url('spark:editor') }">${_('App Runner')}</a></li>
+            <li class="${is_selected(section, 'query')}"><a href="${ url('spark:editor') }">${_('Editor')}</a></li>
             ##<li class="${is_selected(section, 'my queries')}"><a href="${ url(app_name + ':my_queries') }">${_('My Queries')}</a></li>
-            <li class="${is_selected(section, 'saved queries')}"><a href="${ url('spark:list_designs') }">${_('App Configurations')}</a></li>
+            <li class="${is_selected(section, 'saved queries')}"><a href="${ url('spark:list_designs') }">${_('Applications')}</a></li>
             ##<li class="${is_selected(section, 'history')}"><a href="${ url('spark:list_query_history') }">${_('History')}</a></li>
             <li class="${is_selected(section, 'jobs')}"><a href="${ url('spark:list_jobs') }">${_('Dashboard')}</a></li>
             <li class="${is_selected(section, 'contexts')}"><a href="${ url('spark:list_contexts') }">${_('Contexts')}</a></li>
-            <li class="${is_selected(section, 'applications')}"><a href="${ url('spark:list_applications') }">${_('Uploaded Apps')}</a></li>
+            <li class="${is_selected(section, 'applications')}"><a href="${ url('spark:list_applications') }">${_('Uploads')}</a></li>
           </ul>
         </div>
       </div>

+ 1 - 1
apps/beeswax/src/beeswax/templates/list_trashed_designs.mako

@@ -219,7 +219,7 @@ ${layout.menubar(section='saved queries')}
       $("#deleteQueryMessage").text("${ _('Empty the trash?') }");
 
       viewModel.chosenSavedQueries.removeAll();
-      $.each(viewModel.availableSavedContexts(), function(index, query) {
+      $.each(viewModel.availableSavedQueries(), function(index, query) {
         viewModel.chosenSavedQueries.push(query);
       });
 

+ 11 - 8
apps/spark/src/spark/api.py

@@ -31,6 +31,7 @@ from spark.job_server_api import get_api
 from spark.forms import SparkForm, QueryForm
 from desktop.lib.i18n import smart_str
 from spark.design import SparkDesign
+from desktop.lib.rest.http_client import RestException
 
 
 LOG = logging.getLogger(__name__)
@@ -80,11 +81,12 @@ def create_context(request):
   api = get_api(request.user)
   try:
     response = api.create_context(name, memPerNode=memPerNode, numCores=numCores)
+  except ValueError:
+    # No json is returned
+    response = {'status': 'OK'}
   except Exception, e:
-    if e.message != 'No JSON object could be decoded':
-      response = json.loads(e.message)
-    else:
-      response = {'status': 'OK'}
+    response = json.loads(e.message)
+
   response['name'] = name
 
   return HttpResponse(json.dumps(response), mimetype="application/json")
@@ -113,10 +115,11 @@ def delete_context(request):
 
 def job(request, job_id):
   api = get_api(request.user)
-
-  response = {
-    'results': api.job(job_id)
-  }
+  response = {}
+  try:
+    response['results'] = api.job(job_id)
+  except RestException, e:
+    response['results'] = json.loads(e.message)
 
   return HttpResponse(json.dumps(response), mimetype="application/json")
 

+ 1 - 1
apps/spark/src/spark/design.py

@@ -108,7 +108,7 @@ class SparkDesign(object):
     if 'context' not in dic['query']:
       dic['query']['context'] = ''
     if 'params' not in dic['query']:
-      dic['query']['params'] = ''
+      dic['query']['params'] = json.dumps([])
 
     design = SparkDesign()
     design._data_dict = dic

+ 3 - 4
apps/spark/src/spark/templates/common.mako

@@ -38,14 +38,13 @@ def is_selected(section, matcher):
                 ${ _('Spark Igniter') }
               </a>
             </li>
-            <li class="${is_selected(section, 'editor')}"><a href="${ url('spark:editor') }">${_('App Runner')}</a></li>
+            <li class="${is_selected(section, 'editor')}"><a href="${ url('spark:editor') }">${_('Editor')}</a></li>
             ##<li class="${is_selected(section, 'my queries')}"><a href="${ url(app_name + ':my_queries') }">${_('My Queries')}</a></li>
-            <li class="${is_selected(section, 'queries')}"><a href="${ url('spark:list_designs') }">${_('App Configurations')}</a></li>
+            <li class="${is_selected(section, 'saved queries')}"><a href="${ url('spark:list_designs') }">${_('Applications')}</a></li>
             ##<li class="${is_selected(section, 'history')}"><a href="${ url('spark:list_query_history') }">${_('History')}</a></li>
-
             <li class="${is_selected(section, 'jobs')}"><a href="${ url('spark:list_jobs') }">${_('Dashboard')}</a></li>
             <li class="${is_selected(section, 'contexts')}"><a href="${ url('spark:list_contexts') }">${_('Contexts')}</a></li>
-            <li class="${is_selected(section, 'applications')}"><a href="${ url('spark:list_applications') }">${_('Uploaded Apps')}</a></li>
+            <li class="${is_selected(section, 'applications')}"><a href="${ url('spark:list_applications') }">${_('Uploads')}</a></li>
           </ul>
         </div>
       </div>

+ 92 - 32
apps/spark/src/spark/templates/editor.mako

@@ -29,27 +29,27 @@ ${ common.navbar('editor') }
   <div class="row-fluid">
     <div class="card card-small">
       <div style="margin-bottom: 10px">
+        % if can_edit_name:
         <h1 class="card-heading simple">
           <a href="javascript:void(0);"
              id="query-name"
              data-type="text"
              data-name="name"
              data-value="${design.name}"
-             data-original-title="${ _('App name') }"
+             data-original-title="${ _('Application name') }"
              data-placement="right">
           </a>
-        </h1>
-        % if can_edit_name:
-          <p style="margin-left: 20px">
-            <a href="javascript:void(0);"
+
+          <a href="javascript:void(0);"
              id="query-description"
              data-type="textarea"
              data-name="description"
              data-value="${design.desc}"
-             data-original-title="${ _('App description') }"
-             data-placement="right">
+             data-original-title="${ _('Application description') }"
+             data-placement="right" style="font-size: 14px; margin-left: 10px">
           </a>
-          </p>
+
+        </h1>
         % endif
       </div>
       <div class="card-body">
@@ -66,8 +66,10 @@ ${ common.navbar('editor') }
                 ${ _("Missing, add one?") }
               </a>
               <!-- /ko -->
-              <a data-bind="if: $root.appName" data-toggle="dropdown" href="javascript:void(0);"><strong data-bind="text: $root.appName().nice_name"></strong>
-                <i class="fa fa-caret-down"></i></a>
+              <a data-bind="if: $root.appName" data-toggle="dropdown" href="javascript:void(0);">
+                <strong data-bind="text: $root.appName().nice_name"></strong>
+                <i class="fa fa-caret-down"></i>
+              </a>
               <ul data-bind="foreach: $root.appNames" class="dropdown-menu">
                 <li data-bind="click: $root.chooseAppName, text: nice_name" class="selectable"></li>
               </ul>
@@ -79,9 +81,9 @@ ${ common.navbar('editor') }
             </label>
 
             ${_('Context')}&nbsp;
-            <label class="radio" style="margin-left: 5px;margin-top:-2px">
+            <label class="radio" style="margin-top:-2px">
+              <input type="radio" name="autoContext" value="true" data-bind="checked:$root.autoContext.forEditing" style="margin-right: 5px" />
               ${ _('Automatic') }
-              <input type="radio" name="autoContext" value="true" data-bind="checked:$root.autoContext.forEditing" />
             </label>
             <label class="radio" style="margin-left: 5px;margin-top:-2px">
               <span class="dropdown" style="padding-left: 5px">
@@ -108,17 +110,21 @@ ${ common.navbar('editor') }
 
   <div class="row-fluid">
     <div class="card card-small">
-      <h1 class="card-heading simple">${_('Parameters')}</h1>
+      <h1 class="card-heading simple">
+        <a id="collapse-editor" href="javascript:void(0)" class="pull-right"><i class="fa fa-caret-up"></i></a>
+        ${_('Parameters')}
+      </h1>
       <div class="card-body">
         <p>
+          <div id="param-pane">
           <div data-bind="css: {'hide': query.errors().length == 0}" class="hide alert alert-error">
-            <p><strong>${_('Your app configuration has the following error(s):')}</strong></p>
+            <p><strong>${_('Your application has the following error(s):')}</strong></p>
             <div data-bind="foreach: query.errors">
               <p data-bind="text: $data" class="queryErrorMessage"></p>
             </div>
           </div>
 
-          <div data-bind="visible: query.params().length == 0">${_('There currently no parameters defined.')}</div>
+          <div data-bind="visible: query.params().length == 0">${_('No parameters defined yet.')}</div>
 
           <table class="table-condensed">
             <thead data-bind="visible: query.params().length > 0">
@@ -130,33 +136,39 @@ ${ common.navbar('editor') }
             </thead>
             <tbody data-bind="foreach: query.params">
               <tr>
-                <td><input type="text" class="span6 required propKey" data-bind="value: name" /></td>
-                <td><input type="text" class="span6 required pathChooserKo" data-bind="fileChooser: $data, value: value" /></td>
+                <td><input type="text" class="input-large required propKey" data-bind="value: name" /></td>
                 <td>
+                <div class="input-append">
+                  <input type="text" data-bind="value: value" class="input-xxlarge" />
+                  <button class="btn fileChooserBtn" data-bind="click: $root.showFileChooser">..</button>
+                </div>
+                </td>
+                <td style="vertical-align: top">
                   <a class="btn" href="#" data-bind="click: $root.removeParam">${ _('Delete') }</a>
                 </td>
               </tr>
             </tbody>
           </table>
+          <div style="margin-top: 10px">
+            <a class="btn" href="#" data-bind="click: $root.addParam">${ _('Add') }</a>
+          </div>
 
-          <a class="btn" href="#" data-bind="click: $root.addParam">${ _('Add parameter') }</a>
-
-          <div class="actions">
+          <div class="actions" style="margin-top: 50px; margin-bottom: 20px">
             <button data-bind="click: tryExecuteQuery, enable: $root.appNames().length > 0 && $root.classPath()"
               type="button" id="executeQuery" class="btn btn-primary disable-feedback"
               tabindex="2" data-loading-text="${ _("Executing...") }">
               ${_('Execute')}
             </button>
-            <button data-bind="click: trySaveQuery, css: {'hide': !$root.query.id() || $root.query.id() == -1}" type="button" class="btn hide">${_('Save')}</button>
-            <button data-bind="click: trySaveAsQuery" type="button" class="btn">${_('Save as...')}</button>
-            &nbsp; ${_('or create a')} &nbsp;<a type="button" class="btn" href="${ url('spark:editor') }">${_('New app configuration')}</a>
+            <button data-bind="click: trySaveQuery, visible: ($root.query.id() && $root.query.id() != -1)" type="button" class="btn">${_('Save')}</button>
+            <button data-bind="click: trySaveAsQuery, visible: $root.appNames().length > 0" type="button" class="btn">${_('Save as...')}</button>
+            &nbsp; ${_('or create a')} &nbsp;<a type="button" class="btn" href="${ url('spark:editor') }">${_('New application')}</a>
             <span class="pull-right">
               <a type="button" class="btn" data-bind="visible: rows().length != 0, attr: {'href': '${ url('spark:download_result') }' + $root.query.jobId()}">${_('Download')}</a>
             </span>
           </div>
-
+        </div>
           <div data-bind="css: {'hide': rows().length == 0}" class="hide">
-            <div class="card card-small scrollable">
+            <div class="scrollable">
               <table class="table table-striped table-condensed resultTable" cellpadding="0" cellspacing="0" data-tablescroller-min-height-disable="true" data-tablescroller-enforce-height="true">
                 <thead>
                   <tr>
@@ -169,7 +181,7 @@ ${ common.navbar('editor') }
           </div>
 
           <div data-bind="css: {'hide': !resultsEmpty()}" class="hide">
-            <div class="card card-small scrollable">
+            <div class="scrollable">
               <div class="row-fluid">
                 <div class="span10 offset1 center empty-wrapper">
                   <i class="fa fa-frown-o"></i>
@@ -181,7 +193,7 @@ ${ common.navbar('editor') }
           </div>
 
           <div id="wait-info" class="hide">
-            <div class="card card-small scrollable">
+            <div class="scrollable">
               <div class="row-fluid">
                 <div class="span10 offset1 center" style="padding: 30px">
                   <!--[if !IE]><!--><i class="fa fa-spinner fa-spin" style="font-size: 60px; color: #DDD"></i><!--<![endif]-->
@@ -224,6 +236,20 @@ ${ common.navbar('editor') }
   </div>
 </div>
 
+<div id="chooseFile" class="modal hide fade">
+  <div class="modal-header">
+      <a href="#" class="close" data-dismiss="modal">&times;</a>
+      <h3>${_('Choose a file')}</h3>
+  </div>
+  <div class="modal-body">
+      <div id="filechooser">
+      </div>
+  </div>
+  <div class="modal-footer">
+  </div>
+</div>
+
+
 ${ common.uploadAppModal() }
 ${ common.createContextModal() }
 
@@ -376,10 +402,42 @@ ${ common.createContextModal() }
       viewModel.openQuery("${ job_id }");
     % endif
     var hash = window.location.hash;
-    if (hash != "" && hash.indexOf("=") > -1 && hash.split("=")[0] == '#jobId') {
-      viewModel.openQuery(hash.split("=")[1]);
+    if (hash != "" && hash.indexOf("=") > -1) {
+      var hashKey = hash.split("=")[0].substr(1);
+      var hashValue = hash.split("=")[1];
+      if (hashKey == 'jobId') {
+        viewModel.openQuery(hashValue);
+      } else if (hashKey == 'applicationId') {
+        viewModel.query.appName(hashValue);
+      }
+
     }
     ko.applyBindings(viewModel);
+
+    $("#collapse-editor").on("click", function () {
+      if ($("#param-pane").is(":visible")) {
+        $("#param-pane").slideUp(100, function () {
+          $(".dataTables_wrapper").jHueTableScroller();
+          $(".resultTable").jHueTableExtender({
+            hintElement: "#jumpToColumnAlert",
+            fixedHeader: true,
+            firstColumnTooltip: true
+          });
+        });
+        $("#collapse-editor i").removeClass("fa-caret-up").addClass("fa-caret-down");
+      }
+      else {
+        $("#param-pane").slideDown(100, function () {
+          $(".dataTables_wrapper").jHueTableScroller();
+          $(".resultTable").jHueTableExtender({
+            hintElement: "#jumpToColumnAlert",
+            fixedHeader: true,
+            firstColumnTooltip: true
+          });
+        });
+        $("#collapse-editor i").removeClass("fa-caret-down").addClass("fa-caret-up");
+      }
+    });
   });
 
 
@@ -401,7 +459,9 @@ ${ common.createContextModal() }
   }
 
   function trySaveAsQuery() {
-    $('#saveAsQueryModal').modal('show');
+    if (viewModel.appName() != null && viewModel.appName() != ""){
+      $('#saveAsQueryModal').modal('show');
+    }
   }
 
   $('.uploadAppModalBtn').click(function(){
@@ -433,7 +493,7 @@ ${ common.createContextModal() }
     success: function(response, newValue) {
       viewModel.query.name(newValue);
     },
-    emptytext: "${ _('App configuration name') }"
+    emptytext: "${ _('Application name') }"
   });
 
   $("#query-description").editable({
@@ -446,7 +506,7 @@ ${ common.createContextModal() }
 
   // Events and datatables
   $(document).on('saved.query', function() {
-    $.jHueNotify.info("${_('App saved successfully!')}")
+    $.jHueNotify.info("${_('Application saved successfully!')}")
   });
 
   var dataTable = null;

+ 8 - 3
apps/spark/src/spark/templates/list_applications.mako

@@ -47,12 +47,16 @@ ${ common.navbar('applications') }
       </tr>
     </thead>
     <tbody>
-      % for name, ts in applications.iteritems():
+    % for name, ts in applications.iteritems():
       <tr>
-        <td>${ name }</td>
+        <td>
+          <a href="${ url('spark:editor') }#applicationId=${ name }" data-row-selector="true" title="${ _('Click to open and execute') }">
+            ${ name }
+          </a>
+        </td>
         <td>${ ts }</td>
       </tr>
-      % endfor
+    % endfor
 
     </tbody>
   </table>
@@ -100,6 +104,7 @@ ${ common.uploadAppModal() }
       $("#uploadAppModal").modal("show");
     });
 
+    $("a[data-row-selector='true']").jHueRowSelector();
   });
 </script>
 

+ 1 - 1
apps/spark/src/spark/templates/list_jobs.mako

@@ -48,7 +48,7 @@ ${ common.navbar('jobs') }
               can_view = job.get('status') == 'FINISHED'
             %>
             % if can_view:
-              <a href="${ url('spark:view_job', job.get('jobId')) }" data-row-selector="true">
+              <a href="${ url('spark:view_job', job.get('jobId')) }" data-row-selector="true" title="${ _('Click to open') }">
             % endif
             ${ job.get('jobId') }
             % if can_view:

+ 39 - 9
apps/spark/static/js/spark.vm.js

@@ -14,6 +14,12 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
+var SparkParameter = function (property) {
+  var self = this;
+
+  self.name = ko.observable(property.name);
+  self.value = ko.observable(property.value);
+};
 
 function sparkViewModel() {
   var self = this;
@@ -45,7 +51,7 @@ function sparkViewModel() {
     'classPath': '',
     'context': '',
     'autoContext': true,
-    'params': [],
+    'params': []
   });
 
   self.rows = ko.observableArray();
@@ -109,9 +115,9 @@ function sparkViewModel() {
     });
     self.appNames(newAppNames);
 
-    var last = newAppNames.length > 0 ? newAppNames[0].name() : null;
-    if (last) {
-      self.appName(last);
+    // Load back appName or guess
+    if (self.query.appName()) {
+      viewModel.setAppName( self.query.appName());
     }
   };
 
@@ -129,7 +135,7 @@ function sparkViewModel() {
   };
 
   self.addParam = function() {
-    self.query.params.push({name: "", value: ""});
+    self.query.params.push(new SparkParameter({name: "", value: ""}));
   };
 
   self.removeParam = function() {
@@ -154,7 +160,7 @@ function sparkViewModel() {
     self.query.params(design.params);
 
     self.appName(design.appName);
-    self.chooseAppName(design.appName);
+    self.chooseAppName(self.appName());
     self.autoContext(design.autoContext);
     self.context(design.context);
     self.chooseContext(design.context);
@@ -169,6 +175,15 @@ function sparkViewModel() {
     });
   };
 
+  self.setAppName = function(name) {
+    $.each(self.appNames(), function(index, appName) {
+      if (appName.name() == name) {
+        self.appName(name);
+        self.selectedAppName(index);
+      }
+    });
+  };
+
   self.chooseContext = function(value, e) {
     $.each(self.contexts(), function(index, context) {
       if (context.name() == value.name()) {
@@ -194,8 +209,9 @@ function sparkViewModel() {
       data['query-appName'] = self.appName().name;
       data['query-classPath'] = self.classPath();
       data['query-autoContext'] = self.autoContext();
-      data['query-context'] = self.context().name;
-      data['query-params'] = JSON.stringify(self.query.params());
+      data['query-context'] = self.context() ? self.context().name : '';
+      data['query-params'] = ko.toJSON(self.query.params());
+
       var url = '/spark/api/save_query/';
       if (self.query.id() && self.query.id() != -1) {
         url += self.query.id() + '/';
@@ -226,7 +242,7 @@ function sparkViewModel() {
     data.classPath = self.classPath();
     data.autoContext = self.autoContext();
     data.context = self.context() ? self.context().name : '';
-    data.params = JSON.stringify(self.query.params());
+    data.params = ko.toJSON(self.query.params());
     var request = {
       url: '/spark/api/execute',
       dataType: 'json',
@@ -331,4 +347,18 @@ function sparkViewModel() {
     };
     $.ajax(request);
   };
+
+  self.showFileChooser = function() {
+    var inputPath = this;
+    var path = inputPath.value().substr(0, inputPath.value().lastIndexOf("/"));
+    $("#filechooser").jHueFileChooser({
+      initialPath: path,
+      onFileChoose: function (filePath) {
+        inputPath.value(filePath);
+        $("#chooseFile").modal("hide");
+      },
+      createFolder: false
+    });
+    $("#chooseFile").modal("show");
+  };
 }