[Transform] Add support for latest function (#85784)

* [Transform] add latest to the schema definition

* [ML] update interfaces, add guards

* [Transform] WIP support latest function

* [Transform] fix request with missing_bucket after merge

* [Transform] fix error in useDeleteTransforms

* [Transform] fix types and fields sorting for pivot

* [Transform] fix types and jest tests

* [Transform] fix error shape

* [Transform] fixed card width, change description

* [Transform] fixed API integration tests

* [Transform] fix config mapper

* [Transform] improve wizard performance
This commit is contained in:
Dima Arnautov 2020-12-15 22:40:13 +01:00 committed by GitHub
parent 2a71d41a60
commit e17cd65196
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
37 changed files with 1151 additions and 427 deletions

View file

@ -40,7 +40,13 @@ export type TransformIdParamSchema = TypeOf<typeof transformIdParamSchema>;
export interface ResponseStatus { export interface ResponseStatus {
success: boolean; success: boolean;
error?: any; // FIXME error response should have unified shape
error?: {
type: string;
reason: string;
root_cause: any[];
caused_by: any;
} & { response: any };
} }
export interface CommonResponseStatusSchema { export interface CommonResponseStatusSchema {

View file

@ -35,19 +35,32 @@ export const destSchema = schema.object({
index: schema.string(), index: schema.string(),
pipeline: schema.maybe(schema.string()), pipeline: schema.maybe(schema.string()),
}); });
export const pivotSchema = schema.object({ export const pivotSchema = schema.object({
group_by: schema.any(), group_by: schema.any(),
aggregations: schema.any(), aggregations: schema.any(),
}); });
export const latestFunctionSchema = schema.object({
unique_key: schema.arrayOf(schema.string()),
sort: schema.string(),
});
export type PivotConfig = TypeOf<typeof pivotSchema>;
export type LatestFunctionConfig = TypeOf<typeof latestFunctionSchema>;
export const settingsSchema = schema.object({ export const settingsSchema = schema.object({
max_page_search_size: schema.maybe(schema.number()), max_page_search_size: schema.maybe(schema.number()),
// The default value is null, which disables throttling. // The default value is null, which disables throttling.
docs_per_second: schema.maybe(schema.nullable(schema.number())), docs_per_second: schema.maybe(schema.nullable(schema.number())),
}); });
export const sourceSchema = schema.object({ export const sourceSchema = schema.object({
index: schema.oneOf([schema.string(), schema.arrayOf(schema.string())]), index: schema.oneOf([schema.string(), schema.arrayOf(schema.string())]),
query: schema.maybe(schema.recordOf(schema.string(), schema.any())), query: schema.maybe(schema.recordOf(schema.string(), schema.any())),
}); });
export const syncSchema = schema.object({ export const syncSchema = schema.object({
time: schema.object({ time: schema.object({
delay: schema.maybe(schema.string()), delay: schema.maybe(schema.string()),
@ -55,24 +68,52 @@ export const syncSchema = schema.object({
}), }),
}); });
function transformConfigPayloadValidator<
T extends { pivot?: PivotConfig; latest?: LatestFunctionConfig }
>(value: T) {
if (!value.pivot && !value.latest) {
return 'pivot or latest is required for transform configuration';
}
if (value.pivot && value.latest) {
return 'pivot and latest are not allowed together';
}
}
// PUT transforms/{transformId} // PUT transforms/{transformId}
export const putTransformsRequestSchema = schema.object({ export const putTransformsRequestSchema = schema.object(
{
description: schema.maybe(schema.string()), description: schema.maybe(schema.string()),
dest: destSchema, dest: destSchema,
frequency: schema.maybe(schema.string()), frequency: schema.maybe(schema.string()),
pivot: pivotSchema, /**
* Pivot and latest are mutually exclusive, i.e. exactly one must be specified in the transform configuration
*/
pivot: schema.maybe(pivotSchema),
/**
* Latest and pivot are mutually exclusive, i.e. exactly one must be specified in the transform configuration
*/
latest: schema.maybe(latestFunctionSchema),
settings: schema.maybe(settingsSchema), settings: schema.maybe(settingsSchema),
source: sourceSchema, source: sourceSchema,
sync: schema.maybe(syncSchema), sync: schema.maybe(syncSchema),
}); },
{
validate: transformConfigPayloadValidator,
}
);
export interface PutTransformsRequestSchema extends TypeOf<typeof putTransformsRequestSchema> { export type PutTransformsRequestSchema = TypeOf<typeof putTransformsRequestSchema>;
export interface PutTransformsPivotRequestSchema
extends Omit<PutTransformsRequestSchema, 'latest'> {
pivot: { pivot: {
group_by: PivotGroupByDict; group_by: PivotGroupByDict;
aggregations: PivotAggDict; aggregations: PivotAggDict;
}; };
} }
export type PutTransformsLatestRequestSchema = Omit<PutTransformsRequestSchema, 'pivot'>;
interface TransformCreated { interface TransformCreated {
transform: TransformId; transform: TransformId;
} }
@ -86,18 +127,30 @@ export interface PutTransformsResponseSchema {
} }
// POST transforms/_preview // POST transforms/_preview
export const postTransformsPreviewRequestSchema = schema.object({ export const postTransformsPreviewRequestSchema = schema.object(
pivot: pivotSchema, {
pivot: schema.maybe(pivotSchema),
latest: schema.maybe(latestFunctionSchema),
source: sourceSchema, source: sourceSchema,
}); },
{
validate: transformConfigPayloadValidator,
}
);
export interface PostTransformsPreviewRequestSchema export type PostTransformsPreviewRequestSchema = TypeOf<typeof postTransformsPreviewRequestSchema>;
extends TypeOf<typeof postTransformsPreviewRequestSchema> {
export type PivotTransformPreviewRequestSchema = Omit<
PostTransformsPreviewRequestSchema,
'latest'
> & {
pivot: { pivot: {
group_by: PivotGroupByDict; group_by: PivotGroupByDict;
aggregations: PivotAggDict; aggregations: PivotAggDict;
}; };
} };
export type LatestTransformPreviewRequestSchema = Omit<PostTransformsPreviewRequestSchema, 'pivot'>;
interface EsMappingType { interface EsMappingType {
type: ES_FIELD_TYPES; type: ES_FIELD_TYPES;

View file

@ -96,3 +96,10 @@ export const TRANSFORM_MODE = {
const transformModes = Object.values(TRANSFORM_MODE); const transformModes = Object.values(TRANSFORM_MODE);
export type TransformMode = typeof transformModes[number]; export type TransformMode = typeof transformModes[number];
export const TRANSFORM_FUNCTION = {
PIVOT: 'pivot',
LATEST: 'latest',
} as const;
export type TransformFunction = typeof TRANSFORM_FUNCTION[keyof typeof TRANSFORM_FUNCTION];

View file

@ -4,14 +4,58 @@
* you may not use this file except in compliance with the Elastic License. * you may not use this file except in compliance with the Elastic License.
*/ */
import type { PutTransformsRequestSchema } from '../api_schemas/transforms'; import { EuiComboBoxOptionOption } from '@elastic/eui/src/components/combo_box/types';
import type { LatestFunctionConfig, PutTransformsRequestSchema } from '../api_schemas/transforms';
import { PivotGroupByDict } from './pivot_group_by';
import { PivotAggDict } from './pivot_aggs';
export type IndexName = string; export type IndexName = string;
export type IndexPattern = string; export type IndexPattern = string;
export type TransformId = string; export type TransformId = string;
export interface TransformPivotConfig extends PutTransformsRequestSchema { /**
* Generic type for transform response
*/
export type TransformBaseConfig = PutTransformsRequestSchema & {
id: TransformId; id: TransformId;
create_time?: number; create_time?: number;
version?: string; version?: string;
};
export interface PivotConfigDefinition {
group_by: PivotGroupByDict;
aggregations: PivotAggDict;
}
/**
* Transform with pivot configuration
*/
export type TransformPivotConfig = Omit<TransformBaseConfig, 'latest'> & {
pivot: PivotConfigDefinition;
};
/**
* Transform with latest function configuration
*/
export type TransformLatestConfig = Omit<TransformBaseConfig, 'pivot'> & {
latest: LatestFunctionConfig;
};
export type TransformConfigUnion = TransformPivotConfig | TransformLatestConfig;
export function isPivotTransform(
transform: TransformBaseConfig
): transform is TransformPivotConfig {
return transform.hasOwnProperty('pivot');
}
export function isLatestTransform(
transform: TransformBaseConfig
): transform is TransformLatestConfig {
return transform.hasOwnProperty('latest');
}
export interface LatestFunctionConfigUI {
unique_key: Array<EuiComboBoxOptionOption<string>> | undefined;
sort: EuiComboBoxOptionOption<string> | undefined;
} }

View file

@ -4,15 +4,7 @@
* you may not use this file except in compliance with the Elastic License. * you may not use this file except in compliance with the Elastic License.
*/ */
import { PIVOT_SUPPORTED_AGGS } from '../../../common/types/pivot_aggs'; import { getPreviewTransformRequestBody, SimpleQuery } from '../common';
import {
getPreviewTransformRequestBody,
PivotAggsConfig,
PivotGroupByConfig,
PIVOT_SUPPORTED_GROUP_BY_AGGS,
SimpleQuery,
} from '../common';
import { getIndexDevConsoleStatement, getPivotPreviewDevConsoleStatement } from './data_grid'; import { getIndexDevConsoleStatement, getPivotPreviewDevConsoleStatement } from './data_grid';
@ -24,24 +16,26 @@ describe('Transform: Data Grid', () => {
default_operator: 'AND', default_operator: 'AND',
}, },
}; };
const groupBy: PivotGroupByConfig = {
agg: PIVOT_SUPPORTED_GROUP_BY_AGGS.TERMS, const request = getPreviewTransformRequestBody('the-index-pattern-title', query, {
pivot: {
group_by: {
'the-group-by-agg-name': {
terms: {
field: 'the-group-by-field', field: 'the-group-by-field',
aggName: 'the-group-by-agg-name', },
dropDownName: 'the-group-by-drop-down-name', },
}; },
const agg: PivotAggsConfig = { aggregations: {
agg: PIVOT_SUPPORTED_AGGS.AVG, 'the-agg-agg-name': {
avg: {
field: 'the-agg-field', field: 'the-agg-field',
aggName: 'the-agg-agg-name', },
dropDownName: 'the-agg-drop-down-name', },
}; },
const request = getPreviewTransformRequestBody( },
'the-index-pattern-title', });
query,
[groupBy],
[agg]
);
const pivotPreviewDevConsoleStatement = getPivotPreviewDevConsoleStatement(request); const pivotPreviewDevConsoleStatement = getPivotPreviewDevConsoleStatement(request);
expect(pivotPreviewDevConsoleStatement).toBe(`POST _transform/_preview expect(pivotPreviewDevConsoleStatement).toBe(`POST _transform/_preview

View file

@ -18,7 +18,6 @@ import {
getPreviewTransformRequestBody, getPreviewTransformRequestBody,
getCreateTransformRequestBody, getCreateTransformRequestBody,
getCreateTransformSettingsRequestBody, getCreateTransformSettingsRequestBody,
getMissingBucketConfig,
getPivotQuery, getPivotQuery,
isDefaultQuery, isDefaultQuery,
isMatchAllQuery, isMatchAllQuery,
@ -26,6 +25,7 @@ import {
matchAllQuery, matchAllQuery,
PivotQuery, PivotQuery,
} from './request'; } from './request';
import { LatestFunctionConfigUI } from '../../../common/types/transform';
const simpleQuery: PivotQuery = { query_string: { query: 'airline:AAL' } }; const simpleQuery: PivotQuery = { query_string: { query: 'airline:AAL' } };
@ -62,16 +62,6 @@ describe('Transform: Common', () => {
expect(isDefaultQuery(simpleQuery)).toBe(false); expect(isDefaultQuery(simpleQuery)).toBe(false);
}); });
test('getMissingBucketConfig()', () => {
expect(getMissingBucketConfig(groupByTerms)).toEqual({});
expect(getMissingBucketConfig({ ...groupByTerms, ...{ missing_bucket: true } })).toEqual({
missing_bucket: true,
});
expect(getMissingBucketConfig({ ...groupByTerms, ...{ missing_bucket: false } })).toEqual({
missing_bucket: false,
});
});
test('getPivotQuery()', () => { test('getPivotQuery()', () => {
const query = getPivotQuery('the-query'); const query = getPivotQuery('the-query');
@ -85,9 +75,13 @@ describe('Transform: Common', () => {
test('getPreviewTransformRequestBody()', () => { test('getPreviewTransformRequestBody()', () => {
const query = getPivotQuery('the-query'); const query = getPivotQuery('the-query');
const groupBy: PivotGroupByConfig[] = [groupByTerms];
const aggs: PivotAggsConfig[] = [aggsAvg]; const request = getPreviewTransformRequestBody('the-index-pattern-title', query, {
const request = getPreviewTransformRequestBody('the-index-pattern-title', query, groupBy, aggs); 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({ expect(request).toEqual({
pivot: { pivot: {
@ -103,13 +97,15 @@ describe('Transform: Common', () => {
test('getPreviewTransformRequestBody() with comma-separated index pattern', () => { test('getPreviewTransformRequestBody() with comma-separated index pattern', () => {
const query = getPivotQuery('the-query'); const query = getPivotQuery('the-query');
const groupBy: PivotGroupByConfig[] = [groupByTerms];
const aggs: PivotAggsConfig[] = [aggsAvg];
const request = getPreviewTransformRequestBody( const request = getPreviewTransformRequestBody(
'the-index-pattern-title,the-other-title', 'the-index-pattern-title,the-other-title',
query, query,
groupBy, {
aggs 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({ expect(request).toEqual({
@ -126,9 +122,14 @@ describe('Transform: Common', () => {
test('getPreviewTransformRequestBody() with missing_buckets config', () => { test('getPreviewTransformRequestBody() with missing_buckets config', () => {
const query = getPivotQuery('the-query'); const query = getPivotQuery('the-query');
const groupBy: PivotGroupByConfig[] = [{ ...groupByTerms, ...{ missing_bucket: true } }]; const request = getPreviewTransformRequestBody('the-index-pattern-title', query, {
const aggs: PivotAggsConfig[] = [aggsAvg]; pivot: {
const request = getPreviewTransformRequestBody('the-index-pattern-title', query, groupBy, aggs); aggregations: { 'the-agg-agg-name': { avg: { field: 'the-agg-field' } } },
group_by: {
'the-group-by-agg-name': { terms: { field: 'the-group-by-field', missing_bucket: true } },
},
},
});
expect(request).toEqual({ expect(request).toEqual({
pivot: { pivot: {
@ -155,6 +156,17 @@ describe('Transform: Common', () => {
searchString: 'the-query', searchString: 'the-query',
searchQuery: 'the-search-query', searchQuery: 'the-search-query',
valid: true, valid: true,
transformFunction: 'pivot',
latestConfig: {} as LatestFunctionConfigUI,
previewRequest: {
pivot: {
aggregations: { 'the-agg-agg-name': { avg: { field: 'the-agg-field' } } },
group_by: { 'the-group-by-agg-name': { terms: { field: 'the-group-by-field' } } },
},
},
validationStatus: {
isValid: true,
},
}; };
const transformDetailsState: StepDetailsExposedState = { const transformDetailsState: StepDetailsExposedState = {
continuousModeDateField: 'the-continuous-mode-date-field', continuousModeDateField: 'the-continuous-mode-date-field',

View file

@ -11,31 +11,15 @@ import type { IndexPattern } from '../../../../../../src/plugins/data/public';
import type { import type {
PostTransformsPreviewRequestSchema, PostTransformsPreviewRequestSchema,
PutTransformsLatestRequestSchema,
PutTransformsPivotRequestSchema,
PutTransformsRequestSchema, PutTransformsRequestSchema,
} from '../../../common/api_schemas/transforms'; } from '../../../common/api_schemas/transforms';
import type {
DateHistogramAgg,
HistogramAgg,
TermsAgg,
} from '../../../common/types/pivot_group_by';
import { dictionaryToArray } from '../../../common/types/common';
import type { SavedSearchQuery } from '../hooks/use_search_items'; import type { SavedSearchQuery } from '../hooks/use_search_items';
import type { StepDefineExposedState } from '../sections/create_transform/components/step_define'; import type { StepDefineExposedState } from '../sections/create_transform/components/step_define';
import type { StepDetailsExposedState } from '../sections/create_transform/components/step_details/step_details_form'; import type { StepDetailsExposedState } from '../sections/create_transform/components/step_details/step_details_form';
import {
getEsAggFromAggConfig,
getEsAggFromGroupByConfig,
isGroupByDateHistogram,
isGroupByHistogram,
isGroupByTerms,
GroupByConfigWithUiSupport,
PivotGroupByConfig,
} from '../common';
import { PivotAggsConfig } from './pivot_aggs';
export interface SimpleQuery { export interface SimpleQuery {
query_string: { query_string: {
query: string; query: string;
@ -72,72 +56,20 @@ export function isDefaultQuery(query: PivotQuery): boolean {
return isSimpleQuery(query) && query.query_string.query === '*'; return isSimpleQuery(query) && query.query_string.query === '*';
} }
export const getMissingBucketConfig = (
g: GroupByConfigWithUiSupport
): { missing_bucket?: boolean } => {
return g.missing_bucket !== undefined ? { missing_bucket: g.missing_bucket } : {};
};
export function getPreviewTransformRequestBody( export function getPreviewTransformRequestBody(
indexPatternTitle: IndexPattern['title'], indexPatternTitle: IndexPattern['title'],
query: PivotQuery, query: PivotQuery,
groupBy: PivotGroupByConfig[], partialRequest?: StepDefineExposedState['previewRequest'] | undefined
aggs: PivotAggsConfig[]
): PostTransformsPreviewRequestSchema { ): PostTransformsPreviewRequestSchema {
const index = indexPatternTitle.split(',').map((name: string) => name.trim()); const index = indexPatternTitle.split(',').map((name: string) => name.trim());
const request: PostTransformsPreviewRequestSchema = { return {
source: { source: {
index, index,
...(!isDefaultQuery(query) && !isMatchAllQuery(query) ? { query } : {}), ...(!isDefaultQuery(query) && !isMatchAllQuery(query) ? { query } : {}),
}, },
pivot: { ...(partialRequest ?? {}),
group_by: {},
aggregations: {},
},
}; };
groupBy.forEach((g) => {
if (isGroupByTerms(g)) {
const termsAgg: TermsAgg = {
terms: {
field: g.field,
...getMissingBucketConfig(g),
},
};
request.pivot.group_by[g.aggName] = termsAgg;
} else if (isGroupByHistogram(g)) {
const histogramAgg: HistogramAgg = {
histogram: {
field: g.field,
interval: g.interval,
...getMissingBucketConfig(g),
},
};
request.pivot.group_by[g.aggName] = histogramAgg;
} else if (isGroupByDateHistogram(g)) {
const dateHistogramAgg: DateHistogramAgg = {
date_histogram: {
field: g.field,
calendar_interval: g.calendar_interval,
...getMissingBucketConfig(g),
},
};
request.pivot.group_by[g.aggName] = dateHistogramAgg;
} else {
request.pivot.group_by[g.aggName] = getEsAggFromGroupByConfig(g);
}
});
aggs.forEach((agg) => {
const result = getEsAggFromAggConfig(agg);
if (result === null) {
return;
}
request.pivot.aggregations[agg.aggName] = result;
});
return request;
} }
export const getCreateTransformSettingsRequestBody = ( export const getCreateTransformSettingsRequestBody = (
@ -158,12 +90,11 @@ export const getCreateTransformRequestBody = (
indexPatternTitle: IndexPattern['title'], indexPatternTitle: IndexPattern['title'],
pivotState: StepDefineExposedState, pivotState: StepDefineExposedState,
transformDetailsState: StepDetailsExposedState transformDetailsState: StepDetailsExposedState
): PutTransformsRequestSchema => ({ ): PutTransformsPivotRequestSchema | PutTransformsLatestRequestSchema => ({
...getPreviewTransformRequestBody( ...getPreviewTransformRequestBody(
indexPatternTitle, indexPatternTitle,
getPivotQuery(pivotState.searchQuery), getPivotQuery(pivotState.searchQuery),
dictionaryToArray(pivotState.groupByList), pivotState.previewRequest
dictionaryToArray(pivotState.aggList)
), ),
// conditionally add optional description // conditionally add optional description
...(transformDetailsState.transformDescription !== '' ...(transformDetailsState.transformDescription !== ''

View file

@ -6,7 +6,7 @@
import { EuiTableActionsColumnType } from '@elastic/eui'; import { EuiTableActionsColumnType } from '@elastic/eui';
import { TransformId, TransformPivotConfig } from '../../../common/types/transform'; import { TransformConfigUnion, TransformId } from '../../../common/types/transform';
import { TransformStats } from '../../../common/types/transform_stats'; import { TransformStats } from '../../../common/types/transform_stats';
// Used to pass on attribute names to table columns // Used to pass on attribute names to table columns
@ -17,7 +17,7 @@ export enum TRANSFORM_LIST_COLUMN {
export interface TransformListRow { export interface TransformListRow {
id: TransformId; id: TransformId;
config: TransformPivotConfig; config: TransformConfigUnion;
mode?: string; // added property on client side to allow filtering by this field mode?: string; // added property on client side to allow filtering by this field
stats: TransformStats; stats: TransformStats;
} }

View file

@ -7,8 +7,19 @@
import { TRANSFORM_STATE } from '../../../common/constants'; import { TRANSFORM_STATE } from '../../../common/constants';
import { TransformListRow } from './transform_list'; import { TransformListRow } from './transform_list';
import {
PutTransformsLatestRequestSchema,
PutTransformsPivotRequestSchema,
} from '../../../common/api_schemas/transforms';
export function getTransformProgress(item: TransformListRow) { type TransformItem = Omit<TransformListRow, 'config'> & {
config:
| TransformListRow['config']
| PutTransformsLatestRequestSchema
| PutTransformsPivotRequestSchema;
};
export function getTransformProgress(item: TransformItem) {
if (isCompletedBatchTransform(item)) { if (isCompletedBatchTransform(item)) {
return 100; return 100;
} }
@ -17,7 +28,7 @@ export function getTransformProgress(item: TransformListRow) {
return progress !== undefined ? Math.round(progress) : undefined; return progress !== undefined ? Math.round(progress) : undefined;
} }
export function isCompletedBatchTransform(item: TransformListRow) { export function isCompletedBatchTransform(item: TransformItem) {
// If `checkpoint=1`, `sync` is missing from the config and state is stopped, // If `checkpoint=1`, `sync` is missing from the config and state is stopped,
// then this is a completed batch transform. // then this is a completed batch transform.
return ( return (

View file

@ -108,10 +108,7 @@ export const useDeleteIndexAndTargetIndex = (items: TransformListRow[]) => {
type SuccessCountField = keyof Omit<DeleteTransformStatus, 'destinationIndex'>; type SuccessCountField = keyof Omit<DeleteTransformStatus, 'destinationIndex'>;
export const useDeleteTransforms = () => { export const useDeleteTransforms = () => {
const { const { overlays } = useAppDependencies();
overlays,
ml: { extractErrorMessage },
} = useAppDependencies();
const toastNotifications = useToastNotifications(); const toastNotifications = useToastNotifications();
const api = useApi(); const api = useApi();
@ -188,7 +185,7 @@ export const useDeleteTransforms = () => {
}); });
} }
if (status.transformDeleted?.error) { if (status.transformDeleted?.error) {
const error = extractErrorMessage(status.transformDeleted.error); const error = status.transformDeleted.error.reason;
toastNotifications.addDanger({ toastNotifications.addDanger({
title: i18n.translate('xpack.transform.transformList.deleteTransformErrorMessage', { title: i18n.translate('xpack.transform.transformList.deleteTransformErrorMessage', {
defaultMessage: 'An error occurred deleting the transform {transformId}', defaultMessage: 'An error occurred deleting the transform {transformId}',
@ -201,7 +198,7 @@ export const useDeleteTransforms = () => {
} }
if (status.destIndexDeleted?.error) { if (status.destIndexDeleted?.error) {
const error = extractErrorMessage(status.destIndexDeleted.error); const error = status.destIndexDeleted.error.reason;
toastNotifications.addDanger({ toastNotifications.addDanger({
title: i18n.translate( title: i18n.translate(
'xpack.transform.deleteTransform.deleteAnalyticsWithIndexErrorMessage', 'xpack.transform.deleteTransform.deleteAnalyticsWithIndexErrorMessage',
@ -217,7 +214,7 @@ export const useDeleteTransforms = () => {
} }
if (status.destIndexPatternDeleted?.error) { if (status.destIndexPatternDeleted?.error) {
const error = extractErrorMessage(status.destIndexPatternDeleted.error); const error = status.destIndexPatternDeleted.error.reason;
toastNotifications.addDanger({ toastNotifications.addDanger({
title: i18n.translate( title: i18n.translate(
'xpack.transform.deleteTransform.deleteAnalyticsWithIndexPatternErrorMessage', 'xpack.transform.deleteTransform.deleteAnalyticsWithIndexPatternErrorMessage',

View file

@ -15,48 +15,51 @@ import { ES_FIELD_TYPES } from '../../../../../../src/plugins/data/common';
import type { PreviewMappingsProperties } from '../../../common/api_schemas/transforms'; import type { PreviewMappingsProperties } from '../../../common/api_schemas/transforms';
import { isPostTransformsPreviewResponseSchema } from '../../../common/api_schemas/type_guards'; import { isPostTransformsPreviewResponseSchema } from '../../../common/api_schemas/type_guards';
import { dictionaryToArray } from '../../../common/types/common';
import { getNestedProperty } from '../../../common/utils/object_utils'; import { getNestedProperty } from '../../../common/utils/object_utils';
import { RenderCellValue, UseIndexDataReturnType } from '../../shared_imports'; import { RenderCellValue, UseIndexDataReturnType } from '../../shared_imports';
import { getErrorMessage } from '../../../common/utils/errors'; import { getErrorMessage } from '../../../common/utils/errors';
import { useAppDependencies } from '../app_dependencies'; import { useAppDependencies } from '../app_dependencies';
import { import { getPreviewTransformRequestBody, PivotQuery } from '../common';
getPreviewTransformRequestBody,
PivotAggsConfigDict,
PivotGroupByConfigDict,
PivotGroupByConfig,
PivotQuery,
PivotAggsConfig,
} from '../common';
import { isPivotAggsWithExtendedForm } from '../common/pivot_aggs';
import { SearchItems } from './use_search_items'; import { SearchItems } from './use_search_items';
import { useApi } from './use_api'; import { useApi } from './use_api';
import { StepDefineExposedState } from '../sections/create_transform/components/step_define';
import {
isLatestPartialRequest,
isPivotPartialRequest,
} from '../sections/create_transform/components/step_define/common/types';
/** function sortColumns(groupByArr: string[]) {
* Checks if the aggregations collection is invalid.
*/
function isConfigInvalid(aggsArray: PivotAggsConfig[]): boolean {
return aggsArray.some((agg) => {
return (
(isPivotAggsWithExtendedForm(agg) && !agg.isValid()) ||
(agg.subAggs && isConfigInvalid(Object.values(agg.subAggs)))
);
});
}
function sortColumns(groupByArr: PivotGroupByConfig[]) {
return (a: string, b: string) => { return (a: string, b: string) => {
// make sure groupBy fields are always most left columns // make sure groupBy fields are always most left columns
if (groupByArr.some((d) => d.aggName === a) && groupByArr.some((d) => d.aggName === b)) { if (
groupByArr.some((aggName) => aggName === a) &&
groupByArr.some((aggName) => aggName === b)
) {
return a.localeCompare(b); return a.localeCompare(b);
} }
if (groupByArr.some((d) => d.aggName === a)) { if (groupByArr.some((aggName) => aggName === a)) {
return -1; return -1;
} }
if (groupByArr.some((d) => d.aggName === b)) { if (groupByArr.some((aggName) => aggName === b)) {
return 1;
}
return a.localeCompare(b);
};
}
function sortColumnsForLatest(sortField: string) {
return (a: string, b: string) => {
// make sure sort field is always the most left column
if (sortField === a && sortField === b) {
return a.localeCompare(b);
}
if (sortField === a) {
return -1;
}
if (sortField === b) {
return 1; return 1;
} }
return a.localeCompare(b); return a.localeCompare(b);
@ -66,8 +69,8 @@ function sortColumns(groupByArr: PivotGroupByConfig[]) {
export const usePivotData = ( export const usePivotData = (
indexPatternTitle: SearchItems['indexPattern']['title'], indexPatternTitle: SearchItems['indexPattern']['title'],
query: PivotQuery, query: PivotQuery,
aggs: PivotAggsConfigDict, validationStatus: StepDefineExposedState['validationStatus'],
groupBy: PivotGroupByConfigDict requestPayload: StepDefineExposedState['previewRequest']
): UseIndexDataReturnType => { ): UseIndexDataReturnType => {
const [ const [
previewMappingsProperties, previewMappingsProperties,
@ -78,14 +81,17 @@ export const usePivotData = (
ml: { formatHumanReadableDateTimeSeconds, multiColumnSortFactory, useDataGrid, INDEX_STATUS }, ml: { formatHumanReadableDateTimeSeconds, multiColumnSortFactory, useDataGrid, INDEX_STATUS },
} = useAppDependencies(); } = useAppDependencies();
const aggsArr = useMemo(() => dictionaryToArray(aggs), [aggs]);
const groupByArr = useMemo(() => dictionaryToArray(groupBy), [groupBy]);
// Filters mapping properties of type `object`, which get returned for nested field parents. // Filters mapping properties of type `object`, which get returned for nested field parents.
const columnKeys = Object.keys(previewMappingsProperties).filter( const columnKeys = Object.keys(previewMappingsProperties).filter(
(key) => previewMappingsProperties[key].type !== 'object' (key) => previewMappingsProperties[key].type !== 'object'
); );
if (isPivotPartialRequest(requestPayload)) {
const groupByArr = Object.keys(requestPayload.pivot.group_by);
columnKeys.sort(sortColumns(groupByArr)); columnKeys.sort(sortColumns(groupByArr));
} else if (isLatestPartialRequest(requestPayload)) {
columnKeys.sort(sortColumnsForLatest(requestPayload.latest.sort));
}
// EuiDataGrid State // EuiDataGrid State
const columns: EuiDataGridColumn[] = columnKeys.map((id) => { const columns: EuiDataGridColumn[] = columnKeys.map((id) => {
@ -141,18 +147,10 @@ export const usePivotData = (
} = dataGrid; } = dataGrid;
const getPreviewData = async () => { const getPreviewData = async () => {
if (aggsArr.length === 0 || groupByArr.length === 0) { if (!validationStatus.isValid) {
setTableItems([]); setTableItems([]);
setRowCount(0); setRowCount(0);
setNoDataMessage( setNoDataMessage(validationStatus.errorMessage!);
i18n.translate('xpack.transform.pivotPreview.PivotPreviewIncompleteConfigCalloutBody', {
defaultMessage: 'Please choose at least one group-by field and aggregation.',
})
);
return;
}
if (isConfigInvalid(aggsArr)) {
return; return;
} }
@ -160,12 +158,7 @@ export const usePivotData = (
setNoDataMessage(''); setNoDataMessage('');
setStatus(INDEX_STATUS.LOADING); setStatus(INDEX_STATUS.LOADING);
const previewRequest = getPreviewTransformRequestBody( const previewRequest = getPreviewTransformRequestBody(indexPatternTitle, query, requestPayload);
indexPatternTitle,
query,
groupByArr,
aggsArr
);
const resp = await api.getTransformsPreview(previewRequest); const resp = await api.getTransformsPreview(previewRequest);
if (!isPostTransformsPreviewResponseSchema(resp)) { if (!isPostTransformsPreviewResponseSchema(resp)) {
@ -204,8 +197,7 @@ export const usePivotData = (
/* eslint-disable react-hooks/exhaustive-deps */ /* eslint-disable react-hooks/exhaustive-deps */
}, [ }, [
indexPatternTitle, indexPatternTitle,
aggsArr, JSON.stringify([requestPayload, query]),
JSON.stringify([groupByArr, query]),
/* eslint-enable react-hooks/exhaustive-deps */ /* eslint-enable react-hooks/exhaustive-deps */
]); ]);

View file

@ -47,7 +47,8 @@ export const useStartTransforms = () => {
for (const transformId in results) { for (const transformId in results) {
// hasOwnProperty check to ensure only properties on object itself, and not its prototypes // hasOwnProperty check to ensure only properties on object itself, and not its prototypes
if (results.hasOwnProperty(transformId)) { if (results.hasOwnProperty(transformId)) {
if (results[transformId].success === true) { const result = results[transformId];
if (result.success === true) {
toastNotifications.addSuccess( toastNotifications.addSuccess(
i18n.translate('xpack.transform.transformList.startTransformSuccessMessage', { i18n.translate('xpack.transform.transformList.startTransformSuccessMessage', {
defaultMessage: 'Request to start transform {transformId} acknowledged.', defaultMessage: 'Request to start transform {transformId} acknowledged.',
@ -55,12 +56,13 @@ export const useStartTransforms = () => {
}) })
); );
} else { } else {
toastNotifications.addDanger( toastNotifications.addError(new Error(JSON.stringify(result.error!.caused_by, null, 2)), {
i18n.translate('xpack.transform.transformList.startTransformErrorMessage', { title: i18n.translate('xpack.transform.transformList.startTransformErrorMessage', {
defaultMessage: 'An error occurred starting the transform {transformId}', defaultMessage: 'An error occurred starting the transform {transformId}',
values: { transformId }, values: { transformId },
}) }),
); toastMessage: result.error!.reason,
});
} }
} }
} }

View file

@ -25,10 +25,7 @@ import {
import { toMountPoint } from '../../../../../../../../../src/plugins/kibana_react/public'; import { toMountPoint } from '../../../../../../../../../src/plugins/kibana_react/public';
import type { import type { PutTransformsResponseSchema } from '../../../../../../common/api_schemas/transforms';
PutTransformsRequestSchema,
PutTransformsResponseSchema,
} from '../../../../../../common/api_schemas/transforms';
import { import {
isGetTransformsStatsResponseSchema, isGetTransformsStatsResponseSchema,
isPutTransformsResponseSchema, isPutTransformsResponseSchema,
@ -44,6 +41,10 @@ import { useAppDependencies, useToastNotifications } from '../../../../app_depen
import { RedirectToTransformManagement } from '../../../../common/navigation'; import { RedirectToTransformManagement } from '../../../../common/navigation';
import { ToastNotificationText } from '../../../../components'; import { ToastNotificationText } from '../../../../components';
import { DuplicateIndexPatternError } from '../../../../../../../../../src/plugins/data/public'; import { DuplicateIndexPatternError } from '../../../../../../../../../src/plugins/data/public';
import {
PutTransformsLatestRequestSchema,
PutTransformsPivotRequestSchema,
} from '../../../../../../common/api_schemas/transforms';
export interface StepDetailsExposedState { export interface StepDetailsExposedState {
created: boolean; created: boolean;
@ -62,7 +63,7 @@ export function getDefaultStepCreateState(): StepDetailsExposedState {
export interface StepCreateFormProps { export interface StepCreateFormProps {
createIndexPattern: boolean; createIndexPattern: boolean;
transformId: string; transformId: string;
transformConfig: PutTransformsRequestSchema; transformConfig: PutTransformsPivotRequestSchema | PutTransformsLatestRequestSchema;
overrides: StepDetailsExposedState; overrides: StepDetailsExposedState;
timeFieldName?: string | undefined; timeFieldName?: string | undefined;
onChange(s: StepDetailsExposedState): void; onChange(s: StepDetailsExposedState): void;

View file

@ -8,7 +8,11 @@ import { isEqual } from 'lodash';
import { Dictionary } from '../../../../../../../common/types/common'; import { Dictionary } from '../../../../../../../common/types/common';
import { PivotSupportedAggs } from '../../../../../../../common/types/pivot_aggs'; import { PivotSupportedAggs } from '../../../../../../../common/types/pivot_aggs';
import { TransformPivotConfig } from '../../../../../../../common/types/transform'; import {
isLatestTransform,
isPivotTransform,
TransformBaseConfig,
} from '../../../../../../../common/types/transform';
import { import {
matchAllQuery, matchAllQuery,
@ -21,13 +25,24 @@ import {
import { StepDefineExposedState } from './types'; import { StepDefineExposedState } from './types';
import { getAggConfigFromEsAgg } from '../../../../../common/pivot_aggs'; import { getAggConfigFromEsAgg } from '../../../../../common/pivot_aggs';
import { TRANSFORM_FUNCTION } from '../../../../../../../common/constants';
import { StepDefineFormProps } from '../step_define_form';
import { validateLatestConfig } from '../hooks/use_latest_function_config';
import { validatePivotConfig } from '../hooks/use_pivot_config';
export function applyTransformConfigToDefineState( export function applyTransformConfigToDefineState(
state: StepDefineExposedState, state: StepDefineExposedState,
transformConfig?: TransformPivotConfig transformConfig?: TransformBaseConfig,
indexPattern?: StepDefineFormProps['searchItems']['indexPattern']
): StepDefineExposedState { ): StepDefineExposedState {
if (transformConfig === undefined) {
return state;
}
if (isPivotTransform(transformConfig)) {
state.transformFunction = TRANSFORM_FUNCTION.PIVOT;
// apply the transform configuration to wizard DEFINE state // apply the transform configuration to wizard DEFINE state
if (transformConfig !== undefined) {
// transform aggregations config to wizard state // transform aggregations config to wizard state
state.aggList = Object.keys(transformConfig.pivot.aggregations).reduce((aggList, aggName) => { state.aggList = Object.keys(transformConfig.pivot.aggregations).reduce((aggList, aggName) => {
const aggConfig = transformConfig.pivot.aggregations[ const aggConfig = transformConfig.pivot.aggregations[
@ -53,6 +68,34 @@ export function applyTransformConfigToDefineState(
{} as PivotGroupByConfigDict {} as PivotGroupByConfigDict
); );
state.previewRequest = {
pivot: transformConfig.pivot,
};
state.validationStatus = validatePivotConfig(transformConfig.pivot);
}
if (isLatestTransform(transformConfig)) {
state.transformFunction = TRANSFORM_FUNCTION.LATEST;
state.latestConfig = {
unique_key: transformConfig.latest.unique_key.map((v) => ({
value: v,
label: indexPattern ? indexPattern.fields.find((f) => f.name === v)?.displayName ?? v : v,
})),
sort: {
value: transformConfig.latest.sort,
label: indexPattern
? indexPattern.fields.find((f) => f.name === transformConfig.latest.sort)?.displayName ??
transformConfig.latest.sort
: transformConfig.latest.sort,
},
};
state.previewRequest = {
latest: transformConfig.latest,
};
state.validationStatus = validateLatestConfig(transformConfig.latest);
}
// only apply the query from the transform config to wizard state if it's not the default query // only apply the query from the transform config to wizard state if it's not the default query
const query = transformConfig.source.query; const query = transformConfig.source.query;
if (query !== undefined && !isEqual(query, matchAllQuery)) { if (query !== undefined && !isEqual(query, matchAllQuery)) {
@ -63,7 +106,6 @@ export function applyTransformConfigToDefineState(
// applying a transform config to wizard state will always result in a valid configuration // applying a transform config to wizard state will always result in a valid configuration
state.valid = true; state.valid = true;
}
return state; return state;
} }

View file

@ -9,9 +9,13 @@ import { SearchItems } from '../../../../../hooks/use_search_items';
import { defaultSearch, QUERY_LANGUAGE_KUERY } from './constants'; import { defaultSearch, QUERY_LANGUAGE_KUERY } from './constants';
import { StepDefineExposedState } from './types'; import { StepDefineExposedState } from './types';
import { TRANSFORM_FUNCTION } from '../../../../../../../common/constants';
import { LatestFunctionConfigUI } from '../../../../../../../common/types/transform';
export function getDefaultStepDefineState(searchItems: SearchItems): StepDefineExposedState { export function getDefaultStepDefineState(searchItems: SearchItems): StepDefineExposedState {
return { return {
transformFunction: TRANSFORM_FUNCTION.PIVOT,
latestConfig: {} as LatestFunctionConfigUI,
aggList: {} as PivotAggsConfigDict, aggList: {} as PivotAggsConfigDict,
groupByList: {} as PivotGroupByConfigDict, groupByList: {} as PivotGroupByConfigDict,
isAdvancedPivotEditorEnabled: false, isAdvancedPivotEditorEnabled: false,
@ -21,5 +25,9 @@ export function getDefaultStepDefineState(searchItems: SearchItems): StepDefineE
searchQuery: searchItems.savedSearch !== undefined ? searchItems.combinedQuery : defaultSearch, searchQuery: searchItems.savedSearch !== undefined ? searchItems.combinedQuery : defaultSearch,
sourceConfigUpdated: false, sourceConfigUpdated: false,
valid: false, valid: false,
validationStatus: {
isValid: false,
},
previewRequest: undefined,
}; };
} }

View file

@ -12,6 +12,12 @@ import { PivotAggsConfigDict, PivotGroupByConfigDict } from '../../../../../comm
import { SavedSearchQuery } from '../../../../../hooks/use_search_items'; import { SavedSearchQuery } from '../../../../../hooks/use_search_items';
import { QUERY_LANGUAGE } from './constants'; import { QUERY_LANGUAGE } from './constants';
import { TransformFunction } from '../../../../../../../common/constants';
import {
LatestFunctionConfigUI,
PivotConfigDefinition,
} from '../../../../../../../common/types/transform';
import { LatestFunctionConfig } from '../../../../../../../common/api_schemas/transforms';
export interface ErrorMessage { export interface ErrorMessage {
query: string; query: string;
@ -24,8 +30,10 @@ export interface Field {
} }
export interface StepDefineExposedState { export interface StepDefineExposedState {
transformFunction: TransformFunction;
aggList: PivotAggsConfigDict; aggList: PivotAggsConfigDict;
groupByList: PivotGroupByConfigDict; groupByList: PivotGroupByConfigDict;
latestConfig: LatestFunctionConfigUI;
isAdvancedPivotEditorEnabled: boolean; isAdvancedPivotEditorEnabled: boolean;
isAdvancedSourceEditorEnabled: boolean; isAdvancedSourceEditorEnabled: boolean;
searchLanguage: QUERY_LANGUAGE; searchLanguage: QUERY_LANGUAGE;
@ -33,4 +41,17 @@ export interface StepDefineExposedState {
searchQuery: string | SavedSearchQuery; searchQuery: string | SavedSearchQuery;
sourceConfigUpdated: boolean; sourceConfigUpdated: boolean;
valid: boolean; valid: boolean;
validationStatus: { isValid: boolean; errorMessage?: string };
/**
* Undefined when the form is incomplete or invalid
*/
previewRequest: { latest: LatestFunctionConfig } | { pivot: PivotConfigDefinition } | undefined;
}
export function isPivotPartialRequest(arg: any): arg is { pivot: PivotConfigDefinition } {
return typeof arg === 'object' && arg.hasOwnProperty('pivot');
}
export function isLatestPartialRequest(arg: any): arg is { latest: LatestFunctionConfig } {
return typeof arg === 'object' && arg.hasOwnProperty('latest');
} }

View file

@ -0,0 +1,127 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { useCallback, useMemo, useState } from 'react';
import { i18n } from '@kbn/i18n';
import { EuiComboBoxOptionOption } from '@elastic/eui';
import { LatestFunctionConfigUI } from '../../../../../../../common/types/transform';
import { StepDefineFormProps } from '../step_define_form';
import { StepDefineExposedState } from '../common';
import { LatestFunctionConfig } from '../../../../../../../common/api_schemas/transforms';
/**
* Latest function config mapper between API and UI
*/
export const latestConfigMapper = {
toAPIConfig(uiConfig: LatestFunctionConfigUI): LatestFunctionConfig | undefined {
if (uiConfig.sort === undefined || !uiConfig.unique_key?.length) {
return;
}
return {
unique_key: uiConfig.unique_key.map((v) => v.value!),
sort: uiConfig.sort.value!,
};
},
toUIConfig() {},
};
/**
* Provides available options for unique_key and sort fields
* @param indexPattern
*/
function getOptions(indexPattern: StepDefineFormProps['searchItems']['indexPattern']) {
const uniqueKeyOptions: Array<EuiComboBoxOptionOption<string>> = [];
const sortFieldOptions: Array<EuiComboBoxOptionOption<string>> = [];
const ignoreFieldNames = new Set(['_id', '_index', '_type']);
for (const field of indexPattern.fields) {
if (ignoreFieldNames.has(field.name)) {
continue;
}
if (field.aggregatable) {
uniqueKeyOptions.push({ label: field.displayName, value: field.name });
}
if (field.sortable) {
sortFieldOptions.push({ label: field.displayName, value: field.name });
}
}
return { uniqueKeyOptions, sortFieldOptions };
}
/**
* Validates latest function configuration
*/
export function validateLatestConfig(config?: LatestFunctionConfig) {
const isValid: boolean = !!config?.unique_key?.length && config?.sort !== undefined;
return {
isValid,
...(isValid
? {}
: {
errorMessage: i18n.translate(
'xpack.transform.latestPreview.latestPreviewIncompleteConfigCalloutBody',
{
defaultMessage: 'Please choose at least one unique key and sort field.',
}
),
}),
};
}
export function useLatestFunctionConfig(
defaults: StepDefineExposedState['latestConfig'],
indexPattern: StepDefineFormProps['searchItems']['indexPattern']
): {
config: LatestFunctionConfigUI;
uniqueKeyOptions: Array<EuiComboBoxOptionOption<string>>;
sortFieldOptions: Array<EuiComboBoxOptionOption<string>>;
updateLatestFunctionConfig: (update: Partial<LatestFunctionConfigUI>) => void;
validationStatus: { isValid: boolean; errorMessage?: string };
requestPayload: { latest: LatestFunctionConfig } | undefined;
} {
const [config, setLatestFunctionConfig] = useState<LatestFunctionConfigUI>({
unique_key: defaults.unique_key,
sort: defaults.sort,
});
const { uniqueKeyOptions, sortFieldOptions } = useMemo(() => getOptions(indexPattern), [
indexPattern,
]);
const updateLatestFunctionConfig = useCallback(
(update) =>
setLatestFunctionConfig({
...config,
...update,
}),
[config]
);
const requestPayload: { latest: LatestFunctionConfig } | undefined = useMemo(() => {
const latest = latestConfigMapper.toAPIConfig(config);
return latest ? { latest } : undefined;
}, [config]);
const validationStatus = useMemo(() => validateLatestConfig(requestPayload?.latest), [
requestPayload?.latest,
]);
return {
config,
uniqueKeyOptions,
sortFieldOptions,
updateLatestFunctionConfig,
validationStatus,
requestPayload,
};
}
export type LatestFunctionService = ReturnType<typeof useLatestFunctionConfig>;

View file

@ -0,0 +1,27 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { getMissingBucketConfig } from './use_pivot_config';
import { PIVOT_SUPPORTED_GROUP_BY_AGGS, PivotGroupByConfig } from '../../../../../common';
const groupByTerms: PivotGroupByConfig = {
agg: PIVOT_SUPPORTED_GROUP_BY_AGGS.TERMS,
field: 'the-group-by-field',
aggName: 'the-group-by-agg-name',
dropDownName: 'the-group-by-drop-down-name',
};
describe('usePivotConfig', () => {
test('getMissingBucketConfig()', () => {
expect(getMissingBucketConfig(groupByTerms)).toEqual({});
expect(getMissingBucketConfig({ ...groupByTerms, ...{ missing_bucket: true } })).toEqual({
missing_bucket: true,
});
expect(getMissingBucketConfig({ ...groupByTerms, ...{ missing_bucket: false } })).toEqual({
missing_bucket: false,
});
});
});

View file

@ -5,6 +5,7 @@
*/ */
import { useCallback, useMemo, useState } from 'react'; import { useCallback, useMemo, useState } from 'react';
import { i18n } from '@kbn/i18n';
import { AggName } from '../../../../../../../common/types/aggregations'; import { AggName } from '../../../../../../../common/types/aggregations';
import { dictionaryToArray } from '../../../../../../../common/types/common'; import { dictionaryToArray } from '../../../../../../../common/types/common';
@ -12,6 +13,12 @@ import { dictionaryToArray } from '../../../../../../../common/types/common';
import { useToastNotifications } from '../../../../../app_dependencies'; import { useToastNotifications } from '../../../../../app_dependencies';
import { import {
DropDownLabel, DropDownLabel,
getEsAggFromAggConfig,
getEsAggFromGroupByConfig,
GroupByConfigWithUiSupport,
isGroupByDateHistogram,
isGroupByHistogram,
isGroupByTerms,
PivotAggsConfig, PivotAggsConfig,
PivotAggsConfigDict, PivotAggsConfigDict,
PivotGroupByConfig, PivotGroupByConfig,
@ -23,6 +30,14 @@ import {
StepDefineExposedState, StepDefineExposedState,
} from '../common'; } from '../common';
import { StepDefineFormProps } from '../step_define_form'; import { StepDefineFormProps } from '../step_define_form';
import { isPivotAggsWithExtendedForm } from '../../../../../common/pivot_aggs';
import {
DateHistogramAgg,
HistogramAgg,
TermsAgg,
} from '../../../../../../../common/types/pivot_group_by';
import { PivotTransformPreviewRequestSchema } from '../../../../../../../common/api_schemas/transforms';
import { TransformPivotConfig } from '../../../../../../../common/types/transform';
/** /**
* Clones aggregation configuration and updates parent references * Clones aggregation configuration and updates parent references
@ -43,6 +58,37 @@ function cloneAggItem(item: PivotAggsConfig, parentRef?: PivotAggsConfig) {
return newItem; return newItem;
} }
/**
* Checks if the aggregations collection is invalid.
*/
function isConfigInvalid(aggsArray: PivotAggsConfig[]): boolean {
return aggsArray.some((agg) => {
return (
(isPivotAggsWithExtendedForm(agg) && !agg.isValid()) ||
(agg.subAggs && isConfigInvalid(Object.values(agg.subAggs)))
);
});
}
export function validatePivotConfig(config: TransformPivotConfig['pivot']) {
const valid =
Object.values(config.aggregations).length > 0 && Object.values(config.group_by).length > 0;
const isValid: boolean = valid && !isConfigInvalid(dictionaryToArray(config.aggregations));
return {
isValid,
...(isValid
? {}
: {
errorMessage: i18n.translate(
'xpack.transform.pivotPreview.PivotPreviewIncompleteConfigCalloutBody',
{
defaultMessage: 'Please choose at least one group-by field and aggregation.',
}
),
}),
};
}
/** /**
* Returns a root aggregation configuration * Returns a root aggregation configuration
* for provided aggregation item. * for provided aggregation item.
@ -55,6 +101,12 @@ function getRootAggregation(item: PivotAggsConfig) {
return rootItem; return rootItem;
} }
export const getMissingBucketConfig = (
g: GroupByConfigWithUiSupport
): { missing_bucket?: boolean } => {
return g.missing_bucket !== undefined ? { missing_bucket: g.missing_bucket } : {};
};
export const usePivotConfig = ( export const usePivotConfig = (
defaults: StepDefineExposedState, defaults: StepDefineExposedState,
indexPattern: StepDefineFormProps['searchItems']['indexPattern'] indexPattern: StepDefineFormProps['searchItems']['indexPattern']
@ -262,7 +314,60 @@ export const usePivotConfig = (
const pivotAggsArr = useMemo(() => dictionaryToArray(aggList), [aggList]); const pivotAggsArr = useMemo(() => dictionaryToArray(aggList), [aggList]);
const pivotGroupByArr = useMemo(() => dictionaryToArray(groupByList), [groupByList]); const pivotGroupByArr = useMemo(() => dictionaryToArray(groupByList), [groupByList]);
const valid = pivotGroupByArr.length > 0 && pivotAggsArr.length > 0; const requestPayload = useMemo(() => {
const request = {
pivot: {
group_by: {},
aggregations: {},
} as PivotTransformPreviewRequestSchema['pivot'],
};
pivotGroupByArr.forEach((g) => {
if (isGroupByTerms(g)) {
const termsAgg: TermsAgg = {
terms: {
field: g.field,
},
...getMissingBucketConfig(g),
};
request.pivot.group_by[g.aggName] = termsAgg;
} else if (isGroupByHistogram(g)) {
const histogramAgg: HistogramAgg = {
histogram: {
field: g.field,
interval: g.interval,
},
...getMissingBucketConfig(g),
};
request.pivot.group_by[g.aggName] = histogramAgg;
} else if (isGroupByDateHistogram(g)) {
const dateHistogramAgg: DateHistogramAgg = {
date_histogram: {
field: g.field,
calendar_interval: g.calendar_interval,
},
...getMissingBucketConfig(g),
};
request.pivot.group_by[g.aggName] = dateHistogramAgg;
} else {
request.pivot.group_by[g.aggName] = getEsAggFromGroupByConfig(g);
}
});
pivotAggsArr.forEach((agg) => {
const result = getEsAggFromAggConfig(agg);
if (result === null) {
return;
}
request.pivot.aggregations[agg.aggName] = result;
});
return request;
}, [pivotAggsArr, pivotGroupByArr]);
const validationStatus = useMemo(() => {
return validatePivotConfig(requestPayload.pivot);
}, [requestPayload]);
const actions = useMemo(() => { const actions = useMemo(() => {
return { return {
@ -302,7 +407,8 @@ export const usePivotConfig = (
groupByOptionsData, groupByOptionsData,
pivotAggsArr, pivotAggsArr,
pivotGroupByArr, pivotGroupByArr,
valid, validationStatus,
requestPayload,
}, },
}; };
}, [ }, [
@ -315,6 +421,9 @@ export const usePivotConfig = (
groupByOptionsData, groupByOptionsData,
pivotAggsArr, pivotAggsArr,
pivotGroupByArr, pivotGroupByArr,
valid, validationStatus,
requestPayload,
]); ]);
}; };
export type PivotService = ReturnType<typeof usePivotConfig>;

View file

@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License. * you may not use this file except in compliance with the Elastic License.
*/ */
import { useEffect } from 'react'; import { useEffect, useState } from 'react';
import { getPreviewTransformRequestBody } from '../../../../../common'; import { getPreviewTransformRequestBody } from '../../../../../common';
@ -16,6 +16,8 @@ import { useAdvancedPivotEditor } from './use_advanced_pivot_editor';
import { useAdvancedSourceEditor } from './use_advanced_source_editor'; import { useAdvancedSourceEditor } from './use_advanced_source_editor';
import { usePivotConfig } from './use_pivot_config'; import { usePivotConfig } from './use_pivot_config';
import { useSearchBar } from './use_search_bar'; import { useSearchBar } from './use_search_bar';
import { useLatestFunctionConfig } from './use_latest_function_config';
import { TRANSFORM_FUNCTION } from '../../../../../../../common/constants';
export type StepDefineFormHook = ReturnType<typeof useStepDefineForm>; export type StepDefineFormHook = ReturnType<typeof useStepDefineForm>;
@ -23,14 +25,16 @@ export const useStepDefineForm = ({ overrides, onChange, searchItems }: StepDefi
const defaults = { ...getDefaultStepDefineState(searchItems), ...overrides }; const defaults = { ...getDefaultStepDefineState(searchItems), ...overrides };
const { indexPattern } = searchItems; const { indexPattern } = searchItems;
const [transformFunction, setTransformFunction] = useState(defaults.transformFunction);
const searchBar = useSearchBar(defaults, indexPattern); const searchBar = useSearchBar(defaults, indexPattern);
const pivotConfig = usePivotConfig(defaults, indexPattern); const pivotConfig = usePivotConfig(defaults, indexPattern);
const latestFunctionConfig = useLatestFunctionConfig(defaults.latestConfig, indexPattern);
const previewRequest = getPreviewTransformRequestBody( const previewRequest = getPreviewTransformRequestBody(
indexPattern.title, indexPattern.title,
searchBar.state.pivotQuery, searchBar.state.pivotQuery,
pivotConfig.state.pivotGroupByArr, pivotConfig.state.requestPayload
pivotConfig.state.pivotAggsArr
); );
// pivot config hook // pivot config hook
@ -44,8 +48,7 @@ export const useStepDefineForm = ({ overrides, onChange, searchItems }: StepDefi
const previewRequestUpdate = getPreviewTransformRequestBody( const previewRequestUpdate = getPreviewTransformRequestBody(
indexPattern.title, indexPattern.title,
searchBar.state.pivotQuery, searchBar.state.pivotQuery,
pivotConfig.state.pivotGroupByArr, pivotConfig.state.requestPayload
pivotConfig.state.pivotAggsArr
); );
const stringifiedSourceConfigUpdate = JSON.stringify( const stringifiedSourceConfigUpdate = JSON.stringify(
@ -58,6 +61,8 @@ export const useStepDefineForm = ({ overrides, onChange, searchItems }: StepDefi
} }
onChange({ onChange({
transformFunction,
latestConfig: latestFunctionConfig.config,
aggList: pivotConfig.state.aggList, aggList: pivotConfig.state.aggList,
groupByList: pivotConfig.state.groupByList, groupByList: pivotConfig.state.groupByList,
isAdvancedPivotEditorEnabled: advancedPivotEditor.state.isAdvancedPivotEditorEnabled, isAdvancedPivotEditorEnabled: advancedPivotEditor.state.isAdvancedPivotEditorEnabled,
@ -66,7 +71,18 @@ export const useStepDefineForm = ({ overrides, onChange, searchItems }: StepDefi
searchString: searchBar.state.searchString, searchString: searchBar.state.searchString,
searchQuery: searchBar.state.searchQuery, searchQuery: searchBar.state.searchQuery,
sourceConfigUpdated: advancedSourceEditor.state.sourceConfigUpdated, sourceConfigUpdated: advancedSourceEditor.state.sourceConfigUpdated,
valid: pivotConfig.state.valid, valid:
transformFunction === TRANSFORM_FUNCTION.PIVOT
? pivotConfig.state.validationStatus.isValid
: latestFunctionConfig.validationStatus.isValid,
validationStatus:
transformFunction === TRANSFORM_FUNCTION.PIVOT
? pivotConfig.state.validationStatus
: latestFunctionConfig.validationStatus,
previewRequest:
transformFunction === TRANSFORM_FUNCTION.PIVOT
? pivotConfig.state.requestPayload
: latestFunctionConfig.requestPayload,
}); });
// custom comparison // custom comparison
/* eslint-disable react-hooks/exhaustive-deps */ /* eslint-disable react-hooks/exhaustive-deps */
@ -75,13 +91,18 @@ export const useStepDefineForm = ({ overrides, onChange, searchItems }: StepDefi
JSON.stringify(advancedSourceEditor.state), JSON.stringify(advancedSourceEditor.state),
pivotConfig.state, pivotConfig.state,
JSON.stringify(searchBar.state), JSON.stringify(searchBar.state),
latestFunctionConfig.config,
transformFunction,
/* eslint-enable react-hooks/exhaustive-deps */ /* eslint-enable react-hooks/exhaustive-deps */
]); ]);
return { return {
transformFunction,
setTransformFunction,
advancedPivotEditor, advancedPivotEditor,
advancedSourceEditor, advancedSourceEditor,
pivotConfig, pivotConfig,
latestFunctionConfig,
searchBar, searchBar,
}; };
}; };

View file

@ -0,0 +1,75 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React, { FC } from 'react';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import { EuiComboBox, EuiFormRow } from '@elastic/eui';
import { LatestFunctionService } from './hooks/use_latest_function_config';
interface LatestFunctionFormProps {
latestFunctionService: LatestFunctionService;
}
export const LatestFunctionForm: FC<LatestFunctionFormProps> = ({ latestFunctionService }) => {
return (
<>
<EuiFormRow
fullWidth
label={
<FormattedMessage
id="xpack.transform.stepDefineForm.uniqueKeysLabel"
defaultMessage="Unique keys"
/>
}
>
<EuiComboBox<string>
fullWidth
placeholder={i18n.translate('xpack.transform.stepDefineForm.uniqueKeysPlaceholder', {
defaultMessage: 'Add unique keys ...',
})}
options={latestFunctionService.uniqueKeyOptions}
selectedOptions={latestFunctionService.config.unique_key ?? []}
onChange={(selected) => {
latestFunctionService.updateLatestFunctionConfig({
unique_key: selected,
});
}}
isClearable={false}
data-test-subj="transformWizardUniqueKeysSelector"
/>
</EuiFormRow>
<EuiFormRow
fullWidth
label={
<FormattedMessage
id="xpack.transform.stepDefineForm.sortLabel"
defaultMessage="Sort field"
/>
}
>
<EuiComboBox
fullWidth
placeholder={i18n.translate('xpack.transform.stepDefineForm.sortPlaceholder', {
defaultMessage: 'Add a sort field ...',
})}
singleSelection={{ asPlainText: true }}
options={latestFunctionService.sortFieldOptions}
selectedOptions={
latestFunctionService.config.sort ? [latestFunctionService.config.sort] : []
}
onChange={(selected) => {
latestFunctionService.updateLatestFunctionConfig({
sort: { value: selected[0].value, label: selected[0].label as string },
});
}}
isClearable={false}
data-test-subj="transformWizardSortFieldSelector"
/>
</EuiFormRow>
</>
);
};

View file

@ -53,6 +53,9 @@ import { SourceSearchBar } from '../source_search_bar';
import { StepDefineExposedState } from './common'; import { StepDefineExposedState } from './common';
import { useStepDefineForm } from './hooks/use_step_define_form'; import { useStepDefineForm } from './hooks/use_step_define_form';
import { getAggConfigFromEsAgg } from '../../../../common/pivot_aggs'; import { getAggConfigFromEsAgg } from '../../../../common/pivot_aggs';
import { TransformFunctionSelector } from './transform_function_selector';
import { TRANSFORM_FUNCTION } from '../../../../../../common/constants';
import { LatestFunctionForm } from './latest_function_form';
export interface StepDefineFormProps { export interface StepDefineFormProps {
overrides?: StepDefineExposedState; overrides?: StepDefineExposedState;
@ -80,7 +83,6 @@ export const StepDefineForm: FC<StepDefineFormProps> = React.memo((props) => {
isAdvancedSourceEditorEnabled, isAdvancedSourceEditorEnabled,
isAdvancedSourceEditorApplyButtonEnabled, isAdvancedSourceEditorApplyButtonEnabled,
} = stepDefineForm.advancedSourceEditor.state; } = stepDefineForm.advancedSourceEditor.state;
const { aggList, groupByList, pivotGroupByArr, pivotAggsArr } = stepDefineForm.pivotConfig.state;
const pivotQuery = stepDefineForm.searchBar.state.pivotQuery; const pivotQuery = stepDefineForm.searchBar.state.pivotQuery;
const indexPreviewProps = { const indexPreviewProps = {
@ -89,15 +91,21 @@ export const StepDefineForm: FC<StepDefineFormProps> = React.memo((props) => {
toastNotifications, toastNotifications,
}; };
const { requestPayload, validationStatus } =
stepDefineForm.transformFunction === TRANSFORM_FUNCTION.PIVOT
? stepDefineForm.pivotConfig.state
: stepDefineForm.latestFunctionConfig;
const previewRequest = getPreviewTransformRequestBody( const previewRequest = getPreviewTransformRequestBody(
indexPattern.title, indexPattern.title,
pivotQuery, pivotQuery,
pivotGroupByArr, stepDefineForm.transformFunction === TRANSFORM_FUNCTION.PIVOT
pivotAggsArr ? stepDefineForm.pivotConfig.state.requestPayload
: stepDefineForm.latestFunctionConfig.requestPayload
); );
const pivotPreviewProps = { const pivotPreviewProps = {
...usePivotData(indexPattern.title, pivotQuery, aggList, groupByList), ...usePivotData(indexPattern.title, pivotQuery, validationStatus, requestPayload),
dataTestSubj: 'transformPivotPreview', dataTestSubj: 'transformPivotPreview',
toastNotifications, toastNotifications,
}; };
@ -171,6 +179,13 @@ export const StepDefineForm: FC<StepDefineFormProps> = React.memo((props) => {
return ( return (
<div data-test-subj="transformStepDefineForm"> <div data-test-subj="transformStepDefineForm">
<EuiForm> <EuiForm>
<EuiFormRow fullWidth>
<TransformFunctionSelector
selectedFunction={stepDefineForm.transformFunction}
onChange={stepDefineForm.setTransformFunction}
/>
</EuiFormRow>
{searchItems.savedSearch === undefined && ( {searchItems.savedSearch === undefined && (
<EuiFormRow <EuiFormRow
label={i18n.translate('xpack.transform.stepDefineForm.indexPatternLabel', { label={i18n.translate('xpack.transform.stepDefineForm.indexPatternLabel', {
@ -180,6 +195,7 @@ export const StepDefineForm: FC<StepDefineFormProps> = React.memo((props) => {
<span>{indexPattern.title}</span> <span>{indexPattern.title}</span>
</EuiFormRow> </EuiFormRow>
)} )}
<EuiFormRow <EuiFormRow
fullWidth fullWidth
hasEmptyLabelSpace={searchItems?.savedSearch?.id === undefined} hasEmptyLabelSpace={searchItems?.savedSearch?.id === undefined}
@ -284,6 +300,7 @@ export const StepDefineForm: FC<StepDefineFormProps> = React.memo((props) => {
</EuiForm> </EuiForm>
<EuiHorizontalRule margin="m" /> <EuiHorizontalRule margin="m" />
<EuiForm> <EuiForm>
{stepDefineForm.transformFunction === TRANSFORM_FUNCTION.PIVOT ? (
<EuiFlexGroup justifyContent="spaceBetween"> <EuiFlexGroup justifyContent="spaceBetween">
{/* Flex Column #1: Pivot Config Form / Advanced Pivot Config Editor */} {/* Flex Column #1: Pivot Config Form / Advanced Pivot Config Editor */}
<EuiFlexItem> <EuiFlexItem>
@ -358,6 +375,10 @@ export const StepDefineForm: FC<StepDefineFormProps> = React.memo((props) => {
</EuiFlexGroup> </EuiFlexGroup>
</EuiFlexItem> </EuiFlexItem>
</EuiFlexGroup> </EuiFlexGroup>
) : null}
{stepDefineForm.transformFunction === TRANSFORM_FUNCTION.LATEST ? (
<LatestFunctionForm latestFunctionService={stepDefineForm.latestFunctionConfig} />
) : null}
</EuiForm> </EuiForm>
<EuiSpacer size="m" /> <EuiSpacer size="m" />
<DataGrid {...pivotPreviewProps} /> <DataGrid {...pivotPreviewProps} />

View file

@ -59,6 +59,21 @@ describe('Transform: <DefinePivotSummary />', () => {
searchString: 'the-query', searchString: 'the-query',
searchQuery: 'the-search-query', searchQuery: 'the-search-query',
valid: true, valid: true,
validationStatus: {
isValid: true,
},
transformFunction: 'pivot',
previewRequest: {
pivot: {
aggregations: {
// @ts-ignore
'the-agg-name': agg,
},
group_by: {
'the-group-by-name': groupBy,
},
},
},
}; };
const { getByText } = render( const { getByText } = render(

View file

@ -6,11 +6,10 @@
import React, { Fragment, FC } from 'react'; import React, { Fragment, FC } from 'react';
import { FormattedMessage } from '@kbn/i18n/react';
import { i18n } from '@kbn/i18n'; import { i18n } from '@kbn/i18n';
import { EuiCodeBlock, EuiForm, EuiFormRow, EuiSpacer } from '@elastic/eui'; import { EuiBadge, EuiCodeBlock, EuiForm, EuiFormRow, EuiSpacer, EuiText } from '@elastic/eui';
import { dictionaryToArray } from '../../../../../../common/types/common';
import { useAppDependencies, useToastNotifications } from '../../../../app_dependencies'; import { useAppDependencies, useToastNotifications } from '../../../../app_dependencies';
import { import {
@ -27,6 +26,8 @@ import { AggListSummary } from '../aggregation_list';
import { GroupByListSummary } from '../group_by_list'; import { GroupByListSummary } from '../group_by_list';
import { StepDefineExposedState } from './common'; import { StepDefineExposedState } from './common';
import { TRANSFORM_FUNCTION } from '../../../../../../common/constants';
import { isLatestPartialRequest } from './common/types';
interface Props { interface Props {
formState: StepDefineExposedState; formState: StepDefineExposedState;
@ -34,29 +35,35 @@ interface Props {
} }
export const StepDefineSummary: FC<Props> = ({ export const StepDefineSummary: FC<Props> = ({
formState: { searchString, searchQuery, groupByList, aggList }, formState: {
searchString,
searchQuery,
groupByList,
aggList,
transformFunction,
previewRequest: partialPreviewRequest,
validationStatus,
},
searchItems, searchItems,
}) => { }) => {
const { const {
ml: { DataGrid }, ml: { DataGrid },
} = useAppDependencies(); } = useAppDependencies();
const toastNotifications = useToastNotifications(); const toastNotifications = useToastNotifications();
const pivotAggsArr = dictionaryToArray(aggList);
const pivotGroupByArr = dictionaryToArray(groupByList);
const pivotQuery = getPivotQuery(searchQuery); const pivotQuery = getPivotQuery(searchQuery);
const previewRequest = getPreviewTransformRequestBody( const previewRequest = getPreviewTransformRequestBody(
searchItems.indexPattern.title, searchItems.indexPattern.title,
pivotQuery, pivotQuery,
pivotGroupByArr, partialPreviewRequest
pivotAggsArr
); );
const pivotPreviewProps = usePivotData( const pivotPreviewProps = usePivotData(
searchItems.indexPattern.title, searchItems.indexPattern.title,
pivotQuery, pivotQuery,
aggList, validationStatus,
groupByList partialPreviewRequest
); );
const isModifiedQuery = const isModifiedQuery =
@ -64,6 +71,13 @@ export const StepDefineSummary: FC<Props> = ({
!isDefaultQuery(pivotQuery) && !isDefaultQuery(pivotQuery) &&
!isMatchAllQuery(pivotQuery); !isMatchAllQuery(pivotQuery);
let uniqueKeys: string[] = [];
let sortField = '';
if (isLatestPartialRequest(previewRequest)) {
uniqueKeys = previewRequest.latest.unique_key;
sortField = previewRequest.latest.sort;
}
return ( return (
<div data-test-subj="transformStepDefineSummary"> <div data-test-subj="transformStepDefineSummary">
<EuiForm> <EuiForm>
@ -116,6 +130,8 @@ export const StepDefineSummary: FC<Props> = ({
</EuiFormRow> </EuiFormRow>
)} )}
{transformFunction === TRANSFORM_FUNCTION.PIVOT ? (
<>
<EuiFormRow <EuiFormRow
label={i18n.translate('xpack.transform.stepDefineSummary.groupByLabel', { label={i18n.translate('xpack.transform.stepDefineSummary.groupByLabel', {
defaultMessage: 'Group by', defaultMessage: 'Group by',
@ -131,6 +147,38 @@ export const StepDefineSummary: FC<Props> = ({
> >
<AggListSummary list={aggList} /> <AggListSummary list={aggList} />
</EuiFormRow> </EuiFormRow>
</>
) : (
<>
<EuiFormRow
label={
<FormattedMessage
id="xpack.transform.stepDefineForm.uniqueKeysLabel"
defaultMessage="Unique keys"
/>
}
>
<>
{uniqueKeys.map((k) => (
<EuiBadge color="hollow" key={k}>
{k}
</EuiBadge>
))}
</>
</EuiFormRow>
<EuiFormRow
label={
<FormattedMessage
id="xpack.transform.stepDefineForm.sortLabel"
defaultMessage="Sort field"
/>
}
>
<EuiText>{sortField}</EuiText>
</EuiFormRow>
</>
)}
<EuiSpacer size="m" /> <EuiSpacer size="m" />
<DataGrid <DataGrid

View file

@ -0,0 +1,73 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React, { FC } from 'react';
import { i18n } from '@kbn/i18n';
import { EuiCard, EuiFlexGroup, EuiFlexItem, EuiIcon, EuiSpacer } from '@elastic/eui';
import { TRANSFORM_FUNCTION, TransformFunction } from '../../../../../../common/constants';
interface TransformFunctionSelectorProps {
selectedFunction: TransformFunction;
onChange: (update: TransformFunction) => void;
}
export const TransformFunctionSelector: FC<TransformFunctionSelectorProps> = ({
selectedFunction,
onChange,
}) => {
const transformFunctions = [
{
name: TRANSFORM_FUNCTION.PIVOT,
helpText: i18n.translate('xpack.transform.stepDefineForm.pivotHelperText', {
defaultMessage: 'Aggregate and group your data',
}),
icon: 'aggregate',
title: i18n.translate('xpack.transform.stepDefineForm.pivotLabel', {
defaultMessage: 'Pivot',
}),
},
{
name: TRANSFORM_FUNCTION.LATEST,
helpText: i18n.translate('xpack.transform.stepDefineForm.latestHelperText', {
defaultMessage: 'Keep track of your most recent data',
}),
icon: 'clock',
title: i18n.translate('xpack.transform.stepDefineForm.latestLabel', {
defaultMessage: 'Latest',
}),
},
];
return (
<>
<EuiFlexGroup gutterSize="m" data-test-subj="transformFunctionSelection">
{transformFunctions.map(({ helpText, icon, name, title }) => (
<EuiFlexItem key={name} style={{ width: 320 }} grow={false}>
<EuiCard
icon={<EuiIcon size="xl" type={icon} />}
title={title}
description={helpText}
data-test-subj={`transformCreation-${name}-option${
selectedFunction === name ? ' selectedFunction' : ''
}`}
selectable={{
onClick: () => {
// Only allow one function selected at a time and don't allow deselection
if (selectedFunction === name) {
return;
}
onChange(name);
},
isSelected: selectedFunction === name,
}}
/>
</EuiFlexItem>
))}
</EuiFlexGroup>
<EuiSpacer size="m" />
</>
);
};

View file

@ -50,7 +50,6 @@ import {
transformSettingsMaxPageSearchSizeValidator, transformSettingsMaxPageSearchSizeValidator,
} from '../../../../common/validators'; } from '../../../../common/validators';
import { StepDefineExposedState } from '../step_define/common'; import { StepDefineExposedState } from '../step_define/common';
import { dictionaryToArray } from '../../../../../../common/types/common';
export interface StepDetailsExposedState { export interface StepDetailsExposedState {
continuousModeDateField: string; continuousModeDateField: string;
@ -179,15 +178,12 @@ export const StepDetailsForm: FC<Props> = React.memo(
useEffect(() => { useEffect(() => {
// use an IIFE to avoid returning a Promise to useEffect. // use an IIFE to avoid returning a Promise to useEffect.
(async function () { (async function () {
const { searchQuery, groupByList, aggList } = stepDefineState; const { searchQuery, previewRequest: partialPreviewRequest } = stepDefineState;
const pivotAggsArr = dictionaryToArray(aggList);
const pivotGroupByArr = dictionaryToArray(groupByList);
const pivotQuery = getPivotQuery(searchQuery); const pivotQuery = getPivotQuery(searchQuery);
const previewRequest = getPreviewTransformRequestBody( const previewRequest = getPreviewTransformRequestBody(
searchItems.indexPattern.title, searchItems.indexPattern.title,
pivotQuery, pivotQuery,
pivotGroupByArr, partialPreviewRequest
pivotAggsArr
); );
const transformPreview = await api.getTransformsPreview(previewRequest); const transformPreview = await api.getTransformsPreview(previewRequest);

View file

@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License. * you may not use this file except in compliance with the Elastic License.
*/ */
import React, { Fragment, FC, useEffect, useRef, useState, createContext } from 'react'; import React, { Fragment, FC, useEffect, useRef, useState, createContext, useMemo } from 'react';
import { i18n } from '@kbn/i18n'; import { i18n } from '@kbn/i18n';
@ -100,7 +100,11 @@ export const Wizard: FC<WizardProps> = React.memo(({ cloneConfig, searchItems })
// The DEFINE state // The DEFINE state
const [stepDefineState, setStepDefineState] = useState( const [stepDefineState, setStepDefineState] = useState(
applyTransformConfigToDefineState(getDefaultStepDefineState(searchItems), cloneConfig) applyTransformConfigToDefineState(
getDefaultStepDefineState(searchItems),
cloneConfig,
indexPattern
)
); );
// The DETAILS state // The DETAILS state
@ -108,18 +112,6 @@ export const Wizard: FC<WizardProps> = React.memo(({ cloneConfig, searchItems })
applyTransformConfigToDetailsState(getDefaultStepDetailsState(), cloneConfig) applyTransformConfigToDetailsState(getDefaultStepDetailsState(), cloneConfig)
); );
const stepDetails =
currentStep === WIZARD_STEPS.DETAILS ? (
<StepDetailsForm
onChange={setStepDetailsState}
overrides={stepDetailsState}
searchItems={searchItems}
stepDefineState={stepDefineState}
/>
) : (
<StepDetailsSummary {...stepDetailsState} />
);
// The CREATE state // The CREATE state
const [stepCreateState, setStepCreateState] = useState(getDefaultStepCreateState); const [stepCreateState, setStepCreateState] = useState(getDefaultStepCreateState);
@ -157,22 +149,8 @@ export const Wizard: FC<WizardProps> = React.memo(({ cloneConfig, searchItems })
stepDetailsState stepDetailsState
); );
const stepCreate = const stepDefine = useMemo(() => {
currentStep === WIZARD_STEPS.CREATE ? ( return {
<StepCreateForm
createIndexPattern={stepDetailsState.createIndexPattern}
transformId={stepDetailsState.transformId}
transformConfig={transformConfig}
onChange={setStepCreateState}
overrides={stepCreateState}
timeFieldName={stepDetailsState.indexPatternTimeField}
/>
) : (
<StepCreateSummary />
);
const stepsConfig = [
{
title: i18n.translate('xpack.transform.transformsWizard.stepConfigurationTitle', { title: i18n.translate('xpack.transform.transformsWizard.stepConfigurationTitle', {
defaultMessage: 'Configuration', defaultMessage: 'Configuration',
}), }),
@ -185,14 +163,26 @@ export const Wizard: FC<WizardProps> = React.memo(({ cloneConfig, searchItems })
searchItems={searchItems} searchItems={searchItems}
/> />
), ),
}, };
{ }, [currentStep, stepDefineState, setCurrentStep, setStepDefineState, searchItems]);
const stepDetails = useMemo(() => {
return {
title: i18n.translate('xpack.transform.transformsWizard.stepDetailsTitle', { title: i18n.translate('xpack.transform.transformsWizard.stepDetailsTitle', {
defaultMessage: 'Transform details', defaultMessage: 'Transform details',
}), }),
children: ( children: (
<Fragment> <Fragment>
{stepDetails} {currentStep === WIZARD_STEPS.DETAILS ? (
<StepDetailsForm
onChange={setStepDetailsState}
overrides={stepDetailsState}
searchItems={searchItems}
stepDefineState={stepDefineState}
/>
) : (
<StepDetailsSummary {...stepDetailsState} />
)}
{currentStep === WIZARD_STEPS.DETAILS && ( {currentStep === WIZARD_STEPS.DETAILS && (
<WizardNav <WizardNav
previous={() => { previous={() => {
@ -205,22 +195,47 @@ export const Wizard: FC<WizardProps> = React.memo(({ cloneConfig, searchItems })
</Fragment> </Fragment>
), ),
status: currentStep >= WIZARD_STEPS.DETAILS ? undefined : ('incomplete' as EuiStepStatus), status: currentStep >= WIZARD_STEPS.DETAILS ? undefined : ('incomplete' as EuiStepStatus),
}, };
{ }, [currentStep, setStepDetailsState, stepDetailsState, searchItems, stepDefineState]);
const stepCreate = useMemo(() => {
return {
title: i18n.translate('xpack.transform.transformsWizard.stepCreateTitle', { title: i18n.translate('xpack.transform.transformsWizard.stepCreateTitle', {
defaultMessage: 'Create', defaultMessage: 'Create',
}), }),
children: ( children: (
<Fragment> <Fragment>
{stepCreate} {currentStep === WIZARD_STEPS.CREATE ? (
<StepCreateForm
createIndexPattern={stepDetailsState.createIndexPattern}
transformId={stepDetailsState.transformId}
transformConfig={transformConfig}
onChange={setStepCreateState}
overrides={stepCreateState}
timeFieldName={stepDetailsState.indexPatternTimeField}
/>
) : (
<StepCreateSummary />
)}
{currentStep === WIZARD_STEPS.CREATE && !stepCreateState.created && ( {currentStep === WIZARD_STEPS.CREATE && !stepCreateState.created && (
<WizardNav previous={() => setCurrentStep(WIZARD_STEPS.DETAILS)} /> <WizardNav previous={() => setCurrentStep(WIZARD_STEPS.DETAILS)} />
)} )}
</Fragment> </Fragment>
), ),
status: currentStep >= WIZARD_STEPS.CREATE ? undefined : ('incomplete' as EuiStepStatus), status: currentStep >= WIZARD_STEPS.CREATE ? undefined : ('incomplete' as EuiStepStatus),
}, };
]; }, [
currentStep,
setCurrentStep,
stepDetailsState.createIndexPattern,
stepDetailsState.transformId,
transformConfig,
setStepCreateState,
stepCreateState,
stepDetailsState.indexPatternTimeField,
]);
const stepsConfig = [stepDefine, stepDetails, stepCreate];
return ( return (
<CreateTransformWizardContext.Provider value={{ indexPattern }}> <CreateTransformWizardContext.Provider value={{ indexPattern }}>

View file

@ -6,7 +6,7 @@
import React, { useContext, useMemo, useState } from 'react'; import React, { useContext, useMemo, useState } from 'react';
import { TransformPivotConfig } from '../../../../../../common/types/transform'; import { TransformConfigUnion } from '../../../../../../common/types/transform';
import { TransformListAction, TransformListRow } from '../../../../common'; import { TransformListAction, TransformListRow } from '../../../../common';
import { AuthorizationContext } from '../../../../lib/authorization'; import { AuthorizationContext } from '../../../../lib/authorization';
@ -16,10 +16,10 @@ import { editActionNameText, EditActionName } from './edit_action_name';
export const useEditAction = (forceDisable: boolean) => { export const useEditAction = (forceDisable: boolean) => {
const { canCreateTransform } = useContext(AuthorizationContext).capabilities; const { canCreateTransform } = useContext(AuthorizationContext).capabilities;
const [config, setConfig] = useState<TransformPivotConfig>(); const [config, setConfig] = useState<TransformConfigUnion>();
const [isFlyoutVisible, setIsFlyoutVisible] = useState(false); const [isFlyoutVisible, setIsFlyoutVisible] = useState(false);
const closeFlyout = () => setIsFlyoutVisible(false); const closeFlyout = () => setIsFlyoutVisible(false);
const showFlyout = (newConfig: TransformPivotConfig) => { const showFlyout = (newConfig: TransformConfigUnion) => {
setConfig(newConfig); setConfig(newConfig);
setIsFlyoutVisible(true); setIsFlyoutVisible(true);
}; };

View file

@ -24,7 +24,7 @@ import {
} from '@elastic/eui'; } from '@elastic/eui';
import { isPostTransformsUpdateResponseSchema } from '../../../../../../common/api_schemas/type_guards'; import { isPostTransformsUpdateResponseSchema } from '../../../../../../common/api_schemas/type_guards';
import { TransformPivotConfig } from '../../../../../../common/types/transform'; import { TransformConfigUnion } from '../../../../../../common/types/transform';
import { getErrorMessage } from '../../../../../../common/utils/errors'; import { getErrorMessage } from '../../../../../../common/utils/errors';
@ -42,7 +42,7 @@ import {
interface EditTransformFlyoutProps { interface EditTransformFlyoutProps {
closeFlyout: () => void; closeFlyout: () => void;
config: TransformPivotConfig; config: TransformConfigUnion;
} }
export const EditTransformFlyout: FC<EditTransformFlyoutProps> = ({ closeFlyout, config }) => { export const EditTransformFlyout: FC<EditTransformFlyoutProps> = ({ closeFlyout, config }) => {

View file

@ -12,7 +12,7 @@ import { useReducer } from 'react';
import { i18n } from '@kbn/i18n'; import { i18n } from '@kbn/i18n';
import { PostTransformsUpdateRequestSchema } from '../../../../../../common/api_schemas/update_transforms'; import { PostTransformsUpdateRequestSchema } from '../../../../../../common/api_schemas/update_transforms';
import { TransformPivotConfig } from '../../../../../../common/types/transform'; import { TransformConfigUnion } from '../../../../../../common/types/transform';
import { getNestedProperty, setNestedProperty } from '../../../../../../common/utils/object_utils'; import { getNestedProperty, setNestedProperty } from '../../../../../../common/utils/object_utils';
// This custom hook uses nested reducers to provide a generic framework to manage form state // This custom hook uses nested reducers to provide a generic framework to manage form state
@ -167,7 +167,7 @@ const validate = {
export const initializeField = ( export const initializeField = (
formFieldName: string, formFieldName: string,
configFieldName: string, configFieldName: string,
config: TransformPivotConfig, config: TransformConfigUnion,
overloads?: Partial<FormField> overloads?: Partial<FormField>
): FormField => { ): FormField => {
const defaultValue = overloads?.defaultValue !== undefined ? overloads.defaultValue : ''; const defaultValue = overloads?.defaultValue !== undefined ? overloads.defaultValue : '';
@ -207,7 +207,7 @@ interface Action {
// Considers options like if a value is nullable or optional. // Considers options like if a value is nullable or optional.
const getUpdateValue = ( const getUpdateValue = (
attribute: keyof EditTransformFlyoutFieldsState, attribute: keyof EditTransformFlyoutFieldsState,
config: TransformPivotConfig, config: TransformConfigUnion,
formState: EditTransformFlyoutFieldsState, formState: EditTransformFlyoutFieldsState,
enforceFormValue = false enforceFormValue = false
) => { ) => {
@ -245,7 +245,7 @@ const getUpdateValue = (
// request object suitable to be sent to the // request object suitable to be sent to the
// transform update API endpoint. // transform update API endpoint.
export const applyFormFieldsToTransformConfig = ( export const applyFormFieldsToTransformConfig = (
config: TransformPivotConfig, config: TransformConfigUnion,
formState: EditTransformFlyoutFieldsState formState: EditTransformFlyoutFieldsState
): PostTransformsUpdateRequestSchema => ): PostTransformsUpdateRequestSchema =>
// Iterates over all form fields and only if necessary applies them to // Iterates over all form fields and only if necessary applies them to
@ -257,7 +257,7 @@ export const applyFormFieldsToTransformConfig = (
// Takes in a transform configuration and returns // Takes in a transform configuration and returns
// the default state to populate the form. // the default state to populate the form.
export const getDefaultState = (config: TransformPivotConfig): EditTransformFlyoutState => ({ export const getDefaultState = (config: TransformConfigUnion): EditTransformFlyoutState => ({
formFields: { formFields: {
// top level attributes // top level attributes
description: initializeField('description', 'description', config), description: initializeField('description', 'description', config),
@ -319,7 +319,7 @@ const formFieldReducer = (state: FormField, value: string): FormField => {
// - `formFieldReducer` to update the actions field // - `formFieldReducer` to update the actions field
// - compares the most recent state against the original one to update `isFormTouched` // - compares the most recent state against the original one to update `isFormTouched`
// - sets `isFormValid` to have a flag if any of the form fields contains an error. // - sets `isFormValid` to have a flag if any of the form fields contains an error.
export const formReducerFactory = (config: TransformPivotConfig) => { export const formReducerFactory = (config: TransformConfigUnion) => {
const defaultState = getDefaultState(config); const defaultState = getDefaultState(config);
const defaultFieldValues = Object.values(defaultState.formFields).map((f) => f.value); const defaultFieldValues = Object.values(defaultState.formFields).map((f) => f.value);
@ -341,7 +341,7 @@ export const formReducerFactory = (config: TransformPivotConfig) => {
}; };
}; };
export const useEditTransformFlyout = (config: TransformPivotConfig) => { export const useEditTransformFlyout = (config: TransformConfigUnion) => {
return useReducer(formReducerFactory(config), getDefaultState(config)); return useReducer(formReducerFactory(config), getDefaultState(config));
}; };

View file

@ -6,7 +6,7 @@
import React, { useMemo, FC } from 'react'; import React, { useMemo, FC } from 'react';
import { TransformPivotConfig } from '../../../../../../common/types/transform'; import { TransformConfigUnion } from '../../../../../../common/types/transform';
import { useAppDependencies, useToastNotifications } from '../../../../app_dependencies'; import { useAppDependencies, useToastNotifications } from '../../../../app_dependencies';
import { getPivotQuery } from '../../../../common'; import { getPivotQuery } from '../../../../common';
@ -19,7 +19,7 @@ import {
} from '../../../create_transform/components/step_define/'; } from '../../../create_transform/components/step_define/';
interface ExpandedRowPreviewPaneProps { interface ExpandedRowPreviewPaneProps {
transformConfig: TransformPivotConfig; transformConfig: TransformConfigUnion;
} }
export const ExpandedRowPreviewPane: FC<ExpandedRowPreviewPaneProps> = ({ transformConfig }) => { export const ExpandedRowPreviewPane: FC<ExpandedRowPreviewPaneProps> = ({ transformConfig }) => {
@ -28,7 +28,7 @@ export const ExpandedRowPreviewPane: FC<ExpandedRowPreviewPaneProps> = ({ transf
} = useAppDependencies(); } = useAppDependencies();
const toastNotifications = useToastNotifications(); const toastNotifications = useToastNotifications();
const { aggList, groupByList, searchQuery } = useMemo( const { searchQuery, validationStatus, previewRequest } = useMemo(
() => () =>
applyTransformConfigToDefineState( applyTransformConfigToDefineState(
getDefaultStepDefineState({} as SearchItems), getDefaultStepDefineState({} as SearchItems),
@ -43,7 +43,12 @@ export const ExpandedRowPreviewPane: FC<ExpandedRowPreviewPaneProps> = ({ transf
? transformConfig.source.index.join(',') ? transformConfig.source.index.join(',')
: transformConfig.source.index; : transformConfig.source.index;
const pivotPreviewProps = usePivotData(indexPatternTitle, pivotQuery, aggList, groupByList); const pivotPreviewProps = usePivotData(
indexPatternTitle,
pivotQuery,
validationStatus,
previewRequest
);
return ( return (
<DataGrid <DataGrid

View file

@ -38,12 +38,7 @@ export function fillResultsWithTimeouts({ results, id, items, action }: Params)
) )
: ''; : '';
const error = { const reason = i18n.translate(
response: {
error: {
root_cause: [
{
reason: i18n.translate(
'xpack.transform.models.transformService.requestToActionTimedOutErrorMessage', 'xpack.transform.models.transformService.requestToActionTimedOutErrorMessage',
{ {
defaultMessage: `Request to {action} '{id}' timed out. {extra}`, defaultMessage: `Request to {action} '{id}' timed out. {extra}`,
@ -53,11 +48,15 @@ export function fillResultsWithTimeouts({ results, id, items, action }: Params)
extra, extra,
}, },
} }
), );
const error = {
reason,
root_cause: [
{
reason,
}, },
], ],
},
},
}; };
const newResults: CommonResponseStatusSchema | DeleteTransformsResponseSchema = {}; const newResults: CommonResponseStatusSchema | DeleteTransformsResponseSchema = {};
@ -66,6 +65,7 @@ export function fillResultsWithTimeouts({ results, id, items, action }: Params)
if (results[currentVal.id] === undefined) { if (results[currentVal.id] === undefined) {
accumResults[currentVal.id] = { accumResults[currentVal.id] = {
success: false, success: false,
// @ts-ignore
error, error,
}; };
} else { } else {

View file

@ -450,7 +450,7 @@ async function deleteTransforms(
? transformConfig.dest.index[0] ? transformConfig.dest.index[0]
: transformConfig.dest.index; : transformConfig.dest.index;
} catch (getTransformConfigError) { } catch (getTransformConfigError) {
transformDeleted.error = wrapError(getTransformConfigError); transformDeleted.error = getTransformConfigError.meta.body.error;
results[transformId] = { results[transformId] = {
transformDeleted, transformDeleted,
destIndexDeleted, destIndexDeleted,
@ -471,7 +471,7 @@ async function deleteTransforms(
}); });
destIndexDeleted.success = true; destIndexDeleted.success = true;
} catch (deleteIndexError) { } catch (deleteIndexError) {
destIndexDeleted.error = wrapError(deleteIndexError); destIndexDeleted.error = deleteIndexError.meta.body.error;
} }
} }
@ -487,7 +487,7 @@ async function deleteTransforms(
destIndexPatternDeleted.success = true; destIndexPatternDeleted.success = true;
} }
} catch (deleteDestIndexPatternError) { } catch (deleteDestIndexPatternError) {
destIndexPatternDeleted.error = wrapError(deleteDestIndexPatternError); destIndexPatternDeleted.error = deleteDestIndexPatternError.meta.body.error;
} }
} }
@ -498,7 +498,7 @@ async function deleteTransforms(
}); });
transformDeleted.success = true; transformDeleted.success = true;
} catch (deleteTransformJobError) { } catch (deleteTransformJobError) {
transformDeleted.error = wrapError(deleteTransformJobError); transformDeleted.error = deleteTransformJobError.meta.body.error;
if (deleteTransformJobError.statusCode === 403) { if (deleteTransformJobError.statusCode === 403) {
return response.forbidden(); return response.forbidden();
} }
@ -519,7 +519,7 @@ async function deleteTransforms(
action: TRANSFORM_ACTIONS.DELETE, action: TRANSFORM_ACTIONS.DELETE,
}); });
} }
results[transformId] = { transformDeleted: { success: false, error: JSON.stringify(e) } }; results[transformId] = { transformDeleted: { success: false, error: e.meta.body.error } };
} }
} }
return results; return results;
@ -579,7 +579,7 @@ async function startTransforms(
action: TRANSFORM_ACTIONS.START, action: TRANSFORM_ACTIONS.START,
}); });
} }
results[transformId] = { success: false, error: JSON.stringify(e) }; results[transformId] = { success: false, error: e.meta.body.error };
} }
} }
return results; return results;
@ -628,7 +628,7 @@ async function stopTransforms(
action: TRANSFORM_ACTIONS.STOP, action: TRANSFORM_ACTIONS.STOP,
}); });
} }
results[transformId] = { success: false, error: JSON.stringify(e) }; results[transformId] = { success: false, error: e.meta.body.error };
} }
} }
return results; return results;

View file

@ -34,5 +34,6 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) {
loadTestFile(require.resolve('./transforms_preview')); loadTestFile(require.resolve('./transforms_preview'));
loadTestFile(require.resolve('./transforms_stats')); loadTestFile(require.resolve('./transforms_stats'));
loadTestFile(require.resolve('./transforms_update')); loadTestFile(require.resolve('./transforms_update'));
loadTestFile(require.resolve('./transforms_create'));
}); });
} }

View file

@ -75,7 +75,7 @@ export default ({ getService }: FtrProviderContext) => {
.expect(200); .expect(200);
expect(body[transformId].success).to.eql(false); expect(body[transformId].success).to.eql(false);
expect(typeof body[transformId].error).to.eql('string'); expect(typeof body[transformId].error).to.eql('object');
await transform.api.waitForTransformState(transformId, TRANSFORM_STATE.STOPPED); await transform.api.waitForTransformState(transformId, TRANSFORM_STATE.STOPPED);
await transform.api.waitForIndicesNotToExist(destinationIndex); await transform.api.waitForIndicesNotToExist(destinationIndex);

View file

@ -97,7 +97,7 @@ export default ({ getService }: FtrProviderContext) => {
expect(isStopTransformsResponseSchema(body)).to.eql(true); expect(isStopTransformsResponseSchema(body)).to.eql(true);
expect(body[transformId].success).to.eql(false); expect(body[transformId].success).to.eql(false);
expect(typeof body[transformId].error).to.eql('string'); expect(typeof body[transformId].error).to.eql('object');
await transform.api.waitForTransformStateNotToBe(transformId, TRANSFORM_STATE.STOPPED); await transform.api.waitForTransformStateNotToBe(transformId, TRANSFORM_STATE.STOPPED);
await transform.api.waitForIndicesToExist(destinationIndex); await transform.api.waitForIndicesToExist(destinationIndex);

View file

@ -0,0 +1,70 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import expect from '@kbn/expect';
import { FtrProviderContext } from '../../ftr_provider_context';
import { COMMON_REQUEST_HEADERS } from '../../../functional/services/ml/common_api';
import { USER } from '../../../functional/services/transform/security_common';
import { generateTransformConfig } from './common';
export default ({ getService }: FtrProviderContext) => {
const esArchiver = getService('esArchiver');
const supertest = getService('supertestWithoutAuth');
const transform = getService('transform');
describe('/api/transform/transforms/{transformId}/ create', function () {
before(async () => {
await esArchiver.loadIfNeeded('ml/farequote');
await transform.testResources.setKibanaTimeZoneToUTC();
});
after(async () => {
await transform.api.cleanTransformIndices();
});
it('should not allow pivot and latest configs is same transform', async () => {
const transformId = 'test_transform_id';
const { body } = await supertest
.put(`/api/transform/transforms/${transformId}`)
.auth(
USER.TRANSFORM_POWERUSER,
transform.securityCommon.getPasswordForUser(USER.TRANSFORM_POWERUSER)
)
.set(COMMON_REQUEST_HEADERS)
.send({
...generateTransformConfig(transformId),
latest: {
unique_key: ['country', 'gender'],
sort: 'infected',
},
})
.expect(400);
expect(body.message).to.eql('[request body]: pivot and latest are not allowed together');
});
it('should ensure if pivot or latest is provided', async () => {
const transformId = 'test_transform_id';
const { pivot, ...config } = generateTransformConfig(transformId);
const { body } = await supertest
.put(`/api/transform/transforms/${transformId}`)
.auth(
USER.TRANSFORM_POWERUSER,
transform.securityCommon.getPasswordForUser(USER.TRANSFORM_POWERUSER)
)
.set(COMMON_REQUEST_HEADERS)
.send(config)
.expect(400);
expect(body.message).to.eql(
'[request body]: pivot or latest is required for transform configuration'
);
});
});
};