From 0085aaea00875e1b1827b1af891fe9981d1f7691 Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Wed, 1 Feb 2023 09:23:03 +0100 Subject: [PATCH] [ML] Transforms: Adds date picker to transform wizard for data view with time fields. (#149049) Adds a date picker to the transform wizard for data views with time fields. The time range will be applied to previews only. --- .../routes/timeseriesexplorer.test.tsx | 5 +- .../transform/common/types/date_picker.ts | 11 + .../public/app/common/data_grid.test.ts | 39 ++- .../transform/public/app/common/data_grid.ts | 8 +- .../transform/public/app/common/index.ts | 6 +- .../public/app/common/request.test.ts | 64 ++-- .../transform/public/app/common/request.ts | 76 ++++- .../public/app/hooks/use_index_data.ts | 61 +++- .../use_search_items/use_search_items.ts | 33 +- ...t.ts => use_transform_config_data.test.ts} | 2 +- ...t_data.ts => use_transform_config_data.ts} | 21 +- .../date_picker_apply_switch.tsx | 34 ++ .../date_picker_apply_switch/index.ts | 8 + .../common/get_default_step_define_state.ts | 1 + .../components/step_define/common/types.ts | 10 +- .../step_define/hooks/use_date_picker.ts | 82 +++++ .../step_define/hooks/use_search_bar.ts | 6 +- .../step_define/hooks/use_step_define_form.ts | 14 +- .../step_define/pivot_function_form.tsx | 120 +++++++ .../step_define/step_define_form.test.tsx | 26 +- .../step_define/step_define_form.tsx | 305 ++++++++++-------- .../step_define/step_define_summary.tsx | 48 ++- .../step_details/step_details_form.tsx | 8 +- .../components/wizard/storage.ts | 21 ++ .../components/wizard/wizard.tsx | 34 +- .../expanded_row_preview_pane.tsx | 14 +- x-pack/plugins/transform/tsconfig.json | 4 + x-pack/test/accessibility/apps/transform.ts | 23 ++ .../index_pattern/creation_index_pattern.ts | 23 +- .../creation_runtime_mappings.ts | 13 +- .../creation_saved_search.ts | 11 + .../apps/transform/edit_clone/cloning.ts | 11 + .../feature_controls/transform_security.ts | 10 +- .../services/transform/date_picker.ts | 49 +++ .../functional/services/transform/discover.ts | 27 +- .../functional/services/transform/index.ts | 3 + .../services/transform/navigation.ts | 4 +- .../services/transform/security_ui.ts | 8 +- .../functional/services/transform/wizard.ts | 17 +- 39 files changed, 931 insertions(+), 329 deletions(-) create mode 100644 x-pack/plugins/transform/common/types/date_picker.ts rename x-pack/plugins/transform/public/app/hooks/{use_pivot_data.test.ts => use_transform_config_data.test.ts} (95%) rename x-pack/plugins/transform/public/app/hooks/{use_pivot_data.ts => use_transform_config_data.ts} (95%) create mode 100644 x-pack/plugins/transform/public/app/sections/create_transform/components/date_picker_apply_switch/date_picker_apply_switch.tsx create mode 100644 x-pack/plugins/transform/public/app/sections/create_transform/components/date_picker_apply_switch/index.ts create mode 100644 x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/hooks/use_date_picker.ts create mode 100644 x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/pivot_function_form.tsx create mode 100644 x-pack/plugins/transform/public/app/sections/create_transform/components/wizard/storage.ts create mode 100644 x-pack/test/functional/services/transform/date_picker.ts diff --git a/x-pack/plugins/ml/public/application/routing/routes/timeseriesexplorer.test.tsx b/x-pack/plugins/ml/public/application/routing/routes/timeseriesexplorer.test.tsx index fcd413b301108..4b482e2de1ed8 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/timeseriesexplorer.test.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/timeseriesexplorer.test.tsx @@ -55,6 +55,7 @@ const getMockedTimefilter = () => { enableAutoRefreshSelector: jest.fn(), getRefreshInterval: jest.fn(), setRefreshInterval: jest.fn(), + getActiveBounds: jest.fn(), getTime: jest.fn(), isAutoRefreshSelectorEnabled: jest.fn(), isTimeRangeSelectorEnabled: jest.fn(), @@ -68,7 +69,7 @@ const getMockedTimefilter = () => { }; }; -const getMockedDatePickeDependencies = () => { +const getMockedDatePickerDependencies = () => { return { data: { query: { @@ -138,7 +139,7 @@ describe('TimeSeriesExplorerUrlStateManager', () => { render( - + diff --git a/x-pack/plugins/transform/common/types/date_picker.ts b/x-pack/plugins/transform/common/types/date_picker.ts new file mode 100644 index 0000000000000..09b5d0cba83be --- /dev/null +++ b/x-pack/plugins/transform/common/types/date_picker.ts @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export interface TimeRangeMs { + from: number; + to: number; +} diff --git a/x-pack/plugins/transform/public/app/common/data_grid.test.ts b/x-pack/plugins/transform/public/app/common/data_grid.test.ts index 31f0a2bc688d8..0cb2ed18c58f7 100644 --- a/x-pack/plugins/transform/public/app/common/data_grid.test.ts +++ b/x-pack/plugins/transform/public/app/common/data_grid.test.ts @@ -5,12 +5,13 @@ * 2.0. */ -import { getPreviewTransformRequestBody, SimpleQuery } from '.'; +import type { DataView } from '@kbn/data-views-plugin/common'; -import { getIndexDevConsoleStatement, getPivotPreviewDevConsoleStatement } from './data_grid'; +import { getPreviewTransformRequestBody, SimpleQuery } from '.'; +import { getIndexDevConsoleStatement, getTransformPreviewDevConsoleStatement } from './data_grid'; describe('Transform: Data Grid', () => { - test('getPivotPreviewDevConsoleStatement()', () => { + test('getTransformPreviewDevConsoleStatement()', () => { const query: SimpleQuery = { query_string: { query: '*', @@ -18,26 +19,30 @@ describe('Transform: Data Grid', () => { }, }; - const request = getPreviewTransformRequestBody('the-index-pattern-title', query, { - pivot: { - group_by: { - 'the-group-by-agg-name': { - terms: { - field: 'the-group-by-field', + const request = getPreviewTransformRequestBody( + { getIndexPattern: () => 'the-index-pattern-title' } as DataView, + query, + { + pivot: { + group_by: { + 'the-group-by-agg-name': { + terms: { + field: 'the-group-by-field', + }, }, }, - }, - aggregations: { - 'the-agg-agg-name': { - avg: { - field: 'the-agg-field', + aggregations: { + 'the-agg-agg-name': { + avg: { + field: 'the-agg-field', + }, }, }, }, - }, - }); + } + ); - const pivotPreviewDevConsoleStatement = getPivotPreviewDevConsoleStatement(request); + const pivotPreviewDevConsoleStatement = getTransformPreviewDevConsoleStatement(request); expect(pivotPreviewDevConsoleStatement).toBe(`POST _transform/_preview { diff --git a/x-pack/plugins/transform/public/app/common/data_grid.ts b/x-pack/plugins/transform/public/app/common/data_grid.ts index 43d2b27f13cf9..c6a740787e2b1 100644 --- a/x-pack/plugins/transform/public/app/common/data_grid.ts +++ b/x-pack/plugins/transform/public/app/common/data_grid.ts @@ -7,15 +7,17 @@ import type { PostTransformsPreviewRequestSchema } from '../../../common/api_schemas/transforms'; -import { PivotQuery } from './request'; +import { TransformConfigQuery } from './request'; export const INIT_MAX_COLUMNS = 20; -export const getPivotPreviewDevConsoleStatement = (request: PostTransformsPreviewRequestSchema) => { +export const getTransformPreviewDevConsoleStatement = ( + request: PostTransformsPreviewRequestSchema +) => { return `POST _transform/_preview\n${JSON.stringify(request, null, 2)}\n`; }; -export const getIndexDevConsoleStatement = (query: PivotQuery, dataViewTitle: string) => { +export const getIndexDevConsoleStatement = (query: TransformConfigQuery, dataViewTitle: string) => { return `GET ${dataViewTitle}/_search\n${JSON.stringify( { query, diff --git a/x-pack/plugins/transform/public/app/common/index.ts b/x-pack/plugins/transform/public/app/common/index.ts index 1f397ee4285ef..c7656974ec569 100644 --- a/x-pack/plugins/transform/public/app/common/index.ts +++ b/x-pack/plugins/transform/public/app/common/index.ts @@ -8,7 +8,7 @@ export { isAggName } from './aggregations'; export { getIndexDevConsoleStatement, - getPivotPreviewDevConsoleStatement, + getTransformPreviewDevConsoleStatement, INIT_MAX_COLUMNS, } from './data_grid'; export type { EsDoc, EsDocSource } from './fields'; @@ -64,12 +64,12 @@ export { pivotGroupByFieldSupport, PIVOT_SUPPORTED_GROUP_BY_AGGS, } from './pivot_group_by'; -export type { PivotQuery, SimpleQuery } from './request'; +export type { TransformConfigQuery, SimpleQuery } from './request'; export { defaultQuery, getPreviewTransformRequestBody, getCreateTransformRequestBody, - getPivotQuery, + getTransformConfigQuery, getRequestPayload, isDefaultQuery, isMatchAllQuery, diff --git a/x-pack/plugins/transform/public/app/common/request.test.ts b/x-pack/plugins/transform/public/app/common/request.test.ts index 2c4415c56c466..60ad397f6f30a 100644 --- a/x-pack/plugins/transform/public/app/common/request.test.ts +++ b/x-pack/plugins/transform/public/app/common/request.test.ts @@ -5,6 +5,8 @@ * 2.0. */ +import type { DataView } from '@kbn/data-views-plugin/common'; + import { PIVOT_SUPPORTED_AGGS } from '../../../common/types/pivot_aggs'; import { PivotGroupByConfig } from '.'; @@ -19,19 +21,19 @@ import { getPreviewTransformRequestBody, getCreateTransformRequestBody, getCreateTransformSettingsRequestBody, - getPivotQuery, + getTransformConfigQuery, getMissingBucketConfig, getRequestPayload, isDefaultQuery, isMatchAllQuery, isSimpleQuery, matchAllQuery, - PivotQuery, + type TransformConfigQuery, } from './request'; import { LatestFunctionConfigUI } from '../../../common/types/transform'; import type { RuntimeField } from '@kbn/data-views-plugin/common'; -const simpleQuery: PivotQuery = { query_string: { query: 'airline:AAL' } }; +const simpleQuery: TransformConfigQuery = { query_string: { query: 'airline:AAL' } }; const groupByTerms: PivotGroupByConfig = { agg: PIVOT_SUPPORTED_GROUP_BY_AGGS.TERMS, @@ -62,12 +64,12 @@ describe('Transform: Common', () => { test('isDefaultQuery()', () => { expect(isDefaultQuery(defaultQuery)).toBe(true); - expect(isDefaultQuery(matchAllQuery)).toBe(false); + expect(isDefaultQuery(matchAllQuery)).toBe(true); expect(isDefaultQuery(simpleQuery)).toBe(false); }); - test('getPivotQuery()', () => { - const query = getPivotQuery('the-query'); + test('getTransformConfigQuery()', () => { + const query = getTransformConfigQuery('the-query'); expect(query).toEqual({ query_string: { @@ -78,14 +80,18 @@ describe('Transform: Common', () => { }); test('getPreviewTransformRequestBody()', () => { - const query = getPivotQuery('the-query'); + const query = getTransformConfigQuery('the-query'); - const request = getPreviewTransformRequestBody('the-data-view-title', query, { - pivot: { - aggregations: { 'the-agg-agg-name': { avg: { field: 'the-agg-field' } } }, - group_by: { 'the-group-by-agg-name': { terms: { field: 'the-group-by-field' } } }, - }, - }); + const request = getPreviewTransformRequestBody( + { getIndexPattern: () => 'the-data-view-title' } as DataView, + query, + { + pivot: { + aggregations: { 'the-agg-agg-name': { avg: { field: 'the-agg-field' } } }, + group_by: { 'the-group-by-agg-name': { terms: { field: 'the-group-by-field' } } }, + }, + } + ); expect(request).toEqual({ pivot: { @@ -100,13 +106,17 @@ describe('Transform: Common', () => { }); test('getPreviewTransformRequestBody() with comma-separated index pattern', () => { - const query = getPivotQuery('the-query'); - const request = getPreviewTransformRequestBody('the-data-view-title,the-other-title', query, { - pivot: { - aggregations: { 'the-agg-agg-name': { avg: { field: 'the-agg-field' } } }, - group_by: { 'the-group-by-agg-name': { terms: { field: 'the-group-by-field' } } }, - }, - }); + const query = getTransformConfigQuery('the-query'); + const request = getPreviewTransformRequestBody( + { getIndexPattern: () => 'the-data-view-title,the-other-title' } as DataView, + query, + { + pivot: { + aggregations: { 'the-agg-agg-name': { avg: { field: 'the-agg-field' } } }, + group_by: { 'the-group-by-agg-name': { terms: { field: 'the-group-by-field' } } }, + }, + } + ); expect(request).toEqual({ pivot: { @@ -172,9 +182,9 @@ describe('Transform: Common', () => { }); test('getPreviewTransformRequestBody() with missing_buckets config', () => { - const query = getPivotQuery('the-query'); + const query = getTransformConfigQuery('the-query'); const request = getPreviewTransformRequestBody( - 'the-data-view-title', + { getIndexPattern: () => 'the-data-view-title' } as DataView, query, getRequestPayload([aggsAvg], [{ ...groupByTerms, ...{ missing_bucket: true } }]) ); @@ -194,11 +204,12 @@ describe('Transform: Common', () => { }); test('getCreateTransformRequestBody() skips default values', () => { - const pivotState: StepDefineExposedState = { + const transformConfigState: StepDefineExposedState = { aggList: { 'the-agg-name': aggsAvg }, groupByList: { 'the-group-by-name': groupByTerms }, isAdvancedPivotEditorEnabled: false, isAdvancedSourceEditorEnabled: false, + isDatePickerApplyEnabled: false, sourceConfigUpdated: false, searchLanguage: 'kuery', searchString: 'the-query', @@ -239,8 +250,8 @@ describe('Transform: Common', () => { }; const request = getCreateTransformRequestBody( - 'the-data-view-title', - pivotState, + { getIndexPattern: () => 'the-data-view-title' } as DataView, + transformConfigState, transformDetailsState ); @@ -278,6 +289,7 @@ describe('Transform: Common', () => { groupByList: { 'the-group-by-name': groupByTerms }, isAdvancedPivotEditorEnabled: false, isAdvancedSourceEditorEnabled: false, + isDatePickerApplyEnabled: false, sourceConfigUpdated: false, searchLanguage: 'kuery', searchString: 'the-query', @@ -319,7 +331,7 @@ describe('Transform: Common', () => { }; const request = getCreateTransformRequestBody( - 'the-data-view-title', + { getIndexPattern: () => 'the-data-view-title' } as DataView, pivotState, transformDetailsState ); diff --git a/x-pack/plugins/transform/public/app/common/request.ts b/x-pack/plugins/transform/public/app/common/request.ts index c66618df209f7..42162498f3f3c 100644 --- a/x-pack/plugins/transform/public/app/common/request.ts +++ b/x-pack/plugins/transform/public/app/common/request.ts @@ -8,6 +8,7 @@ import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import type { DataView } from '@kbn/data-views-plugin/public'; import { isPopulatedObject } from '@kbn/ml-is-populated-object'; +import { buildBaseFilterCriteria } from '@kbn/ml-query-utils'; import { DEFAULT_CONTINUOUS_MODE_DELAY, @@ -47,9 +48,15 @@ export interface SimpleQuery { }; } -export type PivotQuery = SimpleQuery | SavedSearchQuery; +export interface FilterBasedSimpleQuery { + bool: { + filter: [SimpleQuery]; + }; +} + +export type TransformConfigQuery = FilterBasedSimpleQuery | SimpleQuery | SavedSearchQuery; -export function getPivotQuery(search: string | SavedSearchQuery): PivotQuery { +export function getTransformConfigQuery(search: string | SavedSearchQuery): TransformConfigQuery { if (typeof search === 'string') { return { query_string: { @@ -66,6 +73,16 @@ export function isSimpleQuery(arg: unknown): arg is SimpleQuery { return isPopulatedObject(arg, ['query_string']); } +export function isFilterBasedSimpleQuery(arg: unknown): arg is FilterBasedSimpleQuery { + return ( + isPopulatedObject(arg, ['bool']) && + isPopulatedObject(arg.bool, ['filter']) && + Array.isArray(arg.bool.filter) && + arg.bool.filter.length === 1 && + isSimpleQuery(arg.bool.filter[0]) + ); +} + export const matchAllQuery = { match_all: {} }; export function isMatchAllQuery(query: unknown): boolean { return ( @@ -76,9 +93,14 @@ export function isMatchAllQuery(query: unknown): boolean { ); } -export const defaultQuery: PivotQuery = { query_string: { query: '*' } }; -export function isDefaultQuery(query: PivotQuery): boolean { - return isSimpleQuery(query) && query.query_string.query === '*'; +export const defaultQuery: TransformConfigQuery = { query_string: { query: '*' } }; +export function isDefaultQuery(query: TransformConfigQuery): boolean { + return ( + isMatchAllQuery(query) || + (isSimpleQuery(query) && query.query_string.query === '*') || + (isFilterBasedSimpleQuery(query) && + (query.bool.filter[0].query_string.query === '*' || isMatchAllQuery(query.bool.filter[0]))) + ); } export function getCombinedRuntimeMappings( @@ -171,17 +193,36 @@ export const getRequestPayload = ( }; export function getPreviewTransformRequestBody( - dataViewTitle: DataView['title'], - query: PivotQuery, - partialRequest?: StepDefineExposedState['previewRequest'] | undefined, - runtimeMappings?: StepDefineExposedState['runtimeMappings'] + dataView: DataView, + transformConfigQuery: TransformConfigQuery, + partialRequest?: StepDefineExposedState['previewRequest'], + runtimeMappings?: StepDefineExposedState['runtimeMappings'], + timeRangeMs?: StepDefineExposedState['timeRangeMs'] ): PostTransformsPreviewRequestSchema { + const dataViewTitle = dataView.getIndexPattern(); const index = dataViewTitle.split(',').map((name: string) => name.trim()); + const hasValidTimeField = dataView.timeFieldName !== undefined && dataView.timeFieldName !== ''; + + const baseFilterCriteria = buildBaseFilterCriteria( + dataView.timeFieldName, + timeRangeMs?.from, + timeRangeMs?.to, + isDefaultQuery(transformConfigQuery) ? undefined : transformConfigQuery + ); + + const queryWithBaseFilterCriteria = { + bool: { + filter: baseFilterCriteria, + }, + }; + + const query = hasValidTimeField ? queryWithBaseFilterCriteria : transformConfigQuery; + return { source: { index, - ...(!isDefaultQuery(query) && !isMatchAllQuery(query) ? { query } : {}), + ...(isDefaultQuery(query) ? {} : { query }), ...(isPopulatedObject(runtimeMappings) ? { runtime_mappings: runtimeMappings } : {}), }, ...(partialRequest ?? {}), @@ -212,15 +253,18 @@ export const getCreateTransformSettingsRequestBody = ( }; export const getCreateTransformRequestBody = ( - dataViewTitle: DataView['title'], - pivotState: StepDefineExposedState, + dataView: DataView, + transformConfigState: StepDefineExposedState, transformDetailsState: StepDetailsExposedState ): PutTransformsPivotRequestSchema | PutTransformsLatestRequestSchema => ({ ...getPreviewTransformRequestBody( - dataViewTitle, - getPivotQuery(pivotState.searchQuery), - pivotState.previewRequest, - pivotState.runtimeMappings + dataView, + getTransformConfigQuery(transformConfigState.searchQuery), + transformConfigState.previewRequest, + transformConfigState.runtimeMappings, + transformConfigState.isDatePickerApplyEnabled && transformConfigState.timeRangeMs + ? transformConfigState.timeRangeMs + : undefined ), // conditionally add optional description ...(transformDetailsState.transformDescription !== '' diff --git a/x-pack/plugins/transform/public/app/hooks/use_index_data.ts b/x-pack/plugins/transform/public/app/hooks/use_index_data.ts index 22c314c89850a..d4fbf1c77d054 100644 --- a/x-pack/plugins/transform/public/app/hooks/use_index_data.ts +++ b/x-pack/plugins/transform/public/app/hooks/use_index_data.ts @@ -10,6 +10,9 @@ import { useEffect, useMemo, useState } from 'react'; import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import type { EuiDataGridColumn } from '@elastic/eui'; +import { buildBaseFilterCriteria } from '@kbn/ml-query-utils'; + +import type { TimeRangeMs } from '../../../common/types/date_picker'; import { isEsSearchResponse, isFieldHistogramsResponseSchema, @@ -19,21 +22,23 @@ import { isKeywordDuplicate, removeKeywordPostfix, } from '../../../common/utils/field_utils'; -import type { EsSorting, UseIndexDataReturnType } from '../../shared_imports'; - import { getErrorMessage } from '../../../common/utils/errors'; -import { isDefaultQuery, matchAllQuery, PivotQuery } from '../common'; -import { SearchItems } from './use_search_items'; -import { useApi } from './use_api'; +import { isRuntimeMappings } from '../../../common/shared_imports'; + +import type { EsSorting, UseIndexDataReturnType } from '../../shared_imports'; +import { isDefaultQuery, matchAllQuery, TransformConfigQuery } from '../common'; import { useAppDependencies, useToastNotifications } from '../app_dependencies'; import type { StepDefineExposedState } from '../sections/create_transform/components/step_define/common'; -import { isRuntimeMappings } from '../../../common/shared_imports'; + +import { SearchItems } from './use_search_items'; +import { useApi } from './use_api'; export const useIndexData = ( dataView: SearchItems['dataView'], - query: PivotQuery, - combinedRuntimeMappings?: StepDefineExposedState['runtimeMappings'] + query: TransformConfigQuery, + combinedRuntimeMappings?: StepDefineExposedState['runtimeMappings'], + timeRangeMs?: TimeRangeMs ): UseIndexDataReturnType => { const indexPattern = useMemo(() => dataView.getIndexPattern(), [dataView]); @@ -55,6 +60,24 @@ export const useIndexData = ( const [dataViewFields, setDataViewFields] = useState(); + const baseFilterCriteria = buildBaseFilterCriteria( + dataView.timeFieldName, + timeRangeMs?.from, + timeRangeMs?.to, + query + ); + + const defaultQuery = useMemo( + () => (timeRangeMs && dataView.timeFieldName ? baseFilterCriteria[0] : matchAllQuery), + [baseFilterCriteria, dataView, timeRangeMs] + ); + + const queryWithBaseFilterCriteria = { + bool: { + filter: baseFilterCriteria, + }, + }; + // Fetch 500 random documents to determine populated fields. // This is a workaround to avoid passing potentially thousands of unpopulated fields // (for example, as part of filebeat/metricbeat/ECS based indices) @@ -70,7 +93,7 @@ export const useIndexData = ( _source: false, query: { function_score: { - query: { match_all: {} }, + query: defaultQuery, random_score: {}, }, }, @@ -106,7 +129,7 @@ export const useIndexData = ( useEffect(() => { fetchDataGridSampleDocuments(); // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); + }, [timeRangeMs]); const columns: EuiDataGridColumn[] = useMemo(() => { if (typeof dataViewFields === 'undefined') { @@ -165,7 +188,7 @@ export const useIndexData = ( resetPagination(); // custom comparison // eslint-disable-next-line react-hooks/exhaustive-deps - }, [JSON.stringify(query)]); + }, [JSON.stringify([query, timeRangeMs])]); const fetchDataGridData = async function () { setErrorMessage(''); @@ -181,8 +204,7 @@ export const useIndexData = ( body: { fields: ['*'], _source: false, - // Instead of using the default query (`*`), fall back to a more efficient `match_all` query. - query: isDefaultQuery(query) ? matchAllQuery : query, + query: isDefaultQuery(query) ? defaultQuery : queryWithBaseFilterCriteria, from: pagination.pageIndex * pagination.pageSize, size: pagination.pageSize, ...(Object.keys(sort).length > 0 ? { sort } : {}), @@ -236,7 +258,7 @@ export const useIndexData = ( type: getFieldType(cT.schema), }; }), - isDefaultQuery(query) ? matchAllQuery : query, + isDefaultQuery(query) ? defaultQuery : queryWithBaseFilterCriteria, combinedRuntimeMappings ); @@ -263,7 +285,14 @@ export const useIndexData = ( }, [ indexPattern, // eslint-disable-next-line react-hooks/exhaustive-deps - JSON.stringify([query, pagination, sortingColumns, dataViewFields, combinedRuntimeMappings]), + JSON.stringify([ + query, + pagination, + sortingColumns, + dataViewFields, + combinedRuntimeMappings, + timeRangeMs, + ]), ]); useEffect(() => { @@ -276,7 +305,7 @@ export const useIndexData = ( chartsVisible, indexPattern, // eslint-disable-next-line react-hooks/exhaustive-deps - JSON.stringify([query, dataGrid.visibleColumns, combinedRuntimeMappings]), + JSON.stringify([query, dataGrid.visibleColumns, combinedRuntimeMappings, timeRangeMs]), ]); const renderCellValue = useRenderCellValue(dataView, pagination, tableItems); diff --git a/x-pack/plugins/transform/public/app/hooks/use_search_items/use_search_items.ts b/x-pack/plugins/transform/public/app/hooks/use_search_items/use_search_items.ts index cd24d092f754c..51e6fcaf469e7 100644 --- a/x-pack/plugins/transform/public/app/hooks/use_search_items/use_search_items.ts +++ b/x-pack/plugins/transform/public/app/hooks/use_search_items/use_search_items.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { useEffect, useState } from 'react'; +import { useEffect, useRef, useState } from 'react'; import { i18n } from '@kbn/i18n'; @@ -27,6 +27,13 @@ export const useSearchItems = (defaultSavedObjectId: string | undefined) => { const [searchItems, setSearchItems] = useState(undefined); + const isMounted = useRef(true); + useEffect(() => { + return () => { + isMounted.current = false; + }; + }, []); + async function fetchSavedObject(id: string) { let fetchedDataView; let fetchedSavedSearch; @@ -44,7 +51,7 @@ export const useSearchItems = (defaultSavedObjectId: string | undefined) => { spaces: appDeps.spaces, }); - if (fetchedSavedSearch?.sharingSavedObjectProps?.errorJSON) { + if (isMounted.current && fetchedSavedSearch?.sharingSavedObjectProps?.errorJSON) { setError(await getSavedSearchUrlConflictMessage(fetchedSavedSearch)); return; } @@ -52,17 +59,19 @@ export const useSearchItems = (defaultSavedObjectId: string | undefined) => { // Just let fetchedSavedSearch stay undefined in case it doesn't exist. } - if (!isDataView(fetchedDataView) && fetchedSavedSearch === undefined) { - setError( - i18n.translate('xpack.transform.searchItems.errorInitializationTitle', { - defaultMessage: `An error occurred initializing the Kibana data view or saved search.`, - }) - ); - return; - } + if (isMounted.current) { + if (!isDataView(fetchedDataView) && fetchedSavedSearch === undefined) { + setError( + i18n.translate('xpack.transform.searchItems.errorInitializationTitle', { + defaultMessage: `An error occurred initializing the Kibana data view or saved search.`, + }) + ); + return; + } - setSearchItems(createSearchItems(fetchedDataView, fetchedSavedSearch, uiSettings)); - setError(undefined); + setSearchItems(createSearchItems(fetchedDataView, fetchedSavedSearch, uiSettings)); + setError(undefined); + } } useEffect(() => { diff --git a/x-pack/plugins/transform/public/app/hooks/use_pivot_data.test.ts b/x-pack/plugins/transform/public/app/hooks/use_transform_config_data.test.ts similarity index 95% rename from x-pack/plugins/transform/public/app/hooks/use_pivot_data.test.ts rename to x-pack/plugins/transform/public/app/hooks/use_transform_config_data.test.ts index 6c354c1ed953e..1ee68bdcb48fa 100644 --- a/x-pack/plugins/transform/public/app/hooks/use_pivot_data.test.ts +++ b/x-pack/plugins/transform/public/app/hooks/use_transform_config_data.test.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { getCombinedProperties } from './use_pivot_data'; +import { getCombinedProperties } from './use_transform_config_data'; import { ES_FIELD_TYPES } from '@kbn/field-types'; describe('getCombinedProperties', () => { diff --git a/x-pack/plugins/transform/public/app/hooks/use_pivot_data.ts b/x-pack/plugins/transform/public/app/hooks/use_transform_config_data.ts similarity index 95% rename from x-pack/plugins/transform/public/app/hooks/use_pivot_data.ts rename to x-pack/plugins/transform/public/app/hooks/use_transform_config_data.ts index 79976eb6d6355..1baef3e7fe8d3 100644 --- a/x-pack/plugins/transform/public/app/hooks/use_pivot_data.ts +++ b/x-pack/plugins/transform/public/app/hooks/use_transform_config_data.ts @@ -27,7 +27,7 @@ import { import { getErrorMessage } from '../../../common/utils/errors'; import { useAppDependencies } from '../app_dependencies'; -import { getPreviewTransformRequestBody, PivotQuery } from '../common'; +import { getPreviewTransformRequestBody, type TransformConfigQuery } from '../common'; import { SearchItems } from './use_search_items'; import { useApi } from './use_api'; @@ -95,12 +95,13 @@ export function getCombinedProperties( }; } -export const usePivotData = ( - dataViewTitle: SearchItems['dataView']['title'], - query: PivotQuery, +export const useTransformConfigData = ( + dataView: SearchItems['dataView'], + query: TransformConfigQuery, validationStatus: StepDefineExposedState['validationStatus'], requestPayload: StepDefineExposedState['previewRequest'], - combinedRuntimeMappings?: StepDefineExposedState['runtimeMappings'] + combinedRuntimeMappings?: StepDefineExposedState['runtimeMappings'], + timeRangeMs?: StepDefineExposedState['timeRangeMs'] ): UseIndexDataReturnType => { const [previewMappingsProperties, setPreviewMappingsProperties] = useState({}); @@ -166,10 +167,11 @@ export const usePivotData = ( setStatus(INDEX_STATUS.LOADING); const previewRequest = getPreviewTransformRequestBody( - dataViewTitle, + dataView, query, requestPayload, - combinedRuntimeMappings + combinedRuntimeMappings, + timeRangeMs ); const resp = await api.getTransformsPreview(previewRequest); @@ -238,7 +240,10 @@ export const usePivotData = ( getPreviewData(); // custom comparison /* eslint-disable react-hooks/exhaustive-deps */ - }, [dataViewTitle, JSON.stringify([requestPayload, query, combinedRuntimeMappings])]); + }, [ + dataView.getIndexPattern(), + JSON.stringify([requestPayload, query, combinedRuntimeMappings, timeRangeMs]), + ]); if (sortingColumns.length > 0) { const sortingColumnsWithTypes = sortingColumns.map((c) => ({ diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/date_picker_apply_switch/date_picker_apply_switch.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/date_picker_apply_switch/date_picker_apply_switch.tsx new file mode 100644 index 0000000000000..a57d83b75aa10 --- /dev/null +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/date_picker_apply_switch/date_picker_apply_switch.tsx @@ -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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { FC } from 'react'; + +import { EuiSwitch } from '@elastic/eui'; + +import { i18n } from '@kbn/i18n'; + +import { StepDefineFormHook } from '../step_define'; + +export const DatePickerApplySwitch: FC = ({ + datePicker: { + actions: { setDatePickerApplyEnabled }, + state: { isDatePickerApplyEnabled }, + }, +}) => { + return ( + { + setDatePickerApplyEnabled(!isDatePickerApplyEnabled); + }} + data-test-subj="transformDatePickerApplySwitch" + /> + ); +}; diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/date_picker_apply_switch/index.ts b/x-pack/plugins/transform/public/app/sections/create_transform/components/date_picker_apply_switch/index.ts new file mode 100644 index 0000000000000..cc1760017caf8 --- /dev/null +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/date_picker_apply_switch/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { DatePickerApplySwitch } from './date_picker_apply_switch'; diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/get_default_step_define_state.ts b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/get_default_step_define_state.ts index c75da651f79d0..7e6d336d57a4b 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/get_default_step_define_state.ts +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/get_default_step_define_state.ts @@ -21,6 +21,7 @@ export function getDefaultStepDefineState(searchItems: SearchItems): StepDefineE groupByList: {} as PivotGroupByConfigDict, isAdvancedPivotEditorEnabled: false, isAdvancedSourceEditorEnabled: false, + isDatePickerApplyEnabled: false, searchLanguage: QUERY_LANGUAGE_KUERY, searchString: undefined, searchQuery: searchItems.savedSearch !== undefined ? searchItems.combinedQuery : defaultSearch, diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/types.ts b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/types.ts index c8dc63cae1f9a..2e23cc0e9047f 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/types.ts +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/types.ts @@ -24,8 +24,8 @@ import { PivotConfigDefinition, } from '../../../../../../../common/types/transform'; import { LatestFunctionConfig } from '../../../../../../../common/api_schemas/transforms'; - import { RUNTIME_FIELD_TYPES } from '../../../../../../../common/shared_imports'; +import type { TimeRangeMs } from '../../../../../../../common/types/date_picker'; export interface ErrorMessage { query: string; @@ -62,13 +62,15 @@ export interface StepDefineExposedState { sourceConfigUpdated: boolean; valid: boolean; validationStatus: { isValid: boolean; errorMessage?: string }; + runtimeMappings?: RuntimeMappings; + runtimeMappingsUpdated: boolean; + isRuntimeMappingsEditorEnabled: boolean; + timeRangeMs?: TimeRangeMs; + isDatePickerApplyEnabled: boolean; /** * Undefined when the form is incomplete or invalid */ previewRequest: { latest: LatestFunctionConfig } | { pivot: PivotConfigDefinition } | undefined; - runtimeMappings?: RuntimeMappings; - runtimeMappingsUpdated: boolean; - isRuntimeMappingsEditorEnabled: boolean; } export function isPivotPartialRequest(arg: unknown): arg is { pivot: PivotConfigDefinition } { diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/hooks/use_date_picker.ts b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/hooks/use_date_picker.ts new file mode 100644 index 0000000000000..64a825e424220 --- /dev/null +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/hooks/use_date_picker.ts @@ -0,0 +1,82 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useEffect, useMemo, useState } from 'react'; +import { merge } from 'rxjs'; + +import type { TimeRange } from '@kbn/es-query'; +import { mlTimefilterRefresh$, useTimefilter } from '@kbn/ml-date-picker'; + +import type { TimeRangeMs } from '../../../../../../../common/types/date_picker'; + +import { StepDefineExposedState } from '../common'; +import { StepDefineFormProps } from '../step_define_form'; + +export const useDatePicker = ( + defaults: StepDefineExposedState, + dataView: StepDefineFormProps['searchItems']['dataView'] +) => { + const hasValidTimeField = useMemo( + () => dataView.timeFieldName !== undefined && dataView.timeFieldName !== '', + [dataView.timeFieldName] + ); + + const timefilter = useTimefilter({ + timeRangeSelector: hasValidTimeField, + autoRefreshSelector: false, + }); + + // The internal state of the date picker apply button. + const [isDatePickerApplyEnabled, setDatePickerApplyEnabled] = useState( + defaults.isDatePickerApplyEnabled + ); + + // The time range selected via the date picker + const [timeRange, setTimeRange] = useState(); + + // Set up subscriptions to date picker updates + useEffect(() => { + const updateTimeRange = () => setTimeRange(timefilter.getTime()); + + const timefilterUpdateSubscription = merge( + timefilter.getAutoRefreshFetch$(), + timefilter.getTimeUpdate$(), + mlTimefilterRefresh$ + ).subscribe(updateTimeRange); + + const timefilterEnabledSubscription = timefilter + .getEnabledUpdated$() + .subscribe(updateTimeRange); + + return () => { + timefilterUpdateSubscription.unsubscribe(); + timefilterEnabledSubscription.unsubscribe(); + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + // Derive ms timestamps from timeRange updates. + const timeRangeMs: TimeRangeMs | undefined = useMemo(() => { + const timefilterActiveBounds = timefilter.getActiveBounds(); + if ( + timefilterActiveBounds !== undefined && + timefilterActiveBounds.min !== undefined && + timefilterActiveBounds.max !== undefined + ) { + return { + from: timefilterActiveBounds.min.valueOf(), + to: timefilterActiveBounds.max.valueOf(), + }; + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [timeRange]); + + return { + actions: { setDatePickerApplyEnabled }, + state: { isDatePickerApplyEnabled, hasValidTimeField, timeRange, timeRangeMs }, + }; +}; diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/hooks/use_search_bar.ts b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/hooks/use_search_bar.ts index 4ea56b557c7ee..e8d56fc002981 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/hooks/use_search_bar.ts +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/hooks/use_search_bar.ts @@ -10,7 +10,7 @@ import { useState } from 'react'; import { toElasticsearchQuery, fromKueryExpression, luceneStringToDsl } from '@kbn/es-query'; import type { Query } from '@kbn/es-query'; -import { getPivotQuery } from '../../../../../common'; +import { getTransformConfigQuery } from '../../../../../common'; import { ErrorMessage, @@ -65,7 +65,7 @@ export const useSearchBar = ( } }; - const pivotQuery = getPivotQuery(searchQuery); + const transformConfigQuery = getTransformConfigQuery(searchQuery); return { actions: { @@ -79,7 +79,7 @@ export const useSearchBar = ( }, state: { errorMessage, - pivotQuery, + transformConfigQuery, searchInput, searchLanguage, searchQuery, diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/hooks/use_step_define_form.ts b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/hooks/use_step_define_form.ts index 1cd0a154707b3..849883e6c3041 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/hooks/use_step_define_form.ts +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/hooks/use_step_define_form.ts @@ -15,6 +15,7 @@ import { StepDefineFormProps } from '../step_define_form'; import { useAdvancedPivotEditor } from './use_advanced_pivot_editor'; import { useAdvancedSourceEditor } from './use_advanced_source_editor'; +import { useDatePicker } from './use_date_picker'; import { usePivotConfig } from './use_pivot_config'; import { useSearchBar } from './use_search_bar'; import { useLatestFunctionConfig } from './use_latest_function_config'; @@ -29,6 +30,7 @@ export const useStepDefineForm = ({ overrides, onChange, searchItems }: StepDefi const [transformFunction, setTransformFunction] = useState(defaults.transformFunction); + const datePicker = useDatePicker(defaults, dataView); const searchBar = useSearchBar(defaults, dataView); const pivotConfig = usePivotConfig(defaults, dataView); @@ -39,8 +41,8 @@ export const useStepDefineForm = ({ overrides, onChange, searchItems }: StepDefi ); const previewRequest = getPreviewTransformRequestBody( - dataView.getIndexPattern(), - searchBar.state.pivotQuery, + dataView, + searchBar.state.transformConfigQuery, pivotConfig.state.requestPayload, defaults?.runtimeMappings ); @@ -58,8 +60,8 @@ export const useStepDefineForm = ({ overrides, onChange, searchItems }: StepDefi const runtimeMappings = runtimeMappingsEditor.state.runtimeMappings; if (!advancedSourceEditor.state.isAdvancedSourceEditorEnabled) { const previewRequestUpdate = getPreviewTransformRequestBody( - dataView.getIndexPattern(), - searchBar.state.pivotQuery, + dataView, + searchBar.state.transformConfigQuery, pivotConfig.state.requestPayload, runtimeMappings ); @@ -79,6 +81,7 @@ export const useStepDefineForm = ({ overrides, onChange, searchItems }: StepDefi groupByList: pivotConfig.state.groupByList, isAdvancedPivotEditorEnabled: advancedPivotEditor.state.isAdvancedPivotEditorEnabled, isAdvancedSourceEditorEnabled: advancedSourceEditor.state.isAdvancedSourceEditorEnabled, + isDatePickerApplyEnabled: datePicker.state.isDatePickerApplyEnabled, searchLanguage: searchBar.state.searchLanguage, searchString: searchBar.state.searchString, searchQuery: searchBar.state.searchQuery, @@ -98,12 +101,14 @@ export const useStepDefineForm = ({ overrides, onChange, searchItems }: StepDefi runtimeMappings, runtimeMappingsUpdated: runtimeMappingsEditor.state.runtimeMappingsUpdated, isRuntimeMappingsEditorEnabled: runtimeMappingsEditor.state.isRuntimeMappingsEditorEnabled, + timeRangeMs: datePicker.state.timeRangeMs, }); // custom comparison /* eslint-disable react-hooks/exhaustive-deps */ }, [ JSON.stringify(advancedPivotEditor.state), JSON.stringify(advancedSourceEditor.state), + JSON.stringify(datePicker.state), pivotConfig.state, JSON.stringify(searchBar.state), JSON.stringify([ @@ -121,6 +126,7 @@ export const useStepDefineForm = ({ overrides, onChange, searchItems }: StepDefi advancedPivotEditor, advancedSourceEditor, runtimeMappingsEditor, + datePicker, pivotConfig, latestFunctionConfig, searchBar, diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/pivot_function_form.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/pivot_function_form.tsx new file mode 100644 index 0000000000000..efa28de596a18 --- /dev/null +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/pivot_function_form.tsx @@ -0,0 +1,120 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { FC } from 'react'; + +import { + EuiButton, + EuiButtonIcon, + EuiCopy, + EuiFlexGroup, + EuiFlexItem, + EuiFormRow, + EuiLink, + EuiSpacer, + EuiText, +} from '@elastic/eui'; + +import { i18n } from '@kbn/i18n'; + +import { useDocumentationLinks } from '../../../../hooks/use_documentation_links'; + +import { AdvancedPivotEditor } from '../advanced_pivot_editor'; +import { AdvancedPivotEditorSwitch } from '../advanced_pivot_editor_switch'; +import { PivotConfiguration } from '../pivot_configuration'; + +import type { StepDefineFormHook } from './hooks/use_step_define_form'; + +const advancedEditorsSidebarWidth = '220px'; + +interface PivotFunctionFormProps { + applyPivotChangesHandler: () => void; + copyToClipboardPivot: string; + copyToClipboardPivotDescription: string; + stepDefineForm: StepDefineFormHook; +} + +export const PivotFunctionForm: FC = ({ + applyPivotChangesHandler, + copyToClipboardPivot, + copyToClipboardPivotDescription, + stepDefineForm, +}) => { + const { esTransformPivot } = useDocumentationLinks(); + + const { isAdvancedPivotEditorEnabled, isAdvancedPivotEditorApplyButtonEnabled } = + stepDefineForm.advancedPivotEditor.state; + + return ( + + {/* Flex Column #1: Pivot Config Form / Advanced Pivot Config Editor */} + + {!isAdvancedPivotEditorEnabled && } + {isAdvancedPivotEditorEnabled && ( + + )} + + + + + + + + + + + + {(copy: () => void) => ( + + )} + + + + + + {isAdvancedPivotEditorEnabled && ( + + + + <> + {i18n.translate('xpack.transform.stepDefineForm.advancedEditorHelpText', { + defaultMessage: + 'The advanced editor allows you to edit the pivot configuration of the transform.', + })}{' '} + + {i18n.translate('xpack.transform.stepDefineForm.advancedEditorHelpTextLink', { + defaultMessage: 'Learn more about available options.', + })} + + + + + + {i18n.translate('xpack.transform.stepDefineForm.advancedEditorApplyButtonText', { + defaultMessage: 'Apply changes', + })} + + + )} + + + + ); +}; diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_form.test.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_form.test.tsx index 682d1bde11c32..beb4020378409 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_form.test.tsx +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_form.test.tsx @@ -9,12 +9,11 @@ import React from 'react'; import { render, waitFor } from '@testing-library/react'; import { I18nProvider } from '@kbn/i18n-react'; - +import { DatePickerContextProvider, type DatePickerDependencies } from '@kbn/ml-date-picker'; import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; - import { coreMock } from '@kbn/core/public/mocks'; import { dataPluginMock } from '@kbn/data-plugin/public/mocks'; -const startMock = coreMock.createStart(); +import { timefilterServiceMock } from '@kbn/data-plugin/public/query/timefilter/timefilter_service.mock'; import { PIVOT_SUPPORTED_AGGS } from '../../../../../../common/types/pivot_aggs'; @@ -28,11 +27,24 @@ import { SearchItems } from '../../../../hooks/use_search_items'; import { getAggNameConflictToastMessages } from './common'; import { StepDefineForm } from './step_define_form'; +import { MlSharedContext } from '../../../../__mocks__/shared_context'; +import { getMlSharedImports } from '../../../../../shared_imports'; + jest.mock('../../../../../shared_imports'); jest.mock('../../../../app_dependencies'); -import { MlSharedContext } from '../../../../__mocks__/shared_context'; -import { getMlSharedImports } from '../../../../../shared_imports'; +const startMock = coreMock.createStart(); + +const getMockedDatePickerDependencies = () => { + return { + data: { + query: { + timefilter: timefilterServiceMock.createStartContract(), + }, + }, + notifications: {}, + } as unknown as DatePickerDependencies; +}; const createMockWebStorage = () => ({ clear: jest.fn(), @@ -75,7 +87,9 @@ describe('Transform: ', () => { - + + + diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_form.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_form.tsx index f86d693e605e2..c615e553b8984 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_form.tsx +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_form.tsx @@ -5,9 +5,8 @@ * 2.0. */ -import React, { useMemo, FC } from 'react'; - -import { i18n } from '@kbn/i18n'; +import React, { useEffect, useMemo, FC } from 'react'; +import { merge } from 'rxjs'; import { EuiButton, @@ -17,20 +16,25 @@ import { EuiFlexItem, EuiForm, EuiFormRow, - EuiHorizontalRule, + EuiIconTip, EuiLink, EuiSpacer, EuiText, + EuiTitle, } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { mlTimefilterRefresh$, useTimefilter, DatePickerWrapper } from '@kbn/ml-date-picker'; +import { useUrlState } from '@kbn/ml-url-state'; + import { PivotAggDict } from '../../../../../../common/types/pivot_aggs'; import { PivotGroupByDict } from '../../../../../../common/types/pivot_group_by'; +import { TRANSFORM_FUNCTION } from '../../../../../../common/constants'; import { getIndexDevConsoleStatement, - getPivotPreviewDevConsoleStatement, + getTransformPreviewDevConsoleStatement, } from '../../../../common/data_grid'; - import { getPreviewTransformRequestBody, PivotAggsConfigDict, @@ -40,24 +44,36 @@ import { } from '../../../../common'; import { useDocumentationLinks } from '../../../../hooks/use_documentation_links'; import { useIndexData } from '../../../../hooks/use_index_data'; -import { usePivotData } from '../../../../hooks/use_pivot_data'; +import { useTransformConfigData } from '../../../../hooks/use_transform_config_data'; import { useAppDependencies, useToastNotifications } from '../../../../app_dependencies'; import { SearchItems } from '../../../../hooks/use_search_items'; +import { getAggConfigFromEsAgg } from '../../../../common/pivot_aggs'; -import { AdvancedPivotEditor } from '../advanced_pivot_editor'; -import { AdvancedPivotEditorSwitch } from '../advanced_pivot_editor_switch'; import { AdvancedQueryEditorSwitch } from '../advanced_query_editor_switch'; import { AdvancedSourceEditor } from '../advanced_source_editor'; -import { PivotConfiguration } from '../pivot_configuration'; +import { DatePickerApplySwitch } from '../date_picker_apply_switch'; import { SourceSearchBar } from '../source_search_bar'; +import { AdvancedRuntimeMappingsSettings } from '../advanced_runtime_mappings_settings'; import { StepDefineExposedState } from './common'; import { useStepDefineForm } from './hooks/use_step_define_form'; -import { getAggConfigFromEsAgg } from '../../../../common/pivot_aggs'; import { TransformFunctionSelector } from './transform_function_selector'; -import { TRANSFORM_FUNCTION } from '../../../../../../common/constants'; import { LatestFunctionForm } from './latest_function_form'; -import { AdvancedRuntimeMappingsSettings } from '../advanced_runtime_mappings_settings'; +import { PivotFunctionForm } from './pivot_function_form'; + +const ALLOW_TIME_RANGE_ON_TRANSFORM_CONFIG = false; + +const advancedEditorsSidebarWidth = '220px'; + +export const ConfigSectionTitle: FC<{ title: string }> = ({ title }) => ( + <> + + + {title} + + + +); export interface StepDefineFormProps { overrides?: StepDefineExposedState; @@ -66,6 +82,7 @@ export interface StepDefineFormProps { } export const StepDefineForm: FC = React.memo((props) => { + const [globalState, setGlobalState] = useUrlState('_g'); const { searchItems } = props; const { dataView } = searchItems; const indexPattern = useMemo(() => dataView.getIndexPattern(), [dataView]); @@ -75,24 +92,18 @@ export const StepDefineForm: FC = React.memo((props) => { const toastNotifications = useToastNotifications(); const stepDefineForm = useStepDefineForm(props); - const { - advancedEditorConfig, - isAdvancedPivotEditorEnabled, - isAdvancedPivotEditorApplyButtonEnabled, - } = stepDefineForm.advancedPivotEditor.state; + const { advancedEditorConfig } = stepDefineForm.advancedPivotEditor.state; const { advancedEditorSourceConfig, isAdvancedSourceEditorEnabled, isAdvancedSourceEditorApplyButtonEnabled, } = stepDefineForm.advancedSourceEditor.state; - const pivotQuery = stepDefineForm.searchBar.state.pivotQuery; + const { isDatePickerApplyEnabled, timeRangeMs } = stepDefineForm.datePicker.state; + const { transformConfigQuery } = stepDefineForm.searchBar.state; + const { runtimeMappings } = stepDefineForm.runtimeMappingsEditor.state; const indexPreviewProps = { - ...useIndexData( - dataView, - stepDefineForm.searchBar.state.pivotQuery, - stepDefineForm.runtimeMappingsEditor.state.runtimeMappings - ), + ...useIndexData(dataView, transformConfigQuery, runtimeMappings, timeRangeMs), dataTestSubj: 'transformIndexPreview', toastNotifications, }; @@ -101,16 +112,7 @@ export const StepDefineForm: FC = React.memo((props) => { ? stepDefineForm.pivotConfig.state : stepDefineForm.latestFunctionConfig; - const previewRequest = getPreviewTransformRequestBody( - indexPattern, - pivotQuery, - stepDefineForm.transformFunction === TRANSFORM_FUNCTION.PIVOT - ? stepDefineForm.pivotConfig.state.requestPayload - : stepDefineForm.latestFunctionConfig.requestPayload, - stepDefineForm.runtimeMappingsEditor.state.runtimeMappings - ); - - const copyToClipboardSource = getIndexDevConsoleStatement(pivotQuery, indexPattern); + const copyToClipboardSource = getIndexDevConsoleStatement(transformConfigQuery, indexPattern); const copyToClipboardSourceDescription = i18n.translate( 'xpack.transform.indexPreview.copyClipboardTooltip', { @@ -118,7 +120,17 @@ export const StepDefineForm: FC = React.memo((props) => { } ); - const copyToClipboardPivot = getPivotPreviewDevConsoleStatement(previewRequest); + const copyToClipboardPreviewRequest = getPreviewTransformRequestBody( + dataView, + transformConfigQuery, + requestPayload, + runtimeMappings, + isDatePickerApplyEnabled ? timeRangeMs : undefined + ); + + const copyToClipboardPivot = getTransformPreviewDevConsoleStatement( + copyToClipboardPreviewRequest + ); const copyToClipboardPivotDescription = i18n.translate( 'xpack.transform.pivotPreview.copyClipboardTooltip', { @@ -126,18 +138,16 @@ export const StepDefineForm: FC = React.memo((props) => { } ); - const pivotPreviewProps = { - ...usePivotData( - indexPattern, - pivotQuery, + const previewProps = { + ...useTransformConfigData( + dataView, + transformConfigQuery, validationStatus, requestPayload, - stepDefineForm.runtimeMappingsEditor.state.runtimeMappings + runtimeMappings, + timeRangeMs ), dataTestSubj: 'transformPivotPreview', - title: i18n.translate('xpack.transform.pivotPreview.transformPreviewTitle', { - defaultMessage: 'Transform preview', - }), toastNotifications, ...(stepDefineForm.transformFunction === TRANSFORM_FUNCTION.LATEST ? { @@ -192,9 +202,52 @@ export const StepDefineForm: FC = React.memo((props) => { stepDefineForm.advancedPivotEditor.actions.setAdvancedPivotEditorApplyButtonEnabled(false); }; - const { esQueryDsl, esTransformPivot } = useDocumentationLinks(); + const { esQueryDsl } = useDocumentationLinks(); + + const hasValidTimeField = useMemo( + () => dataView.timeFieldName !== undefined && dataView.timeFieldName !== '', + [dataView.timeFieldName] + ); + + const timefilter = useTimefilter({ + timeRangeSelector: dataView?.timeFieldName !== undefined, + autoRefreshSelector: false, + }); + + useEffect(() => { + if (globalState?.time !== undefined) { + timefilter.setTime({ + from: globalState.time.from, + to: globalState.time.to, + }); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [JSON.stringify(globalState?.time), timefilter]); + + useEffect(() => { + if (globalState?.refreshInterval !== undefined) { + timefilter.setRefreshInterval(globalState.refreshInterval); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [JSON.stringify(globalState?.refreshInterval), timefilter]); - const advancedEditorsSidebarWidth = '220px'; + useEffect(() => { + const timeUpdateSubscription = merge( + timefilter.getAutoRefreshFetch$(), + timefilter.getTimeUpdate$(), + mlTimefilterRefresh$ + ).subscribe(() => { + if (setGlobalState) { + setGlobalState({ + time: timefilter.getTime(), + refreshInterval: timefilter.getRefreshInterval(), + }); + } + }); + return () => { + timeUpdateSubscription.unsubscribe(); + }; + }); return (
@@ -206,6 +259,8 @@ export const StepDefineForm: FC = React.memo((props) => { /> + + {searchItems.savedSearch === undefined && ( = React.memo((props) => { )} + {hasValidTimeField && ( + + {i18n.translate('xpack.transform.stepDefineForm.datePickerLabel', { + defaultMessage: 'Time range', + })}{' '} + + + } + > + + {/* Flex Column #1: Date Picker */} + + + + {/* Flex Column #2: Apply-To-Config option */} + + {ALLOW_TIME_RANGE_ON_TRANSFORM_CONFIG && ( + + + {searchItems.savedSearch === undefined && ( + + )} + + + )} + + + + )} + <> @@ -314,87 +415,30 @@ export const StepDefineForm: FC = React.memo((props) => { - + + + - + + + {stepDefineForm.transformFunction === TRANSFORM_FUNCTION.PIVOT ? ( - - {/* Flex Column #1: Pivot Config Form / Advanced Pivot Config Editor */} - - {!isAdvancedPivotEditorEnabled && ( - - )} - {isAdvancedPivotEditorEnabled && ( - - )} - - - - - - - - - - - - {(copy: () => void) => ( - - )} - - - - - - {isAdvancedPivotEditorEnabled && ( - - - - <> - {i18n.translate('xpack.transform.stepDefineForm.advancedEditorHelpText', { - defaultMessage: - 'The advanced editor allows you to edit the pivot configuration of the transform.', - })}{' '} - - {i18n.translate( - 'xpack.transform.stepDefineForm.advancedEditorHelpTextLink', - { - defaultMessage: 'Learn more about available options.', - } - )} - - - - - - {i18n.translate( - 'xpack.transform.stepDefineForm.advancedEditorApplyButtonText', - { - defaultMessage: 'Apply changes', - } - )} - - - )} - - - + ) : null} {stepDefineForm.transformFunction === TRANSFORM_FUNCTION.LATEST ? ( = React.memo((props) => { {(stepDefineForm.transformFunction !== TRANSFORM_FUNCTION.LATEST || stepDefineForm.latestFunctionConfig.sortFieldOptions.length > 0) && ( - <> - - - + + <> + + + + )}
); diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_summary.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_summary.tsx index 140d58523b38a..630e6278dceba 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_summary.tsx +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_summary.tsx @@ -14,13 +14,13 @@ import { EuiBadge, EuiCodeBlock, EuiForm, EuiFormRow, EuiSpacer, EuiText } from import { useAppDependencies, useToastNotifications } from '../../../../app_dependencies'; import { - getPivotQuery, - getPivotPreviewDevConsoleStatement, + getTransformConfigQuery, + getTransformPreviewDevConsoleStatement, getPreviewTransformRequestBody, isDefaultQuery, isMatchAllQuery, } from '../../../../common'; -import { usePivotData } from '../../../../hooks/use_pivot_data'; +import { useTransformConfigData } from '../../../../hooks/use_transform_config_data'; import { SearchItems } from '../../../../hooks/use_search_items'; import { AggListSummary } from '../aggregation_list'; @@ -37,6 +37,8 @@ interface Props { export const StepDefineSummary: FC = ({ formState: { + isDatePickerApplyEnabled, + timeRangeMs, runtimeMappings, searchString, searchQuery, @@ -49,31 +51,33 @@ export const StepDefineSummary: FC = ({ searchItems, }) => { const { - ml: { DataGrid }, + ml: { formatHumanReadableDateTimeSeconds, DataGrid }, } = useAppDependencies(); const toastNotifications = useToastNotifications(); - const pivotQuery = getPivotQuery(searchQuery); + const transformConfigQuery = getTransformConfigQuery(searchQuery); const previewRequest = getPreviewTransformRequestBody( - searchItems.dataView.getIndexPattern(), - pivotQuery, + searchItems.dataView, + transformConfigQuery, partialPreviewRequest, - runtimeMappings + runtimeMappings, + isDatePickerApplyEnabled ? timeRangeMs : undefined ); - const pivotPreviewProps = usePivotData( - searchItems.dataView.getIndexPattern(), - pivotQuery, + const pivotPreviewProps = useTransformConfigData( + searchItems.dataView, + transformConfigQuery, validationStatus, partialPreviewRequest, - runtimeMappings + runtimeMappings, + isDatePickerApplyEnabled ? timeRangeMs : undefined ); const isModifiedQuery = typeof searchString === 'undefined' && - !isDefaultQuery(pivotQuery) && - !isMatchAllQuery(pivotQuery); + !isDefaultQuery(transformConfigQuery) && + !isMatchAllQuery(transformConfigQuery); let uniqueKeys: string[] = []; let sortField = ''; @@ -94,6 +98,18 @@ export const StepDefineSummary: FC = ({ > {searchItems.dataView.getIndexPattern()} + {isDatePickerApplyEnabled && timeRangeMs && ( + + + {formatHumanReadableDateTimeSeconds(timeRangeMs.from)} -{' '} + {formatHumanReadableDateTimeSeconds(timeRangeMs.to)} + + + )} {typeof searchString === 'string' && ( = ({ overflowHeight={300} isCopyable > - {JSON.stringify(pivotQuery, null, 2)} + {JSON.stringify(transformConfigQuery, null, 2)} )} @@ -187,7 +203,7 @@ export const StepDefineSummary: FC = ({ = React.memo( // use an IIFE to avoid returning a Promise to useEffect. (async function () { const { searchQuery, previewRequest: partialPreviewRequest } = stepDefineState; - const pivotQuery = getPivotQuery(searchQuery); + const transformConfigQuery = getTransformConfigQuery(searchQuery); const previewRequest = getPreviewTransformRequestBody( - searchItems.dataView.getIndexPattern(), - pivotQuery, + searchItems.dataView, + transformConfigQuery, partialPreviewRequest, stepDefineState.runtimeMappings ); diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/wizard/storage.ts b/x-pack/plugins/transform/public/app/sections/create_transform/components/wizard/storage.ts new file mode 100644 index 0000000000000..0bd126708fde8 --- /dev/null +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/wizard/storage.ts @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { type FrozenTierPreference } from '@kbn/ml-date-picker'; + +export const TRANSFORM_FROZEN_TIER_PREFERENCE = 'transform.frozenDataTierPreference'; + +export type Transform = Partial<{ + [TRANSFORM_FROZEN_TIER_PREFERENCE]: FrozenTierPreference; +}> | null; + +export type TransformKey = keyof Exclude; + +export type TransformStorageMapped = + T extends typeof TRANSFORM_FROZEN_TIER_PREFERENCE ? FrozenTierPreference | undefined : null; + +export const TRANSFORM_STORAGE_KEYS = [TRANSFORM_FROZEN_TIER_PREFERENCE] as const; diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/wizard/wizard.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/wizard/wizard.tsx index 0a221cf735395..f0ad9227ac672 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/wizard/wizard.tsx +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/wizard/wizard.tsx @@ -6,16 +6,24 @@ */ import React, { type FC, useRef, useState, createContext, useMemo } from 'react'; - -import { i18n } from '@kbn/i18n'; +import { pick } from 'lodash'; import { EuiSteps, EuiStepStatus } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; import { DataView } from '@kbn/data-views-plugin/public'; +import { DatePickerContextProvider } from '@kbn/ml-date-picker'; +import { Storage } from '@kbn/kibana-utils-plugin/public'; +import { StorageContextProvider } from '@kbn/ml-local-storage'; +import { UrlStateProvider } from '@kbn/ml-url-state'; +import { UI_SETTINGS } from '@kbn/data-plugin/common'; +import { toMountPoint, wrapWithTheme } from '@kbn/kibana-react-plugin/public'; + import type { TransformConfigUnion } from '../../../../../../common/types/transform'; import { getCreateTransformRequestBody } from '../../../../common'; import { SearchItems } from '../../../../hooks/use_search_items'; +import { useAppDependencies } from '../../../../app_dependencies'; import { applyTransformConfigToDefineState, @@ -34,6 +42,10 @@ import { import { WizardNav } from '../wizard_nav'; import type { RuntimeMappings } from '../step_define/common/types'; +import { TRANSFORM_STORAGE_KEYS } from './storage'; + +const localStorage = new Storage(window.localStorage); + enum WIZARD_STEPS { DEFINE, DETAILS, @@ -94,6 +106,7 @@ export const CreateTransformWizardContext = createContext<{ }); export const Wizard: FC = React.memo(({ cloneConfig, searchItems }) => { + const appDependencies = useAppDependencies(); const { dataView } = searchItems; // The current WIZARD_STEP @@ -113,7 +126,7 @@ export const Wizard: FC = React.memo(({ cloneConfig, searchItems }) const [stepCreateState, setStepCreateState] = useState(getDefaultStepCreateState); const transformConfig = getCreateTransformRequestBody( - dataView.getIndexPattern(), + dataView, stepDefineState, stepDetailsState ); @@ -206,11 +219,24 @@ export const Wizard: FC = React.memo(({ cloneConfig, searchItems }) const stepsConfig = [stepDefine, stepDetails, stepCreate]; + const datePickerDeps = { + ...pick(appDependencies, ['data', 'http', 'notifications', 'theme', 'uiSettings']), + toMountPoint, + wrapWithTheme, + uiSettingsKeys: UI_SETTINGS, + }; + return ( - + + + + + + + ); }); diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/expanded_row_preview_pane.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/expanded_row_preview_pane.tsx index 51dfc449b89b2..bfc5d4f664b15 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/expanded_row_preview_pane.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/expanded_row_preview_pane.tsx @@ -7,11 +7,13 @@ import React, { useMemo, FC } from 'react'; +import type { DataView } from '@kbn/data-views-plugin/public'; + import { TransformConfigUnion } from '../../../../../../common/types/transform'; import { useAppDependencies, useToastNotifications } from '../../../../app_dependencies'; -import { getPivotQuery } from '../../../../common'; -import { usePivotData } from '../../../../hooks/use_pivot_data'; +import { getTransformConfigQuery } from '../../../../common'; +import { useTransformConfigData } from '../../../../hooks/use_transform_config_data'; import { SearchItems } from '../../../../hooks/use_search_items'; import { @@ -38,15 +40,15 @@ export const ExpandedRowPreviewPane: FC = ({ transf [transformConfig] ); - const pivotQuery = useMemo(() => getPivotQuery(searchQuery), [searchQuery]); + const transformConfigQuery = useMemo(() => getTransformConfigQuery(searchQuery), [searchQuery]); const dataViewTitle = Array.isArray(transformConfig.source.index) ? transformConfig.source.index.join(',') : transformConfig.source.index; - const pivotPreviewProps = usePivotData( - dataViewTitle, - pivotQuery, + const pivotPreviewProps = useTransformConfigData( + { getIndexPattern: () => dataViewTitle } as DataView, + transformConfigQuery, validationStatus, previewRequest, runtimeMappings diff --git a/x-pack/plugins/transform/tsconfig.json b/x-pack/plugins/transform/tsconfig.json index 8cb77da845d58..ca4191088a8b1 100644 --- a/x-pack/plugins/transform/tsconfig.json +++ b/x-pack/plugins/transform/tsconfig.json @@ -46,6 +46,10 @@ "@kbn/field-types", "@kbn/ml-nested-property", "@kbn/ml-is-defined", + "@kbn/ml-date-picker", + "@kbn/ml-url-state", + "@kbn/ml-local-storage", + "@kbn/ml-query-utils", ], "exclude": [ "target/**/*", diff --git a/x-pack/test/accessibility/apps/transform.ts b/x-pack/test/accessibility/apps/transform.ts index fa54ea4ad6766..417ab317218de 100644 --- a/x-pack/test/accessibility/apps/transform.ts +++ b/x-pack/test/accessibility/apps/transform.ts @@ -112,6 +112,17 @@ export default function ({ getService }: FtrProviderContext) { ); await transform.sourceSelection.selectSource(ecIndexPattern); + await transform.testExecution.logTestStep( + `sets the date picker to the default '15 minutes ago'` + ); + await transform.datePicker.quickSelect(15, 'm'); + + await transform.testExecution.logTestStep('displays an empty index preview'); + await transform.wizard.assertIndexPreviewEmpty(); + + await transform.testExecution.logTestStep(`sets the date picker to '10 Years ago'`); + await transform.datePicker.quickSelect(); + await transform.testExecution.logTestStep('loads the index preview'); await transform.wizard.assertIndexPreviewLoaded(); await transform.testExecution.logTestStep('displays an empty transform preview'); @@ -191,6 +202,18 @@ export default function ({ getService }: FtrProviderContext) { 'selects the source data and loads the Transform wizard page' ); await transform.sourceSelection.selectSource(ecIndexPattern); + + await transform.testExecution.logTestStep( + `sets the date picker to the default '15 minutes ago'` + ); + await transform.datePicker.quickSelect(15, 'm'); + + await transform.testExecution.logTestStep('displays an empty index preview'); + await transform.wizard.assertIndexPreviewEmpty(); + + await transform.testExecution.logTestStep(`sets the date picker to '10 Years ago'`); + await transform.datePicker.quickSelect(); + await transform.wizard.assertIndexPreviewLoaded(); await transform.wizard.assertTransformPreviewEmpty(); diff --git a/x-pack/test/functional/apps/transform/creation/index_pattern/creation_index_pattern.ts b/x-pack/test/functional/apps/transform/creation/index_pattern/creation_index_pattern.ts index a35e9759c212a..1cb93ad03efe3 100644 --- a/x-pack/test/functional/apps/transform/creation/index_pattern/creation_index_pattern.ts +++ b/x-pack/test/functional/apps/transform/creation/index_pattern/creation_index_pattern.ts @@ -20,7 +20,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const canvasElement = getService('canvasElement'); const esArchiver = getService('esArchiver'); const transform = getService('transform'); - const PageObjects = getPageObjects(['discover']); + const pageObjects = getPageObjects(['discover']); describe('creation_index_pattern', function () { before(async () => { @@ -486,6 +486,17 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await transform.testExecution.logTestStep('has correct transform function selected'); await transform.wizard.assertSelectedTransformFunction('pivot'); + await transform.testExecution.logTestStep( + `sets the date picker to the default '15 minutes ago'` + ); + await transform.datePicker.quickSelect(15, 'm'); + + await transform.testExecution.logTestStep('displays an empty index preview'); + await transform.wizard.assertIndexPreviewEmpty(); + + await transform.testExecution.logTestStep(`sets the date picker to '15 Years ago'`); + await transform.datePicker.quickSelect(); + await transform.testExecution.logTestStep('loads the index preview'); await transform.wizard.assertIndexPreviewLoaded(); @@ -699,16 +710,18 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await transform.testExecution.logTestStep('should navigate to discover'); await transform.table.clickTransformRowAction(testData.transformId, 'Discover'); - await PageObjects.discover.waitUntilSearchingHasFinished(); + await pageObjects.discover.waitUntilSearchingHasFinished(); if (testData.discoverAdjustSuperDatePicker) { + await transform.testExecution.logTestStep( + `sets the date picker to the default '15 minutes ago'` + ); + await transform.datePicker.quickSelect(15, 'm'); await transform.discover.assertNoResults(testData.destinationIndex); await transform.testExecution.logTestStep( 'should switch quick select lookback to years' ); - await transform.discover.assertSuperDatePickerToggleQuickMenuButtonExists(); - await transform.discover.openSuperDatePicker(); - await transform.discover.quickSelectYears(); + await transform.datePicker.quickSelect(); } await transform.discover.assertDiscoverQueryHits(testData.expected.discoverQueryHits); diff --git a/x-pack/test/functional/apps/transform/creation/runtime_mappings_saved_search/creation_runtime_mappings.ts b/x-pack/test/functional/apps/transform/creation/runtime_mappings_saved_search/creation_runtime_mappings.ts index 2c456cb91e083..3f0adc5783893 100644 --- a/x-pack/test/functional/apps/transform/creation/runtime_mappings_saved_search/creation_runtime_mappings.ts +++ b/x-pack/test/functional/apps/transform/creation/runtime_mappings_saved_search/creation_runtime_mappings.ts @@ -279,6 +279,11 @@ export default function ({ getService }: FtrProviderContext) { await transform.testExecution.logTestStep('has correct transform function selected'); await transform.wizard.assertSelectedTransformFunction('pivot'); + await transform.testExecution.logTestStep( + `sets the date picker to the default '15 minutes ago'` + ); + await transform.datePicker.quickSelect(15, 'm'); + await transform.testExecution.logTestStep('has correct runtime mappings settings'); await transform.wizard.assertRuntimeMappingsEditorSwitchExists(); await transform.wizard.assertRuntimeMappingsEditorSwitchCheckState(false); @@ -291,6 +296,12 @@ export default function ({ getService }: FtrProviderContext) { await transform.wizard.setRuntimeMappingsEditorContent(JSON.stringify(runtimeMappings)); await transform.wizard.applyRuntimeMappings(); + await transform.testExecution.logTestStep('displays an empty index preview'); + await transform.wizard.assertIndexPreviewEmpty(); + + await transform.testExecution.logTestStep(`sets the date picker to '15 Years ago'`); + await transform.datePicker.quickSelect(10, 'y'); + await transform.testExecution.logTestStep('loads the index preview'); await transform.wizard.assertIndexPreviewLoaded(); @@ -439,7 +450,7 @@ export default function ({ getService }: FtrProviderContext) { if (isLatestTransformTestData(testData)) { const fromTime = 'Feb 7, 2016 @ 00:00:00.000'; const toTime = 'Feb 11, 2016 @ 23:59:54.000'; - await transform.wizard.setDiscoverTimeRange(fromTime, toTime); + await transform.datePicker.setTimeRange(fromTime, toTime); } await transform.testExecution.logTestStep( diff --git a/x-pack/test/functional/apps/transform/creation/runtime_mappings_saved_search/creation_saved_search.ts b/x-pack/test/functional/apps/transform/creation/runtime_mappings_saved_search/creation_saved_search.ts index 60ab3f93ac3a5..9f985a16da98d 100644 --- a/x-pack/test/functional/apps/transform/creation/runtime_mappings_saved_search/creation_saved_search.ts +++ b/x-pack/test/functional/apps/transform/creation/runtime_mappings_saved_search/creation_saved_search.ts @@ -144,6 +144,17 @@ export default function ({ getService }: FtrProviderContext) { await transform.testExecution.logTestStep('has correct transform function selected'); await transform.wizard.assertSelectedTransformFunction('pivot'); + await transform.testExecution.logTestStep( + `sets the date picker to the default '15 minutes ago'` + ); + await transform.datePicker.quickSelect(15, 'm'); + + await transform.testExecution.logTestStep('displays an empty index preview'); + await transform.wizard.assertIndexPreviewEmpty(); + + await transform.testExecution.logTestStep(`sets the date picker to '15 Years ago'`); + await transform.datePicker.quickSelect(10, 'y'); + await transform.testExecution.logTestStep('loads the index preview'); await transform.wizard.assertIndexPreviewLoaded(); diff --git a/x-pack/test/functional/apps/transform/edit_clone/cloning.ts b/x-pack/test/functional/apps/transform/edit_clone/cloning.ts index b823edadaed83..6b3bbac3f7b3d 100644 --- a/x-pack/test/functional/apps/transform/edit_clone/cloning.ts +++ b/x-pack/test/functional/apps/transform/edit_clone/cloning.ts @@ -418,6 +418,17 @@ export default function ({ getService }: FtrProviderContext) { ); } + await transform.testExecution.logTestStep( + `sets the date picker to the default '15 minutes ago'` + ); + await transform.datePicker.quickSelect(15, 'm'); + + await transform.testExecution.logTestStep('displays an empty index preview'); + await transform.wizard.assertIndexPreviewEmpty(); + + await transform.testExecution.logTestStep(`sets the date picker to '15 Years ago'`); + await transform.datePicker.quickSelect(); + await transform.testExecution.logTestStep('should load the index preview'); await transform.wizard.assertIndexPreviewLoaded(); diff --git a/x-pack/test/functional/apps/transform/feature_controls/transform_security.ts b/x-pack/test/functional/apps/transform/feature_controls/transform_security.ts index 5901ee60c7212..c630525d06cf6 100644 --- a/x-pack/test/functional/apps/transform/feature_controls/transform_security.ts +++ b/x-pack/test/functional/apps/transform/feature_controls/transform_security.ts @@ -11,15 +11,15 @@ import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getPageObjects, getService }: FtrProviderContext) { const kibanaServer = getService('kibanaServer'); const security = getService('security'); - const PageObjects = getPageObjects(['common', 'settings', 'security']); + const pageObjects = getPageObjects(['common', 'settings', 'security']); const appsMenu = getService('appsMenu'); const managementMenu = getService('managementMenu'); describe('security', () => { before(async () => { await kibanaServer.savedObjects.cleanStandardList(); - await PageObjects.security.forceLogout(); - await PageObjects.common.navigateToApp('home'); + await pageObjects.security.forceLogout(); + await pageObjects.common.navigateToApp('home'); }); after(async () => { @@ -40,7 +40,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { }); it('should not render the "Stack" section', async () => { - await PageObjects.common.navigateToApp('management'); + await pageObjects.common.navigateToApp('management'); const sections = (await managementMenu.getSections()).map((section) => section.sectionId); expect(sections).to.eql(['insightsAndAlerting', 'kibana']); }); @@ -59,7 +59,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { }); it('should render the "Data" section with Transform', async () => { - await PageObjects.common.navigateToApp('management'); + await pageObjects.common.navigateToApp('management'); const sections = await managementMenu.getSections(); expect(sections).to.have.length(1); expect(sections[0]).to.eql({ diff --git a/x-pack/test/functional/services/transform/date_picker.ts b/x-pack/test/functional/services/transform/date_picker.ts new file mode 100644 index 0000000000000..941a506db6109 --- /dev/null +++ b/x-pack/test/functional/services/transform/date_picker.ts @@ -0,0 +1,49 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; + +import { FtrProviderContext } from '../../ftr_provider_context'; + +export function TransformDatePickerProvider({ getService, getPageObjects }: FtrProviderContext) { + const find = getService('find'); + const testSubjects = getService('testSubjects'); + const pageObjects = getPageObjects(['timePicker']); + + return { + async assertSuperDatePickerToggleQuickMenuButtonExists() { + await testSubjects.existOrFail('superDatePickerToggleQuickMenuButton'); + }, + + async openSuperDatePicker() { + await this.assertSuperDatePickerToggleQuickMenuButtonExists(); + await testSubjects.click('superDatePickerToggleQuickMenuButton'); + await testSubjects.existOrFail('superDatePickerQuickMenu'); + }, + + async quickSelect(timeValue: number = 15, timeUnit: string = 'y') { + await this.openSuperDatePicker(); + const quickMenuElement = await testSubjects.find('superDatePickerQuickMenu'); + + // No test subject, defaults to select `"Years"` to look back 15 years instead of 15 minutes. + await find.selectValue(`[aria-label*="Time value"]`, timeValue.toString()); + await find.selectValue(`[aria-label*="Time unit"]`, timeUnit); + + // Apply + const applyButton = await quickMenuElement.findByClassName('euiQuickSelect__applyButton'); + const actualApplyButtonText = await applyButton.getVisibleText(); + expect(actualApplyButtonText).to.be('Apply'); + + await applyButton.click(); + await testSubjects.missingOrFail('superDatePickerQuickMenu'); + }, + + async setTimeRange(fromTime: string, toTime: string) { + await pageObjects.timePicker.setAbsoluteRange(fromTime, toTime); + }, + }; +} diff --git a/x-pack/test/functional/services/transform/discover.ts b/x-pack/test/functional/services/transform/discover.ts index 944e65b73f6e2..303bc9b171f80 100644 --- a/x-pack/test/functional/services/transform/discover.ts +++ b/x-pack/test/functional/services/transform/discover.ts @@ -10,7 +10,6 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../ftr_provider_context'; export function TransformDiscoverProvider({ getService }: FtrProviderContext) { - const find = getService('find'); const testSubjects = getService('testSubjects'); return { @@ -28,6 +27,8 @@ export function TransformDiscoverProvider({ getService }: FtrProviderContext) { }, async assertNoResults(expectedDestinationIndex: string) { + await testSubjects.missingOrFail('unifiedHistogramQueryHits'); + // Discover should use the destination index pattern const actualIndexPatternSwitchLinkText = await ( await testSubjects.find('discover-dataView-switch-link') @@ -39,29 +40,5 @@ export function TransformDiscoverProvider({ getService }: FtrProviderContext) { await testSubjects.existOrFail('discoverNoResults'); }, - - async assertSuperDatePickerToggleQuickMenuButtonExists() { - await testSubjects.existOrFail('superDatePickerToggleQuickMenuButton'); - }, - - async openSuperDatePicker() { - await testSubjects.click('superDatePickerToggleQuickMenuButton'); - await testSubjects.existOrFail('superDatePickerQuickMenu'); - }, - - async quickSelectYears() { - const quickMenuElement = await testSubjects.find('superDatePickerQuickMenu'); - - // No test subject, select "Years" to look back 15 years instead of 15 minutes. - await find.selectValue(`[aria-label*="Time unit"]`, 'y'); - - // Apply - const applyButton = await quickMenuElement.findByClassName('euiQuickSelect__applyButton'); - const actualApplyButtonText = await applyButton.getVisibleText(); - expect(actualApplyButtonText).to.be('Apply'); - - await applyButton.click(); - await testSubjects.existOrFail('unifiedHistogramQueryHits'); - }, }; } diff --git a/x-pack/test/functional/services/transform/index.ts b/x-pack/test/functional/services/transform/index.ts index 75f0df67f0919..61461bafe34b5 100644 --- a/x-pack/test/functional/services/transform/index.ts +++ b/x-pack/test/functional/services/transform/index.ts @@ -9,6 +9,7 @@ import { FtrProviderContext } from '../../ftr_provider_context'; import { TransformAPIProvider } from './api'; import { TransformEditFlyoutProvider } from './edit_flyout'; +import { TransformDatePickerProvider } from './date_picker'; import { TransformDiscoverProvider } from './discover'; import { TransformManagementProvider } from './management'; import { TransformNavigationProvider } from './navigation'; @@ -25,6 +26,7 @@ import { MachineLearningTestResourcesProvider } from '../ml/test_resources'; export function TransformProvider(context: FtrProviderContext) { const api = TransformAPIProvider(context); const mlApi = MachineLearningAPIProvider(context); + const datePicker = TransformDatePickerProvider(context); const discover = TransformDiscoverProvider(context); const editFlyout = TransformEditFlyoutProvider(context); const management = TransformManagementProvider(context); @@ -39,6 +41,7 @@ export function TransformProvider(context: FtrProviderContext) { return { api, + datePicker, discover, editFlyout, management, diff --git a/x-pack/test/functional/services/transform/navigation.ts b/x-pack/test/functional/services/transform/navigation.ts index 396a99b1b6673..be579cdc0fb42 100644 --- a/x-pack/test/functional/services/transform/navigation.ts +++ b/x-pack/test/functional/services/transform/navigation.ts @@ -8,11 +8,11 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export function TransformNavigationProvider({ getPageObjects }: FtrProviderContext) { - const PageObjects = getPageObjects(['common']); + const pageObjects = getPageObjects(['common']); return { async navigateTo() { - return await PageObjects.common.navigateToApp('transform'); + return await pageObjects.common.navigateToApp('transform'); }, }; } diff --git a/x-pack/test/functional/services/transform/security_ui.ts b/x-pack/test/functional/services/transform/security_ui.ts index 07d3c2759f42c..365f2dfc2e7d4 100644 --- a/x-pack/test/functional/services/transform/security_ui.ts +++ b/x-pack/test/functional/services/transform/security_ui.ts @@ -12,15 +12,15 @@ export function TransformSecurityUIProvider( { getPageObjects }: FtrProviderContext, transformSecurityCommon: TransformSecurityCommon ) { - const PageObjects = getPageObjects(['security']); + const pageObjects = getPageObjects(['security']); return { async loginAs(user: USER) { const password = transformSecurityCommon.getPasswordForUser(user); - await PageObjects.security.forceLogout(); + await pageObjects.security.forceLogout(); - await PageObjects.security.login(user, password, { + await pageObjects.security.login(user, password, { expectSuccess: true, }); }, @@ -34,7 +34,7 @@ export function TransformSecurityUIProvider( }, async logout() { - await PageObjects.security.forceLogout(); + await pageObjects.security.forceLogout(); }, }; } diff --git a/x-pack/test/functional/services/transform/wizard.ts b/x-pack/test/functional/services/transform/wizard.ts index df65911cb4098..e1370706d2902 100644 --- a/x-pack/test/functional/services/transform/wizard.ts +++ b/x-pack/test/functional/services/transform/wizard.ts @@ -29,7 +29,7 @@ export function TransformWizardProvider({ getService, getPageObjects }: FtrProvi const ml = getService('ml'); const toasts = getService('toasts'); - const PageObjects = getPageObjects(['discover', 'timePicker']); + const pageObjects = getPageObjects(['discover', 'timePicker']); return { async clickNextButton() { @@ -80,6 +80,10 @@ export function TransformWizardProvider({ getService, getPageObjects }: FtrProvi await testSubjects.existOrFail(selector); }, + async assertIndexPreviewEmpty() { + await this.assertIndexPreviewExists('empty'); + }, + async assertIndexPreviewLoaded() { await this.assertIndexPreviewExists('loaded'); }, @@ -995,19 +999,14 @@ export function TransformWizardProvider({ getService, getPageObjects }: FtrProvi async redirectToDiscover() { await retry.tryForTime(60 * 1000, async () => { await testSubjects.click('transformWizardCardDiscover'); - await PageObjects.discover.isDiscoverAppOnScreen(); + await pageObjects.discover.isDiscoverAppOnScreen(); }); }, - async setDiscoverTimeRange(fromTime: string, toTime: string) { - await PageObjects.discover.isDiscoverAppOnScreen(); - await PageObjects.timePicker.setAbsoluteRange(fromTime, toTime); - }, - async assertDiscoverContainField(field: string) { - await PageObjects.discover.isDiscoverAppOnScreen(); + await pageObjects.discover.isDiscoverAppOnScreen(); await retry.tryForTime(60 * 1000, async () => { - const allFields = await PageObjects.discover.getAllFieldNames(); + const allFields = await pageObjects.discover.getAllFieldNames(); if (Array.isArray(allFields)) { // For some reasons, Discover returns fields with dot (e.g '.avg') with extra space const fields = allFields.map((n) => n.replace('.​', '.'));