From 83816e8d70c4be498b99eb2cf966ee72c7e12e8a Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Wed, 9 Aug 2023 17:06:48 +0200 Subject: [PATCH 01/36] use zustand for state management --- package.json | 5 +- .../edit_transform_api_error_callout.tsx | 4 +- .../edit_transform_flyout_form_text_input.tsx | 7 +- .../edit_transform_ingest_pipeline.tsx | 7 +- .../edit_transform_retention_policy.tsx | 10 +- .../edit_transform_update_button.tsx | 10 +- .../use_edit_transform_flyout.tsx | 169 +++++++++--------- yarn.lock | 9 +- 8 files changed, 123 insertions(+), 98 deletions(-) diff --git a/package.json b/package.json index 1655f03ace40a..9e9d24d631fcd 100644 --- a/package.json +++ b/package.json @@ -1120,7 +1120,8 @@ "xterm": "^5.1.0", "yauzl": "^2.10.0", "yazl": "^2.5.1", - "zod": "^3.22.3" + "zod": "^3.22.3", + "zustand": "^4.4.1" }, "devDependencies": { "@apidevtools/swagger-parser": "^10.0.3", @@ -1671,4 +1672,4 @@ "yargs": "^15.4.1", "yarn-deduplicate": "^6.0.2" } -} \ No newline at end of file +} diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_api_error_callout.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_api_error_callout.tsx index 6713ab8ac530d..55688d93a3865 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_api_error_callout.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_api_error_callout.tsx @@ -11,10 +11,10 @@ import { EuiCallOut, EuiSpacer } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { useEditTransformFlyout } from './use_edit_transform_flyout'; +import { useEditTransformFlyoutFormState } from './use_edit_transform_flyout'; export const EditTransformApiErrorCallout: FC = () => { - const apiErrorMessage = useEditTransformFlyout('apiErrorMessage'); + const apiErrorMessage = useEditTransformFlyoutFormState('apiErrorMessage'); return ( <> diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_flyout_form_text_input.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_flyout_form_text_input.tsx index 9c93d286cb9c4..73719f56dec10 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_flyout_form_text_input.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_flyout_form_text_input.tsx @@ -13,12 +13,13 @@ import { i18n } from '@kbn/i18n'; import { useEditTransformFlyout, - type EditTransformHookTextInputSelectors, + useEditTransformFlyoutFormField, + type EditTransformFormFields, } from './use_edit_transform_flyout'; import { capitalizeFirstLetter } from './capitalize_first_letter'; interface EditTransformFlyoutFormTextInputProps { - field: EditTransformHookTextInputSelectors; + field: EditTransformFormFields; label: string; helpText?: string; placeHolder?: boolean; @@ -30,7 +31,7 @@ export const EditTransformFlyoutFormTextInput: FC { - const { defaultValue, errorMessages, value } = useEditTransformFlyout(field); + const { defaultValue, errorMessages, value } = useEditTransformFlyoutFormField(field); const { formField } = useEditTransformFlyout('actions'); const upperCaseField = capitalizeFirstLetter(field); diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_ingest_pipeline.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_ingest_pipeline.tsx index 519bdc94011e1..546a9edd89e8e 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_ingest_pipeline.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_ingest_pipeline.tsx @@ -14,7 +14,10 @@ import { i18n } from '@kbn/i18n'; import { useGetEsIngestPipelines } from '../../../../hooks'; import { EditTransformFlyoutFormTextInput } from './edit_transform_flyout_form_text_input'; -import { useEditTransformFlyout } from './use_edit_transform_flyout'; +import { + useEditTransformFlyout, + useEditTransformFlyoutFormField, +} from './use_edit_transform_flyout'; const ingestPipelineLabel = i18n.translate( 'xpack.transform.transformList.editFlyoutFormDestinationIngestPipelineLabel', @@ -25,7 +28,7 @@ const ingestPipelineLabel = i18n.translate( export const EditTransformIngestPipeline: FC = () => { const { euiTheme } = useEuiTheme(); - const { errorMessages, value } = useEditTransformFlyout('destinationIngestPipeline'); + const { errorMessages, value } = useEditTransformFlyoutFormField('destinationIngestPipeline'); const { formField } = useEditTransformFlyout('actions'); const { data: esIngestPipelinesData, isLoading } = useGetEsIngestPipelines(); diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_retention_policy.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_retention_policy.tsx index 162cde153b6e7..10775a0908ed5 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_retention_policy.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_retention_policy.tsx @@ -17,7 +17,11 @@ import { isLatestTransform, isPivotTransform } from '../../../../../../common/ty import { useGetTransformsPreview } from '../../../../hooks'; import { EditTransformFlyoutFormTextInput } from './edit_transform_flyout_form_text_input'; -import { useEditTransformFlyout } from './use_edit_transform_flyout'; +import { + useEditTransformFlyout, + useEditTransformFlyoutFormState, + useEditTransformFlyoutFormField, +} from './use_edit_transform_flyout'; import { getErrorMessage } from '../../../../../../common/utils/errors'; export const EditTransformRetentionPolicy: FC = () => { @@ -26,8 +30,8 @@ export const EditTransformRetentionPolicy: FC = () => { const toastNotifications = useToastNotifications(); const dataViewId = useEditTransformFlyout('dataViewId'); - const formSections = useEditTransformFlyout('stateFormSection'); - const retentionPolicyField = useEditTransformFlyout('retentionPolicyField'); + const formSections = useEditTransformFlyoutFormState('formSections'); + const retentionPolicyField = useEditTransformFlyoutFormField('retentionPolicyField'); const { formField, formSection } = useEditTransformFlyout('actions'); const requestConfig = useEditTransformFlyout('config'); diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_update_button.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_update_button.tsx index b55b6f90a0aa3..70698a55a92d7 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_update_button.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_update_button.tsx @@ -15,16 +15,20 @@ import { getErrorMessage } from '../../../../../../common/utils/errors'; import { useUpdateTransform } from '../../../../hooks'; -import { useEditTransformFlyout } from './use_edit_transform_flyout'; +import { + applyFormStateToTransformConfig, + useEditTransformFlyout, +} from './use_edit_transform_flyout'; interface EditTransformUpdateButtonProps { closeFlyout: () => void; } export const EditTransformUpdateButton: FC = ({ closeFlyout }) => { - const requestConfig = useEditTransformFlyout('requestConfig'); - const isUpdateButtonDisabled = useEditTransformFlyout('isUpdateButtonDisabled'); const config = useEditTransformFlyout('config'); + const formState = useEditTransformFlyout('formState'); + const requestConfig = applyFormStateToTransformConfig(config, formState); + const isUpdateButtonDisabled = !formState.isFormValid || !formState.isFormTouched; const { apiError } = useEditTransformFlyout('actions'); const updateTransfrom = useUpdateTransform(config.id, requestConfig); diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/use_edit_transform_flyout.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/use_edit_transform_flyout.tsx index 2e82edb54b6fd..64310f0cc4e0e 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/use_edit_transform_flyout.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/use_edit_transform_flyout.tsx @@ -5,12 +5,13 @@ * 2.0. */ -import constate from 'constate'; import { isEqual, merge } from 'lodash'; -import { useMemo, useReducer } from 'react'; +import React, { useContext, useRef, type FC } from 'react'; +import { createStore, useStore } from 'zustand'; import { getNestedProperty, setNestedProperty } from '@kbn/ml-nested-property'; +import { createContext } from 'react'; import { PostTransformsUpdateRequestSchema } from '../../../../../../common/api_schemas/update_transforms'; import { DEFAULT_TRANSFORM_FREQUENCY, @@ -147,7 +148,7 @@ export const initializeSection = ( }; }; -export interface EditTransformFlyoutState { +export interface EditTransformFlyoutFormState { apiErrorMessage?: string; formFields: EditTransformFlyoutFieldsState; formSections: EditTransformFlyoutSectionsState; @@ -182,7 +183,7 @@ type Action = ApiErrorAction | FormFieldAction | FormSectionAction; const getUpdateValue = ( attribute: EditTransformFormFields, config: TransformConfigUnion, - formState: EditTransformFlyoutState, + formState: EditTransformFlyoutFormState, enforceFormValue = false ) => { const { formFields, formSections } = formState; @@ -244,7 +245,7 @@ const getUpdateValue = ( // transform update API endpoint. export const applyFormStateToTransformConfig = ( config: TransformConfigUnion, - formState: EditTransformFlyoutState + formState: EditTransformFlyoutFormState ): PostTransformsUpdateRequestSchema => // Iterates over all form fields and only if necessary applies them to // the request object used for updating the transform. @@ -255,7 +256,7 @@ export const applyFormStateToTransformConfig = ( // Takes in a transform configuration and returns // the default state to populate the form. -export const getDefaultState = (config: TransformConfigUnion): EditTransformFlyoutState => ({ +export const getDefaultState = (config: TransformConfigUnion): EditTransformFlyoutFormState => ({ formFields: { // top level attributes description: initializeField('description', 'description', config), @@ -386,7 +387,7 @@ export const formReducerFactory = (config: TransformConfigUnion) => { const defaultFieldValues = getFieldValues(defaultState.formFields); const defaultSectionValues = getSectionValues(defaultState.formSections); - return (state: EditTransformFlyoutState, action: Action): EditTransformFlyoutState => { + return (state: EditTransformFlyoutFormState, action: Action): EditTransformFlyoutFormState => { const formFields = action.name === 'form_field' ? { @@ -427,88 +428,92 @@ interface EditTransformFlyoutOptions { dataViewId?: string; } -const useEditTransformFlyoutInternal = ({ config, dataViewId }: EditTransformFlyoutOptions) => { - const [formState, dispatch] = useReducer(formReducerFactory(config), getDefaultState(config)); - - const actions = useMemo( - () => ({ - apiError: (payload: ApiErrorAction['payload']) => dispatch({ name: 'api_error', payload }), - formField: (payload: FormFieldAction['payload']) => - dispatch({ - name: 'form_field', - payload, - }), - formSection: (payload: FormSectionAction['payload']) => - dispatch({ name: 'form_section', payload }), - }), - [] - ); +type EditTransformFlyoutStore = ReturnType; +type EditTransformFlyoutState = { + formState: EditTransformFlyoutFormState; + actions: { + apiError: (payload: ApiErrorAction['payload']) => void; + formField: (payload: FormFieldAction['payload']) => void; + formSection: (payload: FormSectionAction['payload']) => void; + }; +} & EditTransformFlyoutOptions; + +const createEditTransformFlyoutStore = ({ config, dataViewId }: EditTransformFlyoutOptions) => { + const reducer = formReducerFactory(config); + const initialState = getDefaultState(config); + + // const isUpdateButtonDisabled = !formState.isFormValid || !formState.isFormTouched; + + return createStore()((set) => ({ + config, + dataViewId, + formState: initialState, + actions: { + apiError: (payload) => + set((state) => ({ + ...state, + formState: reducer(state.formState, { name: 'api_error', payload }), + })), + formField: (payload) => + set((state) => ({ + ...state, + formState: reducer(state.formState, { + name: 'form_field', + payload, + }), + })), + formSection: (payload) => + set((state) => ({ + ...state, + formState: reducer(state.formState, { name: 'form_section', payload }), + })), + }, + })); +}; - const requestConfig = useMemo( - () => applyFormStateToTransformConfig(config, formState), - [config, formState] - ); +export const EditTransformFlyoutContext = createContext(null); - const isUpdateButtonDisabled = useMemo( - () => !formState.isFormValid || !formState.isFormTouched, - [formState.isFormValid, formState.isFormTouched] - ); +// Provider wrapper +type EditTransformFlyoutProviderProps = React.PropsWithChildren; - return { config, dataViewId, formState, actions, requestConfig, isUpdateButtonDisabled }; +export const EditTransformFlyoutProvider: FC = ({ + children, + ...props +}) => { + const storeRef = useRef(); + if (!storeRef.current) { + storeRef.current = createEditTransformFlyoutStore(props); + } + return ( + + {children} + + ); }; -// wrap hook with the constate factory to create context provider and custom hooks based on selectors -const [EditTransformFlyoutProvider, ...editTransformHooks] = constate( - useEditTransformFlyoutInternal, - (d) => d.config, - (d) => d.dataViewId, - (d) => d.actions, - (d) => d.formState.apiErrorMessage, - (d) => d.formState.formSections, - (d) => d.formState.formFields.description, - (d) => d.formState.formFields.destinationIndex, - (d) => d.formState.formFields.docsPerSecond, - (d) => d.formState.formFields.frequency, - (d) => d.formState.formFields.destinationIngestPipeline, - (d) => d.formState.formFields.maxPageSearchSize, - (d) => d.formState.formFields.numFailureRetries, - (d) => d.formState.formFields.retentionPolicyField, - (d) => d.formState.formFields.retentionPolicyMaxAge, - (d) => d.requestConfig, - (d) => d.isUpdateButtonDisabled -); - -export enum EDIT_TRANSFORM_HOOK_SELECTORS { - config, - dataViewId, - actions, - apiErrorMessage, - stateFormSection, - description, - destinationIndex, - docsPerSecond, - frequency, - destinationIngestPipeline, - maxPageSearchSize, - numFailureRetries, - retentionPolicyField, - retentionPolicyMaxAge, - requestConfig, - isUpdateButtonDisabled, -} +export const useEditTransformFlyout = ( + accessor: T +): EditTransformFlyoutState[T] => { + const store = useContext(EditTransformFlyoutContext); + if (!store) throw new Error('Missing EditTransformFlyoutContext.Provider in the tree'); -export type EditTransformHookTextInputSelectors = Extract< - keyof typeof EDIT_TRANSFORM_HOOK_SELECTORS, - EditTransformFormFields ->; + return useStore(store, (s) => s[accessor]); +}; -type EditTransformHookSelectors = keyof typeof EDIT_TRANSFORM_HOOK_SELECTORS; -type EditTransformHooks = typeof editTransformHooks; +export const useEditTransformFlyoutFormField = (field: EditTransformFormFields) => { + const store = useContext(EditTransformFlyoutContext); + if (!store) throw new Error('Missing EditTransformFlyoutContext.Provider in the tree'); -export const useEditTransformFlyout = (hookKey: K) => { - return editTransformHooks[EDIT_TRANSFORM_HOOK_SELECTORS[hookKey]]() as ReturnType< - EditTransformHooks[typeof EDIT_TRANSFORM_HOOK_SELECTORS[K]] - >; + return useStore(store, (s) => s.formState.formFields[field]); }; -export { EditTransformFlyoutProvider }; +export const useEditTransformFlyoutFormState = < + T extends keyof EditTransformFlyoutState['formState'] +>( + attr: T +) => { + const store = useContext(EditTransformFlyoutContext); + if (!store) throw new Error('Missing EditTransformFlyoutContext.Provider in the tree'); + + return useStore(store, (s) => s.formState[attr]); +}; diff --git a/yarn.lock b/yarn.lock index 048c16b9462c6..5312a57c8f3e1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -30352,7 +30352,7 @@ use-sidecar@^1.1.2: detect-node-es "^1.1.0" tslib "^2.0.0" -use-sync-external-store@^1.0.0, use-sync-external-store@^1.2.0: +use-sync-external-store@1.2.0, use-sync-external-store@^1.0.0, use-sync-external-store@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz#7dbefd6ef3fe4e767a0cf5d7287aacfb5846928a" integrity sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA== @@ -31918,6 +31918,13 @@ zod@^3.22.3: resolved "https://registry.yarnpkg.com/zod/-/zod-3.22.3.tgz#2fbc96118b174290d94e8896371c95629e87a060" integrity sha512-EjIevzuJRiRPbVH4mGc8nApb/lVLKVpmUhAaR5R5doKGfAnGJ6Gr3CViAVjP+4FWSxCsybeWQdcgCtbX+7oZug== +zustand@^4.4.1: + version "4.4.1" + resolved "https://registry.yarnpkg.com/zustand/-/zustand-4.4.1.tgz#0cd3a3e4756f21811bd956418fdc686877e8b3b0" + integrity sha512-QCPfstAS4EBiTQzlaGP1gmorkh/UL1Leaj2tdj+zZCZ/9bm0WS7sI2wnfD5lpOszFqWJ1DcPnGoY8RDL61uokw== + dependencies: + use-sync-external-store "1.2.0" + zwitch@^1.0.0: version "1.0.5" resolved "https://registry.yarnpkg.com/zwitch/-/zwitch-1.0.5.tgz#d11d7381ffed16b742f6af7b3f223d5cd9fe9920" From cb1e6dd5cc061b44013517fabac554ea53b62781 Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Wed, 9 Aug 2023 17:27:18 +0200 Subject: [PATCH 02/36] fix package.json --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 9e9d24d631fcd..052cddef9eab8 100644 --- a/package.json +++ b/package.json @@ -1672,4 +1672,4 @@ "yargs": "^15.4.1", "yarn-deduplicate": "^6.0.2" } -} +} \ No newline at end of file From 4f9cc86ef9666cf4eb325e83cacb184e05b18085 Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Wed, 9 Aug 2023 17:27:39 +0200 Subject: [PATCH 03/36] remove commented code --- .../edit_transform_flyout/use_edit_transform_flyout.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/use_edit_transform_flyout.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/use_edit_transform_flyout.tsx index 64310f0cc4e0e..50a798e8da25b 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/use_edit_transform_flyout.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/use_edit_transform_flyout.tsx @@ -442,8 +442,6 @@ const createEditTransformFlyoutStore = ({ config, dataViewId }: EditTransformFly const reducer = formReducerFactory(config); const initialState = getDefaultState(config); - // const isUpdateButtonDisabled = !formState.isFormValid || !formState.isFormTouched; - return createStore()((set) => ({ config, dataViewId, From 6d8ce6d5c28ff280630326d7a8c3e00596aa93b4 Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Thu, 10 Aug 2023 10:38:35 +0200 Subject: [PATCH 04/36] get rid of reducer and actions boilerplate --- .../edit_transform_api_error_callout.tsx | 4 +- .../edit_transform_flyout_form_text_input.tsx | 12 +- .../edit_transform_ingest_pipeline.tsx | 11 +- .../edit_transform_retention_policy.tsx | 17 +- .../edit_transform_update_button.tsx | 6 +- .../use_edit_transform_flyout.tsx | 227 ++++++------------ 6 files changed, 102 insertions(+), 175 deletions(-) diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_api_error_callout.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_api_error_callout.tsx index 55688d93a3865..415d14c2be6e2 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_api_error_callout.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_api_error_callout.tsx @@ -11,10 +11,10 @@ import { EuiCallOut, EuiSpacer } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { useEditTransformFlyoutFormState } from './use_edit_transform_flyout'; +import { useEditTransformFlyout } from './use_edit_transform_flyout'; export const EditTransformApiErrorCallout: FC = () => { - const apiErrorMessage = useEditTransformFlyoutFormState('apiErrorMessage'); + const apiErrorMessage = useEditTransformFlyout((s) => s.formState.apiErrorMessage); return ( <> diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_flyout_form_text_input.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_flyout_form_text_input.tsx index 73719f56dec10..84568711dd93a 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_flyout_form_text_input.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_flyout_form_text_input.tsx @@ -11,11 +11,7 @@ import { EuiFieldText, EuiFormRow } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { - useEditTransformFlyout, - useEditTransformFlyoutFormField, - type EditTransformFormFields, -} from './use_edit_transform_flyout'; +import { useEditTransformFlyout, type EditTransformFormFields } from './use_edit_transform_flyout'; import { capitalizeFirstLetter } from './capitalize_first_letter'; interface EditTransformFlyoutFormTextInputProps { @@ -31,8 +27,10 @@ export const EditTransformFlyoutFormTextInput: FC { - const { defaultValue, errorMessages, value } = useEditTransformFlyoutFormField(field); - const { formField } = useEditTransformFlyout('actions'); + const { defaultValue, errorMessages, value } = useEditTransformFlyout( + (s) => s.formState.formFields[field] + ); + const formField = useEditTransformFlyout((s) => s.actions.formField); const upperCaseField = capitalizeFirstLetter(field); return ( diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_ingest_pipeline.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_ingest_pipeline.tsx index 546a9edd89e8e..3db83b2dda2ef 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_ingest_pipeline.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_ingest_pipeline.tsx @@ -14,10 +14,7 @@ import { i18n } from '@kbn/i18n'; import { useGetEsIngestPipelines } from '../../../../hooks'; import { EditTransformFlyoutFormTextInput } from './edit_transform_flyout_form_text_input'; -import { - useEditTransformFlyout, - useEditTransformFlyoutFormField, -} from './use_edit_transform_flyout'; +import { useEditTransformFlyout } from './use_edit_transform_flyout'; const ingestPipelineLabel = i18n.translate( 'xpack.transform.transformList.editFlyoutFormDestinationIngestPipelineLabel', @@ -28,8 +25,10 @@ const ingestPipelineLabel = i18n.translate( export const EditTransformIngestPipeline: FC = () => { const { euiTheme } = useEuiTheme(); - const { errorMessages, value } = useEditTransformFlyoutFormField('destinationIngestPipeline'); - const { formField } = useEditTransformFlyout('actions'); + const { errorMessages, value } = useEditTransformFlyout( + (s) => s.formState.formFields.destinationIngestPipeline + ); + const formField = useEditTransformFlyout((s) => s.actions.formField); const { data: esIngestPipelinesData, isLoading } = useGetEsIngestPipelines(); const ingestPipelineNames = esIngestPipelinesData?.map(({ name }) => name) ?? []; diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_retention_policy.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_retention_policy.tsx index 10775a0908ed5..bf46486ccb393 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_retention_policy.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_retention_policy.tsx @@ -18,9 +18,8 @@ import { useGetTransformsPreview } from '../../../../hooks'; import { EditTransformFlyoutFormTextInput } from './edit_transform_flyout_form_text_input'; import { + applyFormStateToTransformConfig, useEditTransformFlyout, - useEditTransformFlyoutFormState, - useEditTransformFlyoutFormField, } from './use_edit_transform_flyout'; import { getErrorMessage } from '../../../../../../common/utils/errors'; @@ -29,11 +28,15 @@ export const EditTransformRetentionPolicy: FC = () => { const toastNotifications = useToastNotifications(); - const dataViewId = useEditTransformFlyout('dataViewId'); - const formSections = useEditTransformFlyoutFormState('formSections'); - const retentionPolicyField = useEditTransformFlyoutFormField('retentionPolicyField'); - const { formField, formSection } = useEditTransformFlyout('actions'); - const requestConfig = useEditTransformFlyout('config'); + const dataViewId = useEditTransformFlyout((s) => s.dataViewId); + const formSections = useEditTransformFlyout((s) => s.formState.formSections); + const retentionPolicyField = useEditTransformFlyout( + (s) => s.formState.formFields.retentionPolicyField + ); + const { formField, formSection } = useEditTransformFlyout((s) => s.actions); + const config = useEditTransformFlyout((s) => s.config); + const formState = useEditTransformFlyout((s) => s.formState); + const requestConfig = applyFormStateToTransformConfig(config, formState); const previewRequest = useMemo(() => { return { diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_update_button.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_update_button.tsx index 70698a55a92d7..8c4ed2549a221 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_update_button.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_update_button.tsx @@ -25,11 +25,11 @@ interface EditTransformUpdateButtonProps { } export const EditTransformUpdateButton: FC = ({ closeFlyout }) => { - const config = useEditTransformFlyout('config'); - const formState = useEditTransformFlyout('formState'); + const config = useEditTransformFlyout((s) => s.config); + const formState = useEditTransformFlyout((s) => s.formState); const requestConfig = applyFormStateToTransformConfig(config, formState); const isUpdateButtonDisabled = !formState.isFormValid || !formState.isFormTouched; - const { apiError } = useEditTransformFlyout('actions'); + const apiError = useEditTransformFlyout((s) => s.actions.apiError); const updateTransfrom = useUpdateTransform(config.id, requestConfig); diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/use_edit_transform_flyout.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/use_edit_transform_flyout.tsx index 50a798e8da25b..5f8d8a1978a20 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/use_edit_transform_flyout.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/use_edit_transform_flyout.tsx @@ -8,6 +8,7 @@ import { isEqual, merge } from 'lodash'; import React, { useContext, useRef, type FC } from 'react'; import { createStore, useStore } from 'zustand'; +import { produce } from 'immer'; import { getNestedProperty, setNestedProperty } from '@kbn/ml-nested-property'; @@ -35,14 +36,14 @@ import { type Validator, } from '../../../../common/validators'; -// This custom hook uses nested reducers to provide a generic framework to manage form state +// This custom hook uses `zustand` to provide a generic framework to manage form state // and apply it to a final possibly nested configuration object suitable for passing on // directly to an API call. For now this is only used for the transform edit form. // Once we apply the functionality to other places, e.g. the transform creation wizard, // the generic framework code in this file should be moved to a dedicated location. -// The outer most level reducer defines a flat structure of names for form fields. -// This is a flat structure regardless of whether the final request object will be nested. +// The form state defines a flat structure of names for form fields. +// This is a flat structure regardless of whether the final config object will be nested. // For example, `destinationIndex` and `destinationIngestPipeline` will later be nested under `dest`. export type EditTransformFormFields = | 'description' @@ -87,16 +88,13 @@ export interface FormSection { type EditTransformFlyoutSectionsState = Record; -// The reducers and utility functions in this file provide the following features: +// The utility functions in this file provide the following features: // - getDefaultState() // Sets up the initial form state. It supports overrides to apply a pre-existing configuration. // The implementation of this function is the only one that's specifically required to define // the features of the transform edit form. All other functions are generic and could be reused // in the future for other forms. // -// - formReducerFactory() / formFieldReducer() -// These nested reducers take care of updating and validating the form state. -// // - applyFormStateToTransformConfig() (iterates over getUpdateValue()) // Once a user hits the update button, these functions take care of extracting the information // necessary to create the update request. They take into account whether a field needs to @@ -105,7 +103,7 @@ type EditTransformFlyoutSectionsState = Record ({ formFields: { // top level attributes - description: initializeField('description', 'description', config), - frequency: initializeField('frequency', 'frequency', config, { + description: initializeFormField('description', 'description', config), + frequency: initializeFormField('frequency', 'frequency', config, { defaultValue: DEFAULT_TRANSFORM_FREQUENCY, validator: frequencyValidator, }), // dest.* - destinationIndex: initializeField('destinationIndex', 'dest.index', config, { + destinationIndex: initializeFormField('destinationIndex', 'dest.index', config, { dependsOn: ['destinationIngestPipeline'], isOptional: false, }), - destinationIngestPipeline: initializeField( + destinationIngestPipeline: initializeFormField( 'destinationIngestPipeline', 'dest.pipeline', config, @@ -281,13 +258,13 @@ export const getDefaultState = (config: TransformConfigUnion): EditTransformFlyo ), // settings.* - docsPerSecond: initializeField('docsPerSecond', 'settings.docs_per_second', config, { + docsPerSecond: initializeFormField('docsPerSecond', 'settings.docs_per_second', config, { isNullable: true, isOptional: true, validator: integerAboveZeroValidator, valueParser: (v) => (v === '' ? null : +v), }), - maxPageSearchSize: initializeField( + maxPageSearchSize: initializeFormField( 'maxPageSearchSize', 'settings.max_page_search_size', config, @@ -299,7 +276,7 @@ export const getDefaultState = (config: TransformConfigUnion): EditTransformFlyo valueParser: (v) => +v, } ), - numFailureRetries: initializeField( + numFailureRetries: initializeFormField( 'numFailureRetries', 'settings.num_failure_retries', config, @@ -313,7 +290,7 @@ export const getDefaultState = (config: TransformConfigUnion): EditTransformFlyo ), // retention_policy.* - retentionPolicyField: initializeField( + retentionPolicyField: initializeFormField( 'retentionPolicyField', 'retention_policy.time.field', config, @@ -324,7 +301,7 @@ export const getDefaultState = (config: TransformConfigUnion): EditTransformFlyo section: 'retentionPolicy', } ), - retentionPolicyMaxAge: initializeField( + retentionPolicyMaxAge: initializeFormField( 'retentionPolicyMaxAge', 'retention_policy.time.max_age', config, @@ -338,7 +315,7 @@ export const getDefaultState = (config: TransformConfigUnion): EditTransformFlyo ), }, formSections: { - retentionPolicy: initializeSection('retentionPolicy', 'retention_policy', config), + retentionPolicy: initializeFormSection('retentionPolicy', 'retention_policy', config), }, isFormTouched: false, isFormValid: true, @@ -353,76 +330,26 @@ const isFormValid = (fieldsState: EditTransformFlyoutFieldsState) => ); // Updates a form field with its new value, -// runs validation and populates -// `errorMessages` if any errors occur. -const formFieldReducer = (state: FormField, value: string): FormField => { - return { - ...state, - errorMessages: +// runs validation and populates `errorMessages` if any errors occur. +const updateFormField = (state: FormField, value: string): FormField => + produce(state, (d) => { + d.errorMessages = state.isOptional && typeof value === 'string' && value.length === 0 ? [] - : state.validator(value, state.isOptional), - value, - }; -}; + : state.validator(value, state.isOptional); + d.value = value; + }); -const formSectionReducer = (state: FormSection, enabled: boolean): FormSection => { - return { - ...state, - enabled, - }; -}; +const updateFormSection = (state: FormSection, enabled: boolean): FormSection => + produce(state, (d) => { + d.enabled = enabled; + }); const getFieldValues = (fields: EditTransformFlyoutFieldsState) => Object.values(fields).map((f) => f.value); const getSectionValues = (sections: EditTransformFlyoutSectionsState) => Object.values(sections).map((s) => s.enabled); -// Main form reducer triggers -// - `formFieldReducer` to update the actions field -// - compares the most recent state against the original one to update `isFormTouched` -// - sets `isFormValid` to have a flag if any of the form fields contains an error. -export const formReducerFactory = (config: TransformConfigUnion) => { - const defaultState = getDefaultState(config); - const defaultFieldValues = getFieldValues(defaultState.formFields); - const defaultSectionValues = getSectionValues(defaultState.formSections); - - return (state: EditTransformFlyoutFormState, action: Action): EditTransformFlyoutFormState => { - const formFields = - action.name === 'form_field' - ? { - ...state.formFields, - [action.payload.field]: formFieldReducer( - state.formFields[action.payload.field], - action.payload.value - ), - } - : state.formFields; - - const formSections = - action.name === 'form_section' - ? { - ...state.formSections, - [action.payload.section]: formSectionReducer( - state.formSections[action.payload.section], - action.payload.enabled - ), - } - : state.formSections; - - return { - ...state, - apiErrorMessage: action.name === 'api_error' ? action.payload : state.apiErrorMessage, - formFields, - formSections, - isFormTouched: - !isEqual(defaultFieldValues, getFieldValues(formFields)) || - !isEqual(defaultSectionValues, getSectionValues(formSections)), - isFormValid: isFormValid(formFields), - }; - }; -}; - interface EditTransformFlyoutOptions { config: TransformConfigUnion; dataViewId?: string; @@ -431,40 +358,50 @@ interface EditTransformFlyoutOptions { type EditTransformFlyoutStore = ReturnType; type EditTransformFlyoutState = { formState: EditTransformFlyoutFormState; - actions: { - apiError: (payload: ApiErrorAction['payload']) => void; - formField: (payload: FormFieldAction['payload']) => void; - formSection: (payload: FormSectionAction['payload']) => void; - }; + actions: Record void>; } & EditTransformFlyoutOptions; const createEditTransformFlyoutStore = ({ config, dataViewId }: EditTransformFlyoutOptions) => { - const reducer = formReducerFactory(config); - const initialState = getDefaultState(config); + const defaultState = getDefaultState(config); + const defaultFieldValues = getFieldValues(defaultState.formFields); + const defaultSectionValues = getSectionValues(defaultState.formSections); return createStore()((set) => ({ config, dataViewId, - formState: initialState, + formState: defaultState, actions: { - apiError: (payload) => - set((state) => ({ - ...state, - formState: reducer(state.formState, { name: 'api_error', payload }), - })), - formField: (payload) => - set((state) => ({ - ...state, - formState: reducer(state.formState, { - name: 'form_field', - payload, - }), - })), - formSection: (payload) => - set((state) => ({ - ...state, - formState: reducer(state.formState, { name: 'form_section', payload }), - })), + apiError: (payload: string | undefined) => + set((state) => + produce(state, (d) => { + d.formState.apiErrorMessage = payload; + }) + ), + formField: (payload: { field: EditTransformFormFields; value: string }) => + set((state) => + produce(state, (d) => { + d.formState.formFields[payload.field] = updateFormField( + state.formState.formFields[payload.field], + payload.value + ); + d.formState.isFormTouched = + !isEqual(defaultFieldValues, getFieldValues(d.formState.formFields)) || + !isEqual(defaultSectionValues, getSectionValues(d.formState.formSections)); + d.formState.isFormValid = isFormValid(d.formState.formFields); + }) + ), + formSection: (payload: { section: EditTransformFormSections; enabled: boolean }) => + set((state) => + produce(state, (d) => { + d.formState.formSections[payload.section] = updateFormSection( + state.formState.formSections[payload.section], + payload.enabled + ); + d.formState.isFormTouched = + !isEqual(defaultFieldValues, getFieldValues(d.formState.formFields)) || + !isEqual(defaultSectionValues, getSectionValues(d.formState.formSections)); + }) + ), }, })); }; @@ -489,29 +426,19 @@ export const EditTransformFlyoutProvider: FC = ); }; -export const useEditTransformFlyout = ( - accessor: T -): EditTransformFlyoutState[T] => { +// This approach with overloads is necessary for TS support of the selector callback. +// `zustand` TypeScript guide for reference: https://docs.pmnd.rs/zustand/guides/typescript#bounded-usestore-hook-for-vanilla-stores +export function useEditTransformFlyout(): EditTransformFlyoutState; +export function useEditTransformFlyout( + selector: (state: EditTransformFlyoutState) => T, + equals?: (a: T, b: T) => boolean +): T; +export function useEditTransformFlyout( + selector?: (state: EditTransformFlyoutState) => T, + equals?: (a: T, b: T) => boolean +) { const store = useContext(EditTransformFlyoutContext); if (!store) throw new Error('Missing EditTransformFlyoutContext.Provider in the tree'); - return useStore(store, (s) => s[accessor]); -}; - -export const useEditTransformFlyoutFormField = (field: EditTransformFormFields) => { - const store = useContext(EditTransformFlyoutContext); - if (!store) throw new Error('Missing EditTransformFlyoutContext.Provider in the tree'); - - return useStore(store, (s) => s.formState.formFields[field]); -}; - -export const useEditTransformFlyoutFormState = < - T extends keyof EditTransformFlyoutState['formState'] ->( - attr: T -) => { - const store = useContext(EditTransformFlyoutContext); - if (!store) throw new Error('Missing EditTransformFlyoutContext.Provider in the tree'); - - return useStore(store, (s) => s.formState[attr]); -}; + return useStore(store, selector!, equals); +} From ff8f29027beea8f1d2dcf63e0fb482ac829569b3 Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Thu, 10 Aug 2023 11:26:14 +0200 Subject: [PATCH 05/36] flatten state structure --- .../edit_transform_api_error_callout.tsx | 2 +- .../edit_transform_flyout_form_text_input.tsx | 4 +- .../edit_transform_ingest_pipeline.tsx | 2 +- .../edit_transform_retention_policy.tsx | 6 +- .../edit_transform_update_button.tsx | 5 +- ....ts => use_edit_transform_flyout.test.tsx} | 110 ++++++++++++------ .../use_edit_transform_flyout.tsx | 67 ++++++----- 7 files changed, 115 insertions(+), 81 deletions(-) rename x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/{use_edit_transform_flyout.test.ts => use_edit_transform_flyout.test.tsx} (70%) diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_api_error_callout.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_api_error_callout.tsx index 415d14c2be6e2..707a185a85a7d 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_api_error_callout.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_api_error_callout.tsx @@ -14,7 +14,7 @@ import { i18n } from '@kbn/i18n'; import { useEditTransformFlyout } from './use_edit_transform_flyout'; export const EditTransformApiErrorCallout: FC = () => { - const apiErrorMessage = useEditTransformFlyout((s) => s.formState.apiErrorMessage); + const apiErrorMessage = useEditTransformFlyout((s) => s.apiErrorMessage); return ( <> diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_flyout_form_text_input.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_flyout_form_text_input.tsx index 84568711dd93a..2732e82804e60 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_flyout_form_text_input.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_flyout_form_text_input.tsx @@ -27,9 +27,7 @@ export const EditTransformFlyoutFormTextInput: FC { - const { defaultValue, errorMessages, value } = useEditTransformFlyout( - (s) => s.formState.formFields[field] - ); + const { defaultValue, errorMessages, value } = useEditTransformFlyout((s) => s.formFields[field]); const formField = useEditTransformFlyout((s) => s.actions.formField); const upperCaseField = capitalizeFirstLetter(field); diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_ingest_pipeline.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_ingest_pipeline.tsx index 3db83b2dda2ef..3dbe207c5cd59 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_ingest_pipeline.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_ingest_pipeline.tsx @@ -26,7 +26,7 @@ const ingestPipelineLabel = i18n.translate( export const EditTransformIngestPipeline: FC = () => { const { euiTheme } = useEuiTheme(); const { errorMessages, value } = useEditTransformFlyout( - (s) => s.formState.formFields.destinationIngestPipeline + (s) => s.formFields.destinationIngestPipeline ); const formField = useEditTransformFlyout((s) => s.actions.formField); diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_retention_policy.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_retention_policy.tsx index bf46486ccb393..fb874e8a96d62 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_retention_policy.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_retention_policy.tsx @@ -29,10 +29,8 @@ export const EditTransformRetentionPolicy: FC = () => { const toastNotifications = useToastNotifications(); const dataViewId = useEditTransformFlyout((s) => s.dataViewId); - const formSections = useEditTransformFlyout((s) => s.formState.formSections); - const retentionPolicyField = useEditTransformFlyout( - (s) => s.formState.formFields.retentionPolicyField - ); + const formSections = useEditTransformFlyout((s) => s.formSections); + const retentionPolicyField = useEditTransformFlyout((s) => s.formFields.retentionPolicyField); const { formField, formSection } = useEditTransformFlyout((s) => s.actions); const config = useEditTransformFlyout((s) => s.config); const formState = useEditTransformFlyout((s) => s.formState); diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_update_button.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_update_button.tsx index 8c4ed2549a221..c475f3eec8d7a 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_update_button.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_update_button.tsx @@ -25,10 +25,9 @@ interface EditTransformUpdateButtonProps { } export const EditTransformUpdateButton: FC = ({ closeFlyout }) => { - const config = useEditTransformFlyout((s) => s.config); - const formState = useEditTransformFlyout((s) => s.formState); + const { config, formState, isFormValid, isFormTouched } = useEditTransformFlyout(); const requestConfig = applyFormStateToTransformConfig(config, formState); - const isUpdateButtonDisabled = !formState.isFormValid || !formState.isFormTouched; + const isUpdateButtonDisabled = !isFormValid || !isFormTouched; const apiError = useEditTransformFlyout((s) => s.actions.apiError); const updateTransfrom = useUpdateTransform(config.id, requestConfig); diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/use_edit_transform_flyout.test.ts b/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/use_edit_transform_flyout.test.tsx similarity index 70% rename from x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/use_edit_transform_flyout.test.ts rename to x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/use_edit_transform_flyout.test.tsx index ebea339c44300..ceea8ea96e173 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/use_edit_transform_flyout.test.ts +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/use_edit_transform_flyout.test.tsx @@ -5,12 +5,16 @@ * 2.0. */ +import React, { type FC } from 'react'; +import { act, renderHook } from '@testing-library/react-hooks'; + import { TransformPivotConfig } from '../../../../../../common/types/transform'; import { applyFormStateToTransformConfig, - formReducerFactory, getDefaultState, + useEditTransformFlyout, + EditTransformFlyoutProvider, } from './use_edit_transform_flyout'; const getTransformConfigMock = (): TransformPivotConfig => ({ @@ -47,9 +51,13 @@ describe('Transform: applyFormStateToTransformConfig()', () => { it('should exclude unchanged form fields', () => { const transformConfigMock = getTransformConfigMock(); - const formState = getDefaultState(transformConfigMock); + const { formFields, formSections } = getDefaultState(transformConfigMock); - const updateConfig = applyFormStateToTransformConfig(transformConfigMock, formState); + const updateConfig = applyFormStateToTransformConfig( + transformConfigMock, + formFields, + formSections + ); // This case will return an empty object. In the actual UI, this case should not happen // because the Update-Button will be disabled when no form field was changed. @@ -65,7 +73,7 @@ describe('Transform: applyFormStateToTransformConfig()', () => { it('should include previously nonexisting attributes', () => { const { description, frequency, ...transformConfigMock } = getTransformConfigMock(); - const formState = getDefaultState({ + const { formFields, formSections } = getDefaultState({ ...transformConfigMock, description: 'the-new-description', dest: { @@ -77,7 +85,11 @@ describe('Transform: applyFormStateToTransformConfig()', () => { }, }); - const updateConfig = applyFormStateToTransformConfig(transformConfigMock, formState); + const updateConfig = applyFormStateToTransformConfig( + transformConfigMock, + formFields, + formSections + ); expect(Object.keys(updateConfig)).toHaveLength(4); expect(updateConfig.description).toBe('the-new-description'); @@ -89,7 +101,7 @@ describe('Transform: applyFormStateToTransformConfig()', () => { it('should only include changed form fields', () => { const transformConfigMock = getTransformConfigMock(); - const formState = getDefaultState({ + const { formFields, formSections } = getDefaultState({ ...transformConfigMock, description: 'the-updated-description', dest: { @@ -98,7 +110,11 @@ describe('Transform: applyFormStateToTransformConfig()', () => { }, }); - const updateConfig = applyFormStateToTransformConfig(transformConfigMock, formState); + const updateConfig = applyFormStateToTransformConfig( + transformConfigMock, + formFields, + formSections + ); expect(Object.keys(updateConfig)).toHaveLength(2); expect(updateConfig.description).toBe('the-updated-description'); @@ -111,7 +127,7 @@ describe('Transform: applyFormStateToTransformConfig()', () => { it('should include dependent form fields', () => { const transformConfigMock = getTransformConfigMock(); - const formState = getDefaultState({ + const { formFields, formSections } = getDefaultState({ ...transformConfigMock, dest: { ...transformConfigMock.dest, @@ -119,7 +135,11 @@ describe('Transform: applyFormStateToTransformConfig()', () => { }, }); - const updateConfig = applyFormStateToTransformConfig(transformConfigMock, formState); + const updateConfig = applyFormStateToTransformConfig( + transformConfigMock, + formFields, + formSections + ); expect(Object.keys(updateConfig)).toHaveLength(1); // It should include the dependent unchanged destination index expect(updateConfig.dest?.index).toBe(transformConfigMock.dest.index); @@ -135,7 +155,7 @@ describe('Transform: applyFormStateToTransformConfig()', () => { }, }; - const formState = getDefaultState({ + const { formFields, formSections } = getDefaultState({ ...transformConfigMock, dest: { ...transformConfigMock.dest, @@ -143,7 +163,11 @@ describe('Transform: applyFormStateToTransformConfig()', () => { }, }); - const updateConfig = applyFormStateToTransformConfig(transformConfigMock, formState); + const updateConfig = applyFormStateToTransformConfig( + transformConfigMock, + formFields, + formSections + ); expect(Object.keys(updateConfig)).toHaveLength(1); // It should include the dependent unchanged destination index expect(updateConfig.dest?.index).toBe(transformConfigMock.dest.index); @@ -153,12 +177,16 @@ describe('Transform: applyFormStateToTransformConfig()', () => { it('should exclude unrelated dependent form fields', () => { const transformConfigMock = getTransformConfigMock(); - const formState = getDefaultState({ + const { formFields, formSections } = getDefaultState({ ...transformConfigMock, description: 'the-updated-description', }); - const updateConfig = applyFormStateToTransformConfig(transformConfigMock, formState); + const updateConfig = applyFormStateToTransformConfig( + transformConfigMock, + formFields, + formSections + ); expect(Object.keys(updateConfig)).toHaveLength(1); // It should exclude the dependent unchanged destination section expect(typeof updateConfig.dest).toBe('undefined'); @@ -168,16 +196,20 @@ describe('Transform: applyFormStateToTransformConfig()', () => { it('should return the config to reset retention policy', () => { const transformConfigMock = getTransformConfigMock(); - const formState = getDefaultState({ + const { formFields, formSections } = getDefaultState({ ...transformConfigMock, retention_policy: { time: { field: 'the-time-field', max_age: '1d' }, }, }); - formState.formSections.retentionPolicy.enabled = false; + formSections.retentionPolicy.enabled = false; - const updateConfig = applyFormStateToTransformConfig(transformConfigMock, formState); + const updateConfig = applyFormStateToTransformConfig( + transformConfigMock, + formFields, + formSections + ); expect(Object.keys(updateConfig)).toHaveLength(1); // It should exclude the dependent unchanged destination section @@ -186,44 +218,46 @@ describe('Transform: applyFormStateToTransformConfig()', () => { }); }); -describe('Transform: formReducerFactory()', () => { +describe('Transform: useEditTransformFlyout()', () => { it('field updates should trigger form validation', () => { const transformConfigMock = getTransformConfigMock(); - const reducer = formReducerFactory(transformConfigMock); - - const state1 = reducer(getDefaultState(transformConfigMock), { - name: 'form_field', - payload: { + const wrapper: FC = ({ children }) => ( + + {children} + + ); + const { result } = renderHook(() => useEditTransformFlyout(), { wrapper }); + + act(() => { + result.current.actions.formField({ field: 'description', value: 'the-updated-description', - }, + }); }); - expect(state1.isFormTouched).toBe(true); - expect(state1.isFormValid).toBe(true); + expect(result.current.isFormTouched).toBe(true); + expect(result.current.isFormValid).toBe(true); - const state2 = reducer(state1, { - name: 'form_field', - payload: { + act(() => { + result.current.actions.formField({ field: 'description', value: transformConfigMock.description as string, - }, + }); }); - expect(state2.isFormTouched).toBe(false); - expect(state2.isFormValid).toBe(true); + expect(result.current.isFormTouched).toBe(false); + expect(result.current.isFormValid).toBe(true); - const state3 = reducer(state2, { - name: 'form_field', - payload: { + act(() => { + result.current.actions.formField({ field: 'frequency', value: 'the-invalid-value', - }, + }); }); - expect(state3.isFormTouched).toBe(true); - expect(state3.isFormValid).toBe(false); - expect(state3.formFields.frequency.errorMessages).toStrictEqual([ + expect(result.current.isFormTouched).toBe(true); + expect(result.current.isFormValid).toBe(false); + expect(result.current.formFields.frequency.errorMessages).toStrictEqual([ 'The frequency value is not valid.', ]); }); diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/use_edit_transform_flyout.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/use_edit_transform_flyout.tsx index 5f8d8a1978a20..a4a47df3cc50d 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/use_edit_transform_flyout.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/use_edit_transform_flyout.tsx @@ -146,24 +146,16 @@ export const initializeFormSection = ( }; }; -export interface EditTransformFlyoutFormState { - apiErrorMessage?: string; - formFields: EditTransformFlyoutFieldsState; - formSections: EditTransformFlyoutSectionsState; - isFormTouched: boolean; - isFormValid: boolean; -} - // Takes a value from form state and applies it to the structure // of the expected final configuration request object. // Considers options like if a value is nullable or optional. const getUpdateValue = ( attribute: EditTransformFormFields, config: TransformConfigUnion, - formState: EditTransformFlyoutFormState, + formFields: EditTransformFlyoutFieldsState, + formSections: EditTransformFlyoutSectionsState, enforceFormValue = false ) => { - const { formFields, formSections } = formState; const formStateAttribute = formFields[attribute]; const fallbackValue = formStateAttribute.isNullable ? null : formStateAttribute.defaultValue; @@ -185,7 +177,7 @@ const getUpdateValue = ( ? formStateAttribute.dependsOn.reduce((_dependsOnConfig, dependsOnField) => { return merge( { ..._dependsOnConfig }, - getUpdateValue(dependsOnField, config, formState, true) + getUpdateValue(dependsOnField, config, formFields, formSections, true) ); }, {}) : {}; @@ -222,12 +214,14 @@ const getUpdateValue = ( // transform update API endpoint. export const applyFormStateToTransformConfig = ( config: TransformConfigUnion, - formState: EditTransformFlyoutFormState + formFields: EditTransformFlyoutFieldsState, + formSections: EditTransformFlyoutSectionsState ): PostTransformsUpdateRequestSchema => // Iterates over all form fields and only if necessary applies them to // the request object used for updating the transform. - (Object.keys(formState.formFields) as EditTransformFormFields[]).reduce( - (updateConfig, field) => merge({ ...updateConfig }, getUpdateValue(field, config, formState)), + (Object.keys(formFields) as EditTransformFormFields[]).reduce( + (updateConfig, field) => + merge({ ...updateConfig }, getUpdateValue(field, config, formFields, formSections)), {} ); @@ -355,51 +349,62 @@ interface EditTransformFlyoutOptions { dataViewId?: string; } -type EditTransformFlyoutStore = ReturnType; -type EditTransformFlyoutState = { - formState: EditTransformFlyoutFormState; +interface EditTransformFlyoutFormState { + apiErrorMessage?: string; + formFields: EditTransformFlyoutFieldsState; + formSections: EditTransformFlyoutSectionsState; + isFormTouched: boolean; + isFormValid: boolean; +} + +interface EditTransformFlyoutActions { actions: Record void>; -} & EditTransformFlyoutOptions; +} +type EditTransformFlyoutState = EditTransformFlyoutOptions & + EditTransformFlyoutFormState & + EditTransformFlyoutActions; + +type EditTransformFlyoutStore = ReturnType; const createEditTransformFlyoutStore = ({ config, dataViewId }: EditTransformFlyoutOptions) => { const defaultState = getDefaultState(config); const defaultFieldValues = getFieldValues(defaultState.formFields); const defaultSectionValues = getSectionValues(defaultState.formSections); return createStore()((set) => ({ + ...defaultState, config, dataViewId, - formState: defaultState, actions: { apiError: (payload: string | undefined) => set((state) => produce(state, (d) => { - d.formState.apiErrorMessage = payload; + d.apiErrorMessage = payload; }) ), formField: (payload: { field: EditTransformFormFields; value: string }) => set((state) => produce(state, (d) => { - d.formState.formFields[payload.field] = updateFormField( - state.formState.formFields[payload.field], + d.formFields[payload.field] = updateFormField( + state.formFields[payload.field], payload.value ); - d.formState.isFormTouched = - !isEqual(defaultFieldValues, getFieldValues(d.formState.formFields)) || - !isEqual(defaultSectionValues, getSectionValues(d.formState.formSections)); - d.formState.isFormValid = isFormValid(d.formState.formFields); + d.isFormTouched = + !isEqual(defaultFieldValues, getFieldValues(d.formFields)) || + !isEqual(defaultSectionValues, getSectionValues(d.formSections)); + d.isFormValid = isFormValid(d.formFields); }) ), formSection: (payload: { section: EditTransformFormSections; enabled: boolean }) => set((state) => produce(state, (d) => { - d.formState.formSections[payload.section] = updateFormSection( - state.formState.formSections[payload.section], + d.formSections[payload.section] = updateFormSection( + state.formSections[payload.section], payload.enabled ); - d.formState.isFormTouched = - !isEqual(defaultFieldValues, getFieldValues(d.formState.formFields)) || - !isEqual(defaultSectionValues, getSectionValues(d.formState.formSections)); + d.isFormTouched = + !isEqual(defaultFieldValues, getFieldValues(d.formFields)) || + !isEqual(defaultSectionValues, getSectionValues(d.formSections)); }) ), }, From 469d534b2327773f341ba0228e379faa317979db Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Thu, 10 Aug 2023 12:14:37 +0200 Subject: [PATCH 06/36] improve action handling --- .../edit_transform_flyout_form_text_input.tsx | 4 +- .../edit_transform_ingest_pipeline.tsx | 4 +- .../edit_transform_retention_policy.tsx | 7 +- .../edit_transform_update_button.tsx | 6 +- .../use_edit_transform_flyout.test.tsx | 6 +- .../use_edit_transform_flyout.tsx | 81 +++++++++---------- 6 files changed, 51 insertions(+), 57 deletions(-) diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_flyout_form_text_input.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_flyout_form_text_input.tsx index 2732e82804e60..7ae90674691d5 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_flyout_form_text_input.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_flyout_form_text_input.tsx @@ -28,7 +28,7 @@ export const EditTransformFlyoutFormTextInput: FC { const { defaultValue, errorMessages, value } = useEditTransformFlyout((s) => s.formFields[field]); - const formField = useEditTransformFlyout((s) => s.actions.formField); + const setFormField = useEditTransformFlyout((s) => s.setFormField); const upperCaseField = capitalizeFirstLetter(field); return ( @@ -50,7 +50,7 @@ export const EditTransformFlyoutFormTextInput: FC 0} value={value} - onChange={(e) => formField({ field, value: e.target.value })} + onChange={(e) => setFormField({ field, value: e.target.value })} aria-label={label} /> diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_ingest_pipeline.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_ingest_pipeline.tsx index 3dbe207c5cd59..974f9484a8d7a 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_ingest_pipeline.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_ingest_pipeline.tsx @@ -28,7 +28,7 @@ export const EditTransformIngestPipeline: FC = () => { const { errorMessages, value } = useEditTransformFlyout( (s) => s.formFields.destinationIngestPipeline ); - const formField = useEditTransformFlyout((s) => s.actions.formField); + const setFormField = useEditTransformFlyout((s) => s.setFormField); const { data: esIngestPipelinesData, isLoading } = useGetEsIngestPipelines(); const ingestPipelineNames = esIngestPipelinesData?.map(({ name }) => name) ?? []; @@ -68,7 +68,7 @@ export const EditTransformIngestPipeline: FC = () => { options={ingestPipelineNames.map((label: string) => ({ label }))} selectedOptions={[{ label: value }]} onChange={(o) => - formField({ field: 'destinationIngestPipeline', value: o[0]?.label ?? '' }) + setFormField({ field: 'destinationIngestPipeline', value: o[0]?.label ?? '' }) } /> diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_retention_policy.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_retention_policy.tsx index fb874e8a96d62..83ff6735ad44f 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_retention_policy.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_retention_policy.tsx @@ -31,10 +31,11 @@ export const EditTransformRetentionPolicy: FC = () => { const dataViewId = useEditTransformFlyout((s) => s.dataViewId); const formSections = useEditTransformFlyout((s) => s.formSections); const retentionPolicyField = useEditTransformFlyout((s) => s.formFields.retentionPolicyField); - const { formField, formSection } = useEditTransformFlyout((s) => s.actions); const config = useEditTransformFlyout((s) => s.config); const formState = useEditTransformFlyout((s) => s.formState); const requestConfig = applyFormStateToTransformConfig(config, formState); + const setFormField = useEditTransformFlyout((s) => s.setFormField); + const setFormSection = useEditTransformFlyout((s) => s.setFormSection); const previewRequest = useMemo(() => { return { @@ -103,7 +104,7 @@ export const EditTransformRetentionPolicy: FC = () => { )} checked={formSections.retentionPolicy.enabled} onChange={(e) => - formSection({ + setFormSection({ section: 'retentionPolicy', enabled: e.target.checked, }) @@ -147,7 +148,7 @@ export const EditTransformRetentionPolicy: FC = () => { options={retentionDateFieldOptions} value={retentionPolicyField.value} onChange={(e) => - formField({ field: 'retentionPolicyField', value: e.target.value }) + setFormField({ field: 'retentionPolicyField', value: e.target.value }) } hasNoInitialSelection={ !retentionDateFieldOptions diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_update_button.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_update_button.tsx index c475f3eec8d7a..8ad620ca5d9ce 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_update_button.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_update_button.tsx @@ -28,15 +28,15 @@ export const EditTransformUpdateButton: FC = ({ const { config, formState, isFormValid, isFormTouched } = useEditTransformFlyout(); const requestConfig = applyFormStateToTransformConfig(config, formState); const isUpdateButtonDisabled = !isFormValid || !isFormTouched; - const apiError = useEditTransformFlyout((s) => s.actions.apiError); + const setApiError = useEditTransformFlyout((s) => s.setApiError); const updateTransfrom = useUpdateTransform(config.id, requestConfig); async function submitFormHandler() { - apiError(undefined); + setApiError(undefined); updateTransfrom(undefined, { - onError: (error) => apiError(getErrorMessage(error)), + onError: (error) => setApiError(getErrorMessage(error)), onSuccess: () => closeFlyout(), }); } diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/use_edit_transform_flyout.test.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/use_edit_transform_flyout.test.tsx index ceea8ea96e173..30e6e6ed7eefc 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/use_edit_transform_flyout.test.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/use_edit_transform_flyout.test.tsx @@ -229,7 +229,7 @@ describe('Transform: useEditTransformFlyout()', () => { const { result } = renderHook(() => useEditTransformFlyout(), { wrapper }); act(() => { - result.current.actions.formField({ + result.current.setFormField({ field: 'description', value: 'the-updated-description', }); @@ -239,7 +239,7 @@ describe('Transform: useEditTransformFlyout()', () => { expect(result.current.isFormValid).toBe(true); act(() => { - result.current.actions.formField({ + result.current.setFormField({ field: 'description', value: transformConfigMock.description as string, }); @@ -249,7 +249,7 @@ describe('Transform: useEditTransformFlyout()', () => { expect(result.current.isFormValid).toBe(true); act(() => { - result.current.actions.formField({ + result.current.setFormField({ field: 'frequency', value: 'the-invalid-value', }); diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/use_edit_transform_flyout.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/use_edit_transform_flyout.tsx index a4a47df3cc50d..52eaf9796c76a 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/use_edit_transform_flyout.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/use_edit_transform_flyout.tsx @@ -334,11 +334,6 @@ const updateFormField = (state: FormField, value: string): FormField => d.value = value; }); -const updateFormSection = (state: FormSection, enabled: boolean): FormSection => - produce(state, (d) => { - d.enabled = enabled; - }); - const getFieldValues = (fields: EditTransformFlyoutFieldsState) => Object.values(fields).map((f) => f.value); const getSectionValues = (sections: EditTransformFlyoutSectionsState) => @@ -358,7 +353,9 @@ interface EditTransformFlyoutFormState { } interface EditTransformFlyoutActions { - actions: Record void>; + setApiError: (payload: string | undefined) => void; + setFormField: (payload: { field: EditTransformFormFields; value: string }) => void; + setFormSection: (payload: { section: EditTransformFormSections; enabled: boolean }) => void; } type EditTransformFlyoutState = EditTransformFlyoutOptions & @@ -371,44 +368,40 @@ const createEditTransformFlyoutStore = ({ config, dataViewId }: EditTransformFly const defaultFieldValues = getFieldValues(defaultState.formFields); const defaultSectionValues = getSectionValues(defaultState.formSections); - return createStore()((set) => ({ - ...defaultState, - config, - dataViewId, - actions: { - apiError: (payload: string | undefined) => - set((state) => - produce(state, (d) => { - d.apiErrorMessage = payload; - }) - ), - formField: (payload: { field: EditTransformFormFields; value: string }) => - set((state) => - produce(state, (d) => { - d.formFields[payload.field] = updateFormField( - state.formFields[payload.field], - payload.value - ); - d.isFormTouched = - !isEqual(defaultFieldValues, getFieldValues(d.formFields)) || - !isEqual(defaultSectionValues, getSectionValues(d.formSections)); - d.isFormValid = isFormValid(d.formFields); - }) - ), - formSection: (payload: { section: EditTransformFormSections; enabled: boolean }) => - set((state) => - produce(state, (d) => { - d.formSections[payload.section] = updateFormSection( - state.formSections[payload.section], - payload.enabled - ); - d.isFormTouched = - !isEqual(defaultFieldValues, getFieldValues(d.formFields)) || - !isEqual(defaultSectionValues, getSectionValues(d.formSections)); - }) - ), - }, - })); + return createStore()((set) => { + // a helper to wrap a callback in both zustand's set() and immer's produce() + const createAction = (cb: (d: EditTransformFlyoutState) => void) => + set((state) => produce(state, cb)); + + const isFormTouched = (d: EditTransformFlyoutState) => + !isEqual(defaultFieldValues, getFieldValues(d.formFields)) || + !isEqual(defaultSectionValues, getSectionValues(d.formSections)); + + return { + ...defaultState, + config, + dataViewId, + setApiError: (payload) => + createAction((d) => { + d.apiErrorMessage = payload; + }), + // Updates a form field with its new value, runs validation and + // populates `errorMessages` if any errors occur. + setFormField: (payload) => + createAction((d) => { + d.formFields[payload.field] = updateFormField(d.formFields[payload.field], payload.value); + d.isFormTouched = isFormTouched(d); + d.isFormValid = isFormValid(d.formFields); + }), + // Updates a form section. + setFormSection: (payload) => + createAction((d) => { + d.formSections[payload.section].enabled = payload.enabled; + d.isFormTouched = isFormTouched(d); + d.isFormValid = isFormValid(d.formFields); + }), + }; + }); }; export const EditTransformFlyoutContext = createContext(null); From 90d6a422d8500013a90dbc5ae8f774ebd436ccf2 Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Thu, 10 Aug 2023 15:31:38 +0200 Subject: [PATCH 07/36] refactor validators --- .../public/app/common/validators/frequency_validator.test.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/x-pack/plugins/transform/public/app/common/validators/frequency_validator.test.ts b/x-pack/plugins/transform/public/app/common/validators/frequency_validator.test.ts index 1ebdd3d41cd57..ff75231e82ade 100644 --- a/x-pack/plugins/transform/public/app/common/validators/frequency_validator.test.ts +++ b/x-pack/plugins/transform/public/app/common/validators/frequency_validator.test.ts @@ -8,9 +8,6 @@ import { frequencyValidator } from './frequency_validator'; describe('Transform: frequencyValidator()', () => { - // frequencyValidator() returns an array of error messages so - // an array with a length of 0 means a successful validation. - it('should fail when the input is not an integer and valid time unit.', () => { expect(frequencyValidator('0')).toEqual(['The frequency value is not valid.']); expect(frequencyValidator('0.1s')).toEqual(['The frequency value is not valid.']); From 03965dfeff933d881ad2835a7637f250af191d71 Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Thu, 10 Aug 2023 16:43:13 +0200 Subject: [PATCH 08/36] refactor parsers --- .../use_edit_transform_flyout.tsx | 44 +++++++++++-------- 1 file changed, 26 insertions(+), 18 deletions(-) diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/use_edit_transform_flyout.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/use_edit_transform_flyout.tsx index 52eaf9796c76a..5371d1cd1e6f7 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/use_edit_transform_flyout.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/use_edit_transform_flyout.tsx @@ -36,6 +36,15 @@ import { type Validator, } from '../../../../common/validators'; +type DefaultParser = (v: string) => string; +type NullableNumberParser = (v: string) => number | null; +type NumberParser = (v: string) => number; +type ValueParser = DefaultParser | NullableNumberParser | NumberParser; + +const defaultParser: DefaultParser = (v) => v; +const nullableNumberParser: NullableNumberParser = (v) => (v === '' ? null : +v); +const numberParser: NumberParser = (v) => +v; + // This custom hook uses `zustand` to provide a generic framework to manage form state // and apply it to a final possibly nested configuration object suitable for passing on // directly to an API call. For now this is only used for the transform edit form. @@ -69,7 +78,7 @@ export interface FormField { section?: EditTransformFormSections; validator: Validator; value: string; - valueParser: (value: string) => any; + valueParser: ValueParser; } // Defining these sections is only necessary for options where a reset/deletion of that part of the @@ -123,7 +132,7 @@ export const initializeFormField = ( isOptional: true, validator: stringValidator, value, - valueParser: (v) => v, + valueParser: defaultParser, ...(overloads !== undefined ? { ...overloads } : {}), }; }; @@ -256,7 +265,7 @@ export const getDefaultState = (config: TransformConfigUnion): EditTransformFlyo isNullable: true, isOptional: true, validator: integerAboveZeroValidator, - valueParser: (v) => (v === '' ? null : +v), + valueParser: nullableNumberParser, }), maxPageSearchSize: initializeFormField( 'maxPageSearchSize', @@ -267,7 +276,7 @@ export const getDefaultState = (config: TransformConfigUnion): EditTransformFlyo isNullable: true, isOptional: true, validator: transformSettingsPageSearchSizeValidator, - valueParser: (v) => +v, + valueParser: numberParser, } ), numFailureRetries: initializeFormField( @@ -279,7 +288,7 @@ export const getDefaultState = (config: TransformConfigUnion): EditTransformFlyo isNullable: true, isOptional: true, validator: transformSettingsNumberOfRetriesValidator, - valueParser: (v) => +v, + valueParser: numberParser, } ), @@ -318,10 +327,7 @@ export const getDefaultState = (config: TransformConfigUnion): EditTransformFlyo // Checks each form field for error messages to return // if the overall form is valid or not. const isFormValid = (fieldsState: EditTransformFlyoutFieldsState) => - (Object.keys(fieldsState) as EditTransformFormFields[]).reduce( - (p, c) => p && fieldsState[c].errorMessages.length === 0, - true - ); + Object.values(fieldsState).every((d) => d.errorMessages.length === 0); // Updates a form field with its new value, // runs validation and populates `errorMessages` if any errors occur. @@ -339,7 +345,7 @@ const getFieldValues = (fields: EditTransformFlyoutFieldsState) => const getSectionValues = (sections: EditTransformFlyoutSectionsState) => Object.values(sections).map((s) => s.enabled); -interface EditTransformFlyoutOptions { +interface EditTransformFlyoutProviderProps { config: TransformConfigUnion; dataViewId?: string; } @@ -358,12 +364,17 @@ interface EditTransformFlyoutActions { setFormSection: (payload: { section: EditTransformFormSections; enabled: boolean }) => void; } -type EditTransformFlyoutState = EditTransformFlyoutOptions & +// The state we manage via zustand combines the provider props, +// the form state and the zustand actions. +type EditTransformFlyoutState = EditTransformFlyoutProviderProps & EditTransformFlyoutFormState & EditTransformFlyoutActions; type EditTransformFlyoutStore = ReturnType; -const createEditTransformFlyoutStore = ({ config, dataViewId }: EditTransformFlyoutOptions) => { +const createEditTransformFlyoutStore = ({ + config, + dataViewId, +}: EditTransformFlyoutProviderProps) => { const defaultState = getDefaultState(config); const defaultFieldValues = getFieldValues(defaultState.formFields); const defaultSectionValues = getSectionValues(defaultState.formSections); @@ -407,12 +418,9 @@ const createEditTransformFlyoutStore = ({ config, dataViewId }: EditTransformFly export const EditTransformFlyoutContext = createContext(null); // Provider wrapper -type EditTransformFlyoutProviderProps = React.PropsWithChildren; - -export const EditTransformFlyoutProvider: FC = ({ - children, - ...props -}) => { +export const EditTransformFlyoutProvider: FC< + React.PropsWithChildren +> = ({ children, ...props }) => { const storeRef = useRef(); if (!storeRef.current) { storeRef.current = createEditTransformFlyoutStore(props); From 5d97178bf0e0cda3c7bbe1d448fca765551ab096 Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Wed, 20 Dec 2023 14:07:54 +0100 Subject: [PATCH 09/36] fix types and state management. --- .../edit_transform_flyout_form_text_area.tsx | 13 +++++-------- .../edit_transform_retention_policy.tsx | 11 ++++++----- .../edit_transform_update_button.tsx | 4 ++-- 3 files changed, 13 insertions(+), 15 deletions(-) diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_flyout_form_text_area.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_flyout_form_text_area.tsx index b4e5470ae7e00..8db860c902f1a 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_flyout_form_text_area.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_flyout_form_text_area.tsx @@ -11,14 +11,11 @@ import { EuiFormRow, EuiTextArea } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { - useEditTransformFlyout, - type EditTransformHookTextInputSelectors, -} from './use_edit_transform_flyout'; +import { useEditTransformFlyout, type EditTransformFormFields } from './use_edit_transform_flyout'; import { capitalizeFirstLetter } from './capitalize_first_letter'; interface EditTransformFlyoutFormTextInputProps { - field: EditTransformHookTextInputSelectors; + field: EditTransformFormFields; label: string; helpText?: string; placeHolder?: boolean; @@ -30,8 +27,8 @@ export const EditTransformFlyoutFormTextArea: FC { - const { defaultValue, errorMessages, value } = useEditTransformFlyout(field); - const { formField } = useEditTransformFlyout('actions'); + const { defaultValue, errorMessages, value } = useEditTransformFlyout((s) => s.formFields[field]); + const setFormField = useEditTransformFlyout((s) => s.setFormField); const upperCaseField = capitalizeFirstLetter(field); return ( @@ -53,7 +50,7 @@ export const EditTransformFlyoutFormTextArea: FC 0} value={value} - onChange={(e) => formField({ field, value: e.target.value })} + onChange={(e) => setFormField({ field, value: e.target.value })} aria-label={label} /> diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_retention_policy.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_retention_policy.tsx index 83ff6735ad44f..7f802e553c18a 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_retention_policy.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_retention_policy.tsx @@ -13,6 +13,7 @@ import { EuiFormRow, EuiSelect, EuiSpacer, EuiSwitch } from '@elastic/eui'; import { toMountPoint } from '@kbn/react-kibana-mount'; import { useAppDependencies, useToastNotifications } from '../../../../app_dependencies'; import { ToastNotificationText } from '../../../../components'; +import type { PostTransformsPreviewRequestSchema } from '../../../../../../common/api_schemas/transforms'; import { isLatestTransform, isPivotTransform } from '../../../../../../common/types/transform'; import { useGetTransformsPreview } from '../../../../hooks'; @@ -29,21 +30,21 @@ export const EditTransformRetentionPolicy: FC = () => { const toastNotifications = useToastNotifications(); const dataViewId = useEditTransformFlyout((s) => s.dataViewId); + const formFields = useEditTransformFlyout((s) => s.formFields); const formSections = useEditTransformFlyout((s) => s.formSections); const retentionPolicyField = useEditTransformFlyout((s) => s.formFields.retentionPolicyField); const config = useEditTransformFlyout((s) => s.config); - const formState = useEditTransformFlyout((s) => s.formState); - const requestConfig = applyFormStateToTransformConfig(config, formState); + const requestConfig = applyFormStateToTransformConfig(config, formFields, formSections); const setFormField = useEditTransformFlyout((s) => s.setFormField); const setFormSection = useEditTransformFlyout((s) => s.setFormSection); - const previewRequest = useMemo(() => { + const previewRequest: PostTransformsPreviewRequestSchema = useMemo(() => { return { - source: requestConfig.source, + source: config.source, ...(isPivotTransform(requestConfig) ? { pivot: requestConfig.pivot } : {}), ...(isLatestTransform(requestConfig) ? { latest: requestConfig.latest } : {}), }; - }, [requestConfig]); + }, [config, requestConfig]); const { error: transformsPreviewError, data: transformPreview } = useGetTransformsPreview(previewRequest); diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_update_button.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_update_button.tsx index 8ad620ca5d9ce..b96d387b28e79 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_update_button.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_update_button.tsx @@ -25,8 +25,8 @@ interface EditTransformUpdateButtonProps { } export const EditTransformUpdateButton: FC = ({ closeFlyout }) => { - const { config, formState, isFormValid, isFormTouched } = useEditTransformFlyout(); - const requestConfig = applyFormStateToTransformConfig(config, formState); + const { config, formFields, formSections, isFormValid, isFormTouched } = useEditTransformFlyout(); + const requestConfig = applyFormStateToTransformConfig(config, formFields, formSections); const isUpdateButtonDisabled = !isFormValid || !isFormTouched; const setApiError = useEditTransformFlyout((s) => s.setApiError); From 66183415b14b42b031af12bb6f4b824bca0b3a11 Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Wed, 20 Dec 2023 16:25:51 +0100 Subject: [PATCH 10/36] fix preview request body --- .../edit_transform_retention_policy.tsx | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_retention_policy.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_retention_policy.tsx index 7f802e553c18a..684f67f6d319a 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_retention_policy.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_retention_policy.tsx @@ -18,10 +18,7 @@ import { isLatestTransform, isPivotTransform } from '../../../../../../common/ty import { useGetTransformsPreview } from '../../../../hooks'; import { EditTransformFlyoutFormTextInput } from './edit_transform_flyout_form_text_input'; -import { - applyFormStateToTransformConfig, - useEditTransformFlyout, -} from './use_edit_transform_flyout'; +import { useEditTransformFlyout } from './use_edit_transform_flyout'; import { getErrorMessage } from '../../../../../../common/utils/errors'; export const EditTransformRetentionPolicy: FC = () => { @@ -30,21 +27,19 @@ export const EditTransformRetentionPolicy: FC = () => { const toastNotifications = useToastNotifications(); const dataViewId = useEditTransformFlyout((s) => s.dataViewId); - const formFields = useEditTransformFlyout((s) => s.formFields); const formSections = useEditTransformFlyout((s) => s.formSections); const retentionPolicyField = useEditTransformFlyout((s) => s.formFields.retentionPolicyField); const config = useEditTransformFlyout((s) => s.config); - const requestConfig = applyFormStateToTransformConfig(config, formFields, formSections); const setFormField = useEditTransformFlyout((s) => s.setFormField); const setFormSection = useEditTransformFlyout((s) => s.setFormSection); const previewRequest: PostTransformsPreviewRequestSchema = useMemo(() => { return { source: config.source, - ...(isPivotTransform(requestConfig) ? { pivot: requestConfig.pivot } : {}), - ...(isLatestTransform(requestConfig) ? { latest: requestConfig.latest } : {}), + ...(isPivotTransform(config) ? { pivot: config.pivot } : {}), + ...(isLatestTransform(config) ? { latest: config.latest } : {}), }; - }, [config, requestConfig]); + }, [config]); const { error: transformsPreviewError, data: transformPreview } = useGetTransformsPreview(previewRequest); From 50f0f4f85253be8d089620fe01bddd1920839ca3 Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Thu, 21 Dec 2023 15:47:49 +0100 Subject: [PATCH 11/36] use redux toolkit for edit transform flyout state management --- .../edit_transform_api_error_callout.tsx | 4 +- .../edit_transform_flyout_form_text_area.tsx | 12 +- .../edit_transform_flyout_form_text_input.tsx | 12 +- .../edit_transform_ingest_pipeline.tsx | 9 +- .../edit_transform_retention_policy.tsx | 18 ++- .../edit_transform_update_button.tsx | 9 +- .../use_edit_transform_flyout.tsx | 153 +++++++++--------- 7 files changed, 117 insertions(+), 100 deletions(-) diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_api_error_callout.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_api_error_callout.tsx index 707a185a85a7d..9b802dd67f123 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_api_error_callout.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_api_error_callout.tsx @@ -11,10 +11,10 @@ import { EuiCallOut, EuiSpacer } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { useEditTransformFlyout } from './use_edit_transform_flyout'; +import { useEditTransformFlyoutSelector } from './use_edit_transform_flyout'; export const EditTransformApiErrorCallout: FC = () => { - const apiErrorMessage = useEditTransformFlyout((s) => s.apiErrorMessage); + const apiErrorMessage = useEditTransformFlyoutSelector((s) => s.apiErrorMessage); return ( <> diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_flyout_form_text_area.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_flyout_form_text_area.tsx index 8db860c902f1a..7df0edc17c343 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_flyout_form_text_area.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_flyout_form_text_area.tsx @@ -11,7 +11,11 @@ import { EuiFormRow, EuiTextArea } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { useEditTransformFlyout, type EditTransformFormFields } from './use_edit_transform_flyout'; +import { + useEditTransformFlyoutActions, + useEditTransformFlyoutSelector, + type EditTransformFormFields, +} from './use_edit_transform_flyout'; import { capitalizeFirstLetter } from './capitalize_first_letter'; interface EditTransformFlyoutFormTextInputProps { @@ -27,8 +31,10 @@ export const EditTransformFlyoutFormTextArea: FC { - const { defaultValue, errorMessages, value } = useEditTransformFlyout((s) => s.formFields[field]); - const setFormField = useEditTransformFlyout((s) => s.setFormField); + const { defaultValue, errorMessages, value } = useEditTransformFlyoutSelector( + (s) => s.formFields[field] + ); + const { setFormField } = useEditTransformFlyoutActions(); const upperCaseField = capitalizeFirstLetter(field); return ( diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_flyout_form_text_input.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_flyout_form_text_input.tsx index 7ae90674691d5..c177b4ebb690e 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_flyout_form_text_input.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_flyout_form_text_input.tsx @@ -11,7 +11,11 @@ import { EuiFieldText, EuiFormRow } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { useEditTransformFlyout, type EditTransformFormFields } from './use_edit_transform_flyout'; +import { + useEditTransformFlyoutActions, + useEditTransformFlyoutSelector, + type EditTransformFormFields, +} from './use_edit_transform_flyout'; import { capitalizeFirstLetter } from './capitalize_first_letter'; interface EditTransformFlyoutFormTextInputProps { @@ -27,8 +31,10 @@ export const EditTransformFlyoutFormTextInput: FC { - const { defaultValue, errorMessages, value } = useEditTransformFlyout((s) => s.formFields[field]); - const setFormField = useEditTransformFlyout((s) => s.setFormField); + const { defaultValue, errorMessages, value } = useEditTransformFlyoutSelector( + (s) => s.formFields[field] + ); + const { setFormField } = useEditTransformFlyoutActions(); const upperCaseField = capitalizeFirstLetter(field); return ( diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_ingest_pipeline.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_ingest_pipeline.tsx index 974f9484a8d7a..ae79954188266 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_ingest_pipeline.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_ingest_pipeline.tsx @@ -14,7 +14,10 @@ import { i18n } from '@kbn/i18n'; import { useGetEsIngestPipelines } from '../../../../hooks'; import { EditTransformFlyoutFormTextInput } from './edit_transform_flyout_form_text_input'; -import { useEditTransformFlyout } from './use_edit_transform_flyout'; +import { + useEditTransformFlyoutActions, + useEditTransformFlyoutSelector, +} from './use_edit_transform_flyout'; const ingestPipelineLabel = i18n.translate( 'xpack.transform.transformList.editFlyoutFormDestinationIngestPipelineLabel', @@ -25,10 +28,10 @@ const ingestPipelineLabel = i18n.translate( export const EditTransformIngestPipeline: FC = () => { const { euiTheme } = useEuiTheme(); - const { errorMessages, value } = useEditTransformFlyout( + const { errorMessages, value } = useEditTransformFlyoutSelector( (s) => s.formFields.destinationIngestPipeline ); - const setFormField = useEditTransformFlyout((s) => s.setFormField); + const { setFormField } = useEditTransformFlyoutActions(); const { data: esIngestPipelinesData, isLoading } = useGetEsIngestPipelines(); const ingestPipelineNames = esIngestPipelinesData?.map(({ name }) => name) ?? []; diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_retention_policy.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_retention_policy.tsx index 684f67f6d319a..7bddc0975bf04 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_retention_policy.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_retention_policy.tsx @@ -18,7 +18,10 @@ import { isLatestTransform, isPivotTransform } from '../../../../../../common/ty import { useGetTransformsPreview } from '../../../../hooks'; import { EditTransformFlyoutFormTextInput } from './edit_transform_flyout_form_text_input'; -import { useEditTransformFlyout } from './use_edit_transform_flyout'; +import { + useEditTransformFlyoutActions, + useEditTransformFlyoutSelector, +} from './use_edit_transform_flyout'; import { getErrorMessage } from '../../../../../../common/utils/errors'; export const EditTransformRetentionPolicy: FC = () => { @@ -26,12 +29,13 @@ export const EditTransformRetentionPolicy: FC = () => { const toastNotifications = useToastNotifications(); - const dataViewId = useEditTransformFlyout((s) => s.dataViewId); - const formSections = useEditTransformFlyout((s) => s.formSections); - const retentionPolicyField = useEditTransformFlyout((s) => s.formFields.retentionPolicyField); - const config = useEditTransformFlyout((s) => s.config); - const setFormField = useEditTransformFlyout((s) => s.setFormField); - const setFormSection = useEditTransformFlyout((s) => s.setFormSection); + const dataViewId = useEditTransformFlyoutSelector((s) => s.dataViewId); + const formSections = useEditTransformFlyoutSelector((s) => s.formSections); + const retentionPolicyField = useEditTransformFlyoutSelector( + (s) => s.formFields.retentionPolicyField + ); + const config = useEditTransformFlyoutSelector((s) => s.config); + const { setFormField, setFormSection } = useEditTransformFlyoutActions(); const previewRequest: PostTransformsPreviewRequestSchema = useMemo(() => { return { diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_update_button.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_update_button.tsx index b96d387b28e79..0a9d45448d918 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_update_button.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_update_button.tsx @@ -17,7 +17,8 @@ import { useUpdateTransform } from '../../../../hooks'; import { applyFormStateToTransformConfig, - useEditTransformFlyout, + useEditTransformFlyoutActions, + useEditTransformFlyoutState, } from './use_edit_transform_flyout'; interface EditTransformUpdateButtonProps { @@ -25,10 +26,12 @@ interface EditTransformUpdateButtonProps { } export const EditTransformUpdateButton: FC = ({ closeFlyout }) => { - const { config, formFields, formSections, isFormValid, isFormTouched } = useEditTransformFlyout(); + const { config, formFields, formSections, isFormValid, isFormTouched } = + useEditTransformFlyoutState(); const requestConfig = applyFormStateToTransformConfig(config, formFields, formSections); const isUpdateButtonDisabled = !isFormValid || !isFormTouched; - const setApiError = useEditTransformFlyout((s) => s.setApiError); + + const { setApiError } = useEditTransformFlyoutActions(); const updateTransfrom = useUpdateTransform(config.id, requestConfig); diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/use_edit_transform_flyout.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/use_edit_transform_flyout.tsx index 5371d1cd1e6f7..a8ffb421cb0eb 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/use_edit_transform_flyout.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/use_edit_transform_flyout.tsx @@ -6,13 +6,13 @@ */ import { isEqual, merge } from 'lodash'; -import React, { useContext, useRef, type FC } from 'react'; -import { createStore, useStore } from 'zustand'; -import { produce } from 'immer'; +import React, { createContext, useContext, useMemo, type FC } from 'react'; +import { configureStore, createSlice, type PayloadAction } from '@reduxjs/toolkit'; +import { useDispatch, useSelector, useStore, Provider } from 'react-redux'; +import { bindActionCreators } from 'redux'; import { getNestedProperty, setNestedProperty } from '@kbn/ml-nested-property'; -import { createContext } from 'react'; import { PostTransformsUpdateRequestSchema } from '../../../../../../common/api_schemas/update_transforms'; import { DEFAULT_TRANSFORM_FREQUENCY, @@ -45,7 +45,7 @@ const defaultParser: DefaultParser = (v) => v; const nullableNumberParser: NullableNumberParser = (v) => (v === '' ? null : +v); const numberParser: NumberParser = (v) => +v; -// This custom hook uses `zustand` to provide a generic framework to manage form state +// This custom hook uses redux-toolkit to provide a generic framework to manage form state // and apply it to a final possibly nested configuration object suitable for passing on // directly to an API call. For now this is only used for the transform edit form. // Once we apply the functionality to other places, e.g. the transform creation wizard, @@ -329,17 +329,6 @@ export const getDefaultState = (config: TransformConfigUnion): EditTransformFlyo const isFormValid = (fieldsState: EditTransformFlyoutFieldsState) => Object.values(fieldsState).every((d) => d.errorMessages.length === 0); -// Updates a form field with its new value, -// runs validation and populates `errorMessages` if any errors occur. -const updateFormField = (state: FormField, value: string): FormField => - produce(state, (d) => { - d.errorMessages = - state.isOptional && typeof value === 'string' && value.length === 0 - ? [] - : state.validator(value, state.isOptional); - d.value = value; - }); - const getFieldValues = (fields: EditTransformFlyoutFieldsState) => Object.values(fields).map((f) => f.value); const getSectionValues = (sections: EditTransformFlyoutSectionsState) => @@ -358,20 +347,11 @@ interface EditTransformFlyoutFormState { isFormValid: boolean; } -interface EditTransformFlyoutActions { - setApiError: (payload: string | undefined) => void; - setFormField: (payload: { field: EditTransformFormFields; value: string }) => void; - setFormSection: (payload: { section: EditTransformFormSections; enabled: boolean }) => void; -} - -// The state we manage via zustand combines the provider props, -// the form state and the zustand actions. -type EditTransformFlyoutState = EditTransformFlyoutProviderProps & - EditTransformFlyoutFormState & - EditTransformFlyoutActions; - -type EditTransformFlyoutStore = ReturnType; -const createEditTransformFlyoutStore = ({ +// The state we manage via redux combines the provider props and the form state. +export type EditTransformFlyoutState = EditTransformFlyoutProviderProps & + EditTransformFlyoutFormState; +type EditTransformFlyoutSlice = ReturnType; +const createEditTransformFlyoutSlice = ({ config, dataViewId, }: EditTransformFlyoutProviderProps) => { @@ -379,72 +359,87 @@ const createEditTransformFlyoutStore = ({ const defaultFieldValues = getFieldValues(defaultState.formFields); const defaultSectionValues = getSectionValues(defaultState.formSections); - return createStore()((set) => { - // a helper to wrap a callback in both zustand's set() and immer's produce() - const createAction = (cb: (d: EditTransformFlyoutState) => void) => - set((state) => produce(state, cb)); - - const isFormTouched = (d: EditTransformFlyoutState) => - !isEqual(defaultFieldValues, getFieldValues(d.formFields)) || - !isEqual(defaultSectionValues, getSectionValues(d.formSections)); + const isFormTouched = (d: EditTransformFlyoutState) => + !isEqual(defaultFieldValues, getFieldValues(d.formFields)) || + !isEqual(defaultSectionValues, getSectionValues(d.formSections)); - return { + return createSlice({ + name: 'editTransformFlyout', + initialState: { ...defaultState, config, dataViewId, - setApiError: (payload) => - createAction((d) => { - d.apiErrorMessage = payload; - }), + }, + reducers: { + setApiError: (state, action: PayloadAction) => { + state.apiErrorMessage = action.payload; + }, // Updates a form field with its new value, runs validation and // populates `errorMessages` if any errors occur. - setFormField: (payload) => - createAction((d) => { - d.formFields[payload.field] = updateFormField(d.formFields[payload.field], payload.value); - d.isFormTouched = isFormTouched(d); - d.isFormValid = isFormValid(d.formFields); - }), + setFormField: ( + state, + action: PayloadAction<{ field: EditTransformFormFields; value: string }> + ) => { + const formField = state.formFields[action.payload.field]; + formField.errorMessages = + formField.isOptional && + typeof action.payload.value === 'string' && + action.payload.value.length === 0 + ? [] + : formField.validator(action.payload.value, formField.isOptional); + formField.value = action.payload.value; + state.formFields[action.payload.field] = formField; + + state.isFormTouched = isFormTouched(state); + state.isFormValid = isFormValid(state.formFields); + }, // Updates a form section. - setFormSection: (payload) => - createAction((d) => { - d.formSections[payload.section].enabled = payload.enabled; - d.isFormTouched = isFormTouched(d); - d.isFormValid = isFormValid(d.formFields); - }), - }; + setFormSection: ( + state, + action: PayloadAction<{ section: EditTransformFormSections; enabled: boolean }> + ) => { + state.formSections[action.payload.section].enabled = action.payload.enabled; + state.isFormTouched = isFormTouched(state); + state.isFormValid = isFormValid(state.formFields); + }, + }, }); }; -export const EditTransformFlyoutContext = createContext(null); +const EditTransformFlyoutContext = createContext(null); // Provider wrapper export const EditTransformFlyoutProvider: FC< React.PropsWithChildren > = ({ children, ...props }) => { - const storeRef = useRef(); - if (!storeRef.current) { - storeRef.current = createEditTransformFlyoutStore(props); - } + const slice = useMemo(() => createEditTransformFlyoutSlice(props), [props]); + + const store = useMemo(() => { + return configureStore({ + reducer: slice.reducer, + }); + }, [slice]); + return ( - - {children} - + + + {children} + + ); }; -// This approach with overloads is necessary for TS support of the selector callback. -// `zustand` TypeScript guide for reference: https://docs.pmnd.rs/zustand/guides/typescript#bounded-usestore-hook-for-vanilla-stores -export function useEditTransformFlyout(): EditTransformFlyoutState; -export function useEditTransformFlyout( - selector: (state: EditTransformFlyoutState) => T, - equals?: (a: T, b: T) => boolean -): T; -export function useEditTransformFlyout( - selector?: (state: EditTransformFlyoutState) => T, - equals?: (a: T, b: T) => boolean -) { - const store = useContext(EditTransformFlyoutContext); - if (!store) throw new Error('Missing EditTransformFlyoutContext.Provider in the tree'); - - return useStore(store, selector!, equals); +export function useEditTransformFlyoutActions() { + const dispatch = useDispatch(); + const slice = useContext(EditTransformFlyoutContext); + if (!slice) throw new Error('Missing EditTransformFlyoutContext.Provider in the tree'); + return bindActionCreators(slice.actions, dispatch); +} + +export function useEditTransformFlyoutState() { + return useStore().getState(); +} + +export function useEditTransformFlyoutSelector(selector: (s: EditTransformFlyoutState) => T) { + return useSelector(selector); } From 063b5fa66dbf679e5935125ac8274c4f82ee49de Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Thu, 21 Dec 2023 15:48:56 +0100 Subject: [PATCH 12/36] remove zustand from package.json --- package.json | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 052cddef9eab8..e8c65278957bd 100644 --- a/package.json +++ b/package.json @@ -1120,8 +1120,7 @@ "xterm": "^5.1.0", "yauzl": "^2.10.0", "yazl": "^2.5.1", - "zod": "^3.22.3", - "zustand": "^4.4.1" + "zod": "^3.22.3" }, "devDependencies": { "@apidevtools/swagger-parser": "^10.0.3", @@ -1672,4 +1671,4 @@ "yargs": "^15.4.1", "yarn-deduplicate": "^6.0.2" } -} \ No newline at end of file +} From 4328ceb8658acfee7242ec1abed3bda45d69db08 Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Thu, 21 Dec 2023 18:34:45 +0100 Subject: [PATCH 13/36] refactor to move createSlice to top level of file --- .../use_edit_transform_flyout.tsx | 133 ++++++++---------- 1 file changed, 62 insertions(+), 71 deletions(-) diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/use_edit_transform_flyout.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/use_edit_transform_flyout.tsx index a8ffb421cb0eb..5d88ce01ba936 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/use_edit_transform_flyout.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/use_edit_transform_flyout.tsx @@ -6,7 +6,7 @@ */ import { isEqual, merge } from 'lodash'; -import React, { createContext, useContext, useMemo, type FC } from 'react'; +import React, { useEffect, type FC } from 'react'; import { configureStore, createSlice, type PayloadAction } from '@reduxjs/toolkit'; import { useDispatch, useSelector, useStore, Provider } from 'react-redux'; import { bindActionCreators } from 'redux'; @@ -347,93 +347,84 @@ interface EditTransformFlyoutFormState { isFormValid: boolean; } -// The state we manage via redux combines the provider props and the form state. -export type EditTransformFlyoutState = EditTransformFlyoutProviderProps & - EditTransformFlyoutFormState; -type EditTransformFlyoutSlice = ReturnType; -const createEditTransformFlyoutSlice = ({ - config, - dataViewId, -}: EditTransformFlyoutProviderProps) => { +const isFormTouched = (config: TransformConfigUnion, currentState: EditTransformFlyoutState) => { const defaultState = getDefaultState(config); const defaultFieldValues = getFieldValues(defaultState.formFields); const defaultSectionValues = getSectionValues(defaultState.formSections); + return ( + !isEqual(defaultFieldValues, getFieldValues(currentState.formFields)) || + !isEqual(defaultSectionValues, getSectionValues(currentState.formSections)) + ); +}; - const isFormTouched = (d: EditTransformFlyoutState) => - !isEqual(defaultFieldValues, getFieldValues(d.formFields)) || - !isEqual(defaultSectionValues, getSectionValues(d.formSections)); +// The state we manage via redux combines the provider props and the form state. +export type EditTransformFlyoutState = EditTransformFlyoutProviderProps & + EditTransformFlyoutFormState; - return createSlice({ - name: 'editTransformFlyout', - initialState: { - ...defaultState, - config, - dataViewId, +const editTransformFlyoutSlice = createSlice({ + name: 'editTransformFlyout', + initialState: undefined as unknown as EditTransformFlyoutState, + reducers: { + initialize: (state, action: PayloadAction) => { + const defaultState = getDefaultState(action.payload.config); + return { + ...defaultState, + config: action.payload.config, + dataViewId: action.payload.dataViewId, + }; }, - reducers: { - setApiError: (state, action: PayloadAction) => { - state.apiErrorMessage = action.payload; - }, - // Updates a form field with its new value, runs validation and - // populates `errorMessages` if any errors occur. - setFormField: ( - state, - action: PayloadAction<{ field: EditTransformFormFields; value: string }> - ) => { - const formField = state.formFields[action.payload.field]; - formField.errorMessages = - formField.isOptional && - typeof action.payload.value === 'string' && - action.payload.value.length === 0 - ? [] - : formField.validator(action.payload.value, formField.isOptional); - formField.value = action.payload.value; - state.formFields[action.payload.field] = formField; - - state.isFormTouched = isFormTouched(state); - state.isFormValid = isFormValid(state.formFields); - }, - // Updates a form section. - setFormSection: ( - state, - action: PayloadAction<{ section: EditTransformFormSections; enabled: boolean }> - ) => { - state.formSections[action.payload.section].enabled = action.payload.enabled; - state.isFormTouched = isFormTouched(state); - state.isFormValid = isFormValid(state.formFields); - }, + setApiError: (state, action: PayloadAction) => { + state.apiErrorMessage = action.payload; }, - }); -}; - -const EditTransformFlyoutContext = createContext(null); + // Updates a form field with its new value, runs validation and + // populates `errorMessages` if any errors occur. + setFormField: ( + state, + action: PayloadAction<{ field: EditTransformFormFields; value: string }> + ) => { + const formField = state.formFields[action.payload.field]; + formField.errorMessages = + formField.isOptional && + typeof action.payload.value === 'string' && + action.payload.value.length === 0 + ? [] + : formField.validator(action.payload.value, formField.isOptional); + formField.value = action.payload.value; + state.formFields[action.payload.field] = formField; + + state.isFormTouched = isFormTouched(state.config, state); + state.isFormValid = isFormValid(state.formFields); + }, + // Updates a form section. + setFormSection: ( + state, + action: PayloadAction<{ section: EditTransformFormSections; enabled: boolean }> + ) => { + state.formSections[action.payload.section].enabled = action.payload.enabled; + state.isFormTouched = isFormTouched(state.config, state); + state.isFormValid = isFormValid(state.formFields); + }, + }, +}); -// Provider wrapper export const EditTransformFlyoutProvider: FC< React.PropsWithChildren > = ({ children, ...props }) => { - const slice = useMemo(() => createEditTransformFlyoutSlice(props), [props]); + const store = configureStore({ + reducer: editTransformFlyoutSlice.reducer, + }); - const store = useMemo(() => { - return configureStore({ - reducer: slice.reducer, - }); - }, [slice]); + // initialize redux state + useEffect(() => { + store.dispatch(editTransformFlyoutSlice.actions.initialize(props)); + }, [props, store]); - return ( - - - {children} - - - ); + return {children}; }; export function useEditTransformFlyoutActions() { const dispatch = useDispatch(); - const slice = useContext(EditTransformFlyoutContext); - if (!slice) throw new Error('Missing EditTransformFlyoutContext.Provider in the tree'); - return bindActionCreators(slice.actions, dispatch); + return bindActionCreators(editTransformFlyoutSlice.actions, dispatch); } export function useEditTransformFlyoutState() { From 759595b41486d7b81cf1a9e48fae1fc6491458b8 Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Thu, 21 Dec 2023 18:45:27 +0100 Subject: [PATCH 14/36] fix state to be redux compliant --- .../use_edit_transform_flyout.tsx | 46 +++++++++++++------ 1 file changed, 31 insertions(+), 15 deletions(-) diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/use_edit_transform_flyout.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/use_edit_transform_flyout.tsx index 5d88ce01ba936..687bfec137bad 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/use_edit_transform_flyout.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/use_edit_transform_flyout.tsx @@ -33,9 +33,18 @@ import { transformSettingsPageSearchSizeValidator, retentionPolicyMaxAgeValidator, stringValidator, - type Validator, } from '../../../../common/validators'; +const validators = { + frequencyValidator, + integerAboveZeroValidator, + transformSettingsNumberOfRetriesValidator, + transformSettingsPageSearchSizeValidator, + retentionPolicyMaxAgeValidator, + stringValidator, +}; +type ValidatorName = keyof typeof validators; + type DefaultParser = (v: string) => string; type NullableNumberParser = (v: string) => number | null; type NumberParser = (v: string) => number; @@ -45,6 +54,13 @@ const defaultParser: DefaultParser = (v) => v; const nullableNumberParser: NullableNumberParser = (v) => (v === '' ? null : +v); const numberParser: NumberParser = (v) => +v; +const valueParsers = { + defaultParser, + nullableNumberParser, + numberParser, +}; +type ValueParserName = keyof typeof valueParsers; + // This custom hook uses redux-toolkit to provide a generic framework to manage form state // and apply it to a final possibly nested configuration object suitable for passing on // directly to an API call. For now this is only used for the transform edit form. @@ -76,9 +92,9 @@ export interface FormField { isNullable: boolean; isOptional: boolean; section?: EditTransformFormSections; - validator: Validator; + validator: ValidatorName; value: string; - valueParser: ValueParser; + valueParser: ValueParserName; } // Defining these sections is only necessary for options where a reset/deletion of that part of the @@ -130,9 +146,9 @@ export const initializeFormField = ( errorMessages: [], isNullable: false, isOptional: true, - validator: stringValidator, + validator: 'stringValidator', value, - valueParser: defaultParser, + valueParser: 'defaultParser', ...(overloads !== undefined ? { ...overloads } : {}), }; }; @@ -175,7 +191,7 @@ const getUpdateValue = ( const formValue = formStateAttribute.value !== '' - ? formStateAttribute.valueParser(formStateAttribute.value) + ? valueParsers[formStateAttribute.valueParser](formStateAttribute.value) : fallbackValue; const configValue = getNestedProperty(config, formStateAttribute.configFieldName, fallbackValue); @@ -242,7 +258,7 @@ export const getDefaultState = (config: TransformConfigUnion): EditTransformFlyo description: initializeFormField('description', 'description', config), frequency: initializeFormField('frequency', 'frequency', config, { defaultValue: DEFAULT_TRANSFORM_FREQUENCY, - validator: frequencyValidator, + validator: 'frequencyValidator', }), // dest.* @@ -264,8 +280,8 @@ export const getDefaultState = (config: TransformConfigUnion): EditTransformFlyo docsPerSecond: initializeFormField('docsPerSecond', 'settings.docs_per_second', config, { isNullable: true, isOptional: true, - validator: integerAboveZeroValidator, - valueParser: nullableNumberParser, + validator: 'integerAboveZeroValidator', + valueParser: 'nullableNumberParser', }), maxPageSearchSize: initializeFormField( 'maxPageSearchSize', @@ -275,8 +291,8 @@ export const getDefaultState = (config: TransformConfigUnion): EditTransformFlyo defaultValue: `${DEFAULT_TRANSFORM_SETTINGS_MAX_PAGE_SEARCH_SIZE}`, isNullable: true, isOptional: true, - validator: transformSettingsPageSearchSizeValidator, - valueParser: numberParser, + validator: 'transformSettingsPageSearchSizeValidator', + valueParser: 'numberParser', } ), numFailureRetries: initializeFormField( @@ -287,8 +303,8 @@ export const getDefaultState = (config: TransformConfigUnion): EditTransformFlyo defaultValue: undefined, isNullable: true, isOptional: true, - validator: transformSettingsNumberOfRetriesValidator, - valueParser: numberParser, + validator: 'transformSettingsNumberOfRetriesValidator', + valueParser: 'numberParser', } ), @@ -313,7 +329,7 @@ export const getDefaultState = (config: TransformConfigUnion): EditTransformFlyo isNullable: false, isOptional: true, section: 'retentionPolicy', - validator: retentionPolicyMaxAgeValidator, + validator: 'retentionPolicyMaxAgeValidator', } ), }, @@ -388,7 +404,7 @@ const editTransformFlyoutSlice = createSlice({ typeof action.payload.value === 'string' && action.payload.value.length === 0 ? [] - : formField.validator(action.payload.value, formField.isOptional); + : validators[formField.validator](action.payload.value, formField.isOptional); formField.value = action.payload.value; state.formFields[action.payload.field] = formField; From 6b66b5132f6210a6f42c7aa5184ce1d75878aff7 Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Thu, 21 Dec 2023 19:18:28 +0100 Subject: [PATCH 15/36] fix state initialization --- .../edit_transform_update_button.tsx | 4 +- .../use_edit_transform_flyout.tsx | 64 ++++++++++--------- 2 files changed, 37 insertions(+), 31 deletions(-) diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_update_button.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_update_button.tsx index 0a9d45448d918..2b6b300667826 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_update_button.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_update_button.tsx @@ -18,7 +18,7 @@ import { useUpdateTransform } from '../../../../hooks'; import { applyFormStateToTransformConfig, useEditTransformFlyoutActions, - useEditTransformFlyoutState, + useEditTransformFlyoutSelector, } from './use_edit_transform_flyout'; interface EditTransformUpdateButtonProps { @@ -27,7 +27,7 @@ interface EditTransformUpdateButtonProps { export const EditTransformUpdateButton: FC = ({ closeFlyout }) => { const { config, formFields, formSections, isFormValid, isFormTouched } = - useEditTransformFlyoutState(); + useEditTransformFlyoutSelector((d) => d); const requestConfig = applyFormStateToTransformConfig(config, formFields, formSections); const isUpdateButtonDisabled = !isFormValid || !isFormTouched; diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/use_edit_transform_flyout.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/use_edit_transform_flyout.tsx index 687bfec137bad..2771f16a10d62 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/use_edit_transform_flyout.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/use_edit_transform_flyout.tsx @@ -6,9 +6,9 @@ */ import { isEqual, merge } from 'lodash'; -import React, { useEffect, type FC } from 'react'; +import React, { useEffect, useMemo, type FC } from 'react'; import { configureStore, createSlice, type PayloadAction } from '@reduxjs/toolkit'; -import { useDispatch, useSelector, useStore, Provider } from 'react-redux'; +import { useDispatch, useSelector, Provider } from 'react-redux'; import { bindActionCreators } from 'redux'; import { getNestedProperty, setNestedProperty } from '@kbn/ml-nested-property'; @@ -48,7 +48,6 @@ type ValidatorName = keyof typeof validators; type DefaultParser = (v: string) => string; type NullableNumberParser = (v: string) => number | null; type NumberParser = (v: string) => number; -type ValueParser = DefaultParser | NullableNumberParser | NumberParser; const defaultParser: DefaultParser = (v) => v; const nullableNumberParser: NullableNumberParser = (v) => (v === '' ? null : +v); @@ -379,9 +378,9 @@ export type EditTransformFlyoutState = EditTransformFlyoutProviderProps & const editTransformFlyoutSlice = createSlice({ name: 'editTransformFlyout', - initialState: undefined as unknown as EditTransformFlyoutState, + initialState: undefined as EditTransformFlyoutState | undefined, reducers: { - initialize: (state, action: PayloadAction) => { + initialize: (_, action: PayloadAction) => { const defaultState = getDefaultState(action.payload.config); return { ...defaultState, @@ -390,7 +389,9 @@ const editTransformFlyoutSlice = createSlice({ }; }, setApiError: (state, action: PayloadAction) => { - state.apiErrorMessage = action.payload; + if (state) { + state.apiErrorMessage = action.payload; + } }, // Updates a form field with its new value, runs validation and // populates `errorMessages` if any errors occur. @@ -398,27 +399,31 @@ const editTransformFlyoutSlice = createSlice({ state, action: PayloadAction<{ field: EditTransformFormFields; value: string }> ) => { - const formField = state.formFields[action.payload.field]; - formField.errorMessages = - formField.isOptional && - typeof action.payload.value === 'string' && - action.payload.value.length === 0 - ? [] - : validators[formField.validator](action.payload.value, formField.isOptional); - formField.value = action.payload.value; - state.formFields[action.payload.field] = formField; - - state.isFormTouched = isFormTouched(state.config, state); - state.isFormValid = isFormValid(state.formFields); + if (state) { + const formField = state.formFields[action.payload.field]; + formField.errorMessages = + formField.isOptional && + typeof action.payload.value === 'string' && + action.payload.value.length === 0 + ? [] + : validators[formField.validator](action.payload.value, formField.isOptional); + formField.value = action.payload.value; + state.formFields[action.payload.field] = formField; + + state.isFormTouched = isFormTouched(state.config, state); + state.isFormValid = isFormValid(state.formFields); + } }, // Updates a form section. setFormSection: ( state, action: PayloadAction<{ section: EditTransformFormSections; enabled: boolean }> ) => { - state.formSections[action.payload.section].enabled = action.payload.enabled; - state.isFormTouched = isFormTouched(state.config, state); - state.isFormValid = isFormValid(state.formFields); + if (state) { + state.formSections[action.payload.section].enabled = action.payload.enabled; + state.isFormTouched = isFormTouched(state.config, state); + state.isFormValid = isFormValid(state.formFields); + } }, }, }); @@ -426,14 +431,19 @@ const editTransformFlyoutSlice = createSlice({ export const EditTransformFlyoutProvider: FC< React.PropsWithChildren > = ({ children, ...props }) => { - const store = configureStore({ - reducer: editTransformFlyoutSlice.reducer, - }); + const store = useMemo( + () => + configureStore({ + reducer: editTransformFlyoutSlice.reducer, + }), + [] + ); // initialize redux state useEffect(() => { store.dispatch(editTransformFlyoutSlice.actions.initialize(props)); - }, [props, store]); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); return {children}; }; @@ -443,10 +453,6 @@ export function useEditTransformFlyoutActions() { return bindActionCreators(editTransformFlyoutSlice.actions, dispatch); } -export function useEditTransformFlyoutState() { - return useStore().getState(); -} - export function useEditTransformFlyoutSelector(selector: (s: EditTransformFlyoutState) => T) { return useSelector(selector); } From e9c080ea0593b3597e9921e4d52d7f64e147979e Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Thu, 21 Dec 2023 20:11:11 +0100 Subject: [PATCH 16/36] fix dependencies --- package.json | 2 +- yarn.lock | 9 +-------- 2 files changed, 2 insertions(+), 9 deletions(-) diff --git a/package.json b/package.json index e8c65278957bd..1655f03ace40a 100644 --- a/package.json +++ b/package.json @@ -1671,4 +1671,4 @@ "yargs": "^15.4.1", "yarn-deduplicate": "^6.0.2" } -} +} \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index 5312a57c8f3e1..048c16b9462c6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -30352,7 +30352,7 @@ use-sidecar@^1.1.2: detect-node-es "^1.1.0" tslib "^2.0.0" -use-sync-external-store@1.2.0, use-sync-external-store@^1.0.0, use-sync-external-store@^1.2.0: +use-sync-external-store@^1.0.0, use-sync-external-store@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz#7dbefd6ef3fe4e767a0cf5d7287aacfb5846928a" integrity sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA== @@ -31918,13 +31918,6 @@ zod@^3.22.3: resolved "https://registry.yarnpkg.com/zod/-/zod-3.22.3.tgz#2fbc96118b174290d94e8896371c95629e87a060" integrity sha512-EjIevzuJRiRPbVH4mGc8nApb/lVLKVpmUhAaR5R5doKGfAnGJ6Gr3CViAVjP+4FWSxCsybeWQdcgCtbX+7oZug== -zustand@^4.4.1: - version "4.4.1" - resolved "https://registry.yarnpkg.com/zustand/-/zustand-4.4.1.tgz#0cd3a3e4756f21811bd956418fdc686877e8b3b0" - integrity sha512-QCPfstAS4EBiTQzlaGP1gmorkh/UL1Leaj2tdj+zZCZ/9bm0WS7sI2wnfD5lpOszFqWJ1DcPnGoY8RDL61uokw== - dependencies: - use-sync-external-store "1.2.0" - zwitch@^1.0.0: version "1.0.5" resolved "https://registry.yarnpkg.com/zwitch/-/zwitch-1.0.5.tgz#d11d7381ffed16b742f6af7b3f223d5cd9fe9920" From d68e43cd8799bb733dc08bb9cc497dec8385a6d2 Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Fri, 22 Dec 2023 10:21:49 +0100 Subject: [PATCH 17/36] fix jest test --- .../use_edit_transform_flyout.test.tsx | 35 ++++++++++++------- 1 file changed, 22 insertions(+), 13 deletions(-) diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/use_edit_transform_flyout.test.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/use_edit_transform_flyout.test.tsx index 30e6e6ed7eefc..55ee73b616a94 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/use_edit_transform_flyout.test.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/use_edit_transform_flyout.test.tsx @@ -13,7 +13,8 @@ import { TransformPivotConfig } from '../../../../../../common/types/transform'; import { applyFormStateToTransformConfig, getDefaultState, - useEditTransformFlyout, + useEditTransformFlyoutActions, + useEditTransformFlyoutSelector, EditTransformFlyoutProvider, } from './use_edit_transform_flyout'; @@ -218,7 +219,7 @@ describe('Transform: applyFormStateToTransformConfig()', () => { }); }); -describe('Transform: useEditTransformFlyout()', () => { +describe('Transform: useEditTransformFlyoutActions/Selector()', () => { it('field updates should trigger form validation', () => { const transformConfigMock = getTransformConfigMock(); const wrapper: FC = ({ children }) => ( @@ -226,38 +227,46 @@ describe('Transform: useEditTransformFlyout()', () => { {children} ); - const { result } = renderHook(() => useEditTransformFlyout(), { wrapper }); + const { result } = renderHook( + () => { + return { + actions: useEditTransformFlyoutActions(), + state: useEditTransformFlyoutSelector((d) => d), + }; + }, + { wrapper } + ); act(() => { - result.current.setFormField({ + result.current.actions.setFormField({ field: 'description', value: 'the-updated-description', }); }); - expect(result.current.isFormTouched).toBe(true); - expect(result.current.isFormValid).toBe(true); + expect(result.current.state.isFormTouched).toBe(true); + expect(result.current.state.isFormValid).toBe(true); act(() => { - result.current.setFormField({ + result.current.actions.setFormField({ field: 'description', value: transformConfigMock.description as string, }); }); - expect(result.current.isFormTouched).toBe(false); - expect(result.current.isFormValid).toBe(true); + expect(result.current.state.isFormTouched).toBe(false); + expect(result.current.state.isFormValid).toBe(true); act(() => { - result.current.setFormField({ + result.current.actions.setFormField({ field: 'frequency', value: 'the-invalid-value', }); }); - expect(result.current.isFormTouched).toBe(true); - expect(result.current.isFormValid).toBe(false); - expect(result.current.formFields.frequency.errorMessages).toStrictEqual([ + expect(result.current.state.isFormTouched).toBe(true); + expect(result.current.state.isFormValid).toBe(false); + expect(result.current.state.formFields.frequency.errorMessages).toStrictEqual([ 'The frequency value is not valid.', ]); }); From d8c69cfb044c7a40785adfae1786791b5665b0ff Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Fri, 22 Dec 2023 10:34:06 +0100 Subject: [PATCH 18/36] add comment to custom hook --- .../edit_transform_flyout/use_edit_transform_flyout.test.tsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/use_edit_transform_flyout.test.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/use_edit_transform_flyout.test.tsx index 55ee73b616a94..59b2d66c9fbc0 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/use_edit_transform_flyout.test.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/use_edit_transform_flyout.test.tsx @@ -227,6 +227,10 @@ describe('Transform: useEditTransformFlyoutActions/Selector()', () => { {children} ); + + // As we want to test how actions affect the state, + // we set up this custom hook that combines both the hooks for + // actions and state selection, so they react to the same redux store. const { result } = renderHook( () => { return { From 753eee53828f56df577c01db4cb3476b30cbd798 Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Fri, 22 Dec 2023 13:51:56 +0100 Subject: [PATCH 19/36] consolidate code into helper functions for form field and section update --- .../use_edit_transform_flyout.tsx | 58 ++++++++++++++++--- 1 file changed, 50 insertions(+), 8 deletions(-) diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/use_edit_transform_flyout.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/use_edit_transform_flyout.tsx index 2771f16a10d62..8893853300a5d 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/use_edit_transform_flyout.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/use_edit_transform_flyout.tsx @@ -90,6 +90,7 @@ export interface FormField { errorMessages: string[]; isNullable: boolean; isOptional: boolean; + isOptionalInSection?: boolean; section?: EditTransformFormSections; validator: ValidatorName; value: string; @@ -316,6 +317,7 @@ export const getDefaultState = (config: TransformConfigUnion): EditTransformFlyo dependsOn: ['retentionPolicyMaxAge'], isNullable: false, isOptional: true, + isOptionalInSection: false, section: 'retentionPolicy', } ), @@ -328,6 +330,7 @@ export const getDefaultState = (config: TransformConfigUnion): EditTransformFlyo isNullable: false, isOptional: true, section: 'retentionPolicy', + isOptionalInSection: false, validator: 'retentionPolicyMaxAgeValidator', } ), @@ -376,6 +379,30 @@ const isFormTouched = (config: TransformConfigUnion, currentState: EditTransform export type EditTransformFlyoutState = EditTransformFlyoutProviderProps & EditTransformFlyoutFormState; +function isFormFieldOptional(state: EditTransformFlyoutState, field: EditTransformFormFields) { + const formField = state.formFields[field]; + + let isOptional = formField.isOptional; + if (formField.section) { + const section = state.formSections[formField.section]; + if (section.enabled && formField.isOptionalInSection === false) { + isOptional = false; + } + } + + return isOptional; +} + +function getFormFieldErrorMessages( + value: string, + isOptional: boolean, + validatorName: ValidatorName +) { + return isOptional && typeof value === 'string' && value.length === 0 + ? [] + : validators[validatorName](value, isOptional); +} + const editTransformFlyoutSlice = createSlice({ name: 'editTransformFlyout', initialState: undefined as EditTransformFlyoutState | undefined, @@ -401,14 +428,15 @@ const editTransformFlyoutSlice = createSlice({ ) => { if (state) { const formField = state.formFields[action.payload.field]; - formField.errorMessages = - formField.isOptional && - typeof action.payload.value === 'string' && - action.payload.value.length === 0 - ? [] - : validators[formField.validator](action.payload.value, formField.isOptional); + const isOptional = isFormFieldOptional(state, action.payload.field); + + formField.errorMessages = getFormFieldErrorMessages( + action.payload.value, + isOptional, + formField.validator + ); + formField.value = action.payload.value; - state.formFields[action.payload.field] = formField; state.isFormTouched = isFormTouched(state.config, state); state.isFormValid = isFormValid(state.formFields); @@ -421,6 +449,19 @@ const editTransformFlyoutSlice = createSlice({ ) => { if (state) { state.formSections[action.payload.section].enabled = action.payload.enabled; + + // After a section change we re-evaluate all form fields, since if a field + // is optional could change if a section got toggled. + Object.entries(state.formFields).forEach(([formFieldName, formField]) => { + const isOptional = isFormFieldOptional(state, formFieldName as EditTransformFormFields); + + formField.errorMessages = getFormFieldErrorMessages( + formField.value, + isOptional, + formField.validator + ); + }); + state.isFormTouched = isFormTouched(state.config, state); state.isFormValid = isFormValid(state.formFields); } @@ -439,7 +480,8 @@ export const EditTransformFlyoutProvider: FC< [] ); - // initialize redux state + // Initialize redux state. The store's `initialState` is `undefined` since + // we only get the transform config and data view id during runtime. useEffect(() => { store.dispatch(editTransformFlyoutSlice.actions.initialize(props)); // eslint-disable-next-line react-hooks/exhaustive-deps From ee3bda4e4f7bca75bcdf0f851a2ab40aa6f16e7b Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Fri, 22 Dec 2023 14:00:08 +0100 Subject: [PATCH 20/36] cleanup --- .../use_edit_transform_flyout.tsx | 74 +++++++++---------- 1 file changed, 37 insertions(+), 37 deletions(-) diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/use_edit_transform_flyout.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/use_edit_transform_flyout.tsx index 8893853300a5d..fad3aa1efee01 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/use_edit_transform_flyout.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/use_edit_transform_flyout.tsx @@ -329,8 +329,8 @@ export const getDefaultState = (config: TransformConfigUnion): EditTransformFlyo dependsOn: ['retentionPolicyField'], isNullable: false, isOptional: true, - section: 'retentionPolicy', isOptionalInSection: false, + section: 'retentionPolicy', validator: 'retentionPolicyMaxAgeValidator', } ), @@ -367,11 +367,12 @@ interface EditTransformFlyoutFormState { const isFormTouched = (config: TransformConfigUnion, currentState: EditTransformFlyoutState) => { const defaultState = getDefaultState(config); - const defaultFieldValues = getFieldValues(defaultState.formFields); - const defaultSectionValues = getSectionValues(defaultState.formSections); return ( - !isEqual(defaultFieldValues, getFieldValues(currentState.formFields)) || - !isEqual(defaultSectionValues, getSectionValues(currentState.formSections)) + !isEqual(getFieldValues(defaultState.formFields), getFieldValues(currentState.formFields)) || + !isEqual( + getSectionValues(defaultState.formSections), + getSectionValues(currentState.formSections) + ) ); }; @@ -416,9 +417,8 @@ const editTransformFlyoutSlice = createSlice({ }; }, setApiError: (state, action: PayloadAction) => { - if (state) { - state.apiErrorMessage = action.payload; - } + if (!state) return; + state.apiErrorMessage = action.payload; }, // Updates a form field with its new value, runs validation and // populates `errorMessages` if any errors occur. @@ -426,45 +426,45 @@ const editTransformFlyoutSlice = createSlice({ state, action: PayloadAction<{ field: EditTransformFormFields; value: string }> ) => { - if (state) { - const formField = state.formFields[action.payload.field]; - const isOptional = isFormFieldOptional(state, action.payload.field); + if (!state) return; - formField.errorMessages = getFormFieldErrorMessages( - action.payload.value, - isOptional, - formField.validator - ); + const formField = state.formFields[action.payload.field]; + const isOptional = isFormFieldOptional(state, action.payload.field); - formField.value = action.payload.value; + formField.errorMessages = getFormFieldErrorMessages( + action.payload.value, + isOptional, + formField.validator + ); - state.isFormTouched = isFormTouched(state.config, state); - state.isFormValid = isFormValid(state.formFields); - } + formField.value = action.payload.value; + + state.isFormTouched = isFormTouched(state.config, state); + state.isFormValid = isFormValid(state.formFields); }, // Updates a form section. setFormSection: ( state, action: PayloadAction<{ section: EditTransformFormSections; enabled: boolean }> ) => { - if (state) { - state.formSections[action.payload.section].enabled = action.payload.enabled; - - // After a section change we re-evaluate all form fields, since if a field - // is optional could change if a section got toggled. - Object.entries(state.formFields).forEach(([formFieldName, formField]) => { - const isOptional = isFormFieldOptional(state, formFieldName as EditTransformFormFields); - - formField.errorMessages = getFormFieldErrorMessages( - formField.value, - isOptional, - formField.validator - ); - }); + if (!state) return; - state.isFormTouched = isFormTouched(state.config, state); - state.isFormValid = isFormValid(state.formFields); - } + state.formSections[action.payload.section].enabled = action.payload.enabled; + + // After a section change we re-evaluate all form fields, since optionality + // of a field could change if a section got toggled. + Object.entries(state.formFields).forEach(([formFieldName, formField]) => { + const isOptional = isFormFieldOptional(state, formFieldName as EditTransformFormFields); + + formField.errorMessages = getFormFieldErrorMessages( + formField.value, + isOptional, + formField.validator + ); + }); + + state.isFormTouched = isFormTouched(state.config, state); + state.isFormValid = isFormValid(state.formFields); }, }, }); From 66c5e8db9388cd33e5aa7c22f1ea5ea24b1b4c44 Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Wed, 3 Jan 2024 13:32:19 +0100 Subject: [PATCH 21/36] selectors --- .../edit_transform_api_error_callout.tsx | 34 +++--- .../edit_transform_flyout_form_text_area.tsx | 6 +- .../edit_transform_flyout_form_text_input.tsx | 6 +- .../edit_transform_ingest_pipeline.tsx | 9 +- .../edit_transform_retention_policy.tsx | 14 +-- .../edit_transform_update_button.tsx | 13 +- .../use_edit_transform_flyout.test.tsx | 24 ++-- .../use_edit_transform_flyout.tsx | 115 +++++++++++------- 8 files changed, 123 insertions(+), 98 deletions(-) diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_api_error_callout.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_api_error_callout.tsx index 9b802dd67f123..999add067a7a3 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_api_error_callout.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_api_error_callout.tsx @@ -6,35 +6,31 @@ */ import React, { type FC } from 'react'; +import { useSelector } from 'react-redux'; import { EuiCallOut, EuiSpacer } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { useEditTransformFlyoutSelector } from './use_edit_transform_flyout'; +import { selectApiErrorMessage } from './use_edit_transform_flyout'; export const EditTransformApiErrorCallout: FC = () => { - const apiErrorMessage = useEditTransformFlyoutSelector((s) => s.apiErrorMessage); + const apiErrorMessage = useSelector(selectApiErrorMessage); + + if (apiErrorMessage === undefined) return null; return ( <> - {apiErrorMessage !== undefined && ( - <> - - -

{apiErrorMessage}

-
- - )} + + +

{apiErrorMessage}

+
); }; diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_flyout_form_text_area.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_flyout_form_text_area.tsx index 7df0edc17c343..c5dab9b848140 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_flyout_form_text_area.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_flyout_form_text_area.tsx @@ -13,7 +13,7 @@ import { i18n } from '@kbn/i18n'; import { useEditTransformFlyoutActions, - useEditTransformFlyoutSelector, + useFormField, type EditTransformFormFields, } from './use_edit_transform_flyout'; import { capitalizeFirstLetter } from './capitalize_first_letter'; @@ -31,9 +31,7 @@ export const EditTransformFlyoutFormTextArea: FC { - const { defaultValue, errorMessages, value } = useEditTransformFlyoutSelector( - (s) => s.formFields[field] - ); + const { defaultValue, errorMessages, value } = useFormField(field); const { setFormField } = useEditTransformFlyoutActions(); const upperCaseField = capitalizeFirstLetter(field); diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_flyout_form_text_input.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_flyout_form_text_input.tsx index c177b4ebb690e..c20ff9cfa2541 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_flyout_form_text_input.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_flyout_form_text_input.tsx @@ -13,7 +13,7 @@ import { i18n } from '@kbn/i18n'; import { useEditTransformFlyoutActions, - useEditTransformFlyoutSelector, + useFormField, type EditTransformFormFields, } from './use_edit_transform_flyout'; import { capitalizeFirstLetter } from './capitalize_first_letter'; @@ -31,9 +31,7 @@ export const EditTransformFlyoutFormTextInput: FC { - const { defaultValue, errorMessages, value } = useEditTransformFlyoutSelector( - (s) => s.formFields[field] - ); + const { defaultValue, errorMessages, value } = useFormField(field); const { setFormField } = useEditTransformFlyoutActions(); const upperCaseField = capitalizeFirstLetter(field); diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_ingest_pipeline.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_ingest_pipeline.tsx index ae79954188266..5ca88d51af526 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_ingest_pipeline.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_ingest_pipeline.tsx @@ -14,10 +14,7 @@ import { i18n } from '@kbn/i18n'; import { useGetEsIngestPipelines } from '../../../../hooks'; import { EditTransformFlyoutFormTextInput } from './edit_transform_flyout_form_text_input'; -import { - useEditTransformFlyoutActions, - useEditTransformFlyoutSelector, -} from './use_edit_transform_flyout'; +import { useEditTransformFlyoutActions, useFormField } from './use_edit_transform_flyout'; const ingestPipelineLabel = i18n.translate( 'xpack.transform.transformList.editFlyoutFormDestinationIngestPipelineLabel', @@ -28,9 +25,7 @@ const ingestPipelineLabel = i18n.translate( export const EditTransformIngestPipeline: FC = () => { const { euiTheme } = useEuiTheme(); - const { errorMessages, value } = useEditTransformFlyoutSelector( - (s) => s.formFields.destinationIngestPipeline - ); + const { errorMessages, value } = useFormField('destinationIngestPipeline'); const { setFormField } = useEditTransformFlyoutActions(); const { data: esIngestPipelinesData, isLoading } = useGetEsIngestPipelines(); diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_retention_policy.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_retention_policy.tsx index 7bddc0975bf04..3b9cac7dda95a 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_retention_policy.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_retention_policy.tsx @@ -6,6 +6,7 @@ */ import React, { useEffect, useMemo, type FC } from 'react'; +import { useSelector } from 'react-redux'; import { i18n } from '@kbn/i18n'; @@ -19,8 +20,10 @@ import { useGetTransformsPreview } from '../../../../hooks'; import { EditTransformFlyoutFormTextInput } from './edit_transform_flyout_form_text_input'; import { + selectFormSections, + selectRetentionPolicyField, useEditTransformFlyoutActions, - useEditTransformFlyoutSelector, + useEditTransformFlyoutContext, } from './use_edit_transform_flyout'; import { getErrorMessage } from '../../../../../../common/utils/errors'; @@ -29,12 +32,9 @@ export const EditTransformRetentionPolicy: FC = () => { const toastNotifications = useToastNotifications(); - const dataViewId = useEditTransformFlyoutSelector((s) => s.dataViewId); - const formSections = useEditTransformFlyoutSelector((s) => s.formSections); - const retentionPolicyField = useEditTransformFlyoutSelector( - (s) => s.formFields.retentionPolicyField - ); - const config = useEditTransformFlyoutSelector((s) => s.config); + const { config, dataViewId } = useEditTransformFlyoutContext(); + const formSections = useSelector(selectFormSections); + const retentionPolicyField = useSelector(selectRetentionPolicyField); const { setFormField, setFormSection } = useEditTransformFlyoutActions(); const previewRequest: PostTransformsPreviewRequestSchema = useMemo(() => { diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_update_button.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_update_button.tsx index 2b6b300667826..6a6556dc4fecf 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_update_button.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_update_button.tsx @@ -16,9 +16,11 @@ import { getErrorMessage } from '../../../../../../common/utils/errors'; import { useUpdateTransform } from '../../../../hooks'; import { - applyFormStateToTransformConfig, useEditTransformFlyoutActions, - useEditTransformFlyoutSelector, + useEditTransformFlyoutContext, + useIsFormValid, + useIsFormTouched, + useUpdatedTransformConfig, } from './use_edit_transform_flyout'; interface EditTransformUpdateButtonProps { @@ -26,9 +28,10 @@ interface EditTransformUpdateButtonProps { } export const EditTransformUpdateButton: FC = ({ closeFlyout }) => { - const { config, formFields, formSections, isFormValid, isFormTouched } = - useEditTransformFlyoutSelector((d) => d); - const requestConfig = applyFormStateToTransformConfig(config, formFields, formSections); + const { config } = useEditTransformFlyoutContext(); + const isFormValid = useIsFormValid(); + const isFormTouched = useIsFormTouched(); + const requestConfig = useUpdatedTransformConfig(); const isUpdateButtonDisabled = !isFormValid || !isFormTouched; const { setApiError } = useEditTransformFlyoutActions(); diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/use_edit_transform_flyout.test.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/use_edit_transform_flyout.test.tsx index 59b2d66c9fbc0..edbeddc4af9fd 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/use_edit_transform_flyout.test.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/use_edit_transform_flyout.test.tsx @@ -14,7 +14,9 @@ import { applyFormStateToTransformConfig, getDefaultState, useEditTransformFlyoutActions, - useEditTransformFlyoutSelector, + useFormField, + useIsFormTouched, + useIsFormValid, EditTransformFlyoutProvider, } from './use_edit_transform_flyout'; @@ -229,13 +231,15 @@ describe('Transform: useEditTransformFlyoutActions/Selector()', () => { ); // As we want to test how actions affect the state, - // we set up this custom hook that combines both the hooks for + // we set up this custom hook that combines hooks for // actions and state selection, so they react to the same redux store. const { result } = renderHook( () => { return { actions: useEditTransformFlyoutActions(), - state: useEditTransformFlyoutSelector((d) => d), + isFormTouched: useIsFormTouched(), + isFormValid: useIsFormValid(), + frequency: useFormField('frequency'), }; }, { wrapper } @@ -248,8 +252,8 @@ describe('Transform: useEditTransformFlyoutActions/Selector()', () => { }); }); - expect(result.current.state.isFormTouched).toBe(true); - expect(result.current.state.isFormValid).toBe(true); + expect(result.current.isFormTouched).toBe(true); + expect(result.current.isFormValid).toBe(true); act(() => { result.current.actions.setFormField({ @@ -258,8 +262,8 @@ describe('Transform: useEditTransformFlyoutActions/Selector()', () => { }); }); - expect(result.current.state.isFormTouched).toBe(false); - expect(result.current.state.isFormValid).toBe(true); + expect(result.current.isFormTouched).toBe(false); + expect(result.current.isFormValid).toBe(true); act(() => { result.current.actions.setFormField({ @@ -268,9 +272,9 @@ describe('Transform: useEditTransformFlyoutActions/Selector()', () => { }); }); - expect(result.current.state.isFormTouched).toBe(true); - expect(result.current.state.isFormValid).toBe(false); - expect(result.current.state.formFields.frequency.errorMessages).toStrictEqual([ + expect(result.current.isFormTouched).toBe(true); + expect(result.current.isFormValid).toBe(false); + expect(result.current.frequency.errorMessages).toStrictEqual([ 'The frequency value is not valid.', ]); }); diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/use_edit_transform_flyout.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/use_edit_transform_flyout.tsx index fad3aa1efee01..d30b594f36b69 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/use_edit_transform_flyout.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/use_edit_transform_flyout.tsx @@ -6,8 +6,8 @@ */ import { isEqual, merge } from 'lodash'; -import React, { useEffect, useMemo, type FC } from 'react'; -import { configureStore, createSlice, type PayloadAction } from '@reduxjs/toolkit'; +import React, { createContext, useContext, useEffect, useMemo, type FC } from 'react'; +import { configureStore, createSelector, createSlice, type PayloadAction } from '@reduxjs/toolkit'; import { useDispatch, useSelector, Provider } from 'react-redux'; import { bindActionCreators } from 'redux'; @@ -131,11 +131,11 @@ type EditTransformFlyoutSectionsState = Record ): FormField => { const defaultValue = overloads?.defaultValue !== undefined ? overloads.defaultValue : ''; - const rawValue = getNestedProperty(config, configFieldName, undefined); + const rawValue = getNestedProperty(config ?? {}, configFieldName, undefined); const value = rawValue !== null && rawValue !== undefined ? rawValue.toString() : ''; return { @@ -156,11 +156,11 @@ export const initializeFormField = ( export const initializeFormSection = ( formSectionName: EditTransformFormSections, configFieldName: string, - config: TransformConfigUnion, + config?: TransformConfigUnion, overloads?: Partial ): FormSection => { const defaultEnabled = overloads?.defaultEnabled ?? false; - const rawEnabled = getNestedProperty(config, configFieldName, undefined); + const rawEnabled = getNestedProperty(config ?? {}, configFieldName, undefined); const enabled = rawEnabled !== undefined && rawEnabled !== null; return { @@ -249,10 +249,22 @@ export const applyFormStateToTransformConfig = ( merge({ ...updateConfig }, getUpdateValue(field, config, formFields, formSections)), {} ); +const createSelectTransformConfig = (originalConfig: TransformConfigUnion) => + createSelector( + (state: EditTransformFlyoutState) => state.formFields, + (state: EditTransformFlyoutState) => state.formSections, + (formFields, formSections) => + applyFormStateToTransformConfig(originalConfig, formFields, formSections) + ); +export const useUpdatedTransformConfig = () => { + const { config } = useEditTransformFlyoutContext(); + const selectTransformConfig = useMemo(() => createSelectTransformConfig(config), [config]); + return useSelector(selectTransformConfig); +}; // Takes in a transform configuration and returns // the default state to populate the form. -export const getDefaultState = (config: TransformConfigUnion): EditTransformFlyoutFormState => ({ +export const getDefaultState = (config?: TransformConfigUnion): EditTransformFlyoutFormState => ({ formFields: { // top level attributes description: initializeFormField('description', 'description', config), @@ -338,14 +350,17 @@ export const getDefaultState = (config: TransformConfigUnion): EditTransformFlyo formSections: { retentionPolicy: initializeFormSection('retentionPolicy', 'retention_policy', config), }, - isFormTouched: false, - isFormValid: true, }); // Checks each form field for error messages to return // if the overall form is valid or not. const isFormValid = (fieldsState: EditTransformFlyoutFieldsState) => Object.values(fieldsState).every((d) => d.errorMessages.length === 0); +const selectIsFormValid = createSelector( + (state: EditTransformFlyoutState) => state.formFields, + (formFields) => isFormValid(formFields) +); +export const useIsFormValid = () => useSelector(selectIsFormValid); const getFieldValues = (fields: EditTransformFlyoutFieldsState) => Object.values(fields).map((f) => f.value); @@ -361,24 +376,33 @@ interface EditTransformFlyoutFormState { apiErrorMessage?: string; formFields: EditTransformFlyoutFieldsState; formSections: EditTransformFlyoutSectionsState; - isFormTouched: boolean; - isFormValid: boolean; } -const isFormTouched = (config: TransformConfigUnion, currentState: EditTransformFlyoutState) => { +const isFormTouched = ( + config: TransformConfigUnion, + formFields: EditTransformFlyoutFieldsState, + formSections: EditTransformFlyoutSectionsState +) => { const defaultState = getDefaultState(config); return ( - !isEqual(getFieldValues(defaultState.formFields), getFieldValues(currentState.formFields)) || - !isEqual( - getSectionValues(defaultState.formSections), - getSectionValues(currentState.formSections) - ) + !isEqual(getFieldValues(defaultState.formFields), getFieldValues(formFields)) || + !isEqual(getSectionValues(defaultState.formSections), getSectionValues(formSections)) + ); +}; +const createSelectIsFormTouched = (originalConfig: TransformConfigUnion) => + createSelector( + (state: EditTransformFlyoutState) => state.formFields, + (state: EditTransformFlyoutState) => state.formSections, + (formFields, formSections) => isFormTouched(originalConfig, formFields, formSections) ); +export const useIsFormTouched = () => { + const { config } = useEditTransformFlyoutContext(); + const selectIsFormTouched = useMemo(() => createSelectIsFormTouched(config), [config]); + return useSelector(selectIsFormTouched); }; // The state we manage via redux combines the provider props and the form state. -export type EditTransformFlyoutState = EditTransformFlyoutProviderProps & - EditTransformFlyoutFormState; +export type EditTransformFlyoutState = EditTransformFlyoutFormState; function isFormFieldOptional(state: EditTransformFlyoutState, field: EditTransformFormFields) { const formField = state.formFields[field]; @@ -406,16 +430,10 @@ function getFormFieldErrorMessages( const editTransformFlyoutSlice = createSlice({ name: 'editTransformFlyout', - initialState: undefined as EditTransformFlyoutState | undefined, + initialState: getDefaultState(), reducers: { - initialize: (_, action: PayloadAction) => { - const defaultState = getDefaultState(action.payload.config); - return { - ...defaultState, - config: action.payload.config, - dataViewId: action.payload.dataViewId, - }; - }, + initialize: (_, action: PayloadAction) => + getDefaultState(action.payload.config), setApiError: (state, action: PayloadAction) => { if (!state) return; state.apiErrorMessage = action.payload; @@ -438,9 +456,6 @@ const editTransformFlyoutSlice = createSlice({ ); formField.value = action.payload.value; - - state.isFormTouched = isFormTouched(state.config, state); - state.isFormValid = isFormValid(state.formFields); }, // Updates a form section. setFormSection: ( @@ -462,13 +477,12 @@ const editTransformFlyoutSlice = createSlice({ formField.validator ); }); - - state.isFormTouched = isFormTouched(state.config, state); - state.isFormValid = isFormValid(state.formFields); }, }, }); +const EditTransformFlyoutContext = createContext(null); + export const EditTransformFlyoutProvider: FC< React.PropsWithChildren > = ({ children, ...props }) => { @@ -480,21 +494,38 @@ export const EditTransformFlyoutProvider: FC< [] ); - // Initialize redux state. The store's `initialState` is `undefined` since - // we only get the transform config and data view id during runtime. + // Apply original transform config to redux form state. useEffect(() => { store.dispatch(editTransformFlyoutSlice.actions.initialize(props)); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); - return {children}; + return ( + + {children} + + ); +}; + +export const useEditTransformFlyoutContext = () => { + const c = useContext(EditTransformFlyoutContext); + if (c === null) throw new Error('EditTransformFlyoutContext not set.'); + return c; }; -export function useEditTransformFlyoutActions() { +export const useEditTransformFlyoutActions = () => { const dispatch = useDispatch(); return bindActionCreators(editTransformFlyoutSlice.actions, dispatch); -} +}; -export function useEditTransformFlyoutSelector(selector: (s: EditTransformFlyoutState) => T) { - return useSelector(selector); -} +const createSelectFormField = (field: EditTransformFormFields) => (s: EditTransformFlyoutState) => + s.formFields[field]; +export const useFormField = (field: EditTransformFormFields) => { + const selectFormField = useMemo(() => createSelectFormField(field), [field]); + return useSelector(selectFormField); +}; + +export const selectApiErrorMessage = (s: EditTransformFlyoutState) => s.apiErrorMessage; +export const selectFormSections = (s: EditTransformFlyoutState) => s.formSections; +export const selectRetentionPolicyField = (s: EditTransformFlyoutState) => + s.formFields.retentionPolicyField; From eb1506adebd1badbfc2a8a6ed21ca60ac4cb6d8f Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Wed, 3 Jan 2024 13:43:17 +0100 Subject: [PATCH 22/36] renaming --- .../use_edit_transform_flyout.tsx | 43 +++++++------------ 1 file changed, 15 insertions(+), 28 deletions(-) diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/use_edit_transform_flyout.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/use_edit_transform_flyout.tsx index d30b594f36b69..d4938792d416a 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/use_edit_transform_flyout.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/use_edit_transform_flyout.tsx @@ -45,18 +45,10 @@ const validators = { }; type ValidatorName = keyof typeof validators; -type DefaultParser = (v: string) => string; -type NullableNumberParser = (v: string) => number | null; -type NumberParser = (v: string) => number; - -const defaultParser: DefaultParser = (v) => v; -const nullableNumberParser: NullableNumberParser = (v) => (v === '' ? null : +v); -const numberParser: NumberParser = (v) => +v; - const valueParsers = { - defaultParser, - nullableNumberParser, - numberParser, + defaultParser: (v: string) => v, + nullableNumberParser: (v: string) => (v === '' ? null : +v), + numberParser: (v: string) => +v, }; type ValueParserName = keyof typeof valueParsers; @@ -251,8 +243,8 @@ export const applyFormStateToTransformConfig = ( ); const createSelectTransformConfig = (originalConfig: TransformConfigUnion) => createSelector( - (state: EditTransformFlyoutState) => state.formFields, - (state: EditTransformFlyoutState) => state.formSections, + (state: State) => state.formFields, + (state: State) => state.formSections, (formFields, formSections) => applyFormStateToTransformConfig(originalConfig, formFields, formSections) ); @@ -264,7 +256,7 @@ export const useUpdatedTransformConfig = () => { // Takes in a transform configuration and returns // the default state to populate the form. -export const getDefaultState = (config?: TransformConfigUnion): EditTransformFlyoutFormState => ({ +export const getDefaultState = (config?: TransformConfigUnion): State => ({ formFields: { // top level attributes description: initializeFormField('description', 'description', config), @@ -357,7 +349,7 @@ export const getDefaultState = (config?: TransformConfigUnion): EditTransformFly const isFormValid = (fieldsState: EditTransformFlyoutFieldsState) => Object.values(fieldsState).every((d) => d.errorMessages.length === 0); const selectIsFormValid = createSelector( - (state: EditTransformFlyoutState) => state.formFields, + (state: State) => state.formFields, (formFields) => isFormValid(formFields) ); export const useIsFormValid = () => useSelector(selectIsFormValid); @@ -372,7 +364,7 @@ interface EditTransformFlyoutProviderProps { dataViewId?: string; } -interface EditTransformFlyoutFormState { +interface State { apiErrorMessage?: string; formFields: EditTransformFlyoutFieldsState; formSections: EditTransformFlyoutSectionsState; @@ -391,8 +383,8 @@ const isFormTouched = ( }; const createSelectIsFormTouched = (originalConfig: TransformConfigUnion) => createSelector( - (state: EditTransformFlyoutState) => state.formFields, - (state: EditTransformFlyoutState) => state.formSections, + (state: State) => state.formFields, + (state: State) => state.formSections, (formFields, formSections) => isFormTouched(originalConfig, formFields, formSections) ); export const useIsFormTouched = () => { @@ -401,10 +393,7 @@ export const useIsFormTouched = () => { return useSelector(selectIsFormTouched); }; -// The state we manage via redux combines the provider props and the form state. -export type EditTransformFlyoutState = EditTransformFlyoutFormState; - -function isFormFieldOptional(state: EditTransformFlyoutState, field: EditTransformFormFields) { +function isFormFieldOptional(state: State, field: EditTransformFormFields) { const formField = state.formFields[field]; let isOptional = formField.isOptional; @@ -518,14 +507,12 @@ export const useEditTransformFlyoutActions = () => { return bindActionCreators(editTransformFlyoutSlice.actions, dispatch); }; -const createSelectFormField = (field: EditTransformFormFields) => (s: EditTransformFlyoutState) => - s.formFields[field]; +const createSelectFormField = (field: EditTransformFormFields) => (s: State) => s.formFields[field]; export const useFormField = (field: EditTransformFormFields) => { const selectFormField = useMemo(() => createSelectFormField(field), [field]); return useSelector(selectFormField); }; -export const selectApiErrorMessage = (s: EditTransformFlyoutState) => s.apiErrorMessage; -export const selectFormSections = (s: EditTransformFlyoutState) => s.formSections; -export const selectRetentionPolicyField = (s: EditTransformFlyoutState) => - s.formFields.retentionPolicyField; +export const selectApiErrorMessage = (s: State) => s.apiErrorMessage; +export const selectFormSections = (s: State) => s.formSections; +export const selectRetentionPolicyField = (s: State) => s.formFields.retentionPolicyField; From 6e9b88a7c03ed319a6853b8874866436e8638375 Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Wed, 3 Jan 2024 13:49:52 +0100 Subject: [PATCH 23/36] renaming --- .../use_edit_transform_flyout.tsx | 65 +++++++++---------- 1 file changed, 29 insertions(+), 36 deletions(-) diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/use_edit_transform_flyout.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/use_edit_transform_flyout.tsx index d4938792d416a..0a3716056a00e 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/use_edit_transform_flyout.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/use_edit_transform_flyout.tsx @@ -61,7 +61,7 @@ type ValueParserName = keyof typeof valueParsers; // The form state defines a flat structure of names for form fields. // This is a flat structure regardless of whether the final config object will be nested. // For example, `destinationIndex` and `destinationIngestPipeline` will later be nested under `dest`. -export type EditTransformFormFields = +export type FormFields = | 'description' | 'destinationIndex' | 'destinationIngestPipeline' @@ -72,18 +72,18 @@ export type EditTransformFormFields = | 'retentionPolicyField' | 'retentionPolicyMaxAge'; -type EditTransformFlyoutFieldsState = Record; +type FormFieldsState = Record; export interface FormField { - formFieldName: EditTransformFormFields; + formFieldName: FormFields; configFieldName: string; defaultValue: string; - dependsOn: EditTransformFormFields[]; + dependsOn: FormFields[]; errorMessages: string[]; isNullable: boolean; isOptional: boolean; isOptionalInSection?: boolean; - section?: EditTransformFormSections; + section?: FormSections; validator: ValidatorName; value: string; valueParser: ValueParserName; @@ -94,16 +94,16 @@ export interface FormField { // this overall part of the configuration is not optional. However, `retention_policy` is optional, // so we need to support to recognize this based on the form state and be able to reset it by // created a request body containing `{ retention_policy: null }`. -type EditTransformFormSections = 'retentionPolicy'; +type FormSections = 'retentionPolicy'; export interface FormSection { - formSectionName: EditTransformFormSections; + formSectionName: FormSections; configFieldName: string; defaultEnabled: boolean; enabled: boolean; } -type EditTransformFlyoutSectionsState = Record; +type FormSectionsState = Record; // The utility functions in this file provide the following features: // - getDefaultState() @@ -121,7 +121,7 @@ type EditTransformFlyoutSectionsState = Record @@ -146,7 +146,7 @@ export const initializeFormField = ( }; export const initializeFormSection = ( - formSectionName: EditTransformFormSections, + formSectionName: FormSections, configFieldName: string, config?: TransformConfigUnion, overloads?: Partial @@ -167,10 +167,10 @@ export const initializeFormSection = ( // of the expected final configuration request object. // Considers options like if a value is nullable or optional. const getUpdateValue = ( - attribute: EditTransformFormFields, + attribute: FormFields, config: TransformConfigUnion, - formFields: EditTransformFlyoutFieldsState, - formSections: EditTransformFlyoutSectionsState, + formFields: FormFieldsState, + formSections: FormSectionsState, enforceFormValue = false ) => { const formStateAttribute = formFields[attribute]; @@ -231,12 +231,12 @@ const getUpdateValue = ( // transform update API endpoint. export const applyFormStateToTransformConfig = ( config: TransformConfigUnion, - formFields: EditTransformFlyoutFieldsState, - formSections: EditTransformFlyoutSectionsState + formFields: FormFieldsState, + formSections: FormSectionsState ): PostTransformsUpdateRequestSchema => // Iterates over all form fields and only if necessary applies them to // the request object used for updating the transform. - (Object.keys(formFields) as EditTransformFormFields[]).reduce( + (Object.keys(formFields) as FormFields[]).reduce( (updateConfig, field) => merge({ ...updateConfig }, getUpdateValue(field, config, formFields, formSections)), {} @@ -346,7 +346,7 @@ export const getDefaultState = (config?: TransformConfigUnion): State => ({ // Checks each form field for error messages to return // if the overall form is valid or not. -const isFormValid = (fieldsState: EditTransformFlyoutFieldsState) => +const isFormValid = (fieldsState: FormFieldsState) => Object.values(fieldsState).every((d) => d.errorMessages.length === 0); const selectIsFormValid = createSelector( (state: State) => state.formFields, @@ -354,9 +354,8 @@ const selectIsFormValid = createSelector( ); export const useIsFormValid = () => useSelector(selectIsFormValid); -const getFieldValues = (fields: EditTransformFlyoutFieldsState) => - Object.values(fields).map((f) => f.value); -const getSectionValues = (sections: EditTransformFlyoutSectionsState) => +const getFieldValues = (fields: FormFieldsState) => Object.values(fields).map((f) => f.value); +const getSectionValues = (sections: FormSectionsState) => Object.values(sections).map((s) => s.enabled); interface EditTransformFlyoutProviderProps { @@ -366,14 +365,14 @@ interface EditTransformFlyoutProviderProps { interface State { apiErrorMessage?: string; - formFields: EditTransformFlyoutFieldsState; - formSections: EditTransformFlyoutSectionsState; + formFields: FormFieldsState; + formSections: FormSectionsState; } const isFormTouched = ( config: TransformConfigUnion, - formFields: EditTransformFlyoutFieldsState, - formSections: EditTransformFlyoutSectionsState + formFields: FormFieldsState, + formSections: FormSectionsState ) => { const defaultState = getDefaultState(config); return ( @@ -393,7 +392,7 @@ export const useIsFormTouched = () => { return useSelector(selectIsFormTouched); }; -function isFormFieldOptional(state: State, field: EditTransformFormFields) { +function isFormFieldOptional(state: State, field: FormFields) { const formField = state.formFields[field]; let isOptional = formField.isOptional; @@ -429,10 +428,7 @@ const editTransformFlyoutSlice = createSlice({ }, // Updates a form field with its new value, runs validation and // populates `errorMessages` if any errors occur. - setFormField: ( - state, - action: PayloadAction<{ field: EditTransformFormFields; value: string }> - ) => { + setFormField: (state, action: PayloadAction<{ field: FormFields; value: string }>) => { if (!state) return; const formField = state.formFields[action.payload.field]; @@ -447,10 +443,7 @@ const editTransformFlyoutSlice = createSlice({ formField.value = action.payload.value; }, // Updates a form section. - setFormSection: ( - state, - action: PayloadAction<{ section: EditTransformFormSections; enabled: boolean }> - ) => { + setFormSection: (state, action: PayloadAction<{ section: FormSections; enabled: boolean }>) => { if (!state) return; state.formSections[action.payload.section].enabled = action.payload.enabled; @@ -458,7 +451,7 @@ const editTransformFlyoutSlice = createSlice({ // After a section change we re-evaluate all form fields, since optionality // of a field could change if a section got toggled. Object.entries(state.formFields).forEach(([formFieldName, formField]) => { - const isOptional = isFormFieldOptional(state, formFieldName as EditTransformFormFields); + const isOptional = isFormFieldOptional(state, formFieldName as FormFields); formField.errorMessages = getFormFieldErrorMessages( formField.value, @@ -507,8 +500,8 @@ export const useEditTransformFlyoutActions = () => { return bindActionCreators(editTransformFlyoutSlice.actions, dispatch); }; -const createSelectFormField = (field: EditTransformFormFields) => (s: State) => s.formFields[field]; -export const useFormField = (field: EditTransformFormFields) => { +const createSelectFormField = (field: FormFields) => (s: State) => s.formFields[field]; +export const useFormField = (field: FormFields) => { const selectFormField = useMemo(() => createSelectFormField(field), [field]); return useSelector(selectFormField); }; From 1b606c55fef3107427ba78e5d74f566d5c2f56c4 Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Wed, 3 Jan 2024 13:56:28 +0100 Subject: [PATCH 24/36] remove unnecessary state checks --- .../edit_transform_flyout/use_edit_transform_flyout.tsx | 5 ----- 1 file changed, 5 deletions(-) diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/use_edit_transform_flyout.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/use_edit_transform_flyout.tsx index 0a3716056a00e..5874d7cf664b8 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/use_edit_transform_flyout.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/use_edit_transform_flyout.tsx @@ -423,14 +423,11 @@ const editTransformFlyoutSlice = createSlice({ initialize: (_, action: PayloadAction) => getDefaultState(action.payload.config), setApiError: (state, action: PayloadAction) => { - if (!state) return; state.apiErrorMessage = action.payload; }, // Updates a form field with its new value, runs validation and // populates `errorMessages` if any errors occur. setFormField: (state, action: PayloadAction<{ field: FormFields; value: string }>) => { - if (!state) return; - const formField = state.formFields[action.payload.field]; const isOptional = isFormFieldOptional(state, action.payload.field); @@ -444,8 +441,6 @@ const editTransformFlyoutSlice = createSlice({ }, // Updates a form section. setFormSection: (state, action: PayloadAction<{ section: FormSections; enabled: boolean }>) => { - if (!state) return; - state.formSections[action.payload.section].enabled = action.payload.enabled; // After a section change we re-evaluate all form fields, since optionality From 41b2b9fb0ddd65d5cb4058b23bc480b866a203c1 Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Wed, 3 Jan 2024 14:59:14 +0100 Subject: [PATCH 25/36] break out reducer actions from createSlice --- .../use_edit_transform_flyout.tsx | 114 ++++++++++-------- 1 file changed, 65 insertions(+), 49 deletions(-) diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/use_edit_transform_flyout.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/use_edit_transform_flyout.tsx index 5874d7cf664b8..20a5fa4ae9da7 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/use_edit_transform_flyout.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/use_edit_transform_flyout.tsx @@ -6,7 +6,14 @@ */ import { isEqual, merge } from 'lodash'; -import React, { createContext, useContext, useEffect, useMemo, type FC } from 'react'; +import React, { + createContext, + useContext, + useEffect, + useMemo, + type FC, + type PropsWithChildren, +} from 'react'; import { configureStore, createSelector, createSlice, type PayloadAction } from '@reduxjs/toolkit'; import { useDispatch, useSelector, Provider } from 'react-redux'; import { bindActionCreators } from 'redux'; @@ -358,7 +365,7 @@ const getFieldValues = (fields: FormFieldsState) => Object.values(fields).map((f const getSectionValues = (sections: FormSectionsState) => Object.values(sections).map((s) => s.enabled); -interface EditTransformFlyoutProviderProps { +interface ProviderProps { config: TransformConfigUnion; dataViewId?: string; } @@ -415,61 +422,70 @@ function getFormFieldErrorMessages( ? [] : validators[validatorName](value, isOptional); } +const initialize = (_: State, action: PayloadAction) => + getDefaultState(action.payload.config); + +const setApiError = (state: State, action: PayloadAction) => { + state.apiErrorMessage = action.payload; +}; + +const setFormField = ( + state: State, + action: PayloadAction<{ field: FormFields; value: string }> +) => { + const formField = state.formFields[action.payload.field]; + const isOptional = isFormFieldOptional(state, action.payload.field); + + formField.errorMessages = getFormFieldErrorMessages( + action.payload.value, + isOptional, + formField.validator + ); + + formField.value = action.payload.value; +}; + +const setFormSection = ( + state: State, + action: PayloadAction<{ section: FormSections; enabled: boolean }> +) => { + state.formSections[action.payload.section].enabled = action.payload.enabled; + + // After a section change we re-evaluate all form fields, since optionality + // of a field could change if a section got toggled. + Object.entries(state.formFields).forEach(([formFieldName, formField]) => { + const isOptional = isFormFieldOptional(state, formFieldName as FormFields); + formField.errorMessages = getFormFieldErrorMessages( + formField.value, + isOptional, + formField.validator + ); + }); +}; const editTransformFlyoutSlice = createSlice({ name: 'editTransformFlyout', initialState: getDefaultState(), reducers: { - initialize: (_, action: PayloadAction) => - getDefaultState(action.payload.config), - setApiError: (state, action: PayloadAction) => { - state.apiErrorMessage = action.payload; - }, - // Updates a form field with its new value, runs validation and - // populates `errorMessages` if any errors occur. - setFormField: (state, action: PayloadAction<{ field: FormFields; value: string }>) => { - const formField = state.formFields[action.payload.field]; - const isOptional = isFormFieldOptional(state, action.payload.field); - - formField.errorMessages = getFormFieldErrorMessages( - action.payload.value, - isOptional, - formField.validator - ); - - formField.value = action.payload.value; - }, - // Updates a form section. - setFormSection: (state, action: PayloadAction<{ section: FormSections; enabled: boolean }>) => { - state.formSections[action.payload.section].enabled = action.payload.enabled; - - // After a section change we re-evaluate all form fields, since optionality - // of a field could change if a section got toggled. - Object.entries(state.formFields).forEach(([formFieldName, formField]) => { - const isOptional = isFormFieldOptional(state, formFieldName as FormFields); - - formField.errorMessages = getFormFieldErrorMessages( - formField.value, - isOptional, - formField.validator - ); - }); - }, + initialize, + setApiError, + setFormField, + setFormSection, }, }); -const EditTransformFlyoutContext = createContext(null); - -export const EditTransformFlyoutProvider: FC< - React.PropsWithChildren -> = ({ children, ...props }) => { - const store = useMemo( - () => - configureStore({ - reducer: editTransformFlyoutSlice.reducer, - }), - [] - ); +const getReduxStore = () => + configureStore({ + reducer: editTransformFlyoutSlice.reducer, + }); + +const EditTransformFlyoutContext = createContext(null); + +export const EditTransformFlyoutProvider: FC> = ({ + children, + ...props +}) => { + const store = useMemo(getReduxStore, []); // Apply original transform config to redux form state. useEffect(() => { From c8a6c6e5595d3e0245398abcdac83521bf2271cc Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Wed, 3 Jan 2024 16:20:58 +0100 Subject: [PATCH 26/36] fix types --- .../edit_transform_flyout_form_text_area.tsx | 4 ++-- .../edit_transform_flyout_form_text_input.tsx | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_flyout_form_text_area.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_flyout_form_text_area.tsx index c5dab9b848140..7ba2be2b4f5a5 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_flyout_form_text_area.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_flyout_form_text_area.tsx @@ -14,12 +14,12 @@ import { i18n } from '@kbn/i18n'; import { useEditTransformFlyoutActions, useFormField, - type EditTransformFormFields, + type FormFields, } from './use_edit_transform_flyout'; import { capitalizeFirstLetter } from './capitalize_first_letter'; interface EditTransformFlyoutFormTextInputProps { - field: EditTransformFormFields; + field: FormFields; label: string; helpText?: string; placeHolder?: boolean; diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_flyout_form_text_input.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_flyout_form_text_input.tsx index c20ff9cfa2541..214f411c42d1c 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_flyout_form_text_input.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_flyout_form_text_input.tsx @@ -14,12 +14,12 @@ import { i18n } from '@kbn/i18n'; import { useEditTransformFlyoutActions, useFormField, - type EditTransformFormFields, + type FormFields, } from './use_edit_transform_flyout'; import { capitalizeFirstLetter } from './capitalize_first_letter'; interface EditTransformFlyoutFormTextInputProps { - field: EditTransformFormFields; + field: FormFields; label: string; helpText?: string; placeHolder?: boolean; From 3ccbdde3d855131a89016051e17fa63822c08c5a Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Wed, 3 Jan 2024 16:21:48 +0100 Subject: [PATCH 27/36] minor cleanup --- .../edit_transform_flyout/use_edit_transform_flyout.tsx | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/use_edit_transform_flyout.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/use_edit_transform_flyout.tsx index 20a5fa4ae9da7..512c51d2182e8 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/use_edit_transform_flyout.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/use_edit_transform_flyout.tsx @@ -353,12 +353,9 @@ export const getDefaultState = (config?: TransformConfigUnion): State => ({ // Checks each form field for error messages to return // if the overall form is valid or not. -const isFormValid = (fieldsState: FormFieldsState) => - Object.values(fieldsState).every((d) => d.errorMessages.length === 0); -const selectIsFormValid = createSelector( - (state: State) => state.formFields, - (formFields) => isFormValid(formFields) -); +const isFormValid = (formFields: FormFieldsState) => + Object.values(formFields).every((d) => d.errorMessages.length === 0); +const selectIsFormValid = createSelector((state: State) => state.formFields, isFormValid); export const useIsFormValid = () => useSelector(selectIsFormValid); const getFieldValues = (fields: FormFieldsState) => Object.values(fields).map((f) => f.value); From 344dc4fce5af00ee5e2da9009bb91eaeff8c64c8 Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Wed, 3 Jan 2024 17:40:38 +0100 Subject: [PATCH 28/36] minor refactor --- .../use_edit_transform_flyout.test.tsx | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/use_edit_transform_flyout.test.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/use_edit_transform_flyout.test.tsx index edbeddc4af9fd..8ce7dfaf67a75 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/use_edit_transform_flyout.test.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/use_edit_transform_flyout.test.tsx @@ -233,17 +233,14 @@ describe('Transform: useEditTransformFlyoutActions/Selector()', () => { // As we want to test how actions affect the state, // we set up this custom hook that combines hooks for // actions and state selection, so they react to the same redux store. - const { result } = renderHook( - () => { - return { - actions: useEditTransformFlyoutActions(), - isFormTouched: useIsFormTouched(), - isFormValid: useIsFormValid(), - frequency: useFormField('frequency'), - }; - }, - { wrapper } - ); + const useHooks = () => ({ + actions: useEditTransformFlyoutActions(), + isFormTouched: useIsFormTouched(), + isFormValid: useIsFormValid(), + frequency: useFormField('frequency'), + }); + + const { result } = renderHook(useHooks, { wrapper }); act(() => { result.current.actions.setFormField({ From d09f769015c4122a6118b859522ff70ed6755e7c Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Thu, 4 Jan 2024 09:50:22 +0100 Subject: [PATCH 29/36] rename use_edit_transform_flyout to edit_transform_flyout_state --- .../edit_transform_flyout/edit_transform_api_error_callout.tsx | 2 +- .../components/edit_transform_flyout/edit_transform_flyout.tsx | 2 +- .../edit_transform_flyout_form_text_area.tsx | 2 +- .../edit_transform_flyout_form_text_input.tsx | 2 +- ...orm_flyout.test.tsx => edit_transform_flyout_state.test.tsx} | 2 +- ...dit_transform_flyout.tsx => edit_transform_flyout_state.tsx} | 0 .../edit_transform_flyout/edit_transform_ingest_pipeline.tsx | 2 +- .../edit_transform_flyout/edit_transform_retention_policy.tsx | 2 +- .../edit_transform_flyout/edit_transform_update_button.tsx | 2 +- 9 files changed, 8 insertions(+), 8 deletions(-) rename x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/{use_edit_transform_flyout.test.tsx => edit_transform_flyout_state.test.tsx} (99%) rename x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/{use_edit_transform_flyout.tsx => edit_transform_flyout_state.tsx} (100%) diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_api_error_callout.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_api_error_callout.tsx index 999add067a7a3..027106250a873 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_api_error_callout.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_api_error_callout.tsx @@ -12,7 +12,7 @@ import { EuiCallOut, EuiSpacer } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { selectApiErrorMessage } from './use_edit_transform_flyout'; +import { selectApiErrorMessage } from './edit_transform_flyout_state'; export const EditTransformApiErrorCallout: FC = () => { const apiErrorMessage = useSelector(selectApiErrorMessage); diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_flyout.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_flyout.tsx index c3ff7198a44fa..fada9746d3a70 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_flyout.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_flyout.tsx @@ -28,7 +28,7 @@ import type { EditAction } from '../action_edit'; import { EditTransformApiErrorCallout } from './edit_transform_api_error_callout'; import { EditTransformFlyoutCallout } from './edit_transform_flyout_callout'; import { EditTransformFlyoutForm } from './edit_transform_flyout_form'; -import { EditTransformFlyoutProvider } from './use_edit_transform_flyout'; +import { EditTransformFlyoutProvider } from './edit_transform_flyout_state'; import { EditTransformUpdateButton } from './edit_transform_update_button'; export const EditTransformFlyout: FC = ({ diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_flyout_form_text_area.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_flyout_form_text_area.tsx index 7ba2be2b4f5a5..3494a2216409d 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_flyout_form_text_area.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_flyout_form_text_area.tsx @@ -15,7 +15,7 @@ import { useEditTransformFlyoutActions, useFormField, type FormFields, -} from './use_edit_transform_flyout'; +} from './edit_transform_flyout_state'; import { capitalizeFirstLetter } from './capitalize_first_letter'; interface EditTransformFlyoutFormTextInputProps { diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_flyout_form_text_input.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_flyout_form_text_input.tsx index 214f411c42d1c..05df83d5d1ec3 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_flyout_form_text_input.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_flyout_form_text_input.tsx @@ -15,7 +15,7 @@ import { useEditTransformFlyoutActions, useFormField, type FormFields, -} from './use_edit_transform_flyout'; +} from './edit_transform_flyout_state'; import { capitalizeFirstLetter } from './capitalize_first_letter'; interface EditTransformFlyoutFormTextInputProps { diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/use_edit_transform_flyout.test.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_flyout_state.test.tsx similarity index 99% rename from x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/use_edit_transform_flyout.test.tsx rename to x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_flyout_state.test.tsx index 8ce7dfaf67a75..8e4e1a28eb224 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/use_edit_transform_flyout.test.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_flyout_state.test.tsx @@ -18,7 +18,7 @@ import { useIsFormTouched, useIsFormValid, EditTransformFlyoutProvider, -} from './use_edit_transform_flyout'; +} from './edit_transform_flyout_state'; const getTransformConfigMock = (): TransformPivotConfig => ({ id: 'the-transform-id', diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/use_edit_transform_flyout.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_flyout_state.tsx similarity index 100% rename from x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/use_edit_transform_flyout.tsx rename to x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_flyout_state.tsx diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_ingest_pipeline.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_ingest_pipeline.tsx index 5ca88d51af526..7a120e11a0d02 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_ingest_pipeline.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_ingest_pipeline.tsx @@ -14,7 +14,7 @@ import { i18n } from '@kbn/i18n'; import { useGetEsIngestPipelines } from '../../../../hooks'; import { EditTransformFlyoutFormTextInput } from './edit_transform_flyout_form_text_input'; -import { useEditTransformFlyoutActions, useFormField } from './use_edit_transform_flyout'; +import { useEditTransformFlyoutActions, useFormField } from './edit_transform_flyout_state'; const ingestPipelineLabel = i18n.translate( 'xpack.transform.transformList.editFlyoutFormDestinationIngestPipelineLabel', diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_retention_policy.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_retention_policy.tsx index 3b9cac7dda95a..5971791b8a9fd 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_retention_policy.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_retention_policy.tsx @@ -24,7 +24,7 @@ import { selectRetentionPolicyField, useEditTransformFlyoutActions, useEditTransformFlyoutContext, -} from './use_edit_transform_flyout'; +} from './edit_transform_flyout_state'; import { getErrorMessage } from '../../../../../../common/utils/errors'; export const EditTransformRetentionPolicy: FC = () => { diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_update_button.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_update_button.tsx index 6a6556dc4fecf..abded0596778e 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_update_button.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_update_button.tsx @@ -21,7 +21,7 @@ import { useIsFormValid, useIsFormTouched, useUpdatedTransformConfig, -} from './use_edit_transform_flyout'; +} from './edit_transform_flyout_state'; interface EditTransformUpdateButtonProps { closeFlyout: () => void; From 1989f7edade08203789172e0131e75a21e702e6f Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Thu, 4 Jan 2024 09:56:13 +0100 Subject: [PATCH 30/36] update comment --- .../edit_transform_flyout/edit_transform_flyout_state.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_flyout_state.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_flyout_state.tsx index 512c51d2182e8..60fa2b7be8beb 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_flyout_state.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_flyout_state.tsx @@ -59,8 +59,8 @@ const valueParsers = { }; type ValueParserName = keyof typeof valueParsers; -// This custom hook uses redux-toolkit to provide a generic framework to manage form state -// and apply it to a final possibly nested configuration object suitable for passing on +// The edit transform flyout uses a generic framework based on redux-toolkit to manage its form state +// that supports applying its tate to a nested configuration object suitable for passing on // directly to an API call. For now this is only used for the transform edit form. // Once we apply the functionality to other places, e.g. the transform creation wizard, // the generic framework code in this file should be moved to a dedicated location. From af818e68333a8a232ea5219eaed05548f9f91322 Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Thu, 4 Jan 2024 11:07:38 +0100 Subject: [PATCH 31/36] split up state management --- .../edit_transform_api_error_callout.tsx | 5 +- .../components}/edit_transform_flyout.tsx | 9 +- .../edit_transform_flyout_callout.tsx | 2 +- .../edit_transform_flyout_form.tsx | 0 .../edit_transform_flyout_form_text_area.tsx | 10 +- .../edit_transform_flyout_form_text_input.tsx | 10 +- .../edit_transform_ingest_pipeline.tsx | 6 +- .../edit_transform_retention_policy.tsx | 32 +- .../edit_transform_update_button.tsx | 12 +- .../index.ts | 2 +- .../__mocks__/transform_config.ts | 38 ++ .../state_management/actions.ts | 79 +++ ...ly_form_state_to_transform_config.test.ts} | 102 +--- .../apply_form_state_to_transform_config.ts | 37 ++ .../edit_transform_flyout_state.test.tsx | 75 +++ .../edit_transform_flyout_state.tsx | 90 +++ .../state_management/form_field.ts | 70 +++ .../state_management/form_section.ts | 44 ++ .../state_management/get_default_state.ts | 109 ++++ .../state_management/get_update_value.ts | 80 +++ .../selectors/api_error_message.ts | 16 + .../state_management/selectors/form_field.ts | 21 + .../selectors/form_sections.ts | 16 + .../selectors/is_form_touched.ts | 50 ++ .../selectors/is_form_valid.ts | 20 + .../selectors/retention_policy_field.ts | 16 + .../selectors/updated_transform_config.ts | 29 + .../state_management/validators.ts | 25 + .../state_management/value_parsers.ts | 19 + .../utils}/capitalize_first_letter.ts | 0 .../edit_transform_flyout_state.tsx | 519 ------------------ .../components/transform_list/use_actions.tsx | 2 +- 32 files changed, 882 insertions(+), 663 deletions(-) rename x-pack/plugins/transform/public/app/sections/{transform_management/components/edit_transform_flyout => edit_transform/components}/edit_transform_api_error_callout.tsx (83%) rename x-pack/plugins/transform/public/app/sections/{transform_management/components/edit_transform_flyout => edit_transform/components}/edit_transform_flyout.tsx (87%) rename x-pack/plugins/transform/public/app/sections/{transform_management/components/edit_transform_flyout => edit_transform/components}/edit_transform_flyout_callout.tsx (94%) rename x-pack/plugins/transform/public/app/sections/{transform_management/components/edit_transform_flyout => edit_transform/components}/edit_transform_flyout_form.tsx (100%) rename x-pack/plugins/transform/public/app/sections/{transform_management/components/edit_transform_flyout => edit_transform/components}/edit_transform_flyout_form_text_area.tsx (83%) rename x-pack/plugins/transform/public/app/sections/{transform_management/components/edit_transform_flyout => edit_transform/components}/edit_transform_flyout_form_text_input.tsx (83%) rename x-pack/plugins/transform/public/app/sections/{transform_management/components/edit_transform_flyout => edit_transform/components}/edit_transform_ingest_pipeline.tsx (92%) rename x-pack/plugins/transform/public/app/sections/{transform_management/components/edit_transform_flyout => edit_transform/components}/edit_transform_retention_policy.tsx (91%) rename x-pack/plugins/transform/public/app/sections/{transform_management/components/edit_transform_flyout => edit_transform/components}/edit_transform_update_button.tsx (77%) rename x-pack/plugins/transform/public/app/sections/{transform_management/components/edit_transform_flyout => edit_transform}/index.ts (77%) create mode 100644 x-pack/plugins/transform/public/app/sections/edit_transform/state_management/__mocks__/transform_config.ts create mode 100644 x-pack/plugins/transform/public/app/sections/edit_transform/state_management/actions.ts rename x-pack/plugins/transform/public/app/sections/{transform_management/components/edit_transform_flyout/edit_transform_flyout_state.test.tsx => edit_transform/state_management/apply_form_state_to_transform_config.test.ts} (69%) create mode 100644 x-pack/plugins/transform/public/app/sections/edit_transform/state_management/apply_form_state_to_transform_config.ts create mode 100644 x-pack/plugins/transform/public/app/sections/edit_transform/state_management/edit_transform_flyout_state.test.tsx create mode 100644 x-pack/plugins/transform/public/app/sections/edit_transform/state_management/edit_transform_flyout_state.tsx create mode 100644 x-pack/plugins/transform/public/app/sections/edit_transform/state_management/form_field.ts create mode 100644 x-pack/plugins/transform/public/app/sections/edit_transform/state_management/form_section.ts create mode 100644 x-pack/plugins/transform/public/app/sections/edit_transform/state_management/get_default_state.ts create mode 100644 x-pack/plugins/transform/public/app/sections/edit_transform/state_management/get_update_value.ts create mode 100644 x-pack/plugins/transform/public/app/sections/edit_transform/state_management/selectors/api_error_message.ts create mode 100644 x-pack/plugins/transform/public/app/sections/edit_transform/state_management/selectors/form_field.ts create mode 100644 x-pack/plugins/transform/public/app/sections/edit_transform/state_management/selectors/form_sections.ts create mode 100644 x-pack/plugins/transform/public/app/sections/edit_transform/state_management/selectors/is_form_touched.ts create mode 100644 x-pack/plugins/transform/public/app/sections/edit_transform/state_management/selectors/is_form_valid.ts create mode 100644 x-pack/plugins/transform/public/app/sections/edit_transform/state_management/selectors/retention_policy_field.ts create mode 100644 x-pack/plugins/transform/public/app/sections/edit_transform/state_management/selectors/updated_transform_config.ts create mode 100644 x-pack/plugins/transform/public/app/sections/edit_transform/state_management/validators.ts create mode 100644 x-pack/plugins/transform/public/app/sections/edit_transform/state_management/value_parsers.ts rename x-pack/plugins/transform/public/app/sections/{transform_management/components/edit_transform_flyout => edit_transform/utils}/capitalize_first_letter.ts (100%) delete mode 100644 x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_flyout_state.tsx diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_api_error_callout.tsx b/x-pack/plugins/transform/public/app/sections/edit_transform/components/edit_transform_api_error_callout.tsx similarity index 83% rename from x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_api_error_callout.tsx rename to x-pack/plugins/transform/public/app/sections/edit_transform/components/edit_transform_api_error_callout.tsx index 027106250a873..4227a05d78d02 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_api_error_callout.tsx +++ b/x-pack/plugins/transform/public/app/sections/edit_transform/components/edit_transform_api_error_callout.tsx @@ -6,16 +6,15 @@ */ import React, { type FC } from 'react'; -import { useSelector } from 'react-redux'; import { EuiCallOut, EuiSpacer } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { selectApiErrorMessage } from './edit_transform_flyout_state'; +import { useApiErrorMessage } from '../state_management/selectors/api_error_message'; export const EditTransformApiErrorCallout: FC = () => { - const apiErrorMessage = useSelector(selectApiErrorMessage); + const apiErrorMessage = useApiErrorMessage(); if (apiErrorMessage === undefined) return null; diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_flyout.tsx b/x-pack/plugins/transform/public/app/sections/edit_transform/components/edit_transform_flyout.tsx similarity index 87% rename from x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_flyout.tsx rename to x-pack/plugins/transform/public/app/sections/edit_transform/components/edit_transform_flyout.tsx index fada9746d3a70..1369529377a81 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_flyout.tsx +++ b/x-pack/plugins/transform/public/app/sections/edit_transform/components/edit_transform_flyout.tsx @@ -20,15 +20,16 @@ import { EuiTitle, } from '@elastic/eui'; -import { isManagedTransform } from '../../../../common/managed_transforms_utils'; +import { isManagedTransform } from '../../../common/managed_transforms_utils'; -import { ManagedTransformsWarningCallout } from '../managed_transforms_callout/managed_transforms_callout'; -import type { EditAction } from '../action_edit'; +import { ManagedTransformsWarningCallout } from '../../transform_management/components/managed_transforms_callout/managed_transforms_callout'; +import type { EditAction } from '../../transform_management/components/action_edit'; + +import { EditTransformFlyoutProvider } from '../state_management/edit_transform_flyout_state'; import { EditTransformApiErrorCallout } from './edit_transform_api_error_callout'; import { EditTransformFlyoutCallout } from './edit_transform_flyout_callout'; import { EditTransformFlyoutForm } from './edit_transform_flyout_form'; -import { EditTransformFlyoutProvider } from './edit_transform_flyout_state'; import { EditTransformUpdateButton } from './edit_transform_update_button'; export const EditTransformFlyout: FC = ({ diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_flyout_callout.tsx b/x-pack/plugins/transform/public/app/sections/edit_transform/components/edit_transform_flyout_callout.tsx similarity index 94% rename from x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_flyout_callout.tsx rename to x-pack/plugins/transform/public/app/sections/edit_transform/components/edit_transform_flyout_callout.tsx index cdaabb3a3b200..ed99bdc911f3e 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_flyout_callout.tsx +++ b/x-pack/plugins/transform/public/app/sections/edit_transform/components/edit_transform_flyout_callout.tsx @@ -18,7 +18,7 @@ import { EuiTextColor, } from '@elastic/eui'; -import { useDocumentationLinks } from '../../../../hooks/use_documentation_links'; +import { useDocumentationLinks } from '../../../hooks/use_documentation_links'; export const EditTransformFlyoutCallout: FC = () => { const { esTransformUpdate } = useDocumentationLinks(); diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_flyout_form.tsx b/x-pack/plugins/transform/public/app/sections/edit_transform/components/edit_transform_flyout_form.tsx similarity index 100% rename from x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_flyout_form.tsx rename to x-pack/plugins/transform/public/app/sections/edit_transform/components/edit_transform_flyout_form.tsx diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_flyout_form_text_area.tsx b/x-pack/plugins/transform/public/app/sections/edit_transform/components/edit_transform_flyout_form_text_area.tsx similarity index 83% rename from x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_flyout_form_text_area.tsx rename to x-pack/plugins/transform/public/app/sections/edit_transform/components/edit_transform_flyout_form_text_area.tsx index 3494a2216409d..d2ec30de3b104 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_flyout_form_text_area.tsx +++ b/x-pack/plugins/transform/public/app/sections/edit_transform/components/edit_transform_flyout_form_text_area.tsx @@ -11,12 +11,10 @@ import { EuiFormRow, EuiTextArea } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { - useEditTransformFlyoutActions, - useFormField, - type FormFields, -} from './edit_transform_flyout_state'; -import { capitalizeFirstLetter } from './capitalize_first_letter'; +import { useEditTransformFlyoutActions } from '../state_management/edit_transform_flyout_state'; +import { useFormField } from '../state_management/selectors/form_field'; +import type { FormFields } from '../state_management/form_field'; +import { capitalizeFirstLetter } from '../utils/capitalize_first_letter'; interface EditTransformFlyoutFormTextInputProps { field: FormFields; diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_flyout_form_text_input.tsx b/x-pack/plugins/transform/public/app/sections/edit_transform/components/edit_transform_flyout_form_text_input.tsx similarity index 83% rename from x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_flyout_form_text_input.tsx rename to x-pack/plugins/transform/public/app/sections/edit_transform/components/edit_transform_flyout_form_text_input.tsx index 05df83d5d1ec3..8548d3c4b9dc5 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_flyout_form_text_input.tsx +++ b/x-pack/plugins/transform/public/app/sections/edit_transform/components/edit_transform_flyout_form_text_input.tsx @@ -11,12 +11,10 @@ import { EuiFieldText, EuiFormRow } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { - useEditTransformFlyoutActions, - useFormField, - type FormFields, -} from './edit_transform_flyout_state'; -import { capitalizeFirstLetter } from './capitalize_first_letter'; +import { useEditTransformFlyoutActions } from '../state_management/edit_transform_flyout_state'; +import { useFormField } from '../state_management/selectors/form_field'; +import type { FormFields } from '../state_management/form_field'; +import { capitalizeFirstLetter } from '../utils/capitalize_first_letter'; interface EditTransformFlyoutFormTextInputProps { field: FormFields; diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_ingest_pipeline.tsx b/x-pack/plugins/transform/public/app/sections/edit_transform/components/edit_transform_ingest_pipeline.tsx similarity index 92% rename from x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_ingest_pipeline.tsx rename to x-pack/plugins/transform/public/app/sections/edit_transform/components/edit_transform_ingest_pipeline.tsx index 7a120e11a0d02..772f00c7f0a2c 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_ingest_pipeline.tsx +++ b/x-pack/plugins/transform/public/app/sections/edit_transform/components/edit_transform_ingest_pipeline.tsx @@ -11,10 +11,12 @@ import { useEuiTheme, EuiComboBox, EuiFormRow, EuiSkeletonRectangle } from '@ela import { i18n } from '@kbn/i18n'; -import { useGetEsIngestPipelines } from '../../../../hooks'; +import { useGetEsIngestPipelines } from '../../../hooks'; + +import { useEditTransformFlyoutActions } from '../state_management/edit_transform_flyout_state'; +import { useFormField } from '../state_management/selectors/form_field'; import { EditTransformFlyoutFormTextInput } from './edit_transform_flyout_form_text_input'; -import { useEditTransformFlyoutActions, useFormField } from './edit_transform_flyout_state'; const ingestPipelineLabel = i18n.translate( 'xpack.transform.transformList.editFlyoutFormDestinationIngestPipelineLabel', diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_retention_policy.tsx b/x-pack/plugins/transform/public/app/sections/edit_transform/components/edit_transform_retention_policy.tsx similarity index 91% rename from x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_retention_policy.tsx rename to x-pack/plugins/transform/public/app/sections/edit_transform/components/edit_transform_retention_policy.tsx index 5971791b8a9fd..c32409cb6ff7f 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_retention_policy.tsx +++ b/x-pack/plugins/transform/public/app/sections/edit_transform/components/edit_transform_retention_policy.tsx @@ -6,26 +6,28 @@ */ import React, { useEffect, useMemo, type FC } from 'react'; -import { useSelector } from 'react-redux'; - -import { i18n } from '@kbn/i18n'; import { EuiFormRow, EuiSelect, EuiSpacer, EuiSwitch } from '@elastic/eui'; + +import { i18n } from '@kbn/i18n'; import { toMountPoint } from '@kbn/react-kibana-mount'; -import { useAppDependencies, useToastNotifications } from '../../../../app_dependencies'; -import { ToastNotificationText } from '../../../../components'; -import type { PostTransformsPreviewRequestSchema } from '../../../../../../common/api_schemas/transforms'; -import { isLatestTransform, isPivotTransform } from '../../../../../../common/types/transform'; -import { useGetTransformsPreview } from '../../../../hooks'; -import { EditTransformFlyoutFormTextInput } from './edit_transform_flyout_form_text_input'; +import type { PostTransformsPreviewRequestSchema } from '../../../../../common/api_schemas/transforms'; +import { isLatestTransform, isPivotTransform } from '../../../../../common/types/transform'; +import { getErrorMessage } from '../../../../../common/utils/errors'; + +import { useAppDependencies, useToastNotifications } from '../../../app_dependencies'; +import { useGetTransformsPreview } from '../../../hooks'; +import { ToastNotificationText } from '../../../components'; + import { - selectFormSections, - selectRetentionPolicyField, useEditTransformFlyoutActions, useEditTransformFlyoutContext, -} from './edit_transform_flyout_state'; -import { getErrorMessage } from '../../../../../../common/utils/errors'; +} from '../state_management/edit_transform_flyout_state'; +import { useFormSections } from '../state_management/selectors/form_sections'; +import { useRetentionPolicyField } from '../state_management/selectors/retention_policy_field'; + +import { EditTransformFlyoutFormTextInput } from './edit_transform_flyout_form_text_input'; export const EditTransformRetentionPolicy: FC = () => { const { i18n: i18nStart, theme } = useAppDependencies(); @@ -33,8 +35,8 @@ export const EditTransformRetentionPolicy: FC = () => { const toastNotifications = useToastNotifications(); const { config, dataViewId } = useEditTransformFlyoutContext(); - const formSections = useSelector(selectFormSections); - const retentionPolicyField = useSelector(selectRetentionPolicyField); + const formSections = useFormSections(); + const retentionPolicyField = useRetentionPolicyField(); const { setFormField, setFormSection } = useEditTransformFlyoutActions(); const previewRequest: PostTransformsPreviewRequestSchema = useMemo(() => { diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_update_button.tsx b/x-pack/plugins/transform/public/app/sections/edit_transform/components/edit_transform_update_button.tsx similarity index 77% rename from x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_update_button.tsx rename to x-pack/plugins/transform/public/app/sections/edit_transform/components/edit_transform_update_button.tsx index abded0596778e..6fe5c7fe561b2 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_update_button.tsx +++ b/x-pack/plugins/transform/public/app/sections/edit_transform/components/edit_transform_update_button.tsx @@ -11,17 +11,17 @@ import { EuiButton } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { getErrorMessage } from '../../../../../../common/utils/errors'; +import { getErrorMessage } from '../../../../../common/utils/errors'; -import { useUpdateTransform } from '../../../../hooks'; +import { useUpdateTransform } from '../../../hooks'; import { useEditTransformFlyoutActions, useEditTransformFlyoutContext, - useIsFormValid, - useIsFormTouched, - useUpdatedTransformConfig, -} from './edit_transform_flyout_state'; +} from '../state_management/edit_transform_flyout_state'; +import { useIsFormTouched } from '../state_management/selectors/is_form_touched'; +import { useIsFormValid } from '../state_management/selectors/is_form_valid'; +import { useUpdatedTransformConfig } from '../state_management/selectors/updated_transform_config'; interface EditTransformUpdateButtonProps { closeFlyout: () => void; diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/index.ts b/x-pack/plugins/transform/public/app/sections/edit_transform/index.ts similarity index 77% rename from x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/index.ts rename to x-pack/plugins/transform/public/app/sections/edit_transform/index.ts index 018297f859baa..4ce23cc1f56d7 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/index.ts +++ b/x-pack/plugins/transform/public/app/sections/edit_transform/index.ts @@ -5,4 +5,4 @@ * 2.0. */ -export { EditTransformFlyout } from './edit_transform_flyout'; +export { EditTransformFlyout } from './components/edit_transform_flyout'; diff --git a/x-pack/plugins/transform/public/app/sections/edit_transform/state_management/__mocks__/transform_config.ts b/x-pack/plugins/transform/public/app/sections/edit_transform/state_management/__mocks__/transform_config.ts new file mode 100644 index 0000000000000..532f932bfb0b8 --- /dev/null +++ b/x-pack/plugins/transform/public/app/sections/edit_transform/state_management/__mocks__/transform_config.ts @@ -0,0 +1,38 @@ +/* + * 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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { TransformPivotConfig } from '../../../../../../common/types/transform'; + +export const getTransformConfigMock = (): TransformPivotConfig => ({ + id: 'the-transform-id', + source: { + index: ['the-transform-source-index'], + query: { + match_all: {}, + }, + }, + dest: { + index: 'the-transform-destination-index', + }, + pivot: { + group_by: { + airline: { + terms: { + field: 'airline', + }, + }, + }, + aggregations: { + 'responsetime.avg': { + avg: { + field: 'responsetime', + }, + }, + }, + }, + description: 'the-description', +}); diff --git a/x-pack/plugins/transform/public/app/sections/edit_transform/state_management/actions.ts b/x-pack/plugins/transform/public/app/sections/edit_transform/state_management/actions.ts new file mode 100644 index 0000000000000..f0ed9342ceb32 --- /dev/null +++ b/x-pack/plugins/transform/public/app/sections/edit_transform/state_management/actions.ts @@ -0,0 +1,79 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { PayloadAction } from '@reduxjs/toolkit'; + +import type { FormFields } from './form_field'; +import type { FormSections } from './form_section'; +import type { ProviderProps, State } from './edit_transform_flyout_state'; +import { getDefaultState } from './get_default_state'; +import { validators, type ValidatorName } from './validators'; + +function isFormFieldOptional(state: State, field: FormFields) { + const formField = state.formFields[field]; + + let isOptional = formField.isOptional; + if (formField.section) { + const section = state.formSections[formField.section]; + if (section.enabled && formField.isOptionalInSection === false) { + isOptional = false; + } + } + + return isOptional; +} + +function getFormFieldErrorMessages( + value: string, + isOptional: boolean, + validatorName: ValidatorName +) { + return isOptional && typeof value === 'string' && value.length === 0 + ? [] + : validators[validatorName](value, isOptional); +} + +export const initialize = (_: State, action: PayloadAction) => + getDefaultState(action.payload.config); + +export const setApiError = (state: State, action: PayloadAction) => { + state.apiErrorMessage = action.payload; +}; + +export const setFormField = ( + state: State, + action: PayloadAction<{ field: FormFields; value: string }> +) => { + const formField = state.formFields[action.payload.field]; + const isOptional = isFormFieldOptional(state, action.payload.field); + + formField.errorMessages = getFormFieldErrorMessages( + action.payload.value, + isOptional, + formField.validator + ); + + formField.value = action.payload.value; +}; + +export const setFormSection = ( + state: State, + action: PayloadAction<{ section: FormSections; enabled: boolean }> +) => { + state.formSections[action.payload.section].enabled = action.payload.enabled; + + // After a section change we re-evaluate all form fields, since optionality + // of a field could change if a section got toggled. + Object.entries(state.formFields).forEach(([formFieldName, formField]) => { + const isOptional = isFormFieldOptional(state, formFieldName as FormFields); + formField.errorMessages = getFormFieldErrorMessages( + formField.value, + isOptional, + formField.validator + ); + }); +}; diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_flyout_state.test.tsx b/x-pack/plugins/transform/public/app/sections/edit_transform/state_management/apply_form_state_to_transform_config.test.ts similarity index 69% rename from x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_flyout_state.test.tsx rename to x-pack/plugins/transform/public/app/sections/edit_transform/state_management/apply_form_state_to_transform_config.test.ts index 8e4e1a28eb224..4586760fb9b6e 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_flyout_state.test.tsx +++ b/x-pack/plugins/transform/public/app/sections/edit_transform/state_management/apply_form_state_to_transform_config.test.ts @@ -5,50 +5,10 @@ * 2.0. */ -import React, { type FC } from 'react'; -import { act, renderHook } from '@testing-library/react-hooks'; +import { getTransformConfigMock } from './__mocks__/transform_config'; -import { TransformPivotConfig } from '../../../../../../common/types/transform'; - -import { - applyFormStateToTransformConfig, - getDefaultState, - useEditTransformFlyoutActions, - useFormField, - useIsFormTouched, - useIsFormValid, - EditTransformFlyoutProvider, -} from './edit_transform_flyout_state'; - -const getTransformConfigMock = (): TransformPivotConfig => ({ - id: 'the-transform-id', - source: { - index: ['the-transform-source-index'], - query: { - match_all: {}, - }, - }, - dest: { - index: 'the-transform-destination-index', - }, - pivot: { - group_by: { - airline: { - terms: { - field: 'airline', - }, - }, - }, - aggregations: { - 'responsetime.avg': { - avg: { - field: 'responsetime', - }, - }, - }, - }, - description: 'the-description', -}); +import { applyFormStateToTransformConfig } from './apply_form_state_to_transform_config'; +import { getDefaultState } from './get_default_state'; describe('Transform: applyFormStateToTransformConfig()', () => { it('should exclude unchanged form fields', () => { @@ -220,59 +180,3 @@ describe('Transform: applyFormStateToTransformConfig()', () => { expect(updateConfig.retention_policy).toBe(null); }); }); - -describe('Transform: useEditTransformFlyoutActions/Selector()', () => { - it('field updates should trigger form validation', () => { - const transformConfigMock = getTransformConfigMock(); - const wrapper: FC = ({ children }) => ( - - {children} - - ); - - // As we want to test how actions affect the state, - // we set up this custom hook that combines hooks for - // actions and state selection, so they react to the same redux store. - const useHooks = () => ({ - actions: useEditTransformFlyoutActions(), - isFormTouched: useIsFormTouched(), - isFormValid: useIsFormValid(), - frequency: useFormField('frequency'), - }); - - const { result } = renderHook(useHooks, { wrapper }); - - act(() => { - result.current.actions.setFormField({ - field: 'description', - value: 'the-updated-description', - }); - }); - - expect(result.current.isFormTouched).toBe(true); - expect(result.current.isFormValid).toBe(true); - - act(() => { - result.current.actions.setFormField({ - field: 'description', - value: transformConfigMock.description as string, - }); - }); - - expect(result.current.isFormTouched).toBe(false); - expect(result.current.isFormValid).toBe(true); - - act(() => { - result.current.actions.setFormField({ - field: 'frequency', - value: 'the-invalid-value', - }); - }); - - expect(result.current.isFormTouched).toBe(true); - expect(result.current.isFormValid).toBe(false); - expect(result.current.frequency.errorMessages).toStrictEqual([ - 'The frequency value is not valid.', - ]); - }); -}); diff --git a/x-pack/plugins/transform/public/app/sections/edit_transform/state_management/apply_form_state_to_transform_config.ts b/x-pack/plugins/transform/public/app/sections/edit_transform/state_management/apply_form_state_to_transform_config.ts new file mode 100644 index 0000000000000..830316c27f72f --- /dev/null +++ b/x-pack/plugins/transform/public/app/sections/edit_transform/state_management/apply_form_state_to_transform_config.ts @@ -0,0 +1,37 @@ +/* + * 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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { merge } from 'lodash'; + +import { PostTransformsUpdateRequestSchema } from '../../../../../common/api_schemas/update_transforms'; +import { TransformConfigUnion } from '../../../../../common/types/transform'; + +import { getUpdateValue } from './get_update_value'; + +import type { FormFields, FormFieldsState } from './form_field'; +import type { FormSectionsState } from './form_section'; + +// Takes in the form configuration and returns a request object suitable to be sent to the +// transform update API endpoint by iterating over `getUpdateValue()`. +// Once a user hits the update button, this function takes care of extracting the information +// necessary to create the update request. They take into account whether a field needs to +// be included at all in the request (for example, if it hadn't been changed). +// The code is also able to identify relationships/dependencies between form fields. +// For example, if the `pipeline` field was changed, it's necessary to make the `index` +// field part of the request, otherwise the update would fail. +export const applyFormStateToTransformConfig = ( + config: TransformConfigUnion, + formFields: FormFieldsState, + formSections: FormSectionsState +): PostTransformsUpdateRequestSchema => + // Iterates over all form fields and only if necessary applies them to + // the request object used for updating the transform. + (Object.keys(formFields) as FormFields[]).reduce( + (updateConfig, field) => + merge({ ...updateConfig }, getUpdateValue(field, config, formFields, formSections)), + {} + ); diff --git a/x-pack/plugins/transform/public/app/sections/edit_transform/state_management/edit_transform_flyout_state.test.tsx b/x-pack/plugins/transform/public/app/sections/edit_transform/state_management/edit_transform_flyout_state.test.tsx new file mode 100644 index 0000000000000..67f1a49e6ec64 --- /dev/null +++ b/x-pack/plugins/transform/public/app/sections/edit_transform/state_management/edit_transform_flyout_state.test.tsx @@ -0,0 +1,75 @@ +/* + * 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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { type FC } from 'react'; +import { act, renderHook } from '@testing-library/react-hooks'; + +import { getTransformConfigMock } from './__mocks__/transform_config'; + +import { + useEditTransformFlyoutActions, + EditTransformFlyoutProvider, +} from './edit_transform_flyout_state'; +import { useFormField } from './selectors/form_field'; +import { useIsFormTouched } from './selectors/is_form_touched'; +import { useIsFormValid } from './selectors/is_form_valid'; + +describe('Transform: useEditTransformFlyoutActions/Selector()', () => { + it('field updates should trigger form validation', () => { + const transformConfigMock = getTransformConfigMock(); + const wrapper: FC = ({ children }) => ( + + {children} + + ); + + // As we want to test how actions affect the state, + // we set up this custom hook that combines hooks for + // actions and state selection, so they react to the same redux store. + const useHooks = () => ({ + actions: useEditTransformFlyoutActions(), + isFormTouched: useIsFormTouched(), + isFormValid: useIsFormValid(), + frequency: useFormField('frequency'), + }); + + const { result } = renderHook(useHooks, { wrapper }); + + act(() => { + result.current.actions.setFormField({ + field: 'description', + value: 'the-updated-description', + }); + }); + + expect(result.current.isFormTouched).toBe(true); + expect(result.current.isFormValid).toBe(true); + + act(() => { + result.current.actions.setFormField({ + field: 'description', + value: transformConfigMock.description as string, + }); + }); + + expect(result.current.isFormTouched).toBe(false); + expect(result.current.isFormValid).toBe(true); + + act(() => { + result.current.actions.setFormField({ + field: 'frequency', + value: 'the-invalid-value', + }); + }); + + expect(result.current.isFormTouched).toBe(true); + expect(result.current.isFormValid).toBe(false); + expect(result.current.frequency.errorMessages).toStrictEqual([ + 'The frequency value is not valid.', + ]); + }); +}); diff --git a/x-pack/plugins/transform/public/app/sections/edit_transform/state_management/edit_transform_flyout_state.tsx b/x-pack/plugins/transform/public/app/sections/edit_transform/state_management/edit_transform_flyout_state.tsx new file mode 100644 index 0000000000000..c82243c408a0c --- /dev/null +++ b/x-pack/plugins/transform/public/app/sections/edit_transform/state_management/edit_transform_flyout_state.tsx @@ -0,0 +1,90 @@ +/* + * 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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { + createContext, + useContext, + useEffect, + useMemo, + type FC, + type PropsWithChildren, +} from 'react'; +import { configureStore, createSlice } from '@reduxjs/toolkit'; +import { useDispatch, Provider } from 'react-redux'; +import { bindActionCreators } from 'redux'; + +import { TransformConfigUnion } from '../../../../../common/types/transform'; + +import { initialize, setApiError, setFormField, setFormSection } from './actions'; +import { type FormFieldsState } from './form_field'; +import { type FormSectionsState } from './form_section'; +import { getDefaultState } from './get_default_state'; + +// The edit transform flyout uses a redux-toolkit to manage its form state with +// support for applying its state to a nested configuration object suitable for passing on +// directly to the API call. For now this is only used for the transform edit form. +// Once we apply the functionality to other places, e.g. the transform creation wizard, +// the generic framework code in this file should be moved to a dedicated location. + +export interface ProviderProps { + config: TransformConfigUnion; + dataViewId?: string; +} + +export interface State { + apiErrorMessage?: string; + formFields: FormFieldsState; + formSections: FormSectionsState; +} + +const editTransformFlyoutSlice = createSlice({ + name: 'editTransformFlyout', + initialState: getDefaultState(), + reducers: { + initialize, + setApiError, + setFormField, + setFormSection, + }, +}); + +const getReduxStore = () => + configureStore({ + reducer: editTransformFlyoutSlice.reducer, + }); + +const EditTransformFlyoutContext = createContext(null); + +export const EditTransformFlyoutProvider: FC> = ({ + children, + ...props +}) => { + const store = useMemo(getReduxStore, []); + + // Apply original transform config to redux form state. + useEffect(() => { + store.dispatch(editTransformFlyoutSlice.actions.initialize(props)); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + return ( + + {children} + + ); +}; + +export const useEditTransformFlyoutContext = () => { + const c = useContext(EditTransformFlyoutContext); + if (c === null) throw new Error('EditTransformFlyoutContext not set.'); + return c; +}; + +export const useEditTransformFlyoutActions = () => { + const dispatch = useDispatch(); + return useMemo(() => bindActionCreators(editTransformFlyoutSlice.actions, dispatch), [dispatch]); +}; diff --git a/x-pack/plugins/transform/public/app/sections/edit_transform/state_management/form_field.ts b/x-pack/plugins/transform/public/app/sections/edit_transform/state_management/form_field.ts new file mode 100644 index 0000000000000..ea02fbe7ccd78 --- /dev/null +++ b/x-pack/plugins/transform/public/app/sections/edit_transform/state_management/form_field.ts @@ -0,0 +1,70 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { getNestedProperty } from '@kbn/ml-nested-property'; + +import { TransformConfigUnion } from '../../../../../common/types/transform'; + +import type { FormSections } from './form_section'; +import type { ValidatorName } from './validators'; +import type { ValueParserName } from './value_parsers'; + +// The form state defines a flat structure of names for form fields. +// This is a flat structure regardless of whether the final config object will be nested. +// For example, `destinationIndex` and `destinationIngestPipeline` will later be nested under `dest`. +export type FormFields = + | 'description' + | 'destinationIndex' + | 'destinationIngestPipeline' + | 'docsPerSecond' + | 'frequency' + | 'maxPageSearchSize' + | 'numFailureRetries' + | 'retentionPolicyField' + | 'retentionPolicyMaxAge'; + +export type FormFieldsState = Record; + +export interface FormField { + formFieldName: FormFields; + configFieldName: string; + defaultValue: string; + dependsOn: FormFields[]; + errorMessages: string[]; + isNullable: boolean; + isOptional: boolean; + isOptionalInSection?: boolean; + section?: FormSections; + validator: ValidatorName; + value: string; + valueParser: ValueParserName; +} + +export const initializeFormField = ( + formFieldName: FormFields, + configFieldName: string, + config?: TransformConfigUnion, + overloads?: Partial +): FormField => { + const defaultValue = overloads?.defaultValue !== undefined ? overloads.defaultValue : ''; + const rawValue = getNestedProperty(config ?? {}, configFieldName, undefined); + const value = rawValue !== null && rawValue !== undefined ? rawValue.toString() : ''; + + return { + formFieldName, + configFieldName, + defaultValue, + dependsOn: [], + errorMessages: [], + isNullable: false, + isOptional: true, + validator: 'stringValidator', + value, + valueParser: 'defaultParser', + ...(overloads !== undefined ? { ...overloads } : {}), + }; +}; diff --git a/x-pack/plugins/transform/public/app/sections/edit_transform/state_management/form_section.ts b/x-pack/plugins/transform/public/app/sections/edit_transform/state_management/form_section.ts new file mode 100644 index 0000000000000..82e14aa6c9c4c --- /dev/null +++ b/x-pack/plugins/transform/public/app/sections/edit_transform/state_management/form_section.ts @@ -0,0 +1,44 @@ +/* + * 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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { getNestedProperty } from '@kbn/ml-nested-property'; + +import { TransformConfigUnion } from '../../../../../common/types/transform'; + +// Defining these sections is only necessary for options where a reset/deletion of that part of the +// configuration is supported by the API. For example, this isn't suitable to use with `dest` since +// this overall part of the configuration is not optional. However, `retention_policy` is optional, +// so we need to support to recognize this based on the form state and be able to reset it by +// created a request body containing `{ retention_policy: null }`. +export type FormSections = 'retentionPolicy'; + +export interface FormSection { + formSectionName: FormSections; + configFieldName: string; + defaultEnabled: boolean; + enabled: boolean; +} + +export type FormSectionsState = Record; + +export const initializeFormSection = ( + formSectionName: FormSections, + configFieldName: string, + config?: TransformConfigUnion, + overloads?: Partial +): FormSection => { + const defaultEnabled = overloads?.defaultEnabled ?? false; + const rawEnabled = getNestedProperty(config ?? {}, configFieldName, undefined); + const enabled = rawEnabled !== undefined && rawEnabled !== null; + + return { + formSectionName, + configFieldName, + defaultEnabled, + enabled, + }; +}; diff --git a/x-pack/plugins/transform/public/app/sections/edit_transform/state_management/get_default_state.ts b/x-pack/plugins/transform/public/app/sections/edit_transform/state_management/get_default_state.ts new file mode 100644 index 0000000000000..9f8365b8c6a17 --- /dev/null +++ b/x-pack/plugins/transform/public/app/sections/edit_transform/state_management/get_default_state.ts @@ -0,0 +1,109 @@ +/* + * 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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { + DEFAULT_TRANSFORM_FREQUENCY, + DEFAULT_TRANSFORM_SETTINGS_MAX_PAGE_SEARCH_SIZE, +} from '../../../../../common/constants'; +import { TransformConfigUnion } from '../../../../../common/types/transform'; + +import { initializeFormField } from './form_field'; +import { initializeFormSection } from './form_section'; +import type { State } from './edit_transform_flyout_state'; + +// Takes in a transform configuration and returns the default state to populate the form. +// It supports overrides to apply a pre-existing configuration. +// The implementation of this function is the only one that's specifically required to define +// the features of the transform edit form. All other functions are generic and could be reused +// in the future for other forms. +export const getDefaultState = (config?: TransformConfigUnion): State => ({ + formFields: { + // top level attributes + description: initializeFormField('description', 'description', config), + frequency: initializeFormField('frequency', 'frequency', config, { + defaultValue: DEFAULT_TRANSFORM_FREQUENCY, + validator: 'frequencyValidator', + }), + + // dest.* + destinationIndex: initializeFormField('destinationIndex', 'dest.index', config, { + dependsOn: ['destinationIngestPipeline'], + isOptional: false, + }), + destinationIngestPipeline: initializeFormField( + 'destinationIngestPipeline', + 'dest.pipeline', + config, + { + dependsOn: ['destinationIndex'], + isOptional: true, + } + ), + + // settings.* + docsPerSecond: initializeFormField('docsPerSecond', 'settings.docs_per_second', config, { + isNullable: true, + isOptional: true, + validator: 'integerAboveZeroValidator', + valueParser: 'nullableNumberParser', + }), + maxPageSearchSize: initializeFormField( + 'maxPageSearchSize', + 'settings.max_page_search_size', + config, + { + defaultValue: `${DEFAULT_TRANSFORM_SETTINGS_MAX_PAGE_SEARCH_SIZE}`, + isNullable: true, + isOptional: true, + validator: 'transformSettingsPageSearchSizeValidator', + valueParser: 'numberParser', + } + ), + numFailureRetries: initializeFormField( + 'numFailureRetries', + 'settings.num_failure_retries', + config, + { + defaultValue: undefined, + isNullable: true, + isOptional: true, + validator: 'transformSettingsNumberOfRetriesValidator', + valueParser: 'numberParser', + } + ), + + // retention_policy.* + retentionPolicyField: initializeFormField( + 'retentionPolicyField', + 'retention_policy.time.field', + config, + { + dependsOn: ['retentionPolicyMaxAge'], + isNullable: false, + isOptional: true, + isOptionalInSection: false, + section: 'retentionPolicy', + } + ), + retentionPolicyMaxAge: initializeFormField( + 'retentionPolicyMaxAge', + 'retention_policy.time.max_age', + config, + { + dependsOn: ['retentionPolicyField'], + isNullable: false, + isOptional: true, + isOptionalInSection: false, + section: 'retentionPolicy', + validator: 'retentionPolicyMaxAgeValidator', + } + ), + }, + formSections: { + retentionPolicy: initializeFormSection('retentionPolicy', 'retention_policy', config), + }, +}); diff --git a/x-pack/plugins/transform/public/app/sections/edit_transform/state_management/get_update_value.ts b/x-pack/plugins/transform/public/app/sections/edit_transform/state_management/get_update_value.ts new file mode 100644 index 0000000000000..82c6f012cdaf9 --- /dev/null +++ b/x-pack/plugins/transform/public/app/sections/edit_transform/state_management/get_update_value.ts @@ -0,0 +1,80 @@ +/* + * 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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { merge } from 'lodash'; + +import { getNestedProperty, setNestedProperty } from '@kbn/ml-nested-property'; + +import type { PostTransformsUpdateRequestSchema } from '../../../../../common/api_schemas/update_transforms'; +import type { TransformConfigUnion } from '../../../../../common/types/transform'; + +import type { FormFields, FormFieldsState } from './form_field'; +import type { FormSectionsState } from './form_section'; +import { valueParsers } from './value_parsers'; + +// Takes a value from form state and applies it to the structure +// of the expected final configuration request object. +// Considers options like if a value is nullable or optional. +export const getUpdateValue = ( + attribute: FormFields, + config: TransformConfigUnion, + formFields: FormFieldsState, + formSections: FormSectionsState, + enforceFormValue = false +) => { + const formStateAttribute = formFields[attribute]; + const fallbackValue = formStateAttribute.isNullable ? null : formStateAttribute.defaultValue; + + const enabledBasedOnSection = + formStateAttribute.section !== undefined + ? formSections[formStateAttribute.section].enabled + : true; + + const formValue = + formStateAttribute.value !== '' + ? valueParsers[formStateAttribute.valueParser](formStateAttribute.value) + : fallbackValue; + + const configValue = getNestedProperty(config, formStateAttribute.configFieldName, fallbackValue); + + // only get depending values if we're not already in a call to get depending values. + const dependsOnConfig: PostTransformsUpdateRequestSchema = + enforceFormValue === false + ? formStateAttribute.dependsOn.reduce((_dependsOnConfig, dependsOnField) => { + return merge( + { ..._dependsOnConfig }, + getUpdateValue(dependsOnField, config, formFields, formSections, true) + ); + }, {}) + : {}; + + if ( + formValue === formStateAttribute.defaultValue && + formValue === configValue && + formStateAttribute.isOptional + ) { + return {}; + } + + // If the resettable section the form field belongs to is disabled, + // the whole section will be set to `null` to do the actual reset. + if (formStateAttribute.section !== undefined && !enabledBasedOnSection) { + return setNestedProperty( + dependsOnConfig, + formSections[formStateAttribute.section].configFieldName, + null + ); + } + + return enabledBasedOnSection && (formValue !== configValue || enforceFormValue) + ? setNestedProperty( + dependsOnConfig, + formStateAttribute.configFieldName, + formValue === '' && formStateAttribute.isOptional ? undefined : formValue + ) + : {}; +}; diff --git a/x-pack/plugins/transform/public/app/sections/edit_transform/state_management/selectors/api_error_message.ts b/x-pack/plugins/transform/public/app/sections/edit_transform/state_management/selectors/api_error_message.ts new file mode 100644 index 0000000000000..7127c28136ef1 --- /dev/null +++ b/x-pack/plugins/transform/public/app/sections/edit_transform/state_management/selectors/api_error_message.ts @@ -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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useSelector } from 'react-redux'; + +import type { State } from '../edit_transform_flyout_state'; + +export const selectApiErrorMessage = (s: State) => s.apiErrorMessage; + +export const useApiErrorMessage = () => { + return useSelector(selectApiErrorMessage); +}; diff --git a/x-pack/plugins/transform/public/app/sections/edit_transform/state_management/selectors/form_field.ts b/x-pack/plugins/transform/public/app/sections/edit_transform/state_management/selectors/form_field.ts new file mode 100644 index 0000000000000..7b3e4a4f5b100 --- /dev/null +++ b/x-pack/plugins/transform/public/app/sections/edit_transform/state_management/selectors/form_field.ts @@ -0,0 +1,21 @@ +/* + * 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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useMemo } from 'react'; + +import { useSelector } from 'react-redux'; + +import type { State } from '../edit_transform_flyout_state'; + +import type { FormFields } from '../form_field'; + +const createSelectFormField = (field: FormFields) => (s: State) => s.formFields[field]; + +export const useFormField = (field: FormFields) => { + const selectFormField = useMemo(() => createSelectFormField(field), [field]); + return useSelector(selectFormField); +}; diff --git a/x-pack/plugins/transform/public/app/sections/edit_transform/state_management/selectors/form_sections.ts b/x-pack/plugins/transform/public/app/sections/edit_transform/state_management/selectors/form_sections.ts new file mode 100644 index 0000000000000..2a9f4b1d60cfe --- /dev/null +++ b/x-pack/plugins/transform/public/app/sections/edit_transform/state_management/selectors/form_sections.ts @@ -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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useSelector } from 'react-redux'; + +import type { State } from '../edit_transform_flyout_state'; + +export const selectFormSections = (s: State) => s.formSections; + +export const useFormSections = () => { + return useSelector(selectFormSections); +}; diff --git a/x-pack/plugins/transform/public/app/sections/edit_transform/state_management/selectors/is_form_touched.ts b/x-pack/plugins/transform/public/app/sections/edit_transform/state_management/selectors/is_form_touched.ts new file mode 100644 index 0000000000000..8bdb390d73378 --- /dev/null +++ b/x-pack/plugins/transform/public/app/sections/edit_transform/state_management/selectors/is_form_touched.ts @@ -0,0 +1,50 @@ +/* + * 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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { isEqual } from 'lodash'; +import { useMemo } from 'react'; + +import { createSelector } from '@reduxjs/toolkit'; +import { useSelector } from 'react-redux'; + +import type { TransformConfigUnion } from '../../../../../../common/types/transform'; + +import type { State } from '../edit_transform_flyout_state'; + +import type { FormFieldsState } from '../form_field'; +import type { FormSectionsState } from '../form_section'; +import { getDefaultState } from '../get_default_state'; +import { useEditTransformFlyoutContext } from '../edit_transform_flyout_state'; + +const getFieldValues = (fields: FormFieldsState) => Object.values(fields).map((f) => f.value); +const getSectionValues = (sections: FormSectionsState) => + Object.values(sections).map((s) => s.enabled); + +const isFormTouched = ( + config: TransformConfigUnion, + formFields: FormFieldsState, + formSections: FormSectionsState +) => { + const defaultState = getDefaultState(config); + return ( + !isEqual(getFieldValues(defaultState.formFields), getFieldValues(formFields)) || + !isEqual(getSectionValues(defaultState.formSections), getSectionValues(formSections)) + ); +}; + +const createSelectIsFormTouched = (originalConfig: TransformConfigUnion) => + createSelector( + (state: State) => state.formFields, + (state: State) => state.formSections, + (formFields, formSections) => isFormTouched(originalConfig, formFields, formSections) + ); + +export const useIsFormTouched = () => { + const { config } = useEditTransformFlyoutContext(); + const selectIsFormTouched = useMemo(() => createSelectIsFormTouched(config), [config]); + return useSelector(selectIsFormTouched); +}; diff --git a/x-pack/plugins/transform/public/app/sections/edit_transform/state_management/selectors/is_form_valid.ts b/x-pack/plugins/transform/public/app/sections/edit_transform/state_management/selectors/is_form_valid.ts new file mode 100644 index 0000000000000..2a89de46d29e0 --- /dev/null +++ b/x-pack/plugins/transform/public/app/sections/edit_transform/state_management/selectors/is_form_valid.ts @@ -0,0 +1,20 @@ +/* + * 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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { createSelector } from '@reduxjs/toolkit'; +import { useSelector } from 'react-redux'; + +import type { State } from '../edit_transform_flyout_state'; + +import type { FormFieldsState } from '../form_field'; + +// Checks each form field for error messages to return +// if the overall form is valid or not. +const isFormValid = (formFields: FormFieldsState) => + Object.values(formFields).every((d) => d.errorMessages.length === 0); +const selectIsFormValid = createSelector((state: State) => state.formFields, isFormValid); +export const useIsFormValid = () => useSelector(selectIsFormValid); diff --git a/x-pack/plugins/transform/public/app/sections/edit_transform/state_management/selectors/retention_policy_field.ts b/x-pack/plugins/transform/public/app/sections/edit_transform/state_management/selectors/retention_policy_field.ts new file mode 100644 index 0000000000000..410a0ef5f1444 --- /dev/null +++ b/x-pack/plugins/transform/public/app/sections/edit_transform/state_management/selectors/retention_policy_field.ts @@ -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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useSelector } from 'react-redux'; + +import type { State } from '../edit_transform_flyout_state'; + +export const selectRetentionPolicyField = (s: State) => s.formFields.retentionPolicyField; + +export const useRetentionPolicyField = () => { + return useSelector(selectRetentionPolicyField); +}; diff --git a/x-pack/plugins/transform/public/app/sections/edit_transform/state_management/selectors/updated_transform_config.ts b/x-pack/plugins/transform/public/app/sections/edit_transform/state_management/selectors/updated_transform_config.ts new file mode 100644 index 0000000000000..4c9a35f121ca4 --- /dev/null +++ b/x-pack/plugins/transform/public/app/sections/edit_transform/state_management/selectors/updated_transform_config.ts @@ -0,0 +1,29 @@ +/* + * 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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useMemo } from 'react'; +import { createSelector } from '@reduxjs/toolkit'; +import { useSelector } from 'react-redux'; + +import { TransformConfigUnion } from '../../../../../../common/types/transform'; + +import { applyFormStateToTransformConfig } from '../apply_form_state_to_transform_config'; +import { useEditTransformFlyoutContext, type State } from '../edit_transform_flyout_state'; + +const createSelectTransformConfig = (originalConfig: TransformConfigUnion) => + createSelector( + (state: State) => state.formFields, + (state: State) => state.formSections, + (formFields, formSections) => + applyFormStateToTransformConfig(originalConfig, formFields, formSections) + ); + +export const useUpdatedTransformConfig = () => { + const { config } = useEditTransformFlyoutContext(); + const selectTransformConfig = useMemo(() => createSelectTransformConfig(config), [config]); + return useSelector(selectTransformConfig); +}; diff --git a/x-pack/plugins/transform/public/app/sections/edit_transform/state_management/validators.ts b/x-pack/plugins/transform/public/app/sections/edit_transform/state_management/validators.ts new file mode 100644 index 0000000000000..82ef6476a31a1 --- /dev/null +++ b/x-pack/plugins/transform/public/app/sections/edit_transform/state_management/validators.ts @@ -0,0 +1,25 @@ +/* + * 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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { + frequencyValidator, + integerAboveZeroValidator, + transformSettingsNumberOfRetriesValidator, + transformSettingsPageSearchSizeValidator, + retentionPolicyMaxAgeValidator, + stringValidator, +} from '../../../common/validators'; + +export const validators = { + frequencyValidator, + integerAboveZeroValidator, + transformSettingsNumberOfRetriesValidator, + transformSettingsPageSearchSizeValidator, + retentionPolicyMaxAgeValidator, + stringValidator, +}; +export type ValidatorName = keyof typeof validators; diff --git a/x-pack/plugins/transform/public/app/sections/edit_transform/state_management/value_parsers.ts b/x-pack/plugins/transform/public/app/sections/edit_transform/state_management/value_parsers.ts new file mode 100644 index 0000000000000..f04547828e781 --- /dev/null +++ b/x-pack/plugins/transform/public/app/sections/edit_transform/state_management/value_parsers.ts @@ -0,0 +1,19 @@ +/* + * 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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +// Note on the form validation and input components used: +// All inputs use `EuiFieldText` which means all form values will be treated as strings. +// This means we cast other formats like numbers coming from the transform config to strings, +// then revalidate them and cast them again to number before submitting a transform update. +// We do this so we have fine grained control over field validation and the option to +// cast to special values like `null` for disabling `docs_per_second`. +export const valueParsers = { + defaultParser: (v: string) => v, + nullableNumberParser: (v: string) => (v === '' ? null : +v), + numberParser: (v: string) => +v, +}; +export type ValueParserName = keyof typeof valueParsers; diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/capitalize_first_letter.ts b/x-pack/plugins/transform/public/app/sections/edit_transform/utils/capitalize_first_letter.ts similarity index 100% rename from x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/capitalize_first_letter.ts rename to x-pack/plugins/transform/public/app/sections/edit_transform/utils/capitalize_first_letter.ts diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_flyout_state.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_flyout_state.tsx deleted file mode 100644 index 60fa2b7be8beb..0000000000000 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_flyout_state.tsx +++ /dev/null @@ -1,519 +0,0 @@ -/* - * 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; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { isEqual, merge } from 'lodash'; -import React, { - createContext, - useContext, - useEffect, - useMemo, - type FC, - type PropsWithChildren, -} from 'react'; -import { configureStore, createSelector, createSlice, type PayloadAction } from '@reduxjs/toolkit'; -import { useDispatch, useSelector, Provider } from 'react-redux'; -import { bindActionCreators } from 'redux'; - -import { getNestedProperty, setNestedProperty } from '@kbn/ml-nested-property'; - -import { PostTransformsUpdateRequestSchema } from '../../../../../../common/api_schemas/update_transforms'; -import { - DEFAULT_TRANSFORM_FREQUENCY, - DEFAULT_TRANSFORM_SETTINGS_MAX_PAGE_SEARCH_SIZE, -} from '../../../../../../common/constants'; -import { TransformConfigUnion } from '../../../../../../common/types/transform'; - -// Note on the form validation and input components used: -// All inputs use `EuiFieldText` which means all form values will be treated as strings. -// This means we cast other formats like numbers coming from the transform config to strings, -// then revalidate them and cast them again to number before submitting a transform update. -// We do this so we have fine grained control over field validation and the option to -// cast to special values like `null` for disabling `docs_per_second`. -import { - frequencyValidator, - integerAboveZeroValidator, - transformSettingsNumberOfRetriesValidator, - transformSettingsPageSearchSizeValidator, - retentionPolicyMaxAgeValidator, - stringValidator, -} from '../../../../common/validators'; - -const validators = { - frequencyValidator, - integerAboveZeroValidator, - transformSettingsNumberOfRetriesValidator, - transformSettingsPageSearchSizeValidator, - retentionPolicyMaxAgeValidator, - stringValidator, -}; -type ValidatorName = keyof typeof validators; - -const valueParsers = { - defaultParser: (v: string) => v, - nullableNumberParser: (v: string) => (v === '' ? null : +v), - numberParser: (v: string) => +v, -}; -type ValueParserName = keyof typeof valueParsers; - -// The edit transform flyout uses a generic framework based on redux-toolkit to manage its form state -// that supports applying its tate to a nested configuration object suitable for passing on -// directly to an API call. For now this is only used for the transform edit form. -// Once we apply the functionality to other places, e.g. the transform creation wizard, -// the generic framework code in this file should be moved to a dedicated location. - -// The form state defines a flat structure of names for form fields. -// This is a flat structure regardless of whether the final config object will be nested. -// For example, `destinationIndex` and `destinationIngestPipeline` will later be nested under `dest`. -export type FormFields = - | 'description' - | 'destinationIndex' - | 'destinationIngestPipeline' - | 'docsPerSecond' - | 'frequency' - | 'maxPageSearchSize' - | 'numFailureRetries' - | 'retentionPolicyField' - | 'retentionPolicyMaxAge'; - -type FormFieldsState = Record; - -export interface FormField { - formFieldName: FormFields; - configFieldName: string; - defaultValue: string; - dependsOn: FormFields[]; - errorMessages: string[]; - isNullable: boolean; - isOptional: boolean; - isOptionalInSection?: boolean; - section?: FormSections; - validator: ValidatorName; - value: string; - valueParser: ValueParserName; -} - -// Defining these sections is only necessary for options where a reset/deletion of that part of the -// configuration is supported by the API. For example, this isn't suitable to use with `dest` since -// this overall part of the configuration is not optional. However, `retention_policy` is optional, -// so we need to support to recognize this based on the form state and be able to reset it by -// created a request body containing `{ retention_policy: null }`. -type FormSections = 'retentionPolicy'; - -export interface FormSection { - formSectionName: FormSections; - configFieldName: string; - defaultEnabled: boolean; - enabled: boolean; -} - -type FormSectionsState = Record; - -// The utility functions in this file provide the following features: -// - getDefaultState() -// Sets up the initial form state. It supports overrides to apply a pre-existing configuration. -// The implementation of this function is the only one that's specifically required to define -// the features of the transform edit form. All other functions are generic and could be reused -// in the future for other forms. -// -// - applyFormStateToTransformConfig() (iterates over getUpdateValue()) -// Once a user hits the update button, these functions take care of extracting the information -// necessary to create the update request. They take into account whether a field needs to -// be included at all in the request (for example, if it hadn't been changed). -// The code is also able to identify relationships/dependencies between form fields. -// For example, if the `pipeline` field was changed, it's necessary to make the `index` -// field part of the request, otherwise the update would fail. - -export const initializeFormField = ( - formFieldName: FormFields, - configFieldName: string, - config?: TransformConfigUnion, - overloads?: Partial -): FormField => { - const defaultValue = overloads?.defaultValue !== undefined ? overloads.defaultValue : ''; - const rawValue = getNestedProperty(config ?? {}, configFieldName, undefined); - const value = rawValue !== null && rawValue !== undefined ? rawValue.toString() : ''; - - return { - formFieldName, - configFieldName, - defaultValue, - dependsOn: [], - errorMessages: [], - isNullable: false, - isOptional: true, - validator: 'stringValidator', - value, - valueParser: 'defaultParser', - ...(overloads !== undefined ? { ...overloads } : {}), - }; -}; - -export const initializeFormSection = ( - formSectionName: FormSections, - configFieldName: string, - config?: TransformConfigUnion, - overloads?: Partial -): FormSection => { - const defaultEnabled = overloads?.defaultEnabled ?? false; - const rawEnabled = getNestedProperty(config ?? {}, configFieldName, undefined); - const enabled = rawEnabled !== undefined && rawEnabled !== null; - - return { - formSectionName, - configFieldName, - defaultEnabled, - enabled, - }; -}; - -// Takes a value from form state and applies it to the structure -// of the expected final configuration request object. -// Considers options like if a value is nullable or optional. -const getUpdateValue = ( - attribute: FormFields, - config: TransformConfigUnion, - formFields: FormFieldsState, - formSections: FormSectionsState, - enforceFormValue = false -) => { - const formStateAttribute = formFields[attribute]; - const fallbackValue = formStateAttribute.isNullable ? null : formStateAttribute.defaultValue; - - const enabledBasedOnSection = - formStateAttribute.section !== undefined - ? formSections[formStateAttribute.section].enabled - : true; - - const formValue = - formStateAttribute.value !== '' - ? valueParsers[formStateAttribute.valueParser](formStateAttribute.value) - : fallbackValue; - - const configValue = getNestedProperty(config, formStateAttribute.configFieldName, fallbackValue); - - // only get depending values if we're not already in a call to get depending values. - const dependsOnConfig: PostTransformsUpdateRequestSchema = - enforceFormValue === false - ? formStateAttribute.dependsOn.reduce((_dependsOnConfig, dependsOnField) => { - return merge( - { ..._dependsOnConfig }, - getUpdateValue(dependsOnField, config, formFields, formSections, true) - ); - }, {}) - : {}; - - if ( - formValue === formStateAttribute.defaultValue && - formValue === configValue && - formStateAttribute.isOptional - ) { - return {}; - } - - // If the resettable section the form field belongs to is disabled, - // the whole section will be set to `null` to do the actual reset. - if (formStateAttribute.section !== undefined && !enabledBasedOnSection) { - return setNestedProperty( - dependsOnConfig, - formSections[formStateAttribute.section].configFieldName, - null - ); - } - - return enabledBasedOnSection && (formValue !== configValue || enforceFormValue) - ? setNestedProperty( - dependsOnConfig, - formStateAttribute.configFieldName, - formValue === '' && formStateAttribute.isOptional ? undefined : formValue - ) - : {}; -}; - -// Takes in the form configuration and returns a -// request object suitable to be sent to the -// transform update API endpoint. -export const applyFormStateToTransformConfig = ( - config: TransformConfigUnion, - formFields: FormFieldsState, - formSections: FormSectionsState -): PostTransformsUpdateRequestSchema => - // Iterates over all form fields and only if necessary applies them to - // the request object used for updating the transform. - (Object.keys(formFields) as FormFields[]).reduce( - (updateConfig, field) => - merge({ ...updateConfig }, getUpdateValue(field, config, formFields, formSections)), - {} - ); -const createSelectTransformConfig = (originalConfig: TransformConfigUnion) => - createSelector( - (state: State) => state.formFields, - (state: State) => state.formSections, - (formFields, formSections) => - applyFormStateToTransformConfig(originalConfig, formFields, formSections) - ); -export const useUpdatedTransformConfig = () => { - const { config } = useEditTransformFlyoutContext(); - const selectTransformConfig = useMemo(() => createSelectTransformConfig(config), [config]); - return useSelector(selectTransformConfig); -}; - -// Takes in a transform configuration and returns -// the default state to populate the form. -export const getDefaultState = (config?: TransformConfigUnion): State => ({ - formFields: { - // top level attributes - description: initializeFormField('description', 'description', config), - frequency: initializeFormField('frequency', 'frequency', config, { - defaultValue: DEFAULT_TRANSFORM_FREQUENCY, - validator: 'frequencyValidator', - }), - - // dest.* - destinationIndex: initializeFormField('destinationIndex', 'dest.index', config, { - dependsOn: ['destinationIngestPipeline'], - isOptional: false, - }), - destinationIngestPipeline: initializeFormField( - 'destinationIngestPipeline', - 'dest.pipeline', - config, - { - dependsOn: ['destinationIndex'], - isOptional: true, - } - ), - - // settings.* - docsPerSecond: initializeFormField('docsPerSecond', 'settings.docs_per_second', config, { - isNullable: true, - isOptional: true, - validator: 'integerAboveZeroValidator', - valueParser: 'nullableNumberParser', - }), - maxPageSearchSize: initializeFormField( - 'maxPageSearchSize', - 'settings.max_page_search_size', - config, - { - defaultValue: `${DEFAULT_TRANSFORM_SETTINGS_MAX_PAGE_SEARCH_SIZE}`, - isNullable: true, - isOptional: true, - validator: 'transformSettingsPageSearchSizeValidator', - valueParser: 'numberParser', - } - ), - numFailureRetries: initializeFormField( - 'numFailureRetries', - 'settings.num_failure_retries', - config, - { - defaultValue: undefined, - isNullable: true, - isOptional: true, - validator: 'transformSettingsNumberOfRetriesValidator', - valueParser: 'numberParser', - } - ), - - // retention_policy.* - retentionPolicyField: initializeFormField( - 'retentionPolicyField', - 'retention_policy.time.field', - config, - { - dependsOn: ['retentionPolicyMaxAge'], - isNullable: false, - isOptional: true, - isOptionalInSection: false, - section: 'retentionPolicy', - } - ), - retentionPolicyMaxAge: initializeFormField( - 'retentionPolicyMaxAge', - 'retention_policy.time.max_age', - config, - { - dependsOn: ['retentionPolicyField'], - isNullable: false, - isOptional: true, - isOptionalInSection: false, - section: 'retentionPolicy', - validator: 'retentionPolicyMaxAgeValidator', - } - ), - }, - formSections: { - retentionPolicy: initializeFormSection('retentionPolicy', 'retention_policy', config), - }, -}); - -// Checks each form field for error messages to return -// if the overall form is valid or not. -const isFormValid = (formFields: FormFieldsState) => - Object.values(formFields).every((d) => d.errorMessages.length === 0); -const selectIsFormValid = createSelector((state: State) => state.formFields, isFormValid); -export const useIsFormValid = () => useSelector(selectIsFormValid); - -const getFieldValues = (fields: FormFieldsState) => Object.values(fields).map((f) => f.value); -const getSectionValues = (sections: FormSectionsState) => - Object.values(sections).map((s) => s.enabled); - -interface ProviderProps { - config: TransformConfigUnion; - dataViewId?: string; -} - -interface State { - apiErrorMessage?: string; - formFields: FormFieldsState; - formSections: FormSectionsState; -} - -const isFormTouched = ( - config: TransformConfigUnion, - formFields: FormFieldsState, - formSections: FormSectionsState -) => { - const defaultState = getDefaultState(config); - return ( - !isEqual(getFieldValues(defaultState.formFields), getFieldValues(formFields)) || - !isEqual(getSectionValues(defaultState.formSections), getSectionValues(formSections)) - ); -}; -const createSelectIsFormTouched = (originalConfig: TransformConfigUnion) => - createSelector( - (state: State) => state.formFields, - (state: State) => state.formSections, - (formFields, formSections) => isFormTouched(originalConfig, formFields, formSections) - ); -export const useIsFormTouched = () => { - const { config } = useEditTransformFlyoutContext(); - const selectIsFormTouched = useMemo(() => createSelectIsFormTouched(config), [config]); - return useSelector(selectIsFormTouched); -}; - -function isFormFieldOptional(state: State, field: FormFields) { - const formField = state.formFields[field]; - - let isOptional = formField.isOptional; - if (formField.section) { - const section = state.formSections[formField.section]; - if (section.enabled && formField.isOptionalInSection === false) { - isOptional = false; - } - } - - return isOptional; -} - -function getFormFieldErrorMessages( - value: string, - isOptional: boolean, - validatorName: ValidatorName -) { - return isOptional && typeof value === 'string' && value.length === 0 - ? [] - : validators[validatorName](value, isOptional); -} -const initialize = (_: State, action: PayloadAction) => - getDefaultState(action.payload.config); - -const setApiError = (state: State, action: PayloadAction) => { - state.apiErrorMessage = action.payload; -}; - -const setFormField = ( - state: State, - action: PayloadAction<{ field: FormFields; value: string }> -) => { - const formField = state.formFields[action.payload.field]; - const isOptional = isFormFieldOptional(state, action.payload.field); - - formField.errorMessages = getFormFieldErrorMessages( - action.payload.value, - isOptional, - formField.validator - ); - - formField.value = action.payload.value; -}; - -const setFormSection = ( - state: State, - action: PayloadAction<{ section: FormSections; enabled: boolean }> -) => { - state.formSections[action.payload.section].enabled = action.payload.enabled; - - // After a section change we re-evaluate all form fields, since optionality - // of a field could change if a section got toggled. - Object.entries(state.formFields).forEach(([formFieldName, formField]) => { - const isOptional = isFormFieldOptional(state, formFieldName as FormFields); - formField.errorMessages = getFormFieldErrorMessages( - formField.value, - isOptional, - formField.validator - ); - }); -}; - -const editTransformFlyoutSlice = createSlice({ - name: 'editTransformFlyout', - initialState: getDefaultState(), - reducers: { - initialize, - setApiError, - setFormField, - setFormSection, - }, -}); - -const getReduxStore = () => - configureStore({ - reducer: editTransformFlyoutSlice.reducer, - }); - -const EditTransformFlyoutContext = createContext(null); - -export const EditTransformFlyoutProvider: FC> = ({ - children, - ...props -}) => { - const store = useMemo(getReduxStore, []); - - // Apply original transform config to redux form state. - useEffect(() => { - store.dispatch(editTransformFlyoutSlice.actions.initialize(props)); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - return ( - - {children} - - ); -}; - -export const useEditTransformFlyoutContext = () => { - const c = useContext(EditTransformFlyoutContext); - if (c === null) throw new Error('EditTransformFlyoutContext not set.'); - return c; -}; - -export const useEditTransformFlyoutActions = () => { - const dispatch = useDispatch(); - return bindActionCreators(editTransformFlyoutSlice.actions, dispatch); -}; - -const createSelectFormField = (field: FormFields) => (s: State) => s.formFields[field]; -export const useFormField = (field: FormFields) => { - const selectFormField = useMemo(() => createSelectFormField(field), [field]); - return useSelector(selectFormField); -}; - -export const selectApiErrorMessage = (s: State) => s.apiErrorMessage; -export const selectFormSections = (s: State) => s.formSections; -export const selectRetentionPolicyField = (s: State) => s.formFields.retentionPolicyField; diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/use_actions.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/use_actions.tsx index b46628aeb4eff..31d39b12c4a36 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/use_actions.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/use_actions.tsx @@ -15,7 +15,7 @@ import { TransformListRow } from '../../../../common'; import { useCloneAction } from '../action_clone'; import { useDeleteAction, DeleteActionModal } from '../action_delete'; import { useDiscoverAction } from '../action_discover'; -import { EditTransformFlyout } from '../edit_transform_flyout'; +import { EditTransformFlyout } from '../../../edit_transform'; import { useEditAction } from '../action_edit'; import { useResetAction, ResetActionModal } from '../action_reset'; import { useScheduleNowAction } from '../action_schedule_now'; From ca3467bd471c0e46369d562e5348fcdb061d833d Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Tue, 16 Jan 2024 09:52:17 +0100 Subject: [PATCH 32/36] improve selector reuse --- .../state_management/selectors/api_error_message.ts | 3 +-- .../state_management/selectors/form_field.ts | 3 ++- .../state_management/selectors/form_sections.ts | 1 - .../state_management/selectors/is_form_touched.ts | 11 +++++------ .../state_management/selectors/is_form_valid.ts | 6 +++--- .../selectors/retention_policy_field.ts | 1 - .../selectors/updated_transform_config.ts | 12 ++++++------ 7 files changed, 17 insertions(+), 20 deletions(-) diff --git a/x-pack/plugins/transform/public/app/sections/edit_transform/state_management/selectors/api_error_message.ts b/x-pack/plugins/transform/public/app/sections/edit_transform/state_management/selectors/api_error_message.ts index 7127c28136ef1..710cb3d8a5f5b 100644 --- a/x-pack/plugins/transform/public/app/sections/edit_transform/state_management/selectors/api_error_message.ts +++ b/x-pack/plugins/transform/public/app/sections/edit_transform/state_management/selectors/api_error_message.ts @@ -9,8 +9,7 @@ import { useSelector } from 'react-redux'; import type { State } from '../edit_transform_flyout_state'; -export const selectApiErrorMessage = (s: State) => s.apiErrorMessage; - +const selectApiErrorMessage = (s: State) => s.apiErrorMessage; export const useApiErrorMessage = () => { return useSelector(selectApiErrorMessage); }; diff --git a/x-pack/plugins/transform/public/app/sections/edit_transform/state_management/selectors/form_field.ts b/x-pack/plugins/transform/public/app/sections/edit_transform/state_management/selectors/form_field.ts index 7b3e4a4f5b100..920bb67cb1154 100644 --- a/x-pack/plugins/transform/public/app/sections/edit_transform/state_management/selectors/form_field.ts +++ b/x-pack/plugins/transform/public/app/sections/edit_transform/state_management/selectors/form_field.ts @@ -13,8 +13,9 @@ import type { State } from '../edit_transform_flyout_state'; import type { FormFields } from '../form_field'; -const createSelectFormField = (field: FormFields) => (s: State) => s.formFields[field]; +export const selectFormFields = (s: State) => s.formFields; +const createSelectFormField = (field: FormFields) => (s: State) => s.formFields[field]; export const useFormField = (field: FormFields) => { const selectFormField = useMemo(() => createSelectFormField(field), [field]); return useSelector(selectFormField); diff --git a/x-pack/plugins/transform/public/app/sections/edit_transform/state_management/selectors/form_sections.ts b/x-pack/plugins/transform/public/app/sections/edit_transform/state_management/selectors/form_sections.ts index 2a9f4b1d60cfe..e56f48aa21a5e 100644 --- a/x-pack/plugins/transform/public/app/sections/edit_transform/state_management/selectors/form_sections.ts +++ b/x-pack/plugins/transform/public/app/sections/edit_transform/state_management/selectors/form_sections.ts @@ -10,7 +10,6 @@ import { useSelector } from 'react-redux'; import type { State } from '../edit_transform_flyout_state'; export const selectFormSections = (s: State) => s.formSections; - export const useFormSections = () => { return useSelector(selectFormSections); }; diff --git a/x-pack/plugins/transform/public/app/sections/edit_transform/state_management/selectors/is_form_touched.ts b/x-pack/plugins/transform/public/app/sections/edit_transform/state_management/selectors/is_form_touched.ts index 8bdb390d73378..043aea42898fb 100644 --- a/x-pack/plugins/transform/public/app/sections/edit_transform/state_management/selectors/is_form_touched.ts +++ b/x-pack/plugins/transform/public/app/sections/edit_transform/state_management/selectors/is_form_touched.ts @@ -13,13 +13,14 @@ import { useSelector } from 'react-redux'; import type { TransformConfigUnion } from '../../../../../../common/types/transform'; -import type { State } from '../edit_transform_flyout_state'; - import type { FormFieldsState } from '../form_field'; import type { FormSectionsState } from '../form_section'; import { getDefaultState } from '../get_default_state'; import { useEditTransformFlyoutContext } from '../edit_transform_flyout_state'; +import { selectFormFields } from './form_field'; +import { selectFormSections } from './form_sections'; + const getFieldValues = (fields: FormFieldsState) => Object.values(fields).map((f) => f.value); const getSectionValues = (sections: FormSectionsState) => Object.values(sections).map((s) => s.enabled); @@ -37,10 +38,8 @@ const isFormTouched = ( }; const createSelectIsFormTouched = (originalConfig: TransformConfigUnion) => - createSelector( - (state: State) => state.formFields, - (state: State) => state.formSections, - (formFields, formSections) => isFormTouched(originalConfig, formFields, formSections) + createSelector(selectFormFields, selectFormSections, (formFields, formSections) => + isFormTouched(originalConfig, formFields, formSections) ); export const useIsFormTouched = () => { diff --git a/x-pack/plugins/transform/public/app/sections/edit_transform/state_management/selectors/is_form_valid.ts b/x-pack/plugins/transform/public/app/sections/edit_transform/state_management/selectors/is_form_valid.ts index 2a89de46d29e0..2fa91d65c7be0 100644 --- a/x-pack/plugins/transform/public/app/sections/edit_transform/state_management/selectors/is_form_valid.ts +++ b/x-pack/plugins/transform/public/app/sections/edit_transform/state_management/selectors/is_form_valid.ts @@ -8,13 +8,13 @@ import { createSelector } from '@reduxjs/toolkit'; import { useSelector } from 'react-redux'; -import type { State } from '../edit_transform_flyout_state'; - import type { FormFieldsState } from '../form_field'; +import { selectFormFields } from './form_field'; + // Checks each form field for error messages to return // if the overall form is valid or not. const isFormValid = (formFields: FormFieldsState) => Object.values(formFields).every((d) => d.errorMessages.length === 0); -const selectIsFormValid = createSelector((state: State) => state.formFields, isFormValid); +const selectIsFormValid = createSelector(selectFormFields, isFormValid); export const useIsFormValid = () => useSelector(selectIsFormValid); diff --git a/x-pack/plugins/transform/public/app/sections/edit_transform/state_management/selectors/retention_policy_field.ts b/x-pack/plugins/transform/public/app/sections/edit_transform/state_management/selectors/retention_policy_field.ts index 410a0ef5f1444..f899ff3426694 100644 --- a/x-pack/plugins/transform/public/app/sections/edit_transform/state_management/selectors/retention_policy_field.ts +++ b/x-pack/plugins/transform/public/app/sections/edit_transform/state_management/selectors/retention_policy_field.ts @@ -10,7 +10,6 @@ import { useSelector } from 'react-redux'; import type { State } from '../edit_transform_flyout_state'; export const selectRetentionPolicyField = (s: State) => s.formFields.retentionPolicyField; - export const useRetentionPolicyField = () => { return useSelector(selectRetentionPolicyField); }; diff --git a/x-pack/plugins/transform/public/app/sections/edit_transform/state_management/selectors/updated_transform_config.ts b/x-pack/plugins/transform/public/app/sections/edit_transform/state_management/selectors/updated_transform_config.ts index 4c9a35f121ca4..a7d82c75746f1 100644 --- a/x-pack/plugins/transform/public/app/sections/edit_transform/state_management/selectors/updated_transform_config.ts +++ b/x-pack/plugins/transform/public/app/sections/edit_transform/state_management/selectors/updated_transform_config.ts @@ -12,14 +12,14 @@ import { useSelector } from 'react-redux'; import { TransformConfigUnion } from '../../../../../../common/types/transform'; import { applyFormStateToTransformConfig } from '../apply_form_state_to_transform_config'; -import { useEditTransformFlyoutContext, type State } from '../edit_transform_flyout_state'; +import { useEditTransformFlyoutContext } from '../edit_transform_flyout_state'; + +import { selectFormFields } from './form_field'; +import { selectFormSections } from './form_sections'; const createSelectTransformConfig = (originalConfig: TransformConfigUnion) => - createSelector( - (state: State) => state.formFields, - (state: State) => state.formSections, - (formFields, formSections) => - applyFormStateToTransformConfig(originalConfig, formFields, formSections) + createSelector(selectFormFields, selectFormSections, (formFields, formSections) => + applyFormStateToTransformConfig(originalConfig, formFields, formSections) ); export const useUpdatedTransformConfig = () => { From b8cc881138e279f3a55547c92389fb102413934c Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Wed, 17 Jan 2024 13:10:58 +0100 Subject: [PATCH 33/36] add type to imports --- .../state_management/apply_form_state_to_transform_config.ts | 4 ++-- .../state_management/edit_transform_flyout_state.tsx | 2 +- .../sections/edit_transform/state_management/form_field.ts | 2 +- .../sections/edit_transform/state_management/form_section.ts | 2 +- .../edit_transform/state_management/get_default_state.ts | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/x-pack/plugins/transform/public/app/sections/edit_transform/state_management/apply_form_state_to_transform_config.ts b/x-pack/plugins/transform/public/app/sections/edit_transform/state_management/apply_form_state_to_transform_config.ts index 830316c27f72f..2e0f7d05ad287 100644 --- a/x-pack/plugins/transform/public/app/sections/edit_transform/state_management/apply_form_state_to_transform_config.ts +++ b/x-pack/plugins/transform/public/app/sections/edit_transform/state_management/apply_form_state_to_transform_config.ts @@ -7,8 +7,8 @@ import { merge } from 'lodash'; -import { PostTransformsUpdateRequestSchema } from '../../../../../common/api_schemas/update_transforms'; -import { TransformConfigUnion } from '../../../../../common/types/transform'; +import type { PostTransformsUpdateRequestSchema } from '../../../../../common/api_schemas/update_transforms'; +import type { TransformConfigUnion } from '../../../../../common/types/transform'; import { getUpdateValue } from './get_update_value'; diff --git a/x-pack/plugins/transform/public/app/sections/edit_transform/state_management/edit_transform_flyout_state.tsx b/x-pack/plugins/transform/public/app/sections/edit_transform/state_management/edit_transform_flyout_state.tsx index c82243c408a0c..19851d0ef3e52 100644 --- a/x-pack/plugins/transform/public/app/sections/edit_transform/state_management/edit_transform_flyout_state.tsx +++ b/x-pack/plugins/transform/public/app/sections/edit_transform/state_management/edit_transform_flyout_state.tsx @@ -17,7 +17,7 @@ import { configureStore, createSlice } from '@reduxjs/toolkit'; import { useDispatch, Provider } from 'react-redux'; import { bindActionCreators } from 'redux'; -import { TransformConfigUnion } from '../../../../../common/types/transform'; +import type { TransformConfigUnion } from '../../../../../common/types/transform'; import { initialize, setApiError, setFormField, setFormSection } from './actions'; import { type FormFieldsState } from './form_field'; diff --git a/x-pack/plugins/transform/public/app/sections/edit_transform/state_management/form_field.ts b/x-pack/plugins/transform/public/app/sections/edit_transform/state_management/form_field.ts index ea02fbe7ccd78..428f50d289c61 100644 --- a/x-pack/plugins/transform/public/app/sections/edit_transform/state_management/form_field.ts +++ b/x-pack/plugins/transform/public/app/sections/edit_transform/state_management/form_field.ts @@ -7,7 +7,7 @@ import { getNestedProperty } from '@kbn/ml-nested-property'; -import { TransformConfigUnion } from '../../../../../common/types/transform'; +import type { TransformConfigUnion } from '../../../../../common/types/transform'; import type { FormSections } from './form_section'; import type { ValidatorName } from './validators'; diff --git a/x-pack/plugins/transform/public/app/sections/edit_transform/state_management/form_section.ts b/x-pack/plugins/transform/public/app/sections/edit_transform/state_management/form_section.ts index 82e14aa6c9c4c..b90ada983352b 100644 --- a/x-pack/plugins/transform/public/app/sections/edit_transform/state_management/form_section.ts +++ b/x-pack/plugins/transform/public/app/sections/edit_transform/state_management/form_section.ts @@ -7,7 +7,7 @@ import { getNestedProperty } from '@kbn/ml-nested-property'; -import { TransformConfigUnion } from '../../../../../common/types/transform'; +import type { TransformConfigUnion } from '../../../../../common/types/transform'; // Defining these sections is only necessary for options where a reset/deletion of that part of the // configuration is supported by the API. For example, this isn't suitable to use with `dest` since diff --git a/x-pack/plugins/transform/public/app/sections/edit_transform/state_management/get_default_state.ts b/x-pack/plugins/transform/public/app/sections/edit_transform/state_management/get_default_state.ts index 9f8365b8c6a17..859000fdc0ecf 100644 --- a/x-pack/plugins/transform/public/app/sections/edit_transform/state_management/get_default_state.ts +++ b/x-pack/plugins/transform/public/app/sections/edit_transform/state_management/get_default_state.ts @@ -9,7 +9,7 @@ import { DEFAULT_TRANSFORM_FREQUENCY, DEFAULT_TRANSFORM_SETTINGS_MAX_PAGE_SEARCH_SIZE, } from '../../../../../common/constants'; -import { TransformConfigUnion } from '../../../../../common/types/transform'; +import type { TransformConfigUnion } from '../../../../../common/types/transform'; import { initializeFormField } from './form_field'; import { initializeFormSection } from './form_section'; From 358341c8fff301853c5abdc46e4c5be948bc25f4 Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Wed, 17 Jan 2024 13:12:09 +0100 Subject: [PATCH 34/36] fix comment typo --- .../sections/edit_transform/state_management/form_section.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/transform/public/app/sections/edit_transform/state_management/form_section.ts b/x-pack/plugins/transform/public/app/sections/edit_transform/state_management/form_section.ts index b90ada983352b..f9a41653b0d1a 100644 --- a/x-pack/plugins/transform/public/app/sections/edit_transform/state_management/form_section.ts +++ b/x-pack/plugins/transform/public/app/sections/edit_transform/state_management/form_section.ts @@ -13,7 +13,7 @@ import type { TransformConfigUnion } from '../../../../../common/types/transform // configuration is supported by the API. For example, this isn't suitable to use with `dest` since // this overall part of the configuration is not optional. However, `retention_policy` is optional, // so we need to support to recognize this based on the form state and be able to reset it by -// created a request body containing `{ retention_policy: null }`. +// creating a request body containing `{ retention_policy: null }`. export type FormSections = 'retentionPolicy'; export interface FormSection { From 788a4e462aaa7eabbdecf5c9a86b4c09caa56619 Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Wed, 17 Jan 2024 13:14:37 +0100 Subject: [PATCH 35/36] use isDefined --- .../sections/edit_transform/state_management/form_section.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/transform/public/app/sections/edit_transform/state_management/form_section.ts b/x-pack/plugins/transform/public/app/sections/edit_transform/state_management/form_section.ts index f9a41653b0d1a..7f7c49b19d5f4 100644 --- a/x-pack/plugins/transform/public/app/sections/edit_transform/state_management/form_section.ts +++ b/x-pack/plugins/transform/public/app/sections/edit_transform/state_management/form_section.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { isDefined } from '@kbn/ml-is-defined'; import { getNestedProperty } from '@kbn/ml-nested-property'; import type { TransformConfigUnion } from '../../../../../common/types/transform'; @@ -33,7 +34,7 @@ export const initializeFormSection = ( ): FormSection => { const defaultEnabled = overloads?.defaultEnabled ?? false; const rawEnabled = getNestedProperty(config ?? {}, configFieldName, undefined); - const enabled = rawEnabled !== undefined && rawEnabled !== null; + const enabled = isDefined(rawEnabled); return { formSectionName, From 31d5d15577559420ba7ed24370b705e1cce52227 Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Wed, 17 Jan 2024 13:17:17 +0100 Subject: [PATCH 36/36] use useMount --- .../edit_transform_flyout_state.tsx | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/x-pack/plugins/transform/public/app/sections/edit_transform/state_management/edit_transform_flyout_state.tsx b/x-pack/plugins/transform/public/app/sections/edit_transform/state_management/edit_transform_flyout_state.tsx index 19851d0ef3e52..b79fbd55362f6 100644 --- a/x-pack/plugins/transform/public/app/sections/edit_transform/state_management/edit_transform_flyout_state.tsx +++ b/x-pack/plugins/transform/public/app/sections/edit_transform/state_management/edit_transform_flyout_state.tsx @@ -5,17 +5,11 @@ * 2.0. */ -import React, { - createContext, - useContext, - useEffect, - useMemo, - type FC, - type PropsWithChildren, -} from 'react'; +import React, { createContext, useContext, useMemo, type FC, type PropsWithChildren } from 'react'; import { configureStore, createSlice } from '@reduxjs/toolkit'; import { useDispatch, Provider } from 'react-redux'; import { bindActionCreators } from 'redux'; +import useMount from 'react-use/lib/useMount'; import type { TransformConfigUnion } from '../../../../../common/types/transform'; @@ -66,10 +60,9 @@ export const EditTransformFlyoutProvider: FC> = const store = useMemo(getReduxStore, []); // Apply original transform config to redux form state. - useEffect(() => { + useMount(() => { store.dispatch(editTransformFlyoutSlice.actions.initialize(props)); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); + }); return (