diff --git a/solr/core/src/java/org/apache/solr/servlet/ZookeeperInfoServlet.java b/solr/core/src/java/org/apache/solr/servlet/ZookeeperInfoServlet.java index 30aa9cddbc0..8b9a24dfd2b 100644 --- a/solr/core/src/java/org/apache/solr/servlet/ZookeeperInfoServlet.java +++ b/solr/core/src/java/org/apache/solr/servlet/ZookeeperInfoServlet.java @@ -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); - - @Override - public void init() { + + // 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 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 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 applyNameFilter(List 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 filtered = new ArrayList(); + 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 collectionState, Set 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 shards = (Map)collectionState.get("shards"); + for (String shardId : shards.keySet()) { + boolean hasActive = false; + Map shard = (Map)shards.get(shardId); + Map replicas = (Map)shard.get("replicas"); + for (String replicaId : replicas.keySet()) { + Map replicaState = (Map)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(); + } + + } + + /** + * 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, OnReconnect { + + // this is the full merged list of collections from ZooKeeper + private List 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 getCollections(SolrZkClient zkClient) throws KeeperException, InterruptedException { + if (cachedCollections == null) { + // cache is stale, rebuild the full list ... + cachedCollections = new ArrayList(); + + List 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 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 extends HttpServlet { 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 extends HttpServlet { 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 extends HttpServlet { 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 extends HttpServlet { 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 extends HttpServlet { 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 extends HttpServlet { 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 extends HttpServlet { @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 extends HttpServlet { dataStrErr = "data is not parsable as a utf8 String: " + e.toString(); } } - // pull in external collections too - if (ZkStateReader.CLUSTER_STATE.equals(path) && !isTreeView) { - SortedMap collectionStates = null; - List 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 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); - } - - 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)ObjectBuilder.fromJSON(dataStr)); - } - - // now add in the external collections - Map extColl = (Map)ObjectBuilder.fromJSON(childDataStr); - collectionStates.put(collection, extColl.get(collection)); + clusterstateJsonMap = (Map) ObjectBuilder.fromJSON(dataStr); + } catch (Exception e) { + throw new SolrException(ErrorCode.SERVER_ERROR, + "Failed to parse /clusterstate.json from ZooKeeper due to: " + e, e); } + } 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 matchesStatusFilter = applyStatusFilter ? new ArrayList() : null; + Set liveNodes = applyStatusFilter ? + zkController.getZkStateReader().getClusterState().getLiveNodes() : null; + + SortedMap collectionStates = new TreeMap(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)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); + } + + if (childDataStr != null) { + Map extColl = (Map)ObjectBuilder.fromJSON(childDataStr); + collectionState = extColl.get(collection); + + if (applyStatusFilter) { + // verify this collection matches the filtered state + if (page.matchesStatusFilter((Map)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 map = new TreeMap(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 extends HttpServlet { 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()); diff --git a/solr/webapp/web/css/styles/cloud.css b/solr/webapp/web/css/styles/cloud.css index ac275298b38..65b58157f87 100644 --- a/solr/webapp/web/css/styles/cloud.css +++ b/solr/webapp/web/css/styles/cloud.css @@ -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 diff --git a/solr/webapp/web/js/scripts/cloud.js b/solr/webapp/web/js/scripts/cloud.js index d78cb459939..57980059535 100644 --- a/solr/webapp/web/js/scripts/cloud.js +++ b/solr/webapp/web/js/scripts/cloud.js @@ -350,6 +350,106 @@ var generate_rgraph = function( graph_element, graph_data, leaf_count ) ); }; +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_element, callback ) 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 : [] - }; + { + prepare_graph_data(response, graph_element, live_nodes, callback) - 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 ); + if (response.znode && response.znode.paging) { + var parr = response.znode.paging.split('|'); + if (parr.length < 3) { + $( '#cloudGraphPaging' ).hide(); + return; } + + 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] : ''; - 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 ); + 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 ) ); } diff --git a/solr/webapp/web/tpl/cloud.html b/solr/webapp/web/tpl/cloud.html index 377124926a7..3ce78a3f6c9 100644 --- a/solr/webapp/web/tpl/cloud.html +++ b/solr/webapp/web/tpl/cloud.html @@ -50,6 +50,28 @@ limitations under the License. +
+
+ + +   + Filter by:   + +   + Show per page. + +
+
+ @@ -62,4 +84,4 @@ limitations under the License.

   
 
-
\ No newline at end of file
+