Prechádzať zdrojové kódy

AMBARI-10528. Hive View: Visual Explain, Error handling and bugfixes (alexantonenko)

Alex Antonenko 10 rokov pred
rodič
commit
672eee34dc
100 zmenil súbory, kde vykonal 2785 pridanie a 501 odobranie
  1. 8 3
      contrib/views/hive/pom.xml
  2. 109 0
      contrib/views/hive/src/main/java/org/apache/ambari/view/hive/PropertyValidator.java
  3. 21 23
      contrib/views/hive/src/main/java/org/apache/ambari/view/hive/client/Connection.java
  4. 4 3
      contrib/views/hive/src/main/java/org/apache/ambari/view/hive/client/ConnectionFactory.java
  5. 16 6
      contrib/views/hive/src/main/java/org/apache/ambari/view/hive/client/Cursor.java
  6. 1 1
      contrib/views/hive/src/main/java/org/apache/ambari/view/hive/client/DDLDelegator.java
  7. 1 1
      contrib/views/hive/src/main/java/org/apache/ambari/view/hive/client/HiveErrorStatusException.java
  8. 1 1
      contrib/views/hive/src/main/java/org/apache/ambari/view/hive/client/Utils.java
  9. 52 62
      contrib/views/hive/src/main/java/org/apache/ambari/view/hive/persistence/DataStoreStorage.java
  10. 0 5
      contrib/views/hive/src/main/java/org/apache/ambari/view/hive/persistence/InstanceKeyValueStorage.java
  11. 0 5
      contrib/views/hive/src/main/java/org/apache/ambari/view/hive/persistence/LocalKeyValueStorage.java
  12. 0 9
      contrib/views/hive/src/main/java/org/apache/ambari/view/hive/persistence/Storage.java
  13. 1 1
      contrib/views/hive/src/main/java/org/apache/ambari/view/hive/resources/CRUDResourceManager.java
  14. 30 6
      contrib/views/hive/src/main/java/org/apache/ambari/view/hive/resources/files/FileService.java
  15. 2 1
      contrib/views/hive/src/main/java/org/apache/ambari/view/hive/resources/jobs/Aggregator.java
  16. 6 9
      contrib/views/hive/src/main/java/org/apache/ambari/view/hive/resources/jobs/ConnectionController.java
  17. 69 5
      contrib/views/hive/src/main/java/org/apache/ambari/view/hive/resources/jobs/JobService.java
  18. 1 4
      contrib/views/hive/src/main/java/org/apache/ambari/view/hive/resources/jobs/NoOperationStatusSetException.java
  19. 30 15
      contrib/views/hive/src/main/java/org/apache/ambari/view/hive/resources/jobs/OperationHandleController.java
  20. 1 1
      contrib/views/hive/src/main/java/org/apache/ambari/view/hive/resources/jobs/OperationHandleResourceManager.java
  21. 24 0
      contrib/views/hive/src/main/java/org/apache/ambari/view/hive/resources/jobs/ResultsPaginationController.java
  22. 1 1
      contrib/views/hive/src/main/java/org/apache/ambari/view/hive/resources/jobs/StoredOperationHandle.java
  23. 1 0
      contrib/views/hive/src/main/java/org/apache/ambari/view/hive/resources/jobs/atsJobs/ATSParser.java
  24. 8 0
      contrib/views/hive/src/main/java/org/apache/ambari/view/hive/resources/jobs/atsJobs/ATSRequestsDelegate.java
  25. 22 2
      contrib/views/hive/src/main/java/org/apache/ambari/view/hive/resources/jobs/atsJobs/ATSRequestsDelegateImpl.java
  26. 2 0
      contrib/views/hive/src/main/java/org/apache/ambari/view/hive/resources/jobs/atsJobs/HiveQueryId.java
  27. 8 0
      contrib/views/hive/src/main/java/org/apache/ambari/view/hive/resources/jobs/viewJobs/Job.java
  28. 1 0
      contrib/views/hive/src/main/java/org/apache/ambari/view/hive/resources/jobs/viewJobs/JobController.java
  29. 27 12
      contrib/views/hive/src/main/java/org/apache/ambari/view/hive/resources/jobs/viewJobs/JobControllerImpl.java
  30. 27 1
      contrib/views/hive/src/main/java/org/apache/ambari/view/hive/resources/jobs/viewJobs/JobImpl.java
  31. 3 3
      contrib/views/hive/src/main/java/org/apache/ambari/view/hive/resources/udfs/UDF.java
  32. 3 3
      contrib/views/hive/src/main/java/org/apache/ambari/view/hive/utils/HdfsApi.java
  33. 4 4
      contrib/views/hive/src/main/java/org/apache/ambari/view/hive/utils/HdfsUtil.java
  34. 26 0
      contrib/views/hive/src/main/java/org/apache/ambari/view/hive/utils/HiveClientFormattedException.java
  35. 4 0
      contrib/views/hive/src/main/resources/ui/hive-web/.bowerrc
  36. 34 0
      contrib/views/hive/src/main/resources/ui/hive-web/.editorconfig
  37. 27 0
      contrib/views/hive/src/main/resources/ui/hive-web/.ember-cli
  38. 38 0
      contrib/views/hive/src/main/resources/ui/hive-web/.travis.yml
  39. 5 0
      contrib/views/hive/src/main/resources/ui/hive-web/Brocfile.js
  40. 8 8
      contrib/views/hive/src/main/resources/ui/hive-web/app/components/alert-message-widget.js
  41. 5 0
      contrib/views/hive/src/main/resources/ui/hive-web/app/components/collapsible-widget.js
  42. 22 0
      contrib/views/hive/src/main/resources/ui/hive-web/app/components/modal-widget.js
  43. 32 0
      contrib/views/hive/src/main/resources/ui/hive-web/app/components/notify-widget.js
  44. 17 19
      contrib/views/hive/src/main/resources/ui/hive-web/app/components/number-range-widget.js
  45. 121 0
      contrib/views/hive/src/main/resources/ui/hive-web/app/components/query-tabs.js
  46. 47 2
      contrib/views/hive/src/main/resources/ui/hive-web/app/components/typeahead-widget.js
  47. 51 5
      contrib/views/hive/src/main/resources/ui/hive-web/app/controllers/databases.js
  48. 147 41
      contrib/views/hive/src/main/resources/ui/hive-web/app/controllers/index.js
  49. 25 11
      contrib/views/hive/src/main/resources/ui/hive-web/app/controllers/index/history-query/explain.js
  50. 16 6
      contrib/views/hive/src/main/resources/ui/hive-web/app/controllers/index/history-query/logs.js
  51. 34 0
      contrib/views/hive/src/main/resources/ui/hive-web/app/controllers/index/history-query/results.js
  52. 33 0
      contrib/views/hive/src/main/resources/ui/hive-web/app/controllers/messages.js
  53. 42 0
      contrib/views/hive/src/main/resources/ui/hive-web/app/controllers/modal-save-query.js
  54. 92 28
      contrib/views/hive/src/main/resources/ui/hive-web/app/controllers/open-queries.js
  55. 9 2
      contrib/views/hive/src/main/resources/ui/hive-web/app/controllers/queries.js
  56. 62 1
      contrib/views/hive/src/main/resources/ui/hive-web/app/controllers/settings.js
  57. 2 2
      contrib/views/hive/src/main/resources/ui/hive-web/app/controllers/udf.js
  58. 1 1
      contrib/views/hive/src/main/resources/ui/hive-web/app/helpers/all-uppercase.js
  59. 28 0
      contrib/views/hive/src/main/resources/ui/hive-web/app/helpers/preformatted-string.js
  60. 4 0
      contrib/views/hive/src/main/resources/ui/hive-web/app/index.html
  61. 36 7
      contrib/views/hive/src/main/resources/ui/hive-web/app/initializers/i18n.js
  62. 26 0
      contrib/views/hive/src/main/resources/ui/hive-web/app/initializers/notify.js
  63. 1 1
      contrib/views/hive/src/main/resources/ui/hive-web/app/models/file.js
  64. 5 0
      contrib/views/hive/src/main/resources/ui/hive-web/app/models/job.js
  65. 12 5
      contrib/views/hive/src/main/resources/ui/hive-web/app/routes/application.js
  66. 2 2
      contrib/views/hive/src/main/resources/ui/hive-web/app/routes/index/index.js
  67. 88 0
      contrib/views/hive/src/main/resources/ui/hive-web/app/services/notify.js
  68. 99 37
      contrib/views/hive/src/main/resources/ui/hive-web/app/styles/app.scss
  69. 23 0
      contrib/views/hive/src/main/resources/ui/hive-web/app/styles/mixins.scss
  70. 36 0
      contrib/views/hive/src/main/resources/ui/hive-web/app/styles/notifications.scss
  71. 68 0
      contrib/views/hive/src/main/resources/ui/hive-web/app/styles/query-tabs.scss
  72. 20 0
      contrib/views/hive/src/main/resources/ui/hive-web/app/styles/vars.scss
  73. 2 1
      contrib/views/hive/src/main/resources/ui/hive-web/app/templates/application.hbs
  74. 2 2
      contrib/views/hive/src/main/resources/ui/hive-web/app/templates/components/alert-message-widget.hbs
  75. 6 1
      contrib/views/hive/src/main/resources/ui/hive-web/app/templates/components/collapsible-widget.hbs
  76. 3 5
      contrib/views/hive/src/main/resources/ui/hive-web/app/templates/components/notify-widget.hbs
  77. 10 2
      contrib/views/hive/src/main/resources/ui/hive-web/app/templates/components/panel-widget.hbs
  78. 29 0
      contrib/views/hive/src/main/resources/ui/hive-web/app/templates/components/query-tabs.hbs
  79. 2 1
      contrib/views/hive/src/main/resources/ui/hive-web/app/templates/components/tabs-widget.hbs
  80. 3 1
      contrib/views/hive/src/main/resources/ui/hive-web/app/templates/databases-search-results.hbs
  81. 3 3
      contrib/views/hive/src/main/resources/ui/hive-web/app/templates/databases-tree.hbs
  82. 2 2
      contrib/views/hive/src/main/resources/ui/hive-web/app/templates/databases.hbs
  83. 7 17
      contrib/views/hive/src/main/resources/ui/hive-web/app/templates/index.hbs
  84. 3 3
      contrib/views/hive/src/main/resources/ui/hive-web/app/templates/index/history-query/results.hbs
  85. 28 26
      contrib/views/hive/src/main/resources/ui/hive-web/app/templates/insert-udfs.hbs
  86. 36 0
      contrib/views/hive/src/main/resources/ui/hive-web/app/templates/message.hbs
  87. 30 0
      contrib/views/hive/src/main/resources/ui/hive-web/app/templates/messages.hbs
  88. 1 1
      contrib/views/hive/src/main/resources/ui/hive-web/app/templates/modal-delete.hbs
  89. 24 0
      contrib/views/hive/src/main/resources/ui/hive-web/app/templates/modal-save-query.hbs
  90. 23 0
      contrib/views/hive/src/main/resources/ui/hive-web/app/templates/notification.hbs
  91. 1 1
      contrib/views/hive/src/main/resources/ui/hive-web/app/templates/open-queries.hbs
  92. 43 36
      contrib/views/hive/src/main/resources/ui/hive-web/app/templates/settings.hbs
  93. 2 0
      contrib/views/hive/src/main/resources/ui/hive-web/app/templates/tez-ui.hbs
  94. 58 0
      contrib/views/hive/src/main/resources/ui/hive-web/app/templates/visual-explain.hbs
  95. 22 1
      contrib/views/hive/src/main/resources/ui/hive-web/app/utils/constants.js
  96. 141 0
      contrib/views/hive/src/main/resources/ui/hive-web/app/utils/dag-rules.js
  97. 12 23
      contrib/views/hive/src/main/resources/ui/hive-web/app/views/message.js
  98. 51 0
      contrib/views/hive/src/main/resources/ui/hive-web/app/views/notification.js
  99. 401 0
      contrib/views/hive/src/main/resources/ui/hive-web/app/views/visual-explain.js
  100. 8 10
      contrib/views/hive/src/main/resources/ui/hive-web/bower.json

+ 8 - 3
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.1.0-SNAPSHOT</version>
+  <version>0.2.0-SNAPSHOT</version>
   <name>Hive</name>
 
   <parent>
@@ -30,6 +30,11 @@
 
   <dependencies>
     <dependency>
+      <groupId>com.jayway.jsonpath</groupId>
+      <artifactId>json-path</artifactId>
+      <version>2.0.0</version>
+    </dependency>
+      <dependency>
       <groupId>com.google.inject</groupId>
       <artifactId>guice</artifactId>
     </dependency>
@@ -195,8 +200,8 @@
         <artifactId>frontend-maven-plugin</artifactId>
         <version>0.0.14</version>
         <configuration>
-          <nodeVersion>v0.10.32</nodeVersion>
-          <npmVersion>1.4.3</npmVersion>
+          <nodeVersion>v0.12.2</nodeVersion>
+          <npmVersion>1.4.8</npmVersion>
           <workingDirectory>src/main/resources/ui/hive-web/</workingDirectory>
         </configuration>
         <executions>

+ 109 - 0
contrib/views/hive/src/main/java/org/apache/ambari/view/hive/PropertyValidator.java

@@ -0,0 +1,109 @@
+/**
+ * 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.
+ */
+
+package org.apache.ambari.view.hive;
+
+import org.apache.ambari.view.ViewInstanceDefinition;
+import org.apache.ambari.view.validation.ValidationResult;
+import org.apache.ambari.view.validation.Validator;
+
+import java.net.URI;
+import java.net.URISyntaxException;
+
+public class PropertyValidator implements Validator {
+
+  public static final String WEBHDFS_URL = "webhdfs.url";
+  public static final String HIVE_PORT = "hive.port";
+  public static final String YARN_ATS_URL = "yarn.ats.url";
+  public static final String HIVE_AUTH = "hive.auth";
+
+  @Override
+  public ValidationResult validateInstance(ViewInstanceDefinition viewInstanceDefinition, ValidationContext validationContext) {
+    return null;
+  }
+
+  @Override
+  public ValidationResult validateProperty(String property, ViewInstanceDefinition viewInstanceDefinition, ValidationContext validationContext) {
+    if (property.equals(WEBHDFS_URL)) {
+      String webhdfsUrl = viewInstanceDefinition.getPropertyMap().get(WEBHDFS_URL);
+      if (validateURL(webhdfsUrl)) return new InvalidPropertyValidationResult(false, "Must be valid URL");
+    }
+
+    if (property.equals(HIVE_PORT)) {
+      String hivePort = viewInstanceDefinition.getPropertyMap().get(HIVE_PORT);
+      try {
+        int port = Integer.valueOf(hivePort);
+        if (port < 1 || port > 65535) {
+          return new InvalidPropertyValidationResult(false, "Must be from 1 to 65535");
+        }
+      } catch (NumberFormatException e) {
+        return new InvalidPropertyValidationResult(false, "Must be integer");
+      }
+    }
+
+    if (property.equals(YARN_ATS_URL)) {
+      String atsUrl = viewInstanceDefinition.getPropertyMap().get(YARN_ATS_URL);
+      if (validateURL(atsUrl)) return new InvalidPropertyValidationResult(false, "Must be valid URL");
+    }
+
+    if (property.equals(HIVE_AUTH)) {
+      String auth = viewInstanceDefinition.getPropertyMap().get(HIVE_AUTH);
+
+      if (auth != null && !auth.isEmpty()) {
+        for(String param : auth.split(";")) {
+          String[] keyvalue = param.split("=");
+          if (keyvalue.length != 2) {
+            return new InvalidPropertyValidationResult(false, "Can not parse authentication param " + param + " in " + auth);
+          }
+        }
+      }
+    }
+
+    return ValidationResult.SUCCESS;
+  }
+
+  public boolean validateURL(String webhdfsUrl) {
+    try {
+      new URI(webhdfsUrl);
+    } catch (URISyntaxException e) {
+      return true;
+    }
+    return false;
+  }
+
+  public static class InvalidPropertyValidationResult implements ValidationResult {
+    private boolean valid;
+    private String detail;
+
+    public InvalidPropertyValidationResult(boolean valid, String detail) {
+      this.valid = valid;
+      this.detail = detail;
+    }
+
+    @Override
+    public boolean isValid() {
+      return valid;
+    }
+
+    @Override
+    public String getDetail() {
+      return detail;
+    }
+  }
+
+}

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

@@ -82,7 +82,7 @@ public class Connection {
       transport.open();
       client = new TCLIService.Client(new TBinaryProtocol(transport));
     } catch (TTransportException e) {
-      throw new HiveClientException("Could not establish connecton to "
+      throw new HiveClientException("H020 Could not establish connecton to "
           + host + ":" + port + ": " + e.toString(), e);
     }
     LOG.info("Hive connection opened");
@@ -109,7 +109,7 @@ public class Connection {
             try {
               saslQOP = SaslQOP.fromString(authParams.get(Utils.HiveAuthenticationParams.AUTH_QOP));
             } catch (IllegalArgumentException e) {
-              throw new HiveClientException("Invalid " + Utils.HiveAuthenticationParams.AUTH_QOP +
+              throw new HiveClientException("H040 Invalid " + Utils.HiveAuthenticationParams.AUTH_QOP +
                   " parameter. " + e.getMessage(), e);
             }
           }
@@ -162,7 +162,7 @@ public class Connection {
         return HiveAuthFactory.getSocketTransport(host, port, 10000);
       }
     } catch (SaslException e) {
-      throw new HiveClientException("Could not create secure connection to "
+      throw new HiveClientException("H040 Could not create secure connection to "
           + host + ": " + e.getMessage(), e);
     }
     return transport;
@@ -181,7 +181,7 @@ public class Connection {
         tokenStr = ShimLoader.getHadoopShims().
             getTokenStrForm(HiveAuthFactory.HS2_CLIENT_TOKEN);
       } catch (IOException e) {
-        throw new HiveClientException("Error reading token ", e);
+        throw new HiveClientException("H050 Error reading token", e);
       }
     }
     return tokenStr;
@@ -205,12 +205,12 @@ public class Connection {
         try {
           return client.OpenSession(openReq);
         } catch (TException e) {
-          throw new HiveClientException("Unable to open Hive session", e);
+          throw new HiveClientException("H060 Unable to open Hive session", e);
         }
 
       }
     }.call();
-    Utils.verifySuccess(openResp.getStatus(), "Unable to open Hive session");
+    Utils.verifySuccess(openResp.getStatus(), "H070 Unable to open Hive session");
 
     if (protocol == null)
       protocol = openResp.getServerProtocolVersion();
@@ -231,7 +231,7 @@ public class Connection {
   public TSessionHandle getSessionByTag(String tag) throws HiveClientException {
     TSessionHandle sessionHandle = sessHandles.get(tag);
     if (sessionHandle == null) {
-      throw new HiveClientException("Session with provided tag not found", null);
+      throw new HiveClientException("E030 Session with provided tag not found", null);
     }
     return sessionHandle;
   }
@@ -244,15 +244,21 @@ public class Connection {
     }
   }
 
+  public void invalidateSessionByTag(String tag) throws HiveClientException {
+    TSessionHandle sessionHandle = getSessionByTag(tag);
+    closeSession(sessionHandle);
+    sessHandles.remove(tag);
+  }
+
   private synchronized void closeSession(TSessionHandle sessHandle) throws HiveClientException {
     if (sessHandle == null) return;
     TCloseSessionReq closeReq = new TCloseSessionReq(sessHandle);
     TCloseSessionResp closeResp = null;
     try {
       closeResp = client.CloseSession(closeReq);
-      Utils.verifySuccess(closeResp.getStatus(), "Unable to close Hive session");
+      Utils.verifySuccess(closeResp.getStatus(), "H080 Unable to close Hive session");
     } catch (TException e) {
-      throw new HiveClientException("Unable to close Hive session", e);
+      throw new HiveClientException("H090 Unable to close Hive session", e);
     }
     LOG.info("Hive session closed");
   }
@@ -314,18 +320,18 @@ public class Connection {
           try {
             return client.ExecuteStatement(execReq);
           } catch (TException e) {
-            throw new HiveClientException("Unable to submit statement " + cmd, e);
+            throw new HiveClientException("H100 Unable to submit statement " + cmd, e);
           }
 
         }
       }.call();
 
-      Utils.verifySuccess(execResp.getStatus(), "Unable to submit statement " + cmd);
+      Utils.verifySuccess(execResp.getStatus(), "H110 Unable to submit statement");
       //TODO: check if status have results
       handle = execResp.getOperationHandle();
     }
     if (handle == null) {
-      throw new HiveClientException("Empty command given", null);
+      throw new HiveClientException("H120 Empty command given", null);
     }
     return handle;
   }
@@ -373,19 +379,11 @@ public class Connection {
         try {
           return client.GetOperationStatus(statusReq);
         } catch (TException e) {
-          throw new HiveClientException("Unable to fetch operation status", e);
+          throw new HiveClientException("H130 Unable to fetch operation status", e);
         }
 
       }
     }.call();
-//    transportLock.lock();
-//    try {
-//      return client.GetOperationStatus(statusReq);
-//    } catch (TException e) {
-//      throw new HiveClientException("Unable to fetch operation status", e);
-//    } finally {
-//      transportLock.unlock();
-//    }
   }
 
   /**
@@ -400,11 +398,11 @@ public class Connection {
         try {
           return client.CancelOperation(cancelReq);
         } catch (TException e) {
-          throw new HiveClientException("Unable to cancel operation", null);
+          throw new HiveClientException("H140 Unable to cancel operation", null);
         }
       }
     }.call();
-    Utils.verifySuccess(cancelResp.getStatus(), "Unable to cancel operation");
+    Utils.verifySuccess(cancelResp.getStatus(), "H150 Unable to cancel operation");
   }
 
   public int getPort() {

+ 4 - 3
contrib/views/hive/src/main/java/org/apache/ambari/view/hive/client/ConnectionFactory.java

@@ -19,6 +19,7 @@
 package org.apache.ambari.view.hive.client;
 
 import org.apache.ambari.view.ViewContext;
+import org.apache.ambari.view.hive.utils.HiveClientFormattedException;
 import org.apache.ambari.view.hive.utils.ServiceFormattedException;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -41,7 +42,7 @@ public class ConnectionFactory implements IConnectionFactory {
       return new Connection(getHiveHost(), Integer.valueOf(getHivePort()),
           getHiveAuthParams(), context.getUsername());
     } catch (HiveClientException e) {
-      throw new ServiceFormattedException("Couldn't open connection to Hive: " + e.toString(), e);
+      throw new HiveClientFormattedException(e);
     }
   }
 
@@ -62,8 +63,8 @@ public class ConnectionFactory implements IConnectionFactory {
     for(String param : auth.split(";")) {
       String[] keyvalue = param.split("=");
       if (keyvalue.length != 2) {
-        LOG.error("Can not parse authentication param " + param + " in " + auth);
-        continue;
+        //Should never happen because validator already checked this
+        throw new ServiceFormattedException("H010 Can not parse authentication param " + param + " in " + auth);
       }
       params.put(keyvalue[0], keyvalue[1]);
     }

+ 16 - 6
contrib/views/hive/src/main/java/org/apache/ambari/view/hive/client/Cursor.java

@@ -21,6 +21,7 @@ package org.apache.ambari.view.hive.client;
 import static org.apache.hive.service.cli.thrift.TCLIServiceConstants.TYPE_NAMES;
 
 import org.apache.ambari.view.hive.utils.BadRequestFormattedException;
+import org.apache.ambari.view.hive.utils.HiveClientFormattedException;
 import org.apache.hive.service.cli.RowSet;
 import org.apache.hive.service.cli.RowSetFactory;
 import org.apache.hive.service.cli.thrift.*;
@@ -73,12 +74,12 @@ public class Cursor implements Iterator<Row>, Iterable<Row> {
         try {
           return client.FetchResults(fetchReq);
         } catch (TException e) {
-          throw new HiveClientException("Unable to fetch results", e);
+          throw new HiveClientException("H160 Unable to fetch results", e);
         }
 
       }
     }.call();
-    Utils.verifySuccess(fetchResp.getStatus(), "Unable to fetch results");
+    Utils.verifySuccess(fetchResp.getStatus(), "H170 Unable to fetch results");
     TRowSet results = fetchResp.getResults();
     fetched = RowSetFactory.create(results, connection.getProtocol());
     fetchedIterator = fetched.iterator();
@@ -90,7 +91,6 @@ public class Cursor implements Iterator<Row>, Iterable<Row> {
 
   public ArrayList<ColumnDescription> getSchema() throws HiveClientException {
     if (this.schema == null) {
-      // TODO: extract all HiveCall inline classes to separate files
       TGetResultSetMetadataResp fetchResp = new HiveCall<TGetResultSetMetadataResp>(connection) {
         @Override
         public TGetResultSetMetadataResp body() throws HiveClientException {
@@ -99,12 +99,12 @@ public class Cursor implements Iterator<Row>, Iterable<Row> {
           try {
             return client.GetResultSetMetadata(fetchReq);
           } catch (TException e) {
-            throw new HiveClientException("Unable to fetch results metadata", e);
+            throw new HiveClientException("H180 Unable to fetch results metadata", e);
           }
 
         }
       }.call();
-      Utils.verifySuccess(fetchResp.getStatus(), "Unable to fetch results metadata");
+      Utils.verifySuccess(fetchResp.getStatus(), "H190 Unable to fetch results metadata");
       TTableSchema schema = fetchResp.getSchema();
 
       List<TColumnDesc> thriftColumns = schema.getColumns();
@@ -168,7 +168,7 @@ public class Cursor implements Iterator<Row>, Iterable<Row> {
       try {
         fetchNextBlock();
       } catch (HiveClientException e) {
-        throw new HiveClientRuntimeException(e.getMessage(), e);
+        throw new HiveClientFormattedException(e);
       }
     }
   }
@@ -209,6 +209,16 @@ public class Cursor implements Iterator<Row>, Iterable<Row> {
     return read;
   }
 
+  public Row getHeadersRow() throws HiveClientException {
+    ArrayList<ColumnDescription> schema = getSchema();
+
+    Object[] row = new Object[schema.size()];
+    for (ColumnDescription columnDescription : schema) {
+      row[columnDescription.getPosition()-1] = columnDescription.getName();
+    }
+    return new Row(row, selectedColumns);
+  }
+
   public int readRaw(ArrayList<Object[]> rows, int count) {
     int read = 0;
     while(read < count && hasNext()) {

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

@@ -129,7 +129,7 @@ public class DDLDelegator {
         try {
           return connection.getClient().GetColumns(req);
         } catch (TException e) {
-          throw new HiveClientException("Unable to get table columns", e);
+          throw new HiveClientException("H200 Unable to get table columns", e);
         }
       }
 

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

@@ -25,6 +25,6 @@ import org.apache.hive.service.cli.thrift.TStatusCode;
  */
 public class HiveErrorStatusException extends HiveClientException {
   public HiveErrorStatusException(TStatusCode statusCode, String comment) {
-    super(String.format("Failed with status %s: %s", statusCode, comment), null);
+    super(String.format("%s [%s]", comment, statusCode), null);
   }
 }

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

@@ -26,7 +26,7 @@ public class Utils {
     if (status.getStatusCode() != TStatusCode.SUCCESS_STATUS &&
         status.getStatusCode() != TStatusCode.SUCCESS_WITH_INFO_STATUS) {
       String message = (status.getErrorMessage() != null) ? status.getErrorMessage() : "";
-      throw new HiveErrorStatusException(status.getStatusCode(), message + ": " + comment);
+      throw new HiveErrorStatusException(status.getStatusCode(), comment + ". " + message);
     }
   }
 

+ 52 - 62
contrib/views/hive/src/main/java/org/apache/ambari/view/hive/persistence/DataStoreStorage.java

@@ -25,10 +25,14 @@ import org.apache.ambari.view.hive.persistence.utils.Indexed;
 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.ServiceFormattedException;
+import org.apache.commons.beanutils.BeanUtils;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 import javax.ws.rs.WebApplicationException;
+import java.beans.Transient;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
 import java.util.ArrayList;
 import java.util.LinkedList;
 import java.util.List;
@@ -52,14 +56,52 @@ public class DataStoreStorage implements Storage {
 
   @Override
   public synchronized void store(Class model, Indexed obj) {
+    assignId(model, obj);
+
+    Indexed newBean;
     try {
-      if (obj.getId() == null) {
-        String id = nextIdForEntity(context, model);
-        obj.setId(id);
-      }
-      context.getDataStore().store(obj);
+      newBean = (Indexed) BeanUtils.cloneBean(obj);
+    } catch (IllegalAccessException e) {
+      throw new ServiceFormattedException("S010 Data storage error", e);
+    } catch (InstantiationException e) {
+      throw new ServiceFormattedException("S010 Data storage error", e);
+    } catch (InvocationTargetException e) {
+      throw new ServiceFormattedException("S010 Data storage error", e);
+    } catch (NoSuchMethodException e) {
+      throw new ServiceFormattedException("S010 Data storage error", e);
+    }
+    preprocessEntity(newBean);
+
+    try {
+      context.getDataStore().store(newBean);
     } catch (PersistenceException e) {
-      throw new ServiceFormattedException("Error while saving object to DataStorage", e);
+      throw new ServiceFormattedException("S020 Data storage error", e);
+    }
+  }
+
+  public void assignId(Class model, Indexed obj) {
+    if (obj.getId() == null) {
+      String id = nextIdForEntity(context, model);
+      obj.setId(id);
+    }
+  }
+
+  private void preprocessEntity(Indexed obj) {
+    cleanTransientFields(obj);
+  }
+
+  private void cleanTransientFields(Indexed obj) {
+    for (Method m : obj.getClass().getMethods()) {
+      Transient aTransient = m.getAnnotation(Transient.class);
+      if (aTransient != null && m.getName().startsWith("set")) {
+        try {
+          m.invoke(obj, new Object[]{ null });
+        } catch (IllegalAccessException e) {
+          throw new ServiceFormattedException("S030 Data storage error", e);
+        } catch (InvocationTargetException e) {
+          throw new ServiceFormattedException("S030 Data storage error", e);
+        }
+      }
     }
   }
 
@@ -87,7 +129,7 @@ public class DataStoreStorage implements Storage {
         throw new ItemNotFound();
       }
     } catch (PersistenceException e) {
-      throw new ServiceFormattedException("Error while finding object in DataStorage", e);
+      throw new ServiceFormattedException("S040 Data storage error", e);
     }
   }
 
@@ -96,26 +138,15 @@ public class DataStoreStorage implements Storage {
     LinkedList<T> list = new LinkedList<T>();
     LOG.debug(String.format("Loading all %s-s", model.getName()));
     try {
-      //TODO: use WHERE statement instead of this ugly filter
       for(T item: context.getDataStore().findAll(model, filter.whereStatement())) {
         list.add(item);
       }
     } catch (PersistenceException e) {
-      throw new ServiceFormattedException("Error while finding all objects in DataStorage", e);
+      throw new ServiceFormattedException("S050 Data storage error", e);
     }
     return list;
   }
 
-  @Override
-  public <T extends Indexed> List<T> loadWhere(Class<T> model, String where) {
-    LOG.debug(String.format("Loading all %s-s", model.getName()));
-    try {
-      return new ArrayList<T>(context.getDataStore().findAll(model, where));
-    } catch (PersistenceException e) {
-      throw new ServiceFormattedException("Error while finding objects in DataStorage; where = " + where, e);
-    }
-  }
-
   @Override
   public synchronized <T extends Indexed> List<T> loadAll(Class<T> model) {
     return loadAll(model, new OnlyOwnersFilteringStrategy(this.context.getUsername()));
@@ -128,7 +159,7 @@ public class DataStoreStorage implements Storage {
     try {
       context.getDataStore().remove(obj);
     } catch (PersistenceException e) {
-      throw new ServiceFormattedException("Error while removing object from DataStorage", e);
+      throw new ServiceFormattedException("S060 Data storage error", e);
     }
   }
 
@@ -137,48 +168,7 @@ public class DataStoreStorage implements Storage {
     try {
       return context.getDataStore().find(model, id) != null;
     } catch (PersistenceException e) {
-      throw new ServiceFormattedException("Error while finding object in DataStorage", e);
-    }
-  }
-
-  public static void storageSmokeTest(ViewContext context) {
-    try {
-      SmokeTestEntity entity = new SmokeTestEntity();
-      entity.setData("42");
-      DataStoreStorage storage = new DataStoreStorage(context);
-      storage.store(SmokeTestEntity.class, entity);
-
-      if (entity.getId() == null) throw new ServiceFormattedException("Ambari Views instance data DB doesn't work properly (auto increment id doesn't work)", null);
-      Object id = entity.getId();
-      SmokeTestEntity entity2 = storage.load(SmokeTestEntity.class, id);
-      boolean status = entity2.getData().compareTo("42") == 0;
-      storage.delete(SmokeTestEntity.class, id);
-      if (!status) throw new ServiceFormattedException("Ambari Views instance data DB doesn't work properly", null);
-    } catch (WebApplicationException ex) {
-      throw ex;
-    } catch (Exception ex) {
-      throw new ServiceFormattedException(ex.getMessage(), ex);
-    }
-  }
-
-  public static class SmokeTestEntity implements Indexed {
-    private String id = null;
-    private String data = null;
-
-    public String getId() {
-      return id;
-    }
-
-    public void setId(String id) {
-      this.id = id;
-    }
-
-    public String getData() {
-      return data;
-    }
-
-    public void setData(String data) {
-      this.data = data;
+      throw new ServiceFormattedException("S070 Data storage error", e);
     }
   }
 }

+ 0 - 5
contrib/views/hive/src/main/java/org/apache/ambari/view/hive/persistence/InstanceKeyValueStorage.java

@@ -132,9 +132,4 @@ public class InstanceKeyValueStorage extends KeyValueStorage {
       throw new ServiceFormattedException(ex.getMessage(), ex);
     }
   }
-
-  @Override
-  public <T extends Indexed> List<T> loadWhere(Class<T> model, String where) {
-    throw new NotImplementedException();
-  }
 }

+ 0 - 5
contrib/views/hive/src/main/java/org/apache/ambari/view/hive/persistence/LocalKeyValueStorage.java

@@ -70,9 +70,4 @@ public class LocalKeyValueStorage extends KeyValueStorage {
     }
     return config;
   }
-
-  @Override
-  public <T extends Indexed> List<T> loadWhere(Class<T> model, String where) {
-    throw new NotImplementedException();
-  }
 }

+ 0 - 9
contrib/views/hive/src/main/java/org/apache/ambari/view/hive/persistence/Storage.java

@@ -52,15 +52,6 @@ public interface Storage {
    */
   <T extends Indexed> List<T> loadAll(Class<? extends T> model, FilteringStrategy filter);
 
-  /**
-   * Load all objects of given bean class
-   * @param model bean class
-   * @param where filtering strategy (where clause)
-   * @param <T> bean class
-   * @return list of filtered objects
-   */
-  <T extends Indexed> List<T> loadWhere(Class<T> model, String where);
-
   /**
    * Load all objects of given bean class
    * @param model bean class

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

@@ -124,7 +124,7 @@ abstract public class CRUDResourceManager<T extends Indexed> implements IResourc
     try {
       delete(object.getId());
     } catch (ItemNotFound itemNotFound) {
-      throw new ServiceFormattedException("Error in creation, during clean up: " + itemNotFound.toString(), itemNotFound);
+      throw new ServiceFormattedException("E040 Item not found", itemNotFound);
     }
     throw e;
   }

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

@@ -19,11 +19,13 @@
 package org.apache.ambari.view.hive.resources.files;
 
 import com.google.inject.Inject;
+import com.jayway.jsonpath.JsonPath;
 import org.apache.ambari.view.ViewContext;
 import org.apache.ambari.view.ViewResourceHandler;
 import org.apache.ambari.view.hive.BaseService;
 import org.apache.ambari.view.hive.utils.*;
 import org.apache.commons.codec.binary.Base64;
+import org.apache.commons.io.IOUtils;
 import org.apache.hadoop.fs.FSDataOutputStream;
 import org.apache.hadoop.fs.FileAlreadyExistsException;
 import org.json.simple.JSONObject;
@@ -38,6 +40,9 @@ import javax.ws.rs.core.Response;
 import javax.ws.rs.core.UriInfo;
 import java.io.FileNotFoundException;
 import java.io.IOException;
+import java.io.InputStream;
+import java.net.URL;
+import java.util.HashMap;
 
 /**
  * File access resource
@@ -53,6 +58,7 @@ import java.io.IOException;
  */
 public class FileService extends BaseService {
   public static final String FAKE_FILE = "fakefile://";
+  public static final String JSON_PATH_FILE = "jsonpath:";
 
   @Inject
   ViewResourceHandler handler;
@@ -78,10 +84,17 @@ public class FileService extends BaseService {
         if (page > 1)
           throw new IllegalArgumentException("There's only one page in fake files");
 
-        String content = filePath.substring(FAKE_FILE.length());
+        String encodedContent = filePath.substring(FAKE_FILE.length());
+        String content = new String(Base64.decodeBase64(encodedContent));
 
         fillFakeFileObject(filePath, file, content);
-      } else {
+      } else if (filePath.startsWith(JSON_PATH_FILE)) {
+        if (page > 1)
+          throw new IllegalArgumentException("There's only one page in fake files");
+
+        String content = getJsonPathContentByUrl(filePath);
+        fillFakeFileObject(filePath, file, content);
+      } else  {
         FilePaginator paginator = new FilePaginator(filePath, getSharedObjectsFactory().getHdfsApi());
 
         fillRealFileObject(filePath, page, file, paginator);
@@ -101,6 +114,19 @@ public class FileService extends BaseService {
     }
   }
 
+  protected String getJsonPathContentByUrl(String filePath) throws IOException {
+    URL url = new URL(filePath.substring(JSON_PATH_FILE.length()));
+
+    InputStream responseInputStream = context.getURLStreamProvider().readFrom(url.toString(), "GET",
+        null, new HashMap<String, String>());
+    String response = IOUtils.toString(responseInputStream);
+
+    for (String ref : url.getRef().split("!")) {
+      response = JsonPath.read(response, ref);
+    }
+    return response;
+  }
+
   public void fillRealFileObject(String filePath, Long page, FileResource file, FilePaginator paginator) throws IOException, InterruptedException {
     file.setFilePath(filePath);
     file.setFileContent(paginator.readPage(page));
@@ -109,9 +135,7 @@ public class FileService extends BaseService {
     file.setPageCount(paginator.pageCount());
   }
 
-  public void fillFakeFileObject(String filePath, FileResource file, String encodedContent) {
-    String content = new String(Base64.decodeBase64(encodedContent));
-
+  public void fillFakeFileObject(String filePath, FileResource file, String content) {
     file.setFilePath(filePath);
     file.setFileContent(content);
     file.setHasNext(false);
@@ -176,7 +200,7 @@ public class FileService extends BaseService {
         }
         output.close();
       } catch (FileAlreadyExistsException ex) {
-        throw new ServiceFormattedException(ex.getMessage(), ex, 400);
+        throw new ServiceFormattedException("F020 File already exists", ex, 400);
       }
       response.setHeader("Location",
           String.format("%s/%s", ui.getAbsolutePath().toString(), request.file.getFilePath()));

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

@@ -23,6 +23,7 @@ import org.apache.ambari.view.hive.persistence.utils.Indexed;
 import org.apache.ambari.view.hive.persistence.utils.ItemNotFound;
 import org.apache.ambari.view.hive.persistence.utils.OnlyOwnersFilteringStrategy;
 import org.apache.ambari.view.hive.resources.IResourceManager;
+import org.apache.ambari.view.hive.resources.files.FileService;
 import org.apache.ambari.view.hive.resources.jobs.atsJobs.HiveQueryId;
 import org.apache.ambari.view.hive.resources.jobs.atsJobs.IATSParser;
 import org.apache.ambari.view.hive.resources.jobs.atsJobs.TezDagId;
@@ -192,7 +193,7 @@ public class Aggregator {
     String query = atsHiveQuery.query;
     atsJob.setTitle(query.substring(0, (query.length() > 42)?42:query.length()));
 
-    atsJob.setQueryFile("fakefile://" + Base64.encodeBase64URLSafeString(query.getBytes()));  // fake queryFile
+    atsJob.setQueryFile(FileService.JSON_PATH_FILE + atsHiveQuery.url + "#otherinfo.QUERY!queryText");
     return atsJob;
   }
 

+ 6 - 9
contrib/views/hive/src/main/java/org/apache/ambari/view/hive/resources/jobs/ConnectionController.java

@@ -20,6 +20,7 @@ package org.apache.ambari.view.hive.resources.jobs;
 
 import org.apache.ambari.view.hive.client.Connection;
 import org.apache.ambari.view.hive.client.HiveClientException;
+import org.apache.ambari.view.hive.utils.HiveClientFormattedException;
 import org.apache.ambari.view.hive.utils.ServiceFormattedException;
 import org.apache.commons.codec.binary.Hex;
 import org.apache.hive.service.cli.thrift.TOperationHandle;
@@ -35,12 +36,8 @@ public class ConnectionController {
     this.operationHandleControllerFactory = operationHandleControllerFactory;
   }
 
-  public TSessionHandle getSessionByTag(String tag) {
-    try {
-      return connection.getSessionByTag(tag);
-    } catch (HiveClientException e) {
-      throw new ServiceFormattedException(e.toString(), e);
-    }
+  public TSessionHandle getSessionByTag(String tag) throws HiveClientException {
+    return connection.getSessionByTag(tag);
   }
 
   public String openSession() {
@@ -48,7 +45,7 @@ public class ConnectionController {
       TSessionHandle sessionHandle = connection.openSession();
       return getTagBySession(sessionHandle);
     } catch (HiveClientException e) {
-      throw new ServiceFormattedException(e.toString(), e);
+      throw new HiveClientFormattedException(e);
     }
   }
 
@@ -60,7 +57,7 @@ public class ConnectionController {
     try {
       connection.executeSync(session, "use " + database + ";");
     } catch (HiveClientException e) {
-      throw new ServiceFormattedException(e.toString(), e);
+      throw new HiveClientFormattedException(e);
     }
   }
 
@@ -69,7 +66,7 @@ public class ConnectionController {
     try {
       operationHandle = connection.executeAsync(session, cmd);
     } catch (HiveClientException e) {
-      throw new ServiceFormattedException(e.toString(), e);
+      throw new HiveClientFormattedException(e);
     }
     return operationHandleControllerFactory.createControllerForHandle(operationHandle);
   }

+ 69 - 5
contrib/views/hive/src/main/java/org/apache/ambari/view/hive/resources/jobs/JobService.java

@@ -22,7 +22,9 @@ import com.google.inject.Inject;
 import org.apache.ambari.view.ViewResourceHandler;
 import org.apache.ambari.view.hive.BaseService;
 import org.apache.ambari.view.hive.backgroundjobs.BackgroundJobController;
+import org.apache.ambari.view.hive.client.Connection;
 import org.apache.ambari.view.hive.client.Cursor;
+import org.apache.ambari.view.hive.client.HiveClientException;
 import org.apache.ambari.view.hive.persistence.utils.ItemNotFound;
 import org.apache.ambari.view.hive.resources.jobs.atsJobs.ATSRequestsDelegate;
 import org.apache.ambari.view.hive.resources.jobs.atsJobs.ATSRequestsDelegateImpl;
@@ -125,7 +127,7 @@ public class JobService extends BaseService {
     try {
       mergedJob = getAggregator().readATSJob(hiveJob);
     } catch (ItemNotFound itemNotFound) {
-      throw new ServiceFormattedException("Job not found", itemNotFound);
+      throw new ServiceFormattedException("E010 Job not found", itemNotFound);
     }
     Map createdJobMap = PropertyUtils.describe(mergedJob);
     createdJobMap.remove("class"); // no need to show Bean class on client
@@ -143,6 +145,7 @@ public class JobService extends BaseService {
   @Produces("text/csv")
   public Response getResultsCSV(@PathParam("jobId") String jobId,
                                 @Context HttpServletResponse response,
+                                @QueryParam("fileName") String fileName,
                                 @QueryParam("columns") final String requestedColumns) {
     try {
       JobController jobController = getResourceManager().readController(jobId);
@@ -155,6 +158,13 @@ public class JobService extends BaseService {
           Writer writer = new BufferedWriter(new OutputStreamWriter(os));
           CSVPrinter csvPrinter = new CSVPrinter(writer, CSVFormat.DEFAULT);
           try {
+
+            try {
+              csvPrinter.printRecord(resultSet.getHeadersRow().getRow());
+            } catch (HiveClientException e) {
+              LOG.error("Error on reading results header", e);
+            }
+
             while (resultSet.hasNext()) {
               csvPrinter.printRecord(resultSet.next().getRow());
               writer.flush();
@@ -165,7 +175,13 @@ public class JobService extends BaseService {
         }
       };
 
-      return Response.ok(stream).build();
+      if (fileName == null || fileName.isEmpty()) {
+        fileName = "results.csv";
+      }
+
+      return Response.ok(stream).
+          header("Content-Disposition", String.format("attachment; filename=\"%s\"", fileName)).
+          build();
     } catch (WebApplicationException ex) {
       throw ex;
     } catch (ItemNotFound itemNotFound) {
@@ -216,11 +232,11 @@ public class JobService extends BaseService {
               stream.close();
 
             } catch (IOException e) {
-              throw new ServiceFormattedException("Could not write CSV to HDFS for job#" + jobController.getJob().getId(), e);
+              throw new ServiceFormattedException("F010 Could not write CSV to HDFS for job#" + jobController.getJob().getId(), e);
             } catch (InterruptedException e) {
-              throw new ServiceFormattedException("Could not write CSV to HDFS for job#" + jobController.getJob().getId(), e);
+              throw new ServiceFormattedException("F010 Could not write CSV to HDFS for job#" + jobController.getJob().getId(), e);
             } catch (ItemNotFound itemNotFound) {
-              throw new NotFoundFormattedException("Job results are expired", itemNotFound);
+              throw new NotFoundFormattedException("E020 Job results are expired", itemNotFound);
             }
 
           }
@@ -261,6 +277,8 @@ public class JobService extends BaseService {
                              @QueryParam("columns") final String requestedColumns) {
     try {
       final JobController jobController = getResourceManager().readController(jobId);
+      if (!jobController.hasResults())
+        return ResultsPaginationController.emptyResponse().build();
 
       return ResultsPaginationController.getInstance(context)
            .request(jobId, searchId, true, fromBeginning, count,
@@ -384,6 +402,52 @@ public class JobService extends BaseService {
     }
   }
 
+  /**
+   * Invalidate session
+   */
+  @DELETE
+  @Path("sessions/{sessionTag}")
+  public Response invalidateSession(@PathParam("sessionTag") String sessionTag) {
+    try {
+      Connection connection = getSharedObjectsFactory().getHiveConnection();
+      connection.invalidateSessionByTag(sessionTag);
+      return Response.ok().build();
+    } catch (WebApplicationException ex) {
+      throw ex;
+    } catch (Exception ex) {
+      throw new ServiceFormattedException(ex.getMessage(), ex);
+    }
+  }
+
+  /**
+   * Session status
+   */
+  @GET
+  @Path("sessions/{sessionTag}")
+  @Produces(MediaType.APPLICATION_JSON)
+  public Response sessionStatus(@PathParam("sessionTag") String sessionTag) {
+    try {
+      Connection connection = getSharedObjectsFactory().getHiveConnection();
+
+      JSONObject session = new JSONObject();
+      session.put("sessionTag", sessionTag);
+      try {
+        connection.getSessionByTag(sessionTag);
+        session.put("actual", true);
+      } catch (HiveClientException ex) {
+        session.put("actual", false);
+      }
+
+      JSONObject status = new JSONObject();
+      status.put("session", session);
+      return Response.ok(status).build();
+    } catch (WebApplicationException ex) {
+      throw ex;
+    } catch (Exception ex) {
+      throw new ServiceFormattedException(ex.getMessage(), ex);
+    }
+  }
+
   /**
    * Wrapper object for json mapping
    */

+ 1 - 4
contrib/views/hive/src/main/java/org/apache/ambari/view/hive/resources/jobs/NoOperationStatusSetException.java

@@ -19,8 +19,5 @@
 package org.apache.ambari.view.hive.resources.jobs;
 
 
-public class NoOperationStatusSetException extends Throwable {
-  public NoOperationStatusSetException(String s) {
-    super(s);
-  }
+public class NoOperationStatusSetException extends Exception {
 }

+ 30 - 15
contrib/views/hive/src/main/java/org/apache/ambari/view/hive/resources/jobs/OperationHandleController.java

@@ -23,7 +23,7 @@ import org.apache.ambari.view.hive.client.Cursor;
 import org.apache.ambari.view.hive.client.HiveClientException;
 import org.apache.ambari.view.hive.client.IConnectionFactory;
 import org.apache.ambari.view.hive.resources.jobs.viewJobs.Job;
-import org.apache.ambari.view.hive.utils.ServiceFormattedException;
+import org.apache.ambari.view.hive.utils.HiveClientFormattedException;
 import org.apache.hive.service.cli.thrift.TGetOperationStatusResp;
 import org.apache.hive.service.cli.thrift.TOperationHandle;
 import org.slf4j.Logger;
@@ -51,49 +51,54 @@ public class OperationHandleController {
     this.operationHandle = storedOperationHandle;
   }
 
-  public String getOperationStatus() throws NoOperationStatusSetException, HiveClientException {
+  public OperationStatus getOperationStatus() throws NoOperationStatusSetException, HiveClientException {
     TGetOperationStatusResp statusResp = connectionsFabric.getHiveConnection().getOperationStatus(operationHandle);
+
     if (!statusResp.isSetOperationState()) {
-      throw new NoOperationStatusSetException("Operation state is not set");
+      throw new NoOperationStatusSetException();
     }
 
-    String status;
+    OperationStatus opStatus = new OperationStatus();
+    opStatus.sqlState = statusResp.getSqlState();
+    opStatus.message = statusResp.getErrorMessage();
+
     switch (statusResp.getOperationState()) {
       case INITIALIZED_STATE:
-        status = Job.JOB_STATE_INITIALIZED;
+        opStatus.status = Job.JOB_STATE_INITIALIZED;
         break;
       case RUNNING_STATE:
-        status = Job.JOB_STATE_RUNNING;
+        opStatus.status = Job.JOB_STATE_RUNNING;
         break;
       case FINISHED_STATE:
-        status = Job.JOB_STATE_FINISHED;
+        opStatus.status = Job.JOB_STATE_FINISHED;
         break;
       case CANCELED_STATE:
-        status = Job.JOB_STATE_CANCELED;
+        opStatus.status = Job.JOB_STATE_CANCELED;
         break;
       case CLOSED_STATE:
-        status = Job.JOB_STATE_CLOSED;
+        opStatus.status = Job.JOB_STATE_CLOSED;
         break;
       case ERROR_STATE:
-        status = Job.JOB_STATE_ERROR;
+        opStatus.status = Job.JOB_STATE_ERROR;
         break;
       case UKNOWN_STATE:
-        status = Job.JOB_STATE_UNKNOWN;
+        opStatus.status = Job.JOB_STATE_UNKNOWN;
         break;
       case PENDING_STATE:
-        status = Job.JOB_STATE_PENDING;
+        opStatus.status = Job.JOB_STATE_PENDING;
         break;
       default:
-        throw new NoOperationStatusSetException("Unknown status " + statusResp.getOperationState());
+        throw new NoOperationStatusSetException();
     }
-    return status;
+
+    return opStatus;
   }
 
   public void cancel() {
     try {
       connectionsFabric.getHiveConnection().cancelOperation(operationHandle);
     } catch (HiveClientException e) {
-      throw new ServiceFormattedException("Cancel failed: " + e.toString(), e);
+      throw new HiveClientFormattedException(e);
     }
   }
 
@@ -108,4 +113,14 @@ public class OperationHandleController {
   public Cursor getResults() {
     return connectionsFabric.getHiveConnection().getResults(operationHandle);
   }
+
+  public boolean hasResults() {
+    return operationHandle.isHasResultSet();
+  }
+
+  public static class OperationStatus {
+    public String status;
+    public String sqlState;
+    public String message;
+  }
 }

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

@@ -65,7 +65,7 @@ public class OperationHandleResourceManager extends SharedCRUDResourceManager<St
       try {
         update(handle, jobRelatedHandles.get(0).getId());
       } catch (ItemNotFound itemNotFound) {
-        throw new ServiceFormattedException("Error when updating operation handle: " + itemNotFound.toString(), itemNotFound);
+        throw new ServiceFormattedException("E050 Error when updating operation handle: " + itemNotFound.toString(), itemNotFound);
       }
     } else {
       create(handle);

+ 24 - 0
contrib/views/hive/src/main/java/org/apache/ambari/view/hive/resources/jobs/ResultsPaginationController.java

@@ -23,6 +23,7 @@ import org.apache.ambari.view.ViewContext;
 import org.apache.ambari.view.hive.client.ColumnDescription;
 import org.apache.ambari.view.hive.client.HiveClientException;
 import org.apache.ambari.view.hive.client.Cursor;
+import org.apache.ambari.view.hive.utils.HiveClientFormattedException;
 import org.apache.ambari.view.hive.utils.ServiceFormattedException;
 import org.apache.commons.collections4.map.PassiveExpiringMap;
 
@@ -96,6 +97,8 @@ public class ResultsPaginationController {
       Cursor resultSet = null;
       try {
         resultSet = makeResultsSet.call();
+      } catch (HiveClientException ex) {
+        throw new HiveClientFormattedException(ex);
       } catch (Exception ex) {
         throw new ServiceFormattedException(ex.getMessage(), ex);
       }
@@ -127,6 +130,18 @@ public class ResultsPaginationController {
     resultsResponse.setHasNext(resultSet.hasNext());
 //      resultsResponse.setSize(resultSet.size());
     resultsResponse.setOffset(resultSet.getOffset());
+    resultsResponse.setHasResults(true);
+    return Response.ok(resultsResponse);
+  }
+
+  public static Response.ResponseBuilder emptyResponse() {
+    ResultsResponse resultsResponse = new ResultsResponse();
+    resultsResponse.setSchema(new ArrayList<ColumnDescription>());
+    resultsResponse.setRows(new ArrayList<Object[]>());
+    resultsResponse.setReadCount(0);
+    resultsResponse.setHasNext(false);
+    resultsResponse.setOffset(0);
+    resultsResponse.setHasResults(false);
     return Response.ok(resultsResponse);
   }
 
@@ -136,6 +151,7 @@ public class ResultsPaginationController {
     private int readCount;
     private boolean hasNext;
     private long offset;
+    private boolean hasResults;
 
     public void setSchema(ArrayList<ColumnDescription> schema) {
       this.schema = schema;
@@ -176,5 +192,13 @@ public class ResultsPaginationController {
     public void setOffset(long offset) {
       this.offset = offset;
     }
+
+    public boolean getHasResults() {
+      return hasResults;
+    }
+
+    public void setHasResults(boolean hasResults) {
+      this.hasResults = hasResults;
+    }
   }
 }

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

@@ -80,7 +80,7 @@ public class StoredOperationHandle implements Indexed {
       identifier.setGuid(Hex.decodeHex(getGuid().toCharArray()));
       identifier.setSecret(Hex.decodeHex(getSecret().toCharArray()));
     } catch (DecoderException e) {
-      throw new ServiceFormattedException("Wrong identifer of OperationHandle is stored in DB");
+      throw new ServiceFormattedException("E060 Wrong identifier of OperationHandle is stored in DB");
     }
     handle.setOperationId(identifier);
     return handle;

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

@@ -91,6 +91,7 @@ public class ATSParser implements IATSParser {
     HiveQueryId parsedJob = new HiveQueryId();
 
     parsedJob.entity = (String) job.get("entity");
+    parsedJob.url = delegate.hiveQueryIdDirectUrl((String) job.get("entity"));
     parsedJob.starttime = ((Long) job.get("starttime")) / MillisInSecond;
 
     JSONObject primaryfilters = (JSONObject) job.get("primaryfilters");

+ 8 - 0
contrib/views/hive/src/main/java/org/apache/ambari/view/hive/resources/jobs/atsJobs/ATSRequestsDelegate.java

@@ -21,6 +21,14 @@ package org.apache.ambari.view.hive.resources.jobs.atsJobs;
 import org.json.simple.JSONObject;
 
 public interface ATSRequestsDelegate {
+  String hiveQueryIdDirectUrl(String entity);
+
+  String hiveQueryIdOperationIdUrl(String operationId);
+
+  String tezDagDirectUrl(String entity);
+
+  String tezDagNameUrl(String name);
+
   JSONObject hiveQueryIdList(String username);
 
   JSONObject hiveQueryIdByOperationId(String operationId);

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

@@ -42,6 +42,26 @@ public class ATSRequestsDelegateImpl implements ATSRequestsDelegate {
     this.atsUrl = atsUrl;
   }
 
+  @Override
+  public String hiveQueryIdDirectUrl(String entity) {
+    return atsUrl + "/ws/v1/timeline/HIVE_QUERY_ID/" + entity;
+  }
+
+  @Override
+  public String hiveQueryIdOperationIdUrl(String operationId) {
+    return atsUrl + "/ws/v1/timeline/HIVE_QUERY_ID?primaryFilter=operationid:" + operationId;
+  }
+
+  @Override
+  public String tezDagDirectUrl(String entity) {
+    return atsUrl + "/ws/v1/timeline/TEZ_DAG_ID/" + entity;
+  }
+
+  @Override
+  public String tezDagNameUrl(String name) {
+    return atsUrl + "/ws/v1/timeline/TEZ_DAG_ID?primaryFilter=dagName:" + name;
+  }
+
   @Override
   public JSONObject hiveQueryIdList(String username) {
     String hiveQueriesListUrl = atsUrl + "/ws/v1/timeline/HIVE_QUERY_ID?primaryFilter=requestuser:" + username;
@@ -51,14 +71,14 @@ public class ATSRequestsDelegateImpl implements ATSRequestsDelegate {
 
   @Override
   public JSONObject hiveQueryIdByOperationId(String operationId) {
-    String hiveQueriesListUrl = atsUrl + "/ws/v1/timeline/HIVE_QUERY_ID?primaryFilter=operationid:" + operationId;
+    String hiveQueriesListUrl = hiveQueryIdOperationIdUrl(operationId);
     String response = readFromWithDefault(hiveQueriesListUrl, "{ \"entities\" : [  ] }");
     return (JSONObject) JSONValue.parse(response);
   }
 
   @Override
   public JSONObject tezDagByName(String name) {
-    String tezDagUrl = atsUrl + "/ws/v1/timeline/TEZ_DAG_ID?primaryFilter=dagName:" + name;
+    String tezDagUrl = tezDagNameUrl(name);
     String response = readFromWithDefault(tezDagUrl, EMPTY_ENTITIES_JSON);
     return (JSONObject) JSONValue.parse(response);
   }

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

@@ -23,6 +23,8 @@ import org.json.simple.JSONObject;
 import java.util.List;
 
 public class HiveQueryId {
+  public String url;
+
   public String entity;
   public String query;
 

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

@@ -104,4 +104,12 @@ public interface Job extends Serializable,Indexed,PersonalResource {
   String getSessionTag();
 
   void setSessionTag(String sessionTag);
+
+  String getSqlState();
+
+  void setSqlState(String sqlState);
+
+  String getStatusMessage();
+
+  void setStatusMessage(String message);
 }

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

@@ -35,6 +35,7 @@ public interface JobController {
   Job getJobPOJO();
 
   Cursor getResults() throws ItemNotFound;
+  boolean hasResults() throws ItemNotFound;
 
   void afterCreation();
 

+ 27 - 12
contrib/views/hive/src/main/java/org/apache/ambari/view/hive/resources/jobs/viewJobs/JobControllerImpl.java

@@ -77,9 +77,9 @@ public class JobControllerImpl implements JobController, ModifyNotificationDeleg
     try {
       query = paginator.readPage(0);  //warning - reading only 0 page restricts size of query to 1MB
     } catch (IOException e) {
-      throw new ServiceFormattedException("Error when reading file: " + e.toString(), e);
+      throw new ServiceFormattedException("F030 Error when reading file " + job.getQueryFile(), e);
     } catch (InterruptedException e) {
-      throw new ServiceFormattedException("Error when reading file: " + e.toString(), e);
+      throw new ServiceFormattedException("F030 Error when reading file " + job.getQueryFile(), e);
     }
     return query;
   }
@@ -109,12 +109,19 @@ public class JobControllerImpl implements JobController, ModifyNotificationDeleg
   }
 
   private TSessionHandle getSession() {
-    if (job.getSessionTag() != null) {
-      return hiveConnection.getSessionByTag(getJob().getSessionTag());
-    } else {
-      String tag = hiveConnection.openSession();
-      job.setSessionTag(tag);
+    try {
+      if (job.getSessionTag() != null)
+        return hiveConnection.getSessionByTag(getJob().getSessionTag());
+    } catch (HiveClientException ignore) {
+      LOG.debug("Stale sessionTag was provided, new session will be opened");
+    }
+
+    String tag = hiveConnection.openSession();
+    job.setSessionTag(tag);
+    try {
       return hiveConnection.getSessionByTag(tag);
+    } catch (HiveClientException e) {
+      throw new HiveClientFormattedException(e);
     }
   }
 
@@ -136,8 +143,10 @@ public class JobControllerImpl implements JobController, ModifyNotificationDeleg
     try {
 
       OperationHandleController handle = opHandleControllerFactory.getHandleForJob(job);
-      String status = handle.getOperationStatus();
-      job.setStatus(status);
+      OperationHandleController.OperationStatus status = handle.getOperationStatus();
+      job.setStatus(status.status);
+      job.setStatusMessage(status.message);
+      job.setSqlState(status.sqlState);
       LOG.debug("Status of job#" + job.getId() + " is " + job.getStatus());
 
     } catch (NoOperationStatusSetException e) {
@@ -148,7 +157,7 @@ public class JobControllerImpl implements JobController, ModifyNotificationDeleg
       job.setStatus(Job.JOB_STATE_UNKNOWN);
 
     } catch (HiveClientException e) {
-      throw new ServiceFormattedException("Could not fetch job status " + job.getId(), e);
+      throw new HiveClientFormattedException(e);
 
     } catch (ItemNotFound itemNotFound) {
       LOG.debug("No TOperationHandle for job#" + job.getId() + ", can't update status");
@@ -212,6 +221,12 @@ public class JobControllerImpl implements JobController, ModifyNotificationDeleg
     return handle.getResults();
   }
 
+  @Override
+  public boolean hasResults() throws ItemNotFound {
+    OperationHandleController handle = opHandleControllerFactory.getHandleForJob(job);
+    return handle.hasResults();
+  }
+
   @Override
   public void afterCreation() {
     setupStatusDirIfNotPresent();
@@ -311,9 +326,9 @@ public class JobControllerImpl implements JobController, ModifyNotificationDeleg
       }
 
     } catch (IOException e) {
-      throw new ServiceFormattedException("Error in creation: " + e.toString(), e);
+      throw new ServiceFormattedException("F040 Error when creating file " + jobQueryFilePath, e);
     } catch (InterruptedException e) {
-      throw new ServiceFormattedException("Error in creation: " + e.toString(), e);
+      throw new ServiceFormattedException("F040 Error when creating file " + jobQueryFilePath, e);
     }
     job.setQueryFile(jobQueryFilePath);
 

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

@@ -20,6 +20,7 @@ package org.apache.ambari.view.hive.resources.jobs.viewJobs;
 
 import org.apache.commons.beanutils.PropertyUtils;
 
+import java.beans.Transient;
 import java.lang.reflect.InvocationTargetException;
 import java.util.Map;
 
@@ -32,11 +33,14 @@ public class JobImpl implements Job {
   private String statusDir = null;
   private Long dateSubmitted = 0L;
   private Long duration = 0L;
-  private String status = JOB_STATE_UNKNOWN;
   private String forcedContent = null;
   private String dataBase = null;
   private String queryId = null;
 
+  private String status = JOB_STATE_UNKNOWN;
+  private String statusMessage = null;
+  private String sqlState = null;
+
   private String applicationId;
   private String dagId;
   private String dagName;
@@ -148,11 +152,13 @@ public class JobImpl implements Job {
   }
 
   @Override
+  @Transient
   public String getForcedContent() {
     return forcedContent;
   }
 
   @Override
+  @Transient
   public void setForcedContent(String forcedContent) {
     this.forcedContent = forcedContent;
   }
@@ -246,4 +252,24 @@ public class JobImpl implements Job {
   public void setSessionTag(String sessionTag) {
     this.sessionTag = sessionTag;
   }
+
+  @Override
+  public String getStatusMessage() {
+    return statusMessage;
+  }
+
+  @Override
+  public void setStatusMessage(String statusMessage) {
+    this.statusMessage = statusMessage;
+  }
+
+  @Override
+  public String getSqlState() {
+    return sqlState;
+  }
+
+  @Override
+  public void setSqlState(String sqlState) {
+    this.sqlState = sqlState;
+  }
 }

+ 3 - 3
contrib/views/hive/src/main/java/org/apache/ambari/view/hive/resources/udfs/UDF.java

@@ -31,7 +31,7 @@ import java.util.Map;
 public class UDF implements Serializable, PersonalResource {
   private String name;
   private String classname;
-  private Integer fileResource;
+  private String fileResource;
 
   private String id;
   private String owner;
@@ -77,11 +77,11 @@ public class UDF implements Serializable, PersonalResource {
     this.classname = classname;
   }
 
-  public Integer getFileResource() {
+  public String getFileResource() {
     return fileResource;
   }
 
-  public void setFileResource(Integer fileResource) {
+  public void setFileResource(String fileResource) {
     this.fileResource = fileResource;
   }
 }

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

@@ -253,7 +253,7 @@ public class HdfsApi {
       }
     });
     if (!result) {
-      throw new ServiceFormattedException("Can't copy source file from " + src + " to " + dest);
+      throw new ServiceFormattedException("F050 Can't copy source file from " + src + " to " + dest);
     }
   }
 
@@ -341,11 +341,11 @@ public class HdfsApi {
       api = new HdfsApi(defaultFS, getHdfsUsername(context), getHdfsAuthParams(context));
       LOG.info("HdfsApi connected OK");
     } catch (IOException e) {
-      String message = "HdfsApi IO error: " + e.getMessage();
+      String message = "F060 Couldn't open connection to HDFS";
       LOG.error(message);
       throw new ServiceFormattedException(message, e);
     } catch (InterruptedException e) {
-      String message = "HdfsApi Interrupted error: " + e.getMessage();
+      String message = "F060 Couldn't open connection to HDFS";
       LOG.error(message);
       throw new ServiceFormattedException(message, e);
     }

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

@@ -43,9 +43,9 @@ public class HdfsUtil {
         stream.close();
       }
     } catch (IOException e) {
-      throw new ServiceFormattedException("Could not write file " + filePath, e);
+      throw new ServiceFormattedException("F070 Could not write file " + filePath, e);
     } catch (InterruptedException e) {
-      throw new ServiceFormattedException("Could not write file " + filePath, e);
+      throw new ServiceFormattedException("F070 Could not write file " + filePath, e);
     }
   }
 
@@ -74,9 +74,9 @@ public class HdfsUtil {
         triesCount += 1;
       } while (!isUnallocatedFilenameFound);
     } catch (IOException e) {
-      throw new ServiceFormattedException("Error in creation: " + e.toString(), e);
+      throw new ServiceFormattedException("F080 Error in creation " + fullPathAndFilename + "...", e);
     } catch (InterruptedException e) {
-      throw new ServiceFormattedException("Error in creation: " + e.toString(), e);
+      throw new ServiceFormattedException("F080 Error in creation " + fullPathAndFilename + "...", e);
     }
 
     return newFilePath;

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

@@ -0,0 +1,26 @@
+/**
+ * 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.
+ */
+
+package org.apache.ambari.view.hive.utils;
+
+public class HiveClientFormattedException extends ServiceFormattedException {
+
+  public HiveClientFormattedException(Throwable exception) {
+    super(exception.getMessage(), exception);
+  }
+}

+ 4 - 0
contrib/views/hive/src/main/resources/ui/hive-web/.bowerrc

@@ -0,0 +1,4 @@
+{
+  "directory": "bower_components",
+  "analytics": false
+}

+ 34 - 0
contrib/views/hive/src/main/resources/ui/hive-web/.editorconfig

@@ -0,0 +1,34 @@
+# EditorConfig helps developers define and maintain consistent
+# coding styles between different editors and IDEs
+# editorconfig.org
+
+root = true
+
+
+[*]
+end_of_line = lf
+charset = utf-8
+trim_trailing_whitespace = true
+insert_final_newline = true
+indent_style = space
+indent_size = 2
+
+[*.js]
+indent_style = space
+indent_size = 2
+
+[*.hbs]
+insert_final_newline = false
+indent_style = space
+indent_size = 2
+
+[*.css]
+indent_style = space
+indent_size = 2
+
+[*.html]
+indent_style = space
+indent_size = 2
+
+[*.{diff,md}]
+trim_trailing_whitespace = false

+ 27 - 0
contrib/views/hive/src/main/resources/ui/hive-web/.ember-cli

@@ -0,0 +1,27 @@
+/**
+ * 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.
+ */
+
+{
+  /**
+    Ember CLI sends analytics information by default. The data is completely
+    anonymous, but there are times when you might want to disable this behavior.
+
+    Setting `disableAnalytics` to true will prevent any data from being sent.
+  */
+  "disableAnalytics": true
+}

+ 38 - 0
contrib/views/hive/src/main/resources/ui/hive-web/.travis.yml

@@ -0,0 +1,38 @@
+# 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.
+
+---
+language: node_js
+node_js:
+  - "0.12"
+
+sudo: false
+
+cache:
+  directories:
+    - node_modules
+
+before_install:
+  - "npm config set spin false"
+  - "npm install -g npm@^2"
+
+install:
+  - npm install -g bower
+  - npm install
+  - bower install
+
+script:
+  - npm test

+ 5 - 0
contrib/views/hive/src/main/resources/ui/hive-web/Brocfile.js

@@ -25,9 +25,13 @@ var app = new EmberApp({
     //valid values are `default`, `bootstrap2`, `bootstrap3` or false
     'theme': 'bootstrap3'
   },
+  vendorFiles: {
+    'handlebars.js': null
+  },
   hinting: false
 });
 
+app.import('bower_components/ember/ember-template-compiler.js');
 app.import('bower_components/bootstrap/dist/js/bootstrap.js');
 app.import('bower_components/bootstrap/dist/css/bootstrap.css');
 app.import('bower_components/bootstrap/dist/css/bootstrap.css.map', {
@@ -42,5 +46,6 @@ app.import('vendor/codemirror/sql-hint.js');
 app.import('vendor/codemirror/show-hint.js');
 app.import('vendor/codemirror/codemirror.css');
 app.import('vendor/codemirror/show-hint.css');
+app.import('vendor/dagre.min.js');
 
 module.exports = app.toTree();

+ 8 - 8
contrib/views/hive/src/main/resources/ui/hive-web/app/components/alert-message-widget.js

@@ -19,17 +19,17 @@
 import Ember from 'ember';
 
 export default Ember.Component.extend({
-  click: function () {
-    this.toggleProperty('message.isExpanded');
-
-    if (!this.get('message.isExpanded')) {
-      this.sendAction('removeLater', this.get('message'));
-    }
-  },
-
   actions: {
     remove: function () {
       this.sendAction('removeMessage', this.get('message'));
+    },
+
+    toggleMessage: function() {
+      this.toggleProperty('message.isExpanded');
+
+      if (!this.get('message.isExpanded')) {
+        this.sendAction('removeLater', this.get('message'));
+      }
     }
   }
 });

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

@@ -28,6 +28,11 @@ export default Ember.Component.extend({
       if (this.get('isExpanded')) {
         this.sendAction('expanded', this.get('heading'), this.get('toggledParam'));
       }
+    },
+
+    sendControlAction: function(action) {
+      this.set('controlAction', action);
+      this.sendAction('controlAction', this.get('heading'), this.get('toggledParam'));
     }
   }
 });

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

@@ -25,10 +25,32 @@ export default Ember.Component.extend(Ember.I18n.TranslateableProperties, {
     }.bind(this));
   }.on('didInsertElement'),
 
+  keyPress: function(e) {
+    Ember.run.debounce(this, function() {
+      if (e.which === 13) {
+        this.send('ok');
+      } else if (e.which === 27) {
+        this.send('close');
+      }
+    }, 200)
+  },
+
+  setupEvents: function() {
+    this.$(document).on('keyup', Ember.$.proxy(this.keyPress, this));
+  }.on('didInsertElement'),
+
+  destroyEvents: function() {
+    this.$(document).off('keyup', Ember.$.proxy(this.keyPress, this));
+  }.on('willDestroyElement'),
+
   actions: {
     ok: function () {
       this.$('.modal').modal('hide');
       this.sendAction('ok');
+    },
+    close: function () {
+      this.$('.modal').modal('hide');
+      this.sendAction('close');
     }
   }
 });

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

@@ -0,0 +1,32 @@
+/**
+* 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.
+*/
+
+import Ember from 'ember';
+
+
+export default Ember.Component.extend({
+  tagName: 'notifications',
+  classNames: ['notifications-container'],
+  notifications : Ember.computed.alias('notify.notifications'),
+
+  actions: {
+    removeNotification: function(notification) {
+      this.notify.removeNotification(notification);
+    }
+  }
+});

+ 17 - 19
contrib/views/hive/src/main/resources/ui/hive-web/app/components/number-range-widget.js

@@ -24,27 +24,25 @@ export default Ember.Component.extend({
 
     var slider;
 
-    this.$(function() {
-      if (!self.get('numberRange.from') && !self.get('numberRange.to')) {
-        self.get('numberRange').set('from', self.get('numberRange.min'));
-        self.get('numberRange').set('to', self.get('numberRange.max'));
-      }
+    if (!self.get('numberRange.from') && !self.get('numberRange.to')) {
+      self.get('numberRange').set('from', self.get('numberRange.min'));
+      self.get('numberRange').set('to', self.get('numberRange.max'));
+    }
 
-      slider = self.$( ".slider" ).slider({
-        range: true,
-        min: self.get('numberRange.min'),
-        max: self.get('numberRange.max'),
-        units: self.get('numberRange.units'),
-        values: [ self.get('numberRange.from'), self.get('numberRange.to') ],
-        slide: function (event, ui) {
-          self.set('numberRange.from', ui.values[0]);
-          self.set('numberRange.to', ui.values[1]);
-        },
+    slider = self.$( ".slider" ).slider({
+      range: true,
+      min: self.get('numberRange.min'),
+      max: self.get('numberRange.max'),
+      units: self.get('numberRange.units'),
+      values: [ self.get('numberRange.from'), self.get('numberRange.to') ],
+      slide: function (event, ui) {
+        self.set('numberRange.from', ui.values[0]);
+        self.set('numberRange.to', ui.values[1]);
+      },
 
-        change: function () {
-          self.sendAction('rangeChanged', self.get('numberRange'));
-        }
-      });
+      change: function () {
+        self.sendAction('rangeChanged', self.get('numberRange'));
+      }
     });
 
     this.set('slider', slider);

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

@@ -0,0 +1,121 @@
+/**
+ * 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.
+ */
+import Ember from 'ember';
+
+export default Ember.Component.extend({
+  tabClassNames : "fa queries-icon query-context-tab",
+  openOverlayAction   : 'openOverlay',
+  closeOverlayAction  : 'closeOverlay',
+
+  tabs: [
+    Ember.Object.create({
+      iconClass: 'fa-code',
+      action: 'setDefaultActive'
+    }),
+    Ember.Object.create({
+      iconClass: 'fa-gear',
+      action: 'toggleOverlay',
+      template: 'settings',
+      outlet: 'overlay',
+      into: 'open-queries'
+    }),
+    Ember.Object.create({
+      iconClass: 'fa-bar-chart',
+      action: 'toggleOverlay',
+      template: 'visual-explain',
+      outlet: 'overlay',
+      into: 'index'
+    }),
+    Ember.Object.create({
+      iconClass: 'text-icon',
+      text: 'TEZ',
+      action: 'toggleOverlay',
+      template: 'tez-ui',
+      outlet: 'overlay',
+      into: 'index'
+    }),
+    Ember.Object.create({
+      iconClass: 'fa-envelope',
+      action: 'toggleOverlay',
+      template: 'messages',
+      outlet: 'overlay',
+      into: 'open-queries',
+      badgeProperty: 'count'
+    })
+  ],
+
+  setDefaultTab: function() {
+    var defaultTab = this.get('tabs.firstObject');
+
+    defaultTab.set('active', true);
+    this.set('default', defaultTab);
+    this.set('active', defaultTab);
+  }.on('init'),
+
+  setupTabsBadges: function() {
+    var tabs = this.get('tabs');
+    var self = this;
+
+    tabs.map(function(tab) {
+      if (tab.get('badgeProperty')) {
+        var controller = self.container.lookup('controller:' + tab.get('template'));
+        tab.set('controller', controller);
+
+        Ember.oneWay(tab, 'badge', 'controller.count');
+      }
+    });
+  }.on('init'),
+
+  closeActiveOverlay: function() {
+    this.sendAction('closeOverlayAction', this.get('active'));
+  },
+
+  openOverlay: function(tab) {
+    this.closeActiveOverlay();
+    this.set('active.active', false);
+    tab.set('active', true);
+    this.set('active', tab);
+    this.sendAction('openOverlayAction', tab);
+  },
+
+  setDefaultActive: function() {
+    var active     = this.get('active');
+    var defaultTab = this.get('default');
+
+    if (active !== defaultTab) {
+      this.closeActiveOverlay();
+      defaultTab.set('active', true);
+      active.set('active', false);
+      this.set('active', defaultTab);
+    }
+  },
+
+  actions: {
+    toggleOverlay: function(tab) {
+      if (tab !== this.get('default') && tab.get('active')) {
+        this.setDefaultActive();
+      } else {
+        this.openOverlay(tab);
+      }
+    },
+
+    setDefaultActive: function() {
+      this.setDefaultActive();
+    }
+  }
+});

+ 47 - 2
contrib/views/hive/src/main/resources/ui/hive-web/app/components/typeahead-widget.js

@@ -23,15 +23,60 @@ export default Typeahead.extend(Ember.I18n.TranslateableProperties, {
   didInsertElement: function() {
     this._super();
 
-    if(!this.get('selection') && this.get('content.firstObject')) {
+    if (!this.get('selection') && this.get('content.firstObject')) {
       this.set('selection', this.get('content.firstObject'));
     }
 
     this.selectize.on('dropdown_close', Ember.$.proxy(this.onClose, this));
   },
 
+  removeExcludedObserver: function() {
+    var self    = this;
+    var options = this.get('content');
+
+    if (!options) {
+      options = this.removeExcluded(true);
+      this.set('content', options);
+    } else {
+      this.removeExcluded();
+    }
+  }.observes('excluded.@each.key').on('init'),
+
+  removeExcluded: function(shouldReturn) {
+    var self            = this;
+    var excluded        = this.get('excluded') || [];
+    var options         = this.get('options');
+    var selection       = this.get('selection');
+    var objectToModify  = this.get('content');
+    var objectsToRemove = [];
+    var objectsToAdd    = [];
+
+    if (!options) {
+      return;
+    }
+
+    if (shouldReturn) {
+      objectToModify = Ember.copy(options);
+    }
+
+    if (options) {
+      options.forEach(function(option, index) {
+        if (excluded.contains(option) && option !== selection) {
+          objectsToRemove.push(option);
+        } else if (!objectToModify.contains(option)) {
+          objectsToAdd.push(option);
+        }
+      });
+    }
+
+    objectToModify.removeObjects(objectsToRemove);
+    objectToModify.pushObjects(objectsToAdd);
+
+    return objectToModify;
+  },
+
   onClose: function() {
-    if(!this.get('selection') && this.get('prevSelection')) {
+    if (!this.get('selection') && this.get('prevSelection')) {
       this.set('selection', this.get('prevSelection'));
     }
   },

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

@@ -30,13 +30,13 @@ export default Ember.ArrayController.extend({
   dbTables: Ember.computed.alias('controllers.' + constants.namingConventions.tables),
   dbColumns: Ember.computed.alias('controllers.' + constants.namingConventions.columns),
 
-  _handleTablesError: function (err) {
-    this.send('addAlert', constants.alerts.error, err.responseText, "alerts.errors.get.tables");
+  _handleTablesError: function (error) {
+    this.notify.error(error.responseJSON.message, error.responseJSON.trace);
     this.set('isLoading', false);
   },
 
-  _handleColumnsError: function (err) {
-    this.send('addAlert', constants.alerts.error, err.responseText, "alerts.errors.get.columns");
+  _handleColumnsError: function (error) {
+    this.notify.error(error.responseJSON.message, error.responseJSON.trace);
     this.set('isLoading', false);
   },
 
@@ -191,7 +191,52 @@ export default Ember.ArrayController.extend({
     return defer.promise;
   },
 
+  tableControls: Ember.A([
+    Ember.Object.create({
+      icon: 'fa-list',
+      action: 'loadSampleData',
+      tooltip: Ember.I18n.t('tooltips.loadSample')
+    })
+  ]),
+
+  panelIconActions: function () {
+    return [
+      Ember.Object.create({
+        icon: 'fa-refresh',
+        action: 'refreshDatabaseExplorer'
+      })
+    ];
+  }.property(),
+
   actions: {
+    refreshDatabaseExplorer: function() {
+      var self = this;
+      var selectedDatabase = this.get('selectedDatabase');
+
+      this.store.unloadAll('database');
+      this.store.fetchAll('database').then(function() {
+        var database = self.get('model').findBy('id', selectedDatabase.get('id'));
+        self.set('selectedDatabase', database);
+      }).catch(function(response) {
+        self.notify.error(response.responseJSON.message, response.responseJSON.trace);
+      });
+    },
+
+    loadSampleData: function(tableName, database) {
+      var self = this;
+      this.send('addQuery', Ember.I18n.t('titles.tableSample', { tableName: tableName }));
+
+      Ember.run.later(function() {
+        var query = constants.sampleDataQuery.fmt(tableName);
+
+        self.set('selectedDatabase', database);
+        self.get('openQueries.currentQuery')
+          .set('fileContent', query);
+
+        self.send('executeQuery');
+      });
+    },
+
     getTables: function (dbName) {
       var database = this.findBy('name', dbName),
           tables = database.tables,
@@ -257,6 +302,7 @@ export default Ember.ArrayController.extend({
           resultsTab = this.get('tabs').findBy('view', constants.namingConventions.databaseSearch),
           tableSearchResults = this.get('tableSearchResults');
 
+      this.set('tablesSearchTerm', searchTerm);
       resultsTab.set('visible', true);
       this.set('selectedTab', resultsTab);
       this.set('columnSearchTerm', '');
@@ -351,4 +397,4 @@ export default Ember.ArrayController.extend({
       });
     }
   }
-});
+});

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

@@ -42,10 +42,19 @@ export default Ember.Controller.extend({
   visualExplain: Ember.computed.alias('controllers.' + constants.namingConventions.visualExplain),
   tezUI: Ember.computed.alias('controllers.' + constants.namingConventions.tezUI),
 
+  isQueryTabActive: function() {
+    return !this.get('tezUI.showOverlay') && !this.get('visualExplain.showOverlay') && !this.get('settings.showOverlay');
+  }.property('tezUI.showOverlay', 'visualExplain.showOverlay', 'settings.showOverlay'),
+
   shouldShowTez: function() {
     return this.get('model.dagId') && this.get('tezUI.isTezViewAvailable');
   }.property('model.dagId', 'tezUI.isTezViewAvailable'),
 
+  shouldShowVisualExplain: function () {
+    return this.get('openQueries.currentQuery.fileContent');
+  }.property('openQueries.currentQuery.fileContent'),
+
+
   canExecute: function () {
     var isModelRunning = this.get('model.isRunning');
     var hasParams = this.get('queryParams.length');
@@ -84,11 +93,12 @@ export default Ember.Controller.extend({
     currentParams.setObjects(updatedParams);
   }.observes('openQueries.currentQuery.fileContent'),
 
-  _executeQuery: function (shouldExplain) {
+  _executeQuery: function (shouldExplain, shouldGetVisualExplain) {
     var queryId,
         query,
         finalQuery,
         job,
+        defer = Ember.RSVP.defer(),
         originalModel = this.get('model');
 
     job = this.store.createRecord(constants.namingConventions.job, {
@@ -110,15 +120,55 @@ export default Ember.Controller.extend({
 
     query = this.get('openQueries').getQueryForModel(originalModel);
 
-    finalQuery = this.buildQuery(query, shouldExplain);
+    query = this.buildQuery(query, shouldExplain, shouldGetVisualExplain);
+
+    // for now we won't support multiple queries
+    // buildQuery will return false it multiple queries
+    // are selected
+    if (!query) {
+      originalModel.set('isRunning', false);
+      defer.reject({
+        responseJSON: {
+          message: 'Running multiple queries is not supported.'
+        }
+      });
+
+      return defer.promise;
+    }
+
+    finalQuery = query;
     finalQuery = this.bindQueryParams(finalQuery);
     finalQuery = this.prependQuerySettings(finalQuery);
 
     job.set('forcedContent', finalQuery);
 
+    if (shouldGetVisualExplain) {
+      return this.getVisualExplainJson(job, originalModel);
+    }
+
     return this.saveQuery(job, originalModel);
   },
 
+  getVisualExplainJson: function (job, originalModel) {
+    var self = this;
+    var defer = Ember.RSVP.defer();
+
+    job.save().then(function () {
+      self.get('results').getResultsJson(job).then(function (json) {
+        defer.resolve(json);
+        originalModel.set('isRunning', undefined);
+      }, function (err) {
+        defer.reject(err);
+        originalModel.set('isRunning', undefined);
+      });
+    }, function (err) {
+      defer.reject(err);
+        originalModel.set('isRunning', undefined);
+    });
+
+    return defer.promise;
+  },
+
   saveQuery: function (job, originalModel) {
     var defer = Ember.RSVP.defer(),
         self = this,
@@ -158,35 +208,41 @@ export default Ember.Controller.extend({
     return query;
   },
 
-  buildQuery: function (query, shouldExplain) {
+  buildQuery: function (query, shouldExplain, shouldGetVisualExplain) {
     var selections = this.get('openQueries.highlightedText'),
         isQuerySelected = selections && selections[0] !== "",
-        queryComponents = this.extractComponents(query.get('fileContent')),
+        queryContent = query ? query.get('fileContent') : '',
+        queryComponents = this.extractComponents(queryContent),
         finalQuery = '',
-        queries;
+        queries = null;
 
     if (isQuerySelected) {
-      queries = selections.map(function (s) {
-        return s.replace(";", "");
-      });
-    } else {
-      queries = queryComponents.queryString.split(';');
-      queries = queries.filter(Boolean);
+      queryComponents.queryString = selections.join('');
+    }
+
+    queries = queryComponents.queryString.split(';');
+    queries = queries.map(function(s) {
+      return s.trim();
+    });
+    queries = queries.filter(Boolean);
+
+    // return false if multiple queries are selected
+    // @FIXME: Remove this to support multiple queries
+    if (queries.length > 1) {
+      return false;
     }
 
     queries = queries.map(function (query) {
       if (shouldExplain) {
-        if (query.indexOf(constants.namingConventions.explainPrefix) === -1) {
+        query = query.replace(/explain|formatted/gi, '').trim();
+
+        if (shouldGetVisualExplain) {
+          return constants.namingConventions.explainFormattedPrefix + query;
+        } else {
           return constants.namingConventions.explainPrefix + query;
         }
-
-        return query;
       } else {
-        if (query.indexOf(constants.namingConventions.explainPrefix) > -1) {
-          return query.replace(constants.namingConventions.explainPrefix, '');
-        }
-
-        return query;
+        return query.replace(/explain|formatted/gi, '').trim();
       }
     });
 
@@ -199,6 +255,7 @@ export default Ember.Controller.extend({
     }
 
     finalQuery += queries.join(";");
+    finalQuery += ";";
     return finalQuery;
   },
 
@@ -278,6 +335,10 @@ export default Ember.Controller.extend({
     });
   }.observes('content'),
 
+  selectedDatabaseChanged: function() {
+    this.set('content.dataBase', this.get('databases.selectedDatabase.name'));
+  }.observes('databases.selectedDatabase'),
+
   csvUrl: function () {
     if (this.get('content.constructor.typeKey') !== constants.namingConventions.job) {
       return;
@@ -309,7 +370,7 @@ export default Ember.Controller.extend({
         items.push(
           Ember.Object.create({
             title: Ember.I18n.t('buttons.saveCsv'),
-            href: this.get('csvUrl')
+            action: 'downloadAsCSV'
           })
         );
       }
@@ -350,7 +411,7 @@ export default Ember.Controller.extend({
     }).then(function (response) {
       self.pollSaveToHDFS(response);
     }, function (response) {
-      self.send('addAlert', constants.alerts.error, response.message, "alerts.errors.save.results");
+      self.notify.error(response.responseJSON.message, response.responseJSON.trace);
     });
   },
 
@@ -367,7 +428,7 @@ export default Ember.Controller.extend({
           self.set('content.isRunning', false);
         }
       }, function (response) {
-        self.send('addAlert', constants.alerts.error, response.message, "alerts.errors.save.results");
+        self.notify.error(response.responseJSON.message, response.responseJSON.trace);
       });
     }, 2000);
   },
@@ -386,6 +447,25 @@ export default Ember.Controller.extend({
       this.saveToHDFS();
     },
 
+    downloadAsCSV: function() {
+      var self = this,
+          defer = Ember.RSVP.defer();
+
+      this.send('openModal', 'modal-save', {
+        heading: "modals.download.csv",
+        text: this.get('content.title'),
+        defer: defer
+      });
+
+      defer.promise.then(function (text) {
+        // download file ...
+        var urlString = "%@/?fileName=%@.csv";
+        var url = self.get('csvUrl');
+        url = urlString.fmt(url, text);
+        window.open(url);
+      });
+    },
+
     insertUdf: function (item) {
       var query = this.get('openQueries').getQueryForModel(this.get('model'));
 
@@ -418,17 +498,16 @@ export default Ember.Controller.extend({
     addQuery: (function () {
       var idCounter = 0;
 
-      return function () {
+      return function (workSheetName) {
         var model = this.store.createRecord(constants.namingConventions.savedQuery, {
           dataBase: this.get('databases.selectedDatabase.name'),
-          title: 'New Query',
-          type: constants.namingConventions.savedQuery,
+          title: workSheetName ? workSheetName : Ember.I18n.t('titles.query.tab'),
           queryFile: '',
           id: 'fixture_' + idCounter
         });
 
-        if (idCounter) {
-          model.set('title', model.get('title') + ' (' + idCounter + ')')
+        if (idCounter && !workSheetName) {
+          model.set('title', model.get('title') + ' (' + idCounter + ')');
         }
 
         idCounter++;
@@ -438,26 +517,34 @@ export default Ember.Controller.extend({
     }()),
 
     saveQuery: function () {
+      //case 1. Save a new query from a new query tab -> route changes to new id
+      //case 2. Save a new query from an existing query tab -> route changes to new id
+      //case 3. Save a new query from a job tab -> route doesn't change
+      //case 4. Update an existing query tab. -> route doesn't change
+
       var self = this,
-          wasNew = this.get('model.isNew'),
           defer = Ember.RSVP.defer();
 
       this.set('model.dataBase', this.get('databases.selectedDatabase.name'));
 
-      this.send('openModal', 'modal-save', {
-        heading: "modals.save.heading",
+      this.send('openModal', 'modal-save-query', {
+        heading: 'modals.save.heading',
+        message: 'modals.save.overwrite',
         text: this.get('content.title'),
+        content: this.get('content'),
         defer: defer
       });
 
-      defer.promise.then(function (text) {
-        self.get('content').set('title', text);
-
-        self.get('openQueries').save(self.get('content')).then(function () {
-          if (wasNew) {
-            self.transitionToRoute(constants.namingConventions.subroutes.savedQuery, self.get('model.id'));
-          }
-        });
+      defer.promise.then(function (result) {
+        if (result.get('overwrite')) {
+          self.get('openQueries').save(self.get('content'), null, true, result.get('text'));
+        } else {
+          self.get('openQueries').save(self.get('content'), null, false, result.get('text')).then(function (newId) {
+            if (self.get('model.constructor.typeKey') !== constants.namingConventions.job) {
+              self.transitionToRoute(constants.namingConventions.subroutes.savedQuery, newId);
+            }
+          });
+        }
       });
     },
 
@@ -476,7 +563,8 @@ export default Ember.Controller.extend({
 
         self.transitionToRoute(constants.namingConventions.subroutes.historyQuery, job.get('id'));
       }, function (err) {
-        self.send('addAlert', constants.alerts.error, err.responseText, "alerts.errors.save.query");
+        var errorBody = err.responseJSON.trace ? err.responseJSON.trace : false;
+        self.notify.error(err.responseJSON.message, errorBody);
       });
     },
 
@@ -488,11 +576,13 @@ export default Ember.Controller.extend({
 
         self.transitionToRoute(constants.namingConventions.subroutes.historyQuery, job.get('id'));
       }, function (err) {
-        self.send('addAlert', constants.alerts.error, err.responseText, "alerts.errors.save.query");
+        this.notify.error(err.responseJSON.message, err.responseJSON.trace);
       });
     },
 
     toggleOverlay: function (targetController) {
+      var self = this;
+
       if (this.get('visualExplain.showOverlay') && targetController !== 'visualExplain') {
         this.set('visualExplain.showOverlay', false);
       } else if (this.get('tezUI.showOverlay') && targetController !== 'tezUI') {
@@ -501,12 +591,28 @@ export default Ember.Controller.extend({
         this.set('settings.showOverlay', false);
       }
 
+      if (!targetController) {
+        return;
+      }
+
       if (targetController !== 'settings') {
         //set content for visual explain and tez ui.
         this.set(targetController + '.content', this.get('content'));
       }
 
-      this.toggleProperty(targetController + '.showOverlay');
+      if (targetController === 'visualExplain' && !this.get(targetController + '.showOverlay')) {
+        this._executeQuery(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(targetController + '.json', json);
+            self.toggleProperty(targetController + '.showOverlay');
+          }
+        }, function (err) {
+          self.notify.error(err.responseJSON.message, err.responseJSON.trace);
+        });
+      } else {
+        this.toggleProperty(targetController + '.showOverlay');
+      }
     }
   }
 });

+ 25 - 11
contrib/views/hive/src/main/resources/ui/hive-web/app/controllers/index/history-query/explain.js

@@ -39,30 +39,44 @@ export default Ember.ObjectController.extend({
     if (cachedExplain) {
       this.formatExplainResults(cachedExplain);
     } else {
-      this.getExplain();
+      this.getExplain(true);
     }
   }.observes('content'),
 
-  getExplain: function () {
+  getExplain: function (firstPage, rows) {
     var self = this;
     var url = this.container.lookup('adapter:application').buildURL();
-    url += '/' + constants.namingConventions.jobs + '/' + this.get('content.id') + '/results?first=true';
+    url += '/' + constants.namingConventions.jobs + '/' + this.get('content.id') + '/results';
+
+    if (firstPage) {
+      url += '?first=true';
+    }
 
     Ember.$.getJSON(url).then(function (data) {
-      var explainSet = self.get('cachedExplains').pushObject(Ember.Object.create({
-        id: self.get('content.id'),
-        explain: data
-      }));
+      var explainSet;
+
+      //if rows from a previous page read exist, prepend them
+      if (rows) {
+        data.rows.unshiftObjects(rows);
+      }
 
-      self.set('content.explain', explainSet);
+      if (!data.hasNext) {
+        explainSet = self.get('cachedExplains').pushObject(Ember.Object.create({
+          id: self.get('content.id'),
+          explain: data
+        }));
 
-      self.formatExplainResults(explainSet);
+        self.set('content.explain', explainSet);
+
+        self.formatExplainResults(explainSet);
+      } else {
+        self.getExplain(false, data.rows);
+      }
     });
   },
 
   formatExplainResults: function (explainSet) {
     var formatted = [],
-        orderedNodes = [],
         currentNode,
         currentNodeWhitespace,
         previousNode,
@@ -116,4 +130,4 @@ export default Ember.ObjectController.extend({
 
     this.set('formattedExplain', formatted);
   }
-});
+});

+ 16 - 6
contrib/views/hive/src/main/resources/ui/hive-web/app/controllers/index/history-query/logs.js

@@ -28,13 +28,22 @@ export default Ember.ObjectController.extend({
   reloadJobLogs: function (job) {
     var self = this,
         defer = Ember.RSVP.defer(),
-        handleError = function (err) {
-          self.send('addAlert', constants.namingConventions.alerts.error, err.responseText);
+        handleError = function (error) {
+          job.set('isRunning', false);
+
+          if (typeof error === "string") {
+            self.notify.error(error);
+          } else {
+            self.notify.error(error.responseJSON.message, error.responseJSON.trace);
+          }
           defer.reject();
         };
 
     job.reload().then(function () {
-      self.get('files').reload(job.get('logFile')).then(function (file) {
+      if (utils.insensitiveCompare(job.get('status'), constants.statuses.error)) {
+        handleError(job.get('statusMessage'));
+      } else {
+        self.get('files').reload(job.get('logFile')).then(function (file) {
         var fileContent = file.get('fileContent');
 
         if (fileContent) {
@@ -42,9 +51,10 @@ export default Ember.ObjectController.extend({
         }
 
         defer.resolve();
-      },function (err) {
-        handleError(err);
-      });
+        },function (err) {
+          handleError(err);
+        });
+      }
     }, function (err) {
       handleError(err);
     });

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

@@ -22,6 +22,25 @@ import utils from 'hive/utils/functions';
 
 export default Ember.ObjectController.extend({
   cachedResults: [],
+  formattedResults: [],
+
+  processResults: function() {
+    var results = this.get('results');
+
+    if (!results || !results.schema || !results.rows) {
+      return;
+    }
+
+    var schema = results.schema.map(function(column) {
+      return {
+        name: column[0],
+        type: column[1],
+        index: column[2]
+      }
+    });
+
+    this.set('formattedResults', { schema: schema, rows: results.rows });
+  }.observes('results'),
 
   keepAlive: function (job) {
     Ember.run.later(this, function () {
@@ -73,6 +92,20 @@ export default Ember.ObjectController.extend({
     return this.cachedResults.findBy('id', this.get('content.id')).results.indexOf(this.get('results')) <= 0;
   }.property('results'),
 
+  getResultsJson: function (job) {
+    var defer = Ember.RSVP.defer();
+    var url = this.container.lookup('adapter:application').buildURL();
+    url += '/' + constants.namingConventions.jobs + '/' + job.get('id') + '/results?first=true';
+
+    Ember.$.getJSON(url).then(function (results) {
+      defer.resolve(JSON.parse(results.rows[0][0]));
+    }, function (err) {
+      defer.reject(err);
+    });
+
+    return defer.promise;
+  },
+
   actions: {
     getNextPage: function (firstPage, job) {
       var self = this;
@@ -115,6 +148,7 @@ export default Ember.ObjectController.extend({
         if (firstPage) {
           self.keepAlive(job || self.get('content'));
         }
+
       }, function (err) {
         self.set('error', err.responseText);
       });

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

@@ -0,0 +1,33 @@
+/**
+* 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.
+*/
+import Ember from 'ember';
+
+export default Ember.Controller.extend({
+  messages: Ember.computed.alias('notify.messages'),
+  count: Ember.computed.alias('messages.length'),
+
+  actions: {
+    removeMessage: function(message) {
+      this.notify.removeMessage(message);
+    },
+
+    removeAllMessages: function() {
+      this.notify.removeAllMessages();
+    }
+  }
+});

+ 42 - 0
contrib/views/hive/src/main/resources/ui/hive-web/app/controllers/modal-save-query.js

@@ -0,0 +1,42 @@
+/**
+ * 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.
+ */
+
+import Ember from 'ember';
+import ModalSave from '../controllers/modal-save';
+import constants from '../utils/constants';
+
+export default ModalSave.extend({
+  showMessage: function () {
+    var content = this.get('content');
+
+    return !content.get('isNew') &&
+            content.get('title') === this.get('text') &&
+            content.get('constructor.typeKey') !== constants.namingConventions.job;
+  }.property('content.isNew', 'text'),
+
+  actions: {
+    save: function () {
+      this.send('closeModal');
+
+      this.defer.resolve(Ember.Object.create({
+        text: this.get('text'),
+        overwrite: this.get('showMessage')
+      }));
+    }
+  }
+});

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

@@ -154,7 +154,7 @@ export default Ember.ArrayController.extend({
     return defer.promise;
   },
 
-  save: function (model, query) {
+  save: function (model, query, isUpdating, newTitle) {
     var tab = this.getTabForModel(model),
         self = this,
         wasNew,
@@ -168,6 +168,7 @@ export default Ember.ArrayController.extend({
 
     if (model.get('isNew')) {
       wasNew = true;
+      model.set('title', newTitle);
       model.set('id', null);
     }
 
@@ -175,16 +176,30 @@ export default Ember.ArrayController.extend({
     if (model.get('constructor.typeKey') === constants.namingConventions.job) {
       model = this.store.createRecord(constants.namingConventions.savedQuery, {
         dataBase: this.get('databases.selectedDatabase.name'),
-        title: model.get('title'),
+        title: newTitle,
         queryFile: model.get('queryFile'),
         owner: model.get('owner')
       });
+    } else {
+      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, {
+        dataBase: this.get('databases.selectedDatabase.name'),
+        title: newTitle,
+        owner: model.get('owner')
+      });
+
+      wasNew = true;
     }
 
     model.save().then(function (updatedModel) {
-      tab.set('name', updatedModel.get('title'));
       jobModel.set('queryId', updatedModel.get('id'));
 
+      tab.set('isDirty', false);
+
       var content = self.get('index').prependQuerySettings(query.get('fileContent'));
       //update query tab path with saved model id if its a new record
       if (wasNew) {
@@ -198,7 +213,7 @@ export default Ember.ArrayController.extend({
             self.pushObject(updatedFile);
             self.set('currentQuery', updatedFile);
 
-            defer.resolve();
+            defer.resolve(updatedModel.get('id'));
           }, function (err) {
             defer.reject(err);
           });
@@ -209,7 +224,7 @@ export default Ember.ArrayController.extend({
         query.set('fileContent', content);
         query.save().then(function () {
           self.toggleProperty('tabUpdated');
-          defer.resolve();
+          defer.resolve(updatedModel.get('id'));
         }, function (err) {
           defer.reject(err);
         });
@@ -259,34 +274,83 @@ export default Ember.ArrayController.extend({
          hasSettings;
   },
 
+  isDirty: function(model) {
+    var query = this.getQueryForModel(model);
+
+    if (model.get('isNew') && !query.get('fileContent')) {
+      return false;
+    }
+
+    if (query && query.get('isDirty')) {
+      return true;
+    }
+
+    return !!(!model.get('queryId') && model.get('isDirty'));
+
+
+  },
+
+  updatedDeletedQueryTab: function (model) {
+    var tab = this.getTabForModel(model);
+
+    if (tab) {
+      this.closeTab(tab);
+    }
+  },
+
+  dirtyObserver: function () {
+    var tab;
+    var model = this.get('index.model');
+
+    if (model) {
+      tab = this.getTabForModel(model);
+
+      if (tab) {
+        tab.set('isDirty', this.isDirty(model));
+      }
+    }
+  }.observes('currentQuery.isDirty', 'currentQuery.fileContent'),
+
+  closeTab: function (tab, goToNextTab) {
+    var remainingTabs = this.get('queryTabs').without(tab);
+
+    this.set('queryTabs', remainingTabs);
+
+    //remove cached results set
+    if (tab.type === constants.namingConventions.job) {
+      this.get('jobResults').clearCachedResultsSet(tab.id);
+      this.get('jobExplain').clearCachedExplainSet(tab.id);
+    }
+
+    if (goToNextTab) {
+      this.navigateToLastTab();
+    }
+  },
+
+  navigateToLastTab: function () {
+    var lastTab = this.get('queryTabs.lastObject');
+
+    if (lastTab) {
+      if (lastTab.type === constants.namingConventions.job) {
+        this.transitionToRoute(constants.namingConventions.subroutes.historyQuery, lastTab.id);
+      } else {
+        this.transitionToRoute(constants.namingConventions.subroutes.savedQuery, lastTab.id);
+      }
+    } else {
+      this.get('index').send('addQuery');
+    }
+  },
+
   actions: {
     removeQueryTab: function (tab) {
       var self = this,
-          defer,
-          remainingTabs = this.get('queryTabs').without(tab),
-          lastTab = remainingTabs.get('lastObject'),
-          closeTab = function () {
-            self.set('queryTabs', remainingTabs);
-
-            //remove cached results set
-            if (tab.type === constants.namingConventions.job) {
-              self.get('jobResults').clearCachedResultsSet(tab.id);
-              self.get('jobExplain').clearCachedExplainSet(tab.id);
-            }
-
-            if (lastTab.type === constants.namingConventions.job) {
-              self.transitionToRoute(constants.namingConventions.subroutes.historyQuery, lastTab.id);
-            } else {
-              self.transitionToRoute(constants.namingConventions.subroutes.savedQuery, lastTab.id);
-            }
-          };
+          defer;
 
       this.store.find(tab.type, tab.id).then(function (model) {
         var query = self.getQueryForModel(model);
 
-        if ((model.get('isNew') && !query.get('fileContent')) ||
-            (!model.get('isNew') && !query.get('isDirty'))) {
-          closeTab();
+        if (!self.isDirty(model)) {
+          self.closeTab(tab, true);
         } else {
           defer = Ember.RSVP.defer();
           self.send('openModal',
@@ -300,11 +364,11 @@ export default Ember.ArrayController.extend({
           defer.promise.then(function (text) {
             model.set('title', text);
             self.save(model, query).then(function () {
-              closeTab();
+              self.closeTab(tab, true);
             });
           }, function () {
             model.rollback();
-            closeTab();
+            self.closeTab(tab, true);
           });
         }
       });

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

@@ -21,7 +21,11 @@ import FilterableMixin from 'hive/mixins/filterable';
 import constants from 'hive/utils/constants';
 
 export default Ember.ArrayController.extend(FilterableMixin, {
-  needs: [ constants.namingConventions.routes.history ],
+  needs: [ constants.namingConventions.routes.history,
+           constants.namingConventions.openQueries ],
+
+  history: Ember.computed.alias('controllers.' + constants.namingConventions.routes.history),
+  openQueries: Ember.computed.alias('controllers.' + constants.namingConventions.openQueries),
 
   sortAscending: true,
   sortProperties: [],
@@ -68,9 +72,11 @@ export default Ember.ArrayController.extend(FilterableMixin, {
 
   actions: {
     executeAction: function (action, savedQuery) {
+      var self = this;
+
       switch (action) {
         case "buttons.history":
-          this.get('controllers.' + constants.namingConventions.routes.history).filterBy('queryId', savedQuery.get('id'), true);
+          this.get('history').filterBy('queryId', savedQuery.get('id'), true);
           this.transitionToRoute(constants.namingConventions.routes.history);
           break;
         case "buttons.delete":
@@ -85,6 +91,7 @@ export default Ember.ArrayController.extend(FilterableMixin, {
 
           defer.promise.then(function () {
             savedQuery.destroyRecord();
+            self.get('openQueries').updatedDeletedQueryTab(savedQuery);
           });
 
           break;

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

@@ -27,9 +27,22 @@ export default Ember.ArrayController.extend({
 
   index: Ember.computed.alias('controllers.' + constants.namingConventions.index),
   openQueries: Ember.computed.alias('controllers.' + constants.namingConventions.openQueries),
+  sessionTag: Ember.computed.alias('index.model.sessionTag'),
+  sessionActive: Ember.computed.alias('index.model.sessionActive'),
+
+  canInvalidateSession: Ember.computed.and('sessionTag', 'sessionActive'),
 
   predefinedSettings: constants.hiveParameters,
 
+  selectedSettings: function() {
+    var predefined = this.get('predefinedSettings');
+    var current = this.get('currentSettings.settings');
+
+    return predefined.filter(function(setting) {
+      return current.findBy('key.name', setting.name);
+    });
+  }.property('currentSettings.settings.@each.key'),
+
   currentSettings: function () {
     var currentId = this.get('index.model.id');
     var targetSettings = this.findBy('id', currentId);
@@ -150,9 +163,14 @@ export default Ember.ArrayController.extend({
         return;
       }
 
+      if (!predefined.validate) {
+        setting.set('valid', true);
+        return;
+      }
+
       setting.set('valid', false);
     });
-  }.observes('currentSettings.[]', 'currentSettings.settings.@each.value', 'currentSettings.settings.@each.key'),
+  }.observes('currentSettings.[]', 'currentSettings.settings.[]', 'currentSettings.settings.@each.value', 'currentSettings.settings.@each.key'),
 
   currentSettingsAreValid: function() {
     var currentSettings = this.get('currentSettings.settings');
@@ -161,6 +179,24 @@ export default Ember.ArrayController.extend({
     return invalid.length ? false : true;
   }.property('currentSettings.settings.@each.value', 'currentSettings.settings.@each.key'),
 
+  loadSessionStatus: function() {
+    var model         = this.get('index.model');
+    var sessionActive = this.get('sessionActive');
+    var sessionTag    = this.get('sessionTag');
+    var adapter       = this.container.lookup('adapter:application');
+    var url           = adapter.buildURL() + '/jobs/sessions/' + sessionTag;
+
+    if (sessionTag && sessionActive === undefined) {
+      adapter.ajax(url, 'GET')
+        .then(function(response) {
+          model.set('sessionActive', response.session.actual);
+        })
+        .catch(function() {
+          model.set('sessionActive', false);
+        });
+    }
+  }.observes('index.model', 'index.model.status'),
+
   actions: {
     add: function () {
       var currentId = this.get('index.model.id'),
@@ -185,6 +221,31 @@ export default Ember.ArrayController.extend({
       });
 
       this.get('currentSettings.settings').findBy('key', null).set('key', newKey);
+    },
+
+    removeAll: function() {
+      var currentId = this.get('index.model.id'),
+          querySettings = this.findBy('id', currentId);
+
+      querySettings.set('settings', []);
+    },
+
+    invalidateSession: function() {
+      var self       = this;
+      var sessionTag = this.get('sessionTag');
+      var adapter    = this.container.lookup('adapter:application');
+      var url        = adapter.buildURL() + '/jobs/sessions/' + sessionTag;
+      var model = this.get('index.model');
+
+      // @TODO: Split this into then/catch once the BE is fixed
+      adapter.ajax(url, 'DELETE').catch(function(response) {
+        if ([200, 404].contains(response.status)) {
+          model.set('sessionActive', false);
+          self.notify.success('alerts.success.sessions.deleted');
+        } else {
+          self.notify.error(response.responseJSON.message, response.responseJSON.trace);
+        }
+      });
     }
   }
 });

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

@@ -50,8 +50,8 @@ export default Ember.ObjectController.extend({
           this.send('openModal',
                     'modal-delete',
                      {
-                        heading: Ember.I18n.translations.modals.delete.heading,
-                        text: Ember.I18n.translations.modals.delete.message,
+                        heading: 'modals.delete.heading',
+                        text: 'modals.delete.message',
                         defer: defer
                      });
 

+ 1 - 1
contrib/views/hive/src/main/resources/ui/hive-web/app/helpers/all-uppercase.js

@@ -19,7 +19,7 @@
 import Ember from 'ember';
 
 export function allUppercase(input) {
-  return input.toUpperCase();
+  return input ? input.toUpperCase() : input;
 };
 
 export default Ember.Handlebars.makeBoundHelper(allUppercase);

+ 28 - 0
contrib/views/hive/src/main/resources/ui/hive-web/app/helpers/preformatted-string.js

@@ -0,0 +1,28 @@
+/**
+* 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.
+*/
+import Ember from 'ember';
+
+export function preformattedString(string) {
+  string = string.replace(/\\n/g, '&#10;'); // newline
+  string = string.replace(/\\t/g, '&#09;'); // tabs
+  string = string.replace(/^\s+|\s+$/g, ''); // trim
+
+  return new Ember.Handlebars.SafeString(string);
+}
+
+export default Ember.Handlebars.makeBoundHelper(preformattedString);

+ 4 - 0
contrib/views/hive/src/main/resources/ui/hive-web/app/index.html

@@ -28,11 +28,15 @@
 
     <link rel="stylesheet" href="assets/vendor.css">
     <link rel="stylesheet" href="assets/hive.css">
+
+    {{content-for 'head-footer'}}
   </head>
   <body>
     {{content-for 'body'}}
 
     <script src="assets/vendor.js"></script>
     <script src="assets/hive.js"></script>
+
+    {{content-for 'body-footer'}}
   </body>
 </html>

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

@@ -28,9 +28,13 @@ export default {
     Ember.I18n.translations = TRANSLATIONS;
     Ember.TextField.reopen(Ember.I18n.TranslateableAttributes);
   }
-};
+}
 
 TRANSLATIONS = {
+  tooltips: {
+    refresh: 'Refresh database',
+    loadSample: 'Load sample data'
+  },
   alerts: {
     errors: {
       save: {
@@ -40,6 +44,17 @@ TRANSLATIONS = {
       get: {
         tables: 'Error when trying to retrieve the tables for the selected database',
         columns: 'Error when trying to retrieve the table columns.'
+      },
+      sessions: {
+        delete: 'Error invalidating sessions'
+      },
+      job: {
+        status: "An error occured while processing the job."
+      }
+    },
+    success: {
+      sessions: {
+        deleted: 'Session invalidated'
       }
     }
   },
@@ -53,7 +68,12 @@ TRANSLATIONS = {
     save: {
       heading: 'Saving item',
       saveBeforeCloseHeading: "Save item before closing?",
-      message: 'Enter name:'
+      message: 'Enter name:',
+      overwrite: 'Saving will overwrite previously saved query'
+    },
+
+    download: {
+      csv: 'Download results as CSV'
     }
   },
   titles: {
@@ -62,13 +82,15 @@ TRANSLATIONS = {
     results: 'Search Results',
     settings: 'Database Settings',
     query: {
+      tab: 'Worksheet',
       editor: 'Query Editor',
       process: 'Query Process Results',
       parameters: 'Parameters',
       visualExplain: 'Visual Explain',
       tez: 'TEZ'
     },
-    download: 'Save results...'
+    download: 'Save results...',
+    tableSample: '{{tableName}} sample'
   },
   placeholders: {
     search: {
@@ -130,7 +152,7 @@ TRANSLATIONS = {
     explain: 'Explain',
     saveAs: 'Save as...',
     save: 'Save',
-    newQuery: 'New Query',
+    newQuery: 'New Worksheet',
     newUdf: 'New UDF',
     history: 'History',
     ok: 'OK',
@@ -147,9 +169,13 @@ TRANSLATIONS = {
     runOnTez: 'Run on Tez'
   },
   labels: {
-    noTablesMatches: 'No tables matches for'
+    noTablesMatch: 'No tables match',
+    table: 'Table '
   },
   popover: {
+    visualExplain: {
+      statistics: "Statistics"
+    },
     queryEditorHelp: {
       title: "Did you know?",
       content: {
@@ -163,7 +189,10 @@ TRANSLATIONS = {
   tez: {
     errors: {
       'not.deployed': "Tez View isn't deployed.",
-      'no.instance': "No instance of Tez View found."
+      'no.instance': "No instance of Tez View found.",
+      'no.dag': "No DAG available"
     }
-  }
+  },
+
+  generalError: 'Unexpected error'
 };

+ 26 - 0
contrib/views/hive/src/main/resources/ui/hive-web/app/initializers/notify.js

@@ -0,0 +1,26 @@
+/**
+* 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.
+*/
+export default {
+  name: 'notify',
+  initialize: function(container, app) {
+    app.inject('route', 'notify', 'service:notify');
+    app.inject('controller', 'notify', 'service:notify');
+    app.inject('component', 'notify', 'service:notify');
+    app.inject('views', 'notify', 'service:notify');
+  }
+};

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

@@ -21,6 +21,6 @@ import DS from 'ember-data';
 export default DS.Model.extend({
   fileContent: DS.attr(),
   hasNext: DS.attr(),
-  page: DS.attr,
+  page: DS.attr('number'),
   pageCount: DS.attr()
 });

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

@@ -26,12 +26,17 @@ export default DS.Model.extend({
   dataBase: DS.attr('string'),
   duration: DS.attr(),
   status: DS.attr('string'),
+  statusMessage: DS.attr('string'),
   dateSubmitted: DS.attr('date'),
   forcedContent: DS.attr('string'),
   logFile: DS.attr('string'),
   dagName:  DS.attr('string'),
   dagId: DS.attr('string'),
   sessionTag: DS.attr('string'),
+  page: DS.attr(),
+  statusDir: DS.attr('string'),
+  applicationId: DS.attr(),
+  confFile: DS.attr('string'),
 
   dateSubmittedTimestamp: function () {
     var date = this.get('dateSubmitted');

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

@@ -33,6 +33,8 @@ export default Ember.Route.extend({
   actions: {
     openModal: function (modalTemplate, options) {
       this.controllerFor(modalTemplate).setProperties({
+        content: options.content || {},
+        message: options.message,
         heading: options.heading,
         text: options.text,
         defer: options.defer
@@ -51,11 +53,16 @@ export default Ember.Route.extend({
       });
     },
 
-    addAlert: function (type, message, title) {
-      this.controllerFor(constants.namingConventions.alerts).pushObject({
-        type: type,
-        title: title,
-        content: message
+    openOverlay: function(overlay) {
+      return this.render(overlay.template, {
+        outlet: overlay.outlet,
+        into: overlay.into
+      });
+    },
+    closeOverlay: function(overlay) {
+      return this.disconnectOutlet({
+        outlet: overlay.outlet,
+        parentView: overlay.into
       });
     }
   }

+ 2 - 2
contrib/views/hive/src/main/resources/ui/hive-web/app/routes/index/index.js

@@ -23,14 +23,14 @@ export default Ember.Route.extend({
   beforeModel: function () {
     var model = this.controllerFor(constants.namingConventions.routes.index).get('model');
 
-    if (model) {
+    if (model && !model.get('isDeleted')) {
       if (model.get('constructor.typeKey') === constants.namingConventions.job) {
         this.transitionTo(constants.namingConventions.subroutes.historyQuery, model);
       } else {
         this.transitionTo(constants.namingConventions.subroutes.savedQuery, model);
       }
     } else {
-      this.controllerFor(constants.namingConventions.routes.index).send('addQuery');
+      this.controllerFor(constants.namingConventions.openQueries).navigateToLastTab();
     }
   }
 });

+ 88 - 0
contrib/views/hive/src/main/resources/ui/hive-web/app/services/notify.js

@@ -0,0 +1,88 @@
+/**
+* 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.
+*/
+import Ember from 'ember';
+import constants from 'hive/utils/constants';
+
+export default Ember.Service.extend({
+  types: constants.notify,
+
+  messages      : Ember.ArrayProxy.create({ content : [] }),
+  notifications : Ember.ArrayProxy.create({ content : [] }),
+
+  add: function(type, message, body) {
+    var formattedBody = this.formatMessageBody(body);
+
+    var notification = Ember.Object.create({
+      type    : type,
+      message : message,
+      body    : formattedBody
+    });
+
+    this.messages.pushObject(notification);
+    this.notifications.pushObject(notification);
+  },
+
+  info: function(message, body) {
+    this.add(this.types.INFO, message, body);
+  },
+
+  warn: function(message, body) {
+    this.add(this.types.WARN, message, body);
+  },
+
+  error: function(message, body) {
+    this.add(this.types.ERROR, message, body);
+  },
+
+  success: function(message, body) {
+    this.add(this.types.SUCCESS, message, body);
+  },
+
+  formatMessageBody: function(body) {
+    if (!body) {
+      return;
+    }
+
+    if (typeof body === "string") {
+      return body;
+    }
+
+    if (typeof body === "object") {
+      var formattedBody = "";
+      for (var key in body) {
+        formattedBody += "\n\n%@:\n%@".fmt(key, body[key]);
+      }
+
+      return formattedBody;
+    }
+  },
+
+  removeMessage: function(message) {
+    this.messages.removeObject(message);
+    this.notifications.removeObject(message);
+  },
+
+  removeNotification: function(notification) {
+    this.notifications.removeObject(notification);
+  },
+
+  removeAllMessages: function() {
+    this.messages.removeAt(0, this.messages.get('length'));
+  }
+
+});

+ 99 - 37
contrib/views/hive/src/main/resources/ui/hive-web/app/styles/app.scss

@@ -16,11 +16,15 @@
  * limitations under the License.
  */
 
+@import 'vars';
 @import 'dropdown-submenu';
+@import 'mixins';
+@import 'notifications';
+@import 'query-tabs';
 
-$panel-background: #f5f5f5;
-$placeholder-color: #aaa;
-$border-color: #ddd;
+a {
+  word-wrap: break-word;
+}
 
 @-webkit-keyframes fadeIn {
   0% {opacity: 0;}
@@ -60,6 +64,10 @@ $border-color: #ddd;
   display: flex;
 }
 
+#visual-explain {
+  white-space: nowrap;
+}
+
 #visual-explain, #tez-ui {
   position: absolute;
   left: 0;
@@ -68,13 +76,6 @@ $border-color: #ddd;
   background: white;
 }
 
-#alerts-container {
-  position: absolute;
-  left: 15px;
-  right: 15px;
-  z-index: 1100;
-}
-
 aside  hr {
   margin: 10px 0;
 }
@@ -181,21 +182,10 @@ dropdown .fa-remove {
 }
 
 .main-content {
+  width: 90%;
   flex-grow: 1;
 }
 
-.query-menu {
-  margin-top: 57px;
-
-  span, popover {
-    cursor: pointer;
-    overflow: hidden;
-    display: block;
-    border-bottom: 1px solid $border-color;
-    padding: 10px;
-  }
-}
-
 .queries-icon {
   font-size: 20px;
 
@@ -209,11 +199,23 @@ dropdown .fa-remove {
   }
 }
 
+.query-context-tab {
+  background: #f1f1f1;
+
+  &.active {
+    background: white;
+  }
+}
+
 .alert {
   margin-bottom: 5px;
   padding-bottom: 10px;
   padding-top: 10px;
 
+  strong {
+    text-decoration: underline;
+  }
+
   .alert-message {
     max-height: 250px;
     overflow-y: auto;
@@ -317,22 +319,8 @@ body {
 }
 
 .settings-container {
-  width: 100%;
-  overflow-y: scroll;
-  height: calc(100% - 41px);
-  top: 41px;
-  left: 0;
-  background-color: #fff;
-  position: absolute;
-  padding: 0 15px;
-  z-index: 1000;
-
-  border: 1px solid $border-color;
-  -webkit-animation-duration: .5s;
-          animation-duration: .5s;
-  -webkit-animation-fill-mode: both;
-          animation-fill-mode: both;
 }
+
 .settings-container .close-settings {
   float: right;
   font-size: 18px;
@@ -377,3 +365,77 @@ tree-view ul li {
   height: 822px;
   border: none;
 }
+
+.edge {
+  text-align: center;
+  font-size: 10px;
+  font-weight: 800;
+
+  .edge-path {
+    height: 2px;
+    background-color: #dedede;
+    position: absolute;
+  }
+
+  .edge-arrow {
+    position: absolute;
+    width: 0;
+    height: 0;
+    border-top: 5px solid transparent;
+    border-bottom: 5px solid transparent;
+    border-right: 10px solid black;
+  }
+}
+
+.nodes {
+  width: 100%;
+  position: relative;
+
+  .node-container {
+    text-align: center;
+
+    .node {
+      border: 1px solid #bbb;
+      background: #fefefe;
+      font-size: 12px;
+      box-sizing: border-box;
+      text-align: left;
+      max-width: 200px;
+      margin: 0 25px 100px 0;
+      display: inline-block;
+      vertical-align: top;
+
+      @include box-shadow(1px, 1px, 15px, #888888);
+
+      &.table-node, &.output-node {
+        background-color: ghostwhite;
+        color: gray;
+        padding: 5px;
+        text-align: center;
+        min-width: 100px;
+        line-height: 8px;
+        vertical-align: bottom;
+        margin-bottom: 50px;
+      }
+
+      .node-heading {
+        padding: 5px;
+        text-align: center;
+        background-color: lightslategrey;
+        color: white;
+      }
+
+      .node-content {
+        max-height: 300px;
+        white-space: normal;
+        padding: 5px;
+        overflow-y: auto;
+        overflow-x: hidden;
+
+        .fa {
+          color: green;
+        }
+      }
+    }
+  }
+}

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

@@ -0,0 +1,23 @@
+/**
+ * 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.
+ */
+
+@mixin box-shadow($horizontal, $vertical, $blur, $color) {
+  -webkit-box-shadow: $horizontal $vertical $blur $color;
+     -moz-box-shadow: $horizontal $vertical $blur $color;
+          box-shadow: $horizontal $vertical $blur $color;
+}

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

@@ -0,0 +1,36 @@
+/**
+* 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.
+*/
+.notifications-container {
+  position: absolute;
+  top: 4px;
+  right: 20px;
+  width: 600px;
+  z-index: 9999;
+}
+
+.notification > .fa {
+  width: 15px;
+  text-align: center;
+  margin-right: 10px;
+}
+
+.notifications-container .notification {
+  word-wrap: break-word;
+  max-height: 200px;
+  overflow-x: auto;
+}

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

@@ -0,0 +1,68 @@
+/**
+* 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.
+*/
+.query-menu {
+  margin-top: 57px;
+
+  span, popover {
+    cursor: pointer;
+    display: block;
+    border-bottom: 1px solid $border-color;
+    padding: 10px;
+  }
+}
+
+.fa.panel-action-icon {
+  line-height: 22px;
+  font-size: 16px;
+}
+
+.editor-overlay {
+  width: 100%;
+  overflow-y: scroll;
+  height: calc(100% - 41px);
+  top: 41px;
+  left: 0;
+  background-color: #fff;
+  position: absolute;
+  padding: 0 15px;
+  z-index: 1000;
+
+  border: 1px solid $border-color;
+  -webkit-animation-duration: .5s;
+          animation-duration: .5s;
+  -webkit-animation-fill-mode: both;
+          animation-fill-mode: both;
+}
+
+.message-body {
+  margin-top: 10px;
+}
+
+.query-menu-tab {
+  position: relative;
+}
+.query-menu-tab .badge {
+  position: absolute;
+  top: -4px;
+  left: -4px;
+  background-color: red;
+  color: #fff;
+  padding: 2px 4px;
+  font-weight: bold;
+  z-index: 9999;
+}

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

@@ -0,0 +1,20 @@
+/**
+* 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.
+*/
+$panel-background: #f5f5f5;
+$placeholder-color: #aaa;
+$border-color: #ddd;

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

@@ -16,10 +16,11 @@
 * limitations under the License.
 }}
 
+{{notify-widget}}
 {{render 'navbar'}}
 
 <div id="content">
   {{outlet}}
 
   {{outlet "modal"}}
-</div>
+</div>

+ 2 - 2
contrib/views/hive/src/main/resources/ui/hive-web/app/templates/components/alert-message-widget.hbs

@@ -18,11 +18,11 @@
 
 <div {{bind-attr class=":alert :alert-dismissible message.typeClass"}}>
   <button type="button" class="close" data-dismiss="alert" aria-hidden="true" {{action "remove"}}>&times;</button>
-  <strong>{{tb-helper message.title}}</strong>
+  <strong {{action 'toggleMessage'}}>{{tb-helper message.title}}</strong>
 
   {{#if message.isExpanded}}
     <div class="alert-message">
       {{message.content}}
     </div>
   {{/if}}
-</div>
+</div>

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

@@ -18,7 +18,12 @@
 
 <div>
   <a {{action "toggle"}} {{bind-attr class=":fa iconClass"}}> {{heading}}</a>
+  <div class="pull-right">
+    {{#each control in controls}}
+      <a {{action 'sendControlAction' control.action}} {{bind-attr class=":fa control.icon" title="control.tooltip"}}></a>
+    {{/each}}
+  </div>
 </div>
 {{#if isExpanded}}
   {{yield}}
-{{/if}}
+{{/if}}

+ 3 - 5
contrib/views/hive/src/main/resources/ui/hive-web/app/templates/alerts.hbs → contrib/views/hive/src/main/resources/ui/hive-web/app/templates/components/notify-widget.hbs

@@ -16,8 +16,6 @@
 * limitations under the License.
 }}
 
-<div id="alerts-container">
-  {{#each alert in this}}
-    {{alert-message-widget message=alert removeMessage="remove" removeLater="removeLater"}}
-  {{/each}}
-</div>
+{{#each notification in notifications}}
+  {{view "notification" notification=notification}}
+{{/each}}

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

@@ -16,7 +16,7 @@
 * limitations under the License.
 }}
 
-<div class="panel panel-default" {{bind-attr class=classNames}}>
+<div {{bind-attr class=":panel :panel-default classNames"}}>
   {{#if heading}}
     <div class="panel-heading">
       {{#if menuItems}}
@@ -34,6 +34,14 @@
               </ul>
           </div>
       {{/if}}
+
+      {{#if iconActions}}
+        {{#each iconAction in iconActions}}
+        <i {{action "sendMenuItemAction" iconAction.action}}
+          {{bind-attr class=":pull-right :panel-action-icon :fa iconAction.icon"}}></i>
+        {{/each}}
+      {{/if}}
+
       <strong>{{heading}}</strong>
       {{#if isLoading}}
         <div class="spinner small pull-right"></div>
@@ -43,4 +51,4 @@
   <div class="panel-body">
     {{yield}}
   </div>
-</div>
+</div>

+ 29 - 0
contrib/views/hive/src/main/resources/ui/hive-web/app/templates/components/query-tabs.hbs

@@ -0,0 +1,29 @@
+{{!
+* 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.
+}}
+
+{{#each tab in tabs}}
+    <span {{action tab.action tab}} {{bind-attr class=":query-menu-tab tabClassNames tab.iconClass tab.active:active"}}>
+      {{#if tab.badge}}
+        <span class="badge">{{tab.badge}}</span>
+      {{/if}}
+
+      {{#if tab.text}}
+        {{tab.text}}
+      {{/if}}
+    </span>
+{{/each}}

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

@@ -23,6 +23,7 @@
         {{#link-to tab.path tab.id tagName="li"}}
           <a>
             {{tab.name}}
+            {{#if tab.isDirty}}*{{/if}}
             {{#if view.removeEnabled}}
               <i class="fa fa-remove" {{action 'remove' tab}}></i>
             {{/if}}
@@ -37,4 +38,4 @@
   {{/each}}
 </ul>
 
-{{yield}}
+{{yield}}

+ 3 - 1
contrib/views/hive/src/main/resources/ui/hive-web/app/templates/databases-search-results.hbs

@@ -44,5 +44,7 @@
     </div>
   </div>
 {{else}}
-  <h4>{{t "labels.noTablesMatches"}} "{{tablesSearchTerm}}"</h4>
+  <div class="alert alert-warning database-explorer-alert" role="alert">
+    {{t "labels.noTablesMatch"}} <strong>&quot;{{tablesSearchTerm}}&quot;</strong>
+  </div>
 {{/if}}

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

@@ -18,11 +18,11 @@
 
 <div class="databases">
   {{#each database in content}}
-    {{#collapsible-widget heading=database.name isExpanded=database.isExpanded iconClass="fa-database" expanded="getTables"}}
+    {{#collapsible-widget heading=database.name isExpanded=database.isExpanded iconClass="fa-database" expanded="getTables" toggledParam=database}}
       {{#if database.isExpanded}}
         <div class="tables">
           {{#each table in database.visibleTables}}
-            {{#collapsible-widget heading=table.name isExpanded=table.isExpanded toggledParam=database iconClass="fa-th" expanded="getColumns"}}
+            {{#collapsible-widget heading=table.name isExpanded=table.isExpanded toggledParam=database iconClass="fa-table" expanded="getColumns" controls=tableControls}}
               {{#if table.isExpanded}}
                 <div class="columns">
                   {{#each column in table.visibleColumns}}
@@ -45,4 +45,4 @@
       {{/if}}
     {{/collapsible-widget}}
   {{/each}}
-</div>
+</div>

+ 2 - 2
contrib/views/hive/src/main/resources/ui/hive-web/app/templates/databases.hbs

@@ -16,7 +16,7 @@
 * limitations under the License.
 }}
 
-{{#panel-widget headingTranslation="titles.database" isLoading=isLoading classNames="database-explorer"}}
+{{#panel-widget headingTranslation="titles.database" isLoading=isLoading classNames="database-explorer" iconActions=panelIconActions}}
   {{#if model}}
 
     {{typeahead-widget
@@ -51,4 +51,4 @@
       {{partial selectedTab.view}}
     {{/tabs-widget}}
   {{/if}}
-{{/panel-widget}}
+{{/panel-widget}}

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

@@ -23,22 +23,22 @@
     </aside>
 
     <div class="col-md-9 col-xs-12 query-container">
-      {{render 'alerts'}}
 
       {{#panel-widget headingTranslation="titles.query.editor" classNames="query-editor-panel"}}
         {{render 'open-queries'}}
 
         <div class="toolbox">
-          <button type="button" class="btn btn-sm btn-success execute-query"
-                  {{bind-attr class="canExecute::disabled"}}
+          <button type="button"
+                  {{bind-attr class=":btn :btn-sm :btn-success :execute-query canExecute::disabled"}}
                   {{action "executeQuery"}}>
             {{t "buttons.execute"}}
           </button>
-          <button type="button" class="btn btn-sm btn-default"
-                  {{bind-attr class="canExecute::disabled"}}
+          <button type="button"
+                  {{bind-attr class=":btn :btn-sm :btn-default canExecute::disabled"}}
                   {{action "explainQuery"}}>
             {{t "buttons.explain"}}
           </button>
+
           <button type="button" class="btn btn-sm btn-default save-query-as" {{action "saveQuery"}}>{{t "buttons.saveAs"}}</button>
 
           {{render 'insert-udfs'}}
@@ -76,13 +76,7 @@
     </div>
   </div>
 
-  {{#if tezUI.showOverlay}}
-    {{render 'tez-ui'}}
-  {{/if}}
-
-  {{#if visualExplain.showOverlay}}
-    {{render 'visual-explain'}}
-  {{/if}}
+  {{outlet 'overlay'}}
 
   <div class="query-menu">
     {{#popover-widget classNames="fa fa-info-circle queries-icon" titleTranslation="popover.queryEditorHelp.title" }}
@@ -93,10 +87,6 @@
       </ul>
     {{/popover-widget}}
 
-    <span {{bind-attr class="settings.showOverlay:active :fa :fa-gear :queries-icon"}} {{action 'toggleOverlay' 'settings'}}></span>
-
-    <span {{bind-attr class="visualExplain.showOverlay:active :fa :fa-bar-chart :queries-icon"}} {{action 'toggleOverlay' 'visualExplain'}}></span>
-
-    <span {{bind-attr class="tezUI.showOverlay:active shouldShowTez::hide :queries-icon :text-icon"}} {{action 'toggleOverlay' 'tezUI'}}>TEZ</span>
+    {{query-tabs}}
   </div>
 </div>

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

@@ -20,13 +20,13 @@
   <table class="table table-expandable">
     <thead>
       <tr>
-        {{#each column in results.schema}}
+        {{#each column in formattedResults.schema}}
           <th> {{column.name}} </th>
         {{/each}}
       </tr>
     </thead>
     <tbody>
-      {{#each row in results.rows}}
+      {{#each row in formattedResults.rows}}
         <tr>
           {{#each item in row}}
             <td>{{item}}</td>
@@ -44,4 +44,4 @@
   </div>
 {{else}}
   {{error}}
-{{/if}}
+{{/if}}

+ 28 - 26
contrib/views/hive/src/main/resources/ui/hive-web/app/templates/insert-udfs.hbs

@@ -16,29 +16,31 @@
 * limitations under the License.
 }}
 
-<div class="dropdown">
-  <a role="button" data-toggle="dropdown" class="btn btn-default btn-sm" data-target="#">
-    {{t "placeholders.select.udfs"}}
-    <span class="caret"></span>
-  </a>
-  <ul class="dropdown-menu pull-right" role="menu" aria-labelledby="dropdownMenu">
-    {{#each item in this}}
-      <li class="dropdown dropdown-submenu">
-        {{#if item.file}}
-          <a tabindex="-1">{{item.file.name}}</a>
-        {{else}}
-          <a tabindex="-1">{{tb-helper item.name}}</a>
-        {{/if}}
-        <ul class="dropdown-menu">
-          {{#each udf in item.udfs}}
-            <li>
-              {{#no-bubbling click="insertUdf" data=udf tagName="a"}}
-                {{udf.name}}
-              {{/no-bubbling}}
-            </li>
-          {{/each}}
-        </ul>
-      </li>
-    {{/each}}
-  </ul>
-</div>
+{{#if this.length}}
+  <div class="dropdown">
+    <a role="button" data-toggle="dropdown" class="btn btn-default btn-sm" data-target="#">
+      {{t "placeholders.select.udfs"}}
+      <span class="caret"></span>
+    </a>
+    <ul class="dropdown-menu pull-right" role="menu" aria-labelledby="dropdownMenu">
+      {{#each item in this}}
+        <li class="dropdown dropdown-submenu">
+          {{#if item.file}}
+            <a tabindex="-1">{{item.file.name}}</a>
+          {{else}}
+            <a tabindex="-1">{{tb-helper item.name}}</a>
+          {{/if}}
+          <ul class="dropdown-menu">
+            {{#each udf in item.udfs}}
+              <li>
+                {{#no-bubbling click="insertUdf" data=udf tagName="a"}}
+                  {{udf.name}}
+                {{/no-bubbling}}
+              </li>
+            {{/each}}
+          </ul>
+        </li>
+      {{/each}}
+    </ul>
+  </div>
+{{/if}}

+ 36 - 0
contrib/views/hive/src/main/resources/ui/hive-web/app/templates/message.hbs

@@ -0,0 +1,36 @@
+{{!
+* 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 {{bind-attr class=":alert :notification view.typeClass"}}>
+  <button type="button" class="close" {{action "close" target="view"}}><span aria-hidden="true">&times;</span></button>
+  <i {{bind-attr class=":fa view.typeIcon"}}></i>
+
+  {{#if view.notification.body}}
+    <a {{action "expand" target="view"}}>
+      {{view.notification.message}}
+    </a>
+  {{else}}
+      {{view.notification.message}}
+  {{/if}}
+
+  {{#if view.isExpanded}}
+  <pre class="message-body">
+    {{preformatted-string view.notification.body}}
+  </pre>
+  {{/if}}
+</div>

+ 30 - 0
contrib/views/hive/src/main/resources/ui/hive-web/app/templates/messages.hbs

@@ -0,0 +1,30 @@
+{{!
+* 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="editor-overlay messages-container">
+  <h3>Messages
+    {{#if messages.length}}
+      <button class="btn btn-danger btn-xs" {{action 'removeAllMessages'}}><i class="fa fa-minus"></i> Clear All</button>
+    {{/if}}
+  </h3>
+
+
+  {{#each message in messages}}
+    {{view 'message' notification=message}}
+  {{/each}}
+</div>

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

@@ -17,5 +17,5 @@
 }}
 
 {{#modal-widget heading=heading close="close" ok="delete"}}
-  {{text}}
+  {{tb-helper text}}
 {{/modal-widget}}

+ 24 - 0
contrib/views/hive/src/main/resources/ui/hive-web/app/templates/modal-save-query.hbs

@@ -0,0 +1,24 @@
+{{!
+* 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.
+}}
+
+{{#modal-widget heading=heading close="close" ok="save"}}
+  {{input type="text" class="form-control" value=text }}
+  {{#if showMessage}}
+    <span class="label label-warning">{{tb-helper message}}</span>
+  {{/if}}
+{{/modal-widget}}

+ 23 - 0
contrib/views/hive/src/main/resources/ui/hive-web/app/templates/notification.hbs

@@ -0,0 +1,23 @@
+{{!
+* 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 {{bind-attr class=":alert :notification view.typeClass"}}>
+  <button type="button" class="close" {{action "close" target="view"}}><span aria-hidden="true">&times;</span></button>
+  <i {{bind-attr class=":fa view.typeIcon"}}></i>
+  {{view.notification.message}}
+</div>

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

@@ -17,7 +17,7 @@
 }}
 
 {{#tabs-widget tabs=queryTabs removeClicked="removeQueryTab" canRemove=true}}
-  {{render 'settings'}}
+  {{outlet 'overlay'}}
   {{query-editor tables=selectedTables query=currentQuery.fileContent editor=view.editor highlightedText=highlightedText
                  columnsNeeded="getColumnsForAutocomplete"}}
 {{/tabs-widget}}

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

@@ -16,44 +16,51 @@
 * limitations under the License.
 }}
 
-{{#if showOverlay}}
-  <div class="settings-container fadeIn">
-    <h3> Settings
-      <button class="btn btn-success btn-xs" {{action 'add'}}><i class="fa fa-plus"></i> Add</button>
-    </h3>
+<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}}
 
-    {{#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
-                    content=predefinedSettings
-                    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}}
+    {{#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>
+              <span class="fa fa-times-circle remove pull-right" {{action 'remove' setting}}></span>
             </div>
           </div>
-        </form>
-      </div>
-    {{/each}}
-  </div>
-{{/if}}
+        </div>
+      </form>
+    </div>
+  {{/each}}
+</div>

+ 2 - 0
contrib/views/hive/src/main/resources/ui/hive-web/app/templates/tez-ui.hbs

@@ -23,6 +23,8 @@
     {{else}}
       {{#if error}}
         <div class="alert alert-danger" role="alert"><strong>{{tb-helper error}}</strong></div>
+      {{else}}
+        <div class="alert alert-danger" role="alert"><strong>{{tb-helper 'tez.errors.no.dag'}}</strong></div>
       {{/if}}
     {{/if}}
   {{/panel-widget}}

+ 58 - 0
contrib/views/hive/src/main/resources/ui/hive-web/app/templates/visual-explain.hbs

@@ -18,5 +18,63 @@
 
 <div id="visual-explain">
   {{#panel-widget headingTranslation="titles.query.visualExplain"}}
+
+  {{#each edge in view.edges}}
+    <div class="edge">
+      <div class="edge-path" {{bind-attr style="edge.style"}}>
+        {{edge.type}}
+      </div>
+     {{!--  <div class="edge-arrow" ></div> --}}
+    </div>
+  {{/each}}
+
+  <div class="nodes">
+    {{#each group in view.verticesGroups}}
+      <div class="node-container">
+        {{#if group.contents}}
+          {{#each node in group.contents}}
+            <div {{bind-attr class="node.isTableNode:table-node node.isOutputNode:output-node :node" title="node.id"}}>
+              {{#if node.isTableNode}}
+                <p><strong>{{t 'labels.table'}}</strong></p>
+                {{node.label}}
+              {{else}}
+                {{#if node.isOutputNode}}
+                  {{node.label}}
+                {{else}}
+                  <div class="node-heading">
+                    <strong>{{node.label}}</strong>
+                  </div>
+                  <div class="node-content">
+                    {{#each section in node.contents}}
+                      <p>
+                        {{#popover-widget classNames="fa fa-info-circle" titleTranslation="popover.visualExplain.statistics" }}
+                          {{section.statistics}}
+                        {{/popover-widget}}
+                        <strong>
+                          {{section.index}}. {{section.title}}
+                        </strong>
+                        {{section.value}}
+                      </p>
+
+                      {{#each field in section.fields}}
+                        {{#if field.value}}
+                          <p>{{field.label}} {{field.value}}</p>
+                        {{/if}}
+                      {{/each}}
+                    {{/each}}
+                  </div>
+                {{/if}}
+              {{/if}}
+            </div>
+          {{/each}}
+        {{else}}
+          <div class="node" {{bind-attr title="group.label"}}>
+            {{group.label}}
+          </div>
+        {{/if}}
+      </div>
+    {{/each}}
+  </div>
+
   {{/panel-widget}}
 </div>

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

@@ -55,6 +55,7 @@ export default Ember.Object.create({
     udfInsertPrefix: 'create temporary function ',
     fileInsertPrefix: 'add jar ',
     explainPrefix: 'EXPLAIN ',
+    explainFormattedPrefix: 'EXPLAIN FORMATTED ',
     insertUdfs: 'insert-udfs',
     job: 'job',
     jobs: 'jobs',
@@ -180,7 +181,7 @@ export default Ember.Object.create({
 
   //this can be replaced by a string.format implementation
   adapter: {
-    version: '0.0.1',
+    version: '0.2.0',
     instance: 'Hive',
     apiPrefix: '/api/v1/views/HIVE/versions/',
     instancePrefix: '/instances/',
@@ -189,5 +190,25 @@ export default Ember.Object.create({
 
   settings: {
     executionEngine: 'hive.execution.engine'
+  },
+  sampleDataQuery: 'SELECT * FROM %@ LIMIT 100;',
+
+  notify: {
+    ERROR:  {
+      typeClass : 'alert-danger',
+      typeIcon  : 'fa-exclamation-triangle'
+    },
+    WARN: {
+      typeClass : 'alert-warning',
+      typeIcon  : 'fa-times-circle'
+    },
+    SUCCESS: {
+      typeClass : 'alert-success',
+      typeIcon  : 'fa-check'
+    },
+    INFO: {
+      typeClass : 'alert-info',
+      typeIcon  : 'fa-info'
+    }
   }
 });

+ 141 - 0
contrib/views/hive/src/main/resources/ui/hive-web/app/utils/dag-rules.js

@@ -0,0 +1,141 @@
+/**
+ * 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.
+ */
+
+import Ember from 'ember';
+
+export default Ember.ArrayProxy.create({
+  content: Ember.A(
+    [
+      {
+        targetOperator: 'TableScan',
+        targetProperty: 'alias:',
+        label: 'Table Scan:',
+
+        fields: [
+          {
+            label: 'filterExpr:',
+            targetProperty: 'filterExpr:'
+          }
+        ]
+      },
+      {
+        targetOperator: 'Filter Operator',
+        targetProperty: 'predicate:',
+        label: 'Filter:',
+
+        fields: []
+      },
+      {
+        targetOperator: 'Map Join Operator',
+        label: 'Map Join',
+
+        fields: []
+      },
+      {
+        targetOperator: 'Merge Join Operator',
+        label: 'Merge Join',
+
+        fields: []
+      },
+      {
+        targetOperator: 'Select Operator',
+        label: 'Select',
+
+        fields: []
+      },
+      {
+        targetOperator: 'Reduce Output Operator',
+        label: 'Reduce',
+
+        fields: [
+          {
+            label: 'Partition columns:',
+            targetProperty: 'Map-reduce partition columns:'
+          },
+          {
+            label: 'Key expressions:',
+            targetProperty: 'key expressions:'
+          },
+          {
+            label: 'Sort order:',
+            targetProperty: 'sort order:'
+          }
+        ]
+      },
+      {
+        targetOperator: 'File Output Operator',
+        label: 'File Output Operator',
+
+        fields: []
+      },
+      {
+        targetOperator: 'Group By Operator',
+        label: 'Group By:',
+
+        fields: [
+          {
+            label: 'Aggregations:',
+            targetProperties: 'aggregations:'
+          },
+          {
+            label: 'Keys:',
+            targetProperty: 'keys:'
+          }
+        ]
+      },
+      {
+        targetOperator: 'Limit',
+        targetProperty: 'Number of rows:',
+        label: 'Limit:',
+
+        fields: []
+      },
+      {
+        targetOperator: 'Extract',
+        label: 'Extract',
+
+        fields: []
+      },
+      {
+        targetOperator: 'PTF Operator',
+        label: 'Partition Table Function',
+
+        fields: []
+      },
+      {
+        targetOperator: 'Dynamic Partitioning Event Operator',
+        labelel: 'Dynamic Partitioning Event',
+
+        fields: [
+          {
+            label: 'Target column:',
+            targetProperty: 'Target column:'
+          },
+          {
+            label: 'Target Vertex:',
+            targetProperty: 'Target Vertex:'
+          },
+          {
+            label: 'Partition key expr:',
+            targetProperty: 'Partition key expr:'
+          }
+        ]
+      }
+    ]
+  )
+});

+ 12 - 23
contrib/views/hive/src/main/resources/ui/hive-web/app/controllers/alerts.js → contrib/views/hive/src/main/resources/ui/hive-web/app/views/message.js

@@ -1,4 +1,4 @@
-/**
+  /**
  * 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
@@ -15,33 +15,22 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-
 import Ember from 'ember';
+import NotificationView from 'hive/views/notification';
 
-export default Ember.ArrayController.extend({
-  pushObject: function (object) {
-    object.typeClass = 'alert-' + object.type;
-    this._super(object);
-    this.removeLater(object);
-  },
-
-  removeLater: function (object) {
-    var self = this;
-
-    Ember.run.later(function() {
-      if (!object.isExpanded) {
-        self.removeObject(object);
-      }
-    }, 5000);
-  },
+export default NotificationView.extend({
+  templateName : 'message',
+  removeLater  : Ember.K,
+  isExpanded  : false,
+  removeMessage: 'removeMessage',
 
   actions: {
-    remove: function (message) {
-      this.removeObject(message);
+    expand: function() {
+      this.toggleProperty('isExpanded');
     },
 
-    removeLater: function (message) {
-      this.removeLater(message);
+    close: function() {
+      this.get('controller').send('removeMessage', this.get('notification'));
     }
   }
-});
+});

+ 51 - 0
contrib/views/hive/src/main/resources/ui/hive-web/app/views/notification.js

@@ -0,0 +1,51 @@
+/**
+ * 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.
+ */
+
+import Ember from 'ember';
+
+export default Ember.View.extend({
+  closeAfter         : 500000,
+  isHovering         : false,
+  templateName       : 'notification',
+  removeNotification : 'removeNotification',
+
+  setup: function() {
+    this.set('typeClass', this.get('notification.type.typeClass'));
+    this.set('typeIcon', this.get('notification.type.typeIcon'));
+  }.on('init'),
+
+  removeLater: function() {
+    Ember.run.later(this, function() {
+      if (this.get('isHovering')) {
+        this.removeLater();
+      } else if (this.element) {
+        this.send('close');
+      }
+    }, this.get('closeAfter'));
+  }.on('didInsertElement'),
+
+  mouseEnter: function() { this.set('isHovering', true);  },
+  mouseLeave: function() { this.set('isHovering', false); },
+
+  actions: {
+    close: function() {
+      this.remove();
+      this.get('parentView').send('removeNotification', this.get('notification'));
+    }
+  }
+});

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

@@ -17,13 +17,28 @@
  */
 
 import Ember from 'ember';
+import dagRules from '../utils/dag-rules';
 
 export default Ember.View.extend({
+  willInsertElement: function () {
+    this.set('verticesGroups', []);
+    this.set('edges', []);
+    this.set('graph', new dagre.graphlib.Graph());
+
+    if (this.get('controller.json')) {
+      this.renderDag();
+    }
+  },
+
   didInsertElement: function () {
+    this._super();
+
     var target = this.$('#visual-explain');
 
     target.css('min-height', $('.main-content').height());
     target.animate({ width: $('.main-content').width() }, 'fast');
+
+    Ember.run.scheduleOnce('afterRender', this, this.afterRenderEvent);
   },
 
   willDestroyElement: function () {
@@ -31,5 +46,391 @@ export default Ember.View.extend({
 
     target.css('min-height', 0);
     target.css('width', 0);
+  },
+
+  getOffset: function (el) {
+    var _x = 0;
+    var _y = 0;
+    var _w = el.offsetWidth|0;
+    var _h = el.offsetHeight|0;
+    while( el && !isNaN( el.offsetLeft ) && !isNaN( el.offsetTop ) ) {
+        _x += el.offsetLeft - el.scrollLeft;
+        _y += el.offsetTop - el.scrollTop;
+        el = el.offsetParent;
+    }
+    return { top: _y, left: _x, width: _w, height: _h };
+  },
+
+  addEdge: function (div1, div2, thickness, type) {
+    var off1 = this.getOffset(div1);
+    var off2 = this.getOffset(div2);
+    // bottom right
+    var x1 = off1.left + off1.width / 2;
+    var y1 = off1.top + off1.height;
+    // top right
+    var x2 = off2.left + off2.width / 2;
+    var y2 = off2.top;
+    // distance
+    var length = Math.sqrt(((x2-x1) * (x2-x1)) + ((y2-y1) * (y2-y1)));
+    // center
+    var cx = ((x1 + x2) / 2) - (length / 2);
+    var cy = ((y1 + y2) / 2) - (thickness / 2) - 73;
+    // angle
+    var angle = Math.round(Math.atan2((y1-y2), (x1-x2)) * (180 / Math.PI));
+
+    if (angle < -90) {
+      angle = 180 + angle;
+    }
+
+    var style = "left: %@px; top: %@px; width: %@px;" +
+                "-moz-transform:rotate(%@4deg);" +
+                "-webkit-transform:rotate(%@4deg);" +
+                "-ms-transform:rotate(%@4deg);" +
+                "-transform:rotate(%@4deg);";
+
+    style = style.fmt(cx, cy, length, angle);
+
+    var edgeType;
+
+    if (type) {
+      if (type === 'BROADCAST_EDGE') {
+        edgeType = 'BROADCAST';
+      } else {
+        edgeType = 'SHUFFLE';
+      }
+    }
+
+    this.get('edges').pushObject({
+      style: style,
+      type: edgeType
+    });
+  },
+
+  afterRenderEvent : function () {
+    var g = this.get('graph');
+    var self = this;
+
+    //draw edges after the slide aniamtion for the visual explain container is done
+    Ember.run.later(function () {
+      g.edges().forEach(function (value) {
+        var edge = g.edge(value);
+        var v = value.v;
+
+        var firstNode = self.$("[title='" + value.v + "']")[0];
+        var secondNode = self.$("[title='" + value.w + "']")[0];
+
+        self.addEdge(firstNode, secondNode, 2, g.edge(value).type);
+      });
+    }, 300);
+  },
+
+  getNodeContents: function (operator, contents, table, vertex) {
+    var currentTable = table,
+      contents = contents || [],
+      nodeName,
+      node,
+      ruleNode,
+      nodeLabelValue,
+      self = this;
+
+    if (operator.constructor === Array) {
+      operator.forEach(function (childOperator) {
+        self.getNodeContents(childOperator, contents, currentTable, vertex);
+      });
+
+      return contents;
+    } else {
+      nodeName = Object.getOwnPropertyNames(operator)[0];
+      node = operator[nodeName];
+      ruleNode = dagRules.findBy('targetOperator', nodeName);
+
+      if (ruleNode) {
+        if (nodeName.indexOf('Map Join') > -1) {
+          nodeLabelValue = this.handleMapJoinNode(node, currentTable);
+          currentTable = null;
+        } else if (nodeName.indexOf('Merge Join') > -1) {
+          nodeLabelValue = this.handleMergeJoinNode(node, vertex);
+        } else {
+          nodeLabelValue = node[ruleNode.targetProperty];
+        }
+
+        contents.pushObject({
+          title: ruleNode.label,
+          statistics: node["Statistics:"],
+          index: contents.length + 1,
+          value: nodeLabelValue,
+          fields: ruleNode.fields.map(function (field) {
+            var value = node[field.targetProperty || field.targetProperties];
+
+            return {
+              label: field.label,
+              value: value
+            }
+          })
+        });
+
+        if (node.children) {
+          return this.getNodeContents(node.children, contents, currentTable, vertex);
+        } else {
+          return contents;
+        }
+      } else {
+        return contents;
+      }
+    }
+  },
+
+  handleMapJoinNode: function (node, table) {
+    var rows = table || "<rows from above>";
+    var firstTable = node["input vertices:"][0] || rows;
+    var secondTable = node["input vertices:"][1] || rows;
+
+    var joinString = node["condition map:"][0][""];
+    joinString = joinString.replace("0", firstTable);
+    joinString = joinString.replace("1", secondTable);
+    joinString += " on ";
+    joinString += node["keys:"][0] + "=";
+    joinString += node["keys:"][1];
+
+    return joinString;
+  },
+
+  handleMergeJoinNode: function (node, vertex) {
+    var graphData = this.get('controller.json')['STAGE PLANS']['Stage-1']['Tez'];
+    var edges = graphData['Edges:'];
+    var index = 0;
+    var joinString = node["condition map:"][0][""];
+
+    edges[vertex].toArray().forEach(function (edge) {
+      if (edge.type === "SIMPLE_EDGE") {
+        joinString.replace(String(index), edge.parent);
+        index++;
+      }
+    });
+
+    return joinString;
+  },
+
+  //sets operator nodes
+  setNodes: function (vertices) {
+    var g = this.get('graph');
+    var self = this;
+
+    vertices.forEach(function (vertex) {
+      var contents = [];
+      var operator;
+      var currentTable;
+
+      if (vertex.name.indexOf('Map') > -1) {
+        operator = vertex.value['Map Operator Tree:'][0];
+        currentTable = operator["TableScan"]["alias:"];
+      } else if (vertex.name.indexOf('Reducer') > -1) {
+        operator = vertex.value['Reduce Operator Tree:'];
+      }
+
+      // else if (vertex.name.indexOf('Union') > -1) {
+      //   g.setNode(vertex, {
+      //     id: vertex.name,
+      //     label: vertex.name
+      //   });
+      // }
+
+      if (operator) {
+        contents = self.getNodeContents(operator, null, currentTable, vertex.name);
+
+        g.setNode(vertex.name, {
+          contents: contents,
+          id: vertex.name,
+          label: vertex.name
+        });
+      }
+    });
+
+    return this;
+  },
+
+  //sets edges between operator nodes
+  setEdges: function (edges) {
+    var i;
+    var g = this.get('graph');
+    var invalidEdges = [];
+    var edgesToBeRemoved = [];
+    var isValidEdgeType = function (type) {
+      return type === "SIMPLE_EDGE" ||
+             type === "BROADCAST_EDGE";
+    };
+
+    edges.forEach(function (edge) {
+      var parent;
+      var type;
+
+      if (edge.value.constructor === Array) {
+        edge.value.forEach(function (childEdge) {
+          parent = childEdge.parent;
+          type = childEdge.type;
+
+          if (isValidEdgeType(type)) {
+            g.setEdge(parent, edge.name);
+            g.edge({v: parent, w: edge.name}).type = type;
+          } else {
+            invalidEdges.pushObject({
+              vertex: edge.name,
+              edge: childEdge
+            });
+          }
+        });
+      } else {
+        parent = edge.value.parent;
+        type = edge.value.type;
+
+        if (isValidEdgeType(type)) {
+          g.setEdge(parent, edge.name);
+          g.edge({v: parent, w: edge.name}).type = type;
+        } else {
+          invalidEdges.pushObject({
+            vertex: edge.name,
+            edge: edge.name
+          });
+        }
+      }
+    });
+
+    invalidEdges.forEach(function (invalidEdge) {
+      var targetEdge = g.edges().find(function (graphEdge) {
+        return graphEdge.v === invalidEdge.edge.parent ||
+               graphEdge.w === invalidEdge.edge.parent;
+      });
+
+      var targetVertex;
+
+      if (targetEdge) {
+        edgesToBeRemoved.pushObject(targetEdge);
+
+        if (targetEdge.v === invalidEdge.edge.parent) {
+          targetVertex = targetEdge.w;
+        } else {
+          targetVertex = targetEdge.v;
+        }
+
+        parent = invalidEdge.vertex;
+
+        g.setEdge({v: parent, w: targetVertex});
+        g.setEdge({v: parent, w: targetVertex}).type = "BROADCAST_EDGE";
+      }
+    });
+
+    edgesToBeRemoved.uniq().forEach(function (edge) {
+      g.removeEdge(edge.v, edge.w, edge.name);
+    });
+
+    return this;
+  },
+
+  //sets nodes for tables and their edges
+  setTableNodesAndEdges: function (vertices) {
+    var g = this.get('graph');
+
+    vertices.forEach(function (vertex) {
+      var operator;
+      var table;
+      var id;
+
+      if (vertex.name.indexOf('Map') > -1) {
+        operator = vertex.value['Map Operator Tree:'][0];
+        for (var node in operator) {
+          table = operator[node]['alias:'];
+
+          //create unique identifier by using table + map pairs so that we have
+          //different nodes for the same table if it's a table connected to multiple Map operators
+          id = table + ' for ' + vertex.name;
+
+          g.setNode(id, { id: id, label: table, isTableNode: true });
+          g.setEdge(id, vertex.name);
+        }
+      }
+    });
+
+    return this;
+  },
+
+  createNodeGroups: function () {
+    var groupedNodes = [];
+    var g = this.get('graph');
+    var lastRowNode;
+    var fileOutputOperator;
+
+    g.nodes().forEach(function (value) {
+      var node = g.node(value);
+
+      if (node) {
+        var existentRow = groupedNodes.findBy('topOffset', node.y);
+
+        if (!existentRow) {
+           groupedNodes.pushObject({
+              topOffset: node.y,
+              contents: [ node ]
+           });
+        } else {
+          existentRow.contents.pushObject(node);
+        }
+      }
+    });
+
+    groupedNodes = groupedNodes.sortBy('topOffset');
+    groupedNodes.forEach(function (group) {
+      group.contents = group.contents.sortBy('x');
+    });
+
+    lastRowNode = groupedNodes.get('lastObject.contents.lastObject');
+    fileOutputOperator = lastRowNode.contents.get('lastObject');
+
+    g.setNode(fileOutputOperator.title, { id: fileOutputOperator.title, label: fileOutputOperator.title, isOutputNode: true });
+    g.setEdge(fileOutputOperator.title, lastRowNode.id);
+
+    groupedNodes.pushObject({
+      contents: [ g.node(fileOutputOperator.title) ]
+    });
+
+    lastRowNode.contents.removeObject(fileOutputOperator);
+
+    this.set('verticesGroups', groupedNodes);
+  },
+
+  renderDag: function () {
+    var convert = function (inputObj) {
+      var array = [];
+
+      for (var key in inputObj) {
+        if (inputObj.hasOwnProperty(key)) {
+          array.pushObject({
+            name: key,
+            value: inputObj[key]
+          });
+        }
+      }
+
+      return array;
+    };
+
+    // Create a new directed graph
+    var g = this.get('graph');
+
+    var graphData = this.get('controller.json')['STAGE PLANS']['Stage-1']['Tez'];
+    var vertices = convert(graphData['Vertices:']);
+    var edges = convert(graphData['Edges:']);
+
+    // Set an object for the graph label
+    g.setGraph({});
+
+    // Default to assigning a new object as a label for each new edge.
+    g.setDefaultEdgeLabel(function () { return {}; });
+
+    this.setNodes(vertices)
+        .setEdges(edges)
+        .setTableNodesAndEdges(vertices);
+
+    dagre.layout(g);
+
+    this.createNodeGroups();
   }
 });

+ 8 - 10
contrib/views/hive/src/main/resources/ui/hive-web/bower.json

@@ -1,27 +1,25 @@
 {
   "name": "hive",
   "dependencies": {
-    "handlebars": "2.0.0",
     "jquery": "^1.11.1",
-    "ember": "1.9.0",
-    "ember-data": "1.0.0-beta.14.1",
-    "ember-resolver": "~0.1.7",
-    "loader.js": "stefanpenner/loader.js#1.0.1",
+    "ember": "1.10.0",
+    "ember-data": "1.0.0-beta.16.1",
+    "ember-resolver": "~0.1.12",
+    "loader.js": "stefanpenner/loader.js#3.2.0",
     "ember-cli-shims": "stefanpenner/ember-cli-shims#0.0.3",
-    "ember-cli-test-loader": "rwjblue/ember-cli-test-loader#0.0.4",
+    "ember-cli-test-loader": "rwjblue/ember-cli-test-loader#0.1.3",
     "ember-load-initializers": "stefanpenner/ember-load-initializers#0.0.2",
     "ember-qunit": "0.2.8",
     "ember-qunit-notifications": "0.0.7",
-    "qunit": "~1.15.0",
+    "qunit": "~1.17.1",
     "bootstrap": "~3.2.0",
-    "ember-i18n": "~2.9.0",
+    "ember-i18n": "~3.0.0",
     "blanket": "~1.1.5",
     "jquery-ui": "~1.11.2",
     "selectize": "~0.12.0",
     "pretender": "0.1.0"
   },
   "resolutions": {
-    "handlebars": "2.0.0",
-    "ember": "1.9.0"
+    "ember": "1.10.0"
   }
 }

Niektoré súbory nie sú zobrazené, pretože je v týchto rozdielových dátach zmenené mnoho súborov