mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
[data view field editor] Move preview response and value formatter to controller (#153799)
## Summary This is a refactor that moves logic from hooks to a controller class for the data view field editor preview. Functionality is unaffected.
This commit is contained in:
parent
9bb127f26c
commit
fdc23f570e
8 changed files with 136 additions and 120 deletions
|
@ -149,7 +149,7 @@ export const WithFieldEditorDependencies =
|
|||
};
|
||||
|
||||
const mergedDependencies = merge({}, dependencies, overridingDependencies);
|
||||
const previewController = new PreviewController({ dataView, search });
|
||||
const previewController = new PreviewController({ dataView, search, fieldFormats });
|
||||
|
||||
return (
|
||||
<FieldEditorProvider {...mergedDependencies}>
|
||||
|
|
|
@ -24,7 +24,7 @@ import {
|
|||
} from '../../../shared_imports';
|
||||
import type { RuntimeFieldPainlessError } from '../../../types';
|
||||
import { painlessErrorToMonacoMarker } from '../../../lib';
|
||||
import { useFieldPreviewContext, Context } from '../../preview';
|
||||
import { useFieldPreviewContext } from '../../preview';
|
||||
import { schema } from '../form_schema';
|
||||
import type { FieldFormInternal } from '../field_editor';
|
||||
import { useStateSelector } from '../../../state_utils';
|
||||
|
@ -57,6 +57,7 @@ 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 ScriptFieldComponent = ({ existingConcreteFields, links, placeholder }: Props) => {
|
||||
const {
|
||||
|
@ -66,14 +67,15 @@ const ScriptFieldComponent = ({ existingConcreteFields, links, placeholder }: Pr
|
|||
const editorValidationSubscription = useRef<Subscription>();
|
||||
const fieldCurrentValue = useRef<string>('');
|
||||
|
||||
const { error, isLoadingPreview, isPreviewAvailable, controller } = useFieldPreviewContext();
|
||||
const { isLoadingPreview, isPreviewAvailable, controller } = useFieldPreviewContext();
|
||||
const error = useStateSelector(controller.state$, currentErrorSelector);
|
||||
const currentDocument = useStateSelector(controller.state$, currentDocumentSelector);
|
||||
const isFetchingDoc = useStateSelector(controller.state$, currentDocumentIsLoadingSelector);
|
||||
const [validationData$, nextValidationData$] = useBehaviorSubject<
|
||||
| {
|
||||
isFetchingDoc: boolean;
|
||||
isLoadingPreview: boolean;
|
||||
error: Context['error'];
|
||||
error: PreviewState['previewResponse']['error'];
|
||||
}
|
||||
| undefined
|
||||
>(undefined);
|
||||
|
|
|
@ -10,8 +10,8 @@ import { i18n } from '@kbn/i18n';
|
|||
import { EuiComboBoxOptionOption } from '@elastic/eui';
|
||||
|
||||
import { fieldValidators, FieldConfig, RuntimeType, ValidationFunc } from '../../shared_imports';
|
||||
import type { Context } from '../preview';
|
||||
import { RUNTIME_FIELD_OPTIONS } from './constants';
|
||||
import type { PreviewState } from '../preview/types';
|
||||
|
||||
const { containsCharsField, emptyField, numberGreaterThanField } = fieldValidators;
|
||||
const i18nTexts = {
|
||||
|
@ -25,7 +25,7 @@ const i18nTexts = {
|
|||
|
||||
// Validate the painless **script**
|
||||
const painlessScriptValidator: ValidationFunc = async ({ customData: { provider } }) => {
|
||||
const previewError = (await provider()) as Context['error'];
|
||||
const previewError = (await provider()) as PreviewState['previewResponse']['error'];
|
||||
|
||||
if (previewError && previewError.code === 'PAINLESS_SCRIPT_ERROR') {
|
||||
return {
|
||||
|
|
|
@ -85,7 +85,7 @@ export const FieldEditorFlyoutContentContainer = ({
|
|||
fieldFormats,
|
||||
uiSettings,
|
||||
}: Props) => {
|
||||
const [controller] = useState(() => new PreviewController({ dataView, search }));
|
||||
const [controller] = useState(() => new PreviewController({ dataView, search, fieldFormats }));
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
|
||||
const { fields } = dataView;
|
||||
|
|
|
@ -16,6 +16,7 @@ import { DocumentsNavPreview } from './documents_nav_preview';
|
|||
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 './field_preview.scss';
|
||||
|
||||
|
@ -28,12 +29,12 @@ export const FieldPreview = () => {
|
|||
value: { name, script, format },
|
||||
},
|
||||
isLoadingPreview,
|
||||
fields,
|
||||
error,
|
||||
documents: { fetchDocError },
|
||||
reset,
|
||||
isPreviewAvailable,
|
||||
controller,
|
||||
} = useFieldPreviewContext();
|
||||
const { fields, error } = useStateSelector(controller.state$, (state) => state.previewResponse);
|
||||
|
||||
// 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
|
||||
|
|
|
@ -20,7 +20,6 @@ import { renderToString } from 'react-dom/server';
|
|||
import useDebounce from 'react-use/lib/useDebounce';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { get } from 'lodash';
|
||||
import { castEsToKbnFieldTypeName } from '@kbn/field-types';
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
import { RuntimePrimitiveTypes } from '../../shared_imports';
|
||||
import { useStateSelector } from '../../state_utils';
|
||||
|
@ -32,7 +31,6 @@ import type {
|
|||
Context,
|
||||
Params,
|
||||
EsDocument,
|
||||
ScriptErrorCodes,
|
||||
FetchDocError,
|
||||
FieldPreview,
|
||||
PreviewState,
|
||||
|
@ -101,17 +99,11 @@ export const FieldPreviewProvider: FunctionComponent<{ controller: PreviewContro
|
|||
notifications,
|
||||
api: { getFieldPreview },
|
||||
},
|
||||
fieldFormats,
|
||||
fieldName$,
|
||||
} = useFieldEditorContext();
|
||||
|
||||
const fieldPreview$ = useRef(new BehaviorSubject<FieldPreview[] | undefined>(undefined));
|
||||
|
||||
/** Response from the Painless _execute API */
|
||||
const [previewResponse, setPreviewResponse] = useState<{
|
||||
fields: Context['fields'];
|
||||
error: Context['error'];
|
||||
}>({ fields: [], error: null });
|
||||
const [initialPreviewComplete, setInitialPreviewComplete] = useState(false);
|
||||
|
||||
/** Possible error while fetching sample documents */
|
||||
|
@ -169,45 +161,6 @@ export const FieldPreviewProvider: FunctionComponent<{ controller: PreviewContro
|
|||
);
|
||||
}, [type, script, currentDocId]);
|
||||
|
||||
const setPreviewError = useCallback((error: Context['error']) => {
|
||||
setPreviewResponse((prev) => ({
|
||||
...prev,
|
||||
error,
|
||||
}));
|
||||
}, []);
|
||||
|
||||
const clearPreviewError = useCallback((errorCode: ScriptErrorCodes) => {
|
||||
setPreviewResponse((prev) => {
|
||||
const error = prev.error === null || prev.error?.code === errorCode ? null : prev.error;
|
||||
return {
|
||||
...prev,
|
||||
error,
|
||||
};
|
||||
});
|
||||
}, []);
|
||||
|
||||
const valueFormatter = useCallback(
|
||||
(value: unknown) => {
|
||||
if (format?.id) {
|
||||
const formatter = fieldFormats.getInstance(format.id, format.params);
|
||||
if (formatter) {
|
||||
return formatter.getConverterFor('html')(value) ?? JSON.stringify(value);
|
||||
}
|
||||
}
|
||||
|
||||
if (type) {
|
||||
const fieldType = castEsToKbnFieldTypeName(type);
|
||||
const defaultFormatterForType = fieldFormats.getDefaultInstance(fieldType);
|
||||
if (defaultFormatterForType) {
|
||||
return defaultFormatterForType.getConverterFor('html')(value) ?? JSON.stringify(value);
|
||||
}
|
||||
}
|
||||
|
||||
return defaultValueFormatter(value);
|
||||
},
|
||||
[format, type, fieldFormats]
|
||||
);
|
||||
|
||||
const fetchSampleDocuments = useCallback(
|
||||
async (limit: number = 50) => {
|
||||
if (typeof limit !== 'number') {
|
||||
|
@ -217,7 +170,7 @@ export const FieldPreviewProvider: FunctionComponent<{ controller: PreviewContro
|
|||
|
||||
lastExecutePainlessRequestParams.current.documentId = undefined;
|
||||
setIsFetchingDocument(true);
|
||||
setPreviewResponse({ fields: [], error: null });
|
||||
controller.setPreviewResponse({ fields: [], error: null });
|
||||
|
||||
const [response, searchError] = await search
|
||||
.search({
|
||||
|
@ -335,14 +288,14 @@ export const FieldPreviewProvider: FunctionComponent<{ controller: PreviewContro
|
|||
const updateSingleFieldPreview = useCallback(
|
||||
(fieldName: string, values: unknown[]) => {
|
||||
const [value] = values;
|
||||
const formattedValue = valueFormatter(value);
|
||||
const formattedValue = controller.valueFormatter({ value, type, format });
|
||||
|
||||
setPreviewResponse({
|
||||
controller.setPreviewResponse({
|
||||
fields: [{ key: fieldName, value, formattedValue }],
|
||||
error: null,
|
||||
});
|
||||
},
|
||||
[valueFormatter]
|
||||
[controller, type, format]
|
||||
);
|
||||
|
||||
const updateCompositeFieldPreview = useCallback(
|
||||
|
@ -359,7 +312,7 @@ export const FieldPreviewProvider: FunctionComponent<{ controller: PreviewContro
|
|||
updatedFieldsInScript.push(fieldName);
|
||||
|
||||
const [value] = values;
|
||||
const formattedValue = valueFormatter(value);
|
||||
const formattedValue = controller.valueFormatter({ value, type, format });
|
||||
|
||||
return {
|
||||
key: parentName
|
||||
|
@ -375,12 +328,12 @@ export const FieldPreviewProvider: FunctionComponent<{ controller: PreviewContro
|
|||
.sort((a, b) => a.key.localeCompare(b.key));
|
||||
|
||||
fieldPreview$.current.next(fields);
|
||||
setPreviewResponse({
|
||||
controller.setPreviewResponse({
|
||||
fields,
|
||||
error: null,
|
||||
});
|
||||
},
|
||||
[valueFormatter, parentName, name, fieldPreview$, fieldName$]
|
||||
[parentName, name, fieldPreview$, fieldName$, controller, type, format]
|
||||
);
|
||||
|
||||
const updatePreview = useCallback(async () => {
|
||||
|
@ -437,7 +390,7 @@ export const FieldPreviewProvider: FunctionComponent<{ controller: PreviewContro
|
|||
const { values, error } = response.data;
|
||||
|
||||
if (error) {
|
||||
setPreviewResponse({
|
||||
controller.setPreviewResponse({
|
||||
fields: [
|
||||
{
|
||||
key: name ?? '',
|
||||
|
@ -474,6 +427,7 @@ export const FieldPreviewProvider: FunctionComponent<{ controller: PreviewContro
|
|||
updateSingleFieldPreview,
|
||||
updateCompositeFieldPreview,
|
||||
currentDocIndex,
|
||||
controller,
|
||||
]);
|
||||
|
||||
const reset = useCallback(() => {
|
||||
|
@ -482,7 +436,7 @@ export const FieldPreviewProvider: FunctionComponent<{ controller: PreviewContro
|
|||
previewCount.current = 0;
|
||||
|
||||
controller.setDocuments([]);
|
||||
setPreviewResponse({ fields: [], error: null });
|
||||
controller.setPreviewResponse({ fields: [], error: null });
|
||||
setIsLoadingPreview(false);
|
||||
setIsFetchingDocument(false);
|
||||
}, [controller]);
|
||||
|
@ -490,8 +444,6 @@ export const FieldPreviewProvider: FunctionComponent<{ controller: PreviewContro
|
|||
const ctx = useMemo<Context>(
|
||||
() => ({
|
||||
controller,
|
||||
fields: previewResponse.fields,
|
||||
error: previewResponse.error,
|
||||
fieldPreview$: fieldPreview$.current,
|
||||
isPreviewAvailable,
|
||||
isLoadingPreview,
|
||||
|
@ -521,7 +473,6 @@ export const FieldPreviewProvider: FunctionComponent<{ controller: PreviewContro
|
|||
[
|
||||
controller,
|
||||
currentIdx,
|
||||
previewResponse,
|
||||
fieldPreview$,
|
||||
fetchDocError,
|
||||
params,
|
||||
|
@ -583,74 +534,70 @@ export const FieldPreviewProvider: FunctionComponent<{ controller: PreviewContro
|
|||
* Whenever the name or the format changes we immediately update the preview
|
||||
*/
|
||||
useEffect(() => {
|
||||
setPreviewResponse((prev) => {
|
||||
const { fields } = prev;
|
||||
const { previewResponse: prev } = controller.state$.getValue();
|
||||
const { fields } = prev;
|
||||
|
||||
let updatedFields: Context['fields'] = fields.map((field) => {
|
||||
let key = name ?? '';
|
||||
let updatedFields: PreviewState['previewResponse']['fields'] = fields.map((field) => {
|
||||
let key = name ?? '';
|
||||
|
||||
if (type === 'composite') {
|
||||
// restore initial key segement (the parent name), which was not returned
|
||||
const { 1: fieldName } = field.key.split('.');
|
||||
key = `${name ?? ''}.${fieldName}`;
|
||||
}
|
||||
|
||||
return {
|
||||
...field,
|
||||
key,
|
||||
};
|
||||
});
|
||||
|
||||
// If the user has entered a name but not yet any script we will display
|
||||
// the field in the preview with just the name
|
||||
if (updatedFields.length === 0 && name !== null) {
|
||||
updatedFields = [
|
||||
{ key: name, value: undefined, formattedValue: undefined, type: undefined },
|
||||
];
|
||||
if (type === 'composite') {
|
||||
// restore initial key segement (the parent name), which was not returned
|
||||
const { 1: fieldName } = field.key.split('.');
|
||||
key = `${name ?? ''}.${fieldName}`;
|
||||
}
|
||||
|
||||
return {
|
||||
...prev,
|
||||
fields: updatedFields,
|
||||
...field,
|
||||
key,
|
||||
};
|
||||
});
|
||||
}, [name, type, parentName]);
|
||||
|
||||
// If the user has entered a name but not yet any script we will display
|
||||
// the field in the preview with just the name
|
||||
if (updatedFields.length === 0 && name !== null) {
|
||||
updatedFields = [{ key: name, value: undefined, formattedValue: undefined, type: undefined }];
|
||||
}
|
||||
|
||||
controller.setPreviewResponse({
|
||||
...prev,
|
||||
fields: updatedFields,
|
||||
});
|
||||
}, [name, type, parentName, controller]);
|
||||
|
||||
/**
|
||||
* Whenever the format changes we immediately update the preview
|
||||
*/
|
||||
useEffect(() => {
|
||||
setPreviewResponse((prev) => {
|
||||
const { fields } = prev;
|
||||
const { previewResponse: prev } = controller.state$.getValue();
|
||||
const { fields } = prev;
|
||||
|
||||
return {
|
||||
...prev,
|
||||
fields: fields.map((field) => {
|
||||
const nextValue =
|
||||
script === null && Boolean(document)
|
||||
? get(document?._source, name ?? '') ?? get(document?.fields, name ?? '') // When there is no script we try to read the value from _source/fields
|
||||
: field?.value;
|
||||
controller.setPreviewResponse({
|
||||
...prev,
|
||||
fields: fields.map((field) => {
|
||||
const nextValue =
|
||||
script === null && Boolean(document)
|
||||
? get(document?._source, name ?? '') ?? get(document?.fields, name ?? '') // When there is no script we try to read the value from _source/fields
|
||||
: field?.value;
|
||||
|
||||
const formattedValue = valueFormatter(nextValue);
|
||||
const formattedValue = controller.valueFormatter({ value: nextValue, type, format });
|
||||
|
||||
return {
|
||||
...field,
|
||||
value: nextValue,
|
||||
formattedValue,
|
||||
};
|
||||
}),
|
||||
};
|
||||
return {
|
||||
...field,
|
||||
value: nextValue,
|
||||
formattedValue,
|
||||
};
|
||||
}),
|
||||
});
|
||||
}, [name, script, document, valueFormatter]);
|
||||
}, [name, script, document, controller, type, format]);
|
||||
|
||||
useEffect(() => {
|
||||
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;
|
||||
setPreviewError(null);
|
||||
controller.setPreviewError(null);
|
||||
}
|
||||
}, [script?.source, setPreviewError]);
|
||||
}, [script?.source, controller]);
|
||||
|
||||
// Handle the validation state coming from the Painless DiagnosticAdapter
|
||||
// (see @kbn-monaco/src/painless/diagnostics_adapter.ts)
|
||||
|
@ -677,16 +624,16 @@ export const FieldPreviewProvider: FunctionComponent<{ controller: PreviewContro
|
|||
}),
|
||||
},
|
||||
};
|
||||
setPreviewError(error);
|
||||
controller.setPreviewError(error);
|
||||
|
||||
// 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;
|
||||
} else {
|
||||
// Clear possible previous syntax error
|
||||
clearPreviewError('PAINLESS_SYNTAX_ERROR');
|
||||
controller.clearPreviewError('PAINLESS_SYNTAX_ERROR');
|
||||
}
|
||||
}, [scriptEditorValidation, script?.source, setPreviewError, clearPreviewError]);
|
||||
}, [scriptEditorValidation, script?.source, controller]);
|
||||
|
||||
/**
|
||||
* Whenever updatePreview() changes (meaning whenever a param changes)
|
||||
|
|
|
@ -9,13 +9,23 @@
|
|||
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 { BehaviorObservable } from '../../state_utils';
|
||||
import { EsDocument } from './types';
|
||||
import { EsDocument, ScriptErrorCodes, Params } from './types';
|
||||
import type { FieldFormatsStart } from '../../shared_imports';
|
||||
|
||||
export const defaultValueFormatter = (value: unknown) => {
|
||||
const content = typeof value === 'object' ? JSON.stringify(value) : String(value) ?? '-';
|
||||
return renderToString(<>{content}</>);
|
||||
};
|
||||
|
||||
interface PreviewControllerDependencies {
|
||||
dataView: DataView;
|
||||
search: ISearchStart;
|
||||
fieldFormats: FieldFormatsStart;
|
||||
}
|
||||
|
||||
const previewStateDefault: PreviewState = {
|
||||
|
@ -30,12 +40,14 @@ const previewStateDefault: PreviewState = {
|
|||
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 },
|
||||
};
|
||||
|
||||
export class PreviewController {
|
||||
constructor({ dataView, search }: PreviewControllerDependencies) {
|
||||
constructor({ dataView, search, fieldFormats }: PreviewControllerDependencies) {
|
||||
this.dataView = dataView;
|
||||
this.search = search;
|
||||
this.fieldFormats = fieldFormats;
|
||||
|
||||
this.internalState$ = new BehaviorSubject<PreviewState>({
|
||||
...previewStateDefault,
|
||||
|
@ -44,10 +56,13 @@ export class PreviewController {
|
|||
this.state$ = this.internalState$ as BehaviorObservable<PreviewState>;
|
||||
}
|
||||
|
||||
// dependencies
|
||||
// @ts-ignore
|
||||
private dataView: DataView;
|
||||
// @ts-ignore
|
||||
private search: ISearchStart;
|
||||
private fieldFormats: FieldFormatsStart;
|
||||
|
||||
private internalState$: BehaviorSubject<PreviewState>;
|
||||
state$: BehaviorObservable<PreviewState>;
|
||||
|
||||
|
@ -104,4 +119,52 @@ export class PreviewController {
|
|||
setCustomId = (customId?: string) => {
|
||||
this.updateState({ customId });
|
||||
};
|
||||
|
||||
setPreviewError = (error: PreviewState['previewResponse']['error']) => {
|
||||
this.updateState({
|
||||
previewResponse: { ...this.internalState$.getValue().previewResponse, error },
|
||||
});
|
||||
};
|
||||
|
||||
setPreviewResponse = (previewResponse: PreviewState['previewResponse']) => {
|
||||
this.updateState({ previewResponse });
|
||||
};
|
||||
|
||||
clearPreviewError = (errorCode: ScriptErrorCodes) => {
|
||||
const { previewResponse: prev } = this.internalState$.getValue();
|
||||
const error = prev.error === null || prev.error?.code === errorCode ? null : prev.error;
|
||||
this.updateState({
|
||||
previewResponse: {
|
||||
...prev,
|
||||
error,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
valueFormatter = ({
|
||||
value,
|
||||
format,
|
||||
type,
|
||||
}: {
|
||||
value: unknown;
|
||||
format: Params['format'];
|
||||
type: Params['type'];
|
||||
}) => {
|
||||
if (format?.id) {
|
||||
const formatter = this.fieldFormats.getInstance(format.id, format.params);
|
||||
if (formatter) {
|
||||
return formatter.getConverterFor('html')(value) ?? JSON.stringify(value);
|
||||
}
|
||||
}
|
||||
|
||||
if (type) {
|
||||
const fieldType = castEsToKbnFieldTypeName(type);
|
||||
const defaultFormatterForType = this.fieldFormats.getDefaultInstance(fieldType);
|
||||
if (defaultFormatterForType) {
|
||||
return defaultFormatterForType.getConverterFor('html')(value) ?? JSON.stringify(value);
|
||||
}
|
||||
}
|
||||
|
||||
return defaultValueFormatter(value);
|
||||
};
|
||||
}
|
|
@ -55,6 +55,11 @@ export interface PreviewState {
|
|||
isValid: boolean;
|
||||
message: string | null;
|
||||
};
|
||||
/** Response from the Painless _execute API */
|
||||
previewResponse: {
|
||||
fields: FieldPreview[];
|
||||
error: PreviewError | null;
|
||||
};
|
||||
}
|
||||
|
||||
export interface FetchDocError {
|
||||
|
@ -108,9 +113,7 @@ export type ChangeSet = Record<string, Change>;
|
|||
|
||||
export interface Context {
|
||||
controller: PreviewController;
|
||||
fields: FieldPreview[];
|
||||
fieldPreview$: BehaviorSubject<FieldPreview[] | undefined>;
|
||||
error: PreviewError | null;
|
||||
fieldTypeInfo?: FieldTypeInfo[];
|
||||
initialPreviewComplete: boolean;
|
||||
params: {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue