소스 검색

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 년 전
부모
커밋
8dfe6981df
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