mirror of
https://github.com/elastic/kibana.git
synced 2025-04-25 02:09:32 -04:00
[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:
parent
2a71d41a60
commit
e17cd65196
37 changed files with 1151 additions and 427 deletions
|
@ -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 {
|
||||||
|
|
|
@ -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({
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
// PUT transforms/{transformId}
|
function transformConfigPayloadValidator<
|
||||||
export const putTransformsRequestSchema = schema.object({
|
T extends { pivot?: PivotConfig; latest?: LatestFunctionConfig }
|
||||||
description: schema.maybe(schema.string()),
|
>(value: T) {
|
||||||
dest: destSchema,
|
if (!value.pivot && !value.latest) {
|
||||||
frequency: schema.maybe(schema.string()),
|
return 'pivot or latest is required for transform configuration';
|
||||||
pivot: pivotSchema,
|
}
|
||||||
settings: schema.maybe(settingsSchema),
|
if (value.pivot && value.latest) {
|
||||||
source: sourceSchema,
|
return 'pivot and latest are not allowed together';
|
||||||
sync: schema.maybe(syncSchema),
|
}
|
||||||
});
|
}
|
||||||
|
|
||||||
export interface PutTransformsRequestSchema extends TypeOf<typeof putTransformsRequestSchema> {
|
// PUT transforms/{transformId}
|
||||||
|
export const putTransformsRequestSchema = schema.object(
|
||||||
|
{
|
||||||
|
description: schema.maybe(schema.string()),
|
||||||
|
dest: destSchema,
|
||||||
|
frequency: schema.maybe(schema.string()),
|
||||||
|
/**
|
||||||
|
* 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),
|
||||||
|
source: sourceSchema,
|
||||||
|
sync: schema.maybe(syncSchema),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
validate: transformConfigPayloadValidator,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
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,
|
{
|
||||||
source: sourceSchema,
|
pivot: schema.maybe(pivotSchema),
|
||||||
});
|
latest: schema.maybe(latestFunctionSchema),
|
||||||
|
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;
|
||||||
|
|
|
@ -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];
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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, {
|
||||||
field: 'the-group-by-field',
|
pivot: {
|
||||||
aggName: 'the-group-by-agg-name',
|
group_by: {
|
||||||
dropDownName: 'the-group-by-drop-down-name',
|
'the-group-by-agg-name': {
|
||||||
};
|
terms: {
|
||||||
const agg: PivotAggsConfig = {
|
field: 'the-group-by-field',
|
||||||
agg: PIVOT_SUPPORTED_AGGS.AVG,
|
},
|
||||||
field: 'the-agg-field',
|
},
|
||||||
aggName: 'the-agg-agg-name',
|
},
|
||||||
dropDownName: 'the-agg-drop-down-name',
|
aggregations: {
|
||||||
};
|
'the-agg-agg-name': {
|
||||||
const request = getPreviewTransformRequestBody(
|
avg: {
|
||||||
'the-index-pattern-title',
|
field: 'the-agg-field',
|
||||||
query,
|
},
|
||||||
[groupBy],
|
},
|
||||||
[agg]
|
},
|
||||||
);
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const pivotPreviewDevConsoleStatement = getPivotPreviewDevConsoleStatement(request);
|
const pivotPreviewDevConsoleStatement = getPivotPreviewDevConsoleStatement(request);
|
||||||
|
|
||||||
expect(pivotPreviewDevConsoleStatement).toBe(`POST _transform/_preview
|
expect(pivotPreviewDevConsoleStatement).toBe(`POST _transform/_preview
|
||||||
|
|
|
@ -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',
|
||||||
|
|
|
@ -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 !== ''
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 (
|
||||||
|
|
|
@ -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',
|
||||||
|
|
|
@ -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'
|
||||||
);
|
);
|
||||||
columnKeys.sort(sortColumns(groupByArr));
|
|
||||||
|
if (isPivotPartialRequest(requestPayload)) {
|
||||||
|
const groupByArr = Object.keys(requestPayload.pivot.group_by);
|
||||||
|
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 */
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|
|
@ -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,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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 {
|
||||||
// apply the transform configuration to wizard DEFINE state
|
if (transformConfig === undefined) {
|
||||||
if (transformConfig !== undefined) {
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isPivotTransform(transformConfig)) {
|
||||||
|
state.transformFunction = TRANSFORM_FUNCTION.PIVOT;
|
||||||
|
|
||||||
|
// apply the transform configuration to wizard DEFINE state
|
||||||
// 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,17 +68,44 @@ export function applyTransformConfigToDefineState(
|
||||||
{} as PivotGroupByConfigDict
|
{} as PivotGroupByConfigDict
|
||||||
);
|
);
|
||||||
|
|
||||||
// only apply the query from the transform config to wizard state if it's not the default query
|
state.previewRequest = {
|
||||||
const query = transformConfig.source.query;
|
pivot: transformConfig.pivot,
|
||||||
if (query !== undefined && !isEqual(query, matchAllQuery)) {
|
};
|
||||||
state.isAdvancedSourceEditorEnabled = true;
|
|
||||||
state.searchQuery = query;
|
|
||||||
state.sourceConfigUpdated = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// applying a transform config to wizard state will always result in a valid configuration
|
state.validationStatus = validatePivotConfig(transformConfig.pivot);
|
||||||
state.valid = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
const query = transformConfig.source.query;
|
||||||
|
if (query !== undefined && !isEqual(query, matchAllQuery)) {
|
||||||
|
state.isAdvancedSourceEditorEnabled = true;
|
||||||
|
state.searchQuery = query;
|
||||||
|
state.sourceConfigUpdated = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// applying a transform config to wizard state will always result in a valid configuration
|
||||||
|
state.valid = true;
|
||||||
|
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -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');
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>;
|
|
@ -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,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -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>;
|
||||||
|
|
|
@ -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,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
@ -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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
|
@ -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,80 +300,85 @@ export const StepDefineForm: FC<StepDefineFormProps> = React.memo((props) => {
|
||||||
</EuiForm>
|
</EuiForm>
|
||||||
<EuiHorizontalRule margin="m" />
|
<EuiHorizontalRule margin="m" />
|
||||||
<EuiForm>
|
<EuiForm>
|
||||||
<EuiFlexGroup justifyContent="spaceBetween">
|
{stepDefineForm.transformFunction === TRANSFORM_FUNCTION.PIVOT ? (
|
||||||
{/* Flex Column #1: Pivot Config Form / Advanced Pivot Config Editor */}
|
<EuiFlexGroup justifyContent="spaceBetween">
|
||||||
<EuiFlexItem>
|
{/* Flex Column #1: Pivot Config Form / Advanced Pivot Config Editor */}
|
||||||
{!isAdvancedPivotEditorEnabled && (
|
<EuiFlexItem>
|
||||||
<PivotConfiguration {...stepDefineForm.pivotConfig} />
|
{!isAdvancedPivotEditorEnabled && (
|
||||||
)}
|
<PivotConfiguration {...stepDefineForm.pivotConfig} />
|
||||||
{isAdvancedPivotEditorEnabled && (
|
|
||||||
<AdvancedPivotEditor {...stepDefineForm.advancedPivotEditor} />
|
|
||||||
)}
|
|
||||||
</EuiFlexItem>
|
|
||||||
<EuiFlexItem grow={false} style={{ width: advancedEditorsSidebarWidth }}>
|
|
||||||
<EuiFlexGroup gutterSize="xs" direction="column" justifyContent="spaceBetween">
|
|
||||||
<EuiFlexItem grow={false}>
|
|
||||||
<EuiFormRow hasEmptyLabelSpace>
|
|
||||||
<EuiFlexGroup alignItems="center" justifyContent="spaceBetween">
|
|
||||||
<EuiFlexItem grow={false}>
|
|
||||||
<AdvancedPivotEditorSwitch {...stepDefineForm} />
|
|
||||||
</EuiFlexItem>
|
|
||||||
<EuiFlexItem grow={false}>
|
|
||||||
<EuiCopy
|
|
||||||
beforeMessage={copyToClipboardPivotDescription}
|
|
||||||
textToCopy={copyToClipboardPivot}
|
|
||||||
>
|
|
||||||
{(copy: () => void) => (
|
|
||||||
<EuiButtonIcon
|
|
||||||
onClick={copy}
|
|
||||||
iconType="copyClipboard"
|
|
||||||
aria-label={copyToClipboardPivotDescription}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</EuiCopy>
|
|
||||||
</EuiFlexItem>
|
|
||||||
</EuiFlexGroup>
|
|
||||||
</EuiFormRow>
|
|
||||||
</EuiFlexItem>
|
|
||||||
{isAdvancedPivotEditorEnabled && (
|
|
||||||
<EuiFlexItem style={{ width: advancedEditorsSidebarWidth }}>
|
|
||||||
<EuiSpacer size="s" />
|
|
||||||
<EuiText size="xs">
|
|
||||||
<>
|
|
||||||
{i18n.translate('xpack.transform.stepDefineForm.advancedEditorHelpText', {
|
|
||||||
defaultMessage:
|
|
||||||
'The advanced editor allows you to edit the pivot configuration of the transform.',
|
|
||||||
})}{' '}
|
|
||||||
<EuiLink href={esTransformPivot} target="_blank">
|
|
||||||
{i18n.translate(
|
|
||||||
'xpack.transform.stepDefineForm.advancedEditorHelpTextLink',
|
|
||||||
{
|
|
||||||
defaultMessage: 'Learn more about available options.',
|
|
||||||
}
|
|
||||||
)}
|
|
||||||
</EuiLink>
|
|
||||||
</>
|
|
||||||
</EuiText>
|
|
||||||
<EuiSpacer size="s" />
|
|
||||||
<EuiButton
|
|
||||||
style={{ width: 'fit-content' }}
|
|
||||||
size="s"
|
|
||||||
fill
|
|
||||||
onClick={applyPivotChangesHandler}
|
|
||||||
disabled={!isAdvancedPivotEditorApplyButtonEnabled}
|
|
||||||
>
|
|
||||||
{i18n.translate(
|
|
||||||
'xpack.transform.stepDefineForm.advancedEditorApplyButtonText',
|
|
||||||
{
|
|
||||||
defaultMessage: 'Apply changes',
|
|
||||||
}
|
|
||||||
)}
|
|
||||||
</EuiButton>
|
|
||||||
</EuiFlexItem>
|
|
||||||
)}
|
)}
|
||||||
</EuiFlexGroup>
|
{isAdvancedPivotEditorEnabled && (
|
||||||
</EuiFlexItem>
|
<AdvancedPivotEditor {...stepDefineForm.advancedPivotEditor} />
|
||||||
</EuiFlexGroup>
|
)}
|
||||||
|
</EuiFlexItem>
|
||||||
|
<EuiFlexItem grow={false} style={{ width: advancedEditorsSidebarWidth }}>
|
||||||
|
<EuiFlexGroup gutterSize="xs" direction="column" justifyContent="spaceBetween">
|
||||||
|
<EuiFlexItem grow={false}>
|
||||||
|
<EuiFormRow hasEmptyLabelSpace>
|
||||||
|
<EuiFlexGroup alignItems="center" justifyContent="spaceBetween">
|
||||||
|
<EuiFlexItem grow={false}>
|
||||||
|
<AdvancedPivotEditorSwitch {...stepDefineForm} />
|
||||||
|
</EuiFlexItem>
|
||||||
|
<EuiFlexItem grow={false}>
|
||||||
|
<EuiCopy
|
||||||
|
beforeMessage={copyToClipboardPivotDescription}
|
||||||
|
textToCopy={copyToClipboardPivot}
|
||||||
|
>
|
||||||
|
{(copy: () => void) => (
|
||||||
|
<EuiButtonIcon
|
||||||
|
onClick={copy}
|
||||||
|
iconType="copyClipboard"
|
||||||
|
aria-label={copyToClipboardPivotDescription}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</EuiCopy>
|
||||||
|
</EuiFlexItem>
|
||||||
|
</EuiFlexGroup>
|
||||||
|
</EuiFormRow>
|
||||||
|
</EuiFlexItem>
|
||||||
|
{isAdvancedPivotEditorEnabled && (
|
||||||
|
<EuiFlexItem style={{ width: advancedEditorsSidebarWidth }}>
|
||||||
|
<EuiSpacer size="s" />
|
||||||
|
<EuiText size="xs">
|
||||||
|
<>
|
||||||
|
{i18n.translate('xpack.transform.stepDefineForm.advancedEditorHelpText', {
|
||||||
|
defaultMessage:
|
||||||
|
'The advanced editor allows you to edit the pivot configuration of the transform.',
|
||||||
|
})}{' '}
|
||||||
|
<EuiLink href={esTransformPivot} target="_blank">
|
||||||
|
{i18n.translate(
|
||||||
|
'xpack.transform.stepDefineForm.advancedEditorHelpTextLink',
|
||||||
|
{
|
||||||
|
defaultMessage: 'Learn more about available options.',
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
</EuiLink>
|
||||||
|
</>
|
||||||
|
</EuiText>
|
||||||
|
<EuiSpacer size="s" />
|
||||||
|
<EuiButton
|
||||||
|
style={{ width: 'fit-content' }}
|
||||||
|
size="s"
|
||||||
|
fill
|
||||||
|
onClick={applyPivotChangesHandler}
|
||||||
|
disabled={!isAdvancedPivotEditorApplyButtonEnabled}
|
||||||
|
>
|
||||||
|
{i18n.translate(
|
||||||
|
'xpack.transform.stepDefineForm.advancedEditorApplyButtonText',
|
||||||
|
{
|
||||||
|
defaultMessage: 'Apply changes',
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
</EuiButton>
|
||||||
|
</EuiFlexItem>
|
||||||
|
)}
|
||||||
|
</EuiFlexGroup>
|
||||||
|
</EuiFlexItem>
|
||||||
|
</EuiFlexGroup>
|
||||||
|
) : null}
|
||||||
|
{stepDefineForm.transformFunction === TRANSFORM_FUNCTION.LATEST ? (
|
||||||
|
<LatestFunctionForm latestFunctionService={stepDefineForm.latestFunctionConfig} />
|
||||||
|
) : null}
|
||||||
</EuiForm>
|
</EuiForm>
|
||||||
<EuiSpacer size="m" />
|
<EuiSpacer size="m" />
|
||||||
<DataGrid {...pivotPreviewProps} />
|
<DataGrid {...pivotPreviewProps} />
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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,21 +130,55 @@ export const StepDefineSummary: FC<Props> = ({
|
||||||
</EuiFormRow>
|
</EuiFormRow>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<EuiFormRow
|
{transformFunction === TRANSFORM_FUNCTION.PIVOT ? (
|
||||||
label={i18n.translate('xpack.transform.stepDefineSummary.groupByLabel', {
|
<>
|
||||||
defaultMessage: 'Group by',
|
<EuiFormRow
|
||||||
})}
|
label={i18n.translate('xpack.transform.stepDefineSummary.groupByLabel', {
|
||||||
>
|
defaultMessage: 'Group by',
|
||||||
<GroupByListSummary list={groupByList} />
|
})}
|
||||||
</EuiFormRow>
|
>
|
||||||
|
<GroupByListSummary list={groupByList} />
|
||||||
|
</EuiFormRow>
|
||||||
|
|
||||||
<EuiFormRow
|
<EuiFormRow
|
||||||
label={i18n.translate('xpack.transform.stepDefineSummary.aggregationsLabel', {
|
label={i18n.translate('xpack.transform.stepDefineSummary.aggregationsLabel', {
|
||||||
defaultMessage: 'Aggregations',
|
defaultMessage: 'Aggregations',
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<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
|
||||||
|
|
|
@ -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" />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
|
@ -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);
|
||||||
|
|
|
@ -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 }}>
|
||||||
|
|
|
@ -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);
|
||||||
};
|
};
|
||||||
|
|
|
@ -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 }) => {
|
||||||
|
|
|
@ -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));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -38,26 +38,25 @@ export function fillResultsWithTimeouts({ results, id, items, action }: Params)
|
||||||
)
|
)
|
||||||
: '';
|
: '';
|
||||||
|
|
||||||
const error = {
|
const reason = i18n.translate(
|
||||||
response: {
|
'xpack.transform.models.transformService.requestToActionTimedOutErrorMessage',
|
||||||
error: {
|
{
|
||||||
root_cause: [
|
defaultMessage: `Request to {action} '{id}' timed out. {extra}`,
|
||||||
{
|
values: {
|
||||||
reason: i18n.translate(
|
id,
|
||||||
'xpack.transform.models.transformService.requestToActionTimedOutErrorMessage',
|
action,
|
||||||
{
|
extra,
|
||||||
defaultMessage: `Request to {action} '{id}' timed out. {extra}`,
|
|
||||||
values: {
|
|
||||||
id,
|
|
||||||
action,
|
|
||||||
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 {
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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'));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
Loading…
Add table
Add a link
Reference in a new issue