This is an automated email from the ASF dual-hosted git repository.

epugh pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/solr.git


The following commit(s) were added to refs/heads/main by this push:
     new b1cbbbf1a1f Migrate Admin UI screens to v2 endpoints: Java Properties, 
Schema, and Cloud Zookeeper tree view (#4901)
b1cbbbf1a1f is described below

commit b1cbbbf1a1fd8cdd5b5989ae86fab1750cee318a
Author: Eric Pugh <[email protected]>
AuthorDate: Mon Sep 21 08:59:55 2026 -0400

    Migrate Admin UI screens to v2 endpoints: Java Properties, Schema, and 
Cloud Zookeeper tree view (#4901)
    
    Co-authored-by: Claude Sonnet 5 <[email protected]>
---
 .../migrate-zookeeper-tree-to-v2-api.yml           |   7 +
 .../api/model/ZooKeeperListChildrenResponse.java   |   5 +-
 .../solr/handler/admin/api/UpdateSchema.java       |  51 +++--
 solr/webapp/web/js/angular/controllers/cloud.js    |  97 +++++++--
 .../web/js/angular/controllers/java-properties.js  |  42 ++--
 solr/webapp/web/js/angular/controllers/schema.js   | 218 ++++++++++++++-------
 solr/webapp/web/js/angular/services.js             |  54 +++--
 solr/webapp/web/partials/schema.html               |   2 +-
 8 files changed, 340 insertions(+), 136 deletions(-)

diff --git a/changelog/unreleased/migrate-zookeeper-tree-to-v2-api.yml 
b/changelog/unreleased/migrate-zookeeper-tree-to-v2-api.yml
new file mode 100644
index 00000000000..087a14b6b6c
--- /dev/null
+++ b/changelog/unreleased/migrate-zookeeper-tree-to-v2-api.yml
@@ -0,0 +1,7 @@
+title: Admin UI's Cloud > Tree screen now uses the v2 Zookeeper read API. 
Fixed a bug in that API where the per-child stat map returned by GET 
/api/cluster/zookeeper/children was serialized twice.
+type: fixed
+authors:
+  - name: Eric Pugh
+links:
+  - name: PR#4901
+    url: https://github.com/apache/solr/pull/4901
diff --git 
a/solr/api/src/java/org/apache/solr/client/api/model/ZooKeeperListChildrenResponse.java
 
b/solr/api/src/java/org/apache/solr/client/api/model/ZooKeeperListChildrenResponse.java
index be7a69575dc..42135e18849 100644
--- 
a/solr/api/src/java/org/apache/solr/client/api/model/ZooKeeperListChildrenResponse.java
+++ 
b/solr/api/src/java/org/apache/solr/client/api/model/ZooKeeperListChildrenResponse.java
@@ -18,6 +18,7 @@ package org.apache.solr.client.api.model;
 
 import com.fasterxml.jackson.annotation.JsonAnyGetter;
 import com.fasterxml.jackson.annotation.JsonAnySetter;
+import com.fasterxml.jackson.annotation.JsonIgnore;
 import com.fasterxml.jackson.annotation.JsonProperty;
 import java.util.HashMap;
 import java.util.Map;
@@ -30,7 +31,9 @@ public class ZooKeeperListChildrenResponse extends 
ExperimentalResponse {
   //  object with only one key - the name of the root node - with separate 
objects under there for
   //  each child.  The additional nesting under the root node doesn't serve 
much purpose afaict
   //  and should be removed.
-  public Map<String, Map<String, ZooKeeperStat>> unknownFields = new 
HashMap<>();
+  // @JsonIgnore prevents this from ALSO being serialized as its own 
"unknownFields" property --
+  // @JsonAnyGetter below already flattens its entries directly onto the 
response.
+  @JsonIgnore public Map<String, Map<String, ZooKeeperStat>> unknownFields = 
new HashMap<>();
 
   @JsonAnyGetter
   public Map<String, Map<String, ZooKeeperStat>> unknownProperties() {
diff --git 
a/solr/core/src/java/org/apache/solr/handler/admin/api/UpdateSchema.java 
b/solr/core/src/java/org/apache/solr/handler/admin/api/UpdateSchema.java
index 6bb00ea00f6..55b66b4bed5 100644
--- a/solr/core/src/java/org/apache/solr/handler/admin/api/UpdateSchema.java
+++ b/solr/core/src/java/org/apache/solr/handler/admin/api/UpdateSchema.java
@@ -16,19 +16,24 @@
  */
 package org.apache.solr.handler.admin.api;
 
+import static org.apache.solr.common.util.CommandOperation.ERR_MSGS;
+
 import jakarta.inject.Inject;
 import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
 import org.apache.solr.api.JerseyResource;
 import org.apache.solr.client.api.endpoint.UpdateSchemaApi;
 import org.apache.solr.client.api.model.DeleteDynamicFieldOperation;
 import org.apache.solr.client.api.model.DeleteFieldOperation;
 import org.apache.solr.client.api.model.DeleteFieldTypeOperation;
-import org.apache.solr.client.api.model.ErrorInfo;
 import org.apache.solr.client.api.model.SchemaChange;
 import org.apache.solr.client.api.model.SolrJerseyResponse;
 import org.apache.solr.client.api.model.UpsertDynamicFieldOperation;
 import org.apache.solr.client.api.model.UpsertFieldOperation;
 import org.apache.solr.client.api.model.UpsertFieldTypeOperation;
+import org.apache.solr.common.SolrErrorWrappingException;
 import org.apache.solr.common.SolrException;
 import org.apache.solr.core.SolrCore;
 import org.apache.solr.handler.SolrConfigHandler;
@@ -60,7 +65,7 @@ public class UpdateSchema extends JerseyResource implements 
UpdateSchemaApi {
     requestBody.name = fieldName;
     requestBody.operationType = "upsert-field";
 
-    runWithSchemaManager(List.of(requestBody), response);
+    runWithSchemaManager(List.of(requestBody));
 
     return response;
   }
@@ -77,7 +82,7 @@ public class UpdateSchema extends JerseyResource implements 
UpdateSchemaApi {
     deleteFieldOp.operationType = "delete-field";
     deleteFieldOp.name = fieldName;
 
-    runWithSchemaManager(List.of(deleteFieldOp), response);
+    runWithSchemaManager(List.of(deleteFieldOp));
 
     return response;
   }
@@ -94,7 +99,7 @@ public class UpdateSchema extends JerseyResource implements 
UpdateSchemaApi {
     requestBody.name = dynamicFieldName;
     requestBody.operationType = "add-dynamic-field";
 
-    runWithSchemaManager(List.of(requestBody), response);
+    runWithSchemaManager(List.of(requestBody));
 
     return response;
   }
@@ -109,7 +114,7 @@ public class UpdateSchema extends JerseyResource implements 
UpdateSchemaApi {
     final var deleteDynamicFieldOp = new DeleteDynamicFieldOperation();
     deleteDynamicFieldOp.name = dynamicFieldName;
     deleteDynamicFieldOp.operationType = "delete-dynamic-field";
-    runWithSchemaManager(List.of(deleteDynamicFieldOp), response);
+    runWithSchemaManager(List.of(deleteDynamicFieldOp));
 
     return response;
   }
@@ -125,7 +130,7 @@ public class UpdateSchema extends JerseyResource implements 
UpdateSchemaApi {
     ensureRequiredParameterProvided("class", requestBody.propertyClass);
     requestBody.operationType = "add-field-type";
 
-    runWithSchemaManager(List.of(requestBody), response);
+    runWithSchemaManager(List.of(requestBody));
 
     return response;
   }
@@ -141,7 +146,7 @@ public class UpdateSchema extends JerseyResource implements 
UpdateSchemaApi {
     deleteFieldTypeOp.name = fieldTypeName;
     deleteFieldTypeOp.operationType = "delete-field-type";
 
-    runWithSchemaManager(List.of(deleteFieldTypeOp), response);
+    runWithSchemaManager(List.of(deleteFieldTypeOp));
 
     return response;
   }
@@ -154,7 +159,7 @@ public class UpdateSchema extends JerseyResource implements 
UpdateSchemaApi {
     ensureSchemaMutable();
     ensureRequiredRequestBodyProvided(requestBody);
 
-    runWithSchemaManager(requestBody, response);
+    runWithSchemaManager(requestBody);
 
     return response;
   }
@@ -167,13 +172,35 @@ public class UpdateSchema extends JerseyResource 
implements UpdateSchemaApi {
     }
   }
 
-  private void runWithSchemaManager(List<SchemaChange> operations, 
SolrJerseyResponse response)
-      throws Exception {
+  private void runWithSchemaManager(List<SchemaChange> operations) throws 
Exception {
     final var schemaManager = new SchemaManager(solrQueryRequest);
     final var errorDetails = schemaManager.performOperations(operations);
     if (errorDetails != null && !errorDetails.isEmpty()) {
-      response.error = new ErrorInfo();
-      response.error.details = errorDetails;
+      // Mirrors v1's SchemaHandler, so a validation failure gets a proper 
error status instead of
+      // reporting a "successful" response whose body happens to carry an 
error. The per-operation
+      // messages are folded into the exception's own message (surfaced as 
error.msg) so that field
+      // is informative on its own, per this API's error-reporting convention; 
error.details still
+      // carries the full per-operation breakdown for consumers that want it.
+      throw new SolrErrorWrappingException(
+          SolrException.ErrorCode.BAD_REQUEST, 
summarizeSchemaErrors(errorDetails), errorDetails);
     }
   }
+
+  private static String summarizeSchemaErrors(List<Map<String, Object>> 
errorDetails) {
+    final var messages =
+        errorDetails.stream()
+            .map(detail -> detail.get(ERR_MSGS))
+            .flatMap(
+                msgs -> {
+                  if (msgs instanceof List<?> msgList) {
+                    return msgList.stream();
+                  } else if (msgs != null) {
+                    return Stream.of(msgs);
+                  }
+                  return Stream.empty();
+                })
+            .map(String::valueOf)
+            .collect(Collectors.joining(" "));
+    return messages.isBlank() ? "error processing commands" : messages;
+  }
 }
diff --git a/solr/webapp/web/js/angular/controllers/cloud.js 
b/solr/webapp/web/js/angular/controllers/cloud.js
index fafaf02a2fa..2ce9a392d1c 100644
--- a/solr/webapp/web/js/angular/controllers/cloud.js
+++ b/solr/webapp/web/js/angular/controllers/cloud.js
@@ -16,7 +16,7 @@
 */
 
 solrAdminApp.controller('CloudController',
-    function($scope, $location, $timeout, Zookeeper, Constants, Collections, 
ClusterV2, SystemV2, Metrics, MetricsExtractor, ZookeeperStatus, 
ApiErrorHandler) {
+    function($scope, $location, $timeout, $q, Zookeeper, ZookeeperReadV2, 
Constants, Collections, ClusterV2, SystemV2, Metrics, MetricsExtractor, 
ZookeeperStatus, ApiErrorHandler) {
 
         $scope.showDebug = false;
 
@@ -31,7 +31,7 @@ solrAdminApp.controller('CloudController',
         var view = $location.search().view ? $location.search().view : "nodes";
         if (view === "tree") {
             $scope.resetMenu("cloud-tree", Constants.IS_ROOT_PAGE);
-            treeSubController($scope, Zookeeper);
+            treeSubController($scope, $q, ZookeeperReadV2);
         } else if (view === "graph") {
             $scope.resetMenu("cloud-graph", Constants.IS_ROOT_PAGE);
             graphSubController($scope, $timeout, Zookeeper, ClusterV2, 
ApiErrorHandler);
@@ -687,7 +687,25 @@ var zkStatusSubController = function($scope, 
ZookeeperStatus) {
     $scope.initZookeeper();
 };
 
-var treeSubController = function($scope, Zookeeper) {
+function zkStatToProp(stat) {
+    stat = stat || {};
+    var time = function(ms) { return new Date(ms) + " (" + ms + ")"; };
+    return {
+        version: stat.version,
+        aversion: stat.aversion,
+        children_count: stat.children,
+        ctime: time(stat.ctime),
+        cversion: stat.cversion,
+        czxid: stat.czxid,
+        ephemeralOwner: stat.ephemeralOwner,
+        mtime: time(stat.mtime),
+        mzxid: stat.mzxid,
+        pzxid: stat.pzxid,
+        dataLength: stat.dataLength
+    };
+}
+
+var treeSubController = function($scope, $q, ZookeeperReadV2) {
     $scope.showZkStatus = false;
     $scope.showTree = true;
     $scope.showGraph = false;
@@ -700,29 +718,72 @@ var treeSubController = function($scope, Zookeeper) {
           // TODO: Set proper data here to display a warning in right panel 
"You lack the required role to see this file"
           $scope.znode = {};
           $scope.showData = false;
-        } else {
-          Zookeeper.detail({path: path}, function(data) {
-              $scope.znode = data.znode;
-              if (data.znode.path.endsWith("/managed-schema") || 
data.znode.path.endsWith(".xml.bak")) {
-                $scope.lang = "xml";
-              } else {
-                var lastPathElement = data.znode.path.split( '/' ).pop();
-                var lastDotAt = lastPathElement ? 
lastPathElement.lastIndexOf('.') : -1;
-                $scope.lang = lastDotAt != -1 ? 
lastPathElement.substring(lastDotAt+1) : "txt";
-              }
-              $scope.showData = true;
-          });
+          return;
         }
+        $q.all([
+            ZookeeperReadV2.listNodes(path, {children: false}),
+            ZookeeperReadV2.readNode(path)
+        ]).then(function(results) {
+            $scope.znode = {
+                path: path,
+                prop: zkStatToProp(results[0].data.stat),
+                data: results[1].data
+            };
+            if (path.endsWith("/managed-schema") || path.endsWith(".xml.bak")) 
{
+              $scope.lang = "xml";
+            } else {
+              var lastPathElement = path.split( '/' ).pop();
+              var lastDotAt = lastPathElement ? 
lastPathElement.lastIndexOf('.') : -1;
+              $scope.lang = lastDotAt != -1 ? 
lastPathElement.substring(lastDotAt+1) : "txt";
+            }
+            $scope.showData = true;
+        });
+        // A failure here surfaces through the global httpInterceptor (unlike 
the superagent-based
+        // generated clients, plain $http calls are already covered by it), so 
no local handling
+        // is needed beyond leaving showData/znode unchanged.
     };
 
     $scope.hideData = function() {
         $scope.showData = false;
     };
 
+    // Recursively walks the whole ZK namespace client-side and builds the 
same nested array
+    // jstree's static "data" expects, mirroring v1's admin/zookeeper (which 
did this same walk
+    // server-side in one call) -- v2's listNodes only returns one level of 
children at a time.
+    function buildZkSubtree(path) {
+        return ZookeeperReadV2.listNodes(path, {}).then(function(response) {
+            // The per-child stat map is flattened onto the JSON body under 
the requested path
+            // (server-side @JsonAnyGetter), not nested under a well-known 
property.
+            var childStats = (response.data && response.data[path]) || {};
+            var names = Object.keys(childStats).sort();
+            return $q.all(names.map(function(name) {
+                var childPath = (path === '/' ? '' : path) + '/' + name;
+                var node = {
+                    text: name,
+                    a_attr: {href: "admin/zookeeper?detail=true&path=" + 
encodeURIComponent(childPath)}
+                };
+                var stat = childStats[name];
+                if (stat && stat.children > 0) {
+                    return buildZkSubtree(childPath).then(function(children) {
+                        node.children = children;
+                        return node;
+                    });
+                }
+                return node;
+            }));
+        });
+    }
+
     $scope.initTree = function() {
-      Zookeeper.simple(function(data) {
-        $scope.tree = data.tree;
-      });
+        buildZkSubtree('/').then(function(children) {
+            $scope.tree = [{
+                text: '/',
+                a_attr: {href: "admin/zookeeper?detail=true&path=%2F"},
+                children: children
+            }];
+        });
+        // A failure here surfaces through the global httpInterceptor, same as 
any other plain
+        // $http call.
     };
 
     $scope.initTree();
diff --git a/solr/webapp/web/js/angular/controllers/java-properties.js 
b/solr/webapp/web/js/angular/controllers/java-properties.js
index 817f98a7aa8..92c04b13f56 100644
--- a/solr/webapp/web/js/angular/controllers/java-properties.js
+++ b/solr/webapp/web/js/angular/controllers/java-properties.js
@@ -16,28 +16,32 @@
 */
 
 solrAdminApp.controller('JavaPropertiesController',
-  function($scope, Properties, Constants){
+  function($scope, $timeout, NodeV2, Constants, ApiErrorHandler){
     $scope.resetMenu("java-props", Constants.IS_ROOT_PAGE);
     $scope.refresh = function() {
-      Properties.get(function(data) {
-        var sysprops = data["system.properties"];
-        var sep = sysprops["path.separator"]
-        var props = [];
-        for (var key in sysprops) {
-          var value = sysprops[key];
-          var values = value.split(sep);
-          if (value === sep) {
-            values = [':'];
+      NodeV2.getNodeProperties(function(error, data, response) {
+        $timeout(function() {
+          if (error) { ApiErrorHandler.handle(response); return; }
+
+          var sysprops = data["system.properties"];
+          var sep = sysprops["path.separator"]
+          var props = [];
+          for (var key in sysprops) {
+            var value = sysprops[key];
+            var values = value.split(sep);
+            if (value === sep) {
+              values = [':'];
+            }
+            props.push({
+              name: key.replace(/\./g, '.&#8203;')
+                  .replace(/</g, '&lt;')
+                  .replace(/>/g, '&gt;'),
+              values: values
+            });
           }
-          props.push({
-            name: key.replace(/\./g, '.&#8203;')
-                .replace(/</g, '&lt;')
-                .replace(/>/g, '&gt;'),
-            values: values
-          });
-        }
-        $scope.pathSeparator = sep;
-        $scope.props = props;
+          $scope.pathSeparator = sep;
+          $scope.props = props;
+        });
       });
     };
 
diff --git a/solr/webapp/web/js/angular/controllers/schema.js 
b/solr/webapp/web/js/angular/controllers/schema.js
index ba4bc6ef140..d70f60d538e 100644
--- a/solr/webapp/web/js/angular/controllers/schema.js
+++ b/solr/webapp/web/js/angular/controllers/schema.js
@@ -17,8 +17,39 @@
 
 var cookie_schema_browser_autoload = 'schema-browser_autoload';
 
+// SchemaV2 (the generated OpenAPI client) uses superagent directly, so 
genuine transport/auth
+// failures (401/403, network errors) surface as the callback's `error` 
argument. A schema
+// validation failure (e.g. "field type already exists") is now a proper 
non-200 status too
+// (server-side fix), so `error` alone would suffice going forward -- but this 
also checks
+// `data && data.error` defensively, in case some other schema response ever 
reports a failure
+// with a 200 status the way this one used to.
+//
+// The ErrorInfo body carries two messages: a generic top-level `msg` (e.g. 
"error processing
+// commands", the same for every failure) and, for schema-bulk/field-type 
operations, a `details`
+// array of per-operation failures each with their own specific 
`errorMessages`. Prefer the
+// specific per-operation message when present -- it's what's actually 
actionable -- and only
+// fall back to the generic `msg` when there's no per-operation detail to show 
instead.
+function schemaApiErrorMessage(data, response) {
+    var err = (data && data.error) || (response && response.body && 
response.body.error) || {};
+    if (Array.isArray(err.details)) {
+        var messages = [];
+        err.details.forEach(function(detail) {
+            if (Array.isArray(detail.errorMessages)) {
+                messages = messages.concat(detail.errorMessages);
+            }
+        });
+        if (messages.length > 0) {
+            return messages.join(" ");
+        }
+    }
+    if (err.msg) {
+        return err.msg;
+    }
+    return (response && response.statusText) || "Unknown error";
+}
+
 solrAdminApp.controller('SchemaController',
-    function($scope, $routeParams, $location, $cookies, $timeout, Luke, 
Constants, Schema, Config) {
+    function($scope, $routeParams, $location, $cookies, $timeout, Luke, 
Constants, SchemaV2, Config, ApiErrorHandler) {
         $scope.resetMenu("schema", Constants.IS_COLLECTION_PAGE);
 
         $scope.refresh = function () {
@@ -172,21 +203,25 @@ solrAdminApp.controller('SchemaController',
 
         $scope.addField = function() {
             delete $scope.addErrors;
-            var data = {"add-field": $scope.newField};
-            Schema.post({core: $routeParams.core}, data, function(data) {
-                if (data.errors) {
-                    $scope.addErrors = data.errors[0].errorMessages;
-                    if (typeof $scope.addErrors === "string") {
-                        $scope.addErrors = [$scope.addErrors];
+            var indexType = $scope.isCloudEnabled ? "collections" : "cores";
+            SchemaV2.addField(indexType, $routeParams.core, 
$scope.newField.name,
+                {upsertFieldOperation: $scope.newField}, function(error, data, 
response) {
+                $timeout(function() {
+                    if (error || (data && data.error)) {
+                        if (response && (response.status === 401 || 
response.status === 403)) {
+                            ApiErrorHandler.handle(response);
+                        } else {
+                            $scope.addErrors = [schemaApiErrorMessage(data, 
response)];
+                        }
+                        return;
                     }
-                } else {
                     $scope.added = true;
                     $timeout(function() {
                         $scope.showAddField = false;
                         $scope.added = false;
                         $scope.refresh();
                     }, 1500);
-                }
+                });
             });
         }
 
@@ -208,21 +243,25 @@ solrAdminApp.controller('SchemaController',
 
         $scope.addDynamicField = function() {
             delete $scope.addErrors;
-            var data = {"add-dynamic-field": $scope.newField};
-            Schema.post({core: $routeParams.core}, data, function(data) {
-                if (data.errors) {
-                    $scope.addErrors = data.errors[0].errorMessages;
-                    if (typeof $scope.addErrors === "string") {
-                        $scope.addErrors = [$scope.addErrors];
+            var indexType = $scope.isCloudEnabled ? "collections" : "cores";
+            SchemaV2.addDynamicField(indexType, $routeParams.core, 
$scope.newField.name,
+                {upsertDynamicFieldOperation: $scope.newField}, 
function(error, data, response) {
+                $timeout(function() {
+                    if (error || (data && data.error)) {
+                        if (response && (response.status === 401 || 
response.status === 403)) {
+                            ApiErrorHandler.handle(response);
+                        } else {
+                            $scope.addErrors = [schemaApiErrorMessage(data, 
response)];
+                        }
+                        return;
                     }
-                } else {
                     $scope.added = true;
                     $timeout(function() {
                         $scope.showAddField = false;
                         $scope.added = false;
                         $scope.refresh();
                     }, 1500);
-                }
+                });
             });
         }
 
@@ -239,67 +278,84 @@ solrAdminApp.controller('SchemaController',
         }
         $scope.addCopyField = function() {
             delete $scope.addCopyFieldErrors;
-            var data = {"add-copy-field": $scope.copyField};
-            Schema.post({core: $routeParams.core}, data, function(data) {
-                if (data.errors) {
-                    $scope.addCopyFieldErrors = data.errors[0].errorMessages;
-                    if (typeof $scope.addCopyFieldErrors === "string") {
-                        $scope.addCopyFieldErrors = 
[$scope.addCopyFieldErrors];
+            var indexType = $scope.isCloudEnabled ? "collections" : "cores";
+            var schemaChange = [{
+                operationType: "add-copy-field",
+                source: $scope.copyField.source,
+                destinations: [$scope.copyField.dest]
+            }];
+            SchemaV2.bulkSchemaModification(indexType, $routeParams.core, 
{schemaChange: schemaChange},
+                function(error, data, response) {
+                $timeout(function() {
+                    if (error || (data && data.error)) {
+                        if (response && (response.status === 401 || 
response.status === 403)) {
+                            ApiErrorHandler.handle(response);
+                        } else {
+                            $scope.addCopyFieldErrors = 
[schemaApiErrorMessage(data, response)];
+                        }
+                        return;
                     }
-                } else {
                     $scope.showAddCopyField = false;
                     $timeout($scope.refresh, 1500);
-                }
+                });
             });
         }
 
         $scope.toggleDelete = function() {
-            if ($scope.showDelete) {
-                $scope.showDelete = false;
-            } else {
-                if ($scope.is.field) {
-                    $scope.deleteData = {'delete-field': {name: $scope.name}};
-                } else if ($scope.is.dynamicField) {
-                    $scope.deleteData = {'delete-dynamic-field': {name: 
$scope.name}};
-                } else {
-                    alert("TYPE NOT KNOWN");
-                }
-                $scope.showDelete = true;
-            }
+            $scope.showDelete = !$scope.showDelete;
         }
 
         $scope.delete = function() {
-            Schema.post({core: $routeParams.core}, $scope.deleteData, 
function(data) {
-               if (data.errors) {
-                   $scope.deleteErrors = data.errors[0].errorMessages;
-                   if (typeof $scope.deleteErrors === "string") {
-                       $scope.deleteErrors = [$scope.deleteErrors];
-                   }
-               } else {
-                   $scope.deleted = true;
-                   $timeout(function() {
-                       $location.search("");
-                     }, 1500
-                   );
-               }
-            });
+            var indexType = $scope.isCloudEnabled ? "collections" : "cores";
+            function callback(error, data, response) {
+                $timeout(function() {
+                    if (error || (data && data.error)) {
+                        if (response && (response.status === 401 || 
response.status === 403)) {
+                            ApiErrorHandler.handle(response);
+                        } else {
+                            $scope.deleteErrors = [schemaApiErrorMessage(data, 
response)];
+                        }
+                        return;
+                    }
+                    $scope.deleted = true;
+                    $timeout(function() {
+                        $location.search("");
+                    }, 1500);
+                });
+            }
+            if ($scope.is.field) {
+                SchemaV2.deleteField(indexType, $routeParams.core, 
$scope.name, callback);
+            } else if ($scope.is.dynamicField) {
+                SchemaV2.deleteDynamicField(indexType, $routeParams.core, 
$scope.name, callback);
+            } else {
+                alert("TYPE NOT KNOWN");
+            }
         }
         $scope.toggleDeleteCopyField = function(field) {
             field.show = !field.show;
             delete field.errors;
         }
         $scope.deleteCopyField = function(field, source, dest) {
-            data = {'delete-copy-field': {source: source, dest: dest}};
-            Schema.post({core: $routeParams.core}, data, function(data) {
-               if (data.errors) {
-                   field.errors = data.errors[0].errorMessages;
-                   if (typeof $scope.deleteErrors === "string") {
-                       field.errors = [field.errors];
-                   }
-               } else {
-                   field.deleted = true;
-                   $timeout($scope.refresh, 1500);
-               }
+            var indexType = $scope.isCloudEnabled ? "collections" : "cores";
+            var schemaChange = [{
+                operationType: "delete-copy-field",
+                source: source,
+                destinations: [dest]
+            }];
+            SchemaV2.bulkSchemaModification(indexType, $routeParams.core, 
{schemaChange: schemaChange},
+                function(error, data, response) {
+                $timeout(function() {
+                    if (error || (data && data.error)) {
+                        if (response && (response.status === 401 || 
response.status === 403)) {
+                            ApiErrorHandler.handle(response);
+                        } else {
+                            field.errors = [schemaApiErrorMessage(data, 
response)];
+                        }
+                        return;
+                    }
+                    field.deleted = true;
+                    $timeout($scope.refresh, 1500);
+                });
             });
         }
         $scope.toggleManipulateFieldType = function() {
@@ -321,22 +377,42 @@ solrAdminApp.controller('SchemaController',
 
         $scope.manipulateFieldType = function() {
             delete $scope.manipulateFieldTypeErrors;
-            var data = JSON.parse($scope.fieldTypeObj);
-            Schema.post({core: $routeParams.core}, data, function(data) {
-                if (data.errors) {
-                    $scope.manipulateFieldTypeErrors = 
data.errors[0].errorMessages;
-                    if (typeof $scope.manipulateFieldTypeErrors === "string") {
-                        $scope.manipulateFieldTypeErrors = 
[$scope.manipulateFieldTypeErrors];
+            var indexType = $scope.isCloudEnabled ? "collections" : "cores";
+            var parsed = JSON.parse($scope.fieldTypeObj);
+            function callback(error, data, response) {
+                $timeout(function() {
+                    if (error || (data && data.error)) {
+                        if (response && (response.status === 401 || 
response.status === 403)) {
+                            ApiErrorHandler.handle(response);
+                        } else {
+                            $scope.manipulateFieldTypeErrors = 
[schemaApiErrorMessage(data, response)];
+                        }
+                        return;
                     }
-                } else {
                     $scope.added = true;
                     $timeout(function() {
                         $scope.showManipulateFieldType = false;
                         $scope.added = false;
                         $scope.refresh();
                     }, 1500);
-                }
-            });
+                });
+            }
+            var fieldType = parsed["add-field-type"] || 
parsed["replace-field-type"];
+            if (parsed["replace-field-type"]) {
+                var schemaChange = [angular.extend({}, fieldType, {
+                    operationType: "replace-field-type"
+                })];
+                SchemaV2.bulkSchemaModification(indexType, $routeParams.core, 
{schemaChange: schemaChange}, callback);
+            } else if (fieldType) {
+                SchemaV2.addFieldType(indexType, $routeParams.core, 
fieldType.name,
+                    {upsertFieldTypeOperation: fieldType}, callback);
+            } else if (parsed["delete-field-type"]) {
+                SchemaV2.deleteFieldType(indexType, $routeParams.core, 
parsed["delete-field-type"].name, callback);
+            } else {
+                $scope.manipulateFieldTypeErrors = [
+                    "Unrecognized operation - expected add-field-type, 
delete-field-type, or replace-field-type"
+                ];
+            }
         }
     }
 );
diff --git a/solr/webapp/web/js/angular/services.js 
b/solr/webapp/web/js/angular/services.js
index 59c9add0635..2119d3bd174 100644
--- a/solr/webapp/web/js/angular/services.js
+++ b/solr/webapp/web/js/angular/services.js
@@ -154,6 +154,12 @@ solrAdminServices.factory('Metrics',
       delete solrApi.ApiClient.instance.defaultHeaders["User-Agent"];
       return new solrApi.SchemaDesignerApi();
     })
+.factory('SchemaV2',
+    function() {
+      solrApi.ApiClient.instance.basePath = '/api';
+      delete solrApi.ApiClient.instance.defaultHeaders["User-Agent"];
+      return new solrApi.SchemaApi();
+    })
 .factory('Collections',
   ['$resource', function ($resource) {
     // v2 ClusterAPI (/api/cluster) delegates straight through to the same v1 
CollectionsHandler
@@ -192,10 +198,10 @@ solrAdminServices.factory('Metrics',
   }])
 .factory('Zookeeper',
   ['$resource', function($resource) {
+    // Tree browsing (formerly "simple"/"detail") moved to ZookeeperReadV2; 
this factory now only
+    // covers the graph view's cluster state read, which has no v2 equivalent.
     return $resource('admin/zookeeper', {wt:'json', _:Date.now()}, {
-      "simple": {},
-      "clusterState": {params: {detail: "true", path: "/clusterstate.json"}},
-      "detail": {params: {detail: "true", path: "@path"}}
+      "clusterState": {params: {detail: "true", path: "/clusterstate.json"}}
     });
   }])
 .factory('ZookeeperStatus',
@@ -204,10 +210,38 @@ solrAdminServices.factory('Metrics',
       "monitor": {}
     });
   }])
-.factory('Properties',
-  ['$resource', function($resource) {
-    return $resource('admin/info/properties', {'wt':'json', '_':Date.now()});
+.factory('ZookeeperReadV2',
+  ['$http', function($http) {
+    // Hand-rolled rather than the generated solrApi.ZookeeperReadApi client: 
the generated
+    // ApiClient.buildUrl() runs encodeURIComponent() on the whole zkPath 
value, turning its '/'
+    // separators into %2F, which Jetty's URI-ambiguity checks reject for any 
path beyond a
+    // single segment (even the root path "/" itself). SolrJ's generated Java 
client avoids this
+    // by not encoding path params at all; we do the same here by building the 
URL ourselves.
+    return {
+      listNodes: function(zkPath, opts) {
+        opts = opts || {};
+        var params = {};
+        if (opts.children !== undefined) {
+          params.children = opts.children;
+        }
+        return $http.get('/api/cluster/zookeeper/children' + zkPath, {params: 
params});
+      },
+      readNode: function(zkPath) {
+        // Content is raw znode bytes, not JSON -- skip Angular's default 
JSON-parsing attempt
+        // (the server's Content-Type is negotiated and may say 
application/json even when the
+        // body itself is plain text or XML).
+        return $http.get('/api/cluster/zookeeper/data' + zkPath, {
+          transformResponse: [function(data) { return data; }]
+        });
+      }
+    };
   }])
+.factory('NodeV2',
+    function() {
+      solrApi.ApiClient.instance.basePath = '/api';
+      delete solrApi.ApiClient.instance.defaultHeaders["User-Agent"];
+      return new solrApi.NodeApi();
+    })
 .factory('Threads',
   ['$resource', function($resource) {
     // v2 NodeThreadsAPI (/api/node/threads) still just delegates straight 
through to the same v1
@@ -353,14 +387,6 @@ solrAdminServices.factory('Metrics',
        }
        return resource;
 }])
-.factory('Schema',
-   ['$resource', function($resource) {
-     return $resource(':core/schema', {wt: 'json', core: '@core', 
_:Date.now()}, {
-       get: {method: "GET"},
-       check: {method: "GET", headers: {doNotIntercept: "true"}},
-       post: {method: "POST"}
-     });
-}])
 .factory('Config',
    ['$resource', function($resource) {
      return $resource(':core/config', {wt: 'json', core: '@core', 
_:Date.now()}, {
diff --git a/solr/webapp/web/partials/schema.html 
b/solr/webapp/web/partials/schema.html
index 0b60154a05e..e36c5fdf79b 100644
--- a/solr/webapp/web/partials/schema.html
+++ b/solr/webapp/web/partials/schema.html
@@ -229,7 +229,7 @@ limitations under the License.
                     <textarea rows="20" cols="50" type="text" 
id="manipulate_field_type" ng-model="fieldTypeObj" 
focus-when="showManipulateFieldType" placeholder="specify field type 
operation(add, delete, replace) with field type patterns"></textarea>
                 </p>
 
-                <div ng-repeat="error in manipulateFieldTypeErrors" 
ng-show="addFieldTypeErrors" class="clearfix note error">
+                <div ng-repeat="error in manipulateFieldTypeErrors" 
ng-show="manipulateFieldTypeErrors" class="clearfix note error">
                     <span>{{error}}</span></div>
                 <p class="clearfix buttons">
                     <button type="submit" class="submit" ng-class="{success: 
added}" 
ng-click="manipulateFieldType()"><span>{{fieldTypeManipulationOption.label}}</span></button>

Reply via email to