You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@helix.apache.org by gb...@apache.org on 2016/09/17 19:16:25 UTC

helix git commit: helix-ui improvements

Repository: helix
Updated Branches:
  refs/heads/master d32968fbd -> 4d9db5f02


helix-ui improvements

Adds zkAliases to helix-ui configuration

`zkAliases` is a map of alias to ZK connection string. If this is provided,
then one can use the alias in place of the zk connection string in
`/dashboard/{zk}` route. In addition, the root `/dashboard` route will allow
the user to choose among the aliases as opposed to pasting in a zk connection
string.

Add ping / health resources

Add tags to instance table

Show controller leader in UI

Add reset resource button to helix-ui

Fix FULL_AUTO bug

Support SEMI_AUTO resource that do not have state map


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

Branch: refs/heads/master
Commit: 4d9db5f02cad4a05ae4f08143e845284e02ee1aa
Parents: d32968f
Author: Greg Brandt <gr...@airbnb.com>
Authored: Thu Jul 14 14:00:43 2016 -0700
Committer: Greg Brandt <br...@gmail.com>
Committed: Sat Sep 17 12:16:11 2016 -0700

----------------------------------------------------------------------
 .../org/apache/helix/ui/HelixUIApplication.java | 23 +++++--
 .../ui/HelixUIApplicationConfiguration.java     | 12 ++++
 .../org/apache/helix/ui/api/InstanceSpec.java   | 11 +++-
 .../apache/helix/ui/api/ResourceStateSpec.java  | 53 +++++++++++----
 .../helix/ui/resource/DashboardResource.java    | 68 ++++++++++++++++----
 .../helix/ui/resource/HealthResource.java       | 39 +++++++++++
 .../apache/helix/ui/resource/PingResource.java  | 38 +++++++++++
 .../org/apache/helix/ui/util/ClientCache.java   |  9 ++-
 .../org/apache/helix/ui/util/DataCache.java     | 60 +++++++++++++++--
 .../org/apache/helix/ui/view/ClusterView.java   |  9 ++-
 .../org/apache/helix/ui/view/LandingView.java   | 11 +++-
 helix-ui/src/main/resources/assets/css/app.css  |  8 +--
 .../main/resources/assets/js/landing-view.js    | 10 ++-
 .../resources/assets/js/resource-state-table.js | 25 +++++++
 .../org/apache/helix/ui/view/cluster-view.ftl   |  4 ++
 .../helix/ui/view/common/instance-table.ftl     |  2 +
 .../ui/view/common/resource-state-table.ftl     | 13 +++-
 .../org/apache/helix/ui/view/landing-view.ftl   | 27 +++++++-
 18 files changed, 370 insertions(+), 52 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/helix/blob/4d9db5f0/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
index c6e366e..d431f1d 100644
--- a/helix-ui/src/main/java/org/apache/helix/ui/HelixUIApplication.java
+++ b/helix-ui/src/main/java/org/apache/helix/ui/HelixUIApplication.java
@@ -28,6 +28,8 @@ 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.HealthResource;
+import org.apache.helix.ui.resource.PingResource;
 import org.apache.helix.ui.resource.VisualizerResource;
 import org.apache.helix.ui.task.ClearClientCache;
 import org.apache.helix.ui.task.ClearDataCacheTask;
@@ -37,6 +39,10 @@ import org.apache.helix.ui.util.ZkAddressValidator;
 import org.eclipse.jetty.util.component.AbstractLifeCycle;
 import org.eclipse.jetty.util.component.LifeCycle;
 
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
 public class HelixUIApplication extends Application<HelixUIApplicationConfiguration> {
   @Override
   public String getName() {
@@ -60,7 +66,7 @@ public class HelixUIApplication extends Application<HelixUIApplicationConfigurat
   @Override
   public void run(HelixUIApplicationConfiguration config, Environment environment) throws Exception {
     final ZkAddressValidator zkAddressValidator = new ZkAddressValidator(config.getZkAddresses());
-    final ClientCache clientCache = new ClientCache(zkAddressValidator);
+    final ClientCache clientCache = new ClientCache(zkAddressValidator, config.getZkAliases());
 
     // Close all connections when application stops
     environment.lifecycle().addLifeCycleListener(new AbstractLifeCycle.AbstractLifeCycleListener() {
@@ -70,15 +76,24 @@ public class HelixUIApplication extends Application<HelixUIApplicationConfigurat
       }
     });
 
-    DataCache dataCache = new DataCache(clientCache);
+    // Any zk aliases
+    List<String> zkAliases = new ArrayList<String>();
+    zkAliases.addAll(config.getZkAliases().keySet());
+    Collections.sort(zkAliases);
 
+    DataCache dataCache = new DataCache(clientCache);
 
-    DashboardResource dashboardResource
-            = new DashboardResource(clientCache, dataCache, config.isAdminMode());
+    DashboardResource dashboardResource = new DashboardResource(
+        clientCache,
+        dataCache,
+        config.isAdminMode(),
+        zkAliases);
 
     environment.healthChecks().register("clusterConnection", new ClusterConnectionHealthCheck(clientCache));
     environment.jersey().register(dashboardResource);
     environment.jersey().register(new VisualizerResource(clientCache, dataCache));
+    environment.jersey().register(new HealthResource());
+    environment.jersey().register(new PingResource());
     environment.admin().addTask(new ClearDataCacheTask(dataCache));
     environment.admin().addTask(new ClearClientCache(clientCache));
 

http://git-wip-us.apache.org/repos/asf/helix/blob/4d9db5f0/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
index 6547b47..7a0cd74 100644
--- a/helix-ui/src/main/java/org/apache/helix/ui/HelixUIApplicationConfiguration.java
+++ b/helix-ui/src/main/java/org/apache/helix/ui/HelixUIApplicationConfiguration.java
@@ -24,6 +24,8 @@ import com.google.common.collect.ImmutableMap;
 import io.dropwizard.Configuration;
 
 import javax.validation.constraints.NotNull;
+import java.util.Collections;
+import java.util.Map;
 import java.util.Set;
 
 public class HelixUIApplicationConfiguration extends Configuration {
@@ -34,6 +36,8 @@ public class HelixUIApplicationConfiguration extends Configuration {
 
   private Set<String> zkAddresses;
 
+  private Map<String, String> zkAliases = Collections.emptyMap();
+
   @JsonProperty("viewRendererConfiguration")
   public ImmutableMap<String, ImmutableMap<String, String>> getViewRendererConfiguration() {
     return viewRendererConfiguration;
@@ -54,4 +58,12 @@ public class HelixUIApplicationConfiguration extends Configuration {
   public Set<String> getZkAddresses() {
     return zkAddresses;
   }
+
+  public Map<String, String> getZkAliases() {
+    return zkAliases;
+  }
+
+  public void setZkAliases(Map<String, String> zkAliases) {
+    this.zkAliases = zkAliases;
+  }
 }

http://git-wip-us.apache.org/repos/asf/helix/blob/4d9db5f0/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
index 6ce008d..a7573de 100644
--- 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
@@ -19,17 +19,22 @@ package org.apache.helix.ui.api;
  * under the License.
  */
 
+import java.util.List;
+
 public class InstanceSpec implements Comparable<InstanceSpec> {
   private final String instanceName;
   private final boolean enabled;
   private final boolean live;
+  private final List<String> tags;
 
   public InstanceSpec(String instanceName,
                       boolean enabled,
-                      boolean live) {
+                      boolean live,
+                      List<String> tags) {
     this.instanceName = instanceName;
     this.enabled = enabled;
     this.live = live;
+    this.tags = tags;
   }
 
   public String getInstanceName() {
@@ -44,6 +49,10 @@ public class InstanceSpec implements Comparable<InstanceSpec> {
     return live;
   }
 
+  public List<String> getTags() {
+    return tags;
+  }
+
   @Override
   public int compareTo(InstanceSpec o) {
     return instanceName.compareTo(o.instanceName);

http://git-wip-us.apache.org/repos/asf/helix/blob/4d9db5f0/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
index 64fd48f..f6fbba9 100644
--- 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
@@ -61,27 +61,52 @@ public class ResourceStateSpec {
 
   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);
-            }
+    if (useExternalView(idealState)) {
+      Set<String> partitionNames = externalView.getPartitionSet();
+      for (String partitionName : partitionNames) {
+        Map<String, String> stateMap = externalView.getStateMap(partitionName);
+        if (stateMap != null) {
+          for (Map.Entry<String, String> entry : stateMap.entrySet()) {
+            String instanceName = entry.getKey();
+            String state = entry.getValue();
+            resourceStateTable.add(new ResourceStateTableRow(resource, partitionName, instanceName, "N/A", state));
           }
+        }
+      }
+    } else {
+      // By default, we assume the ideal state has something in it
+      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));
+            resourceStateTable.add(new ResourceStateTableRow(resource, partitionName, instanceName, ideal, external));
+          }
         }
       }
     }
 
     return resourceStateTable;
   }
+
+  /**
+   * Returns true if there is no legitimate state mapping in the ideal state.
+   */
+  private static boolean useExternalView(IdealState idealState) {
+    return IdealState.RebalanceMode.FULL_AUTO.equals(idealState.getRebalanceMode())
+        || (IdealState.RebalanceMode.SEMI_AUTO.equals(idealState.getRebalanceMode())
+            && idealState.getRecord().getMapFields().isEmpty());
+  }
 }

http://git-wip-us.apache.org/repos/asf/helix/blob/4d9db5f0/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
index 0991599..c717441 100644
--- 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
@@ -29,6 +29,8 @@ 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 org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 import javax.ws.rs.*;
 import javax.ws.rs.core.MediaType;
@@ -39,6 +41,7 @@ import java.util.*;
 @Path("/")
 @Produces(MediaType.TEXT_HTML)
 public class DashboardResource {
+  private static final Logger LOG = LoggerFactory.getLogger(DashboardResource.class);
   private static final List<String> REBALANCE_MODES = ImmutableList.of(
           IdealState.RebalanceMode.SEMI_AUTO.toString(),
           IdealState.RebalanceMode.FULL_AUTO.toString(),
@@ -49,13 +52,16 @@ public class DashboardResource {
   private final boolean adminMode;
   private final ClientCache clientCache;
   private final DataCache dataCache;
+  private final List<String> zkAliases;
 
   public DashboardResource(ClientCache clientCache,
                            DataCache dataCache,
-                           boolean adminMode) {
+                           boolean adminMode,
+                           List<String> zkAliases) {
     this.clientCache = clientCache;
     this.dataCache = dataCache;
     this.adminMode = adminMode;
+    this.zkAliases = zkAliases;
   }
 
   @GET
@@ -66,7 +72,7 @@ public class DashboardResource {
   @GET
   @Path("/dashboard")
   public LandingView getLandingView() {
-    return new LandingView();
+    return new LandingView(zkAliases);
   }
 
   @GET
@@ -92,7 +98,18 @@ public class DashboardResource {
 
     // Check it
     if (!ZKUtil.isClusterSetup(activeCluster, clusterConnection.getZkClient())) {
-      return new ClusterView(adminMode, zkAddress, clusters, false, activeCluster, null, null, null, null, null);
+      return new ClusterView(
+          adminMode,
+          zkAddress,
+          clusters,
+          false,
+          activeCluster,
+          null,
+          null,
+          null,
+          null,
+          null,
+          null);
     }
 
     // Resources in the active cluster
@@ -108,17 +125,21 @@ public class DashboardResource {
     // Config table
     List<ConfigTableRow> configTable = dataCache.getConfigCache().get(clusterSpec);
 
+    // Controller leader
+    String controllerLeader = dataCache.getControllerLeaderCache().get(clusterSpec);
+
     return new ClusterView(
-            adminMode,
-            zkAddress,
-            clusters,
-            true,
-            activeCluster,
-            activeClusterResources,
-            instanceSpecs,
-            configTable,
-            stateModels,
-            REBALANCE_MODES);
+        adminMode,
+        zkAddress,
+        clusters,
+        true,
+        activeCluster,
+        activeClusterResources,
+        instanceSpecs,
+        configTable,
+        stateModels,
+        REBALANCE_MODES,
+        controllerLeader);
   }
 
   @GET
@@ -188,4 +209,25 @@ public class DashboardResource {
             IdealStateSpec.fromIdealState(idealState),
             instanceSpecs);
   }
+
+  @POST
+  @Path("/dashboard/{zkAddress}/{cluster}/{resource}")
+  public Response resourceAction(
+      @PathParam("zkAddress") String zkAddress,
+      @PathParam("cluster") String cluster,
+      @PathParam("resource") String resource,
+      @QueryParam("action") @DefaultValue("reset") String action) {
+    ClusterConnection clusterConnection = clientCache.get(zkAddress);
+
+    if ("reset".equals(action)) {
+      clusterConnection
+          .getClusterSetup()
+          .getClusterManagementTool()
+          .resetResource(cluster, Collections.singletonList(resource));
+      LOG.info("Reset resource {} in cluster {}", resource, cluster);
+      return Response.ok().build();
+    } else {
+      return Response.status(Response.Status.BAD_REQUEST).entity("Unsupported action " + action).build();
+    }
+  }
 }

http://git-wip-us.apache.org/repos/asf/helix/blob/4d9db5f0/helix-ui/src/main/java/org/apache/helix/ui/resource/HealthResource.java
----------------------------------------------------------------------
diff --git a/helix-ui/src/main/java/org/apache/helix/ui/resource/HealthResource.java b/helix-ui/src/main/java/org/apache/helix/ui/resource/HealthResource.java
new file mode 100644
index 0000000..2ea2520
--- /dev/null
+++ b/helix-ui/src/main/java/org/apache/helix/ui/resource/HealthResource.java
@@ -0,0 +1,39 @@
+package org.apache.helix.ui.resource;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import io.dropwizard.jersey.caching.CacheControl;
+
+import javax.ws.rs.GET;
+import javax.ws.rs.Path;
+import javax.ws.rs.Produces;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+
+@Path("/health")
+@Produces(MediaType.APPLICATION_JSON)
+public class HealthResource {
+  @GET
+  @CacheControl(mustRevalidate = true, noCache = true, noStore = true)
+  public Response health() {
+    // TODO: placeholder for real health checks
+    return Response.status(Response.Status.OK).entity("IMOK").build();
+  }
+}

http://git-wip-us.apache.org/repos/asf/helix/blob/4d9db5f0/helix-ui/src/main/java/org/apache/helix/ui/resource/PingResource.java
----------------------------------------------------------------------
diff --git a/helix-ui/src/main/java/org/apache/helix/ui/resource/PingResource.java b/helix-ui/src/main/java/org/apache/helix/ui/resource/PingResource.java
new file mode 100644
index 0000000..33c37fe
--- /dev/null
+++ b/helix-ui/src/main/java/org/apache/helix/ui/resource/PingResource.java
@@ -0,0 +1,38 @@
+package org.apache.helix.ui.resource;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import io.dropwizard.jersey.caching.CacheControl;
+
+import javax.ws.rs.GET;
+import javax.ws.rs.Path;
+import javax.ws.rs.Produces;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+
+@Path("/ping")
+@Produces(MediaType.APPLICATION_JSON)
+public class PingResource {
+  @GET
+  @CacheControl(mustRevalidate = true, noCache = true, noStore = true)
+  public Response health() {
+    return Response.status(Response.Status.OK).entity("pong").build();
+  }
+}

http://git-wip-us.apache.org/repos/asf/helix/blob/4d9db5f0/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
index f461277..dd02a8c 100644
--- 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
@@ -41,9 +41,11 @@ public class ClientCache {
   private static final int DEFAULT_CONNECTION_TIMEOUT_MILLIS = 5000;
 
   private final ZkAddressValidator zkAddressValidator;
+  private final Map<String, String> zkAliases;
 
-  public ClientCache(ZkAddressValidator zkAddressValidator) {
+  public ClientCache(ZkAddressValidator zkAddressValidator, Map<String, String> zkAliases) {
     this.zkAddressValidator = zkAddressValidator;
+    this.zkAliases = zkAliases;
   }
 
   // Manages and caches lifecycle of connections to ZK
@@ -77,6 +79,11 @@ public class ClientCache {
           });
 
   public ClusterConnection get(String zkAddress) {
+    // Map alias if exists
+    if (zkAliases.containsKey(zkAddress)) {
+      zkAddress = zkAliases.get(zkAddress);
+    }
+
     try {
       zkAddress = URLDecoder.decode(zkAddress, "UTF-8");
     } catch (Exception e) {

http://git-wip-us.apache.org/repos/asf/helix/blob/4d9db5f0/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
index 1441006..9b73a95 100644
--- 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
@@ -22,6 +22,8 @@ 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.ZNRecord;
+import org.apache.helix.manager.zk.ZKHelixDataAccessor;
 import org.apache.helix.manager.zk.ZkClient;
 import org.apache.helix.model.HelixConfigScope;
 import org.apache.helix.model.InstanceConfig;
@@ -40,6 +42,7 @@ public class DataCache {
   private static final TimeUnit CACHE_EXPIRY_UNIT = TimeUnit.SECONDS;
 
   private final LoadingCache<String, List<String>> clusterCache;
+  private final LoadingCache<ClusterSpec, String> controllerLeaderCache;
   private final LoadingCache<ClusterSpec, List<String>> resourceCache;
   private final LoadingCache<ClusterSpec, List<ConfigTableRow>> configCache;
   private final LoadingCache<ResourceSpec, List<ConfigTableRow>> resourceConfigCache;
@@ -58,6 +61,22 @@ public class DataCache {
               }
             });
 
+    this.controllerLeaderCache = CacheBuilder.newBuilder()
+        .expireAfterWrite(CACHE_EXPIRY_TIME, CACHE_EXPIRY_UNIT)
+        .build(new CacheLoader<ClusterSpec, String>() {
+          @Override
+          public String load(ClusterSpec clusterSpec) throws Exception {
+            ZkClient zkClient = clientCache
+                .get(clusterSpec.getZkAddress())
+                .getZkClient();
+
+            ZNRecord znRecord = zkClient.readData(
+                String.format("/%s/CONTROLLER/LEADER", clusterSpec.getClusterName()));
+
+            return znRecord.getId();
+          }
+        });
+
     this.resourceCache = CacheBuilder.newBuilder()
             .expireAfterWrite(CACHE_EXPIRY_TIME, CACHE_EXPIRY_UNIT)
             .build(new CacheLoader<ClusterSpec, List<String>>() {
@@ -142,13 +161,16 @@ public class DataCache {
                 ClusterConnection clusterConnection = clientCache.get(clusterSpec.getZkAddress());
 
                 // Instances in the cluster
-                List<String> instances =
-                        clusterConnection.getClusterSetup().getClusterManagementTool().getInstancesInCluster(clusterSpec.getClusterName());
+                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()));
+                List<String> liveInstances = clusterConnection
+                    .getZkClient()
+                    .getChildren(String.format("/%s/LIVEINSTANCES", clusterSpec.getClusterName()));
                 Set<String> liveInstanceSet = new HashSet<String>();
                 if (liveInstances != null) {
                   liveInstanceSet.addAll(liveInstances);
@@ -167,14 +189,34 @@ public class DataCache {
                   }
                 }
 
+                // Instance tags
+                Map<String, List<String>> instanceTags = new HashMap<String, List<String>>();
+                if (instances != null) {
+                  for (String instance : instances) {
+                    InstanceConfig instanceConfig = clusterConnection
+                        .getClusterSetup()
+                        .getClusterManagementTool()
+                        .getInstanceConfig(clusterSpec.getClusterName(), instance);
+
+                    List<String> tags = instanceConfig.getTags() == null
+                        ? Collections.<String>emptyList()
+                        : instanceConfig.getTags();
+
+                    Collections.sort(tags);
+
+                    instanceTags.put(instance, tags);
+                  }
+                }
+
                 // 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)));
+                        instance,
+                        enabledInstances.contains(instance),
+                        liveInstanceSet.contains(instance),
+                        instanceTags.get(instance)));
                   }
                 }
 
@@ -195,6 +237,10 @@ public class DataCache {
     return clusterCache;
   }
 
+  public LoadingCache<ClusterSpec, String> getControllerLeaderCache() {
+    return controllerLeaderCache;
+  }
+
   public LoadingCache<ClusterSpec, List<String>> getResourceCache() {
     return resourceCache;
   }

http://git-wip-us.apache.org/repos/asf/helix/blob/4d9db5f0/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
index 27e1050..4962c74 100644
--- 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
@@ -38,6 +38,7 @@ public class ClusterView extends View {
   private final List<ConfigTableRow> configTable;
   private final List<String> stateModels;
   private final List<String> rebalanceModes;
+  private final String controllerLeader;
 
   public ClusterView(boolean adminMode,
                      String zkAddress,
@@ -48,7 +49,8 @@ public class ClusterView extends View {
                      List<InstanceSpec> instanceSpecs,
                      List<ConfigTableRow> configTable,
                      List<String> stateModels,
-                     List<String> rebalanceModes) {
+                     List<String> rebalanceModes,
+                     String controllerLeader) {
     super("cluster-view.ftl");
     this.adminMode = adminMode;
     this.zkAddress = zkAddress;
@@ -60,6 +62,7 @@ public class ClusterView extends View {
     this.configTable = configTable;
     this.stateModels = stateModels;
     this.rebalanceModes = rebalanceModes;
+    this.controllerLeader = controllerLeader;
   }
 
   public boolean isAdminMode() {
@@ -101,4 +104,8 @@ public class ClusterView extends View {
   public List<String> getRebalanceModes() {
     return rebalanceModes;
   }
+
+  public String getControllerLeader() {
+    return controllerLeader;
+  }
 }

http://git-wip-us.apache.org/repos/asf/helix/blob/4d9db5f0/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
index 96d69e0..0826be0 100644
--- 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
@@ -21,8 +21,17 @@ package org.apache.helix.ui.view;
 
 import io.dropwizard.views.View;
 
+import java.util.List;
+
 public class LandingView extends View {
-  public LandingView() {
+  private final List<String> zkAliases;
+
+  public LandingView(List<String> zkAliases) {
     super("landing-view.ftl");
+    this.zkAliases = zkAliases;
+  }
+
+  public List<String> getZkAliases() {
+    return zkAliases;
   }
 }

http://git-wip-us.apache.org/repos/asf/helix/blob/4d9db5f0/helix-ui/src/main/resources/assets/css/app.css
----------------------------------------------------------------------
diff --git a/helix-ui/src/main/resources/assets/css/app.css b/helix-ui/src/main/resources/assets/css/app.css
index 0a8f623..0223b25 100644
--- a/helix-ui/src/main/resources/assets/css/app.css
+++ b/helix-ui/src/main/resources/assets/css/app.css
@@ -51,10 +51,6 @@
     width: 50%;
 }
 
-#landing-area input {
-    width: 80%;
-}
-
 /* cluster-view.ftl */
 
 #cluster-views-area {
@@ -109,3 +105,7 @@
 .instance-down {
     background-color: #e5e5e5;
 }
+
+#safe-resource-actions {
+    float: right;
+}

http://git-wip-us.apache.org/repos/asf/helix/blob/4d9db5f0/helix-ui/src/main/resources/assets/js/landing-view.js
----------------------------------------------------------------------
diff --git a/helix-ui/src/main/resources/assets/js/landing-view.js b/helix-ui/src/main/resources/assets/js/landing-view.js
index fe46e8d..0847ce1 100644
--- a/helix-ui/src/main/resources/assets/js/landing-view.js
+++ b/helix-ui/src/main/resources/assets/js/landing-view.js
@@ -19,7 +19,11 @@
 
 $(document).ready(function() {
     $("#landing-form-button").click(function(event) {
-        event.preventDefault()
-        window.location = "/dashboard/" + encodeURIComponent($("#zk-address").val())
-    })
+        event.preventDefault();
+        if ($("#zk-address").val()) {
+            window.location = "/dashboard/" + encodeURIComponent($("#zk-address").val());
+        } else {
+            window.location = "/dashboard/" + encodeURIComponent($("#zk-alias").val());
+        }
+    });
 })

http://git-wip-us.apache.org/repos/asf/helix/blob/4d9db5f0/helix-ui/src/main/resources/assets/js/resource-state-table.js
----------------------------------------------------------------------
diff --git a/helix-ui/src/main/resources/assets/js/resource-state-table.js b/helix-ui/src/main/resources/assets/js/resource-state-table.js
index 6ab112b..2a75b86 100644
--- a/helix-ui/src/main/resources/assets/js/resource-state-table.js
+++ b/helix-ui/src/main/resources/assets/js/resource-state-table.js
@@ -27,6 +27,31 @@ $(document).ready(function() {
         }
     })
 
+    $('#reset-resource').click(function(event) {
+        event.preventDefault()
+        var button = $(this);
+
+        var url = '/dashboard'
+            + '/' + encodeURIComponent(button.attr('zk-address'))
+            + '/' + encodeURIComponent(button.attr('cluster'))
+            + '/' + encodeURIComponent(button.attr('resource'))
+            + '?action=reset';
+
+        $.ajax({
+            type: 'POST',
+            url: url,
+            success: function(data, status, jqXHR) {
+                console.log(jqXHR);
+                alert('Successfully reset ' + button.attr('resource'));
+                location.reload();
+            },
+            error: function(jqXHR, status, ex) {
+                console.error(jqXHR);
+                alert(jqXHR.statusText + ": " + jqXHR.responseText);
+            }
+        });
+    });
+
     $('#filter-add').click(function(event) {
         event.preventDefault()
 

http://git-wip-us.apache.org/repos/asf/helix/blob/4d9db5f0/helix-ui/src/main/resources/org/apache/helix/ui/view/cluster-view.ftl
----------------------------------------------------------------------
diff --git a/helix-ui/src/main/resources/org/apache/helix/ui/view/cluster-view.ftl b/helix-ui/src/main/resources/org/apache/helix/ui/view/cluster-view.ftl
index 602df96..8a51596 100644
--- a/helix-ui/src/main/resources/org/apache/helix/ui/view/cluster-view.ftl
+++ b/helix-ui/src/main/resources/org/apache/helix/ui/view/cluster-view.ftl
@@ -36,6 +36,10 @@ under the License.
                     <#if (activeValid)>
                         <h1>${activeCluster}</h1>
 
+                        <p>
+                            <em>Controller leader is ${controllerLeader}</em>
+                        </p>
+
                         <ul id="switcher-tabs" class="uk-subnav uk-subnav-pill" data-uk-switcher="{connect: '#switcher'}">
                             <li><a href="">Resources</a></li>
                             <li><a href="">Instances</a></li>

http://git-wip-us.apache.org/repos/asf/helix/blob/4d9db5f0/helix-ui/src/main/resources/org/apache/helix/ui/view/common/instance-table.ftl
----------------------------------------------------------------------
diff --git a/helix-ui/src/main/resources/org/apache/helix/ui/view/common/instance-table.ftl b/helix-ui/src/main/resources/org/apache/helix/ui/view/common/instance-table.ftl
index ac979a0..c2a4b18 100644
--- a/helix-ui/src/main/resources/org/apache/helix/ui/view/common/instance-table.ftl
+++ b/helix-ui/src/main/resources/org/apache/helix/ui/view/common/instance-table.ftl
@@ -29,6 +29,7 @@ under the License.
                 <th>Instance</th>
                 <th>Enabled</th>
                 <th>Live</th>
+                <th>Tags</th>
                 <#if (adminMode)>
                     <th></th>
                 </#if>
@@ -40,6 +41,7 @@ under the License.
                     <td>${instanceSpec.instanceName}</td>
                     <td>${instanceSpec.enabled?string("Yes", "No")}</td>
                     <td>${instanceSpec.live?string("Yes", "No")}</td>
+                    <td>${instanceSpec.tags?join(", ")}</td>
                     <#if (adminMode)>
                         <td class="table-button">
                             <div class="uk-button-group">

http://git-wip-us.apache.org/repos/asf/helix/blob/4d9db5f0/helix-ui/src/main/resources/org/apache/helix/ui/view/common/resource-state-table.ftl
----------------------------------------------------------------------
diff --git a/helix-ui/src/main/resources/org/apache/helix/ui/view/common/resource-state-table.ftl b/helix-ui/src/main/resources/org/apache/helix/ui/view/common/resource-state-table.ftl
index fc5db04..7fbe506 100644
--- a/helix-ui/src/main/resources/org/apache/helix/ui/view/common/resource-state-table.ftl
+++ b/helix-ui/src/main/resources/org/apache/helix/ui/view/common/resource-state-table.ftl
@@ -41,7 +41,7 @@ under the License.
         </thead>
         <tbody>
             <#list resourceStateTable as row>
-                <tr class="${(row.ideal == row.external)?string("state-ok", "state-warn")} ${(row.external == "ERROR")?string("state-error", "")}">
+                <tr class='${(row.ideal == row.external || row.ideal == "N/A")?string("state-ok", "state-warn")} ${(row.external == "ERROR")?string("state-error", "")}'>
                     <td>${row.partitionName}</td>
                     <td>${row.instanceName}</td>
                     <td>${row.ideal}</td>
@@ -50,4 +50,15 @@ under the License.
             </#list>
         </tbody>
     </table>
+
+    <form class="uk-form" id="safe-resource-actions">
+        <button class="uk-button uk-button-danger"
+                type="button"
+                id="reset-resource"
+                zk-address="${zkAddress}"
+                cluster="${activeCluster}"
+                resource="${activeResource}">
+            Reset
+        </button>
+    </form>
 </#if>

http://git-wip-us.apache.org/repos/asf/helix/blob/4d9db5f0/helix-ui/src/main/resources/org/apache/helix/ui/view/landing-view.ftl
----------------------------------------------------------------------
diff --git a/helix-ui/src/main/resources/org/apache/helix/ui/view/landing-view.ftl b/helix-ui/src/main/resources/org/apache/helix/ui/view/landing-view.ftl
index 13f91aa..14309ca 100644
--- a/helix-ui/src/main/resources/org/apache/helix/ui/view/landing-view.ftl
+++ b/helix-ui/src/main/resources/org/apache/helix/ui/view/landing-view.ftl
@@ -28,8 +28,31 @@ under the License.
         <div id="landing-area">
             <img src="/assets/img/helix-logo.png">
             <form id="landing-form" class="uk-form">
-                <input id="zk-address" type="text" placeholder="ZooKeeper Address (e.g. localhost:2181)"/>
-                <button id="landing-form-button" class="uk-button">Go</button>
+                <div class="uk-form-row">
+                    <input id="zk-address"
+                           type="text"
+                           placeholder="ZooKeeper Address (e.g. localhost:2181)"
+                           class="uk-form-width-large"/>
+                </div>
+                <#if zkAliases?has_content>
+                    <div class="uk-form-row">
+                        OR
+                    </div>
+                    <div class="uk-form-row">
+                        <select id="zk-alias"
+                                class="uk-form-width-large">
+                            <#list zkAliases as alias>
+                                <option value="${alias}">${alias}</option>
+                            </#list>
+                        </select>
+                    </div>
+                </#if>
+                <div class="uk-form-row">
+                    <button id="landing-form-button"
+                            class="uk-button uk-form-width-large">
+                        Go
+                    </button>
+                </div>
             </form>
         </div>