Przeglądaj źródła

AMBARI-11345. Hive View Stability (Erik Bergenholtz via rlevas)

Erik Bergenholtz 10 lat temu
rodzic
commit
c74e0e85d6
44 zmienionych plików z 761 dodań i 211 usunięć
  1. 1 1
      contrib/views/hive/pom.xml
  2. 0 1
      contrib/views/hive/src/main/java/org/apache/ambari/view/hive/client/Connection.java
  3. 1 2
      contrib/views/hive/src/main/java/org/apache/ambari/view/hive/resources/files/FileService.java
  4. 1 2
      contrib/views/hive/src/main/java/org/apache/ambari/view/hive/resources/jobs/atsJobs/ATSRequestsDelegateImpl.java
  5. 3 6
      contrib/views/hive/src/main/java/org/apache/ambari/view/hive/resources/jobs/rm/RMRequestsDelegateImpl.java
  6. 4 0
      contrib/views/hive/src/main/java/org/apache/ambari/view/hive/resources/jobs/viewJobs/Job.java
  7. 11 0
      contrib/views/hive/src/main/java/org/apache/ambari/view/hive/resources/jobs/viewJobs/JobImpl.java
  8. 48 0
      contrib/views/hive/src/main/java/org/apache/ambari/view/hive/resources/savedQueries/SavedQueryService.java
  9. 2 0
      contrib/views/hive/src/main/java/org/apache/ambari/view/hive/utils/HdfsApi.java
  10. 20 1
      contrib/views/hive/src/main/java/org/apache/ambari/view/hive/utils/HdfsUtil.java
  11. 2 1
      contrib/views/hive/src/main/resources/ui/hive-web/app/adapters/application.js
  12. 1 1
      contrib/views/hive/src/main/resources/ui/hive-web/app/components/column-filter-widget.js
  13. 1 1
      contrib/views/hive/src/main/resources/ui/hive-web/app/components/extended-input.js
  14. 4 0
      contrib/views/hive/src/main/resources/ui/hive-web/app/components/tabs-widget.js
  15. 28 6
      contrib/views/hive/src/main/resources/ui/hive-web/app/controllers/databases.js
  16. 70 34
      contrib/views/hive/src/main/resources/ui/hive-web/app/controllers/index.js
  17. 45 5
      contrib/views/hive/src/main/resources/ui/hive-web/app/controllers/index/history-query/results.js
  18. 19 2
      contrib/views/hive/src/main/resources/ui/hive-web/app/controllers/open-queries.js
  19. 145 31
      contrib/views/hive/src/main/resources/ui/hive-web/app/controllers/settings.js
  20. 1 0
      contrib/views/hive/src/main/resources/ui/hive-web/app/controllers/tez-ui.js
  21. 15 1
      contrib/views/hive/src/main/resources/ui/hive-web/app/controllers/visual-explain.js
  22. 17 4
      contrib/views/hive/src/main/resources/ui/hive-web/app/initializers/i18n.js
  23. 1 0
      contrib/views/hive/src/main/resources/ui/hive-web/app/models/job.js
  24. 8 1
      contrib/views/hive/src/main/resources/ui/hive-web/app/routes/application.js
  25. 7 1
      contrib/views/hive/src/main/resources/ui/hive-web/app/routes/history.js
  26. 79 13
      contrib/views/hive/src/main/resources/ui/hive-web/app/styles/app.scss
  27. 5 0
      contrib/views/hive/src/main/resources/ui/hive-web/app/styles/mixins.scss
  28. 5 5
      contrib/views/hive/src/main/resources/ui/hive-web/app/styles/query-tabs.scss
  29. 1 1
      contrib/views/hive/src/main/resources/ui/hive-web/app/templates/components/panel-widget.hbs
  30. 1 1
      contrib/views/hive/src/main/resources/ui/hive-web/app/templates/components/tabs-widget.hbs
  31. 5 5
      contrib/views/hive/src/main/resources/ui/hive-web/app/templates/index.hbs
  32. 35 26
      contrib/views/hive/src/main/resources/ui/hive-web/app/templates/index/history-query/results.hbs
  33. 1 1
      contrib/views/hive/src/main/resources/ui/hive-web/app/templates/open-queries.hbs
  34. 57 0
      contrib/views/hive/src/main/resources/ui/hive-web/app/templates/settings-global.hbs
  35. 72 0
      contrib/views/hive/src/main/resources/ui/hive-web/app/templates/settings-query.hbs
  36. 3 46
      contrib/views/hive/src/main/resources/ui/hive-web/app/templates/settings.hbs
  37. 10 1
      contrib/views/hive/src/main/resources/ui/hive-web/app/utils/constants.js
  38. 4 0
      contrib/views/hive/src/main/resources/ui/hive-web/app/utils/functions.js
  39. 4 2
      contrib/views/hive/src/main/resources/ui/hive-web/app/views/messages.js
  40. 4 2
      contrib/views/hive/src/main/resources/ui/hive-web/app/views/tez-ui.js
  41. 8 3
      contrib/views/hive/src/main/resources/ui/hive-web/app/views/visual-explain.js
  42. 2 2
      contrib/views/hive/src/main/resources/ui/hive-web/tests/unit/controllers/index-test.js
  43. 9 1
      contrib/views/hive/src/main/resources/view.xml
  44. 1 1
      contrib/views/hive/src/test/java/org/apache/ambari/view/hive/resources/jobs/AggregatorTest.java

+ 1 - 1
contrib/views/hive/pom.xml

@@ -19,7 +19,7 @@
   <modelVersion>4.0.0</modelVersion>
   <groupId>org.apache.ambari.contrib.views</groupId>
   <artifactId>hive</artifactId>
-  <version>0.3.0-SNAPSHOT</version>
+  <version>0.4.0-SNAPSHOT</version>
   <name>Hive</name>
 
   <parent>

+ 0 - 1
contrib/views/hive/src/main/java/org/apache/ambari/view/hive/client/Connection.java

@@ -18,7 +18,6 @@
 
 package org.apache.ambari.view.hive.client;
 
-import org.apache.ambari.view.hive.utils.HiveClientFormattedException;
 import org.apache.commons.codec.binary.Hex;
 import org.apache.hadoop.conf.Configuration;
 import org.apache.hadoop.hive.shims.ShimLoader;

+ 1 - 2
contrib/views/hive/src/main/java/org/apache/ambari/view/hive/resources/files/FileService.java

@@ -118,8 +118,7 @@ public class FileService extends BaseService {
     URL url = new URL(filePath.substring(JSON_PATH_FILE.length()));
 
     InputStream responseInputStream = context.getURLStreamProvider().readFrom(url.toString(), "GET",
-                                                                              (String)null,
-                                                                              new HashMap<String, String>());
+        (String)null, new HashMap<String, String>());
     String response = IOUtils.toString(responseInputStream);
 
     for (String ref : url.getRef().split("!")) {

+ 1 - 2
contrib/views/hive/src/main/java/org/apache/ambari/view/hive/resources/jobs/atsJobs/ATSRequestsDelegateImpl.java

@@ -98,8 +98,7 @@ public class ATSRequestsDelegateImpl implements ATSRequestsDelegate {
     String response;
     try {
       InputStream responseInputStream = context.getURLStreamProvider().readFrom(atsUrl, "GET",
-                                                                                (String)null,
-                                                                                new HashMap<String, String>());
+          (String)null, new HashMap<String, String>());
       response = IOUtils.toString(responseInputStream);
     } catch (IOException e) {
       LOG.error("Error while reading from ATS", e);

+ 3 - 6
contrib/views/hive/src/main/java/org/apache/ambari/view/hive/resources/jobs/rm/RMRequestsDelegateImpl.java

@@ -59,8 +59,7 @@ public class RMRequestsDelegateImpl implements RMRequestsDelegate {
     String response;
     try {
       InputStream responseInputStream = context.getURLStreamProvider().readFrom(url, "GET",
-                                                                                (String)null,
-                                                                                new HashMap<String, String>());
+          (String)null, new HashMap<String, String>());
       response = IOUtils.toString(responseInputStream);
     } catch (IOException e) {
       throw new ServiceFormattedException(
@@ -75,8 +74,7 @@ public class RMRequestsDelegateImpl implements RMRequestsDelegate {
     String response;
     try {
       InputStream responseInputStream = context.getURLStreamProvider().readFrom(url, "GET",
-                                                                                (String)null,
-                                                                                new HashMap<String, String>());
+          (String)null, new HashMap<String, String>());
       response = IOUtils.toString(responseInputStream);
     } catch (IOException e) {
       throw new ServiceFormattedException(
@@ -89,8 +87,7 @@ public class RMRequestsDelegateImpl implements RMRequestsDelegate {
     String response;
     try {
       InputStream responseInputStream = context.getURLStreamProvider().readFrom(url, "GET",
-                                                                                (String)null,
-                                                                                new HashMap<String, String>());
+          (String)null, new HashMap<String, String>());
       response = IOUtils.toString(responseInputStream);
     } catch (IOException e) {
       LOG.error("Error while reading from RM", e);

+ 4 - 0
contrib/views/hive/src/main/java/org/apache/ambari/view/hive/resources/jobs/viewJobs/Job.java

@@ -112,4 +112,8 @@ public interface Job extends Serializable,Indexed,PersonalResource {
   String getStatusMessage();
 
   void setStatusMessage(String message);
+
+  String getReferrer();
+
+  void setReferrer(String referrer);
 }

+ 11 - 0
contrib/views/hive/src/main/java/org/apache/ambari/view/hive/resources/jobs/viewJobs/JobImpl.java

@@ -46,6 +46,7 @@ public class JobImpl implements Job {
   private String dagName;
 
   private String sessionTag;
+  private String referrer;
 
   private String id = null;
   private String owner = null;
@@ -272,4 +273,14 @@ public class JobImpl implements Job {
   public void setSqlState(String sqlState) {
     this.sqlState = sqlState;
   }
+
+  @Override
+  public String getReferrer() {
+    return referrer;
+  }
+
+  @Override
+  public void setReferrer(String referrer) {
+    this.referrer = referrer;
+  }
 }

+ 48 - 0
contrib/views/hive/src/main/java/org/apache/ambari/view/hive/resources/savedQueries/SavedQueryService.java

@@ -22,9 +22,12 @@ import org.apache.ambari.view.ViewResourceHandler;
 import org.apache.ambari.view.hive.BaseService;
 import org.apache.ambari.view.hive.persistence.utils.ItemNotFound;
 import org.apache.ambari.view.hive.persistence.utils.OnlyOwnersFilteringStrategy;
+import org.apache.ambari.view.hive.utils.HdfsApi;
+import org.apache.ambari.view.hive.utils.HdfsUtil;
 import org.apache.ambari.view.hive.utils.NotFoundFormattedException;
 import org.apache.ambari.view.hive.utils.ServiceFormattedException;
 import org.json.simple.JSONObject;
+import org.json.simple.JSONValue;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -173,6 +176,51 @@ public class SavedQueryService extends BaseService {
     }
   }
 
+  /**
+   * Get default settings for query
+   */
+  @GET
+  @Path("defaultSettings")
+  @Produces(MediaType.APPLICATION_JSON)
+  public Response getDefaultSettings() {
+    try {
+      String defaultsFile = context.getProperties().get("scripts.settings.defaults-file");
+      HdfsApi hdfsApi = getSharedObjectsFactory().getHdfsApi();
+
+      String defaults = "{\"settings\": {}}";
+      if (hdfsApi.exists(defaultsFile)) {
+        defaults = HdfsUtil.readFile(hdfsApi, defaultsFile);
+      }
+      return Response.ok(JSONValue.parse(defaults)).build();
+    } catch (WebApplicationException ex) {
+      throw ex;
+    } catch (Exception ex) {
+      throw new ServiceFormattedException(ex.getMessage(), ex);
+    }
+  }
+
+  /**
+   * Set default settings for query (overwrites if present)
+   */
+  @POST
+  @Path("defaultSettings")
+  @Consumes(MediaType.APPLICATION_JSON)
+  public Response setDefaultSettings(JSONObject settings) {
+    try {
+      String defaultsFile = context.getProperties().get("scripts.settings.defaults-file");
+      HdfsApi hdfsApi = getSharedObjectsFactory().getHdfsApi();
+
+      HdfsUtil.putStringToFile(hdfsApi, defaultsFile,
+                               settings.toString());
+      String defaults = HdfsUtil.readFile(hdfsApi, defaultsFile);
+      return Response.ok(JSONValue.parse(defaults)).build();
+    } catch (WebApplicationException ex) {
+      throw ex;
+    } catch (Exception ex) {
+      throw new ServiceFormattedException(ex.getMessage(), ex);
+    }
+  }
+
   /**
    * Wrapper object for json mapping
    */

+ 2 - 0
contrib/views/hive/src/main/java/org/apache/ambari/view/hive/utils/HdfsApi.java

@@ -64,7 +64,9 @@ public class HdfsApi {
     conf.set("fs.hdfs.impl", DistributedFileSystem.class.getName());
     conf.set("fs.webhdfs.impl", WebHdfsFileSystem.class.getName());
     conf.set("fs.file.impl", "org.apache.hadoop.fs.LocalFileSystem");
+
     ugi = UserGroupInformation.createProxyUser(username, getProxyUser());
+
     fs = ugi.doAs(new PrivilegedExceptionAction<FileSystem>() {
       public FileSystem run() throws IOException {
         return FileSystem.get(URI.create(defaultFs), conf);

+ 20 - 1
contrib/views/hive/src/main/java/org/apache/ambari/view/hive/utils/HdfsUtil.java

@@ -19,6 +19,8 @@
 package org.apache.ambari.view.hive.utils;
 
 
+import org.apache.commons.io.IOUtils;
+import org.apache.hadoop.fs.FSDataInputStream;
 import org.apache.hadoop.fs.FSDataOutputStream;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -45,7 +47,24 @@ public class HdfsUtil {
     } catch (IOException e) {
       throw new ServiceFormattedException("F070 Could not write file " + filePath, e);
     } catch (InterruptedException e) {
-      throw new ServiceFormattedException("F070 Could not write file " + filePath, e);
+      throw new ServiceFormattedException("F071 Could not write file " + filePath, e);
+    }
+  }
+
+
+  /**
+   * Read string from file
+   * @param filePath path to file
+   */
+  public static String readFile(HdfsApi hdfs, String filePath) {
+    FSDataInputStream stream;
+    try {
+      stream = hdfs.open(filePath);
+      return IOUtils.toString(stream);
+    } catch (IOException e) {
+      throw new ServiceFormattedException("F080 Could not read file " + filePath, e);
+    } catch (InterruptedException e) {
+      throw new ServiceFormattedException("F081 Could not read file " + filePath, e);
     }
   }
 

+ 2 - 1
contrib/views/hive/src/main/resources/ui/hive-web/app/adapters/application.js

@@ -21,7 +21,8 @@ import constants from 'hive/utils/constants';
 
 export default DS.RESTAdapter.extend({
   headers: {
-    'X-Requested-By': 'ambari'
+    'X-Requested-By': 'ambari',
+    'Content-Type': 'application/json'
   },
 
   buildURL: function () {

+ 1 - 1
contrib/views/hive/src/main/resources/ui/hive-web/app/components/column-filter-widget.js

@@ -43,7 +43,7 @@ export default Ember.Component.extend({
     },
 
     sendFilter: function (params) {
-      if (params) {
+      if (params.from && params.to) {
         this.set('filterValue', Ember.Object.create({
           min: params.from,
           max: params.to

+ 1 - 1
contrib/views/hive/src/main/resources/ui/hive-web/app/components/extended-input.js

@@ -36,7 +36,7 @@ export default Ember.TextField.extend(Ember.I18n.TranslateableProperties, {
       dynamicContext.set(dynamicValue, this.get('value'));
     }
 
-    this.sendAction('valueChanged');
+    this.sendAction('valueChanged', this.get('value'));
   },
 
   keyUp: function (e) {

+ 4 - 0
contrib/views/hive/src/main/resources/ui/hive-web/app/components/tabs-widget.js

@@ -59,6 +59,10 @@ export default Ember.Component.extend({
 
     selectTab: function (tab) {
       this.set('selectedTab', tab);
+    },
+
+    titleClick: function(tab) {
+      this.sendAction('onActiveTitleClick', tab);
     }
   }
 });

+ 28 - 6
contrib/views/hive/src/main/resources/ui/hive-web/app/controllers/databases.js

@@ -169,7 +169,7 @@ export default Ember.ArrayController.extend({
     var table = database.get('tables').findBy('name', tableName);
 
     //if all the columns were already loaded for this table, do not get them again.
-    if (table.columns && !table.get('hasNext')) {
+    if (!table || (table.columns && !table.get('hasNext'))) {
       defer.resolve();
     } else {
       this.set('isLoading', true);
@@ -203,7 +203,8 @@ export default Ember.ArrayController.extend({
     return [
       Ember.Object.create({
         icon: 'fa-refresh',
-        action: 'refreshDatabaseExplorer'
+        action: 'refreshDatabaseExplorer',
+        tooltip: Ember.I18n.t('tooltips.refresh')
       })
     ];
   }.property(),
@@ -213,12 +214,29 @@ export default Ember.ArrayController.extend({
       var self = this;
       var selectedDatabase = this.get('selectedDatabase');
 
+      this.set('isLoading', true);
+
       this.store.unloadAll('database');
-      this.store.fetchAll('database').then(function () {
-        var database = self.get('model').findBy('id', selectedDatabase.get('id'));
-        self.set('selectedDatabase', database);
+      this.store.fetchAll('database').then(function (databases) {
+        var database;
+
+        self.set('model', databases);
+
+        //if a database was previously selected, check if it still exists
+        if (selectedDatabase) {
+          database = databases.findBy('id', selectedDatabase.get('id'));
+
+          if (!database) {
+            self.set('selectedDatabase', databases.objectAt(0));
+          }
+        } else if (self.get('model.length')) {
+          self.set('selectedDatabase', databases.objectAt(0));
+        }
+
+        self.set('isLoading');
       }).catch(function (response) {
         self.notify.error(response.responseJSON.message, response.responseJSON.trace);
+        self.set('isLoading');
       });
     },
 
@@ -233,7 +251,7 @@ export default Ember.ArrayController.extend({
         self.get('openQueries.currentQuery')
           .set('fileContent', query);
 
-        self.send('executeQuery');
+        self.send('executeQuery', constants.jobReferrer.sample);
       });
     },
 
@@ -302,6 +320,8 @@ export default Ember.ArrayController.extend({
           resultsTab = this.get('tabs').findBy('view', constants.namingConventions.databaseSearch),
           tableSearchResults = this.get('tableSearchResults');
 
+      searchTerm = searchTerm ? searchTerm.toLowerCase() : '';
+
       this.set('tablesSearchTerm', searchTerm);
       resultsTab.set('visible', true);
       this.set('selectedTab', resultsTab);
@@ -324,6 +344,8 @@ export default Ember.ArrayController.extend({
           resultsTab = this.get('tabs').findBy('view', constants.namingConventions.databaseSearch),
           tables = this.get('tableSearchResults.tables');
 
+      searchTerm = searchTerm ? searchTerm.toLowerCase() : '';
+
       this.set('selectedTab', resultsTab);
 
       this.set('isLoading', true);

+ 70 - 34
contrib/views/hive/src/main/resources/ui/hive-web/app/controllers/index.js

@@ -44,6 +44,38 @@ export default Ember.Controller.extend({
   tezUI: Ember.computed.alias('controllers.' + constants.namingConventions.tezUI),
   jobProgress: Ember.computed.alias('controllers.' + constants.namingConventions.jobProgress),
 
+  isDatabaseExplorerVisible: true,
+
+  init: function () {
+    this._super();
+
+    // initialize queryParams with an empty array
+    this.set('queryParams', Ember.ArrayProxy.create({ content: Ember.A([]) }));
+
+    this.set('queryProcessTabs', Ember.ArrayProxy.create({ content: Ember.A([
+      Ember.Object.create({
+        name: Ember.I18n.t('menus.logs'),
+        path: constants.namingConventions.subroutes.jobLogs
+      }),
+      Ember.Object.create({
+        name: Ember.I18n.t('menus.results'),
+        path: constants.namingConventions.subroutes.jobResults
+      }),
+      Ember.Object.create({
+        name: Ember.I18n.t('menus.explain'),
+        path: constants.namingConventions.subroutes.jobExplain
+      })
+    ])}));
+
+    this.set('queryPanelActions', Ember.ArrayProxy.create({ content: Ember.A([
+      Ember.Object.create({
+        icon: 'fa-expand',
+        action: 'toggleDatabaseExplorerVisibility',
+        tooltip: Ember.I18n.t('tooltips.expand')
+      })
+    ])}));
+  },
+
   canExecute: function () {
     var isModelRunning = this.get('model.isRunning');
     var hasParams = this.get('queryParams.length');
@@ -82,7 +114,7 @@ export default Ember.Controller.extend({
     currentParams.setObjects(updatedParams);
   }.observes('openQueries.currentQuery.fileContent'),
 
-  _executeQuery: function (shouldExplain, shouldGetVisualExplain) {
+  _executeQuery: function (referrer, shouldExplain, shouldGetVisualExplain) {
     var queryId,
         query,
         finalQuery,
@@ -93,7 +125,8 @@ export default Ember.Controller.extend({
     job = this.store.createRecord(constants.namingConventions.job, {
       title: originalModel.get('title'),
       sessionTag: originalModel.get('sessionTag'),
-      dataBase: this.get('databases.selectedDatabase.name')
+      dataBase: this.get('databases.selectedDatabase.name'),
+      referrer: referrer
     });
 
     originalModel.set('isRunning', true);
@@ -202,7 +235,7 @@ export default Ember.Controller.extend({
     query = query.trim();
 
     //update with the current settings
-    if (validSettings) {
+    if (validSettings.get('length')) {
       query = '\n' + query;
 
       validSettings.forEach(function (setting) {
@@ -278,28 +311,6 @@ export default Ember.Controller.extend({
     return query;
   },
 
-  init: function () {
-    this._super();
-
-    // initialize queryParams with an empty array
-    this.set('queryParams', Ember.ArrayProxy.create({ content: Ember.A([]) }));
-
-    this.set('queryProcessTabs', Ember.ArrayProxy.create({ content: Ember.A([
-      Ember.Object.create({
-        name: Ember.I18n.t('menus.logs'),
-        path: constants.namingConventions.subroutes.jobLogs
-      }),
-      Ember.Object.create({
-        name: Ember.I18n.t('menus.results'),
-        path: constants.namingConventions.subroutes.jobResults
-      }),
-      Ember.Object.create({
-        name: Ember.I18n.t('menus.explain'),
-        path: constants.namingConventions.subroutes.jobExplain
-      })
-    ])}));
-  },
-
   displayJobTabs: function () {
     return this.get('content.constructor.typeKey') === constants.namingConventions.job &&
            utils.isInteger(this.get('content.id'));
@@ -397,7 +408,7 @@ export default Ember.Controller.extend({
     return components;
   },
 
-  saveToHDFS: function () {
+  saveToHDFS: function (path) {
     var job = this.get('content');
 
     if (!utils.insensitiveCompare(job.get('status'), constants.statuses.succeeded)) {
@@ -406,7 +417,7 @@ export default Ember.Controller.extend({
 
     var self = this;
 
-    var file = "/tmp/" + job.get('id') + ".csv";
+    var file = "/tmp/" + path + ".csv";
     var url = this.container.lookup('adapter:application').buildURL();
     url +=  "/jobs/" + job.get('id') + "/results/csv/saveToHDFS";
 
@@ -446,10 +457,25 @@ export default Ember.Controller.extend({
     tabs.findBy('path', constants.namingConventions.subroutes.jobResults).set('visible', !show);
   },
 
+  queryProcessTitle: function () {
+    return Ember.I18n.t('titles.query.process') + ' (' + Ember.I18n.t('titles.query.status') + this.get('content.status') + ')';
+  }.property('content.status'),
+
   actions: {
     saveToHDFS: function () {
-      this.set('content.isRunning', true);
-      this.saveToHDFS();
+      var self = this,
+          defer = Ember.RSVP.defer();
+
+      this.send('openModal', 'modal-save', {
+        heading: "modals.download.hdfs",
+        text: this.get('content.title') + '_' + this.get('content.id'),
+        defer: defer
+      });
+
+      defer.promise.then(function (text) {
+        self.set('content.isRunning', true);
+        self.saveToHDFS(text);
+      });
     },
 
     downloadAsCSV: function () {
@@ -543,6 +569,10 @@ export default Ember.Controller.extend({
 
       defer.promise.then(function (result) {
         currentQuery.set('fileContent', self.prependQuerySettings(currentQuery.get('fileContent')));
+        // we need to update the original model
+        // because when this is executed
+        // it sets the title from the original model
+        self.set('model.title', result.get('text'));
 
         if (result.get('overwrite')) {
           self.get('openQueries').save(self.get('content'), null, true, result.get('text'));
@@ -556,11 +586,13 @@ export default Ember.Controller.extend({
       });
     },
 
-    executeQuery: function () {
+    executeQuery: function (referrer) {
       var self = this;
       var subroute;
 
-      this._executeQuery().then(function (job) {
+      referrer = referrer || constants.jobReferrer.job;
+
+      this._executeQuery(referrer).then(function (job) {
         if (job.get('status') !== constants.statuses.succeeded) {
           subroute = constants.namingConventions.subroutes.jobLogs;
         } else {
@@ -579,13 +611,17 @@ export default Ember.Controller.extend({
     explainQuery: function () {
       var self = this;
 
-      this._executeQuery(true).then(function (job) {
+      this._executeQuery(constants.jobReferrer.explain, true).then(function (job) {
         self.get('openQueries').updateTabSubroute(job, constants.namingConventions.subroutes.jobExplain);
 
         self.transitionToRoute(constants.namingConventions.subroutes.historyQuery, job.get('id'));
       }, function (err) {
-        this.notify.error(err.responseJSON.message, err.responseJSON.trace);
+        self.notify.error(err.responseJSON.message, err.responseJSON.trace);
       });
+    },
+
+    toggleDatabaseExplorerVisibility: function () {
+      this.toggleProperty('isDatabaseExplorerVisible');
     }
   }
-});
+});

+ 45 - 5
contrib/views/hive/src/main/resources/ui/hive-web/app/controllers/index/history-query/results.js

@@ -1,3 +1,4 @@
+
 /**
  * Licensed to the Apache Software Foundation (ASF) under one
  * or more contributor license agreements.  See the NOTICE file
@@ -26,21 +27,56 @@ export default Ember.ObjectController.extend({
 
   processResults: function () {
     var results = this.get('results');
+    var filterValue = this.get('filterValue');
+    var columns;
+    var rows;
+    var filteredColumns;
+    var filteredRows;
+
+    if (!results) {
+      return;
+    }
 
-    if (!results || !results.schema || !results.rows) {
+    columns = results.schema;
+    rows = results.rows;
+
+    if (!columns || !rows) {
       return;
     }
 
-    var schema = results.schema.map(function (column) {
+    columns = columns.map(function (column) {
       return {
         name: column[0],
         type: column[1],
-        index: column[2]
+        index: column[2] - 1 //normalize index to 0 based
       };
     });
 
-    this.set('formattedResults', { schema: schema, rows: results.rows });
-  }.observes('results'),
+    if (filterValue) {
+      filteredColumns = columns.filter(function (column) {
+        return utils.insensitiveContains(column.name, filterValue);
+      });
+
+      if (filteredColumns.length < columns.length) {
+        filteredRows = rows.map(function (row) {
+          var updatedRow = [];
+
+          updatedRow.pushObjects(row.filter(function (item, index) {
+            return this.findBy('index', index);
+          }, this));
+
+          return updatedRow;
+        }, filteredColumns);
+      } else {
+        filteredRows = rows;
+      }
+    } else {
+      filteredColumns = columns;
+      filteredRows = rows;
+    }
+
+    this.set('formattedResults', { columns: filteredColumns, rows: filteredRows });
+  }.observes('results', 'filterValue'),
 
   keepAlive: function (job) {
     Ember.run.later(this, function () {
@@ -164,6 +200,10 @@ export default Ember.ObjectController.extend({
       if (resultsIndex > 0) {
         this.set('results', existingJob.results.objectAt(resultsIndex - 1));
       }
+    },
+
+    filterResults: function (value) {
+      this.set('filterValue', value);
     }
   }
 });

+ 19 - 2
contrib/views/hive/src/main/resources/ui/hive-web/app/controllers/open-queries.js

@@ -180,10 +180,10 @@ export default Ember.ArrayController.extend({
         queryFile: model.get('queryFile'),
         owner: model.get('owner')
       });
-    } else {
-      tab.set('name', newTitle);
     }
 
+    tab.set('name', newTitle);
+
     //if saving a new query from an existing one create a new record and save it
     if (!isUpdating && !model.get('isNew') && model.get('constructor.typeKey') !== constants.namingConventions.job) {
       model = this.store.createRecord(constants.namingConventions.savedQuery, {
@@ -376,6 +376,23 @@ export default Ember.ArrayController.extend({
       this.get('databases').getAllColumns(tableName).then(function () {
         callback();
       });
+    },
+
+    changeTabTitle: function(tab) {
+      var self = this,
+          defer = Ember.RSVP.defer(),
+          title = this.get('index.content.title');
+
+      this.send('openModal', 'modal-save', {
+        heading: 'modals.changeTitle.heading',
+        text: title,
+        defer: defer
+      });
+
+      defer.promise.then(function (result) {
+        self.set('index.model.title', result);
+        tab.set('name', result);
+      });
     }
   }
 });

+ 145 - 31
contrib/views/hive/src/main/resources/ui/hive-web/app/controllers/settings.js

@@ -31,6 +31,49 @@ export default Ember.ArrayController.extend({
   sessionTag: Ember.computed.alias('index.model.sessionTag'),
   sessionActive: Ember.computed.alias('index.model.sessionActive'),
 
+  createDefaultsSettings: function(settings) {
+    var globalSettings = [];
+    var newSetting;
+
+    for (var key in settings) {
+      newSetting = this.createSetting(key, settings[key]);
+      globalSettings.push(newSetting);
+    }
+
+    this.get('globalSettings').setObjects(globalSettings);
+  },
+
+  loadDefaultSettings: function() {
+    var adapter       = this.container.lookup('adapter:application');
+    var url           = adapter.buildURL() + '/savedQueries/defaultSettings';
+    var self = this;
+
+    adapter.ajax(url)
+      .then(function(response) {
+        self.createDefaultsSettings(response.settings);
+      })
+      .catch(function(error) {
+        self.notify.error(error.responseJSON.message, error.responseJSON.trace);
+      });
+  }.on('init'),
+
+  setSettingsTabs: function() {
+    this.set('settingsTabs', Ember.ArrayProxy.create({ content: [
+      Ember.Object.create({
+        name: 'Query Settings',
+        view: constants.namingConventions.settingsQuery,
+        visible: true
+      }),
+      Ember.Object.create({
+        name: 'Global Settings',
+        view: constants.namingConventions.settingsGlobal,
+        visible: true
+      })
+    ]}));
+
+    this.set('selectedTab', this.get('selectTab.firstObject'));
+  }.on('init'),
+
   canInvalidateSession: Ember.computed.and('sessionTag', 'sessionActive'),
 
   predefinedSettings: constants.hiveParameters,
@@ -58,19 +101,35 @@ export default Ember.ArrayController.extend({
     return targetSettings;
   }.property('openQueries.currentQuery'),
 
+  settingsSets: [
+    Ember.Object.create({ name: 'Set 1' }),
+    Ember.Object.create({ name: 'Set 2' }),
+    Ember.Object.create({ name: 'Set 3' })
+  ],
+
+  globalSettings: Ember.ArrayProxy.create({ content: []}),
+
   updateSettingsId: function (oldId, newId) {
     this.filterBy('id', oldId).setEach('id', newId);
   },
 
   getCurrentValidSettings: function () {
     var currentSettings = this.get('currentSettings');
+    var globalSettings = this.get('globalSettings');
     var validSettings = [];
+    var settings = Ember.copy(currentSettings.get('settings'));
+
+    globalSettings.map(function(setting) {
+      if (!settings.findBy('key.name', setting.get('key.name'))) {
+        settings.pushObject(setting);
+      }
+    });
 
-    if (!currentSettings) {
+    if (!currentSettings && !globalSettings) {
       return '';
     }
 
-    currentSettings.get('settings').map(function (setting) {
+    settings.map(function (setting) {
       if (setting.get('valid')) {
         validSettings.pushObject('set %@ = %@;'.fmt(setting.get('key.name'), setting.get('value')));
       }
@@ -88,19 +147,43 @@ export default Ember.ArrayController.extend({
     return settings && settings.get('settings.length');
   },
 
-  parseQuerySettings: function () {
-    var query = this.get('openQueries.currentQuery');
-    var content = query.get('fileContent');
+  createSetting: function(name, value) {
     var self = this;
-    var regex = new RegExp(utils.regexes.setSetting);
-    var settings = content.match(regex) || [];
-    var targetSettings = this.findBy('id', this.get('index.model.id'));
+    var setting = Ember.Object.createWithMixins({
+      valid     : true,
+      value     : Ember.computed.alias('selection.value'),
+      selection : Ember.Object.create(),
+
+      isInGlobal: function() {
+        return self.get('globalSettings').mapProperty('key.name').contains(this.get('key.name'));
+      }.property('key.name')
+    });
+
+    if (name) {
+      setting.set('key', Ember.Object.create({ name: name }));
+    }
+
+    if (value) {
+      setting.set('selection.value', value);
+    }
+
+    return setting;
+  },
 
-    if (!query || !targetSettings) {
+  parseQuerySettings: function () {
+    var self           = this;
+    var query          = this.get('openQueries.currentQuery');
+    var content        = query.get('fileContent');
+    var regex          = new RegExp(utils.regexes.setSetting);
+    var settings       = content.match(regex);
+    var targetSettings = this.get('currentSettings');
+
+    if (!query || !settings) {
       return;
     }
 
-    settings = settings.map(function (setting) {
+    var parsedSettings = [];
+    settings.forEach(function (setting) {
       var KeyValue = setting.split('=');
       var name     = KeyValue[0].replace('set', '').trim();
       var value    = KeyValue[1].replace(';', '').trim();
@@ -111,21 +194,15 @@ export default Ember.ArrayController.extend({
         });
       }
 
-      var settingObj = Ember.Object.createWithMixins({
-        key: Ember.Object.create({ name: 'nam' }),
-        selection : Ember.Object.create({ value: 'val'}),
-
-        value: Ember.computed.alias('selection.value'),
-        valid: true
-      });
-
-      settingObj.set('key.name', name);
-      settingObj.set('selection.value', value);
+      if (self.get('globalSettings').findBy('key.name', name)) {
+        return false;
+      }
 
-      return settingObj;
+      parsedSettings.push(self.createSetting(name, value));
     });
 
-    targetSettings.set('settings', settings);
+    query.set('fileContent', content.replace(regex, '').trim());
+    targetSettings.set('settings', parsedSettings);
   }.observes('openQueries.currentQuery', 'openQueries.currentQuery.fileContent', 'openQueries.tabUpdated'),
 
   validate: function () {
@@ -185,13 +262,7 @@ export default Ember.ArrayController.extend({
 
   actions: {
     add: function () {
-      var setting = Ember.Object.createWithMixins({
-        valid: true,
-        selection: Ember.Object.create(),
-        value: Ember.computed.alias('selection.value')
-      });
-
-      this.get('currentSettings.settings').pushObject(setting);
+      this.get('currentSettings.settings').pushObject(this.createSetting());
     },
 
     remove: function (setting) {
@@ -235,11 +306,54 @@ export default Ember.ArrayController.extend({
       adapter.ajax(url, 'DELETE').catch(function (response) {
         if ([200, 404].contains(response.status)) {
           model.set('sessionActive', false);
-          self.notify.success('alerts.success.sessions.deleted');
+          self.notify.success(Ember.I18n.t('alerts.success.sessions.deleted'));
         } else {
           self.notify.error(response.responseJSON.message, response.responseJSON.trace);
         }
       });
+    },
+
+    makeSettingGlobal: function(setting) {
+      // @TODO: should remove from all query settings?
+      // @TODO: validate setting? maybe its not needed bc it was already validated?
+      this.get('globalSettings').pushObject(setting);
+      this.get('currentSettings.settings').removeObject(setting);
+    },
+
+    removeGlobal: function(setting) {
+      this.get('globalSettings').removeObject(setting);
+    },
+
+    overwriteGlobalValue: function(setting) {
+      var globalSetting = this.get('globalSettings').findBy('key.name', setting.get('key.name'));
+
+      if (globalSetting) {
+        globalSetting.set('value', setting.get('value'));
+        this.get('currentSettings.settings').removeObject(setting);
+      }
+    },
+
+    saveDefaultSettings: function() {
+      var self     = this;
+      var data     = {};
+      var adapter  = this.container.lookup('adapter:application');
+      var url      = adapter.buildURL() + '/savedQueries/defaultSettings';
+      var settings = this.get('globalSettings');
+
+      settings.forEach(function(setting) {
+        data[ setting.get('key.name') ] = setting.get('value');
+      });
+
+      adapter.ajax(url, 'POST', {
+        data: {settings: data }
+      })
+        .then(function(response) {
+          if (response && response.settings) {
+            self.notify.success(Ember.I18n.t('alerts.success.settings.saved'));
+          } else {
+            self.notify.error(response.responseJSON.message, response.responseJSON.trace);
+          }
+        });
     }
   }
-});
+});

+ 1 - 0
contrib/views/hive/src/main/resources/ui/hive-web/app/controllers/tez-ui.js

@@ -100,6 +100,7 @@ export default Ember.Controller.extend({
     // no instance created
     if (data.instances && !data.instances.length) {
       this.set('error', 'tez.errors.no.instance');
+      return;
     }
   }
 });

+ 15 - 1
contrib/views/hive/src/main/resources/ui/hive-web/app/controllers/visual-explain.js

@@ -21,20 +21,34 @@ import constants from 'hive/utils/constants';
 
 export default Ember.Controller.extend({
   needs: [ constants.namingConventions.index,
+           constants.namingConventions.openQueries,
            constants.namingConventions.jobProgress ],
 
   index: Ember.computed.alias('controllers.' + constants.namingConventions.index),
   jobProgress: Ember.computed.alias('controllers.' + constants.namingConventions.jobProgress),
+  openQueries: Ember.computed.alias('controllers.' + constants.namingConventions.openQueries),
 
   updateProgress: function () {
     this.set('verticesProgress', this.get('jobProgress.stages'));
   }.observes('jobProgress.stages', 'jobProgress.stages.@each.value'),
 
+  observeCurrentQuery: function () {
+    this.set('shouldChangeGraph', true);
+  }.observes('openQueries.currentQuery', 'openQueries.currentQuery.fileContent'),
+
   actions: {
     onTabOpen: function () {
       var self = this;
 
-      this.get('index')._executeQuery(true, true).then(function (json) {
+      if (!this.get('shouldChangeGraph') && this.get('json')) {
+        this.set('rerender', true);
+        return;
+      }
+
+      this.set('rerender');
+      this.toggleProperty('shouldChangeGraph');
+
+      this.get('index')._executeQuery(constants.jobReferrer.visualExplain, true, true).then(function (json) {
         //this condition should be changed once we change the way of retrieving this json
         if (json['STAGE PLANS']['Stage-1']) {
           self.set('json', json);

+ 17 - 4
contrib/views/hive/src/main/resources/ui/hive-web/app/initializers/i18n.js

@@ -28,7 +28,7 @@ export default {
     Ember.I18n.translations = TRANSLATIONS;
     Ember.TextField.reopen(Ember.I18n.TranslateableAttributes);
   }
-}
+};
 
 TRANSLATIONS = {
   tooltips: {
@@ -38,7 +38,10 @@ TRANSLATIONS = {
     settings: 'Settings',
     visualExplain: 'Visual Explain',
     tez: 'Tez',
-    notifications: 'Notifications'
+    notifications: 'Notifications',
+    expand: 'Expand query panel',
+    makeSettingGlobal: 'Make setting global',
+    overwriteGlobalValue: 'Overwrite global setting value'
   },
 
   alerts: {
@@ -61,6 +64,9 @@ TRANSLATIONS = {
     success: {
       sessions: {
         deleted: 'Session invalidated'
+      },
+      settings: {
+        saved: 'Settings have been saved.'
       }
     }
   },
@@ -80,7 +86,12 @@ TRANSLATIONS = {
     },
 
     download: {
-      csv: 'Download results as CSV'
+      csv: 'Download results as CSV',
+      hdfs: 'Please enter save path and name'
+    },
+
+    changeTitle: {
+      heading: 'Rename worksheet'
     }
   },
 
@@ -96,6 +107,7 @@ TRANSLATIONS = {
       parameters: 'Parameters',
       visualExplain: 'Visual Explain',
       tez: 'TEZ',
+      status: 'Status: ',
       messages: 'Messages'
     },
     download: 'Save results...',
@@ -105,7 +117,8 @@ TRANSLATIONS = {
   placeholders: {
     search: {
       tables: 'Search tables...',
-      columns: 'Search columns in result tables...'
+      columns: 'Search columns in result tables...',
+      results: 'Filter columns...'
     },
     select: {
       database: 'Select Database...',

+ 1 - 0
contrib/views/hive/src/main/resources/ui/hive-web/app/models/job.js

@@ -36,6 +36,7 @@ export default DS.Model.extend({
   page: DS.attr(),
   statusDir: DS.attr('string'),
   applicationId: DS.attr(),
+  referrer: DS.attr('string'),
   confFile: DS.attr('string'),
 
   dateSubmittedTimestamp: function () {

+ 8 - 1
contrib/views/hive/src/main/resources/ui/hive-web/app/routes/application.js

@@ -23,10 +23,16 @@ export default Ember.Route.extend({
   setupController: function () {
     var self = this;
 
-    this.controllerFor(constants.namingConventions.databases).set('model', this.store.find(constants.namingConventions.database));
+    this.store.find(constants.namingConventions.database).then(function (databases) {
+      self.controllerFor(constants.namingConventions.databases).set('model', databases);
+    }, function (err) {
+      self.notify.error(err.responseJSON.message, err.responseJSON.trace);
+    });
 
     this.store.find(constants.namingConventions.udf).then(function (udfs) {
       self.controllerFor(constants.namingConventions.udfs).set('udfs', udfs);
+    }, function (err) {
+      self.notify.error(err.responseJSON.message, err.responseJSON.trace);
     });
   },
 
@@ -59,6 +65,7 @@ export default Ember.Route.extend({
         into: overlay.into
       });
     },
+
     closeOverlay: function (overlay) {
       return this.disconnectOutlet({
         outlet: overlay.outlet,

+ 7 - 1
contrib/views/hive/src/main/resources/ui/hive-web/app/routes/history.js

@@ -25,6 +25,12 @@ export default Ember.Route.extend({
   },
 
   setupController: function (controller, model) {
-    controller.set('history', model);
+    var filteredModel = model.filter(function (job) {
+       //filter out jobs with referrer type of sample, explain and visual explain
+       return !job.get('referrer') ||
+              job.get('referrer') === constants.jobReferrer.job;
+    });
+
+    controller.set('history', filteredModel);
   }
 });

+ 79 - 13
contrib/views/hive/src/main/resources/ui/hive-web/app/styles/app.scss

@@ -80,8 +80,21 @@ a {
   background: white;
 }
 
-aside  hr {
-  margin: 10px 0;
+aside  {
+  hr {
+    margin: 10px 0;
+  }
+
+  &.no-width {
+    width: 0;
+    overflow: hidden;
+
+    @include animate-width(0.25s);
+  }
+
+  &.col-md-3 {
+    @include animate-width(0.25s);
+  }
 }
 
 .halfed {
@@ -207,6 +220,14 @@ dropdown .fa-remove {
 .query-container {
   position: relative;
   padding-right: 0;
+
+  &.col-md-9 {
+    @include animate-width(0.25s);
+  }
+
+  &.col-md-12 {
+    @include animate-width(0.25s);
+  }
 }
 
 .main-content {
@@ -218,7 +239,7 @@ dropdown .fa-remove {
   font-size: 20px;
 
   &.active {
-    color: #428bca;
+    color: white;
   }
 
   &.text-icon {
@@ -229,9 +250,11 @@ dropdown .fa-remove {
 
 .query-context-tab {
   background: #f1f1f1;
+  border-left: 2px solid #428bca;
 
   &.active {
-    background: white;
+    color: #428bca;
+    border-left: 2px solid white;
   }
 }
 
@@ -356,10 +379,13 @@ body {
   line-height: 24px;
 }
 
+.settings-controls {
+  margin: 10px 0;
+}
+
 .setting {
   float: left;
-  padding-right: 10px;
-  padding-top: 10px;
+  padding: 10px 0 0 0;
 
   .input-group {
     width: 100%;
@@ -370,18 +396,43 @@ body {
   }
 }
 
-.setting .remove {
+.setting .remove, .setting .makeGlobal {
   line-height: 30px;
   font-size: 18px;
   cursor: pointer;
-  position: absolute;
-  right: -5px;
-  top: -10px;
+  // position: absolute;
+  // right: -5px;
+  // top: -10px;
 }
 
-tabs {
-  display: block;
-  position: relative;
+.setting .makeGlobal.overwriteGlobal {
+  color: #ff3322;
+}
+
+.setting .setting-input-value {
+  width: calc(100% - 50px);
+  display: inline-block;
+}
+.setting .global-setting-value {
+  width: calc(100% - 25px);
+}
+
+.setting .makeGlobal {
+  top: 30px;
+}
+
+.settings-set {
+  margin: 10px 0;
+}
+.settings-set h3 {
+  display: inline-block;
+  margin: 0 10px 0 0;
+  vertical-align: top;
+  line-height: 35px;
+}
+.settings-set .settings-set-selector {
+  display: inline-block;
+  width: 300px;
 }
 
 tree-view ul li {
@@ -481,3 +532,18 @@ tree-view ul li {
 .messages-controls {
   margin: 0 0 10px;
 }
+
+#query-results {
+  .table {
+    display: inline-block;
+    overflow: auto;
+  }
+
+  .query-results-tools {
+    margin-top: 10px;
+  }
+
+  input {
+    width: 300px;
+  }
+}

+ 5 - 0
contrib/views/hive/src/main/resources/ui/hive-web/app/styles/mixins.scss

@@ -21,3 +21,8 @@
      -moz-box-shadow: $horizontal $vertical $blur $color;
           box-shadow: $horizontal $vertical $blur $color;
 }
+
+@mixin animate-width($time) {
+  -webkit-transition: $time;
+  transition: width $time;
+}

+ 5 - 5
contrib/views/hive/src/main/resources/ui/hive-web/app/styles/query-tabs.scss

@@ -17,7 +17,7 @@
 */
 
 .query-menu {
-  margin-top: 57px;
+  margin-top: 58px;
 
   span, popover {
     cursor: pointer;
@@ -35,12 +35,12 @@
 .editor-overlay {
   width: 100%;
   overflow-y: scroll;
-  height: calc(100% - 41px);
-  top: 41px;
+  height: calc(100% - 57px);
+  top: 57px;
   left: 0;
   background-color: #fff;
   position: absolute;
-  padding: 0 15px;
+  padding: 10px 15px;
   z-index: 1000;
 
   border: 1px solid $border-color;
@@ -60,7 +60,7 @@
 .query-menu-tab .badge {
   position: absolute;
   top: -4px;
-  right: 0;
+  right: 0px;
   background-color: red;
   color: #fff;
   padding: 2px 4px;

+ 1 - 1
contrib/views/hive/src/main/resources/ui/hive-web/app/templates/components/panel-widget.hbs

@@ -38,7 +38,7 @@
       {{#if iconActions}}
         {{#each iconAction in iconActions}}
         <i {{action "sendMenuItemAction" iconAction.action}}
-          {{bind-attr class=":pull-right :panel-action-icon :fa iconAction.icon"}}></i>
+          {{bind-attr class=":pull-right :panel-action-icon :fa iconAction.icon" title="iconAction.tooltip"}}></i>
         {{/each}}
       {{/if}}
 

+ 1 - 1
contrib/views/hive/src/main/resources/ui/hive-web/app/templates/components/tabs-widget.hbs

@@ -21,7 +21,7 @@
     {{#if tab.visible}}
       {{#if tab.path}}
         {{#link-to tab.path tab.id tagName="li"}}
-          <a>
+          <a {{action 'titleClick' tab on="doubleClick"}}>
             {{tab.name}}
             {{#if tab.isDirty}}*{{/if}}
             {{#if view.removeEnabled}}

+ 5 - 5
contrib/views/hive/src/main/resources/ui/hive-web/app/templates/index.hbs

@@ -18,12 +18,12 @@
 
 <div id="index-content">
   <div class="main-content">
-    <aside class="col-md-3 col-xs-12 no-padding">
+    <aside {{bind-attr class="isDatabaseExplorerVisible:col-md-3:no-width :col-xs-12 :no-padding"}}>
       {{render 'databases'}}
     </aside>
 
-    <div class="col-md-9 col-xs-12 query-container">
-      {{#panel-widget headingTranslation="titles.query.editor" classNames="query-editor-panel"}}
+    <div {{bind-attr class="isDatabaseExplorerVisible:col-md-9:col-md-12 :col-xs-12 :query-container"}}>
+      {{#panel-widget headingTranslation="titles.query.editor" classNames="query-editor-panel" iconActions=queryPanelActions}}
         {{render 'open-queries'}}
 
         <div class="toolbox">
@@ -67,8 +67,8 @@
       {{/if}}
 
       {{#if displayJobTabs}}
-        {{#panel-widget headingTranslation="titles.query.process"
-                        isLoading=model.isRunning
+        {{#panel-widget heading=queryProcessTitle
+                        isLoading=content.isRunning
                         menuItems=downloadMenu
                         menuHeadingTranslation="titles.download"
                         classNames="query-process-results-panel"}}

+ 35 - 26
contrib/views/hive/src/main/resources/ui/hive-web/app/templates/index/history-query/results.hbs

@@ -16,32 +16,41 @@
 * limitations under the License.
 }}
 
-{{#if results}}
-  <table class="table table-expandable">
-    <thead>
-      <tr>
-        {{#each column in formattedResults.schema}}
-          <th> {{column.name}} </th>
-        {{/each}}
-      </tr>
-    </thead>
-    <tbody>
-      {{#each row in formattedResults.rows}}
+<div id="query-results">
+  {{#if results}}
+    <div class="query-results-tools">
+      {{extended-input type="text"
+                       class="pull-left input-sm form-control"
+                       placeholderTranslation='placeholders.search.results'
+                       valueChanged="filterResults"}}
+
+      <div class="pull-right">
+        <button type="button" {{action "getPreviousPage"}}
+                {{bind-attr class=":btn :btn-sm :btn-default disablePrevious:disabled"}}>{{t "buttons.previousPage"}}</button>
+        <button type="button" {{action "getNextPage"}}
+                {{bind-attr class=":btn :btn-sm :btn-default disableNext:disabled"}}>{{t "buttons.nextPage"}}</button>
+      </div>
+    </div>
+
+    <table class="table table-expandable">
+      <thead>
         <tr>
-          {{#each item in row}}
-            <td>{{item}}</td>
+          {{#each column in formattedResults.columns}}
+            <th> {{column.name}} </th>
           {{/each}}
         </tr>
-      {{/each}}
-    </tbody>
-  </table>
-
-  <div class="pull-right">
-    <button type="button" {{action "getPreviousPage"}}
-            {{bind-attr class=":btn :btn-sm :btn-default disablePrevious:disabled"}}>{{t "buttons.previousPage"}}</button>
-    <button type="button" {{action "getNextPage"}}
-            {{bind-attr class=":btn :btn-sm :btn-default disableNext:disabled"}}>{{t "buttons.nextPage"}}</button>
-  </div>
-{{else}}
-  {{error}}
-{{/if}}
+      </thead>
+      <tbody>
+        {{#each row in formattedResults.rows}}
+          <tr>
+            {{#each item in row}}
+              <td>{{item}}</td>
+            {{/each}}
+          </tr>
+        {{/each}}
+      </tbody>
+    </table>
+  {{else}}
+    {{error}}
+  {{/if}}
+</div>

+ 1 - 1
contrib/views/hive/src/main/resources/ui/hive-web/app/templates/open-queries.hbs

@@ -16,7 +16,7 @@
 * limitations under the License.
 }}
 
-{{#tabs-widget tabs=queryTabs removeClicked="removeQueryTab" canRemove=true}}
+{{#tabs-widget tabs=queryTabs removeClicked="removeQueryTab" canRemove=true onActiveTitleClick="changeTabTitle"}}
   {{outlet 'overlay'}}
   {{query-editor tables=selectedTables query=currentQuery.fileContent editor=view.editor highlightedText=highlightedText
                  columnsNeeded="getColumnsForAutocomplete"}}

+ 57 - 0
contrib/views/hive/src/main/resources/ui/hive-web/app/templates/settings-global.hbs

@@ -0,0 +1,57 @@
+{{!
+* Licensed to the Apache Software Foundation (ASF) under one
+* or more contributor license agreements.  See the NOTICE file
+* distributed with this work for additional information
+* regarding copyright ownership.  The ASF 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.
+}}
+
+<div class='settings-controls'>
+  <button class="btn btn-success btn-xs" {{action 'saveDefaultSettings'}}><i class="fa fa-plus"></i> Save Default Settings</button>
+</div>
+
+{{#each setting in globalSettings}}
+  <div class="setting col-md-12 col-sm-12">
+    <form>
+      <div class="form-group">
+        <div class="input-group">
+          <div class="input-group-addon">
+            {{typeahead-widget
+                options=predefinedSettings
+                excluded=selectedSettings
+                optionLabelPath="name"
+                optionValuePath="name"
+                selection=setting.key
+            }}
+          </div>
+          <div {{bind-attr class=":input-group-addon setting.valid::has-error"}}>
+
+            <div class="setting-input-value global-setting-value">
+              {{#if setting.key.values}}
+                {{select-widget items=setting.key.values
+                                labelPath="value"
+                                selectedValue=setting.selection
+                                defaultLabelTranslation="placeholders.select.value"
+                }}
+              {{else}}
+                {{input class="input-sm form-control" placeholderTranslation="placeholders.select.value" value=setting.selection.value}}
+              {{/if}}
+            </div>
+
+            <span class="fa fa-times-circle remove pull-right" {{action 'removeGlobal' setting}}></span>
+          </div>
+        </div>
+      </div>
+    </form>
+  </div>
+{{/each}}

+ 72 - 0
contrib/views/hive/src/main/resources/ui/hive-web/app/templates/settings-query.hbs

@@ -0,0 +1,72 @@
+{{!
+* Licensed to the Apache Software Foundation (ASF) under one
+* or more contributor license agreements.  See the NOTICE file
+* distributed with this work for additional information
+* regarding copyright ownership.  The ASF 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.
+}}
+
+<div class='settings-controls'>
+  <button class="btn btn-success btn-xs" {{action 'add'}}><i class="fa fa-plus"></i> Add</button>
+  {{#if currentSettings.settings}}
+    <button class="btn btn-danger btn-xs" {{action 'removeAll'}}><i class="fa fa-minus"></i> Remove All</button>
+  {{/if}}
+
+
+  {{#if canInvalidateSession}}
+    <button class="btn btn-danger btn-xs pull-right" {{action 'invalidateSession'}}><i class="fa fa-times"></i> Invalidate Session</button>
+  {{/if}}
+</div>
+
+{{#each setting in currentSettings.settings}}
+  <div class="setting col-md-12 col-sm-12">
+    <form>
+      <div class="form-group">
+        <div class="input-group">
+          <div class="input-group-addon">
+            {{typeahead-widget
+                options=predefinedSettings
+                excluded=selectedSettings
+                optionLabelPath="name"
+                optionValuePath="name"
+                selection=setting.key
+                create="addKey"
+            }}
+          </div>
+          <div {{bind-attr class=":input-group-addon setting.valid::has-error"}}>
+
+            <div class="setting-input-value">
+              {{#if setting.key.values}}
+                {{select-widget items=setting.key.values
+                                labelPath="value"
+                                selectedValue=setting.selection
+                                defaultLabelTranslation="placeholders.select.value"
+                }}
+              {{else}}
+                {{input class="input-sm form-control" placeholderTranslation="placeholders.select.value" value=setting.selection.value}}
+              {{/if}}
+            </div>
+
+            <span class="fa fa-times-circle remove pull-right" {{action 'remove' setting}}></span>
+
+            {{#if setting.isInGlobal}}
+              <span class="fa fa-globe makeGlobal overwriteGlobal pull-right" title="Overwrite Global Setting" {{action 'overwriteGlobalValue' setting}}></span>
+            {{else}}
+              <span class="fa fa-globe makeGlobal pull-right" title="Make Global" {{action 'makeSettingGlobal' setting}}></span>
+            {{/if}}
+          </div>
+        </div>
+      </div>
+    </form>
+  </div>
+{{/each}}

+ 3 - 46
contrib/views/hive/src/main/resources/ui/hive-web/app/templates/settings.hbs

@@ -17,50 +17,7 @@
 }}
 
 <div class="editor-overlay settings-container fadeIn">
-  <h3> Settings
-    <button class="btn btn-success btn-xs" {{action 'add'}}><i class="fa fa-plus"></i> Add</button>
-    {{#if currentSettings.settings}}
-      <button class="btn btn-danger btn-xs" {{action 'removeAll'}}><i class="fa fa-minus"></i> Remove All</button>
-    {{/if}}
-
-
-    {{#if canInvalidateSession}}
-      <button class="btn btn-danger btn-xs pull-right" {{action 'invalidateSession'}}><i class="fa fa-times"></i> Invalidate Session</button>
-    {{/if}}
-  </h3>
-
-  {{#each setting in currentSettings.settings}}
-    <div class="setting col-md-6 col-sm-12">
-      <form>
-        <div class="form-group">
-          <div class="input-group">
-            <div class="input-group-addon">
-              {{typeahead-widget
-                  options=predefinedSettings
-                  excluded=selectedSettings
-                  optionLabelPath="name"
-                  optionValuePath="name"
-                  selection=setting.key
-                  create="addKey"
-              }}
-            </div>
-            <div {{bind-attr class=":input-group-addon setting.valid::has-error"}}>
-
-              {{#if setting.key.values}}
-                {{select-widget items=setting.key.values
-                                labelPath="value"
-                                selectedValue=setting.selection
-                                defaultLabelTranslation="placeholders.select.value"
-                }}
-              {{else}}
-                {{input class="input-sm form-control" placeholderTranslation="placeholders.select.value" value=setting.selection.value}}
-              {{/if}}
-
-              <span class="fa fa-times-circle remove pull-right" {{action 'remove' setting}}></span>
-            </div>
-          </div>
-        </div>
-      </form>
-    </div>
-  {{/each}}
+  {{#tabs-widget tabs=settingsTabs selectedTab=selectedTab}}
+    {{partial selectedTab.view}}
+  {{/tabs-widget}}
 </div>

+ 10 - 1
contrib/views/hive/src/main/resources/ui/hive-web/app/utils/constants.js

@@ -81,6 +81,8 @@ export default Ember.Object.create({
     tables: 'tables',
     columns: 'columns',
     settings: 'settings',
+    settingsQuery: 'settings-query',
+    settingsGlobal: 'settings-global',
     jobProgress: 'job-progress',
     queryTabs: 'query-tabs'
   },
@@ -152,6 +154,13 @@ export default Ember.Object.create({
     }
   ],
 
+  jobReferrer: {
+    sample: 'sample',
+    explain: 'explain',
+    visualExplain: 'visualExplain',
+    job: 'job'
+  },
+
   statuses: {
     unknown: "UNKNOWN",
     initialized: "INITIALIZED",
@@ -182,7 +191,7 @@ export default Ember.Object.create({
 
   //this can be replaced by a string.format implementation
   adapter: {
-    version: '0.2.0',
+    version: '0.3.0',
     instance: 'Hive',
     apiPrefix: '/api/v1/views/HIVE/versions/',
     instancePrefix: '/instances/',

+ 4 - 0
contrib/views/hive/src/main/resources/ui/hive-web/app/utils/functions.js

@@ -60,5 +60,9 @@ export default Ember.Object.create({
     return args.find(function (arg) {
       return sourceString.match(new RegExp('^' + arg + '$', 'i'));
     });
+  },
+
+  insensitiveContains: function (sourceString, destString) {
+    return sourceString.toLowerCase().indexOf(destString.toLowerCase()) > -1;
   }
 });

+ 4 - 2
contrib/views/hive/src/main/resources/ui/hive-web/app/views/messages.js

@@ -21,15 +21,17 @@ import Ember from 'ember';
 export default Ember.View.extend({
   didInsertElement: function () {
     var target = this.$('#messages');
+    var panel = this.$('#messages .panel-body');
 
-    target.css('min-height', $('.main-content').height());
+    panel.css('min-height', $('.main-content').height());
     target.animate({ width: $('.main-content').width() }, 'fast');
   },
 
   willDestroyElement: function () {
     var target = this.$('#messages');
+    var panel = this.$('#messages .panel-body');
 
-    target.css('min-height', 0);
+    panel.css('min-height', 0);
     target.css('width', 0);
   }
 });

+ 4 - 2
contrib/views/hive/src/main/resources/ui/hive-web/app/views/tez-ui.js

@@ -21,15 +21,17 @@ import Ember from 'ember';
 export default Ember.View.extend({
   didInsertElement: function () {
     var target = this.$('#tez-ui');
+    var panel = this.$('#tez-ui .panel-body');
 
-    target.css('min-height', $('.main-content').height());
+    panel.css('min-height', $('.main-content').height());
     target.animate({ width: $('.main-content').width() }, 'fast');
   },
 
   willDestroyElement: function () {
     var target = this.$('#tez-ui');
+    var panel = this.$('#tez-ui .panel-body');
 
-    target.css('min-height', 0);
+    panel.css('min-height', 0);
     target.css('width', 0);
   }
 });

+ 8 - 3
contrib/views/hive/src/main/resources/ui/hive-web/app/views/visual-explain.js

@@ -32,17 +32,23 @@ export default Ember.View.extend({
     this._super();
 
     var target = this.$('#visual-explain');
+    var panel = this.$('#visual-explain .panel-body');
 
-    target.css('min-height', $('.main-content').height());
+    panel.css('min-height', $('.main-content').height());
     target.animate({ width: $('.main-content').width() }, 'fast');
 
     this.$('#visual-explain-graph').draggable();
+
+    if (this.get('controller.rerender')) {
+      this.renderDag();
+    }
   },
 
   willDestroyElement: function () {
     var target = this.$('#visual-explain');
+    var panel = this.$('#visual-explain .panel-body');
 
-    target.css('min-height', 0);
+    panel.css('min-height', 0);
     target.css('width', 0);
   },
 
@@ -455,4 +461,3 @@ export default Ember.View.extend({
         .renderEdges();
   }
 });
-

+ 2 - 2
contrib/views/hive/src/main/resources/ui/hive-web/tests/unit/controllers/index-test.js

@@ -252,7 +252,7 @@ test('donwloadMenu returns only saveToHDFS if csvUrl is false', function() {
       constructor: {
         typeKey: 'notjob'
       },
-      status: 'SUCCEEDED'
+      status: 'SUCCEEDED',
   });
 
   var controller = this.subject({ content: content });
@@ -271,7 +271,7 @@ test('donwloadMenu returns saveToHDFS and csvUrl', function() {
       constructor: {
         typeKey: 'job'
       },
-      status: 'SUCCEEDED'
+      status: 'SUCCEEDED',
   });
 
   var controller = this.subject({ content: content });

+ 9 - 1
contrib/views/hive/src/main/resources/view.xml

@@ -17,7 +17,7 @@
 <view>
     <name>HIVE</name>
     <label>Hive</label>
-    <version>0.3.0</version>
+    <version>0.4.0</version>
 
     <min-ambari-version>2.0.*</min-ambari-version>
 
@@ -75,6 +75,14 @@
         <required>true</required>
     </parameter>
 
+    <parameter>
+        <name>scripts.settings.defaults-file</name>
+        <description>File path for saving default settings for query</description>
+        <label>Default script settings file</label>
+        <default-value>/user/${username}/.${instanceName}.defaultSettings</default-value>
+        <required>true</required>
+    </parameter>
+
     <parameter>
         <name>hive.host</name>
         <description>Enter the HiveServer2 host. Host must be accessible from Ambari Server.</description>

+ 1 - 1
contrib/views/hive/src/test/java/org/apache/ambari/view/hive/resources/jobs/AggregatorTest.java

@@ -419,4 +419,4 @@ public class AggregatorTest {
       this.hiveQueryIds = hiveQueryIds;
     }
   }
-}
+}