Răsfoiți Sursa

HUE-1844 [beeswax] Load back from History page

Made the execute_query page automatically fetch results
if 'query_id' in GET request.
Changed APIs to use 'design' instead of 'query' for
SavedQuery and/or design objects. 'query' is used for
QueryHistory now.
Fixed empty results page display.
Move empty-wrapper CSS to main stylesheet.
Abraham Elmahrek 12 ani în urmă
părinte
comite
17c93d5

+ 38 - 14
apps/beeswax/src/beeswax/api.py

@@ -34,7 +34,7 @@ from beeswax.forms import QueryForm
 from beeswax.design import HQLdesign
 from beeswax.server import dbms
 from beeswax.server.dbms import expand_exception, get_query_server_config
-from beeswax.views import authorized_get_design, authorized_get_history, make_parameterization_form,\
+from beeswax.views import authorized_get_design, authorized_get_query_history, make_parameterization_form,\
                           safe_get_design, save_design, massage_columns_for_json, _get_query_handle_and_state,\
                           _parse_out_hadoop_jobs
 from desktop.lib.i18n import force_unicode
@@ -134,7 +134,7 @@ def execute_directly(request, query, design, query_server, tablename=None, **kwa
 
 @error_handler
 def watch_query_refresh_json(request, id):
-  query_history = authorized_get_history(request, id, must_exist=True)
+  query_history = authorized_get_query_history(request, id, must_exist=True)
   db = dbms.get(request.user, query_history.get_query_server_config())
 
   if not request.POST.get('next'): # We need this as multi query would fail as current query is closed
@@ -181,7 +181,7 @@ def watch_query_refresh_json(request, id):
   return HttpResponse(json.dumps(result), mimetype="application/json")
 
 
-def close_operation(request, query_id):
+def close_operation(request, query_history_id):
   response = {
     'status': -1,
     'message': ''
@@ -191,7 +191,7 @@ def close_operation(request, query_id):
     response['message'] = _('A POST request is required.')
   else:
     try:
-      query_history = authorized_get_history(request, query_id, must_exist=True)
+      query_history = authorized_get_query_history(request, query_history_id, must_exist=True)
       db = dbms.get(query_history.owner, query_history.get_query_server_config())
       handle = query_history.get_handle()
       db.close_operation(handle)
@@ -217,7 +217,7 @@ def explain_directly(request, query, design, query_server):
 
 
 @error_handler
-def execute(request, query_id=None):
+def execute(request, design_id=None):
   response = {'status': -1, 'message': ''}
 
   if request.method != 'POST':
@@ -226,7 +226,7 @@ def execute(request, query_id=None):
   app_name = get_app_name(request)
   query_server = get_query_server_config(app_name)
   query_type = beeswax.models.SavedQuery.TYPES_MAPPING[app_name]
-  design = safe_get_design(request, query_type, query_id)
+  design = safe_get_design(request, query_type, design_id)
 
   try:
     query_form = get_query_form(request)
@@ -278,7 +278,7 @@ def execute(request, query_id=None):
 
 
 @error_handler
-def save_query(request, query_id=None):
+def save_query_design(request, design_id=None):
   response = {'status': -1, 'message': ''}
 
   if request.method != 'POST':
@@ -286,7 +286,7 @@ def save_query(request, query_id=None):
 
   app_name = get_app_name(request)
   query_type = beeswax.models.SavedQuery.TYPES_MAPPING[app_name]
-  design = safe_get_design(request, query_type, query_id)
+  design = safe_get_design(request, query_type, design_id)
 
   try:
     query_form = get_query_form(request)
@@ -304,7 +304,7 @@ def save_query(request, query_id=None):
 
 
 @error_handler
-def fetch_saved_query(request, query_id):
+def fetch_saved_design(request, design_id):
   response = {'status': 0, 'message': ''}
 
   if request.method != 'GET':
@@ -312,21 +312,32 @@ def fetch_saved_query(request, query_id):
 
   app_name = get_app_name(request)
   query_type = beeswax.models.SavedQuery.TYPES_MAPPING[app_name]
-  design = safe_get_design(request, query_type, query_id)
+  design = safe_get_design(request, query_type, design_id)
 
   response['design'] = design_to_dict(design)
   return HttpResponse(json.dumps(response), mimetype="application/json")
 
+@error_handler
+def fetch_query_history(request, query_history_id):
+  response = {'status': 0, 'message': ''}
+
+  if request.method != 'GET':
+    response['message'] = _('A GET request is required.')
+
+  query = authorized_get_query_history(request, query_history_id, must_exist=True)
+
+  response['query_history'] = query_history_to_dict(request, query)
+  return HttpResponse(json.dumps(response), mimetype="application/json")
 
 @error_handler
-def cancel_query(request, query_id):
+def cancel_query(request, query_history_id):
   response = {'status': -1, 'message': ''}
 
   if request.method != 'POST':
     response['message'] = _('A POST request is required.')
   else:
     try:
-      query_history = authorized_get_history(request, query_id, must_exist=True)
+      query_history = authorized_get_query_history(request, query_history_id, must_exist=True)
       db = dbms.get(request.user, query_history.get_query_server_config())
       db.cancel_operation(query_history.get_handle())
       _get_query_handle_and_state(query_history)
@@ -338,13 +349,13 @@ def cancel_query(request, query_id):
 
 
 @error_handler
-def save_results(request, query_id):
+def save_results(request, query_history_id):
   """
   Save the results of a query to an HDFS directory or Hive table.
   """
   response = {'status': 0, 'message': ''}
 
-  query_history = authorized_get_history(request, query_id, must_exist=True)
+  query_history = authorized_get_query_history(request, query_history_id, must_exist=True)
   server_id, state = _get_query_handle_and_state(query_history)
   query_history.save_state(state)
   error_msg, log = None, None
@@ -422,6 +433,19 @@ def design_to_dict(design):
   }
 
 
+def query_history_to_dict(request, query_history):
+  return {
+    'id': query_history.id,
+    'state': query_history.last_state,
+    'query': query_history.query,
+    'has_results': query_history.has_results,
+    'statement_number': query_history.statement_number,
+    'design': design_to_dict(query_history.design),
+    'watch_url': reverse(get_app_name(request) + ':api_watch_query_refresh_json', kwargs={'id': query_history.id}),
+    'results_url': reverse(get_app_name(request) + ':view_results', kwargs={'id': query_history.id, 'first_row': 0})
+  }
+
+
 def get_query_form(request):
   # Get database choices
   query_server = dbms.get_query_server_config(get_app_name(request))

+ 1 - 1
apps/beeswax/src/beeswax/models.py

@@ -300,7 +300,7 @@ class SavedQuery(models.Model):
       return ""
 
   def get_absolute_url(self):
-    return reverse(QueryHistory.get_type_name(self.type) + ':execute_query', kwargs={'design_id': self.id})
+    return reverse(QueryHistory.get_type_name(self.type) + ':execute_design', kwargs={'design_id': self.id})
 
 
 class SessionManager(models.Manager):

+ 117 - 117
apps/beeswax/src/beeswax/templates/execute.mako

@@ -28,7 +28,7 @@ ${layout.menubar(section='query')}
 <div id="query-editor" class="container-fluid hide section">
 <div class="row-fluid">
 <div class="span2">
-  <form id="advancedSettingsForm" action="${action}" method="POST" class="form form-horizontal">
+  <form id="advancedSettingsForm" action="" method="POST" class="form form-horizontal">
     <div class="sidebar-nav">
       <ul class="nav nav-list">
         <li class="nav-header">${_('database')}</li>
@@ -37,7 +37,7 @@ ${layout.menubar(section='query')}
         </li>
         <li class="nav-header">${_('settings')}</li>
         <li class="white paramContainer">
-          <!-- ko foreach: query.settings -->
+          <!-- ko foreach: design.settings -->
           <div class="param">
             <div class="remove">
               <button data-bind="click: $root.removeSetting.bind(this, $index())" type="button" class="btn btn-mini settingsDelete" title="${_('Delete this setting')}">x
@@ -71,7 +71,7 @@ ${layout.menubar(section='query')}
              hide
           % endif
           ">
-          <!-- ko foreach: query.fileResources -->
+          <!-- ko foreach: design.fileResources -->
           <div class="param">
             <div class="remove">
               <button data-bind="click: $root.removeFileResources.bind(this, $index())" type="button" class="btn btn-mini" title="${_('Delete this setting')}">&times;</button>
@@ -108,7 +108,7 @@ ${layout.menubar(section='query')}
             hide
           % endif
           ">
-          <!-- ko foreach: query.functions -->
+          <!-- ko foreach: design.functions -->
           <div class="param">
             <div class="remove">
               <button data-bind="click: $root.removeFunction.bind(this, $index())" type="button" class="btn btn-mini settingsDelete" title="${_('Delete this setting')}">&times;</button>
@@ -132,7 +132,7 @@ ${layout.menubar(section='query')}
         <li class="nav-header">${_('Options')}</li>
         <li class="white" style="padding-top:0px">
           <label class="checkbox" rel="tooltip" data-original-title="${_("If checked (the default), you can include parameters like $parameter_name in your query, and users will be prompted for a value when the query is run.")}">
-            <input data-bind="checked: query.isParameterized" type="checkbox"/>
+            <input data-bind="checked: design.isParameterized" type="checkbox"/>
             ${_("Enable parameterization")}
           </label>
           <label class="checkbox
@@ -140,7 +140,7 @@ ${layout.menubar(section='query')}
             hide
           % endif
           " rel="tooltip" data-original-title="${_("If checked, you will receive an email notification when the query completes.")}">
-            <input data-bind="checked: query.email" type="checkbox"/>
+            <input data-bind="checked: design.email" type="checkbox"/>
             ${_("Email me on completion")}
           </label>
         </li>
@@ -215,18 +215,18 @@ ${layout.menubar(section='query')}
       <div class="tab-content">
         <div id="queryPane">
 
-          <div data-bind="css: {'hide': query.errors().length == 0}" class="hide alert alert-error">
+          <div data-bind="css: {'hide': design.errors().length == 0}" class="hide alert alert-error">
             <p><strong>${_('Your query has the following error(s):')}</strong></p>
 
-            <div data-bind="foreach: query.errors">
+            <div data-bind="foreach: design.errors">
               <p data-bind="text: $data" class="queryErrorMessage"></p>
             </div>
           </div>
 
-          <div data-bind="css: {'hide': query.watch.errors().length == 0}" class="alert alert-error">
+          <div data-bind="css: {'hide': design.watch.errors().length == 0}" class="alert alert-error">
             <p><strong>${_('Your query has the following error(s):')}</strong></p>
 
-            <div data-bind="foreach: query.watch.errors">
+            <div data-bind="foreach: design.watch.errors">
               <p data-bind="text: $data" class="queryErrorMessage"></p>
             </div>
           </div>
@@ -235,14 +235,14 @@ ${layout.menubar(section='query')}
 
           <div class="actions">
             % if app_name == 'impala':
-            <button data-bind="click: tryExecuteQuery, visible: !$root.query.isRunning() && $root.query.isFinished()" type="button" id="executeQuery" class="btn btn-primary disable-feedback" tabindex="2">${_('Execute')}</button>
-            <button data-bind="click: tryCancelQuery, visible: $root.query.isRunning()" class="btn btn-danger" data-loading-text="${ _('Canceling...') }" rel="tooltip" data-original-title="${ _('Cancel the query') }">${ _('Cancel') }</button>
+            <button data-bind="click: tryExecuteQuery, visible: !$root.design.isRunning() && $root.design.isFinished()" type="button" id="executeQuery" class="btn btn-primary disable-feedback" tabindex="2">${_('Execute')}</button>
+            <button data-bind="click: tryCancelQuery, visible: $root.design.isRunning()" class="btn btn-danger" data-loading-text="${ _('Canceling...') }" rel="tooltip" data-original-title="${ _('Cancel the query') }">${ _('Cancel') }</button>
             % else:
-            <button data-bind="click: tryExecuteQuery, enable: !$root.query.isRunning(), visible: $root.query.isFinished()" type="button" id="executeQuery" class="btn btn-primary disable-feedback" tabindex="2">${_('Execute')}</button>
+            <button data-bind="click: tryExecuteQuery, enable: !$root.design.isRunning(), visible: $root.design.isFinished()" type="button" id="executeQuery" class="btn btn-primary disable-feedback" tabindex="2">${_('Execute')}</button>
             % endif
-            <button data-bind="click: executeNextStatement, visible: !$root.query.isFinished()" type="button" class="btn btn-primary disable-feedback" tabindex="2">${_('Next')}</button>
+            <button data-bind="click: executeNextStatement, visible: !$root.design.isFinished()" type="button" class="btn btn-primary disable-feedback" tabindex="2">${_('Next')}</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: trySaveDesign, css: {'hide': !$root.design.id() || $root.design.id() == -1}" type="button" class="btn hide">${_('Save')}</button>
             <button data-bind="click: saveAsModal" type="button" class="btn">${_('Save as...')}</button>
             <button data-bind="click: tryExplainQuery" type="button" id="explainQuery" class="btn">${_('Explain')}</button>
             &nbsp; ${_('or create a')} &nbsp;
@@ -262,47 +262,47 @@ ${layout.menubar(section='query')}
     <a id="save-results" data-bind="click: saveResultsModal" href="javascript:void(0)" title="${_('Save the results to HDFS or a new Hive table')}" rel="tooltip"
       class="view-query-results hide pull-right"><h4 style="margin-right: 20px"><i class="fa fa-save"></i></h4></a>
 
-    <a id="download-csv" data-bind="attr: {'href': '/beeswax/download/' + $root.query.id() + '/csv'}" href="javascript:void(0)" title="${_('Download the results in CSV format')}" rel="tooltip"
+    <a id="download-csv" data-bind="attr: {'href': '/beeswax/download/' + $root.design.id() + '/csv'}" href="javascript:void(0)" title="${_('Download the results in CSV format')}" rel="tooltip"
       class="view-query-results hide pull-right"><h4 style="margin-right: 20px"><i class="fa fa-arrow-circle-o-down"></i></h4></a>
 
-    <a id="download-excel" data-bind="attr: {'href': '/beeswax/download/' + $root.query.id() + '/xls'}" href="javascript:void(0)" title="${_('Download the results for excel')}" rel="tooltip"
+    <a id="download-excel" data-bind="attr: {'href': '/beeswax/download/' + $root.design.id() + '/xls'}" href="javascript:void(0)" title="${_('Download the results for excel')}" rel="tooltip"
       class="view-query-results hide pull-right"><h4 style="margin-right: 20px"><i class="fa fa-arrow-circle-o-down"></i></h4></a>
 
     <div class="card-body">
       <ul class="nav nav-tabs">
         <li><a href="#query" data-toggle="tab">${_('Query')}</a></li>
-        <!-- ko if: !query.explain() -->
+        <!-- ko if: !design.explain() -->
         <li><a href="#log" data-toggle="tab">${_('Log')}</a></li>
         <!-- /ko -->
-        <!-- ko if: !query.explain() && !query.isRunning() -->
+        <!-- ko if: !design.explain() && !design.isRunning() -->
         <li><a href="#columns" data-toggle="tab">${_('Columns')}</a></li>
         <li><a href="#results" data-toggle="tab">${_('Results')}</a></li>
         <li><a href="#chart" data-toggle="tab">${_('Chart')}</a></li>
         <!-- /ko -->
-        <!-- ko if: query.explain() && !query.isRunning() -->
+        <!-- ko if: design.explain() && !design.isRunning() -->
         <li><a href="#explanation" data-toggle="tab">${_('Explanation')}</a></li>
         <!-- /ko -->
       </ul>
 
       <div class="tab-content">
         <div class="tab-pane" id="query">
-          <pre data-bind="text: viewModel.query.statement()"></pre>
+          <pre data-bind="text: viewModel.design.statement()"></pre>
         </div>
-        <!-- ko if: query.explain() -->
+        <!-- ko if: design.explain() -->
         <div class="tab-pane" id="explanation">
-          <pre data-bind="text: $root.query.results.explanation()"></pre>
+          <pre data-bind="text: $root.design.results.explanation()"></pre>
         </div>
         <!-- /ko -->
-        <!-- ko if: !query.explain() -->
+        <!-- ko if: !design.explain() -->
         <div class="active tab-pane" id="log">
-          <pre data-bind="text: $root.query.watch.logs().join('\n')"></pre>
+          <pre data-bind="text: $root.design.watch.logs().join('\n')"></pre>
         </div>
         <div class="tab-pane" id="columns">
           <table class="table table-striped table-condensed" cellpadding="0" cellspacing="0">
             <thead>
               <tr><th>${_('Name')}</th></tr>
             </thead>
-            <tbody data-bind="foreach: $root.query.results.columns">
+            <tbody data-bind="foreach: $root.design.results.columns">
               <tr>
                 <td><a href="javascript:void(0)" class="column-selector" data-bind="text: $data.name"></a></td>
               </tr>
@@ -311,17 +311,25 @@ ${layout.menubar(section='query')}
         </div>
         <div class="tab-pane" id="results">
 
-          <div data-bind="css: {'hide': $root.query.results.rows().length == 0}" class="hide">
+          <div data-bind="css: {'hide': design.results.errors().length == 0}" class="alert alert-error">
+            <p><strong>${_('Fetching results ran into the following error(s):')}</strong></p>
+
+            <div data-bind="foreach: design.results.errors">
+              <p data-bind="text: $data" class="queryErrorMessage"></p>
+            </div>
+          </div>
+
+          <div data-bind="css: {'hide': $root.design.results.empty()}" class="hide">
             <table class="table table-striped table-condensed resultTable" cellpadding="0" cellspacing="0" data-tablescroller-enforce-height="true">
               <thead>
-              <tr data-bind="foreach: $root.query.results.columns">
+              <tr data-bind="foreach: $root.design.results.columns">
                 <th data-bind="text: $data.name, css: { 'sort-numeric': $.inArray($data.type, ['TINYINT_TYPE', 'SMALLINT_TYPE', 'INT_TYPE', 'BIGINT_TYPE', 'FLOAT_TYPE', 'DOUBLE_TYPE', 'DECIMAL_TYPE']) > -1, 'sort-date': $.inArray($data.type, ['TIMESTAMP_TYPE', 'DATE_TYPE']) > -1, 'sort-string': $.inArray($data.type, ['TINYINT_TYPE', 'SMALLINT_TYPE', 'INT_TYPE', 'BIGINT_TYPE', 'FLOAT_TYPE', 'DOUBLE_TYPE', 'DECIMAL_TYPE', 'TIMESTAMP_TYPE', 'DATE_TYPE']) == -1 }"></th>
               </tr>
               </thead>
             </table>
           </div>
 
-          <div data-bind="css: {'hide': !$root.query.results.empty()}" class="hide">
+          <div data-bind="css: {'hide': !$root.design.results.empty()}" class="hide">
             <div class="card card-small scrollable">
               <div class="row-fluid">
                 <div class="span10 offset1 center empty-wrapper">
@@ -405,7 +413,7 @@ ${layout.menubar(section='query')}
         <p>
         <form method="POST" action="" class="form-horizontal">
           <fieldset>
-            <!-- ko foreach: $root.query.parameters -->
+            <!-- ko foreach: $root.design.parameters -->
             <div class="control-group">
               <label data-bind="text: name" class="control-label"></label>
               <div class="controls">
@@ -436,7 +444,7 @@ ${layout.menubar(section='query')}
 
         <form method="POST" action="" class="form-horizontal">
           <fieldset>
-            <!-- ko foreach: $root.query.parameters -->
+            <!-- ko foreach: $root.design.parameters -->
             <div class="control-group">
               <label data-bind="text: name" class="control-label"></label>
 
@@ -484,20 +492,20 @@ ${layout.menubar(section='query')}
       <label class="control-label">${_('Name')}</label>
 
       <div class="controls">
-        <input data-bind="value: $root.query.name" type="text" class="input-xlarge">
+        <input data-bind="value: $root.design.name" type="text" class="input-xlarge">
       </div>
     </div>
     <div class="control-group">
       <label class="control-label">${_('Description')}</label>
 
       <div class="controls">
-        <input data-bind="value: $root.query.description" type="text" class="input-xlarge">
+        <input data-bind="value: $root.design.description" type="text" class="input-xlarge">
       </div>
     </div>
   </form>
   <div class="modal-footer">
     <button class="btn" data-dismiss="modal">${_('Cancel')}</button>
-    <button data-bind="click: trySaveAsQuery" class="btn btn-primary">${_('Save')}</button>
+    <button data-bind="click: trySaveAsDesign" class="btn btn-primary">${_('Save')}</button>
   </div>
 </div>
 
@@ -508,36 +516,36 @@ ${layout.menubar(section='query')}
     <h3>${_('Save Query Results')}</h3>
   </div>
   <div class="modal-body">
-    <!-- ko if: $root.query.results.save.saveTargetError() -->
-      <h4 data-bind="text: $root.query.results.save.saveTargetError()"></h4>
+    <!-- ko if: $root.design.results.save.saveTargetError() -->
+      <h4 data-bind="text: $root.design.results.save.saveTargetError()"></h4>
     <!-- /ko -->
-    <!-- ko if: $root.query.results.save.targetTableError() -->
-      <h4 data-bind="text: $root.query.results.save.targetTableError()"></h4>
+    <!-- ko if: $root.design.results.save.targetTableError() -->
+      <h4 data-bind="text: $root.design.results.save.targetTableError()"></h4>
     <!-- /ko -->
-    <!-- ko if: $root.query.results.save.targetDirectoryError() -->
-      <h4 data-bind="text: $root.query.results.save.targetDirectoryError()"></h4>
+    <!-- ko if: $root.design.results.save.targetDirectoryError() -->
+      <h4 data-bind="text: $root.design.results.save.targetDirectoryError()"></h4>
     <!-- /ko -->
     <form id="saveResultsForm" method="POST" class="form form-inline">
       <fieldset>
-        <div data-bind="css: {'error': $root.query.results.save.targetTableError()}" class="control-group">
+        <div data-bind="css: {'error': $root.design.results.save.targetTableError()}" class="control-group">
           <div class="controls">
             <label class="radio">
-              <input data-bind="checked: $root.query.results.save.type" type="radio" name="save-results-type" value="hive-table">
+              <input data-bind="checked: $root.design.results.save.type" type="radio" name="save-results-type" value="hive-table">
               &nbsp;${ _('In a new table') }
             </label>
-            <span data-bind="visible: $root.query.results.save.type() == 'hive-table'">
-              <input data-bind="value: $root.query.results.save.path" type="text" name="target_table" placeholder="${_('Table name')}">
+            <span data-bind="visible: $root.design.results.save.type() == 'hive-table'">
+              <input data-bind="value: $root.design.results.save.path" type="text" name="target_table" placeholder="${_('Table name')}">
             </span>
           </div>
         </div>
-        <div data-bind="css: {'error': $root.query.results.save.targetDirectoryError()}" class="control-group">
+        <div data-bind="css: {'error': $root.design.results.save.targetDirectoryError()}" class="control-group">
           <div class="controls">
             <label class="radio">
-              <input data-bind="checked: $root.query.results.save.type" type="radio" name="save-results-type" value="hdfs">
+              <input data-bind="checked: $root.design.results.save.type" type="radio" name="save-results-type" value="hdfs">
               &nbsp;${ _('In an HDFS directory') }
             </label>
-            <span data-bind="visible: $root.query.results.save.type() == 'hdfs'">
-              <input data-bind="value: $root.query.results.save.path" type="text" name="target_dir" placeholder="${_('Results location')}" class="pathChooser">
+            <span data-bind="visible: $root.design.results.save.type() == 'hdfs'">
+              <input data-bind="value: $root.design.results.save.path" type="text" name="target_dir" placeholder="${_('Results location')}" class="pathChooser">
             </span>
           </div>
         </div>
@@ -1133,7 +1141,7 @@ $(document).ready(function () {
 });
 
 
-$(document).one('fetched.query', function () {
+$(document).one('fetched.design', function () {
   // Edit query name and description.
   $("#query-name").editable({
     validate: function (value) {
@@ -1142,14 +1150,14 @@ $(document).one('fetched.query', function () {
       }
     },
     success: function (response, newValue) {
-      viewModel.query.name(newValue);
+      viewModel.design.name(newValue);
     },
     emptytext: "${ _('Query name') }"
   });
 
   $("#query-description").editable({
     success: function (response, newValue) {
-      viewModel.query.description(newValue);
+      viewModel.design.description(newValue);
     },
     emptytext: "${ _('Empty description') }"
   });
@@ -1174,12 +1182,11 @@ $(document).ready(function () {
 
   $("a[data-toggle='tab']").on("shown", function (e) {
     if ($(e.target).attr("href") != "#results"){
-      $($(e.target).attr("href")).height($(".dataTables_wrapper").height());
+      $($(e.target).attr("href")).css('height', 'auto');
       if ($(e.target).attr("href") == "#chart") {
         predictGraph();
       }
-    }
-    else {
+    } else {
       reinitializeTable();
     }
   });
@@ -1353,7 +1360,7 @@ $(document).ready(function () {
     }
   });
 
-  viewModel.query.watch.logs.subscribe(function(val){
+  viewModel.design.watch.logs.subscribe(function(val){
     if (logsAtEnd) {
       var _logsEl = $("#log pre");
       _logsEl.scrollTop(_logsEl[0].scrollHeight - _logsEl.height());
@@ -1372,26 +1379,26 @@ function cleanResultsTable() {
   if (dataTable) {
     dataTable.fnClearTable();
     dataTable.fnDestroy();
-    viewModel.query.results.columns.valueHasMutated();
-    viewModel.query.results.rows.valueHasMutated();
+    viewModel.design.results.columns.valueHasMutated();
+    viewModel.design.results.rows.valueHasMutated();
     dataTable = null;
   }
 }
 
 function addResults(viewModel, dataTable, index, pageSize) {
-  if (viewModel.hasMoreResults() && index + pageSize > viewModel.query.results.rows().length) {
+  if (viewModel.hasMoreResults() && index + pageSize > viewModel.design.results.rows().length) {
     $(document).one('fetched.results', function () {
       $.totalStorage("${app_name}_temp_query", null);
-      dataTable.fnAddData(viewModel.query.results.rows.slice(index, index + pageSize));
+      dataTable.fnAddData(viewModel.design.results.rows.slice(index, index + pageSize));
     });
     viewModel.fetchResults();
   } else {
-    dataTable.fnAddData(viewModel.query.results.rows.slice(index, index + pageSize));
+    dataTable.fnAddData(viewModel.design.results.rows.slice(index, index + pageSize));
   }
 }
 
 function resultsTable(e, data) {
-  if (!dataTable && viewModel.query.results.columns().length > 0) {
+  if (!dataTable && viewModel.design.results.columns().length > 0) {
     dataTable = $(".resultTable").dataTable({
       "bPaginate": false,
       "bLengthChange": false,
@@ -1432,7 +1439,7 @@ function resultsTable(e, data) {
     $(".dataTables_filter").hide();
     reinitializeTable();
     var _options = '<option value="-1">${ _("Please select a column")}</option>';
-    $(viewModel.query.results.columns()).each(function(cnt, item){
+    $(viewModel.design.results.columns()).each(function(cnt, item){
       _options += '<option value="'+(cnt + 1)+'">'+ item.name +'</option>';
     });
     $(".blueprintSelect").html(_options);
@@ -1498,27 +1505,27 @@ $(document).on('error.query', function () {
 
 
 // Save
-function trySaveQuery() {
+function trySaveDesign() {
   var query = getHighlightedQuery() || codeMirror.getValue();
-  viewModel.query.query(query);
-  if (viewModel.query.id() && viewModel.query.id() != -1) {
-    viewModel.saveQuery();
+  viewModel.design.query(query);
+  if (viewModel.design.id() && viewModel.design.id() != -1) {
+    viewModel.saveDesign();
   }
 }
 
 function saveAsModal() {
   var query = getHighlightedQuery() || codeMirror.getValue();
-  viewModel.query.query(query);
+  viewModel.design.query(query);
   $('#saveAs').modal('show');
 }
 
-function trySaveAsQuery() {
-  if (viewModel.query.query() && viewModel.query.name()) {
-    viewModel.query.id(-1);
-    viewModel.saveQuery();
+function trySaveAsDesign() {
+  if (viewModel.design.query() && viewModel.design.name()) {
+    viewModel.design.id(-1);
+    viewModel.saveDesign();
     $('#saveas-query-name').removeClass('error');
     $('#saveAs').modal('hide');
-  } else if (viewModel.query.name()) {
+  } else if (viewModel.design.name()) {
     $.jHueNotify.error("${_('No query provided to save.')}");
     $('#saveAs').modal('hide');
   } else {
@@ -1543,11 +1550,11 @@ $(document).on('saved.results', function() {
 function tryExecuteQuery() {
   $(".tooltip").remove();
   var query = getHighlightedQuery() || codeMirror.getValue();
-  viewModel.query.query(query);
+  viewModel.design.query(query);
   if ($(".dataTables_wrapper").length > 0) { // forces results to be up
     $(".dataTables_wrapper").scrollTop(0);
   }
-  if (viewModel.query.isParameterized()) {
+  if (viewModel.design.isParameterized()) {
     viewModel.fetchParameters();
   } else {
     viewModel.executeQuery();
@@ -1564,7 +1571,7 @@ function tryExecuteParameterizedQuery() {
 function tryExplainQuery() {
   $(".tooltip").remove();
   var query = getHighlightedQuery() || codeMirror.getValue();
-  viewModel.query.query(query);
+  viewModel.design.query(query);
   viewModel.explainQuery();
 }
 
@@ -1608,31 +1615,29 @@ function clickHard(el) {
   }, 100);
 }
 
-viewModel = new BeeswaxViewModel("${app_name}", ${design.id and design.id or -1});
-viewModel.fetchDatabases();
-var subscription = viewModel.databases.subscribe(function() {
-  if (viewModel.query.id() > 0) {
-    viewModel.fetchQuery();
-  }
-  subscription.dispose();
-});
-if (viewModel.query.id() > 0) {
+viewModel = new BeeswaxViewModel("${app_name}");
+% if query:
+  viewModel.design.history.id(${query.id});
+  viewModel.fetchQueryHistory();
+  $(document).on('fetched.query', function(e) {
+    viewModel.watchQueryLoop();
+  });
+% elif design.id:
+  viewModel.design.id(${design.id});
+  viewModel.fetchDesign();
+% endif
+if (viewModel.design.id() > 0 || viewModel.design.history.id() > 0) {
   // Code mirror and ko.
-  viewModel.query.query.subscribe((function () {
-    // First call skipped to avoid reset of hueBeeswaxLastDatabase
-    var counter = 0;
-    return function (value) {
-      if (counter++ == 0) {
-        codeMirror.setValue(value);
-      }
-    }
-  })());
-  viewModel.fetchQuery();
+  var codeMirrorSubscription = viewModel.design.query.subscribe(function(value) {
+    codeMirror.setValue(value);
+    codeMirrorSubscription.dispose();
+  });
 }
-viewModel.query.fileResources.subscribe(function() {
+viewModel.design.fileResources.subscribe(function() {
   // File chooser button for file resources.
   $(".pathChooser:not(:has(~ button))").after(getFileBrowseButton($(".pathChooser:not(:has(~ button))")));
 });
+viewModel.fetchDatabases();
 ko.applyBindings(viewModel);
 
 // Server error handling.
@@ -1644,9 +1649,9 @@ $(document).on('server.unmanageable_error', function (e, responseText) {
 });
 
 // Other
-$(document).on('saved.query', function (e, id) {
+$(document).on('saved.design', function (e, id) {
   $(document).trigger('info', "${'Query saved.'}");
-  window.location.href = "/beeswax/execute/" + id;
+  window.location.href = "/beeswax/execute/design/" + id;
 });
 $(document).on('error_cancel.query', function (e, message) {
   $(document).trigger("error", "${ _('Problem: ') }" + message);
@@ -1765,19 +1770,19 @@ $(document).ready(function () {
       codeMirror.setSize("99%", $(window).height() - 270 - $("#queryPane .alert-error").outerHeight() - $(".nav-tabs").outerHeight());
     },
     'query/execute/params': function () {
-      if (viewModel.query.parameters().length == 0) {
+      if (viewModel.design.parameters().length == 0) {
         routie('query');
       }
       showSection('execute-parameter-selection');
     },
     'query/explain/params': function () {
-      if (viewModel.query.parameters().length == 0) {
+      if (viewModel.design.parameters().length == 0) {
         routie('query');
       }
       showSection('explain-parameter-selection');
     },
     'query/logs': function () {
-      if (viewModel.query.watch.logs().length == 0 && viewModel.query.watch.errors().length == 0) {
+      if (viewModel.design.watch.logs().length == 0 && viewModel.design.watch.errors().length == 0) {
         routie('query');
       }
       codeMirror.setSize("99%", 100);
@@ -1787,19 +1792,15 @@ $(document).ready(function () {
       clickHard('.resultsContainer .nav-tabs a[href="#log"]');
     },
     'query/results': function () {
-      if (viewModel.query.id() == -1 && viewModel.query.results.empty()) {
-        routie('query');
-      } else {
-        codeMirror.setSize("99%", 100);
-        $('.resultsContainer').show();
-        $('.resultsContainer .watch-query').hide();
-        $('.resultsContainer .view-query-results').show();
-        clickHard('.resultsContainer .nav-tabs a[href="#results"]');
-        $("html, body").animate({ scrollTop: ($(".resultsContainer").position().top - 80) + "px" });
-      }
+      codeMirror.setSize("99%", 100);
+      $('.resultsContainer').show();
+      $('.resultsContainer .watch-query').hide();
+      $('.resultsContainer .view-query-results').show();
+      clickHard('.resultsContainer .nav-tabs a[href="#results"]');
+      $("html, body").animate({ scrollTop: ($(".resultsContainer").position().top - 80) + "px" });
     },
     'query/explanation': function () {
-      if (!viewModel.query.results.explanation()) {
+      if (!viewModel.design.results.explanation()) {
         routie('query');
       }
       codeMirror.setSize("99%", 100);
@@ -1812,8 +1813,11 @@ $(document).ready(function () {
       routie('query');
     }
   });
+
+  routie('query');
+
   $(document).on('fetched.parameters', function () {
-    if (viewModel.query.parameters().length > 0) {
+    if (viewModel.design.parameters().length > 0) {
       routie('query/execute/params');
     } else {
       viewModel.executeQuery();
@@ -1824,7 +1828,7 @@ $(document).ready(function () {
   });
   $(document).on('watched.query', function (e, data) {
     if (data.status && data.status && data.status != 0) {
-      viewModel.query.watch.errors.push(data.error || data.message);
+      viewModel.design.watch.errors.push(data.error || data.message);
     }
     routie('query/logs');
   });
@@ -1839,10 +1843,6 @@ $(document).ready(function () {
   });
 });
 
-// @TODO: Improve resize logs to be more relative. See FF versus Chrome.
-// @TODO: Stop operation
-// @TODO: Re-add download query for impala
-// @TODO: Re-enable type ahead for settings
 </script>
 
 ${ commonfooter(messages) | n,unicode }

+ 2 - 2
apps/beeswax/src/beeswax/templates/list_designs.mako

@@ -95,7 +95,7 @@ ${ layout.menubar(section='saved queries') }
         <td data-row-selector-exclude="true">
           <div class="hueCheckbox savedCheck fa"
             % if may_edit:
-              data-edit-url="${ url(app_name + ':execute_query', design_id=design.id) }"
+              data-edit-url="${ url(app_name + ':execute_design', design_id=design.id) }"
               data-delete-name="${ design.id }"
               data-history-url="${ url(app_name + ':list_query_history') }?q-design_id=${design.id}"
             % endif
@@ -103,7 +103,7 @@ ${ layout.menubar(section='saved queries') }
         </td>
         <td>
         % if may_edit:
-          <a href="${ url(app_name + ':execute_query', design_id=design.id) }" data-row-selector="true">${ force_unicode(design.name) }</a>
+          <a href="${ url(app_name + ':execute_design', design_id=design.id) }" data-row-selector="true">${ force_unicode(design.name) }</a>
         % else:
           ${ force_unicode(design.name) }
         % endif

+ 3 - 6
apps/beeswax/src/beeswax/templates/list_history.mako

@@ -32,7 +32,7 @@ ${ layout.menubar(section='history') }
 <%def name="show_saved_query(design, history)">
   % if design:
     % if request.user == design.owner:
-      <a href="${ url(app_name + ':execute_query', design_id=design.id) }">
+      <a href="${ url(app_name + ':execute_design', design_id=design.id) }">
     % endif
     % if design.is_auto:
       [ ${_('Unsaved')} ]
@@ -124,9 +124,6 @@ ${ layout.menubar(section='history') }
             </thead>
             <tbody>
             % for query in page.object_list:
-              <%
-                qcontext = query.design.get_query_context()
-              %>
               <tr class="histRow">
                 <td data-sort-value="${time.mktime(query.submission_date.timetuple())}">${query.submission_date.strftime("%x %X")}</td>
                 <td>${show_saved_query(query.design, query)}</td>
@@ -140,8 +137,8 @@ ${ layout.menubar(section='history') }
                 <td>${query.owner}</td>
                 <td>${models.QueryHistory.STATE[query.last_state]}</td>
                 <td>
-                  % if qcontext and query.last_state not in (models.QueryHistory.STATE.expired.index, models.QueryHistory.STATE.failed.index):
-                    <a href="${ url(app_name + ':watch_query', id=query.id) }?context=${qcontext|u}" data-row-selector="true">${_('Results')}</a>
+                  % if query.last_state not in (models.QueryHistory.STATE.expired.index, models.QueryHistory.STATE.failed.index):
+                    <a href="${ url(app_name + ':watch_query_history', query_history_id=query.id) }" data-row-selector="true">${_('Results')}</a>
                   % else:
                     ~
                   % endif

+ 4 - 4
apps/beeswax/src/beeswax/templates/my_queries.mako

@@ -91,14 +91,14 @@ ${layout.menubar(section='my queries')}
           <tr>
             <td data-row-selector-exclude="true">
               <div class="hueCheckbox savedCheck fa canDelete"
-                   data-edit-url="${ url(app_name + ':execute_query', design_id=design.id) }"
+                   data-edit-url="${ url(app_name + ':execute_design', design_id=design.id) }"
                    data-delete-name="${ design.id }"
                    data-history-url="${ url(app_name + ':list_query_history') }?q-design_id=${design.id}"
                    data-clone-url="${ url(app_name + ':clone_design', design_id=design.id) }"
                    data-row-selector-exclude="true"></div>
             </td>
             <td>
-              <a href="${ url(app_name + ':execute_query', design_id=design.id) }" data-row-selector="true">${design.name}</a>
+              <a href="${ url(app_name + ':execute_design', design_id=design.id) }" data-row-selector="true">${design.name}</a>
             </td>
             <td>
               % if design.desc:
@@ -134,14 +134,14 @@ ${layout.menubar(section='my queries')}
           <tr>
             <td width="1%" data-row-selector-exclude="true">
               <div class="hueCheckbox runCheck fa"
-                data-edit-url="${ url(app_name + ':execute_query', design_id=query.design.id) }"
+                data-edit-url="${ url(app_name + ':execute_design', design_id=query.design.id) }"
                 % if qcontext and query.last_state != models.QueryHistory.STATE.expired.index:
                   data-view-url="${ url(app_name + ':watch_query', id=query.id) }?context=${qcontext|u}"
                 % endif
                 data-row-selector-exclude="true"></div>
             </td>
             <td width="10%" data-sort-value="${time.mktime(query.submission_date.timetuple())}">${ query.submission_date.strftime("%x %X") }</td>
-            <td width="20%"><a href="${ url(app_name + ':execute_query', design_id=query.design.id) }" data-row-selector="true">${ query.design.name }</a></td>
+            <td width="20%"><a href="${ url(app_name + ':execute_design', design_id=query.design.id) }" data-row-selector="true">${ query.design.name }</a></td>
             <td width="60%">
               % if len(query.query) > 100:
               <code>${collapse_whitespace(query.query[:100])}...</code>

+ 1 - 1
apps/beeswax/src/beeswax/test_base.py

@@ -280,7 +280,7 @@ def make_query(client, query, submission_type="Execute",
   if submission_type == 'Explain':
     execute_url += "?explain=true"
   if submission_type == 'Save':
-    execute_url = reverse("beeswax:api_save_query")
+    execute_url = reverse("beeswax:api_save_design")
 
   response = client.post(execute_url, parameters, **kwargs)
 

+ 2 - 2
apps/beeswax/src/beeswax/tests.py

@@ -589,7 +589,7 @@ for x in sys.stdin:
     resp = cli.get('/beeswax/execute/%s' % (design.id,))
     assert_equal(design, resp.context['design'], resp.context)
 
-    resp = cli.get(reverse('beeswax:api_fetch_saved_query', kwargs={'query_id': design.id}))
+    resp = cli.get(reverse('beeswax:api_fetch_saved_design', kwargs={'design_id': design.id}))
     content = json.loads(resp.content)
     assert_true('SELECT bogus FROM test' in content['design']['query'], content)
 
@@ -615,7 +615,7 @@ for x in sys.stdin:
 
     # Retrieve that design and check correct DB is selected
     design = beeswax.models.SavedQuery.objects.filter(name='rubbish')[0]
-    resp = cli.get(reverse('beeswax:api_fetch_saved_query', kwargs={'query_id': design.id}))
+    resp = cli.get(reverse('beeswax:api_fetch_saved_design', kwargs={'design_id': design.id}))
     content = json.loads(resp.content)
     assert_true(query in content['design']['query'], content)
     assert_equal('', content['design']['desc'], content)

+ 10 - 7
apps/beeswax/src/beeswax/urls.py

@@ -21,7 +21,9 @@ from django.conf.urls.defaults import patterns, url
 urlpatterns = patterns('beeswax.views',
   url(r'^$', 'index', name='index'),
 
-  url(r'^execute/(?P<design_id>\d+)?$', 'execute_query', name='execute_query'),
+  url(r'^execute/?$', 'execute_query', name='execute_query'),
+  url(r'^execute/design/(?P<design_id>\d+)$', 'execute_query', name='execute_design'),
+  url(r'^execute/query/(?P<query_history_id>\d+)$', 'execute_query', name='watch_query_history'),
   url(r'^watch/(?P<id>\d+)$', 'watch_query', name='watch_query'),
   url(r'^results/(?P<id>\d+)/(?P<first_row>\d+)$', 'view_results', name='view_results'),
   url(r'^download/(?P<id>\d+)/(?P<format>\w+)$', 'download', name='download'),
@@ -60,12 +62,13 @@ urlpatterns += patterns(
   url(r'^api/autocomplete/$', 'autocomplete', name='api_autocomplete_databases'),
   url(r'^api/autocomplete/(?P<database>\w+)/$', 'autocomplete', name='api_autocomplete_tables'),
   url(r'^api/autocomplete/(?P<database>\w+)/(?P<table>\w+)$', 'autocomplete', name='api_autocomplete_columns'),
+  url(r'^api/design/(?P<design_id>\d+)?$', 'save_query_design', name='api_save_design'),
+  url(r'^api/design/(?P<design_id>\d+)/get$', 'fetch_saved_design', name='api_fetch_saved_design'),
+  url(r'^api/query/(?P<query_history_id>\d+)/get$', 'fetch_query_history', name='api_fetch_query_history'),
   url(r'^api/query/parameters$', 'parameters', name='api_parameters'),
-  url(r'^api/query/execute/(?P<query_id>\d+)?$', 'execute', name='api_execute'),
-  url(r'^api/query/(?P<query_id>\d+)?$', 'save_query', name='api_save_query'),
-  url(r'^api/query/(?P<query_id>\d+)/get$', 'fetch_saved_query', name='api_fetch_saved_query'),
-  url(r'^api/query/(?P<query_id>\d+)/cancel$', 'cancel_query', name='api_cancel_query'),
-  url(r'^api/query/(?P<query_id>\d+)/close/?$', 'close_operation', name='api_close_operation'),
-  url(r'^api/query/(?P<query_id>\d+)/results/save$', 'save_results', name='api_save_results'),
+  url(r'^api/query/execute/(?P<design_id>\d+)?$', 'execute', name='api_execute'),
+  url(r'^api/query/(?P<query_history_id>\d+)/cancel$', 'cancel_query', name='api_cancel_query'),
+  url(r'^api/query/(?P<query_history_id>\d+)/close/?$', 'close_operation', name='api_close_operation'),
+  url(r'^api/query/(?P<query_history_id>\d+)/results/save$', 'save_results', name='api_save_results'),
   url(r'^api/watch/json/(?P<id>\d+)$', 'watch_query_refresh_json', name='api_watch_query_refresh_json'),
 )

+ 27 - 94
apps/beeswax/src/beeswax/views.py

@@ -184,7 +184,7 @@ def clone_design(request, design_id):
 
   messages.info(request, _('Copied design: %(name)s') % {'name': design.name})
 
-  return format_preserving_redirect(request, reverse(get_app_name(request) + ':execute_query', kwargs={'design_id': copy.id}))
+  return format_preserving_redirect(request, reverse(get_app_name(request) + ':execute_design', kwargs={'design_id': copy.id}))
 
 
 def list_designs(request):
@@ -327,7 +327,7 @@ def list_query_history(request):
 def download(request, id, format):
   assert format in common.DL_FORMATS
 
-  query_history = authorized_get_history(request, id, must_exist=True)
+  query_history = authorized_get_query_history(request, id, must_exist=True)
   db = dbms.get(request.user, query_history.get_query_server_config())
   LOG.debug('Download results for query %s: [ %s ]' % (query_history.server_id, query_history.query))
 
@@ -338,99 +338,30 @@ def download(request, id, format):
 Queries Views
 """
 
-def execute_query(request, design_id=None):
+def execute_query(request, design_id=None, query_history_id=None):
   """
-  Deprecated except for testing I guess.
-
   View function for executing an arbitrary query.
-  It understands the optional GET/POST params:
-
-    on_success_url
-      If given, it will be displayed when the query is successfully finished.
-      Otherwise, it will display the view query results page by default.
   """
-  authorized_get_design(request, design_id)
-
-  error_message = None
-  form = QueryForm()
-  action = request.path
-  log = None
-  app_name = get_app_name(request)
-  query_type = SavedQuery.TYPES_MAPPING[app_name]
-  design = safe_get_design(request, query_type, design_id)
-  on_success_url = request.REQUEST.get('on_success_url')
-  databases = []
-  query_server = get_query_server_config(app_name)
-  db = dbms.get(request.user, query_server)
-
-  try:
-    databases = get_db_choices(request)
-  except Exception, ex:
-    error_message, log = expand_exception(ex, db)
-
-  if request.method == 'POST':
-    form.bind(request.POST)
-    form.query.fields['database'].choices =  databases # Could not do it in the form
-
-    to_explain = request.POST.has_key('button-explain')
-    to_submit = request.POST.has_key('button-submit')
-
-    # Always validate the saveform, which will tell us whether it needs explicit saving
-    if form.is_valid():
-      to_save = form.saveform.cleaned_data['save']
-      to_saveas = form.saveform.cleaned_data['saveas']
-
-      if to_saveas and not design.is_auto:
-        # Save As only affects a previously saved query
-        design = design.clone()
-
-      if to_submit or to_save or to_saveas or to_explain:
-        explicit_save = to_save or to_saveas
-        if explicit_save:
-          request.info(_('Query saved!'))
-        design = save_design(request, form, query_type, design, explicit_save)
-        action = reverse(app_name + ':execute_query', kwargs={'design_id': design.id})
-
-      if to_explain or to_submit:
-        query_str = form.query.cleaned_data["query"]
-
-        # (Optional) Parameterization.
-        parameterization = get_parameterization(request, query_str, form, design, to_explain)
-        if parameterization:
-          return parameterization
-
-        try:
-          query = HQLdesign(form, query_type=query_type)
-          if to_explain:
-            return explain_directly(request, query, design, query_server)
-          else:
-            download = request.POST.has_key('download')
-            return execute_directly(request, query, query_server, design, on_success_url=on_success_url, download=download)
-        except Exception, ex:
-          error_message, log = expand_exception(ex, db)
+  if query_history_id:
+    query = authorized_get_query_history(request, query_history_id, must_exist=True)
+    design = query.design
   else:
-    if design.id is not None:
-      data = HQLdesign.loads(design.data).get_query_dict()
-      form.bind(data)
-      form.saveform.set_data(design.name, design.desc)
-    else:
-      # New design
-      form.bind()
-    form.query.fields['database'].choices = databases # Could not do it in the form
+    # Check perms.
+    authorized_get_design(request, design_id)
 
-  if not databases:
-    request.error(_('No databases are available. Permissions could be missing.'))
+    app_name = get_app_name(request)
+    query_type = SavedQuery.TYPES_MAPPING[app_name]
+    design = safe_get_design(request, query_type, design_id)
+    query = None
 
-  return render('execute.mako', request, {
-    'action': action,
+  context = {
     'design': design,
-    'error_message': error_message,
-    'form': form,
-    'log': log,
+    'query': query,
     'autocomplete_base_url': reverse(get_app_name(request) + ':api_autocomplete_databases', kwargs={}),
-    'on_success_url': on_success_url,
-    'can_edit_name': design.id and not design.is_auto,
-  })
+    'can_edit_name': design.id and not design.is_auto
+  }
+
+  return render('execute.mako', request, context)
 
 
 def watch_query(request, id):
@@ -453,7 +384,7 @@ def watch_query(request, id):
   All other GET params will be passed to on_success_url (if present).
   """
   # Coerce types: manage arguments
-  query_history = authorized_get_history(request, id, must_exist=True)
+  query_history = authorized_get_query_history(request, id, must_exist=True)
   db = dbms.get(request.user, query_history.get_query_server_config())
 
   # GET param: context.
@@ -530,7 +461,7 @@ def view_results(request, id, first_row=0):
   columns = []
   app_name = get_app_name(request)
 
-  query_history = authorized_get_history(request, id, must_exist=True)
+  query_history = authorized_get_query_history(request, id, must_exist=True)
   query_server = query_history.get_query_server_config()
   db = dbms.get(request.user, query_server)
 
@@ -618,8 +549,10 @@ def view_results(request, id, first_row=0):
 
   if request.GET.get('format') == 'json':
     context['columns'] = massage_columns_for_json(columns)
-    del context['save_form']
-    del context['query']
+    if 'save_form' in context:
+      del context['save_form']
+    if 'query' in context:
+      del context['query']
     return HttpResponse(json.dumps(context), mimetype="application/json")
   else:
     return render('watch_results.mako', request, context)
@@ -631,7 +564,7 @@ def save_results(request, id):
 
   Save the results of a query to an HDFS directory or Hive table.
   """
-  query_history = authorized_get_history(request, id, must_exist=True)
+  query_history = authorized_get_query_history(request, id, must_exist=True)
 
   app_name = get_app_name(request)
   server_id, state = _get_query_handle_and_state(query_history)
@@ -792,7 +725,7 @@ def authorized_get_design(request, design_id, owner_only=False, must_exist=False
 
   return design
 
-def authorized_get_history(request, query_history_id, owner_only=False, must_exist=False):
+def authorized_get_query_history(request, query_history_id, owner_only=False, must_exist=False):
   if query_history_id is None and not must_exist:
     return None
   try:
@@ -908,7 +841,7 @@ def _run_parameterized_query(request, design_id, explain):
       db = dbms.get(request.user, query_server)
       error_message, log = expand_exception(ex, db)
       return render('execute.mako', request, {
-        'action': reverse(get_app_name(request) + ':execute_query'),
+        'action': reverse(get_app_name(request) + ':execute_design'),
         'design': design,
         'error_message': error_message,
         'form': query_form,

+ 166 - 131
apps/beeswax/static/js/beeswax.vm.js

@@ -15,11 +15,11 @@
 // limitations under the License.
 
 
-function BeeswaxViewModel(server, query_id) {
+function BeeswaxViewModel(server) {
   var self = this;
 
-  var QUERY_DEFAULTS = {
-    'id': query_id,
+  var DESIGN_DEFAULTS = {
+    'id': -1,
     'query': '',
     'name': null,
     'description': null,
@@ -31,12 +31,16 @@ function BeeswaxViewModel(server, query_id) {
     'email': false,
     'errors': [],
     'explain': false,
+    'history': {
+      'id': -1
+    },
     'results': {
       'rows': [],
       'columns': [],
       'empty': true,
       'explanation': null,
       'url': null,
+      'errors': [],
       'save': {
         'errors': null,
         'type': 'table',
@@ -56,22 +60,23 @@ function BeeswaxViewModel(server, query_id) {
   self.server = ko.observable(server);
   self.databases = ko.observableArray();
   self.selectedDatabase = ko.observable(0);
-  self.query = ko.mapping.fromJS(QUERY_DEFAULTS);
+  self.isReady = ko.observable(false);
+  self.design = ko.mapping.fromJS(DESIGN_DEFAULTS);
 
   self.hasMoreResults = ko.computed(function() {
-    return self.query.results.url() != null;
+    return self.design.results.url() != null;
   });
 
-  self.query.results.save.saveTargetError = ko.computed(function() {
-    return (self.query.results.save.errors() && 'save_target' in self.query.results.save.errors()) ? self.query.results.save.errors()['save_target'] : null;
+  self.design.results.save.saveTargetError = ko.computed(function() {
+    return (self.design.results.save.errors() && 'save_target' in self.design.results.save.errors()) ? self.design.results.save.errors()['save_target'] : null;
   });
 
-  self.query.results.save.targetTableError = ko.computed(function() {
-    return (self.query.results.save.errors() && 'target_table' in self.query.results.save.errors()) ? self.query.results.save.errors()['target_table'] : null;
+  self.design.results.save.targetTableError = ko.computed(function() {
+    return (self.design.results.save.errors() && 'target_table' in self.design.results.save.errors()) ? self.design.results.save.errors()['target_table'] : null;
   });
 
-  self.query.results.save.targetDirectoryError = ko.computed(function() {
-    return (self.query.results.save.errors() && 'target_dir' in self.query.results.save.errors()) ? self.query.results.save.errors()['target_dir'] : null;
+  self.design.results.save.targetDirectoryError = ko.computed(function() {
+    return (self.design.results.save.errors() && 'target_dir' in self.design.results.save.errors()) ? self.design.results.save.errors()['target_dir'] : null;
   });
 
   self.database = ko.computed({
@@ -92,7 +97,7 @@ function BeeswaxViewModel(server, query_id) {
 
   self.hasParametersFilled = ko.computed(function() {
     var hasBlank = false;
-    $.each(self.query.parameters(), function(index, element) {
+    $.each(self.design.parameters(), function(index, element) {
       if (element.value() == '') {
         hasBlank = true;
       }
@@ -101,25 +106,25 @@ function BeeswaxViewModel(server, query_id) {
   });
 
   self.resetQuery = function() {
-    ko.mapping.fromJS(QUERY_DEFAULTS, self.query);
+    ko.mapping.fromJS(QUERY_DEFAULTS, self.design);
   };
 
   self.updateDatabases = function(databases) {
     self.databases(databases);
   };
 
-  self.updateQuery = function(design) {
-    self.query.query(design.query);
-    self.query.id(design.id);
-    self.query.name(design.name);
-    self.query.description(design.desc);
+  self.updateDesign = function(design) {
+    self.design.query(design.query);
+    self.design.id(design.id);
+    self.design.name(design.name);
+    self.design.description(design.desc);
     self.database(design.database);
-    self.query.isParameterized(design.is_parameterized);
-    self.query.email(design.email_notify);
+    self.design.isParameterized(design.is_parameterized);
+    self.design.email(design.email_notify);
 
-    self.query.settings.removeAll();
-    self.query.fileResources.removeAll();
-    self.query.functions.removeAll();
+    self.design.settings.removeAll();
+    self.design.fileResources.removeAll();
+    self.design.functions.removeAll();
 
     $.each(design.settings, function(index, setting) {
       self.addSetting(setting.key, setting.value);
@@ -132,8 +137,15 @@ function BeeswaxViewModel(server, query_id) {
     });
   };
 
+  self.updateHistory = function(history) {
+    self.design.history.id(history.id);
+    viewModel.design.results.url('/' + viewModel.server() + '/results/' + history.id + '/0?format=json');
+    viewModel.design.watch.url('/' + viewModel.server() + '/api/watch/json/' + history.id);
+    self.updateDesign(history.design);
+  };
+
   self.updateParameters = function(parameters) {
-    self.query.parameters.removeAll();
+    self.design.parameters.removeAll();
     $.each(parameters, function(index, parameter) {
       self.addParameter(parameter.parameter, parameter.name, '');
     });
@@ -146,15 +158,15 @@ function BeeswaxViewModel(server, query_id) {
       'value': ko.observable(value)
     };
     obj.parameter.subscribe(function() {
-      self.query.parameters.valueHasMutated();
+      self.design.parameters.valueHasMutated();
     });
     obj.name.subscribe(function() {
-      self.query.parameters.valueHasMutated();
+      self.design.parameters.valueHasMutated();
     });
     obj.value.subscribe(function() {
-      self.query.parameters.valueHasMutated();
+      self.design.parameters.valueHasMutated();
     });
-    self.query.parameters.push(obj);
+    self.design.parameters.push(obj);
   };
 
   self.addSetting = function(key, value) {
@@ -163,16 +175,16 @@ function BeeswaxViewModel(server, query_id) {
       'value': ko.observable(value)
     };
     obj.key.subscribe(function() {
-      self.query.settings.valueHasMutated();
+      self.design.settings.valueHasMutated();
     });
     obj.value.subscribe(function() {
-      self.query.settings.valueHasMutated();
+      self.design.settings.valueHasMutated();
     });
-    self.query.settings.push(obj);
+    self.design.settings.push(obj);
   };
 
   self.removeSetting = function(index) {
-    self.query.settings.splice(index, 1);
+    self.design.settings.splice(index, 1);
   };
 
   self.addFileResources = function(type, path) {
@@ -181,16 +193,16 @@ function BeeswaxViewModel(server, query_id) {
       'path': ko.observable(path)
     };
     obj.type.subscribe(function() {
-      self.query.fileResources.valueHasMutated();
+      self.design.fileResources.valueHasMutated();
     });
     obj.path.subscribe(function() {
-      self.query.fileResources.valueHasMutated();
+      self.design.fileResources.valueHasMutated();
     });
-    self.query.fileResources.push(obj);
+    self.design.fileResources.push(obj);
   };
 
   self.removeFileResources = function(index) {
-    self.query.fileResources.splice(index, 1);
+    self.design.fileResources.splice(index, 1);
   };
 
   self.addFunction = function(name, class_name) {
@@ -199,16 +211,16 @@ function BeeswaxViewModel(server, query_id) {
       'class_name': ko.observable(class_name)
     };
     obj.name.subscribe(function() {
-      self.query.functions.valueHasMutated();
+      self.design.functions.valueHasMutated();
     });
     obj.class_name.subscribe(function() {
-      self.query.functions.valueHasMutated();
+      self.design.functions.valueHasMutated();
     });
-    self.query.functions.push(obj);
+    self.design.functions.push(obj);
   };
 
   self.removeFunction = function(index) {
-    self.query.functions.splice(index, 1);
+    self.design.functions.splice(index, 1);
   };
 
   function getMultiFormData(prefix, arr, members) {
@@ -226,20 +238,20 @@ function BeeswaxViewModel(server, query_id) {
   }
 
   self.getSettingsFormData = function() {
-    return getMultiFormData('settings', self.query.settings(), ['key', 'value']);
+    return getMultiFormData('settings', self.design.settings(), ['key', 'value']);
   };
 
   self.getFileResourcesFormData = function() {
-    return getMultiFormData('file_resources', self.query.fileResources(), ['type', 'path']);
+    return getMultiFormData('file_resources', self.design.fileResources(), ['type', 'path']);
   };
 
   self.getFunctionsFormData = function() {
-    return getMultiFormData('functions', self.query.functions(), ['name', 'class_name']);
+    return getMultiFormData('functions', self.design.functions(), ['name', 'class_name']);
   };
 
   self.getParametersFormData = function() {
     var data = {};
-    $.each(self.query.parameters(), function(index, parameter) {
+    $.each(self.design.parameters(), function(index, parameter) {
       data[parameter.parameter()] = parameter.value();
     });
     return data;
@@ -247,14 +259,14 @@ function BeeswaxViewModel(server, query_id) {
 
   self.getOtherData = function() {
     var data = {
-      'query-email_notify': self.query.email(),
-      'query-is_parameterized': self.query.isParameterized()
+      'query-email_notify': self.design.email(),
+      'query-is_parameterized': self.design.isParameterized()
     };
     return data;
   };
 
   var error_fn = function(jqXHR, status, errorThrown) {
-    self.query.isRunning(false);
+    self.design.isRunning(false);
     try {
       $(document).trigger('server.error', $.parseJSON(jqXHR.responseText));
     } catch(e) {
@@ -275,15 +287,31 @@ function BeeswaxViewModel(server, query_id) {
     $.ajax(request);
   };
 
-  self.fetchQuery = function() {
+  self.fetchDesign = function() {
+    $(document).trigger('fetch.design');
+
+    var request = {
+      url: '/' + self.server() + '/api/design/' + self.design.id() + '/get',
+      dataType: 'json',
+      type: 'GET',
+      success: function(data) {
+        self.updateDesign(data.design);
+        $(document).trigger('fetched.design', data);
+      },
+      error: error_fn
+    };
+    $.ajax(request);
+  };
+
+  self.fetchQueryHistory = function() {
     $(document).trigger('fetch.query');
 
     var request = {
-      url: '/' + self.server() + '/api/query/' + self.query.id() + '/get',
+      url: '/' + self.server() + '/api/query/' + self.design.history.id() + '/get',
       dataType: 'json',
       type: 'GET',
       success: function(data) {
-        self.updateQuery(data.design);
+        self.updateHistory(data.query_history);
         $(document).trigger('fetched.query', data);
       },
       error: error_fn
@@ -291,11 +319,12 @@ function BeeswaxViewModel(server, query_id) {
     $.ajax(request);
   };
 
+
   self.fetchParameters = function() {
     $(document).trigger('fetch.parameters');
 
     var data = {
-      'query-query': self.query.query(),
+      'query-query': self.design.query(),
       'query-database': self.database()
     };
     $.extend(data, self.getSettingsFormData());
@@ -319,11 +348,11 @@ function BeeswaxViewModel(server, query_id) {
 
   self.explainQuery = function() {
     $(document).trigger('explain.query', data);
-    self.query.explain(true);
-    self.query.isRunning(true);
+    self.design.explain(true);
+    self.design.isRunning(true);
 
     var data = {
-      'query-query': self.query.query(),
+      'query-query': self.design.query(),
       'query-database': self.database()
     };
     $.extend(data, self.getSettingsFormData());
@@ -336,18 +365,18 @@ function BeeswaxViewModel(server, query_id) {
       dataType: 'json',
       type: 'POST',
       success: function(data) {
-        self.query.errors.removeAll();
+        self.design.errors.removeAll();
         if (data.status == 0) {
-          self.query.watch.logs.removeAll();
-          self.query.results.rows.removeAll();
-          self.query.results.columns.removeAll();
-          self.query.results.explanation(data.explanation);
+          self.design.watch.logs.removeAll();
+          self.design.results.rows.removeAll();
+          self.design.results.columns.removeAll();
+          self.design.results.explanation(data.explanation);
         } else {
-          self.query.errors.push(data.message);
+          self.design.errors.push(data.message);
           $(document).trigger('error.query');
         }
         $(document).trigger('explained.query', data);
-        self.query.isRunning(false);
+        self.design.isRunning(false);
       },
       error: error_fn,
       data: data
@@ -357,13 +386,13 @@ function BeeswaxViewModel(server, query_id) {
 
   self.executeQuery = function() {
     $(document).trigger('execute.query', data);
-    self.query.explain(false);
-    self.query.isRunning(true);
-    self.query.isFinished(true);
-    self.query.errors.removeAll();
+    self.design.explain(false);
+    self.design.isRunning(true);
+    self.design.isFinished(true);
+    self.design.errors.removeAll();
 
     var data = {
-      'query-query': self.query.query(),
+      'query-query': self.design.query(),
       'query-database': self.database()
     };
     $.extend(data, self.getSettingsFormData());
@@ -376,16 +405,15 @@ function BeeswaxViewModel(server, query_id) {
       dataType: 'json',
       type: 'POST',
       success: function(data) {
-        self.query.errors.removeAll();
+        self.design.errors.removeAll();
         if (data.status == 0) {
-          self.query.id(data.id);
-          self.query.results.url('/' + self.server() + '/results/' + self.query.id() + '/0?format=json');
-          self.query.watch.url(data.watch_url);
-          self.query.statement(data.statement);
+          self.design.results.url('/' + self.server() + '/results/' + data.id + '/0?format=json');
+          self.design.watch.url(data.watch_url);
+          self.design.statement(data.statement);
           self.watchQueryLoop();
         } else {
-          self.query.errors.push(data.message);
-          self.query.isRunning(false);
+          self.design.errors.push(data.message);
+          self.design.isRunning(false);
           $(document).trigger('error.query');
         }
         $(document).trigger('executed.query', data);
@@ -398,24 +426,24 @@ function BeeswaxViewModel(server, query_id) {
 
   self.executeNextStatement = function() {
     $(document).trigger('execute.query', data);
-    self.query.explain(false);
-    self.query.isRunning(true);
-    self.query.isFinished(true);
-    self.query.errors.removeAll();
+    self.design.explain(false);
+    self.design.isRunning(true);
+    self.design.isFinished(true);
+    self.design.errors.removeAll();
 
     var data = {
       'next': true
     };
     var request = {
-      url: self.query.watch.url(),
+      url: self.design.watch.url(),
       dataType: 'json',
       type: 'POST',
       success: function(data) {
-        self.query.errors.removeAll();
-        self.query.watch.logs.removeAll();
-        self.query.statement(data.statement);
-        self.query.watch.url(data.watch_url);
-        self.query.results.url('/' + self.server() + '/results/' + self.query.id() + '/0?format=json');
+        self.design.errors.removeAll();
+        self.design.watch.logs.removeAll();
+        self.design.statement(data.statement);
+        self.design.watch.url(data.watch_url);
+        self.design.results.url('/' + self.server() + '/results/' + data.id + '/0?format=json');
         self.watchQueryLoop();
         $(document).trigger('executed.query', data);
       },
@@ -427,7 +455,7 @@ function BeeswaxViewModel(server, query_id) {
 
   self.watchQuery = function() {
     var data = {
-      'query-query': self.query.query(),
+      'query-query': self.design.query(),
       'query-database': self.database()
     };
     $.extend(data, self.getSettingsFormData());
@@ -436,17 +464,17 @@ function BeeswaxViewModel(server, query_id) {
     $.extend(data, self.getParametersFormData());
     $.extend(data, self.getOtherData());
     var request = {
-      url: self.query.watch.url(),
+      url: self.design.watch.url(),
       dataType: 'json',
       type: 'POST',
       success: function(data) {
         $(document).trigger('watched.query', data);
       },
       error: function(jqXHR, status, errorThrown) {
-        self.query.isRunning(false);
+        self.design.isRunning(false);
         try {
           var data = $.parseJSON(jqXHR.responseText);
-          self.query.errors.push(data.error);
+          self.design.errors.push(data.error);
         } catch(e) {
           $(document).trigger('server.unmanageable_error', jqXHR.responseText);
         }
@@ -460,19 +488,19 @@ function BeeswaxViewModel(server, query_id) {
     var TIMEOUT = 1000;
     var timer = null;
 
-    self.query.watch.logs.removeAll();
-    self.query.watch.errors.removeAll();
-    self.query.results.rows.removeAll();
-    self.query.results.columns.removeAll();
+    self.design.watch.logs.removeAll();
+    self.design.watch.errors.removeAll();
+    self.design.results.rows.removeAll();
+    self.design.results.columns.removeAll();
 
     var _fn = function() {
       $(document).one('watched.query', function(e, data) {
         if (data.isSuccess || data.isFailure) {
           clearTimeout(timer);
-          self.query.isRunning(false);
+          self.design.isRunning(false);
 
           if (data.log) {
-            self.query.watch.logs.push(data.log);
+            self.design.watch.logs.push(data.log);
             // scroll logs
           }
           if (!data.isFailure) {
@@ -485,9 +513,9 @@ function BeeswaxViewModel(server, query_id) {
             }
           }
         } else {
-          self.query.statement(data.statement); // In case new no result statement executed
+          self.design.statement(data.statement); // In case new no result statement executed
           if (data.log) {
-            self.query.watch.logs.push(data.log);
+            self.design.watch.logs.push(data.log);
             // scroll logs
           }
 
@@ -502,22 +530,29 @@ function BeeswaxViewModel(server, query_id) {
 
   self.fetchResults = function() {
     $(document).trigger('fetch.results');
+    self.design.results.errors.removeAll();
     var request = {
-      url: self.query.results.url(),
+      url: self.design.results.url(),
       dataType: 'json',
       type: 'GET',
       success: function(data) {
-        self.query.isRunning(false);
-        self.query.isFinished(data.is_finished);
-        if (self.query.results.columns().length == 0){
-          self.query.results.columns(data.columns);
-        }
-        self.query.results.rows.push.apply(self.query.results.rows, data.results);
-        self.query.results.empty(self.query.results.rows().length == 0);
-        if (data.has_more) {
-          self.query.results.url(data.next_json_set);
+        if (data.error) {
+          self.design.results.errors.push(data.message);
+          self.design.isRunning(false);
+          self.design.results.empty(true);
         } else {
-          self.query.results.url(null);
+          self.design.isRunning(false);
+          self.design.isFinished(data.is_finished);
+          if (self.design.results.columns().length == 0){
+            self.design.results.columns(data.columns);
+          }
+          self.design.results.rows.push.apply(self.design.results.rows, data.results);
+          self.design.results.empty(self.design.results.rows().length == 0);
+          if (data.has_more) {
+            self.design.results.url(data.next_json_set);
+          } else {
+            self.design.results.url(null);
+          }
         }
         $(document).trigger('fetched.results', [data]);
       },
@@ -526,11 +561,11 @@ function BeeswaxViewModel(server, query_id) {
     $.ajax(request);
   };
 
-  self.saveQuery = function() {
+  self.saveDesign = function() {
     var self = this;
-    if (self.query.query() && self.query.name()) {
+    if (self.design.query() && self.design.name()) {
       var data = {
-        'query-query': self.query.query(),
+        'query-query': self.design.query(),
         'query-database': self.database()
       };
       $.extend(data, self.getSettingsFormData());
@@ -538,25 +573,25 @@ function BeeswaxViewModel(server, query_id) {
       $.extend(data, self.getFunctionsFormData());
       $.extend(data, self.getParametersFormData());
       $.extend(data, self.getOtherData());
-      data['saveform-name'] = self.query.name();
-      data['saveform-desc'] = self.query.description();
-      if (self.query.id() > 0) {
-        data['query-id'] = self.query.id();
+      data['saveform-name'] = self.design.name();
+      data['saveform-desc'] = self.design.description();
+      if (self.design.id() > 0) {
+        data['query-id'] = self.design.id();
       }
-      var url = '/' + self.server() + '/api/query/';
-      if (self.query.id() && self.query.id() != -1) {
-        url += self.query.id();
+      var url = '/' + self.server() + '/api/design/';
+      if (self.design.id() && self.design.id() != -1) {
+        url += self.design.id();
       }
       var request = {
         url: url,
         dataType: 'json',
         type: 'POST',
         success: function(data) {
-          self.query.id(data.design_id);
-          $(document).trigger('saved.query', [data.design_id]);
+          self.design.id(data.design_id);
+          $(document).trigger('saved.design', [data.design_id]);
         },
         error: function() {
-          $(document).trigger('error_save.query');
+          $(document).trigger('error_save.design');
         },
         data: data
       };
@@ -566,7 +601,7 @@ function BeeswaxViewModel(server, query_id) {
 
   self.cancelQuery = function() {
     $(document).trigger('cancel.query');
-    var url = '/' + self.server() + '/api/query/' + self.query.id() + '/cancel';
+    var url = '/' + self.server() + '/api/query/' + self.design.id() + '/cancel';
     $.post(url,
       function(response) {
         if (response['status'] != 0) {
@@ -580,9 +615,9 @@ function BeeswaxViewModel(server, query_id) {
 
   self.closeQuery = function() {
     var self = this;
-    if (self.query.id()) {
+    if (self.design.id()) {
       var data = {};
-      var url = '/' + self.server() + '/api/query/' + self.query.id() + '/close';
+      var url = '/' + self.server() + '/api/query/' + self.design.id() + '/close';
       var request = {
         url: url,
         dataType: 'json',
@@ -607,23 +642,23 @@ function BeeswaxViewModel(server, query_id) {
 
   self.saveResults = function() {
     var self = this;
-    if (self.query.id()) {
+    if (self.design.id()) {
       var data = {
-        'type': self.query.results.save.type(),
-        'path': self.query.results.save.path()
+        'type': self.design.results.save.type(),
+        'path': self.design.results.save.path()
       };
-      var url = '/' + self.server() + '/api/query/' + self.query.id() + '/results/save';
+      var url = '/' + self.server() + '/api/query/' + self.design.id() + '/results/save';
       var request = {
         url: url,
         dataType: 'json',
         type: 'POST',
         success: function(data) {
           if (data.status == 0) {
-            self.query.results.save.errors(null);
+            self.design.results.save.errors(null);
             if (data.id) {
               // watch this ID.
-              self.query.watch.url(data.watch_url);
-              self.query.watch.logs.removeAll();
+              self.design.watch.url(data.watch_url);
+              self.design.watch.logs.removeAll();
               self.watchQueryLoop(function() {
                 window.location.href = "/filebrowser/view" + data.path;
               });
@@ -633,12 +668,12 @@ function BeeswaxViewModel(server, query_id) {
             }
             $(document).trigger('saved.results', data);
           } else {
-            self.query.results.save.errors(data.errors);
+            self.design.results.save.errors(data.errors);
             $(document).trigger('error_save.results');
           }
         },
         error: function(data) {
-          self.query.results.save.errors(data);
+          self.design.results.save.errors(data);
           $(document).trigger('error_save.results');
         },
         data: data

+ 0 - 12
apps/filebrowser/src/filebrowser/templates/display.mako

@@ -31,18 +31,6 @@
 ${ commonheader(_('%(filename)s - File Viewer') % dict(filename=truncate(filename)), 'filebrowser', user) | n,unicode }
 ${ fb_components.menubar() }
 
-<style type="text/css">
-  .empty-wrapper {
-    margin-top: 50px;
-    color: #BBB;
-    line-height: 60px;
-  }
-
-  .empty-wrapper i {
-    font-size: 148px;
-  }
-</style>
-
 <div class="container-fluid">
   <div class="row-fluid">
     <div class="span2">

+ 2 - 2
apps/metastore/src/metastore/templates/util.mako

@@ -63,9 +63,9 @@ from django.utils.translation import ugettext as _
     % elif query_context[0] == 'design':
       <% design = query_context[1] %>
       % if design.is_auto:
-		<a href="${ url(app_name + ':execute_query', design.id)}">${_('Unsaved Query')}</a>
+		<a href="${ url(app_name + ':execute_design', design.id)}">${_('Unsaved Query')}</a>
       % else:
-        <a href="${ url(app_name + ':execute_query', design.id)}">${design.name}</a>
+        <a href="${ url(app_name + ':execute_design', design.id)}">${design.name}</a>
       % endif
     % else:
       ${_('Query Results')}

+ 2 - 2
apps/rdbms/src/rdbms/api.py

@@ -30,7 +30,7 @@ from librdbms.design import SQLdesign
 
 from beeswax import models as beeswax_models
 from beeswax.forms import SaveForm
-from beeswax.views import authorized_get_history, safe_get_design
+from beeswax.views import authorized_get_query_history, safe_get_design
 
 from rdbms.forms import SQLForm
 from rdbms.views import save_design
@@ -225,7 +225,7 @@ def fetch_results(request, id, first_row=0):
   fetch_error = False
   error_message = ''
 
-  query_history = authorized_get_history(request, id, must_exist=True)
+  query_history = authorized_get_query_history(request, id, must_exist=True)
   query_server = query_history.get_query_server_config()
   design = SQLdesign.loads(query_history.design.data)
   db = dbms.get(request.user, query_server)

+ 0 - 10
apps/rdbms/src/rdbms/templates/execute.mako

@@ -288,16 +288,6 @@ ${ commonheader(_('Query'), app_name, user) | n,unicode }
     white-space: nowrap;
   }
 
-  .empty-wrapper {
-    margin-top: 50px;
-    color: #BBB;
-    line-height: 60px;
-  }
-
-  .empty-wrapper i {
-    font-size: 148px;
-  }
-
   #navigatorTables li {
     width: 95%;
     white-space: nowrap;

+ 3 - 1
apps/rdbms/src/rdbms/urls.py

@@ -21,7 +21,9 @@ from django.conf.urls.defaults import patterns, url
 # Views
 urlpatterns = patterns('rdbms.views',
   url(r'^$', 'index', name='index'),
-  url(r'^execute/(?P<design_id>\d+)?$', 'execute_query', name='execute_query')
+  url(r'^execute/?$', 'execute_query', name='execute_query'),
+  url(r'^execute/design/(?P<design_id>\d+)$', 'execute_query', name='execute_design'),
+  url(r'^execute/query/(?P<query_history_id>\d+)$', 'execute_query', name='watch_query_history')
 )
 
 # APIs

+ 1 - 1
apps/rdbms/src/rdbms/views.py

@@ -59,7 +59,7 @@ def ensure_configuration(view_func):
 Queries Views
 """
 @ensure_configuration
-def execute_query(request, design_id=None):
+def execute_query(request, design_id=None, query_history_id=None):
   """
   View function for executing an arbitrary synchronously query.
   """

+ 0 - 10
apps/spark/src/spark/templates/editor.mako

@@ -339,16 +339,6 @@ ${ common.createContextModal() }
     white-space: nowrap;
   }
 
-  .empty-wrapper {
-    margin-top: 50px;
-    color: #BBB;
-    line-height: 60px;
-  }
-
-  .empty-wrapper i {
-    font-size: 148px;
-  }
-
   #navigatorTables li {
     width: 95%;
     white-space: nowrap;

+ 2 - 0
apps/spark/src/spark/urls.py

@@ -23,6 +23,8 @@ urlpatterns = patterns('spark.views',
   url(r'^$', 'editor', name='index'),
   url(r'^editor/(?P<design_id>.+)?$', 'editor', name='editor'),
   url(r'^editor/(?P<design_id>.+)?$', 'editor', name='execute_query'), # For Beeswax
+  url(r'^editor/design/(?P<design_id>.+)$', 'editor', name='execute_design'), # For Beeswax
+  url(r'^editor/query/(?P<query_history_id>.+)$', 'editor', name='watch_query_history'), # For Beeswax
   url(r'^editor/(?P<design_id>.+)?$', 'editor', name='view_job'), # For browser
   url(r'^list_jobs', 'list_jobs', name='list_jobs'),
   url(r'^list_contexts', 'list_contexts', name='list_contexts'),

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

@@ -41,7 +41,7 @@ from spark.decorators import view_error_handler
 LOG = logging.getLogger(__name__)
 
 @view_error_handler
-def editor(request, design_id=None):
+def editor(request, design_id=None, query_history_id=None):
   api = get_api(request.user)
   jobs = api.jobs()
   if design_id is not None and not design_id.isdigit():

+ 10 - 0
desktop/core/static/css/hue3.css

@@ -1004,6 +1004,16 @@ div.box {
   right: -27px;
 }
 
+.empty-wrapper {
+  margin-top: 50px;
+  color: #BBB;
+  line-height: 60px;
+}
+
+.empty-wrapper i {
+  font-size: 148px;
+}
+
 /*
  * Advanced options
  */