mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[feature] Implement Setting Categories in Advanced Settings in serverless (#167447)
## Summary This PR follows #166460 by adding Category panels to the Form. <img width="1807" alt="Screenshot 2023-09-27 at 3 36 16 PM" src="2abe8cf5
-5822-473f-affd-148fb7949316"> ## Notes This PR is divided into several commits, the first few being prerequisite codemods. I recommend reviewing each commit separately, as the codemods might obscure the actual component work. - [e78586f - Make SettingType pre-defined to clean up references](e78586fe44
) - This makes the `SettingType` optional, to clean up areas where the generic need not be specific. - [80a9988 - [codemod] Make onFieldChange and onInputChange more distinct](80a9988516
) - The `onChange` handlers weren't very clear as you work your way up the component tree. This makes the implementation and usage easier to understand, (and easier to [replace with state management](https://github.com/elastic/kibana/issues/166579)). - [5d0beff - [fix] Fix logged errors in form tests](5d0beff00c
) - This fixes some logged errors in the Form from `Monaco` and from some missing `act` and `waitFor` calls.
This commit is contained in:
parent
1a8c2f3958
commit
76b832e12e
75 changed files with 1061 additions and 367 deletions
1
.github/CODEOWNERS
vendored
1
.github/CODEOWNERS
vendored
|
@ -484,6 +484,7 @@ packages/kbn-managed-vscode-config @elastic/kibana-operations
|
|||
packages/kbn-managed-vscode-config-cli @elastic/kibana-operations
|
||||
packages/kbn-management/cards_navigation @elastic/platform-deployment-management
|
||||
src/plugins/management @elastic/platform-deployment-management
|
||||
packages/kbn-management/settings/components/field_category @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
|
||||
|
|
|
@ -506,6 +506,7 @@
|
|||
"@kbn/logstash-plugin": "link:x-pack/plugins/logstash",
|
||||
"@kbn/management-cards-navigation": "link:packages/kbn-management/cards_navigation",
|
||||
"@kbn/management-plugin": "link:src/plugins/management",
|
||||
"@kbn/management-settings-components-field-category": "link:packages/kbn-management/settings/components/field_category",
|
||||
"@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",
|
||||
|
|
|
@ -4,7 +4,7 @@ These packages comprise the Management Advanced Settings application. The sourc
|
|||
|
||||
## Notes
|
||||
|
||||
**Be aware**: the functional flow logic we've adopted for these components is not one I would encourage, specifically, using "drilled" onChange handlers and utilizing a composing-component-based store. Ideally, we'd use a Redux store, or, at the very least, a React reducer.
|
||||
**Be aware**: the functional flow logic we've adopted for these components is not one I would encourage, specifically, using "drilled" onFieldChange handlers and utilizing a composing-component-based store. Ideally, we'd use a Redux store, or, at the very least, a React reducer.
|
||||
|
||||
In the interest of time and compatibility, we've opted to use the pattern from the original components in `advancedSettings`. We plan to revisit the state management and prop-drilling when `advancedSettings` is refactored with these components.
|
||||
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
---
|
||||
id: management/settings/components/fieldCategory
|
||||
slug: /management/settings/components/field-category
|
||||
title: Management Settings Field Category Component
|
||||
description: A package containing components for rendering field rows in collections organized by their category.
|
||||
tags: ['management', 'settings']
|
||||
date: 2023-10-25
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
This package contains a component for rendering field rows of `UiSetting` objects in collections organized by their category. It's used primarily by the `Form` component.
|
|
@ -0,0 +1,56 @@
|
|||
/*
|
||||
* 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 type { ComponentMeta, Story } from '@storybook/react';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
import { FieldCategories as Component } from '../categories';
|
||||
import { Params, useCategoryStory } from './use_category_story';
|
||||
import { FieldCategoryProvider } from '../services';
|
||||
|
||||
export default {
|
||||
title: 'Settings/Field Category/Categories',
|
||||
description: '',
|
||||
args: {
|
||||
isFiltered: false,
|
||||
isSavingEnabled: true,
|
||||
},
|
||||
argTypes: {
|
||||
isFiltered: {
|
||||
control: {
|
||||
type: 'boolean',
|
||||
},
|
||||
},
|
||||
isSavingEnabled: {
|
||||
control: {
|
||||
type: 'boolean',
|
||||
},
|
||||
},
|
||||
},
|
||||
parameters: {
|
||||
backgrounds: {
|
||||
default: 'ghost',
|
||||
},
|
||||
},
|
||||
} as ComponentMeta<typeof Component>;
|
||||
|
||||
export const Categories: Story<Params> = (params) => {
|
||||
const { onClearQuery, isSavingEnabled, onFieldChange, unsavedChanges, categorizedFields } =
|
||||
useCategoryStory(params);
|
||||
|
||||
return (
|
||||
<FieldCategoryProvider
|
||||
showDanger={action('showDanger')}
|
||||
links={{ deprecationKey: 'link/to/deprecation/docs' }}
|
||||
>
|
||||
<Component
|
||||
{...{ categorizedFields, onFieldChange, unsavedChanges, onClearQuery, isSavingEnabled }}
|
||||
/>
|
||||
</FieldCategoryProvider>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,83 @@
|
|||
/*
|
||||
* 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 type { ComponentMeta } from '@storybook/react';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
|
||||
import { getSettingsMock } from '@kbn/management-settings-utilities/mocks/settings.mock';
|
||||
import { getFieldDefinitions } from '@kbn/management-settings-field-definition';
|
||||
import { categorizeFields } from '@kbn/management-settings-utilities';
|
||||
import { FieldRow } from '@kbn/management-settings-components-field-row';
|
||||
|
||||
import { FieldCategory as Component, type FieldCategoryProps as ComponentProps } from '../category';
|
||||
import { Params, useCategoryStory } from './use_category_story';
|
||||
import { FieldCategoryProvider } from '../services';
|
||||
|
||||
const settings = getSettingsMock();
|
||||
|
||||
// Markdown and JSON fields require Monaco, which are *notoriously* slow in Storybook due
|
||||
// to the lack of a webworker. Until we can resolve it, filter out those fields.
|
||||
const definitions = getFieldDefinitions(settings, {
|
||||
isCustom: () => {
|
||||
return false;
|
||||
},
|
||||
isOverridden: () => {
|
||||
return false;
|
||||
},
|
||||
}).filter((field) => field.type !== 'json' && field.type !== 'markdown');
|
||||
|
||||
const categories = Object.keys(categorizeFields(definitions));
|
||||
|
||||
export default {
|
||||
title: 'Settings/Field Category/Category',
|
||||
description: '',
|
||||
args: {
|
||||
category: categories[0],
|
||||
isFiltered: false,
|
||||
isSavingEnabled: true,
|
||||
},
|
||||
argTypes: {
|
||||
category: {
|
||||
control: {
|
||||
type: 'select',
|
||||
options: categories,
|
||||
},
|
||||
},
|
||||
},
|
||||
} as ComponentMeta<typeof Component>;
|
||||
|
||||
type FieldCategoryParams = Pick<ComponentProps, 'category'> & Params;
|
||||
|
||||
export const Category = ({ isFiltered, category, isSavingEnabled }: FieldCategoryParams) => {
|
||||
const { onClearQuery, onFieldChange, unsavedChanges } = useCategoryStory({
|
||||
isFiltered,
|
||||
isSavingEnabled,
|
||||
});
|
||||
|
||||
const { count, fields } = categorizeFields(definitions)[category];
|
||||
const rows = isFiltered ? [fields[0]] : fields;
|
||||
|
||||
return (
|
||||
<FieldCategoryProvider
|
||||
showDanger={action('showDanger')}
|
||||
links={{ deprecationKey: 'link/to/deprecation/docs' }}
|
||||
{...{ isSavingEnabled, onFieldChange }}
|
||||
>
|
||||
<Component category={category} fieldCount={count} onClearQuery={onClearQuery}>
|
||||
{rows.map((field) => (
|
||||
<FieldRow
|
||||
key={field.id}
|
||||
unsavedChange={unsavedChanges[field.id]}
|
||||
{...{ field, isSavingEnabled, onFieldChange }}
|
||||
/>
|
||||
))}
|
||||
</Component>
|
||||
</FieldCategoryProvider>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,63 @@
|
|||
/*
|
||||
* 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 { useArgs } from '@storybook/client-api';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
|
||||
import { getSettingsMock } from '@kbn/management-settings-utilities/mocks/settings.mock';
|
||||
import { getFieldDefinitions } from '@kbn/management-settings-field-definition';
|
||||
import { categorizeFields } from '@kbn/management-settings-utilities';
|
||||
import { UnsavedFieldChanges, OnFieldChangeFn } from '@kbn/management-settings-types';
|
||||
|
||||
export interface Params {
|
||||
isFiltered: boolean;
|
||||
isSavingEnabled: boolean;
|
||||
}
|
||||
|
||||
export const useCategoryStory = ({ isFiltered, isSavingEnabled }: Params) => {
|
||||
const [_args, updateArgs] = useArgs();
|
||||
const settings = getSettingsMock();
|
||||
|
||||
// Markdown and JSON fields require Monaco, which are *notoriously* slow in Storybook due
|
||||
// to the lack of a webworker. Until we can resolve it, filter out those fields.
|
||||
const definitions = getFieldDefinitions(settings, {
|
||||
isCustom: () => {
|
||||
return false;
|
||||
},
|
||||
isOverridden: () => {
|
||||
return false;
|
||||
},
|
||||
}).filter((field) => field.type !== 'json' && field.type !== 'markdown');
|
||||
|
||||
const categorizedFields = categorizeFields(definitions);
|
||||
|
||||
if (isFiltered) {
|
||||
Object.keys(categorizedFields).forEach((category) => {
|
||||
categorizedFields[category].fields = categorizedFields[category].fields.slice(0, 1);
|
||||
});
|
||||
}
|
||||
|
||||
const onClearQuery = () => updateArgs({ isFiltered: false });
|
||||
|
||||
const [unsavedChanges, setUnsavedChanges] = React.useState<UnsavedFieldChanges>({});
|
||||
|
||||
const onFieldChange: OnFieldChangeFn = (id, change) => {
|
||||
action('onFieldChange')(id, change);
|
||||
|
||||
if (!change) {
|
||||
const { [id]: unsavedChange, ...rest } = unsavedChanges;
|
||||
setUnsavedChanges(rest);
|
||||
return;
|
||||
}
|
||||
|
||||
setUnsavedChanges((changes) => ({ ...changes, [id]: change }));
|
||||
};
|
||||
|
||||
return { onClearQuery, onFieldChange, isSavingEnabled, unsavedChanges, categorizedFields };
|
||||
};
|
|
@ -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 from 'react';
|
||||
|
||||
import { CategorizedFields, UnsavedFieldChanges } from '@kbn/management-settings-types';
|
||||
|
||||
import { FieldRow, FieldRowProps } from '@kbn/management-settings-components-field-row';
|
||||
import { FieldCategory, type FieldCategoryProps } from './category';
|
||||
|
||||
/**
|
||||
* Props for the {@link FieldCategories} component.
|
||||
*/
|
||||
export interface FieldCategoriesProps
|
||||
extends Pick<FieldCategoryProps, 'onClearQuery'>,
|
||||
Pick<FieldRowProps, 'onFieldChange' | 'isSavingEnabled'> {
|
||||
/** Categorized fields for display. */
|
||||
categorizedFields: CategorizedFields;
|
||||
/** And unsaved changes currently managed by the parent component. */
|
||||
unsavedChanges?: UnsavedFieldChanges;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience component for displaying a set of {@link FieldCategory} components, given
|
||||
* a set of categorized fields.
|
||||
*
|
||||
* @param {FieldCategoriesProps} props props to pass to the {@link FieldCategories} component.
|
||||
*/
|
||||
export const FieldCategories = ({
|
||||
categorizedFields,
|
||||
unsavedChanges = {},
|
||||
onClearQuery,
|
||||
isSavingEnabled,
|
||||
onFieldChange,
|
||||
}: FieldCategoriesProps) => (
|
||||
<>
|
||||
{Object.entries(categorizedFields).map(([category, { count, fields }]) => (
|
||||
<FieldCategory key={category} fieldCount={count} {...{ category, onClearQuery }}>
|
||||
{fields.map((field) => (
|
||||
<FieldRow
|
||||
key={field.id}
|
||||
unsavedChange={unsavedChanges[field.id]}
|
||||
{...{ field, isSavingEnabled, onFieldChange }}
|
||||
/>
|
||||
))}
|
||||
</FieldCategory>
|
||||
))}
|
||||
</>
|
||||
);
|
|
@ -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, { ReactElement, Children } from 'react';
|
||||
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiSplitPanel, EuiTitle, useEuiTheme } from '@elastic/eui';
|
||||
|
||||
import { getCategoryName } from '@kbn/management-settings-utilities';
|
||||
import type { FieldRowProps } from '@kbn/management-settings-components-field-row';
|
||||
import { css } from '@emotion/react';
|
||||
import { ClearQueryLink, ClearQueryLinkProps } from './clear_query_link';
|
||||
|
||||
/**
|
||||
* Props for a {@link FieldCategory} component.
|
||||
*/
|
||||
export interface FieldCategoryProps
|
||||
extends Pick<ClearQueryLinkProps, 'onClearQuery' | 'fieldCount'> {
|
||||
/** The name of the category. */
|
||||
category: string;
|
||||
/** Children-- should be {@link FieldRow} components. */
|
||||
children:
|
||||
| ReactElement<FieldRowProps, 'FieldRow'>
|
||||
| Array<ReactElement<FieldRowProps, 'FieldRow'>>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Component for displaying a container of fields pertaining to a single
|
||||
* category.
|
||||
* @param props - the props to pass to the {@link FieldCategory} component.
|
||||
*/
|
||||
export const FieldCategory = (props: FieldCategoryProps) => {
|
||||
const { category, fieldCount, onClearQuery, children } = props;
|
||||
const {
|
||||
euiTheme: { size },
|
||||
} = useEuiTheme();
|
||||
|
||||
const displayCount = Children.count(children);
|
||||
|
||||
const panelCSS = css`
|
||||
& + & {
|
||||
margin-top: ${size.l};
|
||||
}
|
||||
`;
|
||||
|
||||
return (
|
||||
<EuiSplitPanel.Outer hasBorder key={category} css={panelCSS}>
|
||||
<EuiSplitPanel.Inner color="subdued">
|
||||
<EuiFlexGroup alignItems="baseline">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiTitle>
|
||||
<h2>{getCategoryName(category)}</h2>
|
||||
</EuiTitle>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<ClearQueryLink {...{ displayCount, fieldCount, onClearQuery }} />
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiSplitPanel.Inner>
|
||||
<EuiSplitPanel.Inner>{children}</EuiSplitPanel.Inner>
|
||||
</EuiSplitPanel.Outer>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,59 @@
|
|||
/*
|
||||
* 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 { css } from '@emotion/react';
|
||||
|
||||
import { EuiLink, EuiText } from '@elastic/eui';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
|
||||
/**
|
||||
* Props for the {@link ClearQueryLink} component.
|
||||
*/
|
||||
export interface ClearQueryLinkProps {
|
||||
/** The total number of fields in the category. */
|
||||
fieldCount: number;
|
||||
/** The number of fields currently being displayed. */
|
||||
displayCount: number;
|
||||
/** Handler to invoke when clearing the current filtering query. */
|
||||
onClearQuery: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Component for displaying a link to clear the current filtering query.
|
||||
*/
|
||||
export const ClearQueryLink = ({ fieldCount, displayCount, onClearQuery }: ClearQueryLinkProps) => {
|
||||
if (fieldCount === displayCount) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const linkCSS = css`
|
||||
font-style: italic;
|
||||
`;
|
||||
|
||||
return (
|
||||
<EuiText css={linkCSS} size="s">
|
||||
<FormattedMessage
|
||||
id="management.settings.form.searchResultText"
|
||||
defaultMessage="Search terms are hiding {settingsCount} settings {clearSearch}"
|
||||
values={{
|
||||
settingsCount: fieldCount - displayCount,
|
||||
clearSearch: (
|
||||
<EuiLink onClick={onClearQuery}>
|
||||
<EuiText css={linkCSS} size="s">
|
||||
<FormattedMessage
|
||||
id="management.settings.form.clearSearchResultText"
|
||||
defaultMessage="(clear search)"
|
||||
/>
|
||||
</EuiText>
|
||||
</EuiLink>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</EuiText>
|
||||
);
|
||||
};
|
|
@ -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.
|
||||
*/
|
||||
|
||||
export { FieldCategories, type FieldCategoriesProps } from './categories';
|
||||
export { FieldCategory, type FieldCategoryProps } from './category';
|
||||
export type { ClearQueryLinkProps } from './clear_query_link';
|
||||
export type { FieldCategoryKibanaDependencies, FieldCategoryServices } from './types';
|
||||
export {
|
||||
FieldCategoryKibanaProvider,
|
||||
FieldCategoryProvider,
|
||||
type FieldCategoryProviderProps,
|
||||
} from './services';
|
|
@ -0,0 +1,13 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
preset: '@kbn/test',
|
||||
rootDir: '../../../../..',
|
||||
roots: ['<rootDir>/packages/kbn-management/settings/components/field_category'],
|
||||
};
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"type": "shared-common",
|
||||
"id": "@kbn/management-settings-components-field-category",
|
||||
"owner": "@elastic/platform-deployment-management"
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"name": "@kbn/management-settings-components-field-category",
|
||||
"private": true,
|
||||
"version": "1.0.0",
|
||||
"license": "SSPL-1.0 OR Elastic License 2.0"
|
||||
}
|
|
@ -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 React from 'react';
|
||||
|
||||
import {
|
||||
FieldRowProvider,
|
||||
FieldRowKibanaProvider,
|
||||
} from '@kbn/management-settings-components-field-row';
|
||||
|
||||
import type { FieldCategoryServices } from './types';
|
||||
|
||||
/**
|
||||
* Props for {@link FieldCategoryProvider}.
|
||||
*/
|
||||
export interface FieldCategoryProviderProps extends FieldCategoryServices {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* React Provider that provides services to a {@link FieldCategory} component and its dependents.
|
||||
*/
|
||||
export const FieldCategoryProvider = FieldRowProvider;
|
||||
|
||||
/**
|
||||
* Kibana-specific Provider that maps Kibana plugins and services to a {@link FieldCategoryProvider}.
|
||||
*/
|
||||
export const FieldCategoryKibanaProvider = FieldRowKibanaProvider;
|
|
@ -0,0 +1,25 @@
|
|||
{
|
||||
"extends": "../../../../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "target/types",
|
||||
"types": [
|
||||
"jest",
|
||||
"node",
|
||||
"react"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"**/*.ts",
|
||||
"**/*.tsx",
|
||||
],
|
||||
"exclude": [
|
||||
"target/**/*"
|
||||
],
|
||||
"kbn_references": [
|
||||
"@kbn/management-settings-utilities",
|
||||
"@kbn/management-settings-field-definition",
|
||||
"@kbn/management-settings-components-field-row",
|
||||
"@kbn/management-settings-types",
|
||||
"@kbn/i18n-react",
|
||||
]
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
/*
|
||||
* 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 {
|
||||
FieldRowServices,
|
||||
FieldRowKibanaDependencies,
|
||||
} from '@kbn/management-settings-components-field-row';
|
||||
|
||||
/**
|
||||
* Contextual services used by a {@link FieldCategory} component and its dependents.
|
||||
*/
|
||||
export type FieldCategoryServices = FieldRowServices;
|
||||
|
||||
/**
|
||||
* An interface containing a collection of Kibana plugins and services required to
|
||||
* render a {@link FieldCategory} component and its dependents.
|
||||
*/
|
||||
export type FieldCategoryKibanaDependencies = FieldRowKibanaDependencies;
|
|
@ -13,7 +13,7 @@ import { action } from '@storybook/addon-actions';
|
|||
import { EuiPanel } from '@elastic/eui';
|
||||
import { UiSettingsType } from '@kbn/core-ui-settings-common';
|
||||
import {
|
||||
OnChangeFn,
|
||||
OnInputChangeFn,
|
||||
SettingType,
|
||||
UiSettingMetadata,
|
||||
UnsavedFieldChange,
|
||||
|
@ -108,17 +108,17 @@ export const getInputStory = (type: SettingType, params: Params = {}) => {
|
|||
setting,
|
||||
});
|
||||
|
||||
const onChange: OnChangeFn<typeof type> = (newChange) => {
|
||||
const onInputChange: OnInputChangeFn<typeof type> = (newChange) => {
|
||||
setUnsavedChange(newChange);
|
||||
|
||||
action('onChange')({
|
||||
action('onInputChange')({
|
||||
type,
|
||||
unsavedValue: newChange?.unsavedValue,
|
||||
savedValue: field.savedValue,
|
||||
});
|
||||
};
|
||||
|
||||
return <FieldInput {...{ field, unsavedChange, onChange, isSavingEnabled }} />;
|
||||
return <FieldInput {...{ field, unsavedChange, onInputChange, isSavingEnabled }} />;
|
||||
};
|
||||
|
||||
Story.argTypes = {
|
||||
|
|
|
@ -55,7 +55,7 @@ describe('FieldInput', () => {
|
|||
},
|
||||
options,
|
||||
} as FieldDefinition<typeof type>,
|
||||
onChange: jest.fn(),
|
||||
onInputChange: jest.fn(),
|
||||
isSavingEnabled: true,
|
||||
};
|
||||
|
||||
|
@ -132,7 +132,7 @@ describe('FieldInput', () => {
|
|||
const { getByTestId } = render(wrap(<FieldInput {...props} />));
|
||||
const input = getByTestId(`${TEST_SUBJ_PREFIX_FIELD}-${name}`);
|
||||
fireEvent.change(input, { target: { value: 'new value' } });
|
||||
expect(props.onChange).toHaveBeenCalledWith({ type: 'string', unsavedValue: 'new value' });
|
||||
expect(props.onInputChange).toHaveBeenCalledWith({ type: 'string', unsavedValue: 'new value' });
|
||||
});
|
||||
|
||||
it('disables the input when isDisabled prop is true', () => {
|
||||
|
@ -191,7 +191,7 @@ describe('FieldInput', () => {
|
|||
...defaultProps.field,
|
||||
type: 'foobar',
|
||||
},
|
||||
} as unknown as FieldInputProps<SettingType>;
|
||||
} as unknown as FieldInputProps;
|
||||
|
||||
expect(() => render(wrap(<FieldInput {...props} />))).toThrowError(
|
||||
'Unknown or incompatible field type: foobar'
|
||||
|
|
|
@ -10,7 +10,7 @@ import React, { useImperativeHandle, useRef } from 'react';
|
|||
|
||||
import type {
|
||||
FieldDefinition,
|
||||
OnChangeFn,
|
||||
OnInputChangeFn,
|
||||
ResetInputRef,
|
||||
SettingType,
|
||||
UnsavedFieldChange,
|
||||
|
@ -56,13 +56,13 @@ import {
|
|||
/**
|
||||
* The props that are passed to the {@link FieldInput} component.
|
||||
*/
|
||||
export interface FieldInputProps<T extends SettingType> {
|
||||
export interface FieldInputProps<T extends SettingType = SettingType> {
|
||||
/** The {@link FieldDefinition} for the component. */
|
||||
field: Pick<FieldDefinition<T>, 'type' | 'id' | 'name' | 'ariaAttributes'>;
|
||||
/** An {@link UnsavedFieldChange} for the component, if any. */
|
||||
unsavedChange?: UnsavedFieldChange<T>;
|
||||
/** The `onChange` handler for the input. */
|
||||
onChange: OnChangeFn<T>;
|
||||
/** The `onInputChange` handler for the input. */
|
||||
onInputChange: OnInputChangeFn<T>;
|
||||
/** True if the input can be saved, false otherwise. */
|
||||
isSavingEnabled: boolean;
|
||||
/** True if the value within the input is invalid, false otherwise. */
|
||||
|
@ -81,140 +81,136 @@ const getMismatchError = (type: SettingType, unsavedType?: SettingType) =>
|
|||
*
|
||||
* @param props The props for the {@link FieldInput} component.
|
||||
*/
|
||||
export const FieldInput = React.forwardRef<ResetInputRef, FieldInputProps<SettingType>>(
|
||||
(props, ref) => {
|
||||
const { field, unsavedChange, onChange, isSavingEnabled } = props;
|
||||
export const FieldInput = React.forwardRef<ResetInputRef, FieldInputProps>((props, ref) => {
|
||||
const { field, unsavedChange, onInputChange, isSavingEnabled } = props;
|
||||
|
||||
// Create a ref for those input fields that require an imperative handle.
|
||||
const inputRef = useRef<ResetInputRef>(null);
|
||||
// Create a ref for those input fields that require an imperative handle.
|
||||
const inputRef = useRef<ResetInputRef>(null);
|
||||
|
||||
// Create an imperative handle that passes the invocation to any internal input that
|
||||
// may require it.
|
||||
useImperativeHandle(ref, () => ({
|
||||
reset: () => {
|
||||
if (inputRef.current) {
|
||||
inputRef.current.reset();
|
||||
}
|
||||
},
|
||||
}));
|
||||
|
||||
const inputProps = { isSavingEnabled, onChange };
|
||||
|
||||
// These checks might seem excessive or redundant, but they are necessary to ensure that
|
||||
// the types are honored correctly using type guards. These checks get compiled down to
|
||||
// checks against the `type` property-- which we were doing in the previous code, albeit
|
||||
// in an unenforceable way.
|
||||
//
|
||||
// Based on the success of a check, we can render the `FieldInput` in a indempotent and
|
||||
// type-safe way.
|
||||
//
|
||||
if (isArrayFieldDefinition(field)) {
|
||||
// If the composing component mistakenly provides an incompatible `UnsavedFieldChange`,
|
||||
// we can throw an `Error`. We might consider switching to a `console.error` and not
|
||||
// rendering the input, but that might be less helpful.
|
||||
if (!isArrayFieldUnsavedChange(unsavedChange)) {
|
||||
throw getMismatchError(field.type, unsavedChange?.type);
|
||||
// Create an imperative handle that passes the invocation to any internal input that
|
||||
// may require it.
|
||||
useImperativeHandle(ref, () => ({
|
||||
reset: () => {
|
||||
if (inputRef.current) {
|
||||
inputRef.current.reset();
|
||||
}
|
||||
},
|
||||
}));
|
||||
|
||||
return <ArrayInput {...{ field, unsavedChange, ...inputProps }} />;
|
||||
const inputProps = { isSavingEnabled, onInputChange };
|
||||
|
||||
// These checks might seem excessive or redundant, but they are necessary to ensure that
|
||||
// the types are honored correctly using type guards. These checks get compiled down to
|
||||
// checks against the `type` property-- which we were doing in the previous code, albeit
|
||||
// in an unenforceable way.
|
||||
//
|
||||
// Based on the success of a check, we can render the `FieldInput` in a indempotent and
|
||||
// type-safe way.
|
||||
//
|
||||
if (isArrayFieldDefinition(field)) {
|
||||
// If the composing component mistakenly provides an incompatible `UnsavedFieldChange`,
|
||||
// we can throw an `Error`. We might consider switching to a `console.error` and not
|
||||
// rendering the input, but that might be less helpful.
|
||||
if (!isArrayFieldUnsavedChange(unsavedChange)) {
|
||||
throw getMismatchError(field.type, unsavedChange?.type);
|
||||
}
|
||||
|
||||
if (isBooleanFieldDefinition(field)) {
|
||||
if (!isBooleanFieldUnsavedChange(unsavedChange)) {
|
||||
throw getMismatchError(field.type, unsavedChange?.type);
|
||||
}
|
||||
|
||||
return <BooleanInput {...{ field, unsavedChange, ...inputProps }} />;
|
||||
}
|
||||
|
||||
if (isColorFieldDefinition(field)) {
|
||||
if (!isColorFieldUnsavedChange(unsavedChange)) {
|
||||
throw getMismatchError(field.type, unsavedChange?.type);
|
||||
}
|
||||
|
||||
return <ColorPickerInput {...{ field, unsavedChange, ...inputProps }} />;
|
||||
}
|
||||
|
||||
if (isImageFieldDefinition(field)) {
|
||||
if (!isImageFieldUnsavedChange(unsavedChange)) {
|
||||
throw getMismatchError(field.type, unsavedChange?.type);
|
||||
}
|
||||
|
||||
return <ImageInput {...{ field, unsavedChange, ...inputProps }} ref={inputRef} />;
|
||||
}
|
||||
|
||||
if (isJsonFieldDefinition(field)) {
|
||||
if (!isJsonFieldUnsavedChange(unsavedChange)) {
|
||||
throw getMismatchError(field.type, unsavedChange?.type);
|
||||
}
|
||||
|
||||
return (
|
||||
<CodeEditorInput
|
||||
{...{ field, unsavedChange, ...inputProps }}
|
||||
type="json"
|
||||
defaultValue={field.savedValue || ''}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (isMarkdownFieldDefinition(field)) {
|
||||
if (!isMarkdownFieldUnsavedChange(unsavedChange)) {
|
||||
throw getMismatchError(field.type, unsavedChange?.type);
|
||||
}
|
||||
|
||||
return (
|
||||
<CodeEditorInput
|
||||
{...{ field, unsavedChange, ...inputProps }}
|
||||
type="markdown"
|
||||
defaultValue={field.savedValue || ''}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (isNumberFieldDefinition(field)) {
|
||||
if (!isNumberFieldUnsavedChange(unsavedChange)) {
|
||||
throw getMismatchError(field.type, unsavedChange?.type);
|
||||
}
|
||||
|
||||
return <NumberInput {...{ field, unsavedChange, ...inputProps }} />;
|
||||
}
|
||||
|
||||
if (isSelectFieldDefinition(field)) {
|
||||
if (!isSelectFieldUnsavedChange(unsavedChange)) {
|
||||
throw getMismatchError(field.type, unsavedChange?.type);
|
||||
}
|
||||
|
||||
const {
|
||||
options: { values: optionValues, labels: optionLabels },
|
||||
} = field;
|
||||
|
||||
return (
|
||||
<SelectInput {...{ field, unsavedChange, optionLabels, optionValues, ...inputProps }} />
|
||||
);
|
||||
}
|
||||
|
||||
if (isStringFieldDefinition(field)) {
|
||||
if (!isStringFieldUnsavedChange(unsavedChange)) {
|
||||
throw getMismatchError(field.type, unsavedChange?.type);
|
||||
}
|
||||
|
||||
return <TextInput {...{ field, unsavedChange, ...inputProps }} />;
|
||||
}
|
||||
|
||||
if (isUndefinedFieldDefinition(field)) {
|
||||
if (!isUndefinedFieldUnsavedChange(unsavedChange)) {
|
||||
throw getMismatchError(field.type, unsavedChange?.type);
|
||||
}
|
||||
|
||||
return (
|
||||
<TextInput
|
||||
field={field as unknown as FieldDefinition<'string'>}
|
||||
unsavedChange={unsavedChange as unknown as UnsavedFieldChange<'string'>}
|
||||
{...inputProps}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
throw new Error(`Unknown or incompatible field type: ${field.type}`);
|
||||
return <ArrayInput {...{ field, unsavedChange, ...inputProps }} />;
|
||||
}
|
||||
);
|
||||
|
||||
if (isBooleanFieldDefinition(field)) {
|
||||
if (!isBooleanFieldUnsavedChange(unsavedChange)) {
|
||||
throw getMismatchError(field.type, unsavedChange?.type);
|
||||
}
|
||||
|
||||
return <BooleanInput {...{ field, unsavedChange, ...inputProps }} />;
|
||||
}
|
||||
|
||||
if (isColorFieldDefinition(field)) {
|
||||
if (!isColorFieldUnsavedChange(unsavedChange)) {
|
||||
throw getMismatchError(field.type, unsavedChange?.type);
|
||||
}
|
||||
|
||||
return <ColorPickerInput {...{ field, unsavedChange, ...inputProps }} />;
|
||||
}
|
||||
|
||||
if (isImageFieldDefinition(field)) {
|
||||
if (!isImageFieldUnsavedChange(unsavedChange)) {
|
||||
throw getMismatchError(field.type, unsavedChange?.type);
|
||||
}
|
||||
|
||||
return <ImageInput {...{ field, unsavedChange, ...inputProps }} ref={inputRef} />;
|
||||
}
|
||||
|
||||
if (isJsonFieldDefinition(field)) {
|
||||
if (!isJsonFieldUnsavedChange(unsavedChange)) {
|
||||
throw getMismatchError(field.type, unsavedChange?.type);
|
||||
}
|
||||
|
||||
return (
|
||||
<CodeEditorInput
|
||||
{...{ field, unsavedChange, ...inputProps }}
|
||||
type="json"
|
||||
defaultValue={field.savedValue || ''}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (isMarkdownFieldDefinition(field)) {
|
||||
if (!isMarkdownFieldUnsavedChange(unsavedChange)) {
|
||||
throw getMismatchError(field.type, unsavedChange?.type);
|
||||
}
|
||||
|
||||
return (
|
||||
<CodeEditorInput
|
||||
{...{ field, unsavedChange, ...inputProps }}
|
||||
type="markdown"
|
||||
defaultValue={field.savedValue || ''}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (isNumberFieldDefinition(field)) {
|
||||
if (!isNumberFieldUnsavedChange(unsavedChange)) {
|
||||
throw getMismatchError(field.type, unsavedChange?.type);
|
||||
}
|
||||
|
||||
return <NumberInput {...{ field, unsavedChange, ...inputProps }} />;
|
||||
}
|
||||
|
||||
if (isSelectFieldDefinition(field)) {
|
||||
if (!isSelectFieldUnsavedChange(unsavedChange)) {
|
||||
throw getMismatchError(field.type, unsavedChange?.type);
|
||||
}
|
||||
|
||||
const {
|
||||
options: { values: optionValues, labels: optionLabels },
|
||||
} = field;
|
||||
|
||||
return <SelectInput {...{ field, unsavedChange, optionLabels, optionValues, ...inputProps }} />;
|
||||
}
|
||||
|
||||
if (isStringFieldDefinition(field)) {
|
||||
if (!isStringFieldUnsavedChange(unsavedChange)) {
|
||||
throw getMismatchError(field.type, unsavedChange?.type);
|
||||
}
|
||||
|
||||
return <TextInput {...{ field, unsavedChange, ...inputProps }} />;
|
||||
}
|
||||
|
||||
if (isUndefinedFieldDefinition(field)) {
|
||||
if (!isUndefinedFieldUnsavedChange(unsavedChange)) {
|
||||
throw getMismatchError(field.type, unsavedChange?.type);
|
||||
}
|
||||
|
||||
return (
|
||||
<TextInput
|
||||
field={field as unknown as FieldDefinition<'string'>}
|
||||
unsavedChange={unsavedChange as unknown as UnsavedFieldChange<'string'>}
|
||||
{...inputProps}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
throw new Error(`Unknown or incompatible field type: ${field.type}`);
|
||||
});
|
||||
|
|
|
@ -18,9 +18,9 @@ const name = 'Some array field';
|
|||
const id = 'some:array:field';
|
||||
|
||||
describe('ArrayInput', () => {
|
||||
const onChange = jest.fn();
|
||||
const onInputChange = jest.fn();
|
||||
const defaultProps: InputProps<'array'> = {
|
||||
onChange,
|
||||
onInputChange,
|
||||
field: {
|
||||
name,
|
||||
type: 'array',
|
||||
|
@ -35,7 +35,7 @@ describe('ArrayInput', () => {
|
|||
};
|
||||
|
||||
beforeEach(() => {
|
||||
onChange.mockClear();
|
||||
onInputChange.mockClear();
|
||||
});
|
||||
|
||||
it('renders without errors', () => {
|
||||
|
@ -70,7 +70,7 @@ describe('ArrayInput', () => {
|
|||
expect(input).toHaveValue('foo, bar, baz');
|
||||
});
|
||||
|
||||
it('only calls onChange when blurred ', () => {
|
||||
it('only calls onInputChange when blurred ', () => {
|
||||
render(wrap(<ArrayInput {...defaultProps} />));
|
||||
const input = screen.getByTestId(`${TEST_SUBJ_PREFIX_FIELD}-${id}`);
|
||||
|
||||
|
@ -78,13 +78,13 @@ describe('ArrayInput', () => {
|
|||
userEvent.type(input, ',baz');
|
||||
|
||||
expect(input).toHaveValue('foo, bar,baz');
|
||||
expect(defaultProps.onChange).not.toHaveBeenCalled();
|
||||
expect(defaultProps.onInputChange).not.toHaveBeenCalled();
|
||||
|
||||
act(() => {
|
||||
input.blur();
|
||||
});
|
||||
|
||||
expect(defaultProps.onChange).toHaveBeenCalledWith({
|
||||
expect(defaultProps.onInputChange).toHaveBeenCalledWith({
|
||||
type: 'array',
|
||||
unsavedValue: ['foo', 'bar', 'baz'],
|
||||
});
|
||||
|
|
|
@ -29,7 +29,7 @@ export const ArrayInput = ({
|
|||
field,
|
||||
unsavedChange,
|
||||
isSavingEnabled,
|
||||
onChange: onChangeProp,
|
||||
onInputChange,
|
||||
}: ArrayInputProps) => {
|
||||
const [inputValue] = getFieldInputValue(field, unsavedChange) || [];
|
||||
const [value, setValue] = useState(inputValue?.join(', '));
|
||||
|
@ -39,7 +39,7 @@ export const ArrayInput = ({
|
|||
setValue(newValue);
|
||||
};
|
||||
|
||||
const onUpdate = useUpdate({ onChange: onChangeProp, field });
|
||||
const onUpdate = useUpdate({ onInputChange, field });
|
||||
|
||||
useEffect(() => {
|
||||
setValue(inputValue?.join(', '));
|
||||
|
|
|
@ -19,9 +19,9 @@ const name = 'Some boolean field';
|
|||
const id = 'some:boolean:field';
|
||||
|
||||
describe('BooleanInput', () => {
|
||||
const onChange = jest.fn();
|
||||
const onInputChange = jest.fn();
|
||||
const defaultProps: InputProps<'boolean'> = {
|
||||
onChange,
|
||||
onInputChange,
|
||||
field: {
|
||||
name,
|
||||
type: 'boolean',
|
||||
|
@ -36,7 +36,7 @@ describe('BooleanInput', () => {
|
|||
};
|
||||
|
||||
beforeEach(() => {
|
||||
onChange.mockClear();
|
||||
onInputChange.mockClear();
|
||||
});
|
||||
|
||||
it('renders false', () => {
|
||||
|
@ -60,16 +60,16 @@ describe('BooleanInput', () => {
|
|||
expect(screen.getByTestId(`${TEST_SUBJ_PREFIX_FIELD}-${id}`)).toBeChecked();
|
||||
});
|
||||
|
||||
it('calls onChange when toggled', () => {
|
||||
it('calls onInputChange when toggled', () => {
|
||||
render(wrap(<BooleanInput {...defaultProps} />));
|
||||
const input = screen.getByTestId(`${TEST_SUBJ_PREFIX_FIELD}-${id}`);
|
||||
expect(defaultProps.onChange).not.toHaveBeenCalled();
|
||||
expect(defaultProps.onInputChange).not.toHaveBeenCalled();
|
||||
|
||||
act(() => {
|
||||
fireEvent.click(input);
|
||||
});
|
||||
|
||||
expect(defaultProps.onChange).toBeCalledWith({ type: 'boolean', unsavedValue: true });
|
||||
expect(defaultProps.onInputChange).toBeCalledWith({ type: 'boolean', unsavedValue: true });
|
||||
|
||||
act(() => {
|
||||
fireEvent.click(input);
|
||||
|
|
|
@ -28,15 +28,15 @@ export const BooleanInput = ({
|
|||
field,
|
||||
unsavedChange,
|
||||
isSavingEnabled,
|
||||
onChange: onChangeProp,
|
||||
onInputChange,
|
||||
}: BooleanInputProps) => {
|
||||
const onUpdate = useUpdate({ onInputChange, field });
|
||||
|
||||
const onChange: EuiSwitchProps['onChange'] = (event) => {
|
||||
const inputValue = event.target.checked;
|
||||
onUpdate({ type: field.type, unsavedValue: inputValue });
|
||||
};
|
||||
|
||||
const onUpdate = useUpdate({ onChange: onChangeProp, field });
|
||||
|
||||
const { id, name, ariaAttributes } = field;
|
||||
const { ariaLabel, ariaDescribedBy } = ariaAttributes;
|
||||
const [value] = getFieldInputValue(field, unsavedChange);
|
||||
|
|
|
@ -43,9 +43,9 @@ export const CodeEditorInput = ({
|
|||
type,
|
||||
isSavingEnabled,
|
||||
defaultValue,
|
||||
onChange: onChangeProp,
|
||||
onInputChange,
|
||||
}: CodeEditorInputProps) => {
|
||||
const onUpdate = useUpdate({ onChange: onChangeProp, field });
|
||||
const onUpdate = useUpdate({ onInputChange, field });
|
||||
|
||||
const onChange: CodeEditorProps['onChange'] = (inputValue) => {
|
||||
let newUnsavedValue;
|
||||
|
|
|
@ -15,9 +15,9 @@ const name = 'Some color field';
|
|||
const id = 'some:color:field';
|
||||
|
||||
describe('ColorPickerInput', () => {
|
||||
const onChange = jest.fn();
|
||||
const onInputChange = jest.fn();
|
||||
const defaultProps: ColorPickerInputProps = {
|
||||
onChange,
|
||||
onInputChange,
|
||||
field: {
|
||||
name,
|
||||
type: 'color',
|
||||
|
@ -32,7 +32,7 @@ describe('ColorPickerInput', () => {
|
|||
};
|
||||
|
||||
beforeEach(() => {
|
||||
onChange.mockClear();
|
||||
onInputChange.mockClear();
|
||||
});
|
||||
|
||||
it('renders without errors', () => {
|
||||
|
@ -42,20 +42,23 @@ describe('ColorPickerInput', () => {
|
|||
expect(input).toHaveValue('#000000');
|
||||
});
|
||||
|
||||
it('calls the onChange prop when the value changes', () => {
|
||||
it('calls the onInputChange prop when the value changes', () => {
|
||||
const { getByRole } = render(wrap(<ColorPickerInput {...defaultProps} />));
|
||||
const input = getByRole('textbox');
|
||||
const newValue = '#ffffff';
|
||||
fireEvent.change(input, { target: { value: newValue } });
|
||||
expect(defaultProps.onChange).toHaveBeenCalledWith({ type: 'color', unsavedValue: newValue });
|
||||
expect(defaultProps.onInputChange).toHaveBeenCalledWith({
|
||||
type: 'color',
|
||||
unsavedValue: newValue,
|
||||
});
|
||||
});
|
||||
|
||||
it('calls the onChange prop with an error when the value is malformed', () => {
|
||||
it('calls the onInputChange prop with an error when the value is malformed', () => {
|
||||
const { getByRole } = render(wrap(<ColorPickerInput {...defaultProps} />));
|
||||
const input = getByRole('textbox');
|
||||
const newValue = '#1234';
|
||||
fireEvent.change(input, { target: { value: newValue } });
|
||||
expect(defaultProps.onChange).toHaveBeenCalledWith({
|
||||
expect(defaultProps.onInputChange).toHaveBeenCalledWith({
|
||||
type: 'color',
|
||||
unsavedValue: newValue,
|
||||
isInvalid: true,
|
||||
|
|
|
@ -32,9 +32,9 @@ export const ColorPickerInput = ({
|
|||
field,
|
||||
unsavedChange,
|
||||
isSavingEnabled,
|
||||
onChange: onChangeProp,
|
||||
onInputChange,
|
||||
}: ColorPickerInputProps) => {
|
||||
const onUpdate = useUpdate({ onChange: onChangeProp, field });
|
||||
const onUpdate = useUpdate({ onInputChange, field });
|
||||
|
||||
const onChange: EuiColorPickerProps['onChange'] = (newColor, { isValid }) => {
|
||||
const update: UnsavedFieldChange<'color'> = { type: field.type, unsavedValue: newColor };
|
||||
|
|
|
@ -18,9 +18,9 @@ const name = 'Some image field';
|
|||
const id = 'some:image:field';
|
||||
|
||||
describe('ImageInput', () => {
|
||||
const onChange = jest.fn();
|
||||
const onInputChange = jest.fn();
|
||||
const defaultProps: ImageInputProps = {
|
||||
onChange,
|
||||
onInputChange,
|
||||
field: {
|
||||
name,
|
||||
type: 'image',
|
||||
|
@ -35,7 +35,7 @@ describe('ImageInput', () => {
|
|||
};
|
||||
|
||||
beforeEach(() => {
|
||||
onChange.mockClear();
|
||||
onInputChange.mockClear();
|
||||
});
|
||||
|
||||
it('renders without errors', () => {
|
||||
|
@ -43,7 +43,7 @@ describe('ImageInput', () => {
|
|||
expect(container).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('calls the onChange prop when a file is selected', async () => {
|
||||
it('calls the onInputChange prop when a file is selected', async () => {
|
||||
const { getByTestId } = render(wrap(<ImageInput {...defaultProps} />));
|
||||
const input = getByTestId(`${TEST_SUBJ_PREFIX_FIELD}-${id}`) as HTMLInputElement;
|
||||
const file = new File(['(⌐□_□)'], 'test.png', { type: 'image/png' });
|
||||
|
@ -55,7 +55,7 @@ describe('ImageInput', () => {
|
|||
expect(input.files?.length).toBe(1);
|
||||
|
||||
// This doesn't work for some reason.
|
||||
// expect(defaultProps.onChange).toHaveBeenCalledWith({ value: file });
|
||||
// expect(defaultProps.onInputChange).toHaveBeenCalledWith({ value: file });
|
||||
});
|
||||
|
||||
it('disables the input when isDisabled prop is true', () => {
|
||||
|
|
|
@ -44,7 +44,7 @@ const errorMessage = i18n.translate('management.settings.field.imageChangeErrorM
|
|||
* Component for manipulating an `image` field.
|
||||
*/
|
||||
export const ImageInput = React.forwardRef<ResetInputRef, ImageInputProps>(
|
||||
({ field, unsavedChange, isSavingEnabled, onChange: onChangeProp }, ref) => {
|
||||
({ field, unsavedChange, isSavingEnabled, onInputChange }, ref) => {
|
||||
const inputRef = useRef<EuiFilePicker>(null);
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
|
@ -53,7 +53,7 @@ export const ImageInput = React.forwardRef<ResetInputRef, ImageInputProps>(
|
|||
|
||||
const { showDanger } = useServices();
|
||||
|
||||
const onUpdate = useUpdate({ onChange: onChangeProp, field });
|
||||
const onUpdate = useUpdate({ onInputChange, field });
|
||||
|
||||
const onChange: EuiFilePickerProps['onChange'] = async (files: FileList | null) => {
|
||||
if (files === null || !files.length) {
|
||||
|
|
|
@ -33,9 +33,9 @@ jest.mock('../code_editor', () => ({
|
|||
}));
|
||||
|
||||
describe('JsonEditorInput', () => {
|
||||
const onChange = jest.fn();
|
||||
const onInputChange = jest.fn();
|
||||
const defaultProps: CodeEditorInputProps = {
|
||||
onChange,
|
||||
onInputChange,
|
||||
type: 'json',
|
||||
field: {
|
||||
name,
|
||||
|
@ -51,7 +51,7 @@ describe('JsonEditorInput', () => {
|
|||
};
|
||||
|
||||
beforeEach(() => {
|
||||
onChange.mockClear();
|
||||
onInputChange.mockClear();
|
||||
});
|
||||
|
||||
it('renders without errors', () => {
|
||||
|
@ -65,28 +65,28 @@ describe('JsonEditorInput', () => {
|
|||
expect(input).toHaveValue(initialValue);
|
||||
});
|
||||
|
||||
it('calls the onChange prop when the object value changes', () => {
|
||||
it('calls the onInputChange prop when the object value changes', () => {
|
||||
const { getByTestId } = render(<CodeEditorInput {...defaultProps} />);
|
||||
const input = getByTestId(`${TEST_SUBJ_PREFIX_FIELD}-${id}`);
|
||||
fireEvent.change(input, { target: { value: '{"bar":"foo"}' } });
|
||||
expect(defaultProps.onChange).toHaveBeenCalledWith({
|
||||
expect(defaultProps.onInputChange).toHaveBeenCalledWith({
|
||||
type: 'json',
|
||||
unsavedValue: '{"bar":"foo"}',
|
||||
});
|
||||
});
|
||||
|
||||
it('calls the onChange prop when the object value changes with no value', () => {
|
||||
it('calls the onInputChange prop when the object value changes with no value', () => {
|
||||
const { getByTestId } = render(<CodeEditorInput {...defaultProps} />);
|
||||
const input = getByTestId(`${TEST_SUBJ_PREFIX_FIELD}-${id}`);
|
||||
fireEvent.change(input, { target: { value: '' } });
|
||||
expect(defaultProps.onChange).toHaveBeenCalledWith({ type: 'json', unsavedValue: '' });
|
||||
expect(defaultProps.onInputChange).toHaveBeenCalledWith({ type: 'json', unsavedValue: '' });
|
||||
});
|
||||
|
||||
it('calls the onChange prop with an error when the object value changes to invalid JSON', () => {
|
||||
it('calls the onInputChange prop with an error when the object value changes to invalid JSON', () => {
|
||||
const { getByTestId } = render(<CodeEditorInput {...defaultProps} />);
|
||||
const input = getByTestId(`${TEST_SUBJ_PREFIX_FIELD}-${id}`);
|
||||
fireEvent.change(input, { target: { value: '{"bar" "foo"}' } });
|
||||
expect(defaultProps.onChange).toHaveBeenCalledWith({
|
||||
expect(defaultProps.onInputChange).toHaveBeenCalledWith({
|
||||
type: 'json',
|
||||
unsavedValue: '{"bar" "foo"}',
|
||||
error: 'Invalid JSON syntax',
|
||||
|
@ -94,20 +94,20 @@ describe('JsonEditorInput', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('calls the onChange prop when the array value changes', () => {
|
||||
it('calls the onInputChange prop when the array value changes', () => {
|
||||
const props = { ...defaultProps, defaultValue: '["bar", "foo"]', value: undefined };
|
||||
const { getByTestId } = render(<CodeEditorInput {...props} />);
|
||||
const input = getByTestId(`${TEST_SUBJ_PREFIX_FIELD}-${id}`);
|
||||
fireEvent.change(input, { target: { value: '["foo", "bar", "baz"]' } });
|
||||
waitFor(() =>
|
||||
expect(defaultProps.onChange).toHaveBeenCalledWith({
|
||||
expect(defaultProps.onInputChange).toHaveBeenCalledWith({
|
||||
type: 'json',
|
||||
unsavedValue: '["foo", "bar", "baz"]',
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('calls the onChange prop when the array value changes with no value', () => {
|
||||
it('calls the onInputChange prop when the array value changes with no value', () => {
|
||||
const props = {
|
||||
...defaultProps,
|
||||
defaultValue: '["bar", "foo"]',
|
||||
|
@ -116,15 +116,15 @@ describe('JsonEditorInput', () => {
|
|||
const { getByTestId } = render(<CodeEditorInput {...props} />);
|
||||
const input = getByTestId(`${TEST_SUBJ_PREFIX_FIELD}-${id}`);
|
||||
fireEvent.change(input, { target: { value: '' } });
|
||||
expect(defaultProps.onChange).toHaveBeenCalledWith({ type: 'json', unsavedValue: '' });
|
||||
expect(defaultProps.onInputChange).toHaveBeenCalledWith({ type: 'json', unsavedValue: '' });
|
||||
});
|
||||
|
||||
it('calls the onChange prop with an array when the array value changes to invalid JSON', () => {
|
||||
it('calls the onInputChange prop with an array when the array value changes to invalid JSON', () => {
|
||||
const props = { ...defaultProps, defaultValue: '["bar", "foo"]', value: undefined };
|
||||
const { getByTestId } = render(<CodeEditorInput {...props} />);
|
||||
const input = getByTestId(`${TEST_SUBJ_PREFIX_FIELD}-${id}`);
|
||||
fireEvent.change(input, { target: { value: '["bar", "foo" | "baz"]' } });
|
||||
expect(defaultProps.onChange).toHaveBeenCalledWith({
|
||||
expect(defaultProps.onInputChange).toHaveBeenCalledWith({
|
||||
type: 'json',
|
||||
unsavedValue: '["bar", "foo" | "baz"]',
|
||||
error: 'Invalid JSON syntax',
|
||||
|
|
|
@ -33,9 +33,9 @@ jest.mock('../code_editor', () => ({
|
|||
}));
|
||||
|
||||
describe('MarkdownEditorInput', () => {
|
||||
const onChange = jest.fn();
|
||||
const onInputChange = jest.fn();
|
||||
const defaultProps: CodeEditorInputProps = {
|
||||
onChange,
|
||||
onInputChange,
|
||||
type: 'markdown',
|
||||
field: {
|
||||
name,
|
||||
|
@ -51,7 +51,7 @@ describe('MarkdownEditorInput', () => {
|
|||
};
|
||||
|
||||
beforeEach(() => {
|
||||
onChange.mockClear();
|
||||
onInputChange.mockClear();
|
||||
});
|
||||
|
||||
it('renders without errors', () => {
|
||||
|
@ -65,11 +65,11 @@ describe('MarkdownEditorInput', () => {
|
|||
expect(input).toHaveValue(initialValue);
|
||||
});
|
||||
|
||||
it('calls the onChange prop when the value changes', () => {
|
||||
it('calls the onInputChange prop when the value changes', () => {
|
||||
const { getByTestId } = render(<CodeEditorInput {...defaultProps} />);
|
||||
const input = getByTestId(`${TEST_SUBJ_PREFIX_FIELD}-${id}`);
|
||||
fireEvent.change(input, { target: { value: '# New Markdown Title' } });
|
||||
expect(defaultProps.onChange).toHaveBeenCalledWith({
|
||||
expect(defaultProps.onInputChange).toHaveBeenCalledWith({
|
||||
type: 'markdown',
|
||||
unsavedValue: '# New Markdown Title',
|
||||
});
|
||||
|
|
|
@ -16,9 +16,9 @@ const name = 'Some number field';
|
|||
const id = 'some:number:field';
|
||||
|
||||
describe('NumberInput', () => {
|
||||
const onChange = jest.fn();
|
||||
const onInputChange = jest.fn();
|
||||
const defaultProps: NumberInputProps = {
|
||||
onChange,
|
||||
onInputChange,
|
||||
field: {
|
||||
name,
|
||||
type: 'number',
|
||||
|
@ -33,7 +33,7 @@ describe('NumberInput', () => {
|
|||
};
|
||||
|
||||
beforeEach(() => {
|
||||
onChange.mockClear();
|
||||
onInputChange.mockClear();
|
||||
});
|
||||
|
||||
it('renders without errors', () => {
|
||||
|
@ -65,11 +65,14 @@ describe('NumberInput', () => {
|
|||
expect(input).toHaveValue(4321);
|
||||
});
|
||||
|
||||
it('calls the onChange prop when the value changes', () => {
|
||||
it('calls the onInputChange prop when the value changes', () => {
|
||||
const { getByTestId } = render(wrap(<NumberInput {...defaultProps} />));
|
||||
const input = getByTestId(`${TEST_SUBJ_PREFIX_FIELD}-${id}`);
|
||||
fireEvent.change(input, { target: { value: '54321' } });
|
||||
expect(defaultProps.onChange).toHaveBeenCalledWith({ type: 'number', unsavedValue: 54321 });
|
||||
expect(defaultProps.onInputChange).toHaveBeenCalledWith({
|
||||
type: 'number',
|
||||
unsavedValue: 54321,
|
||||
});
|
||||
});
|
||||
|
||||
it('disables the input when isDisabled prop is true', () => {
|
||||
|
|
|
@ -26,14 +26,14 @@ export const NumberInput = ({
|
|||
field,
|
||||
unsavedChange,
|
||||
isSavingEnabled,
|
||||
onChange: onChangeProp,
|
||||
onInputChange,
|
||||
}: NumberInputProps) => {
|
||||
const onChange: EuiFieldNumberProps['onChange'] = (event) => {
|
||||
const inputValue = Number(event.target.value);
|
||||
onUpdate({ type: field.type, unsavedValue: inputValue });
|
||||
};
|
||||
|
||||
const onUpdate = useUpdate({ onChange: onChangeProp, field });
|
||||
const onUpdate = useUpdate({ onInputChange, field });
|
||||
|
||||
const { id, name, ariaAttributes } = field;
|
||||
const { ariaLabel, ariaDescribedBy } = ariaAttributes;
|
||||
|
|
|
@ -16,9 +16,9 @@ const name = 'Some select field';
|
|||
const id = 'some:select:field';
|
||||
|
||||
describe('SelectInput', () => {
|
||||
const onChange = jest.fn();
|
||||
const onInputChange = jest.fn();
|
||||
const defaultProps: SelectInputProps = {
|
||||
onChange,
|
||||
onInputChange,
|
||||
field: {
|
||||
name,
|
||||
type: 'select',
|
||||
|
@ -39,7 +39,7 @@ describe('SelectInput', () => {
|
|||
};
|
||||
|
||||
beforeEach(() => {
|
||||
onChange.mockClear();
|
||||
onInputChange.mockClear();
|
||||
});
|
||||
|
||||
it('renders without errors', () => {
|
||||
|
@ -49,11 +49,14 @@ describe('SelectInput', () => {
|
|||
expect(input).toHaveValue('option2');
|
||||
});
|
||||
|
||||
it('calls the onChange prop when the value changes', () => {
|
||||
it('calls the onInputChange prop when the value changes', () => {
|
||||
const { getByTestId } = render(wrap(<SelectInput {...defaultProps} />));
|
||||
const input = getByTestId(`${TEST_SUBJ_PREFIX_FIELD}-${id}`);
|
||||
fireEvent.change(input, { target: { value: 'option3' } });
|
||||
expect(defaultProps.onChange).toHaveBeenCalledWith({ type: 'select', unsavedValue: 'option3' });
|
||||
expect(defaultProps.onInputChange).toHaveBeenCalledWith({
|
||||
type: 'select',
|
||||
unsavedValue: 'option3',
|
||||
});
|
||||
});
|
||||
|
||||
it('disables the input when isDisabled prop is true', () => {
|
||||
|
|
|
@ -30,7 +30,7 @@ export interface SelectInputProps extends InputProps<'select'> {
|
|||
export const SelectInput = ({
|
||||
field,
|
||||
unsavedChange,
|
||||
onChange: onChangeProp,
|
||||
onInputChange,
|
||||
optionLabels = {},
|
||||
optionValues: optionsProp,
|
||||
isSavingEnabled,
|
||||
|
@ -53,7 +53,7 @@ export const SelectInput = ({
|
|||
onUpdate({ type: field.type, unsavedValue: inputValue });
|
||||
};
|
||||
|
||||
const onUpdate = useUpdate({ onChange: onChangeProp, field });
|
||||
const onUpdate = useUpdate({ onInputChange, field });
|
||||
|
||||
const { id, ariaAttributes } = field;
|
||||
const { ariaLabel, ariaDescribedBy } = ariaAttributes;
|
||||
|
|
|
@ -16,9 +16,9 @@ const name = 'Some text field';
|
|||
const id = 'some:text:field';
|
||||
|
||||
describe('TextInput', () => {
|
||||
const onChange = jest.fn();
|
||||
const onInputChange = jest.fn();
|
||||
const defaultProps: TextInputProps = {
|
||||
onChange,
|
||||
onInputChange,
|
||||
field: {
|
||||
name,
|
||||
type: 'string',
|
||||
|
@ -33,7 +33,7 @@ describe('TextInput', () => {
|
|||
};
|
||||
|
||||
beforeEach(() => {
|
||||
onChange.mockClear();
|
||||
onInputChange.mockClear();
|
||||
});
|
||||
|
||||
it('renders without errors', () => {
|
||||
|
@ -47,11 +47,11 @@ describe('TextInput', () => {
|
|||
expect(input).toHaveValue('initial value');
|
||||
});
|
||||
|
||||
it('calls the onChange prop when the value changes', () => {
|
||||
it('calls the onInputChange prop when the value changes', () => {
|
||||
const { getByTestId } = render(<TextInput {...defaultProps} />);
|
||||
const input = getByTestId(`${TEST_SUBJ_PREFIX_FIELD}-${id}`);
|
||||
fireEvent.change(input, { target: { value: 'new value' } });
|
||||
expect(defaultProps.onChange).toHaveBeenCalledWith({
|
||||
expect(defaultProps.onInputChange).toHaveBeenCalledWith({
|
||||
type: 'string',
|
||||
unsavedValue: 'new value',
|
||||
});
|
||||
|
|
|
@ -26,14 +26,14 @@ export const TextInput = ({
|
|||
field,
|
||||
unsavedChange,
|
||||
isSavingEnabled,
|
||||
onChange: onChangeProp,
|
||||
onInputChange,
|
||||
}: TextInputProps) => {
|
||||
const onChange: EuiFieldTextProps['onChange'] = (event) => {
|
||||
const inputValue = event.target.value;
|
||||
onUpdate({ type: field.type, unsavedValue: inputValue });
|
||||
};
|
||||
|
||||
const onUpdate = useUpdate({ onChange: onChangeProp, field });
|
||||
const onUpdate = useUpdate({ onInputChange, field });
|
||||
|
||||
const { id, name, ariaAttributes } = field;
|
||||
const { ariaLabel, ariaDescribedBy } = ariaAttributes;
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
|
||||
import {
|
||||
FieldDefinition,
|
||||
OnChangeFn,
|
||||
OnInputChangeFn,
|
||||
SettingType,
|
||||
UnsavedFieldChange,
|
||||
} from '@kbn/management-settings-types';
|
||||
|
@ -44,6 +44,6 @@ export interface InputProps<T extends SettingType> {
|
|||
>;
|
||||
unsavedChange?: UnsavedFieldChange<T>;
|
||||
isSavingEnabled: boolean;
|
||||
/** The `onChange` handler. */
|
||||
onChange: OnChangeFn<T>;
|
||||
/** The `onInputChange` handler. */
|
||||
onInputChange: OnInputChangeFn<T>;
|
||||
}
|
||||
|
|
|
@ -19,7 +19,7 @@ For reference, this is an example of the current Advanced Settings UI:
|
|||
|
||||
## Implementation
|
||||
|
||||
A `FormRow` represents a single UiSetting, and is responsible for rendering the UiSetting's label, description, and equivalent value input. It displays the state of any unsaved change, (e.g. error). It also handles the logic for updating the UiSetting's value in a consuming component through the `onChange` handler.
|
||||
A `FormRow` represents a single UiSetting, and is responsible for rendering the UiSetting's label, description, and equivalent value input. It displays the state of any unsaved change, (e.g. error). It also handles the logic for updating the UiSetting's value in a consuming component through the `onFieldChange` handler.
|
||||
|
||||
<div><img src="./assets/form_row.png" alt="Anatomy of a `FormRow`" width="1200"/></div>
|
||||
|
||||
|
|
|
@ -12,12 +12,15 @@ import { action } from '@storybook/addon-actions';
|
|||
import { EuiPanel } from '@elastic/eui';
|
||||
import { SettingType, UnsavedFieldChange } from '@kbn/management-settings-types';
|
||||
|
||||
import { KnownTypeToMetadata, UiSettingMetadata } from '@kbn/management-settings-types/metadata';
|
||||
import {
|
||||
KnownTypeToMetadata,
|
||||
UiSettingMetadata,
|
||||
OnFieldChangeFn,
|
||||
} from '@kbn/management-settings-types';
|
||||
import { getDefaultValue, getUserValue } from '@kbn/management-settings-utilities/storybook';
|
||||
import { getFieldDefinition } from '@kbn/management-settings-field-definition';
|
||||
import { FieldRow as Component, FieldRow } from '../field_row';
|
||||
import { FieldRowProvider } from '../services';
|
||||
import { RowOnChangeFn } from '../types';
|
||||
|
||||
/**
|
||||
* Props for a {@link FieldInput} Storybook story.
|
||||
|
@ -103,10 +106,7 @@ export const storyArgs = {
|
|||
* @param type The type of the UiSetting for this {@link FieldRow}.
|
||||
* @returns A Storybook Story.
|
||||
*/
|
||||
export const getFieldRowStory = (
|
||||
type: SettingType,
|
||||
settingFields?: Partial<UiSettingMetadata<SettingType>>
|
||||
) => {
|
||||
export const getFieldRowStory = (type: SettingType, settingFields?: Partial<UiSettingMetadata>) => {
|
||||
const Story = ({
|
||||
isCustom,
|
||||
isDeprecated,
|
||||
|
@ -143,17 +143,17 @@ export const getFieldRowStory = (
|
|||
},
|
||||
});
|
||||
|
||||
const onChange: RowOnChangeFn<typeof type> = (_id, newChange) => {
|
||||
const onFieldChange: OnFieldChangeFn<typeof type> = (_id, newChange) => {
|
||||
setUnsavedChange(newChange);
|
||||
|
||||
action('onChange')({
|
||||
action('onFieldChange')({
|
||||
type,
|
||||
unsavedValue: newChange?.unsavedValue,
|
||||
savedValue: field.savedValue,
|
||||
});
|
||||
};
|
||||
|
||||
return <FieldRow {...{ field, unsavedChange, isSavingEnabled, onChange }} />;
|
||||
return <FieldRow {...{ field, unsavedChange, isSavingEnabled, onFieldChange }} />;
|
||||
};
|
||||
|
||||
// In Kibana, the image default value is never anything other than null. There would be a number
|
||||
|
|
|
@ -198,7 +198,7 @@ describe('Field', () => {
|
|||
wrap(
|
||||
<FieldRow
|
||||
field={getFieldDefinition({ id, setting })}
|
||||
onChange={handleChange}
|
||||
onFieldChange={handleChange}
|
||||
isSavingEnabled={true}
|
||||
/>
|
||||
)
|
||||
|
@ -212,7 +212,7 @@ describe('Field', () => {
|
|||
wrap(
|
||||
<FieldRow
|
||||
field={getFieldDefinition({ id, setting })}
|
||||
onChange={handleChange}
|
||||
onFieldChange={handleChange}
|
||||
isSavingEnabled={true}
|
||||
/>
|
||||
)
|
||||
|
@ -243,7 +243,7 @@ describe('Field', () => {
|
|||
setting,
|
||||
params: { isOverridden: true },
|
||||
})}
|
||||
onChange={handleChange}
|
||||
onFieldChange={handleChange}
|
||||
isSavingEnabled={true}
|
||||
/>
|
||||
)
|
||||
|
@ -265,7 +265,7 @@ describe('Field', () => {
|
|||
id,
|
||||
setting,
|
||||
})}
|
||||
onChange={handleChange}
|
||||
onFieldChange={handleChange}
|
||||
isSavingEnabled={false}
|
||||
/>
|
||||
)
|
||||
|
@ -288,7 +288,7 @@ describe('Field', () => {
|
|||
userValue: userValues[type] as any,
|
||||
},
|
||||
})}
|
||||
onChange={handleChange}
|
||||
onFieldChange={handleChange}
|
||||
isSavingEnabled={true}
|
||||
/>
|
||||
)
|
||||
|
@ -319,7 +319,7 @@ describe('Field', () => {
|
|||
setting,
|
||||
params: { isCustom: true },
|
||||
})}
|
||||
onChange={handleChange}
|
||||
onFieldChange={handleChange}
|
||||
isSavingEnabled={true}
|
||||
/>
|
||||
)
|
||||
|
@ -341,7 +341,7 @@ describe('Field', () => {
|
|||
type,
|
||||
unsavedValue: userValues[type] as any,
|
||||
}}
|
||||
onChange={handleChange}
|
||||
onFieldChange={handleChange}
|
||||
isSavingEnabled={true}
|
||||
/>
|
||||
)
|
||||
|
@ -373,7 +373,7 @@ describe('Field', () => {
|
|||
});
|
||||
|
||||
const { getByTestId } = render(
|
||||
wrap(<FieldRow field={field} onChange={handleChange} isSavingEnabled={true} />)
|
||||
wrap(<FieldRow field={field} onFieldChange={handleChange} isSavingEnabled={true} />)
|
||||
);
|
||||
|
||||
const input = getByTestId(`${DATA_TEST_SUBJ_RESET_PREFIX}-${field.id}`);
|
||||
|
@ -395,7 +395,7 @@ describe('Field', () => {
|
|||
<FieldRow
|
||||
field={field}
|
||||
unsavedChange={{ type, unsavedValue: userValues[type] }}
|
||||
onChange={handleChange}
|
||||
onFieldChange={handleChange}
|
||||
isSavingEnabled={true}
|
||||
/>
|
||||
)
|
||||
|
@ -408,12 +408,12 @@ describe('Field', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('should fire onChange when input changes', () => {
|
||||
it('should fire onFieldChange when input changes', () => {
|
||||
const setting = settings.string;
|
||||
const field = getFieldDefinition({ id: setting.name || setting.type, setting });
|
||||
|
||||
const { getByTestId } = render(
|
||||
wrap(<FieldRow field={field} onChange={handleChange} isSavingEnabled={true} />)
|
||||
wrap(<FieldRow field={field} onFieldChange={handleChange} isSavingEnabled={true} />)
|
||||
);
|
||||
|
||||
const input = getByTestId(`${TEST_SUBJ_PREFIX_FIELD}-${field.id}`);
|
||||
|
@ -424,12 +424,12 @@ describe('Field', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('should fire onChange with an error when input changes with invalid value', () => {
|
||||
it('should fire onFieldChange with an error when input changes with invalid value', () => {
|
||||
const setting = settings.color;
|
||||
const field = getFieldDefinition({ id: setting.name || setting.type, setting });
|
||||
|
||||
const { getByTestId } = render(
|
||||
wrap(<FieldRow field={field} onChange={handleChange} isSavingEnabled={true} />)
|
||||
wrap(<FieldRow field={field} onFieldChange={handleChange} isSavingEnabled={true} />)
|
||||
);
|
||||
|
||||
const input = getByTestId(`euiColorPickerAnchor ${TEST_SUBJ_PREFIX_FIELD}-${field.id}`);
|
||||
|
@ -451,7 +451,7 @@ describe('Field', () => {
|
|||
wrap(
|
||||
<FieldRow
|
||||
field={field}
|
||||
onChange={handleChange}
|
||||
onFieldChange={handleChange}
|
||||
isSavingEnabled={true}
|
||||
unsavedChange={{
|
||||
type: setting.type,
|
||||
|
@ -491,7 +491,11 @@ describe('Field', () => {
|
|||
|
||||
const { getByTestId } = render(
|
||||
wrap(
|
||||
<FieldRow {...{ field, unsavedChange }} onChange={handleChange} isSavingEnabled={true} />
|
||||
<FieldRow
|
||||
{...{ field, unsavedChange }}
|
||||
onFieldChange={handleChange}
|
||||
isSavingEnabled={true}
|
||||
/>
|
||||
)
|
||||
);
|
||||
|
||||
|
@ -512,7 +516,7 @@ describe('Field', () => {
|
|||
});
|
||||
|
||||
const { getByTestId, getByAltText } = render(
|
||||
wrap(<FieldRow {...{ field }} onChange={handleChange} isSavingEnabled={true} />)
|
||||
wrap(<FieldRow {...{ field }} onFieldChange={handleChange} isSavingEnabled={true} />)
|
||||
);
|
||||
|
||||
const link = getByTestId(`${DATA_TEST_SUBJ_CHANGE_LINK_PREFIX}-${field.id}`);
|
||||
|
@ -534,7 +538,7 @@ describe('Field', () => {
|
|||
wrap(
|
||||
<FieldRow
|
||||
{...{ field }}
|
||||
onChange={handleChange}
|
||||
onFieldChange={handleChange}
|
||||
unsavedChange={{ type: 'image', unsavedValue: userInputValues.image }}
|
||||
isSavingEnabled={true}
|
||||
/>
|
||||
|
|
|
@ -21,7 +21,8 @@ import type {
|
|||
ResetInputRef,
|
||||
SettingType,
|
||||
UnsavedFieldChange,
|
||||
OnChangeFn,
|
||||
OnInputChangeFn,
|
||||
OnFieldChangeFn,
|
||||
} from '@kbn/management-settings-types';
|
||||
import { isImageFieldDefinition } from '@kbn/management-settings-field-definition';
|
||||
import { FieldInput } from '@kbn/management-settings-components-field-input';
|
||||
|
@ -30,12 +31,11 @@ import { hasUnsavedChange } from '@kbn/management-settings-utilities';
|
|||
import { FieldDescription } from './description';
|
||||
import { FieldTitle } from './title';
|
||||
import { useFieldStyles } from './field_row.styles';
|
||||
import { RowOnChangeFn } from './types';
|
||||
import { FieldInputFooter } from './footer';
|
||||
|
||||
export const DATA_TEST_SUBJ_SCREEN_READER_MESSAGE = 'fieldRowScreenReaderMessage';
|
||||
|
||||
type Definition<T extends SettingType> = Pick<
|
||||
type Definition<T extends SettingType = SettingType> = Pick<
|
||||
FieldDefinition<T>,
|
||||
| 'ariaAttributes'
|
||||
| 'defaultValue'
|
||||
|
@ -57,18 +57,18 @@ type Definition<T extends SettingType> = Pick<
|
|||
*/
|
||||
export interface FieldRowProps {
|
||||
/** The {@link FieldDefinition} corresponding the setting. */
|
||||
field: Definition<SettingType>;
|
||||
field: Definition;
|
||||
/** True if saving settings is enabled, false otherwise. */
|
||||
isSavingEnabled: boolean;
|
||||
/** The {@link OnChangeFn} handler. */
|
||||
onChange: RowOnChangeFn<SettingType>;
|
||||
/** The {@link OnInputChangeFn} handler. */
|
||||
onFieldChange: OnFieldChangeFn;
|
||||
/**
|
||||
* The onClear handler, if a value is cleared to an empty or default state.
|
||||
* @param id The id relating to the field to clear.
|
||||
*/
|
||||
onClear?: (id: string) => void;
|
||||
/** The {@link UnsavedFieldChange} corresponding to any unsaved change to the field. */
|
||||
unsavedChange?: UnsavedFieldChange<SettingType>;
|
||||
unsavedChange?: UnsavedFieldChange;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -76,7 +76,7 @@ export interface FieldRowProps {
|
|||
* @param props The {@link FieldRowProps} for the {@link FieldRow} component.
|
||||
*/
|
||||
export const FieldRow = (props: FieldRowProps) => {
|
||||
const { isSavingEnabled, onChange: onChangeProp, field, unsavedChange } = props;
|
||||
const { isSavingEnabled, onFieldChange, field, unsavedChange } = props;
|
||||
const { id, groupId, isOverridden, unsavedFieldId } = field;
|
||||
const { cssFieldFormGroup } = useFieldStyles({
|
||||
field,
|
||||
|
@ -86,9 +86,9 @@ export const FieldRow = (props: FieldRowProps) => {
|
|||
// Create a ref for those input fields that use a `reset` handle.
|
||||
const ref = useRef<ResetInputRef>(null);
|
||||
|
||||
// Route any change to the `onChange` handler, along with the field id.
|
||||
const onChange: OnChangeFn<SettingType> = (update) => {
|
||||
onChangeProp(id, update);
|
||||
// Route any change to the `onFieldChange` handler, along with the field id.
|
||||
const onInputChange: OnInputChangeFn = (update) => {
|
||||
onFieldChange(id, update);
|
||||
};
|
||||
|
||||
const onReset = () => {
|
||||
|
@ -97,9 +97,9 @@ export const FieldRow = (props: FieldRowProps) => {
|
|||
const update = { type: field.type, unsavedValue: field.defaultValue };
|
||||
|
||||
if (hasUnsavedChange(field, update)) {
|
||||
onChange(update);
|
||||
onInputChange(update);
|
||||
} else {
|
||||
onChange();
|
||||
onInputChange();
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -111,9 +111,9 @@ export const FieldRow = (props: FieldRowProps) => {
|
|||
// Indicate a field is being cleared for a new value by setting its unchanged
|
||||
// value to`undefined`. Currently, this only applies to `image` fields.
|
||||
if (field.savedValue !== undefined && field.savedValue !== null) {
|
||||
onChange({ type: field.type, unsavedValue: undefined });
|
||||
onInputChange({ type: field.type, unsavedValue: undefined });
|
||||
} else {
|
||||
onChange();
|
||||
onInputChange();
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -162,7 +162,7 @@ export const FieldRow = (props: FieldRowProps) => {
|
|||
<FieldInput
|
||||
isSavingEnabled={isSavingEnabled && !isOverridden}
|
||||
isInvalid={unsavedChange?.isInvalid}
|
||||
{...{ field, unsavedChange, ref, onChange }}
|
||||
{...{ field, unsavedChange, ref, onInputChange }}
|
||||
/>
|
||||
{unsavedScreenReaderMessage}
|
||||
</>
|
||||
|
|
|
@ -9,13 +9,11 @@
|
|||
import React from 'react';
|
||||
import { render, fireEvent } from '@testing-library/react';
|
||||
|
||||
import { SettingType } from '@kbn/management-settings-types';
|
||||
|
||||
import { wrap } from '../mocks';
|
||||
import { InputResetLink, InputResetLinkProps } from './reset_link';
|
||||
|
||||
describe('InputResetLink', () => {
|
||||
const defaultProps: InputResetLinkProps<SettingType> = {
|
||||
const defaultProps: InputResetLinkProps = {
|
||||
field: {
|
||||
type: 'string',
|
||||
id: 'test',
|
||||
|
|
|
@ -21,7 +21,7 @@ import { isFieldDefaultValue } from '@kbn/management-settings-utilities';
|
|||
/**
|
||||
* Props for a {@link InputResetLink} component.
|
||||
*/
|
||||
export interface InputResetLinkProps<T extends SettingType> {
|
||||
export interface InputResetLinkProps<T extends SettingType = SettingType> {
|
||||
/** The {@link FieldDefinition} corresponding the setting. */
|
||||
field: Pick<
|
||||
FieldDefinition<T>,
|
||||
|
|
|
@ -6,12 +6,11 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
export { FieldRow, type FieldRowProps as FieldProps } from './field_row';
|
||||
export { FieldRow, type FieldRowProps } from './field_row';
|
||||
export { FieldRowProvider, FieldRowKibanaProvider, type FieldRowProviderProps } from './services';
|
||||
export type {
|
||||
FieldRowServices,
|
||||
FieldRowKibanaDependencies,
|
||||
RowOnChangeFn,
|
||||
KibanaDependencies,
|
||||
Services,
|
||||
} from './types';
|
||||
|
|
|
@ -12,7 +12,6 @@ import type {
|
|||
FieldInputServices,
|
||||
FieldInputKibanaDependencies,
|
||||
} from '@kbn/management-settings-components-field-input';
|
||||
import { SettingType, UnsavedFieldChange } from '@kbn/management-settings-types';
|
||||
|
||||
/**
|
||||
* Contextual services used by a {@link FieldRow} component.
|
||||
|
@ -43,13 +42,3 @@ export interface KibanaDependencies {
|
|||
* render a {@link FieldRow} component and its dependents.
|
||||
*/
|
||||
export type FieldRowKibanaDependencies = KibanaDependencies & FieldInputKibanaDependencies;
|
||||
|
||||
/**
|
||||
* An `onChange` handler for a {@link FieldRow} component.
|
||||
* @param id A unique id corresponding to the particular setting being changed.
|
||||
* @param change The {@link UnsavedFieldChange} corresponding to any unsaved change to the field.
|
||||
*/
|
||||
export type RowOnChangeFn<T extends SettingType> = (
|
||||
id: string,
|
||||
change?: UnsavedFieldChange<T>
|
||||
) => void;
|
||||
|
|
|
@ -7,22 +7,20 @@
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { fireEvent, render, waitFor } from '@testing-library/react';
|
||||
import { act, fireEvent, render, waitFor } from '@testing-library/react';
|
||||
|
||||
import { FieldDefinition, SettingType } from '@kbn/management-settings-types';
|
||||
import { getFieldDefinitions } from '@kbn/management-settings-field-definition';
|
||||
import { getSettingsMock } from '@kbn/management-settings-utilities/mocks/settings.mock';
|
||||
import { TEST_SUBJ_PREFIX_FIELD } from '@kbn/management-settings-components-field-input/input';
|
||||
|
||||
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 { wrap, createFormServicesMock, uiSettingsClientMock } from './mocks';
|
||||
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
|
||||
);
|
||||
const fields: FieldDefinition[] = getFieldDefinitions(settingsMock, uiSettingsClientMock);
|
||||
|
||||
describe('Form', () => {
|
||||
beforeEach(() => {
|
||||
|
@ -78,14 +76,18 @@ describe('Form', () => {
|
|||
fireEvent.change(input, { target: { value: 'test' } });
|
||||
|
||||
const saveButton = getByTestId(DATA_TEST_SUBJ_SAVE_BUTTON);
|
||||
fireEvent.click(saveButton);
|
||||
act(() => {
|
||||
fireEvent.click(saveButton);
|
||||
});
|
||||
|
||||
expect(services.saveChanges).toHaveBeenCalledWith({
|
||||
string: { type: 'string', unsavedValue: 'test' },
|
||||
await waitFor(() => {
|
||||
expect(services.saveChanges).toHaveBeenCalledWith({
|
||||
string: { type: 'string', unsavedValue: 'test' },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('clears changes when Cancel button is clicked', () => {
|
||||
it('clears changes when Cancel button is clicked', async () => {
|
||||
const { getByTestId } = render(wrap(<Form fields={fields} isSavingEnabled={false} />));
|
||||
|
||||
const testFieldType = 'string';
|
||||
|
@ -93,12 +95,16 @@ describe('Form', () => {
|
|||
fireEvent.change(input, { target: { value: 'test' } });
|
||||
|
||||
const cancelButton = getByTestId(DATA_TEST_SUBJ_CANCEL_BUTTON);
|
||||
fireEvent.click(cancelButton);
|
||||
act(() => {
|
||||
fireEvent.click(cancelButton);
|
||||
});
|
||||
|
||||
expect(input).toHaveValue(settingsMock[testFieldType].value);
|
||||
await waitFor(() => {
|
||||
expect(input).toHaveValue(settingsMock[testFieldType].value);
|
||||
});
|
||||
});
|
||||
|
||||
it('fires showError when saving is unsuccessful', () => {
|
||||
it('fires showError when saving is unsuccessful', async () => {
|
||||
const services: FormServices = createFormServicesMock();
|
||||
const saveChangesWithError = jest.fn(() => {
|
||||
throw new Error('Unable to save');
|
||||
|
@ -114,15 +120,19 @@ describe('Form', () => {
|
|||
fireEvent.change(input, { target: { value: 'test' } });
|
||||
|
||||
const saveButton = getByTestId(DATA_TEST_SUBJ_SAVE_BUTTON);
|
||||
fireEvent.click(saveButton);
|
||||
act(() => {
|
||||
fireEvent.click(saveButton);
|
||||
});
|
||||
|
||||
expect(testServices.showError).toHaveBeenCalled();
|
||||
await waitFor(() => {
|
||||
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(
|
||||
const testFields: FieldDefinition[] = getFieldDefinitions(
|
||||
getSettingsMock(true),
|
||||
uiSettingsClientMock
|
||||
);
|
||||
|
@ -135,7 +145,9 @@ describe('Form', () => {
|
|||
fireEvent.change(input, { target: { value: 'test' } });
|
||||
|
||||
const saveButton = getByTestId(DATA_TEST_SUBJ_SAVE_BUTTON);
|
||||
fireEvent.click(saveButton);
|
||||
act(() => {
|
||||
fireEvent.click(saveButton);
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(services.showReloadPagePrompt).toHaveBeenCalled();
|
||||
|
|
|
@ -9,9 +9,10 @@
|
|||
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 { FieldCategories } from '@kbn/management-settings-components-field-category';
|
||||
import { UnsavedFieldChange, OnFieldChangeFn } from '@kbn/management-settings-types';
|
||||
import { isEmpty } from 'lodash';
|
||||
import { categorizeFields } from '@kbn/management-settings-utilities';
|
||||
import { BottomBar } from './bottom_bar';
|
||||
import { useSave } from './use_save';
|
||||
|
||||
|
@ -20,7 +21,7 @@ import { useSave } from './use_save';
|
|||
*/
|
||||
export interface FormProps {
|
||||
/** A list of {@link FieldDefinition} corresponding to settings to be displayed in the form. */
|
||||
fields: Array<FieldDefinition<SettingType>>;
|
||||
fields: FieldDefinition[];
|
||||
/** True if saving settings is enabled, false otherwise. */
|
||||
isSavingEnabled: boolean;
|
||||
}
|
||||
|
@ -32,9 +33,9 @@ export interface FormProps {
|
|||
export const Form = (props: FormProps) => {
|
||||
const { fields, isSavingEnabled } = props;
|
||||
|
||||
const [unsavedChanges, setUnsavedChanges] = React.useState<
|
||||
Record<string, UnsavedFieldChange<SettingType>>
|
||||
>({});
|
||||
const [unsavedChanges, setUnsavedChanges] = React.useState<Record<string, UnsavedFieldChange>>(
|
||||
{}
|
||||
);
|
||||
|
||||
const [isLoading, setIsLoading] = React.useState<boolean>(false);
|
||||
|
||||
|
@ -53,7 +54,7 @@ export const Form = (props: FormProps) => {
|
|||
setIsLoading(false);
|
||||
};
|
||||
|
||||
const onChange: RowOnChangeFn<SettingType> = (id, change) => {
|
||||
const onFieldChange: OnFieldChangeFn = (id, change) => {
|
||||
if (!change) {
|
||||
const { [id]: unsavedChange, ...rest } = unsavedChanges;
|
||||
setUnsavedChanges(rest);
|
||||
|
@ -63,15 +64,16 @@ export const Form = (props: FormProps) => {
|
|||
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 }} />;
|
||||
});
|
||||
const categorizedFields = categorizeFields(fields);
|
||||
|
||||
/** TODO - Querying is not enabled yet. */
|
||||
const onClearQuery = () => {};
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<div>{fieldRows}</div>
|
||||
<FieldCategories
|
||||
{...{ categorizedFields, isSavingEnabled, onFieldChange, onClearQuery, unsavedChanges }}
|
||||
/>
|
||||
{!isEmpty(unsavedChanges) && (
|
||||
<BottomBar
|
||||
onSaveAll={saveAll}
|
||||
|
|
|
@ -7,5 +7,4 @@
|
|||
*/
|
||||
|
||||
export { TestWrapper, createFormServicesMock, wrap } from './context';
|
||||
export { getSettingsMock } from './settings';
|
||||
export { uiSettingsClientMock } from './settings_client';
|
||||
|
|
|
@ -6,13 +6,13 @@
|
|||
* 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 { UnsavedFieldChange } from '@kbn/management-settings-types';
|
||||
|
||||
import {
|
||||
FieldCategoryKibanaProvider,
|
||||
FieldCategoryProvider,
|
||||
} from '@kbn/management-settings-components-field-category';
|
||||
import type { FormServices, FormKibanaDependencies, Services } from './types';
|
||||
import { reloadPageToast } from './reload_page_toast';
|
||||
|
||||
|
@ -33,7 +33,7 @@ export const FormProvider = ({ children, ...services }: FormProviderProps) => {
|
|||
|
||||
return (
|
||||
<FormContext.Provider value={{ saveChanges, showError, showReloadPagePrompt }}>
|
||||
<FieldRowProvider {...rest}>{children}</FieldRowProvider>
|
||||
<FieldCategoryProvider {...rest}>{children}</FieldCategoryProvider>
|
||||
</FormContext.Provider>
|
||||
);
|
||||
};
|
||||
|
@ -47,7 +47,7 @@ export const FormKibanaProvider: FC<FormKibanaDependencies> = ({ children, ...de
|
|||
return (
|
||||
<FormContext.Provider
|
||||
value={{
|
||||
saveChanges: (changes: Record<string, UnsavedFieldChange<SettingType>>) => {
|
||||
saveChanges: (changes: Record<string, UnsavedFieldChange>) => {
|
||||
const arr = Object.entries(changes).map(([key, value]) =>
|
||||
settings.client.set(key, value.unsavedValue)
|
||||
);
|
||||
|
@ -57,7 +57,9 @@ export const FormKibanaProvider: FC<FormKibanaDependencies> = ({ children, ...de
|
|||
showReloadPagePrompt: () => toasts.add(reloadPageToast(theme, i18nStart)),
|
||||
}}
|
||||
>
|
||||
<FieldRowKibanaProvider {...{ docLinks, toasts }}>{children}</FieldRowKibanaProvider>
|
||||
<FieldCategoryKibanaProvider {...{ docLinks, toasts }}>
|
||||
{children}
|
||||
</FieldCategoryKibanaProvider>
|
||||
</FormContext.Provider>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -6,12 +6,13 @@
|
|||
* 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 { FieldDefinition } from '@kbn/management-settings-types';
|
||||
import { getFieldDefinitions } from '@kbn/management-settings-field-definition';
|
||||
import { getSettingsMock, uiSettingsClientMock } from '../mocks';
|
||||
import { getSettingsMock } from '@kbn/management-settings-utilities/mocks/settings.mock';
|
||||
|
||||
import { uiSettingsClientMock } from '../mocks';
|
||||
import { Form as Component } from '../form';
|
||||
import { FormProvider } from '../services';
|
||||
|
||||
|
@ -37,12 +38,15 @@ export default {
|
|||
showError={action('showError')}
|
||||
showReloadPagePrompt={action('showReloadPagePrompt')}
|
||||
>
|
||||
<EuiPanel>
|
||||
<Story />
|
||||
</EuiPanel>
|
||||
<Story />
|
||||
</FormProvider>
|
||||
),
|
||||
],
|
||||
parameters: {
|
||||
backgrounds: {
|
||||
default: 'ghost',
|
||||
},
|
||||
},
|
||||
} as ComponentMeta<typeof Component>;
|
||||
|
||||
interface FormStoryProps {
|
||||
|
@ -53,7 +57,7 @@ interface FormStoryProps {
|
|||
}
|
||||
|
||||
export const Form = ({ isSavingEnabled, requirePageReload }: FormStoryProps) => {
|
||||
const fields: Array<FieldDefinition<SettingType>> = getFieldDefinitions(
|
||||
const fields: FieldDefinition[] = getFieldDefinitions(
|
||||
getSettingsMock(requirePageReload),
|
||||
uiSettingsClientMock
|
||||
);
|
||||
|
|
|
@ -30,5 +30,7 @@
|
|||
"@kbn/core-theme-browser",
|
||||
"@kbn/core-ui-settings-browser",
|
||||
"@kbn/management-settings-components-field-input",
|
||||
"@kbn/management-settings-components-field-category",
|
||||
"@kbn/management-settings-utilities",
|
||||
]
|
||||
}
|
||||
|
|
|
@ -10,7 +10,7 @@ import type {
|
|||
FieldRowKibanaDependencies,
|
||||
FieldRowServices,
|
||||
} from '@kbn/management-settings-components-field-row';
|
||||
import { SettingType, UnsavedFieldChange } from '@kbn/management-settings-types';
|
||||
import { 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';
|
||||
|
@ -20,7 +20,7 @@ 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;
|
||||
saveChanges: (changes: Record<string, UnsavedFieldChange>) => void;
|
||||
showError: (message: string) => void;
|
||||
showReloadPagePrompt: () => void;
|
||||
}
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import type { FieldDefinition, SettingType } from '@kbn/management-settings-types';
|
||||
import type { FieldDefinition } from '@kbn/management-settings-types';
|
||||
import { isEmpty } from 'lodash';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { UnsavedFieldChange } from '@kbn/management-settings-types';
|
||||
|
@ -14,7 +14,7 @@ import { useServices } from './services';
|
|||
|
||||
export interface UseSaveParameters {
|
||||
/** All {@link FieldDefinition} in the form. */
|
||||
fields: Array<FieldDefinition<SettingType>>;
|
||||
fields: FieldDefinition[];
|
||||
/** The function to invoke for clearing all unsaved changes. */
|
||||
clearChanges: () => void;
|
||||
}
|
||||
|
@ -28,7 +28,7 @@ export interface UseSaveParameters {
|
|||
export const useSave = (params: UseSaveParameters) => {
|
||||
const { saveChanges, showError, showReloadPagePrompt } = useServices();
|
||||
|
||||
return async (changes: Record<string, UnsavedFieldChange<SettingType>>) => {
|
||||
return async (changes: Record<string, UnsavedFieldChange>) => {
|
||||
if (isEmpty(changes)) {
|
||||
return;
|
||||
}
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
*/
|
||||
|
||||
import { IUiSettingsClient } from '@kbn/core-ui-settings-browser';
|
||||
import { FieldDefinition, SettingType, UiSettingMetadata } from '@kbn/management-settings-types';
|
||||
import { FieldDefinition, UiSettingMetadata } from '@kbn/management-settings-types';
|
||||
import { getFieldDefinition } from './get_definition';
|
||||
|
||||
type SettingsClient = Pick<IUiSettingsClient, 'isCustom' | 'isOverridden'>;
|
||||
|
@ -21,9 +21,9 @@ type SettingsClient = Pick<IUiSettingsClient, 'isCustom' | 'isOverridden'>;
|
|||
* @returns An array of {@link FieldDefinition} objects.
|
||||
*/
|
||||
export const getFieldDefinitions = (
|
||||
settings: Record<string, UiSettingMetadata<SettingType>>,
|
||||
settings: Record<string, UiSettingMetadata>,
|
||||
client: SettingsClient
|
||||
): Array<FieldDefinition<SettingType>> =>
|
||||
): FieldDefinition[] =>
|
||||
Object.entries(settings).map(([id, setting]) =>
|
||||
getFieldDefinition({
|
||||
id,
|
||||
|
|
|
@ -26,13 +26,12 @@ import {
|
|||
MarkdownFieldDefinition,
|
||||
NumberFieldDefinition,
|
||||
SelectFieldDefinition,
|
||||
SettingType,
|
||||
StringFieldDefinition,
|
||||
UndefinedFieldDefinition,
|
||||
} from '@kbn/management-settings-types';
|
||||
|
||||
/** Simplifed type for a {@link FieldDefinition} */
|
||||
type Definition = Pick<FieldDefinition<SettingType>, 'type'>;
|
||||
type Definition = Pick<FieldDefinition, 'type'>;
|
||||
|
||||
/**
|
||||
* Returns `true` if the given {@link FieldDefinition} is an {@link ArrayFieldDefinition},
|
||||
|
|
|
@ -27,12 +27,11 @@ import {
|
|||
SelectUnsavedFieldChange,
|
||||
StringUnsavedFieldChange,
|
||||
UndefinedUnsavedFieldChange,
|
||||
SettingType,
|
||||
UnsavedFieldChange,
|
||||
} from '@kbn/management-settings-types';
|
||||
|
||||
/** Simplifed type for a {@link UnsavedFieldChange} */
|
||||
type Change = UnsavedFieldChange<SettingType>;
|
||||
type Change = UnsavedFieldChange;
|
||||
|
||||
/**
|
||||
* Returns `true` if the given {@link FieldUnsavedChange} is an {@link ArrayUnsavedFieldChange},
|
||||
|
|
16
packages/kbn-management/settings/types/category.ts
Normal file
16
packages/kbn-management/settings/types/category.ts
Normal file
|
@ -0,0 +1,16 @@
|
|||
/*
|
||||
* 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 { FieldDefinition } from './field_definition';
|
||||
|
||||
export interface CategorizedFields {
|
||||
[category: string]: {
|
||||
count: number;
|
||||
fields: FieldDefinition[];
|
||||
};
|
||||
}
|
|
@ -20,7 +20,10 @@ import { KnownTypeToValue, SettingType } from './setting_type';
|
|||
* representing a UiSetting).
|
||||
* @public
|
||||
*/
|
||||
export interface FieldDefinition<T extends SettingType, V = KnownTypeToValue<T> | null> {
|
||||
export interface FieldDefinition<
|
||||
T extends SettingType = SettingType,
|
||||
V = KnownTypeToValue<T> | null
|
||||
> {
|
||||
/** UX ARIA attributes derived from the setting. */
|
||||
ariaAttributes: {
|
||||
/** The `aria-label` attribute for the field input. */
|
||||
|
|
|
@ -51,6 +51,7 @@ export type {
|
|||
StringUnsavedFieldChange,
|
||||
UndefinedUnsavedFieldChange,
|
||||
UnsavedFieldChange,
|
||||
UnsavedFieldChanges,
|
||||
} from './unsaved_change';
|
||||
|
||||
export type {
|
||||
|
@ -64,6 +65,8 @@ export type {
|
|||
Value,
|
||||
} from './setting_type';
|
||||
|
||||
export type { CategorizedFields } from './category';
|
||||
|
||||
/**
|
||||
* A React `ref` that indicates an input can be reset using an
|
||||
* imperative handle.
|
||||
|
@ -76,4 +79,16 @@ export type ResetInputRef = {
|
|||
* A function that is called when the value of a {@link FieldInput} changes.
|
||||
* @param change The {@link UnsavedFieldChange} passed to the handler.
|
||||
*/
|
||||
export type OnChangeFn<T extends SettingType> = (change?: UnsavedFieldChange<T>) => void;
|
||||
export type OnInputChangeFn<T extends SettingType = SettingType> = (
|
||||
change?: UnsavedFieldChange<T>
|
||||
) => void;
|
||||
|
||||
/**
|
||||
* An `onFieldChange` handler when a Field changes.
|
||||
* @param id A unique id corresponding to the particular setting being changed.
|
||||
* @param change The {@link UnsavedFieldChange} corresponding to any unsaved change to the field.
|
||||
*/
|
||||
export type OnFieldChangeFn<T extends SettingType = SettingType> = (
|
||||
id: string,
|
||||
change?: UnsavedFieldChange<T>
|
||||
) => void;
|
||||
|
|
|
@ -21,8 +21,10 @@ export type UiSetting<T> = PublicUiSettingsParams & UserProvidedValues<T>;
|
|||
*
|
||||
* @public
|
||||
*/
|
||||
export interface UiSettingMetadata<T extends SettingType, V = KnownTypeToValue<T> | null>
|
||||
extends UiSetting<V> {
|
||||
export interface UiSettingMetadata<
|
||||
T extends SettingType = SettingType,
|
||||
V = KnownTypeToValue<T> | null
|
||||
> extends UiSetting<V> {
|
||||
/**
|
||||
* The type of setting being represented.
|
||||
* @see{@link SettingType}
|
||||
|
|
|
@ -63,7 +63,7 @@ export type Value = string | boolean | number | Array<string | number> | undefin
|
|||
* given {@link SettingType}.
|
||||
* @public
|
||||
*/
|
||||
export type KnownTypeToValue<T extends SettingType> =
|
||||
export type KnownTypeToValue<T extends SettingType = SettingType> =
|
||||
T extends 'color' | 'image' | 'json' | 'markdown' | 'select' | 'string' ? string :
|
||||
T extends 'boolean' ? boolean :
|
||||
T extends 'number' | 'bigint' ? number :
|
||||
|
|
|
@ -13,7 +13,7 @@ import { KnownTypeToValue, SettingType } from './setting_type';
|
|||
* yet been saved.
|
||||
* @public
|
||||
*/
|
||||
export interface UnsavedFieldChange<T extends SettingType> {
|
||||
export interface UnsavedFieldChange<T extends SettingType = SettingType> {
|
||||
/**
|
||||
* The type of setting.
|
||||
* @see {@link SettingType}
|
||||
|
@ -125,3 +125,5 @@ export type KnownTypeToUnsavedChange<T extends SettingType> =
|
|||
T extends 'string' ? StringUnsavedFieldChange:
|
||||
T extends 'undefined' ? UndefinedUnsavedFieldChange :
|
||||
never;
|
||||
|
||||
export type UnsavedFieldChanges = Record<string, UnsavedFieldChange>;
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
/*
|
||||
* 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 { CategorizedFields, FieldDefinition } from '@kbn/management-settings-types';
|
||||
|
||||
export const categorizeFields = (fields: FieldDefinition[]): CategorizedFields => {
|
||||
// Group settings by category
|
||||
return fields.reduce((grouped: CategorizedFields, field) => {
|
||||
const category = field.categories[0];
|
||||
const group = grouped[category] || { count: 0, fields: [] };
|
||||
group.fields = [...group.fields, field];
|
||||
group.count = group.fields.length;
|
||||
grouped[category] = group;
|
||||
|
||||
return grouped;
|
||||
}, {});
|
||||
};
|
|
@ -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 { i18n } from '@kbn/i18n';
|
||||
|
||||
const upperFirst = (str = '') => str.replace(/^./, (strng) => strng.toUpperCase());
|
||||
|
||||
const names: Record<string, string> = {
|
||||
general: i18n.translate('management.settings.categoryNames.generalLabel', {
|
||||
defaultMessage: 'General',
|
||||
}),
|
||||
machineLearning: i18n.translate('management.settings.categoryNames.machineLearningLabel', {
|
||||
defaultMessage: 'Machine Learning',
|
||||
}),
|
||||
observability: i18n.translate('management.settings.categoryNames.observabilityLabel', {
|
||||
defaultMessage: 'Observability',
|
||||
}),
|
||||
timelion: i18n.translate('management.settings.categoryNames.timelionLabel', {
|
||||
defaultMessage: 'Timelion',
|
||||
}),
|
||||
notifications: i18n.translate('management.settings.categoryNames.notificationsLabel', {
|
||||
defaultMessage: 'Notifications',
|
||||
}),
|
||||
visualizations: i18n.translate('management.settings.categoryNames.visualizationsLabel', {
|
||||
defaultMessage: 'Visualizations',
|
||||
}),
|
||||
discover: i18n.translate('management.settings.categoryNames.discoverLabel', {
|
||||
defaultMessage: 'Discover',
|
||||
}),
|
||||
dashboard: i18n.translate('management.settings.categoryNames.dashboardLabel', {
|
||||
defaultMessage: 'Dashboard',
|
||||
}),
|
||||
reporting: i18n.translate('management.settings.categoryNames.reportingLabel', {
|
||||
defaultMessage: 'Reporting',
|
||||
}),
|
||||
search: i18n.translate('management.settings.categoryNames.searchLabel', {
|
||||
defaultMessage: 'Search',
|
||||
}),
|
||||
securitySolution: i18n.translate('management.settings.categoryNames.securitySolutionLabel', {
|
||||
defaultMessage: 'Security Solution',
|
||||
}),
|
||||
enterpriseSearch: i18n.translate('management.settings.categoryNames.enterpriseSearchLabel', {
|
||||
defaultMessage: 'Enterprise Search',
|
||||
}),
|
||||
};
|
||||
|
||||
export function getCategoryName(category?: string) {
|
||||
return category ? names[category] || upperFirst(category) : '';
|
||||
}
|
10
packages/kbn-management/settings/utilities/category/index.ts
Normal file
10
packages/kbn-management/settings/utilities/category/index.ts
Normal file
|
@ -0,0 +1,10 @@
|
|||
/*
|
||||
* 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 { categorizeFields } from './categorize_fields';
|
||||
export { getCategoryName } from './get_category_name';
|
|
@ -6,31 +6,33 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import type { FieldDefinition, SettingType, OnChangeFn } from '@kbn/management-settings-types';
|
||||
import type { FieldDefinition, SettingType, OnInputChangeFn } from '@kbn/management-settings-types';
|
||||
import { hasUnsavedChange } from './has_unsaved_change';
|
||||
|
||||
export interface UseUpdateParameters<T extends SettingType> {
|
||||
/** The {@link OnChangeFn} to invoke. */
|
||||
onChange: OnChangeFn<T>;
|
||||
/** The {@link OnInputChangeFn} to invoke. */
|
||||
onInputChange: OnInputChangeFn<T>;
|
||||
/** The {@link FieldDefinition} to use to create an update. */
|
||||
field: Pick<FieldDefinition<T>, 'defaultValue' | 'savedValue'>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to provide a standard {@link OnChangeFn} that will send an update to the
|
||||
* Hook to provide a standard {@link OnInputChangeFn} that will send an update to the
|
||||
* field.
|
||||
*
|
||||
* @param params The {@link UseUpdateParameters} to use.
|
||||
* @returns An {@link OnChangeFn} that will send an update to the field.
|
||||
* @returns An {@link OnInputChangeFn} that will send an update to the field.
|
||||
*/
|
||||
export const useUpdate = <T extends SettingType>(params: UseUpdateParameters<T>): OnChangeFn<T> => {
|
||||
const { onChange, field } = params;
|
||||
export const useUpdate = <T extends SettingType>(
|
||||
params: UseUpdateParameters<T>
|
||||
): OnInputChangeFn<T> => {
|
||||
const { onInputChange, field } = params;
|
||||
|
||||
return (update) => {
|
||||
if (hasUnsavedChange(field, update)) {
|
||||
onChange(update);
|
||||
onInputChange(update);
|
||||
} else {
|
||||
onChange();
|
||||
onInputChange();
|
||||
}
|
||||
};
|
||||
};
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
*/
|
||||
|
||||
export { isSettingDefaultValue, normalizeSettings } from './setting';
|
||||
|
||||
export {
|
||||
getFieldInputValue,
|
||||
hasUnsavedChange,
|
||||
|
@ -14,3 +15,5 @@ export {
|
|||
useUpdate,
|
||||
type UseUpdateParameters,
|
||||
} from './field';
|
||||
|
||||
export { categorizeFields, getCategoryName } from './category';
|
||||
|
|
|
@ -14,13 +14,15 @@ type Settings = {
|
|||
|
||||
/**
|
||||
* A utility function returning a representative set of UiSettings.
|
||||
* @param requirePageReload The value of the `requirePageReload` param for all settings.
|
||||
* @param requiresPageReload The value of the `requirePageReload` param for all settings.
|
||||
*/
|
||||
export const getSettingsMock = (requirePageReload: boolean = false): Settings => {
|
||||
export const getSettingsMock = (
|
||||
requiresPageReload: boolean = false,
|
||||
readonly: boolean = false
|
||||
): Settings => {
|
||||
const defaults = {
|
||||
requiresPageReload: requirePageReload,
|
||||
readonly: false,
|
||||
category: ['category'],
|
||||
requiresPageReload,
|
||||
readonly,
|
||||
};
|
||||
|
||||
return {
|
||||
|
@ -29,7 +31,8 @@ export const getSettingsMock = (requirePageReload: boolean = false): Settings =>
|
|||
name: 'array:test:setting',
|
||||
type: 'array',
|
||||
userValue: null,
|
||||
value: ['example_value'],
|
||||
value: ['foo', 'bar', 'baz'],
|
||||
category: ['general', 'dashboard'],
|
||||
...defaults,
|
||||
},
|
||||
boolean: {
|
||||
|
@ -38,6 +41,7 @@ export const getSettingsMock = (requirePageReload: boolean = false): Settings =>
|
|||
type: 'boolean',
|
||||
userValue: null,
|
||||
value: true,
|
||||
category: ['general', 'dashboard'],
|
||||
...defaults,
|
||||
},
|
||||
color: {
|
||||
|
@ -46,6 +50,7 @@ export const getSettingsMock = (requirePageReload: boolean = false): Settings =>
|
|||
type: 'color',
|
||||
userValue: null,
|
||||
value: '#FF00CC',
|
||||
category: ['general', 'dashboard'],
|
||||
...defaults,
|
||||
},
|
||||
image: {
|
||||
|
@ -54,6 +59,7 @@ export const getSettingsMock = (requirePageReload: boolean = false): Settings =>
|
|||
type: 'image',
|
||||
userValue: null,
|
||||
value: '',
|
||||
category: ['dashboard', 'discover'],
|
||||
...defaults,
|
||||
},
|
||||
number: {
|
||||
|
@ -62,6 +68,7 @@ export const getSettingsMock = (requirePageReload: boolean = false): Settings =>
|
|||
type: 'number',
|
||||
userValue: null,
|
||||
value: 1,
|
||||
category: ['dashboard', 'discover'],
|
||||
...defaults,
|
||||
},
|
||||
json: {
|
||||
|
@ -70,6 +77,7 @@ export const getSettingsMock = (requirePageReload: boolean = false): Settings =>
|
|||
type: 'json',
|
||||
userValue: null,
|
||||
value: '{"foo": "bar"}',
|
||||
category: ['dashboard', 'discover'],
|
||||
...defaults,
|
||||
},
|
||||
markdown: {
|
||||
|
@ -78,6 +86,7 @@ export const getSettingsMock = (requirePageReload: boolean = false): Settings =>
|
|||
type: 'markdown',
|
||||
userValue: null,
|
||||
value: '',
|
||||
category: ['notifications', 'search'],
|
||||
...defaults,
|
||||
},
|
||||
select: {
|
||||
|
@ -92,6 +101,7 @@ export const getSettingsMock = (requirePageReload: boolean = false): Settings =>
|
|||
type: 'select',
|
||||
userValue: null,
|
||||
value: 'apple',
|
||||
category: ['notifications', 'search'],
|
||||
...defaults,
|
||||
},
|
||||
string: {
|
||||
|
@ -100,6 +110,7 @@ export const getSettingsMock = (requirePageReload: boolean = false): Settings =>
|
|||
type: 'string',
|
||||
userValue: null,
|
||||
value: 'hello world',
|
||||
category: ['notifications', 'search'],
|
||||
...defaults,
|
||||
},
|
||||
undefined: {
|
||||
|
@ -108,6 +119,7 @@ export const getSettingsMock = (requirePageReload: boolean = false): Settings =>
|
|||
type: 'undefined',
|
||||
userValue: null,
|
||||
value: undefined,
|
||||
category: ['notifications', 'search'],
|
||||
...defaults,
|
||||
},
|
||||
};
|
|
@ -6,7 +6,7 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { SettingType, UiSettingMetadata, Value } from '@kbn/management-settings-types';
|
||||
import { UiSettingMetadata, Value } from '@kbn/management-settings-types';
|
||||
import isEqual from 'lodash/isEqual';
|
||||
|
||||
/**
|
||||
|
@ -17,7 +17,7 @@ import isEqual from 'lodash/isEqual';
|
|||
* @returns True if the provided value is equal to the setting's default value, false otherwise.
|
||||
*/
|
||||
export const isSettingDefaultValue = (
|
||||
setting: UiSettingMetadata<SettingType>,
|
||||
setting: UiSettingMetadata,
|
||||
userValue: Value = setting.userValue
|
||||
) => {
|
||||
const { value } = setting;
|
||||
|
|
|
@ -92,10 +92,8 @@ const deriveValue = (type: SettingType, value: unknown): Value => {
|
|||
* may be missing the `type` or `value` properties.
|
||||
* @returns A mapped collection of normalized {@link UiSetting} objects.
|
||||
*/
|
||||
export const normalizeSettings = (
|
||||
rawSettings: RawSettings
|
||||
): Record<string, UiSettingMetadata<SettingType>> => {
|
||||
const normalizedSettings: Record<string, UiSettingMetadata<SettingType>> = {};
|
||||
export const normalizeSettings = (rawSettings: RawSettings): Record<string, UiSettingMetadata> => {
|
||||
const normalizedSettings: Record<string, UiSettingMetadata> = {};
|
||||
|
||||
const entries = Object.entries(rawSettings);
|
||||
|
||||
|
|
|
@ -17,5 +17,6 @@
|
|||
],
|
||||
"kbn_references": [
|
||||
"@kbn/management-settings-types",
|
||||
"@kbn/i18n",
|
||||
]
|
||||
}
|
||||
|
|
|
@ -20,3 +20,19 @@ import jest from 'jest-mock';
|
|||
|
||||
/* @ts-expect-error TS doesn't see jest as a property of window, and I don't want to edit our global config. */
|
||||
window.jest = jest;
|
||||
|
||||
export const parameters = {
|
||||
backgrounds: {
|
||||
default: 'body',
|
||||
values: [
|
||||
{
|
||||
name: 'body',
|
||||
value: '##f7f8fc',
|
||||
},
|
||||
{
|
||||
name: 'ghost',
|
||||
value: '#fff',
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
|
|
@ -962,6 +962,8 @@
|
|||
"@kbn/management-cards-navigation/*": ["packages/kbn-management/cards_navigation/*"],
|
||||
"@kbn/management-plugin": ["src/plugins/management"],
|
||||
"@kbn/management-plugin/*": ["src/plugins/management/*"],
|
||||
"@kbn/management-settings-components-field-category": ["packages/kbn-management/settings/components/field_category"],
|
||||
"@kbn/management-settings-components-field-category/*": ["packages/kbn-management/settings/components/field_category/*"],
|
||||
"@kbn/management-settings-components-field-input": ["packages/kbn-management/settings/components/field_input"],
|
||||
"@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"],
|
||||
|
|
|
@ -4867,6 +4867,10 @@
|
|||
version "0.0.0"
|
||||
uid ""
|
||||
|
||||
"@kbn/management-settings-components-field-category@link:packages/kbn-management/settings/components/field_category":
|
||||
version "0.0.0"
|
||||
uid ""
|
||||
|
||||
"@kbn/management-settings-components-field-input@link:packages/kbn-management/settings/components/field_input":
|
||||
version "0.0.0"
|
||||
uid ""
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue