SOLR-15135: Use DocCollection to generate state.json format expected by UI to work with perReplicaState collections (backport to 8x) (#2384) (#2385)

diff --git a/solr/CHANGES.txt b/solr/CHANGES.txt
index 7831efd..233cdf6 100644
--- a/solr/CHANGES.txt
+++ b/solr/CHANGES.txt
@@ -20,6 +20,8 @@
 * SOLR-15138: Collection creation for PerReplicaStates does not scale to large collections as well as regular collections
   (Mike Drob, Ilan Ginzburg, noble, Ishan Chattopadhyaya)
 
+* SOLR-15135: Admin UI display collections with perReplicaState=true in graph view (Timothy Potter)
+
 ==================  8.8.0 ==================
 
 Consult the LUCENE_CHANGES.txt file for additional, low level, changes in this release.
diff --git a/solr/core/src/java/org/apache/solr/handler/admin/ZookeeperInfoHandler.java b/solr/core/src/java/org/apache/solr/handler/admin/ZookeeperInfoHandler.java
index d6afc61..3396e8b 100644
--- a/solr/core/src/java/org/apache/solr/handler/admin/ZookeeperInfoHandler.java
+++ b/solr/core/src/java/org/apache/solr/handler/admin/ZookeeperInfoHandler.java
@@ -42,6 +42,8 @@
 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.ClusterState;
+import org.apache.solr.common.cloud.DocCollection;
 import org.apache.solr.common.cloud.OnReconnect;
 import org.apache.solr.common.cloud.Replica;
 import org.apache.solr.common.cloud.SolrZkClient;
@@ -661,9 +663,9 @@
           // 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;
+          List<String> matchesStatusFilter = applyStatusFilter ? new ArrayList<>() : null;
+          ClusterState cs = zkController.getZkStateReader().getClusterState();
+          Set<String> liveNodes = applyStatusFilter ? cs.getLiveNodes() : null;
 
           SortedMap<String, Object> collectionStates = new TreeMap<String, Object>(pagingSupport);
           for (String collection : page.selected) {
@@ -680,24 +682,11 @@
                 collectionStates.put(collection, ClusterStatus.postProcessCollectionJSON((Map<String, Object>) 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 {} not found in /clusterstate.json or /collections/{}/state.json!"
-                    , collection, collection);
-              } catch (Exception childErr) {
-                log.error("Failed to get {} due to", collStatePath, childErr);
-              }
-
-              if (childDataStr != null) {
-                Map<String, Object> extColl = (Map<String, Object>) Utils.fromJSONString(childDataStr);
-                collectionState = extColl.get(collection);
-
+              // looks like an external collection ... just use DocCollection instead of reading ZK data directly to work with per replica states
+              DocCollection dc = cs.getCollectionOrNull(collection);
+              if (dc != null) {
+                // TODO: for collections with perReplicaState, a ser/deser to JSON was needed to get the state to render correctly for the UI?
+                collectionState = dc.isPerReplicaState() ? Utils.fromJSONString(Utils.toJSONString(dc)) : dc.getProperties();
                 if (applyStatusFilter) {
                   // verify this collection matches the filtered state
                   if (page.matchesStatusFilter((Map<String, Object>) collectionState, liveNodes)) {