Răsfoiți Sursa

[catalog] Create Table Browser App

Making the catalog app entirely dependent on Beeswax for now.
Moved all the table/partition viewing logic to the new app.
Moved around tests so that they are associated with the proper app.
Abraham Elmahrek 12 ani în urmă
părinte
comite
2d0cb7b
36 a modificat fișierele cu 1460 adăugiri și 342 ștergeri
  1. 2 0
      apps/Makefile
  2. 5 2
      apps/beeswax/src/beeswax/create_table.py
  3. 10 6
      apps/beeswax/src/beeswax/design.py
  4. 0 35
      apps/beeswax/src/beeswax/forms.py
  5. 1 1
      apps/beeswax/src/beeswax/templates/index.mako
  6. 0 3
      apps/beeswax/src/beeswax/templates/layout.mako
  7. 1 1
      apps/beeswax/src/beeswax/templates/util.mako
  8. 4 4
      apps/beeswax/src/beeswax/test_base.py
  9. 7 105
      apps/beeswax/src/beeswax/tests.py
  10. 0 7
      apps/beeswax/src/beeswax/urls.py
  11. 3 140
      apps/beeswax/src/beeswax/views.py
  12. 0 0
      apps/catalog/.gitignore
  13. 24 0
      apps/catalog/Makefile
  14. 2 0
      apps/catalog/babel.cfg
  15. 1 0
      apps/catalog/hueversion.py
  16. 29 0
      apps/catalog/setup.py
  17. 16 0
      apps/catalog/src/catalog/__init__.py
  18. 64 0
      apps/catalog/src/catalog/forms.py
  19. 16 0
      apps/catalog/src/catalog/models.py
  20. 24 0
      apps/catalog/src/catalog/settings.py
  21. 205 0
      apps/catalog/src/catalog/templates/components.mako
  22. 34 0
      apps/catalog/src/catalog/templates/confirm.html
  23. 1 4
      apps/catalog/src/catalog/templates/describe_partitions.mako
  24. 7 9
      apps/catalog/src/catalog/templates/describe_table.mako
  25. 38 0
      apps/catalog/src/catalog/templates/layout.mako
  26. 2 5
      apps/catalog/src/catalog/templates/popups/load_data.mako
  27. 14 19
      apps/catalog/src/catalog/templates/tables.mako
  28. 74 0
      apps/catalog/src/catalog/templates/util.mako
  29. 165 0
      apps/catalog/src/catalog/tests.py
  30. 29 0
      apps/catalog/src/catalog/urls.py
  31. 173 0
      apps/catalog/src/catalog/views.py
  32. BIN
      apps/catalog/static/art/table-browser-24-1.png
  33. BIN
      apps/catalog/static/help/images/23888161.png
  34. 507 0
      apps/catalog/static/help/index.html
  35. 1 1
      apps/jobsub/setup.py
  36. 1 0
      desktop/core/src/desktop/lib/django_mako.py

+ 2 - 0
apps/Makefile

@@ -32,6 +32,7 @@ default: env-install
 
 APPS := about \
   beeswax \
+  catalog \
   filebrowser \
   help \
   jobbrowser \
@@ -103,6 +104,7 @@ ext-clean: $(EXT_CLEAN_TARGETS)
 ################################################
 I18N_APPS := about \
   beeswax \
+  catalog \
   filebrowser \
   jobbrowser \
   jobsub \

+ 5 - 2
apps/beeswax/src/beeswax/create_table.py

@@ -268,7 +268,7 @@ def _submit_create_and_load(request, create_hql, table_name, path, do_load, data
     on_success_params['path'] = path
     on_success_url = reverse(app_name + ':load_after_create', kwargs={'database': database})
   else:
-    on_success_url = reverse(app_name + ':describe_table', kwargs={'database': database, 'table': table_name})
+    on_success_url = reverse('catalog:describe_table', kwargs={'database': database, 'table': table_name})
 
   query = hql_query(create_hql, database=database)
   return execute_directly(request, query,
@@ -360,6 +360,9 @@ def _readfields(lines, delimiters):
     n_lines = len(fields_list)
     len_list = [ len(fields) for fields in fields_list ]
 
+    if not len_list:
+      raise PopupException(_("Could not find any columns to import"))
+
     # All lines should break into multiple fields
     if min(len_list) == 1:
       return 0
@@ -460,6 +463,6 @@ def load_after_create(request, database):
   hql = "LOAD DATA INPATH '%s' INTO TABLE `%s.%s`" % (path, database, tablename)
   query = hql_query(hql)
   app_name = get_app_name(request)
-  on_success_url = reverse(app_name + ':describe_table', kwargs={'database': database, 'table': tablename})
+  on_success_url = reverse('catalog:describe_table', kwargs={'database': database, 'table': tablename})
 
   return execute_directly(request, query, on_success_url=on_success_url)

+ 10 - 6
apps/beeswax/src/beeswax/design.py

@@ -18,10 +18,13 @@
 """
 The HQLdesign class can (de)serialize a design to/from a QueryDict.
 """
+try:
+  import json
+except ImportError:
+  import simplejson as json
 
 import logging
 import re
-import simplejson
 
 import django.http
 from django import forms
@@ -36,7 +39,7 @@ SERIALIZATION_VERSION = '0.4.1'
 
 
 def hql_query(hql, database='default'):
-  data_dict = simplejson.loads('{"query": {"email_notify": false, "query": null, "type": 0, "is_parameterized": true, "database": "default"}, '
+  data_dict = json.loads('{"query": {"email_notify": false, "query": null, "type": 0, "is_parameterized": true, "database": "default"}, '
                                '"functions": [], "VERSION": "0.4.1", "file_resources": [], "settings": []}')
   if not (isinstance(hql, str) or isinstance(hql, unicode)):
     raise Exception('Requires a SQL text query of type <str>, <unicode> and not %s' % type(hql))
@@ -77,7 +80,7 @@ class HQLdesign(object):
     """Returns the serialized form of the design in a string"""
     dic = self._data_dict.copy()
     dic['VERSION'] = SERIALIZATION_VERSION
-    return simplejson.dumps(dic)
+    return json.dumps(dic)
 
   @property
   def hql_query(self):
@@ -134,7 +137,8 @@ class HQLdesign(object):
   @staticmethod
   def loads(data):
     """Returns an HQLdesign from the serialized form"""
-    dic = simplejson.loads(data)
+    dic = json.loads(data)
+    dic = dict(map(lambda k: (str(k), dic.get(k)), dic.keys()))
     if dic['VERSION'] != SERIALIZATION_VERSION:
       LOG.error('Design version mismatch. Found %s; expect %s' % (dic['VERSION'], SERIALIZATION_VERSION))
 
@@ -197,7 +201,7 @@ def denormalize_form_dict(data_dict, form, attr_list):
   res = django.http.QueryDict('', mutable=True)
   for attr in attr_list:
     try:
-      res[form.add_prefix(attr)] = data_dict[attr]
+      res[str(form.add_prefix(attr))] = data_dict[attr]
     except KeyError:
       pass
   return res
@@ -215,7 +219,7 @@ def denormalize_formset_dict(data_dict_list, formset, attr_list):
     res.update(denormalize_form_dict(data_dict, form, attr_list))
     res[prefix + '-_exists'] = 'True'
 
-  res[formset.management_form.add_prefix('next_form_id')] = str(len(data_dict_list))
+  res[str(formset.management_form.add_prefix('next_form_id'))] = str(len(data_dict_list))
   return res
 
   def __str__(self):

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

@@ -38,20 +38,6 @@ class QueryForm(MultiForm):
       saveform=SaveForm)
 
 
-class DbForm(forms.Form):
-  """For 'show tables'"""
-  database = forms.ChoiceField(required=False,
-                           label='',
-                           choices=(('default', 'default'),),
-                           initial=0,
-                           widget=forms.widgets.Select(attrs={'class': 'span6'}))
-
-  def __init__(self, *args, **kwargs):
-    databases = kwargs.pop('databases')
-    super(DbForm, self).__init__(*args, **kwargs)
-    self.fields['database'].choices = ((db, db) for db in databases)
-
-
 class SaveForm(forms.Form):
   """Used for saving query design."""
   name = forms.CharField(required=False,
@@ -345,24 +331,3 @@ 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"))
-
-
-class LoadDataForm(forms.Form):
-  """Form used for loading data into an existing table."""
-  path = PathField(label=_t("Path"))
-  overwrite = forms.BooleanField(required=False, initial=False, label=_t("Overwrite?"))
-
-  def __init__(self, table_obj, *args, **kwargs):
-    """
-    @param table_obj is a hive_metastore.thrift Table object,
-    used to add fields corresponding to partition keys.
-    """
-    super(LoadDataForm, self).__init__(*args, **kwargs)
-    self.partition_columns = dict()
-    for i, column in enumerate(table_obj.partition_keys):
-      # We give these numeric names because column names
-      # may be unpleasantly arbitrary.
-      name = "partition_%d" % i
-      char_field = forms.CharField(required=True, label=_t("%(column_name)s (partition key with type %(column_type)s)") % {'column_name': column.name, 'column_type': column.type})
-      self.fields[name] = char_field
-      self.partition_columns[name] = column.name

+ 1 - 1
apps/beeswax/src/beeswax/templates/index.mako

@@ -34,7 +34,7 @@ ${layout.menubar(section='tables')}
                     <li><a href="#installSamples" data-toggle="modal">${_('Install Samples')}</a></li>
                     % endif
                     <li class="nav-header">${_('Tables')}</li>
-                    <li><a href="${ url(app_name + ':show_tables') }">${_('Show Tables')}</a></li>
+                    <li><a href="${ url('catalog:show_tables') }">${_('Show Tables')}</a></li>
                     <li><a href="${ url(app_name + ':create_table') }">${_('Create Table')}</a></li>
                     <li class="nav-header">${_('Queries')}</li>
                     <li><a href="${ url(app_name + ':list_designs') }">${_('Saved Queries')}</a></li>

+ 0 - 3
apps/beeswax/src/beeswax/templates/layout.mako

@@ -35,9 +35,6 @@ def is_selected(section, matcher):
 			<li class="${is_selected(section, 'my queries')}"><a href="${ url(app_name + ':my_queries') }">${_('My Queries')}</a></li>
 			<li class="${is_selected(section, 'saved queries')}"><a href="${ url(app_name + ':list_designs') }">${_('Saved Queries')}</a></li>
 			<li class="${is_selected(section, 'history')}"><a href="${ url(app_name + ':list_query_history') }">${_('History')}</a></li>
-			% if app_name != 'impala':
-			<li class="${is_selected(section, 'tables')}"><a href="${ url(app_name + ':show_tables') }">${_('Tables')}</a></li>
-			% endif
 			<li class="${is_selected(section, 'configuration')}"><a href="${ url(app_name + ':configuration') }">${_('Settings')}</a></li>
 		</ul>
 	</div>

+ 1 - 1
apps/beeswax/src/beeswax/templates/util.mako

@@ -59,7 +59,7 @@ from django.utils.translation import ugettext as _
   % if query_context:
     % if query_context[0] == 'table':
       <% tablename, database = query_context[1].split(':') %>
-      <a href="${ url(app_name + ':describe_table', database, tablename) }">${tablename}</a>
+      <a href="${ url('catalog:describe_table', database, tablename) }">${tablename}</a>
     % elif query_context[0] == 'design':
       <% design = query_context[1] %>
       % if design.is_auto:

+ 4 - 4
apps/beeswax/src/beeswax/test_base.py

@@ -242,14 +242,14 @@ def make_query(client, query, submission_type="Execute",
   parameters["settings-next_form_id"] = str(len(settings))
   for i, settings_pair in enumerate(settings or []):
     key, value = settings_pair
-    parameters["settings-%d-key" % i] = key
-    parameters["settings-%d-value" % i] = value
+    parameters["settings-%d-key" % i] = str(key)
+    parameters["settings-%d-value" % i] = str(value)
     parameters["settings-%d-_exists" % i] = 'True'
   parameters["file_resources-next_form_id"] = str(len(resources or []))
   for i, resources_pair in enumerate(resources or []):
     type, path = resources_pair
-    parameters["file_resources-%d-type" % i] = type
-    parameters["file_resources-%d-path" % i] = path
+    parameters["file_resources-%d-type" % i] = str(type)
+    parameters["file_resources-%d-path" % i] = str(path)
     parameters["file_resources-%d-_exists" % i] = 'True'
 
   kwargs.setdefault('follow', True)

+ 7 - 105
apps/beeswax/src/beeswax/tests.py

@@ -132,23 +132,6 @@ class TestBeeswaxWithHadoop(BeeswaxSampleProvider):
     assert_true("tasktracker.http.threads" in response_verbose.content)
     assert_true("A base for other temporary directories" in response_verbose.content)
 
-  def test_describe_partitions(self):
-    response = self.client.get("/beeswax/table/default/test_partitions/partitions")
-    assert_true("baz_one" in response.content)
-    assert_true("boom_two" in response.content)
-    response = self.client.get("/beeswax/table/default/test/partitions")
-    assert_true("is not partitioned." in response.content)
-
-  def test_browse_partitions_with_limit(self):
-    # Limit to 90
-    finish = beeswax.conf.BROWSE_PARTITIONED_TABLE_LIMIT.set_for_testing("90")
-    try:
-      response = self.client.get("/beeswax/table/default/test_partitions")
-      assert_true("0x%x" % 89 in response.content, response.content)
-      assert_false("0x%x" % 90 in response.content, response.content)
-    finally:
-      finish()
-
   def test_query_with_resource(self):
     script = self.cluster.fs.open("/square.py", "w")
     script.write(
@@ -191,38 +174,6 @@ for x in sys.stdin:
     # Minimal server operation
     assert_equal("echo", self.db.echo("echo"))
 
-    # Table should have been created
-    response = self.client.get("/beeswax/tables/")
-    assert_true("test" in response.context["tables"])
-
-    # Switch databases
-    response = self.client.get("/beeswax/tables/default")
-    assert_true("test" in response.context["tables"])
-
-    response = self.client.get("/beeswax/tables/not_there")
-    assert_false("test" in response.context["tables"])
-
-    # And have detail
-    response = self.client.get("/beeswax/table/default/test")
-    assert_true("foo" in response.content)
-
-    # Remember the number of history items. Use a generic fragment 'test' to pass verification.
-    history_cnt = verify_history(self.client, fragment='test')
-
-    # Show table data.
-    response = self.client.get("/beeswax/table/default/test/read", follow=True)
-    response = wait_for_query_to_finish(self.client, response, max=30.0)
-    # Note that it may not return all rows at once. But we expect at least 10.
-    assert_true(len(response.context['results']) > 10)
-    # Column names
-    assert_true("<td>foo</td>" in response.content)
-    assert_true("<td>bar</td>" in response.content)
-    # This should NOT go into the query history.
-    assert_equal(verify_history(self.client, fragment='test'), history_cnt,
-                 'Implicit queries should not be saved in the history')
-    assert_equal(str(response.context['query_context'][0]), 'table')
-    assert_equal(str(response.context['query_context'][1]), 'test:default')
-
     # Query the data
     # We use a semicolon here for kicks; the code strips it out.
     QUERY = """
@@ -319,7 +270,6 @@ for x in sys.stdin:
     hql = """
       SELECT foo FROM test;
     """
-
     query = hql_query(hql)
     handle = self.db.execute_and_wait(query)
 
@@ -475,22 +425,6 @@ for x in sys.stdin:
     except:
       LOG.exception("Saw exception in child thread.")
 
-  def test_drop_multi_tables(self):
-    hql = """
-      CREATE TABLE test_drop_1 (a int);
-      CREATE TABLE test_drop_2 (a int);
-      CREATE TABLE test_drop_3 (a int);
-    """
-    resp = _make_query(self.client, hql)
-    resp = wait_for_query_to_finish(self.client, resp, max=30.0)
-
-    # Drop them
-    resp = self.client.get('/beeswax/tables/drop/default')
-    assert_true('want to delete' in resp.content, resp.content)
-    resp = self.client.post('/beeswax/tables/drop/default', {u'table_selection': [u'test_drop_1', u'test_drop_2', u'test_drop_3']})
-    assert_equal(resp.status_code, 302)
-
-
   def test_multiple_statements_no_result_set(self):
     hql = """
       CREATE TABLE test_multiple_statements_1 (a int);
@@ -779,33 +713,6 @@ for x in sys.stdin:
     client_not_me.logout()
 
 
-  def test_load_data(self):
-    """
-    Test load data queries.
-    These require Hadoop, because they ask the metastore
-    about whether a table is partitioned.
-    """
-    # Check that view works
-    resp = self.client.get("/beeswax/table/default/test/load")
-    assert_true('Path' in resp.content)
-
-    # Try the submission
-    self.client.post("/beeswax/table/default/test/load", dict(path="/tmp/foo", overwrite=True))
-    query = QueryHistory.objects.latest('id')
-
-    assert_equal_mod_whitespace("LOAD DATA INPATH '/tmp/foo' OVERWRITE INTO TABLE `default.test`", query.query)
-
-    resp = self.client.post("/beeswax/table/default/test/load", dict(path="/tmp/foo", overwrite=False))
-    query = QueryHistory.objects.latest('id')
-    assert_equal_mod_whitespace("LOAD DATA INPATH '/tmp/foo' INTO TABLE `default.test`", query.query)
-
-    # Try it with partitions
-    resp = self.client.post("/beeswax/table/default/test_partitions/load", dict(path="/tmp/foo", partition_0="alpha", partition_1="beta"))
-    query = QueryHistory.objects.latest('id')
-    assert_equal_mod_whitespace("LOAD DATA INPATH '/tmp/foo' INTO TABLE `default.test_partitions` PARTITION (baz='alpha', boom='beta')",
-        query.query)
-
-
   def test_save_results_to_dir(self):
     """Check that saving to directory works"""
 
@@ -904,7 +811,7 @@ for x in sys.stdin:
     self.client.post('/beeswax/install_examples')
 
     # New tables exists
-    resp = self.client.get('/beeswax/tables/')
+    resp = self.client.get('/catalog/tables/')
     assert_true('sample_08' in resp.content)
     assert_true('sample_07' in resp.content)
 
@@ -972,7 +879,7 @@ for x in sys.stdin:
           STORED AS TextFile LOCATION "/tmp/foo"
     """, resp.context['query'].query)
 
-    assert_true('on_success_url=%2Fbeeswax%2Ftable%2Fdefault%2Fmy_table' in resp.context['fwd_params'], resp.context['fwd_params'])
+    assert_true('on_success_url=%2Fcatalog%2Ftable%2Fdefault%2Fmy_table' in resp.context['fwd_params'], resp.context['fwd_params'])
 
   def test_create_table_timestamp(self):
     # Check form
@@ -986,14 +893,14 @@ for x in sys.stdin:
     self._make_custom_data_file(filename, [0, 0, 0])
     self._make_table('timestamp_invalid_data', 'CREATE TABLE timestamp_invalid_data (timestamp1 TIMESTAMP)', filename)
 
-    response = self.client.get("/beeswax/table/default/timestamp_invalid_data")
+    response = self.client.get("/catalog/table/default/timestamp_invalid_data")
     assert_true('Error!' in response.content, response.content)
 
     # Good format
     self._make_custom_data_file(filename, ['2012-01-01 10:11:30', '2012-01-01 10:11:31'])
     self._make_table('timestamp_valid_data', 'CREATE TABLE timestamp_valid_data (timestamp1 TIMESTAMP)', filename)
 
-    response = self.client.get("/beeswax/table/default/timestamp_valid_data")
+    response = self.client.get("/catalog/table/default/timestamp_valid_data")
     assert_true('2012-01-01 10:11:30' in response.content, response.content)
 
   def test_partitioned_create_table(self):
@@ -1174,20 +1081,13 @@ for x in sys.stdin:
     resp = wait_for_query_to_finish(self.client, resp, max=180.0)
 
     # Check data is in the table (by describing it)
-    resp = self.client.get('/beeswax/table/default/test_create_import')
+    resp = self.client.get('/catalog/table/default/test_create_import')
     cols = resp.context['table'].cols
     assert_equal(len(cols), 3)
     assert_equal([ col.name for col in cols ], [ 'col_a', 'col_b', 'col_c' ])
     assert_true("nada</td>" in resp.content)
     assert_true("sp ace</td>" in resp.content)
 
-  def test_describe_view(self):
-    resp = self.client.get('/beeswax/table/default/myview')
-    assert_equal(None, resp.context['sample'])
-    assert_true(resp.context['table'].is_view)
-    assert_true("View Metadata" in resp.content)
-    assert_true("Drop View" in resp.content)
-
 
   def test_select_query_server(self):
     c = make_logged_in_client()
@@ -1452,10 +1352,12 @@ def test_hive_site_sasl():
     assert_equal(beeswax.hive_site.get_conf()['hive.metastore.warehouse.dir'], u'/abc')
     assert_equal(kerberos_principal, 'test/test.com@TEST.COM')
   finally:
+    beeswax.hive_site.reset()
     if saved is not None:
       beeswax.conf.BEESWAX_HIVE_CONF_DIR = saved
     shutil.rmtree(tmpdir)
 
+
 def test_collapse_whitespace():
   assert_equal("", collapse_whitespace("\t\n\n  \n\t \n"))
   assert_equal("x", collapse_whitespace("\t\nx\n  \n\t \n"))

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

@@ -20,13 +20,6 @@ from django.conf.urls.defaults import patterns, url
 urlpatterns = patterns('beeswax.views',
   url(r'^$', 'index', name='index'),
 
-  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'),
-  url(r'^table/(?P<database>\w+)/(?P<table>\w+)/partitions$', 'describe_partitions', name='describe_partitions'),
-  url(r'^table/(?P<database>\w+)/(?P<table>\w+)/load$', 'load_table', name='load_table'),
-  url(r'^table/(?P<database>\w+)/(?P<table>\w+)/read$', 'read_table', name='read_table'),
-
   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'),

+ 3 - 140
apps/beeswax/src/beeswax/views.py

@@ -44,7 +44,7 @@ import beeswax.design
 import beeswax.management.commands.beeswax_install_examples
 
 from beeswax import common, data_export, models, conf
-from beeswax.forms import LoadDataForm, QueryForm, DbForm
+from beeswax.forms import QueryForm
 from beeswax.design import HQLdesign, hql_query
 from beeswax.models import SavedQuery, make_query_context
 from beeswax.server import dbms
@@ -283,141 +283,6 @@ def list_query_history(request):
   })
 
 
-"""
-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()
-
-  if request.method == 'POST':
-    db_form = DbForm(request.POST, databases=databases)
-    if db_form.is_valid():
-      database = db_form.cleaned_data['database']
-  else:
-    db_form = DbForm(initial={'database': database}, databases=databases)
-
-  tables = db.get_tables(database=database)
-  examples_installed = beeswax.models.MetaInstall.get().installed_example
-
-  return render("show_tables.mako", request, {
-      'tables': tables,
-      'examples_installed': examples_installed,
-      'db_form': db_form,
-      'database': database,
-      'tables_json': json.dumps(tables),
-  })
-
-
-def describe_table(request, database, table):
-  db = dbms.get(request.user)
-  error_message = ''
-  table_data = ''
-
-  table = db.get_table(database, table)
-
-  try:
-    table_data = db.get_sample(database, table)
-  except Exception, ex:
-    error_message, logs = expand_exception(ex, db)
-
-  return render("describe_table.mako", request, {
-      'table': table,
-      'sample': table_data and table_data.rows(),
-      'error_message': error_message,
-      'database': database,
-  })
-
-
-def drop_table(request, database):
-  db = dbms.get(request.user)
-
-  if request.method == 'POST':
-    tables = request.POST.getlist('table_selection')
-    tables_objects = [db.get_table(database, table) for table in tables]
-    app_name = get_app_name(request)
-    try:
-      # Can't be simpler without an important refactoring
-      design = SavedQuery.create_empty(app_name=app_name, owner=request.user)
-      query_history = db.drop_tables(database, tables_objects, design)
-      url = reverse(app_name + ':watch_query', args=[query_history.id]) + '?on_success_url=' + reverse(app_name + ':show_tables')
-      return redirect(url)
-    except Exception, ex:
-      error_message, log = expand_exception(ex, db)
-      error = _("Failed to remove %(tables)s.  Error: %(error)s") % {'tables': ','.join(tables), 'error': error_message}
-      raise PopupException(error, title=_("Beeswax Error"), detail=log)
-  else:
-    title = _("Do you really want to delete the table(s)?")
-    return render('confirm.html', request, dict(url=request.path, title=title))
-
-
-def read_table(request, database, table):
-  db = dbms.get(request.user)
-
-  table = db.get_table(database, table)
-
-  try:
-    history = db.select_star_from(database, table)
-    get = request.GET.copy()
-    get['context'] = 'table:%s:%s' % (table.name, database)
-    request.GET = get
-    return watch_query(request, history.id)
-  except Exception, e:
-    raise PopupException(_('Can read table'), detail=e)
-
-
-def load_table(request, database, table):
-  app_name = get_app_name(request)
-  db = dbms.get(request.user)
-  table = db.get_table(database, table)
-  response = {'status': -1, 'data': 'None'}
-
-  if request.method == "POST":
-    load_form = beeswax.forms.LoadDataForm(table, request.POST)
-
-    if load_form.is_valid():
-      on_success_url = reverse(get_app_name(request) + ':describe_table', kwargs={'database': database, 'table': table.name})
-      try:
-        design = SavedQuery.create_empty(app_name=app_name, owner=request.user)
-        query_history = db.load_data(database, table, load_form, design)
-        url = reverse(app_name + ':watch_query', args=[query_history.id]) + '?on_success_url=' + on_success_url
-        response['status'] = 0
-        response['data'] = url
-      except Exception, e:
-        response['status'] = 1
-        response['data'] = _("Can't load the data: ") + str(e)
-  else:
-    load_form = LoadDataForm(table)
-
-  if response['status'] == -1:
-    popup = render('load_data_popup.mako', request, {
-                     'table': table,
-                     'load_form': load_form,
-                     'database': database,
-                     'app_name': app_name
-                 }, force_template=True).content
-    response['data'] = popup
-
-  return HttpResponse(json.dumps(response), mimetype="application/json")
-
-
-def describe_partitions(request, database, table):
-  db = dbms.get(request.user)
-
-  table_obj = db.get_table(database, table)
-  if not table_obj.partition_keys:
-    raise PopupException(_("Table '%(table)s' is not partitioned.") % {'table': table})
-
-  partitions = db.get_partitions(database, table_obj, max_parts=None)
-
-  return render("describe_partitions.mako", request,
-                dict(table=table_obj, partitions=partitions, request=request))
-
-
 def download(request, id, format):
   assert format in common.DL_FORMATS
 
@@ -428,7 +293,6 @@ def download(request, id, format):
   return data_export.download(query_history.get_handle(), format, db)
 
 
-
 """
 Queries Views
 """
@@ -818,7 +682,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(get_app_name(request) + ':show_tables'))
+    return execute_directly(request, query, query_server, on_success_url=reverse('catalog:show_tables'))
 
   # Case 2: The results are in some temporary location
   # 1. Create table
@@ -866,7 +730,7 @@ def _save_results_ctas(request, query_history, target_table, result_meta):
     raise ex
 
   # Show tables upon success
-  return format_preserving_redirect(request, reverse(get_app_name(request) + ':show_tables'))
+  return format_preserving_redirect(request, reverse('catalog:show_tables'))
 
 
 def confirm_query(request, query, on_success_url=None):
@@ -1348,7 +1212,6 @@ def _list_query_history(user, querydict, page_size, prefix=""):
   if not querydict.get(prefix + 'auto_query', False):
     db_queryset = db_queryset.filter(design__isnull=False)
 
-  username = user.username
   user_filter = querydict.get(prefix + 'user', user.username)
   if user_filter != ':all':
     db_queryset = db_queryset.filter(owner__username=user_filter)

+ 0 - 0
apps/catalog/.gitignore


+ 24 - 0
apps/catalog/Makefile

@@ -0,0 +1,24 @@
+#
+# 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.
+#
+
+ifeq ($(ROOT),)
+  $(error "Error: Expect the environment variable $$ROOT to point to the Desktop installation")
+endif
+
+APP_NAME = catalog
+include $(ROOT)/Makefile.sdk

+ 2 - 0
apps/catalog/babel.cfg

@@ -0,0 +1,2 @@
+[python: src/catalog/**.py]
+[mako: src/catalog/templates/**.mako]

+ 1 - 0
apps/catalog/hueversion.py

@@ -0,0 +1 @@
+../../VERSION

+ 29 - 0
apps/catalog/setup.py

@@ -0,0 +1,29 @@
+# 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 setuptools import setup, find_packages
+from hueversion import VERSION
+
+setup(
+      name = "catalog",
+      version = VERSION,
+      author = "Hue",
+      url = 'http://github.com/cloudera/hue',
+      description = "Metastore browser",
+      packages = find_packages('src'),
+      package_dir = {'': 'src'},
+      install_requires = ['setuptools', 'desktop'],
+      entry_points = { 'desktop.sdk.application': 'catalog=catalog' },
+)

+ 16 - 0
apps/catalog/src/catalog/__init__.py

@@ -0,0 +1,16 @@
+#!/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.

+ 64 - 0
apps/catalog/src/catalog/forms.py

@@ -0,0 +1,64 @@
+#!/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.
+
+from django import forms
+from django.utils.translation import ugettext as _, ugettext_lazy as _t
+
+import hive_metastore
+
+from desktop.lib.django_forms import simple_formset_factory, DependencyAwareForm
+from desktop.lib.django_forms import ChoiceOrOtherField, MultiForm, SubmitButton
+from filebrowser.forms import PathField
+
+from beeswax import common
+from beeswax.server.dbms import NoSuchObjectException
+from beeswax.models import SavedQuery
+
+
+class DbForm(forms.Form):
+  """For 'show tables'"""
+  database = forms.ChoiceField(required=False,
+                           label='',
+                           choices=(('default', 'default'),),
+                           initial=0,
+                           widget=forms.widgets.Select(attrs={'class': 'span6'}))
+
+  def __init__(self, *args, **kwargs):
+    databases = kwargs.pop('databases')
+    super(DbForm, self).__init__(*args, **kwargs)
+    self.fields['database'].choices = ((db, db) for db in databases)
+
+
+class LoadDataForm(forms.Form):
+  """Form used for loading data into an existing table."""
+  path = PathField(label=_t("Path"))
+  overwrite = forms.BooleanField(required=False, initial=False, label=_t("Overwrite?"))
+
+  def __init__(self, table_obj, *args, **kwargs):
+    """
+    @param table_obj is a hive_metastore.thrift Table object,
+    used to add fields corresponding to partition keys.
+    """
+    super(LoadDataForm, self).__init__(*args, **kwargs)
+    self.partition_columns = dict()
+    for i, column in enumerate(table_obj.partition_keys):
+      # We give these numeric names because column names
+      # may be unpleasantly arbitrary.
+      name = "partition_%d" % i
+      char_field = forms.CharField(required=True, label=_t("%(column_name)s (partition key with type %(column_type)s)") % {'column_name': column.name, 'column_type': column.type})
+      self.fields[name] = char_field
+      self.partition_columns[name] = column.name

+ 16 - 0
apps/catalog/src/catalog/models.py

@@ -0,0 +1,16 @@
+#!/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.

+ 24 - 0
apps/catalog/src/catalog/settings.py

@@ -0,0 +1,24 @@
+#!/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.
+
+DJANGO_APPS = ['catalog']
+NICE_NAME = "Table Browser"
+REQUIRES_HADOOP = True
+ICON = "/catalog/static/art/table-browser-24-1.png"
+MENU_INDEX = 20
+
+IS_URL_NAMESPACED = True

+ 205 - 0
apps/catalog/src/catalog/templates/components.mako

@@ -0,0 +1,205 @@
+## Licensed to Cloudera, Inc. under one
+## or more contributor license agreements.  See the NOTICE file
+## distributed with this work for additional information
+## regarding copyright ownership.  Cloudera, Inc. licenses this file
+## to you under the Apache License, Version 2.0 (the
+## "License"); you may not use this file except in compliance
+## with the License.  You may obtain a copy of the License at
+##
+##     http://www.apache.org/licenses/LICENSE-2.0
+##
+## Unless required by applicable law or agreed to in writing, software
+## distributed under the License is distributed on an "AS IS" BASIS,
+## WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+## See the License for the specific language governing permissions and
+## limitations under the License.
+<%!
+  from desktop.lib.django_util import extract_field_data
+  from django.utils.translation import ugettext as _
+%>
+
+<%def name="fieldName(field)">
+</%def>
+
+<%def name="bootstrapLabel(field)">
+    <label for="${field.html_name | n}" class="control-label">${field.label}</label>
+</%def>
+
+<%def name="label(
+  field,
+  render_default=False,
+  data_filters=None,
+  hidden=False,
+  notitle=False,
+  tag='input',
+  klass=None,
+  attrs=None,
+  value=None,
+  help=False,
+  help_attrs=None,
+  dd_attrs=None,
+  dt_attrs=None,
+  title_klass=None,
+  button_text=False
+  )">
+<%
+  if value is None:
+    value = extract_field_data(field)
+
+  def make_attr_str(attributes):
+    if attributes is None:
+      attributes = {}
+    ret_str = ""
+    for key, value in attributes.iteritems():
+      if key == "klass":
+        key = "class"
+      ret_str += "%s='%s'" % (key.replace("_", "-"), unicode(value))
+    return ret_str
+
+  if not attrs:
+    attrs = {}
+  if not render_default:
+    attrs.setdefault('type', 'text')
+
+  if data_filters:
+    attrs.data_filters = data_filters
+
+  classes = []
+  if klass:
+    classes.append(klass)
+  if hidden:
+    classes.append("hide")
+  cls = ' '.join(classes)
+
+  title_classes = []
+  if title_klass:
+    title_classes.append(title_klass)
+  if notitle or hidden:
+    title_classes.append("hide")
+  titlecls = ' '.join(title_classes)
+%>
+${field.label_tag() | n}
+</%def>
+
+
+<%def name="field(
+  field,
+  render_default=False,
+  data_filters=None,
+  hidden=False,
+  notitle=False,
+  tag='input',
+  klass=None,
+  attrs=None,
+  value=None,
+  help=False,
+  help_attrs=None,
+  dd_attrs=None,
+  dt_attrs=None,
+  title_klass=None,
+  button_text=False,
+  placeholder=None,
+  file_chooser=False,
+  show_errors=True
+  )">
+<%
+  if value is None:
+    value = extract_field_data(field)
+
+  def make_attr_str(attributes):
+    if attributes is None:
+      attributes = {}
+    ret_str = ""
+    for key, value in attributes.iteritems():
+      if key == "klass":
+        key = "class"
+      ret_str += "%s='%s' " % (key.replace("_", "-"), unicode(value))
+    return ret_str
+
+  if not attrs:
+    attrs = {}
+  if not render_default:
+    attrs.setdefault('type', 'text')
+
+  if data_filters:
+    attrs.data_filters = data_filters
+
+  classes = []
+  if klass:
+    classes.append(klass)
+  if hidden:
+    classes.append("hide")
+  cls = ' '.join(classes)
+
+  title_classes = []
+  if title_klass:
+    title_classes.append(title_klass)
+  if notitle or hidden:
+    title_classes.append("hide")
+  titlecls = ' '.join(title_classes)
+
+  plc = ""
+  if placeholder:
+    plc = "placeholder=\"%s\"" % placeholder
+%>
+    % if field.is_hidden:
+        ${unicode(field) | n}
+    % else:
+        % if render_default:
+            ${unicode(field) | n}
+        % else:
+            % if tag == 'textarea':
+                <textarea name="${field.html_name | n}" ${make_attr_str(attrs) | n} class="${cls}" />${extract_field_data(field) or ''}</textarea>
+            % elif tag == 'button':
+                <button name="${field.html_name | n}" ${make_attr_str(attrs) | n} value="${value}"/>${button_text or field.name or ''}</button>
+            % elif tag == 'checkbox':
+                % if help:
+                    <input type="checkbox" name="${field.html_name | n}" ${make_attr_str(attrs) | n} ${value and "CHECKED" or ""}/ /> <span rel="tooltip" data-original-title="${help}" >${button_text or field.name or ''}</span>
+                % else:
+                    <input type="checkbox" name="${field.html_name | n}" ${make_attr_str(attrs) | n} ${value and "CHECKED" or ""}/> <span>${button_text or field.name or ''}</span>
+                % endif
+            % else:
+                %if file_chooser:
+                    <${tag} name="${field.html_name | n}" value="${extract_field_data(field) or ''}" ${make_attr_str(attrs) | n} class="${cls}" ${plc | n,unicode} /><a class="btn fileChooserBtn" href="#" data-filechooser-destination="${field.html_name | n}">..</a>
+                %else:
+                    <${tag} name="${field.html_name | n}" value="${extract_field_data(field) or ''}" ${make_attr_str(attrs) | n} class="${cls}" ${plc | n,unicode} />
+                %endif
+            % endif
+        % endif
+        % if show_errors and len(field.errors):
+            ${unicode(field.errors) | n}
+        % endif
+    % endif
+</%def>
+
+
+<%def name="pageref(num)">
+  % if hasattr(filter_params, "urlencode"):
+    href="?q-page=${num}&${filter_params.urlencode()}"
+  % else:
+    href="?q-page=${num}&${filter_params}"
+  % endif
+</%def>
+<%def name="prevpage(page)">
+  ${pageref(page.previous_page_number())}
+</%def>
+<%def name="nextpage(page)">
+  ${pageref(page.next_page_number())}
+</%def>
+<%def name="toppage(page)">
+  ${pageref(1)}
+</%def>
+<%def name="bottompage(page)">
+  ${pageref(page.num_pages())}
+</%def>
+<%def name="pagination(page)">
+    <div class="pagination">
+        <ul class="pull-right">
+            <li class="prev"><a title="${_('Beginning of List')}" ${toppage(page)}>&larr; ${_('Beginning of List')}</a></li>
+            <li><a title="${_('Previous Page')}" ${prevpage(page)}>${_('Previous Page')}</a></li>
+            <li><a title="${_('Next page')}" ${nextpage(page)}>${_('Next Page')}</a></li>
+            <li class="next"><a title="${_('End of List')}" ${bottompage(page)}>${_('End of List')} &rarr;</a></li>
+        </ul>
+        <p>${_('Showing %(start)s to %(end)s of %(count)s items, page %(page)s of %(pages)s') % dict(start=page.start_index(),end=page.end_index(),count=page.total_count(),page=page.number,pages=page.num_pages())}</p>
+    </div>
+</%def>

+ 34 - 0
apps/catalog/src/catalog/templates/confirm.html

@@ -0,0 +1,34 @@
+{% comment %}
+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.
+{% endcomment %}
+{% block content %}
+<form action="{{ url }}" method="POST">>
+<div class="modal-header">
+	<a href="#" class="close">&times;</a>
+	<h3>Confirm action</h3>
+</div>
+<div class="modal-body">
+  <div class="alert-message block-message warning">
+        {{title}}
+  </div>
+</div>
+<div class="modal-footer">
+	<input type="submit" class="btn primary" value="Yes"/>
+	<a href="#" class="btn secondary hideModal">No</a>
+</div>
+</form>
+{% endblock %}

+ 1 - 4
apps/beeswax/src/beeswax/templates/describe_partitions.mako → apps/catalog/src/catalog/templates/describe_partitions.mako

@@ -19,10 +19,7 @@
   from django.utils.translation import ugettext as _
 %>
 
-<%namespace name="layout" file="layout.mako" />
-
-${ commonheader(_('Beeswax Table Partitions: %(tableName)s') % dict(tableName=table.name), app_name, user, '100px') | n,unicode }
-${layout.menubar(section='tables')}
+${ commonheader(_('Table Partitions: %(tableName)s') % dict(tableName=table.name), app_name, user) | n,unicode }
 
 <div class="container-fluid">
 <h1>${_('Partitions')}</h1>

+ 7 - 9
apps/beeswax/src/beeswax/templates/describe_table.mako → apps/catalog/src/catalog/templates/describe_table.mako

@@ -18,8 +18,7 @@ from desktop.views import commonheader, commonfooter
 from django.utils.translation import ugettext as _
 %>
 
-<%namespace name="layout" file="layout.mako" />
-<%namespace name="comps" file="beeswax_components.mako" />
+<%namespace name="comps" file="components.mako" />
 
 <%
   if table.is_view:
@@ -27,8 +26,7 @@ from django.utils.translation import ugettext as _
   else:
     view_or_table_noun = _("Table")
 %>
-${ commonheader(_("%s Metadata: %s") % (view_or_table_noun, table.name), app_name, user, '100px') | n,unicode }
-${layout.menubar(section='tables')}
+${ commonheader(_("%s Metadata: %s") % (view_or_table_noun, table.name), app_name, user) | n,unicode }
 
 <%def name="column_table(cols)">
     <table class="table table-striped table-condensed datatables">
@@ -60,7 +58,7 @@ ${layout.menubar(section='tables')}
                 <ul class="nav nav-list">
                     <li class="nav-header">${_('Actions')}</li>
                     <li><a href="#" id="import-data-btn">${_('Import Data')}</a></li>
-                    <li><a href="${ url(app_name + ':read_table', database=database, table=table.name) }">${_('Browse Data')}</a></li>
+                    <li><a href="${ url('catalog:read_table', database=database, table=table.name) }">${_('Browse Data')}</a></li>
                     <li><a href="#dropTable" data-toggle="modal">${_('Drop')} ${view_or_table_noun}</a></li>
                     <li><a href="${ table.hdfs_link }" rel="${ table.path_location }">${_('View File Location')}</a></li>
                 </ul>
@@ -89,7 +87,7 @@ ${layout.menubar(section='tables')}
                 % if table.partition_keys:
                   <div class="tab-pane" id="partitionColumns">
                     ${column_table(table.partition_keys)}
-                    <a href="${ url(app_name + ':describe_partitions', database=database, table=table.name) }">${_('Show Partitions')}</a>
+                    <a href="${ url('catalog:describe_partitions', database=database, table=table.name) }">${_('Show Partitions')}</a>
                   </div>
                 % endif
 
@@ -131,7 +129,7 @@ ${layout.menubar(section='tables')}
 
 
 <div id="dropTable" class="modal hide fade">
-    <form id="dropTableForm" method="POST" action="${ url(app_name + ':drop_table', database=database) }">
+    <form id="dropTableForm" method="POST" action="${ url('catalog:drop_table', database=database) }">
     <div class="modal-header">
         <a href="#" class="close" data-dismiss="modal">&times;</a>
         <h3>${_('Drop Table')}</h3>
@@ -176,7 +174,7 @@ ${layout.menubar(section='tables')}
        }
      });
 
-     $.getJSON("${ url(app_name + ':drop_table', database=database) }", function(data) {
+     $.getJSON("${ url('catalog:drop_table', database=database) }", function(data) {
        $("#dropTableMessage").text(data.title);
      });
 
@@ -201,7 +199,7 @@ ${layout.menubar(section='tables')}
      })
 
     $("#import-data-btn").click(function () {
-      $.get("${ url(app_name + ':load_table', database=database, table=table.name) }", function (response) {
+      $.get("${ url('catalog:load_table', database=database, table=table.name) }", function (response) {
           $("#import-data-modal").html(response['data']);
           $("#import-data-modal").modal("show");
         }

+ 38 - 0
apps/catalog/src/catalog/templates/layout.mako

@@ -0,0 +1,38 @@
+## 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.
+##
+##
+## no spaces in this method please; we're declaring a CSS class, and ART uses this value for stuff, and it splits on spaces, and
+## multiple spaces and line breaks cause issues
+<%!
+from django.utils.translation import ugettext as _
+
+def is_selected(section, matcher):
+  if section == matcher:
+    return "active"
+  else:
+    return ""
+%>
+
+<%def name="menubar(section='')">
+<div class="subnav subnav-fixed">
+  <div class="container-fluid">
+    <ul class="nav nav-pills">
+      <li class="${is_selected(section, 'tables')}"><a href="${ url(app_name + ':show_tables') }">${_('Tables')}</a></li>
+    </ul>
+  </div>
+</div>
+</%def>

+ 2 - 5
apps/beeswax/src/beeswax/templates/load_data_popup.mako → apps/catalog/src/catalog/templates/popups/load_data.mako

@@ -17,10 +17,7 @@
 from django.utils.translation import ugettext as _
 %>
 
-
-<%namespace name="comps" file="beeswax_components.mako" />
-
-
+<%namespace name="comps" file="../components.mako" />
 
 <form method="POST" class="form-horizontal" id="load-data-form">
     <div class="modal-header">
@@ -111,7 +108,7 @@ from django.utils.translation import ugettext as _
      });
 
    $("#load-data-submit-btn").click(function(e){
-     $.post("${ url(app_name + ':load_table', database=database, table=table.name) }",
+     $.post("${ url('catalog:load_table', database=database, table=table.name) }",
        $("#load-data-form").serialize(),
         function (response) {
           if (response['status'] != 0) {

+ 14 - 19
apps/beeswax/src/beeswax/templates/show_tables.mako → apps/catalog/src/catalog/templates/tables.mako

@@ -20,8 +20,7 @@ from django.utils.translation import ugettext as _
 <%namespace name="actionbar" file="actionbar.mako" />
 <%namespace name="layout" file="layout.mako" />
 
-${ commonheader(_('Tables'), app_name, user, '100px') | n,unicode }
-${layout.menubar(section='tables')}
+${ commonheader(_('Tables'), 'catalog', user) | n,unicode }
 
 <div class="container-fluid">
     <h1>${_('Tables')}</h1>
@@ -29,14 +28,10 @@ ${layout.menubar(section='tables')}
         <div class="span3">
             <div class="well sidebar-nav">
                 <ul class="nav nav-list">
-                    <span
-                        % if app_name == 'impala':
-                            class="hide"
-                        % endif
-                    >
+                    <span>
                     <li class="nav-header">${_('database')}</li>
                     <li>
-                       <form action="${ url(app_name + ':show_tables') }" id="db_form" method="POST">
+                       <form action="${ url('catalog:show_tables') }" id="db_form" method="POST">
                          ${ db_form | n,unicode }
                        </form>
                     </li>
@@ -44,9 +39,9 @@ ${layout.menubar(section='tables')}
                     <li class="nav-header">${_('Actions')}</li>
                     % if not examples_installed:
                     <li><a href="#installSamples" data-toggle="modal">${_('Install samples')}</a></li>
-                      % endif
-                      <li><a href="${ url(app_name + ':import_wizard', database=database) }">${_('Create a new table from a file')}</a></li>
-                    <li><a href="${ url(app_name + ':create_table', database=database) }">${_('Create a new table manually')}</a></li>
+                    % endif
+                    <li><a href="${ url('beeswax:import_wizard', database=database) }">${_('Create a new table from a file')}</a></li>
+                    <li><a href="${ url('beeswax:create_table', database=database) }">${_('Create a new table manually')}</a></li>
                 </ul>
             </div>
         </div>
@@ -70,13 +65,13 @@ ${layout.menubar(section='tables')}
                   <tr>
                     <td data-row-selector-exclude="true" width="1%">
                       <div class="hueCheckbox tableCheck"
-                           data-view-url="${ url(app_name + ':describe_table', database=database, table=table) }"
-                           data-browse-url="${ url(app_name + ':read_table', database=database, table=table) }"
+                           data-view-url="${ url('catalog:describe_table', database=database, table=table) }"
+                           data-browse-url="${ url('catalog:read_table', database=database, table=table) }"
                            data-drop-name="${ table }"
                            data-row-selector-exclude="true"></div>
                     </td>
                     <td>
-                      <a href="${ url(app_name + ':describe_table', database=database, table=table) }" data-row-selector="true">${ table }</a>
+                      <a href="${ url('catalog:describe_table', database=database, table=table) }" data-row-selector="true">${ table }</a>
                     </td>
                   </tr>
                 % endfor
@@ -105,7 +100,7 @@ ${layout.menubar(section='tables')}
 % endif
 
 <div id="dropTable" class="modal hide fade">
-  <form id="dropTableForm" action="${ url(app_name + ':drop_table', database=database) }" method="POST">
+  <form id="dropTableForm" action="${ url('catalog:drop_table', database=database) }" method="POST">
     <div class="modal-header">
       <a href="#" class="close" data-dismiss="modal">&times;</a>
       <h3 id="dropTableMessage">${_('Confirm action')}</h3>
@@ -160,17 +155,17 @@ ${layout.menubar(section='tables')}
     });
 
     % if not examples_installed:
-        $.getJSON("${ url(app_name + ':install_examples') }", function (data) {
+        $.getJSON("${ url('beeswax:install_examples') }", function (data) {
           $("#installSamplesMessage").text(data.title);
         });
 
         $("#installSamplesBtn").click(function () {
           $.post(
-              "${ url(app_name + ':install_examples') }",
+              "${ url('beeswax:install_examples') }",
               { submit:"Submit" },
               function (result) {
                 if (result.creationSucceeded) {
-                  window.location.href = "${ url(app_name + ':show_tables') }";
+                  window.location.href = "${ url('catalog:show_tables') }";
                 }
                 else {
                   var message = "${_('There was an error processing your request:')} " + result.message;
@@ -225,7 +220,7 @@ ${layout.menubar(section='tables')}
     }
 
     $("#dropBtn").click(function () {
-      $.getJSON("${ url(app_name + ':drop_table', database=database) }", function(data) {
+      $.getJSON("${ url('catalog:drop_table', database=database) }", function(data) {
         $("#dropTableMessage").text(data.title);
       });
       viewModel.chosenTables.removeAll();

+ 74 - 0
apps/catalog/src/catalog/templates/util.mako

@@ -0,0 +1,74 @@
+## 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 django.utils.translation import ugettext as _
+%>
+<%def name="render_error(err)">
+  <div>
+    ${unicode(err) | n}
+  </div>
+</%def>
+
+<%def name="render_field(field)">
+  % if field.is_hidden:
+    ${unicode(field) | n}
+  % else:
+    <dt>${field.label_tag() | n}</dt>
+    <dd>${unicode(field) | n}</dd>
+    % if len(field.errors):
+      <dd>
+        ${render_error(field.errors)}
+      </dd>
+    % endif
+  % endif
+</%def>
+
+<%def name="render_formset(formset)">
+  <dl>
+  % for f in formset.forms:
+    ${render_form(f)}
+  % endfor
+  ${unicode(formset.management_form) | n }
+  </dl>
+</%def>
+
+<%def name="render_form(form)">
+  % for err in form.non_field_errors():
+    ${render_error(err)}
+  % endfor
+
+  % for field in form:
+    ${render_field(field)}
+  % endfor
+</%def>
+
+<%def name="render_query_context(query_context)">
+  % if query_context:
+    % if query_context[0] == 'table':
+      <% tablename, database = query_context[1].split(':') %>
+      <a href="${ url(app_name + ':describe_table', database, tablename) }">${tablename}</a>
+    % elif query_context[0] == 'design':
+      <% design = query_context[1] %>
+      % if design.is_auto:
+		<a href="${ url(app_name + ':execute_query', design.id)}">${_('Unsaved Query')}</a>
+      % else:
+        <a href="${ url(app_name + ':execute_query', design.id)}">${design.name}</a>
+      % endif
+    % else:
+      ${_('Query Results')}
+    % endif
+  % endif
+</%def>

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

@@ -0,0 +1,165 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+# 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 logging
+
+from nose.tools import assert_true, assert_equal, assert_false
+from nose.plugins.skip import SkipTest
+
+from django.utils.encoding import smart_str
+from django.contrib.auth.models import User
+from django.core.urlresolvers import reverse
+
+from desktop.lib.django_test_util import make_logged_in_client, assert_equal_mod_whitespace
+
+from beeswax.conf import BROWSE_PARTITIONED_TABLE_LIMIT
+from beeswax.views import collapse_whitespace
+from beeswax.test_base import make_query, wait_for_query_to_finish, verify_history, get_query_server_config
+from beeswax.models import QueryHistory
+from beeswax.server import dbms
+from beeswax.test_base import BeeswaxSampleProvider
+import hadoop
+
+
+LOG = logging.getLogger(__name__)
+
+
+def _make_query(client, query, submission_type="Execute",
+                udfs=None, settings=None, resources=[],
+                wait=False, name=None, desc=None, local=True,
+                is_parameterized=True, max=30.0, database='default', email_notify=False, **kwargs):
+  """Wrapper around the real make_query"""
+  res = make_query(client, query, submission_type,
+                   udfs, settings, resources,
+                   wait, name, desc, local, is_parameterized, max, database, email_notify, **kwargs)
+
+  # Should be in the history if it's submitted.
+  if submission_type == 'Execute':
+    fragment = collapse_whitespace(smart_str(query[:20]))
+    verify_history(client, fragment=fragment)
+
+  return res
+
+class TestCatalogWithHadoop(BeeswaxSampleProvider):
+  requires_hadoop = True
+
+  def setUp(self):
+    user = User.objects.get(username='test')
+    self.db = dbms.get(user, get_query_server_config())
+
+  def test_basic_flow(self):
+    """
+    Test basic query submission
+    """
+    # Table should have been created
+    response = self.client.get("/catalog/tables/")
+    assert_true("test" in response.context["tables"])
+
+    # Switch databases
+    response = self.client.get("/catalog/tables/default")
+    assert_true("test" in response.context["tables"])
+
+    response = self.client.get("/catalog/tables/not_there")
+    assert_false("test" in response.context["tables"])
+
+    # And have detail
+    response = self.client.get("/catalog/table/default/test")
+    assert_true("foo" in response.content)
+
+    # Remember the number of history items. Use a generic fragment 'test' to pass verification.
+    history_cnt = verify_history(self.client, fragment='test')
+
+    # Show table data.
+    response = self.client.get("/catalog/table/default/test/read", follow=True)
+    response = wait_for_query_to_finish(self.client, response, max=30.0)
+    # Note that it may not return all rows at once. But we expect at least 10.
+    assert_true(len(response.context['results']) > 10)
+    # Column names
+    assert_true("<td>foo</td>" in response.content)
+    assert_true("<td>bar</td>" in response.content)
+    # This should NOT go into the query history.
+    assert_equal(verify_history(self.client, fragment='test'), history_cnt,
+                 'Implicit queries should not be saved in the history')
+    assert_equal(str(response.context['query_context'][0]), 'table')
+    assert_equal(str(response.context['query_context'][1]), 'test:default')
+
+  def test_describe_view(self):
+    resp = self.client.get('/catalog/table/default/myview')
+    assert_equal(None, resp.context['sample'])
+    assert_true(resp.context['table'].is_view)
+    assert_true("View Metadata" in resp.content)
+    assert_true("Drop View" in resp.content)
+
+  def test_describe_partitions(self):
+    response = self.client.get("/catalog/table/default/test_partitions/partitions", follow=True)
+    assert_true("baz_one" in response.content)
+    assert_true("boom_two" in response.content)
+    response = self.client.get("/catalog/table/default/test/partitions", follow=True)
+    assert_true("is not partitioned." in response.content)
+
+  def test_browse_partitions_with_limit(self):
+    # Limit to 90
+    finish = BROWSE_PARTITIONED_TABLE_LIMIT.set_for_testing("90")
+    try:
+      response = self.client.get("/catalog/table/default/test_partitions")
+      assert_true("0x%x" % 89 in response.content, response.content)
+      assert_false("0x%x" % 90 in response.content, response.content)
+    finally:
+      finish()
+
+  def test_drop_multi_tables(self):
+    hql = """
+      CREATE TABLE test_drop_1 (a int);
+      CREATE TABLE test_drop_2 (a int);
+      CREATE TABLE test_drop_3 (a int);
+    """
+    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/tables/drop/default', follow=True)
+    assert_true('want to delete' in resp.content, resp.content)
+    resp = self.client.post('/catalog/tables/drop/default', {u'table_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.
+    These require Hadoop, because they ask the metastore
+    about whether a table is partitioned.
+    """
+    # Check that view works
+    resp = self.client.get("/catalog/table/default/test/load", follow=True)
+    assert_true('Path' in resp.content)
+
+    # Try the submission
+    self.client.post("/catalog/table/default/test/load", dict(path="/tmp/foo", overwrite=True), follow=True)
+    query = QueryHistory.objects.latest('id')
+
+    assert_equal_mod_whitespace("LOAD DATA INPATH '/tmp/foo' OVERWRITE INTO TABLE `default.test`", query.query)
+
+    resp = self.client.post("/catalog/table/default/test/load", dict(path="/tmp/foo", overwrite=False), follow=True)
+    query = QueryHistory.objects.latest('id')
+    assert_equal_mod_whitespace("LOAD DATA INPATH '/tmp/foo' INTO TABLE `default.test`", query.query)
+
+    # Try it with partitions
+    resp = self.client.post("/catalog/table/default/test_partitions/load", dict(path="/tmp/foo", partition_0="alpha", partition_1="beta"), follow=True)
+    query = QueryHistory.objects.latest('id')
+    assert_equal_mod_whitespace("LOAD DATA INPATH '/tmp/foo' INTO TABLE `default.test_partitions` PARTITION (baz='alpha', boom='beta')",
+        query.query)

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

@@ -0,0 +1,29 @@
+#!/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.
+
+from django.conf.urls.defaults import patterns, url
+
+urlpatterns = patterns('catalog.views',
+  url(r'^$', 'index', name='index'),
+
+  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'),
+  url(r'^table/(?P<database>\w+)/(?P<table>\w+)/partitions$', 'describe_partitions', name='describe_partitions'),
+  url(r'^table/(?P<database>\w+)/(?P<table>\w+)/load$', 'load_table', name='load_table'),
+  url(r'^table/(?P<database>\w+)/(?P<table>\w+)/read$', 'read_table', name='read_table'),
+)

+ 173 - 0
apps/catalog/src/catalog/views.py

@@ -0,0 +1,173 @@
+#!/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.
+
+try:
+  import json
+except ImportError:
+  import simplejson as json
+import logging
+
+from django.http import HttpResponse
+from django.shortcuts import redirect
+from django.utils.translation import ugettext as _
+from django.core.urlresolvers import reverse
+
+from desktop.lib.django_util import render
+from desktop.lib.exceptions_renderable import PopupException
+
+from beeswax.models import SavedQuery, MetaInstall
+from beeswax.server import dbms
+
+from catalog.forms import LoadDataForm, DbForm
+
+LOG = logging.getLogger(__name__)
+SAVE_RESULTS_CTAS_TIMEOUT = 300         # seconds
+
+
+def index(request):
+  return redirect(reverse('catalog:show_tables'))
+
+
+"""
+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()
+
+  if request.method == 'POST':
+    db_form = DbForm(request.POST, databases=databases)
+    if db_form.is_valid():
+      database = db_form.cleaned_data['database']
+  else:
+    db_form = DbForm(initial={'database': database}, databases=databases)
+
+  tables = db.get_tables(database=database)
+  examples_installed = MetaInstall.get().installed_example
+
+  return render("tables.mako", request, {
+      'tables': tables,
+      'examples_installed': examples_installed,
+      'db_form': db_form,
+      'database': database,
+      'tables_json': json.dumps(tables),
+  })
+
+
+def describe_table(request, database, table):
+  db = dbms.get(request.user)
+  error_message = ''
+  table_data = ''
+
+  table = db.get_table(database, table)
+
+  try:
+    table_data = db.get_sample(database, table)
+  except Exception, ex:
+    error_message, logs = dbms.expand_exception(ex, db)
+
+  return render("describe_table.mako", request, {
+      'table': table,
+      'sample': table_data and table_data.rows(),
+      'error_message': error_message,
+      'database': database,
+  })
+
+
+def drop_table(request, database):
+  db = dbms.get(request.user)
+
+  if request.method == 'POST':
+    tables = request.POST.getlist('table_selection')
+    tables_objects = [db.get_table(database, table) for table in tables]
+    try:
+      # Can't be simpler without an important refactoring
+      design = SavedQuery.create_empty(app_name='beeswax', owner=request.user)
+      query_history = db.drop_tables(database, tables_objects, design)
+      url = reverse('beeswax:watch_query', args=[query_history.id]) + '?on_success_url=' + reverse('catalog:show_tables')
+      return redirect(url)
+    except Exception, ex:
+      error_message, log = dbms.expand_exception(ex, db)
+      error = _("Failed to remove %(tables)s.  Error: %(error)s") % {'tables': ','.join(tables), 'error': error_message}
+      raise PopupException(error, title=_("Beeswax Error"), detail=log)
+  else:
+    title = _("Do you really want to delete the table(s)?")
+    return render('confirm.html', request, dict(url=request.path, title=title))
+
+
+def read_table(request, database, table):
+  db = dbms.get(request.user)
+
+  table = db.get_table(database, table)
+
+  try:
+    history = db.select_star_from(database, table)
+    url = reverse('beeswax:watch_query', args=[history.id]) + '?context=table:%s:%s' % (table.name, database)
+    return redirect(url)
+  except Exception, e:
+    raise PopupException(_('Can read table'), detail=e)
+
+
+def load_table(request, database, table):
+  db = dbms.get(request.user)
+  table = db.get_table(database, table)
+  response = {'status': -1, 'data': 'None'}
+
+  if request.method == "POST":
+    load_form = LoadDataForm(table, request.POST)
+
+    if load_form.is_valid():
+      on_success_url = reverse('catalog:describe_table', kwargs={'database': database, 'table': table.name})
+      try:
+        design = SavedQuery.create_empty(app_name='beeswax', owner=request.user)
+        query_history = db.load_data(database, table, load_form, design)
+        url = reverse('beeswax:watch_query', args=[query_history.id]) + '?on_success_url=' + on_success_url
+        response['status'] = 0
+        response['data'] = url
+      except Exception, e:
+        response['status'] = 1
+        response['data'] = _("Can't load the data: ") + str(e)
+  else:
+    load_form = LoadDataForm(table)
+
+  if response['status'] == -1:
+    popup = render('popups/load_data.mako', request, {
+                     'table': table,
+                     'load_form': load_form,
+                     'database': database,
+                     'app_name': 'beeswax'
+                 }, force_template=True).content
+    response['data'] = popup
+
+  return HttpResponse(json.dumps(response), mimetype="application/json")
+
+
+def describe_partitions(request, database, table):
+  db = dbms.get(request.user)
+
+  table_obj = db.get_table(database, table)
+  if not table_obj.partition_keys:
+    raise PopupException(_("Table '%(table)s' is not partitioned.") % {'table': table})
+
+  partitions = db.get_partitions(database, table_obj, max_parts=None)
+
+  return render("describe_partitions.mako", request,
+                dict(table=table_obj, partitions=partitions, request=request))

BIN
apps/catalog/static/art/table-browser-24-1.png


BIN
apps/catalog/static/help/images/23888161.png


+ 507 - 0
apps/catalog/static/help/index.html

@@ -0,0 +1,507 @@
+<h1><a name="Beeswax-IntroducingBeeswax"></a>Introducing Beeswax</h1>
+
+<p>The Beeswax application enables you to perform queries on Apache Hive, a data warehousing system designed to work with Hadoop. For information about Hive, see <a href="http://archive.cloudera.com/cdh4/cdh/4/hive/">Hive Documentation</a>. You can create Hive tables, load data, create, run, and manage queries, and download the results in a Microsoft Office Excel worksheet file or a comma-separated values file.</p>
+
+
+<h2><a name="Beeswax-Contents"></a>Contents</h2>
+
+<style type='text/css'>/*<![CDATA[*/
+div.rbtoc1359395567394 {margin-left: 1.5em;padding: 0px;}
+div.rbtoc1359395567394 ul {margin-left: 0px;padding-left: 20px;}
+div.rbtoc1359395567394 li {margin-left: 0px;padding-left: 0px;}
+
+/*]]>*/</style><div class='rbtoc1359395567394'>
+<ul>
+    <li><a href='#Beeswax-IntroducingBeeswax'>Introducing Beeswax</a></li>
+    <li><a href='#Beeswax-BeeswaxandHiveInstallationandConfiguration'>Beeswax and Hive Installation and Configuration</a></li>
+    <li><a href='#Beeswax-StartingBeeswax'>Starting Beeswax</a></li>
+<ul>
+<ul>
+    <li><a href='#Beeswax-InstallingtheSampleTables'>Installing the Sample Tables</a></li>
+    <li><a href='#Beeswax-ImportingYourOwnData'>Importing Your Own Data</a></li>
+</ul>
+</ul>
+    <li><a href='#Beeswax-WorkingwithQueries'>Working with Queries</a></li>
+<ul>
+<ul>
+    <li><a href='#Beeswax-CreatingandRunningQueries'>Creating and Running Queries</a></li>
+    <li><a href='#Beeswax-AdvancedQuerySettings'>Advanced Query Settings</a></li>
+    <li><a href='#Beeswax-ViewingQueryHistory'>Viewing Query History</a></li>
+    <li><a href='#Beeswax-Viewing%2CEditing%2CorDeletingSavedQueries'>Viewing, Editing, or Deleting Saved Queries</a></li>
+</ul>
+</ul>
+    <li><a href='#Beeswax-WorkingwithTables'>Working with Tables</a></li>
+<ul>
+<ul>
+    <li><a href='#Beeswax-SelectingtheDatabase'>Selecting the Database</a></li>
+    <li><a href='#Beeswax-CreatingTables'>Creating Tables</a></li>
+    <li><a href='#Beeswax-BrowsingTables'>Browsing Tables</a></li>
+    <li><a href='#Beeswax-ImportingDataintoTables'>Importing Data into Tables</a></li>
+    <li><a href='#Beeswax-DroppingTables'>Dropping Tables</a></li>
+    <li><a href='#Beeswax-ViewingaTable%27sLocation'>Viewing a Table's Location</a></li>
+</ul>
+</ul>
+</ul></div>
+<p><br class="atl-forced-newline" /></p>
+
+<h1><a name="Beeswax-BeeswaxandHiveInstallationandConfiguration"></a>Beeswax and Hive Installation and Configuration</h1>
+
+<p>Beeswax is installed and configured as part of Hue. For information about installing and configuring Hue, see <a href="https://ccp.cloudera.com/display/CDH4DOC/Hue+Installation">Hue Installation</a>.</p>
+
+<p>Beeswax assumes an existing Hive installation.  The Hue installation instructions include the configuration necessary for Beeswax to access Hive. You can view the current Hive configuration from from the <b>Settings</b> tab in the Beeswax application.</p>
+
+<p>By default, a Beeswax user can see the saved queries for all users &#8211; both his/her own queries and those of other Beeswax users. To restrict viewing saved queries to the query owner and Hue administrators, set the <tt>share_saved_queries</tt> property under the <tt>[beeswax]</tt> section in the Hue configuration file to <tt>false</tt>. </p>
+
+
+<h1><a name="Beeswax-StartingBeeswax"></a>Starting Beeswax</h1>
+
+<p>To start the Beeswax application, click the <b>Beeswax</b> icon (<span class="image-wrap" style=""><img src="/beeswax/static/help/images/23888161.png" width="30" style="border: 0px solid black"/></span>) in the navigation bar at the top of the Hue browser page.</p>
+
+<h3><a name="Beeswax-InstallingtheSampleTables"></a>Installing the Sample Tables</h3>
+
+<p>You can install two sample tables to use as examples.</p>
+
+<ol>
+	<li>In the Beeswax window, click <b>Tables</b>.</li>
+	<li>In the ACTIONS pane, click <b>Install samples</b>.</li>
+</ol>
+
+
+<p>Once you have installed the sample data, you will no longer see the <b>Install samples</b> link.</p>
+
+<h3><a name="Beeswax-ImportingYourOwnData"></a>Importing Your Own Data</h3>
+
+<p>If you want to import your own data instead of installing the sample tables, following the procedure in <a href="#Beeswax-CreatingTables">Creating Tables</a>.</p>
+
+<h1><a name="Beeswax-WorkingwithQueries"></a>Working with Queries</h1>
+
+<p>The Query Editor view lets you create queries in the <a href="http://wiki.apache.org/hadoop/Hive/LanguageManual">Hive Query Language (HQL)</a>, which is similar to Structured Query Language (SQL). You can name and save your queries to use later. When you submit a query, the Beeswax Server uses Hive to run the queries. You can either wait for the query to complete, or return later to find the queries in the History view. You can also request receive an email message after the query is completed.</p>
+
+<h3><a name="Beeswax-CreatingandRunningQueries"></a>Creating and Running Queries</h3>
+
+<div class='panelMacro'><table class='noteMacro'><colgroup><col width='24'><col></colgroup><tr><td valign='top'><img src="/static/art/help/warning.gif" width="16" height="16" align="absmiddle" alt="" border="0"></td><td><b>Note</b><br />To run a query, you must be logged in to Hue as a user that also has a Unix user account on the remote server.</td></tr></table></div>
+
+<p><b>To create and run a query</b>:</p>
+
+<ol>
+	<li>In the Query Editor window, type the query. For example, to select all data from the <em>sample_08</em> table, you would type:
+<div class="code panel" style="border-width: 1px;"><div class="codeContent panelContent">
+<div id="root">
+		<pre class="theme: Default; brush: plain; gutter: false">SELECT * FROM sample_08</pre>
+		</div>
+</div></div></li>
+	<li>In the box to the left of the Query field, you can override the default Hive and Hadoop settings, specify file resources and user-defined functions, and enable users to enter parameters at run-time, and request email notification when the job is complete.  See <a href="#Beeswax-AdvancedSettings">Advanced Query Settings</a> for details on using these settings.
+<br class="atl-forced-newline" />
+<br class="atl-forced-newline" /></li>
+	<li>To save your query and advanced settings to use again later, click <b>Save As,</b> enter a name and description, and then click <b>OK</b>. To save changes to an existing query, click <b>Save.</b>
+<br class="atl-forced-newline" />
+<br class="atl-forced-newline" /></li>
+	<li>If you want to view the execution plan for the query, click <b>Explain</b>. For more information, see <a href="http://wiki.apache.org/hadoop/Hive/LanguageManual/Explain">http://wiki.apache.org/hadoop/Hive/LanguageManual/Explain</a>.
+<br class="atl-forced-newline" />
+<br class="atl-forced-newline" /></li>
+	<li>To run the query, click <b>Execute</b>.
+<br class="atl-forced-newline" />
+<a name="Beeswax-QueryResults"></a><br/>
+The Query Results window displays with the results of the query.
+<br class="atl-forced-newline" />
+<br class="atl-forced-newline" /></li>
+	<li>Do any of the following to download or save the query results:
+<br class="atl-forced-newline" />
+	<ul>
+		<li>Click <b>Download as CSV</b> to download the results in a comma-separated values file suitable for use in other applications.</li>
+		<li>Click <b>Download as XLS</b> to download the results in a Microsoft Office Excel worksheet file.</li>
+		<li>Click <b>Save</b> to save the results in a table or HDFS file.
+		<ul>
+			<li>To save the results in a new table, select <b>In a new table</b>, enter a table name, and then click <b>Save</b>.</li>
+			<li>To save the results in an HDFS file, select <b>In an HDFS directory</b>, enter a path and then click <b>Save</b>. You can then download the file with FileBrowser.
+<div class='panelMacro'><table class='noteMacro'><colgroup><col width='24'><col></colgroup><tr><td valign='top'><img src="/static/art/help/warning.gif" width="16" height="16" align="absmiddle" alt="" border="0"></td><td><ul>
+	<li>You can only save results to a file when the results were generated by a MapReduce job.</li>
+	<li>This is the preferred way to save when the result is large (for example &gt; 1M rows).</li>
+</ul>
+</td></tr></table></div></li>
+		</ul>
+		</li>
+	</ul>
+	</li>
+</ol>
+
+
+<ul>
+	<li>Under <b>MR Jobs</b>, you can view any MapReduce jobs that the query started.</li>
+	<li>To view a log of the query execution, click <b>Log</b> at the top of the results display. You can use the information in this tab to debug your query.</li>
+	<li>To view the query that generated these results, click <b>Query</b> at the top of the results display.</li>
+	<li>To view the columns of the query, click <b>Columns</b>.</li>
+	<li>To return to the query in the Query Editor, click <b>Unsaved Query</b>.</li>
+</ul>
+
+
+<p><a name="Beeswax-AdvancedSettings"></a></p>
+
+<h3><a name="Beeswax-AdvancedQuerySettings"></a>Advanced Query Settings</h3>
+
+<p>The pane to the left of the Query Editor lets you specify the following options:
+<br class="atl-forced-newline" />
+<br class="atl-forced-newline" /></p>
+
+<style type="text/css">
+td.confluenceTd {
+border: #dddddd solid 1px;
+height: 36px;
+padding: 5px 10px;
+vertical-align: top;
+}
+th.confluenceTh {
+border: #dddddd solid 1px;
+height: 40px;
+padding: 5px 10px;
+vertical-align: center;
+}
+</style>
+
+
+<div class='table-wrap'>
+<table class='confluenceTable'><tbody>
+<tr>
+<th class='confluenceTh'> Option </th>
+<th class='confluenceTh'> Description </th>
+</tr>
+<tr>
+<td class='confluenceTd'> <b>DATABASE</b> </td>
+<td class='confluenceTd'> The database containing the table definitions. </td>
+</tr>
+<tr>
+<td class='confluenceTd'> <b>SETTINGS</b> </td>
+<td class='confluenceTd'> Override the Hive and Hadoop default settings. Click <b>Add</b> to configure a new setting. <br class="atl-forced-newline" />
+»&nbsp;&nbsp; For <b>Key</b>, enter a Hive or Hadoop configuration variable name. <br class="atl-forced-newline" />
+»&nbsp;&nbsp; For <b>Value</b>, enter the value you want to use for the variable. <br class="atl-forced-newline" />  <br class="atl-forced-newline" />
+For example, to override the directory where structured <font color="#000000">Hive&nbsp;</font>query logs are created, you would enter <tt>hive.querylog.location</tt> for <b>Key</b>, and a path for <b>Value.</b> <br class="atl-forced-newline" />  <br class="atl-forced-newline" />
+To view the default settings, click the <b>Settings</b> tab at the top of the page. <br class="atl-forced-newline" />  <br class="atl-forced-newline" />
+For information about Hive configuration variables, see: <a href="http://wiki.apache.org/hadoop/Hive/AdminManual/Configuration">http://wiki.apache.org/hadoop/Hive/AdminManual/Configuration</a>. For information about Hadoop configuration variables, see: <a href="http://hadoop.apache.org/docs/current/hadoop-mapreduce-client/hadoop-mapreduce-client-core/mapred-default.xml">http://hadoop.apache.org/docs/current/hadoop-mapreduce-client/hadoop-mapreduce-client-core/mapred-default.xml</a> </td>
+</tr>
+<tr>
+<td class='confluenceTd'> <b>FILE RESOURCES</b> </td>
+<td class='confluenceTd'> Make locally accessible files available at query execution time on the entire Hadoop cluster. Hive uses Hadoop's Distributed Cache to distribute the added files to all machines in the cluster at query execution time. <br class="atl-forced-newline" />  <br class="atl-forced-newline" />
+Click <b>Add</b> to configure a new setting. <br class="atl-forced-newline" />  <br class="atl-forced-newline" />
+From the <b>Type</b> drop-down menu, choose one of the following: <br class="atl-forced-newline" />
+<br class="atl-forced-newline" />
+<b>jar</b> &#8212; Adds the resources to the Java classpath. This is required in order to reference objects such as user defined functions. <br class="atl-forced-newline" />
+<br class="atl-forced-newline" />
+<b>archive</b> &#8212; Automatically unarchives resources when distributing them. <br class="atl-forced-newline" />
+<br class="atl-forced-newline" />
+<b>file</b> &#8212; Adds resources to the distributed cache. Typically, this might be a transform script (or similar) to be executed. <br class="atl-forced-newline" />
+<br class="atl-forced-newline" />
+For <b>Path</b>, enter the path to the file or click <b>Choose a File</b> to browse and select the file.&nbsp; 
+<div class='panelMacro'><table class='noteMacro'><colgroup><col width='24'><col></colgroup><tr><td valign='top'><img src="/static/art/help/warning.gif" width="16" height="16" align="absmiddle" alt="" border="0"></td><td><br class="atl-forced-newline" />
+It is not necessary to specify files used in a transform script if the files are available in the same path on all machines in the Hadoop cluster.</td></tr></table></div>
+<p> </p></td>
+</tr>
+<tr>
+<td class='confluenceTd'> <b>USER-DEFINED FUNCTIONS</b> </td>
+<td class='confluenceTd'> Specify user-defined functions in a query. Specify the function name for <b>Name</b>, and specify the class name for <b>Class</b> <b>name</b>. <br class="atl-forced-newline" />
+Click <b>Add</b> to configure a new setting. <br class="atl-forced-newline" />  <br class="atl-forced-newline" />
+You must specify a JAR file for the user-defined functions in <b>File Resources</b>. To include a user-defined function in a query, add a $ (dollar sign) before the function name in the query. For example, if <em>MyTable</em> is a user-defined function name in the query, you would type: <tt>SELECT * $MyTable</tt> </td>
+</tr>
+<tr>
+<td class='confluenceTd'> <b>PARAMETERIZATION</b> </td>
+<td class='confluenceTd'> Indicate that a dialog box should display to enter parameter values when a query containing the string $&lt;parametername&gt; is executed. Enabled by default. </td>
+</tr>
+<tr>
+<td class='confluenceTd'> <b>EMAIL NOTIFICATION</b> </td>
+<td class='confluenceTd'> Indicate that an email message should be sent after a query completes. The email is sent to the email address specified in the logged-in user's profile. </td>
+</tr>
+</tbody></table>
+</div>
+
+<p><br class="atl-forced-newline" /></p>
+
+<h3><a name="Beeswax-ViewingQueryHistory"></a>Viewing Query History</h3>
+
+<p>Beeswax enables you to view the history of queries that you have previously run. Results for these queries are available for one week or until Hue is restarted.</p>
+
+
+<p><b>To view query history:</b></p>
+
+<ol>
+	<li>In the Beeswax window, click <b>History</b>.
+<br class="atl-forced-newline" />
+<br class="atl-forced-newline" />
+Beeswax displays a list of your saved and unsaved queries in the Query History window.
+<br class="atl-forced-newline" />
+<br class="atl-forced-newline" /></li>
+	<li>To display the queries for all users, click <b>Show everyone's queries</b>. To display your queries only, click <b>Show my queries</b>.
+<br class="atl-forced-newline" />
+<br class="atl-forced-newline" /></li>
+	<li>To display the automatically generated actions that Beeswax performed on a user's behalf, click <b>Show auto actions</b>. To display user queries again, click <b>Show user queries</b>.</li>
+</ol>
+
+
+<h3><a name="Beeswax-Viewing%2CEditing%2CorDeletingSavedQueries"></a>Viewing, Editing, or Deleting Saved Queries</h3>
+
+<p>You can view a list of saved queries of all users by clicking <b>Saved Queries</b> in the Beeswax window. You can copy any user's query, but you can only edit, delete, and view the history of your own queries.</p>
+
+
+<p><b>To edit a saved query:</b></p>
+
+<ol>
+	<li>In the Beeswax window, click <b>Saved Queries</b>.
+<br class="atl-forced-newline" />
+<br class="atl-forced-newline" />
+The Queries window displays.
+<br class="atl-forced-newline" />
+<br class="atl-forced-newline" /></li>
+	<li>Click the <b>Options</b> button next to the query and choose <b>Edit</b> from the context menu.
+<br class="atl-forced-newline" />
+<br class="atl-forced-newline" />
+The query displays in the Query Editor window.
+<br class="atl-forced-newline" />
+<br class="atl-forced-newline" /></li>
+	<li>Change the query and then click <b>Save.</b> You can also click <b>Save As</b>, enter a new name, and click <b>OK</b> to save a copy of the query.</li>
+</ol>
+
+
+<p><b>To delete a saved query:</b></p>
+
+<ol>
+	<li>In the Beeswax window, click <b>Saved Queries</b>.
+<br class="atl-forced-newline" />
+<br class="atl-forced-newline" />
+The Queries window displays.
+<br class="atl-forced-newline" />
+<br class="atl-forced-newline" /></li>
+	<li>Click the <b>Options</b> button next to the query and choose <b>Delete</b> from the context menu.</li>
+	<li>Click <b>Yes</b> to confirm the deletion.</li>
+</ol>
+
+
+<p><b>To copy a saved query:</b></p>
+
+<ol>
+	<li>In the Beeswax window, click <b>Saved Queries</b>.
+<br class="atl-forced-newline" />
+<br class="atl-forced-newline" />
+The Queries window displays.
+<br class="atl-forced-newline" />
+<br class="atl-forced-newline" /></li>
+	<li>Click the <b>Options</b> button next to the query and choose <b>Clone</b> from the context menu.
+<br class="atl-forced-newline" />
+<br class="atl-forced-newline" />
+Beeswax displays the query in the Query Editor window.
+<br class="atl-forced-newline" />
+<br class="atl-forced-newline" /></li>
+	<li>Change the query as necessary and then click <b>Save.</b> You can also click <b>Save As</b>, enter a new name, and click <b>Ok</b> to save a copy of the query.</li>
+</ol>
+
+
+
+<p><b>To copy a query in the Beeswax Query History window:</b></p>
+
+<ol>
+	<li>In the Beeswax window, click <b>History</b>.
+<br class="atl-forced-newline" />
+<br class="atl-forced-newline" />
+The Query History window displays.
+<br class="atl-forced-newline" />
+<br class="atl-forced-newline" /></li>
+	<li>To display the queries for all users, click <b>Show everyone's queries</b>.
+<br class="atl-forced-newline" />
+<br class="atl-forced-newline" />
+The queries for all users display in the Query History window.
+<br class="atl-forced-newline" />
+<br class="atl-forced-newline" /></li>
+	<li>Click the <b>Clone</b> link next to the query you want to copy.
+<br class="atl-forced-newline" />
+<br class="atl-forced-newline" />
+A copy of the query displays in the Query Editor window.
+<br class="atl-forced-newline" />
+<br class="atl-forced-newline" /></li>
+	<li>Change the query, if necessary, and then click <b>Save As</b>, enter a new name, and click <b>OK</b> to save the query.</li>
+</ol>
+
+
+<h1><a name="Beeswax-WorkingwithTables"></a>Working with Tables</h1>
+
+<p>When working with Hive tables, you can use Beeswax to:</p>
+<ul>
+	<li><a href="#Beeswax-SelectingtheDatabase">Select a database</a></li>
+	<li><a href="#Beeswax-CreatingTables">Create tables</a></li>
+	<li><a href="#Beeswax-BrowsingTables">Browse tables</a></li>
+	<li><a href="#Beeswax-ImportingDataintoTables">Import data into tables</a></li>
+	<li>Drop tables (see <a href="#Beeswax-DroppingTables">Dropping Tables</a></li>
+	<li><a href="#Beeswax-ViewingaTable%27sLocation">View the location of a table</a></li>
+</ul>
+
+
+<p><a name="Beeswax-SelectingtheDatabase"></a></p>
+<h3><a name="Beeswax-SelectingtheDatabase"></a>Selecting the Database</h3>
+
+<ol>
+	<li>In the pane on the left, select the database from the DATABASE drop-down list.</li>
+</ol>
+
+
+<p><a name="Beeswax-CreatingTables"></a></p>
+
+<h3><a name="Beeswax-CreatingTables"></a>Creating Tables</h3>
+
+<p>Although you can create tables by executing the appropriate HQL DDL query commands, it is easier to create a table using the Beeswax table creation wizard.</p>
+
+<p>There are two ways to create a table: from a file or manually.</p>
+
+<p>If you create a table from a file, the format of the data in the file will determine some of the properties of the table, such as the record and file formats. The data from the file you specify is imported automatically upon table creation.</p>
+
+<p>When you create a file manually, you specify all the properties of the table, and then execute the resulting query to actually create the table.  You then import data into the table as an additional step.</p>
+
+
+<p><b>To create a table from a file</b>:</p>
+
+<ol>
+	<li>In the Beeswax window, click <b>Tables</b>.</li>
+	<li>In the ACTIONS pane, click <b>Create a new table from a file</b>.<br/>
+The table creation wizard starts.
+<br class="atl-forced-newline" />
+<br class="atl-forced-newline" /></li>
+	<li>Follow the instructions in the wizard to create the table. The basic steps are:
+	<ul>
+		<li>Choose your input file.  The input file you specify must exist.<br/>
+Note that you can choose to have Beeswax create the table definition only based on the import file you select, without actually importing data from that file.</li>
+		<li>Specify the column delimiter.</li>
+		<li>Define your columns, providing a name and selecting the type.
+<br class="atl-forced-newline" />
+<br class="atl-forced-newline" /></li>
+	</ul>
+	</li>
+	<li>Click <b>Create Table</b> to create the table.<br/>
+The new table's metadata displays on the right side of the <b>Table Metadata</b> window.
+<br class="atl-forced-newline" />
+<br class="atl-forced-newline" />
+At this point, you can view the metadata or a sample of the data in the table.<br/>
+From the ACTIONS pane you can import new data into the table, browse the table, drop it, or go to the File Browser to see the location of the data.</li>
+</ol>
+
+
+
+<p><b>To create a table manually:</b></p>
+
+<ol>
+	<li>In the Beeswax window, click <b>Tables</b>.</li>
+	<li>In the ACTIONS pane, click <b>Create a new table manually</b>.
+<br class="atl-forced-newline" />
+<br class="atl-forced-newline" />
+The table creation wizard starts.
+<br class="atl-forced-newline" />
+<br class="atl-forced-newline" /></li>
+	<li>Follow the instructions in the wizard to create the table. The basic steps are:
+	<ul>
+		<li>Name your table.</li>
+		<li>Choose the record format.</li>
+		<li>Configure record serialization by specifying delimiters for columns, collections, and map keys.</li>
+		<li>Choose the file format.</li>
+		<li>Specify the location for your table's data.</li>
+		<li>Define your columns, providing a name and selecting the type.</li>
+		<li>Add partitions, if appropriate.</li>
+	</ul>
+	</li>
+	<li>Click <b>Create table</b>.<br/>
+The Table Metadata window displays.</li>
+</ol>
+
+
+<p><a name="Beeswax-BrowsingTables"></a></p>
+
+<h3><a name="Beeswax-BrowsingTables"></a>Browsing Tables</h3>
+
+<p><b>To browse the data in a table:</b></p>
+
+<ol>
+	<li>In the Table List window, click the <b>Browse Data</b> button next to the table you want to browse.
+<br class="atl-forced-newline" />
+<br class="atl-forced-newline" />
+The table's data displays in the Query Results window.</li>
+</ol>
+
+
+<p><b>To browse the metadata in a table:</b></p>
+
+<ol>
+	<li>In the Table List window, click the table name.
+<br class="atl-forced-newline" />
+<br class="atl-forced-newline" />
+The table's metadata displays opened to the <b>Columns</b> tab. You can view the data in the table by selecting the <b>Sample</b> tab.</li>
+</ol>
+
+
+<p><a name="Beeswax-ImportingDataintoTables"></a></p>
+
+<h3><a name="Beeswax-ImportingDataintoTables"></a>Importing Data into Tables</h3>
+
+<p>When importing data, you can choose to append or overwrite the table's data with data from a file.</p>
+
+<p><b>To import data into a table:</b></p>
+
+<ol>
+	<li>In the Table List window, click the table name.
+<br class="atl-forced-newline" />
+<br class="atl-forced-newline" />
+The Table Metadata window displays.
+<br class="atl-forced-newline" />
+<br class="atl-forced-newline" /></li>
+	<li>In the ACTIONS pane, click <b>Import Data</b>.
+<br class="atl-forced-newline" />
+<br class="atl-forced-newline" /></li>
+	<li>For <b>Path</b>, enter the path to the file that contains the data you want to import.
+<br class="atl-forced-newline" />
+<br class="atl-forced-newline" /></li>
+	<li>Check <b>Overwrite existing data</b> to replace the data in the selected table with the imported data.  Leave this unchecked to append to the table.
+<br class="atl-forced-newline" />
+<br class="atl-forced-newline" /></li>
+	<li>Click <b>Submit</b>.</li>
+</ol>
+
+
+<p><a name="Beeswax-DroppingTables"></a></p>
+
+<h3><a name="Beeswax-DroppingTables"></a>Dropping Tables</h3>
+
+<p><b>To drop a table:</b></p>
+
+<ol>
+	<li>In the Table List window, click the table name.
+<br class="atl-forced-newline" />
+<br class="atl-forced-newline" />
+The Table Metadata window displays.
+<br class="atl-forced-newline" />
+<br class="atl-forced-newline" /></li>
+	<li>In the ACTIONS pane, click <b>Drop Table</b>.
+<br class="atl-forced-newline" />
+<br class="atl-forced-newline" /></li>
+	<li>Click <b>Yes</b> to confirm the deletion.</li>
+</ol>
+
+
+<p><a name="Beeswax-ViewingaTable%27sLocation"></a></p>
+
+<h3><a name="Beeswax-ViewingaTable%27sLocation"></a>Viewing a Table's Location</h3>
+
+<p><b>To view a table's location:</b></p>
+
+<ol>
+	<li>In the Table List window, click the table name.
+<br class="atl-forced-newline" />
+<br class="atl-forced-newline" />
+The Table Metadata window displays.
+<br class="atl-forced-newline" />
+<br class="atl-forced-newline" /></li>
+	<li>Click <b>View File Location</b>.
+<br class="atl-forced-newline" />
+<br class="atl-forced-newline" />
+The file location of the selected table displays in its directory in the File Browser window. </li>
+</ol>
+
+
+				    					    <br/>
+                        </td>
+		    </tr>
+	    </table>
+	    
+    </body>
+</html>

+ 1 - 1
apps/jobsub/setup.py

@@ -24,6 +24,6 @@ setup(
       description = "Hadoop Job Submission",
       packages = find_packages('src'),
       package_dir = {'': 'src'},
-      install_requires = ['setuptools', 'desktop'],
+      install_requires = ['setuptools', 'desktop', 'oozie'],
       entry_points = { 'desktop.sdk.application': 'jobsub=jobsub' },
 )

+ 1 - 0
desktop/core/src/desktop/lib/django_mako.py

@@ -102,6 +102,7 @@ def render_to_string_normal(template_name, django_context):
     data_dict = django_context
 
   template = lookup.get_template(template_name)
+  data_dict = dict(map(lambda k: (str(k), data_dict.get(k)), data_dict.keys()))
   result = template.render(**data_dict)
   return i18n.smart_unicode(result)