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 fields editor] Expose editor for consuming apps #82116

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 2 additions & 2 deletions docs/developer/plugin-list.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -466,8 +466,8 @@ Elastic.
|Welcome to the Kibana rollup plugin! This plugin provides Kibana support for Elasticsearch's rollup feature. Please refer to the Elasticsearch documentation to understand rollup indices and how to create rollup jobs.


|{kib-repo}blob/{branch}/x-pack/plugins/runtime_fields[runtimeFields]
|WARNING: Missing README.
|{kib-repo}blob/{branch}/x-pack/plugins/runtime_fields/README.md[runtimeFields]
|Welcome to the home of the runtime field editor and everything related to runtime fields!


|{kib-repo}blob/{branch}/x-pack/plugins/searchprofiler/README.md[searchprofiler]
Expand Down
2 changes: 1 addition & 1 deletion packages/kbn-optimizer/limits.yml
Original file line number Diff line number Diff line change
Expand Up @@ -98,4 +98,4 @@ pageLoadAssetSize:
visualizations: 295025
visualize: 57431
watcher: 43598
runtimeFields: 26275
runtimeFields: 41752
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

import React from 'react';
import { i18n } from '@kbn/i18n';
import { PainlessLang } from '@kbn/monaco';
import { EuiFormRow, EuiDescribedFormGroup } from '@elastic/eui';

import { CodeEditor, UseField } from '../../../shared_imports';
Expand All @@ -18,19 +19,18 @@ interface Props {

export const PainlessScriptParameter = ({ stack }: Props) => {
return (
<UseField path="script.source" config={getFieldConfig('script')}>
<UseField<string> path="script.source" config={getFieldConfig('script')}>
{(scriptField) => {
const error = scriptField.getErrorsMessages();
const isInvalid = error ? Boolean(error.length) : false;

const field = (
<EuiFormRow label={scriptField.label} error={error} isInvalid={isInvalid} fullWidth>
<CodeEditor
languageId="painless"
// 99% width allows the editor to resize horizontally. 100% prevents it from resizing.
width="99%"
languageId={PainlessLang.ID}
width="100%"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can safely remove the width prop and it will default to 100%

/** Width of editor. Defaults to 100%. */
width?: string | number;

height="400px"
value={scriptField.value as string}
value={scriptField.value}
onChange={scriptField.setValue}
options={{
fontSize: 12,
Expand Down
136 changes: 136 additions & 0 deletions x-pack/plugins/runtime_fields/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
# Runtime fields

Welcome to the home of the runtime field editor and everything related to runtime fields!

## The runtime field editor

The runtime field editor is exported in 2 flavours:

* As the content of a `<EuiFlyout />`
* As a standalone component that you can inline anywhere

### Content of a `<EuiFlyout />`

```js
import React, { useState } from 'react';
import { EuiFlyoutBody, EuiButton } from '@elastic/eui';
import { RuntimeFieldEditorFlyoutContent, RuntimeField } from '../runtime_fields/public';

const MyComponent = () => {
const { docLinksStart } = useCoreContext(); // access the core start service
const [isFlyoutVisilbe, setIsFlyoutVisible] = useState(false);

const saveRuntimeField = useCallback((field: RuntimeField) => {
// Do something with the field
}, []);

return (
<>
<EuiButton onClick={() => setIsFlyoutVisible(true)}>Create field</EuiButton>

{isFlyoutVisible && (
<EuiFlyout onClose={() => setIsFlyoutVisible(false)}>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I recall us talking about something similar to this in the past but why is the RuntimeFieldEditorFlyout wrapped in EuiFlyout?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So it can be injected in a flyout when calling core.overalys.openFlyout(). It is a similar mechanism that I put in place for the <GlobalFlyout />.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe renaming RuntimeFieldEditorFlyout to RuntimeFieldEditorFlyoutContent would make the relationship clearer? Otherwise it is a bit of a head-scratcher since the code makes it look like a flyout is being putting inside another flyout.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good idea, I will rename the component 👍

<RuntimeFieldEditorFlyoutContent
onSave={saveRuntimeField}
onCancel={() => setIsFlyoutVisible(false)}
docLinks={docLinksStart}
defaultValue={/*optional runtime field to edit*/}
/>
</EuiFlyout>
)}
</>
)
}
```

#### With the `core.overlays.openFlyout`

As an alternative you can open the flyout with the `core.overlays.openFlyout`. In this case you will need to wrap the editor with the `Provider` from the "kibana_react" plugin as it is a required dependency for the `<CodeEditor />` component.

```js
import React, { useRef } from 'react';
import { EuiButton } from '@elastic/eui';
import { OverlayRef } from 'src/core/public';

import { createKibanaReactContext, toMountPoint } from '../../src/plugins/kibana_react/public';
import { RuntimeFieldEditorFlyoutContent, RuntimeField } from '../runtime_fields/public';

const MyComponent = () => {
// Access the core start service
const { docLinksStart, overlays, uiSettings } = useCoreContext();
const flyoutEditor = useRef<OverlayRef | null>(null);

const { openFlyout } = overlays;

const saveRuntimeField = useCallback((field: RuntimeField) => {
// Do something with the field
}, []);

const openRuntimeFieldEditor = useCallback(() => {
const { Provider: KibanaReactContextProvider } = createKibanaReactContext({ uiSettings });

flyoutEditor.current = openFlyout(
toMountPoint(
<KibanaReactContextProvider>
<RuntimeFieldEditorFlyoutContent
onSave={saveRuntimeField}
onCancel={() => flyoutEditor.current?.close()}
docLinks={docLinksStart}
defaultValue={defaultRuntimeField}
/>
</KibanaReactContextProvider>
)
);
}, [openFlyout, saveRuntimeField, uiSettings]);

return (
<>
<EuiButton onClick={openRuntimeFieldEditor}>Create field</EuiButton>
</>
)
}
```

### Standalone component

```js
import React, { useState } from 'react';
import { EuiButton, EuiSpacer } from '@elastic/eui';
import { RuntimeFieldEditor, RuntimeField, RuntimeFieldFormState } from '../runtime_fields/public';

const MyComponent = () => {
const { docLinksStart } = useCoreContext(); // access the core start service
const [runtimeFieldFormState, setRuntimeFieldFormState] = useState<RuntimeFieldFormState>({
isSubmitted: false,
isValid: undefined,
submit: async() => Promise.resolve({ isValid: false, data: {} as RuntimeField })
});

const { submit, isValid: isFormValid, isSubmitted } = runtimeFieldFormState;

const saveRuntimeField = useCallback(async () => {
const { isValid, data } = await submit();
if (isValid) {
// Do something with the field (data)
}
}, [submit]);

return (
<>
<RuntimeFieldEditor
onChange={setRuntimeFieldFormState}
docLinks={docLinksStart}
defaultValue={/*optional runtime field to edit*/}
/>

<EuiSpacer />

<EuiButton
onClick={saveRuntimeField}
disabled={isSubmitted && !isFormValid}>
Save field
</EuiButton>
</>
)
}
```
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ jest.mock('../../../../../src/plugins/kibana_react/public', () => {
data-test-subj={props['data-test-subj'] || 'mockCodeEditor'}
data-value={props.value}
value={props.value}
onChange={(syntheticEvent: any) => {
props.onChange([syntheticEvent['0']]);
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
props.onChange(e.target.value);
}}
/>
);
Expand Down
6 changes: 5 additions & 1 deletion x-pack/plugins/runtime_fields/public/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,8 @@
* you may not use this file except in compliance with the Elastic License.
*/

export { RuntimeFieldForm } from './runtime_field_form';
export { RuntimeFieldForm, FormState as RuntimeFieldFormState } from './runtime_field_form';

export { RuntimeFieldEditor } from './runtime_field_editor';

export { RuntimeFieldEditorFlyoutContent } from './runtime_field_editor_flyout_content';
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/

export { RuntimeFieldEditor } from './runtime_field_editor';
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { act } from 'react-dom/test-utils';
import { DocLinksStart } from 'src/core/public';

import '../../__jest__/setup_environment';
import { registerTestBed, TestBed } from '../../test_utils';
import { RuntimeField } from '../../types';
import { RuntimeFieldForm, FormState } from '../runtime_field_form/runtime_field_form';
import { RuntimeFieldEditor, Props } from './runtime_field_editor';

const setup = (props?: Props) =>
registerTestBed(RuntimeFieldEditor, {
memoryRouter: {
wrapComponent: false,
},
})(props) as TestBed;

const docLinks: DocLinksStart = {
ELASTIC_WEBSITE_URL: 'https://jestTest.elastic.co',
DOC_LINK_VERSION: 'jest',
links: {} as any,
};

describe('Runtime field editor', () => {
let testBed: TestBed;
let onChange: jest.Mock<Props['onChange']> = jest.fn();

const lastOnChangeCall = (): FormState[] => onChange.mock.calls[onChange.mock.calls.length - 1];

beforeEach(() => {
onChange = jest.fn();
});

test('should render the <RuntimeFieldForm />', () => {
testBed = setup({ docLinks });
const { component } = testBed;

expect(component.find(RuntimeFieldForm).length).toBe(1);
});

test('should accept a defaultValue and onChange prop to forward the form state', async () => {
const defaultValue: RuntimeField = {
name: 'foo',
type: 'date',
script: 'test=123',
};
testBed = setup({ onChange, defaultValue, docLinks });

expect(onChange).toHaveBeenCalled();

let lastState = lastOnChangeCall()[0];
expect(lastState.isValid).toBe(undefined);
expect(lastState.isSubmitted).toBe(false);
expect(lastState.submit).toBeDefined();

let data;
await act(async () => {
({ data } = await lastState.submit());
});
expect(data).toEqual(defaultValue);

// Make sure that both isValid and isSubmitted state are now "true"
lastState = lastOnChangeCall()[0];
expect(lastState.isValid).toBe(true);
expect(lastState.isSubmitted).toBe(true);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/

import React from 'react';
import { DocLinksStart } from 'src/core/public';

import { RuntimeField } from '../../types';
import { getLinks } from '../../lib';
import { RuntimeFieldForm, Props as FormProps } from '../runtime_field_form/runtime_field_form';

export interface Props {
docLinks: DocLinksStart;
defaultValue?: RuntimeField;
onChange?: FormProps['onChange'];
}

export const RuntimeFieldEditor = ({ defaultValue, onChange, docLinks }: Props) => {
const links = getLinks(docLinks);

return <RuntimeFieldForm links={links} defaultValue={defaultValue} onChange={onChange} />;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For now, this component only renders the <RuntimeFieldForm /> but in a later stage it will also render the "Preview field" functionality.

};
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/

export { RuntimeFieldEditorFlyoutContent } from './runtime_field_editor_flyout_content';
Loading