소스 검색

[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 년 전
부모
커밋
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"