A user interface for viewing and administering clusters

Project: http://git-wip-us.apache.org/repos/asf/helix/repo
Commit: http://git-wip-us.apache.org/repos/asf/helix/commit/6cbafef0
Tree: http://git-wip-us.apache.org/repos/asf/helix/tree/6cbafef0
Diff: http://git-wip-us.apache.org/repos/asf/helix/diff/6cbafef0

Branch: refs/heads/master
Commit: 6cbafef0351e9957b18c6fcbfe60ad1b4c8de94f
Parents: 9087ce0
Author: Greg Brandt <[email protected]>
Authored: Sat Mar 28 09:56:14 2015 -0700
Committer: Greg Brandt <[email protected]>
Committed: Sat Mar 28 09:56:14 2015 -0700

----------------------------------------------------------------------
 helix-ui/.gitignore                             |  22 ++
 helix-ui/README.md                              |  77 +++++++
 helix-ui/doc/quickstart-admin-instances.png     | Bin 0 -> 80350 bytes
 helix-ui/doc/quickstart-admin-resource.png      | Bin 0 -> 74829 bytes
 helix-ui/doc/quickstart-end-table.png           | Bin 0 -> 135259 bytes
 helix-ui/doc/quickstart-end-visualizer.png      | Bin 0 -> 116886 bytes
 helix-ui/pom.xml                                | 128 ++++++++++++
 helix-ui/sample-admin-server.yml                |  12 ++
 helix-ui/sample-server.yml                      |   7 +
 .../org/apache/helix/ui/HelixUIApplication.java |  74 +++++++
 .../ui/HelixUIApplicationConfiguration.java     |  30 +++
 .../apache/helix/ui/api/ClusterConnection.java  |  22 ++
 .../org/apache/helix/ui/api/ClusterSpec.java    |  33 +++
 .../org/apache/helix/ui/api/ConfigTableRow.java |  49 +++++
 .../helix/ui/api/D3ResourceCoCentricCircle.java | 116 +++++++++++
 .../org/apache/helix/ui/api/IdealStateSpec.java |  94 +++++++++
 .../org/apache/helix/ui/api/InstanceSpec.java   |  32 +++
 .../org/apache/helix/ui/api/ResourceSpec.java   |  31 +++
 .../apache/helix/ui/api/ResourceStateSpec.java  |  68 ++++++
 .../helix/ui/api/ResourceStateTableRow.java     |  69 +++++++
 .../ui/health/ClusterConnectionHealthCheck.java |  24 +++
 .../apache/helix/ui/resource/AdminResource.java | 181 ++++++++++++++++
 .../helix/ui/resource/DashboardResource.java    | 164 +++++++++++++++
 .../helix/ui/resource/VisualizerResource.java   |  53 +++++
 .../apache/helix/ui/task/ClearClientCache.java  |  25 +++
 .../helix/ui/task/ClearDataCacheTask.java       |  25 +++
 .../org/apache/helix/ui/util/ClientCache.java   | 100 +++++++++
 .../org/apache/helix/ui/util/DataCache.java     | 188 +++++++++++++++++
 .../ui/util/DropWizardApplicationRunner.java    |  86 ++++++++
 .../helix/ui/util/ZkAddressValidator.java       |  37 ++++
 .../org/apache/helix/ui/view/ClusterView.java   |  85 ++++++++
 .../org/apache/helix/ui/view/LandingView.java   |   9 +
 .../org/apache/helix/ui/view/ResourceView.java  | 103 +++++++++
 helix-ui/src/main/resources/assets/css/app.css  |  92 +++++++++
 .../assets/css/uikit.almost-flat.min.css        |   2 +
 .../main/resources/assets/fonts/FontAwesome.otf | Bin 0 -> 85908 bytes
 .../assets/fonts/fontawesome-webfont.eot        | Bin 0 -> 56006 bytes
 .../assets/fonts/fontawesome-webfont.ttf        | Bin 0 -> 112160 bytes
 .../assets/fonts/fontawesome-webfont.woff       | Bin 0 -> 65452 bytes
 .../main/resources/assets/img/helix-logo.png    | Bin 0 -> 17992 bytes
 helix-ui/src/main/resources/assets/js/admin.js  | 207 +++++++++++++++++++
 helix-ui/src/main/resources/assets/js/app.js    | 103 +++++++++
 .../assets/js/components/accordion.min.js       |   2 +
 .../assets/js/components/autocomplete.min.js    |   2 +
 .../resources/assets/js/components/cover.min.js |   2 +
 .../assets/js/components/datepicker.min.js      |   3 +
 .../assets/js/components/form-password.min.js   |   2 +
 .../assets/js/components/form-select.min.js     |   2 +
 .../resources/assets/js/components/grid.min.js  |   2 +
 .../assets/js/components/htmleditor.min.js      |   2 +
 .../assets/js/components/lightbox.min.js        |   2 +
 .../assets/js/components/nestable.min.js        |   2 +
 .../assets/js/components/notify.min.js          |   2 +
 .../assets/js/components/pagination.min.js      |   2 +
 .../assets/js/components/search.min.js          |   2 +
 .../assets/js/components/slideshow-fx.min.js    |   2 +
 .../assets/js/components/slideshow.min.js       |   2 +
 .../assets/js/components/sortable.min.js        |   2 +
 .../assets/js/components/sticky.min.js          |   2 +
 .../assets/js/components/timepicker.min.js      |   2 +
 .../assets/js/components/upload.min.js          |   2 +
 helix-ui/src/main/resources/assets/js/d3.min.js |   5 +
 .../resources/assets/js/jquery-1.11.2.min.js    |   4 +
 .../main/resources/assets/js/landing-view.js    |   6 +
 .../resources/assets/js/resource-state-table.js |  55 +++++
 .../main/resources/assets/js/resource-table.js  |  43 ++++
 .../src/main/resources/assets/js/uikit.min.js   |   3 +
 .../src/main/resources/assets/js/visualizer.js  |  90 ++++++++
 .../org/apache/helix/ui/view/cluster-view.ftl   |  61 ++++++
 .../helix/ui/view/common/cluster-admin.ftl      |   5 +
 .../helix/ui/view/common/config-table.ftl       |  24 +++
 .../org/apache/helix/ui/view/common/css.ftl     |   2 +
 .../helix/ui/view/common/ideal-state-table.ftl  |  34 +++
 .../helix/ui/view/common/instance-admin.ftl     |   8 +
 .../helix/ui/view/common/instance-table.ftl     |  38 ++++
 .../org/apache/helix/ui/view/common/js.ftl      |   6 +
 .../helix/ui/view/common/resource-admin.ftl     |  39 ++++
 .../ui/view/common/resource-state-table.ftl     |  35 ++++
 .../helix/ui/view/common/resource-table.ftl     |  35 ++++
 .../ui/view/common/resource-visualizer.ftl      |  10 +
 .../apache/helix/ui/view/common/side-nav.ftl    |  21 ++
 .../org/apache/helix/ui/view/landing-view.ftl   |  21 ++
 .../org/apache/helix/ui/view/resource-view.ftl  |  49 +++++
 pom.xml                                         |   1 +
 84 files changed, 2982 insertions(+)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/helix/blob/6cbafef0/helix-ui/.gitignore
----------------------------------------------------------------------
diff --git a/helix-ui/.gitignore b/helix-ui/.gitignore
new file mode 100644
index 0000000..12b5d6b
--- /dev/null
+++ b/helix-ui/.gitignore
@@ -0,0 +1,22 @@
+*.class
+
+# Mobile Tools for Java (J2ME)
+.mtj.tmp/
+
+# Package Files #
+*.jar
+*.war
+*.ear
+
+# virtual machine crash logs, see 
http://www.java.com/en/download/help/error_hotspot.xml
+hs_err_pid*
+
+# intellij
+.idea
+*.ipr
+*.iws
+*.iml
+
+# maven
+target
+dependency-reduced-pom.xml

http://git-wip-us.apache.org/repos/asf/helix/blob/6cbafef0/helix-ui/README.md
----------------------------------------------------------------------
diff --git a/helix-ui/README.md b/helix-ui/README.md
new file mode 100644
index 0000000..63373eb
--- /dev/null
+++ b/helix-ui/README.md
@@ -0,0 +1,77 @@
+# helix-ui
+
+After building the project from the root directory (i.e. `./build`), find the
+`helix-ui-${version}.jar` artifact in this module's `target` directory.
+
+To run the UI server in read-only mode with no configuration, execute the
+following commands:
+
+```
+java -jar helix-ui-${version}.jar server
+```
+
+Navigate to `http://localhost:8080/dashboard` to get started. At this page,
+enter a ZooKeeper address, e.g. "localhost:2181,localhost:2182" or
+"some-machine:2181/chroot", to get started.
+
+The following shows using the dashboard to view the end state of MyResource in
+the Quick Start, in tabular form:
+
+![Quick Start End Table](doc/quickstart-end-table.png)
+
+And using visualization:
+
+![Quick Start End Visualizer](doc/quickstart-end-visualizer.png)
+
+## Admin
+
+In order to run the server in admin mode, set `adminMode: true` in the
+application configuration. 
+
+To restrict the ZooKeeper machines that the UI will try to connect to, use the
+`zkAddresses` configuration parameter. The application will never try to
+connect using a ZooKeeper connection string that contains machines not in that
+list.
+
+When the server is configured to run in admin mode, several buttons to perform
+actions like add / drop resource, add / enable / disable / drop instance, etc.
+are rendered on the UI at appropriate locations.
+
+The following shows the admin resource view:
+
+![Admin Resource View](doc/quickstart-admin-resource.png)
+
+And the following shows the admin instance view:
+
+![Admin Instance View](doc/quickstart-admin-instances.png)
+
+## Configuration
+
+For example, the following configuration runs the server in admin mode, only
+connecting to "localhost:2181", on ports 60000 for normal application traffic,
+and 60001 for admin actions:
+
+```
+adminMode: true
+
+zkAddresses:
+  - "localhost:2181"
+
+server:
+    applicationConnectors:
+        - type: http
+          port: 60000
+    adminConnectors:
+        - type: http
+          port: 60001
+```
+
+If this configuration exists in a file named `/tmp/my-config.yml`, one would
+run the server in the following way:
+
+```
+java -jar helix-ui-${version}.jar server /tmp/my-config.yml
+```
+
+For more details on configuration, see [Dropwizard Configuration
+Reference](https://dropwizard.github.io/dropwizard/manual/configuration.html)

http://git-wip-us.apache.org/repos/asf/helix/blob/6cbafef0/helix-ui/doc/quickstart-admin-instances.png
----------------------------------------------------------------------
diff --git a/helix-ui/doc/quickstart-admin-instances.png 
b/helix-ui/doc/quickstart-admin-instances.png
new file mode 100644
index 0000000..cc23f15
Binary files /dev/null and b/helix-ui/doc/quickstart-admin-instances.png differ

http://git-wip-us.apache.org/repos/asf/helix/blob/6cbafef0/helix-ui/doc/quickstart-admin-resource.png
----------------------------------------------------------------------
diff --git a/helix-ui/doc/quickstart-admin-resource.png 
b/helix-ui/doc/quickstart-admin-resource.png
new file mode 100644
index 0000000..d11bad4
Binary files /dev/null and b/helix-ui/doc/quickstart-admin-resource.png differ

http://git-wip-us.apache.org/repos/asf/helix/blob/6cbafef0/helix-ui/doc/quickstart-end-table.png
----------------------------------------------------------------------
diff --git a/helix-ui/doc/quickstart-end-table.png 
b/helix-ui/doc/quickstart-end-table.png
new file mode 100644
index 0000000..ce5472d
Binary files /dev/null and b/helix-ui/doc/quickstart-end-table.png differ

http://git-wip-us.apache.org/repos/asf/helix/blob/6cbafef0/helix-ui/doc/quickstart-end-visualizer.png
----------------------------------------------------------------------
diff --git a/helix-ui/doc/quickstart-end-visualizer.png 
b/helix-ui/doc/quickstart-end-visualizer.png
new file mode 100644
index 0000000..1424992
Binary files /dev/null and b/helix-ui/doc/quickstart-end-visualizer.png differ

http://git-wip-us.apache.org/repos/asf/helix/blob/6cbafef0/helix-ui/pom.xml
----------------------------------------------------------------------
diff --git a/helix-ui/pom.xml b/helix-ui/pom.xml
new file mode 100644
index 0000000..c200cb2
--- /dev/null
+++ b/helix-ui/pom.xml
@@ -0,0 +1,128 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+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.
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0"; 
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance";
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 
http://maven.apache.org/maven-v4_0_0.xsd";>
+    <parent>
+      <groupId>org.apache.helix</groupId>
+      <artifactId>helix</artifactId>
+      <version>0.7.2-SNAPSHOT</version>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+    <artifactId>helix-ui</artifactId>
+    <packaging>jar</packaging>
+    <name>Apache Helix :: UI</name>
+    <url>http://maven.apache.org</url>
+
+    <properties>
+        <dropwizard.version>0.8.0</dropwizard.version>
+        <osgi.import>
+          javax.management*,
+          org.apache.commons.math*;version="[2.1,3)",
+          org.apache.log4j*;version="[1.2,2)",
+          *
+        </osgi.import>
+        <osgi.ignore>
+          org.apache.helix.tools*
+        </osgi.ignore>
+    </properties>
+
+    <dependencies>
+        <dependency>
+            <groupId>io.dropwizard</groupId>
+            <artifactId>dropwizard-core</artifactId>
+            <version>${dropwizard.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>io.dropwizard</groupId>
+            <artifactId>dropwizard-assets</artifactId>
+            <version>${dropwizard.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>io.dropwizard</groupId>
+            <artifactId>dropwizard-views-freemarker</artifactId>
+            <version>${dropwizard.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.helix</groupId>
+            <artifactId>helix-core</artifactId>
+            <exclusions>
+                <exclusion>
+                    <groupId>com.google.guava</groupId>
+                    <artifactId>guava</artifactId>
+                </exclusion>
+                <exclusion>
+                    <groupId>log4j</groupId>
+                    <artifactId>log4j</artifactId>
+                </exclusion>
+            </exclusions>
+        </dependency>
+    </dependencies>
+
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-shade-plugin</artifactId>
+                <version>2.3</version>
+                <configuration>
+                    
<createDependencyReducedPom>true</createDependencyReducedPom>
+                    <filters>
+                        <filter>
+                            <artifact>*:*</artifact>
+                            <excludes>
+                                <exclude>META-INF/*.SF</exclude>
+                                <exclude>META-INF/*.DSA</exclude>
+                                <exclude>META-INF/*.RSA</exclude>
+                            </excludes>
+                        </filter>
+                    </filters>
+                </configuration>
+                <executions>
+                    <execution>
+                        <phase>package</phase>
+                        <goals>
+                            <goal>shade</goal>
+                        </goals>
+                        <configuration>
+                            <transformers>
+                                <transformer 
implementation="org.apache.maven.plugins.shade.resource.ServicesResourceTransformer"/>
+                                <transformer 
implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
+                                    
<mainClass>org.apache.helix.ui.HelixUIApplication</mainClass>
+                                </transformer>
+                            </transformers>
+                        </configuration>
+                    </execution>
+                </executions>
+            </plugin>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-jar-plugin</artifactId>
+                <version>2.4</version>
+                <configuration>
+                    <archive>
+                        <manifest>
+                            
<addDefaultImplementationEntries>true</addDefaultImplementationEntries>
+                        </manifest>
+                    </archive>
+                </configuration>
+            </plugin>
+        </plugins>
+    </build>
+</project>

http://git-wip-us.apache.org/repos/asf/helix/blob/6cbafef0/helix-ui/sample-admin-server.yml
----------------------------------------------------------------------
diff --git a/helix-ui/sample-admin-server.yml b/helix-ui/sample-admin-server.yml
new file mode 100644
index 0000000..cd4b924
--- /dev/null
+++ b/helix-ui/sample-admin-server.yml
@@ -0,0 +1,12 @@
+adminMode: true
+
+zkAddresses:
+  - "localhost:2181"
+
+server:
+    applicationConnectors:
+        - type: http
+          port: 60000
+    adminConnectors:
+        - type: http
+          port: 60001

http://git-wip-us.apache.org/repos/asf/helix/blob/6cbafef0/helix-ui/sample-server.yml
----------------------------------------------------------------------
diff --git a/helix-ui/sample-server.yml b/helix-ui/sample-server.yml
new file mode 100644
index 0000000..5ee5933
--- /dev/null
+++ b/helix-ui/sample-server.yml
@@ -0,0 +1,7 @@
+server:
+    applicationConnectors:
+        - type: http
+          port: 50000
+    adminConnectors:
+        - type: http
+          port: 50001

http://git-wip-us.apache.org/repos/asf/helix/blob/6cbafef0/helix-ui/src/main/java/org/apache/helix/ui/HelixUIApplication.java
----------------------------------------------------------------------
diff --git a/helix-ui/src/main/java/org/apache/helix/ui/HelixUIApplication.java 
b/helix-ui/src/main/java/org/apache/helix/ui/HelixUIApplication.java
new file mode 100644
index 0000000..aad605b
--- /dev/null
+++ b/helix-ui/src/main/java/org/apache/helix/ui/HelixUIApplication.java
@@ -0,0 +1,74 @@
+package org.apache.helix.ui;
+
+import com.google.common.collect.ImmutableMap;
+import io.dropwizard.Application;
+import io.dropwizard.assets.AssetsBundle;
+import io.dropwizard.setup.Bootstrap;
+import io.dropwizard.setup.Environment;
+import io.dropwizard.views.ViewBundle;
+import org.apache.helix.ui.health.ClusterConnectionHealthCheck;
+import org.apache.helix.ui.resource.AdminResource;
+import org.apache.helix.ui.resource.DashboardResource;
+import org.apache.helix.ui.resource.VisualizerResource;
+import org.apache.helix.ui.task.ClearClientCache;
+import org.apache.helix.ui.task.ClearDataCacheTask;
+import org.apache.helix.ui.util.ClientCache;
+import org.apache.helix.ui.util.DataCache;
+import org.apache.helix.ui.util.ZkAddressValidator;
+import org.eclipse.jetty.util.component.AbstractLifeCycle;
+import org.eclipse.jetty.util.component.LifeCycle;
+
+public class HelixUIApplication extends 
Application<HelixUIApplicationConfiguration> {
+    @Override
+    public String getName() {
+        return "helix-ui";
+    }
+
+    @Override
+    public void initialize(Bootstrap<HelixUIApplicationConfiguration> 
bootstrap) {
+        bootstrap.addBundle(new ViewBundle<HelixUIApplicationConfiguration>() {
+            @Override
+            public ImmutableMap<String, ImmutableMap<String, String>> 
getViewConfiguration(HelixUIApplicationConfiguration config) {
+                return config.getViewRendererConfiguration();
+            }
+        });
+        bootstrap.addBundle(new AssetsBundle("/assets/css", "/assets/css", 
null, "css"));
+        bootstrap.addBundle(new AssetsBundle("/assets/js", "/assets/js", null, 
"js"));
+        bootstrap.addBundle(new AssetsBundle("/assets/img", "/assets/img", 
null, "img"));
+        bootstrap.addBundle(new AssetsBundle("/assets/fonts", "/assets/fonts", 
null, "fonts"));
+    }
+
+    @Override
+    public void run(HelixUIApplicationConfiguration config, Environment 
environment) throws Exception {
+        final ZkAddressValidator zkAddressValidator = new 
ZkAddressValidator(config.getZkAddresses());
+        final ClientCache clientCache = new ClientCache(zkAddressValidator);
+
+        // Close all connections when application stops
+        environment.lifecycle().addLifeCycleListener(new 
AbstractLifeCycle.AbstractLifeCycleListener() {
+            @Override
+            public void lifeCycleStopping(LifeCycle event) {
+                clientCache.invalidateAll();
+            }
+        });
+
+        DataCache dataCache = new DataCache(clientCache);
+
+
+        DashboardResource dashboardResource
+                = new DashboardResource(clientCache, dataCache, 
config.isAdminMode());
+
+        environment.healthChecks().register("clusterConnection", new 
ClusterConnectionHealthCheck(clientCache));
+        environment.jersey().register(dashboardResource);
+        environment.jersey().register(new VisualizerResource(clientCache, 
dataCache));
+        environment.admin().addTask(new ClearDataCacheTask(dataCache));
+        environment.admin().addTask(new ClearClientCache(clientCache));
+
+        if (config.isAdminMode()) {
+            environment.jersey().register(new AdminResource(clientCache, 
dataCache));
+        }
+    }
+
+    public static void main(String[] args) throws Exception {
+        new HelixUIApplication().run(args);
+    }
+}

http://git-wip-us.apache.org/repos/asf/helix/blob/6cbafef0/helix-ui/src/main/java/org/apache/helix/ui/HelixUIApplicationConfiguration.java
----------------------------------------------------------------------
diff --git 
a/helix-ui/src/main/java/org/apache/helix/ui/HelixUIApplicationConfiguration.java
 
b/helix-ui/src/main/java/org/apache/helix/ui/HelixUIApplicationConfiguration.java
new file mode 100644
index 0000000..e2f433e
--- /dev/null
+++ 
b/helix-ui/src/main/java/org/apache/helix/ui/HelixUIApplicationConfiguration.java
@@ -0,0 +1,30 @@
+package org.apache.helix.ui;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.google.common.collect.ImmutableMap;
+import io.dropwizard.Configuration;
+
+import javax.validation.constraints.NotNull;
+import java.util.Set;
+
+public class HelixUIApplicationConfiguration extends Configuration {
+    @NotNull
+    private ImmutableMap<String, ImmutableMap<String, String>> 
viewRendererConfiguration = ImmutableMap.of();
+
+    private boolean adminMode = false;
+
+    private Set<String> zkAddresses;
+
+    @JsonProperty("viewRendererConfiguration")
+    public ImmutableMap<String, ImmutableMap<String, String>> 
getViewRendererConfiguration() {
+        return viewRendererConfiguration;
+    }
+
+    public boolean isAdminMode() {
+        return adminMode;
+    }
+
+    public Set<String> getZkAddresses() {
+        return zkAddresses;
+    }
+}

http://git-wip-us.apache.org/repos/asf/helix/blob/6cbafef0/helix-ui/src/main/java/org/apache/helix/ui/api/ClusterConnection.java
----------------------------------------------------------------------
diff --git 
a/helix-ui/src/main/java/org/apache/helix/ui/api/ClusterConnection.java 
b/helix-ui/src/main/java/org/apache/helix/ui/api/ClusterConnection.java
new file mode 100644
index 0000000..94af4cf
--- /dev/null
+++ b/helix-ui/src/main/java/org/apache/helix/ui/api/ClusterConnection.java
@@ -0,0 +1,22 @@
+package org.apache.helix.ui.api;
+
+import org.apache.helix.manager.zk.ZkClient;
+import org.apache.helix.tools.ClusterSetup;
+
+public class ClusterConnection {
+    private final ZkClient zkClient;
+    private final ClusterSetup clusterSetup;
+
+    public ClusterConnection(ZkClient zkClient) {
+        this.zkClient = zkClient;
+        this.clusterSetup = new ClusterSetup(zkClient);
+    }
+
+    public ZkClient getZkClient() {
+        return zkClient;
+    }
+
+    public ClusterSetup getClusterSetup() {
+        return clusterSetup;
+    }
+}

http://git-wip-us.apache.org/repos/asf/helix/blob/6cbafef0/helix-ui/src/main/java/org/apache/helix/ui/api/ClusterSpec.java
----------------------------------------------------------------------
diff --git a/helix-ui/src/main/java/org/apache/helix/ui/api/ClusterSpec.java 
b/helix-ui/src/main/java/org/apache/helix/ui/api/ClusterSpec.java
new file mode 100644
index 0000000..1e09615
--- /dev/null
+++ b/helix-ui/src/main/java/org/apache/helix/ui/api/ClusterSpec.java
@@ -0,0 +1,33 @@
+package org.apache.helix.ui.api;
+
+public class ClusterSpec {
+    private final String zkAddress;
+    private final String clusterName;
+
+    public ClusterSpec(String zkAddress, String clusterName) {
+        this.zkAddress = zkAddress;
+        this.clusterName = clusterName;
+    }
+
+    public String getZkAddress() {
+        return zkAddress;
+    }
+
+    public String getClusterName() {
+        return clusterName;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (!(o instanceof ClusterSpec)) {
+            return false;
+        }
+        ClusterSpec c = (ClusterSpec) o;
+        return c.zkAddress.equals(zkAddress) && 
c.clusterName.equals(clusterName);
+    }
+
+    @Override
+    public int hashCode() {
+        return zkAddress.hashCode() + 13 * clusterName.hashCode();
+    }
+}

http://git-wip-us.apache.org/repos/asf/helix/blob/6cbafef0/helix-ui/src/main/java/org/apache/helix/ui/api/ConfigTableRow.java
----------------------------------------------------------------------
diff --git a/helix-ui/src/main/java/org/apache/helix/ui/api/ConfigTableRow.java 
b/helix-ui/src/main/java/org/apache/helix/ui/api/ConfigTableRow.java
new file mode 100644
index 0000000..bd423f5
--- /dev/null
+++ b/helix-ui/src/main/java/org/apache/helix/ui/api/ConfigTableRow.java
@@ -0,0 +1,49 @@
+package org.apache.helix.ui.api;
+
+public class ConfigTableRow implements Comparable<ConfigTableRow> {
+    private final String scope;
+    private final String entity;
+    private final String name;
+    private final String value;
+
+    public ConfigTableRow(String scope,
+                          String entity,
+                          String name,
+                          String value) throws Exception {
+        this.scope = scope;
+        this.entity = entity;
+        this.name = name;
+        this.value = value;
+    }
+
+    public String getScope() {
+        return scope;
+    }
+
+    public String getEntity() {
+        return entity;
+    }
+
+    public String getName() {
+        return name;
+    }
+
+    public String getValue() {
+        return value;
+    }
+
+    @Override
+    public int compareTo(ConfigTableRow o) {
+        int nameResult = name.compareTo(o.getName());
+        if (nameResult != 0) {
+            return nameResult;
+        }
+
+        int valueResult = value.compareTo(o.getValue());
+        if (valueResult != 0) {
+            return valueResult;
+        }
+
+        return 0;
+    }
+}

http://git-wip-us.apache.org/repos/asf/helix/blob/6cbafef0/helix-ui/src/main/java/org/apache/helix/ui/api/D3ResourceCoCentricCircle.java
----------------------------------------------------------------------
diff --git 
a/helix-ui/src/main/java/org/apache/helix/ui/api/D3ResourceCoCentricCircle.java 
b/helix-ui/src/main/java/org/apache/helix/ui/api/D3ResourceCoCentricCircle.java
new file mode 100644
index 0000000..b9548bf
--- /dev/null
+++ 
b/helix-ui/src/main/java/org/apache/helix/ui/api/D3ResourceCoCentricCircle.java
@@ -0,0 +1,116 @@
+package org.apache.helix.ui.api;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+
+import java.util.*;
+
+@JsonInclude(JsonInclude.Include.NON_NULL)
+public class D3ResourceCoCentricCircle {
+
+    public enum CircleType {
+        CLUSTER,
+        INSTANCE,
+        PARTITION
+    }
+
+    private final String name;
+    private final String parentName;
+    private final String state;
+    private final int size;
+    private final CircleType circleType;
+    private final Set<D3ResourceCoCentricCircle> children;
+
+    public D3ResourceCoCentricCircle(String name,
+                                     String parentName,
+                                     String state,
+                                     int size,
+                                     CircleType circleType,
+                                     Set<D3ResourceCoCentricCircle> children) {
+        this.name = name;
+        this.parentName = parentName;
+        this.state = state;
+        this.size = size;
+        this.circleType = circleType;
+        this.children = children;
+    }
+
+    public String getName() {
+        return name;
+    }
+
+    public String getParentName() {
+        return parentName;
+    }
+
+    public String getState() {
+        return state;
+    }
+
+    public int getSize() {
+        return size;
+    }
+
+    public CircleType getCircleType() {
+        return circleType;
+    }
+
+    public Set<D3ResourceCoCentricCircle> getChildren() {
+        return children;
+    }
+
+    public static D3ResourceCoCentricCircle 
fromResourceStateSpec(ResourceStateSpec resourceStateSpec) {
+        Map<String, Set<D3ResourceCoCentricCircle>> partitionByInstance
+                = new HashMap<String, Set<D3ResourceCoCentricCircle>>();
+
+        // Group by instance (first level)
+        for (ResourceStateTableRow row : 
resourceStateSpec.getResourceStateTable()) {
+            Set<D3ResourceCoCentricCircle> partitionCircles = 
partitionByInstance.get(row.getInstanceName());
+            if (partitionCircles == null) {
+                partitionCircles = new HashSet<D3ResourceCoCentricCircle>();
+                partitionByInstance.put(row.getInstanceName(), 
partitionCircles);
+            }
+            partitionCircles.add(new D3ResourceCoCentricCircle(
+                    row.getPartitionName(),
+                    row.getInstanceName(),
+                    row.getExternal(),
+                    10,
+                    CircleType.PARTITION,
+                    null));
+        }
+
+        // Group into cluster
+        Set<D3ResourceCoCentricCircle> instanceCircles = new 
HashSet<D3ResourceCoCentricCircle>();
+        for (Map.Entry<String, Set<D3ResourceCoCentricCircle>> entry : 
partitionByInstance.entrySet()) {
+
+            InstanceSpec instanceSpec = 
resourceStateSpec.getInstanceSpecs().get(entry.getKey());
+            if (instanceSpec == null) {
+                throw new IllegalStateException("No instance spec for " + 
entry.getKey());
+            }
+
+            String state;
+            if (!instanceSpec.isLive()) {
+                state = "DEAD";
+            } else if (!instanceSpec.isEnabled()) {
+                state = "DISABLED";
+            } else {
+                state = "LIVE";
+            }
+
+            instanceCircles.add(new D3ResourceCoCentricCircle(
+                    entry.getKey(),
+                    resourceStateSpec.getResource(),
+                    state,
+                    100,
+                    CircleType.INSTANCE,
+                    entry.getValue()));
+        }
+
+        return new D3ResourceCoCentricCircle(
+                resourceStateSpec.getIdealState().getResourceName(),
+                null,
+                "",
+                900,
+                CircleType.CLUSTER,
+                instanceCircles);
+    }
+}

http://git-wip-us.apache.org/repos/asf/helix/blob/6cbafef0/helix-ui/src/main/java/org/apache/helix/ui/api/IdealStateSpec.java
----------------------------------------------------------------------
diff --git a/helix-ui/src/main/java/org/apache/helix/ui/api/IdealStateSpec.java 
b/helix-ui/src/main/java/org/apache/helix/ui/api/IdealStateSpec.java
new file mode 100644
index 0000000..f37b939
--- /dev/null
+++ b/helix-ui/src/main/java/org/apache/helix/ui/api/IdealStateSpec.java
@@ -0,0 +1,94 @@
+package org.apache.helix.ui.api;
+
+import org.apache.helix.model.IdealState;
+
+import java.util.List;
+
+public class IdealStateSpec {
+    private final int numPartitions;
+    private final String replicas;
+    private final String instanceGroupTag;
+    private final int maxPartitionsPerInstance;
+    private final String rebalanceMode;
+    private final String rebalancerClassName;
+    private final String stateModel;
+    private final int bucketSize;
+    private final int rebalanceTimerPeriod;
+    private final boolean batchMessageMode;
+
+    public IdealStateSpec(int numPartitions,
+                          String replicas,
+                          String instanceGroupTag,
+                          int maxPartitionsPerInstance,
+                          String rebalanceMode,
+                          String rebalancerClassName,
+                          String stateModel,
+                          int bucketSize,
+                          int rebalanceTimerPeriod,
+                          boolean batchMessageMode) {
+        this.numPartitions = numPartitions;
+        this.replicas = replicas;
+        this.instanceGroupTag = instanceGroupTag;
+        this.maxPartitionsPerInstance = maxPartitionsPerInstance;
+        this.rebalanceMode = rebalanceMode;
+        this.rebalancerClassName = rebalancerClassName;
+        this.stateModel = stateModel;
+        this.bucketSize = bucketSize;
+        this.rebalanceTimerPeriod = rebalanceTimerPeriod;
+        this.batchMessageMode = batchMessageMode;
+    }
+
+    public int getNumPartitions() {
+        return numPartitions;
+    }
+
+    public String getReplicas() {
+        return replicas;
+    }
+
+    public String getInstanceGroupTag() {
+        return instanceGroupTag;
+    }
+
+    public int getMaxPartitionsPerInstance() {
+        return maxPartitionsPerInstance;
+    }
+
+    public String getRebalanceMode() {
+        return rebalanceMode;
+    }
+
+    public String getRebalancerClassName() {
+        return rebalancerClassName;
+    }
+
+    public String getStateModel() {
+        return stateModel;
+    }
+
+    public int getBucketSize() {
+        return bucketSize;
+    }
+
+    public int getRebalanceTimerPeriod() {
+        return rebalanceTimerPeriod;
+    }
+
+    public boolean isBatchMessageMode() {
+        return batchMessageMode;
+    }
+
+    public static IdealStateSpec fromIdealState(IdealState idealState) {
+        return new IdealStateSpec(
+                idealState.getNumPartitions(),
+                idealState.getReplicas(),
+                idealState.getInstanceGroupTag(),
+                idealState.getMaxPartitionsPerInstance(),
+                idealState.getRebalanceMode().toString(),
+                idealState.getRebalancerClassName(),
+                idealState.getStateModelDefRef(),
+                idealState.getBucketSize(),
+                idealState.getRebalanceTimerPeriod(),
+                idealState.getBatchMessageMode());
+    }
+}

http://git-wip-us.apache.org/repos/asf/helix/blob/6cbafef0/helix-ui/src/main/java/org/apache/helix/ui/api/InstanceSpec.java
----------------------------------------------------------------------
diff --git a/helix-ui/src/main/java/org/apache/helix/ui/api/InstanceSpec.java 
b/helix-ui/src/main/java/org/apache/helix/ui/api/InstanceSpec.java
new file mode 100644
index 0000000..e87932c
--- /dev/null
+++ b/helix-ui/src/main/java/org/apache/helix/ui/api/InstanceSpec.java
@@ -0,0 +1,32 @@
+package org.apache.helix.ui.api;
+
+public class InstanceSpec implements Comparable<InstanceSpec> {
+    private final String instanceName;
+    private final boolean enabled;
+    private final boolean live;
+
+    public InstanceSpec(String instanceName,
+                        boolean enabled,
+                        boolean live) {
+        this.instanceName = instanceName;
+        this.enabled = enabled;
+        this.live = live;
+    }
+
+    public String getInstanceName() {
+        return instanceName;
+    }
+
+    public boolean isEnabled() {
+        return enabled;
+    }
+
+    public boolean isLive() {
+        return live;
+    }
+
+    @Override
+    public int compareTo(InstanceSpec o) {
+        return instanceName.compareTo(o.instanceName);
+    }
+}

http://git-wip-us.apache.org/repos/asf/helix/blob/6cbafef0/helix-ui/src/main/java/org/apache/helix/ui/api/ResourceSpec.java
----------------------------------------------------------------------
diff --git a/helix-ui/src/main/java/org/apache/helix/ui/api/ResourceSpec.java 
b/helix-ui/src/main/java/org/apache/helix/ui/api/ResourceSpec.java
new file mode 100644
index 0000000..284fbaa
--- /dev/null
+++ b/helix-ui/src/main/java/org/apache/helix/ui/api/ResourceSpec.java
@@ -0,0 +1,31 @@
+package org.apache.helix.ui.api;
+
+public class ResourceSpec extends ClusterSpec {
+    private final String resourceName;
+
+    public ResourceSpec(String zkAddress, String clusterName, String 
resourceName) {
+        super(zkAddress, clusterName);
+        this.resourceName = resourceName;
+    }
+
+    public String getResourceName() {
+        return resourceName;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (!(o instanceof ResourceSpec)) {
+            return false;
+        }
+        ResourceSpec c = (ResourceSpec) o;
+        return getZkAddress().equals(c.getZkAddress())
+                && getClusterName().equals(c.getClusterName())
+                && resourceName.equals(c.getResourceName());
+    }
+
+    @Override
+    public int hashCode() {
+        return super.hashCode() + 27 * resourceName.hashCode();
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/helix/blob/6cbafef0/helix-ui/src/main/java/org/apache/helix/ui/api/ResourceStateSpec.java
----------------------------------------------------------------------
diff --git 
a/helix-ui/src/main/java/org/apache/helix/ui/api/ResourceStateSpec.java 
b/helix-ui/src/main/java/org/apache/helix/ui/api/ResourceStateSpec.java
new file mode 100644
index 0000000..08a8f73
--- /dev/null
+++ b/helix-ui/src/main/java/org/apache/helix/ui/api/ResourceStateSpec.java
@@ -0,0 +1,68 @@
+package org.apache.helix.ui.api;
+
+import org.apache.helix.model.ExternalView;
+import org.apache.helix.model.IdealState;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+public class ResourceStateSpec {
+    private final String resource;
+    private final IdealState idealState;
+    private final ExternalView externalView;
+    private final Map<String, InstanceSpec> instanceSpecs;
+
+    public ResourceStateSpec(String resource,
+                             IdealState idealState,
+                             ExternalView externalView,
+                             Map<String, InstanceSpec> instanceSpecs) {
+        this.resource = resource;
+        this.idealState = idealState;
+        this.externalView = externalView;
+        this.instanceSpecs = instanceSpecs;
+    }
+
+    public String getResource() {
+        return resource;
+    }
+
+    public IdealState getIdealState() {
+        return idealState;
+    }
+
+    public ExternalView getExternalView() {
+        return externalView;
+    }
+
+    public Map<String, InstanceSpec> getInstanceSpecs() {
+        return instanceSpecs;
+    }
+
+    public List<ResourceStateTableRow> getResourceStateTable() {
+        List<ResourceStateTableRow> resourceStateTable = new 
ArrayList<ResourceStateTableRow>();
+        Set<String> partitionNames = idealState.getPartitionSet();
+        for (String partitionName : partitionNames) {
+            Map<String, String> stateMap = 
idealState.getInstanceStateMap(partitionName);
+            if (stateMap != null) {
+                for (Map.Entry<String, String> entry : stateMap.entrySet()) {
+                    String instanceName = entry.getKey();
+                    String ideal = entry.getValue();
+
+                    String external = null;
+                    if (externalView != null) {
+                        Map<String, String> externalStateMap = 
externalView.getStateMap(partitionName);
+                        if (externalStateMap != null) {
+                            external = externalStateMap.get(instanceName);
+                        }
+                    }
+
+                    resourceStateTable.add(new ResourceStateTableRow(resource, 
partitionName, instanceName, ideal, external));
+                }
+            }
+        }
+
+        return resourceStateTable;
+    }
+}

http://git-wip-us.apache.org/repos/asf/helix/blob/6cbafef0/helix-ui/src/main/java/org/apache/helix/ui/api/ResourceStateTableRow.java
----------------------------------------------------------------------
diff --git 
a/helix-ui/src/main/java/org/apache/helix/ui/api/ResourceStateTableRow.java 
b/helix-ui/src/main/java/org/apache/helix/ui/api/ResourceStateTableRow.java
new file mode 100644
index 0000000..bf0ce08
--- /dev/null
+++ b/helix-ui/src/main/java/org/apache/helix/ui/api/ResourceStateTableRow.java
@@ -0,0 +1,69 @@
+package org.apache.helix.ui.api;
+
+public class ResourceStateTableRow implements 
Comparable<ResourceStateTableRow> {
+    private static final String NA = "N/A";
+
+    private final String resourceName;
+    private final String partitionName;
+    private final String instanceName;
+    private final String ideal;
+    private final String external;
+
+    public ResourceStateTableRow(String resourceName,
+                                 String partitionName,
+                                 String instanceName,
+                                 String ideal,
+                                 String external) {
+        this.resourceName = resourceName;
+        this.partitionName = partitionName;
+        this.instanceName = instanceName;
+        this.ideal = ideal;
+        this.external = external == null ? NA : external;
+    }
+
+    public String getResourceName() {
+        return resourceName;
+    }
+
+    public String getPartitionName() {
+        return partitionName;
+    }
+
+    public String getInstanceName() {
+        return instanceName;
+    }
+
+    public String getIdeal() {
+        return ideal;
+    }
+
+    public String getExternal() {
+        return external;
+    }
+
+    @Override
+    public int compareTo(ResourceStateTableRow r) {
+        int partitionResult = partitionName.compareTo(r.getPartitionName());
+        if (partitionResult != 0) {
+            return partitionResult;
+        }
+
+        int instanceResult = instanceName.compareTo(r.getInstanceName());
+        if (instanceResult != 0) {
+            return instanceResult;
+        }
+
+        int idealResult = ideal.compareTo(r.getIdeal());
+        if (idealResult != 0) {
+            return idealResult;
+        }
+
+        int externalResult = external.compareTo(r.getExternal());
+        if (externalResult != 0) {
+            return externalResult;
+        }
+
+        return 0;
+
+    }
+}

http://git-wip-us.apache.org/repos/asf/helix/blob/6cbafef0/helix-ui/src/main/java/org/apache/helix/ui/health/ClusterConnectionHealthCheck.java
----------------------------------------------------------------------
diff --git 
a/helix-ui/src/main/java/org/apache/helix/ui/health/ClusterConnectionHealthCheck.java
 
b/helix-ui/src/main/java/org/apache/helix/ui/health/ClusterConnectionHealthCheck.java
new file mode 100644
index 0000000..4c6df61
--- /dev/null
+++ 
b/helix-ui/src/main/java/org/apache/helix/ui/health/ClusterConnectionHealthCheck.java
@@ -0,0 +1,24 @@
+package org.apache.helix.ui.health;
+
+import com.codahale.metrics.health.HealthCheck;
+import org.apache.helix.ui.util.ClientCache;
+
+import java.util.Set;
+
+public class ClusterConnectionHealthCheck extends HealthCheck {
+
+    private final ClientCache clientCache;
+
+    public ClusterConnectionHealthCheck(ClientCache clientCache) {
+        this.clientCache = clientCache;
+    }
+
+    @Override
+    protected Result check() throws Exception {
+        Set<String> deadConnections = clientCache.getDeadConnections();
+        if (!deadConnections.isEmpty()) {
+            return Result.unhealthy("Dead connections to " + deadConnections);
+        }
+        return Result.healthy();
+    }
+}

http://git-wip-us.apache.org/repos/asf/helix/blob/6cbafef0/helix-ui/src/main/java/org/apache/helix/ui/resource/AdminResource.java
----------------------------------------------------------------------
diff --git 
a/helix-ui/src/main/java/org/apache/helix/ui/resource/AdminResource.java 
b/helix-ui/src/main/java/org/apache/helix/ui/resource/AdminResource.java
new file mode 100644
index 0000000..e6318f2
--- /dev/null
+++ b/helix-ui/src/main/java/org/apache/helix/ui/resource/AdminResource.java
@@ -0,0 +1,181 @@
+package org.apache.helix.ui.resource;
+
+import org.apache.helix.manager.zk.ZKUtil;
+import org.apache.helix.model.IdealState;
+import org.apache.helix.model.InstanceConfig;
+import org.apache.helix.tools.ClusterSetup;
+import org.apache.helix.ui.api.ClusterConnection;
+import org.apache.helix.ui.util.ClientCache;
+import org.apache.helix.ui.util.DataCache;
+
+import javax.ws.rs.*;
+import javax.ws.rs.core.Response;
+import java.util.List;
+
+@Path("/admin")
+public class AdminResource {
+    private final ClientCache clientCache;
+    private final DataCache dataCache;
+
+    public AdminResource(ClientCache clientCache, DataCache dataCache) {
+        this.clientCache = clientCache;
+        this.dataCache = dataCache;
+    }
+
+    @Path("/{zkAddress}/{clusterName}")
+    @POST
+    public Response addCluster(@PathParam("zkAddress") String zkAddress,
+                               @PathParam("clusterName") String clusterName) 
throws Exception {
+        dataCache.invalidate();
+
+        ClusterSetup clusterSetup = 
clientCache.get(zkAddress).getClusterSetup();
+
+        if 
(clusterSetup.getClusterManagementTool().getClusters().contains(clusterName)) {
+            return Response.status(Response.Status.CONFLICT).build();
+        }
+
+        clusterSetup.addCluster(clusterName, false);
+
+        dataCache.invalidate();
+
+        return Response.ok().build();
+    }
+
+    @Path("/{zkAddress}/{clusterName}")
+    @DELETE
+    public Response dropCluster(@PathParam("zkAddress") String zkAddress,
+                                @PathParam("clusterName") String clusterName) 
throws Exception {
+        dataCache.invalidate();
+
+        ClusterSetup clusterSetup = 
clientCache.get(zkAddress).getClusterSetup();
+
+        if 
(!clusterSetup.getClusterManagementTool().getClusters().contains(clusterName)) {
+            throw new NotFoundException();
+        }
+
+        clusterSetup.getClusterManagementTool().dropCluster(clusterName);
+
+        return Response.noContent().build();
+    }
+
+    @Path("/{zkAddress}/{clusterName}/instances/{instanceName}")
+    @POST
+    public Response addInstance(@PathParam("zkAddress") String zkAddress,
+                                @PathParam("clusterName") String clusterName,
+                                @PathParam("instanceName") String instanceName,
+                                @QueryParam("disable") boolean disable,
+                                @QueryParam("failIfNoInstance") boolean 
failIfNoInstance) throws Exception {
+        dataCache.invalidate();
+
+        ClusterSetup clusterSetup = 
clientCache.get(zkAddress).getClusterSetup();
+
+        List<String> instances = 
clusterSetup.getClusterManagementTool().getInstancesInCluster(clusterName);
+
+        Response response;
+
+        if (instances.contains(instanceName)) {
+            response = Response.notModified().build();
+        } else if (failIfNoInstance) {
+            return Response.status(Response.Status.BAD_REQUEST).build();
+        } else {
+            clusterSetup.addInstanceToCluster(clusterName, instanceName);
+            response = Response.ok().build();
+        }
+
+        clusterSetup.getClusterManagementTool().enableInstance(clusterName, 
instanceName, !disable);
+
+        return response;
+    }
+
+    @Path("/{zkAddress}/{clusterName}/instances/{instanceName}")
+    @DELETE
+    public Response dropInstance(@PathParam("zkAddress") String zkAddress,
+                                 @PathParam("clusterName") String clusterName,
+                                 @PathParam("instanceName") String 
instanceName) throws Exception {
+        dataCache.invalidate();
+
+        ClusterSetup clusterSetup = 
clientCache.get(zkAddress).getClusterSetup();
+
+        InstanceConfig instanceConfig
+                = 
clusterSetup.getClusterManagementTool().getInstanceConfig(clusterName, 
instanceName);
+
+        if (instanceConfig == null) {
+            throw new NotFoundException();
+        } else if (instanceConfig.getInstanceEnabled()) {
+            return Response.status(Response.Status.BAD_REQUEST)
+                    .header("X-Error-Message", "Cannot drop instance that is 
enabled")
+                    .build();
+        }
+
+        clusterSetup.dropInstanceFromCluster(clusterName, instanceName);
+
+        return Response.noContent().build();
+    }
+
+    
@Path("/{zkAddress}/{clusterName}/resources/{resourceName}/{partitions}/{replicas}")
+    @POST
+    public Response addResource(@PathParam("zkAddress") String zkAddress,
+                                @PathParam("clusterName") String clusterName,
+                                @PathParam("resourceName") String resourceName,
+                                @PathParam("partitions") int partitions,
+                                @PathParam("replicas") String replicas,
+                                @QueryParam("rebalance") boolean rebalance,
+                                @QueryParam("stateModel") String stateModel,
+                                @QueryParam("rebalanceMode") String 
rebalanceMode) throws Exception {
+        dataCache.invalidate();
+
+        ClusterConnection conn = clientCache.get(zkAddress);
+        ClusterSetup clusterSetup = conn.getClusterSetup();
+
+        if (!ZKUtil.isClusterSetup(clusterName, conn.getZkClient())) {
+            return Response.status(Response.Status.BAD_REQUEST).build();
+        }
+
+        IdealState existingIdealState
+                = 
clusterSetup.getClusterManagementTool().getResourceIdealState(clusterName, 
resourceName);
+
+        IdealState idealState = new IdealState(resourceName);
+        idealState.setNumPartitions(partitions);
+        idealState.setReplicas(replicas);
+        idealState.setStateModelDefRef(stateModel == null
+                ? "OnlineOffline" : stateModel);
+        idealState.setRebalanceMode(rebalanceMode == null
+                ? IdealState.RebalanceMode.FULL_AUTO : 
IdealState.RebalanceMode.valueOf(rebalanceMode));
+
+        Response response;
+
+        if (existingIdealState == null) {
+            clusterSetup.getClusterManagementTool().addResource(clusterName, 
resourceName, idealState);
+            response = Response.ok().build();
+        } else if (!existingIdealState.equals(idealState)) {
+            return Response.status(Response.Status.CONFLICT).build();
+        } else {
+            response = Response.notModified().build();
+        }
+
+        if (rebalance) { // TODO this will break if replicas is not integer 
(e.g. N)
+            clusterSetup.rebalanceResource(clusterName, resourceName, 
Integer.valueOf(replicas));
+        }
+
+        return response;
+    }
+
+    @Path("/{zkAddress}/{clusterName}/resources/{resourceName}")
+    @DELETE
+    public Response dropResource(@PathParam("zkAddress") String zkAddress,
+                                @PathParam("clusterName") String clusterName,
+                                @PathParam("resourceName") String 
resourceName) throws Exception {
+        dataCache.invalidate();
+
+        ClusterSetup clusterSetup = 
clientCache.get(zkAddress).getClusterSetup();
+
+        List<String> resources = 
clusterSetup.getClusterManagementTool().getResourcesInCluster(clusterName);
+        if (!resources.contains(resourceName)) {
+            throw new NotFoundException();
+        }
+
+        clusterSetup.dropResourceFromCluster(clusterName, resourceName);
+
+        return Response.noContent().build();
+    }
+}

http://git-wip-us.apache.org/repos/asf/helix/blob/6cbafef0/helix-ui/src/main/java/org/apache/helix/ui/resource/DashboardResource.java
----------------------------------------------------------------------
diff --git 
a/helix-ui/src/main/java/org/apache/helix/ui/resource/DashboardResource.java 
b/helix-ui/src/main/java/org/apache/helix/ui/resource/DashboardResource.java
new file mode 100644
index 0000000..cb154e5
--- /dev/null
+++ b/helix-ui/src/main/java/org/apache/helix/ui/resource/DashboardResource.java
@@ -0,0 +1,164 @@
+package org.apache.helix.ui.resource;
+
+import com.google.common.collect.ImmutableList;
+import org.apache.helix.manager.zk.ZKUtil;
+import org.apache.helix.model.ExternalView;
+import org.apache.helix.model.IdealState;
+import org.apache.helix.ui.api.*;
+import org.apache.helix.ui.util.ClientCache;
+import org.apache.helix.ui.util.DataCache;
+import org.apache.helix.ui.view.ClusterView;
+import org.apache.helix.ui.view.LandingView;
+import org.apache.helix.ui.view.ResourceView;
+
+import javax.ws.rs.*;
+import javax.ws.rs.core.MediaType;
+import java.util.*;
+
+@Path("/dashboard")
+@Produces(MediaType.TEXT_HTML)
+public class DashboardResource {
+    private static final List<String> REBALANCE_MODES = ImmutableList.of(
+            IdealState.RebalanceMode.SEMI_AUTO.toString(),
+            IdealState.RebalanceMode.FULL_AUTO.toString(),
+            IdealState.RebalanceMode.CUSTOMIZED.toString(),
+            IdealState.RebalanceMode.USER_DEFINED.toString(),
+            IdealState.RebalanceMode.TASK.toString());
+
+    private final boolean adminMode;
+    private final ClientCache clientCache;
+    private final DataCache dataCache;
+
+    public DashboardResource(ClientCache clientCache,
+                             DataCache dataCache,
+                             boolean adminMode) {
+        this.clientCache = clientCache;
+        this.dataCache = dataCache;
+        this.adminMode = adminMode;
+    }
+
+    @GET
+    public LandingView getLandingView() {
+        return new LandingView();
+    }
+
+    @GET
+    @Path("/{zkAddress}")
+    public ClusterView getClusterView(@PathParam("zkAddress") String 
zkAddress) throws Exception {
+        clientCache.get(zkAddress); // n.b. will validate
+        return getClusterView(zkAddress, null);
+    }
+
+    @GET
+    @Path("/{zkAddress}/{cluster}")
+    public ClusterView getClusterView(
+            @PathParam("zkAddress") String zkAddress,
+            @PathParam("cluster") String cluster) throws Exception {
+        ClusterConnection clusterConnection = clientCache.get(zkAddress);
+
+        // All clusters
+        List<String> clusters = dataCache.getClusterCache().get(zkAddress);
+
+        // The active cluster
+        String activeCluster = cluster == null ? clusters.get(0) : cluster;
+        ClusterSpec clusterSpec = new ClusterSpec(zkAddress, activeCluster);
+
+        // Check it
+        if (!ZKUtil.isClusterSetup(activeCluster, 
clusterConnection.getZkClient())) {
+            return new ClusterView(adminMode, zkAddress, clusters, false, 
activeCluster, null, null, null, null, null);
+        }
+
+        // Resources in the active cluster
+        List<String> activeClusterResources = 
dataCache.getResourceCache().get(clusterSpec);
+
+        // Instances in active cluster
+        List<InstanceSpec> instanceSpecs = 
dataCache.getInstanceCache().get(clusterSpec);
+
+        // State models in active cluster
+        List<String> stateModels
+                = 
clusterConnection.getClusterSetup().getClusterManagementTool().getStateModelDefs(activeCluster);
+
+        // Config table
+        List<ConfigTableRow> configTable = 
dataCache.getConfigCache().get(clusterSpec);
+
+        return new ClusterView(
+                adminMode,
+                zkAddress,
+                clusters,
+                true,
+                activeCluster,
+                activeClusterResources,
+                instanceSpecs,
+                configTable,
+                stateModels,
+                REBALANCE_MODES);
+    }
+
+    @GET
+    @Path("/{zkAddress}/{cluster}/{resource}")
+    public ResourceView getResourceView(
+            @PathParam("zkAddress") String zkAddress,
+            @PathParam("cluster") String cluster,
+            @PathParam("resource") String resource) throws Exception {
+        ClusterConnection clusterConnection = clientCache.get(zkAddress);
+
+        // All clusters
+        List<String> clusters = dataCache.getClusterCache().get(zkAddress);
+
+        // The active cluster
+        String activeCluster = cluster == null ? clusters.get(0) : cluster;
+        ClusterSpec clusterSpec = new ClusterSpec(zkAddress, activeCluster);
+
+        // Check it
+        if (!ZKUtil.isClusterSetup(activeCluster, 
clusterConnection.getZkClient())) {
+            return new ResourceView(
+                    adminMode, zkAddress, clusters, false, activeCluster, 
null, null, null, null, null, null, null);
+        }
+
+        // Resources in the active cluster
+        List<String> activeClusterResources = 
dataCache.getResourceCache().get(clusterSpec);
+        if (!activeClusterResources.contains(resource)) {
+            throw new NotFoundException("No resource " + resource + " in " + 
activeCluster);
+        }
+
+        // Instances in active cluster
+        List<InstanceSpec> instanceSpecs = 
dataCache.getInstanceCache().get(clusterSpec);
+        Map<String, InstanceSpec> instanceSpecMap = new HashMap<String, 
InstanceSpec>(instanceSpecs.size());
+        for (InstanceSpec instanceSpec : instanceSpecs) {
+            instanceSpecMap.put(instanceSpec.getInstanceName(), instanceSpec);
+        }
+
+        // Resource state
+        IdealState idealState
+                = 
clusterConnection.getClusterSetup().getClusterManagementTool().getResourceIdealState(cluster,
 resource);
+        ExternalView externalView
+                = 
clusterConnection.getClusterSetup().getClusterManagementTool().getResourceExternalView(cluster,
 resource);
+        ResourceStateSpec resourceStateSpec
+                = new ResourceStateSpec(resource, idealState, externalView, 
instanceSpecMap);
+        List<ResourceStateTableRow> resourceStateTable
+                = resourceStateSpec.getResourceStateTable();
+
+        // Resource config
+        List<ConfigTableRow> configTable = 
dataCache.getResourceConfigCache().get(new ResourceSpec(zkAddress, 
activeCluster, resource));
+
+        // Resource instances
+        Set<String> resourceInstances = new HashSet<String>();
+        for (ResourceStateTableRow row : resourceStateTable) {
+            resourceInstances.add(row.getInstanceName());
+        }
+
+        return new ResourceView(
+                adminMode,
+                zkAddress,
+                clusters,
+                true,
+                activeCluster,
+                activeClusterResources,
+                resource,
+                resourceStateTable,
+                resourceInstances,
+                configTable,
+                IdealStateSpec.fromIdealState(idealState),
+                instanceSpecs);
+    }
+}

http://git-wip-us.apache.org/repos/asf/helix/blob/6cbafef0/helix-ui/src/main/java/org/apache/helix/ui/resource/VisualizerResource.java
----------------------------------------------------------------------
diff --git 
a/helix-ui/src/main/java/org/apache/helix/ui/resource/VisualizerResource.java 
b/helix-ui/src/main/java/org/apache/helix/ui/resource/VisualizerResource.java
new file mode 100644
index 0000000..289d84e
--- /dev/null
+++ 
b/helix-ui/src/main/java/org/apache/helix/ui/resource/VisualizerResource.java
@@ -0,0 +1,53 @@
+package org.apache.helix.ui.resource;
+
+import org.apache.helix.model.ExternalView;
+import org.apache.helix.model.IdealState;
+import org.apache.helix.tools.ClusterSetup;
+import org.apache.helix.ui.api.*;
+import org.apache.helix.ui.util.ClientCache;
+import org.apache.helix.ui.util.DataCache;
+
+import javax.ws.rs.*;
+import javax.ws.rs.core.MediaType;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+@Path("/visualizer")
+public class VisualizerResource {
+    private final ClientCache clientCache;
+    private final DataCache dataCache;
+
+    public VisualizerResource(ClientCache clientCache, DataCache dataCache) {
+        this.clientCache = clientCache;
+        this.dataCache = dataCache;
+    }
+
+    @GET
+    @Path("/{zkAddress}/{clusterName}/{resourceName}")
+    @Produces(MediaType.APPLICATION_JSON)
+    public D3ResourceCoCentricCircle getD3HelixResource(
+            @PathParam("zkAddress") String zkAddress,
+            @PathParam("clusterName") String clusterName,
+            @PathParam("resourceName") String resourceName) throws Exception {
+        ClusterSetup clusterSetup = 
clientCache.get(zkAddress).getClusterSetup();
+
+        IdealState idealState
+                = 
clusterSetup.getClusterManagementTool().getResourceIdealState(clusterName, 
resourceName);
+        ExternalView externalView
+                = 
clusterSetup.getClusterManagementTool().getResourceExternalView(clusterName, 
resourceName);
+        if (idealState == null) {
+            throw new NotFoundException("No resource ideal state for " + 
resourceName);
+        }
+
+        // Instances in active cluster
+        List<InstanceSpec> instanceSpecs = 
dataCache.getInstanceCache().get(new ClusterSpec(zkAddress, clusterName));
+        Map<String, InstanceSpec> instanceSpecMap = new HashMap<String, 
InstanceSpec>(instanceSpecs.size());
+        for (InstanceSpec instanceSpec : instanceSpecs) {
+            instanceSpecMap.put(instanceSpec.getInstanceName(), instanceSpec);
+        }
+
+        return D3ResourceCoCentricCircle.fromResourceStateSpec(
+                new ResourceStateSpec(resourceName, idealState, externalView, 
instanceSpecMap));
+    }
+}

http://git-wip-us.apache.org/repos/asf/helix/blob/6cbafef0/helix-ui/src/main/java/org/apache/helix/ui/task/ClearClientCache.java
----------------------------------------------------------------------
diff --git 
a/helix-ui/src/main/java/org/apache/helix/ui/task/ClearClientCache.java 
b/helix-ui/src/main/java/org/apache/helix/ui/task/ClearClientCache.java
new file mode 100644
index 0000000..d30a479
--- /dev/null
+++ b/helix-ui/src/main/java/org/apache/helix/ui/task/ClearClientCache.java
@@ -0,0 +1,25 @@
+package org.apache.helix.ui.task;
+
+import com.google.common.collect.ImmutableMultimap;
+import io.dropwizard.servlets.tasks.Task;
+import org.apache.helix.ui.util.ClientCache;
+
+import java.io.PrintWriter;
+
+public class ClearClientCache extends Task {
+    private final ClientCache clientCache;
+
+    public ClearClientCache(ClientCache clientCache) {
+        super("clearClientCache");
+        this.clientCache = clientCache;
+    }
+
+    @Override
+    public void execute(ImmutableMultimap<String, String> params, PrintWriter 
printWriter) throws Exception {
+        printWriter.println("Clearing ZK connections ...");
+        printWriter.flush();
+        clientCache.invalidateAll();
+        printWriter.println("Done!");
+        printWriter.flush();
+    }
+}

http://git-wip-us.apache.org/repos/asf/helix/blob/6cbafef0/helix-ui/src/main/java/org/apache/helix/ui/task/ClearDataCacheTask.java
----------------------------------------------------------------------
diff --git 
a/helix-ui/src/main/java/org/apache/helix/ui/task/ClearDataCacheTask.java 
b/helix-ui/src/main/java/org/apache/helix/ui/task/ClearDataCacheTask.java
new file mode 100644
index 0000000..fd5661f
--- /dev/null
+++ b/helix-ui/src/main/java/org/apache/helix/ui/task/ClearDataCacheTask.java
@@ -0,0 +1,25 @@
+package org.apache.helix.ui.task;
+
+import com.google.common.collect.ImmutableMultimap;
+import io.dropwizard.servlets.tasks.Task;
+import org.apache.helix.ui.util.DataCache;
+
+import java.io.PrintWriter;
+
+public class ClearDataCacheTask extends Task {
+    private final DataCache dataCache;
+
+    public ClearDataCacheTask(DataCache dataCache) {
+        super("clearDataCache");
+        this.dataCache = dataCache;
+    }
+
+    @Override
+    public void execute(ImmutableMultimap<String, String> params, PrintWriter 
printWriter) throws Exception {
+        printWriter.println("Clearing data caches ...");
+        printWriter.flush();
+        dataCache.invalidate();
+        printWriter.println("Done!");
+        printWriter.flush();
+    }
+}

http://git-wip-us.apache.org/repos/asf/helix/blob/6cbafef0/helix-ui/src/main/java/org/apache/helix/ui/util/ClientCache.java
----------------------------------------------------------------------
diff --git a/helix-ui/src/main/java/org/apache/helix/ui/util/ClientCache.java 
b/helix-ui/src/main/java/org/apache/helix/ui/util/ClientCache.java
new file mode 100644
index 0000000..968e26c
--- /dev/null
+++ b/helix-ui/src/main/java/org/apache/helix/ui/util/ClientCache.java
@@ -0,0 +1,100 @@
+package org.apache.helix.ui.util;
+
+import com.google.common.cache.*;
+import org.I0Itec.zkclient.exception.ZkTimeoutException;
+import org.apache.helix.manager.zk.ZNRecordSerializer;
+import org.apache.helix.manager.zk.ZkClient;
+import org.apache.helix.ui.api.ClusterConnection;
+import org.apache.zookeeper.ZooKeeper;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.ws.rs.WebApplicationException;
+import javax.ws.rs.core.Response;
+import java.net.URLDecoder;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
+
+public class ClientCache {
+    private static final Logger LOG = 
LoggerFactory.getLogger(ClientCache.class);
+    private static final int DEFAULT_SESSION_TIMEOUT_MILLIS = 5000;
+    private static final int DEFAULT_CONNECTION_TIMEOUT_MILLIS = 5000;
+
+    private final ZkAddressValidator zkAddressValidator;
+
+    public ClientCache(ZkAddressValidator zkAddressValidator) {
+        this.zkAddressValidator = zkAddressValidator;
+    }
+
+    // Manages and caches lifecycle of connections to ZK
+    final LoadingCache<String, ClusterConnection> clientCache = 
CacheBuilder.newBuilder()
+            .maximumSize(3)
+            .expireAfterAccess(5, TimeUnit.MINUTES)
+            .removalListener(new RemovalListener<String, ClusterConnection>() {
+                @Override
+                public void onRemoval(RemovalNotification<String, 
ClusterConnection> removalNotification) {
+                    if (removalNotification.getValue() != null) {
+                        ZkClient zkClient = 
removalNotification.getValue().getZkClient();
+                        if (zkClient != null) {
+                            zkClient.close();
+                            LOG.info("Disconnected from {}", 
removalNotification.getKey());
+                        }
+                    }
+                }
+            })
+            .build(new CacheLoader<String, ClusterConnection>() {
+                @Override
+                public ClusterConnection load(String zkAddress) throws 
Exception {
+                    ZkClient zkClient = new ZkClient(
+                            zkAddress,
+                            DEFAULT_SESSION_TIMEOUT_MILLIS,
+                            DEFAULT_CONNECTION_TIMEOUT_MILLIS,
+                            new ZNRecordSerializer());
+                    zkClient.waitUntilConnected();
+                    LOG.info("Connected to {}", zkAddress);
+                    return new ClusterConnection(zkClient);
+                }
+            });
+
+    public ClusterConnection get(String zkAddress) {
+        try {
+            zkAddress = URLDecoder.decode(zkAddress, "UTF-8");
+        } catch (Exception e) {
+            throw new IllegalArgumentException(e);
+        }
+
+        if (!zkAddressValidator.validate(zkAddress)) {
+            throw new WebApplicationException("Cannot access " + zkAddress, 
Response.Status.UNAUTHORIZED);
+        }
+
+        ClusterConnection clusterConnection;
+        try {
+            clusterConnection = clientCache.get(zkAddress);
+        } catch (Exception e) {
+            throw new WebApplicationException(e, 
Response.Status.GATEWAY_TIMEOUT);
+        }
+
+        if 
(!clusterConnection.getZkClient().getConnection().getZookeeperState().equals(ZooKeeper.States.CONNECTED))
 {
+            clientCache.invalidate(zkAddress);
+            throw new WebApplicationException("ZooKeeper connection was dead", 
Response.Status.GATEWAY_TIMEOUT);
+        }
+
+        return clusterConnection;
+    }
+
+    public void invalidateAll() {
+        clientCache.invalidateAll();
+    }
+
+    public Set<String> getDeadConnections() {
+        Set<String> deadConnections = new HashSet<String>();
+        for (Map.Entry<String, ClusterConnection> entry : 
clientCache.asMap().entrySet()) {
+            if 
(!entry.getValue().getZkClient().getConnection().getZookeeperState().isAlive()) 
{
+                deadConnections.add(entry.getKey());
+            }
+        }
+        return deadConnections;
+    }
+}

http://git-wip-us.apache.org/repos/asf/helix/blob/6cbafef0/helix-ui/src/main/java/org/apache/helix/ui/util/DataCache.java
----------------------------------------------------------------------
diff --git a/helix-ui/src/main/java/org/apache/helix/ui/util/DataCache.java 
b/helix-ui/src/main/java/org/apache/helix/ui/util/DataCache.java
new file mode 100644
index 0000000..ff66a05
--- /dev/null
+++ b/helix-ui/src/main/java/org/apache/helix/ui/util/DataCache.java
@@ -0,0 +1,188 @@
+package org.apache.helix.ui.util;
+
+import com.google.common.cache.CacheBuilder;
+import com.google.common.cache.CacheLoader;
+import com.google.common.cache.LoadingCache;
+import org.apache.helix.manager.zk.ZkClient;
+import org.apache.helix.model.HelixConfigScope;
+import org.apache.helix.model.InstanceConfig;
+import org.apache.helix.model.builder.HelixConfigScopeBuilder;
+import org.apache.helix.tools.ClusterSetup;
+import org.apache.helix.ui.api.*;
+
+import java.util.*;
+import java.util.concurrent.TimeUnit;
+
+public class DataCache {
+    private static final int CACHE_EXPIRY_TIME = 30;
+    private static final TimeUnit CACHE_EXPIRY_UNIT = TimeUnit.SECONDS;
+
+    private final LoadingCache<String, List<String>> clusterCache;
+    private final LoadingCache<ClusterSpec, List<String>> resourceCache;
+    private final LoadingCache<ClusterSpec, List<ConfigTableRow>> configCache;
+    private final LoadingCache<ResourceSpec, List<ConfigTableRow>> 
resourceConfigCache;
+    private final LoadingCache<ClusterSpec, List<InstanceSpec>> instanceCache;
+
+    public DataCache(final ClientCache clientCache) {
+        this.clusterCache = CacheBuilder.newBuilder()
+                .expireAfterWrite(CACHE_EXPIRY_TIME, CACHE_EXPIRY_UNIT)
+                .build(new CacheLoader<String, List<String>>() {
+                    @Override
+                    public List<String> load(String zkAddress) throws 
Exception {
+                        ZkClient zkClient = 
clientCache.get(zkAddress).getZkClient();
+                        List<String> clusters = zkClient.getChildren("/");
+                        Collections.sort(clusters);
+                        return clusters;
+                    }
+                });
+
+        this.resourceCache = CacheBuilder.newBuilder()
+                .expireAfterWrite(CACHE_EXPIRY_TIME, CACHE_EXPIRY_UNIT)
+                .build(new CacheLoader<ClusterSpec, List<String>>() {
+                    @Override
+                    public List<String> load(ClusterSpec clusterSpec) throws 
Exception {
+                        ClusterSetup clusterSetup = 
clientCache.get(clusterSpec.getZkAddress()).getClusterSetup();
+                        List<String> resources = new 
ArrayList<String>(clusterSetup.getClusterManagementTool().getResourcesInCluster(clusterSpec.getClusterName()));
+                        Collections.sort(resources);
+                        return resources;
+                    }
+                });
+
+        this.configCache = CacheBuilder.newBuilder()
+                .expireAfterWrite(CACHE_EXPIRY_TIME, CACHE_EXPIRY_UNIT)
+                .build(new CacheLoader<ClusterSpec, List<ConfigTableRow>>() {
+                    @Override
+                    public List<ConfigTableRow> load(ClusterSpec clusterSpec) 
throws Exception {
+                        ClusterSetup clusterSetup = 
clientCache.get(clusterSpec.getZkAddress()).getClusterSetup();
+                        List<ConfigTableRow> configTable = new 
ArrayList<ConfigTableRow>();
+
+                        // Cluster config
+                        HelixConfigScope configScope
+                                = new 
HelixConfigScopeBuilder(HelixConfigScope.ConfigScopeProperty.CLUSTER)
+                                
.forCluster(clusterSpec.getClusterName()).build();
+                        List<String> clusterConfigKeys
+                                = 
clusterSetup.getClusterManagementTool().getConfigKeys(configScope);
+                        Map<String, String> config
+                                = 
clusterSetup.getClusterManagementTool().getConfig(configScope, 
clusterConfigKeys);
+                        for (Map.Entry<String, String> entry : 
config.entrySet()) {
+                            configTable.add(new ConfigTableRow(
+                                    
HelixConfigScope.ConfigScopeProperty.CLUSTER.toString(),
+                                    clusterSpec.getClusterName(),
+                                    entry.getKey(),
+                                    entry.getValue()));
+                        }
+
+                        Collections.sort(configTable);
+
+                        return configTable;
+                    }
+                });
+
+        this.resourceConfigCache = CacheBuilder.newBuilder()
+                .expireAfterWrite(CACHE_EXPIRY_TIME, CACHE_EXPIRY_UNIT)
+                .build(new CacheLoader<ResourceSpec, List<ConfigTableRow>>() {
+                    @Override
+                    public List<ConfigTableRow> load(ResourceSpec 
resourceSpec) throws Exception {
+                        ClusterSetup clusterSetup = 
clientCache.get(resourceSpec.getZkAddress()).getClusterSetup();
+                        List<ConfigTableRow> configTable = new 
ArrayList<ConfigTableRow>();
+
+                        HelixConfigScope configScope = new 
HelixConfigScopeBuilder(HelixConfigScope.ConfigScopeProperty.RESOURCE)
+                                .forCluster(resourceSpec.getClusterName())
+                                .forResource(resourceSpec.getResourceName())
+                                .build();
+
+                        List<String> clusterConfigKeys = 
clusterSetup.getClusterManagementTool().getConfigKeys(configScope);
+
+                        Map<String, String> config = 
clusterSetup.getClusterManagementTool().getConfig(configScope, 
clusterConfigKeys);
+
+                        if (config != null) {
+                            for (Map.Entry<String, String> entry : 
config.entrySet()) {
+                                configTable.add(new ConfigTableRow(
+                                        
HelixConfigScope.ConfigScopeProperty.RESOURCE.toString(),
+                                        resourceSpec.getClusterName(),
+                                        entry.getKey(),
+                                        entry.getValue()));
+                            }
+                        }
+
+                        return configTable;
+                    }
+                });
+
+        this.instanceCache = CacheBuilder.newBuilder()
+                .expireAfterWrite(CACHE_EXPIRY_TIME, CACHE_EXPIRY_UNIT)
+                .build(new CacheLoader<ClusterSpec, List<InstanceSpec>>() {
+                    @Override
+                    public List<InstanceSpec> load(ClusterSpec clusterSpec) 
throws Exception {
+                        ClusterConnection clusterConnection = 
clientCache.get(clusterSpec.getZkAddress());
+
+                        // Instances in the cluster
+                        List<String> instances =
+                                
clusterConnection.getClusterSetup().getClusterManagementTool().getInstancesInCluster(clusterSpec.getClusterName());
+
+                        // Live instances in the cluster
+                        // TODO: should be able to use clusterSetup for this, 
but no method available
+                        List<String> liveInstances
+                                = 
clusterConnection.getZkClient().getChildren(String.format("/%s/LIVEINSTANCES", 
clusterSpec.getClusterName()));
+                        Set<String> liveInstanceSet = new HashSet<String>();
+                        if (liveInstances != null) {
+                            liveInstanceSet.addAll(liveInstances);
+                        }
+
+                        // Enabled instances
+                        Set<String> enabledInstances = new HashSet<String>();
+                        if (instances != null) {
+                            for (String instance : instances) {
+                                InstanceConfig instanceConfig = 
clusterConnection.getClusterSetup()
+                                        .getClusterManagementTool()
+                                        
.getInstanceConfig(clusterSpec.getClusterName(), instance);
+                                if (instanceConfig.getInstanceEnabled()) {
+                                    enabledInstances.add(instance);
+                                }
+                            }
+                        }
+
+                        // Rows
+                        List<InstanceSpec> instanceSpecs = new 
ArrayList<InstanceSpec>();
+                        if (instances != null) {
+                            for (String instance : instances) {
+                                instanceSpecs.add(new InstanceSpec(
+                                        instance,
+                                        enabledInstances.contains(instance),
+                                        liveInstanceSet.contains(instance)));
+                            }
+                        }
+
+                        return instanceSpecs;
+                    }
+                });
+    }
+
+    public void invalidate() {
+        clusterCache.invalidateAll();
+        resourceCache.invalidateAll();
+        configCache.invalidateAll();
+        resourceConfigCache.invalidateAll();
+        instanceCache.invalidateAll();
+    }
+
+    public LoadingCache<String, List<String>> getClusterCache() {
+        return clusterCache;
+    }
+
+    public LoadingCache<ClusterSpec, List<String>> getResourceCache() {
+        return resourceCache;
+    }
+
+    public LoadingCache<ClusterSpec, List<ConfigTableRow>> getConfigCache() {
+        return configCache;
+    }
+
+    public LoadingCache<ResourceSpec, List<ConfigTableRow>> 
getResourceConfigCache() {
+        return resourceConfigCache;
+    }
+
+    public LoadingCache<ClusterSpec, List<InstanceSpec>> getInstanceCache() {
+        return instanceCache;
+    }
+}

http://git-wip-us.apache.org/repos/asf/helix/blob/6cbafef0/helix-ui/src/main/java/org/apache/helix/ui/util/DropWizardApplicationRunner.java
----------------------------------------------------------------------
diff --git 
a/helix-ui/src/main/java/org/apache/helix/ui/util/DropWizardApplicationRunner.java
 
b/helix-ui/src/main/java/org/apache/helix/ui/util/DropWizardApplicationRunner.java
new file mode 100644
index 0000000..fb27130
--- /dev/null
+++ 
b/helix-ui/src/main/java/org/apache/helix/ui/util/DropWizardApplicationRunner.java
@@ -0,0 +1,86 @@
+package org.apache.helix.ui.util;
+
+import io.dropwizard.Application;
+import io.dropwizard.Configuration;
+import io.dropwizard.cli.ServerCommand;
+import io.dropwizard.configuration.ConfigurationFactory;
+import io.dropwizard.setup.Bootstrap;
+import io.dropwizard.setup.Environment;
+
+import org.eclipse.jetty.server.Server;
+import org.eclipse.jetty.util.component.AbstractLifeCycle;
+import org.eclipse.jetty.util.component.LifeCycle;
+
+import java.io.File;
+
+/**
+ * A utility to run DropWizard (http://dropwizard.io/) applications in-process.
+ */
+public class DropWizardApplicationRunner {
+    /**
+     * Creates a Jetty server for an application that can be started / stopped 
in-process
+     *
+     * @param config           An application configuration instance (with 
properties set)
+     * @param applicationClass The {@link io.dropwizard.Application} 
implementation class
+     * @param <T>              The configuration class
+     * @return A Jetty server
+     */
+    @SuppressWarnings("unchecked")
+    public static <T extends Configuration>
+    Server createServer(T config, Class<? extends Application<T>> 
applicationClass) throws Exception {
+        // Create application
+        final Application<T> application = 
applicationClass.getConstructor().newInstance();
+
+        // Create bootstrap
+        final ServerCommand<T> serverCommand = new 
ServerCommand<T>(application);
+        final Bootstrap<T> bootstrap = new Bootstrap<T>(application);
+        bootstrap.addCommand(serverCommand);
+        application.initialize(bootstrap);
+
+        // Write a temporary config file
+        File tmpConfigFile = new File(
+                System.getProperty("java.io.tmpdir"),
+                config.getClass().getCanonicalName() + "_" + 
System.currentTimeMillis());
+        tmpConfigFile.deleteOnExit();
+        bootstrap.getObjectMapper().writeValue(tmpConfigFile, config);
+
+        // Parse configuration
+        ConfigurationFactory<T> configurationFactory
+                = bootstrap.getConfigurationFactoryFactory()
+                .create((Class<T>) config.getClass(),
+                        bootstrap.getValidatorFactory().getValidator(),
+                        bootstrap.getObjectMapper(),
+                        "dw");
+        final T builtConfig = configurationFactory.build(
+                bootstrap.getConfigurationSourceProvider(), 
tmpConfigFile.getAbsolutePath());
+
+        // Configure logging
+        builtConfig.getLoggingFactory()
+                .configure(bootstrap.getMetricRegistry(),
+                        bootstrap.getApplication().getName());
+
+        // Environment
+        final Environment environment = new 
Environment(bootstrap.getApplication().getName(),
+                bootstrap.getObjectMapper(),
+                bootstrap.getValidatorFactory().getValidator(),
+                bootstrap.getMetricRegistry(),
+                bootstrap.getClassLoader());
+
+        // Initialize environment
+        builtConfig.getMetricsFactory().configure(environment.lifecycle(), 
bootstrap.getMetricRegistry());
+        bootstrap.run(builtConfig, environment);
+        application.run(builtConfig, environment);
+
+        // Server
+        final Server server = 
builtConfig.getServerFactory().build(environment);
+        server.addLifeCycleListener(new 
AbstractLifeCycle.AbstractLifeCycleListener() {
+            @Override
+            public void lifeCycleStopped(LifeCycle event) {
+                builtConfig.getLoggingFactory().stop();
+            }
+        });
+
+        return server;
+    }
+}
+

http://git-wip-us.apache.org/repos/asf/helix/blob/6cbafef0/helix-ui/src/main/java/org/apache/helix/ui/util/ZkAddressValidator.java
----------------------------------------------------------------------
diff --git 
a/helix-ui/src/main/java/org/apache/helix/ui/util/ZkAddressValidator.java 
b/helix-ui/src/main/java/org/apache/helix/ui/util/ZkAddressValidator.java
new file mode 100644
index 0000000..a77ee3b
--- /dev/null
+++ b/helix-ui/src/main/java/org/apache/helix/ui/util/ZkAddressValidator.java
@@ -0,0 +1,37 @@
+package org.apache.helix.ui.util;
+
+import java.util.HashSet;
+import java.util.Set;
+
+public class ZkAddressValidator {
+
+    private final Set<String> zkMachines;
+
+    public ZkAddressValidator(Set<String> zkAddresses) {
+        if (zkAddresses == null) {
+            this.zkMachines = null;
+        } else {
+            this.zkMachines = new HashSet<String>();
+            for (String zkAddress : zkAddresses) {
+                for (String machine : zkAddress.split(",")) {
+                    this.zkMachines.add(machine);
+                }
+            }
+        }
+    }
+
+    public boolean validate(String zkAddress) {
+        if (zkMachines == null) {
+            return true;
+        }
+
+        String[] machines = zkAddress.split(",");
+        for (String machine : machines) {
+            if (!zkMachines.contains(machine)) {
+                return false;
+            }
+        }
+
+        return true;
+    }
+}

http://git-wip-us.apache.org/repos/asf/helix/blob/6cbafef0/helix-ui/src/main/java/org/apache/helix/ui/view/ClusterView.java
----------------------------------------------------------------------
diff --git a/helix-ui/src/main/java/org/apache/helix/ui/view/ClusterView.java 
b/helix-ui/src/main/java/org/apache/helix/ui/view/ClusterView.java
new file mode 100644
index 0000000..b9fc0a6
--- /dev/null
+++ b/helix-ui/src/main/java/org/apache/helix/ui/view/ClusterView.java
@@ -0,0 +1,85 @@
+package org.apache.helix.ui.view;
+
+import io.dropwizard.views.View;
+import org.apache.helix.ui.api.ConfigTableRow;
+import org.apache.helix.ui.api.InstanceSpec;
+
+import java.io.IOException;
+import java.net.URLEncoder;
+import java.util.List;
+
+public class ClusterView extends View {
+    private final boolean adminMode;
+    private final String zkAddress;
+    private final List<String> clusters;
+    private final boolean activeValid;
+    private final String activeCluster;
+    private final List<String> activeClusterResources;
+    private final List<InstanceSpec> instanceSpecs;
+    private final List<ConfigTableRow> configTable;
+    private final List<String> stateModels;
+    private final List<String> rebalanceModes;
+
+    public ClusterView(boolean adminMode,
+                       String zkAddress,
+                       List<String> clusters,
+                       boolean activeValid,
+                       String activeCluster,
+                       List<String> activeClusterResources,
+                       List<InstanceSpec> instanceSpecs,
+                       List<ConfigTableRow> configTable,
+                       List<String> stateModels,
+                       List<String> rebalanceModes) {
+        super("cluster-view.ftl");
+        this.adminMode = adminMode;
+        this.zkAddress = zkAddress;
+        this.clusters = clusters;
+        this.activeValid = activeValid;
+        this.activeCluster = activeCluster;
+        this.activeClusterResources = activeClusterResources;
+        this.instanceSpecs = instanceSpecs;
+        this.configTable = configTable;
+        this.stateModels = stateModels;
+        this.rebalanceModes = rebalanceModes;
+    }
+
+    public boolean isAdminMode() {
+        return adminMode;
+    }
+
+    public String getZkAddress() throws IOException {
+        return URLEncoder.encode(zkAddress, "UTF-8");
+    }
+
+    public List<String> getClusters() {
+        return clusters;
+    }
+
+    public boolean isActiveValid() {
+        return activeValid;
+    }
+
+    public String getActiveCluster() {
+        return activeCluster;
+    }
+
+    public List<String> getActiveClusterResources() {
+        return activeClusterResources;
+    }
+
+    public List<InstanceSpec> getInstanceSpecs() {
+        return instanceSpecs;
+    }
+
+    public List<ConfigTableRow> getConfigTable() {
+        return configTable;
+    }
+
+    public List<String> getStateModels() {
+        return stateModels;
+    }
+
+    public List<String> getRebalanceModes() {
+        return rebalanceModes;
+    }
+}

http://git-wip-us.apache.org/repos/asf/helix/blob/6cbafef0/helix-ui/src/main/java/org/apache/helix/ui/view/LandingView.java
----------------------------------------------------------------------
diff --git a/helix-ui/src/main/java/org/apache/helix/ui/view/LandingView.java 
b/helix-ui/src/main/java/org/apache/helix/ui/view/LandingView.java
new file mode 100644
index 0000000..330fa4b
--- /dev/null
+++ b/helix-ui/src/main/java/org/apache/helix/ui/view/LandingView.java
@@ -0,0 +1,9 @@
+package org.apache.helix.ui.view;
+
+import io.dropwizard.views.View;
+
+public class LandingView extends View {
+    public LandingView() {
+        super("landing-view.ftl");
+    }
+}

Reply via email to