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, '.​')
+ .replace(/</g, '<')
+ .replace(/>/g, '>'),
+ values: values
+ });
}
- props.push({
- name: key.replace(/\./g, '.​')
- .replace(/</g, '<')
- .replace(/>/g, '>'),
- 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>