drill-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From jacq...@apache.org
Subject [12/17] git commit: DRILL-77: REST API implemented using jersey and jackson + graph viz.
Date Mon, 21 Jul 2014 02:46:34 GMT
DRILL-77: REST API implemented using jersey and jackson + graph viz.


Project: http://git-wip-us.apache.org/repos/asf/incubator-drill/repo
Commit: http://git-wip-us.apache.org/repos/asf/incubator-drill/commit/65c83582
Tree: http://git-wip-us.apache.org/repos/asf/incubator-drill/tree/65c83582
Diff: http://git-wip-us.apache.org/repos/asf/incubator-drill/diff/65c83582

Branch: refs/heads/master
Commit: 65c835824c864192cbd8612b14d08d816360d31e
Parents: 02f1c82
Author: Sudheesh Katkam <skatkam@maprtech.com>
Authored: Tue Jul 8 17:23:57 2014 -0700
Committer: Jacques Nadeau <jacques@apache.org>
Committed: Sun Jul 20 16:33:39 2014 -0700

----------------------------------------------------------------------
 .../drill/exec/server/rest/DrillRestServer.java |   1 +
 .../exec/server/rest/PluginConfigWrapper.java   |  78 +++++++
 .../exec/server/rest/ProfileResources.java      | 155 ++++++++-----
 .../drill/exec/server/rest/QueryResources.java  | 115 +++-------
 .../drill/exec/server/rest/QueryWrapper.java    | 165 ++++++++++++++
 .../exec/server/rest/StorageResources.java      | 228 +++++++++++--------
 .../exec/server/rest/WebResourceServer.java     |  71 ++++++
 .../src/main/resources/rest/generic.ftl         |   3 +-
 .../src/main/resources/rest/metrics/metrics.ftl |  16 +-
 .../src/main/resources/rest/profile/list.ftl    |  63 ++---
 .../src/main/resources/rest/profile/profile.ftl |  33 ++-
 .../src/main/resources/rest/query/query.ftl     |   6 +-
 .../src/main/resources/rest/query/result.ftl    |  32 ++-
 .../src/main/resources/rest/storage/list.ftl    |  84 ++++---
 .../src/main/resources/rest/storage/update.ftl  |  54 ++++-
 .../src/main/resources/rest/www/graph.js        |  82 +++++++
 .../src/main/resources/rest/www/style.css       |  27 +++
 17 files changed, 867 insertions(+), 346 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/incubator-drill/blob/65c83582/exec/java-exec/src/main/java/org/apache/drill/exec/server/rest/DrillRestServer.java
----------------------------------------------------------------------
diff --git a/exec/java-exec/src/main/java/org/apache/drill/exec/server/rest/DrillRestServer.java b/exec/java-exec/src/main/java/org/apache/drill/exec/server/rest/DrillRestServer.java
index cdbaaaf..ac6a537 100644
--- a/exec/java-exec/src/main/java/org/apache/drill/exec/server/rest/DrillRestServer.java
+++ b/exec/java-exec/src/main/java/org/apache/drill/exec/server/rest/DrillRestServer.java
@@ -44,6 +44,7 @@ public class DrillRestServer extends ResourceConfig {
     register(QueryResources.class);
     register(MetricsResources.class);
     register(ThreadsResources.class);
+    register(WebResourceServer.class);
     register(FreemarkerMvcFeature.class);
     property(ServerProperties.METAINF_SERVICES_LOOKUP_DISABLE, true);
     register(MultiPartFeature.class);

http://git-wip-us.apache.org/repos/asf/incubator-drill/blob/65c83582/exec/java-exec/src/main/java/org/apache/drill/exec/server/rest/PluginConfigWrapper.java
----------------------------------------------------------------------
diff --git a/exec/java-exec/src/main/java/org/apache/drill/exec/server/rest/PluginConfigWrapper.java b/exec/java-exec/src/main/java/org/apache/drill/exec/server/rest/PluginConfigWrapper.java
new file mode 100644
index 0000000..003f8ae
--- /dev/null
+++ b/exec/java-exec/src/main/java/org/apache/drill/exec/server/rest/PluginConfigWrapper.java
@@ -0,0 +1,78 @@
+/**
+ * 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.drill.exec.server.rest;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import org.apache.drill.common.exceptions.ExecutionSetupException;
+import org.apache.drill.common.logical.StoragePluginConfig;
+import org.apache.drill.exec.store.StoragePluginRegistry;
+
+import javax.xml.bind.annotation.XmlRootElement;
+
+@XmlRootElement
+public class PluginConfigWrapper {
+
+  private String name;
+  private StoragePluginConfig config;
+  private boolean exists;
+
+  @JsonCreator
+  public PluginConfigWrapper(@JsonProperty("name") String name, @JsonProperty("config") StoragePluginConfig config) {
+    this.name = name;
+    this.config = config;
+    this.exists = config != null;
+  }
+
+  public String getName() {
+    return name;
+  }
+
+  public StoragePluginConfig getConfig() {
+    return config;
+  }
+
+  public boolean enabled() {
+    return exists && config.isEnabled();
+  }
+
+  public void createOrUpdateInStorage(StoragePluginRegistry storage) throws ExecutionSetupException {
+    storage.createOrUpdate(name, config, true);
+  }
+
+  public boolean setEnabledInStorage(StoragePluginRegistry storage, boolean enabled) throws ExecutionSetupException {
+    if (exists) {
+      config.setEnabled(enabled);
+      createOrUpdateInStorage(storage);
+    }
+    return exists;
+  }
+
+  public boolean exists() {
+    return exists;
+  }
+
+  public boolean deleteFromStorage(StoragePluginRegistry storage) {
+    if (exists) {
+      storage.deletePlugin(name);
+      return true;
+    }
+    return false;
+  }
+}

http://git-wip-us.apache.org/repos/asf/incubator-drill/blob/65c83582/exec/java-exec/src/main/java/org/apache/drill/exec/server/rest/ProfileResources.java
----------------------------------------------------------------------
diff --git a/exec/java-exec/src/main/java/org/apache/drill/exec/server/rest/ProfileResources.java b/exec/java-exec/src/main/java/org/apache/drill/exec/server/rest/ProfileResources.java
index 7d29ea3..5ae4267 100644
--- a/exec/java-exec/src/main/java/org/apache/drill/exec/server/rest/ProfileResources.java
+++ b/exec/java-exec/src/main/java/org/apache/drill/exec/server/rest/ProfileResources.java
@@ -19,9 +19,8 @@ package org.apache.drill.exec.server.rest;
 
 import java.io.IOException;
 import java.text.SimpleDateFormat;
-import java.util.AbstractMap;
+import java.util.ArrayList;
 import java.util.Collections;
-import java.util.Comparator;
 import java.util.Date;
 import java.util.List;
 import java.util.Map;
@@ -32,6 +31,7 @@ import javax.ws.rs.Path;
 import javax.ws.rs.PathParam;
 import javax.ws.rs.Produces;
 import javax.ws.rs.core.MediaType;
+import javax.xml.bind.annotation.XmlRootElement;
 
 import org.apache.drill.exec.proto.UserBitShared.QueryProfile;
 import org.apache.drill.exec.proto.UserBitShared.QueryResult.QueryState;
@@ -42,82 +42,131 @@ import org.glassfish.jersey.server.mvc.Viewable;
 
 import com.google.common.collect.Lists;
 
-@Path("/profiles")
+@Path("/")
 public class ProfileResources {
   static final org.slf4j.Logger logger = org.slf4j.LoggerFactory.getLogger(ProfileResources.class);
 
   @Inject WorkManager work;
 
+  public static class ProfileInfo implements Comparable<ProfileInfo> {
+    public static final SimpleDateFormat format = new SimpleDateFormat("MM/dd/yyyy HH:mm:ss");
 
-  @GET
-  @Path("/{queryid}")
-  @Produces(MediaType.TEXT_HTML)
-  public Viewable getQuery(@PathParam("queryid") String queryId) throws IOException {
-    PStore<QueryProfile> profiles = work.getContext().getPersistentStoreProvider().getPStore(QueryStatus.QUERY_PROFILE);
-    QueryProfile profile = profiles.get(queryId);
-    if(profile == null) profile = QueryProfile.getDefaultInstance();
+    private String queryId;
+    private Date time;
+    private String location;
 
-    ProfileWrapper wrapper = new ProfileWrapper(profile);
+    public ProfileInfo(String queryId, long time) {
+      this.queryId = queryId;
+      this.time = new Date(time);
+      this.location = "http://localhost:8047/profile/" + queryId + ".json";
+    }
 
-    return new Viewable("/rest/profile/profile.ftl", wrapper);
+    public String getQueryId() {
+      return queryId;
+    }
+
+    public String getTime() {
+      return format.format(time);
+    }
+
+    public String getLocation() {
+      return location;
+    }
 
+    @Override
+    public int compareTo(ProfileInfo other) {
+      return time.compareTo(other.time);
+    }
   }
 
-  @GET
-  @Produces(MediaType.TEXT_HTML)
-  public Viewable getResults() throws IOException {
-    PStore<QueryProfile> profiles = work.getContext().getPersistentStoreProvider().getPStore(QueryStatus.QUERY_PROFILE);
-
-    List<Map.Entry<String, Long>> runningIds = Lists.newArrayList();
-    List<Map.Entry<String, Long>> finishedIds = Lists.newArrayList();
-    for(Map.Entry<String, QueryProfile> entry : profiles){
-      QueryProfile q = entry.getValue();
-      if (q.getState() == QueryState.RUNNING || q.getState() == QueryState.PENDING) {
-        runningIds.add(new AbstractMap.SimpleEntry<>(entry.getKey(), q.getStart()));
-      } else {
-        finishedIds.add(new AbstractMap.SimpleEntry<>(entry.getKey(), q.getStart()));
-      }
+  @XmlRootElement
+  public class QProfiles {
+    private List<ProfileInfo> runningQueries;
+    private List<ProfileInfo> finishedQueries;
+
+    public QProfiles(List<ProfileInfo> runningQueries, List<ProfileInfo> finishedQueries) {
+      this.runningQueries = runningQueries;
+      this.finishedQueries = finishedQueries;
     }
 
-    Comparator<Map.Entry<String,Long>> comparator = new Comparator<Map.Entry<String,Long>>() {
-      @Override
-      public int compare(Map.Entry<String, Long> o1, Map.Entry<String, Long> o2) {
-        return o2.getValue().compareTo(o1.getValue());
-      }
-    };
+    public List<ProfileInfo> getRunningQueries() {
+      return runningQueries;
+    }
 
-    Collections.sort(runningIds, comparator);
-    Collections.sort(finishedIds, comparator);
+    public List<ProfileInfo> getFinishedQueries() {
+      return  finishedQueries;
+    }
+  }
 
-    List<Map.Entry<String, String>> runningQueries = Lists.newArrayList();
-    List<Map.Entry<String, String>> oldQueries = Lists.newArrayList();
-    for(Map.Entry<String, Long> entry : runningIds){
-      runningQueries.add(new AbstractMap.SimpleEntry<>(entry.getKey(), new SimpleDateFormat("MM/dd/yyyy HH:mm:ss").format(new Date(entry.getValue()))));
+  @GET
+  @Path("/profiles.json")
+  @Produces(MediaType.APPLICATION_JSON)
+  public QProfiles getProfilesJSON() {
+    PStore<QueryProfile> store = null;
+    try {
+      store = work.getContext().getPersistentStoreProvider().getPStore(QueryStatus.QUERY_PROFILE);
+    } catch (IOException e) {
+      logger.debug("Failed to get profiles from persistent store.");
+      return new QProfiles(new ArrayList<ProfileInfo>(), new ArrayList<ProfileInfo>());
     }
 
-    for(Map.Entry<String, Long> entry : finishedIds){
-      oldQueries.add(new AbstractMap.SimpleEntry<>(entry.getKey(), new SimpleDateFormat("MM/dd/yyyy HH:mm:ss").format(new Date(entry.getValue()))));
+    List<ProfileInfo> runningQueries = Lists.newArrayList();
+    List<ProfileInfo> finishedQueries = Lists.newArrayList();
+
+    for(Map.Entry<String, QueryProfile> entry : store){
+      QueryProfile profile = entry.getValue();
+      if (profile.getState() == QueryState.RUNNING || profile.getState() == QueryState.PENDING) {
+        runningQueries.add(new ProfileInfo(entry.getKey(), profile.getStart()));
+      } else {
+        finishedQueries.add(new ProfileInfo(entry.getKey(), profile.getStart()));
+      }
     }
-    // add status (running, done)
 
-    Queries queries = new Queries();
-    queries.runningQueries = runningQueries;
-    queries.oldQueries = oldQueries;
+    Collections.sort(runningQueries, Collections.reverseOrder());
+    Collections.sort(finishedQueries, Collections.reverseOrder());
 
-    return new Viewable("/rest/profile/list.ftl", queries);
+    return new QProfiles(runningQueries, finishedQueries);
   }
 
-  public static class Queries {
-    List<Map.Entry<String, String>> runningQueries;
-    List<Map.Entry<String, String>> oldQueries;
+  @GET
+  @Path("/profiles")
+  @Produces(MediaType.TEXT_HTML)
+  public Viewable getProfiles() {
+    QProfiles profiles = getProfilesJSON();
+    return new Viewable("/rest/profile/list.ftl", profiles);
+  }
 
-    public List<Map.Entry<String, String>> getRunningQueries() {
-      return runningQueries;
+  private QueryProfile getQueryProfile(String queryId) {
+    PStore<QueryProfile> store = null;
+    try {
+      store = work.getContext().getPersistentStoreProvider().getPStore(QueryStatus.QUERY_PROFILE);
+    } catch (IOException e) {
+      logger.debug("Failed to get profile for: " + queryId);
+      return QueryProfile.getDefaultInstance();
     }
+    QueryProfile profile = store.get(queryId);
+    return profile == null ?  QueryProfile.getDefaultInstance() : profile;
+  }
 
-    public List<Map.Entry<String, String>> getOldQueries() {
-      return oldQueries;
+  @GET
+  @Path("/profiles/{queryid}.json")
+  @Produces(MediaType.APPLICATION_JSON)
+  public String getProfileJSON(@PathParam("queryid") String queryId) {
+    try {
+      return new String(QueryStatus.QUERY_PROFILE.getSerializer().serialize(getQueryProfile(queryId)));
+    } catch (IOException e) {
+      logger.debug("Failed to serialize profile for: " + queryId);
+      return ("{ 'message' : 'error (unable to serialize profile)' }");
     }
   }
 
-}
+  @GET
+  @Path("/profiles/{queryid}")
+  @Produces(MediaType.TEXT_HTML)
+  public Viewable getProfile(@PathParam("queryid") String queryId) {
+    ProfileWrapper wrapper = new ProfileWrapper(getQueryProfile(queryId));
+
+    return new Viewable("/rest/profile/profile.ftl", wrapper);
+
+  }
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-drill/blob/65c83582/exec/java-exec/src/main/java/org/apache/drill/exec/server/rest/QueryResources.java
----------------------------------------------------------------------
diff --git a/exec/java-exec/src/main/java/org/apache/drill/exec/server/rest/QueryResources.java b/exec/java-exec/src/main/java/org/apache/drill/exec/server/rest/QueryResources.java
index 9c1dce1..8ad32b1 100644
--- a/exec/java-exec/src/main/java/org/apache/drill/exec/server/rest/QueryResources.java
+++ b/exec/java-exec/src/main/java/org/apache/drill/exec/server/rest/QueryResources.java
@@ -17,10 +17,9 @@
  */
 package org.apache.drill.exec.server.rest;
 
-import java.util.LinkedList;
+import java.util.ArrayList;
 import java.util.List;
-import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.atomic.AtomicInteger;
+import java.util.Map;
 
 import javax.inject.Inject;
 import javax.ws.rs.Consumes;
@@ -30,24 +29,15 @@ import javax.ws.rs.POST;
 import javax.ws.rs.Path;
 import javax.ws.rs.Produces;
 import javax.ws.rs.core.MediaType;
+import javax.xml.bind.annotation.XmlRootElement;
 
 import org.apache.drill.common.config.DrillConfig;
-import org.apache.drill.exec.client.DrillClient;
 import org.apache.drill.exec.coord.ClusterCoordinator;
-import org.apache.drill.exec.exception.SchemaChangeException;
 import org.apache.drill.exec.memory.BufferAllocator;
-import org.apache.drill.exec.proto.UserBitShared;
-import org.apache.drill.exec.record.RecordBatchLoader;
-import org.apache.drill.exec.record.VectorWrapper;
-import org.apache.drill.exec.rpc.RpcException;
-import org.apache.drill.exec.rpc.user.ConnectionThrottle;
-import org.apache.drill.exec.rpc.user.QueryResultBatch;
-import org.apache.drill.exec.rpc.user.UserResultsListener;
-import org.apache.drill.exec.vector.ValueVector;
 import org.apache.drill.exec.work.WorkManager;
 import org.glassfish.jersey.server.mvc.Viewable;
 
-@Path("/query")
+@Path("/")
 public class QueryResources {
   static final org.slf4j.Logger logger = org.slf4j.LoggerFactory.getLogger(QueryResources.class);
 
@@ -55,92 +45,55 @@ public class QueryResources {
   WorkManager work;
 
   @GET
+  @Path("/query")
   @Produces(MediaType.TEXT_HTML)
   public Viewable getQuery() {
     return new Viewable("/rest/query/query.ftl");
   }
 
   @POST
-  @Consumes(MediaType.APPLICATION_FORM_URLENCODED)
-  @Produces(MediaType.TEXT_HTML)
-  public Viewable submitQuery(@FormParam("query") String query, @FormParam("queryType") String queryType) throws Exception {
+  @Path("/query.json")
+  @Consumes(MediaType.APPLICATION_JSON)
+  @Produces(MediaType.APPLICATION_JSON)
+  public List<Map<String, Object>> submitQueryJSON(QueryWrapper query) throws Exception {
     final DrillConfig config = work.getContext().getConfig();
     final ClusterCoordinator coordinator = work.getContext().getClusterCoordinator();
     final BufferAllocator allocator = work.getContext().getAllocator();
-    DrillClient client = new DrillClient(config, coordinator, allocator);
-
-    UserBitShared.QueryType type = UserBitShared.QueryType.SQL;
-    switch (queryType){
-      case "SQL" : type = UserBitShared.QueryType.SQL; break;
-      case "LOGICAL" : type = UserBitShared.QueryType.LOGICAL; break;
-      case "PHYSICAL" : type = UserBitShared.QueryType.PHYSICAL; break;
-    }
-
-    client.connect();
-    Listener listener = new Listener(new RecordBatchLoader(work.getContext().getAllocator()));
-    client.runQuery(type, query, listener);
-    List<LinkedList<String>> result = listener.waitForCompletion();
-    client.close();
-
-    return new Viewable("/rest/query/result.ftl", result);
+    return query.run(config, coordinator, allocator);
   }
 
-  private static class Listener implements UserResultsListener {
-    private volatile Exception exception;
-    private AtomicInteger count = new AtomicInteger();
-    private CountDownLatch latch = new CountDownLatch(1);
-    private LinkedList<LinkedList<String>> output = new LinkedList<>();
-    private RecordBatchLoader loader;
+  @POST
+  @Path("/query")
+  @Consumes(MediaType.APPLICATION_FORM_URLENCODED)
+  @Produces(MediaType.TEXT_HTML)
+  public Viewable submitQuery(@FormParam("query") String query, @FormParam("queryType") String queryType) throws Exception {
+    List<Map<String, Object>> result = submitQueryJSON(new QueryWrapper(query, queryType));
 
-    Listener(RecordBatchLoader loader) {
-      this.loader = loader;
+    List<String> columnNames = new ArrayList<>(result.get(0).keySet());
+    List<List<Object>> records = new ArrayList<>();
+    for(Map m : result) {
+      records.add(new ArrayList<Object>(m.values()));
     }
+    Table table = new Table(columnNames, records);
 
-    @Override
-    public void submissionFailed(RpcException ex) {
-      exception = ex;
-      System.out.println("Query failed: " + ex.getMessage());
-      latch.countDown();
-    }
+    return new Viewable("/rest/query/result.ftl", table);
+  }
+
+  public class Table {
+    private List<String> columnNames;
+    private List<List<Object>> records;
 
-    @Override
-    public void resultArrived(QueryResultBatch result, ConnectionThrottle throttle) {
-      int rows = result.getHeader().getRowCount();
-      if (result.getData() != null) {
-        count.addAndGet(rows);
-        try {
-          loader.load(result.getHeader().getDef(), result.getData());
-          output.add(new LinkedList<String>());
-          for (int i = 0; i < loader.getSchema().getFieldCount(); ++i) {
-            output.getLast().add(loader.getSchema().getColumn(i).getPath().getAsUnescapedPath());
-          }
-        } catch (SchemaChangeException e) {
-          throw new RuntimeException(e);
-        }
-        for (int i = 0; i < rows; ++i) {
-          output.add(new LinkedList<String>());
-          for (VectorWrapper<?> vw : loader) {
-            ValueVector.Accessor accessor = vw.getValueVector().getAccessor();
-            output.getLast().add(accessor.getObject(i).toString());
-          }
-        }
-      }
-      result.release();
-      if (result.getHeader().getIsLastChunk()) {
-        latch.countDown();
-      }
+    public Table(List<String> columnNames, List<List<Object>> records) {
+      this.columnNames = columnNames;
+      this.records = records;
     }
 
-    @Override
-    public void queryIdArrived(UserBitShared.QueryId queryId) {
+    public List<String> getColumnNames() {
+      return columnNames;
     }
 
-    public List<LinkedList<String>> waitForCompletion() throws Exception {
-      latch.await();
-      if (exception != null) {
-        throw exception;
-      }
-      return output;
+    public List<List<Object>> getRecords() {
+      return records;
     }
   }
 }

http://git-wip-us.apache.org/repos/asf/incubator-drill/blob/65c83582/exec/java-exec/src/main/java/org/apache/drill/exec/server/rest/QueryWrapper.java
----------------------------------------------------------------------
diff --git a/exec/java-exec/src/main/java/org/apache/drill/exec/server/rest/QueryWrapper.java b/exec/java-exec/src/main/java/org/apache/drill/exec/server/rest/QueryWrapper.java
new file mode 100644
index 0000000..83c82db
--- /dev/null
+++ b/exec/java-exec/src/main/java/org/apache/drill/exec/server/rest/QueryWrapper.java
@@ -0,0 +1,165 @@
+/**
+ * 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.drill.exec.server.rest;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import org.apache.drill.common.config.DrillConfig;
+import org.apache.drill.exec.client.DrillClient;
+import org.apache.drill.exec.coord.ClusterCoordinator;
+import org.apache.drill.exec.exception.SchemaChangeException;
+import org.apache.drill.exec.memory.BufferAllocator;
+import org.apache.drill.exec.proto.UserBitShared;
+import org.apache.drill.exec.record.RecordBatchLoader;
+import org.apache.drill.exec.record.VectorWrapper;
+import org.apache.drill.exec.rpc.RpcException;
+import org.apache.drill.exec.rpc.user.ConnectionThrottle;
+import org.apache.drill.exec.rpc.user.QueryResultBatch;
+import org.apache.drill.exec.rpc.user.UserResultsListener;
+import org.apache.drill.exec.vector.ValueVector;
+
+import javax.xml.bind.annotation.XmlRootElement;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.atomic.AtomicInteger;
+
+@XmlRootElement
+public class QueryWrapper {
+
+  private String query;
+  private String queryType;
+
+  @JsonCreator
+  public QueryWrapper(@JsonProperty("query") String query, @JsonProperty("queryType") String queryType) {
+    this.query = query;
+    this.queryType = queryType;
+  }
+
+  public String getQuery() {
+    return query;
+  }
+
+  public String getQueryType() {
+    return queryType;
+  }
+
+  public UserBitShared.QueryType getType() {
+    UserBitShared.QueryType type = UserBitShared.QueryType.SQL;
+    switch (queryType) {
+      case "SQL" : type = UserBitShared.QueryType.SQL; break;
+      case "LOGICAL" : type = UserBitShared.QueryType.LOGICAL; break;
+      case "PHYSICAL" : type = UserBitShared.QueryType.PHYSICAL; break;
+    }
+    return type;
+  }
+
+  public List<Map<String, Object>> run(DrillConfig config, ClusterCoordinator coordinator, BufferAllocator allocator)
+    throws Exception {
+    DrillClient client = new DrillClient(config, coordinator, allocator);
+    Listener listener = new Listener(new RecordBatchLoader(allocator));
+
+    client.connect();
+    client.runQuery(getType(), query, listener);
+
+    List<Map<String, Object>> result = listener.waitForCompletion();
+    client.close();
+    return result;
+  }
+
+  @Override
+  public String toString() {
+    return "QueryRequest [queryType=" + queryType + ", query=" + query + "]";
+  }
+
+
+  private static class Listener implements UserResultsListener {
+    private volatile Exception exception;
+    private AtomicInteger count = new AtomicInteger();
+    private CountDownLatch latch = new CountDownLatch(1);
+    private List<Map<String, Object>> output = new LinkedList<>();
+    private ArrayList<String> columnNames;
+    private RecordBatchLoader loader;
+    private boolean schemaAdded = false;
+
+    Listener(RecordBatchLoader loader) {
+      this.loader = loader;
+    }
+
+    @Override
+    public void submissionFailed(RpcException ex) {
+      exception = ex;
+      System.out.println("Query failed: " + ex.getMessage());
+      latch.countDown();
+    }
+
+    @Override
+    public void resultArrived(QueryResultBatch result, ConnectionThrottle throttle) {
+      int rows = result.getHeader().getRowCount();
+      if (result.getData() != null) {
+        count.addAndGet(rows);
+        try {
+          loader.load(result.getHeader().getDef(), result.getData());
+          if (!schemaAdded) {
+            columnNames = new ArrayList<>();
+            for (int i = 0; i < loader.getSchema().getFieldCount(); ++i) {
+              columnNames.add(loader.getSchema().getColumn(i).getPath().getAsUnescapedPath());
+            }
+            schemaAdded = true;
+          }
+        } catch (SchemaChangeException e) {
+          throw new RuntimeException(e);
+        }
+        for (int i = 0; i < rows; ++i) {
+          Map<String, Object> record = new HashMap<>();
+          int j = 0;
+          for (VectorWrapper<?> vw : loader) {
+            ValueVector.Accessor accessor = vw.getValueVector().getAccessor();
+            Object object = accessor.getObject(i);
+            if (! object.getClass().getName().startsWith("java.lang")) {
+              object = object.toString();
+            }
+            record.put(columnNames.get(j), object);
+            ++j;
+          }
+          output.add(record);
+        }
+      }
+      result.release();
+      if (result.getHeader().getIsLastChunk()) {
+        latch.countDown();
+      }
+    }
+
+    @Override
+    public void queryIdArrived(UserBitShared.QueryId queryId) {
+    }
+
+    public List<Map<String, Object>> waitForCompletion() throws Exception {
+      latch.await();
+      if (exception != null) {
+        throw exception;
+      }
+      return output;
+    }
+  }
+}

http://git-wip-us.apache.org/repos/asf/incubator-drill/blob/65c83582/exec/java-exec/src/main/java/org/apache/drill/exec/server/rest/StorageResources.java
----------------------------------------------------------------------
diff --git a/exec/java-exec/src/main/java/org/apache/drill/exec/server/rest/StorageResources.java b/exec/java-exec/src/main/java/org/apache/drill/exec/server/rest/StorageResources.java
index 714e711..e841430 100644
--- a/exec/java-exec/src/main/java/org/apache/drill/exec/server/rest/StorageResources.java
+++ b/exec/java-exec/src/main/java/org/apache/drill/exec/server/rest/StorageResources.java
@@ -17,160 +17,186 @@
  */
 package org.apache.drill.exec.server.rest;
 
-import java.io.IOException;
-import java.io.StringReader;
-import java.net.URI;
-import java.util.Collections;
-import java.util.Comparator;
-import java.util.List;
-import java.util.Map;
+import com.fasterxml.jackson.core.JsonParseException;
+import com.fasterxml.jackson.databind.JsonMappingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.google.common.collect.Lists;
+import org.apache.drill.common.exceptions.ExecutionSetupException;
+import org.apache.drill.common.logical.StoragePluginConfig;
+import org.apache.drill.exec.store.StoragePlugin;
+import org.apache.drill.exec.store.StoragePluginRegistry;
+import org.apache.drill.exec.store.sys.PStoreProvider;
+import org.glassfish.jersey.server.mvc.Viewable;
 
 import javax.inject.Inject;
 import javax.ws.rs.Consumes;
+import javax.ws.rs.DELETE;
 import javax.ws.rs.FormParam;
 import javax.ws.rs.GET;
 import javax.ws.rs.POST;
 import javax.ws.rs.Path;
 import javax.ws.rs.PathParam;
 import javax.ws.rs.Produces;
-import javax.ws.rs.core.Context;
 import javax.ws.rs.core.MediaType;
-import javax.ws.rs.core.Response;
-import javax.ws.rs.core.UriInfo;
-
-import org.apache.commons.lang3.tuple.ImmutablePair;
-import org.apache.commons.lang3.tuple.Pair;
-import org.apache.drill.common.exceptions.ExecutionSetupException;
-import org.apache.drill.common.logical.StoragePluginConfig;
-import org.apache.drill.exec.store.StoragePlugin;
-import org.apache.drill.exec.store.StoragePluginRegistry;
-import org.apache.drill.exec.store.sys.PStoreProvider;
-import org.glassfish.jersey.server.mvc.Viewable;
-
-import com.fasterxml.jackson.core.JsonParseException;
-import com.fasterxml.jackson.core.JsonProcessingException;
-import com.fasterxml.jackson.databind.JsonMappingException;
-import com.fasterxml.jackson.databind.ObjectMapper;
-import com.google.common.collect.Lists;
-
-import freemarker.template.SimpleHash;
+import javax.xml.bind.annotation.XmlRootElement;
+import java.io.IOException;
+import java.io.StringReader;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Map;
 
-@Path("/storage")
+@Path("/")
 public class StorageResources {
   static final org.slf4j.Logger logger = org.slf4j.LoggerFactory.getLogger(StorageResources.class);
-  
-  static final Comparator<Pair<String, Boolean>> PLUGIN_NAME_COMPARATOR = new Comparator<Pair<String, Boolean>>() {
+
+  @Inject
+  StoragePluginRegistry storage;
+//  @Inject
+//  PStoreProvider storeProvider;
+  @Inject
+  ObjectMapper mapper;
+
+  static final Comparator<PluginConfigWrapper> PLUGIN_COMPARATOR = new Comparator<PluginConfigWrapper>() {
     @Override
-    public int compare(Pair<String, Boolean> o1, Pair<String, Boolean> o2) {
-      return o1.getKey().compareTo(o2.getKey());
+    public int compare(PluginConfigWrapper o1, PluginConfigWrapper o2) {
+      return o1.getName().compareTo(o2.getName());
     }
   };
 
-  @Inject StoragePluginRegistry storage;
-  @Inject PStoreProvider storeProvider;
-  @Inject ObjectMapper mapper;
-
   @GET
-  @Produces(MediaType.TEXT_HTML)
-  public Viewable listPlugins() {
-    // build a list and sort by plugin instance name
-    List<Pair<String, Boolean>> pluginList = Lists.newArrayList();
-    for (Map.Entry<String, StoragePluginConfig> config : storage.getStore()) {
-      pluginList.add(ImmutablePair.of(config.getKey(), config.getValue().isEnabled()));
-    }
-    Collections.sort(pluginList, PLUGIN_NAME_COMPARATOR);
-
-    // now use the sorted list to build the freemarker model
-    List<SimpleHash> modelList = Lists.newArrayList();
-    for (Pair<String, Boolean> plugin : pluginList) {
-      SimpleHash map = new SimpleHash();
-      map.put("name", plugin.getLeft());
-      map.put("enabled", plugin.getRight());
-      modelList.add(map);
+  @Path("/storage.json")
+  @Produces(MediaType.APPLICATION_JSON)
+  public List<PluginConfigWrapper> getStoragePluginsJSON() {
+
+    List<PluginConfigWrapper> list = Lists.newArrayList();
+    for (Map.Entry<String, StoragePluginConfig> entry : storage.getStore()) {
+      PluginConfigWrapper plugin = new PluginConfigWrapper(entry.getKey(), entry.getValue());
+      list.add(plugin);
     }
 
-    return new Viewable("/rest/storage/list.ftl", modelList);
+    Collections.sort(list, PLUGIN_COMPARATOR);
+
+    return list;
   }
 
   @GET
-  @Path("/{name}/config/update")
+  @Path("/storage")
   @Produces(MediaType.TEXT_HTML)
-  public Viewable update(@PathParam("name") String name) throws JsonProcessingException {
-    StoragePluginConfig config = findConfig(name);
-    String conf = config == null ? "" : mapper.writeValueAsString(config);
-
-    SimpleHash map = new SimpleHash();
-    map.put("config", conf);
-    map.put("name", name);
-    map.put("exists", config != null);
-    map.put("enabled", config != null && config.isEnabled());
-    return new Viewable("/rest/storage/update.ftl", map);
+  public Viewable getStoragePlugins() {
+    List<PluginConfigWrapper> list = getStoragePluginsJSON();
+    return new Viewable("/rest/storage/list.ftl", list);
   }
 
   @GET
-  @Path("/{name}/config/enable/{val}")
-  @Produces(MediaType.TEXT_HTML)
-  public Response setEnable(@Context UriInfo uriInfo, @PathParam("name") String name, @PathParam("val") Boolean enable) throws ExecutionSetupException {
-    StoragePluginConfig config = findConfig(name);
-    if (config != null) {
-      config.setEnabled(enable);
-      storage.createOrUpdate(name, config, true);
+  @Path("/storage/{name}.json")
+  @Produces(MediaType.APPLICATION_JSON)
+  public PluginConfigWrapper getStoragePluginJSON(@PathParam("name") String name) {
+    try {
+      StoragePlugin plugin = storage.getPlugin(name);
+      if (plugin != null) {
+        return new PluginConfigWrapper(name, plugin.getConfig());
+      }
+    } catch (Exception e) {
+      logger.info("Failure while trying to access storage config: {}", name, e);
     }
-
-    URI uri = uriInfo.getBaseUriBuilder().path("/storage").build();
-    return Response.seeOther(uri).build();
+    return new PluginConfigWrapper(name, null);
   }
 
   @GET
-  @Path("/{name}/config/delete")
+  @Path("/storage/{name}")
   @Produces(MediaType.TEXT_HTML)
-  public Viewable deleteConfig(@PathParam("name") String name) {
-    storage.deletePlugin(name);
-    return new Viewable("/rest/status.ftl", "Deleted " + name);
+  public Viewable getStoragePlugin(@PathParam("name") String name) {
+    PluginConfigWrapper plugin = getStoragePluginJSON(name);
+    return new Viewable("/rest/storage/update.ftl", plugin);
+  }
+
+  @GET
+  @Path("/storage/{name}/enable/{val}")
+  @Produces(MediaType.APPLICATION_JSON)
+  public JsonResult enablePlugin(@PathParam("name") String name, @PathParam("val") Boolean enable) {
+    PluginConfigWrapper plugin = getStoragePluginJSON(name);
+    try {
+      if (plugin.setEnabledInStorage(storage, enable)) {
+        return message("success");
+      } else {
+        return message("error (plugin does not exist)");
+      }
+    } catch (ExecutionSetupException e) {
+      logger.debug("Error in enabling storage name: " + name + " flag: " + enable);
+      return message("error (unable to enable/ disable storage)");
+    }
+  }
+
+  @DELETE
+  @Path("/storage/{name}.json")
+  @Produces(MediaType.APPLICATION_JSON)
+  public JsonResult deletePluginJSON(@PathParam("name") String name) {
+    PluginConfigWrapper plugin = getStoragePluginJSON(name);
+    if (plugin.deleteFromStorage(storage)) {
+      return message("success");
+    } else {
+      return message("error (unable to delete storage)");
+    }
   }
 
   @GET
+  @Path("/storage/{name}/delete")
   @Produces(MediaType.APPLICATION_JSON)
-  @Path("/{name}/config")
-  public StoragePluginConfig getConfig(@PathParam("name") String name) {
-    return findConfig(name);
+  public JsonResult deletePlugin(@PathParam("name") String name) {
+    return deletePluginJSON(name);
   }
 
-  private StoragePluginConfig findConfig(String name) {
+  @POST
+  @Path("/storage/{name}.json")
+  @Consumes(MediaType.APPLICATION_JSON)
+  @Produces(MediaType.APPLICATION_JSON)
+  public JsonResult createOrUpdatePluginJSON(PluginConfigWrapper plugin) {
     try {
-      StoragePlugin plugin = storage.getPlugin(name);
-      if (plugin != null) {
-        return plugin.getConfig();
-      }
-    } catch (Exception e) {
-      logger.info("Failure while trying to access storage config: {}", name, e);
-      ;
+      plugin.createOrUpdateInStorage(storage);
+      return message("success");
+    } catch (ExecutionSetupException e) {
+      logger.debug("Unable to create/ update plugin: " + plugin.getName());
+      return message("error (unable to create/ update storage)");
     }
-    return null;
   }
 
   @POST
-  @Path("/config/update")
-  @Produces(MediaType.TEXT_HTML)
+  @Path("/storage/{name}")
   @Consumes("application/x-www-form-urlencoded")
-  public Viewable createTrackInJSON(@FormParam("name") String name, @FormParam("config") String storagePluginConfig)
-      throws ExecutionSetupException, JsonParseException, JsonMappingException, IOException {
-    StoragePluginConfig config = mapper.readValue(new StringReader(storagePluginConfig), StoragePluginConfig.class);
-    storage.createOrUpdate(name, config, true);
-    return new Viewable("/rest/status.ftl", "Updated " + name);
+  @Produces(MediaType.APPLICATION_JSON)
+  public JsonResult createOrUpdatePlugin(@FormParam("name") String name, @FormParam("config") String storagePluginConfig) {
+    try {
+      StoragePluginConfig config = mapper.readValue(new StringReader(storagePluginConfig), StoragePluginConfig.class);
+      return createOrUpdatePluginJSON(new PluginConfigWrapper(name, config));
+    } catch (JsonMappingException e) {
+      logger.debug("Error in JSON mapping: " + storagePluginConfig);
+      return message("error (invalid JSON mapping)");
+    } catch (JsonParseException e) {
+      logger.debug("Error parsing JSON: " + storagePluginConfig);
+      return message("error (unable to parse JSON)");
+    } catch (IOException e) {
+      logger.debug("Failed to read: " + storagePluginConfig);
+      return message("error (unable to read)");
+    }
   }
 
-  private JsonResult r(String message) {
+  private JsonResult message(String message) {
     return new JsonResult(message);
   }
 
-  public static class JsonResult {
-    public String result = "data updated";
+  @XmlRootElement
+  public class JsonResult {
+
+    private String result;
 
     public JsonResult(String result) {
-      super();
       this.result = result;
     }
 
+    public String getResult() {
+      return result;
+    }
+
   }
 }

http://git-wip-us.apache.org/repos/asf/incubator-drill/blob/65c83582/exec/java-exec/src/main/java/org/apache/drill/exec/server/rest/WebResourceServer.java
----------------------------------------------------------------------
diff --git a/exec/java-exec/src/main/java/org/apache/drill/exec/server/rest/WebResourceServer.java b/exec/java-exec/src/main/java/org/apache/drill/exec/server/rest/WebResourceServer.java
new file mode 100644
index 0000000..33b9be9
--- /dev/null
+++ b/exec/java-exec/src/main/java/org/apache/drill/exec/server/rest/WebResourceServer.java
@@ -0,0 +1,71 @@
+/**
+ * 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.drill.exec.server.rest;
+
+import java.io.BufferedInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URLConnection;
+
+import javax.inject.Inject;
+import javax.ws.rs.GET;
+import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
+import javax.ws.rs.Produces;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.Response.Status;
+
+import org.apache.commons.io.IOUtils;
+import org.apache.drill.exec.work.WorkManager;
+
+@Path("/www")
+public class WebResourceServer {
+  static final org.slf4j.Logger logger = org.slf4j.LoggerFactory.getLogger(WebResourceServer.class);
+
+  @Inject WorkManager work;
+
+
+  @GET
+  @Path("/{path}")
+  @Produces(MediaType.TEXT_PLAIN)
+  public Response getResource(@PathParam("path") String path) throws IOException {
+    try {
+      String s = "rest/www/" + path;
+      ClassLoader cl = Thread.currentThread().getContextClassLoader();
+      InputStream is = new BufferedInputStream(cl.getResource(s).openStream());
+      
+      String mime = "text/plain";
+      if (s.endsWith(".js")) {
+        mime = "text/javascript";
+      } else if (s.endsWith(".css")) {
+        mime = "text/css";
+      } else {
+        mime = URLConnection.guessContentTypeFromStream(is);
+      }
+      
+      byte[] d = IOUtils.toByteArray(is);
+      return Response.ok(d).type(mime).build(); 
+    } catch (Exception e) {
+      e.printStackTrace();
+      e.printStackTrace(System.out);
+    }
+    
+    return Response.noContent().status(Status.NOT_FOUND).build();
+  }
+}

http://git-wip-us.apache.org/repos/asf/incubator-drill/blob/65c83582/exec/java-exec/src/main/resources/rest/generic.ftl
----------------------------------------------------------------------
diff --git a/exec/java-exec/src/main/resources/rest/generic.ftl b/exec/java-exec/src/main/resources/rest/generic.ftl
index 256d582..1cbfc23 100644
--- a/exec/java-exec/src/main/resources/rest/generic.ftl
+++ b/exec/java-exec/src/main/resources/rest/generic.ftl
@@ -49,11 +49,10 @@
               <span class="icon-bar"></span>
               <span class="icon-bar"></span>
             </button>
-            <a class="navbar-brand" href="/">Apache Drill</a>
+            <a class="navbar-brand">Apache Drill</a>
           </div>
           <div class="navbar-collapse collapse">
             <ul class="nav navbar-nav">
-              <li><a href="/status">Status</a></li>
               <li><a href="/query">Query</a></li>
               <li><a href="/profiles">Profiles</a></li>
               <li><a href="/storage">Storage</a></li>

http://git-wip-us.apache.org/repos/asf/incubator-drill/blob/65c83582/exec/java-exec/src/main/resources/rest/metrics/metrics.ftl
----------------------------------------------------------------------
diff --git a/exec/java-exec/src/main/resources/rest/metrics/metrics.ftl b/exec/java-exec/src/main/resources/rest/metrics/metrics.ftl
index b0ccde8..1600a2d 100644
--- a/exec/java-exec/src/main/resources/rest/metrics/metrics.ftl
+++ b/exec/java-exec/src/main/resources/rest/metrics/metrics.ftl
@@ -60,7 +60,7 @@
       <h3 id="histograms">Histograms</h3>
       <div id="histogramsVal">
         <div class="alert alert-info">
-          <strong>No counters.</strong>
+          <strong>No histograms.</strong>
         </div>
       </div>
       <h3 id="meters">Meters</h3>
@@ -114,13 +114,12 @@
       });
     };
 
-    function updateTimers(timers) {
-      console.log("yup");
-      $("#timersVal").html(function() {
+    function createTable(metric, name) {
+      $("#" + name + "Val").html(function() {
         var tables = "";
-        $.each(timers, function(name, stats) {
+        $.each(metric, function(clazz, stats) {
 
-          var table = "<strong>Reporting class:</strong> " + name + "<br>";
+          var table = "<strong>Reporting class:</strong> " + clazz + "<br>";
           table += "<table class=\"table table-striped\"><tbody>";
           $.each(stats, function(key, value) {
             table += "<tr>";
@@ -140,7 +139,7 @@
     };
 
     function updateOthers(metrics) {
-      $.each(["counters", "histograms", "meters"], function(i, key) {
+      $.each(["counters", "meters"], function(i, key) {
         if(! $.isEmptyObject(metrics[key])) {
           $("#" + key + "Val").html(JSON.stringify(metrics[key], null, 2));
         }
@@ -151,7 +150,8 @@
       $.get("/status/metrics", function(metrics) {
         updateGauges(metrics.gauges);
         updateBars(metrics.gauges);
-        if(! $.isEmptyObject(metrics.timers)) updateTimers(metrics.timers);
+        if(! $.isEmptyObject(metrics.timers)) createTable(metrics.timers, "timers");
+        if(! $.isEmptyObject(metrics.histograms)) createTable(metrics.histograms, "histograms");
         updateOthers(metrics);
       });
     };

http://git-wip-us.apache.org/repos/asf/incubator-drill/blob/65c83582/exec/java-exec/src/main/resources/rest/profile/list.ftl
----------------------------------------------------------------------
diff --git a/exec/java-exec/src/main/resources/rest/profile/list.ftl b/exec/java-exec/src/main/resources/rest/profile/list.ftl
index 61d3466..76f9a07 100644
--- a/exec/java-exec/src/main/resources/rest/profile/list.ftl
+++ b/exec/java-exec/src/main/resources/rest/profile/list.ftl
@@ -17,29 +17,38 @@
   <a href="/queries">back</a><br/>
   <div class="page-header">
   </div>
-  <h3>Running Queries</h3>
-  <div class="table-responsive">
-    <table class="table table-hover">
-      <thead>
-         <td>Time</td>
-         <td>Query</td>
-      </thead>
-      <tbody>
-        <#list model.getRunningQueries() as query>
-        <tr>
-          <td>${query.getValue()}</td>
-          <td>
-            <a href="/profiles/${query.getKey()}">
-              <div style="height:100%;width:100%">
-                ${query.getKey()}
-              </div>
-            </a>
-          </td>
-        </tr>
-        </#list>
-      </tbody>
-    </table>
-  </div>
+  <#if (model.getRunningQueries()?size > 0) >
+    <h3>Running Queries</h3>
+    <div class="table-responsive">
+      <table class="table table-hover">
+        <thead>
+           <td>Time</td>
+           <td>Query</td>
+        </thead>
+        <tbody>
+          <#list model.getRunningQueries() as query>
+          <tr>
+            <td>${query.getTime()}</td>
+            <td>
+              <a href="/profiles/${query.getQueryId()}">
+                <div style="height:100%;width:100%">
+                  ${query.getQueryId()}
+                </div>
+              </a>
+            </td>
+          </tr>
+          </#list>
+        </tbody>
+      </table>
+    </div>
+    <div class="page-header">
+    </div>
+  <#else>
+    <div id="message" class="alert alert-info alert-dismissable">
+      <button type="button" class="close" data-dismiss="alert" aria-hidden="true">&times;</button>
+      <strong>No running queries.</strong>
+    </div>
+  </#if>
   <h3>Completed Queries</h3>
   <div class="table-responsive">
     <table class="table table-hover">
@@ -48,13 +57,13 @@
          <td>Query</td>
       </thead>
       <tbody>
-        <#list model.getOldQueries() as query>
+        <#list model.getFinishedQueries() as query>
         <tr>
-          <td>${query.getValue()}</td>
+          <td>${query.getTime()}</td>
           <td>
-            <a href="/profiles/${query.getKey()}">
+            <a href="/profiles/${query.getQueryId()}">
               <div style="height:100%;width:100%">
-                ${query.getKey()}
+                ${query.getQueryId()}
               </div>
             </a>
           </td>

http://git-wip-us.apache.org/repos/asf/incubator-drill/blob/65c83582/exec/java-exec/src/main/resources/rest/profile/profile.ftl
----------------------------------------------------------------------
diff --git a/exec/java-exec/src/main/resources/rest/profile/profile.ftl b/exec/java-exec/src/main/resources/rest/profile/profile.ftl
index 94b8aa1..8abb316 100644
--- a/exec/java-exec/src/main/resources/rest/profile/profile.ftl
+++ b/exec/java-exec/src/main/resources/rest/profile/profile.ftl
@@ -11,16 +11,21 @@
 
 <#include "*/generic.ftl">
 <#macro page_head>
+<link href="/www/style.css" rel="stylesheet">
+
+<script src="http://d3js.org/d3.v3.min.js"></script>
+<script src="http://cpettitt.github.io/project/dagre-d3/latest/dagre-d3.js"></script>
+<script src="/www/graph.js"></script>
 </#macro>
 
 <#macro page_body>
   <a href="/queries">back</a><br/>
   <div class="page-header">
   </div>
-  <h2>Query</h2>
+  <h3>Query</h3>
   <form role="form" action="/query" method="POST">
     <div class="form-group">
-      <textarea class="form-control" id="query" name="query">${model.getProfile().query}</textarea>
+      <textarea class="form-control" id="query" name="query" style="font-family: Courier;">${model.getProfile().query}</textarea>
     </div>
     <div class="form-group">
       <div class="radio-inline">
@@ -45,17 +50,29 @@
     <button type="submit" class="btn btn-default">Re-run query</button>
   </form>
   <div class="page-header">
-    <h2>Physical Plan</h2>
   </div>
-    <p><pre>${model.profile.plan}</pre></p>
+  <h3>Visualized Plan</h3>
+  <button id="renderbutton" class="btn btn-default">Generate</button>
+  <svg id="svg-canvas" style="margin: auto; display: block;">
+    <g transform="translate(20, 20)"/>
+  </svg>
+  <div class="page-header">
+  </div>
+  <h3>Physical Plan</h3>
+  <p><pre>${model.profile.plan}</pre></p>
   <div class="page-header">
-    <h2>Profile Summary</h2>
   </div>
-    <p>${model.toString()}</p>
+  <h3>Profile Summary</h3>
+  <p>${model.toString()}</p>
   <div class="page-header">
-    <h2>Complete Profile</h2>
   </div>
-    <p><pre>${model.profile.toString()}</pre></p>
+  <div class="span4 collapse-group">
+    <a class="btn btn-default" data-toggle="collapse" data-target="#viewdetails">View complete profile</a>
+    <br> <br>
+    <pre class="collapse" id="viewdetails">${model.profile.toString()}</pre>
+  </div>
+  <div class="page-header">
+  </div> <br>
 </#macro>
 
 <@page_html/>

http://git-wip-us.apache.org/repos/asf/incubator-drill/blob/65c83582/exec/java-exec/src/main/resources/rest/query/query.ftl
----------------------------------------------------------------------
diff --git a/exec/java-exec/src/main/resources/rest/query/query.ftl b/exec/java-exec/src/main/resources/rest/query/query.ftl
index 32e18de..5033aca 100644
--- a/exec/java-exec/src/main/resources/rest/query/query.ftl
+++ b/exec/java-exec/src/main/resources/rest/query/query.ftl
@@ -17,6 +17,10 @@
   <a href="/queries">back</a><br/>
   <div class="page-header">
   </div>
+  <div id="message" class="alert alert-info alert-dismissable" style="font-family: Courier;">
+    <button type="button" class="close" data-dismiss="alert" aria-hidden="true">&times;</button>
+    Sample SQL query: <strong>SELECT * FROM cp.`employee.json` LIMIT 20</strong>
+  </div>
   <form role="form" action="/query" method="POST">
     <div class="form-group">
       <label for="queryType">Query Type</label>
@@ -41,7 +45,7 @@
     </div>
     <div class="form-group">
       <label for="query">Query</label>
-      <textarea class="form-control" id="query" rows="5" name="query"></textarea>
+      <textarea class="form-control" id="query" rows="5" name="query" style="font-family: Courier;"></textarea>
     </div>
     <button type="submit" class="btn btn-default">Submit</button>
   </form>

http://git-wip-us.apache.org/repos/asf/incubator-drill/blob/65c83582/exec/java-exec/src/main/resources/rest/query/result.ftl
----------------------------------------------------------------------
diff --git a/exec/java-exec/src/main/resources/rest/query/result.ftl b/exec/java-exec/src/main/resources/rest/query/result.ftl
index a3cb0ce..0d60da8 100644
--- a/exec/java-exec/src/main/resources/rest/query/result.ftl
+++ b/exec/java-exec/src/main/resources/rest/query/result.ftl
@@ -15,8 +15,7 @@
   <link rel="stylesheet" type="text/css" href="//cdn.datatables.net/plug-ins/be7019ee387/integration/jqueryui/dataTables.jqueryui.css">
 
   <script type="text/javascript" language="javascript" src="//code.jquery.com/jquery-1.10.2.min.js"></script>
-  <script type="text/javascript" language="javascript" src="//cdn.datatables.net/1.10.0/js/jquery.dataTables.min.js"></script>
-  <script type="text/javascript" language="javascript" src="//cdn.datatables.net/plug-ins/be7019ee387/integration/jqueryui/dataTables.jqueryui.js"></script>
+  <script type="text/javascript" language="javascript" src="//cdn.datatables.net/1.10.0/js/jquery.dataTables.js"></script>
 
   <link rel="stylesheet" type="text/css" href="//cdn.datatables.net/colvis/1.1.0/css/dataTables.colVis.css">
   <script type="text/javascript" language="javascript" src="//cdn.datatables.net/colvis/1.1.0/js/dataTables.colVis.min.js"></script>
@@ -26,35 +25,32 @@
   <a href="/queries">back</a><br/>
   <div class="page-header">
   </div>
-  <h2>Result</h2>
-  <div style="width=100%; overflow: auto;">
-    <table id="relation" class="table table-striped table-bordered table-condensed" style="table-layout: auto; width=100%;">
-      <#assign rows = model[0]>
+  <div>
+    <table id="result" class="table table-striped table-bordered table-condensed" style="table-layout: auto; width=100%;">
       <thead>
         <tr>
-          <#list rows as row>
-          <th>${row}</th>
+          <#list model.getColumnNames() as value>
+          <th>${value}</th>
           </#list>
         </tr>
       </thead>
       <tbody>
-      <#list model as rows>
-        <#if (rows_index > 0)>
-          <tr>
-            <#list rows as row>
-            <td>${row}</td>
-            </#list>
-          </tr>
-        </#if>
+      <#list model.getRecords() as record>
+        <tr>
+          <#list record as value>
+          <td>${value}</td>
+          </#list>
+        </tr>
       </#list>
       </tbody>
     </table>
   </div>
   <script charset="utf-8">
     $(document).ready(function() {
-      $('#relation').dataTable( {
+      $('#result').dataTable( {
         "scrollX" : true,
-        "dom": '<Clfrtip>'
+        "dom": '<"H"lCfr>t<"F"ip>',
+        "jQueryUI" : true
       } );
     } );
   </script>

http://git-wip-us.apache.org/repos/asf/incubator-drill/blob/65c83582/exec/java-exec/src/main/resources/rest/storage/list.ftl
----------------------------------------------------------------------
diff --git a/exec/java-exec/src/main/resources/rest/storage/list.ftl b/exec/java-exec/src/main/resources/rest/storage/list.ftl
index 226bd2b..ef97561 100644
--- a/exec/java-exec/src/main/resources/rest/storage/list.ftl
+++ b/exec/java-exec/src/main/resources/rest/storage/list.ftl
@@ -17,58 +17,72 @@
   <a href="/queries">back</a><br/>
   <div class="page-header">
   </div>
-  <h3>Registered Storage Plugins</h3>
+  <h4>Enabled Storage Plugins</h4>
   <div class="table-responsive">
     <table class="table">
       <tbody>
-        <tr><th colspan="2" style="border:none;">Enabled Plugins</th></tr>
         <#list model as plugin>
-        <#if plugin.enabled == true>
-          <tr>
-            <td style="border:none;">
-              ${plugin.name}
-            </td>
-            <td style="border:none;">
-              <a class="btn btn-default" href="/storage/${plugin.name}/config/update">Update</a>
-              <a class="btn btn-default" href="/storage/${plugin.name}/config/enable/false">Disable</a>
-            </td>
-          </tr>
-        </#if>
+          <#if plugin.enabled() == true>
+            <tr>
+              <td style="border:none; width:200px;">
+                ${plugin.getName()}
+              </td>
+              <td style="border:none;">
+                <a class="btn btn-primary" href="/storage/${plugin.getName()}">Update</a>
+                <a class="btn btn-default" onclick="doEnable('${plugin.getName()}', false)">Disable</a>
+              </td>
+            </tr>
+          </#if>
         </#list>
-        <tr><th colspan="2" style="border:none;">Disabled Plugins</th></tr>
+      </tbody>
+    </table>
+  </div>
+  <div class="page-header">
+  </div>
+  <h4>Disabled Storage Plugins</h4>
+  <div class="table-responsive">
+    <table class="table">
+      <tbody>
         <#list model as plugin>
-        <#if plugin.enabled == false>
-          <tr>
-            <td style="border:none;">
-              ${plugin.name}
-            </td>
-            <td style="border:none;">
-              <a class="btn btn-default" href="/storage/${plugin.name}/config/update">Update</a>
-              <a class="btn btn-default" href="/storage/${plugin.name}/config/enable/true">Enable</a>
-            </td>
-          </tr>
-        </#if>
+          <#if plugin.enabled() == false>
+            <tr>
+              <td style="border:none; width:200px;">
+                ${plugin.getName()}
+              </td>
+              <td style="border:none;">
+                <a class="btn btn-primary" href="/storage/${plugin.getName()}">Update</a>
+                <a class="btn btn-primary" onclick="doEnable('${plugin.getName()}', true)">Enable</a>
+              </td>
+            </tr>
+          </#if>
         </#list>
       </tbody>
     </table>
   </div>
+  <div class="page-header">
+  </div>
   <div>
-    <h4>Create new storage configuration</h4>
+    <h4>New Storage Plugin</h4>
     <form class="form-inline" id="newStorage" role="form" action="/" method="GET">
       <div class="form-group">
         <input type="text" class="form-control" id="storageName" placeholder="Storage Name">
       </div>
-      <script>
-        function doSubmit() {
-          var name = document.getElementById("storageName");
-          var form = document.getElementById("newStorage");
-          form.action = "/storage/" + name.value + "/config/update";
-          form.submit();
-        }
-      </script>
-      <button type="submit" class="btn btn-default" onclick="javascript:doSubmit();">Create</button>
+      <button type="submit" class="btn btn-default" onclick="doSubmit()">Create</button>
     </form>
   </div>
+  <script>
+    function doSubmit() {
+      var name = document.getElementById("storageName");
+      var form = document.getElementById("newStorage");
+      form.action = "/storage/" + name.value;
+      form.submit();
+    };
+    function doEnable(name, flag) {
+      $.get("/storage/" + name + "/enable/" + flag, function(data) {
+        location.reload();
+      });
+    };
+  </script>
 </#macro>
 
 <@page_html/>
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-drill/blob/65c83582/exec/java-exec/src/main/resources/rest/storage/update.ftl
----------------------------------------------------------------------
diff --git a/exec/java-exec/src/main/resources/rest/storage/update.ftl b/exec/java-exec/src/main/resources/rest/storage/update.ftl
index 3ba375d..1b3316a 100644
--- a/exec/java-exec/src/main/resources/rest/storage/update.ftl
+++ b/exec/java-exec/src/main/resources/rest/storage/update.ftl
@@ -11,6 +11,7 @@
 
 <#include "*/generic.ftl">
 <#macro page_head>
+  <script src="http://malsup.github.com/jquery.form.js"></script>
 </#macro>
 
 <#macro page_body>
@@ -18,24 +19,53 @@
   <div class="page-header">
   </div>
   <h3>Configuration</h3>
-  <form role="form" action="/storage/config/update" method="POST">
-    <input type="hidden" name="name" value="${model.name}" />
+  <form id="updateForm" role="form" action="/storage/${model.getName()}" method="POST">
+    <input type="hidden" name="name" value="${model.getName()}" />
     <div class="form-group">
-      <textarea class="form-control" id="config" rows="20" cols="50" name="config" style="font-family: Courier;">${model.config}</textarea>
+      <textarea class="form-control" id="config" rows="20" cols="50" name="config" style="font-family: Courier;">
+      </textarea>
     </div>
     <a class="btn btn-default" href="/storage">Back</a>
-    <button class="btn btn-default" type="submit">
-      <#if model.exists >Update<#else>Create</#if>
+    <button class="btn btn-default" type="submit" onclick="doUpdate();">
+      <#if model.exists()>Update<#else>Create</#if>
     </button>
-    <#if model.enabled>
-      <a class="btn btn-default" href="/storage/${model.name}/config/enable/false">Disable</a>
-    <#else>
-      <a class="btn btn-default" href="/storage/${model.name}/config/enable/true">Enable</a>
-    </#if>
-    <#if model.exists>
-      <a class="btn btn-danger" href="/storage/${model.name}/config/delete">Delete</a>
+    <#if model.exists()>
+      <#if model.enabled()>
+        <a id="enabled" class="btn btn-default">Disable</a>
+      <#else>
+        <a id="enabled" class="btn btn-primary">Enable</a>
+      </#if>
+      <a id="del" class="btn btn-danger" onclick="deleteFunction()">Delete</a>
     </#if>
   </form>
+  <br>
+  <div id="message" class="hidden alert alert-info">
+  </div>
+  <script>
+    $.get("/storage/${model.getName()}.json", function(data) {
+      $("#config").val(JSON.stringify(data.config, null, 2));
+    });
+    $("#enabled").click(function() {
+      $.get("/storage/${model.getName()}/enable/<#if model.enabled()>false<#else>true</#if>", function(data) {
+        $("#message").removeClass("hidden").text(data.result).alert();
+        setTimeout(function() { location.reload(); }, 800);
+      });
+    });
+    function doUpdate() {
+      $("#updateForm").ajaxForm(function(data) {
+        $("#message").removeClass("hidden").text(data.result).alert();
+        setTimeout(function() { location.reload(); }, 800);
+      });
+    };
+    function deleteFunction() {
+      var temp = confirm("Are you sure?");
+      if (temp == true) {
+        $.get("/storage/${model.getName()}/delete", function(data) {
+          window.location.href = "/storage";
+        });
+      }
+    };
+  </script>
 </#macro>
 
 <@page_html/>
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-drill/blob/65c83582/exec/java-exec/src/main/resources/rest/www/graph.js
----------------------------------------------------------------------
diff --git a/exec/java-exec/src/main/resources/rest/www/graph.js b/exec/java-exec/src/main/resources/rest/www/graph.js
new file mode 100644
index 0000000..0173da5
--- /dev/null
+++ b/exec/java-exec/src/main/resources/rest/www/graph.js
@@ -0,0 +1,82 @@
+/*
+  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. */
+
+
+$(window).load(function () {
+    document.getElementById("renderbutton").addEventListener("click", function () {
+	this.remove();
+	var queryid = window.location.href.split("/").splice(-1);
+	$.ajax({
+	    type: "GET",
+	    dataType: "json",
+	    url: "/profiles/" + queryid + ".json",
+	    success: function (profile) {
+		var colors = ["red", "green", "blue", "orange", "yellow", "pink", "purple", "gray"];
+
+		var plan = $.map(profile.plan.trim().split("\n"), function (s) {
+		    return [/^([0-9-]+)( *)([a-zA-Z]*)/.exec(s).slice(1)];
+		});
+
+		// nodes
+		var g = new dagreD3.Digraph();
+		for (var i = 0; i < plan.length; i++) {
+		    g.addNode(plan[i][0], {
+			label: plan[i][2],
+			fragment: parseInt(plan[i][0].split("-")[0])
+		    });
+		}
+
+		// edges
+		var st = [plan[0]];
+		for (var i = 1; i < plan.length; i++) {
+		    var top = st.pop();
+		    while (top[1].length >= plan[i][1].length)
+			top = st.pop();
+
+		    g.addEdge(null, plan[i][0], top[0]);
+
+		    if (plan[i][1].length != top[1].length)
+			st.push(top);
+		    if (plan[i][1].length >= top[1].length)
+			st.push(plan[i]);
+		}
+
+		// rendering
+		var renderer = new dagreD3.Renderer();
+		renderer.zoom(function () {return function (graph, root) {}});
+		var oldDrawNodes = renderer.drawNodes();
+		renderer.drawNodes(function(graph, root) {
+		    var svgNodes = oldDrawNodes(graph, root);
+		    svgNodes.each(function(u) {
+			d3.select(this).style("fill", colors[graph.node(u).fragment % colors.length]);
+		    });
+		    return svgNodes;
+		});
+
+		// page placement
+		var layout = dagreD3.layout()
+                    .nodeSep(20);//.rankDir("LR");
+		var layout = renderer.layout(layout).run(g, d3.select("svg g"));
+
+		//var layout = renderer.run(g, d3.select("svg g"));
+		d3.select("svg")
+		    .attr("width", layout.graph().width + 40)
+		    .attr("height", layout.graph().height + 40);
+	    },
+	    error: function (x, y, z) {
+		console.log(x);
+		console.log(y);
+		console.log(z);
+	    }
+	});
+    }, false);
+});

http://git-wip-us.apache.org/repos/asf/incubator-drill/blob/65c83582/exec/java-exec/src/main/resources/rest/www/style.css
----------------------------------------------------------------------
diff --git a/exec/java-exec/src/main/resources/rest/www/style.css b/exec/java-exec/src/main/resources/rest/www/style.css
new file mode 100644
index 0000000..46ff811
--- /dev/null
+++ b/exec/java-exec/src/main/resources/rest/www/style.css
@@ -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. */
+
+
+.node rect {
+    stroke: #000;
+    stroke-width: 1px;
+}
+
+.node text {
+    fill: #000;
+}
+
+.edgePath path {
+    stroke: #333;
+    stroke-width: 1.5px;
+    fill: none;
+}


Mime
View raw message