diff --git a/solr/api/src/java/org/apache/solr/client/api/endpoint/SchemaDesignerApi.java b/solr/api/src/java/org/apache/solr/client/api/endpoint/SchemaDesignerApi.java index 572891d6ec55..d319471857dc 100644 --- a/solr/api/src/java/org/apache/solr/client/api/endpoint/SchemaDesignerApi.java +++ b/solr/api/src/java/org/apache/solr/client/api/endpoint/SchemaDesignerApi.java @@ -16,6 +16,7 @@ */ package org.apache.solr.client.api.endpoint; +import static org.apache.solr.client.api.util.Constants.ADDTL_FIELDS_PROPERTY; import static org.apache.solr.client.api.util.Constants.GENERIC_ENTITY_PROPERTY; import io.swagger.v3.oas.annotations.Operation; @@ -147,7 +148,12 @@ SchemaDesignerAddResponse addSchemaObject( SchemaDesignerUpdateResponse updateSchemaObject( @PathParam("configSet") String configSet, @QueryParam("schemaVersion") Integer schemaVersion, - SchemaDesignerUpdateRequestBody requestBody) + @RequestBody( + extensions = { + @Extension( + properties = {@ExtensionProperty(name = ADDTL_FIELDS_PROPERTY, value = "true")}) + }) + SchemaDesignerUpdateRequestBody requestBody) throws Exception; @PUT diff --git a/solr/api/src/java/org/apache/solr/client/api/model/SchemaDesignerAddRequestBody.java b/solr/api/src/java/org/apache/solr/client/api/model/SchemaDesignerAddRequestBody.java index 821cabc4246a..25e0da72a79a 100644 --- a/solr/api/src/java/org/apache/solr/client/api/model/SchemaDesignerAddRequestBody.java +++ b/solr/api/src/java/org/apache/solr/client/api/model/SchemaDesignerAddRequestBody.java @@ -17,7 +17,6 @@ package org.apache.solr.client.api.model; import com.fasterxml.jackson.annotation.JsonProperty; -import io.swagger.v3.oas.annotations.media.Schema; import java.util.Map; /** @@ -27,19 +26,15 @@ */ public class SchemaDesignerAddRequestBody { - @Schema(name = "addField") @JsonProperty("add-field") public Map addField; - @Schema(name = "addDynamicField") @JsonProperty("add-dynamic-field") public Map addDynamicField; - @Schema(name = "addCopyField") @JsonProperty("add-copy-field") public Map addCopyField; - @Schema(name = "addFieldType") @JsonProperty("add-field-type") public Map addFieldType; } diff --git a/solr/api/src/java/org/apache/solr/client/api/model/SchemaDesignerUpdateRequestBody.java b/solr/api/src/java/org/apache/solr/client/api/model/SchemaDesignerUpdateRequestBody.java index 54b9bb9e56cb..e88114462e59 100644 --- a/solr/api/src/java/org/apache/solr/client/api/model/SchemaDesignerUpdateRequestBody.java +++ b/solr/api/src/java/org/apache/solr/client/api/model/SchemaDesignerUpdateRequestBody.java @@ -19,6 +19,7 @@ import com.fasterxml.jackson.annotation.JsonAnyGetter; import com.fasterxml.jackson.annotation.JsonAnySetter; import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.v3.oas.annotations.media.Schema; import java.util.HashMap; import java.util.Map; @@ -28,6 +29,7 @@ * indexed}, {@code stored}, {@code analyzer}, {@code copyDest}) are captured via the dynamic {@code * additionalProperties} map and forwarded to the Schema API. */ +@Schema(additionalProperties = Schema.AdditionalPropertiesValue.TRUE) public class SchemaDesignerUpdateRequestBody { @JsonProperty public String name; @@ -36,6 +38,7 @@ public class SchemaDesignerUpdateRequestBody { // Accessed via @JsonAnyGetter / @JsonAnySetter for JSON (de)serialization. public Map additionalProperties = new HashMap<>(); + @Schema(hidden = true) @JsonAnyGetter public Map getAdditionalProperties() { return additionalProperties; diff --git a/solr/api/src/java/org/apache/solr/client/api/model/UpsertDynamicFieldOperation.java b/solr/api/src/java/org/apache/solr/client/api/model/UpsertDynamicFieldOperation.java index db010fbacca4..2a0376fafc39 100644 --- a/solr/api/src/java/org/apache/solr/client/api/model/UpsertDynamicFieldOperation.java +++ b/solr/api/src/java/org/apache/solr/client/api/model/UpsertDynamicFieldOperation.java @@ -19,9 +19,11 @@ import com.fasterxml.jackson.annotation.JsonAnyGetter; import com.fasterxml.jackson.annotation.JsonAnySetter; import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.v3.oas.annotations.media.Schema; import java.util.HashMap; import java.util.Map; +@Schema(additionalProperties = Schema.AdditionalPropertiesValue.TRUE) public class UpsertDynamicFieldOperation extends SchemaChange { @JsonProperty public String name; @JsonProperty public String type; @@ -29,6 +31,7 @@ public class UpsertDynamicFieldOperation extends SchemaChange { // Used for setting index and stored settings, etc. private Map additionalProperties = new HashMap<>(); + @Schema(hidden = true) @JsonAnyGetter public Map getAdditionalProperties() { return additionalProperties; diff --git a/solr/api/src/java/org/apache/solr/client/api/model/UpsertFieldOperation.java b/solr/api/src/java/org/apache/solr/client/api/model/UpsertFieldOperation.java index c6a7d1c3c322..6946f3536c8a 100644 --- a/solr/api/src/java/org/apache/solr/client/api/model/UpsertFieldOperation.java +++ b/solr/api/src/java/org/apache/solr/client/api/model/UpsertFieldOperation.java @@ -19,9 +19,11 @@ import com.fasterxml.jackson.annotation.JsonAnyGetter; import com.fasterxml.jackson.annotation.JsonAnySetter; import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.v3.oas.annotations.media.Schema; import java.util.HashMap; import java.util.Map; +@Schema(additionalProperties = Schema.AdditionalPropertiesValue.TRUE) public class UpsertFieldOperation extends SchemaChange { @JsonProperty public String name; @@ -30,6 +32,7 @@ public class UpsertFieldOperation extends SchemaChange { // Used for setting index and stored settings, etc. private Map additionalProperties = new HashMap<>(); + @Schema(hidden = true) @JsonAnyGetter public Map getAdditionalProperties() { return additionalProperties; diff --git a/solr/api/src/java/org/apache/solr/client/api/model/UpsertFieldTypeOperation.java b/solr/api/src/java/org/apache/solr/client/api/model/UpsertFieldTypeOperation.java index 6a517830b2ed..60ee25122c81 100644 --- a/solr/api/src/java/org/apache/solr/client/api/model/UpsertFieldTypeOperation.java +++ b/solr/api/src/java/org/apache/solr/client/api/model/UpsertFieldTypeOperation.java @@ -19,18 +19,26 @@ import com.fasterxml.jackson.annotation.JsonAnyGetter; import com.fasterxml.jackson.annotation.JsonAnySetter; import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.v3.oas.annotations.media.Schema; import java.util.HashMap; import java.util.Map; +@Schema(additionalProperties = Schema.AdditionalPropertiesValue.TRUE) public class UpsertFieldTypeOperation extends SchemaChange { @JsonProperty public String name; + // Field named to match what the OpenAPI-generated SolrJ client derives as a Java-safe + // identifier for the reserved word "class" (see api.mustache's {{name}} usage) -- the + // SolrJ codegen assigns into this field by that exact name, so a hand-picked name here + // (e.g. "className") would compile-fail the generated client the moment this class stops + // being shielded from per-field setter generation (see SchemaChange's oneOf discriminator). @JsonProperty("class") - public String className; + public String propertyClass; // Used for setting analyzers, index and stored settings, etc. private Map additionalProperties = new HashMap<>(); + @Schema(hidden = true) @JsonAnyGetter public Map getAdditionalProperties() { return additionalProperties; diff --git a/solr/api/src/test/org/apache/solr/client/api/model/SchemaChangeOperationSerializationTest.java b/solr/api/src/test/org/apache/solr/client/api/model/SchemaChangeOperationSerializationTest.java index 48afd83e694d..a0606bd6d3d7 100644 --- a/solr/api/src/test/org/apache/solr/client/api/model/SchemaChangeOperationSerializationTest.java +++ b/solr/api/src/test/org/apache/solr/client/api/model/SchemaChangeOperationSerializationTest.java @@ -58,7 +58,7 @@ public void testAddFieldType() throws Exception { assertThat(parsedGeneric, instanceOf(UpsertFieldTypeOperation.class)); final var parsedSpecific = (UpsertFieldTypeOperation) parsedGeneric; assertEquals("my-new-field-type", parsedSpecific.name); - assertEquals("org.apache.my.ClassName", parsedSpecific.className); + assertEquals("org.apache.my.ClassName", parsedSpecific.propertyClass); // Arbitrary properties are put in a map, and can contain nesting assertEquals(100, parsedSpecific.getAdditionalProperties().get("positionIncrementGap")); assertThat(parsedSpecific.getAdditionalProperties().get("analyzer"), instanceOf(Map.class)); @@ -272,7 +272,7 @@ public void testReplaceFieldType() throws Exception { assertThat(parsedGeneric, instanceOf(UpsertFieldTypeOperation.class)); final var parsedSpecific = (UpsertFieldTypeOperation) parsedGeneric; assertEquals("my-new-field-type", parsedSpecific.name); - assertEquals("org.apache.my.ClassName", parsedSpecific.className); + assertEquals("org.apache.my.ClassName", parsedSpecific.propertyClass); // Arbitrary properties are put in a map, and can contain nesting assertEquals(100, parsedSpecific.getAdditionalProperties().get("positionIncrementGap")); assertThat(parsedSpecific.getAdditionalProperties().get("analyzer"), instanceOf(Map.class)); 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 1bb3f51c4c32..6bb00ea00f60 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 @@ -122,7 +122,7 @@ public SolrJerseyResponse addFieldType(String fieldTypeName, UpsertFieldTypeOper ensureSchemaMutable(); ensureRequiredRequestBodyProvided(requestBody); ensureRequiredParameterProvided("fieldTypeName", fieldTypeName); - ensureRequiredParameterProvided("class", requestBody.className); + ensureRequiredParameterProvided("class", requestBody.propertyClass); requestBody.operationType = "add-field-type"; runWithSchemaManager(List.of(requestBody), response); diff --git a/solr/core/src/java/org/apache/solr/schema/SchemaManager.java b/solr/core/src/java/org/apache/solr/schema/SchemaManager.java index a65eb18df6aa..05efe512c21d 100644 --- a/solr/core/src/java/org/apache/solr/schema/SchemaManager.java +++ b/solr/core/src/java/org/apache/solr/schema/SchemaManager.java @@ -240,7 +240,7 @@ public enum OpType { public boolean perform(SchemaChange op, SchemaManager mgr) throws SchemaOperationException { final var addFieldTypeOp = (UpsertFieldTypeOperation) op; String name = ensureNotNull("name", addFieldTypeOp.name); - String className = ensureNotNull("class", addFieldTypeOp.className); + String className = ensureNotNull("class", addFieldTypeOp.propertyClass); try { FieldType fieldType = mgr.managedIndexSchema.newFieldType(name, className, convertToMap(addFieldTypeOp)); @@ -420,7 +420,7 @@ public boolean perform(SchemaChange op, SchemaManager mgr) throws SchemaOperatio public boolean perform(SchemaChange op, SchemaManager mgr) throws SchemaOperationException { final var replaceFieldTypeOp = (UpsertFieldTypeOperation) op; String name = ensureNotNull("name", replaceFieldTypeOp.name); - String className = ensureNotNull("class", replaceFieldTypeOp.className); + String className = ensureNotNull("class", replaceFieldTypeOp.propertyClass); try { mgr.managedIndexSchema = mgr.managedIndexSchema.replaceFieldType( diff --git a/solr/core/src/test/org/apache/solr/handler/admin/api/V2UpdateSchemaErrorCaseTests.java b/solr/core/src/test/org/apache/solr/handler/admin/api/V2UpdateSchemaErrorCaseTests.java index 54d063dc31e9..d981f572a0b1 100644 --- a/solr/core/src/test/org/apache/solr/handler/admin/api/V2UpdateSchemaErrorCaseTests.java +++ b/solr/core/src/test/org/apache/solr/handler/admin/api/V2UpdateSchemaErrorCaseTests.java @@ -123,7 +123,7 @@ public void testDeleteDynamicFieldOperationRequiresFieldName() { @Test public void testAddFieldTypeOperationRequiresTypeNameAndClass() { final var noTypeOp = new UpsertFieldTypeOperation(); - noTypeOp.className = "solr.TextField"; + noTypeOp.propertyClass = "solr.TextField"; var thrown = expectThrows( SolrException.class, diff --git a/solr/core/src/test/org/apache/solr/handler/designer/TestSchemaDesignerSolrJ.java b/solr/core/src/test/org/apache/solr/handler/designer/TestSchemaDesignerSolrJ.java index d755b6964d99..9d3a4fe13841 100644 --- a/solr/core/src/test/org/apache/solr/handler/designer/TestSchemaDesignerSolrJ.java +++ b/solr/core/src/test/org/apache/solr/handler/designer/TestSchemaDesignerSolrJ.java @@ -123,7 +123,9 @@ public void testTypedBodyRoundTrip() throws Exception { var update = new SchemaDesignerApi.UpdateSchemaObject(configSet); update.setSchemaVersion(schemaVersion); update.setName("keywords"); - update.setAdditionalProperties(Map.of("type", "string", "stored", true, "multiValued", true)); + update.setAdditionalProperty("type", "string"); + update.setAdditionalProperty("stored", true); + update.setAdditionalProperty("multiValued", true); SchemaDesignerUpdateResponse updateResp = update.process(cluster.getSolrClient()); assertNotNull(updateResp.field); assertEquals("field", updateResp.updateType); diff --git a/solr/solrj/src/resources/java-template/api.mustache b/solr/solrj/src/resources/java-template/api.mustache index 81cdff1b3a87..4aae48285610 100644 --- a/solr/solrj/src/resources/java-template/api.mustache +++ b/solr/solrj/src/resources/java-template/api.mustache @@ -179,13 +179,18 @@ public class {{classname}} { {{#bodyParam}} {{#vars}} // TODO find a way to add required parameters in the request body to the class constructor + // The setter parameter (and the requestBody. access below) both use the + // "name" var: openapi-generator's Java-safe sanitization of this property's wire + // name (e.g. the reserved word "class" becomes "propertyClass"). The hand-written + // requestBody model class's field MUST be named to match exactly, or the field + // access below fails to compile. {{#description}} /** - * @param {{baseName}} {{description}} + * @param {{name}} {{description}} */ {{/description}} - public void {{setter}}({{{dataType}}} {{baseName}}) { - this.requestBody.{{baseName}} = {{baseName}}; + public void {{setter}}({{{dataType}}} {{name}}) { + this.requestBody.{{name}} = {{name}}; } {{/vars}} diff --git a/solr/webapp/src/test/org/apache/solr/webapp/AdminUiSchemaDesignerTest.java b/solr/webapp/src/test/org/apache/solr/webapp/AdminUiSchemaDesignerTest.java index 85a76aec800c..739c6312e6eb 100644 --- a/solr/webapp/src/test/org/apache/solr/webapp/AdminUiSchemaDesignerTest.java +++ b/solr/webapp/src/test/org/apache/solr/webapp/AdminUiSchemaDesignerTest.java @@ -16,7 +16,6 @@ */ package org.apache.solr.webapp; -import org.apache.lucene.tests.util.LuceneTestCase; import org.junit.Test; import org.openqa.selenium.By; import org.openqa.selenium.WebElement; @@ -25,11 +24,9 @@ * Happy-path test of the Schema Designer screen: create a new schema, paste a sample document and * let the designer analyze it. * - *

AwaitsFix: the designer backend transiently fails its own prep/analyze calls ("version - * mismatch, retry", "Error loading solr config") when driven at automation speed, making this test - * flaky even with retries. + *

The Analyze action remains disabled until creation of the mutable schema has completed, so a + * fast user cannot race the prep and analyze requests. */ -@LuceneTestCase.AwaitsFix(bugUrl = "https://issues.apache.org/jira/browse/SOLR-18347") public class AdminUiSchemaDesignerTest extends AdminUiTestBase { @Test @@ -47,34 +44,17 @@ public void testDesignSchemaFromSampleDocument() throws Exception { WebElement sampleDocs = waitFor(By.cssSelector("#sample-docs textarea#document")); sampleDocs.clear(); sampleDocs.sendKeys("[{\"id\":\"1\",\"designer_title\":\"Hello Designer\"}]"); - click(By.id("analyze")); + click(By.cssSelector("#analyze:not([disabled])")); - // the analyzed schema lists the field derived from the sample doc. The designer - // backend transiently fails its own calls ("version mismatch, retry", "Error - // loading solr config") and surfaces an error dialog - dismiss it and analyze - // again, with a generous budget since each round trips several requests - long deadlineNanos = System.nanoTime() + WAIT_TIMEOUT.multipliedBy(3).toNanos(); - boolean analyzed = false; - while (!analyzed && System.nanoTime() < deadlineNanos) { - analyzed = driver.getPageSource().contains("designer_title"); - if (!analyzed) { - for (String dismissButton : new String[] {"Reload Schema", "OK"}) { - driver.findElements(By.xpath("//button[contains(., '" + dismissButton + "')]")).stream() - .filter(WebElement::isDisplayed) - .findFirst() - .ifPresent(WebElement::click); - } - driver.findElements(By.id("analyze")).stream() - .filter(WebElement::isDisplayed) - .findFirst() - .ifPresent(WebElement::click); - Thread.sleep(500); - } - } - assertTrue("Analyzed schema should list the sample doc field", analyzed); - // the designer's own API calls (prep/analyze/luke against its temp core) error - // transiently while it persists and reloads the schema - it recovers via its retry - // dialog, so only unrelated console errors fail the test - assertNoSevereConsoleErrors("schema-designer/", "._designer_"); + waitForPageContains("designer_title"); + assertNoSevereConsoleErrors(); + + // add a field through the UI + click(By.cssSelector("#addField")); + setText(By.id("add_name"), "extra_test_field"); + click(By.xpath("//button[@ng-click='addField()']")); + + waitForPageContains("extra_test_field"); + assertNoSevereConsoleErrors(); } } diff --git a/solr/webapp/web/js/angular/controllers/schema-designer.js b/solr/webapp/web/js/angular/controllers/schema-designer.js index 7ec4282bb053..a8d4c963245d 100644 --- a/solr/webapp/web/js/angular/controllers/schema-designer.js +++ b/solr/webapp/web/js/angular/controllers/schema-designer.js @@ -15,7 +15,7 @@ limitations under the License. */ -solrAdminApp.controller('SchemaDesignerController', function ($scope, $timeout, $cookies, $window, Constants, SchemaDesigner, ConfigSetFiles, Luke) { +solrAdminApp.controller('SchemaDesignerController', function ($scope, $timeout, $cookies, $window, Constants, SchemaDesigner, SchemaDesignerV2, ConfigSetFiles, Luke) { $scope.resetMenu("schema-designer", Constants.IS_ROOT_PAGE); $scope.schemas = []; @@ -24,6 +24,7 @@ solrAdminApp.controller('SchemaDesignerController', function ($scope, $timeout, $scope.sortableFields = []; $scope.hlFields = []; $scope.types = []; + $scope.preparingSchema = false; $scope.onWarning = function (warnMsg, warnDetails) { $scope.updateWorking = false; @@ -34,6 +35,7 @@ solrAdminApp.controller('SchemaDesignerController', function ($scope, $timeout, $scope.onError = function (errorMsg, errorCode, errorDetails) { $scope.updateWorking = false; + $scope.preparingSchema = false; delete $scope.updateStatusMessage; $scope.designerAPIError = errorMsg; if (errorDetails) { @@ -68,20 +70,38 @@ solrAdminApp.controller('SchemaDesignerController', function ($scope, $timeout, } // else 500 errors get the top-level error message }; - $scope.errorHandler = function (e) { - var error = e.data && e.data.error ? e.data.error : null; + // Shared by $scope.errorHandler (v1 $http) and $scope.errorHandlerV2 (v2 generated-client) below: + // given a structured API error (or null, for a network-level failure like a timeout), drives the + // same local error dialog via $scope.onError. + function reportApiFailure(error, errorDetails, fallbackPath, fallbackCode, extraFailureHint) { if (error) { - $scope.onError(error.msg, error.code, e.data.errorDetails); + $scope.onError(error.msg, error.code, errorDetails); } else { - // when a timeout occurs, the error details are sparse so just give the user a hint that something was off - var path = e.config && e.config.url ? e.config.url : "/api/schema-designer"; - var reloadMsg = ""; - if (path.includes("/analyze")) { - reloadMsg = " Re-try analyzing your sample docs by clicking on 'Analyze Documents' again." - } - $scope.onError("Request to "+path+" failed!", 408, - {"error":"Most likely the request timed out; check server log for more details."+reloadMsg}); + $scope.onError("Request to "+fallbackPath+" failed!", fallbackCode, + {"error":"Most likely the request timed out; check server log for more details."+(extraFailureHint || "")}); } + } + + $scope.errorHandler = function (e) { + var error = e.data && e.data.error ? e.data.error : null; + // when a timeout occurs, the error details are sparse so just give the user a hint that something was off + var path = e.config && e.config.url ? e.config.url : "/api/schema-designer"; + var reloadMsg = path.includes("/analyze") + ? " Re-try analyzing your sample docs by clicking on 'Analyze Documents' again." + : ""; + reportApiFailure(error, e.data && e.data.errorDetails, path, 408, reloadMsg); + }; + + // Error handler for SchemaDesignerV2 (generated OpenAPI client) callbacks: response is the raw + // superagent response (may be undefined for a network-level failure like a timeout). This deliberately + // does NOT go through the shared ApiErrorHandler service -- app.js's httpInterceptor already carves + // out /api/schema-designer/ from the global 401/403 handling so a failure here degrades this one + // screen instead of forcing a full login redirect mid-design-session; this mirrors that same intent + // for v2 calls by driving the same local error dialog as $scope.errorHandler. + $scope.errorHandlerV2 = function (response) { + var data = (response && response.body) || {}; + var path = (response && response.req && response.req.url) || "/api/schema-designer"; + reportApiFailure(data.error, data.errorDetails, path, (response && response.status) || 408); }; $scope.closeWarnDialog = function () { @@ -149,36 +169,40 @@ solrAdminApp.controller('SchemaDesignerController', function ($scope, $timeout, // query form $scope.query = {q: '*:*', sortBy: 'score', sortDir: 'desc'}; - SchemaDesigner.get({path: "configs"}, function (data) { + SchemaDesignerV2.listDesignerConfigs(function (error, data, response) { + $timeout(function () { + if (error) { + if (response && (response.status === 401 || response.status === 403)) { + $scope.isSchemaDesignerEnabled = false; + $scope.hideAll(); + } + return; + } - $scope.schemas = []; - $scope.publishedSchemas = ["_default"]; + $scope.schemas = []; + $scope.publishedSchemas = ["_default"]; - for (var s in data.configSets) { - // 1 means published but not editable - if (data.configSets[s] !== 1) { - $scope.schemas.push(s); - } + for (var s in data.configSets) { + // 1 means published but not editable + if (data.configSets[s] !== 1) { + $scope.schemas.push(s); + } - // 0 means not published yet (so can't copy from it yet) - if (data.configSets[s] > 0) { - $scope.publishedSchemas.push(s); + // 0 means not published yet (so can't copy from it yet) + if (data.configSets[s] > 0) { + $scope.publishedSchemas.push(s); + } } - } - $scope.schemas.sort(); - $scope.publishedSchemas.sort(); + $scope.schemas.sort(); + $scope.publishedSchemas.sort(); - // if no schemas available to select, open the pop-up immediately - if ($scope.schemas.length === 0) { - $scope.firstSchemaMessage = true; - $scope.showNewSchemaDialog(); - } - }, function(e) { - if (e.status === 401 || e.status === 403) { - $scope.isSchemaDesignerEnabled = false; - $scope.hideAll(); - } + // if no schemas available to select, open the pop-up immediately + if ($scope.schemas.length === 0) { + $scope.firstSchemaMessage = true; + $scope.showNewSchemaDialog(); + } + }); }); }; @@ -239,26 +263,31 @@ solrAdminApp.controller('SchemaDesignerController', function ($scope, $timeout, } $scope.resetSchema(); - var params = {configSet: $scope.currentSchema}; - SchemaDesigner.get(params, function (data) { - $scope.currentSchema = data.configSet; - $("#select-schema").trigger("chosen:updated"); - - $scope.confirmSchema = data.configSet; - $scope.collectionsForConfig = data.collections; - $scope.hasDocsOnServer = data.numDocs > 0; - $scope.published = data.published; - $scope.initDesignerSettingsFromResponse(data); - if ($scope.collectionsForConfig && $scope.collectionsForConfig.length > 0) { - $scope.showConfirmEditSchema = true; - } else { - if ($scope.hasDocsOnServer || $scope.published) { - $scope.doAnalyze(); + SchemaDesignerV2.getInfo($scope.currentSchema, function (error, data, response) { + $timeout(function () { + if (error) { + $scope.errorHandlerV2(response); + return; + } + $scope.currentSchema = data.configSet; + $("#select-schema").trigger("chosen:updated"); + + $scope.confirmSchema = data.configSet; + $scope.collectionsForConfig = data.collections; + $scope.hasDocsOnServer = data.numDocs > 0; + $scope.published = data.published; + $scope.initDesignerSettingsFromResponse(data); + if ($scope.collectionsForConfig && $scope.collectionsForConfig.length > 0) { + $scope.showConfirmEditSchema = true; } else { - $scope.sampleMessage = "Please upload or paste some sample documents to build the '" + $scope.currentSchema + "' schema."; + if ($scope.hasDocsOnServer || $scope.published) { + $scope.doAnalyze(); + } else { + $scope.sampleMessage = "Please upload or paste some sample documents to build the '" + $scope.currentSchema + "' schema."; + } } - } - }, $scope.errorHandler); + }); + }); }; $scope.showNewSchemaDialog = function () { @@ -303,9 +332,17 @@ solrAdminApp.controller('SchemaDesignerController', function ($scope, $timeout, $scope.currentSchema = $scope.newSchema; $scope.sampleMessage = "Please upload or paste some sample documents to analyze for building the '" + $scope.currentSchema + "' schema."; - SchemaDesigner.post({path: "prep", configSet: $scope.newSchema, copyFrom: $scope.copyFrom}, null, function (data) { - $scope.initDesignerSettingsFromResponse(data); - }, $scope.errorHandler); + $scope.preparingSchema = true; + SchemaDesignerV2.prepNewSchema($scope.newSchema, {copyFrom: $scope.copyFrom}, function (error, data, response) { + $timeout(function () { + if (error) { + $scope.errorHandlerV2(response); + return; + } + $scope.preparingSchema = false; + $scope.initDesignerSettingsFromResponse(data); + }); + }); }; $scope.cancelAddSchema = function () { @@ -451,9 +488,11 @@ solrAdminApp.controller('SchemaDesignerController', function ($scope, $timeout, // re-apply the filters on the updated schema $scope.onTreeFilterOptionChanged(); - // Load the Luke schema - Luke.schema({core: data.core}, function (schema) { - Luke.raw({core: data.core}, function (index) { + // Load the Luke schema. Route through the temporary collection so the request reaches its + // active replica even when the Admin UI is connected to a different node. + var lukeTarget = data.tempCollection || data.core; + Luke.schema({core: lukeTarget}, function (schema) { + Luke.raw({core: lukeTarget}, function (index) { $scope.luke = mergeIndexAndSchemaData(index, schema.schema); $scope.types = Object.keys(schema.schema.types); $scope.showSchemaActions = true; @@ -656,31 +695,37 @@ solrAdminApp.controller('SchemaDesignerController', function ($scope, $timeout, } delete $scope.addErrors; // no errors! - SchemaDesigner.post({ - configSet: $scope.currentSchema, - schemaVersion: $scope.schemaVersion - }, addData, function (data) { - if (data.errors) { - $scope.addErrors = data.errors[0].errorMessages; - if (typeof $scope.addErrors === "string") { - $scope.addErrors = [$scope.addErrors]; + SchemaDesignerV2.addSchemaObject($scope.currentSchema, { + schemaVersion: $scope.schemaVersion, + schemaDesignerAddRequestBody: addData + }, function (error, data, response) { + $timeout(function () { + if (error) { + $scope.errorHandlerV2(response); + return; } - } else { - delete $scope.textAnalysisJson; - $scope.added = true; - $timeout(function () { - $scope.showAddField = false; - $scope.added = false; - var nodeId = "/"; - if ("field" === $scope.adding) { - nodeId = "field/" + ("add-dynamic-field" === command ? data.dynamicField : data.field); - } else if ("type" === $scope.adding) { - nodeId = "type/" + data.fieldType; + if (data.errors) { + $scope.addErrors = data.errors[0].errorMessages; + if (typeof $scope.addErrors === "string") { + $scope.addErrors = [$scope.addErrors]; } - $scope.onSchemaUpdated(data.configSet, data, nodeId); - }, 500); - } - }, $scope.errorHandler); + } else { + delete $scope.textAnalysisJson; + $scope.added = true; + $timeout(function () { + $scope.showAddField = false; + $scope.added = false; + var nodeId = "/"; + if ("field" === $scope.adding) { + nodeId = "field/" + ("add-dynamic-field" === command ? data.dynamicField : data.field); + } else if ("type" === $scope.adding) { + nodeId = "type/" + data.fieldType; + } + $scope.onSchemaUpdated(data.configSet, data, nodeId); + }, 500); + } + }); + }); } function toSortedNameAndTypeList(fields, typeAttr) { @@ -721,38 +766,44 @@ solrAdminApp.controller('SchemaDesignerController', function ($scope, $timeout, $('#show-diff-dialog').css({left: leftPos}); } - SchemaDesigner.get({ path: "diff", configSet: $scope.currentSchema }, function (data) { - var diff = data.diff; + SchemaDesignerV2.getSchemaDiff($scope.currentSchema, function (error, data, response) { + $timeout(function () { + if (error) { + $scope.errorHandlerV2(response); + return; + } + var diff = data.diff; - var dynamicFields = diff.dynamicFields; - var enableDynamicFields = data.enableDynamicFields !== null ? data.enableDynamicFields : true; - if (!enableDynamicFields) { - dynamicFields = null; - } + var dynamicFields = diff.dynamicFields; + var enableDynamicFields = data.enableDynamicFields !== null ? data.enableDynamicFields : true; + if (!enableDynamicFields) { + dynamicFields = null; + } - $scope.diffSource = data["diff-source"]; - $scope.schemaDiff = { - "fieldsDiff": diff.fields, - "addedFields": [], - "removedFields": [], - "fieldTypesDiff": diff.fieldTypes, - "removedTypes": [], - "dynamicFieldsDiff": dynamicFields, - "copyFieldsDiff": diff.copyFields - } - if (diff.fields && diff.fields.added) { - $scope.schemaDiff.addedFields = toSortedFieldList(diff.fields.added); - } - if (diff.fields && diff.fields.removed) { - $scope.schemaDiff.removedFields = toSortedNameAndTypeList(diff.fields.removed, "type"); - } - if (diff.fieldTypes && diff.fieldTypes.removed) { - $scope.schemaDiff.removedTypes = toSortedNameAndTypeList(diff.fieldTypes.removed, "class"); - } + $scope.diffSource = data["diff-source"]; + $scope.schemaDiff = { + "fieldsDiff": diff.fields, + "addedFields": [], + "removedFields": [], + "fieldTypesDiff": diff.fieldTypes, + "removedTypes": [], + "dynamicFieldsDiff": dynamicFields, + "copyFieldsDiff": diff.copyFields + } + if (diff.fields && diff.fields.added) { + $scope.schemaDiff.addedFields = toSortedFieldList(diff.fields.added); + } + if (diff.fields && diff.fields.removed) { + $scope.schemaDiff.removedFields = toSortedNameAndTypeList(diff.fields.removed, "type"); + } + if (diff.fieldTypes && diff.fieldTypes.removed) { + $scope.schemaDiff.removedTypes = toSortedNameAndTypeList(diff.fieldTypes.removed, "class"); + } - $scope.schemaDiffExists = !(diff.fields == null && diff.fieldTypes == null && dynamicFields == null && diff.copyFields == null); - $scope.showDiff = true; - }, $scope.errorHandler); + $scope.schemaDiffExists = !(diff.fields == null && diff.fieldTypes == null && dynamicFields == null && diff.copyFields == null); + $scope.showDiff = true; + }); + }); } $scope.togglePublish = function (event) { @@ -789,22 +840,28 @@ solrAdminApp.controller('SchemaDesignerController', function ($scope, $timeout, } $scope.addCopyField = function () { delete $scope.addCopyFieldErrors; - var data = {"add-copy-field": $scope.copyField}; - SchemaDesigner.post({ - configSet: $scope.currentSchema, - schemaVersion: $scope.schemaVersion - }, data, function (data) { - if (data.errors) { - $scope.addCopyFieldErrors = data.errors[0].errorMessages; - if (typeof $scope.addCopyFieldErrors === "string") { - $scope.addCopyFieldErrors = [$scope.addCopyFieldErrors]; + var copyFieldData = {"add-copy-field": $scope.copyField}; + SchemaDesignerV2.addSchemaObject($scope.currentSchema, { + schemaVersion: $scope.schemaVersion, + schemaDesignerAddRequestBody: copyFieldData + }, function (error, data, response) { + $timeout(function () { + if (error) { + $scope.errorHandlerV2(response); + return; } - } else { - $scope.showAddCopyField = false; - // TODO: - //$timeout($scope.refresh, 1500); - } - }, $scope.errorHandler); + if (data.errors) { + $scope.addCopyFieldErrors = data.errors[0].errorMessages; + if (typeof $scope.addCopyFieldErrors === "string") { + $scope.addCopyFieldErrors = [$scope.addCopyFieldErrors]; + } + } else { + $scope.showAddCopyField = false; + // TODO: + //$timeout($scope.refresh, 1500); + } + }); + }); } $scope.toggleAnalyzer = function (analyzer) { @@ -834,22 +891,28 @@ solrAdminApp.controller('SchemaDesignerController', function ($scope, $timeout, } var field = $scope.selectedNode.name; - var params = {path: "sample"}; - params.configSet = $scope.currentSchema; - params.uniqueKeyField = $scope.uniqueKeyField; - params.field = field; + var opts = {uniqueKeyField: $scope.uniqueKeyField, field: field}; if ($scope.sampleDocId) { - params.docId = $scope.sampleDocId; + opts.docId = $scope.sampleDocId; } // else the server will pick the first doc with a non-empty text value for the desired field - SchemaDesigner.get(params, function (data) { - $scope.sampleDocId = data[$scope.uniqueKeyField]; - $scope.indexText = data[field]; - if (data.analysis && data.analysis["field_names"]) { - $scope.result = processFieldAnalysisData(data.analysis["field_names"][field]); - } - }, $scope.errorHandler); + SchemaDesignerV2.getSampleValue($scope.currentSchema, opts, function (error, data, response) { + $timeout(function () { + if (error) { + $scope.errorHandlerV2(response); + return; + } + // FlexibleSolrJerseyResponse only declares responseHeader/error since the sample value and + // analysis are dynamic per-field data; read the raw parsed body instead of the typed `data`. + var raw = (response && response.body) || {}; + $scope.sampleDocId = raw[$scope.uniqueKeyField]; + $scope.indexText = raw[field]; + if (raw.analysis && raw.analysis["field_names"]) { + $scope.result = processFieldAnalysisData(raw.analysis["field_names"][field]); + } + }); + }); }; $scope.changeLanguages = function () { @@ -881,23 +944,28 @@ solrAdminApp.controller('SchemaDesignerController', function ($scope, $timeout, $scope.updateFile = function () { var nodeId = "files/" + $scope.selectedFile; - var params = {path: "file", file: $scope.selectedFile, configSet: $scope.currentSchema}; $scope.updateWorking = true; $scope.updateStatusMessage = "Updating file ..."; - SchemaDesigner.put(params, $scope.fileNodeText, function (data) { - if (data.updateFileError) { - if (data.fileContent) { - $scope.fileNodeText = data.fileContent; + SchemaDesignerV2.updateFileContents($scope.currentSchema, $scope.fileNodeText, {file: $scope.selectedFile}, function (error, data, response) { + $timeout(function () { + if (error) { + $scope.errorHandlerV2(response); + return; } - $scope.updateFileError = data.updateFileError; - } else { - delete $scope.updateFileError; - $scope.updateStatusMessage = "File '"+$scope.selectedFile+"' updated."; - $scope.onSchemaUpdated(data.configSet, data, nodeId); - } - }, $scope.errorHandler); + if (data.updateFileError) { + if (data.fileContent) { + $scope.fileNodeText = data.fileContent; + } + $scope.updateFileError = data.updateFileError; + } else { + delete $scope.updateFileError; + $scope.updateStatusMessage = "File '"+$scope.selectedFile+"' updated."; + $scope.onSchemaUpdated(data.configSet, data, nodeId); + } + }); + }); }; $scope.onSelectFileNode = function (id, doSelectOnTree) { @@ -1387,31 +1455,37 @@ solrAdminApp.controller('SchemaDesignerController', function ($scope, $timeout, $scope.updateWorking = true; $scope.updateStatusMessage = "Updating " + $scope.selectedType + " ..."; - SchemaDesigner.put({ - configSet: $scope.currentSchema, - schemaVersion: $scope.schemaVersion - }, putData, function (data) { + SchemaDesignerV2.updateSchemaObject($scope.currentSchema, { + schemaVersion: $scope.schemaVersion, + schemaDesignerUpdateRequestBody: putData + }, function (error, data, response) { + $timeout(function () { + if (error) { + $scope.errorHandlerV2(response); + return; + } - var nodeType = data.updateType; - $scope.schemaVersion = data.schemaVersion; - $scope.currentSchema = data.configSet; - $scope.core = data.core; + var nodeType = data.updateType; + $scope.schemaVersion = data.schemaVersion; + $scope.currentSchema = data.configSet; + $scope.core = data.core; - $scope.selectedNode = data[nodeType]; - $scope.selectedNode.href = href; - $scope.selectedNode.id = id; + $scope.selectedNode = data[nodeType]; + $scope.selectedNode.href = href; + $scope.selectedNode.id = id; - var name = nodeType === "field" ? $scope.selectedNode.type : $scope.selectedNode.name; - $scope.initTypeAnalysisInfo(name, "type"); - $scope.showFieldDetails = true; + var name = nodeType === "field" ? $scope.selectedNode.type : $scope.selectedNode.name; + $scope.initTypeAnalysisInfo(name, "type"); + $scope.showFieldDetails = true; - if (nodeType === "field" && $scope.selectedNode.tokenized) { - $scope.showAnalysis = true; - $scope.updateSampleDocId(); - } + if (nodeType === "field" && $scope.selectedNode.tokenized) { + $scope.showAnalysis = true; + $scope.updateSampleDocId(); + } - $scope.onSchemaUpdated($scope.currentSchema, data, href); - }, $scope.errorHandler); + $scope.onSchemaUpdated($scope.currentSchema, data, href); + }); + }); }; // TODO: These are copied from analysis.js, so move to a shared location for both vs. duplicating @@ -1489,34 +1563,38 @@ solrAdminApp.controller('SchemaDesignerController', function ($scope, $timeout, }; $scope.doPublish = function () { - var params = { - path: "publish", - configSet: $scope.currentSchema, + var opts = { schemaVersion: $scope.schemaVersion, reloadCollections: $scope.reloadOnPublish, cleanupTemp: true, disableDesigner: $scope.disableDesigner }; if ($scope.newCollection && $scope.newCollection.name) { - params.newCollection = $scope.newCollection.name; - params.numShards = $scope.newCollection.numShards; - params.replicationFactor = $scope.newCollection.replicationFactor; - params.indexToCollection = $scope.newCollection.indexToCollection; - } - SchemaDesigner.put(params, null, function (data) { - $scope.schemaVersion = data.schemaVersion; - $scope.currentSchema = data.configSet; + opts.newCollection = $scope.newCollection.name; + opts.numShards = $scope.newCollection.numShards; + opts.replicationFactor = $scope.newCollection.replicationFactor; + opts.indexToCollection = $scope.newCollection.indexToCollection; + } + SchemaDesignerV2.publish($scope.currentSchema, opts, function (error, data, response) { + $timeout(function () { + if (error) { + $scope.errorHandlerV2(response); + return; + } + $scope.schemaVersion = data.schemaVersion; + $scope.currentSchema = data.configSet; - delete $scope.selectedNode; - $scope.currentSchema = ""; - delete $scope.newSchema; - $scope.showPublish = false; - $scope.refresh(); + delete $scope.selectedNode; + $scope.currentSchema = ""; + delete $scope.newSchema; + $scope.showPublish = false; + $scope.refresh(); - if (data.newCollection) { - $window.location.href = "#/" + data.newCollection + "/collection-overview"; - } - }, $scope.errorHandler); + if (data.newCollection) { + $window.location.href = "#/" + data.newCollection + "/collection-overview"; + } + }); + }); }; $scope.downloadConfig = function () { diff --git a/solr/webapp/web/js/angular/services.js b/solr/webapp/web/js/angular/services.js index f75a2f9d3c6c..1272fceff94f 100644 --- a/solr/webapp/web/js/angular/services.js +++ b/solr/webapp/web/js/angular/services.js @@ -148,6 +148,12 @@ solrAdminServices.factory('Metrics', delete solrApi.ApiClient.instance.defaultHeaders["User-Agent"]; return new solrApi.SegmentsApi(); }) +.factory('SchemaDesignerV2', + function() { + solrApi.ApiClient.instance.basePath = '/api'; + delete solrApi.ApiClient.instance.defaultHeaders["User-Agent"]; + return new solrApi.SchemaDesignerApi(); + }) .factory('Collections', ['$resource', function ($resource) { // v2 ClusterAPI (/api/cluster) delegates straight through to the same v1 CollectionsHandler @@ -208,7 +214,7 @@ solrAdminServices.factory('Metrics', // v2 NodeThreadsAPI (/api/node/threads) still just delegates straight through to the same v1 // ThreadDumpHandler, so the response shape is byte-identical -- no generated solrApi client // class exists for it (it predates the OpenAPI-based v2 API framework), so this stays a plain - // $resource, like SchemaDesigner/Security. + // $resource, like Security and (partially) SchemaDesigner. return $resource('/api/node/threads', {'wt':'json', '_':Date.now()}); }]) .factory('Replication', @@ -364,11 +370,16 @@ solrAdminServices.factory('Metrics', }]) .factory('SchemaDesigner', ['$resource', function($resource) { + // Schema Designer's analyze (sample-doc upload/paste, dynamic content-type) and query + // (arbitrary forwarded Solr query params) endpoints read their request bodies/params in ways + // the OpenAPI-generated SchemaDesignerApi client can't express: analyze() always sends a null + // body (the server deliberately reads the raw content stream, dispatched by Content-Type, + // rather than a formal parameter) and query() takes no query params at all (the server + // forwards arbitrary SolrParams straight through). Both stay on this plain $resource, like + // Threads/Collections/ParamSet. Every other Schema Designer endpoint uses SchemaDesignerV2. return $resource('/api/schema-designer/:configSet/:path', {wt: 'json', path: '@path', configSet: '@configSet', filePath: '@filePath', _:Date.now()}, { get: {method: "GET"}, post: {method: "POST", timeout: 90000}, - put: {method: "PUT"}, - delete: {method: "DELETE"}, postXml: {headers: {'Content-type': 'text/xml'}, method: "POST", timeout: 90000}, postCsv: {headers: {'Content-type': 'application/csv'}, method: "POST", timeout: 90000}, upload: {method: "POST", transformRequest: angular.identity, headers: {'Content-Type': undefined}, timeout: 90000} diff --git a/solr/webapp/web/partials/schema-designer.html b/solr/webapp/web/partials/schema-designer.html index 4d7fbd5b4b38..c5c0e1b6e7e0 100644 --- a/solr/webapp/web/partials/schema-designer.html +++ b/solr/webapp/web/partials/schema-designer.html @@ -487,7 +487,9 @@

Sample Documents

{{sampleMessage}}

- +