| /* |
| Licensed to the Apache Software Foundation (ASF) under one or more |
| contributor license agreements. See the NOTICE file distributed with |
| this work for additional information regarding copyright ownership. |
| The ASF licenses this file to You under the Apache License, Version 2.0 |
| (the "License"); you may not use this file except in compliance with |
| the License. You may obtain a copy of the License at |
| |
| http://www.apache.org/licenses/LICENSE-2.0 |
| |
| Unless required by applicable law or agreed to in writing, software |
| distributed under the License is distributed on an "AS IS" BASIS, |
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| See the License for the specific language governing permissions and |
| limitations under the License. |
| */ |
| |
| solrAdminApp.controller('SchemaDesignerController', function ($scope, $timeout, $cookies, $window, Constants, SchemaDesigner, Luke) { |
| $scope.resetMenu("schema-designer", Constants.IS_ROOT_PAGE); |
| |
| $scope.schemas = []; |
| $scope.publishedSchemas = []; |
| $scope.sampleDocIds = []; |
| $scope.sortableFields = []; |
| $scope.hlFields = []; |
| $scope.types = []; |
| |
| $scope.onWarning = function (warnMsg, warnDetails) { |
| $scope.updateWorking = false; |
| delete $scope.updateStatusMessage; |
| $scope.apiWarning = warnMsg; |
| $scope.apiWarningDetails = warnDetails; |
| }; |
| |
| $scope.onError = function (errorMsg, errorCode, errorDetails) { |
| $scope.updateWorking = false; |
| delete $scope.updateStatusMessage; |
| $scope.designerAPIError = errorMsg; |
| if (errorDetails) { |
| var errorDetailsStr = ""; |
| if (errorDetails["error"]) { |
| errorDetailsStr = errorDetails["error"]; |
| } else { |
| for (var id in errorDetails) { |
| var msg = errorDetails[id]; |
| var at = msg.indexOf("ERROR: "); |
| if (at !== -1) { |
| msg = msg.substring(at+7); |
| } |
| if (!msg.includes(id)) { |
| msg = id+": "+msg; |
| } |
| errorDetailsStr += msg+"\n\n"; |
| } |
| } |
| $scope.designerAPIErrorDetails = errorDetailsStr; |
| } else { |
| delete $scope.designerAPIErrorDetails; |
| } |
| |
| if (errorCode === 409) { |
| $scope.schemaVersion = -1; // reset to get the latest |
| $scope.isVersionMismatch = true; |
| $scope.errorButton = "Reload Schema"; |
| } else if (errorCode < 500) { |
| $scope.isVersionMismatch = false; |
| $scope.errorButton = "OK"; |
| } // else 500 errors get the top-level error message |
| }; |
| |
| $scope.errorHandler = function (e) { |
| var error = e.data && e.data.error ? e.data.error : null; |
| if (error) { |
| $scope.onError(error.msg, error.code, e.data.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.closeWarnDialog = function () { |
| delete $scope.apiWarning; |
| delete $scope.apiWarningDetails; |
| }; |
| |
| $scope.closeErrorDialog = function () { |
| delete $scope.designerAPIError; |
| delete $scope.designerAPIErrorDetails; |
| if ($scope.isVersionMismatch) { |
| $scope.isVersionMismatch = false; |
| var nodeId = "/"; |
| if ($scope.selectedNode) { |
| nodeId = $scope.selectedNode.href; |
| } |
| $scope.doAnalyze(nodeId); |
| } |
| }; |
| |
| $scope.refresh = function () { |
| $scope.isSchemaDesignerEnabled = true; |
| |
| delete $scope.helpId; |
| |
| $scope.updateStatusMessage = ""; |
| $scope.analysisVerbose = false; |
| $scope.updateWorking = false; |
| $scope.currentSchema = ""; |
| |
| delete $scope.hasDocsOnServer; |
| delete $scope.queryResultsTree; |
| |
| $scope.languages = ["*"]; |
| $scope.copyFrom = "_default"; |
| delete $scope.sampleMessage; |
| delete $scope.sampleDocuments; |
| |
| $scope.schemaVersion = -1; |
| $scope.schemaTree = {}; |
| $scope.showSchemaActions = false; |
| $scope.sampleDocIds = []; |
| $scope.isSchemaRoot = false; |
| |
| delete $scope.enableNestedDocs; |
| delete $scope.enableDynamicFields; |
| delete $scope.enableFieldGuessing; |
| |
| // schema editor |
| $scope.showFieldDetails = false; |
| $scope.selectedNode = null; |
| $scope.selectedUpdated = false; |
| delete $scope.updateStatusMessage; |
| |
| // text field analysis |
| $scope.showAnalysis = false; |
| $scope.sampleDocId = null; |
| $scope.indexText = ""; |
| $scope.result = {}; |
| |
| // publish vars |
| delete $scope.newCollection; |
| $scope.reloadOnPublish = "true"; |
| |
| // query form |
| $scope.query = {q: '*:*', sortBy: 'score', sortDir: 'desc'}; |
| |
| SchemaDesigner.get({path: "configs"}, function (data) { |
| |
| $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); |
| } |
| |
| // 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(); |
| |
| // 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(); |
| } |
| }); |
| }; |
| |
| $scope.selectNodeInTree = function(nodeId) { |
| nodeId = stripAnchorSuffix(nodeId); |
| if (!nodeId) return; |
| |
| var jst = $('#schemaJsTree').jstree(true); |
| if (jst) { |
| var selectedId = null; |
| var selected_node = jst.get_selected(); |
| if (selected_node && selected_node.length > 0) { |
| selectedId = selected_node[0]; |
| } |
| if (selectedId) { |
| try { |
| jst.deselect_node(selectedId); |
| } catch (err) { |
| // just ignore |
| //console.log("error deselecting "+selectedId); |
| } |
| } |
| |
| try { |
| jst.select_node(nodeId, true); |
| } catch (err) { |
| // just ignore, some low-level tree issue |
| //console.log("error selecting "+nodeId); |
| } |
| } |
| }; |
| |
| $scope.loadFile = function (event) { |
| var t = event.target || event.srcElement || event.currentTarget; |
| if (t && t.text) { |
| $scope.onSelectFileNode("files/" + t.text, true); |
| } |
| }; |
| |
| $scope.confirmEditSchema = function () { |
| $scope.showConfirmEditSchema = false; |
| if ($scope.hasDocsOnServer || $scope.published) { |
| $scope.doAnalyze(); |
| } else { |
| $scope.sampleMessage = "Please upload or paste some sample documents to analyze for building the '" + $scope.currentSchema + "' schema."; |
| } |
| }; |
| |
| $scope.cancelEditSchema = function () { |
| $scope.currentSchema = ""; |
| $scope.showConfirmEditSchema = false; |
| }; |
| |
| $scope.loadSchema = function () { |
| |
| if (!$scope.currentSchema) { |
| return; |
| } |
| |
| $scope.resetSchema(); |
| var params = {path: "info", 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(); |
| } else { |
| $scope.sampleMessage = "Please upload or paste some sample documents to build the '" + $scope.currentSchema + "' schema."; |
| } |
| } |
| }); |
| }; |
| |
| $scope.showNewSchemaDialog = function () { |
| $scope.hideAll(); |
| $scope.showNewSchema = true; |
| $scope.newSchema = ""; |
| }; |
| |
| $scope.addSchema = function () { |
| $scope.firstSchemaMessage = false; |
| delete $scope.addMessage; |
| |
| if (!$scope.newSchema) { |
| $scope.addMessage = "Please provide a schema name!"; |
| return; |
| } |
| |
| $scope.newSchema = $scope.newSchema.trim(); |
| if ($scope.newSchema.length > 50) { |
| $scope.addMessage = "Schema name be 50 characters or less"; |
| return; |
| } |
| |
| if ($scope.newSchema.indexOf(" ") !== -1 || $scope.newSchema.indexOf("/") !== -1) { |
| $scope.addMessage = "Schema name should not contain spaces or /"; |
| return; |
| } |
| |
| if ($scope.publishedSchemas.includes($scope.newSchema) || $scope.schemas.includes($scope.newSchema)) { |
| $scope.addMessage = "Schema '" + $scope.newSchema + "' already exists!"; |
| return; |
| } |
| |
| delete $scope.addMessage; |
| if (!$scope.copyFrom) { |
| $scope.copyFrom = "_default"; |
| } |
| |
| $scope.resetSchema(); |
| $scope.schemas.push($scope.newSchema); |
| $scope.showNewSchema = false; |
| $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.cancelAddSchema = function () { |
| delete $scope.addMessage; |
| delete $scope.sampleMessage; |
| |
| $scope.showNewSchema = false |
| }; |
| |
| $scope.hideAll = function () { |
| delete $scope.helpId; |
| $scope.showPublish = false; |
| $scope.showDiff = false; |
| $scope.showNewSchema = false; |
| $scope.showAddField = false; |
| $scope.showAddDynamicField = false; |
| $scope.showAddCopyField = false; |
| $scope.showAnalysis = false; |
| // add more dialogs here |
| }; |
| |
| $scope.showHelp = function (id) { |
| if ($scope.helpId && ($scope.helpId === id || id === '')) { |
| delete $scope.helpId; |
| } else { |
| $scope.helpId = id; |
| } |
| }; |
| |
| $scope.hideData = function () { |
| $scope.showData = false; |
| }; |
| |
| $scope.rootChanged = function () { |
| $scope.selectedUpdated = true; |
| $scope.selectedType = "Schema"; |
| }; |
| |
| $scope.updateUniqueKey = function () { |
| delete $scope.schemaRootMessage; |
| var jst = $('#schemaJsTree').jstree(); |
| if (jst) { |
| var node = jst.get_node("field/" + $scope.updateUniqueKeyField); |
| if (node && node.a_attr) { |
| var attrs = node.a_attr; |
| if (attrs.multiValued || attrs.tokenized || !attrs.stored || !attrs.indexed) { |
| $scope.schemaRootMessage = "Field '" + $scope.updateUniqueKeyField + |
| "' cannot be used as the uniqueKey field! Must be single-valued, stored, indexed, and not tokenized."; |
| $scope.updateUniqueKeyField = $scope.uniqueKeyField; |
| return; |
| } |
| } |
| } |
| $scope.uniqueKeyField = $scope.updateUniqueKeyField; |
| $scope.selectedUpdated = true; |
| $scope.selectedType = "Schema"; |
| }; |
| |
| $scope.resetSchema = function () { |
| $scope.hideAll(); |
| $scope.analysisVerbose = false; |
| $scope.showSchemaActions = false; |
| $scope.showAnalysis = false; |
| $scope.showFieldDetails = false; |
| $scope.hasDocsOnServer = false; |
| $scope.query = {q: '*:*', sortBy: 'score', sortDir: 'desc'}; |
| $scope.schemaVersion = -1; |
| |
| $scope.updateWorking = false; |
| $scope.isVersionMismatch = false; |
| delete $scope.updateStatusMessage; |
| delete $scope.designerAPIError; |
| delete $scope.designerAPIErrorDetails; |
| delete $scope.selectedFacets; |
| delete $scope.sampleDocuments; |
| delete $scope.selectedNode; |
| delete $scope.queryResultsTree; |
| }; |
| |
| $scope.onSchemaUpdated = function (schema, data, nodeId) { |
| $scope.hasDocsOnServer = data.numDocs && data.numDocs > 0; |
| $scope.uniqueKeyField = data.uniqueKeyField; |
| $scope.updateUniqueKeyField = $scope.uniqueKeyField; |
| $scope.initDesignerSettingsFromResponse(data); |
| |
| var fieldTypes = fieldTypesToTree(data.fieldTypes); |
| var files = filesToTree(data.files); |
| |
| var rootChildren = []; |
| $scope.fieldsSrc = fieldsToTree(data.fields); |
| $scope.fieldsNode = { |
| "id": "fields", |
| "text": "Fields", |
| "state": {"opened": true}, |
| "a_attr": {"href": "fields"}, |
| "children": $scope.fieldsSrc |
| }; |
| rootChildren.push($scope.fieldsNode); |
| |
| if ($scope.enableDynamicFields === "true") { |
| $scope.dynamicFieldsSrc = fieldsToTree(data.dynamicFields); |
| $scope.dynamicFieldsNode = { |
| "id": "dynamicFields", |
| "text": "Dynamic Fields", |
| "a_attr": {"href": "dynamicFields"}, |
| "children": $scope.dynamicFieldsSrc |
| }; |
| rootChildren.push($scope.dynamicFieldsNode); |
| } else { |
| delete $scope.dynamicFieldsNode; |
| delete $scope.dynamicFieldsSrc; |
| } |
| |
| rootChildren.push({"id":"fieldTypes", "text": "Field Types", "a_attr": {"href": "fieldTypes"}, "children": fieldTypes}); |
| rootChildren.push(files); |
| |
| var tree = [{"id":"/", "text": schema, "a_attr": {"href": "/"}, "state":{"opened": true}, "children": rootChildren}]; |
| |
| $scope.fields = data.fields; |
| $scope.fieldNames = data.fields.map(f => f.name).sort(); |
| $scope.possibleIdFields = data.fields.filter(f => f.indexed && f.stored && !f.tokenized).map(f => f.name).sort(); |
| $scope.sortableFields = data.fields.filter(f => (f.indexed || f.docValues) && !f.multiValued && !f.tokenized).map(f => f.name).sort(); |
| $scope.sortableFields.push("score"); |
| |
| $scope.facetFields = data.fields.filter(f => (f.indexed || f.docValues) && !f.tokenized && f.name !== '_version_').map(f => f.name).sort(); |
| $scope.hlFields = data.fields.filter(f => f.stored && f.tokenized).map(f => f.name).sort(); |
| |
| $scope.schemaVersion = data.schemaVersion; |
| $scope.currentSchema = data.configSet; |
| $scope.fieldTypes = fieldTypes; |
| $scope.core = data.core; |
| $scope.schemaTree = tree; |
| $scope.refreshTree(); |
| |
| $scope.collectionsForConfig = data.collectionsForConfig; |
| |
| if (data.docIds) { |
| $scope.sampleDocIds = data.docIds; |
| } else { |
| $scope.sampleDocIds = []; |
| } |
| |
| // Load the Luke schema |
| Luke.schema({core: data.core}, function (schema) { |
| Luke.raw({core: data.core}, function (index) { |
| $scope.luke = mergeIndexAndSchemaData(index, schema.schema); |
| $scope.types = Object.keys(schema.schema.types); |
| $scope.showSchemaActions = true; |
| if (!nodeId) { |
| nodeId = "/"; |
| } |
| // re-apply the filters on the updated schema |
| $scope.applyTreeFilterOption(); |
| $scope.onSelectSchemaTreeNode(nodeId); |
| |
| $scope.updateWorking = false; |
| |
| if (data.updateError != null) { |
| $scope.onError(data.updateError, data.updateErrorCode, data.errorDetails); |
| } else { |
| if ($scope.selectedUpdated) { |
| $scope.selectedUpdated = false; |
| $scope.updateStatusMessage = "Changes applied successfully."; |
| var waitMs = 3000; |
| if (data.rebuild) { |
| $scope.updateStatusMessage += " Did full re-index of sample docs due to incompatible update."; |
| waitMs = 5000; // longer message, more time to read |
| } |
| |
| if (data.analysisError) { |
| var updateType = data["updateType"]; |
| var updatedObject = data[updateType]; |
| var updatedName = updatedObject && updatedObject.name ? updatedObject.name : ""; |
| var warnMsg = "Changes to "+updateType+" "+updatedName+" applied, but required the temp collection to be deleted " + |
| "and re-created due to an incompatible Lucene change, see details below."; |
| $scope.onWarning(warnMsg, data.analysisError); |
| } else { |
| $timeout(function () { delete $scope.updateStatusMessage; }, waitMs); |
| } |
| } else { |
| var source = data.sampleSource; |
| if (source) { |
| if (source === "paste") { |
| source = "pasted sample" |
| } else if (source === "blob") { |
| source = "previous upload stored on the server" |
| } |
| if (data.numDocs > 0) { |
| $scope.updateStatusMessage = "Analyzed "+data.numDocs+" docs from "+source; |
| } else { |
| $scope.updateStatusMessage = "Schema '"+$scope.currentSchema+"' loaded."; |
| } |
| } |
| $timeout(function () { |
| delete $scope.updateStatusMessage; |
| }, 5000); |
| } |
| } |
| |
| // re-fire the current query to reflect the updated schema |
| $scope.doQuery(); |
| $scope.selectNodeInTree(nodeId); |
| }); |
| }); |
| }; |
| |
| $scope.toggleAddField = function (type) { |
| if ($scope.showAddField) { |
| $scope.hideAll(); |
| } else { |
| $scope.hideAll(); |
| $scope.showAddField = true; |
| $scope.adding = type; |
| |
| $scope.newField = { |
| stored: "true", |
| indexed: "true", |
| uninvertible: "true", |
| docValues: "true" |
| } |
| |
| if (type === "field") { |
| $scope.newField.type = "string"; |
| } |
| |
| delete $scope.addErrors; |
| } |
| }; |
| |
| function applyConstraintsOnField(f) { |
| |
| if (!f.docValues) { |
| delete f.useDocValuesAsStored; |
| } |
| |
| if (!f.docValues && !f.uninvertible) { |
| delete f.sortMissingLast; // remove this setting if no docValues / uninvertible |
| } |
| |
| if (f.indexed) { |
| if (f.omitTermFreqAndPositions && !f.omitPositions) { |
| delete f.omitPositions; // :shrug ~ see SchemaField ln 295 |
| } |
| if (!f.termVectors) { |
| delete f.termPositions; |
| delete f.termOffsets; |
| delete f.termPayloads; |
| } |
| } else { |
| // if not indexed, a bunch of fields are false |
| f.tokenized = false; |
| f.uninvertible = false; |
| |
| // drop these from the request |
| delete f.termVectors; |
| delete f.termPositions; |
| delete f.termOffsets; |
| delete f.termPayloads; |
| delete f.omitNorms; |
| delete f.omitPositions; |
| delete f.omitTermFreqAndPositions; |
| delete f.storeOffsetsWithPositions; |
| } |
| |
| return f; |
| } |
| |
| $scope.addField = function () { |
| |
| // validate the form input |
| $scope.addErrors = []; |
| if (!$scope.newField.name) { |
| $scope.addErrors.push($scope.adding + " name is required!"); |
| } |
| |
| if ($scope.newField.name.indexOf(" ") != -1) { |
| $scope.addErrors.push($scope.adding + " name should not have whitespace"); |
| } |
| |
| var command = "add-field-type"; |
| if ("field" === $scope.adding) { |
| |
| if ($scope.fieldNames.includes($scope.newField.name)) { |
| $scope.addErrors.push("Field '" + $scope.newField.name + "' already exists!"); |
| return; |
| } |
| |
| // TODO: is this the correct logic for detecting dynamic? Probably good enough for the designer |
| var isDynamic = $scope.newField.name.startsWith("*") || $scope.newField.name.endsWith("*"); |
| if (isDynamic) { |
| if ($scope.luke && $scope.luke.dynamic_fields[$scope.newField.name]) { |
| $scope.addErrors.push("dynamic field '" + $scope.newField.name + "' already exists!"); |
| } |
| } else { |
| if ($scope.luke && $scope.luke.fields[$scope.newField.name]) { |
| $scope.addErrors.push("field '" + $scope.newField.name + "' already exists!"); |
| } |
| } |
| |
| if (!$scope.newField.type) { |
| $scope.addErrors.push("field type is required!"); |
| } |
| |
| command = isDynamic ? "add-dynamic-field" : "add-field"; |
| } else if ("type" === $scope.adding) { |
| if ($scope.types.includes($scope.newField.name)) { |
| $scope.addErrors.push("Type '" + $scope.newField.name + "' already exists!"); |
| } |
| |
| if (!$scope.newField.class) { |
| $scope.addErrors.push("class is required when creating a new field type!"); |
| } |
| } |
| |
| var addData = {}; |
| addData[command] = applyConstraintsOnField($scope.newField); |
| if ($scope.textAnalysisJson) { |
| var text = $scope.textAnalysisJson.trim(); |
| if (text.length > 0) { |
| text = text.replace(/\s+/g, ' '); |
| if (!text.startsWith("{")) { |
| text = "{ " + text + " }"; |
| } |
| try { |
| var textJson = JSON.parse(text); |
| if (textJson.analyzer) { |
| addData[command].analyzer = textJson.analyzer; |
| } else { |
| if (!textJson.indexAnalyzer || !textJson.queryAnalyzer) { |
| $scope.addErrors.push("Text analysis JSON should define either an 'analyzer' or an 'indexAnalyzer' and 'queryAnalyzer'"); |
| return; |
| } |
| addData[command].indexAnalyzer = textJson.indexAnalyzer; |
| addData[command].queryAnalyzer = textJson.queryAnalyzer; |
| } |
| } catch (e) { |
| $scope.addErrors.push("Failed to parse analysis as JSON due to: " + e.message + |
| "; expected JSON object with either an 'analyzer' or 'indexAnalyzer' and 'queryAnalyzer'"); |
| return; |
| } |
| } |
| } |
| |
| if ($scope.addErrors.length > 0) { |
| return; |
| } |
| delete $scope.addErrors; // no errors! |
| |
| SchemaDesigner.post({ |
| path: "add", |
| 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]; |
| } |
| } else { |
| delete $scope.textAnalysisJson; |
| $scope.added = true; |
| $timeout(function () { |
| $scope.showAddField = false; |
| $scope.added = false; |
| var nodeId = "/"; |
| if ("field" === $scope.adding) { |
| nodeId = "field/" + data[command]; |
| } else if ("type" === $scope.adding) { |
| nodeId = "type/" + data[command]; |
| } |
| $scope.onSchemaUpdated(data.configSet, data, nodeId); |
| }, 500); |
| } |
| }, $scope.errorHandler); |
| } |
| |
| function toSortedNameAndTypeList(fields, typeAttr) { |
| var list = []; |
| var keys = Object.keys(fields); |
| for (var f in keys) { |
| var field = fields[keys[f]]; |
| var type = field[typeAttr]; |
| if (type) { |
| list.push(field.name + ": "+type); |
| } else { |
| list.push(field.name); |
| } |
| } |
| return list.sort(); |
| } |
| |
| function toSortedFieldList(fields) { |
| var list = []; |
| var keys = Object.keys(fields); |
| for (var f in keys) { |
| list.push(fields[keys[f]]); |
| } |
| return list.sort((a, b) => (a.name > b.name) ? 1 : -1); |
| } |
| |
| $scope.toggleDiff = function (event) { |
| if ($scope.showDiff) { |
| // toggle, close dialog |
| $scope.showDiff = false; |
| return; |
| } |
| |
| if (event) { |
| var t = event.target || event.currentTarget; |
| var leftPos = t.getBoundingClientRect().left - 600; |
| if (leftPos < 0) leftPos = 0; |
| $('#show-diff-dialog').css({left: leftPos}); |
| } |
| |
| SchemaDesigner.get({ path: "diff", configSet: $scope.currentSchema }, function (data) { |
| var diff = data.diff; |
| |
| 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.schemaDiffExists = !(diff.fields == null && diff.fieldTypes == null && dynamicFields == null && diff.copyFields == null); |
| $scope.showDiff = true; |
| }); |
| } |
| |
| $scope.togglePublish = function (event) { |
| if (event) { |
| var t = event.target || event.currentTarget; |
| var leftPos = t.getBoundingClientRect().left - 515; |
| if (leftPos < 0) leftPos = 0; |
| $('#publish-dialog').css({left: leftPos}); |
| } |
| |
| $scope.showDiff = false; |
| $scope.showPublish = !$scope.showPublish; |
| delete $scope.publishErrors; |
| |
| $scope.disableDesigner = "false"; |
| |
| if ($scope.showPublish && !$scope.newCollection) { |
| $scope.newCollection = {numShards: 1, replicationFactor: 1, indexToCollection: "true"}; |
| } |
| }; |
| |
| $scope.toggleAddCopyField = function () { |
| if ($scope.showAddCopyField) { |
| $scope.hideAll(); |
| $scope.showFieldDetails = true; |
| } else { |
| $scope.hideAll(); |
| $scope.showAddCopyField = true; |
| $scope.showFieldDetails = false; |
| |
| $scope.copyField = {}; |
| delete $scope.addCopyFieldErrors; |
| } |
| } |
| $scope.addCopyField = function () { |
| delete $scope.addCopyFieldErrors; |
| var data = {"add-copy-field": $scope.copyField}; |
| SchemaDesigner.post({ |
| path: "add", |
| 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]; |
| } |
| } else { |
| $scope.showAddCopyField = false; |
| // TODO: |
| //$timeout($scope.refresh, 1500); |
| } |
| }, $scope.errorHandler); |
| } |
| |
| $scope.toggleAnalyzer = function (analyzer) { |
| analyzer.show = !analyzer.show; |
| } |
| |
| $scope.initTypeAnalysisInfo = function (typeName) { |
| $scope.analysis = getAnalysisInfo($scope.luke, {type: true}, typeName); |
| if ($scope.analysis && $scope.analysis.data) { |
| $scope.className = $scope.analysis.data.className |
| } |
| $scope.editAnalysis = "Edit JSON"; |
| $scope.showAnalysisJson = false; |
| delete $scope.analysisJsonText; |
| }; |
| |
| $scope.toggleVerbose = function () { |
| $scope.analysisVerbose = !$scope.analysisVerbose; |
| }; |
| |
| $scope.updateSampleDocId = function () { |
| $scope.indexText = ""; |
| $scope.result = {}; |
| |
| if (!$scope.selectedNode) { |
| return; |
| } |
| |
| var field = $scope.selectedNode.name; |
| var params = {path: "sample"}; |
| params.configSet = $scope.currentSchema; |
| params.uniqueKeyField = $scope.uniqueKeyField; |
| params.field = field; |
| |
| if ($scope.sampleDocId) { |
| params.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.changeLanguages = function () { |
| $scope.selectedUpdated = true; |
| $scope.selectedType = "Schema"; |
| }; |
| |
| function getType(typeName) { |
| if ($scope.fieldTypes) { |
| for (i in $scope.fieldTypes) { |
| if ($scope.fieldTypes[i].text === typeName) { |
| return $scope.fieldTypes[i]; |
| } |
| } |
| } |
| return null; |
| } |
| |
| $scope.refreshTree = function() { |
| var jst = $('#schemaJsTree').jstree(true); |
| if (jst) { |
| jst.refresh(); |
| } |
| }; |
| |
| $scope.onSchemaTreeLoaded = function (id) { |
| //console.log(">> on tree loaded"); |
| }; |
| |
| $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.post(params, $scope.fileNodeText, function (data) { |
| if (data.updateFileError) { |
| if (data[$scope.selectedFile]) { |
| $scope.fileNodeText = data[$scope.selectedFile]; |
| } |
| $scope.updateFileError = data.updateFileError; |
| } else { |
| delete $scope.updateFileError; |
| $scope.updateStatusMessage = "File '"+$scope.selectedFile+"' updated."; |
| $scope.onSchemaUpdated(data.configSet, data, nodeId); |
| } |
| }, $scope.errorHandler); |
| }; |
| |
| $scope.onSelectFileNode = function (id, doSelectOnTree) { |
| $scope.selectedFile = id.startsWith("files/") ? id.substring("files/".length) : id; |
| |
| var params = {path: "file", file: $scope.selectedFile, configSet: $scope.currentSchema}; |
| SchemaDesigner.get(params, function (data) { |
| $scope.fileNodeText = data[$scope.selectedFile]; |
| $scope.isLeafNode = false; |
| if (doSelectOnTree) { |
| delete $scope.selectedNode; |
| $scope.isLeafNode = false; |
| $scope.showFieldDetails = true; |
| delete $scope.sampleDocId; |
| $scope.showAnalysis = false; |
| $scope.selectNodeInTree(id); |
| } |
| }); |
| }; |
| |
| function fieldNodes(src, type) { |
| var nodes = []; |
| for (var c in src) { |
| var childNode = src[c]; |
| if (childNode && childNode.a_attr) { |
| var a = childNode.a_attr; |
| var stored = a.stored || (a.docValues && a.useDocValuesAsStored); |
| var obj = {"name":a.name, "indexed":a.indexed, "docValues": a.docValues, |
| "multiValued":a.multiValued, "stored":stored, "tokenized": a.tokenized}; |
| if (type === "field" || type === "dynamicField") { |
| obj.type = a.type; |
| } else if (type === "type") { |
| obj.class = a.class; |
| } |
| nodes.push(obj); |
| } |
| } |
| return nodes; |
| } |
| |
| function stripAnchorSuffix(id) { |
| if (id && id.endsWith("_anchor")) { |
| id = id.substring(0, id.length - "_anchor".length); |
| } |
| return id; |
| } |
| |
| $scope.onSelectSchemaTreeNode = function (id) { |
| id = stripAnchorSuffix(id); |
| $scope.showFieldDetails = false; |
| $scope.isSchemaRoot = false; |
| $scope.isLeafNode = false; |
| delete $scope.containerNodeLabel; |
| delete $scope.containerNode; |
| delete $scope.containerNodes; |
| delete $scope.selectedFile; |
| |
| if (id === "/") { |
| $scope.selectedType = "Schema"; |
| $scope.selectedNode = null; |
| $scope.isSchemaRoot = true; |
| $scope.isLeafNode = false; |
| $scope.isContainerNode = false; |
| $scope.showFieldDetails = true; |
| delete $scope.sampleDocId; |
| $scope.showAnalysis = false; |
| |
| if (!$scope.treeFilter) { |
| $scope.treeFilter = "type"; |
| $scope.treeFilterOption = "*"; |
| $scope.initTreeFilters(); |
| } |
| return; |
| } |
| |
| var jst = $('#schemaJsTree').jstree(true); |
| if (!jst) { |
| return; |
| } |
| |
| var node = jst.get_node(id); |
| if (!node) { |
| return; |
| } |
| |
| if (id === "files") { |
| $scope.selectedNode = null; |
| $scope.isLeafNode = false; |
| return; |
| } |
| |
| if (id.indexOf("/") === -1) { |
| $scope.selectedNode = null; |
| $scope.isLeafNode = false; |
| $scope.containerNode = id; |
| |
| if (id === "fields") { |
| $scope.containerNodes = fieldNodes($scope.fieldsNode ? $scope.fieldsNode.children : $scope.fieldsSrc, "field"); |
| } else if (id === "dynamicFields") { |
| $scope.containerNodes = fieldNodes($scope.dynamicFieldsNode ? $scope.dynamicFieldsNode.children : $scope.dynamicFieldsSrc, "dynamicField"); |
| } else if (id === "fieldTypes") { |
| $scope.containerNodes = fieldNodes($scope.fieldTypes, "type"); |
| } |
| |
| $scope.containerNodeLabel = node.text; |
| $scope.showFieldDetails = true; |
| delete $scope.sampleDocId; |
| $scope.showAnalysis = false; |
| return; |
| } |
| |
| if (id.startsWith("files/")) { |
| $scope.selectedNode = null; |
| $scope.isLeafNode = false; |
| delete $scope.sampleDocId; |
| $scope.showAnalysis = false; |
| if (node.children.length === 0) { |
| // file |
| $scope.showFieldDetails = true; |
| $scope.onSelectFileNode(id, false); |
| } else { |
| // folder |
| $scope.showFieldDetails = false; |
| delete $scope.selectedFile; |
| } |
| return; |
| } |
| |
| delete $scope.selectedFile; |
| $scope.selectedNode = node["a_attr"]; // all the info we need is in the a_attr object |
| if (!$scope.selectedNode) { |
| // a node in the tree that isn't a field was selected, just ignore |
| return; |
| } |
| |
| $scope.selectedNode.fieldType = getType($scope.selectedNode.type); |
| $scope.isLeafNode = true; |
| |
| var nodeType = id.substring(0, id.indexOf("/")); |
| var name = null; |
| if (nodeType === "field") { |
| $scope.selectedType = "Field"; |
| name = $scope.selectedNode.type; |
| } else if (nodeType === "dynamic") { |
| $scope.selectedType = "Dynamic Field"; |
| } else if (nodeType === "type") { |
| $scope.selectedType = "Type"; |
| name = $scope.selectedNode.name; |
| } |
| |
| if (name) { |
| $scope.initTypeAnalysisInfo(name, "type"); |
| } |
| |
| // apply some sanity to the checkboxes |
| $scope.selectedNode = applyConstraintsOnField($scope.selectedNode); |
| $scope.showFieldDetails = true; |
| |
| if (nodeType === "field" && $scope.selectedNode.tokenized && $scope.selectedNode.stored) { |
| $scope.showAnalysis = true; |
| $scope.updateSampleDocId(); |
| } else { |
| $scope.showAnalysis = false; |
| $scope.indexText = ""; |
| $scope.result = {}; |
| } |
| }; |
| |
| function addFileNode(dirs, parent, f) { |
| var path = f.split("/"); |
| if (path.length === 1) { |
| if (!parent.children) { |
| parent.children = []; |
| dirs.push(parent); // now parent has children, so track in dirs ... |
| } |
| var nodeId = parent.id + "/" + f; |
| parent.children.push({"text": f, "id": nodeId, "a_attr": {"href": nodeId}}); |
| } else { |
| // get the parent for this path |
| var parentId = "files/" + path.slice(0, path.length - 1).join("/"); |
| var dir = null; |
| for (var d in dirs) { |
| if (dirs[d].id === parentId) { |
| dir = dirs[d]; |
| break; |
| } |
| } |
| if (!dir) { |
| dir = {"text": path[0], "id": parentId, "a_attr": {"href": parentId}, "children": []}; |
| dirs.push(dir); |
| parent.children.push(dir); |
| } |
| |
| // walk down the next level in this path |
| addFileNode(dirs, dir, path.slice(1).join("/")); |
| } |
| } |
| |
| // transform a flat list structure into the nested tree structure |
| function filesToTree(files) { |
| var filesNode = {"text": "Files", "a_attr": {"href": "files"}, "id": "files", "children": []}; |
| if (files) { |
| var dirs = []; // lookup for known dirs by path since nodes don't keep a ref to their parent node |
| for (var i in files) { |
| // hide the configoverlay.json from the UI |
| if (files[i] === "configoverlay.json") { |
| continue; |
| } |
| |
| addFileNode(dirs, filesNode, files[i]); |
| } |
| delete dirs; |
| } |
| return filesNode; |
| } |
| |
| function fieldsToTree(fields) { |
| var children = []; |
| if (fields) { |
| for (var i in fields) { |
| var id = "field/" + fields[i].name; |
| fields[i].href = id; |
| var text = fields[i].name; |
| if (fields[i].name === $scope.uniqueKeyField) { |
| text += "*"; // unique key field |
| } |
| children.push({"text": text, "a_attr": fields[i], "id": id}); |
| } |
| } |
| return children; |
| } |
| |
| function fieldTypesToTree(types) { |
| var children = []; |
| for (var i in types) { |
| var ft = types[i] |
| var id = "type/" + ft.name; |
| ft.href = id; |
| children.push({"text": ft.name, "a_attr": ft, "id": id}); |
| } |
| return children; |
| } |
| |
| $scope.onSampleDocumentsChanged = function () { |
| $scope.hasDocsOnServer = false; // so the updates get sent on next analyze action |
| }; |
| |
| $scope.initDesignerSettingsFromResponse = function (data) { |
| $scope.enableDynamicFields = data.enableDynamicFields !== null ? "" + data.enableDynamicFields : "true"; |
| $scope.enableFieldGuessing = data.enableFieldGuessing !== null ? "" + data.enableFieldGuessing : "true"; |
| $scope.enableNestedDocs = data.enableNestedDocs !== null ? "" + data.enableNestedDocs : "false"; |
| $scope.languages = data.languages !== null && data.languages.length > 0 ? data.languages : ["*"]; |
| $scope.copyFrom = data.copyFrom !== null ? data.copyFrom : "_default"; |
| }; |
| |
| $scope.doAnalyze = function (nodeId) { |
| delete $scope.sampleMessage; |
| |
| var schema = $scope.currentSchema; |
| if (schema) { |
| delete $scope.copyFrom; |
| } else { |
| schema = $scope.newSchema; |
| if (!$scope.copyFrom) { |
| $scope.copyFrom = "_default"; |
| } |
| } |
| |
| if (!schema) { |
| return; |
| } |
| |
| var params = {path: "analyze", configSet: schema}; |
| if ($scope.schemaVersion && $scope.schemaVersion !== -1) { |
| params.schemaVersion = $scope.schemaVersion; |
| } |
| |
| if ($scope.enableDynamicFields) { |
| params.enableDynamicFields = $scope.enableDynamicFields; |
| } |
| if ($scope.enableFieldGuessing) { |
| params.enableFieldGuessing = $scope.enableFieldGuessing; |
| } |
| if ($scope.enableNestedDocs) { |
| params.enableNestedDocs = $scope.enableNestedDocs; |
| } |
| |
| if ($scope.languages && $scope.languages.length > 0) { |
| params.languages = $scope.languages; |
| } |
| |
| if ($scope.copyFrom) { |
| params.copyFrom = $scope.copyFrom; |
| } |
| |
| $scope.updateWorking = true; |
| if ($scope.selectedUpdated) { |
| $scope.updateStatusMessage = "Applying " + $scope.selectedType + " updates ..." |
| } else { |
| $scope.updateStatusMessage = "Analyzing your sample data, schema will load momentarily ..." |
| } |
| |
| if (!nodeId && $scope.selectedNode) { |
| nodeId = $scope.selectedNode.id; |
| } |
| |
| // a bit tricky ... |
| // so users can upload a file or paste in docs |
| // if they upload a file containing a small (<15K) of text data, then we'll show it in the textarea |
| // they can change the text and re-analyze too |
| // if no changes or nothing uploaded, the server-side uses the latest sample data stored in the blob store |
| if ($scope.fileUpload) { |
| var file = $scope.fileUpload; |
| var fd = new FormData(); |
| fd.append('file', file); |
| SchemaDesigner.upload(params, fd, function (data) { |
| $("#upload-file").val(""); |
| delete $scope.fileUpload; |
| $scope.onSchemaUpdated(schema, data, nodeId); |
| }, $scope.errorHandler); |
| } else { |
| // don't need to keep re-posting the same sample if already stored in the blob store |
| var postData = null; |
| if (!$scope.hasDocsOnServer) { |
| postData = $scope.sampleDocuments; |
| if (!postData && !$scope.published) { |
| return; |
| } |
| } |
| |
| var respHandler = function (data) { |
| $scope.onSchemaUpdated(schema, data, nodeId); |
| }; |
| |
| // TODO: need a better approach to detecting the content type from text content |
| var contentType = "text/plain"; |
| if (postData != null) { |
| var txt = postData.trim(); |
| if ((txt.startsWith("[") && txt.includes("]")) || (txt.startsWith("{") && txt.includes("}"))) { |
| contentType = "application/json" |
| } else if (txt.startsWith("<") || txt.includes("<add>") || txt.includes("<!--")) { |
| contentType = "text/xml"; |
| } else { |
| contentType = "application/csv"; |
| } |
| } |
| |
| if (contentType === "text/xml") { |
| SchemaDesigner.postXml(params, postData, respHandler, $scope.errorHandler); |
| } else if (contentType === "application/csv") { |
| SchemaDesigner.postCsv(params, postData, respHandler, $scope.errorHandler); |
| } else { |
| SchemaDesigner.post(params, postData, respHandler, $scope.errorHandler); |
| } |
| } |
| }; |
| |
| $scope.onFieldTypeChanged = function () { |
| |
| var copyFromType = ["stored", "indexed", "multiValued", "docValues", "useDocValuesAsStored", "tokenized", "uninvertible", "termVectors", "termPositions", "termOffsets", |
| "omitNorms", "omitTermFreqAndPositions", "omitPositions", "storeOffsetsWithPositions"]; |
| |
| var type = $scope.selectedNode.type |
| |
| // when the user updates the selected field's type, we go apply the |
| // new field type's properties to the selected node |
| for (var i in $scope.fieldTypes) { |
| if ($scope.fieldTypes[i].text == type) { |
| var ft = $scope.fieldTypes[i]; |
| $scope.selectedNode.fieldType = ft; |
| for (var i in copyFromType) { |
| var x = copyFromType[i]; |
| if (ft.a_attr[x] !== null) { |
| $scope.selectedNode[x] = ft.a_attr[x]; |
| } else { |
| delete $scope.selectedNode[x]; |
| } |
| } |
| $scope.selectedUpdated = true; |
| break; |
| } |
| } |
| |
| $scope.selectedNode = applyConstraintsOnField($scope.selectedNode); |
| if ($scope.selectedUpdated) { |
| // for luke analysis, we need the type info here, not the specific field into b/c it just changed. |
| $scope.initTypeAnalysisInfo(type, "type"); |
| } |
| }; |
| |
| $scope.isDisabled = function (dep) { |
| if (!$scope.selectedNode) { |
| return false; |
| } |
| |
| if (dep === "termVectors") { |
| // termVectors dependency depends on indexed |
| return !($scope.selectedNode.indexed && $scope.selectedNode.termVectors); |
| } |
| |
| if (dep === "not-text" || dep === "docValues") { |
| if ($scope.selectedNode.fieldType && $scope.selectedNode.fieldType.a_attr.class === "solr.TextField") { |
| // no doc values for TextField |
| return true; |
| } |
| } |
| |
| return $scope.selectedNode[dep] === false; |
| }; |
| |
| // this updates checkboxes based on the current settings |
| $scope.markSelectedUpdated = function (event) { |
| delete $scope.updateStatusMessage; |
| $scope.selectedUpdated = true; // enable the update button for this field |
| }; |
| |
| $scope.deleteSelected = function () { |
| // console.log(">> deleteSelected"); |
| }; |
| |
| $scope.updateSelected = function () { |
| if (!$scope.selectedNode) { |
| |
| if ($scope.selectedUpdated) { |
| // some root level property changed ... re-analyze |
| $scope.doAnalyze("/"); |
| } |
| |
| return; |
| } |
| |
| delete $scope.updateSelectedError; |
| |
| // make a copy for the PUT |
| var href = $scope.selectedNode.href; |
| var id = $scope.selectedNode.id; |
| |
| var putData = JSON.parse(JSON.stringify($scope.selectedNode)); |
| if ($scope.selectedType === "Field" && putData.copyDest) { |
| var fields = putData.copyDest.split(","); |
| for (var f in fields) { |
| var name = fields[f].trim(); |
| if (!$scope.fieldNames.includes(name)) { |
| $scope.updateSelectedError = "Copy to field '" + name + "' doesn't exist!"; |
| return; |
| } |
| if (name === $scope.selectedNode.name) { |
| $scope.updateSelectedError = "Cannot copy a field to itself!"; |
| return; |
| } |
| } |
| } else { |
| delete putData.copyDest; |
| } |
| |
| delete putData.fieldType; |
| delete putData.href; |
| delete putData.id; |
| |
| putData = applyConstraintsOnField(putData); |
| |
| if ($scope.analysisJsonText && !$scope.selectedNode.type) { |
| var text = $scope.analysisJsonText.trim(); |
| if (text.length > 0) { |
| text = text.replace(/\s+/g, ' '); |
| if (!text.startsWith("{")) { |
| text = "{ " + text + " }"; |
| } |
| try { |
| var textJson = JSON.parse(text); |
| if (textJson.analyzer) { |
| putData.analyzer = textJson.analyzer; |
| } else { |
| if (textJson.indexAnalyzer && textJson.queryAnalyzer) { |
| putData.indexAnalyzer = textJson.indexAnalyzer; |
| putData.queryAnalyzer = textJson.queryAnalyzer; |
| } |
| } |
| } catch (e) { |
| $scope.updateSelectedError = "Failed to parse analysis as JSON due to: " + e.message + |
| "; expected JSON object with either an 'analyzer' or 'indexAnalyzer' and 'queryAnalyzer'"; |
| return; |
| } |
| } |
| } |
| |
| $scope.updateWorking = true; |
| $scope.updateStatusMessage = "Updating " + $scope.selectedType + " ..."; |
| |
| SchemaDesigner.put({ |
| path: "update", |
| configSet: $scope.currentSchema, |
| schemaVersion: $scope.schemaVersion |
| }, putData, function (data) { |
| |
| 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; |
| |
| 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(); |
| } |
| |
| $scope.onSchemaUpdated($scope.currentSchema, data, href); |
| }, $scope.errorHandler); |
| }; |
| |
| // TODO: These are copied from analysis.js, so move to a shared location for both vs. duplicating |
| var getShortComponentName = function (longname) { |
| var short = -1 !== longname.indexOf('$') |
| ? longname.split('$')[1] |
| : longname.split('.').pop(); |
| return short.match(/[A-Z]/g).join(''); |
| }; |
| |
| var getCaptionsForComponent = function (data) { |
| var captions = []; |
| for (var key in data[0]) { |
| key = key.replace(/.*#/, ''); |
| if (key != "match" && key != "positionHistory") { |
| captions.push(key.replace(/.*#/, '')); |
| } |
| } |
| return captions; |
| }; |
| |
| var getTokensForComponent = function (data) { |
| var tokens = []; |
| var previousPosition = 0; |
| var index = 0; |
| for (var i in data) { |
| var tokenhash = data[i]; |
| var positionDifference = tokenhash.position - previousPosition; |
| for (var j = positionDifference; j > 1; j--) { |
| tokens.push({position: tokenhash.position - j + 1, blank: true, index: index++}); |
| } |
| |
| var token = {position: tokenhash.position, keys: [], index: index++}; |
| |
| for (key in tokenhash) { |
| if (key == "match" || key == "positionHistory") { |
| //skip, to not display these keys in the UI |
| } else { |
| var tokenInfo = new Object(); |
| tokenInfo.name = key; |
| tokenInfo.value = tokenhash[key]; |
| if ('text' === key || 'raw_bytes' === key) { |
| if (tokenhash.match) { |
| tokenInfo.extraclass = 'match'; //to highlight matching text strings |
| } |
| } |
| token.keys.push(tokenInfo); |
| } |
| } |
| tokens.push(token); |
| previousPosition = tokenhash.position; |
| } |
| return tokens; |
| }; |
| |
| var extractComponents = function (data, result, name) { |
| if (data) { |
| result[name] = []; |
| for (var i = 0; i < data.length; i += 2) { |
| var component = { |
| name: data[i], |
| short: getShortComponentName(data[i]), |
| captions: getCaptionsForComponent(data[i + 1]), |
| tokens: getTokensForComponent(data[i + 1]) |
| }; |
| result[name].push(component); |
| } |
| } |
| }; |
| |
| var processFieldAnalysisData = function (analysis) { |
| var response = {}; |
| extractComponents(analysis.index, response, "index"); |
| return response; |
| }; |
| |
| $scope.doPublish = function () { |
| var params = { |
| path: "publish", |
| configSet: $scope.currentSchema, |
| 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; |
| |
| 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); |
| }; |
| |
| $scope.downloadConfig = function () { |
| // have to use an AJAX request so we can supply the Authorization header |
| if (sessionStorage.getItem("auth.header")) { |
| var fileName = $scope.currentSchema+"_configset.zip"; |
| var xhr = new XMLHttpRequest(); |
| xhr.open("GET", "/api/schema-designer/download/"+fileName+"?wt=raw&configSet="+$scope.currentSchema, true); |
| xhr.setRequestHeader('Authorization', sessionStorage.getItem("auth.header")); |
| xhr.responseType = 'blob'; |
| xhr.addEventListener('load',function() { |
| if (xhr.status === 200) { |
| var url = window.URL.createObjectURL(xhr.response); |
| var a = document.createElement('a'); |
| a.href = url; |
| a.download = fileName; |
| document.body.append(a); |
| a.click(); |
| a.remove(); |
| window.URL.revokeObjectURL(url); |
| } |
| }) |
| xhr.send(); |
| } else { |
| location.href = "/api/schema-designer/download/"+$scope.currentSchema+"_configset.zip?wt=raw&configSet=" + $scope.currentSchema; |
| } |
| }; |
| |
| function docsToTree(docs) { |
| var children = []; |
| for (var i in docs) { |
| var id = docs[i][$scope.uniqueKeyField]; |
| if (!id) { |
| id = "" + i; // id not in results so use the position in results as the value |
| } |
| var nodeId = "doc/" + id; |
| docs[i].href = nodeId; |
| children.push({"text": id, "a_attr": docs[i], "id": nodeId}); |
| } |
| return children; |
| } |
| |
| function debugToTree(debugObj) { |
| var children = []; |
| for (var x in debugObj) { |
| if (typeof debugObj[x] === 'object') { |
| var obj = debugObj[x]; |
| var nodeId = "debug/" + x; |
| var tdata = []; |
| for (var a in obj) { |
| if (typeof obj[a] !== 'object') { |
| tdata.push({name: a, value: obj[a]}); |
| } |
| } |
| children.push({"text": x, "a_attr": {"href": nodeId}, "tdata": tdata, "id": nodeId, "children": []}); |
| } |
| } |
| return children; |
| } |
| |
| function facetsToTree(ff) { |
| var children = []; |
| for (var f in ff) { |
| var nodeId = "facet/" + f; |
| if (ff[f] && ff[f].length > 0) { |
| var facet = ff[f]; |
| var tdata = []; |
| for (let i = 0; i < facet.length; i += 2) { |
| tdata.push({name: facet[i], value: facet[i + 1]}); |
| } |
| children.push({"text": f, "a_attr": {"href": nodeId}, "tdata": tdata, "id": nodeId, "children": []}); |
| } |
| } |
| return children; |
| } |
| |
| function hlToTree(hl) { |
| var children = []; |
| for (var f in hl) { |
| var nodeId = "hl/" + f; |
| var tdata = []; |
| var obj = hl[f]; |
| for (var a in obj) { |
| var v = obj[a]; |
| if (v && v.length > 0) { |
| tdata.push({name: a, value: v[0]}); |
| } |
| } |
| if (tdata.length > 0) { |
| children.push({"text": f, "a_attr": {"href": nodeId}, "tdata": tdata, "id": nodeId, "children": []}); |
| } |
| } |
| return children; |
| } |
| |
| $scope.selectField = function (event) { |
| var t = event.target || event.srcElement || event.currentTarget; |
| if (t && t.text) { |
| var nodeId = "field/" + t.text; |
| $scope.onSelectSchemaTreeNode(nodeId); |
| $scope.selectNodeInTree(nodeId); |
| } |
| }; |
| |
| $scope.selectFieldType = function (event) { |
| var t = event.target || event.srcElement || event.currentTarget; |
| if (t && t.text) { |
| var nodeId = "type/" + t.text; |
| $scope.onSelectSchemaTreeNode(nodeId); |
| $scope.selectNodeInTree(nodeId); |
| } |
| }; |
| |
| function omitVersionField(key,value) { |
| return (key === "_version_") ? undefined : value; |
| } |
| |
| $scope.editDocuments = function() { |
| delete $scope.helpId; |
| if ($scope.queryDocs) { |
| $scope.hasDocsOnServer = false; // so the updated docs apply |
| $("#upload-file").val(""); |
| delete $scope.fileUpload; |
| $scope.sampleDocuments = $scope.queryDocs; |
| $scope.onSampleDocumentsChanged(); |
| } |
| }; |
| |
| $scope.renderResultsTree = function (data) { |
| var h = data.responseHeader; |
| var sort = h.params.sort; |
| if (!sort) { |
| sort = "score desc"; |
| } |
| |
| if (data.response && data.response.docs && data.response.docs.length > 0) { |
| $scope.queryDocs = JSON.stringify(data.response.docs, omitVersionField, 2); |
| } else { |
| delete $scope.queryDocs; |
| } |
| |
| $scope.resultsMeta = [ |
| {name: "Query", value: h.params.q}, |
| {name: "QTime", value: h.QTime}, |
| {name: "Hits", value: data.response.numFound}, |
| {name: "sort", value: sort}, |
| ]; |
| |
| var excParams = ["q", "handler", "debug", "configSet", "wt", "version", "_", "sort"]; |
| for (var p in h.params) { |
| if (!excParams.includes(p)) { |
| $scope.resultsMeta.push({name: p, value: h.params[p]}); |
| } |
| } |
| |
| $scope.debugMeta = []; |
| for (var d in data.debug) { |
| if (typeof data.debug[d] !== 'object') { |
| var nvp = {name: d, value: data.debug[d]}; |
| $scope.debugMeta.push(nvp); |
| $scope.resultsMeta.push(nvp); |
| } |
| } |
| |
| var rootChildren = [{ |
| "id":"docs", |
| "text": "Documents", |
| "state": {"opened": true}, |
| "a_attr": {"href": "docs"}, |
| "children": docsToTree(data.response.docs) |
| }]; |
| |
| if (data.facet_counts && data.facet_counts.facet_fields) { |
| rootChildren.push({ |
| "id":"facets", |
| "text": "Facets", |
| "state": {"opened": true}, |
| "a_attr": {"href": "facets"}, |
| "children": facetsToTree(data.facet_counts.facet_fields) |
| }); |
| } |
| |
| if (data.highlighting) { |
| var hlNodes = hlToTree(data.highlighting); |
| if (hlNodes.length > 0) { |
| rootChildren.push({ |
| "id":"hl", |
| "text": "Highlighting", |
| "state": {"opened": true}, |
| "a_attr": {"href": "hl"}, |
| "children": hlNodes |
| }); |
| } |
| } |
| |
| if (data.debug) { |
| rootChildren.push({"id":"debug", "text": "Debug", "a_attr": {"href": "debug"}, "children": debugToTree(data.debug)}); |
| } |
| |
| var tree = [{"id":"/","text": "Results", "a_attr": {"href": "/"}, "children": rootChildren}]; |
| $scope.queryResultsTree = tree; |
| }; |
| |
| $scope.onSelectQueryResultsNode = function (id) { |
| $scope.selectedResultsNode = id; |
| |
| if (id === "/" || id === "docs") { |
| $scope.resultsData = $scope.resultsMeta; |
| return; |
| } |
| |
| if (id === "debug") { |
| $scope.resultsData = $scope.debugMeta; |
| return; |
| } |
| |
| var jst = $('#queryResultsJsTree').jstree(); |
| var node = jst.get_node(id); |
| if (!node || !node.a_attr) { |
| $scope.resultsData = $scope.resultsMeta; |
| return; |
| } |
| |
| if (node.original && node.original.tdata) { |
| $scope.resultsData = node.original.tdata; |
| } else { |
| $scope.resultsData = []; |
| for (var a in node.a_attr) { |
| if (a === "href") continue; |
| var row = {name: a, value: node.a_attr[a]}; |
| if (id.startsWith("doc/")) { |
| row.type = "f"; // so we can link to fields in the schema tree from results! |
| } |
| |
| $scope.resultsData.push(row); |
| } |
| } |
| |
| if (id.startsWith("doc/")) { |
| $scope.sampleDocId = id.substring(4); |
| $scope.updateSampleDocId(); |
| } |
| }; |
| |
| $scope.doQuery = function () { |
| |
| delete $scope.queryDocs; |
| |
| var params = {path: "query", configSet: $scope.currentSchema, debug: "true", "wt": "json"}; |
| |
| if ($scope.selectedFacets && $scope.selectedFacets.length > 0) { |
| params["facet"] = true; |
| params["facet.field"] = $scope.selectedFacets; |
| params["facet.limit"] = 20; |
| params["facet.mincount"] = 1; |
| } else { |
| params["facet"] = false; |
| delete params["facet.field"]; |
| } |
| |
| var set = function (key, value) { |
| if (params[key]) { |
| params[key].push(value); |
| } else { |
| params[key] = [value]; |
| } |
| } |
| |
| params["sort"] = $scope.query.sortBy + " " + $scope.query.sortDir; |
| params["q"] = $scope.query.q.trim(); |
| if (!params["q"]) { |
| params["q"] = "*:*"; |
| } |
| |
| if ($scope.rawParams) { |
| var rawParams = $scope.rawParams.split(/[&\n]/); |
| for (var i in rawParams) { |
| var param = rawParams[i]; |
| var equalPos = param.indexOf("="); |
| if (equalPos > -1) { |
| set(param.substring(0, equalPos), param.substring(equalPos + 1)); |
| } else { |
| set(param, ""); // Use empty value for params without "=" |
| } |
| } |
| } |
| |
| if (params["q"] !== '*:*' && $scope.query.highlight) { |
| params["hl"] = true; |
| params["hl.fl"] = $scope.query.highlight; |
| if (!params["hl.method"]) { |
| // lookup the field props |
| var method = "unified"; |
| var field = $scope.fields.find(f => f.name === $scope.query.highlight); |
| if (field) { |
| if (field.termVectors && field.termOffsets && field.termPositions) { |
| method = "fastVector"; |
| } |
| } |
| params["hl.method"] = method; |
| } |
| } else { |
| delete params["hl"]; |
| delete params["hl.fl"]; |
| } |
| |
| var qt = params["qt"] ? params["qt"] : "/select"; |
| if (qt[0] === '/') { |
| params.handler = qt.substring(1); |
| } else { // Support legacy style handleSelect=true configs |
| params.handler = "select"; |
| params["qt"] = qt; |
| } |
| |
| SchemaDesigner.get(params, function (data) { |
| $("#sort").trigger("chosen:updated"); |
| $("#ff").trigger("chosen:updated"); |
| $("#hl").trigger("chosen:updated"); |
| |
| $scope.renderResultsTree(data); |
| |
| var nodeId = "/"; |
| if ($scope.sampleDocId) { |
| if (data.response.docs) { |
| var hit = data.response.docs.find(d => d[$scope.uniqueKeyField] === $scope.sampleDocId); |
| if (hit) { |
| nodeId = "doc/"+$scope.sampleDocId; |
| } |
| } |
| } |
| $scope.onSelectQueryResultsNode(nodeId); |
| }); |
| }; |
| |
| $scope.toggleShowAnalysisJson = function () { |
| if ($scope.showAnalysisJson) { |
| $scope.showAnalysisJson = false; |
| $scope.editAnalysis = "Edit JSON"; |
| } else { |
| $scope.showAnalysisJson = true; |
| $scope.editAnalysis = "Hide JSON"; |
| |
| var node = $scope.selectedNode; |
| var analysisJson = {}; |
| if (node.analyzer) { |
| analysisJson.analyzer = node.analyzer; |
| } else { |
| if (node.indexAnalyzer) { |
| analysisJson.indexAnalyzer = node.indexAnalyzer; |
| } |
| if (node.queryAnalyzer) { |
| analysisJson.queryAnalyzer = node.queryAnalyzer; |
| } |
| } |
| $scope.analysisJsonText = JSON.stringify(analysisJson, null, 2); |
| } |
| }; |
| |
| function filterFieldsByType(fieldsSrc, typeFilter) { |
| var children = []; |
| for (var f in fieldsSrc) { |
| var field = fieldsSrc[f]; |
| if (field.a_attr && field.a_attr.type === typeFilter) { |
| children.push(field); |
| } |
| } |
| return children; |
| } |
| |
| function filterFieldsByFeature(fieldsSrc, opt, enabled) { |
| var children = []; |
| var isEnabled = enabled === "true"; |
| for (var f in fieldsSrc) { |
| var field = fieldsSrc[f]; |
| if (!field.a_attr) { |
| continue; |
| } |
| var attr = field.a_attr; |
| if (opt === "indexed") { |
| if (attr.indexed === isEnabled) { |
| children.push(field); |
| } |
| } else if (opt === "text") { |
| if (attr.tokenized === isEnabled) { |
| children.push(field); |
| } |
| } else if (opt === "facet") { |
| if (((attr.indexed || attr.docValues) && !attr.tokenized && attr.name !== '_version_') === isEnabled) { |
| children.push(field); |
| } |
| } else if (opt === "highlight") { |
| if ((attr.stored && attr.tokenized) === isEnabled) { |
| children.push(field); |
| } |
| } else if (opt === "sortable") { |
| if (((attr.indexed || attr.docValues) && !attr.multiValued) === isEnabled) { |
| children.push(field); |
| } |
| } else if (opt === "docValues") { |
| if (attr.docValues === isEnabled) { |
| children.push(field); |
| } |
| } else if (opt === "stored") { |
| if ((attr.stored || (attr.docValues && attr.useDocValuesAsStored)) === isEnabled) { |
| children.push(field); |
| } |
| } |
| } |
| return children; |
| } |
| |
| $scope.findSelectedNodeId = function() { |
| var nodeId = "/"; |
| if ($scope.fieldsNode.children.length > 0) { |
| if ($scope.selectedNode) { |
| var found = $scope.fieldsNode.children.find(n => n.a_attr.name === $scope.selectedNode.name); |
| if (found) { |
| nodeId = $scope.selectedNode.href; |
| } else { |
| delete $scope.selectedNode; |
| nodeId = $scope.fieldsNode.children[0].a_attr.href; |
| } |
| } else { |
| nodeId = $scope.fieldsNode.children[0].a_attr.href; |
| } |
| } |
| return nodeId; |
| }; |
| |
| $scope.onTreeFilterOptionChanged = function() { |
| if (!$scope.fieldsNode) { |
| return; |
| } |
| |
| if (!$scope.treeFilter || !$scope.treeFilterOption || $scope.treeFilterOption === "*") { |
| // restore tree to unfiltered state |
| $scope.fieldsNode.children = $scope.fieldsSrc; |
| if ($scope.dynamicFieldsNode) { |
| $scope.dynamicFieldsNode.children = $scope.dynamicFieldsSrc; |
| } |
| $scope.refreshTree(); |
| return; |
| } |
| |
| if ($scope.treeFilter === "type") { |
| $scope.fieldsNode.children = filterFieldsByType($scope.fieldsSrc, $scope.treeFilterOption); |
| if ($scope.dynamicFieldsNode) { |
| $scope.dynamicFieldsNode.children = filterFieldsByType($scope.dynamicFieldsSrc, $scope.treeFilterOption); |
| } |
| } else if ($scope.treeFilter === "feature") { |
| $scope.fieldsNode.children = filterFieldsByFeature($scope.fieldsSrc, $scope.treeFilterOption, $scope.treeFilterFeatureEnabled); |
| if ($scope.dynamicFieldsNode) { |
| $scope.dynamicFieldsNode.children = filterFieldsByFeature($scope.dynamicFieldsSrc, $scope.treeFilterOption, $scope.treeFilterFeatureEnabled); |
| } |
| } else { |
| // otherwise, restore the tree to original state |
| $scope.fieldsNode.children = $scope.fieldsSrc; |
| if ($scope.dynamicFieldsNode) { |
| $scope.dynamicFieldsNode.children = $scope.dynamicFieldsSrc; |
| } |
| } |
| $scope.refreshTree(); |
| |
| var nodeId = $scope.findSelectedNodeId(); |
| $scope.onSelectSchemaTreeNode(nodeId); |
| $scope.selectNodeInTree(nodeId); |
| }; |
| |
| $scope.initTreeFilters = function() { |
| $scope.treeFilterFeatureEnabled = "true"; |
| $scope.treeFilterOptions = []; |
| if ($scope.treeFilter === "type") { |
| var usedFieldTypes = []; |
| if ($scope.fields) { |
| var usedTypes = $scope.fields.map(f => f.type); |
| for (var t in usedTypes) { |
| if (!usedFieldTypes.includes(usedTypes[t])) { |
| usedFieldTypes.push(usedTypes[t]); |
| } |
| } |
| } |
| $scope.treeFilterOptions = usedFieldTypes.sort(); |
| $scope.treeFilterOptions.unshift("*"); |
| if (!$scope.treeFilterOption) { |
| $scope.treeFilterOption = "*"; |
| } |
| } else if ($scope.treeFilter === "feature") { |
| $scope.treeFilterOptions = ["indexed","text","facet","highlight","sortable","docValues","stored"].sort(); |
| $scope.treeFilterOptions.unshift("*"); |
| if (!$scope.treeFilterOption) { |
| $scope.treeFilterOption = "*"; |
| } |
| } |
| }; |
| |
| $scope.applyTreeFilterOption = function() { |
| if (!$scope.fields) { |
| return; |
| } |
| $scope.initTreeFilters(); |
| $scope.onTreeFilterOptionChanged(); |
| }; |
| |
| $scope.refresh(); |
| }) |