"use strict"; var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault"); Object.defineProperty(exports, "__esModule", { value: true }); exports.defaultValueFormatter = exports.PreviewController = void 0; var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty")); var _i18n = require("@kbn/i18n"); var _rxjs = require("rxjs"); var _fieldTypes = require("@kbn/field-types"); var _server = require("react-dom/server"); var _react = _interopRequireDefault(require("react")); var _debounce = _interopRequireDefault(require("lodash/debounce")); var _field_preview_context = require("./field_preview_context"); /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License * 2.0 and the Server Side Public License, v 1; you may not use this file except * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ const defaultValueFormatter = value => { var _String; const content = typeof value === 'object' ? JSON.stringify(value) : (_String = String(value)) !== null && _String !== void 0 ? _String : '-'; return (0, _server.renderToString)( /*#__PURE__*/_react.default.createElement(_react.default.Fragment, null, content)); }; exports.defaultValueFormatter = defaultValueFormatter; const previewStateDefault = { /** Map of fields pinned to the top of the list */ pinnedFields: {}, isLoadingDocuments: true, /** Flag to indicate if we are loading a single document by providing its ID */ customId: undefined, /** sample documents fetched from cluster */ documents: [], currentIdx: 0, documentSource: 'cluster', /** Keep track if the script painless syntax is being validated and if it is valid */ scriptEditorValidation: { isValidating: false, isValid: true, message: null }, previewResponse: { fields: [], error: null }, /** Flag to indicate if we are loading document from cluster */ isFetchingDocument: false, /** Possible error while fetching sample documents */ fetchDocError: null, /** Flag to indicate if we are loading a single document by providing its ID */ customDocIdToLoad: null, // not used externally // We keep in cache the latest params sent to the _execute API so we don't make unecessary requests // when changing parameters that don't affect the preview result (e.g. changing the "name" field). isLoadingPreview: false, initialPreviewComplete: false, isPreviewAvailable: true, /** Flag to show/hide the preview panel */ isPanelVisible: true }; class PreviewController { constructor({ dataView, search, fieldFormats }) { // dependencies (0, _defineProperty2.default)(this, "dataView", void 0); (0, _defineProperty2.default)(this, "search", void 0); (0, _defineProperty2.default)(this, "fieldFormats", void 0); (0, _defineProperty2.default)(this, "internalState$", void 0); (0, _defineProperty2.default)(this, "state$", void 0); (0, _defineProperty2.default)(this, "previewCount", 0); (0, _defineProperty2.default)(this, "updateState", newState => { this.internalState$.next({ ...this.state$.getValue(), ...newState }); }); (0, _defineProperty2.default)(this, "lastExecutePainlessRequestParams", { type: null, script: undefined, documentId: undefined }); (0, _defineProperty2.default)(this, "togglePinnedField", fieldName => { const currentState = this.state$.getValue(); const pinnedFields = { ...currentState.pinnedFields, [fieldName]: !currentState.pinnedFields[fieldName] }; this.updateState({ pinnedFields }); }); (0, _defineProperty2.default)(this, "setDocuments", documents => { this.updateState({ documents, currentIdx: 0, isLoadingDocuments: false, isPreviewAvailable: this.getIsPreviewAvailable({ documents }) }); }); (0, _defineProperty2.default)(this, "goToNextDocument", () => { const currentState = this.state$.getValue(); if (currentState.currentIdx >= currentState.documents.length - 1) { this.updateState({ currentIdx: 0 }); } else { this.updateState({ currentIdx: currentState.currentIdx + 1 }); } }); (0, _defineProperty2.default)(this, "goToPreviousDocument", () => { const currentState = this.state$.getValue(); if (currentState.currentIdx === 0) { this.updateState({ currentIdx: currentState.documents.length - 1 }); } else { this.updateState({ currentIdx: currentState.currentIdx - 1 }); } }); /* disabled while investigating issues with painless script editor setScriptEditorValidation = (scriptEditorValidation: PreviewState['scriptEditorValidation']) => { this.updateState({ scriptEditorValidation }); }; */ (0, _defineProperty2.default)(this, "setPreviewError", error => { this.updateState({ previewResponse: { ...this.internalState$.getValue().previewResponse, error } }); }); (0, _defineProperty2.default)(this, "setPreviewResponse", previewResponse => { this.updateState({ previewResponse }); }); (0, _defineProperty2.default)(this, "setCustomDocIdToLoad", customDocIdToLoad => { this.updateState({ customDocIdToLoad, customId: customDocIdToLoad || undefined, isPreviewAvailable: this.getIsPreviewAvailable({ customDocIdToLoad }) }); // load document if id is present this.setIsFetchingDocument(!!customDocIdToLoad); if (customDocIdToLoad) { this.debouncedLoadDocument(customDocIdToLoad); } }); // If no documents could be fetched from the cluster (and we are not trying to load // a custom doc ID) then we disable preview as the script field validation expect the result // of the preview to before resolving. If there are no documents we can't have a preview // (the _execute API expects one) and thus the validation should not expect a value. (0, _defineProperty2.default)(this, "getIsPreviewAvailable", update => { var _merged$documents; const { isFetchingDocument: existingIsFetchingDocument, customDocIdToLoad: existingCustomDocIdToLoad, documents: existingDocuments } = this.internalState$.getValue(); const existing = { existingIsFetchingDocument, existingCustomDocIdToLoad, existingDocuments }; const merged = { ...existing, ...update }; if (!merged.isFetchingDocument && !merged.customDocIdToLoad && ((_merged$documents = merged.documents) === null || _merged$documents === void 0 ? void 0 : _merged$documents.length) === 0) { return false; } else { return true; } }); (0, _defineProperty2.default)(this, "clearPreviewError", errorCode => { var _prev$error; const { previewResponse: prev } = this.internalState$.getValue(); const error = prev.error === null || ((_prev$error = prev.error) === null || _prev$error === void 0 ? void 0 : _prev$error.code) === errorCode ? null : prev.error; this.updateState({ previewResponse: { ...prev, error } }); }); (0, _defineProperty2.default)(this, "setIsFetchingDocument", isFetchingDocument => { this.updateState({ isFetchingDocument, isPreviewAvailable: this.getIsPreviewAvailable({ isFetchingDocument }) }); }); (0, _defineProperty2.default)(this, "setFetchDocError", fetchDocError => { this.updateState({ fetchDocError }); }); (0, _defineProperty2.default)(this, "setIsLoadingPreview", isLoadingPreview => { this.updateState({ isLoadingPreview }); }); (0, _defineProperty2.default)(this, "setInitialPreviewComplete", initialPreviewComplete => { this.updateState({ initialPreviewComplete }); }); (0, _defineProperty2.default)(this, "getIsFirstDoc", () => this.internalState$.getValue().currentIdx === 0); (0, _defineProperty2.default)(this, "getIsLastDoc", () => { const { currentIdx, documents } = this.internalState$.getValue(); return currentIdx >= documents.length - 1; }); (0, _defineProperty2.default)(this, "setLastExecutePainlessRequestParams", lastExecutePainlessRequestParams => { const state = this.internalState$.getValue(); const currentDocument = state.documents[state.currentIdx]; const updated = { ...this.lastExecutePainlessRequestParams, ...lastExecutePainlessRequestParams }; if (this.allParamsDefined(updated.type, updated.script, // todo get current doc index currentDocument === null || currentDocument === void 0 ? void 0 : currentDocument._index) && this.hasSomeParamsChanged(lastExecutePainlessRequestParams.type, lastExecutePainlessRequestParams.script, lastExecutePainlessRequestParams.documentId)) { /** * In order to immediately display the "Updating..." state indicator and not have to wait * the 500ms of the debounce, we set the isLoadingPreview state in this effect whenever * one of the _execute API param changes */ this.setIsLoadingPreview(true); } this.lastExecutePainlessRequestParams = updated; }); (0, _defineProperty2.default)(this, "valueFormatter", ({ value, format, type }) => { if (format !== null && format !== void 0 && format.id) { const formatter = this.fieldFormats.getInstance(format.id, format.params); if (formatter) { var _formatter$getConvert; return (_formatter$getConvert = formatter.getConverterFor('html')(value)) !== null && _formatter$getConvert !== void 0 ? _formatter$getConvert : JSON.stringify(value); } } if (type) { const fieldType = (0, _fieldTypes.castEsToKbnFieldTypeName)(type); const defaultFormatterForType = this.fieldFormats.getDefaultInstance(fieldType); if (defaultFormatterForType) { var _defaultFormatterForT; return (_defaultFormatterForT = defaultFormatterForType.getConverterFor('html')(value)) !== null && _defaultFormatterForT !== void 0 ? _defaultFormatterForT : JSON.stringify(value); } } return defaultValueFormatter(value); }); (0, _defineProperty2.default)(this, "fetchSampleDocuments", async (limit = 50) => { if (typeof limit !== 'number') { // We guard ourself from passing an event accidentally throw new Error('The "limit" option must be a number'); } this.setLastExecutePainlessRequestParams({ documentId: undefined }); this.setIsFetchingDocument(true); this.setPreviewResponse({ fields: [], error: null }); const [response, searchError] = await this.search.search({ params: { index: this.dataView.getIndexPattern(), body: { fields: ['*'], size: limit } } }).toPromise().then(res => [res, null]).catch(err => [null, err]); this.setIsFetchingDocument(false); this.setCustomDocIdToLoad(null); const error = Boolean(searchError) ? { code: 'ERR_FETCHING_DOC', error: { message: searchError.toString(), reason: _i18n.i18n.translate('indexPatternFieldEditor.fieldPreview.error.errorLoadingSampleDocumentsDescription', { defaultMessage: 'Error loading sample documents.' }) } } : null; this.setFetchDocError(error); if (error === null) { this.setDocuments(response ? response.rawResponse.hits.hits : []); } }); (0, _defineProperty2.default)(this, "loadDocument", async id => { if (!Boolean(id.trim())) { return; } this.setLastExecutePainlessRequestParams({ documentId: undefined }); this.setIsFetchingDocument(true); const [response, searchError] = await this.search.search({ params: { index: this.dataView.getIndexPattern(), body: { size: 1, fields: ['*'], query: { ids: { values: [id] } } } } }).toPromise().then(res => [res, null]).catch(err => [null, err]); this.setIsFetchingDocument(false); const isDocumentFound = (response === null || response === void 0 ? void 0 : response.rawResponse.hits.total) > 0; const loadedDocuments = isDocumentFound ? response.rawResponse.hits.hits : []; const error = Boolean(searchError) ? { code: 'ERR_FETCHING_DOC', error: { message: searchError.toString(), reason: _i18n.i18n.translate('indexPatternFieldEditor.fieldPreview.error.errorLoadingDocumentDescription', { defaultMessage: 'Error loading document.' }) } } : isDocumentFound === false ? { code: 'DOC_NOT_FOUND', error: { message: _i18n.i18n.translate('indexPatternFieldEditor.fieldPreview.error.documentNotFoundDescription', { defaultMessage: 'Document ID not found' }) } } : null; this.setFetchDocError(error); if (error === null) { this.setDocuments(loadedDocuments); } else { // Make sure we disable the "Updating..." indicator as we have an error // and we won't fetch the preview this.setIsLoadingPreview(false); } }); (0, _defineProperty2.default)(this, "debouncedLoadDocument", (0, _debounce.default)(this.loadDocument, 500, { leading: true })); (0, _defineProperty2.default)(this, "reset", () => { this.previewCount = 0; this.updateState({ documents: [], previewResponse: { fields: [], error: null }, isLoadingPreview: false, isFetchingDocument: false }); }); (0, _defineProperty2.default)(this, "hasSomeParamsChanged", (type, script, currentDocId) => { return this.lastExecutePainlessRequestParams.type !== type || this.lastExecutePainlessRequestParams.script !== script || this.lastExecutePainlessRequestParams.documentId !== currentDocId; }); (0, _defineProperty2.default)(this, "getPreviewCount", () => this.previewCount); (0, _defineProperty2.default)(this, "incrementPreviewCount", () => ++this.previewCount); (0, _defineProperty2.default)(this, "allParamsDefined", (type, script, currentDocIndex) => { if (!currentDocIndex || !script || !type) { return false; } return true; }); (0, _defineProperty2.default)(this, "updateSingleFieldPreview", (fieldName, values, type, format) => { const [value] = values; const formattedValue = this.valueFormatter({ value, type, format }); this.setPreviewResponse({ fields: [{ key: fieldName, value, formattedValue }], error: null }); }); (0, _defineProperty2.default)(this, "updateCompositeFieldPreview", (compositeValues, parentName, name, fieldName$Value, type, format, onNext) => { const updatedFieldsInScript = []; // if we're displaying a composite subfield, filter results const filterSubfield = parentName ? field => field.key === name : () => true; const fields = Object.entries(compositeValues).map(([key, values]) => { // The Painless _execute API returns the composite field values under a map. // Each of the key is prefixed with "composite_field." (e.g. "composite_field.field1: ['value']") const { 1: fieldName } = key.split('composite_field.'); updatedFieldsInScript.push(fieldName); const [value] = values; const formattedValue = this.valueFormatter({ value, type, format }); return { key: parentName ? `${parentName !== null && parentName !== void 0 ? parentName : ''}.${fieldName}` : `${fieldName$Value !== null && fieldName$Value !== void 0 ? fieldName$Value : ''}.${fieldName}`, value, formattedValue, type: (0, _field_preview_context.valueTypeToSelectedType)(value) }; }).filter(filterSubfield) // ...and sort alphabetically .sort((a, b) => a.key.localeCompare(b.key)); onNext(fields); this.setPreviewResponse({ fields, error: null }); }); this.dataView = dataView; this.search = search; this.fieldFormats = fieldFormats; this.internalState$ = new _rxjs.BehaviorSubject({ ...previewStateDefault }); this.state$ = this.internalState$; this.fetchSampleDocuments(); } } exports.PreviewController = PreviewController;