From 63cb9ff7be925d9f140c096c90e1eabe9cca9d75 Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Tue, 3 Mar 2020 17:43:38 +0100 Subject: [PATCH] [ML] Use EuiDataGrid for transform wizard. (#52510) Replaces the custom EuiInMemoryTable component with EuiDataGrid for the transforms wizard. --- .../transform/public/app/common/data_grid.ts | 23 + .../transform/public/app/common/fields.ts | 32 +- .../transform/public/app/common/index.ts | 2 +- .../source_index_preview.tsx | 435 ++++++------------ .../use_source_index_data.test.tsx | 3 +- .../use_source_index_data.ts | 81 +--- .../step_create/step_create_form.tsx | 54 ++- .../components/step_define/common.test.ts | 56 ++- .../components/step_define/common.ts | 49 +- .../components/step_define/pivot_preview.tsx | 234 +++++----- .../step_define/step_define_form.tsx | 16 +- .../components/wizard/_wizard.scss | 22 +- .../translations/translations/ja-JP.json | 9 - .../translations/translations/zh-CN.json | 9 - .../apps/transform/creation_index_pattern.ts | 2 +- .../apps/transform/creation_saved_search.ts | 2 +- .../services/transform_ui/wizard.ts | 68 +-- 17 files changed, 515 insertions(+), 582 deletions(-) create mode 100644 x-pack/legacy/plugins/transform/public/app/common/data_grid.ts diff --git a/x-pack/legacy/plugins/transform/public/app/common/data_grid.ts b/x-pack/legacy/plugins/transform/public/app/common/data_grid.ts new file mode 100644 index 0000000000000..0783839afee83 --- /dev/null +++ b/x-pack/legacy/plugins/transform/public/app/common/data_grid.ts @@ -0,0 +1,23 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiDataGridStyle } from '@elastic/eui'; + +export const euiDataGridStyle: EuiDataGridStyle = { + border: 'all', + fontSize: 's', + cellPadding: 's', + stripes: false, + rowHover: 'highlight', + header: 'shade', +}; + +export const euiDataGridToolbarSettings = { + showColumnSelector: true, + showStyleSelector: false, + showSortSelector: true, + showFullScreenSelector: false, +}; diff --git a/x-pack/legacy/plugins/transform/public/app/common/fields.ts b/x-pack/legacy/plugins/transform/public/app/common/fields.ts index f2181654286db..108f45ce67e37 100644 --- a/x-pack/legacy/plugins/transform/public/app/common/fields.ts +++ b/x-pack/legacy/plugins/transform/public/app/common/fields.ts @@ -15,8 +15,6 @@ export interface EsDoc extends Dictionary { _source: EsDocSource; } -export const MAX_COLUMNS = 5; - export function getFlattenedFields(obj: EsDocSource): EsFieldName[] { const flatDocFields: EsFieldName[] = []; const newDocFields = Object.keys(obj); @@ -33,35 +31,33 @@ export function getFlattenedFields(obj: EsDocSource): EsFieldName[] { return flatDocFields; } -export const getSelectableFields = (docs: EsDoc[]): EsFieldName[] => { +export const getSelectableFields = (docs: EsDocSource[]): EsFieldName[] => { if (docs.length === 0) { return []; } - const newDocFields = getFlattenedFields(docs[0]._source); + const newDocFields = getFlattenedFields(docs[0]); newDocFields.sort(); return newDocFields; }; -export const getDefaultSelectableFields = (docs: EsDoc[]): EsFieldName[] => { +export const getDefaultSelectableFields = (docs: EsDocSource[]): EsFieldName[] => { if (docs.length === 0) { return []; } - const newDocFields = getFlattenedFields(docs[0]._source); + const newDocFields = getFlattenedFields(docs[0]); newDocFields.sort(); - return newDocFields - .filter(k => { - let value = false; - docs.forEach(row => { - const source = row._source; - if (source[k] !== null) { - value = true; - } - }); - return value; - }) - .slice(0, MAX_COLUMNS); + return newDocFields.filter(k => { + let value = false; + docs.forEach(row => { + const source = row; + if (source[k] !== null) { + value = true; + } + }); + return value; + }); }; export const toggleSelectedField = ( diff --git a/x-pack/legacy/plugins/transform/public/app/common/index.ts b/x-pack/legacy/plugins/transform/public/app/common/index.ts index 3f515db389b45..52a6884367bc5 100644 --- a/x-pack/legacy/plugins/transform/public/app/common/index.ts +++ b/x-pack/legacy/plugins/transform/public/app/common/index.ts @@ -5,6 +5,7 @@ */ export { AggName, isAggName } from './aggregations'; +export { euiDataGridStyle, euiDataGridToolbarSettings } from './data_grid'; export { getDefaultSelectableFields, getFlattenedFields, @@ -13,7 +14,6 @@ export { EsDoc, EsDocSource, EsFieldName, - MAX_COLUMNS, } from './fields'; export { DropDownLabel, DropDownOption, Label } from './dropdown'; export { diff --git a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/source_index_preview/source_index_preview.tsx b/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/source_index_preview/source_index_preview.tsx index 2b7d36cada3c6..0c9dcfb9b1c04 100644 --- a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/source_index_preview/source_index_preview.tsx +++ b/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/source_index_preview/source_index_preview.tsx @@ -4,60 +4,36 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useState } from 'react'; -import moment from 'moment-timezone'; +import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { i18n } from '@kbn/i18n'; import { - EuiBadge, - EuiButtonEmpty, EuiButtonIcon, EuiCallOut, - EuiCheckbox, EuiCodeBlock, EuiCopy, + EuiDataGrid, EuiFlexGroup, EuiFlexItem, - EuiPanel, - EuiPopover, - EuiPopoverTitle, EuiProgress, - EuiText, EuiTitle, - EuiToolTip, - RIGHT_ALIGNMENT, } from '@elastic/eui'; -import { - ColumnType, - mlInMemoryTableBasicFactory, - SortingPropType, - SORT_DIRECTION, -} from '../../../../../shared_imports'; - -import { KBN_FIELD_TYPES } from '../../../../../../../../../../src/plugins/data/public'; -import { Dictionary } from '../../../../../../common/types/common'; -import { formatHumanReadableDateTimeSeconds } from '../../../../../../common/utils/date_utils'; +import { getNestedProperty } from '../../../../../../common/utils/object_utils'; import { useCurrentIndexPattern } from '../../../../lib/kibana'; import { - toggleSelectedField, - EsDoc, + euiDataGridStyle, + euiDataGridToolbarSettings, EsFieldName, - MAX_COLUMNS, PivotQuery, } from '../../../../common'; import { getSourceIndexDevConsoleStatement } from './common'; -import { ExpandedRow } from './expanded_row'; import { SOURCE_INDEX_STATUS, useSourceIndexData } from './use_source_index_data'; -type ItemIdToExpandedRowMap = Dictionary; - -const CELL_CLICK_ENABLED = false; - interface SourceIndexPreviewTitle { indexPatternTitle: string; } @@ -74,67 +50,112 @@ const SourceIndexPreviewTitle: React.FC = ({ indexPatte interface Props { query: PivotQuery; - cellClick?(search: string): void; } -export const SourceIndexPreview: React.FC = React.memo(({ cellClick, query }) => { - const [clearTable, setClearTable] = useState(false); +const defaultPagination = { pageIndex: 0, pageSize: 5 }; +export const SourceIndexPreview: React.FC = React.memo(({ query }) => { const indexPattern = useCurrentIndexPattern(); + const allFields = indexPattern.fields.map(f => f.name); + const indexPatternFields: string[] = allFields.filter(f => { + if (indexPattern.metaFields.includes(f)) { + return false; + } - const [selectedFields, setSelectedFields] = useState([] as EsFieldName[]); - const [isColumnsPopoverVisible, setColumnsPopoverVisible] = useState(false); - - // EuiInMemoryTable has an issue with dynamic sortable columns - // and will trigger a full page Kibana error in such a case. - // The following is a workaround until this is solved upstream: - // - If the sortable/columns config changes, - // the table will be unmounted/not rendered. - // This is what setClearTable(true) in toggleColumn() does. - // - After that on next render it gets re-enabled. To make sure React - // doesn't consolidate the state updates, setTimeout is used. - if (clearTable) { - setTimeout(() => setClearTable(false), 0); - } + const fieldParts = f.split('.'); + const lastPart = fieldParts.pop(); + if (lastPart === 'keyword' && allFields.includes(fieldParts.join('.'))) { + return false; + } - function toggleColumnsPopover() { - setColumnsPopoverVisible(!isColumnsPopoverVisible); - } + return true; + }); - function closeColumnsPopover() { - setColumnsPopoverVisible(false); - } + // Column visibility + const [visibleColumns, setVisibleColumns] = useState(indexPatternFields); - function toggleColumn(column: EsFieldName) { - // spread to a new array otherwise the component wouldn't re-render - setClearTable(true); - setSelectedFields([...toggleSelectedField(selectedFields, column)]); - } + const [pagination, setPagination] = useState(defaultPagination); + + useEffect(() => { + setPagination(defaultPagination); + }, [query]); - const [itemIdToExpandedRowMap, setItemIdToExpandedRowMap] = useState( - {} as ItemIdToExpandedRowMap + const { errorMessage, status, rowCount, tableItems: data } = useSourceIndexData( + indexPattern, + query, + pagination ); - function toggleDetails(item: EsDoc) { - if (itemIdToExpandedRowMap[item._id]) { - delete itemIdToExpandedRowMap[item._id]; - } else { - itemIdToExpandedRowMap[item._id] = ; + // EuiDataGrid State + const dataGridColumns = indexPatternFields.map(id => { + const field = indexPattern.fields.getByName(id); + + let schema = 'string'; + + switch (field?.type) { + case 'date': + schema = 'datetime'; + break; + case 'geo_point': + schema = 'json'; + break; + case 'number': + schema = 'numeric'; + break; } - // spread to a new object otherwise the component wouldn't re-render - setItemIdToExpandedRowMap({ ...itemIdToExpandedRowMap }); - } - const { errorMessage, status, tableItems } = useSourceIndexData( - indexPattern, - query, - selectedFields, - setSelectedFields + return { id, schema }; + }); + + const onChangeItemsPerPage = useCallback( + pageSize => { + setPagination(p => { + const pageIndex = Math.floor((p.pageSize * p.pageIndex) / pageSize); + return { pageIndex, pageSize }; + }); + }, + [setPagination] ); + const onChangePage = useCallback(pageIndex => setPagination(p => ({ ...p, pageIndex })), [ + setPagination, + ]); + + // ** Sorting config + const [sortingColumns, setSortingColumns] = useState([]); + const onSort = useCallback(sc => setSortingColumns(sc), [setSortingColumns]); + + const renderCellValue = useMemo(() => { + return ({ + rowIndex, + columnId, + setCellProps, + }: { + rowIndex: number; + columnId: string; + setCellProps: any; + }) => { + const adjustedRowIndex = rowIndex - pagination.pageIndex * pagination.pageSize; + + const cellValue = data.hasOwnProperty(adjustedRowIndex) + ? getNestedProperty(data[adjustedRowIndex], columnId, null) + : null; + + if (typeof cellValue === 'object' && cellValue !== null) { + return JSON.stringify(cellValue); + } + + if (cellValue === undefined) { + return null; + } + + return cellValue; + }; + }, [data, pagination.pageIndex, pagination.pageSize]); + if (status === SOURCE_INDEX_STATUS.ERROR) { return ( - +
= React.memo(({ cellClick, quer {errorMessage} - +
); } - if (status === SOURCE_INDEX_STATUS.LOADED && tableItems.length === 0) { + if (status === SOURCE_INDEX_STATUS.LOADED && data.length === 0) { return ( - +
= React.memo(({ cellClick, quer })}

- +
); } - let docFields: EsFieldName[] = []; - let docFieldsCount = 0; - if (tableItems.length > 0) { - docFields = Object.keys(tableItems[0]._source); - docFields.sort(); - docFieldsCount = docFields.length; - } - - const columns: Array> = selectedFields.map(k => { - const column: ColumnType = { - field: `_source["${k}"]`, - name: k, - sortable: true, - truncateText: true, - }; - - const field = indexPattern.fields.find(f => f.name === k); - - const formatField = (d: string) => { - return field !== undefined && field.type === KBN_FIELD_TYPES.DATE - ? formatHumanReadableDateTimeSeconds(moment(d).unix() * 1000) - : d; - }; - - const render = (d: any) => { - if (Array.isArray(d) && d.every(item => typeof item === 'string')) { - // If the cells data is an array of strings, return as a comma separated list. - // The list will get limited to 5 items with `…` at the end if there's more in the original array. - return `${d - .map(item => formatField(item)) - .slice(0, 5) - .join(', ')}${d.length > 5 ? ', …' : ''}`; - } else if (Array.isArray(d)) { - // If the cells data is an array of e.g. objects, display a 'array' badge with a - // tooltip that explains that this type of field is not supported in this table. - return ( - - - {i18n.translate('xpack.transform.sourceIndexPreview.SourceIndexArrayBadgeContent', { - defaultMessage: 'array', - })} - - - ); - } else if (typeof d === 'object' && d !== null) { - // If the cells data is an object, display a 'object' badge with a - // tooltip that explains that this type of field is not supported in this table. - return ( - - - {i18n.translate('xpack.transform.sourceIndexPreview.SourceIndexObjectBadgeContent', { - defaultMessage: 'object', - })} - - - ); - } - - return formatField(d); - }; - - if (typeof field !== 'undefined') { - switch (field.type) { - case KBN_FIELD_TYPES.BOOLEAN: - column.dataType = 'boolean'; - break; - case KBN_FIELD_TYPES.DATE: - column.align = 'right'; - column.render = (d: any) => formatHumanReadableDateTimeSeconds(moment(d).unix() * 1000); - break; - case KBN_FIELD_TYPES.NUMBER: - column.dataType = 'number'; - break; - default: - column.render = render; - break; - } - } else { - column.render = render; - } - - if (CELL_CLICK_ENABLED && cellClick) { - column.render = (d: string) => ( - cellClick(`${k}:(${d})`)}> - {render(d)} - - ); - } - - return column; - }); - - let sorting: SortingPropType = false; - - if (columns.length > 0) { - sorting = { - sort: { - field: `_source["${selectedFields[0]}"]`, - direction: SORT_DIRECTION.ASC, - }, - }; - } - - columns.unshift({ - align: RIGHT_ALIGNMENT, - width: '40px', - isExpander: true, - render: (item: EsDoc) => ( - toggleDetails(item)} - aria-label={ - itemIdToExpandedRowMap[item._id] - ? i18n.translate('xpack.transform.sourceIndexPreview.rowCollapse', { - defaultMessage: 'Collapse', - }) - : i18n.translate('xpack.transform.sourceIndexPreview.rowExpand', { - defaultMessage: 'Expand', - }) - } - iconType={itemIdToExpandedRowMap[item._id] ? 'arrowUp' : 'arrowDown'} - /> - ), - }); - const euiCopyText = i18n.translate('xpack.transform.sourceIndexPreview.copyClipboardTooltip', { defaultMessage: 'Copy Dev Console statement of the source index preview to the clipboard.', }); - const MlInMemoryTableBasic = mlInMemoryTableBasicFactory(); - return ( - +
- + - - - - {docFieldsCount > MAX_COLUMNS && ( - - {i18n.translate('xpack.transform.sourceIndexPreview.fieldSelection', { - defaultMessage: - '{selectedFieldsLength, number} of {docFieldsCount, number} {docFieldsCount, plural, one {field} other {fields}} selected', - values: { selectedFieldsLength: selectedFields.length, docFieldsCount }, - })} - - )} - - - - - } - isOpen={isColumnsPopoverVisible} - closePopover={closeColumnsPopover} - ownFocus - > - - {i18n.translate('xpack.transform.sourceIndexPreview.selectFieldsPopoverTitle', { - defaultMessage: 'Select fields', - })} - -
- {docFields.map(d => ( - toggleColumn(d)} - disabled={selectedFields.includes(d) && selectedFields.length === 1} - /> - ))} -
-
-
-
- - - {(copy: () => void) => ( - - )} - - -
+ + + {(copy: () => void) => ( + + )} +
- {status === SOURCE_INDEX_STATUS.LOADING && } - {status !== SOURCE_INDEX_STATUS.LOADING && ( - - )} - {clearTable === false && columns.length > 0 && sorting !== false && ( - + {status === SOURCE_INDEX_STATUS.LOADING && } + {status !== SOURCE_INDEX_STATUS.LOADING && ( + + )} +
+ {dataGridColumns.length > 0 && data.length > 0 && ( + ({ - 'data-test-subj': `transformSourceIndexPreviewRow row-${item._id}`, - })} - sorting={sorting} /> )} -
+ ); }); diff --git a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/source_index_preview/use_source_index_data.test.tsx b/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/source_index_preview/use_source_index_data.test.tsx index fb0a71baea321..715573e3a6f67 100644 --- a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/source_index_preview/use_source_index_data.test.tsx +++ b/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/source_index_preview/use_source_index_data.test.tsx @@ -51,8 +51,7 @@ describe('useSourceIndexData', () => { sourceIndexObj = useSourceIndexData( { id: 'the-id', title: 'the-title', fields: [] }, query, - [], - () => {} + { pageIndex: 0, pageSize: 10 } ); }); }); diff --git a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/source_index_preview/use_source_index_data.ts b/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/source_index_preview/use_source_index_data.ts index e5c6783db1022..ae5bd9040baca 100644 --- a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/source_index_preview/use_source_index_data.ts +++ b/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/source_index_preview/use_source_index_data.ts @@ -4,27 +4,15 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useEffect, useState } from 'react'; +import { useEffect, useState } from 'react'; import { SearchResponse } from 'elasticsearch'; import { IIndexPattern } from 'src/plugins/data/public'; import { useApi } from '../../../../hooks/use_api'; -import { getNestedProperty } from '../../../../../../common/utils/object_utils'; -import { - getDefaultSelectableFields, - getFlattenedFields, - isDefaultQuery, - matchAllQuery, - EsDoc, - EsDocSource, - EsFieldName, - PivotQuery, -} from '../../../../common'; - -const SEARCH_SIZE = 1000; +import { isDefaultQuery, matchAllQuery, EsDocSource, PivotQuery } from '../../../../common'; export enum SOURCE_INDEX_STATUS { UNUSED, @@ -48,23 +36,34 @@ const isErrorResponse = (arg: any): arg is ErrorResponse => { return arg.error !== undefined; }; -type SourceIndexSearchResponse = ErrorResponse | SearchResponse; +// The types specified in `@types/elasticsearch` are out of date and still have `total: number`. +interface SearchResponse7 extends SearchResponse { + hits: SearchResponse['hits'] & { + total: { + value: number; + relation: string; + }; + }; +} + +type SourceIndexSearchResponse = ErrorResponse | SearchResponse7; export interface UseSourceIndexDataReturnType { errorMessage: string; status: SOURCE_INDEX_STATUS; - tableItems: EsDoc[]; + rowCount: number; + tableItems: EsDocSource[]; } export const useSourceIndexData = ( indexPattern: IIndexPattern, query: PivotQuery, - selectedFields: EsFieldName[], - setSelectedFields: React.Dispatch> + pagination: { pageIndex: number; pageSize: number } ): UseSourceIndexDataReturnType => { const [errorMessage, setErrorMessage] = useState(''); const [status, setStatus] = useState(SOURCE_INDEX_STATUS.UNUSED); - const [tableItems, setTableItems] = useState([]); + const [rowCount, setRowCount] = useState(0); + const [tableItems, setTableItems] = useState([]); const api = useApi(); const getSourceIndexData = async function() { @@ -74,7 +73,8 @@ export const useSourceIndexData = ( try { const resp: SourceIndexSearchResponse = await api.esSearch({ index: indexPattern.title, - size: SEARCH_SIZE, + from: pagination.pageIndex * pagination.pageSize, + size: pagination.pageSize, // Instead of using the default query (`*`), fall back to a more efficient `match_all` query. body: { query: isDefaultQuery(query) ? matchAllQuery : query }, }); @@ -83,41 +83,10 @@ export const useSourceIndexData = ( throw resp.error; } - const docs = resp.hits.hits; - - if (docs.length === 0) { - setTableItems([]); - setStatus(SOURCE_INDEX_STATUS.LOADED); - return; - } - - if (selectedFields.length === 0) { - const newSelectedFields = getDefaultSelectableFields(docs); - setSelectedFields(newSelectedFields); - } - - // Create a version of the doc's source with flattened field names. - // This avoids confusion later on if a field name has dots in its name - // or is a nested fields when displaying it via EuiInMemoryTable. - const flattenedFields = getFlattenedFields(docs[0]._source); - const transformedTableItems = docs.map(doc => { - const item: EsDocSource = {}; - flattenedFields.forEach(ff => { - item[ff] = getNestedProperty(doc._source, ff); - if (item[ff] === undefined) { - // If the attribute is undefined, it means it was not a nested property - // but had dots in its actual name. This selects the property by its - // full name and assigns it to `item[ff]`. - item[ff] = doc._source[`"${ff}"`]; - } - }); - return { - ...doc, - _source: item, - }; - }); + const docs = resp.hits.hits.map(d => d._source); - setTableItems(transformedTableItems); + setRowCount(resp.hits.total.value); + setTableItems(docs); setStatus(SOURCE_INDEX_STATUS.LOADED); } catch (e) { if (e.message !== undefined) { @@ -134,6 +103,6 @@ export const useSourceIndexData = ( getSourceIndexData(); // custom comparison // eslint-disable-next-line react-hooks/exhaustive-deps - }, [indexPattern.title, JSON.stringify(query)]); - return { errorMessage, status, tableItems }; + }, [indexPattern.title, JSON.stringify(query), JSON.stringify(pagination)]); + return { errorMessage, status, rowCount, tableItems }; }; 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 312d8a30dab77..bbeb97b6b8113 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 @@ -67,6 +67,7 @@ export const StepCreateForm: FC = React.memo( const [redirectToTransformManagement, setRedirectToTransformManagement] = useState(false); + const [loading, setLoading] = useState(false); const [created, setCreated] = useState(defaults.created); const [started, setStarted] = useState(defaults.started); const [indexPatternId, setIndexPatternId] = useState(defaults.indexPatternId); @@ -87,7 +88,7 @@ export const StepCreateForm: FC = React.memo( const api = useApi(); async function createTransform() { - setCreated(true); + setLoading(true); try { const resp = await api.createTransform(transformId, transformConfig); @@ -107,8 +108,9 @@ export const StepCreateForm: FC = React.memo( values: { transformId }, }) ); + setCreated(true); + setLoading(false); } catch (e) { - setCreated(false); toastNotifications.addDanger({ title: i18n.translate('xpack.transform.stepCreateForm.createTransformErrorMessage', { defaultMessage: 'An error occurred creating the transform {transformId}:', @@ -116,6 +118,8 @@ export const StepCreateForm: FC = React.memo( }), text: toMountPoint(), }); + setCreated(false); + setLoading(false); return false; } @@ -127,18 +131,27 @@ export const StepCreateForm: FC = React.memo( } async function startTransform() { - setStarted(true); + setLoading(true); try { - await api.startTransforms([{ id: transformId }]); - toastNotifications.addSuccess( - i18n.translate('xpack.transform.stepCreateForm.startTransformSuccessMessage', { - defaultMessage: 'Request to start transform {transformId} acknowledged.', - values: { transformId }, - }) - ); + const resp = await api.startTransforms([{ id: transformId }]); + if (typeof resp === 'object' && resp !== null && resp[transformId]?.success === true) { + toastNotifications.addSuccess( + i18n.translate('xpack.transform.stepCreateForm.startTransformSuccessMessage', { + defaultMessage: 'Request to start transform {transformId} acknowledged.', + values: { transformId }, + }) + ); + setStarted(true); + setLoading(false); + } else { + const errorMessage = + typeof resp === 'object' && resp !== null && resp[transformId]?.success === false + ? resp[transformId].error + : resp; + throw new Error(errorMessage); + } } catch (e) { - setStarted(false); toastNotifications.addDanger({ title: i18n.translate('xpack.transform.stepCreateForm.startTransformErrorMessage', { defaultMessage: 'An error occurred starting the transform {transformId}:', @@ -146,6 +159,8 @@ export const StepCreateForm: FC = React.memo( }), text: toMountPoint(), }); + setStarted(false); + setLoading(false); } } @@ -157,6 +172,7 @@ export const StepCreateForm: FC = React.memo( } const createKibanaIndexPattern = async () => { + setLoading(true); const indexPatternName = transformConfig.dest.index; try { @@ -178,6 +194,7 @@ export const StepCreateForm: FC = React.memo( values: { indexPatternName }, }) ); + setLoading(false); return; } @@ -195,6 +212,7 @@ export const StepCreateForm: FC = React.memo( ); setIndexPatternId(id); + setLoading(false); return true; } catch (e) { toastNotifications.addDanger({ @@ -205,13 +223,19 @@ export const StepCreateForm: FC = React.memo( }), text: toMountPoint(), }); + setLoading(false); return false; } }; const isBatchTransform = typeof transformConfig.sync === 'undefined'; - if (started === true && progressPercentComplete === undefined && isBatchTransform) { + if ( + loading === false && + started === true && + progressPercentComplete === undefined && + isBatchTransform + ) { // wrapping in function so we can keep the interval id in local scope function startProgressBar() { const interval = setInterval(async () => { @@ -266,7 +290,7 @@ export const StepCreateForm: FC = React.memo( @@ -293,7 +317,7 @@ export const StepCreateForm: FC = React.memo( @@ -315,7 +339,7 @@ export const StepCreateForm: FC = React.memo( diff --git a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_define/common.test.ts b/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_define/common.test.ts index 78ad217a69e3d..88e009c63339a 100644 --- a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_define/common.test.ts +++ b/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_define/common.test.ts @@ -4,6 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +import { EuiDataGridSorting } from '@elastic/eui'; + import { getPreviewRequestBody, PivotAggsConfig, @@ -13,10 +15,62 @@ import { SimpleQuery, } from '../../../../common'; -import { getPivotPreviewDevConsoleStatement, getPivotDropdownOptions } from './common'; +import { + multiColumnSortFactory, + getPivotPreviewDevConsoleStatement, + getPivotDropdownOptions, +} from './common'; import { IndexPattern } from '../../../../../../../../../../src/plugins/data/public'; describe('Transform: Define Pivot Common', () => { + test('customSortFactory()', () => { + const data = [ + { s: 'a', n: 1 }, + { s: 'a', n: 2 }, + { s: 'b', n: 3 }, + { s: 'b', n: 4 }, + ]; + + const sortingColumns1: EuiDataGridSorting['columns'] = [{ id: 's', direction: 'desc' }]; + const multiColumnSort1 = multiColumnSortFactory(sortingColumns1); + data.sort(multiColumnSort1); + + expect(data).toStrictEqual([ + { s: 'b', n: 3 }, + { s: 'b', n: 4 }, + { s: 'a', n: 1 }, + { s: 'a', n: 2 }, + ]); + + const sortingColumns2: EuiDataGridSorting['columns'] = [ + { id: 's', direction: 'asc' }, + { id: 'n', direction: 'desc' }, + ]; + const multiColumnSort2 = multiColumnSortFactory(sortingColumns2); + data.sort(multiColumnSort2); + + expect(data).toStrictEqual([ + { s: 'a', n: 2 }, + { s: 'a', n: 1 }, + { s: 'b', n: 4 }, + { s: 'b', n: 3 }, + ]); + + const sortingColumns3: EuiDataGridSorting['columns'] = [ + { id: 'n', direction: 'desc' }, + { id: 's', direction: 'desc' }, + ]; + const multiColumnSort3 = multiColumnSortFactory(sortingColumns3); + data.sort(multiColumnSort3); + + expect(data).toStrictEqual([ + { s: 'b', n: 4 }, + { s: 'b', n: 3 }, + { s: 'a', n: 2 }, + { s: 'a', n: 1 }, + ]); + }); + test('getPivotDropdownOptions()', () => { // The field name includes the characters []> as well as a leading and ending space charcter // which cannot be used for aggregation names. The test results verifies that the characters diff --git a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_define/common.ts b/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_define/common.ts index b4b03c1f0d571..7b78d4ffccfa1 100644 --- a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_define/common.ts +++ b/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/step_define/common.ts @@ -4,12 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ import { get } from 'lodash'; -import { EuiComboBoxOptionProps } from '@elastic/eui'; +import { EuiComboBoxOptionProps, EuiDataGridSorting } from '@elastic/eui'; import { IndexPattern, KBN_FIELD_TYPES, } from '../../../../../../../../../../src/plugins/data/public'; +import { getNestedProperty } from '../../../../../../common/utils/object_utils'; + import { PreviewRequestBody, DropDownLabel, @@ -28,6 +30,51 @@ export interface Field { type: KBN_FIELD_TYPES; } +/** + * Helper to sort an array of objects based on an EuiDataGrid sorting configuration. + * `sortFn()` is recursive to support sorting on multiple columns. + * + * @param sortingColumns - The EUI data grid sorting configuration + * @returns The sorting function which can be used with an array's sort() function. + */ +export const multiColumnSortFactory = (sortingColumns: EuiDataGridSorting['columns']) => { + const isString = (arg: any): arg is string => { + return typeof arg === 'string'; + }; + + const sortFn = (a: any, b: any, sortingColumnIndex = 0): number => { + const sort = sortingColumns[sortingColumnIndex]; + const aValue = getNestedProperty(a, sort.id, null); + const bValue = getNestedProperty(b, sort.id, null); + + if (typeof aValue === 'number' && typeof bValue === 'number') { + if (aValue < bValue) { + return sort.direction === 'asc' ? -1 : 1; + } + if (aValue > bValue) { + return sort.direction === 'asc' ? 1 : -1; + } + } + + if (isString(aValue) && isString(bValue)) { + if (aValue.localeCompare(bValue) === -1) { + return sort.direction === 'asc' ? -1 : 1; + } + if (aValue.localeCompare(bValue) === 1) { + return sort.direction === 'asc' ? 1 : -1; + } + } + + if (sortingColumnIndex + 1 < sortingColumns.length) { + return sortFn(a, b, sortingColumnIndex + 1); + } + + return 0; + }; + + return sortFn; +}; + function getDefaultGroupByConfig( aggName: string, dropDownName: string, 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 241f65614cea2..b755956eae24e 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 @@ -4,8 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { FC, useEffect, useRef, useState } from 'react'; -import moment from 'moment-timezone'; +import React, { FC, useCallback, useEffect, useMemo, useState } from 'react'; import { i18n } from '@kbn/i18n'; @@ -14,26 +13,23 @@ import { EuiCallOut, EuiCodeBlock, EuiCopy, + EuiDataGrid, + EuiDataGridSorting, EuiFlexGroup, EuiFlexItem, - EuiPanel, EuiProgress, EuiTitle, } from '@elastic/eui'; -import { - ColumnType, - mlInMemoryTableBasicFactory, - SORT_DIRECTION, -} from '../../../../../shared_imports'; import { dictionaryToArray } from '../../../../../../common/types/common'; -import { ES_FIELD_TYPES } from '../../../../../../../../../../src/plugins/data/public'; -import { formatHumanReadableDateTimeSeconds } from '../../../../../../common/utils/date_utils'; +import { getNestedProperty } from '../../../../../../common/utils/object_utils'; import { useCurrentIndexPattern } from '../../../../lib/kibana'; import { - getFlattenedFields, + euiDataGridStyle, + euiDataGridToolbarSettings, + EsFieldName, PreviewRequestBody, PivotAggsConfigDict, PivotGroupByConfig, @@ -41,8 +37,8 @@ import { PivotQuery, } from '../../../../common'; -import { getPivotPreviewDevConsoleStatement } from './common'; -import { PreviewItem, PIVOT_PREVIEW_STATUS, usePivotPreviewData } from './use_pivot_preview_data'; +import { getPivotPreviewDevConsoleStatement, multiColumnSortFactory } from './common'; +import { PIVOT_PREVIEW_STATUS, usePivotPreviewData } from './use_pivot_preview_data'; function sortColumns(groupByArr: PivotGroupByConfig[]) { return (a: string, b: string) => { @@ -60,14 +56,6 @@ function sortColumns(groupByArr: PivotGroupByConfig[]) { }; } -function usePrevious(value: any) { - const ref = useRef(null); - useEffect(() => { - ref.current = value; - }); - return ref.current; -} - interface PreviewTitleProps { previewRequest: PreviewRequestBody; } @@ -118,51 +106,103 @@ interface PivotPreviewProps { query: PivotQuery; } -export const PivotPreview: FC = React.memo(({ aggs, groupBy, query }) => { - const [clearTable, setClearTable] = useState(false); +const defaultPagination = { pageIndex: 0, pageSize: 5 }; +export const PivotPreview: FC = React.memo(({ aggs, groupBy, query }) => { const indexPattern = useCurrentIndexPattern(); const { - previewData, + previewData: data, previewMappings, errorMessage, previewRequest, status, } = usePivotPreviewData(indexPattern, query, aggs, groupBy); - const groupByArr = dictionaryToArray(groupBy); - // EuiInMemoryTable has an issue with dynamic sortable columns - // and will trigger a full page Kibana error in such a case. - // The following is a workaround until this is solved upstream: - // - If the sortable/columns config changes, - // the table will be unmounted/not rendered. - // This is what the useEffect() part does. - // - After that the table gets re-enabled. To make sure React - // doesn't consolidate the state updates, setTimeout is used. - const firstColumnName = - previewData.length > 0 - ? Object.keys(previewData[0]).sort(sortColumns(groupByArr))[0] - : undefined; - - const firstColumnNameChanged = usePrevious(firstColumnName) !== firstColumnName; + // Filters mapping properties of type `object`, which get returned for nested field parents. + const columnKeys = Object.keys(previewMappings.properties).filter( + key => previewMappings.properties[key].type !== 'object' + ); + columnKeys.sort(sortColumns(groupByArr)); + + // Column visibility + const [visibleColumns, setVisibleColumns] = useState(columnKeys); + useEffect(() => { - if (firstColumnNameChanged) { - setClearTable(true); - } - if (clearTable) { - setTimeout(() => setClearTable(false), 0); - } - }, [firstColumnNameChanged, clearTable]); + setVisibleColumns(columnKeys); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [JSON.stringify(columnKeys)]); - if (firstColumnNameChanged) { - return null; + const [pagination, setPagination] = useState(defaultPagination); + + // Reset pagination if data changes. This is to avoid ending up with an empty table + // when for example the user selected a page that is not available with the updated data. + useEffect(() => { + setPagination(defaultPagination); + }, [data.length]); + + // EuiDataGrid State + const dataGridColumns = columnKeys.map(id => ({ id })); + + const onChangeItemsPerPage = useCallback( + pageSize => { + setPagination(p => { + const pageIndex = Math.floor((p.pageSize * p.pageIndex) / pageSize); + return { pageIndex, pageSize }; + }); + }, + [setPagination] + ); + + const onChangePage = useCallback(pageIndex => setPagination(p => ({ ...p, pageIndex })), [ + setPagination, + ]); + + // Sorting config + const [sortingColumns, setSortingColumns] = useState([]); + const onSort = useCallback(sc => setSortingColumns(sc), [setSortingColumns]); + + if (sortingColumns.length > 0) { + data.sort(multiColumnSortFactory(sortingColumns)); } + const pageData = data.slice( + pagination.pageIndex * pagination.pageSize, + (pagination.pageIndex + 1) * pagination.pageSize + ); + + const renderCellValue = useMemo(() => { + return ({ + rowIndex, + columnId, + setCellProps, + }: { + rowIndex: number; + columnId: string; + setCellProps: any; + }) => { + const adjustedRowIndex = rowIndex - pagination.pageIndex * pagination.pageSize; + + const cellValue = pageData.hasOwnProperty(adjustedRowIndex) + ? getNestedProperty(pageData[adjustedRowIndex], columnId, null) + : null; + + if (typeof cellValue === 'object' && cellValue !== null) { + return JSON.stringify(cellValue); + } + + if (cellValue === undefined) { + return null; + } + + return cellValue; + }; + }, [pageData, pagination.pageIndex, pagination.pageSize]); + if (status === PIVOT_PREVIEW_STATUS.ERROR) { return ( - +
= React.memo(({ aggs, groupBy, > - +
); } - if (previewData.length === 0) { + if (data.length === 0) { let noDataMessage = i18n.translate( 'xpack.transform.pivotPreview.PivotPreviewNoDataCalloutBody', { @@ -196,7 +236,7 @@ export const PivotPreview: FC = React.memo(({ aggs, groupBy, ); } return ( - +
= React.memo(({ aggs, groupBy, >

{noDataMessage}

- +
); } - const columnKeys = getFlattenedFields(previewData[0]); - columnKeys.sort(sortColumns(groupByArr)); - - const columns = columnKeys.map(k => { - const column: ColumnType = { - field: k, - name: k, - sortable: true, - truncateText: true, - }; - if (typeof previewMappings.properties[k] !== 'undefined') { - const esFieldType = previewMappings.properties[k].type; - switch (esFieldType) { - case ES_FIELD_TYPES.BOOLEAN: - column.dataType = 'boolean'; - break; - case ES_FIELD_TYPES.DATE: - column.align = 'right'; - column.render = (d: any) => formatHumanReadableDateTimeSeconds(moment(d).unix() * 1000); - break; - case ES_FIELD_TYPES.BYTE: - case ES_FIELD_TYPES.DOUBLE: - case ES_FIELD_TYPES.FLOAT: - case ES_FIELD_TYPES.HALF_FLOAT: - case ES_FIELD_TYPES.INTEGER: - case ES_FIELD_TYPES.LONG: - case ES_FIELD_TYPES.SCALED_FLOAT: - case ES_FIELD_TYPES.SHORT: - column.dataType = 'number'; - break; - case ES_FIELD_TYPES.KEYWORD: - case ES_FIELD_TYPES.TEXT: - column.dataType = 'string'; - break; - } - } - return column; - }); - - if (columns.length === 0) { + if (columnKeys.length === 0) { return null; } - const sorting = { - sort: { - field: columns[0].field as string, - direction: SORT_DIRECTION.ASC, - }, - }; - - const MlInMemoryTableBasic = mlInMemoryTableBasicFactory(); - return ( - +
- {status === PIVOT_PREVIEW_STATUS.LOADING && } - {status !== PIVOT_PREVIEW_STATUS.LOADING && ( - - )} - {previewData.length > 0 && clearTable === false && columns.length > 0 && ( - + {status === PIVOT_PREVIEW_STATUS.LOADING && } + {status !== PIVOT_PREVIEW_STATUS.LOADING && ( + + )} +
+ {dataGridColumns.length > 0 && data.length > 0 && ( + ({ - 'data-test-subj': 'transformPivotPreviewRow', - })} - sorting={sorting} /> )} -
+ ); }); 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 bde832894632c..9b96e4b1ee758 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 @@ -17,6 +17,7 @@ import { EuiForm, EuiFormHelpText, EuiFormRow, + EuiHorizontalRule, EuiLink, EuiPanel, // @ts-ignore @@ -255,11 +256,6 @@ export const StepDefineForm: FC = React.memo(({ overrides = {}, onChange const [searchQuery, setSearchQuery] = useState(defaults.searchQuery); const [useKQL] = useState(true); - const addToSearch = (newSearch: string) => { - const currentDisplaySearch = searchString === defaultSearch ? emptySearch : searchString; - setSearchString(`${currentDisplaySearch} ${newSearch}`.trim()); - }; - const searchHandler = (d: Record) => { const { filterQuery, queryString } = d; const newSearch = queryString === emptySearch ? defaultSearch : queryString; @@ -568,8 +564,8 @@ export const StepDefineForm: FC = React.memo(({ overrides = {}, onChange const disabledQuery = numIndexFields > maxIndexFields; return ( - - + +
{kibanaContext.currentSavedSearch === undefined && typeof searchString === 'string' && ( @@ -906,9 +902,9 @@ export const StepDefineForm: FC = React.memo(({ overrides = {}, onChange
- - - + + +
diff --git a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/wizard/_wizard.scss b/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/wizard/_wizard.scss index 7c43e34d5f1b4..b235e9ebf7c21 100644 --- a/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/wizard/_wizard.scss +++ b/x-pack/legacy/plugins/transform/public/app/sections/create_transform/components/wizard/_wizard.scss @@ -1,13 +1,29 @@ +.transform__progress progress[value]::-webkit-progress-bar { + background-color: $euiColorGhost; +} + .transform__steps { .euiStep__content { padding-right: 0px; } } -/* This is an override to replicate the previous full-page-width of the transforms creation wizard +.transform__stepDefineForm { + align-items: flex-start; +} + +.transform__stepDefineFormLeftColumn { + min-width: 420px; + border-right: 1px solid $euiColorLightShade; +} + +/* +This is an override to replicate the previous full-page-width of the transforms creation wizard when it was in use within the ML plugin. The Kibana management section limits a max-width to 1200px which is a bit narrow for the two column layout of the transform wizard. We might revisit this for -future versions to blend in more with the overall design of the Kibana management section. */ +future versions to blend in more with the overall design of the Kibana management section. +The management section's navigation width is 192px + 24px right margin +*/ .mgtPage__body--transformWizard { - max-width: 100%; + max-width: calc(100% - 216px); } diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 75db834a969d0..0504343e4dcc3 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -12386,17 +12386,8 @@ "xpack.transform.progress": "進捗", "xpack.transform.sourceIndex": "ソースインデックス", "xpack.transform.sourceIndexPreview.copyClipboardTooltip": "ソースインデックスプレビューの開発コンソールステートメントをクリップボードにコピーします。", - "xpack.transform.sourceIndexPreview.fieldSelection": "{docFieldsCount, number} 件中 {selectedFieldsLength, number} 件の {docFieldsCount, plural, one {フィールド} other {フィールド}}を選択済み", - "xpack.transform.sourceIndexPreview.rowCollapse": "縮小", - "xpack.transform.sourceIndexPreview.rowExpand": "拡張", - "xpack.transform.sourceIndexPreview.selectColumnsAriaLabel": "列を選択", - "xpack.transform.sourceIndexPreview.selectFieldsPopoverTitle": "フィールドを選択", - "xpack.transform.sourceIndexPreview.SourceIndexArrayBadgeContent": "配列", - "xpack.transform.sourceIndexPreview.SourceIndexArrayToolTipContent": "この配列に基づく列の完全な内容は、展開された行に表示されます。", "xpack.transform.sourceIndexPreview.SourceIndexNoDataCalloutBody": "ソースインデックスのクエリが結果を返しませんでした。インデックスにドキュメントが含まれていて、クエリ要件が妥当であることを確認してください。", "xpack.transform.sourceIndexPreview.SourceIndexNoDataCalloutTitle": "ソースインデックスクエリの結果がありません", - "xpack.transform.sourceIndexPreview.SourceIndexObjectBadgeContent": "オブジェクト", - "xpack.transform.sourceIndexPreview.SourceIndexObjectToolTipContent": "このオブジェクトベースの列の完全な内容は、展開された行に表示されます。", "xpack.transform.sourceIndexPreview.sourceIndexPatternError": "ソースインデックスデータの読み込み中にエラーが発生しました。", "xpack.transform.sourceIndexPreview.sourceIndexPatternTitle": "ソースインデックス {indexPatternTitle}", "xpack.transform.statsBar.batchTransformsLabel": "一斉", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index d67d2054a2da6..156b1d3d24153 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -12386,17 +12386,8 @@ "xpack.transform.progress": "进度", "xpack.transform.sourceIndex": "源索引", "xpack.transform.sourceIndexPreview.copyClipboardTooltip": "将源索引预览的开发控制台语句复制到剪贴板。", - "xpack.transform.sourceIndexPreview.fieldSelection": "已选择 {selectedFieldsLength, number} 个{docFieldsCount, plural, one {字段} other {字段}},共 {docFieldsCount, number} 个", - "xpack.transform.sourceIndexPreview.rowCollapse": "折叠", - "xpack.transform.sourceIndexPreview.rowExpand": "展开", - "xpack.transform.sourceIndexPreview.selectColumnsAriaLabel": "选择列", - "xpack.transform.sourceIndexPreview.selectFieldsPopoverTitle": "选择字段", - "xpack.transform.sourceIndexPreview.SourceIndexArrayBadgeContent": "数组", - "xpack.transform.sourceIndexPreview.SourceIndexArrayToolTipContent": "此基于数组的列的完整内容在展开的行中。", "xpack.transform.sourceIndexPreview.SourceIndexNoDataCalloutBody": "源索引的查询未返回结果。请确保索引包含文档且您的查询限制不过于严格。", "xpack.transform.sourceIndexPreview.SourceIndexNoDataCalloutTitle": "源索引查询结果为空。", - "xpack.transform.sourceIndexPreview.SourceIndexObjectBadgeContent": "对象", - "xpack.transform.sourceIndexPreview.SourceIndexObjectToolTipContent": "此基于对象的列的完整内容在展开的行中。", "xpack.transform.sourceIndexPreview.sourceIndexPatternError": "加载源索引数据时出错。", "xpack.transform.sourceIndexPreview.sourceIndexPatternTitle": "源索引 {indexPatternTitle}", "xpack.transform.statsBar.batchTransformsLabel": "批量", diff --git a/x-pack/test/functional/apps/transform/creation_index_pattern.ts b/x-pack/test/functional/apps/transform/creation_index_pattern.ts index 5b54bfdafdbdb..4d1300ffaad06 100644 --- a/x-pack/test/functional/apps/transform/creation_index_pattern.ts +++ b/x-pack/test/functional/apps/transform/creation_index_pattern.ts @@ -89,7 +89,7 @@ export default function({ getService }: FtrProviderContext) { progress: '100', }, sourcePreview: { - columns: 6, + columns: 45, rows: 5, }, }, diff --git a/x-pack/test/functional/apps/transform/creation_saved_search.ts b/x-pack/test/functional/apps/transform/creation_saved_search.ts index 2f5f60e1573c8..bf501c65bc79b 100644 --- a/x-pack/test/functional/apps/transform/creation_saved_search.ts +++ b/x-pack/test/functional/apps/transform/creation_saved_search.ts @@ -63,7 +63,7 @@ export default function({ getService }: FtrProviderContext) { }, sourceIndex: 'farequote', sourcePreview: { - column: 3, + column: 2, values: ['ASA'], }, }, diff --git a/x-pack/test/functional/services/transform_ui/wizard.ts b/x-pack/test/functional/services/transform_ui/wizard.ts index aca08f7083aa8..2d20f3617cf06 100644 --- a/x-pack/test/functional/services/transform_ui/wizard.ts +++ b/x-pack/test/functional/services/transform_ui/wizard.ts @@ -76,17 +76,17 @@ export function TransformWizardProvider({ getService }: FtrProviderContext) { await testSubjects.existOrFail(selector); }, - async parseEuiInMemoryTable(tableSubj: string) { + async parseEuiDataGrid(tableSubj: string) { const table = await testSubjects.find(`~${tableSubj}`); const $ = await table.parseDomContent(); const rows = []; // For each row, get the content of each cell and // add its values as an array to each row. - for (const tr of $.findTestSubjects(`~${tableSubj}Row`).toArray()) { + for (const tr of $.findTestSubjects(`~dataGridRow`).toArray()) { rows.push( $(tr) - .find('.euiTableCellContent') + .find('.euiDataGridRowCell__truncate') .toArray() .map(cell => $(cell) @@ -99,14 +99,14 @@ export function TransformWizardProvider({ getService }: FtrProviderContext) { return rows; }, - async assertEuiInMemoryTableColumnValues( + async assertEuiDataGridColumnValues( tableSubj: string, column: number, expectedColumnValues: string[] ) { await retry.tryForTime(2000, async () => { // get a 2D array of rows and cell values - const rows = await this.parseEuiInMemoryTable(tableSubj); + const rows = await this.parseEuiDataGrid(tableSubj); // reduce the rows data to an array of unique values in the specified column const uniqueColumnValues = rows @@ -119,7 +119,7 @@ export function TransformWizardProvider({ getService }: FtrProviderContext) { // check if the returned unique value matches the supplied filter value expect(uniqueColumnValues).to.eql( expectedColumnValues, - `Unique EuiInMemoryTable column values should be '${expectedColumnValues.join()}' (got ${uniqueColumnValues.join()})` + `Unique EuiDataGrid column values should be '${expectedColumnValues.join()}' (got ${uniqueColumnValues.join()})` ); }); }, @@ -127,28 +127,28 @@ export function TransformWizardProvider({ getService }: FtrProviderContext) { async assertSourceIndexPreview(columns: number, rows: number) { await retry.tryForTime(2000, async () => { // get a 2D array of rows and cell values - const rowsData = await this.parseEuiInMemoryTable('transformSourceIndexPreview'); + const rowsData = await this.parseEuiDataGrid('transformSourceIndexPreview'); expect(rowsData).to.length( rows, - `EuiInMemoryTable rows should be ${rows} (got ${rowsData.length})` + `EuiDataGrid rows should be ${rows} (got ${rowsData.length})` ); rowsData.map((r, i) => expect(r).to.length( columns, - `EuiInMemoryTable row #${i + 1} column count should be ${columns} (got ${r.length})` + `EuiDataGrid row #${i + 1} column count should be ${columns} (got ${r.length})` ) ); }); }, async assertSourceIndexPreviewColumnValues(column: number, values: string[]) { - await this.assertEuiInMemoryTableColumnValues('transformSourceIndexPreview', column, values); + await this.assertEuiDataGridColumnValues('transformSourceIndexPreview', column, values); }, async assertPivotPreviewColumnValues(column: number, values: string[]) { - await this.assertEuiInMemoryTableColumnValues('transformPivotPreview', column, values); + await this.assertEuiDataGridColumnValues('transformPivotPreview', column, values); }, async assertPivotPreviewLoaded() { @@ -445,21 +445,25 @@ export function TransformWizardProvider({ getService }: FtrProviderContext) { }, async assertStartButtonExists() { - await testSubjects.existOrFail('transformWizardStartButton'); - expect(await testSubjects.isDisplayed('transformWizardStartButton')).to.eql( - true, - `Expected 'Start' button to be displayed` - ); + await retry.tryForTime(5000, async () => { + await testSubjects.existOrFail('transformWizardStartButton'); + expect(await testSubjects.isDisplayed('transformWizardStartButton')).to.eql( + true, + `Expected 'Start' button to be displayed` + ); + }); }, async assertStartButtonEnabled(expectedValue: boolean) { - const isEnabled = await testSubjects.isEnabled('transformWizardStartButton'); - expect(isEnabled).to.eql( - expectedValue, - `Expected 'Start' button to be '${expectedValue ? 'enabled' : 'disabled'}' (got ${ - isEnabled ? 'enabled' : 'disabled' - }')` - ); + await retry.tryForTime(5000, async () => { + const isEnabled = await testSubjects.isEnabled('transformWizardStartButton'); + expect(isEnabled).to.eql( + expectedValue, + `Expected 'Start' button to be '${expectedValue ? 'enabled' : 'disabled'}' (got ${ + isEnabled ? 'enabled' : 'disabled' + }')` + ); + }); }, async assertManagementCardExists() { @@ -492,17 +496,21 @@ export function TransformWizardProvider({ getService }: FtrProviderContext) { async createTransform() { await testSubjects.click('transformWizardCreateButton'); - await this.assertStartButtonExists(); - await this.assertStartButtonEnabled(true); - await this.assertManagementCardExists(); - await this.assertCreateButtonEnabled(false); + await retry.tryForTime(5000, async () => { + await this.assertStartButtonExists(); + await this.assertStartButtonEnabled(true); + await this.assertManagementCardExists(); + await this.assertCreateButtonEnabled(false); + }); }, async startTransform() { await testSubjects.click('transformWizardStartButton'); - await this.assertDiscoverCardExists(); - await this.assertStartButtonEnabled(false); - await this.assertProgressbarExists(); + await retry.tryForTime(5000, async () => { + await this.assertDiscoverCardExists(); + await this.assertStartButtonEnabled(false); + await this.assertProgressbarExists(); + }); }, }; }