/* 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.errorHandler); }; $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 = []; } // 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) { $scope.luke = mergeIndexAndSchemaData(index, schema.schema); $scope.types = Object.keys(schema.schema.types); $scope.showSchemaActions = true; if (!nodeId) { nodeId = "/"; } $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.errorHandler); } $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.errorHandler); }; $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); } }, $scope.errorHandler); }; 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("") || txt.includes("