Browse Source

[catalog] Database management in table browser

Added new layer to table browser: Database management.
Users will first land on the database management page.
last known selected database will be saved in cookies.
Abraham Elmahrek 12 năm trước cách đây
mục cha
commit
70e5aa7

+ 59 - 0
apps/beeswax/src/beeswax/create_database.py

@@ -0,0 +1,59 @@
+#!/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.
+
+"""
+Views & controls for creating tables
+"""
+
+import logging
+
+from django.core.urlresolvers import reverse
+
+from desktop.lib import django_mako
+from desktop.lib.django_util import render
+
+from beeswax.design import hql_query
+from beeswax.forms import CreateDatabaseForm
+from beeswax.server import dbms
+from beeswax.views import execute_directly
+
+
+LOG = logging.getLogger(__name__)
+
+
+def create_database(request):
+  db = dbms.get(request.user)
+
+  if request.method == "POST":
+    data = request.POST.copy()
+    data.setdefault("use_default_location", False)
+    form = CreateDatabaseForm(data)
+
+    if form.is_valid():
+      proposed_query = django_mako.render_to_string("create_database_statement.mako", {
+        'database': form.cleaned_data,
+      })
+      # Mako outputs bytestring in utf8
+      proposed_query = proposed_query.decode('utf-8')
+      query = hql_query(proposed_query)
+      return execute_directly(request, query, on_success_url=reverse('catalog:show_databases'))
+  else:
+    form = CreateDatabaseForm()
+
+  return render("create_database.mako", request, {
+    'database_form': form,
+  })

+ 30 - 0
apps/beeswax/src/beeswax/forms.py

@@ -333,3 +333,33 @@ class ColumnTypeForm(DependencyAwareForm):
 ColumnTypeFormSet = simple_formset_factory(ColumnTypeForm, initial=[{}], add_label=_t("add a column"))
 # Default to no partitions
 PartitionTypeFormSet = simple_formset_factory(PartitionTypeForm, add_label=_t("add a partition"))
+
+
+def _clean_databasename(name):
+  try:
+    if name in db.get_databases():
+      raise forms.ValidationError(_('Database "%(name)s" already exists') % {'name': name})
+  except Exception:
+    return name
+
+
+class CreateDatabaseForm(DependencyAwareForm):
+  """
+  Form used in the create database page
+  """
+  dependencies = []
+
+  # Basic Data
+  name = common.HiveIdentifierField(label=_t("Database Name"), required=True)
+  comment = forms.CharField(label=_t("Description"), required=False)
+
+  # External if not true
+  use_default_location = forms.BooleanField(required=False, initial=True, label=_t("Use default location"))
+  external_location = forms.CharField(required=False, help_text=_t("Path to HDFS directory or file of database data."))
+
+  dependencies += [
+    ("use_default_location", False, "external_location")
+  ]
+
+  def clean_name(self):
+    return _clean_databasename(self.cleaned_data['name'])

+ 16 - 0
apps/beeswax/src/beeswax/server/dbms.py

@@ -185,6 +185,22 @@ class Dbms:
     return self.execute_query(query, design)
 
 
+  def drop_database(self, database):
+    return self.execute_statement("DROP DATABASE `%s`" % database)
+
+
+  def drop_databases(self, databases, design):
+    hql = []
+
+    for database in databases:
+      hql.append("DROP DATABASE `%s`" % database)
+    query = hql_query(';'.join(hql), database)
+    design.data = query.dumps()
+    design.save()
+
+    return self.execute_query(query, design)
+
+
   def use(self, database):
     """Beeswax interface does not support use directly."""
     if SERVER_INTERFACE.get() == HIVE_SERVER2:

+ 293 - 0
apps/beeswax/src/beeswax/templates/create_database.mako

@@ -0,0 +1,293 @@
+## 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(_("Create database"), app_name, user, '100px') | n,unicode }
+${layout.menubar(section='databases')}
+
+<script src="/static/ext/js/routie-0.3.0.min.js" type="text/javascript" charset="utf-8"></script>
+
+<div class="container-fluid">
+  <h1>${_('Create a new database')}</h1>
+  <div class="row-fluid">
+    <div class="span3">
+        <div class="well sidebar-nav">
+            <ul class="nav nav-list">
+                <li class="nav-header">${_('Actions')}</li>
+                <li><a href="${ url(app_name + ':create_database')}">${_('Create a new database')}</a></li>
+            </ul>
+        </div>
+    </div>
+
+    <div class="span9">
+      <ul id="step-nav" class="nav nav-pills">
+          <li class="active"><a href="#step/1" class="step">${_('Step 1: Name')}</a></li>
+          <li><a href="#step/2" class="step">${_('Step 2: Location')}</a></li>
+      </ul>
+
+      <form action="${ url(app_name + ':create_database')}" method="POST" id="mainForm" class="form-horizontal">
+        <div class="steps">
+          <div id="step1" class="stepDetails">
+              <fieldset>
+                  <div class="alert alert-info"><h3>${_('Create a database')}</h3>${_("Let's start with a name and description for where we'll store your data.")}</div>
+                  <div class="control-group">
+                      ${comps.bootstrapLabel(database_form["name"])}
+                      <div class="controls">
+                          ${comps.field(database_form["name"], attrs=dict(
+                              placeholder=_('database_name'),
+                            )
+                          )}
+                          <span  class="help-inline error-inline hide">${_('This field is required. Spaces are not allowed.')}</span>
+                          <p class="help-block">
+                              ${_('Name of the new database. Database names must be globally unique. Database names tend to correspond as well to the directory where the data will be stored.')}
+                          </p>
+                      </div>
+                  </div>
+                  <div class="control-group">
+                      ${comps.bootstrapLabel(database_form["comment"])}
+                      <div class="controls">
+                          ${comps.field(database_form["comment"], attrs=dict(
+                            placeholder=_('Optional'),
+                            )
+                          )}
+                          <p class="help-block">
+                              ${_("Use a database comment to describe your database. For example, you might note the data's provenance and any caveats users need to know.")}
+                          </p>
+                      </div>
+                  </div>
+              </fieldset>
+          </div>
+
+          <div id="step2" class="stepDetails hide">
+            <fieldset>
+                <div class="alert alert-info"><h3>${_("Choose Where Your Database's Data is Stored")}</h3>
+                </div>
+                <div class="control-group">
+                    <label class="control-label">${_('Location')}</label>
+                    <div class="controls">
+                        <label class="checkbox">
+                            ${comps.field(database_form["use_default_location"],
+                            render_default=True
+                            )}
+                            ${_('Use default location')}
+                        </label>
+                        <span class="help-block">
+                            ${_('Store your database in the default location (controlled by Hive, and typically')} <em>/user/hive/warehouse/database_name</em>).
+                        </span>
+                    </div>
+                </div>
+
+                <div id="location" class="control-group hide">
+                    ${comps.bootstrapLabel(database_form["external_location"])}
+                    <div class="controls">
+                        ${comps.field(database_form["external_location"],
+                        placeholder="/user/user_name/data_dir",
+                        klass="pathChooser input-xxlarge",
+                        file_chooser=True,
+                        show_errors=False
+                        )}
+                        <span  class="help-inline error-inline hide">${_('This field is required.')}</span>
+                        <span class="help-block">
+                        ${_("Enter the path (on HDFS) to your database's data location")}
+                        </span>
+                    </div>
+                </div>
+            </fieldset>
+        </div>
+        </div>
+        <div class="form-actions">
+            <button type="button" id="backBtn" class="btn hide">${_('Back')}</button>
+            <button type="button" id="nextBtn" class="btn btn-primary">${_('Next')}</button>
+            <input id="submit" type="submit" name="create" class="btn btn-primary hide" value="${_('Create database')}" />
+        </div>
+      </form>
+    </div>
+  </div>
+</div>
+
+
+<div id="chooseFile" class="modal hide fade">
+    <div class="modal-header">
+        <a href="#" class="close" data-dismiss="modal">&times;</a>
+        <h3>${_('Choose a file')}</h3>
+    </div>
+    <div class="modal-body">
+        <div id="filechooser">
+        </div>
+    </div>
+    <div class="modal-footer">
+    </div>
+</div>
+
+<style>
+  #filechooser {
+    min-height: 100px;
+    overflow-y: scroll;
+    margin-top: 10px;
+    height: 250px;
+  }
+
+  .inputs-list {
+    list-style: none outside none;
+    margin-left: 0;
+  }
+
+  .remove {
+    float: right;
+  }
+
+  .error-inline {
+    color: #B94A48;
+    font-weight: bold;
+  }
+
+  .steps {
+    min-height: 350px;
+    margin-top: 10px;
+  }
+
+  div .alert {
+    margin-bottom: 30px;
+  }
+</style>
+
+</div>
+
+<script type="text/javascript" charset="utf-8">
+$(document).ready(function () {
+  // Routing
+  var step = 1;
+  routie({
+    'step/1': function(node_type) {
+      $("#step-nav").children().removeClass("active");
+      $("#step-nav").children(":nth-child(1)").addClass("active");
+      $('.stepDetails').hide();
+      $('#step1').show();
+      $("#backBtn").hide();
+      $("#nextBtn").show();
+      $("#submit").hide();
+    },
+    'step/2': function(node_type) {
+      $("#step-nav").children().removeClass("active");
+      $("#step-nav").children(":nth-child(2)").addClass("active");
+      $('.stepDetails').hide();
+      $('#step2').show();
+      $("#backBtn").show();
+      $("#nextBtn").hide();
+      $("#submit").show();
+    }
+  });
+  routie('step/' + step);
+
+  // events
+  $(".fileChooserBtn").click(function (e) {
+    e.preventDefault();
+    var _destination = $(this).attr("data-filechooser-destination");
+    $("#filechooser").jHueFileChooser({
+      initialPath: $("input[name='" + _destination + "']").val(),
+      onFolderChoose: function (filePath) {
+        $("input[name='" + _destination + "']").val(filePath);
+        $("#chooseFile").modal("hide");
+      },
+      createFolder: false,
+      selectFolder: true,
+      uploadFile: false
+    });
+    $("#chooseFile").modal("show");
+  });
+
+  $("#id_use_default_location").change(function () {
+    if (!$(this).is(":checked")) {
+      $("#location").slideDown();
+    }
+    else {
+      $("#location").slideUp();
+    }
+  });
+
+  $("#submit").click(function() {
+    return validate();
+  });
+
+  $("#nextBtn").click(function () {
+    if (validate()) {
+      routie('step/' + ++step);
+    }
+  });
+
+  $("#backBtn").click(function () {
+    // To get to the current step
+    // users will have to get through all previous steps.
+    routie('step/' + --step);
+  });
+
+  $(".step").click(function() {
+    return validate();
+  });
+
+  // Validation
+  function validate() {
+    switch(step) {
+      case 1:
+        var databaseNameFld = $("input[name='name']");
+        if (!isValid($.trim(databaseNameFld.val()))) {
+          showFieldError(databaseNameFld);
+          return false;
+        } else {
+          hideFieldError(databaseNameFld);
+        }
+      break;
+
+      case 2:
+        var externalLocationFld = $("input[name='external_location']");
+        if (!($("#id_use_default_location").is(":checked"))) {
+          if (!isValid($.trim(externalLocationFld.val()))) {
+            showFieldError(externalLocationFld);
+            return false;
+          }
+          else {
+            hideFieldError(externalLocationFld);
+          }
+        }
+      break;
+    }
+
+    return true;
+  }
+
+  function isValid(str) {
+    return (str != "" && str.indexOf(" ") == -1);
+  }
+
+  function showFieldError(field) {
+    field.nextAll(".error-inline").not(".error-inline-bis").removeClass("hide");
+  }
+
+  function hideFieldError(field) {
+    if (!(field.nextAll(".error-inline").hasClass("hide"))) {
+      field.nextAll(".error-inline").addClass("hide");
+    }
+  }
+});
+</script>
+
+${ commonfooter(messages) | n,unicode }

+ 34 - 0
apps/beeswax/src/beeswax/templates/create_database_statement.mako

@@ -0,0 +1,34 @@
+## 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.
+
+## TODO(philip): Check escaping more carefully?
+## TODO(philip): The whitespace management here is mediocre.
+##
+## |n is used throughout here, since this is not going to HTML.
+##
+## Reference: http://wiki.apache.org/hadoop/Hive/LanguageManual/DDL#Create_Table
+CREATE DATABASE ${database["name"]} \
+% if database["comment"]:
+COMMENT "${database["comment"] | n}"
+% endif
+% if not database.get("use_default_location", True):
+LOCATION "${database["external_location"] | n}"
+% endif
+% if database.get("properties", False):
+WITH DBPROPERTIES (\
+${ ','.join('%s=%s' % (prop["name"], prop["value"]) for prop in database["properties"]) }\
+)
+% endif

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

@@ -43,6 +43,7 @@ from desktop.lib.test_utils import grant_access
 from beeswaxd import ttypes
 
 import beeswax.create_table
+import beeswax.create_database
 import beeswax.forms
 import beeswax.hive_site
 import beeswax.models
@@ -1089,6 +1090,19 @@ for x in sys.stdin:
     assert_true("sp ace</td>" in resp.content)
 
 
+  def test_create_database(self):
+    resp = self.client.post("/beeswax/create/database", {
+      'name': 'my_db',
+      'comment': 'foo',
+      'create': 'Create database',
+      'use_default_location': True,
+    }, follow=True)
+    assert_equal_mod_whitespace("CREATE DATABASE my_db COMMENT \"foo\"", resp.context['query'].query)
+
+    resp = wait_for_query_to_finish(self.client, resp, max=180.0)
+    assert_true('my_db' in resp.context['databases'], resp)
+
+
   def test_select_query_server(self):
     c = make_logged_in_client()
     _make_query(c, 'SELECT bogus FROM test') # Improvement: mock another server

+ 6 - 0
apps/beeswax/src/beeswax/urls.py

@@ -41,6 +41,12 @@ urlpatterns = patterns('beeswax.views',
   url(r'^query_cb/done/(?P<server_id>\S+)$', 'query_done_cb', name='query_done_cb'),
 )
 
+urlpatterns += patterns(
+  'beeswax.create_database',
+
+  url(r'^create/database$', 'create_database', name='create_database'),
+)
+
 urlpatterns += patterns(
   'beeswax.create_table',
 

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

@@ -726,7 +726,7 @@ def _save_results_ctas(request, query_history, target_table, result_meta):
     hql = 'CREATE TABLE `%s` AS SELECT * FROM %s' % (target_table, result_meta.in_tablename)
     query = hql_query(hql)
     # Display the CTAS running. Could take a long time.
-    return execute_directly(request, query, query_server, on_success_url=reverse('catalog:show_tables'))
+    return execute_directly(request, query, query_server, on_success_url=reverse('catalog:index'))
 
   # Case 2: The results are in some temporary location
   # 1. Create table
@@ -774,7 +774,7 @@ def _save_results_ctas(request, query_history, target_table, result_meta):
     raise ex
 
   # Show tables upon success
-  return format_preserving_redirect(request, reverse('catalog:show_tables'))
+  return format_preserving_redirect(request, reverse('catalog:index'))
 
 
 def confirm_query(request, query, on_success_url=None):

+ 166 - 0
apps/catalog/src/catalog/templates/databases.mako

@@ -0,0 +1,166 @@
+## 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 _
+import json
+%>
+<%namespace name="actionbar" file="actionbar.mako" />
+<%namespace name="components" file="components.mako" />
+
+${ commonheader(_('Databases'), 'catalog', user) | n,unicode }
+
+<div class="container-fluid" id="databases">
+    <h1>${_('Databases')}</h1>
+    ${ components.breadcrumbs(breadcrumbs) }
+    <div class="row-fluid">
+        <div class="span3">
+            <div class="well sidebar-nav">
+                <ul class="nav nav-list">
+                    <li><a href="${ url('beeswax:create_database') }">${_('Create a new database')}</a></li>
+                </ul>
+            </div>
+        </div>
+        <div class="span9">
+          <%actionbar:render>
+            <%def name="actions()">
+                <button id="dropBtn" class="btn toolbarBtn" title="${_('Drop the selected databases')}" disabled="disabled"><i class="icon-trash"></i>  ${_('Drop')}</button>
+            </%def>
+          </%actionbar:render>
+            <table class="table table-condensed table-striped datatables">
+                <thead>
+                  <tr>
+                    <th width="1%"><div class="hueCheckbox selectAll" data-selectables="databaseCheck"></div></th>
+                    <th>${_('Database Name')}</th>
+                  </tr>
+                </thead>
+                <tbody>
+                % for database in databases:
+                  <tr>
+                    <td data-row-selector-exclude="true" width="1%">
+                      <div class="hueCheckbox databaseCheck"
+                           data-view-url="${ url('catalog:show_tables', database=database) }"
+                           data-drop-name="${ database }"
+                           data-row-selector-exclude="true"></div>
+                    </td>
+                    <td>
+                      <a href="${ url('catalog:show_tables', database=database) }" data-row-selector="true">${ database }</a>
+                    </td>
+                  </tr>
+                % endfor
+                </tbody>
+            </table>
+        </div>
+    </div>
+</div>
+
+<div id="dropDatabase" class="modal hide fade">
+  <form id="dropDatabaseForm" action="${ url('catalog:drop_database') }" method="POST">
+    <div class="modal-header">
+      <a href="#" class="close" data-dismiss="modal">&times;</a>
+      <h3 id="dropDatabaseMessage">${_('Confirm action')}</h3>
+    </div>
+    <div class="modal-footer">
+      <input type="button" class="btn" data-dismiss="modal" value="${_('Cancel')}" />
+      <input type="submit" class="btn btn-danger" value="${_('Yes')}"/>
+    </div>
+    <div class="hide">
+      <select name="database_selection" data-bind="options: availableDatabases, selectedOptions: chosenDatabases" size="5" multiple="true"></select>
+    </div>
+  </form>
+</div>
+
+<link rel="stylesheet" href="/catalog/static/css/catalog.css" type="text/css">
+
+<script src="/static/ext/js/jquery/plugins/jquery.cookie.js"></script>
+<script src="/static/ext/js/knockout-2.1.0.js" type="text/javascript" charset="utf-8"></script>
+
+<script type="text/javascript" charset="utf-8">
+  $(document).ready(function () {
+    var viewModel = {
+        availableDatabases : ko.observableArray(${ json.dumps(databases) | n }),
+        chosenDatabases : ko.observableArray([])
+    };
+
+    ko.applyBindings(viewModel);
+
+    var databases = $(".datatables").dataTable({
+      "sDom":"<'row'r>t<'row'<'span8'i><''p>>",
+      "bPaginate":false,
+      "bLengthChange":false,
+      "bInfo":false,
+      "bFilter":true,
+      "aoColumns":[
+        {"bSortable":false, "sWidth":"1%" },
+        null
+      ],
+      "oLanguage":{
+        "sEmptyTable":"${_('No data available')}",
+        "sZeroRecords":"${_('No matching records')}",
+      }
+    });
+
+    $("#filterInput").keyup(function () {
+      databases.fnFilter($(this).val());
+    });
+
+    $("a[data-row-selector='true']").jHueRowSelector();
+
+    $(".selectAll").click(function () {
+      if ($(this).attr("checked")) {
+        $(this).removeAttr("checked").removeClass("icon-ok");
+        $("." + $(this).data("selectables")).removeClass("icon-ok").removeAttr("checked");
+      }
+      else {
+        $(this).attr("checked", "checked").addClass("icon-ok");
+        $("." + $(this).data("selectables")).addClass("icon-ok").attr("checked", "checked");
+      }
+      toggleActions();
+    });
+
+    $(".databaseCheck").click(function () {
+      if ($(this).attr("checked")) {
+        $(this).removeClass("icon-ok").removeAttr("checked");
+      }
+      else {
+        $(this).addClass("icon-ok").attr("checked", "checked");
+      }
+      $(".selectAll").removeAttr("checked").removeClass("icon-ok");
+      toggleActions();
+    });
+
+    function toggleActions() {
+      $(".toolbarBtn").attr("disabled", "disabled");
+      var selector = $(".hueCheckbox[checked='checked']");
+      if (selector.length >= 1) {
+        $("#dropBtn").removeAttr("disabled");
+      }
+    }
+
+    $("#dropBtn").click(function () {
+      $.getJSON("${ url('catalog:drop_database') }", function(data) {
+        $("#dropDatabaseMessage").text(data.title);
+      });
+      viewModel.chosenDatabases.removeAll();
+      $(".hueCheckbox[checked='checked']").each(function( index ) {
+        viewModel.chosenDatabases.push($(this).data("drop-name"));
+      });
+      $("#dropDatabase").modal("show");
+    });
+  });
+</script>
+
+${ commonfooter(messages) | n,unicode }

+ 1 - 1
apps/catalog/src/catalog/templates/tables.mako

@@ -23,7 +23,7 @@ from django.utils.translation import ugettext as _
 ${ commonheader(_('Tables'), 'catalog', user) | n,unicode }
 
 <div class="container-fluid" id="tables">
-    <h1>${_('Tables')}</h1>
+    <h1>${_('Database %s') % database}</h1>
     ${ components.breadcrumbs(breadcrumbs) }
     <div class="row-fluid">
         <div class="span3">

+ 20 - 0
apps/catalog/src/catalog/tests.py

@@ -64,6 +64,10 @@ class TestCatalogWithHadoop(BeeswaxSampleProvider):
     self.db = dbms.get(user, get_query_server_config())
 
   def test_basic_flow(self):
+    # Default database should exist
+    response = self.client.get("/catalog/databases")
+    assert_true("default" in response.context["databases"])
+
     # Table should have been created
     response = self.client.get("/catalog/tables/")
     assert_true("test" in response.context["tables"])
@@ -145,6 +149,22 @@ class TestCatalogWithHadoop(BeeswaxSampleProvider):
     assert_equal(resp.status_code, 302)
 
 
+  def test_drop_multi_databases(self):
+    hql = """
+      CREATE DATABASE test_drop_1;
+      CREATE DATABASE test_drop_2;
+      CREATE DATABASE test_drop_3;
+    """
+    resp = _make_query(self.client, hql)
+    resp = wait_for_query_to_finish(self.client, resp, max=30.0)
+
+    # Drop them
+    resp = self.client.get('/catalog/databases/drop', follow=True)
+    assert_true('want to delete' in resp.content, resp.content)
+    resp = self.client.post('/catalog/databases/drop', {u'database_selection': [u'test_drop_1', u'test_drop_2', u'test_drop_3']})
+    assert_equal(resp.status_code, 302)
+
+
   def test_load_data(self):
     """
     Test load data queries.

+ 3 - 0
apps/catalog/src/catalog/urls.py

@@ -20,6 +20,9 @@ from django.conf.urls.defaults import patterns, url
 urlpatterns = patterns('catalog.views',
   url(r'^$', 'index', name='index'),
 
+  url(r'^databases/?$', 'show_databases', name='show_databases'),
+  url(r'^databases/drop/?$', 'drop_database', name='drop_database'),
+
   url(r'^tables/(?P<database>\w+)?$', 'show_tables', name='show_tables'),
   url(r'^tables/drop/(?P<database>\w+)$', 'drop_table', name='drop_table'),
   url(r'^table/(?P<database>\w+)/(?P<table>\w+)$', 'describe_table', name='describe_table'),

+ 39 - 1
apps/catalog/src/catalog/views.py

@@ -32,6 +32,7 @@ from desktop.lib.exceptions_renderable import PopupException
 from beeswax.models import SavedQuery, MetaInstall
 from beeswax.server import dbms
 
+from catalog.conf import CATALOG_DATABASE_COOKIE_EXPIRE
 from catalog.forms import LoadDataForm, DbForm
 
 LOG = logging.getLogger(__name__)
@@ -42,6 +43,40 @@ def index(request):
   return redirect(reverse('catalog:show_tables'))
 
 
+"""
+Database Views
+"""
+
+def show_databases(request):
+  db = dbms.get(request.user)
+  databases = db.get_databases()
+  return render("databases.mako", request, {
+    'breadcrumbs': [],
+    'databases': databases,
+  })
+
+
+def drop_database(request):
+  db = dbms.get(request.user)
+
+  if request.method == 'POST':
+    databases = request.POST.getlist('database_selection')
+
+    try:
+      # Can't be simpler without an important refactoring
+      design = SavedQuery.create_empty(app_name='beeswax', owner=request.user)
+      query_history = db.drop_databases(databases, design)
+      url = reverse('beeswax:watch_query', args=[query_history.id]) + '?on_success_url=' + reverse('catalog:show_databases')
+      return redirect(url)
+    except Exception, ex:
+      error_message, log = dbms.expand_exception(ex, db)
+      error = _("Failed to remove %(databases)s.  Error: %(error)s") % {'databases': ','.join(databases), 'error': error_message}
+      raise PopupException(error, title=_("Beeswax Error"), detail=log)
+  else:
+    title = _("Do you really want to delete the database(s)?")
+    return render('confirm.html', request, dict(url=request.path, title=title))
+
+
 """
 Table Views
 """
@@ -49,6 +84,7 @@ Table Views
 def show_tables(request, database=None):
   if database is None:
     database = request.COOKIES.get('hueBeeswaxLastDatabase', 'default') # Assume always 'default'
+
   db = dbms.get(request.user)
 
   databases = db.get_databases()
@@ -63,7 +99,7 @@ def show_tables(request, database=None):
   tables = db.get_tables(database=database)
   examples_installed = MetaInstall.get().installed_example
 
-  return render("tables.mako", request, {
+  resp = render("tables.mako", request, {
     'breadcrumbs': [
       {
         'name': database,
@@ -76,6 +112,8 @@ def show_tables(request, database=None):
     'database': database,
     'tables_json': json.dumps(tables),
   })
+  resp.set_cookie("hueBeeswaxLastDatabase", database, expires=90)
+  return resp
 
 
 def describe_table(request, database, table):

+ 1 - 1
desktop/core/src/desktop/lib/django_forms.py

@@ -489,7 +489,7 @@ class DependencyAwareForm(forms.Form):
   Inherit from this class and add
   (condition name, condition value, child name) tuples
   to self.dependencies to describe dependencies between
-  certain form feilds.
+  certain form fields.
 
   The semantic meaning is that the field named "child name"
   is required if and only if the field "condition name"