mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[serverless] Advanced Settings - Form component (#166460)
Addresses https://github.com/elastic/kibana/issues/160411 ## Summary This PR adds a package that contains a form component for the Advanced Settings UI in serverless. This implementation was extracted from the the `Form` component in the `advancedSettings` plugin, excluding some functionalities: - The form doesn't support search queries. - The form doesn't divide the settings into categories. ### Testing The form can be tested in the Storybook Preview from the CI build. Some things to be tested: - Making changes to any of the fields displays the bottom bar. - Clicking the Cancel button clears the changes. - Clicking the Save button triggers a `saveChanges` action with the correct changes. - The bottom bar correctly shows the number of unsaved settings. - Toggling the `isSavingEnabled` control to `false` disables all fields. - Toggling the `requirePageReload` control to `true` causes saving of changes to any of the fields to trigger a `showReloadPagePrompt` action. ### Checklist - [x] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md) - [x] [Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html) was added for features that require explanation or tutorials - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios - [x] Any UI touched in this PR is usable by keyboard only (learn more about [keyboard accessibility](https://webaim.org/techniques/keyboard/)) - [x] Any UI touched in this PR does not create any new axe failures (run axe in browser: [FF](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/), [Chrome](https://chrome.google.com/webstore/detail/axe-web-accessibility-tes/lhdoppojpmngadmnindnejefpokejbdd?hl=en-US)) - [x] This renders correctly on smaller devices using a responsive layout. (You can test this [in your browser](https://www.browserstack.com/guide/responsive-testing-on-local-server)) - [x] This was checked for [cross-browser compatibility](https://www.elastic.co/support/matrix#matrix_browsers) <!-- ### Risk Matrix Delete this section if it is not applicable to this PR. Before closing this PR, invite QA, stakeholders, and other developers to identify risks that should be tested prior to the change/feature release. When forming the risk matrix, consider some of the following examples and how they may potentially impact the change: | Risk | Probability | Severity | Mitigation/Notes | |---------------------------|-------------|----------|-------------------------| | Multiple Spaces—unexpected behavior in non-default Kibana Space. | Low | High | Integration tests will verify that all features are still supported in non-default Kibana Space and when user switches between spaces. | | Multiple nodes—Elasticsearch polling might have race conditions when multiple Kibana nodes are polling for the same tasks. | High | Low | Tasks are idempotent, so executing them multiple times will not result in logical error, but will degrade performance. To test for this case we add plenty of unit tests around this logic and document manual testing procedure. | | Code should gracefully handle cases when feature X or plugin Y are disabled. | Medium | High | Unit tests will verify that any feature flag or plugin combination still results in our service operational. | | [See more potential risk examples](https://github.com/elastic/kibana/blob/main/RISK_MATRIX.mdx) | ### For maintainers - [ ] This was checked for breaking API changes and was [labeled appropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process) --> --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Clint Andrew Hall <clint@clintandrewhall.com>
This commit is contained in:
parent
62f9b56c4c
commit
fd1a1f93f3
26 changed files with 1075 additions and 1 deletions
1
.github/CODEOWNERS
vendored
1
.github/CODEOWNERS
vendored
|
@ -484,6 +484,7 @@ packages/kbn-management/cards_navigation @elastic/platform-deployment-management
|
|||
src/plugins/management @elastic/platform-deployment-management
|
||||
packages/kbn-management/settings/components/field_input @elastic/platform-deployment-management
|
||||
packages/kbn-management/settings/components/field_row @elastic/platform-deployment-management
|
||||
packages/kbn-management/settings/components/form @elastic/platform-deployment-management
|
||||
packages/kbn-management/settings/field_definition @elastic/platform-deployment-management
|
||||
packages/kbn-management/settings/setting_ids @elastic/appex-sharedux @elastic/platform-deployment-management
|
||||
packages/kbn-management/settings/section_registry @elastic/appex-sharedux @elastic/platform-deployment-management
|
||||
|
|
|
@ -506,6 +506,7 @@
|
|||
"@kbn/management-plugin": "link:src/plugins/management",
|
||||
"@kbn/management-settings-components-field-input": "link:packages/kbn-management/settings/components/field_input",
|
||||
"@kbn/management-settings-components-field-row": "link:packages/kbn-management/settings/components/field_row",
|
||||
"@kbn/management-settings-components-form": "link:packages/kbn-management/settings/components/form",
|
||||
"@kbn/management-settings-field-definition": "link:packages/kbn-management/settings/field_definition",
|
||||
"@kbn/management-settings-ids": "link:packages/kbn-management/settings/setting_ids",
|
||||
"@kbn/management-settings-section-registry": "link:packages/kbn-management/settings/section_registry",
|
||||
|
|
18
packages/kbn-management/settings/components/form/README.mdx
Normal file
18
packages/kbn-management/settings/components/form/README.mdx
Normal file
|
@ -0,0 +1,18 @@
|
|||
---
|
||||
id: management/settings/components/form
|
||||
slug: /management/settings/components/form
|
||||
title: Management Settings Form Component
|
||||
description: A package containing a component for rendering the form in the Advanced Settings UI.
|
||||
tags: ['management', 'settings']
|
||||
date: 2023-09-12
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
This package contains a component for rendering the Advanced Settings UI form that contains `FieldRow` components, each of which displays a single UiSetting field row.
|
||||
The form also handles the logic for saving any changes to the UiSettings values by directly communicating with the uiSettings service.
|
||||
|
||||
|
||||
## Notes
|
||||
|
||||
- This implementation was extracted from the `Form` component in the `advancedSettings` plugin.
|
|
@ -0,0 +1,79 @@
|
|||
/*
|
||||
* 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';
|
||||
import { fireEvent, render } from '@testing-library/react';
|
||||
import {
|
||||
BottomBar,
|
||||
BottomBarProps,
|
||||
DATA_TEST_SUBJ_SAVE_BUTTON,
|
||||
DATA_TEST_SUBJ_CANCEL_BUTTON,
|
||||
} from './bottom_bar';
|
||||
import { wrap } from '../mocks';
|
||||
|
||||
const saveAll = jest.fn();
|
||||
const clearAllUnsaved = jest.fn();
|
||||
const unsavedChangesCount = 3;
|
||||
|
||||
const defaultProps: BottomBarProps = {
|
||||
onSaveAll: saveAll,
|
||||
onClearAllUnsaved: clearAllUnsaved,
|
||||
hasInvalidChanges: false,
|
||||
unsavedChangesCount,
|
||||
isLoading: false,
|
||||
};
|
||||
|
||||
const unsavedChangesCountText = unsavedChangesCount + ' unsaved settings';
|
||||
|
||||
describe('BottomBar', () => {
|
||||
it('renders without errors', () => {
|
||||
const { container } = render(wrap(<BottomBar {...defaultProps} />));
|
||||
expect(container).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('fires saveAll when the Save button is clicked', () => {
|
||||
const { getByTestId } = render(wrap(<BottomBar {...defaultProps} />));
|
||||
|
||||
const input = getByTestId(DATA_TEST_SUBJ_SAVE_BUTTON);
|
||||
fireEvent.click(input);
|
||||
expect(saveAll).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('fires clearAllUnsaved when the Cancel button is clicked', () => {
|
||||
const { getByTestId } = render(wrap(<BottomBar {...defaultProps} />));
|
||||
|
||||
const input = getByTestId(DATA_TEST_SUBJ_CANCEL_BUTTON);
|
||||
fireEvent.click(input);
|
||||
expect(saveAll).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('renders unsaved changes count', () => {
|
||||
const { getByText } = render(wrap(<BottomBar {...defaultProps} />));
|
||||
|
||||
expect(getByText(unsavedChangesCountText)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('save button is disabled when there are invalid changes', () => {
|
||||
const { getByTestId } = render(
|
||||
wrap(<BottomBar {...{ ...defaultProps, hasInvalidChanges: true }} />)
|
||||
);
|
||||
|
||||
const input = getByTestId(DATA_TEST_SUBJ_SAVE_BUTTON);
|
||||
expect(input).toBeDisabled();
|
||||
});
|
||||
|
||||
it('save button is loading when in loading state', () => {
|
||||
const { getByTestId, getByLabelText } = render(
|
||||
wrap(<BottomBar {...{ ...defaultProps, isLoading: true }} />)
|
||||
);
|
||||
|
||||
const input = getByTestId(DATA_TEST_SUBJ_SAVE_BUTTON);
|
||||
expect(input).toBeDisabled();
|
||||
expect(getByLabelText('Loading')).toBeInTheDocument();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,104 @@
|
|||
/*
|
||||
* 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';
|
||||
|
||||
import {
|
||||
EuiBottomBar,
|
||||
EuiButton,
|
||||
EuiButtonEmpty,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiToolTip,
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { UnsavedCount } from './unsaved_count';
|
||||
import { useFormStyles } from '../form.styles';
|
||||
|
||||
export const DATA_TEST_SUBJ_SAVE_BUTTON = 'settings-save-button';
|
||||
export const DATA_TEST_SUBJ_CANCEL_BUTTON = 'settings-cancel-button';
|
||||
|
||||
/**
|
||||
* Props for a {@link BottomBar} component.
|
||||
*/
|
||||
export interface BottomBarProps {
|
||||
onSaveAll: () => void;
|
||||
onClearAllUnsaved: () => void;
|
||||
hasInvalidChanges: boolean;
|
||||
isLoading: boolean;
|
||||
unsavedChangesCount: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Component for displaying the bottom bar of a {@link Form}.
|
||||
*/
|
||||
export const BottomBar = ({
|
||||
onSaveAll,
|
||||
onClearAllUnsaved,
|
||||
hasInvalidChanges,
|
||||
isLoading,
|
||||
unsavedChangesCount,
|
||||
}: BottomBarProps) => {
|
||||
const { cssFormButton, cssFormUnsavedCount } = useFormStyles();
|
||||
|
||||
return (
|
||||
<EuiBottomBar>
|
||||
<EuiFlexGroup
|
||||
justifyContent="spaceBetween"
|
||||
alignItems="center"
|
||||
responsive={false}
|
||||
gutterSize="s"
|
||||
>
|
||||
<EuiFlexItem grow={false} css={cssFormUnsavedCount}>
|
||||
<UnsavedCount unsavedCount={unsavedChangesCount} />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem />
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonEmpty
|
||||
css={cssFormButton}
|
||||
color="ghost"
|
||||
size="s"
|
||||
iconType="cross"
|
||||
onClick={onClearAllUnsaved}
|
||||
data-test-subj={DATA_TEST_SUBJ_CANCEL_BUTTON}
|
||||
>
|
||||
{i18n.translate('management.settings.form.cancelButtonLabel', {
|
||||
defaultMessage: 'Cancel changes',
|
||||
})}
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiToolTip
|
||||
content={
|
||||
hasInvalidChanges &&
|
||||
i18n.translate('management.settings.form.saveButtonTooltipWithInvalidChanges', {
|
||||
defaultMessage: 'Fix invalid settings before saving.',
|
||||
})
|
||||
}
|
||||
>
|
||||
<EuiButton
|
||||
css={cssFormButton}
|
||||
disabled={hasInvalidChanges}
|
||||
color="success"
|
||||
fill
|
||||
size="s"
|
||||
iconType="check"
|
||||
onClick={onSaveAll}
|
||||
isLoading={isLoading}
|
||||
data-test-subj={DATA_TEST_SUBJ_SAVE_BUTTON}
|
||||
>
|
||||
{i18n.translate('management.settings.form.saveButtonLabel', {
|
||||
defaultMessage: 'Save changes',
|
||||
})}
|
||||
</EuiButton>
|
||||
</EuiToolTip>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiBottomBar>
|
||||
);
|
||||
};
|
|
@ -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 { BottomBar } from './bottom_bar';
|
|
@ -0,0 +1,41 @@
|
|||
/*
|
||||
* 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';
|
||||
|
||||
import { EuiText } from '@elastic/eui';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { useFormStyles } from '../form.styles';
|
||||
|
||||
/**
|
||||
* Props for a {@link UnsavedCount} component.
|
||||
*/
|
||||
interface UnsavedCountProps {
|
||||
unsavedCount: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Component for displaying the count of unsaved changes in a {@link BottomBar}.
|
||||
*/
|
||||
export const UnsavedCount = ({ unsavedCount }: UnsavedCountProps) => {
|
||||
const { cssFormUnsavedCountMessage } = useFormStyles();
|
||||
return (
|
||||
<EuiText size="s" color="ghost" css={cssFormUnsavedCountMessage}>
|
||||
<FormattedMessage
|
||||
id="management.settings.form.countOfSettingsChanged"
|
||||
defaultMessage="{unsavedCount} unsaved {unsavedCount, plural,
|
||||
one {setting}
|
||||
other {settings}
|
||||
}"
|
||||
values={{
|
||||
unsavedCount,
|
||||
}}
|
||||
/>
|
||||
</EuiText>
|
||||
);
|
||||
};
|
|
@ -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 { useEuiTheme, euiBreakpoint } from '@elastic/eui';
|
||||
import { css } from '@emotion/react';
|
||||
|
||||
/**
|
||||
* A React hook that provides stateful `css` classes for the {@link Form} component.
|
||||
*/
|
||||
export const useFormStyles = () => {
|
||||
const euiTheme = useEuiTheme();
|
||||
const { size, colors } = euiTheme.euiTheme;
|
||||
|
||||
return {
|
||||
cssFormButton: css`
|
||||
width: 100%;
|
||||
`,
|
||||
cssFormUnsavedCount: css`
|
||||
${euiBreakpoint(euiTheme, ['xs'])} {
|
||||
display: none;
|
||||
}
|
||||
`,
|
||||
cssFormUnsavedCountMessage: css`
|
||||
box-shadow: -${size.xs} 0 ${colors.warning};
|
||||
padding-left: ${size.s};
|
||||
`,
|
||||
};
|
||||
};
|
144
packages/kbn-management/settings/components/form/form.test.tsx
Normal file
144
packages/kbn-management/settings/components/form/form.test.tsx
Normal file
|
@ -0,0 +1,144 @@
|
|||
/*
|
||||
* 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';
|
||||
import { fireEvent, render, waitFor } from '@testing-library/react';
|
||||
|
||||
import { FieldDefinition, SettingType } from '@kbn/management-settings-types';
|
||||
import { getFieldDefinitions } from '@kbn/management-settings-field-definition';
|
||||
|
||||
import { Form } from './form';
|
||||
import { wrap, getSettingsMock, createFormServicesMock, uiSettingsClientMock } from './mocks';
|
||||
import { TEST_SUBJ_PREFIX_FIELD } from '@kbn/management-settings-components-field-input/input';
|
||||
import { DATA_TEST_SUBJ_SAVE_BUTTON, DATA_TEST_SUBJ_CANCEL_BUTTON } from './bottom_bar/bottom_bar';
|
||||
import { FormServices } from './types';
|
||||
|
||||
const settingsMock = getSettingsMock();
|
||||
const fields: Array<FieldDefinition<SettingType>> = getFieldDefinitions(
|
||||
settingsMock,
|
||||
uiSettingsClientMock
|
||||
);
|
||||
|
||||
describe('Form', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('renders without errors', () => {
|
||||
const { container } = render(wrap(<Form fields={fields} isSavingEnabled={true} />));
|
||||
|
||||
expect(container).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders as read only if saving is disabled', () => {
|
||||
const { getByTestId } = render(wrap(<Form fields={fields} isSavingEnabled={false} />));
|
||||
|
||||
(Object.keys(settingsMock) as SettingType[]).forEach((type) => {
|
||||
if (type === 'json' || type === 'markdown') {
|
||||
return;
|
||||
}
|
||||
|
||||
const inputTestSubj = `${TEST_SUBJ_PREFIX_FIELD}-${type}`;
|
||||
|
||||
if (type === 'color') {
|
||||
expect(getByTestId(`euiColorPickerAnchor ${inputTestSubj}`)).toBeDisabled();
|
||||
} else {
|
||||
expect(getByTestId(inputTestSubj)).toBeDisabled();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('renders bottom bar when a field is changed', () => {
|
||||
const { getByTestId, queryByTestId } = render(
|
||||
wrap(<Form fields={fields} isSavingEnabled={true} />)
|
||||
);
|
||||
|
||||
expect(queryByTestId(DATA_TEST_SUBJ_SAVE_BUTTON)).not.toBeInTheDocument();
|
||||
expect(queryByTestId(DATA_TEST_SUBJ_CANCEL_BUTTON)).not.toBeInTheDocument();
|
||||
|
||||
const testFieldType = 'string';
|
||||
const input = getByTestId(`${TEST_SUBJ_PREFIX_FIELD}-${testFieldType}`);
|
||||
fireEvent.change(input, { target: { value: 'test' } });
|
||||
|
||||
expect(getByTestId(DATA_TEST_SUBJ_SAVE_BUTTON)).toBeInTheDocument();
|
||||
expect(getByTestId(DATA_TEST_SUBJ_CANCEL_BUTTON)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('fires saveChanges when Save button is clicked', async () => {
|
||||
const services: FormServices = createFormServicesMock();
|
||||
const { getByTestId } = render(wrap(<Form fields={fields} isSavingEnabled={true} />, services));
|
||||
|
||||
const testFieldType = 'string';
|
||||
const input = getByTestId(`${TEST_SUBJ_PREFIX_FIELD}-${testFieldType}`);
|
||||
fireEvent.change(input, { target: { value: 'test' } });
|
||||
|
||||
const saveButton = getByTestId(DATA_TEST_SUBJ_SAVE_BUTTON);
|
||||
fireEvent.click(saveButton);
|
||||
|
||||
expect(services.saveChanges).toHaveBeenCalledWith({
|
||||
string: { type: 'string', unsavedValue: 'test' },
|
||||
});
|
||||
});
|
||||
|
||||
it('clears changes when Cancel button is clicked', () => {
|
||||
const { getByTestId } = render(wrap(<Form fields={fields} isSavingEnabled={false} />));
|
||||
|
||||
const testFieldType = 'string';
|
||||
const input = getByTestId(`${TEST_SUBJ_PREFIX_FIELD}-${testFieldType}`);
|
||||
fireEvent.change(input, { target: { value: 'test' } });
|
||||
|
||||
const cancelButton = getByTestId(DATA_TEST_SUBJ_CANCEL_BUTTON);
|
||||
fireEvent.click(cancelButton);
|
||||
|
||||
expect(input).toHaveValue(settingsMock[testFieldType].value);
|
||||
});
|
||||
|
||||
it('fires showError when saving is unsuccessful', () => {
|
||||
const services: FormServices = createFormServicesMock();
|
||||
const saveChangesWithError = jest.fn(() => {
|
||||
throw new Error('Unable to save');
|
||||
});
|
||||
const testServices = { ...services, saveChanges: saveChangesWithError };
|
||||
|
||||
const { getByTestId } = render(
|
||||
wrap(<Form fields={fields} isSavingEnabled={true} />, testServices)
|
||||
);
|
||||
|
||||
const testFieldType = 'string';
|
||||
const input = getByTestId(`${TEST_SUBJ_PREFIX_FIELD}-${testFieldType}`);
|
||||
fireEvent.change(input, { target: { value: 'test' } });
|
||||
|
||||
const saveButton = getByTestId(DATA_TEST_SUBJ_SAVE_BUTTON);
|
||||
fireEvent.click(saveButton);
|
||||
|
||||
expect(testServices.showError).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('fires showReloadPagePrompt when changing a reloadPageRequired setting', async () => {
|
||||
const services: FormServices = createFormServicesMock();
|
||||
// Make all settings require a page reload
|
||||
const testFields: Array<FieldDefinition<SettingType>> = getFieldDefinitions(
|
||||
getSettingsMock(true),
|
||||
uiSettingsClientMock
|
||||
);
|
||||
const { getByTestId } = render(
|
||||
wrap(<Form fields={testFields} isSavingEnabled={true} />, services)
|
||||
);
|
||||
|
||||
const testFieldType = 'string';
|
||||
const input = getByTestId(`${TEST_SUBJ_PREFIX_FIELD}-${testFieldType}`);
|
||||
fireEvent.change(input, { target: { value: 'test' } });
|
||||
|
||||
const saveButton = getByTestId(DATA_TEST_SUBJ_SAVE_BUTTON);
|
||||
fireEvent.click(saveButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(services.showReloadPagePrompt).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
86
packages/kbn-management/settings/components/form/form.tsx
Normal file
86
packages/kbn-management/settings/components/form/form.tsx
Normal file
|
@ -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, { Fragment } from 'react';
|
||||
|
||||
import type { FieldDefinition } from '@kbn/management-settings-types';
|
||||
import { FieldRow, RowOnChangeFn } from '@kbn/management-settings-components-field-row';
|
||||
import { SettingType, UnsavedFieldChange } from '@kbn/management-settings-types';
|
||||
import { isEmpty } from 'lodash';
|
||||
import { BottomBar } from './bottom_bar';
|
||||
import { useSave } from './use_save';
|
||||
|
||||
/**
|
||||
* Props for a {@link Form} component.
|
||||
*/
|
||||
export interface FormProps {
|
||||
/** A list of {@link FieldDefinition} corresponding to settings to be displayed in the form. */
|
||||
fields: Array<FieldDefinition<SettingType>>;
|
||||
/** True if saving settings is enabled, false otherwise. */
|
||||
isSavingEnabled: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Component for displaying a set of {@link FieldRow} in a form.
|
||||
* @param props The {@link FormProps} for the {@link Form} component.
|
||||
*/
|
||||
export const Form = (props: FormProps) => {
|
||||
const { fields, isSavingEnabled } = props;
|
||||
|
||||
const [unsavedChanges, setUnsavedChanges] = React.useState<
|
||||
Record<string, UnsavedFieldChange<SettingType>>
|
||||
>({});
|
||||
|
||||
const [isLoading, setIsLoading] = React.useState<boolean>(false);
|
||||
|
||||
const unsavedChangesCount = Object.keys(unsavedChanges).length;
|
||||
const hasInvalidChanges = Object.values(unsavedChanges).some(({ isInvalid }) => isInvalid);
|
||||
|
||||
const clearAllUnsaved = () => {
|
||||
setUnsavedChanges({});
|
||||
};
|
||||
|
||||
const saveChanges = useSave({ fields, clearChanges: clearAllUnsaved });
|
||||
|
||||
const saveAll = async () => {
|
||||
setIsLoading(true);
|
||||
await saveChanges(unsavedChanges);
|
||||
setIsLoading(false);
|
||||
};
|
||||
|
||||
const onChange: RowOnChangeFn<SettingType> = (id, change) => {
|
||||
if (!change) {
|
||||
const { [id]: unsavedChange, ...rest } = unsavedChanges;
|
||||
setUnsavedChanges(rest);
|
||||
return;
|
||||
}
|
||||
|
||||
setUnsavedChanges((changes) => ({ ...changes, [id]: change }));
|
||||
};
|
||||
|
||||
const fieldRows = fields.map((field) => {
|
||||
const { id: key } = field;
|
||||
const unsavedChange = unsavedChanges[key];
|
||||
return <FieldRow {...{ key, field, unsavedChange, onChange, isSavingEnabled }} />;
|
||||
});
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<div>{fieldRows}</div>
|
||||
{!isEmpty(unsavedChanges) && (
|
||||
<BottomBar
|
||||
onSaveAll={saveAll}
|
||||
onClearAllUnsaved={clearAllUnsaved}
|
||||
hasInvalidChanges={hasInvalidChanges}
|
||||
isLoading={isLoading}
|
||||
unsavedChangesCount={unsavedChangesCount}
|
||||
/>
|
||||
)}
|
||||
</Fragment>
|
||||
);
|
||||
};
|
12
packages/kbn-management/settings/components/form/index.ts
Normal file
12
packages/kbn-management/settings/components/form/index.ts
Normal file
|
@ -0,0 +1,12 @@
|
|||
/*
|
||||
* 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 { Form } from './form';
|
||||
|
||||
export type { FormKibanaDependencies, FormServices } from './types';
|
||||
export { FormProvider, FormKibanaProvider } from './services';
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"type": "shared-common",
|
||||
"id": "@kbn/management-settings-components-form",
|
||||
"owner": "@elastic/platform-deployment-management"
|
||||
}
|
|
@ -0,0 +1,54 @@
|
|||
/*
|
||||
* 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, { ReactChild } from 'react';
|
||||
import { I18nProvider } from '@kbn/i18n-react';
|
||||
|
||||
import { KibanaRootContextProvider } from '@kbn/react-kibana-context-root';
|
||||
import { themeServiceMock } from '@kbn/core-theme-browser-mocks';
|
||||
import { I18nStart } from '@kbn/core-i18n-browser';
|
||||
|
||||
import { createFieldRowServicesMock } from '@kbn/management-settings-components-field-row/mocks';
|
||||
import { FormProvider } from '../services';
|
||||
import type { FormServices } from '../types';
|
||||
|
||||
const createRootMock = () => {
|
||||
const i18n: I18nStart = {
|
||||
Context: ({ children }) => <I18nProvider>{children}</I18nProvider>,
|
||||
};
|
||||
const theme = themeServiceMock.createStartContract();
|
||||
return {
|
||||
i18n,
|
||||
theme,
|
||||
};
|
||||
};
|
||||
|
||||
export const createFormServicesMock = (): FormServices => ({
|
||||
...createFieldRowServicesMock(),
|
||||
saveChanges: jest.fn(),
|
||||
showError: jest.fn(),
|
||||
showReloadPagePrompt: jest.fn(),
|
||||
});
|
||||
|
||||
export const TestWrapper = ({
|
||||
children,
|
||||
services = createFormServicesMock(),
|
||||
}: {
|
||||
children: ReactChild;
|
||||
services?: FormServices;
|
||||
}) => {
|
||||
return (
|
||||
<KibanaRootContextProvider {...createRootMock()}>
|
||||
<FormProvider {...services}>{children}</FormProvider>
|
||||
</KibanaRootContextProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export const wrap = (component: JSX.Element, services: FormServices = createFormServicesMock()) => (
|
||||
<TestWrapper services={services} children={component} />
|
||||
);
|
|
@ -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 { TestWrapper, createFormServicesMock, wrap } from './context';
|
||||
export { getSettingsMock } from './settings';
|
||||
export { uiSettingsClientMock } from './settings_client';
|
|
@ -0,0 +1,114 @@
|
|||
/*
|
||||
* 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 { KnownTypeToMetadata, SettingType } from '@kbn/management-settings-types';
|
||||
|
||||
type Settings = {
|
||||
[key in SettingType]: KnownTypeToMetadata<key>;
|
||||
};
|
||||
|
||||
/**
|
||||
* A utility function returning a representative set of UiSettings.
|
||||
* @param requirePageReload The value of the `requirePageReload` param for all settings.
|
||||
*/
|
||||
export const getSettingsMock = (requirePageReload: boolean = false): Settings => {
|
||||
const defaults = {
|
||||
requiresPageReload: requirePageReload,
|
||||
readonly: false,
|
||||
category: ['category'],
|
||||
};
|
||||
|
||||
return {
|
||||
array: {
|
||||
description: 'Description for Array test setting',
|
||||
name: 'array:test:setting',
|
||||
type: 'array',
|
||||
userValue: null,
|
||||
value: ['example_value'],
|
||||
...defaults,
|
||||
},
|
||||
boolean: {
|
||||
description: 'Description for Boolean test setting',
|
||||
name: 'boolean:test:setting',
|
||||
type: 'boolean',
|
||||
userValue: null,
|
||||
value: true,
|
||||
...defaults,
|
||||
},
|
||||
color: {
|
||||
description: 'Description for Color test setting',
|
||||
name: 'color:test:setting',
|
||||
type: 'color',
|
||||
userValue: null,
|
||||
value: '#FF00CC',
|
||||
...defaults,
|
||||
},
|
||||
image: {
|
||||
description: 'Description for Image test setting',
|
||||
name: 'image:test:setting',
|
||||
type: 'image',
|
||||
userValue: null,
|
||||
value: '',
|
||||
...defaults,
|
||||
},
|
||||
number: {
|
||||
description: 'Description for Number test setting',
|
||||
name: 'number:test:setting',
|
||||
type: 'number',
|
||||
userValue: null,
|
||||
value: 1,
|
||||
...defaults,
|
||||
},
|
||||
json: {
|
||||
name: 'json:test:setting',
|
||||
description: 'Description for Json test setting',
|
||||
type: 'json',
|
||||
userValue: null,
|
||||
value: '{"foo": "bar"}',
|
||||
...defaults,
|
||||
},
|
||||
markdown: {
|
||||
name: 'markdown:test:setting',
|
||||
description: 'Description for Markdown test setting',
|
||||
type: 'markdown',
|
||||
userValue: null,
|
||||
value: '',
|
||||
...defaults,
|
||||
},
|
||||
select: {
|
||||
description: 'Description for Select test setting',
|
||||
name: 'select:test:setting',
|
||||
options: ['apple', 'orange', 'banana'],
|
||||
optionLabels: {
|
||||
apple: 'Apple',
|
||||
orange: 'Orange',
|
||||
banana: 'Banana',
|
||||
},
|
||||
type: 'select',
|
||||
userValue: null,
|
||||
value: 'apple',
|
||||
...defaults,
|
||||
},
|
||||
string: {
|
||||
description: 'Description for String test setting',
|
||||
name: 'string:test:setting',
|
||||
type: 'string',
|
||||
userValue: null,
|
||||
value: 'hello world',
|
||||
...defaults,
|
||||
},
|
||||
undefined: {
|
||||
description: 'Description for Undefined test setting',
|
||||
name: 'undefined:test:setting',
|
||||
type: 'undefined',
|
||||
userValue: null,
|
||||
value: undefined,
|
||||
...defaults,
|
||||
},
|
||||
};
|
||||
};
|
|
@ -0,0 +1,17 @@
|
|||
/*
|
||||
* 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 { IUiSettingsClient } from '@kbn/core-ui-settings-browser';
|
||||
|
||||
/**
|
||||
* Mock of the portion of the {@link IUiSettingsClient} used as a parameter in the {@link getFieldDefinitions} function.
|
||||
*/
|
||||
export const uiSettingsClientMock: Pick<IUiSettingsClient, 'isCustom' | 'isOverridden'> = {
|
||||
isCustom: () => false,
|
||||
isOverridden: () => false,
|
||||
};
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"name": "@kbn/management-settings-components-form",
|
||||
"private": true,
|
||||
"version": "1.0.0",
|
||||
"license": "SSPL-1.0 OR Elastic License 2.0"
|
||||
}
|
|
@ -0,0 +1,49 @@
|
|||
/*
|
||||
* 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';
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { toMountPoint } from '@kbn/react-kibana-mount';
|
||||
import { EuiButton, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
||||
import { ToastInput } from '@kbn/core-notifications-browser';
|
||||
import { I18nStart } from '@kbn/core-i18n-browser';
|
||||
import { ThemeServiceStart } from '@kbn/core-theme-browser';
|
||||
|
||||
export const DATA_TEST_SUBJ_PAGE_RELOAD_BUTTON = 'pageReloadButton';
|
||||
|
||||
/**
|
||||
* Utility function for returning a {@link ToastInput} for displaying a prompt for reloading the page.
|
||||
* @param theme The {@link ThemeServiceStart} contract.
|
||||
* @param i18nStart The {@link I18nStart} contract.
|
||||
* @returns A toast.
|
||||
*/
|
||||
export const reloadPageToast = (theme: ThemeServiceStart, i18nStart: I18nStart): ToastInput => {
|
||||
return {
|
||||
title: i18n.translate('management.settings.form.requiresPageReloadToastDescription', {
|
||||
defaultMessage: 'One or more settings require you to reload the page to take effect.',
|
||||
}),
|
||||
text: toMountPoint(
|
||||
<EuiFlexGroup justifyContent="flexEnd" gutterSize="s">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton
|
||||
size="s"
|
||||
onClick={() => window.location.reload()}
|
||||
data-test-subj={DATA_TEST_SUBJ_PAGE_RELOAD_BUTTON}
|
||||
>
|
||||
{i18n.translate('management.settings.form.requiresPageReloadToastButtonLabel', {
|
||||
defaultMessage: 'Reload page',
|
||||
})}
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>,
|
||||
{ i18n: i18nStart, theme }
|
||||
),
|
||||
color: 'success',
|
||||
};
|
||||
};
|
|
@ -0,0 +1,78 @@
|
|||
/*
|
||||
* 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 {
|
||||
FieldRowProvider,
|
||||
FieldRowKibanaProvider,
|
||||
} from '@kbn/management-settings-components-field-row';
|
||||
import React, { FC, useContext } from 'react';
|
||||
import { SettingType, UnsavedFieldChange } from '@kbn/management-settings-types';
|
||||
|
||||
import type { FormServices, FormKibanaDependencies, Services } from './types';
|
||||
import { reloadPageToast } from './reload_page_toast';
|
||||
|
||||
const FormContext = React.createContext<Services | null>(null);
|
||||
|
||||
/**
|
||||
* Props for {@link FormProvider}.
|
||||
*/
|
||||
export interface FormProviderProps extends FormServices {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* React Provider that provides services to a {@link Form} component and its dependents.
|
||||
*/
|
||||
export const FormProvider = ({ children, ...services }: FormProviderProps) => {
|
||||
const { saveChanges, showError, showReloadPagePrompt, ...rest } = services;
|
||||
|
||||
return (
|
||||
<FormContext.Provider value={{ saveChanges, showError, showReloadPagePrompt }}>
|
||||
<FieldRowProvider {...rest}>{children}</FieldRowProvider>
|
||||
</FormContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Kibana-specific Provider that maps Kibana plugins and services to a {@link FormProvider}.
|
||||
*/
|
||||
export const FormKibanaProvider: FC<FormKibanaDependencies> = ({ children, ...deps }) => {
|
||||
const { settings, toasts, docLinks, theme, i18nStart } = deps;
|
||||
|
||||
return (
|
||||
<FormContext.Provider
|
||||
value={{
|
||||
saveChanges: (changes: Record<string, UnsavedFieldChange<SettingType>>) => {
|
||||
const arr = Object.entries(changes).map(([key, value]) =>
|
||||
settings.client.set(key, value.unsavedValue)
|
||||
);
|
||||
return Promise.all(arr);
|
||||
},
|
||||
showError: (message: string) => toasts.addDanger(message),
|
||||
showReloadPagePrompt: () => toasts.add(reloadPageToast(theme, i18nStart)),
|
||||
}}
|
||||
>
|
||||
<FieldRowKibanaProvider {...{ docLinks, toasts }}>{children}</FieldRowKibanaProvider>
|
||||
</FormContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* React hook for accessing pre-wired services.
|
||||
*/
|
||||
export const useServices = () => {
|
||||
const context = useContext(FormContext);
|
||||
|
||||
if (!context) {
|
||||
throw new Error(
|
||||
'FormContext is missing. Ensure your component or React root is wrapped with FormProvider.'
|
||||
);
|
||||
}
|
||||
|
||||
return context;
|
||||
};
|
|
@ -0,0 +1,67 @@
|
|||
/*
|
||||
* 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';
|
||||
import { EuiPanel } from '@elastic/eui';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
import { ComponentMeta } from '@storybook/react';
|
||||
import { FieldDefinition, SettingType } from '@kbn/management-settings-types';
|
||||
import { getFieldDefinitions } from '@kbn/management-settings-field-definition';
|
||||
import { getSettingsMock, uiSettingsClientMock } from '../mocks';
|
||||
import { Form as Component } from '../form';
|
||||
import { FormProvider } from '../services';
|
||||
|
||||
export default {
|
||||
title: `Settings/Form/Form`,
|
||||
description: 'A form with field rows',
|
||||
argTypes: {
|
||||
isSavingEnabled: {
|
||||
name: 'Saving is enabled?',
|
||||
control: { type: 'boolean' },
|
||||
},
|
||||
requirePageReload: {
|
||||
name: 'Settings require page reload?',
|
||||
control: { type: 'boolean' },
|
||||
},
|
||||
},
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<FormProvider
|
||||
showDanger={action('showDanger')}
|
||||
links={{ deprecationKey: 'link/to/deprecation/docs' }}
|
||||
saveChanges={action('saveChanges')}
|
||||
showError={action('showError')}
|
||||
showReloadPagePrompt={action('showReloadPagePrompt')}
|
||||
>
|
||||
<EuiPanel>
|
||||
<Story />
|
||||
</EuiPanel>
|
||||
</FormProvider>
|
||||
),
|
||||
],
|
||||
} as ComponentMeta<typeof Component>;
|
||||
|
||||
interface FormStoryProps {
|
||||
/** True if saving settings is enabled, false otherwise. */
|
||||
isSavingEnabled: boolean;
|
||||
/** True if settings require page reload, false otherwise. */
|
||||
requirePageReload: boolean;
|
||||
}
|
||||
|
||||
export const Form = ({ isSavingEnabled, requirePageReload }: FormStoryProps) => {
|
||||
const fields: Array<FieldDefinition<SettingType>> = getFieldDefinitions(
|
||||
getSettingsMock(requirePageReload),
|
||||
uiSettingsClientMock
|
||||
);
|
||||
|
||||
return <Component {...{ fields, isSavingEnabled }} />;
|
||||
};
|
||||
|
||||
Form.args = {
|
||||
isSavingEnabled: true,
|
||||
requirePageReload: false,
|
||||
};
|
|
@ -0,0 +1,34 @@
|
|||
{
|
||||
"extends": "../../../../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "target/types",
|
||||
"types": [
|
||||
"jest",
|
||||
"node",
|
||||
"react",
|
||||
"@testing-library/jest-dom",
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"**/*.ts",
|
||||
"**/*.tsx",
|
||||
],
|
||||
"exclude": [
|
||||
"target/**/*"
|
||||
],
|
||||
"kbn_references": [
|
||||
"@kbn/management-settings-types",
|
||||
"@kbn/management-settings-field-definition",
|
||||
"@kbn/i18n",
|
||||
"@kbn/i18n-react",
|
||||
"@kbn/management-settings-components-field-row",
|
||||
"@kbn/react-kibana-context-root",
|
||||
"@kbn/core-theme-browser-mocks",
|
||||
"@kbn/core-i18n-browser",
|
||||
"@kbn/react-kibana-mount",
|
||||
"@kbn/core-notifications-browser",
|
||||
"@kbn/core-theme-browser",
|
||||
"@kbn/core-ui-settings-browser",
|
||||
"@kbn/management-settings-components-field-input",
|
||||
]
|
||||
}
|
51
packages/kbn-management/settings/components/form/types.ts
Normal file
51
packages/kbn-management/settings/components/form/types.ts
Normal file
|
@ -0,0 +1,51 @@
|
|||
/*
|
||||
* 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 type {
|
||||
FieldRowKibanaDependencies,
|
||||
FieldRowServices,
|
||||
} from '@kbn/management-settings-components-field-row';
|
||||
import { SettingType, UnsavedFieldChange } from '@kbn/management-settings-types';
|
||||
import { SettingsStart } from '@kbn/core-ui-settings-browser';
|
||||
import { I18nStart } from '@kbn/core-i18n-browser';
|
||||
import { ThemeServiceStart } from '@kbn/core-theme-browser';
|
||||
import { ToastsStart } from '@kbn/core-notifications-browser';
|
||||
|
||||
/**
|
||||
* Contextual services used by a {@link Form} component.
|
||||
*/
|
||||
export interface Services {
|
||||
saveChanges: (changes: Record<string, UnsavedFieldChange<SettingType>>) => void;
|
||||
showError: (message: string) => void;
|
||||
showReloadPagePrompt: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Contextual services used by a {@link Form} component and its dependents.
|
||||
*/
|
||||
export type FormServices = FieldRowServices & Services;
|
||||
|
||||
/**
|
||||
* An interface containing a collection of Kibana plugins and services required to
|
||||
* render a {@link Form} component.
|
||||
*/
|
||||
interface KibanaDependencies {
|
||||
settings: {
|
||||
client: SettingsStart['client'];
|
||||
};
|
||||
theme: ThemeServiceStart;
|
||||
i18nStart: I18nStart;
|
||||
/** The portion of the {@link ToastsStart} contract used by this component. */
|
||||
toasts: Pick<ToastsStart, 'addError' | 'add'>;
|
||||
}
|
||||
|
||||
/**
|
||||
* An interface containing a collection of Kibana plugins and services required to
|
||||
* render a {@link Form} component and its dependents.
|
||||
*/
|
||||
export type FormKibanaDependencies = KibanaDependencies & FieldRowKibanaDependencies;
|
52
packages/kbn-management/settings/components/form/use_save.ts
Normal file
52
packages/kbn-management/settings/components/form/use_save.ts
Normal file
|
@ -0,0 +1,52 @@
|
|||
/*
|
||||
* 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 type { FieldDefinition, SettingType } from '@kbn/management-settings-types';
|
||||
import { isEmpty } from 'lodash';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { UnsavedFieldChange } from '@kbn/management-settings-types';
|
||||
import { useServices } from './services';
|
||||
|
||||
export interface UseSaveParameters {
|
||||
/** All {@link FieldDefinition} in the form. */
|
||||
fields: Array<FieldDefinition<SettingType>>;
|
||||
/** The function to invoke for clearing all unsaved changes. */
|
||||
clearChanges: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to provide a function that will save all given {@link UnsavedFieldChange}.
|
||||
*
|
||||
* @param params The {@link UseSaveParameters} to use.
|
||||
* @returns A function that will save all {@link UnsavedFieldChange} that are passed as an argument.
|
||||
*/
|
||||
export const useSave = (params: UseSaveParameters) => {
|
||||
const { saveChanges, showError, showReloadPagePrompt } = useServices();
|
||||
|
||||
return async (changes: Record<string, UnsavedFieldChange<SettingType>>) => {
|
||||
if (isEmpty(changes)) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await saveChanges(changes);
|
||||
params.clearChanges();
|
||||
const requiresReload = params.fields.some(
|
||||
(setting) => changes.hasOwnProperty(setting.id) && setting.requiresPageReload
|
||||
);
|
||||
if (requiresReload) {
|
||||
showReloadPagePrompt();
|
||||
}
|
||||
} catch (e) {
|
||||
showError(
|
||||
i18n.translate('management.settings.form.saveErrorMessage', {
|
||||
defaultMessage: 'Unable to save',
|
||||
})
|
||||
);
|
||||
}
|
||||
};
|
||||
};
|
|
@ -10,6 +10,8 @@ import { IUiSettingsClient } from '@kbn/core-ui-settings-browser';
|
|||
import { FieldDefinition, SettingType, UiSettingMetadata } from '@kbn/management-settings-types';
|
||||
import { getFieldDefinition } from './get_definition';
|
||||
|
||||
type SettingsClient = Pick<IUiSettingsClient, 'isCustom' | 'isOverridden'>;
|
||||
|
||||
/**
|
||||
* Convenience function to convert settings taken from a UiSettingsClient into
|
||||
* {@link FieldDefinition} objects.
|
||||
|
@ -20,7 +22,7 @@ import { getFieldDefinition } from './get_definition';
|
|||
*/
|
||||
export const getFieldDefinitions = (
|
||||
settings: Record<string, UiSettingMetadata<SettingType>>,
|
||||
client: IUiSettingsClient
|
||||
client: SettingsClient
|
||||
): Array<FieldDefinition<SettingType>> =>
|
||||
Object.entries(settings).map(([id, setting]) =>
|
||||
getFieldDefinition({
|
||||
|
|
|
@ -962,6 +962,8 @@
|
|||
"@kbn/management-settings-components-field-input/*": ["packages/kbn-management/settings/components/field_input/*"],
|
||||
"@kbn/management-settings-components-field-row": ["packages/kbn-management/settings/components/field_row"],
|
||||
"@kbn/management-settings-components-field-row/*": ["packages/kbn-management/settings/components/field_row/*"],
|
||||
"@kbn/management-settings-components-form": ["packages/kbn-management/settings/components/form"],
|
||||
"@kbn/management-settings-components-form/*": ["packages/kbn-management/settings/components/form/*"],
|
||||
"@kbn/management-settings-field-definition": ["packages/kbn-management/settings/field_definition"],
|
||||
"@kbn/management-settings-field-definition/*": ["packages/kbn-management/settings/field_definition/*"],
|
||||
"@kbn/management-settings-ids": ["packages/kbn-management/settings/setting_ids"],
|
||||
|
|
|
@ -4867,6 +4867,10 @@
|
|||
version "0.0.0"
|
||||
uid ""
|
||||
|
||||
"@kbn/management-settings-components-form@link:packages/kbn-management/settings/components/form":
|
||||
version "0.0.0"
|
||||
uid ""
|
||||
|
||||
"@kbn/management-settings-field-definition@link:packages/kbn-management/settings/field_definition":
|
||||
version "0.0.0"
|
||||
uid ""
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue