Browse Source

[pig] First round of beautification of the app

Enrico Berti 12 năm trước cách đây
mục cha
commit
fff06f0ee4

+ 5 - 2
apps/pig/src/pig/models.py

@@ -38,9 +38,9 @@ class Document(models.Model):
 
 
 class PigScript(Document):
-  _ATTRIBUTES = ['script', 'properties']
+  _ATTRIBUTES = ['script', 'name', 'properties']
   
-  data = models.TextField(default=json.dumps({'script': ''}))
+  data = models.TextField(default=json.dumps({'script': '', 'name': ''}))
     
   def update_from_dict(self, attrs):
     data_dict = self.dict
@@ -48,6 +48,9 @@ class PigScript(Document):
     if attrs.get('script'):
       data_dict['script'] = attrs['script']
 
+    if attrs.get('name'):
+      data_dict['name'] = attrs['name']
+
     self.data = json.dumps(data_dict)
     
   @property

+ 1 - 1
apps/pig/src/pig/settings.py

@@ -16,7 +16,7 @@
 DJANGO_APPS = ['pig']
 NICE_NAME = 'Pig'
 MENU_INDEX = 12
-ICON = '/pig/static/art/icon_pig_24.ico'
+ICON = '/pig/static/art/icon_pig_24.png'
 
 REQUIRES_HADOOP = False
 IS_URL_NAMESPACED = True

+ 458 - 0
apps/pig/src/pig/templates/app.mako

@@ -0,0 +1,458 @@
+## Licensed to Cloudera, Inc. under one
+## or more contributor license agreements.  See the NOTICE file
+## distributed with this work for additional information
+## regarding copyright ownership.  Cloudera, Inc. licenses this file
+## to you under the Apache License, Version 2.0 (the
+## "License"); you may not use this file except in compliance
+## with the License.  You may obtain a copy of the License at
+##
+##     http://www.apache.org/licenses/LICENSE-2.0
+##
+## Unless required by applicable law or agreed to in writing, software
+## distributed under the License is distributed on an "AS IS" BASIS,
+## WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+## See the License for the specific language governing permissions and
+## limitations under the License.
+<%!
+  from desktop.views import commonheader, commonfooter
+  from django.utils.translation import ugettext as _
+%>
+
+<%namespace name="actionbar" file="actionbar.mako" />
+
+${ commonheader(_('Pig'), "pig", user, "100px") | n,unicode }
+
+<div class="subnav subnav-fixed">
+  <div class="container-fluid">
+    <ul class="nav nav nav-pills">
+      <li class="active"><a href="#scripts">${ _('Scripts') }</a></li>
+      <li><a href="#editor">${ _('Editor') }</a></li>
+      <li><a href="#dashboard">${ _('Dashboard') }</a></li>
+      ##<li class="${utils.is_selected(section, 'udfs')}"><a href="${ url('pig:udfs') }">${ _('UDF') }</a></li>
+      </ul>
+  </div>
+</div>
+
+
+<div class="container-fluid">
+  <div id="scripts" class="row-fluid mainSection hide">
+    <%actionbar:render>
+      <%def name="search()">
+          <input id="filter" type="text" class="input-xlarge search-query" placeholder="${_('Search for script name or content')}">
+      </%def>
+
+      <%def name="actions()">
+          <button class="btn fileToolbarBtn" title="${_('Run this script')}" data-bind="enable: selectedScripts().length == 1, click: listRunScript, visible: scripts().length > 0"><i class="icon-play"></i> ${_('Run')}</button>
+          <button class="btn fileToolbarBtn" title="${_('Copy this script')}" data-bind="enable: selectedScripts().length == 1, click: listCopyScript, visible: scripts().length > 0"><i class="icon-retweet"></i> ${_('Copy')}</button>
+          <button class="btn fileToolbarBtn" title="${_('Delete this script')}" data-bind="enable: selectedScripts().length > 0, click: listConfirmDeleteScripts, visible: scripts().length > 0"><i class="icon-trash"></i> ${_('Delete')}</button>
+      </%def>
+
+      <%def name="creation()">
+          <button class="btn fileToolbarBtn" title="${_('Create a new script')}" data-bind="click: newScript"><i class="icon-plus-sign"></i> ${_('New script')}</button>
+      </%def>
+    </%actionbar:render>
+    <div class="alert alert-info" data-bind="visible: scripts().length == 0">
+      ${_('There are currently no scripts defined. Please add a new script clicking on "New script"')}
+    </div>
+
+    <table class="table table-striped table-condensed tablescroller-disable" data-bind="visible: scripts().length > 0">
+      <thead>
+      <tr>
+        <th width="1%"><div data-bind="click: selectAll, css: {hueCheckbox: true, 'icon-ok': allSelected}"></div></th>
+        <th width="20%">${_('Name')}</th>
+        <th width="79%">${_('Script')}</th>
+      </tr>
+      </thead>
+      <tbody id="scriptTable" data-bind="template: {name: 'scriptTemplate', foreach: filteredScripts}">
+
+      </tbody>
+      <tfoot>
+      <tr data-bind="visible: isLoading()">
+        <td colspan="3" class="left">
+          <img src="/static/art/spinner.gif" />
+        </td>
+      </tr>
+      <tr data-bind="visible: filteredScripts().length == 0 && !isLoading()">
+        <td colspan="3">
+          <div class="alert">
+              ${_('There are no scripts matching the search criteria.')}
+          </div>
+        </td>
+      </tr>
+      </tfoot>
+    </table>
+
+    <script id="scriptTemplate" type="text/html">
+      <tr style="cursor: pointer" data-bind="event: { mouseover: toggleHover, mouseout: toggleHover}">
+        <td class="center" data-bind="click: handleSelect" style="cursor: default">
+          <div data-bind="css: {hueCheckbox: true, 'icon-ok': selected}"></div>
+        </td>
+        <td data-bind="click: $root.viewScript">
+          <strong><a href="#" data-bind="click: $root.viewScript, text: name"></a></strong>
+        </td>
+        <td data-bind="click: $root.viewScript">
+          <span data-bind="text: script"></span>
+        </td>
+      </tr>
+    </script>
+  </div>
+
+  <div id="editor" class="row-fluid mainSection hide">
+    <div class="span2">
+      <div class="well sidebar-nav">
+        <form id="advancedSettingsForm" method="POST" class="form form-horizontal noPadding">
+          <ul class="nav nav-list">
+            <li class="nav-header">${_('Editor')}</li>
+            <li data-bind="click: editScript" class="active" data-section="edit"><a href="#">${ _('Edit script') }</a></li>
+            <li class="nav-header">${_('Properties')}</li>
+            <li data-bind="click: editScriptProperties" data-section="properties"><a href="#">${ _('Edit properties') }</a></li>
+            ##<li class="nav-header">${_('UDF')}</li>
+                        ##<li><a href="#createDataset">${ _('New') }</a></li>
+                        ##<li><a href="#createDataset">${ _('Add') }</a></li>
+                        <li class="nav-header">${_('Actions')}</li>
+            <li data-bind="click: saveScript">
+              <a href="#" title="${ _('Save the script') }" rel="tooltip" data-placement="right">
+                <i class="icon-save"></i> ${ _('Save') }
+              </a>
+            </li>
+            <li data-bind="click: runScript">
+              <a href="#" title="${ _('Run the script') }" rel="tooltip" data-placement="right">
+                <i class="icon-play"></i> ${ _('Run') }
+              </a>
+            </li>
+            <li data-bind="visible: currentScript().id() != -1, click: copyScript">
+              <a href="#" title="${ _('Copy the script') }" rel="tooltip" data-placement="right">
+                <i class="icon-retweet"></i> ${ _('Copy') }
+              </a>
+            </li>
+            <li data-bind="visible: currentScript().id() != -1, click: confirmDeleteScript">
+              <a href="#" title="${ _('Delete the script') }" rel="tooltip" data-placement="right">
+                <i class="icon-trash"></i> ${ _('Delete') }
+              </a>
+            </li>
+          </ul>
+        </form>
+      </div>
+    </div>
+
+    <div class="span10">
+      <div id="edit" class="section">
+        <div class="alert alert-info"><h3>${ _('Edit') } '<span data-bind="text: currentScript().name"></span>'</h3></div>
+
+        <form id="queryForm">
+          <textarea id="scriptEditor" data-bind="text:currentScript().script"></textarea>
+        </form>
+      </div>
+      <div id="properties" class="section hide">
+        <div class="alert alert-info"><h3>${ _('Edit properties for') } '<span data-bind="text: currentScript().name"></span>'</h3></div>
+        <form class="form-inline" style="padding-left: 10px">
+          <label>${ _('Script name') } &nbsp; <input type="text" id="scriptName" class="input-xlarge" data-bind="value: currentScript().name" />
+          </label>
+        </form>
+      </div>
+
+    </div>
+
+  </div>
+
+  <div id="dashboard" class="row-fluid mainSection hide">
+    <h3>Running</h3>
+    <div class="alert alert-info" data-bind="visible: runningScripts().length == 0">
+      ${_('There are currently no running scripts.')}
+    </div>
+    <table class="table table-striped table-condensed datatables tablescroller-disable" data-bind="visible: runningScripts().length > 0">
+      <thead>
+      <tr>
+        <th width="20%">${_('Name')}</th>
+        <th width="10%">${_('Status')}</th>
+        <th width="">${_('Created on')}</th>
+      </tr>
+      </thead>
+      <tbody data-bind="template: {name: 'dashboardTemplate', foreach: runningScripts}">
+
+      </tbody>
+    </table>
+
+    <h3>Completed</h3>
+    <div class="alert alert-info" data-bind="visible: completedScripts().length == 0">
+      ${_('There are currently no completed scripts.')}
+    </div>
+    <table class="table table-striped table-condensed datatables tablescroller-disable" data-bind="visible: completedScripts().length > 0">
+      <thead>
+      <tr>
+        <th width="20%">${_('Name')}</th>
+        <th width="10%">${_('Status')}</th>
+        <th width="">${_('Created on')}</th>
+      </tr>
+      </thead>
+      <tbody data-bind="template: {name: 'dashboardTemplate', foreach: completedScripts}">
+
+      </tbody>
+    </table>
+
+    <script id="dashboardTemplate" type="text/html">
+      <tr style="cursor: pointer">
+        <td>
+          <strong><a data-bind="text: appName, attr: {'href': absoluteUrl}" target="_blank"></a></strong>
+        </td>
+        <td>
+          <span data-bind="attr: {'class': statusClass}, text: status"></span>
+        </td>
+        <td>
+          <span data-bind="text: created"></span>
+        </td>
+      </tr>
+    </script>
+  </div>
+</div>
+
+<!-- delete modal -->
+<div id="deleteModal" class="modal hide fade">
+  <div class="modal-header">
+    <a href="#" class="close" data-dismiss="modal">&times;</a>
+    <h3>${_('Confirm Delete')}</h3>
+  </div>
+  <div class="modal-body">
+    <p class="deleteMsg hide single">${_('Are you sure you want to delete this script?')}</p>
+    <p class="deleteMsg hide multiple">${_('Are you sure you want to delete these scripts?')}</p>
+  </div>
+  <div class="modal-footer">
+    <a class="btn" data-dismiss="modal">${_('No')}</a>
+    <a class="btn btn-danger" data-bind="click: deleteScripts">${_('Yes')}</a>
+  </div>
+</div>
+
+<div class="bottomAlert alert"></div>
+
+<script src="/static/ext/js/knockout-2.1.0.js" type="text/javascript" charset="utf-8"></script>
+<script src="/pig/static/js/utils.js" type="text/javascript" charset="utf-8"></script>
+<script src="/pig/static/js/pig.ko.js" type="text/javascript" charset="utf-8"></script>
+<script src="/static/ext/js/routie-0.3.0.min.js" type="text/javascript" charset="utf-8"></script>
+<link rel="stylesheet" href="/pig/static/css/pig.css">
+<script src="/static/ext/js/codemirror-3.0.js"></script>
+<link rel="stylesheet" href="/static/ext/css/codemirror.css">
+<script src="/static/ext/js/codemirror-pig.js"></script>
+<script src="/static/ext/js/codemirror-show-hint.js"></script>
+<script src="/static/ext/js/codemirror-pig-hint.js"></script>
+<link rel="stylesheet" href="/static/ext/css/codemirror-show-hint.css">
+
+
+<script type="text/javascript" charset="utf-8">
+
+  var LABELS = {
+    KILL_ERROR: "${ _('The pig job could not be killed.') }",
+    TOOLTIP_PLAY: "${ _('Run this pig script') }",
+    TOOLTIP_STOP: "${ _('Stop the execution') }",
+    SAVED: "${ _('Saved') }",
+    NEW_SCRIPT_NAME: "${ _('Unsaved script') }",
+    NEW_SCRIPT_CONTENT: "ie. a = LOAD '/user/${ user }/data';"
+  };
+
+  var scripts = ${ scripts | n,unicode };
+
+  var appProperties = {
+    labels: LABELS,
+    listScripts: "${ url('pig:scripts') }",
+    saveUrl: "${ url('pig:save') }",
+    runUrl: "${ url('pig:run') }",
+    copyUrl: "${ url('pig:copy') }",
+    deleteUrl: "${ url('pig:delete') }"
+  }
+
+  var viewModel = new PigViewModel(scripts, appProperties);
+  ko.applyBindings(viewModel);
+
+  $(document).ready(function () {
+
+    var scriptEditor = $("#scriptEditor")[0];
+
+    CodeMirror.commands.autocomplete = function(cm) {
+      CodeMirror.showHint(cm, CodeMirror.pigHint);
+    }
+    var codeMirror = CodeMirror(function (elt) {
+      scriptEditor.parentNode.replaceChild(elt, scriptEditor);
+    }, {
+      value: scriptEditor.value,
+      readOnly: false,
+      lineNumbers: true,
+      mode: "text/x-pig",
+      extraKeys: {"Shift-Space": "autocomplete"}
+    });
+
+    codeMirror.on("focus", function () {
+      if (codeMirror.getValue() == LABELS.NEW_SCRIPT_CONTENT) {
+        codeMirror.setValue("");
+      }
+    });
+
+    codeMirror.on("change", function () {
+      viewModel.currentScript().script(codeMirror.getValue());
+    });
+
+    showMainSection("scripts");
+
+    $(document).on("loadEditor", function () {
+      codeMirror.setValue(viewModel.currentScript().script());
+    });
+
+    $(document).on("showEditor", function () {
+      if (viewModel.currentScript().id() != -1) {
+        routie("edit/" + viewModel.currentScript().id());
+      }
+      else {
+        routie("edit");
+      }
+    });
+
+    $(document).on("showProperties", function () {
+      if (viewModel.currentScript().id() != -1) {
+        routie("properties/" + viewModel.currentScript().id());
+      }
+      else {
+        routie("properties");
+      }
+    });
+
+    $(document).on("updateTooltips", function () {
+      $("a[rel=tooltip]").tooltip("destroy");
+      $("a[rel=tooltip]").tooltip();
+    });
+
+    $(document).on("saving", function () {
+      showAlert("${_('Saving')} <b>" + viewModel.currentScript().name() + "</b>...");
+    });
+
+    $(document).on("running", function () {
+      showAlert("${_('Running')} <b>" + viewModel.currentScript().name() + "</b>...", "info");
+    });
+
+    $(document).on("saved", function () {
+      showAlert("<b>" + viewModel.currentScript().name() + "</b> ${_('has been saved correctly.')}", "success");
+    });
+
+    $(document).on("error", function () {
+      showAlert("<b>${_('There was an error with your request!')}</b>", "error");
+    });
+
+    $(document).on("showDashboard", function () {
+      routie("dashboard");
+    });
+
+    $(document).on("showScripts", function () {
+      routie("scripts");
+    });
+
+    $(document).on("scriptsRefreshed", function () {
+      $("#filter").val("");
+    });
+
+    var _resizeTimeout = -1;
+    $(window).on("resize", function () {
+      window.clearTimeout(_resizeTimeout);
+      _resizeTimeout = window.setTimeout(function () {
+        codeMirror.setSize("100%", $(window).height() - 250);
+      }, 100);
+    });
+
+    var _filterTimeout = -1;
+    $("#filter").on("keyup", function () {
+      window.clearTimeout(_filterTimeout);
+      _filterTimeout = window.setTimeout(function () {
+        viewModel.filterScripts($("#filter").val());
+      }, 350);
+    });
+
+    refreshDashboard();
+
+    var dashboardRefreshInterval = window.setInterval(function () {
+      refreshDashboard();
+    }, 1000);
+
+    function refreshDashboard() {
+      $.getJSON("${ url('pig:dashboard') }", function (data) {
+        viewModel.updateDashboard(data);
+      });
+    }
+
+    function showMainSection(mainSection) {
+      window.setTimeout(function () {
+        codeMirror.refresh();
+        codeMirror.setSize("100%", $(window).height() - 250);
+      }, 100);
+
+      if ($("#" + mainSection).is(":hidden")) {
+        $(".mainSection").hide();
+        $("#" + mainSection).show();
+        highlightMainMenu(mainSection);
+      }
+    }
+
+    function showSection(mainSection, section) {
+      showMainSection(mainSection);
+      if ($("#" + section).is(":hidden")) {
+        $(".section").hide();
+        $("#" + section).show();
+        highlightMenu(section);
+      }
+    }
+
+    function highlightMainMenu(mainSection) {
+      $(".nav-pills li").removeClass("active");
+      $("a[href='#" + mainSection + "']").parent().addClass("active");
+    }
+
+    function highlightMenu(section) {
+      $(".nav-list li").removeClass("active");
+      $("li[data-section='" + section + "']").addClass("active");
+    }
+
+    routie({
+      "editor": function () {
+        showMainSection("editor");
+      },
+      "scripts": function () {
+        showMainSection("scripts");
+      },
+      "dashboard": function () {
+        showMainSection("dashboard");
+      },
+
+      "edit": function () {
+        showSection("editor", "edit");
+      },
+      "edit/:scriptId": function (scriptId) {
+        if (scriptId !== "undefined" && scriptId != viewModel.currentScript().id()) {
+          viewModel.loadScript(scriptId);
+          $(document).trigger("loadEditor");
+        }
+        showSection("editor", "edit");
+      },
+      "properties": function () {
+        showSection("editor", "properties");
+      },
+      "properties/:scriptId": function (scriptId) {
+        if (scriptId !== "undefined" && scriptId != viewModel.currentScript().id()) {
+          viewModel.loadScript(scriptId);
+          $(document).trigger("loadEditor");
+        }
+        showSection("editor", "properties");
+      }
+    });
+
+  });
+
+  var _bottomAlertFade = -1;
+  function showAlert(msg, extraClass) {
+    var klass = "alert ";
+    if (extraClass != null && extraClass != undefined) {
+      klass += "alert-" + extraClass;
+    }
+    $(".bottomAlert").attr("class", "bottomAlert " + klass).html(msg).show();
+    window.clearTimeout(_bottomAlertFade);
+    _bottomAlertFade = window.setTimeout(function () {
+      $(".bottomAlert").fadeOut();
+    }, 3000);
+  }
+</script>
+
+${ commonfooter(messages) | n,unicode }

+ 0 - 48
apps/pig/src/pig/templates/dashboard.mako

@@ -1,48 +0,0 @@
-## Licensed to Cloudera, Inc. under one
-## or more contributor license agreements.  See the NOTICE file
-## distributed with this work for additional information
-## regarding copyright ownership.  Cloudera, Inc. licenses this file
-## to you under the Apache License, Version 2.0 (the
-## "License"); you may not use this file except in compliance
-## with the License.  You may obtain a copy of the License at
-##
-##     http://www.apache.org/licenses/LICENSE-2.0
-##
-## Unless required by applicable law or agreed to in writing, software
-## distributed under the License is distributed on an "AS IS" BASIS,
-## WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-## See the License for the specific language governing permissions and
-## limitations under the License.
-<%!
-from desktop.views import commonheader, commonfooter
-from django.utils.translation import ugettext as _
-%>
-
-<%namespace name="navigation" file="navigation-bar.inc.mako" />
-<%namespace name="utils" file="utils.inc.mako" />
-
-${ commonheader(_('Pig'), "pig", user, "100px") | n,unicode }
-
-
-<div class="container-fluid">
-   ${ navigation.menubar(section='dashboard') }
-  
-    <div class="tab-content">
-      <div class="tab-pane active">
-
-		<div class="container-fluid">
-		    <div class="row-fluid">                  
-		       % for workflow in workflows.jobs:
-		         ${ workflow } </br>
-		       % endfor
-		    </div>
-		</div>
-
-   
-      </div>
-
-    </div>
-  </div>
-</div>
-
-${ commonfooter(messages) | n,unicode }

+ 0 - 190
apps/pig/src/pig/templates/editor.mako

@@ -1,190 +0,0 @@
-## Licensed to Cloudera, Inc. under one
-## or more contributor license agreements.  See the NOTICE file
-## distributed with this work for additional information
-## regarding copyright ownership.  Cloudera, Inc. licenses this file
-## to you under the Apache License, Version 2.0 (the
-## "License"); you may not use this file except in compliance
-## with the License.  You may obtain a copy of the License at
-##
-##     http://www.apache.org/licenses/LICENSE-2.0
-##
-## Unless required by applicable law or agreed to in writing, software
-## distributed under the License is distributed on an "AS IS" BASIS,
-## WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-## See the License for the specific language governing permissions and
-## limitations under the License.
-<%!
-from desktop.views import commonheader, commonfooter
-from django.utils.translation import ugettext as _
-%>
-
-<%namespace name="navigation" file="navigation-bar.inc.mako" />
-<%namespace name="utils" file="utils.inc.mako" />
-
-${ commonheader(_('Pig'), "pig", user, "100px") | n,unicode }
-
-<script src="/static/ext/js/datatables-paging-0.1.js" type="text/javascript" charset="utf-8"></script>
-<script src="/static/ext/js/knockout-2.1.0.js" type="text/javascript" charset="utf-8"></script>
-<script src="/static/ext/js/moment.min.js" type="text/javascript" charset="utf-8"></script>
-<script src="/pig/static/js/pig.ko.js" type="text/javascript" charset="utf-8"></script>
-<script src="/static/ext/js/routie-0.3.0.min.js" type="text/javascript" charset="utf-8"></script>
-
-<div class="container-fluid">
-  ${ navigation.menubar(section='editor') }
-  
-</div>
-
-<br/>
-
-<div class="container-fluid">
-    <div class="row-fluid">
-        <div class="span3">
-            <div class="well sidebar-nav">
-                <form id="advancedSettingsForm" method="POST" class="form form-horizontal noPadding">
-                    <ul class="nav nav-list">
-                        <li class="nav-header">${_('Editor')}</li>
-                        <li class="active"><a href="#editor">${ _('Edit script') }</a></li>                        
-                        <li class="nav-header">${_('Properties')}</li>
-                        <li><a href="#edit-properties">${ _('Edit properties') }</a></li>
-                        ##<li class="nav-header">${_('UDF')}</li>
-                        ##<li><a href="#createDataset">${ _('New') }</a></li>
-                        ##<li><a href="#createDataset">${ _('Add') }</a></li>
-                        <li class="nav-header">${_('Actions')}</li>
-                        <li>
-                          <a href="#save" title="${ _('Save the script') }" rel="tooltip" data-placement="right">
-                            <i class="icon-save"></i> ${ _('Save') }
-                          </a>
-                        </li>                            
-                        <li>
-                          <a id="clone-btn" href="javascript:void(0)" data-clone-url="" title="${ _('Copy the script') }" rel="tooltip" data-placement="right">
-                            <i class="icon-retweet"></i> ${ _('Copy') }
-                          </a>
-                        </li>
-                    </ul>
-                </form>
-            </div>
-        </div>
-
-        <div id="editor" class="section span9">
-          <h1>${_('Editor')}</h1>
-            <ul class="nav nav nav-pills">
-              <li class="active"><a href="#pig" data-toggle="tab">${ _('Pig') }</a></li>
-            </ul>                  
-		    <form id="queryForm">
-		      <textarea class="span11" rows="20" placeholder="A = LOAD '/user/${ user }/data';" name="script" id="script"></textarea>
-		    </form>
-            <br/>
-        </div>
-        
-        <div id="edit-properties" class="section hide span9">
-          <div class="alert alert-info"><h3>${ _('Edit properties') }</h3></div>
-          <div id="edit-properties-body">
-            None for now
-          </div>
-          <div class="form-actions">
-            <a href="#" class="btn btn-primary" id="add-dataset-btn">${ _('Save') }</a>
-          </div>
-        </div>        
-        
-      </div>
-   </div>
-
-    <div data-bind="visible: isLoading()">
-      <img src="/static/art/spinner.gif"/>
-    </div>
-    <div class="alert" data-bind="visible: apps().length == 0  && !isLoading()">
-      ${ _('Sorry, there are no editor matching the search criteria.') }
-    </div>
-    <div data-bind="template: {name: 'appTemplate', foreach: apps}"></div>
-  </div>
-
-    </div>
-  </div>
-</div>
-
-
-<script id="appTemplate" type="text/html">
-  <div class="appWidget" data-bind="css: {isRunning: isRunning()}">
-    <div class="appWidgetHeader">
-      <div class="row-fluid">
-        <div class="span11">
-          <h4>
-            <span data-bind="text: name"></span>
-            <img src="/static/art/spinner.gif" data-bind="visible: isRunning()" align="middle" style="margin-left: 14px"/>
-          </h4>
-          <p data-bind="text: description"></p>
-          <div data-bind="visible: output() != '' && output() != null" style="margin-bottom:6px">
-            <a data-bind="attr: {'href': output(), 'target': '_blank'}"><i class="icon-file"></i> ${ _('Output') }</a>
-          </div>
-        </div>
-        <div class="span1 shortcuts">
-          <a class="shortcut" data-bind="click: $root.manageApp, attr: {'title': tooltip()}" rel="tooltip">
-            <i data-bind="css: {'shortcutIcon': name != '', 'icon-play': status() == '' || status() == 'STOPPED' || status() == 'SUCCEEDED', 'icon-stop': status() == 'RUNNING'}"></i>
-          </a>
-        </div>
-      </div>
-    </div>
-    <div data-bind="visible: name != '', css: {'demoProgress': progress() != '', 'running': status() == 'RUNNING', 'succeeded': status() == 'SUCCEEDED', 'failed': status() == 'FAILED' || status() == 'STOPPED'}, attr: { style: progressCss }"></div>
-    <div class="appWidgetContent" data-bind="visible: status() != ''">
-      <div data-bind="foreach: actions">
-        <div data-bind="css: {'action': name != '' ,'running': status == 'RUNNING' || status == 'PREP', 'succeeded': status == 'OK', 'failed': status == 'FAILED' || $parent.status() == 'STOPPED'}">
-          <img src="/static/art/spinner.gif"
-               data-bind="visible: (status == 'RUNNING' || status == 'PREP') && $parent.isRunning()" class="pull-right"
-               style="margin-right: 10px;margin-top:10px"/>
-          <h6 data-bind="text: name"></h6>
-        </div>
-        ## scroll to top + go up or 'Log' tab?
-        <pre data-bind="text: logs, visible: logs.length > 0"></pre>
-      </div>
-    </div>
-  </div>
-</script>
-
-
-<link rel="stylesheet" href="/pig/static/css/pig.css">
-
-<script type="text/javascript" charset="utf-8">
-  $(document).ready(function () {
-    var LABELS = {
-      KILL_ERROR:"${ _('The pig job could not be killed.') }",
-      TOOLTIP_PLAY:"${ _('Run this pig script') }",
-      TOOLTIP_STOP:"${ _('Stop the execution') }",
-      SAVED:"${ _('Saved') }",
-    }
-    
-    var viewModel = new pigModel("${ url('pig:load_script') }", LABELS);
-    ko.applyBindings(viewModel);
-
-    viewModel.retrieveData();
-
-    $(document).bind("updateTooltips", function () {
-      $("a[rel=tooltip]").tooltip("destroy");
-      $("a[rel=tooltip]").tooltip();
-    });
-  
-    function showSection(section) {
-      $(".section").hide();
-      $("#" + section).show();
-      highlightMenu(section);
-    }
-       
-    function highlightMenu(section) {
-      $(".nav-list li").removeClass("active");
-      $("a[href='#" + section + "']").parent().addClass("active");
-    }       
-          
-    routie({
-      "editor":function () {
-        showSection("editor");
-      },
-      "edit-properties":function () {
-        showSection("edit-properties");
-      },
-      "save":function () {
-	    viewModel.save();
-      }      
-    });    
-  });
-</script>
-
-${ commonfooter(messages) | n,unicode }

+ 0 - 33
apps/pig/src/pig/templates/navigation-bar.inc.mako

@@ -1,33 +0,0 @@
-## Licensed to Cloudera, Inc. under one
-## or more contributor license agreements.  See the NOTICE file
-## distributed with this work for additional information
-## regarding copyright ownership.  Cloudera, Inc. licenses this file
-## to you under the Apache License, Version 2.0 (the
-## "License"); you may not use this file except in compliance
-## with the License.  You may obtain a copy of the License at
-##
-##     http://www.apache.org/licenses/LICENSE-2.0
-##
-## Unless required by applicable law or agreed to in writing, software
-## distributed under the License is distributed on an "AS IS" BASIS,
-## WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-## See the License for the specific language governing permissions and
-## limitations under the License.
-
-<%!
-  from django.utils.translation import ugettext as _
-%>
-
-<%namespace name="utils" file="utils.inc.mako" />
-
-
-<%def name="menubar(section='')">
-  <div class="subnav subnav-fixed">
-    <ul class="nav nav nav-pills">
-      <li class="${utils.is_selected(section, 'editor')}"><a href="${ url('pig:editor') }">${ _('Editor') }</a></li>
-      <li class="${utils.is_selected(section, 'scripts')}"><a href="${ url('pig:scripts') }">${ _('Scripts') }</a></li>
-      <li class="${utils.is_selected(section, 'dashboard')}"><a href="${ url('pig:dashboard') }">${ _('Dashboard') }</a></li>
-      ##<li class="${utils.is_selected(section, 'udfs')}"><a href="${ url('pig:udfs') }">${ _('UDF') }</a></li>
-    </ul>
-  </div>
-</%def>

+ 0 - 50
apps/pig/src/pig/templates/scripts.mako

@@ -1,50 +0,0 @@
-## Licensed to Cloudera, Inc. under one
-## or more contributor license agreements.  See the NOTICE file
-## distributed with this work for additional information
-## regarding copyright ownership.  Cloudera, Inc. licenses this file
-## to you under the Apache License, Version 2.0 (the
-## "License"); you may not use this file except in compliance
-## with the License.  You may obtain a copy of the License at
-##
-##     http://www.apache.org/licenses/LICENSE-2.0
-##
-## Unless required by applicable law or agreed to in writing, software
-## distributed under the License is distributed on an "AS IS" BASIS,
-## WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-## See the License for the specific language governing permissions and
-## limitations under the License.
-<%!
-from desktop.views import commonheader, commonfooter
-from django.utils.translation import ugettext as _
-%>
-
-<%namespace name="navigation" file="navigation-bar.inc.mako" />
-<%namespace name="utils" file="utils.inc.mako" />
-
-${ commonheader(_('Pig'), "pig", user, "100px") | n,unicode }
-
-
-<div class="container-fluid">
-   ${ navigation.menubar(section='scripts') }
-  
-    <div class="tab-content">
-      <div class="tab-pane active">
-
-		<div class="container-fluid">
-		    <div class="row-fluid">    
-		      Scripts
-		      </br>
-               % for script in scripts:
-                 ${ script } </br>
-               % endfor
-		    </div>
-		</div>
-
-   
-      </div>
-
-    </div>
-  </div>
-</div>
-
-${ commonfooter(messages) | n,unicode }

+ 0 - 24
apps/pig/src/pig/templates/utils.inc.mako

@@ -1,24 +0,0 @@
-## Licensed to Cloudera, Inc. under one
-## or more contributor license agreements.  See the NOTICE file
-## distributed with this work for additional information
-## regarding copyright ownership.  Cloudera, Inc. licenses this file
-## to you under the Apache License, Version 2.0 (the
-## "License"); you may not use this file except in compliance
-## with the License.  You may obtain a copy of the License at
-##
-##     http://www.apache.org/licenses/LICENSE-2.0
-##
-## Unless required by applicable law or agreed to in writing, software
-## distributed under the License is distributed on an "AS IS" BASIS,
-## WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-## See the License for the specific language governing permissions and
-## limitations under the License.
-
-<%def name="is_selected(section, matcher)">
-  <%
-    if section == matcher:
-      return "active"
-    else:
-      return ""
-  %>
-</%def>

+ 9 - 8
apps/pig/src/pig/urls.py

@@ -18,16 +18,17 @@
 from django.conf.urls.defaults import patterns, url
 
 urlpatterns = patterns('pig.views',
-  url(r'^$', 'editor', name='index'),
+  url(r'^$', 'app', name='index'),
 
-  url(r'^editor/$', 'editor', name='editor'),
-  url(r'^dashboard/$', 'dashboard', name='dashboard'),
-  url(r'^scripts/$', 'scripts', name='scripts'),
+  url(r'^app/$', 'app', name='app'),
   url(r'^udfs/$', 'udfs', name='udfs'),
 
   # Ajax
-  url(r'^load_script/(?P<doc_id>\d+)?', 'load_script', name='load_script'),
-  url(r'^save(?P<doc_id>[\w\.]+)', 'save', name='save'),
-  url(r'^submit/(?P<doc_id>[\w\.]+)$', 'submit', name='submit'),
-  url(r'^watch/(?P<doc_id>[\w\.]+)/(?P<job_id>[-\w]+)$', 'watch', name='watch')
+  url(r'^scripts/$', 'scripts', name='scripts'),
+  url(r'^dashboard/$', 'dashboard', name='dashboard'),
+  url(r'^save/$', 'save', name='save'),
+  url(r'^run/$', 'run', name='run'),
+  url(r'^copy/$', 'copy', name='copy'),
+  url(r'^delete/$', 'delete', name='delete'),
+  url(r'^watch/(?P<job_id>[-\w]+)$', 'watch', name='watch'),
 )

+ 134 - 59
apps/pig/src/pig/views.py

@@ -20,14 +20,16 @@ try:
 except ImportError:
   import simplejson as json
 import logging
+import time
 
 from django.core.urlresolvers import reverse
 from django.utils.translation import ugettext as _
 from django.views.decorators.http import require_http_methods
 from django.http import HttpResponse
 
-from desktop.lib.django_util import render
+from desktop.lib.django_util import render, encode_json_for_js
 from desktop.lib.exceptions_renderable import PopupException
+from desktop.lib.view_util import format_duration_in_millis
 from liboozie.oozie_api import get_oozie
 from oozie.views.dashboard import show_oozie_error, check_job_access_permission
 
@@ -37,13 +39,29 @@ from pig.models import get_workflow_output, hdfs_link, PigScript
 
 LOG = logging.getLogger(__name__)
 
+def app(request):
+  return render('app.mako', request, {
+    'scripts': json.dumps(get_scripts())
+    }
+  )
+
+def scripts(request):
+  return HttpResponse(json.dumps(get_scripts()), mimetype="application/json")
+
+
+def get_scripts():
+  scripts = []
+
+  for script in PigScript.objects.filter(is_history=False):
+    data = json.loads(script.data)
+    massaged_script = {
+      'id': script.id,
+      'name': data["name"],
+      'script': data["script"]
+    }
+    scripts.append(massaged_script)
 
-def editor(request):
-  pig_script_json = {}
-  return render('editor.mako', request, {
-                  'pig_script_json': json.dumps(pig_script_json)
-                  }
-                )
+  return scripts
 
 @show_oozie_error
 def dashboard(request):
@@ -51,57 +69,21 @@ def dashboard(request):
   kwargs['user'] = request.user.username
   kwargs['name'] = Api.WORKFLOW_NAME
 
-  workflows = get_oozie().get_workflows(**kwargs)
-    
-  return render('dashboard.mako', request, {
-                    'workflows': workflows
-                  }
-                )
-
-def scripts(request):
-  
-  return render('scripts.mako', request, {
-                    'scripts': PigScript.objects.filter(is_history=False)
-                  }
-                )
+  jobs = get_oozie().get_workflows(**kwargs).jobs
+  return HttpResponse(json.dumps(massaged_oozie_jobs_for_json(jobs, request.user)), mimetype="application/json")
 
 
 def udfs(request):
-  
-  return render('udfs.mako', request, {
-                  }
-                )
-
-
-def load_script(request, doc_id):
-
-  ko_repo = {
-    'id': doc_id,
-    'name': 'Run',
-    'description': 'Outputs and MR jobs',
-    'submitUrl': reverse('pig:submit', args=[doc_id]),
-    'saveUrl': reverse('pig:save', args=[doc_id])
-    }
-
-  response = {
-    'apps': [ko_repo],
-    'repo_id': 1,
-  }
-
-  return HttpResponse(json.dumps(response), content_type="text/plain")
+  return render('udfs.mako', request, {})
 
 
 @require_http_methods(["POST"])
-def save(request, doc_id=None):
+def save(request):
   # TODO security
-  if doc_id is None:
-    script = request.POST.get('script')
-    pig_script = PigScript.objects.create(owner=request.user)
-  pig_script.update_from_dict({'script': script})
-  pig_script.save()
+  pig_script = create_or_update_script(request.POST.get('id'), request.POST.get('name'), request.POST.get('script'), request.user)
   
   response = {
-    'doc_id': pig_script.id,
+    'id': pig_script.id,
   }
 
   return HttpResponse(json.dumps(response), content_type="text/plain")
@@ -109,13 +91,10 @@ def save(request, doc_id=None):
 
 @require_http_methods(["POST"])
 @show_oozie_error
-def submit(request, doc_id):
+def run(request):
   # TODO security
-  script = request.POST.get('script')
-  pig_script = PigScript.objects.create(owner=request.user, is_history=True)
-  pig_script.update_from_dict({'script': script})
-  pig_script.save()
-  
+  pig_script = create_or_update_script(request.POST.get('id'), request.POST.get('name'), request.POST.get('script'), request.user)
+
   # Todo, will come from script properties later
   mapping = {
     'oozie.use.system.libpath':  'true',
@@ -124,16 +103,63 @@ def submit(request, doc_id):
   oozie_id = Api(request.fs, request.user).submit(pig_script, mapping)
 
   response = {
-    'demoId': doc_id,
-    'jobId': pig_script.id,
-    'watchUrl': reverse('pig:watch', kwargs={'doc_id': doc_id, 'job_id': oozie_id}) + '?format=python'
+    'id': pig_script.id,
+    'watchUrl': reverse('pig:watch', kwargs={'job_id': oozie_id}) + '?format=python'
   }
 
   return HttpResponse(json.dumps(response), content_type="text/plain")
 
+@require_http_methods(["POST"])
+def copy(request):
+  # TODO security
+  existing_script_data = json.loads((PigScript.objects.get(id=request.POST.get('id'))).data)
+  name = existing_script_data["name"] + _(' (Copy)')
+  script = existing_script_data["script"]
+
+  pig_script = PigScript.objects.create(owner=request.user)
+  pig_script.update_from_dict({'name': name, 'script': script})
+  pig_script.save()
+
+  response = {
+    'id': pig_script.id,
+    'name': name,
+    'script': script
+    }
+
+  return HttpResponse(json.dumps(response), content_type="text/plain")
+
+@require_http_methods(["POST"])
+def delete(request):
+  # TODO security
+  ids = request.POST.get('ids').split(",")
+
+  for script_id in ids:
+    try:
+      pig_script = PigScript.objects.get(id=script_id)
+      pig_script.delete()
+    except:
+      None
+
+  response = {
+    'ids': ids,
+  }
+
+  return HttpResponse(json.dumps(response), content_type="text/plain")
+
+
+def create_or_update_script(id, name, script, user):
+  try:
+    pig_script = PigScript.objects.get(id=id)
+  except:
+    pig_script = PigScript.objects.create(owner=user)
+
+  pig_script.update_from_dict({'name': name, 'script': script})
+  pig_script.save()
+
+  return pig_script
 
 @show_oozie_error
-def watch(request, doc_id, job_id):
+def watch(request, job_id):
   oozie_workflow = check_job_access_permission(request, job_id) 
   logs, workflow_actions = Api(request, job_id).get_log(request, oozie_workflow)
   output = get_workflow_output(oozie_workflow, request.fs)
@@ -156,3 +182,52 @@ def watch(request, doc_id, job_id):
   
   return HttpResponse(json.dumps(response), content_type="text/plain")
 
+
+def format_time(st_time):
+  if st_time is None:
+    return '-'
+  else:
+    return time.strftime("%a, %d %b %Y %H:%M:%S", st_time)
+
+def has_job_edition_permission(oozie_job, user):
+  return user.is_superuser or oozie_job.user == user.username
+
+def has_dashboard_jobs_access(user):
+  return user.is_superuser or user.has_hue_permission(action="dashboard_jobs_access", app=DJANGO_APPS[0])
+
+def massaged_oozie_jobs_for_json(oozie_jobs, user):
+  jobs = []
+
+  for job in oozie_jobs:
+    if job.is_running():
+      if job.type == 'Workflow':
+        job = get_oozie().get_job(job.id)
+      elif job.type == 'Coordinator':
+        job = get_oozie().get_coordinator(job.id)
+      else:
+        job = get_oozie().get_bundle(job.id)
+
+    massaged_job = {
+      'id': job.id,
+      'lastModTime': hasattr(job, 'lastModTime') and job.lastModTime and format_time(job.lastModTime) or None,
+      'kickoffTime': hasattr(job, 'kickoffTime') and job.kickoffTime or None,
+      'timeOut': hasattr(job, 'timeOut') and job.timeOut or None,
+      'endTime': job.endTime and format_time(job.endTime) or None,
+      'status': job.status,
+      'isRunning': job.is_running(),
+      'duration': job.endTime and job.startTime and format_duration_in_millis(( time.mktime(job.endTime) - time.mktime(job.startTime) ) * 1000) or None,
+      'appName': job.appName,
+      'progress': job.get_progress(),
+      'user': job.user,
+      'absoluteUrl': job.get_absolute_url(),
+      'canEdit': has_job_edition_permission(job, user),
+      'killUrl': reverse('oozie:manage_oozie_jobs', kwargs={'job_id':job.id, 'action':'kill'}),
+      'created': hasattr(job, 'createdTime') and job.createdTime and job.createdTime and ((job.type == 'Bundle' and job.createdTime) or format_time(job.createdTime)),
+      'startTime': hasattr(job, 'startTime') and format_time(job.startTime) or None,
+      'run': hasattr(job, 'run') and job.run or 0,
+      'frequency': hasattr(job, 'frequency') and job.frequency or None,
+      'timeUnit': hasattr(job, 'timeUnit') and job.timeUnit or None,
+      }
+    jobs.append(massaged_job)
+
+  return jobs

+ 0 - 0
apps/pig/static/art/icon_pig_24.ico → apps/pig/static/art/icon_pig_24.png


+ 8 - 100
apps/pig/static/css/pig.css

@@ -1,98 +1,3 @@
-body {
-  padding-top: 60px;
-}
-
-.appWidget {
-  margin: 10px auto 5px auto;
-  background-color: #FFFFFF;
-}
-
-.appWidgetHeader {
-  background: #FAFAFA;
-  border: 1px solid #D5D5D5;
-  -webkit-border-top-left-radius: 4px;
-  -webkit-border-top-right-radius: 4px;
-  -moz-border-radius-topleft: 4px;
-  -moz-border-radius-topright: 4px;
-  border-top-left-radius: 4px;
-  border-top-right-radius: 4px;
-  -webkit-background-clip: padding-box;
-  padding-left: 10px;
-}
-
-.appWidget:nth-child(even) .appWidgetHeader{
-  background: #F0F0F0;
-}
-
-.appWidgetHeader:hover, .appWidget:nth-child(even) .appWidgetHeader:hover {
-  background: #E8F1FB;
-}
-
-.appWidgetHeader p {
-  color: #333;
-}
-
-.appWidgetContent {
-  border: 1px solid #D5D5D5;
-  border-top: none;
-  -webkit-border-bottom-left-radius: 4px;
-  -webkit-border-bottom-right-radius: 4px;
-  -moz-border-radius-bottomleft: 4px;
-  -moz-border-radius-bottomright: 4px;
-  border-bottom-left-radius: 4px;
-  border-bottom-right-radius: 4px;
-  padding: 10px;
-  max-height: 340px;
-  overflow-y: scroll;
-}
-
-.appWidgetContent h6 {
-  padding: 10px;
-  margin: 0;
-}
-
-.appWidgetContent pre {
-  border:0;
-}
-
-.shortcuts {
-  text-align: center;
-}
-
-.shortcuts .shortcut {
-  width: 80px;
-  display: inline-block;
-  padding: 12px 0;
-  margin: 1em .9% 1em;
-  vertical-align: top;
-  text-decoration: none;
-  border-radius: 5px;
-  cursor: pointer;
-}
-
-.shortcuts .shortcut .shortcutIcon {
-  margin-right: .25em;
-  margin-bottom: .25em;
-  font-size: 60px;
-  color: #888;
-}
-
-.shortcuts .shortcut .shortcutIcon:hover {
-  color: #333;
-}
-
-.availableDemos {
-  color: #888;
-  padding-left: 6px;
-  font-size: 12px;
-  text-align: left;
-  margin-top: 14px;
-}
-
-.demoProgress {
-  height: 2px;
-}
-
 .running {
   background-color: #faa732;
   border-left: 2px solid #faa732;
@@ -108,10 +13,13 @@ body {
   border-left: 2px solid #dd514c;
 }
 
-.action {
-  background: #F5F5F5;
+
+.sidebar-nav {
+  padding: 9px 0;
 }
 
-.tab-content, .tab-pane {
-  overflow-x: hidden;
-}
+.CodeMirror {border: 1px solid #eee;}
+
+.bottomAlert {
+  position: fixed; bottom: 0; left: 0;margin: 0;width: 100%;text-align: center;display:none;
+}

+ 243 - 145
apps/pig/static/js/pig.ko.js

@@ -15,180 +15,278 @@
 // limitations under the License.
 
 
-var App = function (app) {
+var PigScript = function (pigScript) {
   return {
-    id:app.id,
-    name:app.name,
-    archive:app.archive,
-    submitUrl:app.submitUrl,
-    watchUrl:app.watchUrl,
-    saveUrl:app.saveUrl,
-    killUrl:"",
-    rerunUrl:"",
-    description:app.description,
-    isRunning:ko.observable(false),
-    status:ko.observable(""),
-    progress:ko.observable(""),
-    progressCss:ko.observable("width: 0%"),
-    intervalId:-1,
-    actions:ko.observableArray(),
-    output:ko.observable(""),
-    tooltip:ko.observable("")
+    id: ko.observable(pigScript.id),
+    name: ko.observable(pigScript.name),
+    script: ko.observable(pigScript.script),
+    isRunning: ko.observable(false),
+    selected: ko.observable(false),
+    handleSelect: function (row, e) {
+      this.selected(!this.selected());
+    },
+    hovered: ko.observable(false),
+    toggleHover: function (row, e) {
+      this.hovered(!this.hovered());
+    }
+  }
+}
+
+var Workflow = function (wf) {
+  return {
+    id: wf.id,
+    lastModTime: wf.lastModTime,
+    endTime: wf.endTime,
+    status: wf.status,
+    statusClass: "label " + getStatusClass(wf.status),
+    isRunning: wf.isRunning,
+    duration: wf.duration,
+    appName: wf.appName,
+    progress: wf.progress,
+    progressClass: "bar " + getStatusClass(wf.status, "bar-"),
+    user: wf.user,
+    absoluteUrl: wf.absoluteUrl,
+    canEdit: wf.canEdit,
+    killUrl: wf.killUrl,
+    created: wf.created,
+    run: wf.run
   }
 }
 
-var pigModel = function (serviceUrl, labels) {
+
+var PigViewModel = function (scripts, props) {
   var self = this;
 
-  self.serviceUrl = ko.observable(serviceUrl);
-  self.LABELS = labels;
-  self.apps = ko.observableArray();
-  self.history = ko.observableArray();
-  self.isLoading = ko.observable(true);
+  self.LABELS = props.labels;
 
-  self.retrieveData = function () {
-    self.isLoading(true);
+  self.LIST_SCRIPTS = props.listScripts;
+  self.SAVE_URL = props.saveUrl;
+  self.RUN_URL = props.runUrl;
+  self.COPY_URL = props.copyUrl;
+  self.DELETE_URL = props.deleteUrl;
 
-    $.getJSON(self.serviceUrl() + self.getDocId(), function (data) {
-      self.apps(ko.utils.arrayMap(data.apps, function (app) {
-        return getInitApp(app);
-      }));
-      self.isLoading(false);
-      if ($){
-        $(document).trigger("updateTooltips");
-      }
+  self.isLoading = ko.observable(false);
+  self.allSelected = ko.observable(false);
+
+  self.scripts = ko.observableArray(ko.utils.arrayMap(scripts, function (pigScript) {
+    return new PigScript(pigScript);
+  }));
+
+  self.filteredScripts = ko.observableArray(self.scripts());
+
+  self.runningScripts = ko.observableArray([]);
+  self.completedScripts = ko.observableArray([]);
+
+  var _defaultScript = {
+    id: -1,
+    name: self.LABELS.NEW_SCRIPT_NAME,
+    script: self.LABELS.NEW_SCRIPT_CONTENT
+  };
+
+  self.currentScript = ko.observable(new PigScript(_defaultScript));
+  self.currentDeleteType = ko.observable("");
+
+  self.selectedScripts = ko.computed(function () {
+    return ko.utils.arrayFilter(self.scripts(), function (script) {
+      return script.selected();
     });
+  }, self);
 
-    function getInitApp(app) {
-      var a = new App(app);
-      a.tooltip(self.LABELS.TOOLTIP_PLAY);
-      return a;
-    }
+  self.selectedScript = ko.computed(function () {
+    return self.selectedScripts()[0];
+  }, self);
+
+  self.selectAll = function () {
+    self.allSelected(!self.allSelected());
+    ko.utils.arrayForEach(self.scripts(), function (script) {
+      script.selected(self.allSelected());
+    });
+    return true;
   };
-  
-  self.getDocId = function() {
-    var hash = window.location.hash;
-    var jobId = ""
-    if (hash != null && hash.indexOf("/") > -1) {
-      jobId = hash.substring(hash.indexOf("/") + 1);
-    }
-    return jobId;
+
+  self.getScriptById = function (id) {
+    var _s = null;
+    ko.utils.arrayForEach(self.scripts(), function (script) {
+      if (script.id() == id) {
+        _s = script;
+      }
+    });
+    return _s;
   }
 
-  self.manageApp = function (app) {
-    if (app.status() == "" || app.status() == "FAILED" || app.status() == "STOPPED") {
-      self.submitApp(app);
-    }
-    if (app.isRunning()) {
-      self.killApp(app);
+  self.filterScripts = function (filter) {
+    self.filteredScripts(ko.utils.arrayFilter(self.scripts(), function (script) {
+      return script.name().toLowerCase().indexOf(filter.toLowerCase()) > -1
+    }));
+  };
+
+  self.loadScript = function (id) {
+    var _s = self.getScriptById(id);
+    if (_s != null) {
+      self.currentScript(_s);
     }
-    if (app.status() == "SUCCEEDED") {
-      self.rerunApp(app);
+    else {
+      self.currentScript(new PigScript(_defaultScript));
     }
+  }
+
+  self.newScript = function () {
+    self.currentScript(new PigScript(_defaultScript));
+    $(document).trigger("loadEditor");
+    $(document).trigger("showEditor");
   };
 
-  self.submitApp = function (app) {
-    $.post(app.submitUrl, $("#queryForm").serialize(), function (data) {
-      window.location.hash = "#/" + data.jobId;
-      app.watchUrl = data.watchUrl;
-      self.updateAppStatus(app);
-      app.intervalId = window.setInterval(function () {
-        self.updateAppStatus(app);
-      }, 1000);
-    }, "json");
+  self.editScript = function (script) {
+    $(document).trigger("showEditor");
   };
 
-  self.save = function (app) {
-    $.post(app.submitUrl, $("#queryForm").serialize(), function (data) {
-      if (self.getDocId() == "") {
-    	  window.location.hash += "/" + data.doc_id;  
-      }
-      $.jHueNotify.info(self.LABELS.SAVED);
-    }, "json");
-  };
-  
-  self.killApp = function (app) {
-    app.isRunning(false);
-    app.status("STOPPED");
-    app.tooltip(self.LABELS.TOOLTIP_PLAY);
-    if (app.intervalId > -1) {
-      window.clearInterval(app.intervalId);
-      self.appendToHistory(app);
-    }
-    if ($) {
-      $(document).trigger("updateTooltips");
+  self.editScriptProperties = function (script) {
+    $(document).trigger("showProperties");
+  };
+
+  self.viewScript = function (script) {
+    self.currentScript(script);
+    $(document).trigger("loadEditor");
+    $(document).trigger("showEditor");
+  };
+
+  self.saveScript = function () {
+    callSave(self.currentScript());
+  };
+
+  self.runScript = function () {
+    callRun(self.currentScript());
+  };
+
+  self.copyScript = function () {
+    callCopy(self.currentScript());
+  };
+
+  self.confirmDeleteScript = function () {
+    self.currentDeleteType("single");
+    showDeleteModal();
+  };
+
+  self.listRunScript = function () {
+    callRun(self.selectedScript());
+  };
+
+  self.listCopyScript = function () {
+    callCopy(self.selectedScript());
+  };
+
+  self.listConfirmDeleteScripts = function () {
+    self.currentDeleteType("multiple");
+    showDeleteModal();
+  };
+
+  self.deleteScripts = function () {
+    var ids = [];
+    if (self.currentDeleteType() == "single") {
+      ids.push(self.currentScript().id());
     }
-    if (app.killUrl) {
-      $.post(app.killUrl,function (data) {
-        if (data.status != 0) {
-          $.jHueNotify.error(data.data);
-        }
-      }, "json").error(function () {
-        $.jHueNotify.error(self.LABELS.KILL_ERROR);
+    if (self.currentDeleteType() == "multiple") {
+      $(self.selectedScripts()).each(function (index, script) {
+        ids.push(script.id());
       });
     }
+    callDelete(ids);
+  };
 
+  self.updateScripts = function () {
+    $.getJSON(self.LIST_SCRIPTS, function (data) {
+      self.scripts(ko.utils.arrayMap(data, function (script) {
+        return new PigScript(script);
+      }));
+      self.filteredScripts(self.scripts());
+      $(document).trigger("scriptsRefreshed");
+    });
   };
 
-  self.rerunApp = function (app) {
-    if (app.intervalId > -1) {
-      window.clearInterval(app.intervalId);
+  self.updateDashboard = function (workflows) {
+    var koWorkflows = ko.utils.arrayMap(workflows, function (wf) {
+      return new Workflow(wf);
+    });
+    self.runningScripts(ko.utils.arrayFilter(koWorkflows, function (wf) {
+      return wf.isRunning
+    }));
+    self.completedScripts(ko.utils.arrayFilter(koWorkflows, function (wf) {
+      return !wf.isRunning
+    }));
+  }
+
+  function showDeleteModal() {
+    $(".deleteMsg").addClass("hide");
+    if (self.currentDeleteType() == "single") {
+      $(".deleteMsg.single").removeClass("hide");
     }
-    self.submitApp(app);
-  };
-
-  self.updateAppStatus = function (app) {
-    $.getJSON(app.watchUrl, function (watch) {
-      var previousTooltip = app.tooltip();
-      app.tooltip(self.LABELS.TOOLTIP_STOP);
-      app.isRunning(watch.workflow.isRunning);
-      app.status(watch.workflow.status);
-      app.progress(watch.workflow.progress);
-      app.progressCss("width: " + app.progress() + "%");
-      app.actions(watch.workflow.actions);
-      app.killUrl = watch.workflow.killUrl;
-      app.rerunUrl = watch.workflow.rerunUrl;
-      app.output(watch.output);
-      if (!watch.workflow.isRunning) {
-        window.clearInterval(app.intervalId);
-        app.tooltip(self.LABELS.TOOLTIP_PLAY);
-        self.appendToHistory(app);
+    if (self.currentDeleteType() == "multiple") {
+      if (self.selectedScripts().length > 1) {
+        $(".deleteMsg.multiple").removeClass("hide");
       }
-      if (app.tooltip() != previousTooltip) {
-        if ($) {
-          $(document).trigger("updateTooltips");
-        }
+      else {
+        $(".deleteMsg.single").removeClass("hide");
       }
+    }
+    $("#deleteModal").modal({
+      keyboard: true,
+      show: true
     });
-  };
+  }
 
-  self.resetAppStatus = function (app) {
-    app.isRunning(false);
-    app.status("");
-  };
-
-  self.appendToHistory = function (app) {
-    var newApp = {
-      id:app.id,
-      name:app.name,
-      when:moment().format('MM/DD/YYYY, h:mm:ss a'),
-      archive:app.archive,
-      submitUrl:app.submitUrl,
-      watchUrl:app.watchUrl,
-      killUrl:app.killUrl,
-      rerunUrl:app.rerunUrl,
-      description:app.description,
-      isRunning:ko.observable(app.isRunning()),
-      status:ko.observable(app.status()),
-      progress:ko.observable(app.progress()),
-      progressCss:ko.observable(app.progressCss()),
-      intervalId:app.intervalId,
-      actions:ko.observableArray(app.actions()),
-      output:ko.observable(app.output())
-    };
-    var history = self.history();
-    history.push(newApp);
-    self.history(history);
-  };
+  function callSave(script) {
+    $(document).trigger("saving");
+    $.post(self.SAVE_URL,
+        {
+          id: script.id(),
+          name: script.name(),
+          script: script.script()
+        },
+        function (data) {
+          self.currentScript().id(data.id);
+          $(document).trigger("saved");
+          self.updateScripts();
+        }, "json");
+  }
+
+  function callRun(script) {
+    $(document).trigger("running");
+    $.post(self.RUN_URL,
+        {
+          id: script.id(),
+          name: script.name(),
+          script: script.script()
+        },
+        function (data) {
+          $(document).trigger("showDashboard");
+          self.updateScripts();
+        }, "json");
+  }
 
+  function callCopy(script) {
+    $.post(self.COPY_URL,
+        {
+          id: script.id()
+        },
+        function (data) {
+          self.currentScript(new PigScript(data));
+          $(document).trigger("loadEditor");
+          self.updateScripts();
+        }, "json");
+  }
+
+  function callDelete(ids) {
+    if (ids.indexOf(self.currentScript().id()) > -1) {
+      self.currentScript(new PigScript(_defaultScript));
+      $(document).trigger("loadEditor");
+    }
+    $.post(self.DELETE_URL,
+        {
+          ids: ids.join(",")
+        },
+        function (data) {
+          self.updateScripts();
+          $("#deleteModal").modal("hide");
+        }, "json");
+  }
 };

+ 49 - 0
apps/pig/static/js/utils.js

@@ -0,0 +1,49 @@
+// 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.
+
+if (!Array.prototype.indexOf) {
+  Array.prototype.indexOf = function (needle) {
+    for (var i = 0; i < this.length; i++) {
+      if (this[i] === needle) {
+        return i;
+      }
+    }
+    return -1;
+  };
+}
+
+function getStatusClass(status, prefix) {
+  if (prefix == null) {
+    prefix = "label-";
+  }
+  var klass = "";
+  if (['SUCCEEDED', 'OK'].indexOf(status) > -1) {
+    klass = prefix + "success";
+  }
+  else if (['RUNNING', 'READY', 'PREP', 'WAITING', 'SUSPENDED', 'PREPSUSPENDED', 'PREPPAUSED', 'PAUSED',
+    'SUBMITTED',
+    'SUSPENDEDWITHERROR',
+    'PAUSEDWITHERROR'].indexOf(status) > -1) {
+    klass = prefix + "warning";
+  }
+  else {
+    klass = prefix + "important";
+    if (prefix == "bar-") {
+      klass = prefix + "danger";
+    }
+  }
+  return klass;
+}

+ 38 - 0
desktop/core/static/ext/css/codemirror-show-hint.css

@@ -0,0 +1,38 @@
+.CodeMirror-hints {
+  position: absolute;
+  z-index: 10;
+  overflow: hidden;
+  list-style: none;
+
+  margin: 0;
+  padding: 2px;
+
+  -webkit-box-shadow: 2px 3px 5px rgba(0,0,0,.2);
+  -moz-box-shadow: 2px 3px 5px rgba(0,0,0,.2);
+  box-shadow: 2px 3px 5px rgba(0,0,0,.2);
+  border-radius: 3px;
+  border: 1px solid silver;
+
+  background: white;
+  font-size: 90%;
+  font-family: monospace;
+
+  max-height: 20em;
+  overflow-y: auto;
+}
+
+.CodeMirror-hint {
+  margin: 0;
+  padding: 0 4px;
+  border-radius: 2px;
+  max-width: 19em;
+  overflow: hidden;
+  white-space: pre;
+  color: black;
+  cursor: pointer;
+}
+
+.CodeMirror-hint-active {
+  background: #08f;
+  color: white;
+}

+ 117 - 0
desktop/core/static/ext/js/codemirror-pig-hint.js

@@ -0,0 +1,117 @@
+(function () {
+  function forEach(arr, f) {
+    for (var i = 0, e = arr.length; i < e; ++i) f(arr[i]);
+  }
+  
+  function arrayContains(arr, item) {
+    if (!Array.prototype.indexOf) {
+      var i = arr.length;
+      while (i--) {
+        if (arr[i] === item) {
+          return true;
+        }
+      }
+      return false;
+    }
+    return arr.indexOf(item) != -1;
+  }
+
+  function scriptHint(editor, _keywords, getToken) {
+    // Find the token at the cursor
+    var cur = editor.getCursor(), token = getToken(editor, cur), tprop = token;
+    // If it's not a 'word-style' token, ignore the token.
+
+    if (!/^[\w$_]*$/.test(token.string)) {
+        token = tprop = {start: cur.ch, end: cur.ch, string: "", state: token.state,
+                         className: token.string == ":" ? "pig-type" : null};
+    }
+      
+    if (!context) var context = [];
+    context.push(tprop);
+    
+    var completionList = getCompletions(token, context); 
+    completionList = completionList.sort();
+    //prevent autocomplete for last word, instead show dropdown with one word
+    if(completionList.length == 1) {
+      completionList.push(" ");
+    }
+
+    return {list: completionList,
+            from: CodeMirror.Pos(cur.line, token.start),
+            to: CodeMirror.Pos(cur.line, token.end)};
+  }
+  
+  CodeMirror.pigHint = function(editor) {
+    return scriptHint(editor, pigKeywordsU, function (e, cur) {return e.getTokenAt(cur);});
+  };
+ 
+  var pigKeywords = "VOID IMPORT RETURNS DEFINE LOAD FILTER FOREACH ORDER CUBE DISTINCT COGROUP "
+  + "JOIN CROSS UNION SPLIT INTO IF OTHERWISE ALL AS BY USING INNER OUTER ONSCHEMA PARALLEL "
+  + "PARTITION GROUP AND OR NOT GENERATE FLATTEN ASC DESC IS STREAM THROUGH STORE MAPREDUCE "
+  + "SHIP CACHE INPUT OUTPUT STDERROR STDIN STDOUT LIMIT SAMPLE LEFT RIGHT FULL EQ GT LT GTE LTE " 
+  + "NEQ MATCHES TRUE FALSE";
+  var pigKeywordsU = pigKeywords.split(" ");
+  var pigKeywordsL = pigKeywords.toLowerCase().split(" ");
+  
+  var pigTypes = "BOOLEAN INT LONG FLOAT DOUBLE CHARARRAY BYTEARRAY BAG TUPLE MAP";
+  var pigTypesU = pigTypes.split(" ");
+  var pigTypesL = pigTypes.toLowerCase().split(" ");
+  
+  var pigBuiltins = "ABS ACOS ARITY ASIN ATAN AVG BAGSIZE BINSTORAGE BLOOM BUILDBLOOM CBRT CEIL " 
+  + "CONCAT COR COS COSH COUNT COUNT_STAR COV CONSTANTSIZE CUBEDIMENSIONS DIFF DISTINCT DOUBLEABS "
+  + "DOUBLEAVG DOUBLEBASE DOUBLEMAX DOUBLEMIN DOUBLEROUND DOUBLESUM EXP FLOOR FLOATABS FLOATAVG "
+  + "FLOATMAX FLOATMIN FLOATROUND FLOATSUM GENERICINVOKER INDEXOF INTABS INTAVG INTMAX INTMIN "
+  + "INTSUM INVOKEFORDOUBLE INVOKEFORFLOAT INVOKEFORINT INVOKEFORLONG INVOKEFORSTRING INVOKER "
+  + "ISEMPTY JSONLOADER JSONMETADATA JSONSTORAGE LAST_INDEX_OF LCFIRST LOG LOG10 LOWER LONGABS "
+  + "LONGAVG LONGMAX LONGMIN LONGSUM MAX MIN MAPSIZE MONITOREDUDF NONDETERMINISTIC OUTPUTSCHEMA  "
+  + "PIGSTORAGE PIGSTREAMING RANDOM REGEX_EXTRACT REGEX_EXTRACT_ALL REPLACE ROUND SIN SINH SIZE "
+  + "SQRT STRSPLIT SUBSTRING SUM STRINGCONCAT STRINGMAX STRINGMIN STRINGSIZE TAN TANH TOBAG "
+  + "TOKENIZE TOMAP TOP TOTUPLE TRIM TEXTLOADER TUPLESIZE UCFIRST UPPER UTF8STORAGECONVERTER";  
+  var pigBuiltinsU = pigBuiltins.split(" ").join("() ").split(" ");  
+  var pigBuiltinsL = pigBuiltins.toLowerCase().split(" ").join("() ").split(" ");   
+  var pigBuiltinsC = ("BagSize BinStorage Bloom BuildBloom ConstantSize CubeDimensions DoubleAbs "
+  + "DoubleAvg DoubleBase DoubleMax DoubleMin DoubleRound DoubleSum FloatAbs FloatAvg FloatMax "
+  + "FloatMin FloatRound FloatSum GenericInvoker IntAbs IntAvg IntMax IntMin IntSum "
+  + "InvokeForDouble InvokeForFloat InvokeForInt InvokeForLong InvokeForString Invoker "
+  + "IsEmpty JsonLoader JsonMetadata JsonStorage LongAbs LongAvg LongMax LongMin LongSum MapSize "
+  + "MonitoredUDF Nondeterministic OutputSchema PigStorage PigStreaming StringConcat StringMax "
+  + "StringMin StringSize TextLoader TupleSize Utf8StorageConverter").split(" ").join("() ").split(" ");
+                    
+  function getCompletions(token, context) {
+    var found = [], start = token.string;
+    function maybeAdd(str) {
+      if (str.indexOf(start) == 0 && !arrayContains(found, str)) found.push(str);
+    }
+    
+    function gatherCompletions(obj) {
+      if(obj == ":") {
+        forEach(pigTypesL, maybeAdd);
+      }
+      else {
+        forEach(pigBuiltinsU, maybeAdd);
+        forEach(pigBuiltinsL, maybeAdd);
+        forEach(pigBuiltinsC, maybeAdd);
+        forEach(pigTypesU, maybeAdd);
+        forEach(pigTypesL, maybeAdd);
+        forEach(pigKeywordsU, maybeAdd);
+        forEach(pigKeywordsL, maybeAdd);
+      }
+    }
+
+    if (context) {
+      // If this is a property, see if it belongs to some object we can
+      // find in the current environment.
+      var obj = context.pop(), base;
+
+      if (obj.type == "variable") 
+          base = obj.string;
+      else if(obj.type == "variable-3")
+          base = ":" + obj.string;
+        
+      while (base != null && context.length)
+        base = base[context.pop().string];
+      if (base != null) gatherCompletions(base);
+    }
+    return found;
+  }
+})();

+ 171 - 0
desktop/core/static/ext/js/codemirror-pig.js

@@ -0,0 +1,171 @@
+/*
+ *	Pig Latin Mode for CodeMirror 2 
+ *	@author Prasanth Jayachandran
+ *	@link 	https://github.com/prasanthj/pig-codemirror-2
+ *  This implementation is adapted from PL/SQL mode in CodeMirror 2.
+*/
+CodeMirror.defineMode("pig", function(_config, parserConfig) {
+	var keywords = parserConfig.keywords,
+		builtins = parserConfig.builtins,
+		types = parserConfig.types,
+		multiLineStrings = parserConfig.multiLineStrings;
+	
+	var isOperatorChar = /[*+\-%<>=&?:\/!|]/;
+	
+	function chain(stream, state, f) {
+		state.tokenize = f;
+		return f(stream, state);
+	}
+	
+	var type;
+	function ret(tp, style) {
+		type = tp;
+		return style;
+	}
+	
+	function tokenComment(stream, state) {
+		var isEnd = false;
+		var ch;
+		while(ch = stream.next()) {
+			if(ch == "/" && isEnd) {
+				state.tokenize = tokenBase;
+				break;
+			}
+			isEnd = (ch == "*");
+		}
+		return ret("comment", "comment");
+	}
+	
+	function tokenString(quote) {
+		return function(stream, state) {
+			var escaped = false, next, end = false;
+			while((next = stream.next()) != null) {
+				if (next == quote && !escaped) {
+					end = true; break;
+				}
+				escaped = !escaped && next == "\\";
+			}
+			if (end || !(escaped || multiLineStrings))
+				state.tokenize = tokenBase;
+			return ret("string", "error");
+		};
+	}
+	
+	function tokenBase(stream, state) {
+		var ch = stream.next();
+		
+		// is a start of string?
+		if (ch == '"' || ch == "'")
+			return chain(stream, state, tokenString(ch));
+		// is it one of the special chars
+		else if(/[\[\]{}\(\),;\.]/.test(ch))
+			return ret(ch);
+		// is it a number?
+		else if(/\d/.test(ch)) {
+			stream.eatWhile(/[\w\.]/);
+			return ret("number", "number");
+		}
+		// multi line comment or operator
+		else if (ch == "/") {
+			if (stream.eat("*")) {
+				return chain(stream, state, tokenComment);
+			}
+			else {
+				stream.eatWhile(isOperatorChar);
+				return ret("operator", "operator");
+			}
+		}
+		// single line comment or operator
+		else if (ch=="-") {
+			if(stream.eat("-")){
+				stream.skipToEnd();
+				return ret("comment", "comment");
+			}
+			else {
+				stream.eatWhile(isOperatorChar);
+				return ret("operator", "operator");
+			}
+		}
+		// is it an operator
+		else if (isOperatorChar.test(ch)) {
+			stream.eatWhile(isOperatorChar);
+			return ret("operator", "operator");
+		}
+		else {
+			// get the while word
+			stream.eatWhile(/[\w\$_]/);
+			// is it one of the listed keywords?
+			if (keywords && keywords.propertyIsEnumerable(stream.current().toUpperCase())) {
+				if (stream.eat(")") || stream.eat(".")) {
+					//keywords can be used as variables like flatten(group), group.$0 etc..
+				}
+				else {
+					return ("keyword", "keyword");
+				}
+			}
+			// is it one of the builtin functions?
+			if (builtins && builtins.propertyIsEnumerable(stream.current().toUpperCase()))
+			{
+				return ("keyword", "variable-2");
+			}
+			// is it one of the listed types?
+			if (types && types.propertyIsEnumerable(stream.current().toUpperCase()))
+				return ("keyword", "variable-3");
+			// default is a 'variable'
+			return ret("variable", "pig-word");
+		}
+	}
+	
+	// Interface
+	return {
+		startState: function() {
+			return {
+				tokenize: tokenBase,
+				startOfLine: true
+			};
+		},
+		
+		token: function(stream, state) {
+			if(stream.eatSpace()) return null;
+			var style = state.tokenize(stream, state);
+			return style;
+		}
+	};
+});
+
+(function() {
+	function keywords(str) {
+		var obj = {}, words = str.split(" ");
+		for (var i = 0; i < words.length; ++i) obj[words[i]] = true;
+ 		return obj;
+ 	}
+
+	// builtin funcs taken from trunk revision 1303237
+	var pBuiltins = "ABS ACOS ARITY ASIN ATAN AVG BAGSIZE BINSTORAGE BLOOM BUILDBLOOM CBRT CEIL " 
+	+ "CONCAT COR COS COSH COUNT COUNT_STAR COV CONSTANTSIZE CUBEDIMENSIONS DIFF DISTINCT DOUBLEABS "
+	+ "DOUBLEAVG DOUBLEBASE DOUBLEMAX DOUBLEMIN DOUBLEROUND DOUBLESUM EXP FLOOR FLOATABS FLOATAVG "
+	+ "FLOATMAX FLOATMIN FLOATROUND FLOATSUM GENERICINVOKER INDEXOF INTABS INTAVG INTMAX INTMIN "
+	+ "INTSUM INVOKEFORDOUBLE INVOKEFORFLOAT INVOKEFORINT INVOKEFORLONG INVOKEFORSTRING INVOKER "
+	+ "ISEMPTY JSONLOADER JSONMETADATA JSONSTORAGE LAST_INDEX_OF LCFIRST LOG LOG10 LOWER LONGABS "
+	+ "LONGAVG LONGMAX LONGMIN LONGSUM MAX MIN MAPSIZE MONITOREDUDF NONDETERMINISTIC OUTPUTSCHEMA  "
+	+ "PIGSTORAGE PIGSTREAMING RANDOM REGEX_EXTRACT REGEX_EXTRACT_ALL REPLACE ROUND SIN SINH SIZE "
+	+ "SQRT STRSPLIT SUBSTRING SUM STRINGCONCAT STRINGMAX STRINGMIN STRINGSIZE TAN TANH TOBAG "
+	+ "TOKENIZE TOMAP TOP TOTUPLE TRIM TEXTLOADER TUPLESIZE UCFIRST UPPER UTF8STORAGECONVERTER "; 
+	
+	// taken from QueryLexer.g
+	var pKeywords = "VOID IMPORT RETURNS DEFINE LOAD FILTER FOREACH ORDER CUBE DISTINCT COGROUP "
+	+ "JOIN CROSS UNION SPLIT INTO IF OTHERWISE ALL AS BY USING INNER OUTER ONSCHEMA PARALLEL "
+	+ "PARTITION GROUP AND OR NOT GENERATE FLATTEN ASC DESC IS STREAM THROUGH STORE MAPREDUCE "
+	+ "SHIP CACHE INPUT OUTPUT STDERROR STDIN STDOUT LIMIT SAMPLE LEFT RIGHT FULL EQ GT LT GTE LTE " 
+	+ "NEQ MATCHES TRUE FALSE "; 
+	
+	// data types
+	var pTypes = "BOOLEAN INT LONG FLOAT DOUBLE CHARARRAY BYTEARRAY BAG TUPLE MAP ";
+	
+	CodeMirror.defineMIME("text/x-pig", {
+	 name: "pig",
+	 builtins: keywords(pBuiltins),
+	 keywords: keywords(pKeywords),
+	 types: keywords(pTypes)
+	 });
+}());

+ 155 - 0
desktop/core/static/ext/js/codemirror-show-hint.js

@@ -0,0 +1,155 @@
+CodeMirror.showHint = function(cm, getHints, options) {
+  if (!options) options = {};
+  var startCh = cm.getCursor().ch, continued = false;
+
+  function startHinting() {
+    // We want a single cursor position.
+    if (cm.somethingSelected()) return;
+
+    if (options.async)
+      getHints(cm, showHints, options);
+    else
+      return showHints(getHints(cm, options));
+  }
+
+  function showHints(data) {
+    if (!data || !data.list.length) return;
+    var completions = data.list;
+    // When there is only one completion, use it directly.
+    if (!continued && options.completeSingle !== false && completions.length == 1) {
+      cm.replaceRange(completions[0], data.from, data.to);
+      return true;
+    }
+
+    // Build the select widget
+    var hints = document.createElement("ul"), selectedHint = 0;
+    hints.className = "CodeMirror-hints";
+    for (var i = 0; i < completions.length; ++i) {
+      var elt = hints.appendChild(document.createElement("li"));
+      elt.className = "CodeMirror-hint" + (i ? "" : " CodeMirror-hint-active");
+      elt.appendChild(document.createTextNode(completions[i]));
+      elt.hintId = i;
+    }
+    var pos = cm.cursorCoords(options.alignWithWord !== false ? data.from : null);
+    var left = pos.left, top = pos.bottom, below = true;
+    hints.style.left = left + "px";
+    hints.style.top = top + "px";
+    document.body.appendChild(hints);
+
+    // If we're at the edge of the screen, then we want the menu to appear on the left of the cursor.
+    var winW = window.innerWidth || Math.max(document.body.offsetWidth, document.documentElement.offsetWidth);
+    var winH = window.innerHeight || Math.max(document.body.offsetHeight, document.documentElement.offsetHeight);
+    var box = hints.getBoundingClientRect();
+    var overlapX = box.right - winW, overlapY = box.bottom - winH;
+    if (overlapX > 0) {
+      if (box.right - box.left > winW) {
+        hints.style.width = (winW - 5) + "px";
+        overlapX -= (box.right - box.left) - winW;
+      }
+      hints.style.left = (left = pos.left - overlapX) + "px";
+    }
+    if (overlapY > 0) {
+      var height = box.bottom - box.top;
+      if (box.top - (pos.bottom - pos.top) - height > 0) {
+        overlapY = height + (pos.bottom - pos.top);
+        below = false;
+      } else if (height > winH) {
+        hints.style.height = (winH - 5) + "px";
+        overlapY -= height - winH;
+      }
+      hints.style.top = (top = pos.bottom - overlapY) + "px";
+    }
+
+    function changeActive(i) {
+      i = Math.max(0, Math.min(i, completions.length - 1));
+      if (selectedHint == i) return;
+      hints.childNodes[selectedHint].className = "CodeMirror-hint";
+      var node = hints.childNodes[selectedHint = i];
+      node.className = "CodeMirror-hint CodeMirror-hint-active";
+      if (node.offsetTop < hints.scrollTop)
+        hints.scrollTop = node.offsetTop - 3;
+      else if (node.offsetTop + node.offsetHeight > hints.scrollTop + hints.clientHeight)
+        hints.scrollTop = node.offsetTop + node.offsetHeight - hints.clientHeight + 3;
+    }
+
+    function screenAmount() {
+      return Math.floor(hints.clientHeight / hints.firstChild.offsetHeight) || 1;
+    }
+
+    var ourMap = {
+      Up: function() {changeActive(selectedHint - 1);},
+      Down: function() {changeActive(selectedHint + 1);},
+      PageUp: function() {changeActive(selectedHint - screenAmount());},
+      PageDown: function() {changeActive(selectedHint + screenAmount());},
+      Home: function() {changeActive(0);},
+      End: function() {changeActive(completions.length - 1);},
+      Enter: pick,
+      Tab: pick,
+      Esc: close
+    };
+    if (options.customKeys) for (var key in options.customKeys) if (options.customKeys.hasOwnProperty(key)) {
+      var val = options.customKeys[key];
+      if (/^(Up|Down|Enter|Esc)$/.test(key)) val = ourMap[val];
+      ourMap[key] = val;
+    }
+
+    cm.addKeyMap(ourMap);
+    cm.on("cursorActivity", cursorActivity);
+    var closingOnBlur;
+    function onBlur(){ closingOnBlur = setTimeout(close, 100); };
+    function onFocus(){ clearTimeout(closingOnBlur); };
+    cm.on("blur", onBlur);
+    cm.on("focus", onFocus);
+    var startScroll = cm.getScrollInfo();
+    function onScroll() {
+      var curScroll = cm.getScrollInfo(), editor = cm.getWrapperElement().getBoundingClientRect();
+      var newTop = top + startScroll.top - curScroll.top, point = newTop;
+      if (!below) point += hints.offsetHeight;
+      if (point <= editor.top || point >= editor.bottom) return close();
+      hints.style.top = newTop + "px";
+      hints.style.left = (left + startScroll.left - curScroll.left) + "px";
+    }
+    cm.on("scroll", onScroll);
+    CodeMirror.on(hints, "dblclick", function(e) {
+      var t = e.target || e.srcElement;
+      if (t.hintId != null) {selectedHint = t.hintId; pick();}
+      setTimeout(function(){cm.focus();}, 20);
+    });
+    CodeMirror.on(hints, "click", function(e) {
+      var t = e.target || e.srcElement;
+      if (t.hintId != null) changeActive(t.hintId);
+      setTimeout(function(){cm.focus();}, 20);
+    });
+
+    var done = false, once;
+    function close() {
+      if (done) return;
+      done = true;
+      clearTimeout(once);
+      hints.parentNode.removeChild(hints);
+      cm.removeKeyMap(ourMap);
+      cm.off("cursorActivity", cursorActivity);
+      cm.off("blur", onBlur);
+      cm.off("focus", onFocus);
+      cm.off("scroll", onScroll);
+    }
+    function pick() {
+      cm.replaceRange(completions[selectedHint], data.from, data.to);
+      close();
+    }
+    var once, lastPos = cm.getCursor(), lastLen = cm.getLine(lastPos.line).length;
+    function cursorActivity() {
+      clearTimeout(once);
+
+      var pos = cm.getCursor(), len = cm.getLine(pos.line).length;
+      if (pos.line != lastPos.line || len - pos.ch != lastLen - lastPos.ch ||
+          pos.ch < startCh || cm.somethingSelected())
+        close();
+      else
+        once = setTimeout(function(){close(); continued = true; startHinting();}, 70);
+    }
+    return true;
+  }
+
+  return startHinting();
+};