mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[data views] Field editor accepts DataViewLazy (#187717)
## Summary Data View field editor will now accept a DataView or DataViewLazy to help in moving kibana apps over to DataViewLazy and loading fields on an as-needed basis. Internally, the field editor's namesNotAllowed list has been removed as loading it can be expensive. The painless field editor still needs the full field list for autocomplete suggestions but the list is only loaded for that specific component. DataViewLazy provides a method to get a sorted field list - as a result, number of tests have been updated to accommodate a properly ordered list. Part of https://github.com/elastic/kibana/issues/178926
This commit is contained in:
parent
1dfe886b53
commit
ff3456602a
36 changed files with 310 additions and 197 deletions
|
@ -193,8 +193,8 @@ const UnifiedFieldListSidebarContainer = memo(
|
|||
const deleteField = useMemo(
|
||||
() =>
|
||||
dataView && dataViewFieldEditor && editField
|
||||
? (fieldName: string) => {
|
||||
const ref = dataViewFieldEditor.openDeleteModal({
|
||||
? async (fieldName: string) => {
|
||||
const ref = await dataViewFieldEditor.openDeleteModal({
|
||||
ctx: {
|
||||
dataView,
|
||||
},
|
||||
|
|
|
@ -20,15 +20,22 @@ export const defaultProps: Props = {
|
|||
|
||||
export type FieldEditorTestBed = TestBed & { actions: ReturnType<typeof getCommonActions> };
|
||||
|
||||
export const setup = async (props?: Partial<Props>, deps?: Partial<Context>) => {
|
||||
export const setup = async (
|
||||
props?: Partial<Props>,
|
||||
deps?: Partial<Context>,
|
||||
getByNameOverride?: () => any
|
||||
) => {
|
||||
let testBed: TestBed<string>;
|
||||
|
||||
await act(async () => {
|
||||
testBed = await registerTestBed(WithFieldEditorDependencies(FieldEditor, deps), {
|
||||
memoryRouter: {
|
||||
wrapComponent: false,
|
||||
},
|
||||
})({ ...defaultProps, ...props });
|
||||
testBed = await registerTestBed(
|
||||
WithFieldEditorDependencies(FieldEditor, deps, getByNameOverride),
|
||||
{
|
||||
memoryRouter: {
|
||||
wrapComponent: false,
|
||||
},
|
||||
}
|
||||
)({ ...defaultProps, ...props });
|
||||
});
|
||||
testBed!.component.update();
|
||||
|
||||
|
|
|
@ -121,19 +121,22 @@ describe('<FieldEditor />', () => {
|
|||
});
|
||||
|
||||
describe('validation', () => {
|
||||
test('should accept an optional list of existing fields and prevent creating duplicates', async () => {
|
||||
test('should prevent creating duplicates', async () => {
|
||||
const existingFields = ['myRuntimeField'];
|
||||
testBed = await setup(
|
||||
{
|
||||
onChange,
|
||||
},
|
||||
{
|
||||
namesNotAllowed: {
|
||||
fields: existingFields,
|
||||
runtimeComposites: [],
|
||||
},
|
||||
existingConcreteFields: [],
|
||||
fieldTypeToProcess: 'runtime',
|
||||
},
|
||||
// getByName returns a value, which means that the field already exists
|
||||
() => {
|
||||
return {
|
||||
name: 'myRuntimeField',
|
||||
type: 'boolean',
|
||||
script: { source: 'emit("hello")' },
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
|
@ -155,7 +158,6 @@ describe('<FieldEditor />', () => {
|
|||
});
|
||||
|
||||
test('should not count the default value as a duplicate', async () => {
|
||||
const existingRuntimeFieldNames = ['myRuntimeField'];
|
||||
const field: Field = {
|
||||
name: 'myRuntimeField',
|
||||
type: 'boolean',
|
||||
|
@ -168,11 +170,6 @@ describe('<FieldEditor />', () => {
|
|||
onChange,
|
||||
},
|
||||
{
|
||||
namesNotAllowed: {
|
||||
fields: existingRuntimeFieldNames,
|
||||
runtimeComposites: [],
|
||||
},
|
||||
existingConcreteFields: [],
|
||||
fieldTypeToProcess: 'runtime',
|
||||
}
|
||||
);
|
||||
|
|
|
@ -18,7 +18,7 @@ import {
|
|||
import {
|
||||
WithFieldEditorDependencies,
|
||||
getCommonActions,
|
||||
spyIndexPatternGetAllFields,
|
||||
spyIndexPatternGetByName,
|
||||
spySearchQuery,
|
||||
spySearchQueryResponse,
|
||||
TestDoc,
|
||||
|
@ -34,7 +34,7 @@ const defaultProps: Props = {
|
|||
* @param fields The fields of the index pattern
|
||||
*/
|
||||
export const setIndexPatternFields = (fields: Array<{ name: string; displayName: string }>) => {
|
||||
spyIndexPatternGetAllFields.mockReturnValue(fields);
|
||||
spyIndexPatternGetByName.mockReturnValue(fields);
|
||||
};
|
||||
|
||||
export const getSearchCallMeta = () => {
|
||||
|
|
|
@ -21,6 +21,7 @@ import {
|
|||
setSearchResponse,
|
||||
FieldEditorFlyoutContentTestBed,
|
||||
} from './field_editor_flyout_preview.helpers';
|
||||
import { spyGetFieldsForWildcard } from './helpers/setup_environment';
|
||||
import { mockDocuments, createPreviewError } from './helpers/mocks';
|
||||
|
||||
describe('Field editor Preview panel', () => {
|
||||
|
@ -40,22 +41,23 @@ describe('Field editor Preview panel', () => {
|
|||
|
||||
const indexPatternFields: Array<{ name: string; displayName: string }> = [
|
||||
{
|
||||
name: 'title',
|
||||
displayName: 'title',
|
||||
name: 'description',
|
||||
displayName: 'description',
|
||||
},
|
||||
{
|
||||
name: 'subTitle',
|
||||
displayName: 'subTitle',
|
||||
},
|
||||
{
|
||||
name: 'description',
|
||||
displayName: 'description',
|
||||
name: 'title',
|
||||
displayName: 'title',
|
||||
},
|
||||
];
|
||||
|
||||
beforeEach(async () => {
|
||||
httpRequestsMockHelpers.setFieldPreviewResponse({ values: ['mockedScriptValue'] });
|
||||
setIndexPatternFields(indexPatternFields);
|
||||
spyGetFieldsForWildcard.mockResolvedValue({ fields: indexPatternFields });
|
||||
setSearchResponse(mockDocuments);
|
||||
setSearchResponseLatency(0);
|
||||
|
||||
|
@ -91,16 +93,16 @@ describe('Field editor Preview panel', () => {
|
|||
|
||||
expect(getRenderedIndexPatternFields()).toEqual([
|
||||
{
|
||||
key: 'title',
|
||||
value: mockDocuments[0].fields.title,
|
||||
key: 'description',
|
||||
value: mockDocuments[0].fields.description,
|
||||
},
|
||||
{
|
||||
key: 'subTitle',
|
||||
value: mockDocuments[0].fields.subTitle,
|
||||
},
|
||||
{
|
||||
key: 'description',
|
||||
value: mockDocuments[0].fields.description,
|
||||
key: 'title',
|
||||
value: mockDocuments[0].fields.title,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
@ -126,8 +128,8 @@ describe('Field editor Preview panel', () => {
|
|||
await setFilterFieldsValue('title');
|
||||
expect(exists('emptySearchResult')).toBe(false);
|
||||
expect(getRenderedIndexPatternFields()).toEqual([
|
||||
{ key: 'title', value: 'First doc - title' },
|
||||
{ key: 'subTitle', value: 'First doc - subTitle' },
|
||||
{ key: 'title', value: 'First doc - title' },
|
||||
]);
|
||||
|
||||
// Should display an empty search result with a button to clear
|
||||
|
@ -140,16 +142,16 @@ describe('Field editor Preview panel', () => {
|
|||
component.update();
|
||||
expect(getRenderedIndexPatternFields()).toEqual([
|
||||
{
|
||||
key: 'title',
|
||||
value: mockDocuments[0].fields.title,
|
||||
key: 'description',
|
||||
value: mockDocuments[0].fields.description,
|
||||
},
|
||||
{
|
||||
key: 'subTitle',
|
||||
value: mockDocuments[0].fields.subTitle,
|
||||
},
|
||||
{
|
||||
key: 'description',
|
||||
value: mockDocuments[0].fields.description,
|
||||
key: 'title',
|
||||
value: mockDocuments[0].fields.title,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
@ -174,7 +176,7 @@ describe('Field editor Preview panel', () => {
|
|||
expect(fieldsRendered).not.toBe(null);
|
||||
expect(fieldsRendered!.length).toBe(Object.keys(doc1.fields).length);
|
||||
// make sure that the last one if the "description" field
|
||||
expect(fieldsRendered!.at(2).text()).toBe('descriptionFirst doc - description');
|
||||
expect(fieldsRendered!.at(0).text()).toBe('descriptionFirst doc - description');
|
||||
|
||||
// Click the third field in the list ("description")
|
||||
const descriptionField = fieldsRendered!.at(2);
|
||||
|
@ -182,8 +184,8 @@ describe('Field editor Preview panel', () => {
|
|||
component.update();
|
||||
|
||||
expect(getRenderedIndexPatternFields()).toEqual([
|
||||
{ key: 'description', value: 'First doc - description' }, // Pinned!
|
||||
{ key: 'title', value: 'First doc - title' },
|
||||
{ key: 'description', value: 'First doc - description' }, // Pinned!
|
||||
{ key: 'subTitle', value: 'First doc - subTitle' },
|
||||
]);
|
||||
});
|
||||
|
@ -548,39 +550,39 @@ describe('Field editor Preview panel', () => {
|
|||
|
||||
await fields.updateName('myRuntimeField'); // Give a name to remove empty prompt
|
||||
|
||||
expect(getRenderedIndexPatternFields()[0]).toEqual({
|
||||
expect(getRenderedIndexPatternFields()[2]).toEqual({
|
||||
key: 'title',
|
||||
value: doc1.fields.title,
|
||||
});
|
||||
|
||||
await goToNextDocument();
|
||||
expect(getRenderedIndexPatternFields()[0]).toEqual({
|
||||
expect(getRenderedIndexPatternFields()[2]).toEqual({
|
||||
key: 'title',
|
||||
value: doc2.fields.title,
|
||||
});
|
||||
|
||||
await goToNextDocument();
|
||||
expect(getRenderedIndexPatternFields()[0]).toEqual({
|
||||
expect(getRenderedIndexPatternFields()[2]).toEqual({
|
||||
key: 'title',
|
||||
value: doc3.fields.title,
|
||||
});
|
||||
|
||||
// Going next we circle back to the first document of the list
|
||||
await goToNextDocument();
|
||||
expect(getRenderedIndexPatternFields()[0]).toEqual({
|
||||
expect(getRenderedIndexPatternFields()[2]).toEqual({
|
||||
key: 'title',
|
||||
value: doc1.fields.title,
|
||||
});
|
||||
|
||||
// Let's go backward
|
||||
await goToPreviousDocument();
|
||||
expect(getRenderedIndexPatternFields()[0]).toEqual({
|
||||
expect(getRenderedIndexPatternFields()[2]).toEqual({
|
||||
key: 'title',
|
||||
value: doc3.fields.title,
|
||||
});
|
||||
|
||||
await goToPreviousDocument();
|
||||
expect(getRenderedIndexPatternFields()[0]).toEqual({
|
||||
expect(getRenderedIndexPatternFields()[2]).toEqual({
|
||||
key: 'title',
|
||||
value: doc2.fields.title,
|
||||
});
|
||||
|
@ -618,7 +620,7 @@ describe('Field editor Preview panel', () => {
|
|||
|
||||
// First make sure that we have the original cluster data is loaded
|
||||
// and the preview value rendered.
|
||||
expect(getRenderedIndexPatternFields()[0]).toEqual({
|
||||
expect(getRenderedIndexPatternFields()[2]).toEqual({
|
||||
key: 'title',
|
||||
value: doc1.fields.title,
|
||||
});
|
||||
|
@ -636,16 +638,16 @@ describe('Field editor Preview panel', () => {
|
|||
|
||||
expect(getRenderedIndexPatternFields()).toEqual([
|
||||
{
|
||||
key: 'title',
|
||||
value: 'loaded doc - title',
|
||||
key: 'description',
|
||||
value: 'loaded doc - description',
|
||||
},
|
||||
{
|
||||
key: 'subTitle',
|
||||
value: 'loaded doc - subTitle',
|
||||
},
|
||||
{
|
||||
key: 'description',
|
||||
value: 'loaded doc - description',
|
||||
key: 'title',
|
||||
value: 'loaded doc - title',
|
||||
},
|
||||
]);
|
||||
|
||||
|
@ -734,8 +736,8 @@ describe('Field editor Preview panel', () => {
|
|||
expect(exists('documentsNav')).toBe(false);
|
||||
expect(exists('loadDocsFromClusterButton')).toBe(true);
|
||||
expect(getRenderedIndexPatternFields()[0]).toEqual({
|
||||
key: 'title',
|
||||
value: 'loaded doc - title',
|
||||
key: 'description',
|
||||
value: 'loaded doc - description',
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -14,7 +14,7 @@ export {
|
|||
WithFieldEditorDependencies,
|
||||
spySearchQuery,
|
||||
spySearchQueryResponse,
|
||||
spyIndexPatternGetAllFields,
|
||||
spyIndexPatternGetByName,
|
||||
fieldFormatsOptions,
|
||||
indexPatternNameForTest,
|
||||
setSearchResponseLatency,
|
||||
|
|
|
@ -18,7 +18,7 @@ import { dataPluginMock } from '@kbn/data-plugin/public/mocks';
|
|||
import { fieldFormatsMock as fieldFormats } from '@kbn/field-formats-plugin/common/mocks';
|
||||
import { dataViewPluginMocks } from '@kbn/data-views-plugin/public/mocks';
|
||||
import { FieldFormat } from '@kbn/field-formats-plugin/common';
|
||||
import { createStubDataView } from '@kbn/data-views-plugin/common/data_views/data_view.stub';
|
||||
import { createStubDataViewLazy } from '@kbn/data-views-plugin/common/data_views/data_view_lazy.stub';
|
||||
import type { UsageCollectionStart } from '@kbn/usage-collection-plugin/public';
|
||||
import { PreviewController } from '../../../public/components/preview/preview_controller';
|
||||
import { FieldEditorProvider, Context } from '../../../public/components/field_editor_context';
|
||||
|
@ -32,7 +32,8 @@ const { search } = dataStart;
|
|||
|
||||
export const spySearchQuery = jest.fn();
|
||||
export const spySearchQueryResponse = jest.fn(() => Promise.resolve({}));
|
||||
export const spyIndexPatternGetAllFields = jest.fn().mockImplementation(() => []);
|
||||
export const spyIndexPatternGetByName = jest.fn().mockImplementation(() => {});
|
||||
export const spyGetFieldsForWildcard = jest.fn().mockResolvedValue({ fields: [] });
|
||||
|
||||
let searchResponseDelay = 0;
|
||||
|
||||
|
@ -91,7 +92,8 @@ export const indexPatternNameForTest = 'testIndexPattern';
|
|||
export const WithFieldEditorDependencies =
|
||||
<T extends object = { [key: string]: unknown }>(
|
||||
Comp: FunctionComponent<T>,
|
||||
overridingDependencies?: Partial<Context>
|
||||
overridingDependencies?: Partial<Context>,
|
||||
getByNameOverride?: () => any
|
||||
) =>
|
||||
(props: T) => {
|
||||
// Setup mocks
|
||||
|
@ -119,20 +121,25 @@ export const WithFieldEditorDependencies =
|
|||
return new MockDefaultFieldFormat();
|
||||
});
|
||||
|
||||
const dataView = createStubDataView({
|
||||
const dataView = createStubDataViewLazy({
|
||||
spec: {
|
||||
title: indexPatternNameForTest,
|
||||
},
|
||||
deps: {
|
||||
apiClient: {
|
||||
getFieldsForWildcard: spyGetFieldsForWildcard,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
jest.spyOn(dataView.fields, 'getAll').mockImplementation(spyIndexPatternGetAllFields);
|
||||
jest
|
||||
.spyOn(dataView, 'getFieldByName')
|
||||
.mockImplementation(getByNameOverride || spyIndexPatternGetByName);
|
||||
|
||||
const dependencies: Context = {
|
||||
dataView,
|
||||
uiSettings: uiSettingsServiceMock.createStartContract(),
|
||||
fieldTypeToProcess: 'runtime',
|
||||
existingConcreteFields: [],
|
||||
namesNotAllowed: { fields: [], runtimeComposites: [] },
|
||||
links: {
|
||||
runtimePainless: 'https://elastic.co',
|
||||
},
|
||||
|
@ -162,6 +169,7 @@ export const WithFieldEditorDependencies =
|
|||
notifications: notificationServiceMock.createStartContract(),
|
||||
},
|
||||
dataView,
|
||||
dataViewToUpdate: dataView,
|
||||
onSave: jest.fn(),
|
||||
fieldTypeToProcess: 'runtime',
|
||||
});
|
||||
|
|
|
@ -27,7 +27,7 @@ export interface Props {
|
|||
}
|
||||
|
||||
export const getDeleteFieldProvider = (
|
||||
modalOpener: (options: OpenFieldDeleteModalOptions) => CloseEditor
|
||||
modalOpener: (options: OpenFieldDeleteModalOptions) => Promise<CloseEditor>
|
||||
): React.FunctionComponent<Props> => {
|
||||
return React.memo(({ dataView, children, onDelete }: Props) => {
|
||||
const closeModal = useRef<CloseEditor | null>(null);
|
||||
|
@ -36,7 +36,7 @@ export const getDeleteFieldProvider = (
|
|||
if (closeModal.current) {
|
||||
closeModal.current();
|
||||
}
|
||||
closeModal.current = modalOpener({
|
||||
closeModal.current = await modalOpener({
|
||||
ctx: {
|
||||
dataView,
|
||||
},
|
||||
|
|
|
@ -32,16 +32,12 @@ export interface CompositeEditorProps {
|
|||
}
|
||||
|
||||
export const CompositeEditor = ({ onReset }: CompositeEditorProps) => {
|
||||
const { links, existingConcreteFields, subfields$ } = useFieldEditorContext();
|
||||
const { links, subfields$ } = useFieldEditorContext();
|
||||
const subfields = useObservable(subfields$) || {};
|
||||
|
||||
return (
|
||||
<div data-test-subj="compositeEditor">
|
||||
<ScriptField
|
||||
existingConcreteFields={existingConcreteFields}
|
||||
links={links}
|
||||
placeholder={"emit('field_name', 'hello world');"}
|
||||
/>
|
||||
<ScriptField links={links} placeholder={"emit('field_name', 'hello world');"} />
|
||||
<EuiSpacer size="xl" />
|
||||
<>
|
||||
<EuiFlexGroup gutterSize="s" alignItems="center" justifyContent="spaceBetween">
|
||||
|
|
|
@ -79,7 +79,7 @@ const geti18nTexts = (): {
|
|||
});
|
||||
|
||||
export const FieldDetail = ({}) => {
|
||||
const { links, existingConcreteFields, fieldTypeToProcess } = useFieldEditorContext();
|
||||
const { links, fieldTypeToProcess } = useFieldEditorContext();
|
||||
const i18nTexts = geti18nTexts();
|
||||
return (
|
||||
<>
|
||||
|
@ -114,7 +114,7 @@ export const FieldDetail = ({}) => {
|
|||
data-test-subj="valueRow"
|
||||
withDividerRule
|
||||
>
|
||||
<ScriptField existingConcreteFields={existingConcreteFields} links={links} />
|
||||
<ScriptField links={links} />
|
||||
</FormRow>
|
||||
)}
|
||||
|
||||
|
|
|
@ -107,7 +107,7 @@ const formSerializer = (field: FieldFormInternal): Field => {
|
|||
};
|
||||
|
||||
const FieldEditorComponent = ({ field, onChange, onFormModifiedChange }: Props) => {
|
||||
const { namesNotAllowed, fieldTypeToProcess, fieldName$, subfields$ } = useFieldEditorContext();
|
||||
const { fieldTypeToProcess, fieldName$, subfields$, dataView } = useFieldEditorContext();
|
||||
const {
|
||||
params: { update: updatePreviewParams },
|
||||
fieldPreview$,
|
||||
|
@ -121,7 +121,7 @@ const FieldEditorComponent = ({ field, onChange, onFormModifiedChange }: Props)
|
|||
|
||||
const { submit, isValid: isFormValid, isSubmitted, getFields, isSubmitting } = form;
|
||||
|
||||
const nameFieldConfig = getNameFieldConfig(namesNotAllowed, field);
|
||||
const nameFieldConfig = getNameFieldConfig(dataView, field);
|
||||
|
||||
const [formData] = useFormData<FieldFormInternal>({ form });
|
||||
const isFormModified = useFormIsModified({
|
||||
|
|
|
@ -20,7 +20,7 @@ import { FormatSelectEditor } from '../../field_format_editor';
|
|||
import type { FieldFormInternal } from '../field_editor';
|
||||
|
||||
export const FormatField = () => {
|
||||
const { dataView, uiSettings, fieldFormats, fieldFormatEditors } = useFieldEditorContext();
|
||||
const { uiSettings, fieldFormats, fieldFormatEditors } = useFieldEditorContext();
|
||||
const isMounted = useRef(false);
|
||||
const [{ type }] = useFormData<FieldFormInternal>({ watch: ['name', 'type'] });
|
||||
const { getFields, isSubmitted } = useFormContext();
|
||||
|
@ -67,7 +67,6 @@ export const FormatField = () => {
|
|||
|
||||
<FormatSelectEditor
|
||||
esTypes={typeValue || (['keyword'] as ES_FIELD_TYPES[])}
|
||||
indexPattern={dataView}
|
||||
fieldFormatEditors={fieldFormatEditors}
|
||||
fieldFormats={fieldFormats}
|
||||
uiSettings={uiSettings}
|
||||
|
|
|
@ -32,7 +32,6 @@ import { PreviewState } from '../../preview/types';
|
|||
|
||||
interface Props {
|
||||
links: { runtimePainless: string };
|
||||
existingConcreteFields?: Array<{ name: string; type: string }>;
|
||||
placeholder?: string;
|
||||
}
|
||||
|
||||
|
@ -60,8 +59,9 @@ const currentDocumentIsLoadingSelector = (state: PreviewState) => state.isLoadin
|
|||
const currentErrorSelector = (state: PreviewState) => state.previewResponse?.error;
|
||||
const isLoadingPreviewSelector = (state: PreviewState) => state.isLoadingPreview;
|
||||
const isPreviewAvailableSelector = (state: PreviewState) => state.isPreviewAvailable;
|
||||
const concreteFieldsSelector = (state: PreviewState) => state.concreteFields;
|
||||
|
||||
const ScriptFieldComponent = ({ existingConcreteFields, links, placeholder }: Props) => {
|
||||
const ScriptFieldComponent = ({ links, placeholder }: Props) => {
|
||||
const {
|
||||
validation: { setScriptEditorValidation },
|
||||
} = useFieldPreviewContext();
|
||||
|
@ -75,6 +75,13 @@ const ScriptFieldComponent = ({ existingConcreteFields, links, placeholder }: Pr
|
|||
const isFetchingDoc = useStateSelector(controller.state$, currentDocumentIsLoadingSelector);
|
||||
const isLoadingPreview = useStateSelector(controller.state$, isLoadingPreviewSelector);
|
||||
const isPreviewAvailable = useStateSelector(controller.state$, isPreviewAvailableSelector);
|
||||
/**
|
||||
* An array of existing concrete fields. If the user gives a name to the runtime
|
||||
* field that matches one of the concrete fields, a callout will be displayed
|
||||
* to indicate that this runtime field will shadow the concrete field.
|
||||
* It is also used to provide the list of field autocomplete suggestions to the code editor.
|
||||
*/
|
||||
const concreteFields = useStateSelector(controller.state$, concreteFieldsSelector);
|
||||
const [validationData$, nextValidationData$] = useBehaviorSubject<
|
||||
| {
|
||||
isFetchingDoc: boolean;
|
||||
|
@ -91,8 +98,8 @@ const ScriptFieldComponent = ({ existingConcreteFields, links, placeholder }: Pr
|
|||
const currentDocId = currentDocument?._id;
|
||||
|
||||
const suggestionProvider = useMemo(
|
||||
() => PainlessLang.getSuggestionProvider(painlessContext, existingConcreteFields),
|
||||
[painlessContext, existingConcreteFields]
|
||||
() => PainlessLang.getSuggestionProvider(painlessContext, concreteFields),
|
||||
[painlessContext, concreteFields]
|
||||
);
|
||||
|
||||
const { validateFields } = useFormContext();
|
||||
|
|
|
@ -28,9 +28,13 @@ export interface Change {
|
|||
export type ChangeSet = Record<string, Change>;
|
||||
|
||||
const createNameNotAllowedValidator =
|
||||
(namesNotAllowed: Context['namesNotAllowed']): ValidationFunc<{}, string, string> =>
|
||||
({ value }) => {
|
||||
if (namesNotAllowed.fields.includes(value)) {
|
||||
(dataView: Context['dataView'], fieldName?: string): ValidationFunc<{}, string, string> =>
|
||||
async ({ value }) => {
|
||||
const runtimeComposites = Object.entries(dataView.getAllRuntimeFields())
|
||||
.filter(([, _runtimeField]) => _runtimeField.type === 'composite')
|
||||
.map(([_runtimeFieldName]) => _runtimeFieldName);
|
||||
|
||||
if (value !== fieldName && (await dataView.getFieldByName(value, true))) {
|
||||
return {
|
||||
message: i18n.translate(
|
||||
'indexPatternFieldEditor.editor.runtimeFieldsEditor.existRuntimeFieldNamesValidationErrorMessage',
|
||||
|
@ -39,7 +43,7 @@ const createNameNotAllowedValidator =
|
|||
}
|
||||
),
|
||||
};
|
||||
} else if (namesNotAllowed.runtimeComposites.includes(value)) {
|
||||
} else if (value !== fieldName && runtimeComposites.includes(value)) {
|
||||
return {
|
||||
message: i18n.translate(
|
||||
'indexPatternFieldEditor.editor.runtimeFieldsEditor.existCompositeNamesValidationErrorMessage',
|
||||
|
@ -55,31 +59,21 @@ const createNameNotAllowedValidator =
|
|||
* Dynamically retrieve the config for the "name" field, adding
|
||||
* a validator to avoid duplicated runtime fields to be created.
|
||||
*
|
||||
* @param namesNotAllowed Array of names not allowed for the field "name"
|
||||
* @param field Initial value of the form
|
||||
*/
|
||||
export const getNameFieldConfig = (
|
||||
namesNotAllowed?: Context['namesNotAllowed'],
|
||||
dataView: Context['dataView'],
|
||||
field?: Props['field']
|
||||
): FieldConfig<string, Field> => {
|
||||
const nameFieldConfig = schema.name as FieldConfig<string, Field>;
|
||||
|
||||
if (!namesNotAllowed) {
|
||||
return nameFieldConfig;
|
||||
}
|
||||
|
||||
const filterOutCurrentFieldName = (name: string) => name !== field?.name;
|
||||
|
||||
// Add validation to not allow duplicates
|
||||
return {
|
||||
...nameFieldConfig!,
|
||||
validations: [
|
||||
...(nameFieldConfig.validations ?? []),
|
||||
{
|
||||
validator: createNameNotAllowedValidator({
|
||||
fields: namesNotAllowed.fields.filter(filterOutCurrentFieldName),
|
||||
runtimeComposites: namesNotAllowed.runtimeComposites.filter(filterOutCurrentFieldName),
|
||||
}),
|
||||
validator: createNameNotAllowedValidator(dataView, field?.name),
|
||||
},
|
||||
],
|
||||
};
|
||||
|
|
|
@ -16,7 +16,7 @@ import React, {
|
|||
import { NotificationsStart, CoreStart } from '@kbn/core/public';
|
||||
import type { BehaviorSubject } from 'rxjs';
|
||||
import type {
|
||||
DataView,
|
||||
DataViewLazy,
|
||||
DataPublicPluginStart,
|
||||
FieldFormatsStart,
|
||||
RuntimeFieldSubFields,
|
||||
|
@ -25,7 +25,7 @@ import { ApiService } from '../lib/api';
|
|||
import type { InternalFieldType, PluginStart } from '../types';
|
||||
|
||||
export interface Context {
|
||||
dataView: DataView;
|
||||
dataView: DataViewLazy;
|
||||
fieldTypeToProcess: InternalFieldType;
|
||||
uiSettings: CoreStart['uiSettings'];
|
||||
links: {
|
||||
|
@ -38,22 +38,7 @@ export interface Context {
|
|||
};
|
||||
fieldFormatEditors: PluginStart['fieldFormatEditors'];
|
||||
fieldFormats: FieldFormatsStart;
|
||||
/**
|
||||
* An array of field names not allowed.
|
||||
* e.g we probably don't want a user to give a name of an existing
|
||||
* runtime field (for that the user should edit the existing runtime field).
|
||||
*/
|
||||
namesNotAllowed: {
|
||||
fields: string[];
|
||||
runtimeComposites: string[];
|
||||
};
|
||||
/**
|
||||
* An array of existing concrete fields. If the user gives a name to the runtime
|
||||
* field that matches one of the concrete fields, a callout will be displayed
|
||||
* to indicate that this runtime field will shadow the concrete field.
|
||||
* It is also used to provide the list of field autocomplete suggestions to the code editor.
|
||||
*/
|
||||
existingConcreteFields: Array<{ name: string; type: string }>;
|
||||
|
||||
fieldName$: BehaviorSubject<string>;
|
||||
subfields$: BehaviorSubject<RuntimeFieldSubFields | undefined>;
|
||||
}
|
||||
|
@ -68,8 +53,6 @@ export const FieldEditorProvider: FunctionComponent<PropsWithChildren<Context>>
|
|||
fieldTypeToProcess,
|
||||
fieldFormats,
|
||||
fieldFormatEditors,
|
||||
namesNotAllowed,
|
||||
existingConcreteFields,
|
||||
children,
|
||||
fieldName$,
|
||||
subfields$,
|
||||
|
@ -83,8 +66,6 @@ export const FieldEditorProvider: FunctionComponent<PropsWithChildren<Context>>
|
|||
services,
|
||||
fieldFormats,
|
||||
fieldFormatEditors,
|
||||
namesNotAllowed,
|
||||
existingConcreteFields,
|
||||
fieldName$,
|
||||
subfields$,
|
||||
}),
|
||||
|
@ -96,8 +77,6 @@ export const FieldEditorProvider: FunctionComponent<PropsWithChildren<Context>>
|
|||
uiSettings,
|
||||
fieldFormats,
|
||||
fieldFormatEditors,
|
||||
namesNotAllowed,
|
||||
existingConcreteFields,
|
||||
fieldName$,
|
||||
subfields$,
|
||||
]
|
||||
|
|
|
@ -11,8 +11,9 @@ import { DocLinksStart, NotificationsStart, CoreStart } from '@kbn/core/public';
|
|||
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
import {
|
||||
DataViewField,
|
||||
DataView,
|
||||
DataViewField,
|
||||
DataViewLazy,
|
||||
DataPublicPluginStart,
|
||||
UsageCollectionStart,
|
||||
DataViewsPublicPluginStart,
|
||||
|
@ -37,7 +38,8 @@ export interface Props {
|
|||
/** The docLinks start service from core */
|
||||
docLinks: DocLinksStart;
|
||||
/** The index pattern where the field will be added */
|
||||
dataView: DataView;
|
||||
dataView: DataViewLazy;
|
||||
dataViewToUpdate: DataView | DataViewLazy;
|
||||
/** The Kibana field type of the field to create or edit (default: "runtime") */
|
||||
fieldTypeToProcess: InternalFieldType;
|
||||
/** Optional field to edit */
|
||||
|
@ -72,6 +74,7 @@ export const FieldEditorFlyoutContentContainer = ({
|
|||
docLinks,
|
||||
fieldTypeToProcess,
|
||||
dataView,
|
||||
dataViewToUpdate,
|
||||
dataViews,
|
||||
search,
|
||||
notifications,
|
||||
|
@ -92,6 +95,7 @@ export const FieldEditorFlyoutContentContainer = ({
|
|||
notifications,
|
||||
},
|
||||
dataView,
|
||||
dataViewToUpdate,
|
||||
onSave,
|
||||
fieldToEdit,
|
||||
fieldTypeToProcess,
|
||||
|
@ -116,8 +120,6 @@ export const FieldEditorFlyoutContentContainer = ({
|
|||
services={services}
|
||||
fieldFormatEditors={fieldFormatEditors}
|
||||
fieldFormats={fieldFormats}
|
||||
namesNotAllowed={controller.getNamesNotAllowed()}
|
||||
existingConcreteFields={controller.getExistingConcreteFields()}
|
||||
fieldName$={new BehaviorSubject(fieldToEdit?.name || '')}
|
||||
subfields$={new BehaviorSubject(fieldToEdit?.fields)}
|
||||
>
|
||||
|
|
|
@ -9,7 +9,6 @@
|
|||
import { EuiCode, EuiFormRow, EuiSelect } from '@elastic/eui';
|
||||
import { CoreStart } from '@kbn/core/public';
|
||||
import { ES_FIELD_TYPES, KBN_FIELD_TYPES } from '@kbn/data-plugin/public';
|
||||
import { DataView } from '@kbn/data-views-plugin/public';
|
||||
import type {
|
||||
FieldFormatInstanceType,
|
||||
FieldFormatParams,
|
||||
|
@ -25,7 +24,6 @@ import { FormatEditor } from './format_editor';
|
|||
|
||||
export interface FormatSelectEditorProps {
|
||||
esTypes: ES_FIELD_TYPES[];
|
||||
indexPattern: DataView;
|
||||
fieldFormatEditors: FormatEditorServiceStart['fieldFormatEditors'];
|
||||
fieldFormats: FieldFormatsStart;
|
||||
uiSettings: CoreStart['uiSettings'];
|
||||
|
|
|
@ -50,6 +50,7 @@ function fuzzyMatch(searchValue: string, text: string) {
|
|||
|
||||
const pinnedFieldsSelector = (s: PreviewState) => s.pinnedFields;
|
||||
const currentDocumentSelector = (s: PreviewState) => s.documents[s.currentIdx];
|
||||
const fieldMapSelector = (s: PreviewState) => s.fieldMap;
|
||||
|
||||
interface RowProps {
|
||||
index: number;
|
||||
|
@ -74,13 +75,13 @@ export const PreviewFieldList: React.FC<Props> = ({ height, clearSearch, searchV
|
|||
const { controller } = useFieldPreviewContext();
|
||||
const pinnedFields = useStateSelector(controller.state$, pinnedFieldsSelector, isEqual);
|
||||
const currentDocument = useStateSelector(controller.state$, currentDocumentSelector);
|
||||
const fieldMap = useStateSelector(controller.state$, fieldMapSelector);
|
||||
|
||||
const [showAllFields, setShowAllFields] = useState(false);
|
||||
|
||||
const fieldList: DocumentField[] = useMemo(
|
||||
() =>
|
||||
dataView.fields
|
||||
.getAll()
|
||||
Object.values(fieldMap)
|
||||
.map((field) => {
|
||||
const { name, displayName } = field;
|
||||
const formatter = dataView.getFormatterForField(field);
|
||||
|
@ -95,7 +96,7 @@ export const PreviewFieldList: React.FC<Props> = ({ height, clearSearch, searchV
|
|||
};
|
||||
})
|
||||
.filter(({ value }) => value !== undefined),
|
||||
[dataView, currentDocument?.fields]
|
||||
[dataView, fieldMap, currentDocument?.fields]
|
||||
);
|
||||
|
||||
const fieldListWithPinnedFields: DocumentField[] = useMemo(() => {
|
||||
|
|
|
@ -9,6 +9,7 @@
|
|||
import { i18n } from '@kbn/i18n';
|
||||
import type {
|
||||
DataView,
|
||||
DataViewLazy,
|
||||
DataViewField,
|
||||
DataViewsPublicPluginStart,
|
||||
} from '@kbn/data-views-plugin/public';
|
||||
|
@ -36,7 +37,8 @@ export const defaultValueFormatter = (value: unknown) => {
|
|||
};
|
||||
|
||||
interface PreviewControllerArgs {
|
||||
dataView: DataView;
|
||||
dataView: DataViewLazy;
|
||||
dataViewToUpdate: DataView | DataViewLazy;
|
||||
onSave: (field: DataViewField[]) => void;
|
||||
fieldToEdit?: Field;
|
||||
fieldTypeToProcess: InternalFieldType;
|
||||
|
@ -79,13 +81,25 @@ const previewStateDefault: PreviewState = {
|
|||
/** Flag to show/hide the preview panel */
|
||||
isPanelVisible: true,
|
||||
isSaving: false,
|
||||
concreteFields: [],
|
||||
fieldMap: {},
|
||||
};
|
||||
|
||||
export class PreviewController {
|
||||
constructor({ deps, dataView, onSave, fieldToEdit, fieldTypeToProcess }: PreviewControllerArgs) {
|
||||
constructor({
|
||||
deps,
|
||||
// using two different data view references while API consumers might be passing in
|
||||
// dataView or dataViewLazy. Don't want to rely on DataView with full field list.
|
||||
dataView,
|
||||
dataViewToUpdate,
|
||||
onSave,
|
||||
fieldToEdit,
|
||||
fieldTypeToProcess,
|
||||
}: PreviewControllerArgs) {
|
||||
this.deps = deps;
|
||||
|
||||
this.dataView = dataView;
|
||||
this.dataViewToUpdate = dataViewToUpdate;
|
||||
this.onSave = onSave;
|
||||
|
||||
this.fieldToEdit = fieldToEdit;
|
||||
|
@ -98,10 +112,14 @@ export class PreviewController {
|
|||
this.state$ = this.internalState$ as BehaviorObservable<PreviewState>;
|
||||
|
||||
this.fetchSampleDocuments();
|
||||
|
||||
this.setExistingConcreteFields();
|
||||
this.setFieldList();
|
||||
}
|
||||
|
||||
// dependencies
|
||||
private dataView: DataView;
|
||||
private dataView: DataViewLazy;
|
||||
private dataViewToUpdate: DataView | DataViewLazy;
|
||||
|
||||
private deps: {
|
||||
search: ISearchStart;
|
||||
|
@ -120,11 +138,6 @@ export class PreviewController {
|
|||
|
||||
private previewCount = 0;
|
||||
|
||||
private namesNotAllowed?: {
|
||||
fields: string[];
|
||||
runtimeComposites: string[];
|
||||
};
|
||||
|
||||
private updateState = (newState: Partial<PreviewState>) => {
|
||||
this.internalState$.next({ ...this.state$.getValue(), ...newState });
|
||||
};
|
||||
|
@ -139,68 +152,81 @@ export class PreviewController {
|
|||
documentId: undefined,
|
||||
};
|
||||
|
||||
getNamesNotAllowed = () => {
|
||||
if (!this.namesNotAllowed) {
|
||||
const fieldNames = this.dataView.fields.map((fld) => fld.name);
|
||||
const runtimeCompositeNames = Object.entries(this.dataView.getAllRuntimeFields())
|
||||
.filter(([, _runtimeField]) => _runtimeField.type === 'composite')
|
||||
.map(([_runtimeFieldName]) => _runtimeFieldName);
|
||||
this.namesNotAllowed = {
|
||||
fields: fieldNames,
|
||||
runtimeComposites: runtimeCompositeNames,
|
||||
};
|
||||
}
|
||||
private setFieldList = async () => {
|
||||
const fieldMap = (
|
||||
await this.dataView.getFields({
|
||||
fieldName: ['*'],
|
||||
scripted: false,
|
||||
runtime: false,
|
||||
})
|
||||
).getFieldMapSorted();
|
||||
|
||||
return this.namesNotAllowed;
|
||||
this.updateState({ fieldMap });
|
||||
};
|
||||
|
||||
getExistingConcreteFields = () => {
|
||||
private setExistingConcreteFields = async () => {
|
||||
const existing: Array<{ name: string; type: string }> = [];
|
||||
|
||||
this.dataView.fields
|
||||
.filter((fld) => {
|
||||
const isFieldBeingEdited = this.fieldToEdit?.name === fld.name;
|
||||
return !isFieldBeingEdited && fld.isMapped;
|
||||
const fieldMap = (
|
||||
await this.dataView.getFields({
|
||||
fieldName: ['*'],
|
||||
scripted: false,
|
||||
runtime: false,
|
||||
})
|
||||
.forEach((fld) => {
|
||||
existing.push({
|
||||
name: fld.name,
|
||||
type: (fld.esTypes && fld.esTypes[0]) || '',
|
||||
});
|
||||
});
|
||||
).getFieldMap();
|
||||
|
||||
return existing;
|
||||
// remove name of currently edited field
|
||||
if (this.fieldToEdit?.name) {
|
||||
delete fieldMap[this.fieldToEdit?.name];
|
||||
}
|
||||
|
||||
Object.values(fieldMap).forEach((fld) => {
|
||||
existing.push({
|
||||
name: fld.name,
|
||||
type: (fld.esTypes && fld.esTypes[0]) || '',
|
||||
});
|
||||
});
|
||||
|
||||
this.updateState({ concreteFields: existing });
|
||||
};
|
||||
|
||||
updateConcreteField = (updatedField: Field): DataViewField[] => {
|
||||
const editedField = this.dataView.getFieldByName(updatedField.name);
|
||||
updateConcreteField = async (updatedField: Field): Promise<DataViewField[]> => {
|
||||
const editedField = await this.dataViewToUpdate.getFieldByName(updatedField.name);
|
||||
|
||||
if (!editedField) {
|
||||
throw new Error(
|
||||
`Unable to find field named '${
|
||||
updatedField.name
|
||||
}' on index pattern '${this.dataView.getIndexPattern()}'`
|
||||
}' on index pattern '${this.dataViewToUpdate.getIndexPattern()}'`
|
||||
);
|
||||
}
|
||||
|
||||
// Update custom label, popularity and format
|
||||
this.dataViewToUpdate.setFieldCustomLabel(updatedField.name, updatedField.customLabel);
|
||||
this.dataView.setFieldCustomLabel(updatedField.name, updatedField.customLabel);
|
||||
this.dataViewToUpdate.setFieldCustomDescription(
|
||||
updatedField.name,
|
||||
updatedField.customDescription
|
||||
);
|
||||
this.dataView.setFieldCustomDescription(updatedField.name, updatedField.customDescription);
|
||||
|
||||
if (updatedField.popularity !== undefined) {
|
||||
this.dataViewToUpdate.setFieldCount(updatedField.name, updatedField.popularity || 0);
|
||||
this.dataView.setFieldCount(updatedField.name, updatedField.popularity || 0);
|
||||
}
|
||||
|
||||
if (updatedField.format) {
|
||||
this.dataViewToUpdate.setFieldFormat(updatedField.name, updatedField.format!);
|
||||
this.dataView.setFieldFormat(updatedField.name, updatedField.format!);
|
||||
} else {
|
||||
this.dataViewToUpdate.deleteFieldFormat(updatedField.name);
|
||||
this.dataView.deleteFieldFormat(updatedField.name);
|
||||
}
|
||||
|
||||
return [editedField];
|
||||
};
|
||||
|
||||
updateRuntimeField = (updatedField: Field): DataViewField[] => {
|
||||
updateRuntimeField = async (updatedField: Field): Promise<DataViewField[]> => {
|
||||
const nameHasChanged =
|
||||
Boolean(this.fieldToEdit) && this.fieldToEdit!.name !== updatedField.name;
|
||||
const typeHasChanged =
|
||||
|
@ -211,6 +237,7 @@ export class PreviewController {
|
|||
|
||||
const { script } = updatedField;
|
||||
|
||||
// this seems a bit convoluted
|
||||
if (this.fieldTypeToProcess === 'runtime') {
|
||||
try {
|
||||
this.deps.usageCollection.reportUiCounter(pluginName, METRIC_TYPE.COUNT, 'save_runtime');
|
||||
|
@ -218,9 +245,15 @@ export class PreviewController {
|
|||
} catch {}
|
||||
// rename an existing runtime field
|
||||
if (nameHasChanged || hasChangeToOrFromComposite) {
|
||||
this.dataViewToUpdate.removeRuntimeField(this.fieldToEdit!.name);
|
||||
this.dataView.removeRuntimeField(this.fieldToEdit!.name);
|
||||
}
|
||||
|
||||
this.dataViewToUpdate.addRuntimeField(updatedField.name, {
|
||||
type: updatedField.type as RuntimeType,
|
||||
script,
|
||||
fields: updatedField.fields,
|
||||
});
|
||||
this.dataView.addRuntimeField(updatedField.name, {
|
||||
type: updatedField.type as RuntimeType,
|
||||
script,
|
||||
|
@ -233,7 +266,8 @@ export class PreviewController {
|
|||
} catch {}
|
||||
}
|
||||
|
||||
return this.dataView.addRuntimeField(updatedField.name, updatedField);
|
||||
this.dataView.addRuntimeField(updatedField.name, updatedField);
|
||||
return this.dataViewToUpdate.addRuntimeField(updatedField.name, updatedField);
|
||||
};
|
||||
|
||||
saveField = async (updatedField: Field) => {
|
||||
|
@ -251,8 +285,8 @@ export class PreviewController {
|
|||
try {
|
||||
const editedFields: DataViewField[] =
|
||||
this.fieldTypeToProcess === 'runtime'
|
||||
? this.updateRuntimeField(updatedField)
|
||||
: this.updateConcreteField(updatedField as Field);
|
||||
? await this.updateRuntimeField(updatedField)
|
||||
: await this.updateConcreteField(updatedField as Field);
|
||||
|
||||
const afterSave = () => {
|
||||
const message = i18n.translate('indexPatternFieldEditor.deleteField.savedHeader', {
|
||||
|
@ -264,8 +298,8 @@ export class PreviewController {
|
|||
this.onSave(editedFields);
|
||||
};
|
||||
|
||||
if (this.dataView.isPersisted()) {
|
||||
await this.deps.dataViews.updateSavedObject(this.dataView);
|
||||
if (this.dataViewToUpdate.isPersisted()) {
|
||||
await this.deps.dataViews.updateSavedObject(this.dataViewToUpdate);
|
||||
}
|
||||
afterSave();
|
||||
|
||||
|
|
|
@ -12,6 +12,7 @@ import type {
|
|||
RuntimeField,
|
||||
SerializedFieldFormat,
|
||||
RuntimePrimitiveTypes,
|
||||
DataViewField,
|
||||
} from '../../shared_imports';
|
||||
import type { RuntimeFieldPainlessError } from '../../types';
|
||||
import type { PreviewController } from './preview_controller';
|
||||
|
@ -69,6 +70,8 @@ export interface PreviewState {
|
|||
isPreviewAvailable: boolean;
|
||||
isPanelVisible: boolean;
|
||||
isSaving: boolean;
|
||||
concreteFields: Array<{ name: string; type: string }>;
|
||||
fieldMap: Record<string, DataViewField>;
|
||||
}
|
||||
|
||||
export interface FetchDocError {
|
||||
|
|
|
@ -10,18 +10,28 @@ import { i18n } from '@kbn/i18n';
|
|||
import { METRIC_TYPE } from '@kbn/analytics';
|
||||
import { NotificationsStart } from '@kbn/core/public';
|
||||
import { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public';
|
||||
import { DataView, UsageCollectionStart } from '../shared_imports';
|
||||
import { DataView, DataViewLazy, UsageCollectionStart } from '../shared_imports';
|
||||
import { pluginName } from '../constants';
|
||||
|
||||
export async function removeFields(
|
||||
fieldNames: string[],
|
||||
dataView: DataView,
|
||||
dataView: DataView | DataViewLazy,
|
||||
services: {
|
||||
dataViews: DataViewsPublicPluginStart;
|
||||
usageCollection: UsageCollectionStart;
|
||||
notifications: NotificationsStart;
|
||||
}
|
||||
) {
|
||||
// if we're not handed a DataViewLazy, then check to see if there's one in use
|
||||
// it'll be in the cache
|
||||
if (dataView.id && !(dataView instanceof DataViewLazy)) {
|
||||
const lazy = await services.dataViews.getDataViewLazyFromCache(dataView.id);
|
||||
if (lazy) {
|
||||
fieldNames.forEach((fieldName) => {
|
||||
lazy.removeRuntimeField(fieldName);
|
||||
});
|
||||
}
|
||||
}
|
||||
fieldNames.forEach((fieldName) => {
|
||||
dataView.removeRuntimeField(fieldName);
|
||||
});
|
||||
|
|
|
@ -13,6 +13,7 @@ import {
|
|||
toMountPoint,
|
||||
DataViewsPublicPluginStart,
|
||||
DataView,
|
||||
DataViewLazy,
|
||||
UsageCollectionStart,
|
||||
} from './shared_imports';
|
||||
|
||||
|
@ -29,7 +30,7 @@ export interface OpenFieldDeleteModalOptions {
|
|||
* Config for the delete modal
|
||||
*/
|
||||
ctx: {
|
||||
dataView: DataView;
|
||||
dataView: DataView | DataViewLazy;
|
||||
};
|
||||
/**
|
||||
* Callback fired when fields are deleted
|
||||
|
@ -60,9 +61,9 @@ export class DeleteCompositeSubfield extends Error {
|
|||
|
||||
export const getFieldDeleteModalOpener =
|
||||
({ core, dataViews, usageCollection }: Dependencies) =>
|
||||
(options: OpenFieldDeleteModalOptions): CloseEditor => {
|
||||
async (options: OpenFieldDeleteModalOptions): Promise<CloseEditor> => {
|
||||
if (typeof options.fieldName === 'string') {
|
||||
const fieldToDelete = options.ctx.dataView.getFieldByName(options.fieldName);
|
||||
const fieldToDelete = await options.ctx.dataView.getFieldByName(options.fieldName);
|
||||
// we can check for composite type since composite runtime field definitions themselves don't become fields
|
||||
const doesBelongToCompositeField = fieldToDelete?.runtimeField?.type === 'composite';
|
||||
|
||||
|
|
|
@ -20,9 +20,8 @@ import type {
|
|||
DataViewsPublicPluginStart,
|
||||
FieldFormatsStart,
|
||||
DataViewField,
|
||||
DataViewLazy,
|
||||
} from './shared_imports';
|
||||
import { DataView } from './shared_imports';
|
||||
import { DataView, DataViewLazy } from './shared_imports';
|
||||
import { createKibanaReactContext } from './shared_imports';
|
||||
import type { CloseEditor, Field, InternalFieldType, PluginStart } from './types';
|
||||
|
||||
|
@ -130,13 +129,14 @@ export const getFieldEditorOpener =
|
|||
};
|
||||
};
|
||||
|
||||
const dataView =
|
||||
dataViewLazyOrNot instanceof DataView
|
||||
const dataViewLazy =
|
||||
dataViewLazyOrNot instanceof DataViewLazy
|
||||
? dataViewLazyOrNot
|
||||
: await dataViews.toDataView(dataViewLazyOrNot);
|
||||
: await dataViews.toDataViewLazy(dataViewLazyOrNot);
|
||||
|
||||
const dataViewField = fieldNameToEdit
|
||||
? dataView.getFieldByName(fieldNameToEdit) || getRuntimeField(fieldNameToEdit)
|
||||
? (await dataViewLazy.getFieldByName(fieldNameToEdit, true)) ||
|
||||
getRuntimeField(fieldNameToEdit)
|
||||
: undefined;
|
||||
|
||||
if (fieldNameToEdit && !dataViewField) {
|
||||
|
@ -168,8 +168,8 @@ export const getFieldEditorOpener =
|
|||
customLabel: dataViewField.customLabel,
|
||||
customDescription: dataViewField.customDescription,
|
||||
popularity: dataViewField.count,
|
||||
format: dataView.getFormatterForFieldNoDefault(fieldNameToEdit!)?.toJSON(),
|
||||
...dataView.getRuntimeField(fieldNameToEdit!)!,
|
||||
format: dataViewLazy.getFormatterForFieldNoDefault(fieldNameToEdit!)?.toJSON(),
|
||||
...dataViewLazy.getRuntimeField(fieldNameToEdit!)!,
|
||||
};
|
||||
} else {
|
||||
// Concrete field
|
||||
|
@ -179,7 +179,7 @@ export const getFieldEditorOpener =
|
|||
customLabel: dataViewField.customLabel,
|
||||
customDescription: dataViewField.customDescription,
|
||||
popularity: dataViewField.count,
|
||||
format: dataView.getFormatterForFieldNoDefault(fieldNameToEdit!)?.toJSON(),
|
||||
format: dataViewLazy.getFormatterForFieldNoDefault(fieldNameToEdit!)?.toJSON(),
|
||||
parentName: dataViewField.spec.parentName,
|
||||
};
|
||||
}
|
||||
|
@ -195,7 +195,11 @@ export const getFieldEditorOpener =
|
|||
fieldToEdit={field}
|
||||
fieldToCreate={fieldToCreate}
|
||||
fieldTypeToProcess={fieldTypeToProcess}
|
||||
dataView={dataView}
|
||||
// currently using two dataView versions since API consumer is still potentially using legacy dataView
|
||||
// this is what is used internally
|
||||
dataView={dataViewLazy}
|
||||
// this is what has been passed by API consumer
|
||||
dataViewToUpdate={dataViewLazyOrNot}
|
||||
search={search}
|
||||
dataViews={dataViews}
|
||||
notifications={notifications}
|
||||
|
|
|
@ -118,7 +118,7 @@ describe('DataViewFieldEditorPlugin', () => {
|
|||
isPersisted: () => true,
|
||||
} as unknown as DataView;
|
||||
|
||||
openDeleteModal({
|
||||
await openDeleteModal({
|
||||
onDelete: onDeleteSpy,
|
||||
ctx: { dataView: indexPatternMock },
|
||||
fieldName: ['a', 'b', 'c'],
|
||||
|
@ -147,7 +147,7 @@ describe('DataViewFieldEditorPlugin', () => {
|
|||
test('should return a handler to close the modal', async () => {
|
||||
const { openDeleteModal } = plugin.start(coreStart, pluginStart);
|
||||
|
||||
const closeModal = openDeleteModal({ fieldName: ['a'], ctx: { dataView: {} as any } });
|
||||
const closeModal = await openDeleteModal({ fieldName: ['a'], ctx: { dataView: {} as any } });
|
||||
expect(typeof closeModal).toBe('function');
|
||||
});
|
||||
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
export type { DataPublicPluginStart } from '@kbn/data-plugin/public';
|
||||
|
||||
export type { DataViewsPublicPluginStart, DataViewField } from '@kbn/data-views-plugin/public';
|
||||
export { DataView } from '@kbn/data-views-plugin/public';
|
||||
export { DataView, DataViewLazy } from '@kbn/data-views-plugin/public';
|
||||
export type { FieldFormatsStart } from '@kbn/field-formats-plugin/public';
|
||||
|
||||
export type { UsageCollectionStart } from '@kbn/usage-collection-plugin/public';
|
||||
|
@ -21,7 +21,6 @@ export type {
|
|||
RuntimeFieldSubField,
|
||||
RuntimeFieldSubFields,
|
||||
RuntimePrimitiveTypes,
|
||||
DataViewLazy,
|
||||
} from '@kbn/data-views-plugin/common';
|
||||
export { KBN_FIELD_TYPES, ES_FIELD_TYPES } from '@kbn/data-plugin/common';
|
||||
|
||||
|
|
|
@ -43,7 +43,7 @@ export interface PluginStart {
|
|||
* Method to open the data view field delete fly-out
|
||||
* @param options Configuration options for the fly-out
|
||||
*/
|
||||
openDeleteModal(options: OpenFieldDeleteModalOptions): CloseEditor;
|
||||
openDeleteModal(options: OpenFieldDeleteModalOptions): Promise<CloseEditor>;
|
||||
fieldFormatEditors: FormatEditorServiceStart['fieldFormatEditors'];
|
||||
/**
|
||||
* Convenience method for user permissions checks
|
||||
|
|
|
@ -0,0 +1,61 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
import { FieldFormatsStartCommon } from '@kbn/field-formats-plugin/common';
|
||||
import { fieldFormatsMock } from '@kbn/field-formats-plugin/common/mocks';
|
||||
import { DataViewLazy } from './data_view_lazy';
|
||||
import { DataViewSpec } from '../types';
|
||||
|
||||
/**
|
||||
* Create a custom stub index pattern. Use it in your unit tests where an {@link DataViewLazy} expected.
|
||||
* @param spec - Serialized index pattern object
|
||||
* @param opts - Specify index pattern options
|
||||
* @param deps - Optionally provide dependencies, you can provide a custom field formats implementation, by default a dummy mock is used
|
||||
*
|
||||
* @returns - an {@link DataViewLazy} instance
|
||||
*
|
||||
*
|
||||
* @example
|
||||
*
|
||||
* You can provide a custom implementation or assert calls using jest.spyOn:
|
||||
*
|
||||
* ```ts
|
||||
* const indexPattern = createStubIndexPattern({spec: {title: 'logs-*'}});
|
||||
* const spy = jest.spyOn(indexPattern, 'getFormatterForField');
|
||||
*
|
||||
* // use `spy` as a regular jest mock
|
||||
*
|
||||
* ```
|
||||
*/
|
||||
export const createStubDataViewLazy = ({
|
||||
spec,
|
||||
opts,
|
||||
deps,
|
||||
}: {
|
||||
spec: DataViewSpec;
|
||||
opts?: {
|
||||
shortDotsEnable?: boolean;
|
||||
metaFields?: string[];
|
||||
};
|
||||
deps?: {
|
||||
fieldFormats?: FieldFormatsStartCommon;
|
||||
apiClient?: any;
|
||||
};
|
||||
}): DataViewLazy => {
|
||||
return new DataViewLazy({
|
||||
spec: { version: '1', ...spec },
|
||||
metaFields: opts?.metaFields ?? ['_id', '_type', '_source'],
|
||||
shortDotsEnable: opts?.shortDotsEnable,
|
||||
fieldFormats: deps?.fieldFormats ?? fieldFormatsMock,
|
||||
apiClient: {
|
||||
getFieldsForWildcard: jest.fn().mockResolvedValue({ fields: [] }),
|
||||
...deps?.apiClient,
|
||||
},
|
||||
scriptedFieldsEnabled: true,
|
||||
});
|
||||
};
|
|
@ -491,6 +491,9 @@ describe('DataViewLazy', () => {
|
|||
Object.values((await dataViewLazy.getFields({ fieldName: ['*'] })).getFieldMap()).length -
|
||||
fieldCount
|
||||
).toEqual(2);
|
||||
expect(Object.keys(dataViewLazy.getRuntimeFields({ fieldName: ['new_field.a'] }))).toEqual([
|
||||
'new_field.a',
|
||||
]);
|
||||
expect(dataViewLazy.getRuntimeField('new_field')).toMatchSnapshot();
|
||||
expect((await dataViewLazy.toSpec(toSpecGetAllFields))!.fields!['new_field.a']).toBeDefined();
|
||||
expect((await dataViewLazy.toSpec(toSpecGetAllFields))!.fields!['new_field.b']).toBeDefined();
|
||||
|
|
|
@ -120,7 +120,7 @@ export class DataViewLazy extends AbstractDataView {
|
|||
|
||||
public getRuntimeFields = ({ fieldName = ['*'] }: Pick<GetFieldsParams, 'fieldName'>) =>
|
||||
// getRuntimeFieldSpecMap flattens composites into a list of fields
|
||||
Object.values(this.getRuntimeFieldSpecMap({ fieldName })).reduce<DataViewFieldMap>(
|
||||
Object.values(this.getRuntimeFieldSpecMap({ fieldName: ['*'] })).reduce<DataViewFieldMap>(
|
||||
(col, field) => {
|
||||
if (!fieldMatchesFieldsRequested(field.name, fieldName)) {
|
||||
return col;
|
||||
|
|
|
@ -294,7 +294,7 @@ export interface DataViewsServicePublicMethods {
|
|||
* @param displayErrors - If set false, API consumer is responsible for displaying and handling errors.
|
||||
*/
|
||||
updateSavedObject: (
|
||||
indexPattern: DataView,
|
||||
indexPattern: AbstractDataView,
|
||||
saveAttempts?: number,
|
||||
ignoreErrors?: boolean,
|
||||
displayErrors?: boolean
|
||||
|
@ -315,6 +315,7 @@ export interface DataViewsServicePublicMethods {
|
|||
getAllDataViewLazy: () => Promise<DataViewLazy[]>;
|
||||
|
||||
getDataViewLazy: (id: string) => Promise<DataViewLazy>;
|
||||
getDataViewLazyFromCache: (id: string) => Promise<DataViewLazy | undefined>;
|
||||
|
||||
createDataViewLazy: (spec: DataViewSpec) => Promise<DataViewLazy>;
|
||||
|
||||
|
@ -512,8 +513,10 @@ export class DataViewsService {
|
|||
*/
|
||||
clearInstanceCache = (id?: string) => {
|
||||
if (id) {
|
||||
this.dataViewLazyCache.delete(id);
|
||||
this.dataViewCache.delete(id);
|
||||
} else {
|
||||
this.dataViewLazyCache.clear();
|
||||
this.dataViewCache.clear();
|
||||
}
|
||||
};
|
||||
|
@ -1011,6 +1014,10 @@ export class DataViewsService {
|
|||
}
|
||||
};
|
||||
|
||||
getDataViewLazyFromCache = async (id: string) => {
|
||||
return this.dataViewLazyCache.get(id);
|
||||
};
|
||||
|
||||
/**
|
||||
* Get an index pattern by id, cache optimized.
|
||||
* @param id
|
||||
|
@ -1436,7 +1443,7 @@ export class DataViewsService {
|
|||
// unsaved DataViewLazy changes will not be reflected in the returned DataView
|
||||
async toDataView(dataViewLazy: DataViewLazy) {
|
||||
// if persisted
|
||||
if (dataViewLazy.id) {
|
||||
if (dataViewLazy.id && dataViewLazy.isPersisted()) {
|
||||
return this.get(dataViewLazy.id);
|
||||
}
|
||||
|
||||
|
@ -1460,7 +1467,7 @@ export class DataViewsService {
|
|||
// unsaved DataView changes will not be reflected in the returned DataViewLazy
|
||||
async toDataViewLazy(dataView: DataView) {
|
||||
// if persisted
|
||||
if (dataView.id) {
|
||||
if (dataView.id && dataView.isPersisted()) {
|
||||
const dataViewLazy = await this.getDataViewLazy(dataView.id);
|
||||
return dataViewLazy!;
|
||||
}
|
||||
|
|
|
@ -47,7 +47,7 @@ export type {
|
|||
DataViewsServicePublic,
|
||||
DataViewsServicePublicDeps,
|
||||
} from './data_views_service_public';
|
||||
export { DataViewsApiClient, DataViewsService, DataView } from './data_views';
|
||||
export { DataViewsApiClient, DataViewsService, DataView, DataViewLazy } from './data_views';
|
||||
export type { DataViewListItem } from './data_views';
|
||||
export { UiSettingsPublicToCommon } from './ui_settings_wrapper';
|
||||
|
||||
|
|
|
@ -41,6 +41,7 @@ const createStartContract = (): Start => {
|
|||
getFieldsForIndexPattern: jest.fn(),
|
||||
create: jest.fn().mockReturnValue(Promise.resolve({})),
|
||||
toDataView: jest.fn().mockReturnValue(Promise.resolve({})),
|
||||
toDataViewLazy: jest.fn().mockReturnValue(Promise.resolve({})),
|
||||
} as unknown as jest.Mocked<DataViewsContract>;
|
||||
};
|
||||
|
||||
|
|
|
@ -549,7 +549,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
|
||||
await PageObjects.settings.setFieldFormat(spec.applyFormatterType);
|
||||
if (spec.beforeSave) {
|
||||
await spec.beforeSave(await testSubjects.find('formatRow'));
|
||||
await spec.beforeSave();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
@ -604,7 +604,7 @@ interface FieldFormatEditorSpecDescriptor {
|
|||
* Use it set specific configuration params for applied field formatter
|
||||
* @param formatRowContainer - field format editor container
|
||||
*/
|
||||
beforeSave?: (formatRowContainer: WebElementWrapper) => Promise<void>;
|
||||
beforeSave?: () => Promise<void>;
|
||||
|
||||
/**
|
||||
* An expected formatted value rendered by Discover app,
|
||||
|
|
|
@ -147,8 +147,8 @@ export function getActions(
|
|||
available: (item: FieldVisConfig) => {
|
||||
return item.deletable === true;
|
||||
},
|
||||
onClick: (item: FieldVisConfig) => {
|
||||
dataViewEditorRef.current = services.dataViewFieldEditor?.openDeleteModal({
|
||||
onClick: async (item: FieldVisConfig) => {
|
||||
dataViewEditorRef.current = await services.dataViewFieldEditor?.openDeleteModal({
|
||||
ctx: { dataView },
|
||||
fieldName: item.fieldName!,
|
||||
onDelete: refreshPage,
|
||||
|
|
|
@ -339,7 +339,7 @@ export const InnerFormBasedDataPanel = function InnerFormBasedDataPanel({
|
|||
editPermission
|
||||
? async (fieldName: string) => {
|
||||
const indexPatternInstance = await dataViews.get(currentIndexPattern?.id);
|
||||
closeFieldEditor.current = indexPatternFieldEditor.openDeleteModal({
|
||||
closeFieldEditor.current = await indexPatternFieldEditor.openDeleteModal({
|
||||
ctx: {
|
||||
dataView: indexPatternInstance,
|
||||
},
|
||||
|
|
|
@ -236,7 +236,7 @@ describe('useFieldBrowserOptions', () => {
|
|||
it('should dispatch the proper actions when a field is removed', async () => {
|
||||
let onDelete: ((fields: string[]) => void) | undefined;
|
||||
useKibanaMock().services.data.dataViews.get = () => Promise.resolve({} as DataView);
|
||||
useKibanaMock().services.dataViewFieldEditor.openDeleteModal = (options) => {
|
||||
useKibanaMock().services.dataViewFieldEditor.openDeleteModal = async (options) => {
|
||||
onDelete = options.onDelete;
|
||||
return () => {};
|
||||
};
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue