Explorar o código

HUE-1569 [beeswax] Table and fields helper

Added table and field navigator with double click editor fill in and data preview
Enrico Berti %!s(int64=12) %!d(string=hai) anos
pai
achega
252af7c

+ 127 - 6
apps/beeswax/src/beeswax/templates/execute.mako

@@ -61,7 +61,7 @@ ${layout.menubar(section='query')}
 
 <div class="container-fluid">
   <div class="row-fluid">
-    <div class="span3">
+    <div class="span2">
       <form id="advancedSettingsForm" action="${action}" method="POST" class="form form-horizontal">
         <div class="sidebar-nav">
           <ul class="nav nav-list">
@@ -255,7 +255,7 @@ ${layout.menubar(section='query')}
                 </form>
         </div>
 
-        <div id="querySide" class="span9">
+        <div id="querySide" class="span8">
           <div class="card card-small">
             % if on_success_url:
               <input type="hidden" name="on_success_url" value="${on_success_url}"/>
@@ -331,6 +331,23 @@ ${layout.menubar(section='query')}
             </div>
         </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"><i class="icon-question-sign"></i></a>
+        <h1 class="card-heading simple">${_('Navigator')}</h1>
+        <div class="card-body">
+          <p>
+            <input id="navigatorSearch" type="text" class="input-medium" placeholder="${ _('Table name...') }"/>
+            <ul id="navigatorTables" class="unstyled"></ul>
+            <div id="navigatorLoader">
+              <!--[if !IE]><!--><i class="icon-spinner icon-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>
 
@@ -375,6 +392,24 @@ ${layout.menubar(section='query')}
     </div>
 </div>
 
+<div id="navigatorQuicklook" class="modal hide fade">
+    <div class="modal-header">
+        <a href="#" class="close" data-dismiss="modal">&times;</a>
+        <a class="tableLink pull-right" href="#" target="_blank" style="margin-right: 20px;margin-top:6px"><i class="icon-external-link"></i> ${ _('View in Metastore Browser') }</a>
+        <h3>${_('Data sample for')} <span class="tableName"></span></h3>
+    </div>
+    <div class="modal-body" style="min-height: 100px">
+      <div class="loader">
+        <!--[if !IE]><!--><i class="icon-spinner icon-spin" style="font-size: 30px; color: #DDD"></i><!--<![endif]-->
+        <!--[if IE]><img src="/static/art/spinner.gif" /><![endif]-->
+      </div>
+      <div class="sample"></div>
+    </div>
+    <div class="modal-footer">
+        <button class="btn btn-primary disable-feedback" data-dismiss="modal">${_('Ok')}</button>
+    </div>
+</div>
+
 <script src="/static/ext/js/codemirror-3.11.js"></script>
 <link rel="stylesheet" href="/static/ext/css/codemirror.css">
 <script src="/static/js/codemirror-hql.js"></script>
@@ -450,6 +485,18 @@ ${layout.menubar(section='query')}
     color: #666;
     font-style: normal;
   }
+
+  #navigatorTables li {
+    width: 95%;
+    white-space: nowrap;
+    overflow: hidden;
+    text-overflow: ellipsis;
+  }
+
+  #navigatorSearch {
+    display: none;
+  }
+
 </style>
 
 <script src="/static/ext/js/jquery/plugins/jquery-fieldselection.js" type="text/javascript"></script>
@@ -457,8 +504,82 @@ ${layout.menubar(section='query')}
 
 <script type="text/javascript" charset="utf-8">
     var HIVE_AUTOCOMPLETE_BASE_URL = "${ autocomplete_base_url | n,unicode }";
-
+    var codeMirror;
     $(document).ready(function(){
+
+      $("#navigatorQuicklook").modal({
+        show: false
+      });
+
+      var navigatorSearchTimeout = -1;
+      $("#navigatorSearch").on("keyup", function () {
+        window.clearTimeout(navigatorSearchTimeout);
+        navigatorSearchTimeout = window.setTimeout(function () {
+          $("#navigatorTables li").removeClass("hide");
+          $("#navigatorTables li").each(function () {
+            if ($(this).text().toLowerCase().indexOf($("#navigatorSearch").val().toLowerCase()) == -1) {
+              $(this).addClass("hide");
+            }
+          });
+        }, 300);
+      });
+
+      hac_getTables($("#id_query-database").val(), function (data) {  //preload tables for the default db
+        $(data.split(" ")).each(function (cnt, table) {
+          var _table = $("<li>");
+          _table.html("<a href='#' class='pull-right hide'><i class='icon-eye-open'></i></a><a href='#' title='" + table + "'><i class='icon-table'></i> " + table + "</a><ul class='unstyled'></ul>");
+          _table.data("table", table).attr("id", "navigatorTables_" + table);
+          _table.find("a:eq(1)").on("click", function () {
+            _table.find(".icon-table").removeClass("icon-table").addClass("icon-spin").addClass("icon-spinner");
+            hac_getTableColumns($("#id_query-database").val(), table, "", function (plain_columns, extended_columns) {
+              _table.find("a:eq(0)").removeClass("hide");
+              _table.find("ul").empty();
+              _table.find(".icon-spinner").removeClass("icon-spinner").removeClass("icon-spin").addClass("icon-table");
+              $(extended_columns).each(function (iCnt, col) {
+                var _column = $("<li>");
+                _column.html("<a href='#' style='padding-left:10px'" + (col.comment != null && col.comment != "" ? " title='" + col.comment + "'" : "") + "><i class='icon-columns'></i> " + col.name + " (" + col.type + ")</a>");
+                _column.appendTo(_table.find("ul"));
+                _column.on("dblclick", function () {
+                  codeMirror.replaceSelection(col.name);
+                  codeMirror.setSelection(codeMirror.getCursor());
+                  codeMirror.focus();
+                });
+              });
+            });
+          });
+          _table.find("a:eq(1)").on("dblclick", function () {
+            codeMirror.replaceSelection(table);
+            codeMirror.setSelection(codeMirror.getCursor());
+            codeMirror.focus();
+          });
+          _table.find("a:eq(0)").on("click", function () {
+            $("#navigatorQuicklook").find(".tableName").text(table);
+            $("#navigatorQuicklook").find(".tableLink").attr("href", "/metastore/table/" + $("#id_query-database").val() + "/" + _table.data("table"));
+            $("#navigatorQuicklook").find(".sample").empty("");
+            $("#navigatorQuicklook").attr("style", "width: " + ($(window).width() - 120) + "px;margin-left:-" + (($(window).width() - 80) / 2) + "px!important;");
+            $.ajax({
+              url: "/metastore/table/" + $("#id_query-database").val() + "/" + _table.data("table"),
+              data: {"sample": true},
+              beforeSend: function (xhr) {
+                xhr.setRequestHeader("X-Requested-With", "Hue");
+              },
+              dataType: "html",
+              success: function (data) {
+                $("#navigatorQuicklook").find(".loader").hide();
+                $("#navigatorQuicklook").find(".sample").html(data);
+              }
+            });
+            $("#navigatorQuicklook").modal("show");
+          });
+          _table.appendTo($("#navigatorTables"));
+        });
+        $("#navigatorLoader").hide();
+        $("#navigatorSearch").show();
+      });
+
+      $("#navigator .card").css("min-height", ($(window).height() - 130) + "px");
+      $("#navigatorTables").css("max-height", ($(window).height() - 260) + "px").css("overflow-y", "auto");
+
       var queryPlaceholder = "${_('Example: SELECT * FROM tablename, or press CTRL + space')}";
 
       var successfunction = function (response, newValue) {
@@ -603,6 +724,8 @@ ${layout.menubar(section='query')}
           // prevents endless loop in IE8
           if (winWidth != $(window).width() || winHeight != $(window).height()) {
             codeMirror.setSize("95%", $(window).height() - 270 - $("#queryPane .alert-error").outerHeight() - $(".nav-tabs").outerHeight());
+            $("#navigator .card").css("min-height", ($(window).height() - 130) + "px");
+            $("#navigatorTables").css("max-height", ($(window).height() - 260) + "px").css("overflow-y", "auto");
             winWidth = $(window).width();
             winHeight = $(window).height();
           }
@@ -617,8 +740,6 @@ ${layout.menubar(section='query')}
 
       var queryEditor = $("#queryField")[0];
 
-      hac_getTables($("#id_query-database").val(), function(){}); //preload tables for the default db
-
       % if app_name == 'impala':
         var AUTOCOMPLETE_SET = CodeMirror.impalaSQLHint;
       % else:
@@ -703,7 +824,7 @@ ${layout.menubar(section='query')}
 
       CodeMirror.fromDot = false;
 
-      var codeMirror = CodeMirror(function (elt) {
+      codeMirror = CodeMirror(function (elt) {
         queryEditor.parentNode.replaceChild(elt, queryEditor);
       }, {
         value: queryEditor.value,

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

@@ -900,6 +900,7 @@ def autocomplete(request, database=None, table=None):
     else:
       t = db.get_table(database, table)
       response['columns'] = [column.name for column in t.cols]
+      response['extended_columns'] = massage_columns_for_json(t.cols)
   except TTransportException, tx:
     response['code'] = 503
     response['error'] = tx.message
@@ -916,6 +917,17 @@ def autocomplete(request, database=None, table=None):
 Utils
 """
 
+def massage_columns_for_json(cols):
+  massaged_cols = []
+  for column in cols:
+    massaged_cols.append({
+      'name': column.name,
+      'type': column.type,
+      'comment': column.comment
+    })
+  return massaged_cols
+
+
 # owner_only is deprecated
 def authorized_get_design(request, design_id, owner_only=False, must_exist=False):
   if design_id is None and not must_exist:

+ 5 - 3
apps/beeswax/static/js/autocomplete.utils.js

@@ -68,8 +68,8 @@ function hac_getTableColumns(databaseName, tableName, textScanned, callback) {
     tableName = _aliases[tableName];
   }
 
-  if ($.totalStorage('columns_' + databaseName + '_' + tableName) != null) {
-    callback($.totalStorage('columns_' + databaseName + '_' + tableName));
+  if ($.totalStorage('columns_' + databaseName + '_' + tableName) != null && $.totalStorage('extended_columns_' + databaseName + '_' + tableName) != null) {
+    callback($.totalStorage('columns_' + databaseName + '_' + tableName), $.totalStorage('extended_columns_' + databaseName + '_' + tableName));
     hac_jsoncalls({
       database: databaseName,
       table: tableName,
@@ -81,6 +81,7 @@ function hac_getTableColumns(databaseName, tableName, textScanned, callback) {
         }
         else {
           $.totalStorage('columns_' + databaseName + '_' + tableName, (data.columns ? data.columns.join(" ") : ""));
+          $.totalStorage('extended_columns_' + databaseName + '_' + tableName, (data.extended_columns ? data.extended_columns : []));
         }
       }
     });
@@ -97,7 +98,8 @@ function hac_getTableColumns(databaseName, tableName, textScanned, callback) {
         }
         else {
           $.totalStorage('columns_' + databaseName + '_' + tableName, (data.columns ? data.columns.join(" ") : ""));
-          callback($.totalStorage('columns_' + databaseName + '_' + tableName));
+          $.totalStorage('extended_columns_' + databaseName + '_' + tableName, (data.extended_columns ? data.extended_columns : []));
+          callback($.totalStorage('columns_' + databaseName + '_' + tableName), $.totalStorage('extended_columns_' + databaseName + '_' + tableName));
         }
       }
     });

+ 69 - 0
apps/metastore/src/metastore/templates/sample.mako

@@ -0,0 +1,69 @@
+## Licensed to Cloudera, Inc. under one
+## or more contributor license agreements.  See the NOTICE file
+## distributed with this work for additional information
+## regarding copyright ownership.  Cloudera, Inc. licenses this file
+## to you under the Apache License, Version 2.0 (the
+## "License"); you may not use this file except in compliance
+## with the License.  You may obtain a copy of the License at
+##
+##     http://www.apache.org/licenses/LICENSE-2.0
+##
+## Unless required by applicable law or agreed to in writing, software
+## distributed under the License is distributed on an "AS IS" BASIS,
+## WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+## See the License for the specific language governing permissions and
+## limitations under the License.
+<%!
+from desktop.lib.i18n import smart_unicode
+from desktop.views import commonheader, commonfooter
+from django.utils.translation import ugettext as _
+%>
+
+% if sample is not None:
+  % if error_message:
+    <div class="alert alert-error">
+      <h3>${_('Error!')}</h3>
+      <pre>${error_message | h}</pre>
+    </div>
+  % else:
+  <table class="table table-striped table-condensed sampleTable">
+    <thead>
+      <tr>
+        % for col in table.cols:
+          <th>${col.name}</th>
+        % endfor
+      </tr>
+    </thead>
+    <tbody>
+      % for i, row in enumerate(sample):
+      <tr>
+        % for item in row:
+        <td>${ smart_unicode(item, errors='ignore') }</td>
+        % endfor
+      </tr>
+      % endfor
+    </tbody>
+  </table>
+  % endif
+% endif
+
+<style type="text/css">
+  .sampleTable td, .sampleTable th {
+    white-space: nowrap;
+  }
+</style>
+
+<script type="text/javascript" charset="utf-8">
+  $(document).ready(function () {
+    $(".sampleTable").dataTable({
+      "bPaginate": false,
+      "bLengthChange": false,
+      "bInfo": false,
+      "bFilter": false,
+      "oLanguage": {
+        "sEmptyTable": "${_('No data available')}",
+        "sZeroRecords": "${_('No matching records')}",
+      }
+    });
+  });
+</script>

+ 5 - 1
apps/metastore/src/metastore/views.py

@@ -150,7 +150,11 @@ def describe_table(request, database, table):
   except Exception, ex:
     error_message, logs = dbms.expand_exception(ex, db)
 
-  return render("describe_table.mako", request, {
+  renderable = "describe_table.mako"
+  if request.REQUEST.get("sample", "false") == "true":
+    renderable = "sample.mako"
+
+  return render(renderable, request, {
     'breadcrumbs': [{
         'name': database,
         'url': reverse('metastore:show_tables', kwargs={'database': database})