From 785807f676d99f1a43bbbc8987427dc41162c092 Mon Sep 17 00:00:00 2001 From: Robert Oskamp Date: Wed, 4 Dec 2019 11:24:06 +0100 Subject: [PATCH] [7.5] [ML] Add tests to create transforms (#49760) (#52080) * [ML] Add tests to create transforms (#49760) * Apply #51066 to backport * Adjust eui switch handling for 7.5 --- .../kql_filter_bar/filter_bar/filter_bar.js | 7 +- .../kql_filter_bar/kql_filter_bar.js | 6 +- .../components/ml_in_memory_table/types.ts | 4 +- .../aggregation_dropdown/dropdown.tsx | 3 + .../agg_label_form.test.tsx.snap | 3 + .../__snapshots__/list_form.test.tsx.snap | 1 + .../aggregation_list/agg_label_form.tsx | 6 +- .../components/aggregation_list/list_form.tsx | 4 +- .../group_by_label_form.test.tsx.snap | 11 + .../__snapshots__/list_form.test.tsx.snap | 1 + .../group_by_list/group_by_label_form.tsx | 12 +- .../components/group_by_list/list_form.tsx | 4 +- .../source_index_preview.tsx | 6 +- .../step_create/step_create_form.tsx | 296 +++++---- .../components/step_define/pivot_preview.tsx | 6 +- .../step_define/step_define_form.tsx | 577 +++++++++--------- .../step_define/step_define_summary.tsx | 127 ++-- .../step_details/step_details_form.tsx | 379 ++++++------ .../step_details/step_details_summary.tsx | 6 +- .../components/wizard_nav/wizard_nav.tsx | 16 +- .../transform_list.test.tsx.snap | 1 + .../components/transform_list/columns.tsx | 16 +- .../transform_list/transform_list.tsx | 19 +- .../transform_management_section.tsx | 6 +- .../functional/apps/transform/creation.ts | 229 +++++++ .../test/functional/apps/transform/index.ts | 14 + x-pack/test/functional/config.js | 5 + x-pack/test/functional/services/index.ts | 2 + x-pack/test/functional/services/transform.ts | 34 ++ .../functional/services/transform_ui/api.ts | 43 ++ .../functional/services/transform_ui/index.ts | 12 + .../services/transform_ui/management.ts | 42 ++ .../services/transform_ui/navigation.ts | 17 + .../services/transform_ui/source_selection.ts | 30 + .../services/transform_ui/transform_table.ts | 87 +++ .../services/transform_ui/wizard.ts | 352 +++++++++++ 36 files changed, 1697 insertions(+), 687 deletions(-) create mode 100644 x-pack/test/functional/apps/transform/creation.ts create mode 100644 x-pack/test/functional/apps/transform/index.ts create mode 100644 x-pack/test/functional/services/transform.ts create mode 100644 x-pack/test/functional/services/transform_ui/api.ts create mode 100644 x-pack/test/functional/services/transform_ui/index.ts create mode 100644 x-pack/test/functional/services/transform_ui/management.ts create mode 100644 x-pack/test/functional/services/transform_ui/navigation.ts create mode 100644 x-pack/test/functional/services/transform_ui/source_selection.ts create mode 100644 x-pack/test/functional/services/transform_ui/transform_table.ts create mode 100644 x-pack/test/functional/services/transform_ui/wizard.ts diff --git a/x-pack/legacy/plugins/ml/public/components/kql_filter_bar/filter_bar/filter_bar.js b/x-pack/legacy/plugins/ml/public/components/kql_filter_bar/filter_bar/filter_bar.js index 19b170485817f..27c5e9d87b21d 100644 --- a/x-pack/legacy/plugins/ml/public/components/kql_filter_bar/filter_bar/filter_bar.js +++ b/x-pack/legacy/plugins/ml/public/components/kql_filter_bar/filter_bar/filter_bar.js @@ -178,6 +178,7 @@ export class FilterBar extends Component { onClick={this.onClickInput} autoComplete="off" spellCheck={false} + data-test-subj={this.props.testSubj} /> {this.props.isLoading && ( @@ -213,12 +214,14 @@ FilterBar.propTypes = { placeholder: PropTypes.string, onSubmit: PropTypes.func.isRequired, valueExternal: PropTypes.string, - suggestions: PropTypes.array.isRequired + suggestions: PropTypes.array.isRequired, + testSubj: PropTypes.string, }; FilterBar.defaultProps = { isLoading: false, disabled: false, placeholder: 'tag : engineering OR tag : marketing', - suggestions: [] + suggestions: [], + testSubj: undefined, }; diff --git a/x-pack/legacy/plugins/ml/public/components/kql_filter_bar/kql_filter_bar.js b/x-pack/legacy/plugins/ml/public/components/kql_filter_bar/kql_filter_bar.js index 85d301d91f8e2..ef5bacdc1777c 100644 --- a/x-pack/legacy/plugins/ml/public/components/kql_filter_bar/kql_filter_bar.js +++ b/x-pack/legacy/plugins/ml/public/components/kql_filter_bar/kql_filter_bar.js @@ -90,7 +90,7 @@ export class KqlFilterBar extends Component { render() { const { error } = this.state; - const { initialValue, placeholder, valueExternal } = this.props; + const { initialValue, placeholder, valueExternal, testSubj } = this.props; return ( @@ -103,6 +103,7 @@ export class KqlFilterBar extends Component { onSubmit={this.onSubmit} suggestions={this.state.suggestions} valueExternal={valueExternal} + testSubj={testSubj} /> { error && @@ -118,6 +119,7 @@ KqlFilterBar.propTypes = { initialValue: PropTypes.string, onSubmit: PropTypes.func.isRequired, placeholder: PropTypes.string, - valueExternal: PropTypes.string + valueExternal: PropTypes.string, + testSubj: PropTypes.string, }; diff --git a/x-pack/legacy/plugins/ml/public/components/ml_in_memory_table/types.ts b/x-pack/legacy/plugins/ml/public/components/ml_in_memory_table/types.ts index 22c35fc453b88..fac0309e0aeb6 100644 --- a/x-pack/legacy/plugins/ml/public/components/ml_in_memory_table/types.ts +++ b/x-pack/legacy/plugins/ml/public/components/ml_in_memory_table/types.ts @@ -31,6 +31,7 @@ export interface FieldDataColumnType { render?: RenderFunc; footer?: string | ReactElement | FooterFunc; textOnly?: boolean; + 'data-test-subj'?: string; } export interface ComputedColumnType { @@ -40,6 +41,7 @@ export interface ComputedColumnType { sortable?: (item: Item) => any; width?: string; truncateText?: boolean; + 'data-test-subj'?: string; } type ICON_TYPES = any; @@ -183,7 +185,7 @@ export type EuiInMemoryTableProps = CommonProps & { selection?: SelectionType; itemId?: ItemIdType; itemIdToExpandedRowMap?: Record; - rowProps?: () => void | Record; + rowProps?: (item: Item) => void | Record; cellProps?: () => void | Record; onTableChange?: (arg: OnTableChangeArg) => void; }; diff --git a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/aggregation_dropdown/dropdown.tsx b/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/aggregation_dropdown/dropdown.tsx index 964909205c507..7617237e72a7d 100644 --- a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/aggregation_dropdown/dropdown.tsx +++ b/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/aggregation_dropdown/dropdown.tsx @@ -12,12 +12,14 @@ interface Props { options: EuiComboBoxOptionProps[]; placeholder?: string; changeHandler(d: EuiComboBoxOptionProps[]): void; + testSubj?: string; } export const DropDown: React.SFC = ({ changeHandler, options, placeholder = 'Search ...', + testSubj, }) => { return ( = ({ selectedOptions={[]} onChange={changeHandler} isClearable={false} + data-test-subj={testSubj} /> ); }; diff --git a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/aggregation_list/__snapshots__/agg_label_form.test.tsx.snap b/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/aggregation_list/__snapshots__/agg_label_form.test.tsx.snap index 835ed9dc9f849..ed32fb3d6ad5f 100644 --- a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/aggregation_list/__snapshots__/agg_label_form.test.tsx.snap +++ b/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/aggregation_list/__snapshots__/agg_label_form.test.tsx.snap @@ -11,6 +11,7 @@ exports[`Transform: Date histogram aggregation 1`] = ` > the-group-by-agg-name @@ -24,6 +25,7 @@ exports[`Transform: Date histogram aggregation 1`] = ` button={ Date histogram aggregation 1`] = ` > Minimal initialization 1`] = ` = ({ return ( - {item.aggName} + + {item.aggName} + = ({ size="s" iconType="pencil" onClick={() => setPopoverVisibility(!isPopoverVisible)} + data-test-subj="transformAggregationEntryEditButton" /> } isOpen={isPopoverVisible} @@ -74,6 +77,7 @@ export const AggLabelForm: React.SFC = ({ size="s" iconType="cross" onClick={() => deleteHandler(item.aggName)} + data-test-subj="transformAggregationEntryDeleteButton" /> diff --git a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/aggregation_list/list_form.tsx b/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/aggregation_list/list_form.tsx index 0d93b75bc50e5..b9437fee89efd 100644 --- a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/aggregation_list/list_form.tsx +++ b/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/aggregation_list/list_form.tsx @@ -33,11 +33,11 @@ export const AggListForm: React.SFC = ({ const listKeys = Object.keys(list); return ( - {listKeys.map((aggName: AggName) => { + {listKeys.map((aggName: AggName, i) => { const otherAggNames = listKeys.filter(k => k !== aggName); return ( - + Date histogram aggregation 1`] = ` > the-group-by-agg-name @@ -22,6 +23,7 @@ exports[`Transform: Date histogram aggregation 1`] = ` 1m @@ -35,6 +37,7 @@ exports[`Transform: Date histogram aggregation 1`] = ` button={ Date histogram aggregation 1`] = ` > Histogram aggregation 1`] = ` > the-group-by-agg-name @@ -100,6 +105,7 @@ exports[`Transform: Histogram aggregation 1`] = ` 100 @@ -113,6 +119,7 @@ exports[`Transform: Histogram aggregation 1`] = ` button={ Histogram aggregation 1`] = ` > Terms aggregation 1`] = ` > the-group-by-agg-name @@ -180,6 +189,7 @@ exports[`Transform: Terms aggregation 1`] = ` button={ Terms aggregation 1`] = ` > Minimal initialization 1`] = ` = ({ return ( - {item.aggName} + + {item.aggName} + {interval !== undefined && ( - + {interval} @@ -77,6 +83,7 @@ export const GroupByLabelForm: React.SFC = ({ size="s" iconType="pencil" onClick={() => setPopoverVisibility(!isPopoverVisible)} + data-test-subj="transformGroupByEntryEditButton" /> } isOpen={isPopoverVisible} @@ -98,6 +105,7 @@ export const GroupByLabelForm: React.SFC = ({ size="s" iconType="cross" onClick={() => deleteHandler(item.aggName)} + data-test-subj="transformGroupByEntryDeleteButton" /> diff --git a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/group_by_list/list_form.tsx b/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/group_by_list/list_form.tsx index bee708f6f25ea..484314df5067d 100644 --- a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/group_by_list/list_form.tsx +++ b/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/group_by_list/list_form.tsx @@ -33,11 +33,11 @@ export const GroupByListForm: React.SFC = ({ const listKeys = Object.keys(list); return ( - {listKeys.map((aggName: AggName) => { + {listKeys.map((aggName: AggName, i) => { const otherAggNames = listKeys.filter(k => k !== aggName); return ( - + = React.memo(({ cellClick, que if (status === SOURCE_INDEX_STATUS.ERROR) { return ( - + = React.memo(({ cellClick, que if (status === SOURCE_INDEX_STATUS.LOADED && tableItems.length === 0) { return ( - + = React.memo(({ cellClick, que }); return ( - + diff --git a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_create/step_create_form.tsx b/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_create/step_create_form.tsx index 9623ff6abeced..e2ffa1d452447 100644 --- a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_create/step_create_form.tsx +++ b/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_create/step_create_form.tsx @@ -257,169 +257,197 @@ export const StepCreateForm: SFC = React.memo( } return ( - - {!created && ( +
+ + {!created && ( + + + + {i18n.translate('xpack.transform.stepCreateForm.createAndStartTransformButton', { + defaultMessage: 'Create and start', + })} + + + + + {i18n.translate( + 'xpack.transform.stepCreateForm.createAndStartTransformDescription', + { + defaultMessage: + 'Creates and starts the transform. A transform will increase search and indexing load in your cluster. Please stop the transform if excessive load is experienced. After the transform is started, you will be offered options to continue exploring the transform.', + } + )} + + + + )} + {created && ( + + + + {i18n.translate('xpack.transform.stepCreateForm.startTransformButton', { + defaultMessage: 'Start', + })} + + + + + {i18n.translate('xpack.transform.stepCreateForm.startTransformDescription', { + defaultMessage: + 'Starts the transform. A transform will increase search and indexing load in your cluster. Please stop the transform if excessive load is experienced. After the transform is started, you will be offered options to continue exploring the transform.', + })} + + + + )} - - {i18n.translate('xpack.transform.stepCreateForm.createAndStartTransformButton', { - defaultMessage: 'Create and start', + + {i18n.translate('xpack.transform.stepCreateForm.createTransformButton', { + defaultMessage: 'Create', })} - {i18n.translate( - 'xpack.transform.stepCreateForm.createAndStartTransformDescription', - { - defaultMessage: - 'Creates and starts the transform. A transform will increase search and indexing load in your cluster. Please stop the transform if excessive load is experienced. After the transform is started, you will be offered options to continue exploring the transform.', - } - )} + {i18n.translate('xpack.transform.stepCreateForm.createTransformDescription', { + defaultMessage: + 'Create the transform without starting it. You will be able to start the transform later by returning to the transforms list.', + })} - )} - {created && ( - - {i18n.translate('xpack.transform.stepCreateForm.startTransformButton', { - defaultMessage: 'Start', - })} - + + {(copy: () => void) => ( + + {i18n.translate( + 'xpack.transform.stepCreateForm.copyTransformConfigToClipboardButton', + { + defaultMessage: 'Copy to clipboard', + } + )} + + )} + - {i18n.translate('xpack.transform.stepCreateForm.startTransformDescription', { - defaultMessage: - 'Starts the transform. A transform will increase search and indexing load in your cluster. Please stop the transform if excessive load is experienced. After the transform is started, you will be offered options to continue exploring the transform.', - })} + {i18n.translate( + 'xpack.transform.stepCreateForm.copyTransformConfigToClipboardDescription', + { + defaultMessage: + 'Copies to the clipboard the Kibana Dev Console command for creating the transform.', + } + )} - )} - - - - {i18n.translate('xpack.transform.stepCreateForm.createTransformButton', { - defaultMessage: 'Create', - })} - - - - - {i18n.translate('xpack.transform.stepCreateForm.createTransformDescription', { - defaultMessage: - 'Create the transform without starting it. You will be able to start the transform later by returning to the transforms list.', - })} - - - - - - - {(copy: () => void) => ( - - {i18n.translate( - 'xpack.transform.stepCreateForm.copyTransformConfigToClipboardButton', - { - defaultMessage: 'Copy to clipboard', - } - )} - - )} - - - - - {i18n.translate( - 'xpack.transform.stepCreateForm.copyTransformConfigToClipboardDescription', - { - defaultMessage: - 'Copies to the clipboard the Kibana Dev Console command for creating the transform.', - } - )} - - - - {progressPercentComplete !== undefined && isBatchTransform && ( - - - - - {i18n.translate('xpack.transform.stepCreateForm.progressTitle', { - defaultMessage: 'Progress', - })} - - - - - - - - {progressPercentComplete}% - - - - )} - {created && ( - - - - - } - title={i18n.translate('xpack.transform.stepCreateForm.transformListCardTitle', { - defaultMessage: 'Transforms', + {progressPercentComplete !== undefined && isBatchTransform && ( + + + + + {i18n.translate('xpack.transform.stepCreateForm.progressTitle', { + defaultMessage: 'Progress', })} - description={i18n.translate( - 'xpack.transform.stepCreateForm.transformListCardDescription', - { - defaultMessage: 'Return to the transform management page.', - } - )} - onClick={() => setRedirectToTransformManagement(true)} - /> - - {started === true && createIndexPattern === true && indexPatternId === undefined && ( - - - - -

- {i18n.translate( - 'xpack.transform.stepCreateForm.creatingIndexPatternMessage', - { - defaultMessage: 'Creating Kibana index pattern ...', - } - )} -

-
-
+
+ + + + - )} - {started === true && indexPatternId !== undefined && ( + + {progressPercentComplete}% + + +
+ )} + {created && ( + + + } - title={i18n.translate('xpack.transform.stepCreateForm.discoverCardTitle', { - defaultMessage: 'Discover', + icon={} + title={i18n.translate('xpack.transform.stepCreateForm.transformListCardTitle', { + defaultMessage: 'Transforms', })} description={i18n.translate( - 'xpack.transform.stepCreateForm.discoverCardDescription', + 'xpack.transform.stepCreateForm.transformListCardDescription', { - defaultMessage: 'Use Discover to explore the transform.', + defaultMessage: 'Return to the transform management page.', } )} - href={getDiscoverUrl(indexPatternId, kibanaContext.kbnBaseUrl)} + onClick={() => setRedirectToTransformManagement(true)} + data-test-subj="transformWizardCardManagement" /> - )} - - - )} -
+ {started === true && createIndexPattern === true && indexPatternId === undefined && ( + + + + +

+ {i18n.translate( + 'xpack.transform.stepCreateForm.creatingIndexPatternMessage', + { + defaultMessage: 'Creating Kibana index pattern ...', + } + )} +

+
+
+
+ )} + {started === true && indexPatternId !== undefined && ( + + } + title={i18n.translate('xpack.transform.stepCreateForm.discoverCardTitle', { + defaultMessage: 'Discover', + })} + description={i18n.translate( + 'xpack.transform.stepCreateForm.discoverCardDescription', + { + defaultMessage: 'Use Discover to explore the transform.', + } + )} + href={getDiscoverUrl(indexPatternId, kibanaContext.kbnBaseUrl)} + data-test-subj="transformWizardCardDiscover" + /> + + )} + + + )} + +
); } ); diff --git a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_define/pivot_preview.tsx b/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_define/pivot_preview.tsx index bbfc6b11d3619..53d1c2385c7e5 100644 --- a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_define/pivot_preview.tsx +++ b/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_define/pivot_preview.tsx @@ -158,7 +158,7 @@ export const PivotPreview: SFC = React.memo(({ aggs, groupBy, if (status === PIVOT_PREVIEW_STATUS.ERROR) { return ( - + = React.memo(({ aggs, groupBy, ); } return ( - + = React.memo(({ aggs, groupBy, }; return ( - + {status === PIVOT_PREVIEW_STATUS.LOADING && } {status !== PIVOT_PREVIEW_STATUS.LOADING && ( diff --git a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_form.tsx b/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_form.tsx index 6cde57fc2316d..f69bfa76a8de5 100644 --- a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_form.tsx +++ b/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_form.tsx @@ -505,145 +505,318 @@ export const StepDefineForm: SFC = React.memo(({ overrides = {}, onChange return ( - - {kibanaContext.currentSavedSearch === undefined && typeof searchString === 'string' && ( - - - {indexPattern.title} - - {!disabledQuery && ( - - {!isAdvancedSourceEditorEnabled && ( - - + + {kibanaContext.currentSavedSearch === undefined && typeof searchString === 'string' && ( + + + {indexPattern.title} + + {!disabledQuery && ( + + {!isAdvancedSourceEditorEnabled && ( + + + + )} + + )} + + )} + + {isAdvancedSourceEditorEnabled && ( + + + + { + setAdvancedEditorSourceConfig(d); + + // Disable the "Apply"-Button if the config hasn't changed. + if (advancedEditorSourceConfigLastApplied === d) { + setAdvancedSourceEditorApplyButtonEnabled(false); + return; + } + + // Try to parse the string passed on from the editor. + // If parsing fails, the "Apply"-Button will be disabled + try { + JSON.parse(d); + setAdvancedSourceEditorApplyButtonEnabled(true); + } catch (e) { + setAdvancedSourceEditorApplyButtonEnabled(false); + } + }} + setOptions={{ + fontSize: '12px', + }} + theme="textmate" + aria-label={i18n.translate( + 'xpack.transform.stepDefineForm.advancedSourceEditorAriaLabel', + { + defaultMessage: 'Advanced query editor', + } + )} + /> + + + + )} + {kibanaContext.currentSavedSearch === undefined && ( + + + + { + if (isAdvancedSourceEditorEnabled && sourceConfigUpdated) { + setAdvancedSourceEditorSwitchModalVisible(true); + return; + } + + toggleAdvancedSourceEditor(); + }} + data-test-subj="transformAdvancedQueryEditorSwitch" + /> + {isAdvancedSourceEditorSwitchModalVisible && ( + setAdvancedSourceEditorSwitchModalVisible(false)} + onConfirm={() => { + setAdvancedSourceEditorSwitchModalVisible(false); + toggleAdvancedSourceEditor(true); + }} + type={'source'} /> - + )} + + {isAdvancedSourceEditorEnabled && ( + + {i18n.translate( + 'xpack.transform.stepDefineForm.advancedSourceEditorApplyButtonText', + { + defaultMessage: 'Apply changes', + } + )} + )} -
+ + + )} + {kibanaContext.currentSavedSearch !== undefined && + kibanaContext.currentSavedSearch.id !== undefined && ( + + {kibanaContext.currentSavedSearch.title} + )} -
- )} - - {isAdvancedSourceEditorEnabled && ( - - - - { - setAdvancedEditorSourceConfig(d); - - // Disable the "Apply"-Button if the config hasn't changed. - if (advancedEditorSourceConfigLastApplied === d) { - setAdvancedSourceEditorApplyButtonEnabled(false); - return; - } - // Try to parse the string passed on from the editor. - // If parsing fails, the "Apply"-Button will be disabled - try { - JSON.parse(d); - setAdvancedSourceEditorApplyButtonEnabled(true); - } catch (e) { - setAdvancedSourceEditorApplyButtonEnabled(false); - } - }} - setOptions={{ - fontSize: '12px', - }} - theme="textmate" - aria-label={i18n.translate( - 'xpack.transform.stepDefineForm.advancedSourceEditorAriaLabel', - { - defaultMessage: 'Advanced query editor', - } - )} - /> - - - - )} - {kibanaContext.currentSavedSearch === undefined && ( + {!isAdvancedPivotEditorEnabled && ( + + + + + + + + + + + + + + + + )} + + {isAdvancedPivotEditorEnabled && ( + + + + { + setAdvancedEditorConfig(d); + + // Disable the "Apply"-Button if the config hasn't changed. + if (advancedEditorConfigLastApplied === d) { + setAdvancedPivotEditorApplyButtonEnabled(false); + return; + } + + // Try to parse the string passed on from the editor. + // If parsing fails, the "Apply"-Button will be disabled + try { + JSON.parse(d); + setAdvancedPivotEditorApplyButtonEnabled(true); + } catch (e) { + setAdvancedPivotEditorApplyButtonEnabled(false); + } + }} + setOptions={{ + fontSize: '12px', + }} + theme="textmate" + aria-label={i18n.translate( + 'xpack.transform.stepDefineForm.advancedEditorAriaLabel', + { + defaultMessage: 'Advanced pivot editor', + } + )} + /> + + + + )} { - if (isAdvancedSourceEditorEnabled && sourceConfigUpdated) { - setAdvancedSourceEditorSwitchModalVisible(true); + if ( + isAdvancedPivotEditorEnabled && + (isAdvancedPivotEditorApplyButtonEnabled || + advancedEditorConfig !== advancedEditorConfigLastApplied) + ) { + setAdvancedEditorSwitchModalVisible(true); return; } - toggleAdvancedSourceEditor(); + toggleAdvancedEditor(); }} + data-test-subj="transformAdvancedPivotEditorSwitch" /> - {isAdvancedSourceEditorSwitchModalVisible && ( + {isAdvancedEditorSwitchModalVisible && ( setAdvancedSourceEditorSwitchModalVisible(false)} + onCancel={() => setAdvancedEditorSwitchModalVisible(false)} onConfirm={() => { - setAdvancedSourceEditorSwitchModalVisible(false); - toggleAdvancedSourceEditor(true); + setAdvancedEditorSwitchModalVisible(false); + toggleAdvancedEditor(); }} - type={'source'} + type={'pivot'} /> )} - {isAdvancedSourceEditorEnabled && ( + {isAdvancedPivotEditorEnabled && ( {i18n.translate( - 'xpack.transform.stepDefineForm.advancedSourceEditorApplyButtonText', + 'xpack.transform.stepDefineForm.advancedEditorApplyButtonText', { defaultMessage: 'Apply changes', } @@ -652,179 +825,19 @@ export const StepDefineForm: SFC = React.memo(({ overrides = {}, onChange )} - )} - {kibanaContext.currentSavedSearch !== undefined && - kibanaContext.currentSavedSearch.id !== undefined && ( - - {kibanaContext.currentSavedSearch.title} - - )} - - {!isAdvancedPivotEditorEnabled && ( - - - - - - - - - - - - - - - - )} - - {isAdvancedPivotEditorEnabled && ( - - - - { - setAdvancedEditorConfig(d); - - // Disable the "Apply"-Button if the config hasn't changed. - if (advancedEditorConfigLastApplied === d) { - setAdvancedPivotEditorApplyButtonEnabled(false); - return; - } - - // Try to parse the string passed on from the editor. - // If parsing fails, the "Apply"-Button will be disabled - try { - JSON.parse(d); - setAdvancedPivotEditorApplyButtonEnabled(true); - } catch (e) { - setAdvancedPivotEditorApplyButtonEnabled(false); - } - }} - setOptions={{ - fontSize: '12px', - }} - theme="textmate" - aria-label={i18n.translate( - 'xpack.transform.stepDefineForm.advancedEditorAriaLabel', - { - defaultMessage: 'Advanced pivot editor', - } - )} - /> - - - - )} - - - - { - if ( - isAdvancedPivotEditorEnabled && - (isAdvancedPivotEditorApplyButtonEnabled || - advancedEditorConfig !== advancedEditorConfigLastApplied) - ) { - setAdvancedEditorSwitchModalVisible(true); - return; - } - - toggleAdvancedEditor(); - }} - /> - {isAdvancedEditorSwitchModalVisible && ( - setAdvancedEditorSwitchModalVisible(false)} - onConfirm={() => { - setAdvancedEditorSwitchModalVisible(false); - toggleAdvancedEditor(); - }} - type={'pivot'} - /> - )} - - {isAdvancedPivotEditorEnabled && ( - - {i18n.translate('xpack.transform.stepDefineForm.advancedEditorApplyButtonText', { - defaultMessage: 'Apply changes', + {!valid && ( + + + + {i18n.translate('xpack.transform.stepDefineForm.formHelp', { + defaultMessage: + 'Transforms are scalable and automated processes for pivoting. Choose at least one group-by and aggregation to get started.', })} - - )} - - - {!valid && ( - - - - {i18n.translate('xpack.transform.stepDefineForm.formHelp', { - defaultMessage: - 'Transforms are scalable and automated processes for pivoting. Choose at least one group-by and aggregation to get started.', - })} - - - )} - + +
+ )} + + diff --git a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_summary.tsx b/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_summary.tsx index 5f1f265b5f5bb..bb900766483df 100644 --- a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_summary.tsx +++ b/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_summary.tsx @@ -57,75 +57,80 @@ export const StepDefineSummary: FC = ({ return ( - - {kibanaContext.currentSavedSearch !== undefined && - kibanaContext.currentSavedSearch.id === undefined && - typeof searchString === 'string' && ( - - - {kibanaContext.currentIndexPattern.title} - - {useCodeBlock === false && displaySearch !== emptySearch && ( +
+ + {kibanaContext.currentSavedSearch !== undefined && + kibanaContext.currentSavedSearch.id === undefined && + typeof searchString === 'string' && ( + - {displaySearch} + {kibanaContext.currentIndexPattern.title} - )} - {useCodeBlock === true && displaySearch !== emptySearch && ( - - - {displaySearch} - - - )} - - )} + {displaySearch} + + )} + {useCodeBlock === true && displaySearch !== emptySearch && ( + + + {displaySearch} + + + )} + + )} - {kibanaContext.currentSavedSearch !== undefined && - kibanaContext.currentSavedSearch.id !== undefined && ( - - {kibanaContext.currentSavedSearch.title} - - )} + {kibanaContext.currentSavedSearch !== undefined && + kibanaContext.currentSavedSearch.id !== undefined && ( + + {kibanaContext.currentSavedSearch.title} + + )} - - - + + + - - - - + + + + +
diff --git a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_details/step_details_form.tsx b/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_details/step_details_form.tsx index ba43e020674ac..014e4e7593cca 100644 --- a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_details/step_details_form.tsx +++ b/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_details/step_details_form.tsx @@ -181,200 +181,217 @@ export const StepDetailsForm: SFC = React.memo(({ overrides = {}, onChang ]); return ( - - - setTransformId(e.target.value)} - aria-label={i18n.translate('xpack.transform.stepDetailsForm.transformIdInputAriaLabel', { - defaultMessage: 'Choose a unique transform ID.', +
+ + - - - setTransformDescription(e.target.value)} - aria-label={i18n.translate( - 'xpack.transform.stepDetailsForm.transformDescriptionInputAriaLabel', - { - defaultMessage: 'Choose an optional transform description.', - } - )} - /> - - - {i18n.translate('xpack.transform.stepDetailsForm.destinationIndexInvalidError', { - defaultMessage: 'Invalid destination index name.', - })} -
- - {i18n.translate( - 'xpack.transform.stepDetailsForm.destinationIndexInvalidErrorLink', - { - defaultMessage: 'Learn more about index name limitations.', - } - )} - - , - ] - } - > - setDestinationIndex(e.target.value)} - aria-label={i18n.translate( - 'xpack.transform.stepDetailsForm.destinationIndexInputAriaLabel', - { - defaultMessage: 'Choose a unique destination index name.', - } - )} - isInvalid={!indexNameEmpty && !indexNameValid} - /> -
- - + setTransformId(e.target.value)} + aria-label={i18n.translate( + 'xpack.transform.stepDetailsForm.transformIdInputAriaLabel', + { + defaultMessage: 'Choose a unique transform ID.', + } + )} + isInvalid={(!transformIdEmpty && !transformIdValid) || transformIdExists} + data-test-subj="transformIdInput" + /> + + setCreateIndexPattern(!createIndexPattern)} - /> - - - setContinuousModeEnabled(!isContinuousModeEnabled)} - disabled={isContinuousModeAvailable === false} - /> - - {isContinuousModeEnabled && ( - - + setTransformDescription(e.target.value)} + aria-label={i18n.translate( + 'xpack.transform.stepDetailsForm.transformDescriptionInputAriaLabel', { - defaultMessage: 'Select the date field that can be used to identify new documents.', + defaultMessage: 'Choose an optional transform description.', } )} - > - ({ text }))} - value={continuousModeDateField} - onChange={e => setContinuousModeDateField(e.target.value)} - /> - - + + + {i18n.translate('xpack.transform.stepDetailsForm.destinationIndexInvalidError', { + defaultMessage: 'Invalid destination index name.', + })} +
+ + {i18n.translate( + 'xpack.transform.stepDetailsForm.destinationIndexInvalidErrorLink', + { + defaultMessage: 'Learn more about index name limitations.', + } + )} + +
, + ] + } + > + setDestinationIndex(e.target.value)} + aria-label={i18n.translate( + 'xpack.transform.stepDetailsForm.destinationIndexInputAriaLabel', { - defaultMessage: 'Time delay between current time and latest input data time.', + defaultMessage: 'Choose a unique destination index name.', } )} - > - setContinuousModeDelay(e.target.value)} - aria-label={i18n.translate( - 'xpack.transform.stepDetailsForm.continuousModeAriaLabel', + isInvalid={!indexNameEmpty && !indexNameValid} + data-test-subj="transformDestinationIndexInput" + /> + + + setCreateIndexPattern(!createIndexPattern)} + data-test-subj="transformCreateIndexPatternSwitch" + /> + + + setContinuousModeEnabled(!isContinuousModeEnabled)} + disabled={isContinuousModeAvailable === false} + data-test-subj="transformContinuousModeSwitch" + /> + + {isContinuousModeEnabled && ( + + + ({ text }))} + value={continuousModeDateField} + onChange={e => setContinuousModeDateField(e.target.value)} + data-test-subj="transformContinuousDateFieldSelect" + /> + + - - - )} -
+ error={ + !isContinuousModeDelayValid && [ + i18n.translate('xpack.transform.stepDetailsForm.continuousModeDelayError', { + defaultMessage: 'Invalid delay format', + }), + ] + } + helpText={i18n.translate( + 'xpack.transform.stepDetailsForm.continuousModeDelayHelpText', + { + defaultMessage: 'Time delay between current time and latest input data time.', + } + )} + > + setContinuousModeDelay(e.target.value)} + aria-label={i18n.translate( + 'xpack.transform.stepDetailsForm.continuousModeAriaLabel', + { + defaultMessage: 'Choose a delay.', + } + )} + isInvalid={!isContinuousModeDelayValid} + data-test-subj="transformContinuousDelayInput" + /> + + + )} + +
); }); diff --git a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_details/step_details_summary.tsx b/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_details/step_details_summary.tsx index 1f2280fa4fb19..ddb34dfa3f87d 100644 --- a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_details/step_details_summary.tsx +++ b/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_details/step_details_summary.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { Fragment, SFC } from 'react'; +import React, { SFC } from 'react'; import { i18n } from '@kbn/i18n'; @@ -33,7 +33,7 @@ export const StepDetailsSummary: SFC = React.memo( : ''; return ( - +
= React.memo( )} - +
); } ); diff --git a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/wizard_nav/wizard_nav.tsx b/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/wizard_nav/wizard_nav.tsx index 78273d825c152..bfdfcbf696d35 100644 --- a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/wizard_nav/wizard_nav.tsx +++ b/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/wizard_nav/wizard_nav.tsx @@ -27,7 +27,13 @@ export const WizardNav: SFC = ({ {previous && ( - + {i18n.translate('xpack.transform.wizard.previousStepButton', { defaultMessage: 'Previous', })} @@ -36,7 +42,13 @@ export const WizardNav: SFC = ({ )} {next && ( - + {i18n.translate('xpack.transform.wizard.nextStepButton', { defaultMessage: 'Next', })} diff --git a/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/__snapshots__/transform_list.test.tsx.snap b/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/__snapshots__/transform_list.test.tsx.snap index 1ddbbbb2be8e0..e7a5e027e6f8d 100644 --- a/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/__snapshots__/transform_list.test.tsx.snap +++ b/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/__snapshots__/transform_list.test.tsx.snap @@ -9,6 +9,7 @@ exports[`Transform: Transform List Minimal initialization 1`] actions={ Array [ diff --git a/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/columns.tsx b/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/columns.tsx index 700b659028b7e..19ab74cc9ed85 100644 --- a/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/columns.tsx +++ b/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/columns.tsx @@ -116,29 +116,34 @@ export const getColumns = ( }) } iconType={expandedRowItemIds.includes(item.config.id) ? 'arrowUp' : 'arrowDown'} + data-test-subj="transformListRowDetailsToggle" /> ), }, { field: TRANSFORM_LIST_COLUMN.ID, + 'data-test-subj': 'transformListColumnId', name: 'ID', sortable: true, truncateText: true, }, { field: TRANSFORM_LIST_COLUMN.DESCRIPTION, + 'data-test-subj': 'transformListColumnDescription', name: i18n.translate('xpack.transform.description', { defaultMessage: 'Description' }), sortable: true, truncateText: true, }, { field: TRANSFORM_LIST_COLUMN.CONFIG_SOURCE_INDEX, + 'data-test-subj': 'transformListColumnSourceIndex', name: i18n.translate('xpack.transform.sourceIndex', { defaultMessage: 'Source index' }), sortable: true, truncateText: true, }, { field: TRANSFORM_LIST_COLUMN.CONFIG_DEST_INDEX, + 'data-test-subj': 'transformListColumnDestinationIndex', name: i18n.translate('xpack.transform.destinationIndex', { defaultMessage: 'Destination index', }), @@ -147,6 +152,7 @@ export const getColumns = ( }, { name: i18n.translate('xpack.transform.status', { defaultMessage: 'Status' }), + 'data-test-subj': 'transformListColumnStatus', sortable: (item: TransformListRow) => item.stats.state, truncateText: true, render(item: TransformListRow) { @@ -156,6 +162,7 @@ export const getColumns = ( }, { name: i18n.translate('xpack.transform.mode', { defaultMessage: 'Mode' }), + 'data-test-subj': 'transformListColumnMode', sortable: (item: TransformListRow) => item.mode, truncateText: true, render(item: TransformListRow) { @@ -167,6 +174,7 @@ export const getColumns = ( }, { name: i18n.translate('xpack.transform.progress', { defaultMessage: 'Progress' }), + 'data-test-subj': 'transformListColumnProgress', sortable: (item: TransformListRow) => getTransformProgress(item) || 0, truncateText: true, render(item: TransformListRow) { @@ -183,7 +191,13 @@ export const getColumns = ( {isBatchTransform && ( - + {progress}% diff --git a/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/transform_list.tsx b/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/transform_list.tsx index a7e4e49440089..e85a52b671934 100644 --- a/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/transform_list.tsx +++ b/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/transform_list/transform_list.tsx @@ -219,7 +219,11 @@ export const TransformList: FC = ({ } actions={[ - + {i18n.translate('xpack.transform.list.emptyPromptButtonText', { defaultMessage: 'Create your first transform', })} @@ -374,7 +378,7 @@ export const TransformList: FC = ({ }; return ( - +
= ({ itemIdToExpandedRowMap={itemIdToExpandedRowMap} onTableChange={onTableChange} pagination={pagination} + rowProps={item => ({ + 'data-test-subj': `transformListRow row-${item.id}`, + })} selection={selection} sorting={sorting} search={search} - data-test-subj="transformTableTransforms" + data-test-subj={ + isLoading || transformsLoading + ? 'transformListTable loading' + : 'transformListTable loaded' + } /> - +
); }; diff --git a/x-pack/legacy/plugins/transform/public/app/sections/transform_management/transform_management_section.tsx b/x-pack/legacy/plugins/transform/public/app/sections/transform_management/transform_management_section.tsx index 2e0e930990e6e..2bd03df9957d2 100644 --- a/x-pack/legacy/plugins/transform/public/app/sections/transform_management/transform_management_section.tsx +++ b/x-pack/legacy/plugins/transform/public/app/sections/transform_management/transform_management_section.tsx @@ -138,7 +138,11 @@ export const TransformManagement: FC = () => { {isSearchSelectionVisible && ( - + diff --git a/x-pack/test/functional/apps/transform/creation.ts b/x-pack/test/functional/apps/transform/creation.ts new file mode 100644 index 0000000000000..3ab17c0d90a83 --- /dev/null +++ b/x-pack/test/functional/apps/transform/creation.ts @@ -0,0 +1,229 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import expect from '@kbn/expect'; + +import { FtrProviderContext } from '../../ftr_provider_context'; + +interface GroupByEntry { + identifier: string; + label: string; + intervalLabel?: string; +} + +export default function({ getService }: FtrProviderContext) { + const esArchiver = getService('esArchiver'); + const transform = getService('transform'); + + describe('creation', function() { + this.tags(['smoke']); + before(async () => { + await esArchiver.load('ml/ecommerce'); + }); + + after(async () => { + await esArchiver.unload('ml/ecommerce'); + await transform.api.cleanTransformIndices(); + }); + + const testDataList = [ + { + suiteTitle: 'batch transform with terms+date_histogram groups and avg agg', + source: 'ecommerce', + groupByEntries: [ + { + identifier: 'terms(category.keyword)', + label: 'category.keyword', + } as GroupByEntry, + { + identifier: 'date_histogram(order_date)', + label: 'order_date', + intervalLabel: '1m', + } as GroupByEntry, + ], + aggregationEntries: [ + { + identifier: 'avg(products.base_price)', + label: 'products.base_price.avg', + }, + ], + transformId: `ec_1_${Date.now()}`, + transformDescription: + 'ecommerce batch transform with groups terms(category.keyword) + date_histogram(order_date) 1m and aggregation avg(products.base_price)', + get destinationIndex(): string { + return `dest_${this.transformId}`; + }, + expected: { + row: { + status: 'stopped', + mode: 'batch', + progress: '100', + }, + }, + }, + ]; + + for (const testData of testDataList) { + describe(`${testData.suiteTitle}`, function() { + after(async () => { + await transform.api.deleteIndices(testData.destinationIndex); + }); + + it('loads the home page', async () => { + await transform.navigation.navigateTo(); + await transform.management.assertTransformListPageExists(); + }); + + it('displays the stats bar', async () => { + await transform.management.assertTransformStatsBarExists(); + }); + + it('loads the source selection modal', async () => { + await transform.management.startTransformCreation(); + }); + + it('selects the source data', async () => { + await transform.sourceSelection.selectSource(testData.source); + }); + + it('displays the define pivot step', async () => { + await transform.wizard.assertDefineStepActive(); + }); + + it('loads the source index preview', async () => { + await transform.wizard.assertSourceIndexPreviewLoaded(); + }); + + it('displays an empty pivot preview', async () => { + await transform.wizard.assertPivotPreviewEmpty(); + }); + + it('displays the query input', async () => { + await transform.wizard.assertQueryInputExists(); + await transform.wizard.assertQueryValue(''); + }); + + it('displays the advanced query editor switch', async () => { + await transform.wizard.assertAdvancedQueryEditorSwitchExists(); + await transform.wizard.assertAdvancedQueryEditorSwitchCheckState(false); + }); + + it('adds the group by entries', async () => { + for (const [index, entry] of testData.groupByEntries.entries()) { + await transform.wizard.assertGroupByInputExists(); + await transform.wizard.assertGroupByInputValue([]); + await transform.wizard.addGroupByEntry( + index, + entry.identifier, + entry.label, + entry.intervalLabel + ); + } + }); + + it('adds the aggregation entries', async () => { + for (const [index, agg] of testData.aggregationEntries.entries()) { + await transform.wizard.assertAggregationInputExists(); + await transform.wizard.assertAggregationInputValue([]); + await transform.wizard.addAggregationEntry(index, agg.identifier, agg.label); + } + }); + + it('displays the advanced pivot editor switch', async () => { + await transform.wizard.assertAdvancedPivotEditorSwitchExists(); + await transform.wizard.assertAdvancedPivotEditorSwitchCheckState(false); + }); + + it('loads the pivot preview', async () => { + await transform.wizard.assertPivotPreviewLoaded(); + }); + + it('loads the details step', async () => { + await transform.wizard.advanceToDetailsStep(); + }); + + it('inputs the transform id', async () => { + await transform.wizard.assertTransformIdInputExists(); + await transform.wizard.assertTransformIdValue(''); + await transform.wizard.setTransformId(testData.transformId); + }); + + it('inputs the transform description', async () => { + await transform.wizard.assertTransformDescriptionInputExists(); + await transform.wizard.assertTransformDescriptionValue(''); + await transform.wizard.setTransformDescription(testData.transformDescription); + }); + + it('inputs the destination index', async () => { + await transform.wizard.assertDestinationIndexInputExists(); + await transform.wizard.assertDestinationIndexValue(''); + await transform.wizard.setDestinationIndex(testData.destinationIndex); + }); + + it('displays the create index pattern switch', async () => { + await transform.wizard.assertCreateIndexPatternSwitchExists(); + await transform.wizard.assertCreateIndexPatternSwitchCheckState(true); + }); + + it('displays the continuous mode switch', async () => { + await transform.wizard.assertContinuousModeSwitchExists(); + await transform.wizard.assertContinuousModeSwitchCheckState(false); + }); + + it('loads the create step', async () => { + await transform.wizard.advanceToCreateStep(); + }); + + it('displays the create and start button', async () => { + await transform.wizard.assertCreateAndStartButtonExists(); + }); + + it('displays the create button', async () => { + await transform.wizard.assertCreateButtonExists(); + }); + + it('displays the copy to clipboard button', async () => { + await transform.wizard.assertCreateAndStartButtonExists(); + }); + + it('creates the transform', async () => { + await transform.wizard.createTransform(); + }); + + it('starts the transform and finishes processing', async () => { + await transform.wizard.startTransform(); + await transform.wizard.waitForProgressBarComplete(); + }); + + it('returns to the management page', async () => { + await transform.wizard.returnToManagement(); + }); + + it('displays the transforms table', async () => { + await transform.management.assertTransformsTableExists(); + }); + + it('displays the created transform in the transform list', async () => { + await transform.table.refreshTransformList(); + await transform.table.filterWithSearchString(testData.transformId); + const rows = await transform.table.parseTransformTable(); + expect(rows.filter(row => row.id === testData.transformId)).to.have.length(1); + }); + + it('job creation displays details for the created job in the job list', async () => { + await transform.table.assertTransformRowFields(testData.transformId, { + id: testData.transformId, + description: testData.transformDescription, + sourceIndex: testData.source, + destinationIndex: testData.destinationIndex, + status: testData.expected.row.status, + mode: testData.expected.row.mode, + progress: testData.expected.row.progress, + }); + }); + }); + } + }); +} diff --git a/x-pack/test/functional/apps/transform/index.ts b/x-pack/test/functional/apps/transform/index.ts new file mode 100644 index 0000000000000..adee997905a31 --- /dev/null +++ b/x-pack/test/functional/apps/transform/index.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { FtrProviderContext } from '../../ftr_provider_context'; + +export default function({ loadTestFile }: FtrProviderContext) { + describe('transform', function() { + this.tags(['ciGroup9', 'transform']); + + loadTestFile(require.resolve('./creation')); + }); +} diff --git a/x-pack/test/functional/config.js b/x-pack/test/functional/config.js index 6f9d4cbd5764e..55fe53e12e403 100644 --- a/x-pack/test/functional/config.js +++ b/x-pack/test/functional/config.js @@ -55,6 +55,7 @@ export default async function ({ readConfigFile }) { resolve(__dirname, './apps/snapshot_restore'), resolve(__dirname, './apps/cross_cluster_replication'), resolve(__dirname, './apps/remote_clusters'), + resolve(__dirname, './apps/transform'), // This license_management file must be last because it is destructive. resolve(__dirname, './apps/license_management'), ], @@ -193,6 +194,10 @@ export default async function ({ readConfigFile }) { pathname: '/app/kibana', hash: '/management/elasticsearch/watcher/watches/', }, + transform: { + pathname: '/app/kibana/', + hash: '/management/elasticsearch/transform' + } }, // choose where esArchiver should load archives from diff --git a/x-pack/test/functional/services/index.ts b/x-pack/test/functional/services/index.ts index 9a256b3bbb141..a8e5749004afe 100644 --- a/x-pack/test/functional/services/index.ts +++ b/x-pack/test/functional/services/index.ts @@ -47,6 +47,7 @@ import { UptimeProvider } from './uptime'; import { InfraSourceConfigurationFormProvider } from './infra_source_configuration_form'; import { InfraLogStreamProvider } from './infra_log_stream'; import { MachineLearningProvider } from './ml'; +import { TransformProvider } from './transform'; import { SecurityServiceProvider, SpacesServiceProvider } from '../../common/services'; @@ -89,4 +90,5 @@ export const services = { infraSourceConfigurationForm: InfraSourceConfigurationFormProvider, infraLogStream: InfraLogStreamProvider, ml: MachineLearningProvider, + transform: TransformProvider, }; diff --git a/x-pack/test/functional/services/transform.ts b/x-pack/test/functional/services/transform.ts new file mode 100644 index 0000000000000..f495626bac9b8 --- /dev/null +++ b/x-pack/test/functional/services/transform.ts @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { FtrProviderContext } from '../ftr_provider_context'; + +import { + TransformAPIProvider, + TransformManagementProvider, + TransformNavigationProvider, + TransformSourceSelectionProvider, + TransformTableProvider, + TransformWizardProvider, +} from './transform_ui'; + +export function TransformProvider(context: FtrProviderContext) { + const api = TransformAPIProvider(context); + const management = TransformManagementProvider(context); + const navigation = TransformNavigationProvider(context); + const sourceSelection = TransformSourceSelectionProvider(context); + const table = TransformTableProvider(context); + const wizard = TransformWizardProvider(context); + + return { + api, + management, + navigation, + sourceSelection, + table, + wizard, + }; +} diff --git a/x-pack/test/functional/services/transform_ui/api.ts b/x-pack/test/functional/services/transform_ui/api.ts new file mode 100644 index 0000000000000..a6756e5940d72 --- /dev/null +++ b/x-pack/test/functional/services/transform_ui/api.ts @@ -0,0 +1,43 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import expect from '@kbn/expect'; + +import { FtrProviderContext } from '../../ftr_provider_context'; + +export function TransformAPIProvider({ getService }: FtrProviderContext) { + const es = getService('legacyEs'); + const log = getService('log'); + const retry = getService('retry'); + + return { + async deleteIndices(indices: string) { + log.debug(`Deleting indices: '${indices}'...`); + if ((await es.indices.exists({ index: indices, allowNoIndices: false })) === false) { + log.debug(`Indices '${indices}' don't exist. Nothing to delete.`); + return; + } + + const deleteResponse = await es.indices.delete({ + index: indices, + }); + expect(deleteResponse) + .to.have.property('acknowledged') + .eql(true, 'Response for delete request should be acknowledged'); + + await retry.waitForWithTimeout(`'${indices}' indices to be deleted`, 30 * 1000, async () => { + if ((await es.indices.exists({ index: indices, allowNoIndices: false })) === false) { + return true; + } else { + throw new Error(`expected indices '${indices}' to be deleted`); + } + }); + }, + + async cleanTransformIndices() { + await this.deleteIndices('.transform-*'); + }, + }; +} diff --git a/x-pack/test/functional/services/transform_ui/index.ts b/x-pack/test/functional/services/transform_ui/index.ts new file mode 100644 index 0000000000000..5b5270a575028 --- /dev/null +++ b/x-pack/test/functional/services/transform_ui/index.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { TransformAPIProvider } from './api'; +export { TransformManagementProvider } from './management'; +export { TransformNavigationProvider } from './navigation'; +export { TransformSourceSelectionProvider } from './source_selection'; +export { TransformTableProvider } from './transform_table'; +export { TransformWizardProvider } from './wizard'; diff --git a/x-pack/test/functional/services/transform_ui/management.ts b/x-pack/test/functional/services/transform_ui/management.ts new file mode 100644 index 0000000000000..ffc581ad06d7b --- /dev/null +++ b/x-pack/test/functional/services/transform_ui/management.ts @@ -0,0 +1,42 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { FtrProviderContext } from '../../ftr_provider_context'; + +export function TransformManagementProvider({ getService }: FtrProviderContext) { + const testSubjects = getService('testSubjects'); + + return { + async assertTransformListPageExists() { + await testSubjects.existOrFail('transformPageTransformList'); + }, + + async assertNoTransformsFoundMessageExists() { + await testSubjects.existOrFail('transformNoTransformsFound'); + }, + + async assertTransformsTableExists() { + await testSubjects.existOrFail('~transformListTable'); + }, + + async assertCreateNewTransformButtonExists() { + await testSubjects.existOrFail('transformButtonCreate'); + }, + + async assertTransformStatsBarExists() { + await testSubjects.existOrFail('transformStatsBar'); + }, + + async startTransformCreation() { + if (await testSubjects.exists('transformNoTransformsFound')) { + await testSubjects.click('transformCreateFirstButton'); + } else { + await testSubjects.click('transformButtonCreate'); + } + await testSubjects.existOrFail('transformSelectSourceModal'); + }, + }; +} diff --git a/x-pack/test/functional/services/transform_ui/navigation.ts b/x-pack/test/functional/services/transform_ui/navigation.ts new file mode 100644 index 0000000000000..a26cc90012694 --- /dev/null +++ b/x-pack/test/functional/services/transform_ui/navigation.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { FtrProviderContext } from '../../ftr_provider_context'; + +export function TransformNavigationProvider({ getPageObjects }: FtrProviderContext) { + const PageObjects = getPageObjects(['common']); + + return { + async navigateTo() { + return await PageObjects.common.navigateToApp('transform'); + }, + }; +} diff --git a/x-pack/test/functional/services/transform_ui/source_selection.ts b/x-pack/test/functional/services/transform_ui/source_selection.ts new file mode 100644 index 0000000000000..d2ef2c67f0004 --- /dev/null +++ b/x-pack/test/functional/services/transform_ui/source_selection.ts @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { FtrProviderContext } from '../../ftr_provider_context'; + +export function TransformSourceSelectionProvider({ getService }: FtrProviderContext) { + const testSubjects = getService('testSubjects'); + + return { + async assertSourceListContainsEntry(sourceName: string) { + await testSubjects.existOrFail(`savedObjectTitle${sourceName}`); + }, + + async filterSourceSelection(sourceName: string) { + await testSubjects.setValue('savedObjectFinderSearchInput', sourceName, { + clearWithKeyboard: true, + }); + await this.assertSourceListContainsEntry(sourceName); + }, + + async selectSource(sourceName: string) { + await this.filterSourceSelection(sourceName); + await testSubjects.clickWhenNotDisabled(`savedObjectTitle${sourceName}`); + await testSubjects.existOrFail('transformPageCreateTransform'); + }, + }; +} diff --git a/x-pack/test/functional/services/transform_ui/transform_table.ts b/x-pack/test/functional/services/transform_ui/transform_table.ts new file mode 100644 index 0000000000000..b9eff5e2b2435 --- /dev/null +++ b/x-pack/test/functional/services/transform_ui/transform_table.ts @@ -0,0 +1,87 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import expect from '@kbn/expect'; + +import { FtrProviderContext } from '../../ftr_provider_context'; + +export function TransformTableProvider({ getService }: FtrProviderContext) { + const testSubjects = getService('testSubjects'); + + return new (class TransformTable { + public async parseTransformTable() { + const table = await testSubjects.find('~transformListTable'); + const $ = await table.parseDomContent(); + const rows = []; + + for (const tr of $.findTestSubjects('~transformListRow').toArray()) { + const $tr = $(tr); + + rows.push({ + id: $tr + .findTestSubject('transformListColumnId') + .find('.euiTableCellContent') + .text() + .trim(), + description: $tr + .findTestSubject('transformListColumnDescription') + .find('.euiTableCellContent') + .text() + .trim(), + sourceIndex: $tr + .findTestSubject('transformListColumnSourceIndex') + .find('.euiTableCellContent') + .text() + .trim(), + destinationIndex: $tr + .findTestSubject('transformListColumnDestinationIndex') + .find('.euiTableCellContent') + .text() + .trim(), + status: $tr + .findTestSubject('transformListColumnStatus') + .find('.euiTableCellContent') + .text() + .trim(), + mode: $tr + .findTestSubject('transformListColumnMode') + .find('.euiTableCellContent') + .text() + .trim(), + progress: $tr + .findTestSubject('transformListColumnProgress') + .findTestSubject('transformListProgress') + .attr('value'), + }); + } + + return rows; + } + + public async refreshTransformList() { + await testSubjects.click('transformRefreshTransformListButton'); + await this.waitForTransformsToLoad(); + } + + public async waitForTransformsToLoad() { + await testSubjects.existOrFail('~transformListTable', { timeout: 60 * 1000 }); + await testSubjects.existOrFail('transformListTable loaded', { timeout: 30 * 1000 }); + } + + public async filterWithSearchString(filter: string) { + await this.waitForTransformsToLoad(); + const tableListContainer = await testSubjects.find('transformListTableContainer'); + const searchBarInput = await tableListContainer.findByClassName('euiFieldSearch'); + await searchBarInput.clearValueWithKeyboard(); + await searchBarInput.type(filter); + } + + public async assertTransformRowFields(transformId: string, expectedRow: object) { + const rows = await this.parseTransformTable(); + const transformRow = rows.filter(row => row.id === transformId)[0]; + expect(transformRow).to.eql(expectedRow); + } + })(); +} diff --git a/x-pack/test/functional/services/transform_ui/wizard.ts b/x-pack/test/functional/services/transform_ui/wizard.ts new file mode 100644 index 0000000000000..3daaa5e8db5c3 --- /dev/null +++ b/x-pack/test/functional/services/transform_ui/wizard.ts @@ -0,0 +1,352 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import expect from '@kbn/expect'; + +import { FtrProviderContext } from '../../ftr_provider_context'; + +export function TransformWizardProvider({ getService }: FtrProviderContext) { + const testSubjects = getService('testSubjects'); + const comboBox = getService('comboBox'); + const retry = getService('retry'); + + return { + async clickNextButton() { + await testSubjects.existOrFail('transformWizardNavButtonNext'); + await testSubjects.clickWhenNotDisabled('transformWizardNavButtonNext'); + }, + + async assertDefineStepActive() { + await testSubjects.existOrFail('transformStepDefineForm'); + }, + + async assertDefineSummaryExists() { + await testSubjects.existOrFail('transformStepDefineSummary'); + }, + + async assertDetailsStepActive() { + await testSubjects.existOrFail('transformStepDetailsForm'); + }, + + async assertDetailsSummaryExists() { + await testSubjects.existOrFail('transformStepDetailsSummary'); + }, + + async assertCreateStepActive() { + await testSubjects.existOrFail('transformStepCreateForm'); + }, + + async advanceToDetailsStep() { + await this.clickNextButton(); + await this.assertDetailsStepActive(); + await this.assertDefineSummaryExists(); + }, + + async advanceToCreateStep() { + await this.clickNextButton(); + await this.assertCreateStepActive(); + await this.assertDefineSummaryExists(); + await this.assertDetailsSummaryExists(); + }, + + async assertSourceIndexPreviewExists(subSelector?: string) { + let selector = 'transformSourceIndexPreview'; + if (subSelector !== undefined) { + selector = `${selector} ${subSelector}`; + } else { + selector = `~${selector}`; + } + await testSubjects.existOrFail(selector); + }, + + async assertSourceIndexPreviewLoaded() { + await this.assertSourceIndexPreviewExists('loaded'); + }, + + async assertPivotPreviewExists(subSelector?: string) { + let selector = 'transformPivotPreview'; + if (subSelector !== undefined) { + selector = `${selector} ${subSelector}`; + } else { + selector = `~${selector}`; + } + await testSubjects.existOrFail(selector); + }, + + async assertPivotPreviewLoaded() { + await this.assertPivotPreviewExists('loaded'); + }, + + async assertPivotPreviewEmpty() { + await this.assertPivotPreviewExists('empty'); + }, + + async assertQueryInputExists() { + await testSubjects.existOrFail('tarnsformQueryInput'); + }, + + async assertQueryValue(expectedQuery: string) { + const actualQuery = await testSubjects.getVisibleText('tarnsformQueryInput'); + expect(actualQuery).to.eql( + expectedQuery, + `Query input text should be '${expectedQuery}' (got ${actualQuery})` + ); + }, + + async assertAdvancedQueryEditorSwitchExists() { + await testSubjects.existOrFail(`transformAdvancedQueryEditorSwitch`, { allowHidden: true }); + }, + + async assertAdvancedQueryEditorSwitchCheckState(expectedCheckState: boolean) { + const actualCheckState = await testSubjects.isSelected('transformAdvancedQueryEditorSwitch'); + expect(actualCheckState).to.eql( + expectedCheckState, + `Advanced query editor switch check state should be ${expectedCheckState} (got ${actualCheckState})` + ); + }, + + async assertGroupByInputExists() { + await testSubjects.existOrFail('transformGroupBySelection > comboBoxInput'); + }, + + async assertGroupByInputValue(expectedIdentifier: string[]) { + await retry.tryForTime(2000, async () => { + const comboBoxSelectedOptions = await comboBox.getComboBoxSelectedOptions( + 'transformGroupBySelection > comboBoxInput' + ); + expect(comboBoxSelectedOptions).to.eql(expectedIdentifier); + }); + }, + + async assertGroupByEntryExists( + index: number, + expectedLabel: string, + expectedIntervalLabel?: string + ) { + await testSubjects.existOrFail(`transformGroupByEntry ${index}`); + + const actualLabel = await testSubjects.getVisibleText( + `transformGroupByEntry ${index} > transformGroupByEntryLabel` + ); + expect(actualLabel).to.eql( + expectedLabel, + `Label for group by entry ${index} should be '${expectedLabel}' (got '${actualLabel}')` + ); + + if (expectedIntervalLabel !== undefined) { + const actualIntervalLabel = await testSubjects.getVisibleText( + `transformGroupByEntry ${index} > transformGroupByEntryIntervalLabel` + ); + expect(actualIntervalLabel).to.eql( + expectedIntervalLabel, + `Label for group by entry ${index} should be '${expectedIntervalLabel}' (got '${actualIntervalLabel}')` + ); + } + }, + + async addGroupByEntry( + index: number, + identifier: string, + expectedLabel: string, + expectedIntervalLabel?: string + ) { + await comboBox.set('transformGroupBySelection > comboBoxInput', identifier); + await this.assertGroupByInputValue([]); + await this.assertGroupByEntryExists(index, expectedLabel, expectedIntervalLabel); + }, + + async assertAggregationInputExists() { + await testSubjects.existOrFail('transformAggregationSelection > comboBoxInput'); + }, + + async assertAggregationInputValue(expectedIdentifier: string[]) { + await retry.tryForTime(2000, async () => { + const comboBoxSelectedOptions = await comboBox.getComboBoxSelectedOptions( + 'transformAggregationSelection > comboBoxInput' + ); + expect(comboBoxSelectedOptions).to.eql(expectedIdentifier); + }); + }, + + async assertAggregationEntryExists(index: number, expectedLabel: string) { + await testSubjects.existOrFail(`transformAggregationEntry ${index}`); + + const actualLabel = await testSubjects.getVisibleText( + `transformAggregationEntry ${index} > transformAggregationEntryLabel` + ); + expect(actualLabel).to.eql( + expectedLabel, + `Label for aggregation entry ${index} should be '${expectedLabel}' (got '${actualLabel}')` + ); + }, + + async addAggregationEntry(index: number, identifier: string, expectedLabel: string) { + await comboBox.set('transformAggregationSelection > comboBoxInput', identifier); + await this.assertAggregationInputValue([]); + await this.assertAggregationEntryExists(index, expectedLabel); + }, + + async assertAdvancedPivotEditorSwitchExists() { + await testSubjects.existOrFail(`transformAdvancedPivotEditorSwitch`, { allowHidden: true }); + }, + + async assertAdvancedPivotEditorSwitchCheckState(expectedCheckState: boolean) { + const actualCheckState = await testSubjects.isSelected('transformAdvancedPivotEditorSwitch'); + expect(actualCheckState).to.eql( + expectedCheckState, + `Advanced pivot editor switch check state should be ${expectedCheckState} (got ${actualCheckState})` + ); + }, + + async assertTransformIdInputExists() { + await testSubjects.existOrFail('transformIdInput'); + }, + + async assertTransformIdValue(expectedValue: string) { + const actualTransformId = await testSubjects.getAttribute('transformIdInput', 'value'); + expect(actualTransformId).to.eql( + expectedValue, + `Transform id input text should be ${expectedValue} (got ${actualTransformId})` + ); + }, + + async setTransformId(transformId: string) { + await testSubjects.setValue('transformIdInput', transformId, { clearWithKeyboard: true }); + await this.assertTransformIdValue(transformId); + }, + + async assertTransformDescriptionInputExists() { + await testSubjects.existOrFail('transformDescriptionInput'); + }, + + async assertTransformDescriptionValue(expectedValue: string) { + const actualTransformDescription = await testSubjects.getAttribute( + 'transformDescriptionInput', + 'value' + ); + expect(actualTransformDescription).to.eql( + expectedValue, + `Transform description input text should be ${expectedValue} (got ${actualTransformDescription})` + ); + }, + + async setTransformDescription(transformDescription: string) { + await testSubjects.setValue('transformDescriptionInput', transformDescription, { + clearWithKeyboard: true, + }); + await this.assertTransformDescriptionValue(transformDescription); + }, + + async assertDestinationIndexInputExists() { + await testSubjects.existOrFail('transformDestinationIndexInput'); + }, + + async assertDestinationIndexValue(expectedValue: string) { + const actualDestinationIndex = await testSubjects.getAttribute( + 'transformDestinationIndexInput', + 'value' + ); + expect(actualDestinationIndex).to.eql( + expectedValue, + `Destination index input text should be ${expectedValue} (got ${actualDestinationIndex})` + ); + }, + + async setDestinationIndex(destinationIndex: string) { + await testSubjects.setValue('transformDestinationIndexInput', destinationIndex, { + clearWithKeyboard: true, + }); + await this.assertDestinationIndexValue(destinationIndex); + }, + + async assertCreateIndexPatternSwitchExists() { + await testSubjects.existOrFail(`transformCreateIndexPatternSwitch`, { allowHidden: true }); + }, + + async assertCreateIndexPatternSwitchCheckState(expectedCheckState: boolean) { + const actualCheckState = await testSubjects.isSelected('transformCreateIndexPatternSwitch'); + expect(actualCheckState).to.eql( + expectedCheckState, + `Create index pattern switch check state should be ${expectedCheckState} (got ${actualCheckState})` + ); + }, + + async assertContinuousModeSwitchExists() { + await testSubjects.existOrFail(`transformContinuousModeSwitch`, { allowHidden: true }); + }, + + async assertContinuousModeSwitchCheckState(expectedCheckState: boolean) { + const actualCheckState = await testSubjects.isSelected('transformContinuousModeSwitch'); + expect(actualCheckState).to.eql( + expectedCheckState, + `Continuous mode switch check state should be ${expectedCheckState} (got ${actualCheckState})` + ); + }, + + async assertCreateAndStartButtonExists() { + await testSubjects.existOrFail(`transformWizardCreateAndStartButton`); + }, + + async assertCreateButtonExists() { + await testSubjects.existOrFail(`transformWizardCreateButton`); + }, + + async assertCopyToClipboardButtonExists() { + await testSubjects.existOrFail(`transformWizardCopyToClipboardButton`); + }, + + async assertStartButtonExists() { + await testSubjects.existOrFail(`transformWizardStartButton`); + }, + + async assertManagementCardExists() { + await testSubjects.existOrFail(`transformWizardCardManagement`); + }, + + async returnToManagement() { + await testSubjects.click('transformWizardCardManagement'); + await testSubjects.existOrFail('transformPageTransformList'); + }, + + async assertDiscoverCardExists() { + await testSubjects.existOrFail(`transformWizardCardDiscover`); + }, + + async assertProgressbarExists() { + await testSubjects.existOrFail(`transformWizardProgressBar`); + }, + + async waitForProgressBarComplete() { + await retry.tryForTime(60 * 1000, async () => { + const actualValue = await testSubjects.getAttribute('transformWizardProgressBar', 'value'); + if (actualValue === '100') { + return true; + } else { + throw new Error(`Expected progress bar value to be 100 (got ${actualValue})`); + } + }); + }, + + async createTransform() { + await testSubjects.click('transformWizardCreateButton'); + await this.assertStartButtonExists(); + await this.assertManagementCardExists(); + expect(await testSubjects.isEnabled('transformWizardCreateButton')).to.eql( + false, + 'The create button should not be enabled any more' + ); + }, + + async startTransform() { + await testSubjects.click('transformWizardStartButton'); + await this.assertDiscoverCardExists(); + expect(await testSubjects.isEnabled('transformWizardStartButton')).to.eql( + false, + 'The start button should not be enabled any more' + ); + await this.assertProgressbarExists(); + }, + }; +}