[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.
This commit is contained in:
Walter Rafelsberger 2023-02-01 09:23:03 +01:00 committed by GitHub
parent ea699561f4
commit 0085aaea00
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
39 changed files with 933 additions and 331 deletions

View file

@ -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(
<MlContext.Provider value={kibanaContextValueMock}>
<I18nProvider>
<DatePickerContextProvider {...getMockedDatePickeDependencies()}>
<DatePickerContextProvider {...getMockedDatePickerDependencies()}>
<TimeSeriesExplorerUrlStateManager {...props} />
</DatePickerContextProvider>
</I18nProvider>

View file

@ -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;
}

View file

@ -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
{

View file

@ -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,

View file

@ -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,

View file

@ -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
);

View file

@ -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 function getPivotQuery(search: string | SavedSearchQuery): PivotQuery {
export type TransformConfigQuery = FilterBasedSimpleQuery | SimpleQuery | SavedSearchQuery;
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 !== ''

View file

@ -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 { getErrorMessage } from '../../../common/utils/errors';
import { isRuntimeMappings } from '../../../common/shared_imports';
import type { EsSorting, UseIndexDataReturnType } from '../../shared_imports';
import { getErrorMessage } from '../../../common/utils/errors';
import { isDefaultQuery, matchAllQuery, PivotQuery } from '../common';
import { isDefaultQuery, matchAllQuery, TransformConfigQuery } from '../common';
import { useAppDependencies, useToastNotifications } from '../app_dependencies';
import type { StepDefineExposedState } from '../sections/create_transform/components/step_define/common';
import { SearchItems } from './use_search_items';
import { useApi } from './use_api';
import { useAppDependencies, useToastNotifications } from '../app_dependencies';
import type { StepDefineExposedState } from '../sections/create_transform/components/step_define/common';
import { isRuntimeMappings } from '../../../common/shared_imports';
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<string[]>();
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);

View file

@ -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<SearchItems | undefined>(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(() => {

View file

@ -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', () => {

View file

@ -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<PreviewMappingsProperties>({});
@ -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) => ({

View file

@ -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<StepDefineFormHook> = ({
datePicker: {
actions: { setDatePickerApplyEnabled },
state: { isDatePickerApplyEnabled },
},
}) => {
return (
<EuiSwitch
label={i18n.translate('xpack.transform.stepDefineForm.datePickerApplySwitchLabel', {
defaultMessage: 'Apply time range',
})}
checked={isDatePickerApplyEnabled}
onChange={() => {
setDatePickerApplyEnabled(!isDatePickerApplyEnabled);
}}
data-test-subj="transformDatePickerApplySwitch"
/>
);
};

View file

@ -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';

View file

@ -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,

View file

@ -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 } {

View file

@ -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<TimeRange>();
// 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 },
};
};

View file

@ -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,

View file

@ -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,

View file

@ -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<PivotFunctionFormProps> = ({
applyPivotChangesHandler,
copyToClipboardPivot,
copyToClipboardPivotDescription,
stepDefineForm,
}) => {
const { esTransformPivot } = useDocumentationLinks();
const { isAdvancedPivotEditorEnabled, isAdvancedPivotEditorApplyButtonEnabled } =
stepDefineForm.advancedPivotEditor.state;
return (
<EuiFlexGroup justifyContent="spaceBetween">
{/* Flex Column #1: Pivot Config Form / Advanced Pivot Config Editor */}
<EuiFlexItem>
{!isAdvancedPivotEditorEnabled && <PivotConfiguration {...stepDefineForm.pivotConfig} />}
{isAdvancedPivotEditorEnabled && (
<AdvancedPivotEditor {...stepDefineForm.advancedPivotEditor} />
)}
</EuiFlexItem>
<EuiFlexItem grow={false} style={{ width: advancedEditorsSidebarWidth }}>
<EuiFlexGroup gutterSize="xs" direction="column" justifyContent="spaceBetween">
<EuiFlexItem grow={false}>
<EuiFormRow hasEmptyLabelSpace>
<EuiFlexGroup alignItems="center" justifyContent="spaceBetween">
<EuiFlexItem grow={false}>
<AdvancedPivotEditorSwitch {...stepDefineForm} />
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiCopy
beforeMessage={copyToClipboardPivotDescription}
textToCopy={copyToClipboardPivot}
>
{(copy: () => void) => (
<EuiButtonIcon
onClick={copy}
iconType="copyClipboard"
aria-label={copyToClipboardPivotDescription}
/>
)}
</EuiCopy>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFormRow>
</EuiFlexItem>
{isAdvancedPivotEditorEnabled && (
<EuiFlexItem style={{ width: advancedEditorsSidebarWidth }}>
<EuiSpacer size="s" />
<EuiText size="xs">
<>
{i18n.translate('xpack.transform.stepDefineForm.advancedEditorHelpText', {
defaultMessage:
'The advanced editor allows you to edit the pivot configuration of the transform.',
})}{' '}
<EuiLink href={esTransformPivot} target="_blank">
{i18n.translate('xpack.transform.stepDefineForm.advancedEditorHelpTextLink', {
defaultMessage: 'Learn more about available options.',
})}
</EuiLink>
</>
</EuiText>
<EuiSpacer size="s" />
<EuiButton
style={{ width: 'fit-content' }}
size="s"
fill
onClick={applyPivotChangesHandler}
disabled={!isAdvancedPivotEditorApplyButtonEnabled}
>
{i18n.translate('xpack.transform.stepDefineForm.advancedEditorApplyButtonText', {
defaultMessage: 'Apply changes',
})}
</EuiButton>
</EuiFlexItem>
)}
</EuiFlexGroup>
</EuiFlexItem>
</EuiFlexGroup>
);
};

View file

@ -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: <DefinePivotForm />', () => {
<I18nProvider>
<KibanaContextProvider services={services}>
<MlSharedContext.Provider value={mlSharedImports}>
<StepDefineForm onChange={jest.fn()} searchItems={searchItems as SearchItems} />
<DatePickerContextProvider {...getMockedDatePickerDependencies()}>
<StepDefineForm onChange={jest.fn()} searchItems={searchItems as SearchItems} />
</DatePickerContextProvider>
</MlSharedContext.Provider>
</KibanaContextProvider>
</I18nProvider>

View file

@ -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 }) => (
<>
<EuiSpacer size="m" />
<EuiTitle size="xs">
<span>{title}</span>
</EuiTitle>
<EuiSpacer size="s" />
</>
);
export interface StepDefineFormProps {
overrides?: StepDefineExposedState;
@ -66,6 +82,7 @@ export interface StepDefineFormProps {
}
export const StepDefineForm: FC<StepDefineFormProps> = 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<StepDefineFormProps> = 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<StepDefineFormProps> = 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<StepDefineFormProps> = 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<StepDefineFormProps> = 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<StepDefineFormProps> = React.memo((props) => {
stepDefineForm.advancedPivotEditor.actions.setAdvancedPivotEditorApplyButtonEnabled(false);
};
const { esQueryDsl, esTransformPivot } = useDocumentationLinks();
const { esQueryDsl } = useDocumentationLinks();
const advancedEditorsSidebarWidth = '220px';
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]);
useEffect(() => {
const timeUpdateSubscription = merge(
timefilter.getAutoRefreshFetch$(),
timefilter.getTimeUpdate$(),
mlTimefilterRefresh$
).subscribe(() => {
if (setGlobalState) {
setGlobalState({
time: timefilter.getTime(),
refreshInterval: timefilter.getRefreshInterval(),
});
}
});
return () => {
timeUpdateSubscription.unsubscribe();
};
});
return (
<div data-test-subj="transformStepDefineForm">
@ -206,6 +259,8 @@ export const StepDefineForm: FC<StepDefineFormProps> = React.memo((props) => {
/>
</EuiFormRow>
<ConfigSectionTitle title="Source data" />
{searchItems.savedSearch === undefined && (
<EuiFormRow
label={i18n.translate('xpack.transform.stepDefineForm.dataViewLabel', {
@ -216,15 +271,61 @@ export const StepDefineForm: FC<StepDefineFormProps> = React.memo((props) => {
</EuiFormRow>
)}
{hasValidTimeField && (
<EuiFormRow
fullWidth
label={
<>
{i18n.translate('xpack.transform.stepDefineForm.datePickerLabel', {
defaultMessage: 'Time range',
})}{' '}
<EuiIconTip
content={i18n.translate(
'xpack.transform.stepDefineForm.datePickerIconTipContent',
{
defaultMessage:
'The time range will be applied to previews only and will not be part of the final transform configuration.',
}
)}
/>
</>
}
>
<EuiFlexGroup alignItems="flexStart" justifyContent="spaceBetween">
{/* Flex Column #1: Date Picker */}
<EuiFlexItem>
<DatePickerWrapper
isAutoRefreshOnly={!hasValidTimeField}
showRefresh={!hasValidTimeField}
width="full"
/>
</EuiFlexItem>
{/* Flex Column #2: Apply-To-Config option */}
<EuiFlexItem grow={false} style={{ width: advancedEditorsSidebarWidth }}>
{ALLOW_TIME_RANGE_ON_TRANSFORM_CONFIG && (
<EuiFlexGroup alignItems="center" justifyContent="spaceBetween">
<EuiFlexItem grow={false}>
{searchItems.savedSearch === undefined && (
<DatePickerApplySwitch {...stepDefineForm} />
)}
</EuiFlexItem>
</EuiFlexGroup>
)}
</EuiFlexItem>
</EuiFlexGroup>
</EuiFormRow>
)}
<EuiFormRow
fullWidth
hasEmptyLabelSpace={searchItems?.savedSearch?.id === undefined}
label={
searchItems?.savedSearch?.id !== undefined
? i18n.translate('xpack.transform.stepDefineForm.savedSearchLabel', {
defaultMessage: 'Saved search',
})
: ''
: i18n.translate('xpack.transform.stepDefineForm.searchFilterLabel', {
defaultMessage: 'Search filter',
})
}
>
<>
@ -314,87 +415,30 @@ export const StepDefineForm: FC<StepDefineFormProps> = React.memo((props) => {
<AdvancedRuntimeMappingsSettings {...stepDefineForm} />
<EuiSpacer size="s" />
<DataGrid {...indexPreviewProps} />
<EuiFormRow
fullWidth={true}
label={i18n.translate('xpack.transform.stepDefineForm.dataGridLabel', {
defaultMessage: 'Source documents',
})}
>
<DataGrid {...indexPreviewProps} />
</EuiFormRow>
</>
</EuiFormRow>
</EuiForm>
<EuiHorizontalRule margin="m" />
<ConfigSectionTitle title="Transform configuration" />
<EuiForm>
{stepDefineForm.transformFunction === TRANSFORM_FUNCTION.PIVOT ? (
<EuiFlexGroup justifyContent="spaceBetween">
{/* Flex Column #1: Pivot Config Form / Advanced Pivot Config Editor */}
<EuiFlexItem>
{!isAdvancedPivotEditorEnabled && (
<PivotConfiguration {...stepDefineForm.pivotConfig} />
)}
{isAdvancedPivotEditorEnabled && (
<AdvancedPivotEditor {...stepDefineForm.advancedPivotEditor} />
)}
</EuiFlexItem>
<EuiFlexItem grow={false} style={{ width: advancedEditorsSidebarWidth }}>
<EuiFlexGroup gutterSize="xs" direction="column" justifyContent="spaceBetween">
<EuiFlexItem grow={false}>
<EuiFormRow hasEmptyLabelSpace>
<EuiFlexGroup alignItems="center" justifyContent="spaceBetween">
<EuiFlexItem grow={false}>
<AdvancedPivotEditorSwitch {...stepDefineForm} />
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiCopy
beforeMessage={copyToClipboardPivotDescription}
textToCopy={copyToClipboardPivot}
>
{(copy: () => void) => (
<EuiButtonIcon
onClick={copy}
iconType="copyClipboard"
aria-label={copyToClipboardPivotDescription}
/>
)}
</EuiCopy>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFormRow>
</EuiFlexItem>
{isAdvancedPivotEditorEnabled && (
<EuiFlexItem style={{ width: advancedEditorsSidebarWidth }}>
<EuiSpacer size="s" />
<EuiText size="xs">
<>
{i18n.translate('xpack.transform.stepDefineForm.advancedEditorHelpText', {
defaultMessage:
'The advanced editor allows you to edit the pivot configuration of the transform.',
})}{' '}
<EuiLink href={esTransformPivot} target="_blank">
{i18n.translate(
'xpack.transform.stepDefineForm.advancedEditorHelpTextLink',
{
defaultMessage: 'Learn more about available options.',
}
)}
</EuiLink>
</>
</EuiText>
<EuiSpacer size="s" />
<EuiButton
style={{ width: 'fit-content' }}
size="s"
fill
onClick={applyPivotChangesHandler}
disabled={!isAdvancedPivotEditorApplyButtonEnabled}
>
{i18n.translate(
'xpack.transform.stepDefineForm.advancedEditorApplyButtonText',
{
defaultMessage: 'Apply changes',
}
)}
</EuiButton>
</EuiFlexItem>
)}
</EuiFlexGroup>
</EuiFlexItem>
</EuiFlexGroup>
<PivotFunctionForm
{...{
applyPivotChangesHandler,
copyToClipboardPivot,
copyToClipboardPivotDescription,
stepDefineForm,
}}
/>
) : null}
{stepDefineForm.transformFunction === TRANSFORM_FUNCTION.LATEST ? (
<LatestFunctionForm
@ -407,10 +451,17 @@ export const StepDefineForm: FC<StepDefineFormProps> = React.memo((props) => {
<EuiSpacer size="m" />
{(stepDefineForm.transformFunction !== TRANSFORM_FUNCTION.LATEST ||
stepDefineForm.latestFunctionConfig.sortFieldOptions.length > 0) && (
<>
<DataGrid {...pivotPreviewProps} />
<EuiSpacer size="m" />
</>
<EuiFormRow
fullWidth
label={i18n.translate('xpack.transform.stepDefineForm.previewLabel', {
defaultMessage: 'Preview',
})}
>
<>
<DataGrid {...previewProps} />
<EuiSpacer size="m" />
</>
</EuiFormRow>
)}
</div>
);

View file

@ -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<Props> = ({
formState: {
isDatePickerApplyEnabled,
timeRangeMs,
runtimeMappings,
searchString,
searchQuery,
@ -49,31 +51,33 @@ export const StepDefineSummary: FC<Props> = ({
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<Props> = ({
>
<span>{searchItems.dataView.getIndexPattern()}</span>
</EuiFormRow>
{isDatePickerApplyEnabled && timeRangeMs && (
<EuiFormRow
label={i18n.translate('xpack.transform.stepDefineSummary.timeRangeLabel', {
defaultMessage: 'Time range',
})}
>
<span>
{formatHumanReadableDateTimeSeconds(timeRangeMs.from)} -{' '}
{formatHumanReadableDateTimeSeconds(timeRangeMs.to)}
</span>
</EuiFormRow>
)}
{typeof searchString === 'string' && (
<EuiFormRow
label={i18n.translate('xpack.transform.stepDefineSummary.queryLabel', {
@ -117,7 +133,7 @@ export const StepDefineSummary: FC<Props> = ({
overflowHeight={300}
isCopyable
>
{JSON.stringify(pivotQuery, null, 2)}
{JSON.stringify(transformConfigQuery, null, 2)}
</EuiCodeBlock>
</EuiFormRow>
)}
@ -187,7 +203,7 @@ export const StepDefineSummary: FC<Props> = ({
<EuiSpacer size="m" />
<DataGrid
{...pivotPreviewProps}
copyToClipboard={getPivotPreviewDevConsoleStatement(previewRequest)}
copyToClipboard={getTransformPreviewDevConsoleStatement(previewRequest)}
copyToClipboardDescription={i18n.translate(
'xpack.transform.pivotPreview.copyClipboardTooltip',
{

View file

@ -47,7 +47,7 @@ import { SearchItems } from '../../../../hooks/use_search_items';
import { useApi } from '../../../../hooks/use_api';
import { StepDetailsTimeField } from './step_details_time_field';
import {
getPivotQuery,
getTransformConfigQuery,
getPreviewTransformRequestBody,
isTransformIdValid,
} from '../../../../common';
@ -132,10 +132,10 @@ export const StepDetailsForm: FC<StepDetailsFormProps> = 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
);

View file

@ -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<Transform, null>;
export type TransformStorageMapped<T extends TransformKey> =
T extends typeof TRANSFORM_FROZEN_TIER_PREFERENCE ? FrozenTierPreference | undefined : null;
export const TRANSFORM_STORAGE_KEYS = [TRANSFORM_FROZEN_TIER_PREFERENCE] as const;

View file

@ -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<WizardProps> = React.memo(({ cloneConfig, searchItems }) => {
const appDependencies = useAppDependencies();
const { dataView } = searchItems;
// The current WIZARD_STEP
@ -113,7 +126,7 @@ export const Wizard: FC<WizardProps> = 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<WizardProps> = React.memo(({ cloneConfig, searchItems })
const stepsConfig = [stepDefine, stepDetails, stepCreate];
const datePickerDeps = {
...pick(appDependencies, ['data', 'http', 'notifications', 'theme', 'uiSettings']),
toMountPoint,
wrapWithTheme,
uiSettingsKeys: UI_SETTINGS,
};
return (
<CreateTransformWizardContext.Provider
value={{ dataView, runtimeMappings: stepDefineState.runtimeMappings }}
>
<EuiSteps className="transform__steps" steps={stepsConfig} />
<UrlStateProvider>
<StorageContextProvider storage={localStorage} storageKeys={TRANSFORM_STORAGE_KEYS}>
<DatePickerContextProvider {...datePickerDeps}>
<EuiSteps className="transform__steps" steps={stepsConfig} />
</DatePickerContextProvider>
</StorageContextProvider>
</UrlStateProvider>
</CreateTransformWizardContext.Provider>
);
});

View file

@ -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<ExpandedRowPreviewPaneProps> = ({ 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

View file

@ -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/**/*",

View file

@ -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();

View file

@ -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);

View file

@ -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(

View file

@ -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();

View file

@ -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();

View file

@ -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({

View file

@ -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);
},
};
}

View file

@ -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');
},
};
}

View file

@ -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,

View file

@ -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');
},
};
}

View file

@ -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();
},
};
}

View file

@ -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('.', '.'));