From 11838b88ddf752701b2c09b32bbd13cc640d7a82 Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Mon, 7 Feb 2022 13:18:51 +0100 Subject: [PATCH] [ML] Transforms: Fix retention policy reset (#124698) The transform edit form was not able to reset the retention policy configuration by emptying the existing form field. This fix adds a switch similar to the creation wizard to allow the user to completely enable/disable the retention policy. The form state management was updated to support passing on `{ retention_policy: null }` to reset the config. (cherry picked from commit ca77565228367ace82a0bba254a4be8d6f0fb028) --- .../common/api_schemas/update_transforms.ts | 4 +- .../edit_transform_flyout.tsx | 4 +- .../edit_transform_flyout_form.tsx | 193 ++++++++++-------- .../use_edit_transform_flyout.test.ts | 54 ++--- .../use_edit_transform_flyout.ts | 142 ++++++++++--- .../translations/translations/ja-JP.json | 1 - .../translations/translations/zh-CN.json | 1 - .../test/functional/apps/transform/editing.ts | 83 +++++--- .../permissions/full_transform_access.ts | 9 +- .../services/transform/edit_flyout.ts | 27 ++- .../services/transform/transform_table.ts | 31 +++ 11 files changed, 365 insertions(+), 184 deletions(-) diff --git a/x-pack/plugins/transform/common/api_schemas/update_transforms.ts b/x-pack/plugins/transform/common/api_schemas/update_transforms.ts index 9bd4df5108049..7f59e7ef1f052 100644 --- a/x-pack/plugins/transform/common/api_schemas/update_transforms.ts +++ b/x-pack/plugins/transform/common/api_schemas/update_transforms.ts @@ -22,7 +22,9 @@ export const postTransformsUpdateRequestSchema = schema.object({ }) ), frequency: schema.maybe(schema.string()), - retention_policy: schema.maybe(retentionPolicySchema), + // maybe: If not set, any existing `retention_policy` config will not be updated. + // nullable: If set to `null`, any existing `retention_policy` will be removed. + retention_policy: schema.maybe(schema.nullable(retentionPolicySchema)), settings: schema.maybe(settingsSchema), source: schema.maybe(sourceSchema), sync: schema.maybe(syncSchema), 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 fd8360d02eca0..b988b61c5b0b7 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 @@ -35,7 +35,7 @@ import { useApi } from '../../../../hooks/use_api'; import { EditTransformFlyoutCallout } from './edit_transform_flyout_callout'; import { EditTransformFlyoutForm } from './edit_transform_flyout_form'; import { - applyFormFieldsToTransformConfig, + applyFormStateToTransformConfig, useEditTransformFlyout, } from './use_edit_transform_flyout'; import { ManagedTransformsWarningCallout } from '../managed_transforms_callout/managed_transforms_callout'; @@ -60,7 +60,7 @@ export const EditTransformFlyout: FC = ({ async function submitFormHandler() { setErrorMessage(undefined); - const requestConfig = applyFormFieldsToTransformConfig(config, state.formFields); + const requestConfig = applyFormStateToTransformConfig(config, state); const transformId = config.id; const resp = await api.updateTransform(transformId, requestConfig); 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/transform_management/components/edit_transform_flyout/edit_transform_flyout_form.tsx index 1bdaf2cc31763..22f31fc6139e8 100644 --- 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/transform_management/components/edit_transform_flyout/edit_transform_flyout_form.tsx @@ -7,7 +7,15 @@ import React, { FC, useEffect, useMemo, useState } from 'react'; -import { EuiComboBox, EuiForm, EuiAccordion, EuiSpacer, EuiSelect, EuiFormRow } from '@elastic/eui'; +import { + EuiAccordion, + EuiComboBox, + EuiForm, + EuiFormRow, + EuiSelect, + EuiSpacer, + EuiSwitch, +} from '@elastic/eui'; import { i18n } from '@kbn/i18n'; @@ -28,10 +36,12 @@ export const EditTransformFlyoutForm: FC = ({ editTransformFlyout: [state, dispatch], indexPatternId, }) => { - const formFields = state.formFields; + const { formFields, formSections } = state; const [dateFieldNames, setDateFieldNames] = useState([]); const [ingestPipelineNames, setIngestPipelineNames] = useState([]); + const isRetentionPolicyAvailable = dateFieldNames.length > 0; + const appDeps = useAppDependencies(); const indexPatternsClient = appDeps.data.indexPatterns; const api = useApi(); @@ -119,6 +129,100 @@ export const EditTransformFlyoutForm: FC = ({ + + dispatch({ + section: 'retentionPolicy', + enabled: e.target.checked, + }) + } + disabled={!isRetentionPolicyAvailable} + data-test-subj="transformEditRetentionPolicySwitch" + /> + {formSections.retentionPolicy.enabled && ( +
+ + { + // If data view or date fields info not available + // gracefully defaults to text input + indexPatternId ? ( + 0} + error={formFields.retentionPolicyField.errorMessages} + helpText={i18n.translate( + 'xpack.transform.transformList.editFlyoutFormRetentionPolicyDateFieldHelpText', + { + defaultMessage: + 'Select the date field that can be used to identify out of date documents in the destination index.', + } + )} + > + + dispatch({ field: 'retentionPolicyField', value: e.target.value }) + } + hasNoInitialSelection={ + !retentionDateFieldOptions + .map((d) => d.text) + .includes(formFields.retentionPolicyField.value) + } + /> + + ) : ( + dispatch({ field: 'retentionPolicyField', value })} + value={formFields.retentionPolicyField.value} + /> + ) + } + dispatch({ field: 'retentionPolicyMaxAge', value })} + value={formFields.retentionPolicyMaxAge.value} + /> +
+ )} + + + = ({ - -
- { - // If data view or date fields info not available - // gracefully defaults to text input - indexPatternId ? ( - 0} - error={formFields.retentionPolicyField.errorMessages} - helpText={i18n.translate( - 'xpack.transform.transformList.editFlyoutFormRetentionPolicyDateFieldHelpText', - { - defaultMessage: - 'Select the date field that can be used to identify out of date documents in the destination index.', - } - )} - > - - dispatch({ field: 'retentionPolicyField', value: e.target.value }) - } - hasNoInitialSelection={ - !retentionDateFieldOptions - .map((d) => d.text) - .includes(formFields.retentionPolicyField.value) - } - /> - - ) : ( - dispatch({ field: 'retentionPolicyField', value })} - value={formFields.retentionPolicyField.value} - /> - ) - } - dispatch({ field: 'retentionPolicyMaxAge', value })} - value={formFields.retentionPolicyMaxAge.value} - /> -
-
- - - ({ description: 'the-description', }); -describe('Transform: applyFormFieldsToTransformConfig()', () => { +describe('Transform: applyFormStateToTransformConfig()', () => { it('should exclude unchanged form fields', () => { const transformConfigMock = getTransformConfigMock(); const formState = getDefaultState(transformConfigMock); - const updateConfig = applyFormFieldsToTransformConfig( - transformConfigMock, - formState.formFields - ); + const updateConfig = applyFormStateToTransformConfig(transformConfigMock, formState); // 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. @@ -84,10 +81,7 @@ describe('Transform: applyFormFieldsToTransformConfig()', () => { }, }); - const updateConfig = applyFormFieldsToTransformConfig( - transformConfigMock, - formState.formFields - ); + const updateConfig = applyFormStateToTransformConfig(transformConfigMock, formState); expect(Object.keys(updateConfig)).toHaveLength(4); expect(updateConfig.description).toBe('the-new-description'); @@ -108,10 +102,7 @@ describe('Transform: applyFormFieldsToTransformConfig()', () => { }, }); - const updateConfig = applyFormFieldsToTransformConfig( - transformConfigMock, - formState.formFields - ); + const updateConfig = applyFormStateToTransformConfig(transformConfigMock, formState); expect(Object.keys(updateConfig)).toHaveLength(2); expect(updateConfig.description).toBe('the-updated-description'); @@ -132,10 +123,7 @@ describe('Transform: applyFormFieldsToTransformConfig()', () => { }, }); - const updateConfig = applyFormFieldsToTransformConfig( - transformConfigMock, - formState.formFields - ); + const updateConfig = applyFormStateToTransformConfig(transformConfigMock, formState); expect(Object.keys(updateConfig)).toHaveLength(1); // It should include the dependent unchanged destination index expect(updateConfig.dest?.index).toBe(transformConfigMock.dest.index); @@ -159,10 +147,7 @@ describe('Transform: applyFormFieldsToTransformConfig()', () => { }, }); - const updateConfig = applyFormFieldsToTransformConfig( - transformConfigMock, - formState.formFields - ); + const updateConfig = applyFormStateToTransformConfig(transformConfigMock, formState); expect(Object.keys(updateConfig)).toHaveLength(1); // It should include the dependent unchanged destination index expect(updateConfig.dest?.index).toBe(transformConfigMock.dest.index); @@ -177,15 +162,32 @@ describe('Transform: applyFormFieldsToTransformConfig()', () => { description: 'the-updated-description', }); - const updateConfig = applyFormFieldsToTransformConfig( - transformConfigMock, - formState.formFields - ); + const updateConfig = applyFormStateToTransformConfig(transformConfigMock, formState); expect(Object.keys(updateConfig)).toHaveLength(1); // It should exclude the dependent unchanged destination section expect(typeof updateConfig.dest).toBe('undefined'); expect(updateConfig.description).toBe('the-updated-description'); }); + + it('should return the config to reset retention policy', () => { + const transformConfigMock = getTransformConfigMock(); + + const formState = getDefaultState({ + ...transformConfigMock, + retention_policy: { + time: { field: 'the-time-field', max_age: '1d' }, + }, + }); + + formState.formSections.retentionPolicy.enabled = false; + + const updateConfig = applyFormStateToTransformConfig(transformConfigMock, formState); + + expect(Object.keys(updateConfig)).toHaveLength(1); + // It should exclude the dependent unchanged destination section + expect(typeof updateConfig.dest).toBe('undefined'); + expect(updateConfig.retention_policy).toBe(null); + }); }); describe('Transform: formReducerFactory()', () => { diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/use_edit_transform_flyout.ts b/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/use_edit_transform_flyout.ts index 09e9702604ef5..6c8c6ea78187c 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/use_edit_transform_flyout.ts +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/use_edit_transform_flyout.ts @@ -12,6 +12,7 @@ import { useReducer } from 'react'; import { i18n } from '@kbn/i18n'; +import { isPopulatedObject } from '../../../../../../common/shared_imports'; import { PostTransformsUpdateRequestSchema } from '../../../../../../common/api_schemas/update_transforms'; import { TransformConfigUnion } from '../../../../../../common/types/transform'; import { getNestedProperty, setNestedProperty } from '../../../../../../common/utils/object_utils'; @@ -40,22 +41,39 @@ type EditTransformFormFields = | 'maxPageSearchSize' | 'retentionPolicyField' | 'retentionPolicyMaxAge'; + type EditTransformFlyoutFieldsState = Record; -// The inner reducers apply validation based on supplied attributes of each field. export interface FormField { - formFieldName: string; + formFieldName: EditTransformFormFields; configFieldName: string; defaultValue: string; dependsOn: EditTransformFormFields[]; errorMessages: string[]; isNullable: boolean; isOptional: boolean; + section?: EditTransformFormSections; validator: keyof typeof validate; value: string; valueParser: (value: string) => any; } +// 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 EditTransformFormSections = 'retentionPolicy'; + +export interface FormSection { + formSectionName: EditTransformFormSections; + configFieldName: string; + defaultEnabled: boolean; + enabled: boolean; +} + +type EditTransformFlyoutSectionsState = Record; + // The reducers and 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. @@ -66,7 +84,7 @@ export interface FormField { // - formReducerFactory() / formFieldReducer() // These nested reducers take care of updating and validating the form state. // -// - applyFormFieldsToTransformConfig() (iterates over getUpdateValue()) +// - 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). @@ -221,18 +239,47 @@ export const initializeField = ( }; }; +export const initializeSection = ( + formSectionName: EditTransformFormSections, + 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, + }; +}; + export interface EditTransformFlyoutState { formFields: EditTransformFlyoutFieldsState; + formSections: EditTransformFlyoutSectionsState; isFormTouched: boolean; isFormValid: boolean; } -// This is not a redux type action, -// since for now we only have one action type. -interface Action { +// Actions for fields and sections +interface FormFieldAction { field: EditTransformFormFields; value: string; } +function isFormFieldAction(action: unknown): action is FormFieldAction { + return isPopulatedObject(action, ['field']); +} +interface FormSectionAction { + section: EditTransformFormSections; + enabled: boolean; +} +function isFormSectionAction(action: unknown): action is FormSectionAction { + return isPopulatedObject(action, ['section']); +} +type Action = FormFieldAction | FormSectionAction; // Takes a value from form state and applies it to the structure // of the expected final configuration request object. @@ -240,12 +287,18 @@ interface Action { const getUpdateValue = ( attribute: EditTransformFormFields, config: TransformConfigUnion, - formState: EditTransformFlyoutFieldsState, + formState: EditTransformFlyoutState, enforceFormValue = false ) => { - const formStateAttribute = formState[attribute]; + const { formFields, formSections } = formState; + 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 !== '' ? formStateAttribute.valueParser(formStateAttribute.value) @@ -268,7 +321,17 @@ const getUpdateValue = ( return formValue !== configValue ? dependsOnConfig : {}; } - return formValue !== configValue || enforceFormValue + // 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) : {}; }; @@ -276,13 +339,13 @@ const getUpdateValue = ( // Takes in the form configuration and returns a // request object suitable to be sent to the // transform update API endpoint. -export const applyFormFieldsToTransformConfig = ( +export const applyFormStateToTransformConfig = ( config: TransformConfigUnion, - formState: EditTransformFlyoutFieldsState + formState: EditTransformFlyoutState ): PostTransformsUpdateRequestSchema => // Iterates over all form fields and only if necessary applies them to // the request object used for updating the transform. - (Object.keys(formState) as EditTransformFormFields[]).reduce( + (Object.keys(formState.formFields) as EditTransformFormFields[]).reduce( (updateConfig, field) => merge({ ...updateConfig }, getUpdateValue(field, config, formState)), {} ); @@ -335,7 +398,12 @@ export const getDefaultState = (config: TransformConfigUnion): EditTransformFlyo 'retentionPolicyField', 'retention_policy.time.field', config, - { dependsOn: ['retentionPolicyMaxAge'], isNullable: false, isOptional: true } + { + dependsOn: ['retentionPolicyMaxAge'], + isNullable: false, + isOptional: true, + section: 'retentionPolicy', + } ), retentionPolicyMaxAge: initializeField( 'retentionPolicyMaxAge', @@ -345,10 +413,14 @@ export const getDefaultState = (config: TransformConfigUnion): EditTransformFlyo dependsOn: ['retentionPolicyField'], isNullable: false, isOptional: true, + section: 'retentionPolicy', validator: 'retentionPolicyMaxAge', } ), }, + formSections: { + retentionPolicy: initializeSection('retentionPolicy', 'retention_policy', config), + }, isFormTouched: false, isFormValid: true, }); @@ -375,27 +447,49 @@ const formFieldReducer = (state: FormField, value: string): FormField => { }; }; +const formSectionReducer = (state: FormSection, enabled: boolean): FormSection => { + return { + ...state, + 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 = Object.values(defaultState.formFields).map((f) => f.value); - - return (state: EditTransformFlyoutState, { field, value }: Action): EditTransformFlyoutState => { - const formFields = { - ...state.formFields, - [field]: formFieldReducer(state.formFields[field], value), - }; + const defaultFieldValues = getFieldValues(defaultState.formFields); + const defaultSectionValues = getSectionValues(defaultState.formSections); + + return (state: EditTransformFlyoutState, action: Action): EditTransformFlyoutState => { + const formFields = isFormFieldAction(action) + ? { + ...state.formFields, + [action.field]: formFieldReducer(state.formFields[action.field], action.value), + } + : state.formFields; + + const formSections = isFormSectionAction(action) + ? { + ...state.formSections, + [action.section]: formSectionReducer(state.formSections[action.section], action.enabled), + } + : state.formSections; return { ...state, formFields, - isFormTouched: !isEqual( - defaultFieldValues, - Object.values(formFields).map((f) => f.value) - ), + formSections, + isFormTouched: + !isEqual(defaultFieldValues, getFieldValues(formFields)) || + !isEqual(defaultSectionValues, getSectionValues(formSections)), isFormValid: isFormValid(formFields), }; }; diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 1dda671cb7f19..ddb61ecedc797 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -25992,7 +25992,6 @@ "xpack.transform.transformList.editFlyoutFormNumberRange10To10000NotValidErrorMessage": "値は10~10000の範囲の整数でなければなりません。", "xpack.transform.transformList.editFlyoutFormRequiredErrorMessage": "必須フィールド。", "xpack.transform.transformList.editFlyoutFormRetentionMaxAgeFieldLabel": "最大年齢", - "xpack.transform.transformList.editFlyoutFormRetentionPolicyButtonContent": "保持ポリシー", "xpack.transform.transformList.editFlyoutFormRetentionPolicyDateFieldHelpText": "デスティネーションインデックスで古いドキュメントを特定するために使用できる日付フィールドを選択します。", "xpack.transform.transformList.editFlyoutFormRetentionPolicyFieldLabel": "フィールド", "xpack.transform.transformList.editFlyoutFormRetentionPolicyFieldSelectAriaLabel": "保持ポリシーを設定する日付フィールド", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 6ed3624cf8e8d..200564953f518 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -26453,7 +26453,6 @@ "xpack.transform.transformList.editFlyoutFormNumberRange10To10000NotValidErrorMessage": "值必须是介于 10 到 10000 之间的整数。", "xpack.transform.transformList.editFlyoutFormRequiredErrorMessage": "必填字段。", "xpack.transform.transformList.editFlyoutFormRetentionMaxAgeFieldLabel": "最大存在时间", - "xpack.transform.transformList.editFlyoutFormRetentionPolicyButtonContent": "保留策略", "xpack.transform.transformList.editFlyoutFormRetentionPolicyDateFieldHelpText": "选择可用于从目标索引中识别出日期文档的日期字段。", "xpack.transform.transformList.editFlyoutFormRetentionPolicyFieldLabel": "字段", "xpack.transform.transformList.editFlyoutFormRetentionPolicyFieldSelectAriaLabel": "设置保留策略的日期字段", diff --git a/x-pack/test/functional/apps/transform/editing.ts b/x-pack/test/functional/apps/transform/editing.ts index 154c91ef6c149..36177fcaa3016 100644 --- a/x-pack/test/functional/apps/transform/editing.ts +++ b/x-pack/test/functional/apps/transform/editing.ts @@ -6,6 +6,10 @@ */ import { TRANSFORM_STATE } from '../../../../plugins/transform/common/constants'; +import type { + TransformLatestConfig, + TransformPivotConfig, +} from '../../../../plugins/transform/common/types/transform'; import { FtrProviderContext } from '../../ftr_provider_context'; import { getLatestTransformConfig, getPivotTransformConfig } from './index'; @@ -15,8 +19,11 @@ export default function ({ getService }: FtrProviderContext) { const transform = getService('transform'); describe('editing', function () { - const transformConfigWithPivot = getPivotTransformConfig('editing'); - const transformConfigWithLatest = getLatestTransformConfig('editing'); + const transformConfigWithPivot: TransformPivotConfig = getPivotTransformConfig('editing'); + const transformConfigWithLatest: TransformLatestConfig = { + ...getLatestTransformConfig('editing'), + retention_policy: { time: { field: 'order_date', max_age: '1d' } }, + }; before(async () => { await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/ml/ecommerce'); @@ -51,6 +58,7 @@ export default function ({ getService }: FtrProviderContext) { transformDescription: 'updated description', transformDocsPerSecond: '1000', transformFrequency: '10m', + resetRetentionPolicy: false, transformRetentionPolicyField: 'order_date', transformRetentionPolicyMaxAge: '1d', expected: { @@ -73,13 +81,12 @@ export default function ({ getService }: FtrProviderContext) { transformDescription: 'updated description', transformDocsPerSecond: '1000', transformFrequency: '10m', - transformRetentionPolicyField: 'order_date', - transformRetentionPolicyMaxAge: '1d', + resetRetentionPolicy: true, expected: { messageText: 'updated transform.', retentionPolicy: { - field: '', - maxAge: '', + field: 'order_date', + maxAge: '1d', }, row: { status: TRANSFORM_STATE.STOPPED, @@ -150,30 +157,40 @@ export default function ({ getService }: FtrProviderContext) { ); await transform.testExecution.logTestStep('should update the transform retention policy'); - await transform.editFlyout.openTransformEditAccordionRetentionPolicySettings(); - - await transform.editFlyout.assertTransformEditFlyoutRetentionPolicyFieldSelectEnabled( - true - ); - await transform.editFlyout.assertTransformEditFlyoutRetentionPolicyFieldSelectValue( - testData.expected.retentionPolicy.field - ); - await transform.editFlyout.setTransformEditFlyoutRetentionPolicyFieldSelectValue( - testData.transformRetentionPolicyField - ); - - await transform.editFlyout.assertTransformEditFlyoutInputEnabled( - 'RetentionPolicyMaxAge', - true - ); - await transform.editFlyout.assertTransformEditFlyoutInputValue( - 'RetentionPolicyMaxAge', - testData.expected.retentionPolicy.maxAge - ); - await transform.editFlyout.setTransformEditFlyoutInputValue( - 'RetentionPolicyMaxAge', - testData.transformRetentionPolicyMaxAge - ); + await transform.editFlyout.clickTransformEditRetentionPolicySettings( + !testData.resetRetentionPolicy + ); + + if ( + !testData.resetRetentionPolicy && + testData?.transformRetentionPolicyField && + testData?.transformRetentionPolicyMaxAge + ) { + await transform.editFlyout.assertTransformEditFlyoutRetentionPolicyFieldSelectEnabled( + true + ); + await transform.editFlyout.assertTransformEditFlyoutRetentionPolicyFieldSelectValue( + testData.expected.retentionPolicy.field + ); + + await transform.editFlyout.setTransformEditFlyoutRetentionPolicyFieldSelectValue( + testData.transformRetentionPolicyField + ); + + await transform.editFlyout.assertTransformEditFlyoutInputEnabled( + 'RetentionPolicyMaxAge', + true + ); + await transform.editFlyout.assertTransformEditFlyoutInputValue( + 'RetentionPolicyMaxAge', + testData.expected.retentionPolicy.maxAge + ); + + await transform.editFlyout.setTransformEditFlyoutInputValue( + 'RetentionPolicyMaxAge', + testData.transformRetentionPolicyMaxAge + ); + } }); it('updates the transform and displays it correctly in the job list', async () => { @@ -206,6 +223,12 @@ export default function ({ getService }: FtrProviderContext) { await transform.testExecution.logTestStep( 'should display the messages tab and include an update message' ); + + await transform.table.assertTransformExpandedRowJson( + 'retention_policy', + !testData.resetRetentionPolicy + ); + await transform.table.assertTransformExpandedRowJson('updated description'); await transform.table.assertTransformExpandedRowMessages(testData.expected.messageText); }); }); diff --git a/x-pack/test/functional/apps/transform/permissions/full_transform_access.ts b/x-pack/test/functional/apps/transform/permissions/full_transform_access.ts index 878307ec71996..fe29510e82497 100644 --- a/x-pack/test/functional/apps/transform/permissions/full_transform_access.ts +++ b/x-pack/test/functional/apps/transform/permissions/full_transform_access.ts @@ -154,14 +154,9 @@ export default function ({ getService }: FtrProviderContext) { await transform.editFlyout.assertTransformEditFlyoutIngestPipelineFieldSelectExists(); await transform.testExecution.logTestStep( - 'should have the retention policy inputs enabled' - ); - await transform.editFlyout.openTransformEditAccordionRetentionPolicySettings(); - await transform.editFlyout.assertTransformEditFlyoutRetentionPolicyFieldSelectEnabled(true); - await transform.editFlyout.assertTransformEditFlyoutInputEnabled( - 'RetentionPolicyMaxAge', - true + 'should have the retention policy switch enabled' ); + await transform.editFlyout.assertTransformEditFlyoutRetentionPolicySwitchEnabled(true); await transform.testExecution.logTestStep( 'should have the advanced settings inputs enabled' diff --git a/x-pack/test/functional/services/transform/edit_flyout.ts b/x-pack/test/functional/services/transform/edit_flyout.ts index f8cedb67aa37a..fb1d77f7abc6c 100644 --- a/x-pack/test/functional/services/transform/edit_flyout.ts +++ b/x-pack/test/functional/services/transform/edit_flyout.ts @@ -41,6 +41,19 @@ export function TransformEditFlyoutProvider({ getService }: FtrProviderContext) await testSubjects.existOrFail(`transformEditFlyoutDestinationIngestPipelineFieldSelect`); }, + async assertTransformEditFlyoutRetentionPolicySwitchEnabled(expectedValue: boolean) { + await testSubjects.existOrFail(`transformEditRetentionPolicySwitch`, { + timeout: 1000, + }); + const isEnabled = await testSubjects.isEnabled(`transformEditRetentionPolicySwitch`); + expect(isEnabled).to.eql( + expectedValue, + `Expected 'transformEditRetentionPolicySwitch' input to be '${ + expectedValue ? 'enabled' : 'disabled' + }' (got '${isEnabled ? 'enabled' : 'disabled'}')` + ); + }, + async assertTransformEditFlyoutRetentionPolicyFieldSelectEnabled(expectedValue: boolean) { await testSubjects.existOrFail(`transformEditFlyoutRetentionPolicyFieldSelect`, { timeout: 1000, @@ -95,16 +108,20 @@ export function TransformEditFlyoutProvider({ getService }: FtrProviderContext) await testSubjects.existOrFail('transformEditAccordionDestinationContent'); }, - async openTransformEditAccordionRetentionPolicySettings() { - await testSubjects.click('transformEditAccordionRetentionPolicy'); - await testSubjects.existOrFail('transformEditAccordionRetentionPolicyContent'); - }, - async openTransformEditAccordionAdvancedSettings() { await testSubjects.click('transformEditAccordionAdvancedSettings'); await testSubjects.existOrFail('transformEditAccordionAdvancedSettingsContent'); }, + async clickTransformEditRetentionPolicySettings(expectExists: boolean) { + await testSubjects.click('transformEditRetentionPolicySwitch'); + if (expectExists) { + await testSubjects.existOrFail('transformEditRetentionPolicyContent'); + } else { + await testSubjects.missingOrFail('transformEditRetentionPolicyContent'); + } + }, + async setTransformEditFlyoutInputValue(input: string, value: string) { await testSubjects.setValue(`transformEditFlyout${input}Input`, value, { clearWithKeyboard: true, diff --git a/x-pack/test/functional/services/transform/transform_table.ts b/x-pack/test/functional/services/transform/transform_table.ts index 67b2c37438405..bd413eb2893c2 100644 --- a/x-pack/test/functional/services/transform/transform_table.ts +++ b/x-pack/test/functional/services/transform/transform_table.ts @@ -234,6 +234,34 @@ export function TransformTableProvider({ getService }: FtrProviderContext) { await this.switchToExpandedRowTab('transformPreviewTab', '~transformPivotPreview'); } + public async assertTransformExpandedRowJson(expectedText: string, expectedToContain = true) { + await this.ensureDetailsOpen(); + + // The expanded row should show the details tab content by default + await testSubjects.existOrFail('transformDetailsTab'); + await testSubjects.existOrFail('~transformDetailsTabContent'); + + // Click on the JSON tab and assert the messages + await this.switchToExpandedRowTab('transformJsonTab', '~transformJsonTabContent'); + await retry.tryForTime(30 * 1000, async () => { + const actualText = await testSubjects.getVisibleText('~transformJsonTabContent'); + if (expectedToContain) { + expect(actualText.toLowerCase()).to.contain( + expectedText.toLowerCase(), + `Expected transform messages text to include '${expectedText}'` + ); + } else { + expect(actualText.toLowerCase()).to.not.contain( + expectedText.toLowerCase(), + `Expected transform messages text to not include '${expectedText}'` + ); + } + }); + + // Switch back to details tab + await this.switchToExpandedRowTab('transformDetailsTab', '~transformDetailsTabContent'); + } + public async assertTransformExpandedRowMessages(expectedText: string) { await this.ensureDetailsOpen(); @@ -250,6 +278,9 @@ export function TransformTableProvider({ getService }: FtrProviderContext) { `Expected transform messages text to include '${expectedText}'` ); }); + + // Switch back to details tab + await this.switchToExpandedRowTab('transformDetailsTab', '~transformDetailsTabContent'); } public rowSelector(transformId: string, subSelector?: string) {