mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
[data view field editor] Refactor business logic from hooks to controller (#156203)
## Summary This PR moves a lot of business logic from `field_preview_context.tsx` to the controller. No functionality is changed. This is a step to simply the code and eventually reduce bugs. My approach to this change was - 1. Move `useState` calls to controller state, adding necessary setters 2. Move `useCallback` calls to controller methods. 3. Move `useEffect` calls - this one is the most complex - they get called when state values are set. Sometimes the code is inlined or a new private method might be created. The bits that remain are mostly related to form values referred to as `params`. I'm saving this for a subsequent PR as this PR is large enough as it is. Closes https://github.com/elastic/kibana/issues/151243 --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
ca0c9bd2a0
commit
b3561b9328
11 changed files with 438 additions and 404 deletions
|
@ -58,6 +58,8 @@ const mapReturnTypeToPainlessContext = (runtimeType: RuntimeType): PainlessConte
|
|||
const currentDocumentSelector = (state: PreviewState) => state.documents[state.currentIdx];
|
||||
const currentDocumentIsLoadingSelector = (state: PreviewState) => state.isLoadingDocuments;
|
||||
const currentErrorSelector = (state: PreviewState) => state.previewResponse?.error;
|
||||
const isLoadingPreviewSelector = (state: PreviewState) => state.isLoadingPreview;
|
||||
const isPreviewAvailableSelector = (state: PreviewState) => state.isPreviewAvailable;
|
||||
|
||||
const ScriptFieldComponent = ({ existingConcreteFields, links, placeholder }: Props) => {
|
||||
const {
|
||||
|
@ -67,10 +69,12 @@ const ScriptFieldComponent = ({ existingConcreteFields, links, placeholder }: Pr
|
|||
const editorValidationSubscription = useRef<Subscription>();
|
||||
const fieldCurrentValue = useRef<string>('');
|
||||
|
||||
const { isLoadingPreview, isPreviewAvailable, controller } = useFieldPreviewContext();
|
||||
const { controller } = useFieldPreviewContext();
|
||||
const error = useStateSelector(controller.state$, currentErrorSelector);
|
||||
const currentDocument = useStateSelector(controller.state$, currentDocumentSelector);
|
||||
const isFetchingDoc = useStateSelector(controller.state$, currentDocumentIsLoadingSelector);
|
||||
const isLoadingPreview = useStateSelector(controller.state$, isLoadingPreviewSelector);
|
||||
const isPreviewAvailable = useStateSelector(controller.state$, isPreviewAvailableSelector);
|
||||
const [validationData$, nextValidationData$] = useBehaviorSubject<
|
||||
| {
|
||||
isFetchingDoc: boolean;
|
||||
|
|
|
@ -20,12 +20,14 @@ import { FormattedMessage } from '@kbn/i18n-react';
|
|||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { euiFlyoutClassname } from '../constants';
|
||||
import type { Field } from '../types';
|
||||
import { PreviewState } from './preview/types';
|
||||
import { ModifiedFieldModal, SaveFieldTypeOrNameChangedModal } from './confirm_modals';
|
||||
|
||||
import { FieldEditor, FieldEditorFormState } from './field_editor/field_editor';
|
||||
import { useFieldEditorContext } from './field_editor_context';
|
||||
import { FlyoutPanels } from './flyout_panels';
|
||||
import { FieldPreview, useFieldPreviewContext } from './preview';
|
||||
import { useStateSelector } from '../state_utils';
|
||||
|
||||
const i18nTexts = {
|
||||
cancelButtonLabel: i18n.translate('indexPatternFieldEditor.editor.flyoutCancelButtonLabel', {
|
||||
|
@ -61,6 +63,8 @@ export interface Props {
|
|||
onMounted?: (args: { canCloseValidator: () => boolean }) => void;
|
||||
}
|
||||
|
||||
const isPanelVisibleSelector = (state: PreviewState) => state.isPanelVisible;
|
||||
|
||||
const FieldEditorFlyoutContentComponent = ({
|
||||
fieldToEdit,
|
||||
fieldToCreate,
|
||||
|
@ -75,9 +79,8 @@ const FieldEditorFlyoutContentComponent = ({
|
|||
|
||||
const isMobile = useIsWithinMaxBreakpoint('s');
|
||||
|
||||
const {
|
||||
panel: { isVisible: isPanelVisible },
|
||||
} = useFieldPreviewContext();
|
||||
const { controller } = useFieldPreviewContext();
|
||||
const isPanelVisible = useStateSelector(controller.state$, isPanelVisibleSelector);
|
||||
|
||||
const [formState, setFormState] = useState<FieldEditorFormState>({
|
||||
isSubmitted: false,
|
||||
|
|
|
@ -27,14 +27,13 @@ const docIdSelector = (state: PreviewState) => {
|
|||
customId: state.customId,
|
||||
};
|
||||
};
|
||||
const fetchDocErrorSelector = (state: PreviewState) => state.fetchDocError;
|
||||
|
||||
export const DocumentsNavPreview = () => {
|
||||
const {
|
||||
documents: { loadSingle, loadFromCluster, fetchDocError },
|
||||
controller,
|
||||
} = useFieldPreviewContext();
|
||||
const { controller } = useFieldPreviewContext();
|
||||
const { goToPreviousDocument: prev, goToNextDocument: next } = controller;
|
||||
const { documentId, customId } = useStateSelector(controller.state$, docIdSelector);
|
||||
const fetchDocError = useStateSelector(controller.state$, fetchDocErrorSelector);
|
||||
|
||||
const isInvalid = fetchDocError?.code === 'DOC_NOT_FOUND';
|
||||
|
||||
|
@ -45,9 +44,9 @@ export const DocumentsNavPreview = () => {
|
|||
const onDocumentIdChange = useCallback(
|
||||
(e: React.SyntheticEvent<HTMLInputElement>) => {
|
||||
const nextId = (e.target as HTMLInputElement).value;
|
||||
loadSingle(nextId);
|
||||
controller.setCustomDocIdToLoad(nextId);
|
||||
},
|
||||
[loadSingle]
|
||||
[controller]
|
||||
);
|
||||
|
||||
return (
|
||||
|
@ -75,7 +74,7 @@ export const DocumentsNavPreview = () => {
|
|||
color="primary"
|
||||
size="xs"
|
||||
flush="left"
|
||||
onClick={() => loadFromCluster()}
|
||||
onClick={() => controller.fetchSampleDocuments()}
|
||||
data-test-subj="loadDocsFromClusterButton"
|
||||
>
|
||||
{i18n.translate(
|
||||
|
|
|
@ -23,6 +23,8 @@ import { useFieldPreviewContext } from '../field_preview_context';
|
|||
import { IsUpdatingIndicator } from '../is_updating_indicator';
|
||||
import { ImagePreviewModal } from '../image_preview_modal';
|
||||
import type { DocumentField } from './field_list';
|
||||
import { PreviewState } from '../types';
|
||||
import { useStateSelector } from '../../../state_utils';
|
||||
|
||||
interface Props {
|
||||
field: DocumentField;
|
||||
|
@ -32,13 +34,16 @@ interface Props {
|
|||
isFromScript?: boolean;
|
||||
}
|
||||
|
||||
const isLoadingPreviewSelector = (state: PreviewState) => state.isLoadingPreview;
|
||||
|
||||
export const PreviewListItem: React.FC<Props> = ({
|
||||
field: { key, value, formattedValue, isPinned = false },
|
||||
toggleIsPinned,
|
||||
hasScriptError,
|
||||
isFromScript = false,
|
||||
}) => {
|
||||
const { isLoadingPreview } = useFieldPreviewContext();
|
||||
const { controller } = useFieldPreviewContext();
|
||||
const isLoadingPreview = useStateSelector(controller.state$, isLoadingPreviewSelector);
|
||||
|
||||
const [isPreviewImageModalVisible, setIsPreviewImageModalVisible] = useState(false);
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
import React, { useState, useCallback, useEffect } from 'react';
|
||||
import React, { useState, useCallback } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { EuiSpacer, EuiResizeObserver, EuiFieldSearch, EuiCallOut } from '@elastic/eui';
|
||||
|
||||
|
@ -17,9 +17,15 @@ import { FieldPreviewError } from './field_preview_error';
|
|||
import { PreviewListItem } from './field_list/field_list_item';
|
||||
import { PreviewFieldList } from './field_list/field_list';
|
||||
import { useStateSelector } from '../../state_utils';
|
||||
import { PreviewState } from './types';
|
||||
|
||||
import './field_preview.scss';
|
||||
|
||||
const previewResponseSelector = (state: PreviewState) => state.previewResponse;
|
||||
const fetchDocErrorSelector = (state: PreviewState) => state.fetchDocError;
|
||||
const isLoadingPreviewSelector = (state: PreviewState) => state.isLoadingPreview;
|
||||
const isPreviewAvailableSelector = (state: PreviewState) => state.isPreviewAvailable;
|
||||
|
||||
export const FieldPreview = () => {
|
||||
const [fieldListHeight, setFieldListHeight] = useState(-1);
|
||||
const [searchValue, setSearchValue] = useState('');
|
||||
|
@ -28,13 +34,12 @@ export const FieldPreview = () => {
|
|||
params: {
|
||||
value: { name, script, format },
|
||||
},
|
||||
isLoadingPreview,
|
||||
documents: { fetchDocError },
|
||||
reset,
|
||||
isPreviewAvailable,
|
||||
controller,
|
||||
} = useFieldPreviewContext();
|
||||
const { fields, error } = useStateSelector(controller.state$, (state) => state.previewResponse);
|
||||
const { fields, error } = useStateSelector(controller.state$, previewResponseSelector);
|
||||
const fetchDocError = useStateSelector(controller.state$, fetchDocErrorSelector);
|
||||
const isLoadingPreview = useStateSelector(controller.state$, isLoadingPreviewSelector);
|
||||
const isPreviewAvailable = useStateSelector(controller.state$, isPreviewAvailableSelector);
|
||||
|
||||
// To show the preview we at least need a name to be defined, the script or the format
|
||||
// and an first response from the _execute API
|
||||
|
@ -71,12 +76,6 @@ export const FieldPreview = () => {
|
|||
);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
// When unmounting the preview pannel we make sure to reset
|
||||
// the state of the preview panel.
|
||||
return reset;
|
||||
}, [reset]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="indexPatternFieldEditor__previewPannel"
|
||||
|
|
|
@ -26,15 +26,7 @@ import { useStateSelector } from '../../state_utils';
|
|||
|
||||
import { parseEsError } from '../../lib/runtime_field_validation';
|
||||
import { useFieldEditorContext } from '../field_editor_context';
|
||||
import type {
|
||||
PainlessExecuteContext,
|
||||
Context,
|
||||
Params,
|
||||
EsDocument,
|
||||
FetchDocError,
|
||||
FieldPreview,
|
||||
PreviewState,
|
||||
} from './types';
|
||||
import type { PainlessExecuteContext, Context, Params, FieldPreview, PreviewState } from './types';
|
||||
import type { PreviewController } from './preview_controller';
|
||||
|
||||
const fieldPreviewContext = createContext<Context | undefined>(undefined);
|
||||
|
@ -69,7 +61,6 @@ const documentsSelector = (state: PreviewState) => {
|
|||
totalDocs: state.documents.length,
|
||||
currentDocIndex: currentDocument?._index,
|
||||
currentDocId: currentDocument?._id,
|
||||
currentIdx: state.currentIdx,
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -77,25 +68,9 @@ export const FieldPreviewProvider: FunctionComponent<{ controller: PreviewContro
|
|||
controller,
|
||||
children,
|
||||
}) => {
|
||||
const previewCount = useRef(0);
|
||||
|
||||
// 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).
|
||||
const lastExecutePainlessRequestParams = useRef<{
|
||||
type: Params['type'];
|
||||
script: string | undefined;
|
||||
documentId: string | undefined;
|
||||
}>({
|
||||
type: null,
|
||||
script: undefined,
|
||||
documentId: undefined,
|
||||
});
|
||||
|
||||
const {
|
||||
dataView,
|
||||
fieldTypeToProcess,
|
||||
services: {
|
||||
search,
|
||||
notifications,
|
||||
api: { getFieldPreview },
|
||||
},
|
||||
|
@ -104,10 +79,6 @@ export const FieldPreviewProvider: FunctionComponent<{ controller: PreviewContro
|
|||
|
||||
const fieldPreview$ = useRef(new BehaviorSubject<FieldPreview[] | undefined>(undefined));
|
||||
|
||||
const [initialPreviewComplete, setInitialPreviewComplete] = useState(false);
|
||||
|
||||
/** Possible error while fetching sample documents */
|
||||
const [fetchDocError, setFetchDocError] = useState<FetchDocError | null>(null);
|
||||
/** The parameters required for the Painless _execute API */
|
||||
const [params, setParams] = useState<Params>(defaultParams);
|
||||
|
||||
|
@ -117,28 +88,10 @@ export const FieldPreviewProvider: FunctionComponent<{ controller: PreviewContro
|
|||
message: string | null;
|
||||
}>({ isValidating: false, isValid: true, message: null });
|
||||
|
||||
/** Flag to show/hide the preview panel */
|
||||
const [isPanelVisible, setIsPanelVisible] = useState(true);
|
||||
/** Flag to indicate if we are loading document from cluster */
|
||||
const [isFetchingDocument, setIsFetchingDocument] = useState(false);
|
||||
/** Flag to indicate if we are calling the _execute API */
|
||||
const [isLoadingPreview, setIsLoadingPreview] = useState(false);
|
||||
|
||||
/** Flag to indicate if we are loading a single document by providing its ID */
|
||||
const [customDocIdToLoad, setCustomDocIdToLoad] = useState<string | null>(null);
|
||||
|
||||
const { currentDocument, currentDocIndex, currentDocId, totalDocs, currentIdx } =
|
||||
useStateSelector(controller.state$, documentsSelector);
|
||||
|
||||
let isPreviewAvailable = true;
|
||||
|
||||
// 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.
|
||||
if (!isFetchingDocument && !customDocIdToLoad && totalDocs === 0) {
|
||||
isPreviewAvailable = false;
|
||||
}
|
||||
const { currentDocument, currentDocIndex, currentDocId } = useStateSelector(
|
||||
controller.state$,
|
||||
documentsSelector
|
||||
);
|
||||
|
||||
const { name, document, script, format, type, parentName } = params;
|
||||
|
||||
|
@ -146,196 +99,6 @@ export const FieldPreviewProvider: FunctionComponent<{ controller: PreviewContro
|
|||
setParams((prev) => ({ ...prev, ...updated }));
|
||||
}, []);
|
||||
|
||||
const allParamsDefined = useMemo(() => {
|
||||
if (!currentDocIndex || !script?.source || !type) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}, [currentDocIndex, script?.source, type]);
|
||||
|
||||
const hasSomeParamsChanged = useMemo(() => {
|
||||
return (
|
||||
lastExecutePainlessRequestParams.current.type !== type ||
|
||||
lastExecutePainlessRequestParams.current.script !== script?.source ||
|
||||
lastExecutePainlessRequestParams.current.documentId !== currentDocId
|
||||
);
|
||||
}, [type, script, currentDocId]);
|
||||
|
||||
const fetchSampleDocuments = useCallback(
|
||||
async (limit: number = 50) => {
|
||||
if (typeof limit !== 'number') {
|
||||
// We guard ourself from passing an <input /> event accidentally
|
||||
throw new Error('The "limit" option must be a number');
|
||||
}
|
||||
|
||||
lastExecutePainlessRequestParams.current.documentId = undefined;
|
||||
setIsFetchingDocument(true);
|
||||
controller.setPreviewResponse({ fields: [], error: null });
|
||||
|
||||
const [response, searchError] = await search
|
||||
.search({
|
||||
params: {
|
||||
index: dataView.getIndexPattern(),
|
||||
body: {
|
||||
fields: ['*'],
|
||||
size: limit,
|
||||
},
|
||||
},
|
||||
})
|
||||
.toPromise()
|
||||
.then((res) => [res, null])
|
||||
.catch((err) => [null, err]);
|
||||
|
||||
setIsFetchingDocument(false);
|
||||
setCustomDocIdToLoad(null);
|
||||
|
||||
const error: FetchDocError | null = Boolean(searchError)
|
||||
? {
|
||||
code: 'ERR_FETCHING_DOC',
|
||||
error: {
|
||||
message: searchError.toString(),
|
||||
reason: i18n.translate(
|
||||
'indexPatternFieldEditor.fieldPreview.error.errorLoadingSampleDocumentsDescription',
|
||||
{
|
||||
defaultMessage: 'Error loading sample documents.',
|
||||
}
|
||||
),
|
||||
},
|
||||
}
|
||||
: null;
|
||||
|
||||
setFetchDocError(error);
|
||||
|
||||
if (error === null) {
|
||||
controller.setDocuments(response ? response.rawResponse.hits.hits : []);
|
||||
}
|
||||
},
|
||||
[dataView, search, controller]
|
||||
);
|
||||
|
||||
const loadDocument = useCallback(
|
||||
async (id: string) => {
|
||||
if (!Boolean(id.trim())) {
|
||||
return;
|
||||
}
|
||||
|
||||
lastExecutePainlessRequestParams.current.documentId = undefined;
|
||||
setIsFetchingDocument(true);
|
||||
|
||||
const [response, searchError] = await search
|
||||
.search({
|
||||
params: {
|
||||
index: dataView.getIndexPattern(),
|
||||
body: {
|
||||
size: 1,
|
||||
fields: ['*'],
|
||||
query: {
|
||||
ids: {
|
||||
values: [id],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
.toPromise()
|
||||
.then((res) => [res, null])
|
||||
.catch((err) => [null, err]);
|
||||
|
||||
setIsFetchingDocument(false);
|
||||
|
||||
const isDocumentFound = response?.rawResponse.hits.total > 0;
|
||||
const loadedDocuments: EsDocument[] = isDocumentFound ? response.rawResponse.hits.hits : [];
|
||||
const error: FetchDocError | null = Boolean(searchError)
|
||||
? {
|
||||
code: 'ERR_FETCHING_DOC',
|
||||
error: {
|
||||
message: searchError.toString(),
|
||||
reason: i18n.translate(
|
||||
'indexPatternFieldEditor.fieldPreview.error.errorLoadingDocumentDescription',
|
||||
{
|
||||
defaultMessage: 'Error loading document.',
|
||||
}
|
||||
),
|
||||
},
|
||||
}
|
||||
: isDocumentFound === false
|
||||
? {
|
||||
code: 'DOC_NOT_FOUND',
|
||||
error: {
|
||||
message: i18n.translate(
|
||||
'indexPatternFieldEditor.fieldPreview.error.documentNotFoundDescription',
|
||||
{
|
||||
defaultMessage: 'Document ID not found',
|
||||
}
|
||||
),
|
||||
},
|
||||
}
|
||||
: null;
|
||||
|
||||
setFetchDocError(error);
|
||||
|
||||
if (error === null) {
|
||||
controller.setDocuments(loadedDocuments);
|
||||
} else {
|
||||
// Make sure we disable the "Updating..." indicator as we have an error
|
||||
// and we won't fetch the preview
|
||||
setIsLoadingPreview(false);
|
||||
}
|
||||
},
|
||||
[dataView, search, controller]
|
||||
);
|
||||
|
||||
const updateSingleFieldPreview = useCallback(
|
||||
(fieldName: string, values: unknown[]) => {
|
||||
const [value] = values;
|
||||
const formattedValue = controller.valueFormatter({ value, type, format });
|
||||
|
||||
controller.setPreviewResponse({
|
||||
fields: [{ key: fieldName, value, formattedValue }],
|
||||
error: null,
|
||||
});
|
||||
},
|
||||
[controller, type, format]
|
||||
);
|
||||
|
||||
const updateCompositeFieldPreview = useCallback(
|
||||
(compositeValues: Record<string, unknown[]>) => {
|
||||
const updatedFieldsInScript: string[] = [];
|
||||
// if we're displaying a composite subfield, filter results
|
||||
const filterSubfield = parentName ? (field: FieldPreview) => field.key === name : () => true;
|
||||
|
||||
const fields = Object.entries(compositeValues)
|
||||
.map<FieldPreview>(([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 = controller.valueFormatter({ value, type, format });
|
||||
|
||||
return {
|
||||
key: parentName
|
||||
? `${parentName ?? ''}.${fieldName}`
|
||||
: `${fieldName$.getValue() ?? ''}.${fieldName}`,
|
||||
value,
|
||||
formattedValue,
|
||||
type: valueTypeToSelectedType(value),
|
||||
};
|
||||
})
|
||||
.filter(filterSubfield)
|
||||
// ...and sort alphabetically
|
||||
.sort((a, b) => a.key.localeCompare(b.key));
|
||||
|
||||
fieldPreview$.current.next(fields);
|
||||
controller.setPreviewResponse({
|
||||
fields,
|
||||
error: null,
|
||||
});
|
||||
},
|
||||
[parentName, name, fieldPreview$, fieldName$, controller, type, format]
|
||||
);
|
||||
|
||||
const updatePreview = useCallback(async () => {
|
||||
// don't prevent rendering if we're working with a composite subfield (has parentName)
|
||||
if (!parentName && scriptEditorValidation.isValidating) {
|
||||
|
@ -344,19 +107,21 @@ export const FieldPreviewProvider: FunctionComponent<{ controller: PreviewContro
|
|||
|
||||
if (
|
||||
!parentName &&
|
||||
(!allParamsDefined || !hasSomeParamsChanged || scriptEditorValidation.isValid === false)
|
||||
(!controller.allParamsDefined(type, script?.source, currentDocIndex) ||
|
||||
!controller.hasSomeParamsChanged(type, script?.source, currentDocId) ||
|
||||
scriptEditorValidation.isValid === false)
|
||||
) {
|
||||
setIsLoadingPreview(false);
|
||||
controller.setIsLoadingPreview(false);
|
||||
return;
|
||||
}
|
||||
|
||||
lastExecutePainlessRequestParams.current = {
|
||||
controller.setLastExecutePainlessRequestParams({
|
||||
type,
|
||||
script: script?.source,
|
||||
documentId: currentDocId,
|
||||
};
|
||||
});
|
||||
|
||||
const currentApiCall = ++previewCount.current;
|
||||
const currentApiCall = controller.incrementPreviewCount(); // ++previewCount.current;
|
||||
|
||||
const previewScript = (parentName && dataView.getRuntimeField(parentName)?.script) || script!;
|
||||
|
||||
|
@ -367,7 +132,7 @@ export const FieldPreviewProvider: FunctionComponent<{ controller: PreviewContro
|
|||
script: previewScript,
|
||||
});
|
||||
|
||||
if (currentApiCall !== previewCount.current) {
|
||||
if (currentApiCall !== controller.getPreviewCount()) {
|
||||
// Discard this response as there is another one inflight
|
||||
// or we have called reset() and no longer need the response.
|
||||
return;
|
||||
|
@ -382,7 +147,7 @@ export const FieldPreviewProvider: FunctionComponent<{ controller: PreviewContro
|
|||
});
|
||||
notifications.toasts.addError(serverError, { title });
|
||||
|
||||
setIsLoadingPreview(false);
|
||||
controller.setIsLoadingPreview(false);
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -402,15 +167,23 @@ export const FieldPreviewProvider: FunctionComponent<{ controller: PreviewContro
|
|||
});
|
||||
} else {
|
||||
if (!Array.isArray(values)) {
|
||||
updateCompositeFieldPreview(values);
|
||||
controller.updateCompositeFieldPreview(
|
||||
values,
|
||||
parentName,
|
||||
name!,
|
||||
fieldName$.getValue(),
|
||||
type,
|
||||
format,
|
||||
(value: FieldPreview[] | undefined) => fieldPreview$.current.next(value)
|
||||
);
|
||||
} else {
|
||||
updateSingleFieldPreview(name!, values);
|
||||
controller.updateSingleFieldPreview(name!, values, type, format);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setInitialPreviewComplete(true);
|
||||
setIsLoadingPreview(false);
|
||||
controller.setInitialPreviewComplete(true);
|
||||
controller.setIsLoadingPreview(false);
|
||||
}, [
|
||||
name,
|
||||
type,
|
||||
|
@ -421,108 +194,40 @@ export const FieldPreviewProvider: FunctionComponent<{ controller: PreviewContro
|
|||
currentDocId,
|
||||
getFieldPreview,
|
||||
notifications.toasts,
|
||||
allParamsDefined,
|
||||
scriptEditorValidation,
|
||||
hasSomeParamsChanged,
|
||||
updateSingleFieldPreview,
|
||||
updateCompositeFieldPreview,
|
||||
currentDocIndex,
|
||||
controller,
|
||||
format,
|
||||
fieldName$,
|
||||
]);
|
||||
|
||||
const reset = useCallback(() => {
|
||||
// By resetting the previewCount we will discard previous inflight
|
||||
// API call response coming in after calling reset() was called
|
||||
previewCount.current = 0;
|
||||
|
||||
controller.setDocuments([]);
|
||||
controller.setPreviewResponse({ fields: [], error: null });
|
||||
setIsLoadingPreview(false);
|
||||
setIsFetchingDocument(false);
|
||||
}, [controller]);
|
||||
|
||||
const ctx = useMemo<Context>(
|
||||
() => ({
|
||||
controller,
|
||||
fieldPreview$: fieldPreview$.current,
|
||||
isPreviewAvailable,
|
||||
isLoadingPreview,
|
||||
initialPreviewComplete,
|
||||
params: {
|
||||
value: params,
|
||||
update: updateParams,
|
||||
},
|
||||
documents: {
|
||||
loadSingle: setCustomDocIdToLoad,
|
||||
loadFromCluster: fetchSampleDocuments,
|
||||
fetchDocError,
|
||||
},
|
||||
navigation: {
|
||||
isFirstDoc: currentIdx === 0,
|
||||
isLastDoc: currentIdx >= totalDocs - 1,
|
||||
},
|
||||
panel: {
|
||||
isVisible: isPanelVisible,
|
||||
setIsVisible: setIsPanelVisible,
|
||||
},
|
||||
validation: {
|
||||
// todo do this next
|
||||
setScriptEditorValidation,
|
||||
},
|
||||
reset,
|
||||
}),
|
||||
[
|
||||
controller,
|
||||
currentIdx,
|
||||
fieldPreview$,
|
||||
fetchDocError,
|
||||
params,
|
||||
isPreviewAvailable,
|
||||
isLoadingPreview,
|
||||
updateParams,
|
||||
fetchSampleDocuments,
|
||||
totalDocs,
|
||||
isPanelVisible,
|
||||
reset,
|
||||
initialPreviewComplete,
|
||||
]
|
||||
[controller, fieldPreview$, params, updateParams]
|
||||
);
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
useEffect(() => {
|
||||
if (allParamsDefined && hasSomeParamsChanged) {
|
||||
setIsLoadingPreview(true);
|
||||
}
|
||||
}, [allParamsDefined, hasSomeParamsChanged, script?.source, type, currentDocId]);
|
||||
|
||||
/**
|
||||
* In order to immediately display the "Updating..." state indicator and not have to wait
|
||||
* the 500ms of the debounce, we set the isFetchingDocument state in this effect whenever
|
||||
* "customDocIdToLoad" changes
|
||||
*/
|
||||
useEffect(() => {
|
||||
controller.setCustomId(customDocIdToLoad || undefined);
|
||||
if (customDocIdToLoad !== null && Boolean(customDocIdToLoad.trim())) {
|
||||
setIsFetchingDocument(true);
|
||||
}
|
||||
}, [customDocIdToLoad, controller]);
|
||||
|
||||
/**
|
||||
* Whenever we show the preview panel we will update the documents from the cluster
|
||||
*/
|
||||
useEffect(() => {
|
||||
if (isPanelVisible) {
|
||||
fetchSampleDocuments();
|
||||
}
|
||||
}, [isPanelVisible, fetchSampleDocuments, fieldTypeToProcess]);
|
||||
|
||||
/**
|
||||
* Each time the current document changes we update the parameters
|
||||
* that will be sent in the _execute HTTP request.
|
||||
*/
|
||||
// This game me problems
|
||||
useEffect(() => {
|
||||
updateParams({
|
||||
document: currentDocument,
|
||||
|
@ -594,7 +299,7 @@ export const FieldPreviewProvider: FunctionComponent<{ controller: PreviewContro
|
|||
if (script?.source === undefined) {
|
||||
// Whenever the source is not defined ("Set value" is toggled off or the
|
||||
// script is empty) we clear the error and update the params cache.
|
||||
lastExecutePainlessRequestParams.current.script = undefined;
|
||||
controller.setLastExecutePainlessRequestParams({ script: undefined });
|
||||
controller.setPreviewError(null);
|
||||
}
|
||||
}, [script?.source, controller]);
|
||||
|
@ -608,7 +313,7 @@ export const FieldPreviewProvider: FunctionComponent<{ controller: PreviewContro
|
|||
|
||||
if (scriptEditorValidation.isValid === false) {
|
||||
// Make sure to remove the "Updating..." spinner
|
||||
setIsLoadingPreview(false);
|
||||
controller.setIsLoadingPreview(false);
|
||||
|
||||
// Set preview response error so it is displayed in the flyout footer
|
||||
const error =
|
||||
|
@ -628,7 +333,8 @@ export const FieldPreviewProvider: FunctionComponent<{ controller: PreviewContro
|
|||
|
||||
// Make sure to update the lastExecutePainlessRequestParams cache so when the user updates
|
||||
// the script and fixes the syntax the "updatePreview()" will run
|
||||
lastExecutePainlessRequestParams.current.script = script?.source;
|
||||
// lastExecutePainlessRequestParams.current.script = script?.source;
|
||||
controller.setLastExecutePainlessRequestParams({ script: script?.source });
|
||||
} else {
|
||||
// Clear possible previous syntax error
|
||||
controller.clearPreviewError('PAINLESS_SYNTAX_ERROR');
|
||||
|
@ -641,21 +347,6 @@ export const FieldPreviewProvider: FunctionComponent<{ controller: PreviewContro
|
|||
*/
|
||||
useDebounce(updatePreview, 500, [updatePreview]);
|
||||
|
||||
/**
|
||||
* Whenever the doc ID to load changes we load the document (after a 500ms debounce)
|
||||
*/
|
||||
useDebounce(
|
||||
() => {
|
||||
if (customDocIdToLoad === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
loadDocument(customDocIdToLoad);
|
||||
},
|
||||
500,
|
||||
[customDocIdToLoad]
|
||||
);
|
||||
|
||||
return <fieldPreviewContext.Provider value={ctx}>{children}</fieldPreviewContext.Provider>;
|
||||
};
|
||||
|
||||
|
|
|
@ -8,13 +8,17 @@
|
|||
import React from 'react';
|
||||
import { EuiCallOut } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { useStateSelector } from '../../state_utils';
|
||||
import { PreviewState } from './types';
|
||||
|
||||
import { useFieldPreviewContext } from './field_preview_context';
|
||||
|
||||
const fetchDocErrorSelector = (state: PreviewState) => state.fetchDocError;
|
||||
|
||||
export const FieldPreviewError = () => {
|
||||
const {
|
||||
documents: { fetchDocError },
|
||||
} = useFieldPreviewContext();
|
||||
const { controller } = useFieldPreviewContext();
|
||||
|
||||
const fetchDocError = useStateSelector(controller.state$, fetchDocErrorSelector);
|
||||
|
||||
if (fetchDocError === null) {
|
||||
return null;
|
||||
|
|
|
@ -6,16 +6,19 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import type { DataView } from '@kbn/data-views-plugin/public';
|
||||
import type { ISearchStart } from '@kbn/data-plugin/public';
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
import { castEsToKbnFieldTypeName } from '@kbn/field-types';
|
||||
import { renderToString } from 'react-dom/server';
|
||||
import React from 'react';
|
||||
import { PreviewState } from './types';
|
||||
import debounce from 'lodash/debounce';
|
||||
import { PreviewState, FetchDocError } from './types';
|
||||
import { BehaviorObservable } from '../../state_utils';
|
||||
import { EsDocument, ScriptErrorCodes, Params } from './types';
|
||||
import { EsDocument, ScriptErrorCodes, Params, FieldPreview } from './types';
|
||||
import type { FieldFormatsStart } from '../../shared_imports';
|
||||
import { valueTypeToSelectedType } from './field_preview_context';
|
||||
|
||||
export const defaultValueFormatter = (value: unknown) => {
|
||||
const content = typeof value === 'object' ? JSON.stringify(value) : String(value) ?? '-';
|
||||
|
@ -41,6 +44,20 @@ const previewStateDefault: PreviewState = {
|
|||
/** 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,
|
||||
};
|
||||
|
||||
export class PreviewController {
|
||||
|
@ -54,22 +71,34 @@ export class PreviewController {
|
|||
});
|
||||
|
||||
this.state$ = this.internalState$ as BehaviorObservable<PreviewState>;
|
||||
|
||||
this.fetchSampleDocuments();
|
||||
}
|
||||
|
||||
// dependencies
|
||||
// @ts-ignore
|
||||
private dataView: DataView;
|
||||
// @ts-ignore
|
||||
private search: ISearchStart;
|
||||
private fieldFormats: FieldFormatsStart;
|
||||
|
||||
private internalState$: BehaviorSubject<PreviewState>;
|
||||
state$: BehaviorObservable<PreviewState>;
|
||||
|
||||
private previewCount = 0;
|
||||
|
||||
private updateState = (newState: Partial<PreviewState>) => {
|
||||
this.internalState$.next({ ...this.state$.getValue(), ...newState });
|
||||
};
|
||||
|
||||
private lastExecutePainlessRequestParams: {
|
||||
type: Params['type'];
|
||||
script: string | undefined;
|
||||
documentId: string | undefined;
|
||||
} = {
|
||||
type: null,
|
||||
script: undefined,
|
||||
documentId: undefined,
|
||||
};
|
||||
|
||||
togglePinnedField = (fieldName: string) => {
|
||||
const currentState = this.state$.getValue();
|
||||
const pinnedFields = {
|
||||
|
@ -80,18 +109,15 @@ export class PreviewController {
|
|||
this.updateState({ pinnedFields });
|
||||
};
|
||||
|
||||
setDocuments = (documents: EsDocument[]) => {
|
||||
private setDocuments = (documents: EsDocument[]) => {
|
||||
this.updateState({
|
||||
documents,
|
||||
currentIdx: 0,
|
||||
isLoadingDocuments: false,
|
||||
isPreviewAvailable: this.getIsPreviewAvailable({ documents }),
|
||||
});
|
||||
};
|
||||
|
||||
setCurrentIdx = (currentIdx: number) => {
|
||||
this.updateState({ currentIdx });
|
||||
};
|
||||
|
||||
goToNextDocument = () => {
|
||||
const currentState = this.state$.getValue();
|
||||
if (currentState.currentIdx >= currentState.documents.length - 1) {
|
||||
|
@ -116,10 +142,6 @@ export class PreviewController {
|
|||
};
|
||||
*/
|
||||
|
||||
setCustomId = (customId?: string) => {
|
||||
this.updateState({ customId });
|
||||
};
|
||||
|
||||
setPreviewError = (error: PreviewState['previewResponse']['error']) => {
|
||||
this.updateState({
|
||||
previewResponse: { ...this.internalState$.getValue().previewResponse, error },
|
||||
|
@ -130,6 +152,46 @@ export class PreviewController {
|
|||
this.updateState({ previewResponse });
|
||||
};
|
||||
|
||||
setCustomDocIdToLoad = (customDocIdToLoad: string | null) => {
|
||||
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.
|
||||
|
||||
private getIsPreviewAvailable = (update: {
|
||||
isFetchingDocument?: boolean;
|
||||
customDocIdToLoad?: string | null;
|
||||
documents?: EsDocument[];
|
||||
}) => {
|
||||
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?.length === 0) {
|
||||
return false;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
clearPreviewError = (errorCode: ScriptErrorCodes) => {
|
||||
const { previewResponse: prev } = this.internalState$.getValue();
|
||||
const error = prev.error === null || prev.error?.code === errorCode ? null : prev.error;
|
||||
|
@ -141,6 +203,65 @@ export class PreviewController {
|
|||
});
|
||||
};
|
||||
|
||||
private setIsFetchingDocument = (isFetchingDocument: boolean) => {
|
||||
this.updateState({
|
||||
isFetchingDocument,
|
||||
isPreviewAvailable: this.getIsPreviewAvailable({ isFetchingDocument }),
|
||||
});
|
||||
};
|
||||
|
||||
private setFetchDocError = (fetchDocError: FetchDocError | null) => {
|
||||
this.updateState({ fetchDocError });
|
||||
};
|
||||
|
||||
setIsLoadingPreview = (isLoadingPreview: boolean) => {
|
||||
this.updateState({ isLoadingPreview });
|
||||
};
|
||||
|
||||
setInitialPreviewComplete = (initialPreviewComplete: boolean) => {
|
||||
this.updateState({ initialPreviewComplete });
|
||||
};
|
||||
|
||||
getIsFirstDoc = () => this.internalState$.getValue().currentIdx === 0;
|
||||
|
||||
getIsLastDoc = () => {
|
||||
const { currentIdx, documents } = this.internalState$.getValue();
|
||||
return currentIdx >= documents.length - 1;
|
||||
};
|
||||
|
||||
setLastExecutePainlessRequestParams = (
|
||||
lastExecutePainlessRequestParams: Partial<typeof this.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?._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;
|
||||
};
|
||||
|
||||
valueFormatter = ({
|
||||
value,
|
||||
format,
|
||||
|
@ -167,4 +288,219 @@ export class PreviewController {
|
|||
|
||||
return defaultValueFormatter(value);
|
||||
};
|
||||
|
||||
fetchSampleDocuments = async (limit: number = 50) => {
|
||||
if (typeof limit !== 'number') {
|
||||
// We guard ourself from passing an <input /> 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: FetchDocError | null = Boolean(searchError)
|
||||
? {
|
||||
code: 'ERR_FETCHING_DOC',
|
||||
error: {
|
||||
message: searchError.toString(),
|
||||
reason: 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 : []);
|
||||
}
|
||||
};
|
||||
|
||||
loadDocument = async (id: string) => {
|
||||
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?.rawResponse.hits.total > 0;
|
||||
const loadedDocuments: EsDocument[] = isDocumentFound ? response.rawResponse.hits.hits : [];
|
||||
const error: FetchDocError | null = Boolean(searchError)
|
||||
? {
|
||||
code: 'ERR_FETCHING_DOC',
|
||||
error: {
|
||||
message: searchError.toString(),
|
||||
reason: i18n.translate(
|
||||
'indexPatternFieldEditor.fieldPreview.error.errorLoadingDocumentDescription',
|
||||
{
|
||||
defaultMessage: 'Error loading document.',
|
||||
}
|
||||
),
|
||||
},
|
||||
}
|
||||
: isDocumentFound === false
|
||||
? {
|
||||
code: 'DOC_NOT_FOUND',
|
||||
error: {
|
||||
message: 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);
|
||||
}
|
||||
};
|
||||
|
||||
debouncedLoadDocument = debounce(this.loadDocument, 500, { leading: true });
|
||||
|
||||
reset = () => {
|
||||
this.previewCount = 0;
|
||||
this.updateState({
|
||||
documents: [],
|
||||
previewResponse: { fields: [], error: null },
|
||||
isLoadingPreview: false,
|
||||
isFetchingDocument: false,
|
||||
});
|
||||
};
|
||||
|
||||
hasSomeParamsChanged = (
|
||||
type: Params['type'],
|
||||
script: string | undefined,
|
||||
currentDocId: string | undefined
|
||||
) => {
|
||||
return (
|
||||
this.lastExecutePainlessRequestParams.type !== type ||
|
||||
this.lastExecutePainlessRequestParams.script !== script ||
|
||||
this.lastExecutePainlessRequestParams.documentId !== currentDocId
|
||||
);
|
||||
};
|
||||
|
||||
getPreviewCount = () => this.previewCount;
|
||||
|
||||
incrementPreviewCount = () => ++this.previewCount;
|
||||
|
||||
allParamsDefined = (
|
||||
type: Params['type'],
|
||||
script: string | undefined,
|
||||
currentDocIndex: string
|
||||
) => {
|
||||
if (!currentDocIndex || !script || !type) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
updateSingleFieldPreview = (
|
||||
fieldName: string,
|
||||
values: unknown[],
|
||||
type: Params['type'],
|
||||
format: Params['format']
|
||||
) => {
|
||||
const [value] = values;
|
||||
const formattedValue = this.valueFormatter({ value, type, format });
|
||||
|
||||
this.setPreviewResponse({
|
||||
fields: [{ key: fieldName, value, formattedValue }],
|
||||
error: null,
|
||||
});
|
||||
};
|
||||
|
||||
updateCompositeFieldPreview = (
|
||||
compositeValues: Record<string, unknown[]>,
|
||||
parentName: string | null,
|
||||
name: string,
|
||||
fieldName$Value: string,
|
||||
type: Params['type'],
|
||||
format: Params['format'],
|
||||
onNext: (fields: FieldPreview[]) => void
|
||||
) => {
|
||||
const updatedFieldsInScript: string[] = [];
|
||||
// if we're displaying a composite subfield, filter results
|
||||
const filterSubfield = parentName ? (field: FieldPreview) => field.key === name : () => true;
|
||||
|
||||
const fields = Object.entries(compositeValues)
|
||||
.map<FieldPreview>(([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 ?? ''}.${fieldName}`
|
||||
: `${fieldName$Value ?? ''}.${fieldName}`,
|
||||
value,
|
||||
formattedValue,
|
||||
type: valueTypeToSelectedType(value),
|
||||
};
|
||||
})
|
||||
.filter(filterSubfield)
|
||||
// ...and sort alphabetically
|
||||
.sort((a, b) => a.key.localeCompare(b.key));
|
||||
|
||||
onNext(fields);
|
||||
this.setPreviewResponse({
|
||||
fields,
|
||||
error: null,
|
||||
});
|
||||
};
|
||||
}
|
||||
|
|
|
@ -60,6 +60,14 @@ export interface PreviewState {
|
|||
fields: FieldPreview[];
|
||||
error: PreviewError | null;
|
||||
};
|
||||
isFetchingDocument: boolean;
|
||||
fetchDocError: FetchDocError | null;
|
||||
customDocIdToLoad: string | null;
|
||||
/** Flag to indicate if we are calling the _execute API */
|
||||
isLoadingPreview: boolean;
|
||||
initialPreviewComplete: boolean;
|
||||
isPreviewAvailable: boolean;
|
||||
isPanelVisible: boolean;
|
||||
}
|
||||
|
||||
export interface FetchDocError {
|
||||
|
@ -115,27 +123,10 @@ export interface Context {
|
|||
controller: PreviewController;
|
||||
fieldPreview$: BehaviorSubject<FieldPreview[] | undefined>;
|
||||
fieldTypeInfo?: FieldTypeInfo[];
|
||||
initialPreviewComplete: boolean;
|
||||
params: {
|
||||
value: Params;
|
||||
update: (updated: Partial<Params>) => void;
|
||||
};
|
||||
isPreviewAvailable: boolean;
|
||||
isLoadingPreview: boolean;
|
||||
documents: {
|
||||
loadSingle: (id: string) => void;
|
||||
loadFromCluster: () => Promise<void>;
|
||||
fetchDocError: FetchDocError | null;
|
||||
};
|
||||
panel: {
|
||||
isVisible: boolean;
|
||||
setIsVisible: (isVisible: boolean) => void;
|
||||
};
|
||||
navigation: {
|
||||
isFirstDoc: boolean;
|
||||
isLastDoc: boolean;
|
||||
};
|
||||
reset: () => void;
|
||||
validation: {
|
||||
setScriptEditorValidation: React.Dispatch<
|
||||
React.SetStateAction<{ isValid: boolean; isValidating: boolean; message: string | null }>
|
||||
|
|
|
@ -163,6 +163,8 @@ export const getFieldEditorOpener =
|
|||
// Runtime field
|
||||
field = {
|
||||
name: fieldNameToEdit!,
|
||||
customLabel: dataViewField.customLabel,
|
||||
popularity: dataViewField.count,
|
||||
format: dataView.getFormatterForFieldNoDefault(fieldNameToEdit!)?.toJSON(),
|
||||
...dataView.getRuntimeField(fieldNameToEdit!)!,
|
||||
};
|
||||
|
|
|
@ -233,9 +233,9 @@ export const IndexPatternTable = ({
|
|||
</>
|
||||
)}
|
||||
{dataView?.tags?.map(({ key: tagKey, name: tagName }) => (
|
||||
<>
|
||||
 <EuiBadge key={tagKey}>{tagName}</EuiBadge>
|
||||
</>
|
||||
<span key={tagKey}>
|
||||
 <EuiBadge>{tagName}</EuiBadge>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
),
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue