You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@lucene.apache.org by th...@apache.org on 2014/12/30 23:38:51 UTC

svn commit: r1648621 - in /lucene/dev/trunk/solr: core/src/java/org/apache/solr/servlet/ZookeeperInfoServlet.java webapp/web/css/styles/cloud.css webapp/web/js/scripts/cloud.js webapp/web/tpl/cloud.html

Author: thelabdude
Date: Tue Dec 30 22:38:50 2014
New Revision: 1648621

URL: http://svn.apache.org/r1648621
Log:
SOLR-5810: basic paged nav support for cloud graph panel for many collections.

Modified:
    lucene/dev/trunk/solr/core/src/java/org/apache/solr/servlet/ZookeeperInfoServlet.java
    lucene/dev/trunk/solr/webapp/web/css/styles/cloud.css
    lucene/dev/trunk/solr/webapp/web/js/scripts/cloud.js
    lucene/dev/trunk/solr/webapp/web/tpl/cloud.html

Modified: lucene/dev/trunk/solr/core/src/java/org/apache/solr/servlet/ZookeeperInfoServlet.java
URL: http://svn.apache.org/viewvc/lucene/dev/trunk/solr/core/src/java/org/apache/solr/servlet/ZookeeperInfoServlet.java?rev=1648621&r1=1648620&r2=1648621&view=diff
==============================================================================
--- lucene/dev/trunk/solr/core/src/java/org/apache/solr/servlet/ZookeeperInfoServlet.java (original)
+++ lucene/dev/trunk/solr/core/src/java/org/apache/solr/servlet/ZookeeperInfoServlet.java Tue Dec 30 22:38:50 2014
@@ -22,26 +22,36 @@ import java.io.OutputStreamWriter;
 import java.io.Writer;
 import java.net.URLEncoder;
 import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
 import java.util.Date;
 import java.util.List;
+import java.util.Locale;
 import java.util.Map;
+import java.util.Set;
 import java.util.SortedMap;
 import java.util.TreeMap;
+import java.util.regex.Pattern;
+import java.util.regex.Matcher;
 
 import javax.servlet.ServletException;
-import javax.servlet.http.HttpServlet;
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
 
 import org.apache.lucene.util.BytesRef;
 import org.apache.solr.cloud.ZkController;
 import org.apache.solr.common.SolrException;
+import org.apache.solr.common.SolrException.ErrorCode;
+import org.apache.solr.common.cloud.OnReconnect;
 import org.apache.solr.common.cloud.SolrZkClient;
-import org.apache.solr.common.cloud.ZkStateReader;
+import org.apache.solr.common.cloud.ZkNodeProps;
 import org.apache.solr.common.params.SolrParams;
 import org.apache.solr.core.CoreContainer;
 import org.apache.solr.util.FastWriter;
 import org.apache.zookeeper.KeeperException;
+import org.apache.zookeeper.WatchedEvent;
+import org.apache.zookeeper.Watcher;
 import org.apache.zookeeper.data.Stat;
 import org.noggit.CharArr;
 import org.noggit.JSONWriter;
@@ -56,12 +66,259 @@ import org.slf4j.LoggerFactory;
  *
  * @since solr 4.0
  */
-public final class ZookeeperInfoServlet extends HttpServlet {
+public final class ZookeeperInfoServlet extends BaseSolrServlet {
   static final Logger log = LoggerFactory.getLogger(ZookeeperInfoServlet.class);
+  
+  // used for custom sorting collection names looking like prefix##
+  // only go out to 7 digits (which safely fits in an int)
+  private static final Pattern endsWithDigits = Pattern.compile("^(\\D*)(\\d{1,7}?)$");
+  
+  /**
+   * Enumeration of ways to filter collections on the graph panel.
+   */
+  static enum FilterType {
+    none, name, status  
+  }
+  
+  /**
+   * Holds state of a single page of collections requested from the cloud panel.
+   */
+  static final class PageOfCollections {
+    List<String> selected;
+    int numFound = 0; // total number of matches (across all pages)
+    int start = 0;
+    int rows = -1;
+    FilterType filterType;
+    String filter;
+    
+    PageOfCollections(int start, int rows, FilterType filterType, String filter) {
+      this.start = start;
+      this.rows = rows;
+      this.filterType = filterType;
+      this.filter = filter;
+    }
+    
+    void selectPage(List<String> collections) {
+      numFound = collections.size();
+      // start with full set and then find the sublist for the desired selected
+      selected = collections;
+                  
+      if (rows > 0) { // paging desired
+        if (start > numFound)
+          start = 0; // this might happen if they applied a new filter
+        
+        int lastIndex = Math.min(start+rows, numFound);        
+        if (start > 0 || lastIndex < numFound)
+          selected = collections.subList(start, lastIndex);
+      }     
+    }
+                
+    /**
+     * Filters a list of collections by name if applicable. 
+     */
+    List<String> applyNameFilter(List<String> collections) {
+      
+      if (filterType != FilterType.name || filter == null)
+        return collections; // name filter doesn't apply
+            
+      // typically, a user will type a prefix and then *, e.g. tj*
+      // when they really mean tj.*
+      String regexFilter = (!filter.endsWith(".*") && filter.endsWith("*")) 
+          ? filter.substring(0,filter.length()-1)+".*" : filter; 
+      
+      // case-insensitive
+      if (!regexFilter.startsWith("(?i)"))
+        regexFilter = "(?i)"+regexFilter;
+      
+      Pattern filterRegex = Pattern.compile(regexFilter);        
+      List<String> filtered = new ArrayList<String>();
+      for (String next : collections) {
+        if (matches(filterRegex, next))
+          filtered.add(next);
+      }
+      
+      return filtered;
+    }
+    
+    /**
+     * Walk the collection state JSON object to see if it has any replicas that match
+     * the state the user is filtering by. 
+     */
+    @SuppressWarnings("unchecked")
+    final boolean matchesStatusFilter(Map<String,Object> collectionState, Set<String> liveNodes) {
+      
+      if (filterType != FilterType.status || filter == null || filter.length() == 0)
+        return true; // no status filter, so all match
+      
+      boolean isHealthy = true; // means all replicas for all shards active
+      boolean hasDownedShard = false; // means one or more shards is down
+      boolean replicaInRecovery = false;
+      
+      Map<String,Object> shards = (Map<String,Object>)collectionState.get("shards");
+      for (String shardId : shards.keySet()) {
+        boolean hasActive = false;
+        Map<String,Object> shard = (Map<String,Object>)shards.get(shardId);
+        Map<String,Object> replicas = (Map<String,Object>)shard.get("replicas");
+        for (String replicaId : replicas.keySet()) {
+          Map<String,Object> replicaState = (Map<String,Object>)replicas.get(replicaId);
+          String coreState = (String)replicaState.get("state");
+          String nodeName = (String)replicaState.get("node_name");
+          
+          // state can lie to you if the node is offline, so need to reconcile with live_nodes too
+          if (!liveNodes.contains(nodeName))
+            coreState = "down"; // not on a live node, so must be down
+          
+          if ("active".equals(coreState)) {
+            hasActive = true; // assumed no replicas active and found one that is for this shard
+          } else {
+            if ("recovering".equals(coreState)) {
+              replicaInRecovery = true;
+            }
+            isHealthy = false; // assumed healthy and found one replica that is not
+          }          
+        }
+        
+        if (!hasActive)
+          hasDownedShard = true; // this is bad
+      }
+      
+      if ("healthy".equals(filter)) {
+        return isHealthy;
+      } else if ("degraded".equals(filter)) {
+        return !hasDownedShard && !isHealthy; // means no shards offline but not 100% healthy either
+      } else if ("downed_shard".equals(filter)) {
+        return hasDownedShard;
+      } else if ("recovering".equals(filter)) {
+        return !isHealthy && replicaInRecovery;
+      }
+      
+      return true;
+    }
+    
+    final boolean matches(final Pattern filter, final String collName) {
+      return filter.matcher(collName).matches();
+    }
+    
+    String getPagingHeader() {
+      return start+"|"+rows+"|"+numFound+"|"+(filterType != null ? filterType.toString() : "")+"|"+(filter != null ? filter : "");
+    }
+    
+    public String toString() {
+      return getPagingHeader();
+    }
 
-  @Override
-  public void init() {
   }
+  
+  /**
+   * Supports paged navigation of collections on the cloud panel. To avoid serving
+   * stale collection data, this object watches the /collections znode, which will
+   * change if a collection is added or removed.
+   */
+  static final class PagedCollectionSupport implements Watcher, Comparator<String>, OnReconnect {
+
+    // this is the full merged list of collections from ZooKeeper
+    private List<String> cachedCollections;
+
+    /**
+     * If the list of collections changes, mark the cache as stale.
+     */
+    @Override
+    public void process(WatchedEvent event) {
+      synchronized (this) {
+        cachedCollections = null;
+      }
+    }
+    
+    /**
+     * Create a merged view of all collections (internal from /clusterstate.json and external from /collections/?/state.json 
+     */
+    private synchronized List<String> getCollections(SolrZkClient zkClient) throws KeeperException, InterruptedException {
+      if (cachedCollections == null) {
+        // cache is stale, rebuild the full list ...
+        cachedCollections = new ArrayList<String>();
+        
+        List<String> fromZk = zkClient.getChildren("/collections", this, true);
+        if (fromZk != null)
+          cachedCollections.addAll(fromZk);
+                
+        // sort the final merged set of collections
+        Collections.sort(cachedCollections, this);
+      }
+      
+      return cachedCollections;
+    }
+                
+    /**
+     * Gets the requested page of collections after applying filters and offsets. 
+     */
+    public PageOfCollections fetchPage(PageOfCollections page, SolrZkClient zkClient) 
+        throws KeeperException, InterruptedException {
+
+
+      List<String> children = getCollections(zkClient);
+      page.selected = children; // start with the page being the full list
+      
+      // activate paging (if disabled) for large collection sets
+      if (page.start == 0 && page.rows == -1 && page.filter == null && children.size() > 10) {
+        page.rows = 20;
+        page.start = 0;
+      }
+      
+      // apply the name filter if supplied (we don't need to pull state
+      // data from ZK to do name filtering
+      if (page.filterType == FilterType.name && page.filter != null)
+        children = page.applyNameFilter(children);
+
+      // a little hacky ... we can't select the page when filtering by
+      // status until reading all status objects from ZK
+      if (page.filterType != FilterType.status)
+        page.selectPage(children);        
+      
+      return page;
+    }
+        
+    @Override
+    public int compare(String left, String right) {
+      if (left == null)
+        return -1;
+      
+      if (left.equals(right))
+        return 0;
+      
+      // sort lexically unless the two collection names start with the same base prefix
+      // and end in a number (which is a common enough naming scheme to have direct 
+      // support for it)
+      Matcher leftMatcher = endsWithDigits.matcher(left);
+      if (leftMatcher.matches()) {
+        Matcher rightMatcher = endsWithDigits.matcher(right);
+        if (rightMatcher.matches()) {
+          String leftGroup1 = leftMatcher.group(1);
+          String rightGroup1 = rightMatcher.group(1);
+          if (leftGroup1.equals(rightGroup1)) {
+            // both start with the same prefix ... compare indexes
+            // using longs here as we don't know how long the 2nd group is
+            int leftGroup2 = Integer.parseInt(leftMatcher.group(2));
+            int rightGroup2 = Integer.parseInt(rightMatcher.group(2));            
+            return (leftGroup2 > rightGroup2) ? 1 : ((leftGroup2 == rightGroup2) ? 0 : -1);
+          }
+        }
+      }
+      return left.compareTo(right);
+    }
+
+    /**
+     * Called after a ZooKeeper session expiration occurs
+     */
+    @Override
+    public void command() {
+      // we need to re-establish the watcher on the collections list after session expires
+      synchronized (this) {
+        cachedCollections = null;
+      }
+    }
+  }
+  
+  private PagedCollectionSupport pagingSupport;
 
   @Override
   public void doGet(HttpServletRequest request,
@@ -73,6 +330,17 @@ public final class ZookeeperInfoServlet
       throw new ServletException("Missing request attribute org.apache.solr.CoreContainer.");
     }
 
+    synchronized (this) {
+      if (pagingSupport == null) {
+        pagingSupport = new PagedCollectionSupport();
+        ZkController zkController = cores.getZkController();
+        if (zkController != null) {
+          // get notified when the ZK session expires (so we can clear the cached collections and rebuild)
+          zkController.addOnReconnectListener(pagingSupport);
+        }
+      }
+    }
+    
     final SolrParams params;
     try {
       params = SolrRequestParsers.DEFAULT.parse(null, request.getServletPath(), request).getParams();
@@ -97,7 +365,25 @@ public final class ZookeeperInfoServlet
 
     String dumpS = params.get("dump");
     boolean dump = dumpS != null && dumpS.equals("true");
-
+    
+    int start = paramAsInt("start", params, 0);
+    int rows = paramAsInt("rows", params, -1);
+    
+    String filterType = params.get("filterType");
+    if (filterType != null) {
+      filterType = filterType.trim().toLowerCase(Locale.ROOT);
+      if (filterType.length() == 0)
+        filterType = null;
+    }
+    FilterType type = (filterType != null) ? FilterType.valueOf(filterType) : FilterType.none;
+    
+    String filter = (type != FilterType.none) ? params.get("filter") : null;
+    if (filter != null) {
+      filter = filter.trim();
+      if (filter.length() == 0)
+        filter = null;
+    }
+    
     response.setCharacterEncoding("UTF-8");
     response.setContentType("application/json");
 
@@ -106,14 +392,17 @@ public final class ZookeeperInfoServlet
     ZKPrinter printer = new ZKPrinter(response, out, cores.getZkController(), addr);
     printer.detail = detail;
     printer.dump = dump;
-    printer.isTreeView = (params.get("wt") == null); // this is hacky but tree view requests don't come in with the wt set
+    boolean isGraphView = "graph".equals(params.get("view"));
+    printer.page = (isGraphView && "/clusterstate.json".equals(path))
+        ? new PageOfCollections(start, rows, type, filter) : null;
+    printer.pagingSupport = pagingSupport;
 
     try {
       printer.print(path);
     } finally {
       printer.close();
     }
-
+    
     out.flush();
   }
 
@@ -124,6 +413,21 @@ public final class ZookeeperInfoServlet
     doGet(request, response);
   }
 
+  protected int paramAsInt(final String paramName, final SolrParams params, final int defaultVal) {
+    int val = defaultVal;
+    String paramS = params.get(paramName);
+    if (paramS != null) {
+      String trimmed = paramS.trim();
+      if (trimmed.length() > 0) {
+        try {
+          val = Integer.parseInt(trimmed);
+        } catch (NumberFormatException nfe) {
+          log.warn("Invalid value "+paramS+" passed for parameter "+paramName+"; expected integer!");
+        }        
+      }
+    }
+    return val;
+  }
 
   //--------------------------------------------------------------------------------------
   //
@@ -136,9 +440,7 @@ public final class ZookeeperInfoServlet
     boolean fullpath = FULLPATH_DEFAULT;
     boolean detail = false;
     boolean dump = false;
-
-    boolean isTreeView = false;
-
+    
     String addr; // the address passed to us
     String keeperAddr; // the address we're connected to
 
@@ -150,8 +452,13 @@ public final class ZookeeperInfoServlet
 
     int level;
     int maxData = 95;
+    
+    PageOfCollections page;
+    PagedCollectionSupport pagingSupport;
+    ZkController zkController;
 
     public ZKPrinter(HttpServletResponse response, Writer out, ZkController controller, String addr) throws IOException {
+      this.zkController = controller;
       this.response = response;
       this.out = out;
       this.addr = addr;
@@ -370,13 +677,12 @@ public final class ZookeeperInfoServlet
 
     @SuppressWarnings("unchecked")
     boolean printZnode(JSONWriter json, String path) throws IOException {
-      try {
-        Stat stat = new Stat();
-        // Trickily, the call to zkClient.getData fills in the stat variable
-        byte[] data = zkClient.getData(path, null, stat, true);
-
+      try {     
         String dataStr = null;
         String dataStrErr = null;
+        Stat stat = new Stat();
+        // Trickily, the call to zkClient.getData fills in the stat variable
+        byte[] data = zkClient.getData(path, null, stat, true);          
         if (null != data) {
           try {
             dataStr = (new BytesRef(data)).utf8ToString();
@@ -384,41 +690,89 @@ public final class ZookeeperInfoServlet
             dataStrErr = "data is not parsable as a utf8 String: " + e.toString();
           }
         }
-        // pull in external collections too
-        if (ZkStateReader.CLUSTER_STATE.equals(path) && !isTreeView) {
-          SortedMap<String,Object> collectionStates = null;
-          List<String> children = zkClient.getChildren(ZkStateReader.COLLECTIONS_ZKNODE, null, true);
-          java.util.Collections.sort(children);
-          for (String collection : children) {
-            String collStatePath = ZkStateReader.getCollectionPath(collection);
-            String childDataStr = null;
+        // support paging of the collections graph view (in case there are many collections)
+        if (page != null) {
+          // we've already pulled the data for /clusterstate.json from ZooKeeper above,
+          // but it needs to be parsed into a map so we can lookup collection states before
+          // trying to find them in the /collections/?/state.json znode
+          Map<String,Object> clusterstateJsonMap = null;
+          if (dataStr != null) {
             try {
-              byte[] childData = zkClient.getData(collStatePath, null, null, true);
-              if (childData != null) {
-                childDataStr = (new BytesRef(childData)).utf8ToString();
-              }
-            } catch (KeeperException.NoNodeException nne) {
-              // safe to ignore
-            } catch (Exception childErr) {
-              log.error("Failed to get "+collStatePath+" due to: "+childErr);
+              clusterstateJsonMap = (Map<String, Object>) ObjectBuilder.fromJSON(dataStr);
+            } catch (Exception e) {
+              throw new SolrException(ErrorCode.SERVER_ERROR,
+                  "Failed to parse /clusterstate.json from ZooKeeper due to: " + e, e);
             }
-
-            if (childDataStr != null) {
-              if (collectionStates == null) {
-                // initialize lazily as there may not be any external collections
-                collectionStates = new TreeMap<>();
-
-                // add the internal collections
-                if (dataStr != null)
-                  collectionStates.putAll((Map<String,Object>)ObjectBuilder.fromJSON(dataStr));
+          } else {
+            clusterstateJsonMap = ZkNodeProps.makeMap();
+          }
+          
+          // fetch the requested page of collections and then retrieve the state for each 
+          page = pagingSupport.fetchPage(page, zkClient);
+          // keep track of how many collections match the filter
+          boolean applyStatusFilter = 
+              (page.filterType == FilterType.status && page.filter != null);
+          List<String> matchesStatusFilter = applyStatusFilter ? new ArrayList<String>() : null;           
+          Set<String> liveNodes = applyStatusFilter ? 
+              zkController.getZkStateReader().getClusterState().getLiveNodes() : null;
+          
+          SortedMap<String,Object> collectionStates = new TreeMap<String,Object>(pagingSupport);          
+          for (String collection : page.selected) {
+            Object collectionState = clusterstateJsonMap.get(collection);
+            if (collectionState != null) {              
+              // collection state was in /clusterstate.json
+              if (applyStatusFilter) {
+                // verify this collection matches the status filter
+                if (page.matchesStatusFilter((Map<String,Object>)collectionState,liveNodes)) {
+                  matchesStatusFilter.add(collection);
+                  collectionStates.put(collection, collectionState);
+                }
+              } else {
+                collectionStates.put(collection, collectionState);                
+              }              
+            } else {
+              // looks like an external collection ...
+              String collStatePath = String.format(Locale.ROOT, "/collections/%s/state.json", collection);
+              String childDataStr = null;
+              try {              
+                byte[] childData = zkClient.getData(collStatePath, null, null, true);
+                if (childData != null)
+                  childDataStr = (new BytesRef(childData)).utf8ToString();
+              } catch (KeeperException.NoNodeException nne) {
+                log.warn("State for collection "+collection+
+                    " not found in /clusterstate.json or /collections/"+collection+"/state.json!");
+              } catch (Exception childErr) {
+                log.error("Failed to get "+collStatePath+" due to: "+childErr);
               }
-
-              // now add in the external collections
-              Map<String,Object> extColl = (Map<String,Object>)ObjectBuilder.fromJSON(childDataStr);
-              collectionStates.put(collection, extColl.get(collection));
-            }
+              
+              if (childDataStr != null) {
+                Map<String,Object> extColl = (Map<String,Object>)ObjectBuilder.fromJSON(childDataStr);
+                collectionState = extColl.get(collection);
+                
+                if (applyStatusFilter) {
+                  // verify this collection matches the filtered state
+                  if (page.matchesStatusFilter((Map<String,Object>)collectionState,liveNodes)) {
+                    matchesStatusFilter.add(collection);
+                    collectionStates.put(collection, collectionState);
+                  }
+                } else {
+                  collectionStates.put(collection, collectionState);                
+                }              
+              }              
+            }            
           }
-
+          
+          if (applyStatusFilter) {
+            // update the paged navigation info after applying the status filter
+            page.selectPage(matchesStatusFilter);
+            
+            // rebuild the Map of state data
+            SortedMap<String,Object> map = new TreeMap<String,Object>(pagingSupport);                      
+            for (String next : page.selected)
+              map.put(next, collectionStates.get(next));
+            collectionStates = map;
+          }          
+          
           if (collectionStates != null) {
             CharArr out = new CharArr();
             new JSONWriter(out, 2).write(collectionStates);
@@ -455,6 +809,11 @@ public final class ZookeeperInfoServlet
         if (null != dataStr) {
           writeKeyValue(json, "data", dataStr, false);
         }
+
+        if (page != null) {
+          writeKeyValue(json, "paging", page.getPagingHeader(), false);
+        }
+
         json.endObject();
       } catch (KeeperException e) {
         writeError(500, e.toString());

Modified: lucene/dev/trunk/solr/webapp/web/css/styles/cloud.css
URL: http://svn.apache.org/viewvc/lucene/dev/trunk/solr/webapp/web/css/styles/cloud.css?rev=1648621&r1=1648620&r2=1648621&view=diff
==============================================================================
--- lucene/dev/trunk/solr/webapp/web/css/styles/cloud.css (original)
+++ lucene/dev/trunk/solr/webapp/web/css/styles/cloud.css Tue Dec 30 22:38:50 2014
@@ -401,3 +401,10 @@ limitations under the License.
 {
   stroke: #fff;
 }
+
+#cloudGraphPaging
+{
+  display: inline-block,
+  padding-top: 15px,
+  padding-bottom: 15px
+}
\ No newline at end of file

Modified: lucene/dev/trunk/solr/webapp/web/js/scripts/cloud.js
URL: http://svn.apache.org/viewvc/lucene/dev/trunk/solr/webapp/web/js/scripts/cloud.js?rev=1648621&r1=1648620&r2=1648621&view=diff
==============================================================================
--- lucene/dev/trunk/solr/webapp/web/js/scripts/cloud.js (original)
+++ lucene/dev/trunk/solr/webapp/web/js/scripts/cloud.js Tue Dec 30 22:38:50 2014
@@ -350,6 +350,106 @@ var generate_rgraph = function( graph_el
     );
 };
 
+var prepare_graph_data = function( response, graph_element, live_nodes, callback )
+{  
+    var state = null;
+    eval( 'state = ' + response.znode.data + ';' );
+    
+    var leaf_count = 0;
+    var graph_data = {
+      name: null,
+      children : []
+    };
+
+    for( var c in state )
+    {
+      var shards = [];
+      for( var s in state[c].shards )
+      {
+        var nodes = [];
+        for( var n in state[c].shards[s].replicas )
+        {
+          leaf_count++;
+          var replica = state[c].shards[s].replicas[n]
+
+          var uri = replica.base_url;
+          var parts = uri.match( /^(\w+:)\/\/(([\w\d\.-]+)(:(\d+))?)(.+)$/ );
+          var uri_parts = {
+            protocol: parts[1],
+            host: parts[2],
+            hostname: parts[3],
+            port: parseInt( parts[5] || 80, 10 ),
+            pathname: parts[6]
+          };
+          
+          helper_data.protocol.push( uri_parts.protocol );
+          helper_data.host.push( uri_parts.host );
+          helper_data.hostname.push( uri_parts.hostname );
+          helper_data.port.push( uri_parts.port );
+          helper_data.pathname.push( uri_parts.pathname );
+
+          var status = replica.state;
+
+          if( !live_nodes[replica.node_name] )
+          {
+            status = 'gone';
+          }
+
+          var node = {
+            name: uri,
+            data: {
+              type : 'node',
+              state : status,
+              leader : 'true' === replica.leader,
+              uri : uri_parts
+            }
+          };
+          nodes.push( node );
+        }
+
+        var shard = {
+          name: s,
+          data: {
+            type : 'shard'
+          },
+          children: nodes
+        };
+        shards.push( shard );
+      }
+
+      var collection = {
+        name: c,
+        data: {
+          type : 'collection'
+        },
+        children: shards
+      };
+      graph_data.children.push( collection );
+    }
+    
+    helper_data.protocol = $.unique( helper_data.protocol );
+    helper_data.host = $.unique( helper_data.host );
+    helper_data.hostname = $.unique( helper_data.hostname );
+    helper_data.port = $.unique( helper_data.port );
+    helper_data.pathname = $.unique( helper_data.pathname );
+
+    callback( graph_element, graph_data, leaf_count );  
+}
+
+var update_status_filter = function(filterType, filterVal) {
+  if (filterType == 'status') {
+    $( '#cloudGraphPagingStatusFilter' ).val(filterVal);
+    $( '#cloudGraphPagingStatusFilter' ).show();
+    $( '#cloudGraphPagingFilter' ).hide();
+    $( '#cloudGraphPagingFilter' ).val('');
+  } else {
+    $( '#cloudGraphPagingStatusFilter' ).hide();
+    $( '#cloudGraphPagingStatusFilter' ).val('');
+    $( '#cloudGraphPagingFilter' ).val(filterVal);
+    $( '#cloudGraphPagingFilter' ).show();                  
+  }  
+};
+
 var prepare_graph = function( graph_element, callback )
 {
   $.ajax
@@ -365,101 +465,82 @@ var prepare_graph = function( graph_elem
           live_nodes[response.tree[0].children[c].data.title] = true;
         }
 
+        var start = $( '#cloudGraphPagingStart' ).val();
+        var rows = $( '#cloudGraphPagingRows' ).val();
+        var clusterStateUrl = app.config.solr_path + '/zookeeper?wt=json&detail=true&path=%2Fclusterstate.json&view=graph';
+        if (start && rows)
+          clusterStateUrl += ('&start='+start+'&rows='+rows);
+        
+        var filterType = $( '#cloudGraphPagingFilterType' ).val();
+        if (filterType) {
+          var filter = (filterType == 'status')
+                         ? $( '#cloudGraphPagingStatusFilter' ).val() 
+                         : $( '#cloudGraphPagingFilter' ).val();  
+          if (filter)
+            clusterStateUrl += ('&filterType='+filterType+'&filter='+filter);
+        }
+                
         $.ajax
         (
           {
-            url : app.config.solr_path + '/zookeeper?wt=json&detail=true&path=%2Fclusterstate.json',
+            url : clusterStateUrl,
             dataType : 'json',
             context : graph_element,
             beforeSend : function( xhr, settings )
             {
-              this
-                .show();
+              this.show();
             },
             success : function( response, text_status, xhr )
-            {
-              var state = null;
-              eval( 'state = ' + response.znode.data + ';' );
-              
-              var leaf_count = 0;
-              var graph_data = {
-                name: null,
-                children : []
-              };
-
-              for( var c in state )
-              {
-                var shards = [];
-                for( var s in state[c].shards )
-                {
-                  var nodes = [];
-                  for( var n in state[c].shards[s].replicas )
-                  {
-                    leaf_count++;
-                    var replica = state[c].shards[s].replicas[n]
+            {              
+              prepare_graph_data(response, graph_element, live_nodes, callback)
 
-                    var uri = replica.base_url;
-                    var parts = uri.match( /^(\w+:)\/\/(([\w\d\.-]+)(:(\d+))?)(.+)$/ );
-                    var uri_parts = {
-                      protocol: parts[1],
-                      host: parts[2],
-                      hostname: parts[3],
-                      port: parseInt( parts[5] || 80, 10 ),
-                      pathname: parts[6]
-                    };
-                    
-                    helper_data.protocol.push( uri_parts.protocol );
-                    helper_data.host.push( uri_parts.host );
-                    helper_data.hostname.push( uri_parts.hostname );
-                    helper_data.port.push( uri_parts.port );
-                    helper_data.pathname.push( uri_parts.pathname );
-
-                    var status = replica.state;
-
-                    if( !live_nodes[replica.node_name] )
-                    {
-                      status = 'gone';
-                    }
-
-                    var node = {
-                      name: uri,
-                      data: {
-                        type : 'node',
-                        state : status,
-                        leader : 'true' === replica.leader,
-                        uri : uri_parts
-                      }
-                    };
-                    nodes.push( node );
-                  }
-
-                  var shard = {
-                    name: s,
-                    data: {
-                      type : 'shard'
-                    },
-                    children: nodes
-                  };
-                  shards.push( shard );
+              if (response.znode && response.znode.paging) {
+                var parr = response.znode.paging.split('|');
+                if (parr.length < 3) {
+                  $( '#cloudGraphPaging' ).hide();
+                  return;
                 }
-
-                var collection = {
-                  name: c,
-                  data: {
-                    type : 'collection'
-                  },
-                  children: shards
-                };
-                graph_data.children.push( collection );
-              }
-              
-              helper_data.protocol = $.unique( helper_data.protocol );
-              helper_data.host = $.unique( helper_data.host );
-              helper_data.hostname = $.unique( helper_data.hostname );
-              helper_data.port = $.unique( helper_data.port );
-              helper_data.pathname = $.unique( helper_data.pathname );
-
-              callback( graph_element, graph_data, leaf_count );
+                
+                var start = Math.max(parseInt(parr[0]),0);                  
+                var prevEnabled = (start > 0);
+                $('#cloudGraphPagingPrev').prop('disabled', !prevEnabled);
+                if (prevEnabled)
+                  $('#cloudGraphPagingPrev').show();                    
+                else
+                  $('#cloudGraphPagingPrev').hide();
+                
+                var rows = parseInt(parr[1])
+                var total = parseInt(parr[2])
+                $( '#cloudGraphPagingStart' ).val(start);
+                $( '#cloudGraphPagingRows' ).val(rows);
+                if (rows == -1)
+                  $( '#cloudGraphPaging' ).hide();
+                                  
+                var filterType = parr.length > 3 ? parr[3] : '';
+                if (filterType == '' || filterType == 'none') filterType = 'status';
+                
+                $( '#cloudGraphPagingFilterType' ).val(filterType);                  
+                var filter = parr.length > 4 ? parr[4] : '';
+
+                update_status_filter(filterType, filter);
+                
+                var page = Math.floor(start/rows)+1;
+                var pages = Math.ceil(total/rows);
+                var last = Math.min(start+rows,total);
+                var nextEnabled = (last < total);                  
+                $('#cloudGraphPagingNext').prop('disabled', !nextEnabled);
+                if (nextEnabled)
+                  $('#cloudGraphPagingNext').show();
+                else
+                  $('#cloudGraphPagingNext').hide();                    
+                
+                var status = (total > 0) 
+                               ? 'Collections '+(start+1)+' - '+last+' of '+total+'. ' 
+                               : 'No collections found.';
+                $( '#cloudGraphPagingStatus' ).html(status);
+              } else {
+                $( '#cloudGraphPaging' ).hide();
+              }            
             },
             error : function( xhr, text_status, error_thrown)
             {
@@ -662,6 +743,21 @@ var init_tree = function( tree_element )
   );
 };
 
+// updates the starting position for paged navigation
+// and then rebuilds the graph based on the selected page
+var update_start = function(direction, cloud_element) {
+  var start = $( '#cloudGraphPagingStart' ).val();
+  var rows = $( '#cloudGraphPagingRows' ).val();
+  var startAt = start ? parseInt(start) : 0;
+  var numRows = rows ? parseInt(rows) : 20;
+  var newStart = Math.max(startAt + (rows * direction),0); 
+  $( '#cloudGraphPagingStart' ).val(newStart);
+  
+  var graph_element = $( '#graph-content', cloud_element );
+  $( '#canvas', graph_element).empty();
+  init_graph( graph_element );  
+};
+
 // #/~cloud
 sammy.get
 (
@@ -704,6 +800,45 @@ sammy.get
             {
               $( this ).addClass( 'active' );
               init_graph( $( '#graph-content', cloud_element ) );
+              
+              $('#cloudGraphPagingNext').click(function() {
+                update_start(1, cloud_element);                  
+              });
+              
+              $('#cloudGraphPagingPrev').click(function() {
+                update_start(-1, cloud_element);                                    
+              });              
+
+              $('#cloudGraphPagingRows').change(function() {
+                var rows = $( this ).val();
+                if (!rows || rows == '')
+                  $( this ).val("20");
+                
+                // ? restart the start position when rows changes?
+                $( '#cloudGraphPagingStart' ).val(0);                  
+                update_start(-1, cloud_element);                
+              });              
+              
+              $('#cloudGraphPagingFilter').change(function() {
+                var filter = $( this ).val();
+                // reset the start position when the filter changes
+                $( '#cloudGraphPagingStart' ).val(0);
+                update_start(-1, cloud_element);
+              });
+
+              $( '#cloudGraphPagingStatusFilter' ).show();
+              $( '#cloudGraphPagingFilter' ).hide();
+              
+              $('#cloudGraphPagingFilterType').change(function() {
+                update_status_filter($( this ).val(), '');
+              });
+              
+              $('#cloudGraphPagingStatusFilter').change(function() {
+                // just reset the paged navigation controls based on this update
+                $( '#cloudGraphPagingStart' ).val(0);                  
+                update_start(-1, cloud_element);                                    
+              });
+              
             }
           );
 
@@ -714,6 +849,8 @@ sammy.get
             'activate',
             function( event )
             {
+              $( "#cloudGraphPaging" ).hide(); // TODO: paging for rgraph too
+              
               $( this ).addClass( 'active' );
               init_rgraph( $( '#graph-content', cloud_element ) );
             }

Modified: lucene/dev/trunk/solr/webapp/web/tpl/cloud.html
URL: http://svn.apache.org/viewvc/lucene/dev/trunk/solr/webapp/web/tpl/cloud.html?rev=1648621&r1=1648620&r2=1648621&view=diff
==============================================================================
--- lucene/dev/trunk/solr/webapp/web/tpl/cloud.html (original)
+++ lucene/dev/trunk/solr/webapp/web/tpl/cloud.html Tue Dec 30 22:38:50 2014
@@ -50,6 +50,28 @@ limitations under the License.
         </ul>
       </div>
 
+      <div style="width: 100%; text-align: center;">
+        <div id="cloudGraphPaging">
+         <button id="cloudGraphPagingPrev">&lt; Previous</button>
+         <input id="cloudGraphPagingStart" type="hidden" name="start" /> 
+         <span id="cloudGraphPagingStatus"></span>&nbsp;
+         Filter by:&nbsp;<select id="cloudGraphPagingFilterType">
+           <option value="status">Status</option>
+           <option value="name">Name</option>
+         </select>&nbsp;
+         <select id="cloudGraphPagingStatusFilter">
+           <option value=""> - Any - </option>
+           <option value="healthy">Healthy</option>
+           <option value="degraded">Degraded</option>
+           <option value="downed_shard">Downed Shard</option>
+           <option value="recovering">Replica in Recovery</option>
+         </select>         
+         <input id="cloudGraphPagingFilter" type="text" size="10" name="filter" />&nbsp;
+         Show <input id="cloudGraphPagingRows" type="text" size="2" name="rows" /> per page.
+         <button id="cloudGraphPagingNext">Next &gt;</button>
+        </div>
+      </div>
+
     </div>
 
   </div>
@@ -62,4 +84,4 @@ limitations under the License.
     <pre class="debug"></pre>
   </div>
 
-</div>
\ No newline at end of file
+</div>