Skip to content

Commit

Permalink
[Index pattern field editor] Add preview for runtime fields (elastic#…
Browse files Browse the repository at this point in the history
  • Loading branch information
alisonelizabeth authored Aug 14, 2021
1 parent 66926a8 commit d96cffa
Show file tree
Hide file tree
Showing 103 changed files with 5,260 additions and 731 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->

[Home](./index.md) &gt; [kibana-plugin-core-public](./kibana-plugin-core-public.md) &gt; [OverlayFlyoutOpenOptions](./kibana-plugin-core-public.overlayflyoutopenoptions.md) &gt; ["aria-label"](./kibana-plugin-core-public.overlayflyoutopenoptions._aria-label_.md)

## OverlayFlyoutOpenOptions."aria-label" property

<b>Signature:</b>

```typescript
'aria-label'?: string;
```
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,13 @@ export interface OverlayFlyoutOpenOptions

| Property | Type | Description |
| --- | --- | --- |
| ["aria-label"](./kibana-plugin-core-public.overlayflyoutopenoptions._aria-label_.md) | <code>string</code> | |
| ["data-test-subj"](./kibana-plugin-core-public.overlayflyoutopenoptions._data-test-subj_.md) | <code>string</code> | |
| [className](./kibana-plugin-core-public.overlayflyoutopenoptions.classname.md) | <code>string</code> | |
| [closeButtonAriaLabel](./kibana-plugin-core-public.overlayflyoutopenoptions.closebuttonarialabel.md) | <code>string</code> | |
| [hideCloseButton](./kibana-plugin-core-public.overlayflyoutopenoptions.hideclosebutton.md) | <code>boolean</code> | |
| [maxWidth](./kibana-plugin-core-public.overlayflyoutopenoptions.maxwidth.md) | <code>boolean &#124; number &#124; string</code> | |
| [onClose](./kibana-plugin-core-public.overlayflyoutopenoptions.onclose.md) | <code>(flyout: OverlayRef) =&gt; void</code> | EuiFlyout onClose handler. If provided the consumer is responsible for calling flyout.close() to close the flyout; |
| [ownFocus](./kibana-plugin-core-public.overlayflyoutopenoptions.ownfocus.md) | <code>boolean</code> | |
| [size](./kibana-plugin-core-public.overlayflyoutopenoptions.size.md) | <code>EuiFlyoutSize</code> | |

Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->

[Home](./index.md) &gt; [kibana-plugin-core-public](./kibana-plugin-core-public.md) &gt; [OverlayFlyoutOpenOptions](./kibana-plugin-core-public.overlayflyoutopenoptions.md) &gt; [onClose](./kibana-plugin-core-public.overlayflyoutopenoptions.onclose.md)

## OverlayFlyoutOpenOptions.onClose property

EuiFlyout onClose handler. If provided the consumer is responsible for calling flyout.close() to close the flyout;

<b>Signature:</b>

```typescript
onClose?: (flyout: OverlayRef) => void;
```
16 changes: 15 additions & 1 deletion src/core/public/overlays/flyout/flyout_service.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -82,9 +82,15 @@ export interface OverlayFlyoutOpenOptions {
closeButtonAriaLabel?: string;
ownFocus?: boolean;
'data-test-subj'?: string;
'aria-label'?: string;
size?: EuiFlyoutSize;
maxWidth?: boolean | number | string;
hideCloseButton?: boolean;
/**
* EuiFlyout onClose handler.
* If provided the consumer is responsible for calling flyout.close() to close the flyout;
*/
onClose?: (flyout: OverlayRef) => void;
}

interface StartDeps {
Expand Down Expand Up @@ -119,9 +125,17 @@ export class FlyoutService {

this.activeFlyout = flyout;

const onCloseFlyout = () => {
if (options.onClose) {
options.onClose(flyout);
} else {
flyout.close();
}
};

render(
<i18n.Context>
<EuiFlyout {...options} onClose={() => flyout.close()}>
<EuiFlyout {...options} onClose={onCloseFlyout}>
<MountWrapper mount={mount} className="kbnOverlayMountWrapper" />
</EuiFlyout>
</i18n.Context>,
Expand Down
3 changes: 3 additions & 0 deletions src/core/public/public.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -1006,6 +1006,8 @@ export interface OverlayBannersStart {

// @public (undocumented)
export interface OverlayFlyoutOpenOptions {
// (undocumented)
'aria-label'?: string;
// (undocumented)
'data-test-subj'?: string;
// (undocumented)
Expand All @@ -1016,6 +1018,7 @@ export interface OverlayFlyoutOpenOptions {
hideCloseButton?: boolean;
// (undocumented)
maxWidth?: boolean | number | string;
onClose?: (flyout: OverlayRef) => void;
// (undocumented)
ownFocus?: boolean;
// (undocumented)
Expand Down
10 changes: 9 additions & 1 deletion src/plugins/data/common/index_patterns/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,12 @@
* Side Public License, v 1.
*/

export const RUNTIME_FIELD_TYPES = ['keyword', 'long', 'double', 'date', 'ip', 'boolean'] as const;
export const RUNTIME_FIELD_TYPES = [
'keyword',
'long',
'double',
'date',
'ip',
'boolean',
'geo_point',
] as const;
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
---
id: formLibCoreUseFormIsModified
slug: /form-lib/core/use-form-is-modified
title: useFormIsModified()
summary: Know when your form has been modified by the user
tags: ['forms', 'kibana', 'dev']
date: 2021-06-15
---

**Returns:** `boolean`

There might be cases where you need to know if the form has been modified by the user. For example: the user is about to leave the form after making some changes, you might want to show a modal indicating that the changes will be lost.

For that you can use the `useFormIsModified` hook which will update each time any of the field value changes. If the user makes a change and then undoes the change and puts the initial value back, the form **won't be marked** as modified.

**Important:** If you form dynamically adds and removes fields, the `isModified` state will be set to `true` when a field is removed from the DOM **only** if it was declared in the form initial `defaultValue` object.

## Options

### form

**Type:** `FormHook`

The form hook object. It is only required to provide the form hook object in your **root form component**.

```js
const RootFormComponent = () => {
// root form component, where the form object is declared
const { form } = useForm();
const isModified = useFormIsModified({ form });

return (
<Form form={form}>
<ChildComponent />
</Form>
);
};

const ChildComponent = () => {
const isModified = useFormIsModified(); // no need to provide the form object
return (
<div>...</div>
);
};
```

### discard

**Type:** `string[]`

If there are certain fields that you want to discard when checking if the form has been modified you can provide an array of field paths to the `discard` option.
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import React, { useEffect, FunctionComponent } from 'react';
import { act } from 'react-dom/test-utils';

import { registerTestBed, TestBed } from '../shared_imports';
import { FormHook, OnUpdateHandler, FieldConfig } from '../types';
import { FormHook, OnUpdateHandler, FieldConfig, FieldHook } from '../types';
import { useForm } from '../hooks/use_form';
import { Form } from './form';
import { UseField } from './use_field';
Expand Down Expand Up @@ -54,6 +54,145 @@ describe('<UseField />', () => {
});
});

describe('state', () => {
describe('isPristine, isDirty, isModified', () => {
// Dummy component to handle object type data
const ObjectField: React.FC<{ field: FieldHook }> = ({ field: { setValue } }) => {
const onFieldChange = (e: React.ChangeEvent<HTMLInputElement>) => {
// Make sure to set the field value to an **object**
setValue(JSON.parse(e.target.value));
};

return <input onChange={onFieldChange} data-test-subj="testField" />;
};

interface FieldState {
isModified: boolean;
isDirty: boolean;
isPristine: boolean;
value: unknown;
}

const getChildrenFunc = (
onStateChange: (state: FieldState) => void,
Component?: React.ComponentType<{ field: FieldHook }>
) => {
// This is the children passed down to the <UseField path="name" /> of our form
const childrenFunc = (field: FieldHook) => {
const { onChange, isModified, isPristine, isDirty, value } = field;

// Forward the field state to our jest.fn() spy
onStateChange({ isModified, isPristine, isDirty, value });

// Render the child component if any (useful to test the Object field type)
return Component ? (
<Component field={field} />
) : (
<input onChange={onChange} data-test-subj="testField" />
);
};

return childrenFunc;
};

interface Props {
fieldProps: Record<string, any>;
}

const TestComp = ({ fieldProps }: Props) => {
const { form } = useForm();
return (
<Form form={form}>
<UseField path="name" {...fieldProps} />
</Form>
);
};

const onStateChangeSpy = jest.fn<void, [FieldState]>();
const lastFieldState = (): FieldState =>
onStateChangeSpy.mock.calls[onStateChangeSpy.mock.calls.length - 1][0];
const toString = (value: unknown): string =>
typeof value === 'string' ? value : JSON.stringify(value);

const setup = registerTestBed(TestComp, {
defaultProps: { onStateChangeSpy },
memoryRouter: { wrapComponent: false },
});

[
{
description: 'should update the state for field without default values',
initialValue: '',
changedValue: 'changed',
fieldProps: { children: getChildrenFunc(onStateChangeSpy) },
},
{
description: 'should update the state for field with default value in their config',
initialValue: 'initialValue',
changedValue: 'changed',
fieldProps: {
children: getChildrenFunc(onStateChangeSpy),
config: { defaultValue: 'initialValue' },
},
},
{
description: 'should update the state for field with default value passed through props',
initialValue: 'initialValue',
changedValue: 'changed',
fieldProps: {
children: getChildrenFunc(onStateChangeSpy),
defaultValue: 'initialValue',
},
},
// "Object" field type must be JSON.serialized to compare old and new value
// this test makes sure this is done and "isModified" is indeed "false" when
// putting back the original object
{
description: 'should update the state for field with object field type',
initialValue: { initial: 'value' },
changedValue: { foo: 'bar' },
fieldProps: {
children: getChildrenFunc(onStateChangeSpy, ObjectField),
defaultValue: { initial: 'value' },
},
},
].forEach(({ description, fieldProps, initialValue, changedValue }) => {
test(description, async () => {
const { form } = await setup({ fieldProps });

expect(lastFieldState()).toEqual({
isPristine: true,
isDirty: false,
isModified: false,
value: initialValue,
});

await act(async () => {
form.setInputValue('testField', toString(changedValue));
});

expect(lastFieldState()).toEqual({
isPristine: false,
isDirty: true,
isModified: true,
value: changedValue,
});

// Put back to the initial value --> isModified should be false
await act(async () => {
form.setInputValue('testField', toString(initialValue));
});
expect(lastFieldState()).toEqual({
isPristine: false,
isDirty: true,
isModified: false,
value: initialValue,
});
});
});
});
});

describe('validation', () => {
let formHook: FormHook | null = null;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,15 @@ export const FormProvider = ({ children, form }: Props) => (
<FormContext.Provider value={form}>{children}</FormContext.Provider>
);

export const useFormContext = function <T extends FormData = FormData>() {
interface Options {
throwIfNotFound?: boolean;
}

export const useFormContext = function <T extends FormData = FormData>({
throwIfNotFound = true,
}: Options = {}) {
const context = useContext(FormContext) as FormHook<T>;
if (context === undefined) {
if (throwIfNotFound && context === undefined) {
throw new Error('useFormContext must be used within a <FormProvider />');
}
return context;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@
export { useField, InternalFieldConfig } from './use_field';
export { useForm } from './use_form';
export { useFormData } from './use_form_data';
export { useFormIsModified } from './use_form_is_modified';
Loading

0 comments on commit d96cffa

Please sign in to comment.