瀏覽代碼

HUE-1889 [beeswax] Slickier editor UX

Merged DB dropdown into the Navigator tab
Moved Navigator to the left side and expanded the query area
Added recent queries
Added resize handle
Enrico Berti 11 年之前
父節點
當前提交
8dfe698
共有 2 個文件被更改,包括 383 次插入199 次删除
  1. 367 199
      apps/beeswax/src/beeswax/templates/execute.mako
  2. 16 0
      apps/beeswax/src/beeswax/views.py

+ 367 - 199
apps/beeswax/src/beeswax/templates/execute.mako

@@ -26,171 +26,196 @@
 ${ commonheader(_('Query'), app_name, user) | n,unicode }
 ${layout.menubar(section='query')}
 
+<div id="temporaryPlaceholder"></div>
 
 <div id="query-editor" class="container-fluid hide section">
 <div class="row-fluid">
-<div class="span2" id="advanced-settings">
-  <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>
-        <li class="white" style="padding-top:0px">
-          <select data-bind="options: databases, value: database" class="input-medium chosen-select" name="query-database" data-placeholder="${_('Choose a database...')}"></select>
-        </li>
-        <li class="nav-header">${_('settings')}</li>
-        <li class="white paramContainer">
-          <!-- ko foreach: design.settings.values -->
-          <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
-              </button>
-            </div>
-            <div data-bind="css: {'error': $root.getSettingKeyErrors($index()).length > 0}" class="control-group">
-              <label>${_('Key')}</label>
-              <input data-bind="value: key" type="text" class="settingsField span8" autocomplete="off" placeholder="mapred.reduce.tasks"/>
-            </div>
-
-            <div data-bind="css: {'error': $root.getSettingValueErrors($index()).length > 0}" class="control-group">
-              <label>${_('Value')}</label>
-              <input data-bind="value: value" type="text" class="settingValuesField span8" placeholder="1"/>
-            </div>
-          </div>
-          <!-- /ko -->
 
-          <div class="control-group">
-            <a data-bind="click: function() { $root.addSetting('','') }" class="btn btn-mini paramAdd">${_('Add')}</a>
-          </div>
-        </li>
-        <li class="nav-header
-          % if app_name == 'impala':
-             hide
-          % endif
-          ">
-          ${_('File Resources')}
-        </li>
-        <li class="white paramContainer
-          % if app_name == 'impala':
-             hide
-          % endif
-          ">
-          <!-- ko foreach: design.fileResources.values -->
-          <div class="param">
-            <div class="remove">
-              <button data-bind="click: $root.removeFileResource.bind(this, $index())" type="button" class="btn btn-mini" title="${_('Delete this setting')}">&times;</button>
-            </div>
-            <div data-bind="css: {'error': $root.getFileResourceTypeErrors($index()).length > 0}" class="control-group">
-              <label>${_('Type')}</label>
-              <select data-bind="value: type" class="input-small">
-                <option value="JAR">${_('jar')}</option>
-                <option value="ARCHIVE">${_('archive')}</option>
-                <option value="FILE">${_('file')}</option>
-              </select>
-            </div>
-
-            <div data-bind="css: {'error': $root.getFileResourcePathErrors($index()).length > 0}" class="control-group">
-              <label>${_('Path')}</label>
-              <input data-bind="value: path" type="text" class="filesField span7 fileChooser" placeholder="/user/foo/udf.jar"/>
-            </div>
-          </div>
-          <!-- /ko -->
-
-          <div class="control-group">
-            <a data-bind="click: function() { $root.addFileResource('','') }" class="btn btn-mini paramAdd">${_('Add')}</a>
-          </div>
-        </li>
-        <li title="${ _('User-Defined Functions') }" class="nav-header
-          % if app_name == 'impala':
-            hide
-          % endif
-          ">
-          ${_('UDFs')}
-        </li>
-        <li class="white paramContainer
-          % if app_name == 'impala':
-            hide
-          % endif
-          ">
-          <!-- ko foreach: design.functions.values -->
-          <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>
-            </div>
-            <div data-bind="css: {'error': $root.getFunctionNameErrors($index()).length > 0}" class="control-group">
-              <label>${_('Name')}</label>
-              <input data-bind="value: name" type="text" class="functionsField span8" autocomplete="off" placeholder="myFunction"/>
-            </div>
-
-            <div data-bind="css: {'error': $root.getFunctionClassNameErrors($index()).length > 0}" class="control-group">
-              <label>${_('Class name')}</label>
-              <input data-bind="value: class_name" type="text" class="classNamesField span8" placeholder="com.acme.example"/>
-            </div>
-          </div>
-          <!-- /ko -->
-
-          <div class="control-group">
-            <a data-bind="click: function() { $root.addFunction('','') }" class="btn btn-mini paramAdd">${_('Add')}</a>
+<div class="span2" id="navigator">
+  <ul class="nav nav-tabs" style="margin-bottom: 0">
+    <li class="active"><a href="#navigatorTab" data-toggle="tab" class="sidetab">${_('Navigator')}</a></li>
+    <li><a href="#settingsTab" data-toggle="tab" class="sidetab">${_('Settings')} <span data-bind="visible:design.settings.values().length + design.fileResources.values().length + design.functions.values().length > 0, text: design.settings.values().length + design.fileResources.values().length + design.functions.values().length" class="badge badge-info">12</span></a></li>
+  </ul>
+  <div class="tab-content">
+    <div class="tab-pane active" id="navigatorTab">
+      <div class="card card-small card-tab">
+        <div class="card-body" style="margin-top: 0">
+          <a href="#" title="${_('Double click on a table name or field to insert it in the editor')}" rel="tooltip" data-placement="top" class="pull-right" style="margin:3px; margin-top:7px"><i class="fa fa-question-circle"></i></a>
+          <a id="refreshNavigator" href="#" title="${_('Manually refresh the table list')}" rel="tooltip" data-placement="top" class="pull-right" style="margin:3px; margin-top:7px"><i class="fa fa-refresh"></i></a>
+          <ul class="nav nav-list" style="border: none; padding: 0; background-color: #FFF">
+            <li class="nav-header">${_('database')}</li>
+          </ul>
+          <select data-bind="options: databases, value: database" class="input-medium chosen-select" name="query-database" data-placeholder="${_('Choose a database...')}"></select>
+          <input id="navigatorSearch" type="text" placeholder="${ _('Table name...') }" style="width:90%; margin-top: 20px"/>
+          <span id="navigatorNoTables">${_('The selected database has no tables.')}</span>
+          <ul id="navigatorTables" class="unstyled"></ul>
+          <div id="navigatorLoader">
+            <!--[if !IE]><!--><i class="fa fa-spinner fa-spin" style="font-size: 20px; color: #DDD"></i><!--<![endif]-->
+            <!--[if IE]><img src="/static/art/spinner.gif"/><![endif]-->
           </div>
-        </li>
-        <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: design.isParameterized" type="checkbox"/>
-            ${_("Enable parameterization")}
-          </label>
-          <label class="checkbox
-          % if app_name == 'impala':
-            hide
-          % endif
-          " rel="tooltip" data-original-title="${_("If checked, you will receive an email notification when the query completes.")}">
-            <input data-bind="checked: design.email" type="checkbox"/>
-            ${_("Email me on completion")}
-          </label>
-        </li>
-        % if app_name == 'impala':
-          <li class="nav-header">
-            ${_('Metastore Catalog')}
-          </li>
-          <li class="white">
-            <div class="control-group">
-              <span id="refresh-dyk">
-                <i class="fa fa-refresh"></i>
-                ${ _('Sync tables tips') }
-              </span>
-
-              <div id="refresh-content" class="hide">
-                <ul style="text-align: left;">
-                  <li>"invalidate
-                    metadata" ${ _("invalidates the entire catalog metadata. All table metadata will be reloaded on the next access.") }</li>
-                  <li>"invalidate metadata
-                    &lt;table&gt;" ${ _("invalidates the metadata, load on the next access") }</li>
-                  <li>"refresh
-                    &lt;table&gt;" ${ _("refreshes the metadata immediately. It is a faster, incremental refresh.") }</li>
-                </ul>
-              </div>
-            </div>
-          </li>
-        % endif
-        <li class="nav-header"></li>
-        <li class="white">
-          <div class="control-group">
-            <i class="fa fa-question-circle" id="help"></i>
-
-            <div id="help-content" class="hide">
-              <ul style="text-align: left;">
-                <li>${ _('Press CTRL + Space to autocomplete') }</li>
-                <li>${ _("You can execute queries with multiple SQL statements delimited by a semicolon ';'") }</li>
-                <li>${ _('You can highlight and run a fragment of a query') }</li>
+        </div>
+      </div>
+    </div>
+    <div class="tab-pane" id="settingsTab">
+      <div class="card card-small card-tab">
+        <div class="card-body">
+          <div id="advanced-settings">
+          <form id="advancedSettingsForm" action="" method="POST" class="form form-horizontal">
+              <ul class="nav nav-list" style="border: none; padding: 0;">
+                <li class="nav-header">${_('settings')}</li>
+                <li class="white paramContainer">
+                  <!-- ko foreach: design.settings.values -->
+                  <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
+                      </button>
+                    </div>
+                    <div data-bind="css: {'error': $root.getSettingKeyErrors($index()).length > 0}" class="control-group">
+                      <label>${_('Key')}</label>
+                      <input data-bind="value: key" type="text" class="settingsField span8" autocomplete="off" placeholder="mapred.reduce.tasks"/>
+                    </div>
+
+                    <div data-bind="css: {'error': $root.getSettingValueErrors($index()).length > 0}" class="control-group">
+                      <label>${_('Value')}</label>
+                      <input data-bind="value: value" type="text" class="settingValuesField span8" placeholder="1"/>
+                    </div>
+                  </div>
+                  <!-- /ko -->
+
+                  <div class="control-group">
+                    <a data-bind="click: function() { $root.addSetting('','') }" class="btn btn-mini paramAdd">${_('Add')}</a>
+                  </div>
+                </li>
+                <li class="nav-header
+                  % if app_name == 'impala':
+                     hide
+                  % endif
+                  ">
+                  ${_('File Resources')}
+                </li>
+                <li class="white paramContainer
+                  % if app_name == 'impala':
+                     hide
+                  % endif
+                  ">
+                  <!-- ko foreach: design.fileResources.values -->
+                  <div class="param">
+                    <div class="remove">
+                      <button data-bind="click: $root.removeFileResource.bind(this, $index())" type="button" class="btn btn-mini" title="${_('Delete this setting')}">&times;</button>
+                    </div>
+                    <div data-bind="css: {'error': $root.getFileResourceTypeErrors($index()).length > 0}" class="control-group">
+                      <label>${_('Type')}</label>
+                      <select data-bind="value: type" class="input-small">
+                        <option value="JAR">${_('jar')}</option>
+                        <option value="ARCHIVE">${_('archive')}</option>
+                        <option value="FILE">${_('file')}</option>
+                      </select>
+                    </div>
+
+                    <div data-bind="css: {'error': $root.getFileResourcePathErrors($index()).length > 0}" class="control-group">
+                      <label>${_('Path')}</label>
+                      <input data-bind="value: path" type="text" class="filesField span7 fileChooser" placeholder="/user/foo/udf.jar"/>
+                    </div>
+                  </div>
+                  <!-- /ko -->
+
+                  <div class="control-group">
+                    <a data-bind="click: function() { $root.addFileResource('','') }" class="btn btn-mini paramAdd">${_('Add')}</a>
+                  </div>
+                </li>
+                <li title="${ _('User-Defined Functions') }" class="nav-header
+                  % if app_name == 'impala':
+                    hide
+                  % endif
+                  ">
+                  ${_('UDFs')}
+                </li>
+                <li class="white paramContainer
+                  % if app_name == 'impala':
+                    hide
+                  % endif
+                  ">
+                  <!-- ko foreach: design.functions.values -->
+                  <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>
+                    </div>
+                    <div data-bind="css: {'error': $root.getFunctionNameErrors($index()).length > 0}" class="control-group">
+                      <label>${_('Name')}</label>
+                      <input data-bind="value: name" type="text" class="functionsField span8" autocomplete="off" placeholder="myFunction"/>
+                    </div>
+
+                    <div data-bind="css: {'error': $root.getFunctionClassNameErrors($index()).length > 0}" class="control-group">
+                      <label>${_('Class name')}</label>
+                      <input data-bind="value: class_name" type="text" class="classNamesField span8" placeholder="com.acme.example"/>
+                    </div>
+                  </div>
+                  <!-- /ko -->
+
+                  <div class="control-group">
+                    <a data-bind="click: function() { $root.addFunction('','') }" class="btn btn-mini paramAdd">${_('Add')}</a>
+                  </div>
+                </li>
+                <li class="nav-header">${_('Options')}</li>
+                <li class="white" style="padding-top:0; padding-left:0">
+                  <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: design.isParameterized" type="checkbox"/>
+                    ${_("Enable parameterization")}
+                  </label>
+                  <label class="checkbox
+                  % if app_name == 'impala':
+                    hide
+                  % endif
+                  " rel="tooltip" data-original-title="${_("If checked, you will receive an email notification when the query completes.")}">
+                    <input data-bind="checked: design.email" type="checkbox"/>
+                    ${_("Email me on completion")}
+                  </label>
+                </li>
+                % if app_name == 'impala':
+                  <li class="nav-header">
+                    ${_('Metastore Catalog')}
+                  </li>
+                  <li class="white">
+                    <div class="control-group">
+                      <span id="refresh-dyk">
+                        <i class="fa fa-refresh"></i>
+                        ${ _('Sync tables tips') }
+                      </span>
+
+                      <div id="refresh-content" class="hide">
+                        <ul style="text-align: left;">
+                          <li>"invalidate
+                            metadata" ${ _("invalidates the entire catalog metadata. All table metadata will be reloaded on the next access.") }</li>
+                          <li>"invalidate metadata
+                            &lt;table&gt;" ${ _("invalidates the metadata, load on the next access") }</li>
+                          <li>"refresh
+                            &lt;table&gt;" ${ _("refreshes the metadata immediately. It is a faster, incremental refresh.") }</li>
+                        </ul>
+                      </div>
+                    </div>
+                  </li>
+                % endif
               </ul>
-            </div>
+          </form>
           </div>
-        </li>
-      </ul>
+        </div>
+      </div>
     </div>
-  </form>
+  </div>
 </div>
 
-<div id="querySide" class="span8">
+<div id="querySide" class="span10">
   <div id="queryContainer" class="card card-small">
+    <div class="pull-right" style="margin: 10px">
+      <i class="fa fa-question-circle" id="help"></i>
+      <div id="help-content" class="hide">
+        <ul style="text-align: left;">
+          <li>${ _('Press CTRL + Space to autocomplete') }</li>
+          <li>${ _("You can execute queries with multiple SQL statements delimited by a semicolon ';'") }</li>
+          <li>${ _('You can highlight and run a fragment of a query') }</li>
+        </ul>
+      </div>
+    </div>
     <div style="margin-bottom: 30px">
         % if can_edit_name:
         <h1 class="card-heading simple">
@@ -259,8 +284,38 @@ ${layout.menubar(section='query')}
     </div>
   </div>
 
-  <div class="card card-small scrollable resultsContainer">
+  <div id="resizePanel"><a href="javascript:void(0)"><i class="fa fa-ellipsis-h"></i></a></div>
+
+  <div id="recentSection">
+    <ul class="nav nav-tabs" style="margin-bottom: 0">
+      <li class="active"><a href="#recentTab" data-toggle="tab" class="sidetab">${_('Recent queries')}</a></li>
+    </ul>
+    <div class="tab-content">
+      <div class="tab-pane active" id="recentTab">
+        <div class="card card-tab" style="padding-top: 1px;">
+          <div class="card-body">
+            <div id="recentLoader">
+              <!--[if !IE]><!--><i class="fa fa-spinner fa-spin" style="font-size: 20px; color: #DDD"></i><!--<![endif]-->
+              <!--[if IE]><img src="/static/art/spinner.gif"/><![endif]-->
+            </div>
+            <table id="recentQueries" class="table table-striped table-condensed datatables" style="padding-left: 0;">
+              <thead>
+                <tr>
+                  <th>${_('Time')}</th>
+                  <th>${_('Query')}</th>
+                  <th>${_('Result')}</th>
+                </tr>
+              </thead>
+              <tbody>
+              </tbody>
+            </table>
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
 
+  <div class="card card-small scrollable resultsContainer">
     <div data-bind="visible: !$root.design.results.empty()">
       <a id="expandResults" href="javascript:void(0)" title="${_('See results in full screen')}" rel="tooltip"
         class="view-query-results hide pull-right"><h4 style="margin-right: 20px"><i class="fa fa-expand"></i></h4></a>
@@ -398,26 +453,7 @@ ${layout.menubar(section='query')}
   </div>
 </div>
 
-<div class="span2" id="navigator">
-  <div class="card card-small">
-    <a href="#" title="${_('Double click on a table name or field to insert it in the editor')}" rel="tooltip" data-placement="left" class="pull-right" style="margin:10px;margin-left: 0"><i class="fa fa-question-circle"></i></a>
-    <a id="refreshNavigator" href="#" title="${_('Manually refresh the table list')}" rel="tooltip" data-placement="left" class="pull-right" style="margin:10px"><i class="fa fa-refresh"></i></a>
-
-    <h1 class="card-heading simple"><i class="fa fa-compass"></i> ${_('Navigator')}</h1>
 
-    <div class="card-body">
-      <p>
-        <input id="navigatorSearch" type="text" placeholder="${ _('Table name...') }" style="width:90%"/>
-        <span id="navigatorNoTables">${_('The selected database has no tables.')}</span>
-        <ul id="navigatorTables" class="unstyled"></ul>
-        <div id="navigatorLoader">
-          <!--[if !IE]><!--><i class="fa fa-spinner fa-spin" style="font-size: 20px; color: #DDD"></i><!--<![endif]-->
-          <!--[if IE]><img src="/static/art/spinner.gif"/><![endif]-->
-        </div>
-      </p>
-    </div>
-  </div>
-</div>
 </div>
 </div>
 
@@ -609,6 +645,7 @@ ${layout.menubar(section='query')}
   </div>
 </div>
 
+<script src="/static/ext/js/jquery/plugins/jquery-ui-draggable-droppable-sortable-1.8.23.min.js" type="text/javascript" charset="utf-8"></script>
 <script src="/static/ext/js/routie-0.3.0.min.js" type="text/javascript" charset="utf-8"></script>
 <script src="/static/ext/js/knockout-min.js" type="text/javascript" charset="utf-8"></script>
 <script src="/static/ext/js/knockout.mapping-2.3.2.js" type="text/javascript" charset="utf-8"></script>
@@ -683,10 +720,6 @@ ${layout.menubar(section='query')}
     background-color: #F0F0F0;
   }
 
-  .paramAdd {
-    margin-left: 18px;
-  }
-
   .remove {
     float: right;
   }
@@ -760,6 +793,52 @@ ${layout.menubar(section='query')}
     color: #999;
   }
 
+  #queryContainer {
+    margin-bottom: 0;
+  }
+
+  #resizePanel a {
+    position: absolute;
+    cursor: ns-resize;
+    color: #666;
+    margin-left: auto;
+    margin-right: auto;
+  }
+
+  .resultsContainer {
+    margin-top: 20px;
+  }
+
+  #recentSection {
+    margin-top: 20px;
+  }
+
+  #recentSection code {
+    cursor: pointer;
+    white-space: normal;
+  }
+
+  #recentQueries {
+    width: 100%;
+  }
+
+  #navigator .card-body {
+    margin-top: 1px !important;
+    padding: 6px !important;
+  }
+
+  #navigator .nav-header {
+    padding-left: 0;
+  }
+
+  #navigator .control-group {
+    padding-left: 0;
+  }
+
+  #navigator .nav-list > li.white, #navigator .nav-list .nav-header {
+    margin: 0;
+  }
+
 </style>
 
 <link href="/static/ext/css/leaflet.css" rel="stylesheet">
@@ -771,7 +850,7 @@ ${layout.menubar(section='query')}
 <script src="/static/ext/chosen/chosen.jquery.min.js" type="text/javascript" charset="utf-8"></script>
 
 <script type="text/javascript" charset="utf-8">
-var codeMirror, renderNavigator, resetNavigator, dataTable;
+var codeMirror, renderNavigator, resetNavigator, resizeNavigator, dataTable, renderRecent;
 
 var HIVE_AUTOCOMPLETE_BASE_URL = "${ autocomplete_base_url | n,unicode }";
 var HIVE_AUTOCOMPLETE_FAILS_SILENTLY_ON = [500]; // error codes from beeswax/views.py - autocomplete
@@ -782,10 +861,90 @@ var HIVE_AUTOCOMPLETE_GLOBAL_CALLBACK = function (data) {
   }
 };
 
+function placeResizePanelHandle() {
+  // dynamically positioning the resize panel handle since IE doesn't play well with styles.
+  $("#resizePanel a").css("left", $("#resizePanel").position().left + $("#resizePanel").width()/2 - 8);
+}
+
+var CURRENT_CODEMIRROR_SIZE = 100;
 
-// Navigator.
+// Navigator, recent queries
 $(document).ready(function () {
 
+  var INITIAL_RESIZE_POSITION = 299;
+  $("#resizePanel a").draggable({
+    axis: "y",
+    drag: function(e, ui) {
+      draggableHelper($(this), e, ui);
+    },
+    stop: function(e, ui) {
+      draggableHelper($(this), e, ui);
+    }
+  });
+
+  function draggableHelper(el, e, ui) {
+    if (el.offset().top > INITIAL_RESIZE_POSITION){
+      CURRENT_CODEMIRROR_SIZE = 100 + (el.offset().top - INITIAL_RESIZE_POSITION);
+      codeMirror.setSize("99%", CURRENT_CODEMIRROR_SIZE);
+    }
+    if (ui.position.top < INITIAL_RESIZE_POSITION) {
+      ui.position.top = INITIAL_RESIZE_POSITION;
+    }
+  }
+
+
+  var recentQueries = $("#recentQueries").dataTable({
+      "bPaginate": false,
+      "bLengthChange": false,
+      "bInfo": false,
+      "bFilter": false,
+      "aoColumns": [
+        { "sWidth" : "20%"},
+        { "sWidth" : "75%"},
+        { "sWidth" : "5%", "bSortable": false }
+      ],
+      "aaSorting": [
+        [0, 'desc']
+      ],
+      "oLanguage": {
+        "sEmptyTable": "${_('No data available')}",
+        "sInfo": "${_('Showing _START_ to _END_ of _TOTAL_ entries')}",
+        "sInfoEmpty": "${_('Showing 0 to 0 of 0 entries')}",
+        "sInfoFiltered": "${_('(filtered from _MAX_ total entries)')}",
+        "sZeroRecords": "${_('No matching records')}",
+        "oPaginate": {
+          "sFirst": "${_('First')}",
+          "sLast": "${_('Last')}",
+          "sNext": "${_('Next')}",
+          "sPrevious": "${_('Previous')}"
+        }
+      }
+    });
+
+  renderRecent = function() {
+    $("#recentLoader").show();
+    recentQueries.fnClearTable();
+    $.getJSON("${ url(app_name + ':list_query_history') }?format=json", function(data) {
+      if (data && data.queries) {
+        $(data.queries).each(function(cnt, item){
+          recentQueries.fnAddData([
+            '<span data-time="' + item.timeInMs + '">' + item.timeFormatted + '</span>',
+            '<code style="cursor:pointer">' + item.query + '</code>',
+            (item.resultsUrl != "" ? '<a href="' + item.resultsUrl + '">${_('See results...')}</a>': '')
+          ]);
+        });
+      }
+      $("#recentLoader").hide();
+      $("#recentQueries").css("width", "100%");
+    });
+  }
+
+  $(document).on("click", "#recentQueries code", function(){
+    codeMirror.setValue($(this).text());
+  });
+
+  renderRecent();
+
   $("#navigatorQuicklook").modal({
     show: false
   });
@@ -808,6 +967,12 @@ $(document).ready(function () {
     });
   });
 
+  resizeNavigator = function () {
+    $("#navigator .card").css("min-height", ($(window).height() - 150) + "px");
+    $("#navigatorTables").css("max-height", ($(window).height() - 280) + "px").css("overflow-y", "auto");
+    ##$("#recentQueries").css("max-height", ($(window).height() - 200) + "px").css("overflow-y", "auto");
+  }
+
   resetNavigator = function () {
     var _db = viewModel.database();
     if (_db != null) {
@@ -908,8 +1073,7 @@ $(document).ready(function () {
     resetNavigator();
   });
 
-  $("#navigator .card").css("min-height", ($(window).height() - 130) + "px");
-  $("#navigatorTables").css("max-height", ($(window).height() - 260) + "px").css("overflow-y", "auto");
+  resizeNavigator();
 
   viewModel.databases.subscribe(function () {
     if ($.totalStorage("${app_name}_last_database") != null && $.inArray($.totalStorage("${app_name}_last_database"), viewModel.databases())) {
@@ -936,7 +1100,7 @@ $(document).ready(function () {
     $("a[href='#results']").click();
   });
 
-  $(document).on("shown", "a[data-toggle='tab']", function (e) {
+  $(document).on("shown", "a[data-toggle='tab']:not(.sidetab)", function (e) {
     if ($(e.target).attr("href") == "#log") {
       logsAtEnd = true;
       window.setTimeout(resizeLogs, 150);
@@ -967,7 +1131,7 @@ function reinitializeTable(max) {
   var _max = max || 10;
 
   function fn(){
-    var container = $($("a[data-toggle='tab']").parent(".active").find("a").attr("href"));
+    var container = $($("a[data-toggle='tab']:not(.sidetab)").parent(".active").find("a").attr("href"));
     if ($(".dataTables_wrapper").height() > 0) {
       $(".dataTables_wrapper").jHueTableScroller({
         minHeight: $(window).height() - 190,
@@ -1013,8 +1177,7 @@ $(document).ready(function () {
     resizeTimeout = window.setTimeout(function () {
       // prevents endless loop in IE8
       if (winWidth != $(window).width() || winHeight != $(window).height()) {
-        $("#navigator .card").css("min-height", ($(window).height() - 130) + "px");
-        $("#navigatorTables").css("max-height", ($(window).height() - 260) + "px").css("overflow-y", "auto");
+        resizeNavigator();
         winWidth = $(window).width();
         winHeight = $(window).height();
       }
@@ -1177,6 +1340,7 @@ $(document).ready(function () {
   codeMirror.on("blur", function () {
     $(document.body).off("contextmenu");
   });
+
 });
 
 var editables = function() {
@@ -1236,7 +1400,7 @@ $(document).ready(function () {
     resizeLogs();
   });
 
-  $(document).on("shown", "a[data-toggle='tab']", function (e) {
+  $(document).on("shown", "a[data-toggle='tab']:not(.sidetab)", function (e) {
     if ($(e.target).attr("href") != "#results"){
       $($(e.target).attr("href")).css('height', 'auto');
       if ($(e.target).attr("href") == "#chart") {
@@ -1733,6 +1897,7 @@ $(document).ready(function () {
     'title': "${_('Did you know?')}",
     'content': $("#help-content").html(),
     'trigger': 'hover',
+    'placement': 'left',
     'html': true
   });
 
@@ -1825,8 +1990,8 @@ $(document).ready(function () {
     $('#navigator').show();
     $('#queryContainer').show();
     $('a[href="#query"]').parent().show();
-    if (!$('#querySide').hasClass('span8')) {
-      $('#querySide').addClass('span8');
+    if (!$('#querySide').hasClass('span10')) {
+      $('#querySide').addClass('span10');
     }
   }
 
@@ -1835,13 +2000,14 @@ $(document).ready(function () {
     $('#navigator').hide();
     $('#queryContainer').hide();
     $('a[href="#query"]').parent().hide();
-    if ($('#querySide').hasClass('span8')) {
-      $('#querySide').removeClass('span8');
+    if ($('#querySide').hasClass('span10')) {
+      $('#querySide').removeClass('span10');
     }
   }
 
   function queryPage() {
     queryPageComponents();
+    $('#recentSection').show();
     $('.resultsContainer').hide();
     $('.resultsContainer .watch-query').hide();
     $('.resultsContainer .view-query-results').hide();
@@ -1849,6 +2015,7 @@ $(document).ready(function () {
 
   function queryLogPage() {
     queryPageComponents();
+    $('#recentSection').hide();
     $('.resultsContainer').show();
     $('.resultsContainer .watch-query').show();
     $('.resultsContainer .view-query-results').hide();
@@ -1856,6 +2023,7 @@ $(document).ready(function () {
 
   function queryResultsPage() {
     queryPageComponents();
+    $('#recentSection').hide();
     $('.resultsContainer').show();
     $('.resultsContainer .watch-query').hide();
     $('.resultsContainer .view-query-results').show();
@@ -1863,6 +2031,7 @@ $(document).ready(function () {
 
   function parametersPage() {
     queryPageComponents();
+    $('#recentSection').hide();
     $('.resultsContainer').hide();
     $('.resultsContainer .watch-query').hide();
     $('.resultsContainer .view-query-results').hide();
@@ -1870,6 +2039,7 @@ $(document).ready(function () {
 
   function watchLogsPage() {
     watchPageComponents();
+    $('#recentSection').hide();
     $('.resultsContainer').show();
     $('.resultsContainer .watch-query').show();
     $('.resultsContainer .view-query-results').hide();
@@ -1877,6 +2047,7 @@ $(document).ready(function () {
 
   function watchResultsPage() {
     watchPageComponents();
+    $('#recentSection').hide();
     $('.resultsContainer').show();
     $('.resultsContainer .watch-query').hide();
     $('.resultsContainer .view-query-results').show();
@@ -1886,8 +2057,8 @@ $(document).ready(function () {
     'query': function () {
       showSection('query-editor');
       queryPage();
-
-      codeMirror.setSize("99%", $(window).height() - 270 - $("#queryPane .alert-error").outerHeight() - $(".nav-tabs").outerHeight());
+      codeMirror.setSize("99%", CURRENT_CODEMIRROR_SIZE);
+      placeResizePanelHandle();
     },
     'query/execute/params': function () {
       if (viewModel.design.parameters().length == 0) {
@@ -1914,17 +2085,16 @@ $(document).ready(function () {
       queryLogPage();
 
       clickHard('.resultsContainer .nav-tabs a[href="#log"]');
-
-      codeMirror.setSize("99%", 100);
     },
     'query/results': function () {
       showSection('query-editor');
       queryResultsPage();
 
-      codeMirror.setSize("99%", 100);
-
       clickHard('.resultsContainer .nav-tabs a[href="#results"]');
 
+      renderRecent();
+      placeResizePanelHandle();
+
       logGA('query/results');
     },
     'query/explanation': function () {
@@ -1936,8 +2106,6 @@ $(document).ready(function () {
       queryResultsPage();
 
       clickHard('.resultsContainer .nav-tabs a[href="#explanation"]');
-
-      codeMirror.setSize("99%", 100);
     },
     'watch/logs': function() {
       showSection('query-editor');

+ 16 - 0
apps/beeswax/src/beeswax/views.py

@@ -47,6 +47,8 @@ from beeswax.models import SavedQuery, QueryHistory
 from beeswax.server import dbms
 from beeswax.server.dbms import expand_exception, get_query_server_config
 
+import time
+
 
 LOG = logging.getLogger(__name__)
 
@@ -313,6 +315,13 @@ def list_query_history(request):
 
   filter = request.GET.get(prefix + 'search') and request.GET.get(prefix + 'search') or ''
 
+  if request.GET.get('format') == 'json':
+    resp = {
+      'queries': [massage_query_history_for_json(app_name, query_history) for query_history in page.object_list]
+    }
+    return HttpResponse(json.dumps(resp), mimetype="application/json")
+
+
   return render('list_history.mako', request, {
     'request': request,
     'page': page,
@@ -322,6 +331,13 @@ def list_query_history(request):
     'filter': filter,
   })
 
+def massage_query_history_for_json(app_name, query_history):
+  return {
+    'query': query_history.query,
+    'timeInMs': time.mktime(query_history.submission_date.timetuple()),
+    'timeFormatted': query_history.submission_date.strftime("%x %X"),
+    'resultsUrl': not query_history.is_failure() and reverse(app_name + ':watch_query_history', kwargs={'query_history_id': query_history.id}) or ""
+  }
 
 def download(request, id, format):
   assert format in common.DL_FORMATS