You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@helix.apache.org by ka...@apache.org on 2015/03/29 01:04:40 UTC
[08/20] helix git commit: A user interface for viewing and
administering clusters
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 <br...@gmail.com>
Authored: Sat Mar 28 09:56:14 2015 -0700
Committer: Greg Brandt <br...@gmail.com>
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:
+
+
+
+And using visualization:
+
+
+
+## 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:
+
+
+
+And the following shows the admin instance view:
+
+
+
+## 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");
+ }
+}