浏览代码

HUE-1219 [impala] One page app

Removing all the mako template logic and moving to front end.
Added APIs for Beeswax.
Moved a bunch of logic from wait_results.mako and watch_query.mako
to execute.mako.
Explain uses one API now.
Make settings, file resources, and functions managed by front end.
Enable parameterization and send email notifications on front end.
Render navigator after database change via subscription to database
observable.
save and save as working with knockout.
Make typeahead source dynamically update.
parameterized queries are working with routie support.
Cancel query moved over from watch_wait.mako.
Abraham Elmahrek 12 年之前
父节点
当前提交
632716a

+ 287 - 0
apps/beeswax/src/beeswax/api.py

@@ -0,0 +1,287 @@
+#!/usr/bin/env python
+# 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.
+
+import json
+import logging
+
+from django.core.urlresolvers import reverse
+from django.http import HttpResponse, Http404
+from django.utils.translation import ugettext as _
+
+from thrift.transport.TTransport import TTransportException
+
+from desktop.context_processors import get_app_name
+
+from jobsub.parameterization import substitute_variables
+
+import beeswax.models
+
+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,\
+                          safe_get_design, save_design, massage_columns_for_json, _get_query_handle_and_state
+
+
+LOG = logging.getLogger(__name__)
+
+
+def error_handler(view_fn):
+  def decorator(*args, **kwargs):
+    try:
+      return view_fn(*args, **kwargs)
+    except Http404, e:
+      raise e
+    except Exception, e:
+      response = {
+        'error': str(e)
+      }
+      return HttpResponse(json.dumps(response), mimetype="application/json", status=500)
+  return decorator
+
+
+@error_handler
+def autocomplete(request, database=None, table=None):
+  app_name = get_app_name(request)
+  query_server = get_query_server_config(app_name)
+  db = dbms.get(request.user, query_server)
+  response = {}
+
+  try:
+    if database is None:
+      response['databases'] = db.get_databases()
+    elif table is None:
+      response['tables'] = db.get_tables(database=database)
+    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
+  except Exception, e:
+    LOG.warn('Autocomplete data fetching error %s.%s: %s' % (database, table, e))
+    response['code'] = 500
+    response['error'] = e.message
+
+  return HttpResponse(json.dumps(response), mimetype="application/json")
+
+
+@error_handler
+def parameters(request, design_id=None):
+  response = {'status': -1, 'message': ''}
+
+  # Use POST request to not confine query length.
+  if request.method != 'POST':
+    response['message'] = _('A POST request is required.')
+
+  parameterization_form_cls = make_parameterization_form(request.POST.get('query-query', ''))
+  if parameterization_form_cls:
+    parameterization_form = parameterization_form_cls(prefix="parameterization")
+
+    response['parameters'] = [{'parameter': field.html_name, 'name': field.name} for field in parameterization_form]
+    response['status']= 0
+  else:
+    response['parameters'] = []
+    response['status']= 0
+
+  return HttpResponse(json.dumps(response), mimetype="application/json")
+
+
+def execute_directly(request, query, design, query_server, tablename=None, **kwargs):
+  if design is not None:
+    design = authorized_get_design(request, design.id)
+
+  db = dbms.get(request.user, query_server)
+  database = query.query.get('database', 'default')
+  db.use(database)
+
+  history_obj = db.execute_query(query, design)
+  watch_url = reverse(get_app_name(request) + ':watch_query_refresh_json', kwargs={'id': history_obj.id})
+
+  response = {
+    'status': 0,
+    'id': history_obj.id,
+    'watch_url': watch_url
+  }
+
+  return HttpResponse(json.dumps(response), mimetype="application/json")
+
+
+def explain_directly(request, query, design, query_server):
+  explanation = dbms.get(request.user, query_server).explain(query)
+  
+  response = {
+    'status': 0,
+    'explanation': explanation.textual
+  }
+
+  return HttpResponse(json.dumps(response), mimetype="application/json")
+
+
+@error_handler
+def execute(request, query_id=None):
+  response = {'status': -1, 'message': ''}
+
+  if request.method != 'POST':
+    response['message'] = _('A POST request is required.')
+  
+  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)
+
+  try:
+    query_form = get_query_form(request)
+
+    if query_form.is_valid():
+      query_str = query_form.query.cleaned_data["query"]
+      explain = request.GET.get('explain', 'false').lower() == 'true'
+      design = save_design(request, query_form, query_type, design, False)
+
+      if query_form.query.cleaned_data['is_parameterized']:
+        # Parameterized query
+        parameterization_form_cls = make_parameterization_form(query_str)
+        if parameterization_form_cls:
+          parameterization_form = parameterization_form_cls(request.REQUEST, prefix="parameterization")
+
+          if parameterization_form.is_valid():
+            real_query = substitute_variables(query_str, parameterization_form.cleaned_data)
+            query = HQLdesign(query_form, query_type=query_type)
+            query._data_dict['query']['query'] = real_query
+
+            try:
+              if explain:
+                return explain_directly(request, query, design, query_server)
+              else:
+                return execute_directly(request, query, design, query_server)
+
+            except Exception, ex:
+              db = dbms.get(request.user, query_server)
+              error_message, log = expand_exception(ex, db)
+              response['message'] = error_message
+              return HttpResponse(json.dumps(response), mimetype="application/json")
+          else:
+            response['errors'] = parameterization_form.errors
+            return HttpResponse(json.dumps(response), mimetype="application/json")
+
+      # non-parameterized query
+      query = HQLdesign(query_form, query_type=query_type)
+      if request.GET.get('explain', 'false').lower() == 'true':
+        return explain_directly(request, query, design, query_server)
+      else:
+        return execute_directly(request, query, design, query_server)
+    else:
+      response['message'] = _('There was an error with your query.')
+      response['errors'] = query_form.query.errors
+  except RuntimeError, e:
+    response['message']= str(e)
+
+  return HttpResponse(json.dumps(response), mimetype="application/json")
+
+
+@error_handler
+def save_query(request, query_id=None):
+  response = {'status': -1, 'message': ''}
+
+  if request.method != 'POST':
+    response['message'] = _('A POST request is required.')
+
+  app_name = get_app_name(request)
+  query_type = beeswax.models.SavedQuery.TYPES_MAPPING[app_name]
+  design = safe_get_design(request, query_type, query_id)
+
+  try:
+    query_form = get_query_form(request)
+
+    if query_form.is_valid():
+      design = save_design(request, query_form, query_type, design, True)
+      response['design_id'] = design.id
+      response['status'] = 0
+    else:
+      response['errors'] = query_form.errors
+  except RuntimeError, e:
+    response['message'] = str(e)
+
+  return HttpResponse(json.dumps(response), mimetype="application/json")
+
+
+@error_handler
+def fetch_saved_query(request, query_id):
+  response = {'status': 0, 'message': ''}
+
+  if request.method != 'GET':
+    response['message'] = _('A GET request is required.')
+
+  app_name = get_app_name(request)
+  query_type = beeswax.models.SavedQuery.TYPES_MAPPING[app_name]
+  design = safe_get_design(request, query_type, query_id)
+
+  response['design'] = design_to_dict(design)
+  return HttpResponse(json.dumps(response), mimetype="application/json")
+
+
+@error_handler
+def cancel_query(request, query_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)
+      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)
+      response['status'] = 0
+    except Exception, e:
+      response['message'] = unicode(e)
+
+  return HttpResponse(json.dumps(response), mimetype="application/json")
+
+
+def design_to_dict(design):
+  hql_design = HQLdesign.loads(design.data)
+  return {
+    'id': design.id,
+    'query': hql_design.hql_query,
+    'name': design.name,
+    'desc': design.desc,
+    'database': hql_design.query.get('database', None),
+    'settings': hql_design.settings,
+    'file_resources': hql_design.file_resources,
+    'functions': hql_design.functions,
+    'is_parameterized': hql_design.query.get('is_parameterized', True),
+    'email_notify': hql_design.query.get('email_notify', True)
+  }
+
+
+def get_query_form(request):
+  # Get database choices
+  query_server = dbms.get_query_server_config(get_app_name(request))
+  db = dbms.get(request.user, query_server)
+  databases = [(database, database) for database in db.get_databases()]
+
+  if not databases:
+    raise RuntimeError(_("No databases are available. Permissions could be missing."))
+
+  query_form = QueryForm()
+  query_form.bind(request.POST)
+  query_form.query.fields['database'].choices = databases # Could not do it in the form
+
+  return query_form

文件差异内容过多而无法显示
+ 832 - 633
apps/beeswax/src/beeswax/templates/execute.mako


+ 0 - 54
apps/beeswax/src/beeswax/templates/explain.mako

@@ -1,54 +0,0 @@
-## 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.views import commonheader, commonfooter
-from django.utils.translation import ugettext as _
-%>
-
-<%namespace name="comps" file="beeswax_components.mako" />
-<%namespace name="layout" file="layout.mako" />
-<%namespace name="util" file="util.mako" />
-
-${ commonheader(_('Query Explanation'), app_name, user) | n,unicode }
-${layout.menubar(section='saved queries')}
-
-<div class="container-fluid" style="margin-bottom: 14px">
-  <div class="card card-small">
-    <h1 class="card-heading simple">${_('Query Explanation:')} ${util.render_query_context(query_context)}</h1>
-    <div class="card-body">
-      <p>
-        <ul class="nav nav-tabs">
-          <li class="active"><a href="#explanation" data-toggle="tab">${_('Explanation')}</a></li>
-          <li><a href="#query" data-toggle="tab">${_('Query')}</a></li>
-        </ul>
-
-        <div class="tab-content">
-          <div class="tab-pane active" id="explanation">
-              <pre>${explanation | h}</pre>
-          </div>
-          <div class="tab-pane" id="query">
-              <pre>${query.hql_query | h}</pre>
-          </div>
-        </div>
-
-        <br/>
-        ${util.render_query_context(query_context, _('Back'), 'btn')}
-      </p>
-    </div>
-  </div>
-</div>
-
-${ commonfooter(messages) | n,unicode }

+ 0 - 16
apps/beeswax/src/beeswax/templates/hql_set.mako

@@ -1,16 +0,0 @@
-## 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.
-SET ${key}=${value}

+ 0 - 71
apps/beeswax/src/beeswax/templates/parameterization.mako

@@ -1,71 +0,0 @@
-## 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.views import commonheader, commonfooter
-from django.utils.translation import ugettext as _
-%>
-
-<%namespace name="comps" file="beeswax_components.mako" />
-<%namespace name="layout" file="layout.mako" />
-
-${ commonheader(_('Parameterize Query'), app_name, user) | n,unicode }
-
-${layout.menubar()}
-
-<style class="text/css">
-  .form-horizontal .controls {
-    margin-left: 100px!important;
-  }
-  .control-label {
-    width: 80px!important;
-  }
-</style>
-
-<div class="container-fluid">
-   <div class="card card-small">
-    <%
-        if explain:
-            action = url(app_name + ':explain_parameterized_query', design.id)
-            btn = _("Explain query")
-        else:
-            action = url(app_name + ':execute_parameterized_query', design.id)
-            btn = _("Execute query")
-    %>
-     <h1 class="card-heading simple">${_('Please specify parameters for this query')}</h1>
-     <div class="card-body">
-       <p>
-          <form method="POST" action="${action}" class="form-horizontal">
-            <fieldset>
-              % for field in form:
-              <div class="control-group">
-                <label class="control-label">${comps.bootstrapLabel(field)}</label>
-                <div class="controls">
-                ${comps.field(field)}
-                </div>
-              </div>
-              % endfor
-              <div class="form-actions" style="padding-left: 10px">
-                <a class="btn" href="javascript:history.go(-1);">${_('Cancel')}</a>
-                <button type="submit" class="btn btn-primary">${btn}</button>
-              </div>
-            </fieldset>
-          </form>
-       </p>
-     </div>
-  </div>
-</div>
-
-${ commonfooter(messages) | n,unicode }

+ 0 - 170
apps/beeswax/src/beeswax/templates/watch_wait.mako

@@ -1,170 +0,0 @@
-## 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.views import commonheader, commonfooter
-from django.utils.encoding import force_unicode
-from django.utils.translation import ugettext as _
-%>
-
-<%namespace name="layout" file="layout.mako" />
-<%namespace name="util" file="util.mako" />
-
-${ commonheader(_('Waiting for query...'), app_name, user) | n,unicode }
-${layout.menubar(section='query')}
-
-## Required for unit tests
-<!-- <meta http-equiv="refresh" content="3;${url(app_name + ':watch_query', query.id)}?${fwd_params}" /> -->
-
-<div class="container-fluid">
-    <div class="row-fluid">
-        <div class="span3">
-            % if app_name == 'impala':
-              <li class="nav">
-                <div class="control-group">
-                  <button id="cancel-btn" class="btn btn-small" data-loading-text="${ _('Canceling...') }" rel="tooltip" data-placement="right" data-original-title="${ _('Cancel the query') }">
-                    ${ _('Cancel') }
-                  </button>
-                </div>
-              </li>
-            % endif
-            <div class="sidebar-nav
-                % if app_name == 'impala':
-                  hide
-                % endif
-            ">
-                <ul class="nav nav-list">
-                    <%
-                      n_jobs = hadoop_jobs and len(hadoop_jobs) or 0
-                      mr_jobs = (n_jobs == 1) and _('MR Job') or _('MR Jobs')
-                    %>
-                     % if n_jobs > 0:
-                        <li id="jobsHeader" class="nav-header">${mr_jobs} (${n_jobs})</li>
-                        % for jobid in hadoop_jobs:
-                            <li><a class="jobLink" href="${url("jobbrowser.views.single_job", job=jobid)}">${jobid.replace("job_", "")}</a></li>
-                        % endfor
-                    % else:
-                        <li id="jobsHeader" class="nav-header">${mr_jobs}</li>
-                        <li class="white jobLink">${_('No Hadoop jobs were launched in running this query.')}</li>
-                    % endif
-                </ul>
-            </div>
-        </div>
-        <div class="span9">
-          <div class="card card-small">
-            <h1 class="card-heading simple">${_('Waiting for query...')} ${util.render_query_context(query_context)}</h1>
-            <div class="card-body">
-              <p>
-                <ul class="nav nav-tabs">
-                    <li class="active"><a href="#log" data-toggle="tab">${_('Log')}</a></li>
-                    <li><a href="#query" data-toggle="tab">${_('Query')}</a></li>
-                </ul>
-
-                <div class="tab-content">
-                    <div class="active tab-pane" id="log">
-                        <pre>${ force_unicode(log) }</pre>
-                    </div>
-                    <div class="tab-pane" id="query">
-                        <pre>${ query.get_current_statement() }</pre>
-                    </div>
-                </div>
-              </p>
-            </div>
-          </div>
-        </div>
-    </div>
-</div>
-
-<script>
-  $(document).ready(function(){
-    var fwdUrl = "${url(app_name + ':watch_query', query.id)}?${fwd_params}";
-    var labels = {
-      MRJOB: "${_('MR Job')}",
-      MRJOBS: "${_('MR Jobs')}"
-    }
-
-    resizeLogs();
-    refreshView();
-    var logsAtEnd = true;
-
-    function refreshView() {
-      $.ajaxSetup({ cache: false });
-      $.getJSON("${url(app_name + ':watch_query_refresh_json', query.id)}", function (data) {
-        if (data.isSuccess || data.isFailure) {
-          location.href = fwdUrl;
-        }
-        if (data.jobs && data.jobs.length > 0) {
-          $(".jobLink").remove();
-          $("#jobsHeader").text((data.jobs.length > 1 ? labels.MRJOBS : labels.MRJOB) + " (" + data.jobs.length + ")");
-          for (var i = 0; i < data.jobs.length; i++) {
-            $("#jobsHeader").after($("<li>").addClass("jobLink").html("<a href=\"" + data.jobUrls[data.jobs[i]] + "\">" + data.jobs[i].replace("job_", "") + "</a>"));
-          }
-        }
-        var _logsEl = $("#log pre");
-        % if app_name == 'impala':
-          _logsEl.text($.trim(data.log));
-        % else:
-          var newLines = data.log.split("\n").slice(_logsEl.text().split("\n").length);
-          _logsEl.text(_logsEl.text() + newLines.join("\n"));
-        % endif
-
-        if (logsAtEnd) {
-          _logsEl.scrollTop(_logsEl[0].scrollHeight - _logsEl.height());
-        }
-        window.setTimeout(refreshView, 1000);
-      });
-    }
-
-    $(window).resize(function () {
-      resizeLogs();
-    });
-
-    $("a[href='#log']").on("shown", function () {
-      resizeLogs();
-    });
-
-    $("#log pre").scroll(function () {
-      if ($(this).scrollTop() + $(this).height() + 20 >= $(this)[0].scrollHeight) {
-        logsAtEnd = true;
-      }
-      else {
-        logsAtEnd = false;
-      }
-    });
-
-    function resizeLogs() {
-      $("#log pre").css("overflow", "auto").height($(window).height() - $("#log pre").position().top - 40);
-    }
-
-    % if app_name == 'impala':
-      $("#cancel-btn").click(function() {
-        var _this = this;
-        $(_this).button('loading');
-        $.post("${ url(app_name + ':cancel_operation', query.id) }",
-          function(response) {
-            if (response['status'] != 0) {
-              $(document).trigger("error", "${ _('Problem: ') }" + response['message']);
-            } else {
-              $(document).trigger("info", "${ _('Query canceled!') }")
-            }
-          }
-        );
-        return false;
-      });
-    % endif
-  });
-</script>
-
-${ commonfooter(messages) | n,unicode }

+ 0 - 31
apps/beeswax/src/beeswax/tests.py

@@ -1571,37 +1571,6 @@ class TestWithMockedServer(object):
   def tearDown(self):
     dbms.HiveServer2Dbms = dbms.OriginalBeeswaxApi
 
-  def test_save_design_properties(self):
-    resp = self.client.get('/beeswax/save_design_properties')
-    content = json.loads(resp.content)
-    assert_equal(-1, content['status'])
-
-    response = _make_query(self.client, 'SELECT', submission_type='Save', name='My Name', desc='My Description')
-    design = response.context['design']
-
-    try:
-      resp = self.client.post('/beeswax/save_design_properties', {'name': 'name', 'value': 'New Name', 'pk': design.id})
-      design = SavedQuery.objects.get(id=design.id)
-      content = json.loads(resp.content)
-      assert_equal(0, content['status'])
-      assert_equal('New Name', design.name)
-      assert_equal('My Description', design.desc)
-    finally:
-      design.delete()
-
-    response = _make_query(self.client, 'SELECT', submission_type='Save', name='My Name', desc='My Description')
-    design = response.context['design']
-
-    try:
-      resp = self.client.post('/beeswax/save_design_properties', {'name': 'description', 'value': 'New Description', 'pk': design.id})
-      design = SavedQuery.objects.get(id=design.id)
-      content = json.loads(resp.content)
-      assert_equal(0, content['status'])
-      assert_equal('My Name', design.name)
-      assert_equal('New Description', design.desc)
-    finally:
-      design.delete()
-
   def test_bulk_query_trash(self):
     response = _make_query(self.client, 'SELECT', submission_type='Save', name='My Name 1', desc='My Description')
     query = response.context['design']

+ 13 - 9
apps/beeswax/src/beeswax/urls.py

@@ -21,21 +21,12 @@ urlpatterns = patterns('beeswax.views',
   url(r'^$', 'index', name='index'),
 
   url(r'^execute/(?P<design_id>\d+)?$', 'execute_query', name='execute_query'),
-  url(r'^explain_parameterized/(?P<design_id>\d+)$', 'explain_parameterized_query', name='explain_parameterized_query'),
-  url(r'^execute_parameterized/(?P<design_id>\d+)$', 'execute_parameterized_query', name='execute_parameterized_query'),
   url(r'^watch/(?P<id>\d+)$', 'watch_query', name='watch_query'),
   url(r'^watch/json/(?P<id>\d+)$', 'watch_query_refresh_json', name='watch_query_refresh_json'),
-  url(r'^cancel_operation/(?P<query_id>\d+)?$', 'cancel_operation', name='cancel_operation'),
   url(r'^close_operation/(?P<query_id>\d+)?$', 'close_operation', name='close_operation'),
   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'),
   url(r'^save_results/(?P<id>\d+)$', 'save_results', name='save_results'),
-  url(r'^save_design_properties$', 'save_design_properties', name='save_design_properties'), # Ajax
-
-  url(r'^autocomplete/$', 'autocomplete', name='autocomplete'),
-  url(r'^autocomplete/(?P<database>\w+)/$', 'autocomplete', name='autocomplete'),
-  url(r'^autocomplete/(?P<database>\w+)/(?P<table>\w+)$', 'autocomplete', name='autocomplete'),
-
 
   url(r'^my_queries$', 'my_queries', name='my_queries'),
   url(r'^list_designs$', 'list_designs', name='list_designs'),
@@ -63,3 +54,16 @@ urlpatterns += patterns(
   url(r'^create/import_wizard/(?P<database>\w+)$', 'import_wizard', name='import_wizard'),
   url(r'^create/auto_load/(?P<database>\w+)$', 'load_after_create', name='load_after_create'),
 )
+
+urlpatterns += patterns(
+  'beeswax.api',
+
+  url(r'^api/autocomplete/$', 'autocomplete', name='api_autocomplete'),
+  url(r'^api/autocomplete/(?P<database>\w+)/$', 'autocomplete', name='api_autocomplete'),
+  url(r'^api/autocomplete/(?P<database>\w+)/(?P<table>\w+)$', 'autocomplete', name='api_autocomplete'),
+  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'),
+)

+ 9 - 110
apps/beeswax/src/beeswax/views.py

@@ -48,8 +48,6 @@ from beeswax.models import SavedQuery, make_query_context, QueryHistory
 from beeswax.server import dbms
 from beeswax.server.dbms import expand_exception, get_query_server_config, QueryServerException
 
-from thrift.transport.TTransport import TTransportException
-
 
 LOG = logging.getLogger(__name__)
 
@@ -124,30 +122,6 @@ def save_design(request, form, type_, design, explicit_save):
   return design
 
 
-def save_design_properties(request):
-  response = {'status': -1, 'data': ''}
-
-  try:
-    if request.method != 'POST':
-      raise PopupException(_('POST request required.'))
-
-    design_id = request.POST.get('pk')
-    design = authorized_get_design(request, design_id)
-
-    field = request.POST.get('name')
-    if field == 'name':
-      design.name = request.POST.get('value')
-    elif field == 'description':
-      design.desc = request.POST.get('value')
-    design.save()
-    design.doc.update(name=design.name, description=design.desc)
-    response['status'] = 0
-  except Exception, e:
-    response['data'] = str(e)
-
-  return HttpResponse(json.dumps(response), mimetype="application/json")
-
-
 def delete_design(request):
   if request.method == 'POST':
     ids = request.POST.getlist('designs_selection')
@@ -446,22 +420,16 @@ def execute_query(request, design_id=None):
     'error_message': error_message,
     'form': form,
     'log': log,
-    'autocomplete_base_url': reverse(get_app_name(request) + ':autocomplete', kwargs={}),
+    'autocomplete_base_url': reverse(get_app_name(request) + ':api_autocomplete', kwargs={}),
     'on_success_url': on_success_url,
     'can_edit_name': design.id and not design.is_auto,
   })
 
 
-def execute_parameterized_query(request, design_id):
-  return _run_parameterized_query(request, design_id, False)
-
-
-def explain_parameterized_query(request, design_id):
-  return _run_parameterized_query(request, design_id, True)
-
-
 def watch_query(request, id):
   """
+  DEPRECATED!!!
+
   Wait for the query to finish and (by default) displays the results of query id.
   It understands the optional GET params:
 
@@ -525,6 +493,8 @@ def watch_query(request, id):
                 'query_context': query_context,
               })
 
+
+
 def watch_query_refresh_json(request, id):
   query_history = authorized_get_history(request, id, must_exist=True)
   db = dbms.get(request.user, query_history.get_query_server_config())
@@ -558,24 +528,6 @@ def watch_query_refresh_json(request, id):
   return HttpResponse(json.dumps(result), mimetype="application/json")
 
 
-def cancel_operation(request, query_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)
-      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)
-      response = {'status': 0}
-    except Exception, e:
-      response = {'message': unicode(e)}
-
-  return HttpResponse(json.dumps(response), mimetype="application/json")
-
-
 def close_operation(request, query_id):
   response = {'status': -1, 'message': ''}
 
@@ -706,6 +658,7 @@ def view_results(request, id, first_row=0):
 
   if request.GET.get('format') == 'json':
     context = {
+      'columns': [column.name for column in columns],
       'results': data,
       'has_more': results.has_more,
       'next_row': results.start_row + len(data),
@@ -776,36 +729,6 @@ def save_results(request, id):
   })
 
 
-def confirm_query(request, query, on_success_url=None):
-  """
-  Used by other forms to confirm a query before it's executed.
-  The form is the same as execute_query below.
-
-  query - The HQL about to be executed
-  on_success_url - The page to go to upon successful execution
-  """
-  mform = QueryForm()
-  mform.bind()
-  mform.query.initial = dict(query=query)
-
-  return render('execute.mako', request, {
-    'form': mform,
-    'action': reverse(get_app_name(request) + ':execute_query'),
-    'error_message': None,
-    'design': None,
-    'on_success_url': on_success_url,
-    'design': None,
-    'autocomplete_base_url': reverse(get_app_name(request) + ':autocomplete', kwargs={}),
-  })
-
-
-def explain_directly(request, query, design, query_server):
-  explanation = dbms.get(request.user, query_server).explain(query)
-  context = ("design", design)
-
-  return render('explain.mako', request, dict(query=query, explanation=explanation.textual, query_context=context))
-
-
 def configuration(request):
   app_name = get_app_name(request)
   query_server = get_query_server_config(app_name)
@@ -883,32 +806,6 @@ def query_done_cb(request, server_id):
   return HttpResponse(message_template % message)
 
 
-def autocomplete(request, database=None, table=None):
-  app_name = get_app_name(request)
-  query_server = get_query_server_config(app_name)
-  db = dbms.get(request.user, query_server)
-  response = {}
-
-  try:
-    if database is None:
-      response['databases'] = db.get_databases()
-    elif table is None:
-      response['tables'] = db.get_tables(database=database)
-    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
-  except Exception, e:
-    LOG.warn('Autocomplete data fetching error %s.%s: %s' % (database, table, e))
-    response['code'] = 500
-    response['error'] = e.message
-
-  return HttpResponse(json.dumps(response), mimetype="application/json")
-
-
 """
 Utils
 """
@@ -982,6 +879,7 @@ def safe_get_design(request, design_type, design_id=None):
 
 def get_parameterization(request, query_str, form, design, is_explain):
   """
+  DEPRECATED!!!
   Figures out whether a design is parameterizable, and, if so,
   returns a form to fill out.  Returns None if there's no parameterization
   to do.
@@ -1012,6 +910,7 @@ def make_parameterization_form(query_str):
 
 def _run_parameterized_query(request, design_id, explain):
   """
+  DEPRECATED!!!
   Given a design and arguments to parameterize that design, runs the query.
   - explain is a boolean to determine whether to run as an explain or as an
   execute.
@@ -1062,7 +961,7 @@ def _run_parameterized_query(request, design_id, explain):
         'error_message': error_message,
         'form': query_form,
         'log': log,
-        'autocomplete_base_url': reverse(get_app_name(request) + ':autocomplete', kwargs={}),
+        'autocomplete_base_url': reverse(get_app_name(request) + ':api_autocomplete', kwargs={}),
       })
   else:
     return render("parameterization.mako", request, dict(form=parameterization_form, design=design, explain=explain))

+ 491 - 0
apps/beeswax/static/js/beeswax.vm.js

@@ -0,0 +1,491 @@
+// 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.
+
+
+function BeeswaxViewModel(server, query_id) {
+  var self = this;
+
+  self.server = ko.observable(server);
+  self.databases = ko.observableArray();
+  self.selectedDatabase = ko.observable(0);
+  self.query = ko.mapping.fromJS({
+    'id': query_id,
+    'query': '',
+    'name': null,
+    'description': null,
+    'settings': [],
+    'fileResources': [],
+    'functions': [],
+    'parameters': [],
+    'isParameterized': true,
+    'email': false,
+    'errors': [],
+    'explain': false
+  });
+  self.logs = ko.observableArray();
+  self.explanation = ko.observable();
+  self.watchURL = ko.observable();
+  self.resultsURL = ko.observable(null);
+  self.columns = ko.observableArray();
+  self.rows = ko.observableArray();
+  self.resultsEmpty = ko.observable(true);
+
+  self.hasMoreResults = ko.computed(function() {
+    return self.resultsURL() != null;
+  });
+
+  self.database = ko.computed({
+    'read': function() {
+      if (self.databases()) {
+        return self.databases()[self.selectedDatabase()];
+      } else{
+        return "";
+      }
+    },
+    'write': function(value) {
+      self.selectedDatabase(self.databases.indexOf(value));
+    },
+    'deferEvaluation': true
+  });
+
+  self.updateDatabases = function(databases) {
+    self.databases(databases);
+
+    var key = 'hueBeeswaxLastDatabase-' + self.server();
+    var last = $.totalStorage(key) || ((databases.length > 0) ? databases[0] : null);
+    if (last) {
+      self.database(last);
+    }
+  };
+
+  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.database(design.database);
+    self.query.isParameterized(design.is_parameterized);
+    self.query.email(design.email_notify);
+    
+    self.query.settings.removeAll();
+    self.query.fileResources.removeAll();
+    self.query.functions.removeAll();
+
+    $.each(design.settings, function(index, setting) {
+      self.addSetting(setting.key, setting.value);
+    });
+    $.each(design.file_resources, function(index, file_resource) {
+      self.addFileResources(file_resource.type, file_resource.path);
+    });
+    $.each(design.functions, function(index, _function) {
+      self.addFunction(_function.name, _function.class_name);
+    });
+  };
+
+  self.updateParameters = function(parameters) {
+    self.query.parameters.removeAll();
+    $.each(parameters, function(index, parameter) {
+      self.addParameter(parameter.parameter, parameter.name, '');
+    });
+  };
+
+  self.addParameter = function(parameter, name, value) {
+    var obj = {
+      'parameter': ko.observable(parameter),
+      'name': ko.observable(name),
+      'value': ko.observable(value)
+    };
+    obj.parameter.subscribe(function() {
+      self.query.parameters.valueHasMutated();
+    });
+    obj.name.subscribe(function() {
+      self.query.parameters.valueHasMutated();
+    });
+    obj.value.subscribe(function() {
+      self.query.parameters.valueHasMutated();
+    });
+    self.query.parameters.push(obj);
+  };
+
+  self.addSetting = function(key, value) {
+    var obj = {
+      'key': ko.observable(key),
+      'value': ko.observable(value)
+    };
+    obj.key.subscribe(function() {
+      self.query.settings.valueHasMutated();
+    });
+    obj.value.subscribe(function() {
+      self.query.settings.valueHasMutated();
+    });
+    self.query.settings.push(obj);
+  };
+
+  self.removeSetting = function(index) {
+    self.query.settings.splice(index, 1);
+  };
+
+  self.addFileResources = function(type, path) {
+    var obj = {
+      'type': ko.observable(type),
+      'path': ko.observable(path)
+    };
+    obj.type.subscribe(function() {
+      self.query.fileResources.valueHasMutated();
+    });
+    obj.path.subscribe(function() {
+      self.query.fileResources.valueHasMutated();
+    });
+    self.query.fileResources.push(obj);
+  };
+
+  self.removeFileResources = function(index) {
+    self.query.fileResources.splice(index, 1);
+  };
+
+  self.addFunction = function(name, class_name) {
+    var obj = {
+      'name': ko.observable(name),
+      'class_name': ko.observable(class_name)
+    };
+    obj.name.subscribe(function() {
+      self.query.functions.valueHasMutated();
+    });
+    obj.class_name.subscribe(function() {
+      self.query.functions.valueHasMutated();
+    });
+    self.query.functions.push(obj);
+  };
+
+  self.removeFunction = function(index) {
+    self.query.functions.splice(index, 1);
+  };
+
+  function getMultiFormData(prefix, arr, members) {
+    var data = {};
+    var index = 0;
+    data[prefix + '-next_form_id'] = arr.length;
+    ko.utils.arrayForEach(arr, function(obj) {
+      $.each(members, function(index, member) {
+        data[prefix + '-' + index + '-' + member] = obj[member]();
+      });
+      data[prefix + '-' + index + '-_exists'] = true;
+      data[prefix + '-' + index + '-_deleted'] = false;
+    });
+    return data;
+  }
+
+  self.getSettingsFormData = function() {
+    return getMultiFormData('settings', self.query.settings(), ['key', 'value']);
+  };
+
+  self.getFileResourcesFormData = function() {
+    return getMultiFormData('file_resources', self.query.fileResources(), ['type', 'path']);
+  };
+
+  self.getFunctionsFormData = function() {
+    return getMultiFormData('functions', self.query.functions(), ['name', 'class_name']);
+  };
+
+  self.getParametersFormData = function() {
+    var data = {};
+    $.each(self.query.parameters(), function(index, parameter) {
+      data[parameter.parameter()] = parameter.value();
+    });
+    return data;
+  };
+
+  self.getOtherData = function() {
+    var data = {
+      'query-email_notify': self.query.email(),
+      'query-is_parameterized': self.query.isParameterized()
+    };
+    return data;
+  };
+
+  var error_fn = function(jqXHR, status, errorThrown) {
+    try {
+      $(document).trigger('server.error', $.parseJSON(jqXHR.responseText));
+    } catch(e) {
+      $(document).trigger('server.unmanageable_error', jqXHR.responseText);
+    }
+  };
+
+  self.fetchDatabases = function() {
+    var request = {
+      url: '/' + self.server() + '/api/autocomplete',
+      dataType: 'json',
+      type: 'GET',
+      success: function(data) {
+        self.updateDatabases(data.databases);
+      },
+      error: error_fn
+    };
+    $.ajax(request);
+  };
+
+  self.fetchQuery = function() {
+    $(document).trigger('fetch.query');
+
+    var request = {
+      url: '/' + self.server() + '/api/query/' + self.query.id() + '/get',
+      dataType: 'json',
+      type: 'GET',
+      success: function(data) {
+        self.updateQuery(data.design);
+        $(document).trigger('fetched.query', data);
+      },
+      error: error_fn
+    };
+    $.ajax(request);
+  };
+
+  self.fetchParameters = function() {
+    $(document).trigger('fetch.parameters');
+
+    var data = {
+      'query-query': self.query.query(),
+      'query-database': self.database()
+    };
+    $.extend(data, self.getSettingsFormData());
+    $.extend(data, self.getFileResourcesFormData());
+    $.extend(data, self.getFunctionsFormData());
+    $.extend(data, self.getParametersFormData());
+    $.extend(data, self.getOtherData());
+    var request = {
+      url: '/' + self.server() + '/api/query/parameters',
+      dataType: 'json',
+      type: 'POST',
+      success: function(data) {
+        self.updateParameters(data.parameters);
+        $(document).trigger('fetched.parameters');
+      },
+      error: error_fn,
+      data: data
+    };
+    $.ajax(request);
+  };
+
+  self.explainQuery = function() {
+    $(document).trigger('explain.query', data);
+    self.query.explain(true);
+
+    var data = {
+      'query-query': self.query.query(),
+      'query-database': self.database()
+    };
+    $.extend(data, self.getSettingsFormData());
+    $.extend(data, self.getFileResourcesFormData());
+    $.extend(data, self.getFunctionsFormData());
+    $.extend(data, self.getParametersFormData());
+    $.extend(data, self.getOtherData());
+    var request = {
+      url: '/' + self.server() + '/api/query/execute/?explain=true',
+      dataType: 'json',
+      type: 'POST',
+      success: function(data) {
+        self.query.errors.removeAll();
+        if (data.status == 0) {
+          self.logs.removeAll();
+          self.rows.removeAll();
+          self.columns.removeAll();
+          self.explanation(data.explanation);
+        } else {
+          self.query.errors.push(data.message);
+        }
+        $(document).trigger('explained.query', data);
+      },
+      error: error_fn,
+      data: data
+    };
+    $.ajax(request);
+  };
+
+  self.executeQuery = function() {
+    $(document).trigger('execute.query', data);
+    self.query.explain(false);
+
+    var data = {
+      'query-query': self.query.query(),
+      'query-database': self.database()
+    };
+    $.extend(data, self.getSettingsFormData());
+    $.extend(data, self.getFileResourcesFormData());
+    $.extend(data, self.getFunctionsFormData());
+    $.extend(data, self.getParametersFormData());
+    $.extend(data, self.getOtherData());
+    var request = {
+      url: '/' + self.server() + '/api/query/execute/',
+      dataType: 'json',
+      type: 'POST',
+      success: function(data) {
+        self.query.errors.removeAll();
+        if (data.status == 0) {
+          self.query.id(data.id);
+          self.logs.removeAll();
+          self.rows.removeAll();
+          self.columns.removeAll();
+          self.resultsURL('/' + self.server() + '/results/' + self.query.id() + '/0?format=json');
+          self.watchURL(data.watch_url);
+          self.watchQueryLoop();
+        } else {
+          self.query.errors.push(data.message);
+        }
+        $(document).trigger('executed.query', data);
+      },
+      error: error_fn,
+      data: data
+    };
+    $.ajax(request);
+  };
+
+  self.watchQuery = function() {
+    $(document).trigger('watch.query');
+
+    var data = {
+      'query-query': self.query.query(),
+      'query-database': self.database()
+    };
+    $.extend(data, self.getSettingsFormData());
+    $.extend(data, self.getFileResourcesFormData());
+    $.extend(data, self.getFunctionsFormData());
+    $.extend(data, self.getParametersFormData());
+    $.extend(data, self.getOtherData());
+    var request = {
+      url: self.watchURL(),
+      dataType: 'json',
+      type: 'POST',
+      success: function(data) {
+        $(document).trigger('watched.query', data);
+      },
+      error: error_fn,
+      data: data
+    };
+    $.ajax(request);
+  };
+
+  self.watchQueryLoop = function() {
+    var TIMEOUT = 1000;
+    var timer = null;
+    var executed_once = false;
+    var fn = function() {
+      $(document).one('watched.query', function(e, data) {
+        if (executed_once && (data.isSuccess || data.isFailure)) {
+          clearTimeout(timer);
+          $(document).trigger('stop_watch.query');
+          self.fetchResults();
+        } else {
+          executed_once = true;
+          if (data.log) {
+            self.logs.push(data.log);
+          }
+          timer = setTimeout(fn, TIMEOUT);
+        }
+      });
+      self.watchQuery();
+    };
+    $(document).trigger('start_watch.query');
+    timer = setTimeout(fn, TIMEOUT);
+  };
+
+  self.fetchResults = function() {
+    $(document).trigger('fetch.results');
+    var request = {
+      url: self.resultsURL(),
+      dataType: 'json',
+      type: 'GET',
+      success: function(data) {
+        self.columns(data.columns);
+        self.rows.push.apply(self.rows, data.results);
+        self.resultsEmpty(self.rows().length == 0);
+        if (data.has_more) {
+          self.resultsURL(data.next_json_set);
+        } else {
+          self.resultsURL(null);
+        }
+        $(document).trigger('fetched.results', data);
+      },
+      error: error_fn
+    };
+    $.ajax(request);
+  };
+
+  self.saveQuery = function() {
+    var self = this;
+    if (self.query.query() && self.query.name()) {
+      var data = {
+        'query-query': self.query.query(),
+        'query-database': self.database()
+      };
+      $.extend(data, self.getSettingsFormData());
+      $.extend(data, self.getFileResourcesFormData());
+      $.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();
+      }
+      var url = '/' + self.server() + '/api/query/';
+      if (self.query.id() && self.query.id() != -1) {
+        url += self.query.id();
+      }
+      var request = {
+        url: url,
+        dataType: 'json',
+        type: 'POST',
+        success: function(data) {
+          self.query.id(data.design_id);
+          $(document).trigger('saved.query', data);
+        },
+        error: function() {
+          $(document).trigger('error.query');
+        },
+        data: data
+      };
+      $.ajax(request);
+    }
+  };
+
+  self.cancelQuery = function() {
+    $(document).trigger('cancel.query');
+    var url = '/' + self.server() + '/api/query/' + self.query.id() + '/cancel';
+    $.post(url,
+      function(response) {
+        if (response['status'] != 0) {
+          $(document).trigger('error_cancel.query', response['message']);
+        } else {
+          $(document).trigger('cancelled.query');
+        }
+      }
+    );
+  };
+
+  // Events
+  // Remove watched query event that watchQueryLoop may be bound to.
+  $(document).on('server.unmanageable_error', function() {
+    $(document).off('watched.query');
+  });
+}
+
+
+// For routie
+function showSection(section) {
+  $('.section').hide();
+  $('#' + section).show();
+  $(window).scrollTop(0);
+}

+ 0 - 3
apps/impala/src/impala/tests.py

@@ -71,9 +71,6 @@ class TestMockedImpala:
       response = self.client.get("/impala/list_designs")
       assert_equal(len(response.context['page'].object_list), 1)
 
-      response = self.client.get("/impala/execute_parameterized/%s" % impala_query.id)
-      assert_true('specify parameters' in response.content)
-
       # Test my query page
       QueryHistory.objects.create(owner=user, design=impala_query, query='', last_state=QueryHistory.STATE.available.index)
 

+ 6 - 6
apps/rdbms/src/rdbms/urls.py

@@ -38,12 +38,6 @@ urlpatterns += patterns('rdbms.api',
 )
 
 urlpatterns += patterns('beeswax.views',
-  url(r'^autocomplete/$', 'autocomplete', name='autocomplete'),
-  url(r'^autocomplete/(?P<database>\w+)/$', 'autocomplete', name='autocomplete'),
-  url(r'^autocomplete/(?P<database>\w+)/(?P<table>\w+)$', 'autocomplete', name='autocomplete'),
-
-  url(r'^save_design_properties$', 'save_design_properties', name='save_design_properties'), # Ajax
-
   url(r'^my_queries$', 'my_queries', name='my_queries'),
   url(r'^list_designs$', 'list_designs', name='list_designs'),
   url(r'^list_trashed_designs$', 'list_trashed_designs', name='list_trashed_designs'),
@@ -52,3 +46,9 @@ urlpatterns += patterns('beeswax.views',
   url(r'^clone_design/(?P<design_id>\d+)$', 'clone_design', name='clone_design'),
   url(r'^query_history$', 'list_query_history', name='list_query_history')
 )
+
+urlpatterns += patterns('beeswax.api',
+  url(r'^autocomplete/$', 'autocomplete', name='api_autocomplete_databases'),
+  url(r'^autocomplete/(?P<database>\w+)/$', 'autocomplete', name='api_autocomplete_tables'),
+  url(r'^autocomplete/(?P<database>\w+)/(?P<table>\w+)$', 'autocomplete', name='api_autocomplete_columns')
+)

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

@@ -71,7 +71,7 @@ def execute_query(request, design_id=None):
   return render('execute.mako', request, {
     'action': action,
     'design': design,
-    'autocomplete_base_url': reverse('rdbms:autocomplete', kwargs={}),
+    'autocomplete_base_url': reverse('rdbms:api_autocomplete_databases', kwargs={}),
     'can_edit_name': design.id and not design.is_auto,
   })
 

+ 12 - 0
desktop/core/static/js/jquery.tableextender.js

@@ -58,6 +58,17 @@
     drawHeader(this);
   };
 
+  Plugin.prototype.resetSource = function() {
+    var _this = this;
+
+    var source = [];
+    $(this.element).find("th").each(function () {
+      source.push($(this).text());
+    });
+
+    $("#jHueTableExtenderNavigator").find("input").data('typeahead').source = source;
+  };
+
   Plugin.prototype.init = function () {
 
     $.expr[":"].econtains = function (obj, index, meta, stack) {
@@ -207,6 +218,7 @@
         $.data(this, 'plugin_' + pluginName, new Plugin(this, options));
       }
       else {
+        $.data(this, 'plugin_' + pluginName).resetSource();
         $.data(this, 'plugin_' + pluginName).setOptions(options);
       }
     });

部分文件因为文件数量过多而无法显示