Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Runtime field editor] Add server side for preview functionality #100029

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
04692cd
Add server side plugin with preview field endpoint
sebelga May 13, 2021
2d8effd
Add ApiService to client code + add a context to pass down services
sebelga May 13, 2021
ea5557f
Setup component integration tests
sebelga May 13, 2021
3a47ca5
[To be reverted] Update field editor to call preview endpoint
sebelga May 13, 2021
aa5d73b
Update route to require "context" to be provided
sebelga May 13, 2021
c547740
Add ES error handling on the server
sebelga May 13, 2021
8c13b54
Skip component integration until migration to the __jest__folder
sebelga May 13, 2021
77df557
Fix tsconfig issue
sebelga May 14, 2021
20157d8
Add FieldPreviewContext to provide preview anywhere down the tree
sebelga May 14, 2021
7582067
Fix tests
sebelga May 14, 2021
f394149
Fix eslint issue
sebelga May 15, 2021
b277a0c
Only fetch documents and preview field for runtime fields
sebelga May 17, 2021
53653b3
Add API integration tests
sebelga May 17, 2021
a7f1747
Fix TS issue
sebelga May 17, 2021
0674134
Access initApi directly from service file and not barrel
sebelga May 17, 2021
71b4160
Avoid importing from barrel to limit page bundle size
sebelga May 18, 2021
7166f11
Update plugin size limit
sebelga May 18, 2021
80f169f
Merge branch 'runtime-field-editor/preview-field-workstream' into run…
kibanamachine May 19, 2021
5ba6755
Merge branch 'runtime-field-editor/preview-field-workstream' into run…
kibanamachine May 19, 2021
0e143e8
Merge branch 'runtime-field-editor/preview-field-workstream' into run…
kibanamachine May 20, 2021
c11b273
Merge branch 'runtime-field-editor/preview-field-workstream' into run…
kibanamachine May 25, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/kbn-optimizer/limits.yml
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ pageLoadAssetSize:
stackAlerts: 29684
presentationUtil: 49767
spacesOss: 18817
indexPatternFieldEditor: 90489
indexPatternFieldEditor: 108063
osquery: 107090
fileUpload: 25664
fileDataVisualizer: 27530
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/*
* 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 { registerTestBed, TestBed } from '@kbn/test/jest';

import { FieldEditor, Props } from '../../public/components/field_editor/field_editor';
import { WithFieldEditorDependencies } from './helpers';

const defaultProps: Props = {
onChange: jest.fn(),
links: {
runtimePainless: 'https://elastic.co',
},
ctx: {
existingConcreteFields: [],
namesNotAllowed: [],
fieldTypeToProcess: 'runtime',
},
indexPattern: { fields: [] } as any,
fieldFormatEditors: {
getAll: () => [],
getById: () => undefined,
},
fieldFormats: {} as any,
uiSettings: {} as any,
syntaxError: {
error: null,
clear: () => {},
},
};

export const setup = (props?: Partial<Props>) => {
const testBed = registerTestBed(WithFieldEditorDependencies(FieldEditor), {
memoryRouter: {
wrapComponent: false,
},
})({ ...defaultProps, ...props }) as TestBed;

return testBed;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/*
* 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 { act } from 'react-dom/test-utils';
import { TestBed } from '@kbn/test/jest';
import { setupEnvironment } from './helpers';
import { setup } from './field_editor.helpers';

describe('<FieldEditor />', () => {
const { server, httpRequestsMockHelpers } = setupEnvironment();

let testBed: TestBed;

afterAll(() => {
server.restore();
});

beforeEach(async () => {
httpRequestsMockHelpers.setFieldPreviewResponse({ message: 'TODO: set by Jest test' });

await act(async () => {
testBed = setup();
});
});

test('it should work', () => {
expect(testBed.component).toBeDefined();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/*
* 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 sinon, { SinonFakeServer } from 'sinon';
import { API_BASE_PATH } from '../../../common/constants';

type HttpResponse = Record<string, any> | any[];

// Register helpers to mock HTTP Requests
const registerHttpRequestMockHelpers = (server: SinonFakeServer) => {
const setFieldPreviewResponse = (response?: HttpResponse, error?: any) => {
const status = error ? error.body.status || 400 : 200;
const body = error ? JSON.stringify(error.body) : JSON.stringify(response);

server.respondWith('POST', `${API_BASE_PATH}/field_preview`, [
status,
{ 'Content-Type': 'application/json' },
body,
]);
};

return {
setFieldPreviewResponse,
};
};

export const init = () => {
const server = sinon.fakeServer.create();
server.respondImmediately = true;

// Define default response for unhandled requests.
// We make requests to APIs which don't impact the component under test, e.g. UI metric telemetry,
// and we can mock them all with a 200 instead of mocking each one individually.
server.respondWith([200, {}, 'DefaultSinonMockServerResponse']);

const httpRequestsMockHelpers = registerHttpRequestMockHelpers(server);

return {
server,
httpRequestsMockHelpers,
};
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
/*
* 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.
*/

export { findTestSubject, TestBed } from '@kbn/test/jest';

export { setupEnvironment, WithFieldEditorDependencies } from './setup_environment';
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
/*
* 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 React from 'react';

const EDITOR_ID = 'testEditor';

jest.mock('@elastic/eui/lib/services/accessibility', () => {
return {
htmlIdGenerator: () => () => `generated-id`,
};
});

jest.mock('@elastic/eui', () => {
const original = jest.requireActual('@elastic/eui');

return {
...original,
EuiComboBox: (props: any) => (
<input
data-test-subj={props['data-test-subj'] || 'mockComboBox'}
data-currentvalue={props.selectedOptions}
value={props.selectedOptions[0]?.value}
onChange={async (syntheticEvent: any) => {
props.onChange([syntheticEvent['0']]);
}}
/>
),
};
});

jest.mock('@kbn/monaco', () => {
const original = jest.requireActual('@kbn/monaco');

return {
...original,
PainlessLang: {
ID: 'painless',
getSuggestionProvider: () => undefined,
getSyntaxErrors: () => ({
[EDITOR_ID]: [],
}),
},
};
});

jest.mock('../../../../kibana_react/public', () => {
const original = jest.requireActual('../../../../kibana_react/public');

/**
* We mock the CodeEditor because it requires the <KibanaReactContextProvider>
* with the uiSettings passed down. Let's use a simple <input /> in our tests.
*/
const CodeEditorMock = (props: any) => {
// Forward our deterministic ID to the consumer
// We need below for the PainlessLang.getSyntaxErrors mock
props.editorDidMount({
getModel() {
return {
id: EDITOR_ID,
};
},
});

return (
<input
data-test-subj={props['data-test-subj'] || 'mockCodeEditor'}
data-value={props.value}
value={props.value}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
props.onChange(e.target.value);
}}
/>
);
};

return {
...original,
toMountPoint: (node: React.ReactNode) => node,
CodeEditor: CodeEditorMock,
};
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
/*
* 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 './jest.mocks';

import React, { FunctionComponent } from 'react';
import axios from 'axios';
import axiosXhrAdapter from 'axios/lib/adapters/xhr';
import { merge } from 'lodash';

import { notificationServiceMock } from '../../../../../core/public/mocks';
import { dataPluginMock } from '../../../../data/public/mocks';
import { FieldEditorProvider, Context } from '../../../public/components/field_editor_context';
import { FieldPreviewProvider } from '../../../public/components/field_preview_context';
import { initApi, ApiService } from '../../../public/lib';
import { init as initHttpRequests } from './http_requests';

const mockHttpClient = axios.create({ adapter: axiosXhrAdapter });
const dataStart = dataPluginMock.createStartContract();
const { search } = dataStart;

export const spySearchResult = jest.fn();

search.search = () =>
({
toPromise: spySearchResult,
} as any);

let apiService: ApiService;

export const setupEnvironment = () => {
// @ts-expect-error Axios does not fullfill HttpSetupn from core but enough for our tests
apiService = initApi(mockHttpClient);
const { server, httpRequestsMockHelpers } = initHttpRequests();

return {
server,
httpRequestsMockHelpers,
};
};

export const WithFieldEditorDependencies = <T extends object = { [key: string]: unknown }>(
Comp: FunctionComponent<T>,
overridingDependencies?: Partial<Context>
) => (props: T) => {
const dependencies: Context = {
indexPattern: { title: 'testIndexPattern' } as any,
fieldTypeToProcess: 'runtime',
services: {
notifications: notificationServiceMock.createStartContract(),
search,
api: apiService,
},
};

const mergedDependencies = merge({}, dependencies, overridingDependencies);

return (
<FieldEditorProvider {...mergedDependencies}>
<FieldPreviewProvider>
<Comp {...props} />
</FieldPreviewProvider>
</FieldEditorProvider>
);
};
9 changes: 9 additions & 0 deletions src/plugins/index_pattern_field_editor/common/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/*
* 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.
*/

export const API_BASE_PATH = '/api/index_pattern_field_editor';
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,8 @@ const setup = (props?: Partial<Props>) => {
};
};

describe('<FieldEditor />', () => {
// Skipping for now, I will unskip after migrating to the __jest__/client_integration folder
describe.skip('<FieldEditor />', () => {
beforeAll(() => {
jest.useFakeTimers();
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import {
DataPublicPluginStart,
} from '../../shared_imports';
import { Field, InternalFieldType, PluginStart } from '../../types';
import { useFieldPreviewContext } from '../field_preview_context';

import { RUNTIME_FIELD_OPTIONS } from './constants';
import { schema } from './form_schema';
Expand Down Expand Up @@ -184,6 +185,7 @@ const FieldEditorComponent = ({
syntaxError,
ctx: { fieldTypeToProcess, namesNotAllowed, existingConcreteFields },
}: Props) => {
const { fields, error, updateParams: updatePreviewParams } = useFieldPreviewContext();
const { form } = useForm<Field, FieldFormInternal>({
defaultValue: field,
schema,
Expand All @@ -198,6 +200,11 @@ const FieldEditorComponent = ({
const nameFieldConfig = getNameFieldConfig(namesNotAllowed, field);
const i18nTexts = geti18nTexts();

const [{ name: updatedName, type: updatedType, script: updatedScript }] = useFormData({ form });
const nameHasChanged = Boolean(field?.name) && field?.name !== updatedName;
const typeHasChanged =
Boolean(field?.type) && field?.type !== (updatedType && updatedType[0].value);

useEffect(() => {
if (onChange) {
onChange({ isValid: isFormValid, isSubmitted, submit });
Expand All @@ -210,10 +217,22 @@ const FieldEditorComponent = ({
clearSyntaxError();
}, [type, clearSyntaxError]);

const [{ name: updatedName, type: updatedType }] = useFormData({ form });
const nameHasChanged = Boolean(field?.name) && field?.name !== updatedName;
const typeHasChanged =
Boolean(field?.type) && field?.type !== (updatedType && updatedType[0].value);
useEffect(() => {
// TODO: remove console.log
if (error) {
console.log('Preview error', error); // eslint-disable-line no-console
} else {
console.log('Field preview:', JSON.stringify(fields[0], null, 4)); // eslint-disable-line no-console
}
}, [fields, error]);

useEffect(() => {
updatePreviewParams({
name: updatedName,
type: updatedType?.[0].value,
script: Boolean(updatedScript?.source.trim()) ? updatedScript : null,
});
}, [updatedName, updatedType, updatedScript, updatePreviewParams]);

return (
<Form form={form} className="indexPatternFieldEditor__form">
Expand Down Expand Up @@ -316,4 +335,4 @@ const FieldEditorComponent = ({
);
};

export const FieldEditor = React.memo(FieldEditorComponent);
export const FieldEditor = React.memo(FieldEditorComponent) as typeof FieldEditorComponent;
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ import { EuiCallOut, EuiSpacer } from '@elastic/eui';

import { UseField, useFormData, ES_FIELD_TYPES, useFormContext } from '../../../shared_imports';
import { FormatSelectEditor, FormatSelectEditorProps } from '../../field_format_editor';
import { FieldFormInternal } from '../field_editor';
import { FieldFormatConfig } from '../../../types';
import type { FieldFormInternal } from '../field_editor';
import type { FieldFormatConfig } from '../../../types';

export const FormatField = ({
indexPattern,
Expand Down
Loading