[ML] Transforms: API schemas and integration tests (#75164)

- Adds schema definitions to transform API endpoints and adds API integration tests.
- The type definitions based on the schema definitions can be used on the client side too.
- Adds apidoc documentation.
This commit is contained in:
Walter Rafelsberger 2020-09-14 16:31:23 +02:00 committed by GitHub
parent cbcd1ebd32
commit dd1822047c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
127 changed files with 3110 additions and 1374 deletions

View file

@ -0,0 +1,7 @@
/*
* 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.
*/
export { SearchResponse7 } from './types/es_client';

View file

@ -0,0 +1,25 @@
/*
* 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 { SearchResponse, ShardsResponse } from 'elasticsearch';
// The types specified in `@types/elasticsearch` are out of date and still have `total: number`.
interface SearchResponse7Hits<T> {
hits: SearchResponse<T>['hits']['hits'];
max_score: number;
total: {
value: number;
relation: string;
};
}
export interface SearchResponse7<T = any> {
took: number;
timed_out: boolean;
_scroll_id?: string;
_shards: ShardsResponse;
hits: SearchResponse7Hits<T>;
aggregations?: any;
}

View file

@ -19,7 +19,6 @@ export {
DataGridItem,
EsSorting,
RenderCellValue,
SearchResponse7,
UseDataGridReturnType,
UseIndexDataReturnType,
} from './types';

View file

@ -5,7 +5,6 @@
*/
import { Dispatch, SetStateAction } from 'react';
import { SearchResponse } from 'elasticsearch';
import { EuiDataGridPaginationProps, EuiDataGridSorting, EuiDataGridColumn } from '@elastic/eui';
@ -43,16 +42,6 @@ export type EsSorting = Dictionary<{
order: 'asc' | 'desc';
}>;
// The types specified in `@types/elasticsearch` are out of date and still have `total: number`.
export interface SearchResponse7 extends SearchResponse<any> {
hits: SearchResponse<any>['hits'] & {
total: {
value: number;
relation: string;
};
};
}
export interface UseIndexDataReturnType
extends Pick<
UseDataGridReturnType,

View file

@ -4,9 +4,10 @@
* you may not use this file except in compliance with the Elastic License.
*/
import type { SearchResponse7 } from '../../../../common/types/es_client';
import { extractErrorMessage } from '../../../../common/util/errors';
import { EsSorting, SearchResponse7, UseDataGridReturnType } from '../../components/data_grid';
import { EsSorting, UseDataGridReturnType } from '../../components/data_grid';
import { ml } from '../../services/ml_api_service';
import { isKeywordAndTextType } from '../common/fields';

View file

@ -22,9 +22,9 @@ import {
useDataGrid,
useRenderCellValue,
EsSorting,
SearchResponse7,
UseIndexDataReturnType,
} from '../../../../components/data_grid';
import type { SearchResponse7 } from '../../../../../../common/types/es_client';
import { extractErrorMessage } from '../../../../../../common/util/errors';
import { INDEX_STATUS } from '../../../common/analytics';
import { ml } from '../../../../services/ml_api_service';

View file

@ -0,0 +1,9 @@
/*
* 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 { TransformMessage } from '../types/messages';
export type GetTransformsAuditMessagesResponseSchema = TransformMessage[];

View file

@ -0,0 +1,48 @@
/*
* 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 { schema, TypeOf } from '@kbn/config-schema';
import { TRANSFORM_STATE } from '../constants';
export const transformIdsSchema = schema.arrayOf(
schema.object({
id: schema.string(),
})
);
export type TransformIdsSchema = TypeOf<typeof transformIdsSchema>;
export const transformStateSchema = schema.oneOf([
schema.literal(TRANSFORM_STATE.ABORTING),
schema.literal(TRANSFORM_STATE.FAILED),
schema.literal(TRANSFORM_STATE.INDEXING),
schema.literal(TRANSFORM_STATE.STARTED),
schema.literal(TRANSFORM_STATE.STOPPED),
schema.literal(TRANSFORM_STATE.STOPPING),
]);
export const indexPatternTitleSchema = schema.object({
/** Title of the index pattern for which to return stats. */
indexPatternTitle: schema.string(),
});
export type IndexPatternTitleSchema = TypeOf<typeof indexPatternTitleSchema>;
export const transformIdParamSchema = schema.object({
transformId: schema.string(),
});
export type TransformIdParamSchema = TypeOf<typeof transformIdParamSchema>;
export interface ResponseStatus {
success: boolean;
error?: any;
}
export interface CommonResponseStatusSchema {
[key: string]: ResponseStatus;
}

View file

@ -0,0 +1,37 @@
/*
* 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 { schema, TypeOf } from '@kbn/config-schema';
import { transformStateSchema, ResponseStatus } from './common';
export const deleteTransformsRequestSchema = schema.object({
/**
* Delete Transform & Destination Index
*/
transformsInfo: schema.arrayOf(
schema.object({
id: schema.string(),
state: transformStateSchema,
})
),
deleteDestIndex: schema.maybe(schema.boolean()),
deleteDestIndexPattern: schema.maybe(schema.boolean()),
forceDelete: schema.maybe(schema.boolean()),
});
export type DeleteTransformsRequestSchema = TypeOf<typeof deleteTransformsRequestSchema>;
export interface DeleteTransformStatus {
transformDeleted: ResponseStatus;
destIndexDeleted?: ResponseStatus;
destIndexPatternDeleted?: ResponseStatus;
destinationIndex?: string | undefined;
}
export interface DeleteTransformsResponseSchema {
[key: string]: DeleteTransformStatus;
}

View file

@ -0,0 +1,19 @@
/*
* 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 { schema, TypeOf } from '@kbn/config-schema';
export const fieldHistogramsRequestSchema = schema.object({
/** Query to match documents in the index. */
query: schema.any(),
/** The fields to return histogram data. */
fields: schema.arrayOf(schema.any()),
/** Number of documents to be collected in the sample processed on each shard, or -1 for no sampling. */
samplerShardSize: schema.number(),
});
export type FieldHistogramsRequestSchema = TypeOf<typeof fieldHistogramsRequestSchema>;
export type FieldHistogramsResponseSchema = any[];

View file

@ -0,0 +1,13 @@
/*
* 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 { TypeOf } from '@kbn/config-schema';
import { transformIdsSchema, CommonResponseStatusSchema } from './common';
export const startTransformsRequestSchema = transformIdsSchema;
export type StartTransformsRequestSchema = TypeOf<typeof startTransformsRequestSchema>;
export type StartTransformsResponseSchema = CommonResponseStatusSchema;

View file

@ -0,0 +1,19 @@
/*
* 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 { schema, TypeOf } from '@kbn/config-schema';
import { transformStateSchema, CommonResponseStatusSchema } from './common';
export const stopTransformsRequestSchema = schema.arrayOf(
schema.object({
id: schema.string(),
state: transformStateSchema,
})
);
export type StopTransformsRequestSchema = TypeOf<typeof stopTransformsRequestSchema>;
export type StopTransformsResponseSchema = CommonResponseStatusSchema;

View file

@ -0,0 +1,127 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { schema, TypeOf } from '@kbn/config-schema';
import type { ES_FIELD_TYPES } from '../../../../../src/plugins/data/common';
import type { Dictionary } from '../types/common';
import type { PivotAggDict } from '../types/pivot_aggs';
import type { PivotGroupByDict } from '../types/pivot_group_by';
import type { TransformId, TransformPivotConfig } from '../types/transform';
import { transformStateSchema } from './common';
// GET transforms
export const getTransformsRequestSchema = schema.arrayOf(
schema.object({
id: schema.string(),
state: transformStateSchema,
})
);
export type GetTransformsRequestSchema = TypeOf<typeof getTransformsRequestSchema>;
export interface GetTransformsResponseSchema {
count: number;
transforms: TransformPivotConfig[];
}
// schemas shared by parts of the preview, create and update endpoint
export const destSchema = schema.object({
index: schema.string(),
pipeline: schema.maybe(schema.string()),
});
export const pivotSchema = schema.object({
group_by: schema.any(),
aggregations: schema.any(),
});
export const settingsSchema = schema.object({
max_page_search_size: schema.maybe(schema.number()),
// The default value is null, which disables throttling.
docs_per_second: schema.maybe(schema.nullable(schema.number())),
});
export const sourceSchema = schema.object({
index: schema.oneOf([schema.string(), schema.arrayOf(schema.string())]),
query: schema.maybe(schema.recordOf(schema.string(), schema.any())),
});
export const syncSchema = schema.object({
time: schema.object({
delay: schema.maybe(schema.string()),
field: schema.string(),
}),
});
// PUT transforms/{transformId}
export const putTransformsRequestSchema = schema.object({
description: schema.maybe(schema.string()),
dest: destSchema,
frequency: schema.maybe(schema.string()),
pivot: pivotSchema,
settings: schema.maybe(settingsSchema),
source: sourceSchema,
sync: schema.maybe(syncSchema),
});
export interface PutTransformsRequestSchema extends TypeOf<typeof putTransformsRequestSchema> {
pivot: {
group_by: PivotGroupByDict;
aggregations: PivotAggDict;
};
}
interface TransformCreated {
transform: TransformId;
}
interface TransformCreatedError {
id: TransformId;
error: any;
}
export interface PutTransformsResponseSchema {
transformsCreated: TransformCreated[];
errors: TransformCreatedError[];
}
// POST transforms/_preview
export const postTransformsPreviewRequestSchema = schema.object({
pivot: pivotSchema,
source: sourceSchema,
});
export interface PostTransformsPreviewRequestSchema
extends TypeOf<typeof postTransformsPreviewRequestSchema> {
pivot: {
group_by: PivotGroupByDict;
aggregations: PivotAggDict;
};
}
interface EsMappingType {
type: ES_FIELD_TYPES;
}
export type PreviewItem = Dictionary<any>;
export type PreviewData = PreviewItem[];
export type PreviewMappingsProperties = Dictionary<EsMappingType>;
export interface PostTransformsPreviewResponseSchema {
generated_dest_index: {
mappings: {
_meta: {
_transform: {
transform: string;
version: { create: string };
creation_date_in_millis: number;
};
created_by: string;
};
properties: PreviewMappingsProperties;
};
settings: { index: { number_of_shards: string; auto_expand_replicas: string } };
aliases: Record<string, any>;
};
preview: PreviewData;
}

View file

@ -0,0 +1,21 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { TypeOf } from '@kbn/config-schema';
import { TransformStats } from '../types/transform_stats';
import { getTransformsRequestSchema } from './transforms';
export const getTransformsStatsRequestSchema = getTransformsRequestSchema;
export type GetTransformsRequestSchema = TypeOf<typeof getTransformsStatsRequestSchema>;
export interface GetTransformsStatsResponseSchema {
node_failures?: object;
count: number;
transforms: TransformStats[];
}

View file

@ -0,0 +1,114 @@
/*
* 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 type { SearchResponse7 } from '../../../ml/common';
import type { EsIndex } from '../types/es_index';
// To be able to use the type guards on the client side, we need to make sure we don't import
// the code of '@kbn/config-schema' but just its types, otherwise the client side code will
// fail to build.
import type { FieldHistogramsResponseSchema } from './field_histograms';
import type { GetTransformsAuditMessagesResponseSchema } from './audit_messages';
import type { DeleteTransformsResponseSchema } from './delete_transforms';
import type { StartTransformsResponseSchema } from './start_transforms';
import type { StopTransformsResponseSchema } from './stop_transforms';
import type {
GetTransformsResponseSchema,
PostTransformsPreviewResponseSchema,
PutTransformsResponseSchema,
} from './transforms';
import type { GetTransformsStatsResponseSchema } from './transforms_stats';
import type { PostTransformsUpdateResponseSchema } from './update_transforms';
const isBasicObject = (arg: any) => {
return typeof arg === 'object' && arg !== null;
};
const isGenericResponseSchema = <T>(arg: any): arg is T => {
return (
isBasicObject(arg) &&
{}.hasOwnProperty.call(arg, 'count') &&
{}.hasOwnProperty.call(arg, 'transforms') &&
Array.isArray(arg.transforms)
);
};
export const isGetTransformsResponseSchema = (arg: any): arg is GetTransformsResponseSchema => {
return isGenericResponseSchema<GetTransformsResponseSchema>(arg);
};
export const isGetTransformsStatsResponseSchema = (
arg: any
): arg is GetTransformsStatsResponseSchema => {
return isGenericResponseSchema<GetTransformsStatsResponseSchema>(arg);
};
export const isDeleteTransformsResponseSchema = (
arg: any
): arg is DeleteTransformsResponseSchema => {
return (
isBasicObject(arg) &&
Object.values(arg).every((d) => ({}.hasOwnProperty.call(d, 'transformDeleted')))
);
};
export const isEsIndices = (arg: any): arg is EsIndex[] => {
return Array.isArray(arg);
};
export const isEsSearchResponse = (arg: any): arg is SearchResponse7 => {
return isBasicObject(arg) && {}.hasOwnProperty.call(arg, 'hits');
};
export const isFieldHistogramsResponseSchema = (arg: any): arg is FieldHistogramsResponseSchema => {
return Array.isArray(arg);
};
export const isGetTransformsAuditMessagesResponseSchema = (
arg: any
): arg is GetTransformsAuditMessagesResponseSchema => {
return Array.isArray(arg);
};
export const isPostTransformsPreviewResponseSchema = (
arg: any
): arg is PostTransformsPreviewResponseSchema => {
return (
isBasicObject(arg) &&
{}.hasOwnProperty.call(arg, 'generated_dest_index') &&
{}.hasOwnProperty.call(arg, 'preview') &&
typeof arg.generated_dest_index !== undefined &&
Array.isArray(arg.preview)
);
};
export const isPostTransformsUpdateResponseSchema = (
arg: any
): arg is PostTransformsUpdateResponseSchema => {
return isBasicObject(arg) && {}.hasOwnProperty.call(arg, 'id') && typeof arg.id === 'string';
};
export const isPutTransformsResponseSchema = (arg: any): arg is PutTransformsResponseSchema => {
return (
isBasicObject(arg) &&
{}.hasOwnProperty.call(arg, 'transformsCreated') &&
{}.hasOwnProperty.call(arg, 'errors') &&
Array.isArray(arg.transformsCreated) &&
Array.isArray(arg.errors)
);
};
const isGenericSuccessResponseSchema = (arg: any) =>
isBasicObject(arg) && Object.values(arg).every((d) => ({}.hasOwnProperty.call(d, 'success')));
export const isStartTransformsResponseSchema = (arg: any): arg is StartTransformsResponseSchema => {
return isGenericSuccessResponseSchema(arg);
};
export const isStopTransformsResponseSchema = (arg: any): arg is StopTransformsResponseSchema => {
return isGenericSuccessResponseSchema(arg);
};

View file

@ -0,0 +1,24 @@
/*
* 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 { schema, TypeOf } from '@kbn/config-schema';
import { TransformPivotConfig } from '../types/transform';
import { destSchema, settingsSchema, sourceSchema, syncSchema } from './transforms';
// POST _transform/{transform_id}/_update
export const postTransformsUpdateRequestSchema = schema.object({
description: schema.maybe(schema.string()),
dest: schema.maybe(destSchema),
frequency: schema.maybe(schema.string()),
settings: schema.maybe(settingsSchema),
source: schema.maybe(sourceSchema),
sync: schema.maybe(syncSchema),
});
export type PostTransformsUpdateRequestSchema = TypeOf<typeof postTransformsUpdateRequestSchema>;
export type PostTransformsUpdateResponseSchema = TransformPivotConfig;

View file

@ -75,3 +75,24 @@ export const APP_CREATE_TRANSFORM_CLUSTER_PRIVILEGES = [
];
export const APP_INDEX_PRIVILEGES = ['monitor'];
// reflects https://github.com/elastic/elasticsearch/blob/master/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/dataframe/transforms/DataFrameTransformStats.java#L243
export const TRANSFORM_STATE = {
ABORTING: 'aborting',
FAILED: 'failed',
INDEXING: 'indexing',
STARTED: 'started',
STOPPED: 'stopped',
STOPPING: 'stopping',
} as const;
const transformStates = Object.values(TRANSFORM_STATE);
export type TransformState = typeof transformStates[number];
export const TRANSFORM_MODE = {
BATCH: 'batch',
CONTINUOUS: 'continuous',
} as const;
const transformModes = Object.values(TRANSFORM_MODE);
export type TransformMode = typeof transformModes[number];

View file

@ -1,58 +0,0 @@
/*
* 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.
*/
export interface MissingPrivileges {
[key: string]: string[] | undefined;
}
export interface Privileges {
hasAllPrivileges: boolean;
missingPrivileges: MissingPrivileges;
}
export type TransformId = string;
// reflects https://github.com/elastic/elasticsearch/blob/master/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/dataframe/transforms/DataFrameTransformStats.java#L243
export enum TRANSFORM_STATE {
ABORTING = 'aborting',
FAILED = 'failed',
INDEXING = 'indexing',
STARTED = 'started',
STOPPED = 'stopped',
STOPPING = 'stopping',
}
export interface TransformEndpointRequest {
id: TransformId;
state?: TRANSFORM_STATE;
}
export interface ResultData {
success: boolean;
error?: any;
}
export interface TransformEndpointResult {
[key: string]: ResultData;
}
export interface DeleteTransformEndpointRequest {
transformsInfo: TransformEndpointRequest[];
deleteDestIndex?: boolean;
deleteDestIndexPattern?: boolean;
forceDelete?: boolean;
}
export interface DeleteTransformStatus {
transformDeleted: ResultData;
destIndexDeleted?: ResultData;
destIndexPatternDeleted?: ResultData;
destinationIndex?: string | undefined;
}
export interface DeleteTransformEndpointResult {
[key: string]: DeleteTransformStatus;
}

View file

@ -0,0 +1,7 @@
/*
* 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.
*/
export type { SearchResponse7 } from '../../ml/common';

View file

@ -0,0 +1,7 @@
/*
* 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.
*/
export type AggName = string;

View file

@ -0,0 +1,7 @@
/*
* 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.
*/
export type EsFieldName = string;

View file

@ -0,0 +1,31 @@
/*
* 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 { AggName } from './aggregations';
import { EsFieldName } from './fields';
export const PIVOT_SUPPORTED_AGGS = {
AVG: 'avg',
CARDINALITY: 'cardinality',
MAX: 'max',
MIN: 'min',
PERCENTILES: 'percentiles',
SUM: 'sum',
VALUE_COUNT: 'value_count',
FILTER: 'filter',
} as const;
export type PivotSupportedAggs = typeof PIVOT_SUPPORTED_AGGS[keyof typeof PIVOT_SUPPORTED_AGGS];
export type PivotAgg = {
[key in PivotSupportedAggs]?: {
field: EsFieldName;
};
};
export type PivotAggDict = {
[key in AggName]: PivotAgg;
};

View file

@ -0,0 +1,33 @@
/*
* 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 { Dictionary } from './common';
import { EsFieldName } from './fields';
export type GenericAgg = object;
export interface TermsAgg {
terms: {
field: EsFieldName;
};
}
export interface HistogramAgg {
histogram: {
field: EsFieldName;
interval: string;
};
}
export interface DateHistogramAgg {
date_histogram: {
field: EsFieldName;
calendar_interval: string;
};
}
export type PivotGroupBy = GenericAgg | TermsAgg | HistogramAgg | DateHistogramAgg;
export type PivotGroupByDict = Dictionary<PivotGroupBy>;

View file

@ -0,0 +1,14 @@
/*
* 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.
*/
export interface MissingPrivileges {
[key: string]: string[] | undefined;
}
export interface Privileges {
hasAllPrivileges: boolean;
missingPrivileges: MissingPrivileges;
}

View file

@ -0,0 +1,17 @@
/*
* 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 type { PutTransformsRequestSchema } from '../api_schemas/transforms';
export type IndexName = string;
export type IndexPattern = string;
export type TransformId = string;
export interface TransformPivotConfig extends PutTransformsRequestSchema {
id: TransformId;
create_time?: number;
version?: string;
}

View file

@ -0,0 +1,62 @@
/*
* 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 { TransformState, TRANSFORM_STATE } from '../constants';
import { TransformId } from './transform';
export interface TransformStats {
id: TransformId;
checkpointing: {
last: {
checkpoint: number;
timestamp_millis?: number;
};
next?: {
checkpoint: number;
checkpoint_progress?: {
total_docs: number;
docs_remaining: number;
percent_complete: number;
};
};
operations_behind: number;
};
node?: {
id: string;
name: string;
ephemeral_id: string;
transport_address: string;
attributes: Record<string, any>;
};
stats: {
documents_indexed: number;
documents_processed: number;
index_failures: number;
index_time_in_ms: number;
index_total: number;
pages_processed: number;
search_failures: number;
search_time_in_ms: number;
search_total: number;
trigger_count: number;
processing_time_in_ms: number;
processing_total: number;
exponential_avg_checkpoint_duration_ms: number;
exponential_avg_documents_indexed: number;
exponential_avg_documents_processed: number;
};
reason?: string;
state: TransformState;
}
export function isTransformStats(arg: any): arg is TransformStats {
return (
typeof arg === 'object' &&
arg !== null &&
{}.hasOwnProperty.call(arg, 'state') &&
Object.values(TRANSFORM_STATE).includes(arg.state)
);
}

View file

@ -23,7 +23,6 @@ export {
DataGrid,
EsSorting,
RenderCellValue,
SearchResponse7,
UseDataGridReturnType,
UseIndexDataReturnType,
INDEX_STATUS,

View file

@ -6,7 +6,7 @@
import { composeValidators, patternValidator } from '../../../../ml/public';
export type AggName = string;
import { AggName } from '../../../common/types/aggregations';
export function isAggName(arg: any): arg is AggName {
// allow all characters except `[]>` and must not start or end with a space.

View file

@ -4,11 +4,12 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { PIVOT_SUPPORTED_AGGS } from '../../../common/types/pivot_aggs';
import {
getPreviewRequestBody,
getPreviewTransformRequestBody,
PivotAggsConfig,
PivotGroupByConfig,
PIVOT_SUPPORTED_AGGS,
PIVOT_SUPPORTED_GROUP_BY_AGGS,
SimpleQuery,
} from '../common';
@ -35,7 +36,12 @@ describe('Transform: Data Grid', () => {
aggName: 'the-agg-agg-name',
dropDownName: 'the-agg-drop-down-name',
};
const request = getPreviewRequestBody('the-index-pattern-title', query, [groupBy], [agg]);
const request = getPreviewTransformRequestBody(
'the-index-pattern-title',
query,
[groupBy],
[agg]
);
const pivotPreviewDevConsoleStatement = getPivotPreviewDevConsoleStatement(request);
expect(pivotPreviewDevConsoleStatement).toBe(`POST _transform/_preview

View file

@ -4,12 +4,13 @@
* you may not use this file except in compliance with the Elastic License.
*/
import type { PostTransformsPreviewRequestSchema } from '../../../common/api_schemas/transforms';
import { PivotQuery } from './request';
import { PreviewRequestBody } from './transform';
export const INIT_MAX_COLUMNS = 20;
export const getPivotPreviewDevConsoleStatement = (request: PreviewRequestBody) => {
export const getPivotPreviewDevConsoleStatement = (request: PostTransformsPreviewRequestSchema) => {
return `POST _transform/_preview\n${JSON.stringify(request, null, 2)}\n`;
};

View file

@ -5,10 +5,10 @@
*/
import { Dictionary } from '../../../common/types/common';
import { EsFieldName } from '../../../common/types/fields';
export type EsId = string;
export type EsDocSource = Dictionary<any>;
export type EsFieldName = string;
export interface EsDoc extends Dictionary<any> {
_id: EsId;

View file

@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
export { AggName, isAggName } from './aggregations';
export { isAggName } from './aggregations';
export {
getIndexDevConsoleStatement,
getPivotPreviewDevConsoleStatement,
@ -17,44 +17,28 @@ export {
toggleSelectedField,
EsDoc,
EsDocSource,
EsFieldName,
} from './fields';
export { DropDownLabel, DropDownOption, Label } from './dropdown';
export {
isTransformIdValid,
refreshTransformList$,
useRefreshTransformList,
CreateRequestBody,
PreviewRequestBody,
TransformPivotConfig,
IndexName,
IndexPattern,
REFRESH_TRANSFORM_LIST_STATE,
} from './transform';
export { TRANSFORM_LIST_COLUMN, TransformListAction, TransformListRow } from './transform_list';
export {
getTransformProgress,
isCompletedBatchTransform,
isTransformStats,
TransformStats,
TRANSFORM_MODE,
} from './transform_stats';
export { getTransformProgress, isCompletedBatchTransform } from './transform_stats';
export { getDiscoverUrl } from './navigation';
export { GetTransformsResponse, PreviewData, PreviewMappings } from './pivot_preview';
export {
getEsAggFromAggConfig,
isPivotAggsConfigWithUiSupport,
isPivotAggsConfigPercentiles,
PERCENTILES_AGG_DEFAULT_PERCENTS,
PivotAgg,
PivotAggDict,
PivotAggsConfig,
PivotAggsConfigDict,
PivotAggsConfigBase,
PivotAggsConfigWithUiSupport,
PivotAggsConfigWithUiSupportDict,
pivotAggsFieldSupport,
PIVOT_SUPPORTED_AGGS,
} from './pivot_aggs';
export {
dateHistogramIntervalFormatRegex,
@ -65,25 +49,19 @@ export {
isGroupByHistogram,
isGroupByTerms,
pivotGroupByFieldSupport,
DateHistogramAgg,
GenericAgg,
GroupByConfigWithInterval,
GroupByConfigWithUiSupport,
HistogramAgg,
PivotGroupBy,
PivotGroupByConfig,
PivotGroupByDict,
PivotGroupByConfigDict,
PivotGroupByConfigWithUiSupportDict,
PivotSupportedGroupByAggs,
PivotSupportedGroupByAggsWithInterval,
PIVOT_SUPPORTED_GROUP_BY_AGGS,
TermsAgg,
} from './pivot_group_by';
export {
defaultQuery,
getPreviewRequestBody,
getCreateRequestBody,
getPreviewTransformRequestBody,
getCreateTransformRequestBody,
getPivotQuery,
isDefaultQuery,
isMatchAllQuery,

View file

@ -5,31 +5,22 @@
*/
import { FC } from 'react';
import { Dictionary } from '../../../common/types/common';
import { KBN_FIELD_TYPES } from '../../../../../../src/plugins/data/common';
import { AggName } from './aggregations';
import { EsFieldName } from './fields';
import type { AggName } from '../../../common/types/aggregations';
import type { Dictionary } from '../../../common/types/common';
import type { EsFieldName } from '../../../common/types/fields';
import type { PivotAgg, PivotSupportedAggs } from '../../../common/types/pivot_aggs';
import { PIVOT_SUPPORTED_AGGS } from '../../../common/types/pivot_aggs';
import { getAggFormConfig } from '../sections/create_transform/components/step_define/common/get_agg_form_config';
import { PivotAggsConfigFilter } from '../sections/create_transform/components/step_define/common/filter_agg/types';
export type PivotSupportedAggs = typeof PIVOT_SUPPORTED_AGGS[keyof typeof PIVOT_SUPPORTED_AGGS];
export function isPivotSupportedAggs(arg: any): arg is PivotSupportedAggs {
return Object.values(PIVOT_SUPPORTED_AGGS).includes(arg);
}
export const PIVOT_SUPPORTED_AGGS = {
AVG: 'avg',
CARDINALITY: 'cardinality',
MAX: 'max',
MIN: 'min',
PERCENTILES: 'percentiles',
SUM: 'sum',
VALUE_COUNT: 'value_count',
FILTER: 'filter',
} as const;
export const PERCENTILES_AGG_DEFAULT_PERCENTS = [1, 5, 25, 50, 75, 95, 99];
export const pivotAggsFieldSupport = {
@ -69,16 +60,6 @@ export const pivotAggsFieldSupport = {
[KBN_FIELD_TYPES.CONFLICT]: [PIVOT_SUPPORTED_AGGS.VALUE_COUNT, PIVOT_SUPPORTED_AGGS.FILTER],
};
export type PivotAgg = {
[key in PivotSupportedAggs]?: {
field: EsFieldName;
};
};
export type PivotAggDict = {
[key in AggName]: PivotAgg;
};
/**
* The maximum level of sub-aggregations
*/

View file

@ -4,12 +4,12 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { AggName } from '../../../common/types/aggregations';
import { Dictionary } from '../../../common/types/common';
import { EsFieldName } from '../../../common/types/fields';
import { GenericAgg } from '../../../common/types/pivot_group_by';
import { KBN_FIELD_TYPES } from '../../../../../../src/plugins/data/common';
import { AggName } from './aggregations';
import { EsFieldName } from './fields';
export enum PIVOT_SUPPORTED_GROUP_BY_AGGS {
DATE_HISTOGRAM = 'date_histogram',
HISTOGRAM = 'histogram',
@ -106,31 +106,6 @@ export function isPivotGroupByConfigWithUiSupport(arg: any): arg is GroupByConfi
return isGroupByDateHistogram(arg) || isGroupByHistogram(arg) || isGroupByTerms(arg);
}
export type GenericAgg = object;
export interface TermsAgg {
terms: {
field: EsFieldName;
};
}
export interface HistogramAgg {
histogram: {
field: EsFieldName;
interval: string;
};
}
export interface DateHistogramAgg {
date_histogram: {
field: EsFieldName;
calendar_interval: string;
};
}
export type PivotGroupBy = GenericAgg | TermsAgg | HistogramAgg | DateHistogramAgg;
export type PivotGroupByDict = Dictionary<PivotGroupBy>;
export function getEsAggFromGroupByConfig(groupByConfig: GroupByConfigBase): GenericAgg {
const { agg, aggName, dropDownName, ...esAgg } = groupByConfig;

View file

@ -1,29 +0,0 @@
/*
* 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 { ES_FIELD_TYPES } from '../../../../../../src/plugins/data/public';
import { Dictionary } from '../../../common/types/common';
interface EsMappingType {
type: ES_FIELD_TYPES;
}
export type PreviewItem = Dictionary<any>;
export type PreviewData = PreviewItem[];
export interface PreviewMappings {
properties: Dictionary<EsMappingType>;
}
export interface GetTransformsResponse {
preview: PreviewData;
generated_dest_index: {
mappings: PreviewMappings;
// Not in use yet
aliases: any;
settings: any;
};
}

View file

@ -4,17 +4,19 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { PIVOT_SUPPORTED_AGGS } from '../../../common/types/pivot_aggs';
import { PivotGroupByConfig } from '../common';
import { StepDefineExposedState } from '../sections/create_transform/components/step_define';
import { StepDetailsExposedState } from '../sections/create_transform/components/step_details/step_details_form';
import { PIVOT_SUPPORTED_GROUP_BY_AGGS } from './pivot_group_by';
import { PivotAggsConfig, PIVOT_SUPPORTED_AGGS } from './pivot_aggs';
import { PivotAggsConfig } from './pivot_aggs';
import {
defaultQuery,
getPreviewRequestBody,
getCreateRequestBody,
getPreviewTransformRequestBody,
getCreateTransformRequestBody,
getPivotQuery,
isDefaultQuery,
isMatchAllQuery,
@ -55,7 +57,7 @@ describe('Transform: Common', () => {
});
});
test('getPreviewRequestBody()', () => {
test('getPreviewTransformRequestBody()', () => {
const query = getPivotQuery('the-query');
const groupBy: PivotGroupByConfig[] = [
{
@ -73,7 +75,7 @@ describe('Transform: Common', () => {
dropDownName: 'the-agg-drop-down-name',
},
];
const request = getPreviewRequestBody('the-index-pattern-title', query, groupBy, aggs);
const request = getPreviewTransformRequestBody('the-index-pattern-title', query, groupBy, aggs);
expect(request).toEqual({
pivot: {
@ -87,7 +89,7 @@ describe('Transform: Common', () => {
});
});
test('getPreviewRequestBody() with comma-separated index pattern', () => {
test('getPreviewTransformRequestBody() with comma-separated index pattern', () => {
const query = getPivotQuery('the-query');
const groupBy: PivotGroupByConfig[] = [
{
@ -105,7 +107,7 @@ describe('Transform: Common', () => {
dropDownName: 'the-agg-drop-down-name',
},
];
const request = getPreviewRequestBody(
const request = getPreviewTransformRequestBody(
'the-index-pattern-title,the-other-title',
query,
groupBy,
@ -124,7 +126,7 @@ describe('Transform: Common', () => {
});
});
test('getCreateRequestBody()', () => {
test('getCreateTransformRequestBody()', () => {
const groupBy: PivotGroupByConfig = {
agg: PIVOT_SUPPORTED_GROUP_BY_AGGS.TERMS,
field: 'the-group-by-field',
@ -160,7 +162,7 @@ describe('Transform: Common', () => {
valid: true,
};
const request = getCreateRequestBody(
const request = getCreateTransformRequestBody(
'the-index-pattern-title',
pivotState,
transformDetailsState

View file

@ -4,15 +4,25 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { DefaultOperator } from 'elasticsearch';
import type { DefaultOperator } from 'elasticsearch';
import { HttpFetchError } from '../../../../../../src/core/public';
import type { IndexPattern } from '../../../../../../src/plugins/data/public';
import type {
PostTransformsPreviewRequestSchema,
PutTransformsRequestSchema,
} from '../../../common/api_schemas/transforms';
import type {
DateHistogramAgg,
HistogramAgg,
TermsAgg,
} from '../../../common/types/pivot_group_by';
import { dictionaryToArray } from '../../../common/types/common';
import { SavedSearchQuery } from '../hooks/use_search_items';
import { StepDefineExposedState } from '../sections/create_transform/components/step_define';
import { StepDetailsExposedState } from '../sections/create_transform/components/step_details/step_details_form';
import { IndexPattern } from '../../../../../../src/plugins/data/public';
import type { SavedSearchQuery } from '../hooks/use_search_items';
import type { StepDefineExposedState } from '../sections/create_transform/components/step_define';
import type { StepDetailsExposedState } from '../sections/create_transform/components/step_details/step_details_form';
import {
getEsAggFromAggConfig,
@ -24,8 +34,6 @@ import {
} from '../common';
import { PivotAggsConfig } from './pivot_aggs';
import { DateHistogramAgg, HistogramAgg, TermsAgg } from './pivot_group_by';
import { PreviewRequestBody, CreateRequestBody } from './transform';
export interface SimpleQuery {
query_string: {
@ -63,17 +71,18 @@ export function isDefaultQuery(query: PivotQuery): boolean {
return isSimpleQuery(query) && query.query_string.query === '*';
}
export function getPreviewRequestBody(
export function getPreviewTransformRequestBody(
indexPatternTitle: IndexPattern['title'],
query: PivotQuery,
groupBy: PivotGroupByConfig[],
aggs: PivotAggsConfig[]
): PreviewRequestBody {
): PostTransformsPreviewRequestSchema {
const index = indexPatternTitle.split(',').map((name: string) => name.trim());
const request: PreviewRequestBody = {
const request: PostTransformsPreviewRequestSchema = {
source: {
index,
...(!isDefaultQuery(query) && !isMatchAllQuery(query) ? { query } : {}),
},
pivot: {
group_by: {},
@ -81,10 +90,6 @@ export function getPreviewRequestBody(
},
};
if (!isDefaultQuery(query) && !isMatchAllQuery(query)) {
request.source.query = query;
}
groupBy.forEach((g) => {
if (isGroupByTerms(g)) {
const termsAgg: TermsAgg = {
@ -125,37 +130,41 @@ export function getPreviewRequestBody(
return request;
}
export function getCreateRequestBody(
export const getCreateTransformRequestBody = (
indexPatternTitle: IndexPattern['title'],
pivotState: StepDefineExposedState,
transformDetailsState: StepDetailsExposedState
): CreateRequestBody {
const request: CreateRequestBody = {
...getPreviewRequestBody(
indexPatternTitle,
getPivotQuery(pivotState.searchQuery),
dictionaryToArray(pivotState.groupByList),
dictionaryToArray(pivotState.aggList)
),
// conditionally add optional description
...(transformDetailsState.transformDescription !== ''
? { description: transformDetailsState.transformDescription }
: {}),
dest: {
index: transformDetailsState.destinationIndex,
},
// conditionally add continuous mode config
...(transformDetailsState.isContinuousModeEnabled
? {
sync: {
time: {
field: transformDetailsState.continuousModeDateField,
delay: transformDetailsState.continuousModeDelay,
},
): PutTransformsRequestSchema => ({
...getPreviewTransformRequestBody(
indexPatternTitle,
getPivotQuery(pivotState.searchQuery),
dictionaryToArray(pivotState.groupByList),
dictionaryToArray(pivotState.aggList)
),
// conditionally add optional description
...(transformDetailsState.transformDescription !== ''
? { description: transformDetailsState.transformDescription }
: {}),
dest: {
index: transformDetailsState.destinationIndex,
},
// conditionally add continuous mode config
...(transformDetailsState.isContinuousModeEnabled
? {
sync: {
time: {
field: transformDetailsState.continuousModeDateField,
delay: transformDetailsState.continuousModeDelay,
},
}
: {}),
};
},
}
: {}),
});
return request;
export function isHttpFetchError(error: any): error is HttpFetchError {
return (
error instanceof HttpFetchError &&
typeof error.name === 'string' &&
typeof error.message !== 'undefined'
);
}

View file

@ -9,13 +9,7 @@ import { BehaviorSubject } from 'rxjs';
import { filter, distinctUntilChanged } from 'rxjs/operators';
import { Subscription } from 'rxjs';
import { TransformId } from '../../../common';
import { PivotAggDict } from './pivot_aggs';
import { PivotGroupByDict } from './pivot_group_by';
export type IndexName = string;
export type IndexPattern = string;
import { TransformId } from '../../../common/types/transform';
// Transform name must contain lowercase alphanumeric (a-z and 0-9), hyphens or underscores;
// It must also start and end with an alphanumeric character.
@ -23,41 +17,6 @@ export function isTransformIdValid(transformId: TransformId) {
return /^[a-z0-9\-\_]+$/g.test(transformId) && !/^([_-].*)?(.*[_-])?$/g.test(transformId);
}
export interface PreviewRequestBody {
pivot: {
group_by: PivotGroupByDict;
aggregations: PivotAggDict;
};
source: {
index: IndexPattern | IndexPattern[];
query?: any;
};
}
export interface CreateRequestBody extends PreviewRequestBody {
description?: string;
dest: {
index: IndexName;
};
frequency?: string;
settings?: {
max_page_search_size?: number;
docs_per_second?: number;
};
sync?: {
time: {
field: string;
delay: string;
};
};
}
export interface TransformPivotConfig extends CreateRequestBody {
id: TransformId;
create_time?: number;
version?: string;
}
export enum REFRESH_TRANSFORM_LIST_STATE {
ERROR = 'error',
IDLE = 'idle',

View file

@ -6,9 +6,8 @@
import { EuiTableActionsColumnType } from '@elastic/eui';
import { TransformId } from '../../../common';
import { TransformPivotConfig } from './transform';
import { TransformStats } from './transform_stats';
import { TransformId, TransformPivotConfig } from '../../../common/types/transform';
import { TransformStats } from '../../../common/types/transform_stats';
// Used to pass on attribute names to table columns
export enum TRANSFORM_LIST_COLUMN {

View file

@ -4,64 +4,10 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { TransformId, TRANSFORM_STATE } from '../../../common';
import { TRANSFORM_STATE } from '../../../common/constants';
import { TransformListRow } from './transform_list';
export enum TRANSFORM_MODE {
BATCH = 'batch',
CONTINUOUS = 'continuous',
}
export interface TransformStats {
id: TransformId;
checkpointing: {
last: {
checkpoint: number;
timestamp_millis?: number;
};
next?: {
checkpoint: number;
checkpoint_progress?: {
total_docs: number;
docs_remaining: number;
percent_complete: number;
};
};
operations_behind: number;
};
node?: {
id: string;
name: string;
ephemeral_id: string;
transport_address: string;
attributes: Record<string, any>;
};
stats: {
documents_indexed: number;
documents_processed: number;
index_failures: number;
index_time_in_ms: number;
index_total: number;
pages_processed: number;
search_failures: number;
search_time_in_ms: number;
search_total: number;
trigger_count: number;
};
reason?: string;
state: TRANSFORM_STATE;
}
export function isTransformStats(arg: any): arg is TransformStats {
return (
typeof arg === 'object' &&
arg !== null &&
{}.hasOwnProperty.call(arg, 'state') &&
Object.values(TRANSFORM_STATE).includes(arg.state)
);
}
export function getTransformProgress(item: TransformListRow) {
if (isCompletedBatchTransform(item)) {
return 100;

View file

@ -4,66 +4,161 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { TransformId, TransformEndpointRequest } from '../../../../common';
import { HttpFetchError } from 'kibana/public';
import { PreviewRequestBody } from '../../common';
import { KBN_FIELD_TYPES } from '../../../../../../../src/plugins/data/public';
import { TransformId } from '../../../../common/types/transform';
import type { FieldHistogramsResponseSchema } from '../../../../common/api_schemas/field_histograms';
import type { GetTransformsAuditMessagesResponseSchema } from '../../../../common/api_schemas/audit_messages';
import type {
DeleteTransformsRequestSchema,
DeleteTransformsResponseSchema,
} from '../../../../common/api_schemas/delete_transforms';
import type {
StartTransformsRequestSchema,
StartTransformsResponseSchema,
} from '../../../../common/api_schemas/start_transforms';
import type {
StopTransformsRequestSchema,
StopTransformsResponseSchema,
} from '../../../../common/api_schemas/stop_transforms';
import type {
GetTransformsResponseSchema,
PostTransformsPreviewRequestSchema,
PostTransformsPreviewResponseSchema,
PutTransformsRequestSchema,
PutTransformsResponseSchema,
} from '../../../../common/api_schemas/transforms';
import type { GetTransformsStatsResponseSchema } from '../../../../common/api_schemas/transforms_stats';
import type {
PostTransformsUpdateRequestSchema,
PostTransformsUpdateResponseSchema,
} from '../../../../common/api_schemas/update_transforms';
import type { SearchResponse7 } from '../../../../common/shared_imports';
import { EsIndex } from '../../../../common/types/es_index';
import type { SavedSearchQuery } from '../use_search_items';
// Default sampler shard size used for field histograms
export const DEFAULT_SAMPLER_SHARD_SIZE = 5000;
export interface FieldHistogramRequestConfig {
fieldName: string;
type?: KBN_FIELD_TYPES;
}
const apiFactory = () => ({
getTransforms(transformId?: TransformId): Promise<any> {
return new Promise((resolve, reject) => {
resolve([]);
async getTransform(
transformId: TransformId
): Promise<GetTransformsResponseSchema | HttpFetchError> {
return Promise.resolve({ count: 0, transforms: [] });
},
async getTransforms(): Promise<GetTransformsResponseSchema | HttpFetchError> {
return Promise.resolve({ count: 0, transforms: [] });
},
async getTransformStats(
transformId: TransformId
): Promise<GetTransformsStatsResponseSchema | HttpFetchError> {
return Promise.resolve({ count: 0, transforms: [] });
},
async getTransformsStats(): Promise<GetTransformsStatsResponseSchema | HttpFetchError> {
return Promise.resolve({ count: 0, transforms: [] });
},
async createTransform(
transformId: TransformId,
transformConfig: PutTransformsRequestSchema
): Promise<PutTransformsResponseSchema | HttpFetchError> {
return Promise.resolve({ transformsCreated: [], errors: [] });
},
async updateTransform(
transformId: TransformId,
transformConfig: PostTransformsUpdateRequestSchema
): Promise<PostTransformsUpdateResponseSchema | HttpFetchError> {
return Promise.resolve({
id: 'the-test-id',
source: { index: ['the-index-name'], query: { match_all: {} } },
dest: { index: 'user-the-destination-index-name' },
frequency: '10m',
pivot: {
group_by: { the_group: { terms: { field: 'the-group-by-field' } } },
aggregations: { the_agg: { value_count: { field: 'the-agg-field' } } },
},
description: 'the-description',
settings: { docs_per_second: null },
version: '8.0.0',
create_time: 1598860879097,
});
},
async deleteTransforms(
reqBody: DeleteTransformsRequestSchema
): Promise<DeleteTransformsResponseSchema | HttpFetchError> {
return Promise.resolve({});
},
async getTransformsPreview(
obj: PostTransformsPreviewRequestSchema
): Promise<PostTransformsPreviewResponseSchema | HttpFetchError> {
return Promise.resolve({
generated_dest_index: {
mappings: {
_meta: {
_transform: {
transform: 'the-transform',
version: { create: 'the-version' },
creation_date_in_millis: 0,
},
created_by: 'mock',
},
properties: {},
},
settings: { index: { number_of_shards: '1', auto_expand_replicas: '0-1' } },
aliases: {},
},
preview: [],
});
},
async startTransforms(
reqBody: StartTransformsRequestSchema
): Promise<StartTransformsResponseSchema | HttpFetchError> {
return Promise.resolve({});
},
async stopTransforms(
transformsInfo: StopTransformsRequestSchema
): Promise<StopTransformsResponseSchema | HttpFetchError> {
return Promise.resolve({});
},
async getTransformAuditMessages(
transformId: TransformId
): Promise<GetTransformsAuditMessagesResponseSchema | HttpFetchError> {
return Promise.resolve([]);
},
async esSearch(payload: any): Promise<SearchResponse7 | HttpFetchError> {
return Promise.resolve({
hits: {
hits: [],
total: {
value: 0,
relation: 'the-relation',
},
max_score: 0,
},
timed_out: false,
took: 10,
_shards: { total: 1, successful: 1, failed: 0, skipped: 0 },
});
},
getTransformsStats(transformId?: TransformId): Promise<any> {
if (transformId !== undefined) {
return new Promise((resolve, reject) => {
resolve([]);
});
}
return new Promise((resolve, reject) => {
resolve([]);
});
async getEsIndices(): Promise<EsIndex[] | HttpFetchError> {
return Promise.resolve([]);
},
createTransform(transformId: TransformId, transformConfig: any): Promise<any> {
return new Promise((resolve, reject) => {
resolve([]);
});
},
deleteTransforms(transformsInfo: TransformEndpointRequest[]) {
return new Promise((resolve, reject) => {
resolve([]);
});
},
getTransformsPreview(obj: PreviewRequestBody): Promise<any> {
return new Promise((resolve, reject) => {
resolve([]);
});
},
startTransforms(transformsInfo: TransformEndpointRequest[]) {
return new Promise((resolve, reject) => {
resolve([]);
});
},
stopTransforms(transformsInfo: TransformEndpointRequest[]) {
return new Promise((resolve, reject) => {
resolve([]);
});
},
getTransformAuditMessages(transformId: TransformId): Promise<any> {
return new Promise((resolve, reject) => {
resolve([]);
});
},
esSearch(payload: any) {
return new Promise((resolve, reject) => {
resolve([]);
});
},
getIndices() {
return new Promise((resolve, reject) => {
resolve([]);
});
async getHistogramsForFields(
indexPatternTitle: string,
fields: FieldHistogramRequestConfig[],
query: string | SavedSearchQuery,
samplerShardSize = DEFAULT_SAMPLER_SHARD_SIZE
): Promise<FieldHistogramsResponseSchema | HttpFetchError> {
return Promise.resolve([]);
},
});

View file

@ -6,20 +6,43 @@
import { useMemo } from 'react';
import { HttpFetchError } from 'kibana/public';
import { KBN_FIELD_TYPES } from '../../../../../../src/plugins/data/public';
import {
TransformId,
TransformEndpointRequest,
TransformEndpointResult,
DeleteTransformEndpointResult,
} from '../../../common';
import type { GetTransformsAuditMessagesResponseSchema } from '../../../common/api_schemas/audit_messages';
import type {
DeleteTransformsRequestSchema,
DeleteTransformsResponseSchema,
} from '../../../common/api_schemas/delete_transforms';
import type { FieldHistogramsResponseSchema } from '../../../common/api_schemas/field_histograms';
import type {
StartTransformsRequestSchema,
StartTransformsResponseSchema,
} from '../../../common/api_schemas/start_transforms';
import type {
StopTransformsRequestSchema,
StopTransformsResponseSchema,
} from '../../../common/api_schemas/stop_transforms';
import type {
GetTransformsResponseSchema,
PostTransformsPreviewRequestSchema,
PostTransformsPreviewResponseSchema,
PutTransformsRequestSchema,
PutTransformsResponseSchema,
} from '../../../common/api_schemas/transforms';
import type {
PostTransformsUpdateRequestSchema,
PostTransformsUpdateResponseSchema,
} from '../../../common/api_schemas/update_transforms';
import type { GetTransformsStatsResponseSchema } from '../../../common/api_schemas/transforms_stats';
import { TransformId } from '../../../common/types/transform';
import { API_BASE_PATH } from '../../../common/constants';
import { EsIndex } from '../../../common/types/es_index';
import type { SearchResponse7 } from '../../../common/shared_imports';
import { useAppDependencies } from '../app_dependencies';
import { GetTransformsResponse, PreviewRequestBody } from '../common';
import { EsIndex } from './use_api_types';
import { SavedSearchQuery } from './use_search_items';
// Default sampler shard size used for field histograms
@ -35,81 +58,146 @@ export const useApi = () => {
return useMemo(
() => ({
getTransforms(transformId?: TransformId): Promise<any> {
const transformIdString = transformId !== undefined ? `/${transformId}` : '';
return http.get(`${API_BASE_PATH}transforms${transformIdString}`);
},
getTransformsStats(transformId?: TransformId): Promise<any> {
if (transformId !== undefined) {
return http.get(`${API_BASE_PATH}transforms/${transformId}/_stats`);
async getTransform(
transformId: TransformId
): Promise<GetTransformsResponseSchema | HttpFetchError> {
try {
return await http.get(`${API_BASE_PATH}transforms/${transformId}`);
} catch (e) {
return e;
}
return http.get(`${API_BASE_PATH}transforms/_stats`);
},
createTransform(transformId: TransformId, transformConfig: any): Promise<any> {
return http.put(`${API_BASE_PATH}transforms/${transformId}`, {
body: JSON.stringify(transformConfig),
});
async getTransforms(): Promise<GetTransformsResponseSchema | HttpFetchError> {
try {
return await http.get(`${API_BASE_PATH}transforms`);
} catch (e) {
return e;
}
},
updateTransform(transformId: TransformId, transformConfig: any): Promise<any> {
return http.post(`${API_BASE_PATH}transforms/${transformId}/_update`, {
body: JSON.stringify(transformConfig),
});
async getTransformStats(
transformId: TransformId
): Promise<GetTransformsStatsResponseSchema | HttpFetchError> {
try {
return await http.get(`${API_BASE_PATH}transforms/${transformId}/_stats`);
} catch (e) {
return e;
}
},
deleteTransforms(
transformsInfo: TransformEndpointRequest[],
deleteDestIndex: boolean | undefined,
deleteDestIndexPattern: boolean | undefined,
forceDelete: boolean
): Promise<DeleteTransformEndpointResult> {
return http.post(`${API_BASE_PATH}delete_transforms`, {
body: JSON.stringify({
transformsInfo,
deleteDestIndex,
deleteDestIndexPattern,
forceDelete,
}),
});
async getTransformsStats(): Promise<GetTransformsStatsResponseSchema | HttpFetchError> {
try {
return await http.get(`${API_BASE_PATH}transforms/_stats`);
} catch (e) {
return e;
}
},
getTransformsPreview(obj: PreviewRequestBody): Promise<GetTransformsResponse> {
return http.post(`${API_BASE_PATH}transforms/_preview`, {
body: JSON.stringify(obj),
});
async createTransform(
transformId: TransformId,
transformConfig: PutTransformsRequestSchema
): Promise<PutTransformsResponseSchema | HttpFetchError> {
try {
return await http.put(`${API_BASE_PATH}transforms/${transformId}`, {
body: JSON.stringify(transformConfig),
});
} catch (e) {
return e;
}
},
startTransforms(
transformsInfo: TransformEndpointRequest[]
): Promise<TransformEndpointResult> {
return http.post(`${API_BASE_PATH}start_transforms`, {
body: JSON.stringify(transformsInfo),
});
async updateTransform(
transformId: TransformId,
transformConfig: PostTransformsUpdateRequestSchema
): Promise<PostTransformsUpdateResponseSchema | HttpFetchError> {
try {
return await http.post(`${API_BASE_PATH}transforms/${transformId}/_update`, {
body: JSON.stringify(transformConfig),
});
} catch (e) {
return e;
}
},
stopTransforms(transformsInfo: TransformEndpointRequest[]): Promise<TransformEndpointResult> {
return http.post(`${API_BASE_PATH}stop_transforms`, {
body: JSON.stringify(transformsInfo),
});
async deleteTransforms(
reqBody: DeleteTransformsRequestSchema
): Promise<DeleteTransformsResponseSchema | HttpFetchError> {
try {
return await http.post(`${API_BASE_PATH}delete_transforms`, {
body: JSON.stringify(reqBody),
});
} catch (e) {
return e;
}
},
getTransformAuditMessages(transformId: TransformId): Promise<any> {
return http.get(`${API_BASE_PATH}transforms/${transformId}/messages`);
async getTransformsPreview(
obj: PostTransformsPreviewRequestSchema
): Promise<PostTransformsPreviewResponseSchema | HttpFetchError> {
try {
return await http.post(`${API_BASE_PATH}transforms/_preview`, {
body: JSON.stringify(obj),
});
} catch (e) {
return e;
}
},
esSearch(payload: any): Promise<any> {
return http.post(`${API_BASE_PATH}es_search`, { body: JSON.stringify(payload) });
async startTransforms(
reqBody: StartTransformsRequestSchema
): Promise<StartTransformsResponseSchema | HttpFetchError> {
try {
return await http.post(`${API_BASE_PATH}start_transforms`, {
body: JSON.stringify(reqBody),
});
} catch (e) {
return e;
}
},
getIndices(): Promise<EsIndex[]> {
return http.get(`/api/index_management/indices`);
async stopTransforms(
transformsInfo: StopTransformsRequestSchema
): Promise<StopTransformsResponseSchema | HttpFetchError> {
try {
return await http.post(`${API_BASE_PATH}stop_transforms`, {
body: JSON.stringify(transformsInfo),
});
} catch (e) {
return e;
}
},
getHistogramsForFields(
async getTransformAuditMessages(
transformId: TransformId
): Promise<GetTransformsAuditMessagesResponseSchema | HttpFetchError> {
try {
return await http.get(`${API_BASE_PATH}transforms/${transformId}/messages`);
} catch (e) {
return e;
}
},
async esSearch(payload: any): Promise<SearchResponse7 | HttpFetchError> {
try {
return await http.post(`${API_BASE_PATH}es_search`, { body: JSON.stringify(payload) });
} catch (e) {
return e;
}
},
async getEsIndices(): Promise<EsIndex[] | HttpFetchError> {
try {
return await http.get(`/api/index_management/indices`);
} catch (e) {
return e;
}
},
async getHistogramsForFields(
indexPatternTitle: string,
fields: FieldHistogramRequestConfig[],
query: string | SavedSearchQuery,
samplerShardSize = DEFAULT_SAMPLER_SHARD_SIZE
) {
return http.post(`${API_BASE_PATH}field_histograms/${indexPatternTitle}`, {
body: JSON.stringify({
query,
fields,
samplerShardSize,
}),
});
): Promise<FieldHistogramsResponseSchema | HttpFetchError> {
try {
return await http.post(`${API_BASE_PATH}field_histograms/${indexPatternTitle}`, {
body: JSON.stringify({
query,
fields,
samplerShardSize,
}),
});
} catch (e) {
return e;
}
},
}),
[http]

View file

@ -7,11 +7,11 @@
import React, { useCallback, useEffect, useState } from 'react';
import { i18n } from '@kbn/i18n';
import { toMountPoint } from '../../../../../../src/plugins/kibana_react/public';
import {
DeleteTransformEndpointResult,
import type {
DeleteTransformStatus,
TransformEndpointRequest,
} from '../../../common';
DeleteTransformsRequestSchema,
} from '../../../common/api_schemas/delete_transforms';
import { isDeleteTransformsResponseSchema } from '../../../common/api_schemas/type_guards';
import { extractErrorMessage } from '../../shared_imports';
import { getErrorMessage } from '../../../common/utils/errors';
import { useAppDependencies, useToastNotifications } from '../app_dependencies';
@ -109,161 +109,10 @@ export const useDeleteTransforms = () => {
const toastNotifications = useToastNotifications();
const api = useApi();
return async (
transforms: TransformListRow[],
shouldDeleteDestIndex: boolean,
shouldDeleteDestIndexPattern: boolean,
shouldForceDelete = false
) => {
const transformsInfo: TransformEndpointRequest[] = transforms.map((tf) => ({
id: tf.config.id,
state: tf.stats.state,
}));
return async (reqBody: DeleteTransformsRequestSchema) => {
const results = await api.deleteTransforms(reqBody);
try {
const results: DeleteTransformEndpointResult = await api.deleteTransforms(
transformsInfo,
shouldDeleteDestIndex,
shouldDeleteDestIndexPattern,
shouldForceDelete
);
const isBulk = Object.keys(results).length > 1;
const successCount: Record<SuccessCountField, number> = {
transformDeleted: 0,
destIndexDeleted: 0,
destIndexPatternDeleted: 0,
};
for (const transformId in results) {
// hasOwnProperty check to ensure only properties on object itself, and not its prototypes
if (results.hasOwnProperty(transformId)) {
const status = results[transformId];
const destinationIndex = status.destinationIndex;
// if we are only deleting one transform, show the success toast messages
if (!isBulk && status.transformDeleted) {
if (status.transformDeleted?.success) {
toastNotifications.addSuccess(
i18n.translate('xpack.transform.transformList.deleteTransformSuccessMessage', {
defaultMessage: 'Request to delete transform {transformId} acknowledged.',
values: { transformId },
})
);
}
if (status.destIndexDeleted?.success) {
toastNotifications.addSuccess(
i18n.translate(
'xpack.transform.deleteTransform.deleteAnalyticsWithIndexSuccessMessage',
{
defaultMessage:
'Request to delete destination index {destinationIndex} acknowledged.',
values: { destinationIndex },
}
)
);
}
if (status.destIndexPatternDeleted?.success) {
toastNotifications.addSuccess(
i18n.translate(
'xpack.transform.deleteTransform.deleteAnalyticsWithIndexPatternSuccessMessage',
{
defaultMessage:
'Request to delete index pattern {destinationIndex} acknowledged.',
values: { destinationIndex },
}
)
);
}
} else {
(Object.keys(successCount) as SuccessCountField[]).forEach((key) => {
if (status[key]?.success) {
successCount[key] = successCount[key] + 1;
}
});
}
if (status.transformDeleted?.error) {
const error = extractErrorMessage(status.transformDeleted.error);
toastNotifications.addDanger({
title: i18n.translate('xpack.transform.transformList.deleteTransformErrorMessage', {
defaultMessage: 'An error occurred deleting the transform {transformId}',
values: { transformId },
}),
text: toMountPoint(
<ToastNotificationText previewTextLength={50} overlays={overlays} text={error} />
),
});
}
if (status.destIndexDeleted?.error) {
const error = extractErrorMessage(status.destIndexDeleted.error);
toastNotifications.addDanger({
title: i18n.translate(
'xpack.transform.deleteTransform.deleteAnalyticsWithIndexErrorMessage',
{
defaultMessage: 'An error occurred deleting destination index {destinationIndex}',
values: { destinationIndex },
}
),
text: toMountPoint(
<ToastNotificationText previewTextLength={50} overlays={overlays} text={error} />
),
});
}
if (status.destIndexPatternDeleted?.error) {
const error = extractErrorMessage(status.destIndexPatternDeleted.error);
toastNotifications.addDanger({
title: i18n.translate(
'xpack.transform.deleteTransform.deleteAnalyticsWithIndexPatternErrorMessage',
{
defaultMessage: 'An error occurred deleting index pattern {destinationIndex}',
values: { destinationIndex },
}
),
text: toMountPoint(
<ToastNotificationText previewTextLength={50} overlays={overlays} text={error} />
),
});
}
}
}
// if we are deleting multiple transforms, combine the success messages
if (isBulk) {
if (successCount.transformDeleted > 0) {
toastNotifications.addSuccess(
i18n.translate('xpack.transform.transformList.bulkDeleteTransformSuccessMessage', {
defaultMessage:
'Successfully deleted {count} {count, plural, one {transform} other {transforms}}.',
values: { count: successCount.transformDeleted },
})
);
}
if (successCount.destIndexDeleted > 0) {
toastNotifications.addSuccess(
i18n.translate('xpack.transform.transformList.bulkDeleteDestIndexSuccessMessage', {
defaultMessage:
'Successfully deleted {count} destination {count, plural, one {index} other {indices}}.',
values: { count: successCount.destIndexDeleted },
})
);
}
if (successCount.destIndexPatternDeleted > 0) {
toastNotifications.addSuccess(
i18n.translate(
'xpack.transform.transformList.bulkDeleteDestIndexPatternSuccessMessage',
{
defaultMessage:
'Successfully deleted {count} destination index {count, plural, one {pattern} other {patterns}}.',
values: { count: successCount.destIndexPatternDeleted },
}
)
);
}
}
refreshTransformList$.next(REFRESH_TRANSFORM_LIST_STATE.REFRESH);
} catch (e) {
if (!isDeleteTransformsResponseSchema(results)) {
toastNotifications.addDanger({
title: i18n.translate('xpack.transform.transformList.deleteTransformGenericErrorMessage', {
defaultMessage: 'An error occurred calling the API endpoint to delete transforms.',
@ -272,10 +121,145 @@ export const useDeleteTransforms = () => {
<ToastNotificationText
previewTextLength={50}
overlays={overlays}
text={getErrorMessage(e)}
text={getErrorMessage(results)}
/>
),
});
return;
}
const isBulk = Object.keys(results).length > 1;
const successCount: Record<SuccessCountField, number> = {
transformDeleted: 0,
destIndexDeleted: 0,
destIndexPatternDeleted: 0,
};
for (const transformId in results) {
// hasOwnProperty check to ensure only properties on object itself, and not its prototypes
if (results.hasOwnProperty(transformId)) {
const status = results[transformId];
const destinationIndex = status.destinationIndex;
// if we are only deleting one transform, show the success toast messages
if (!isBulk && status.transformDeleted) {
if (status.transformDeleted?.success) {
toastNotifications.addSuccess(
i18n.translate('xpack.transform.transformList.deleteTransformSuccessMessage', {
defaultMessage: 'Request to delete transform {transformId} acknowledged.',
values: { transformId },
})
);
}
if (status.destIndexDeleted?.success) {
toastNotifications.addSuccess(
i18n.translate(
'xpack.transform.deleteTransform.deleteAnalyticsWithIndexSuccessMessage',
{
defaultMessage:
'Request to delete destination index {destinationIndex} acknowledged.',
values: { destinationIndex },
}
)
);
}
if (status.destIndexPatternDeleted?.success) {
toastNotifications.addSuccess(
i18n.translate(
'xpack.transform.deleteTransform.deleteAnalyticsWithIndexPatternSuccessMessage',
{
defaultMessage:
'Request to delete index pattern {destinationIndex} acknowledged.',
values: { destinationIndex },
}
)
);
}
} else {
(Object.keys(successCount) as SuccessCountField[]).forEach((key) => {
if (status[key]?.success) {
successCount[key] = successCount[key] + 1;
}
});
}
if (status.transformDeleted?.error) {
const error = extractErrorMessage(status.transformDeleted.error);
toastNotifications.addDanger({
title: i18n.translate('xpack.transform.transformList.deleteTransformErrorMessage', {
defaultMessage: 'An error occurred deleting the transform {transformId}',
values: { transformId },
}),
text: toMountPoint(
<ToastNotificationText previewTextLength={50} overlays={overlays} text={error} />
),
});
}
if (status.destIndexDeleted?.error) {
const error = extractErrorMessage(status.destIndexDeleted.error);
toastNotifications.addDanger({
title: i18n.translate(
'xpack.transform.deleteTransform.deleteAnalyticsWithIndexErrorMessage',
{
defaultMessage: 'An error occurred deleting destination index {destinationIndex}',
values: { destinationIndex },
}
),
text: toMountPoint(
<ToastNotificationText previewTextLength={50} overlays={overlays} text={error} />
),
});
}
if (status.destIndexPatternDeleted?.error) {
const error = extractErrorMessage(status.destIndexPatternDeleted.error);
toastNotifications.addDanger({
title: i18n.translate(
'xpack.transform.deleteTransform.deleteAnalyticsWithIndexPatternErrorMessage',
{
defaultMessage: 'An error occurred deleting index pattern {destinationIndex}',
values: { destinationIndex },
}
),
text: toMountPoint(
<ToastNotificationText previewTextLength={50} overlays={overlays} text={error} />
),
});
}
}
}
// if we are deleting multiple transforms, combine the success messages
if (isBulk) {
if (successCount.transformDeleted > 0) {
toastNotifications.addSuccess(
i18n.translate('xpack.transform.transformList.bulkDeleteTransformSuccessMessage', {
defaultMessage:
'Successfully deleted {count} {count, plural, one {transform} other {transforms}}.',
values: { count: successCount.transformDeleted },
})
);
}
if (successCount.destIndexDeleted > 0) {
toastNotifications.addSuccess(
i18n.translate('xpack.transform.transformList.bulkDeleteDestIndexSuccessMessage', {
defaultMessage:
'Successfully deleted {count} destination {count, plural, one {index} other {indices}}.',
values: { count: successCount.destIndexDeleted },
})
);
}
if (successCount.destIndexPatternDeleted > 0) {
toastNotifications.addSuccess(
i18n.translate('xpack.transform.transformList.bulkDeleteDestIndexPatternSuccessMessage', {
defaultMessage:
'Successfully deleted {count} destination index {count, plural, one {pattern} other {patterns}}.',
values: { count: successCount.destIndexPatternDeleted },
})
);
}
}
refreshTransformList$.next(REFRESH_TRANSFORM_LIST_STATE.REFRESH);
};
};

View file

@ -4,52 +4,24 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { HttpFetchError } from 'src/core/public';
import {
TransformListRow,
TransformStats,
TRANSFORM_MODE,
isTransformStats,
TransformPivotConfig,
refreshTransformList$,
REFRESH_TRANSFORM_LIST_STATE,
} from '../common';
isGetTransformsResponseSchema,
isGetTransformsStatsResponseSchema,
} from '../../../common/api_schemas/type_guards';
import { TRANSFORM_MODE } from '../../../common/constants';
import { isTransformStats } from '../../../common/types/transform_stats';
import { TransformListRow, refreshTransformList$, REFRESH_TRANSFORM_LIST_STATE } from '../common';
import { useApi } from './use_api';
interface GetTransformsResponse {
count: number;
transforms: TransformPivotConfig[];
}
interface GetTransformsStatsResponseOk {
node_failures?: object;
count: number;
transforms: TransformStats[];
}
const isGetTransformsStatsResponseOk = (arg: any): arg is GetTransformsStatsResponseOk => {
return (
{}.hasOwnProperty.call(arg, 'count') &&
{}.hasOwnProperty.call(arg, 'transforms') &&
Array.isArray(arg.transforms)
);
};
interface GetTransformsStatsResponseError {
statusCode: number;
error: string;
message: string;
}
type GetTransformsStatsResponse = GetTransformsStatsResponseOk | GetTransformsStatsResponseError;
export type GetTransforms = (forceRefresh?: boolean) => void;
export const useGetTransforms = (
setTransforms: React.Dispatch<React.SetStateAction<TransformListRow[]>>,
setErrorMessage: React.Dispatch<
React.SetStateAction<GetTransformsStatsResponseError | undefined>
>,
setErrorMessage: React.Dispatch<React.SetStateAction<HttpFetchError | undefined>>,
setIsInitialized: React.Dispatch<React.SetStateAction<boolean>>,
blockRefresh: boolean
): GetTransforms => {
@ -66,45 +38,57 @@ export const useGetTransforms = (
return;
}
try {
const transformConfigs: GetTransformsResponse = await api.getTransforms();
const transformStats: GetTransformsStatsResponse = await api.getTransformsStats();
const transformConfigs = await api.getTransforms();
const transformStats = await api.getTransformsStats();
const tableRows = transformConfigs.transforms.reduce((reducedtableRows, config) => {
const stats = isGetTransformsStatsResponseOk(transformStats)
? transformStats.transforms.find((d) => config.id === d.id)
: undefined;
// A newly created transform might not have corresponding stats yet.
// If that's the case we just skip the transform and don't add it to the transform list yet.
if (!isTransformStats(stats)) {
return reducedtableRows;
}
// Table with expandable rows requires `id` on the outer most level
reducedtableRows.push({
id: config.id,
config,
mode:
typeof config.sync !== 'undefined' ? TRANSFORM_MODE.CONTINUOUS : TRANSFORM_MODE.BATCH,
stats,
});
return reducedtableRows;
}, [] as TransformListRow[]);
setTransforms(tableRows);
setErrorMessage(undefined);
setIsInitialized(true);
refreshTransformList$.next(REFRESH_TRANSFORM_LIST_STATE.IDLE);
} catch (e) {
if (
!isGetTransformsResponseSchema(transformConfigs) ||
!isGetTransformsStatsResponseSchema(transformStats)
) {
// An error is followed immediately by setting the state to idle.
// This way we're able to treat ERROR as a one-time-event like REFRESH.
refreshTransformList$.next(REFRESH_TRANSFORM_LIST_STATE.ERROR);
refreshTransformList$.next(REFRESH_TRANSFORM_LIST_STATE.IDLE);
setTransforms([]);
setErrorMessage(e);
setIsInitialized(true);
if (!isGetTransformsResponseSchema(transformConfigs)) {
setErrorMessage(transformConfigs);
} else if (!isGetTransformsStatsResponseSchema(transformStats)) {
setErrorMessage(transformStats);
}
return;
}
const tableRows = transformConfigs.transforms.reduce((reducedtableRows, config) => {
const stats = isGetTransformsStatsResponseSchema(transformStats)
? transformStats.transforms.find((d) => config.id === d.id)
: undefined;
// A newly created transform might not have corresponding stats yet.
// If that's the case we just skip the transform and don't add it to the transform list yet.
if (!isTransformStats(stats)) {
return reducedtableRows;
}
// Table with expandable rows requires `id` on the outer most level
reducedtableRows.push({
id: config.id,
config,
mode:
typeof config.sync !== 'undefined' ? TRANSFORM_MODE.CONTINUOUS : TRANSFORM_MODE.BATCH,
stats,
});
return reducedtableRows;
}, [] as TransformListRow[]);
setTransforms(tableRows);
setErrorMessage(undefined);
setIsInitialized(true);
refreshTransformList$.next(REFRESH_TRANSFORM_LIST_STATE.IDLE);
concurrentLoads--;
if (concurrentLoads > 0) {

View file

@ -8,6 +8,11 @@ import { useEffect } from 'react';
import { EuiDataGridColumn } from '@elastic/eui';
import {
isEsSearchResponse,
isFieldHistogramsResponseSchema,
} from '../../../common/api_schemas/type_guards';
import {
getFieldType,
getDataGridSchemaFromKibanaFieldType,
@ -16,7 +21,6 @@ import {
useDataGrid,
useRenderCellValue,
EsSorting,
SearchResponse7,
UseIndexDataReturnType,
INDEX_STATUS,
} from '../../shared_imports';
@ -29,8 +33,6 @@ import { useApi } from './use_api';
import { useToastNotifications } from '../app_dependencies';
type IndexSearchResponse = SearchResponse7;
export const useIndexData = (
indexPattern: SearchItems['indexPattern'],
query: PivotQuery
@ -90,37 +92,39 @@ export const useIndexData = (
},
};
try {
const resp: IndexSearchResponse = await api.esSearch(esSearchRequest);
const resp = await api.esSearch(esSearchRequest);
const docs = resp.hits.hits.map((d) => d._source);
setRowCount(resp.hits.total.value);
setTableItems(docs);
setStatus(INDEX_STATUS.LOADED);
} catch (e) {
setErrorMessage(getErrorMessage(e));
if (!isEsSearchResponse(resp)) {
setErrorMessage(getErrorMessage(resp));
setStatus(INDEX_STATUS.ERROR);
return;
}
const docs = resp.hits.hits.map((d) => d._source);
setRowCount(resp.hits.total.value);
setTableItems(docs);
setStatus(INDEX_STATUS.LOADED);
};
const fetchColumnChartsData = async function () {
try {
const columnChartsData = await api.getHistogramsForFields(
indexPattern.title,
columns
.filter((cT) => dataGrid.visibleColumns.includes(cT.id))
.map((cT) => ({
fieldName: cT.id,
type: getFieldType(cT.schema),
})),
isDefaultQuery(query) ? matchAllQuery : query
);
setColumnCharts(columnChartsData);
} catch (e) {
showDataGridColumnChartErrorMessageToast(e, toastNotifications);
const columnChartsData = await api.getHistogramsForFields(
indexPattern.title,
columns
.filter((cT) => dataGrid.visibleColumns.includes(cT.id))
.map((cT) => ({
fieldName: cT.id,
type: getFieldType(cT.schema),
})),
isDefaultQuery(query) ? matchAllQuery : query
);
if (!isFieldHistogramsResponseSchema(columnChartsData)) {
showDataGridColumnChartErrorMessageToast(columnChartsData, toastNotifications);
return;
}
setColumnCharts(columnChartsData);
};
useEffect(() => {

View file

@ -13,11 +13,13 @@ import { i18n } from '@kbn/i18n';
import { ES_FIELD_TYPES } from '../../../../../../src/plugins/data/common';
import type { PreviewMappingsProperties } from '../../../common/api_schemas/transforms';
import { isPostTransformsPreviewResponseSchema } from '../../../common/api_schemas/type_guards';
import { dictionaryToArray } from '../../../common/types/common';
import { formatHumanReadableDateTimeSeconds } from '../../shared_imports';
import { getNestedProperty } from '../../../common/utils/object_utils';
import {
formatHumanReadableDateTimeSeconds,
multiColumnSortFactory,
useDataGrid,
RenderCellValue,
@ -27,12 +29,11 @@ import {
import { getErrorMessage } from '../../../common/utils/errors';
import {
getPreviewRequestBody,
getPreviewTransformRequestBody,
PivotAggsConfigDict,
PivotGroupByConfigDict,
PivotGroupByConfig,
PivotQuery,
PreviewMappings,
PivotAggsConfig,
} from '../common';
@ -74,21 +75,23 @@ export const usePivotData = (
aggs: PivotAggsConfigDict,
groupBy: PivotGroupByConfigDict
): UseIndexDataReturnType => {
const [previewMappings, setPreviewMappings] = useState<PreviewMappings>({ properties: {} });
const [previewMappingsProperties, setPreviewMappingsProperties] = useState<
PreviewMappingsProperties
>({});
const api = useApi();
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.
const columnKeys = Object.keys(previewMappings.properties).filter(
(key) => previewMappings.properties[key].type !== 'object'
const columnKeys = Object.keys(previewMappingsProperties).filter(
(key) => previewMappingsProperties[key].type !== 'object'
);
columnKeys.sort(sortColumns(groupByArr));
// EuiDataGrid State
const columns: EuiDataGridColumn[] = columnKeys.map((id) => {
const field = previewMappings.properties[id];
const field = previewMappingsProperties[id];
// Built-in values are ['boolean', 'currency', 'datetime', 'numeric', 'json']
// To fall back to the default string schema it needs to be undefined.
@ -159,28 +162,35 @@ export const usePivotData = (
setNoDataMessage('');
setStatus(INDEX_STATUS.LOADING);
try {
const previewRequest = getPreviewRequestBody(indexPatternTitle, query, groupByArr, aggsArr);
const resp = await api.getTransformsPreview(previewRequest);
setTableItems(resp.preview);
setRowCount(resp.preview.length);
setPreviewMappings(resp.generated_dest_index.mappings);
setStatus(INDEX_STATUS.LOADED);
const previewRequest = getPreviewTransformRequestBody(
indexPatternTitle,
query,
groupByArr,
aggsArr
);
const resp = await api.getTransformsPreview(previewRequest);
if (resp.preview.length === 0) {
setNoDataMessage(
i18n.translate('xpack.transform.pivotPreview.PivotPreviewNoDataCalloutBody', {
defaultMessage:
'The preview request did not return any data. Please ensure the optional query returns data and that values exist for the field used by group-by and aggregation fields.',
})
);
}
} catch (e) {
setErrorMessage(getErrorMessage(e));
if (!isPostTransformsPreviewResponseSchema(resp)) {
setErrorMessage(getErrorMessage(resp));
setTableItems([]);
setRowCount(0);
setPreviewMappings({ properties: {} });
setPreviewMappingsProperties({});
setStatus(INDEX_STATUS.ERROR);
return;
}
setTableItems(resp.preview);
setRowCount(resp.preview.length);
setPreviewMappingsProperties(resp.generated_dest_index.mappings.properties);
setStatus(INDEX_STATUS.LOADED);
if (resp.preview.length === 0) {
setNoDataMessage(
i18n.translate('xpack.transform.pivotPreview.PivotPreviewNoDataCalloutBody', {
defaultMessage:
'The preview request did not return any data. Please ensure the optional query returns data and that values exist for the field used by group-by and aggregation fields.',
})
);
}
};
@ -236,19 +246,19 @@ export const usePivotData = (
if (
[ES_FIELD_TYPES.DATE, ES_FIELD_TYPES.DATE_NANOS].includes(
previewMappings.properties[columnId].type
previewMappingsProperties[columnId].type
)
) {
return formatHumanReadableDateTimeSeconds(moment(cellValue).unix() * 1000);
}
if (previewMappings.properties[columnId].type === ES_FIELD_TYPES.BOOLEAN) {
if (previewMappingsProperties[columnId].type === ES_FIELD_TYPES.BOOLEAN) {
return cellValue ? 'true' : 'false';
}
return cellValue;
};
}, [pageData, pagination.pageIndex, pagination.pageSize, previewMappings.properties]);
}, [pageData, pagination.pageIndex, pagination.pageSize, previewMappingsProperties]);
return {
...dataGrid,

View file

@ -4,25 +4,45 @@
* you may not use this file except in compliance with the Elastic License.
*/
import React from 'react';
import { i18n } from '@kbn/i18n';
import { TransformEndpointRequest, TransformEndpointResult } from '../../../common';
import { toMountPoint } from '../../../../../../src/plugins/kibana_react/public';
import { useToastNotifications } from '../app_dependencies';
import { TransformListRow, refreshTransformList$, REFRESH_TRANSFORM_LIST_STATE } from '../common';
import type { StartTransformsRequestSchema } from '../../../common/api_schemas/start_transforms';
import { isStartTransformsResponseSchema } from '../../../common/api_schemas/type_guards';
import { getErrorMessage } from '../../../common/utils/errors';
import { useAppDependencies, useToastNotifications } from '../app_dependencies';
import { refreshTransformList$, REFRESH_TRANSFORM_LIST_STATE } from '../common';
import { ToastNotificationText } from '../components';
import { useApi } from './use_api';
export const useStartTransforms = () => {
const deps = useAppDependencies();
const toastNotifications = useToastNotifications();
const api = useApi();
return async (transforms: TransformListRow[]) => {
const transformsInfo: TransformEndpointRequest[] = transforms.map((tf) => ({
id: tf.config.id,
state: tf.stats.state,
}));
const results: TransformEndpointResult = await api.startTransforms(transformsInfo);
return async (transformsInfo: StartTransformsRequestSchema) => {
const results = await api.startTransforms(transformsInfo);
if (!isStartTransformsResponseSchema(results)) {
toastNotifications.addDanger({
title: i18n.translate(
'xpack.transform.stepCreateForm.startTransformResponseSchemaErrorMessage',
{
defaultMessage: 'An error occurred calling the start transforms request.',
}
),
text: toMountPoint(
<ToastNotificationText overlays={deps.overlays} text={getErrorMessage(results)} />
),
});
return;
}
for (const transformId in results) {
// hasOwnProperty check to ensure only properties on object itself, and not its prototypes

View file

@ -4,25 +4,45 @@
* you may not use this file except in compliance with the Elastic License.
*/
import React from 'react';
import { i18n } from '@kbn/i18n';
import { TransformEndpointRequest, TransformEndpointResult } from '../../../common';
import { toMountPoint } from '../../../../../../src/plugins/kibana_react/public';
import { useToastNotifications } from '../app_dependencies';
import { TransformListRow, refreshTransformList$, REFRESH_TRANSFORM_LIST_STATE } from '../common';
import type { StopTransformsRequestSchema } from '../../../common/api_schemas/stop_transforms';
import { isStopTransformsResponseSchema } from '../../../common/api_schemas/type_guards';
import { getErrorMessage } from '../../../common/utils/errors';
import { useAppDependencies, useToastNotifications } from '../app_dependencies';
import { refreshTransformList$, REFRESH_TRANSFORM_LIST_STATE } from '../common';
import { ToastNotificationText } from '../components';
import { useApi } from './use_api';
export const useStopTransforms = () => {
const deps = useAppDependencies();
const toastNotifications = useToastNotifications();
const api = useApi();
return async (transforms: TransformListRow[]) => {
const transformsInfo: TransformEndpointRequest[] = transforms.map((df) => ({
id: df.config.id,
state: df.stats.state,
}));
const results: TransformEndpointResult = await api.stopTransforms(transformsInfo);
return async (transformsInfo: StopTransformsRequestSchema) => {
const results = await api.stopTransforms(transformsInfo);
if (!isStopTransformsResponseSchema(results)) {
toastNotifications.addDanger({
title: i18n.translate(
'xpack.transform.transformList.stopTransformResponseSchemaErrorMessage',
{
defaultMessage: 'An error occurred called the stop transforms request.',
}
),
text: toMountPoint(
<ToastNotificationText overlays={deps.overlays} text={getErrorMessage(results)} />
),
});
return;
}
for (const transformId in results) {
// hasOwnProperty check to ensure only properties on object itself, and not its prototypes

View file

@ -6,7 +6,7 @@
import React, { createContext } from 'react';
import { Privileges } from '../../../../../common';
import { Privileges } from '../../../../../common/types/privileges';
import { useRequest } from '../../../hooks';

View file

@ -6,7 +6,7 @@
import { i18n } from '@kbn/i18n';
import { Privileges } from '../../../../../common';
import { Privileges } from '../../../../../common/types/privileges';
export interface Capabilities {
canGetTransform: boolean;

View file

@ -10,7 +10,7 @@ import { EuiPageContent } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import { MissingPrivileges } from '../../../../../common';
import { MissingPrivileges } from '../../../../../common/types/privileges';
import { SectionLoading } from '../../../components';

View file

@ -21,40 +21,20 @@ import {
EuiTitle,
} from '@elastic/eui';
import { APP_CREATE_TRANSFORM_CLUSTER_PRIVILEGES } from '../../../../common/constants';
import { TransformPivotConfig } from '../../../../common/types/transform';
import { isHttpFetchError } from '../../common/request';
import { useApi } from '../../hooks/use_api';
import { useDocumentationLinks } from '../../hooks/use_documentation_links';
import { useSearchItems } from '../../hooks/use_search_items';
import { APP_CREATE_TRANSFORM_CLUSTER_PRIVILEGES } from '../../../../common/constants';
import { useAppDependencies } from '../../app_dependencies';
import { TransformPivotConfig } from '../../common';
import { breadcrumbService, docTitleService, BREADCRUMB_SECTION } from '../../services/navigation';
import { PrivilegesWrapper } from '../../lib/authorization';
import { Wizard } from '../create_transform/components/wizard';
interface GetTransformsResponseOk {
count: number;
transforms: TransformPivotConfig[];
}
interface GetTransformsResponseError {
error: {
msg: string;
path: string;
query: any;
statusCode: number;
response: string;
};
}
function isGetTransformsResponseError(arg: any): arg is GetTransformsResponseError {
return arg.error !== undefined;
}
type GetTransformsResponse = GetTransformsResponseOk | GetTransformsResponseError;
type Props = RouteComponentProps<{ transformId: string }>;
export const CloneTransformSection: FC<Props> = ({ match }) => {
// Set breadcrumb and page title
@ -84,15 +64,15 @@ export const CloneTransformSection: FC<Props> = ({ match }) => {
} = useSearchItems(undefined);
const fetchTransformConfig = async () => {
try {
const transformConfigs: GetTransformsResponse = await api.getTransforms(transformId);
if (isGetTransformsResponseError(transformConfigs)) {
setTransformConfig(undefined);
setErrorMessage(transformConfigs.error.msg);
setIsInitialized(true);
return;
}
const transformConfigs = await api.getTransform(transformId);
if (isHttpFetchError(transformConfigs)) {
setTransformConfig(undefined);
setErrorMessage(transformConfigs.message);
setIsInitialized(true);
return;
}
try {
await loadIndexPatterns(savedObjectsClient, indexPatterns);
const indexPatternTitle = Array.isArray(transformConfigs.transforms[0].source.index)
? transformConfigs.transforms[0].source.index.join(',')

View file

@ -7,7 +7,10 @@
import { shallow } from 'enzyme';
import React from 'react';
import { AggName, PivotAggsConfig, PIVOT_SUPPORTED_AGGS } from '../../../../common';
import { AggName } from '../../../../../../common/types/aggregations';
import { PIVOT_SUPPORTED_AGGS } from '../../../../../../common/types/pivot_aggs';
import { PivotAggsConfig } from '../../../../common';
import { AggLabelForm } from './agg_label_form';

View file

@ -10,8 +10,9 @@ import { i18n } from '@kbn/i18n';
import { EuiButtonIcon, EuiFlexGroup, EuiFlexItem, EuiPopover, EuiTextColor } from '@elastic/eui';
import { AggName } from '../../../../../../common/types/aggregations';
import {
AggName,
isPivotAggsConfigWithUiSupport,
PivotAggsConfig,
PivotAggsConfigWithUiSupportDict,

View file

@ -7,7 +7,9 @@
import { shallow } from 'enzyme';
import React from 'react';
import { PivotAggsConfig, PIVOT_SUPPORTED_AGGS } from '../../../../common';
import { PIVOT_SUPPORTED_AGGS } from '../../../../../../common/types/pivot_aggs';
import { PivotAggsConfig } from '../../../../common';
import { AggListForm, AggListProps } from './list_form';

View file

@ -8,8 +8,9 @@ import React, { Fragment } from 'react';
import { EuiPanel, EuiSpacer } from '@elastic/eui';
import { AggName } from '../../../../../../common/types/aggregations';
import {
AggName,
PivotAggsConfig,
PivotAggsConfigDict,
PivotAggsConfigWithUiSupportDict,

View file

@ -7,7 +7,9 @@
import { shallow } from 'enzyme';
import React from 'react';
import { PivotAggsConfig, PIVOT_SUPPORTED_AGGS } from '../../../../common';
import { PIVOT_SUPPORTED_AGGS } from '../../../../../../common/types/pivot_aggs';
import { PivotAggsConfig } from '../../../../common';
import { AggListSummary, AggListSummaryProps } from './list_summary';

View file

@ -8,7 +8,9 @@ import React, { Fragment } from 'react';
import { EuiForm, EuiPanel, EuiSpacer } from '@elastic/eui';
import { AggName, PivotAggsConfigDict } from '../../../../common';
import { AggName } from '../../../../../../common/types/aggregations';
import { PivotAggsConfigDict } from '../../../../common';
export interface AggListSummaryProps {
list: PivotAggsConfigDict;

View file

@ -7,7 +7,10 @@
import { shallow } from 'enzyme';
import React from 'react';
import { AggName, PIVOT_SUPPORTED_AGGS, PivotAggsConfig } from '../../../../common';
import { AggName } from '../../../../../../common/types/aggregations';
import { PIVOT_SUPPORTED_AGGS } from '../../../../../../common/types/pivot_aggs';
import { PivotAggsConfig } from '../../../../common';
import { PopoverForm } from './popover_form';

View file

@ -20,10 +20,14 @@ import {
import { cloneDeep } from 'lodash';
import { useUpdateEffect } from 'react-use';
import { AggName } from '../../../../../../common/types/aggregations';
import { dictionaryToArray } from '../../../../../../common/types/common';
import {
PivotSupportedAggs,
PIVOT_SUPPORTED_AGGS,
} from '../../../../../../common/types/pivot_aggs';
import {
AggName,
isAggName,
isPivotAggsConfigPercentiles,
isPivotAggsConfigWithUiSupport,
@ -31,9 +35,8 @@ import {
PERCENTILES_AGG_DEFAULT_PERCENTS,
PivotAggsConfig,
PivotAggsConfigWithUiSupportDict,
PIVOT_SUPPORTED_AGGS,
} from '../../../../common';
import { isPivotAggsWithExtendedForm, PivotSupportedAggs } from '../../../../common/pivot_aggs';
import { isPivotAggsWithExtendedForm } from '../../../../common/pivot_aggs';
import { getAggFormConfig } from '../step_define/common/get_agg_form_config';
interface Props {

View file

@ -10,8 +10,9 @@ import { i18n } from '@kbn/i18n';
import { EuiButtonIcon, EuiFlexGroup, EuiFlexItem, EuiPopover, EuiTextColor } from '@elastic/eui';
import { AggName } from '../../../../../../common/types/aggregations';
import {
AggName,
isGroupByDateHistogram,
isGroupByHistogram,
PivotGroupByConfig,

View file

@ -8,8 +8,9 @@ import React, { Fragment } from 'react';
import { EuiPanel, EuiSpacer } from '@elastic/eui';
import { AggName } from '../../../../../../common/types/aggregations';
import {
AggName,
PivotGroupByConfig,
PivotGroupByConfigDict,
PivotGroupByConfigWithUiSupportDict,

View file

@ -7,7 +7,9 @@
import { shallow } from 'enzyme';
import React from 'react';
import { AggName, PIVOT_SUPPORTED_GROUP_BY_AGGS, PivotGroupByConfig } from '../../../../common';
import { AggName } from '../../../../../../common/types/aggregations';
import { PIVOT_SUPPORTED_GROUP_BY_AGGS, PivotGroupByConfig } from '../../../../common';
import { isIntervalValid, PopoverForm } from './popover_form';

View file

@ -18,10 +18,10 @@ import {
EuiSpacer,
} from '@elastic/eui';
import { AggName } from '../../../../../../common/types/aggregations';
import { dictionaryToArray } from '../../../../../../common/types/common';
import {
AggName,
dateHistogramIntervalFormatRegex,
getEsAggFromGroupByConfig,
isGroupByDateHistogram,

View file

@ -30,6 +30,12 @@ import {
import { toMountPoint } from '../../../../../../../../../src/plugins/kibana_react/public';
import type { PutTransformsResponseSchema } from '../../../../../../common/api_schemas/transforms';
import {
isGetTransformsStatsResponseSchema,
isPutTransformsResponseSchema,
isStartTransformsResponseSchema,
} from '../../../../../../common/api_schemas/type_guards';
import { PROGRESS_REFRESH_INTERVAL_MS } from '../../../../../../common/constants';
import { getErrorMessage } from '../../../../../../common/utils/errors';
@ -93,34 +99,28 @@ export const StepCreateForm: FC<Props> = React.memo(
async function createTransform() {
setLoading(true);
try {
const resp = await api.createTransform(transformId, transformConfig);
if (resp.errors !== undefined && Array.isArray(resp.errors)) {
if (resp.errors.length === 1) {
throw resp.errors[0];
}
const resp = await api.createTransform(transformId, transformConfig);
if (resp.errors.length > 1) {
throw resp.errors;
}
if (!isPutTransformsResponseSchema(resp) || resp.errors.length > 0) {
let respErrors:
| PutTransformsResponseSchema['errors']
| PutTransformsResponseSchema['errors'][number]
| undefined;
if (isPutTransformsResponseSchema(resp) && resp.errors.length > 0) {
respErrors = resp.errors.length === 1 ? resp.errors[0] : resp.errors;
}
toastNotifications.addSuccess(
i18n.translate('xpack.transform.stepCreateForm.createTransformSuccessMessage', {
defaultMessage: 'Request to create transform {transformId} acknowledged.',
values: { transformId },
})
);
setCreated(true);
setLoading(false);
} catch (e) {
toastNotifications.addDanger({
title: i18n.translate('xpack.transform.stepCreateForm.createTransformErrorMessage', {
defaultMessage: 'An error occurred creating the transform {transformId}:',
values: { transformId },
}),
text: toMountPoint(
<ToastNotificationText overlays={deps.overlays} text={getErrorMessage(e)} />
<ToastNotificationText
overlays={deps.overlays}
text={getErrorMessage(isPutTransformsResponseSchema(resp) ? respErrors : resp)}
/>
),
});
setCreated(false);
@ -128,6 +128,15 @@ export const StepCreateForm: FC<Props> = React.memo(
return false;
}
toastNotifications.addSuccess(
i18n.translate('xpack.transform.stepCreateForm.createTransformSuccessMessage', {
defaultMessage: 'Request to create transform {transformId} acknowledged.',
values: { transformId },
})
);
setCreated(true);
setLoading(false);
if (createIndexPattern) {
createKibanaIndexPattern();
}
@ -138,37 +147,36 @@ export const StepCreateForm: FC<Props> = React.memo(
async function startTransform() {
setLoading(true);
try {
const resp = await api.startTransforms([{ id: transformId }]);
if (typeof resp === 'object' && resp !== null && resp[transformId]?.success === true) {
toastNotifications.addSuccess(
i18n.translate('xpack.transform.stepCreateForm.startTransformSuccessMessage', {
defaultMessage: 'Request to start transform {transformId} acknowledged.',
values: { transformId },
})
);
setStarted(true);
setLoading(false);
} else {
const errorMessage =
typeof resp === 'object' && resp !== null && resp[transformId]?.success === false
? resp[transformId].error
: resp;
throw new Error(errorMessage);
}
} catch (e) {
toastNotifications.addDanger({
title: i18n.translate('xpack.transform.stepCreateForm.startTransformErrorMessage', {
defaultMessage: 'An error occurred starting the transform {transformId}:',
const resp = await api.startTransforms([{ id: transformId }]);
if (isStartTransformsResponseSchema(resp) && resp[transformId]?.success === true) {
toastNotifications.addSuccess(
i18n.translate('xpack.transform.stepCreateForm.startTransformSuccessMessage', {
defaultMessage: 'Request to start transform {transformId} acknowledged.',
values: { transformId },
}),
text: toMountPoint(
<ToastNotificationText overlays={deps.overlays} text={getErrorMessage(e)} />
),
});
setStarted(false);
})
);
setStarted(true);
setLoading(false);
return;
}
const errorMessage =
isStartTransformsResponseSchema(resp) && resp[transformId]?.success === false
? resp[transformId].error
: resp;
toastNotifications.addDanger({
title: i18n.translate('xpack.transform.stepCreateForm.startTransformErrorMessage', {
defaultMessage: 'An error occurred starting the transform {transformId}:',
values: { transformId },
}),
text: toMountPoint(
<ToastNotificationText overlays={deps.overlays} text={getErrorMessage(errorMessage)} />
),
});
setStarted(false);
setLoading(false);
}
async function createAndStartTransform() {
@ -250,27 +258,30 @@ export const StepCreateForm: FC<Props> = React.memo(
// wrapping in function so we can keep the interval id in local scope
function startProgressBar() {
const interval = setInterval(async () => {
try {
const stats = await api.getTransformsStats(transformId);
if (stats && Array.isArray(stats.transforms) && stats.transforms.length > 0) {
const percent =
getTransformProgress({
id: transformConfig.id,
config: transformConfig,
stats: stats.transforms[0],
}) || 0;
setProgressPercentComplete(percent);
if (percent >= 100) {
clearInterval(interval);
}
const stats = await api.getTransformStats(transformId);
if (
isGetTransformsStatsResponseSchema(stats) &&
Array.isArray(stats.transforms) &&
stats.transforms.length > 0
) {
const percent =
getTransformProgress({
id: transformConfig.id,
config: transformConfig,
stats: stats.transforms[0],
}) || 0;
setProgressPercentComplete(percent);
if (percent >= 100) {
clearInterval(interval);
}
} catch (e) {
} else {
toastNotifications.addDanger({
title: i18n.translate('xpack.transform.stepCreateForm.progressErrorMessage', {
defaultMessage: 'An error occurred getting the progress percentage:',
}),
text: toMountPoint(
<ToastNotificationText overlays={deps.overlays} text={getErrorMessage(e)} />
<ToastNotificationText overlays={deps.overlays} text={getErrorMessage(stats)} />
),
});
clearInterval(interval);

View file

@ -6,19 +6,21 @@
import { isEqual } from 'lodash';
import { Dictionary } from '../../../../../../../common/types/common';
import { PivotSupportedAggs } from '../../../../../../../common/types/pivot_aggs';
import { TransformPivotConfig } from '../../../../../../../common/types/transform';
import {
matchAllQuery,
PivotAggsConfig,
PivotAggsConfigDict,
PivotGroupByConfig,
PivotGroupByConfigDict,
TransformPivotConfig,
PIVOT_SUPPORTED_GROUP_BY_AGGS,
} from '../../../../../common';
import { Dictionary } from '../../../../../../../common/types/common';
import { StepDefineExposedState } from './types';
import { getAggConfigFromEsAgg, PivotSupportedAggs } from '../../../../../common/pivot_aggs';
import { getAggConfigFromEsAgg } from '../../../../../common/pivot_aggs';
export function applyTransformConfigToDefineState(
state: StepDefineExposedState,

View file

@ -10,6 +10,7 @@ import { FormattedMessage } from '@kbn/i18n/react';
import { debounce } from 'lodash';
import { useUpdateEffect } from 'react-use';
import { i18n } from '@kbn/i18n';
import { isEsSearchResponse } from '../../../../../../../../../common/api_schemas/type_guards';
import { useApi } from '../../../../../../../hooks';
import { CreateTransformWizardContext } from '../../../../wizard/wizard';
import { FilterAggConfigTerm } from '../types';
@ -55,22 +56,24 @@ export const FilterTermForm: FilterAggConfigTerm['aggTypeConfig']['FilterAggForm
},
};
try {
const response = await api.esSearch(esSearchRequest);
setOptions(
response.aggregations.field_values.buckets.map(
(value: { key: string; doc_count: number }) => ({ label: value.key })
)
);
} catch (e) {
const response = await api.esSearch(esSearchRequest);
setIsLoading(false);
if (!isEsSearchResponse(response)) {
toastNotifications.addWarning(
i18n.translate('xpack.transform.agg.popoverForm.filerAgg.term.errorFetchSuggestions', {
defaultMessage: 'Unable to fetch suggestions',
})
);
return;
}
setIsLoading(false);
setOptions(
response.aggregations.field_values.buckets.map(
(value: { key: string; doc_count: number }) => ({ label: value.key })
)
);
}, 600),
[selectedField]
);

View file

@ -5,11 +5,11 @@
*/
import {
PIVOT_SUPPORTED_AGGS,
PivotAggsConfigBase,
PivotAggsConfigWithUiBase,
PivotSupportedAggs,
} from '../../../../../common/pivot_aggs';
PIVOT_SUPPORTED_AGGS,
} from '../../../../../../../common/types/pivot_aggs';
import { PivotAggsConfigBase, PivotAggsConfigWithUiBase } from '../../../../../common/pivot_aggs';
import { getFilterAggConfig } from './filter_agg/config';
/**

View file

@ -6,7 +6,8 @@
import { i18n } from '@kbn/i18n';
import { AggName, PivotAggsConfigDict, PivotGroupByConfigDict } from '../../../../../common';
import { AggName } from '../../../../../../../common/types/aggregations';
import { PivotAggsConfigDict, PivotGroupByConfigDict } from '../../../../../common';
export function getAggNameConflictToastMessages(
aggName: AggName,
@ -36,7 +37,7 @@ export function getAggNameConflictToastMessages(
// check the new aggName against existing aggs and groupbys
const aggNameSplit = aggName.split('.');
let aggNameCheck: string;
aggNameSplit.forEach((aggNamePart) => {
aggNameSplit.forEach((aggNamePart: string) => {
aggNameCheck = aggNameCheck === undefined ? aggNamePart : `${aggNameCheck}.${aggNamePart}`;
if (aggList[aggNameCheck] !== undefined || groupByList[aggNameCheck] !== undefined) {
conflicts.push(

View file

@ -4,13 +4,15 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { EsFieldName } from '../../../../../../../common/types/fields';
import {
EsFieldName,
PERCENTILES_AGG_DEFAULT_PERCENTS,
PivotSupportedAggs,
PIVOT_SUPPORTED_AGGS,
} from '../../../../../../../common/types/pivot_aggs';
import {
PERCENTILES_AGG_DEFAULT_PERCENTS,
PivotAggsConfigWithUiSupport,
} from '../../../../../common';
import { PivotSupportedAggs } from '../../../../../common/pivot_aggs';
import { getFilterAggConfig } from './filter_agg/config';
/**

View file

@ -4,11 +4,9 @@
* you may not use this file except in compliance with the Elastic License.
*/
import {
EsFieldName,
GroupByConfigWithUiSupport,
PIVOT_SUPPORTED_GROUP_BY_AGGS,
} from '../../../../../common';
import { EsFieldName } from '../../../../../../../common/types/fields';
import { GroupByConfigWithUiSupport, PIVOT_SUPPORTED_GROUP_BY_AGGS } from '../../../../../common';
export function getDefaultGroupByConfig(
aggName: string,

View file

@ -6,7 +6,9 @@
import { KBN_FIELD_TYPES } from '../../../../../../../../../../src/plugins/data/public';
import { EsFieldName, PivotAggsConfigDict, PivotGroupByConfigDict } from '../../../../../common';
import { EsFieldName } from '../../../../../../../common/types/fields';
import { PivotAggsConfigDict, PivotGroupByConfigDict } from '../../../../../common';
import { SavedSearchQuery } from '../../../../../hooks/use_search_items';
import { QUERY_LANGUAGE } from './constants';

View file

@ -8,13 +8,13 @@ import { useEffect, useState } from 'react';
import { useXJsonMode } from '../../../../../../../../../../src/plugins/es_ui_shared/static/ace_x_json/hooks';
import { PreviewRequestBody } from '../../../../../common';
import { PostTransformsPreviewRequestSchema } from '../../../../../../../common/api_schemas/transforms';
import { StepDefineExposedState } from '../common';
export const useAdvancedPivotEditor = (
defaults: StepDefineExposedState,
previewRequest: PreviewRequestBody
previewRequest: PostTransformsPreviewRequestSchema
) => {
const stringifiedPivotConfig = JSON.stringify(previewRequest.pivot, null, 2);

View file

@ -6,13 +6,13 @@
import { useState } from 'react';
import { PreviewRequestBody } from '../../../../../common';
import { PostTransformsPreviewRequestSchema } from '../../../../../../../common/api_schemas/transforms';
import { StepDefineExposedState } from '../common';
export const useAdvancedSourceEditor = (
defaults: StepDefineExposedState,
previewRequest: PreviewRequestBody
previewRequest: PostTransformsPreviewRequestSchema
) => {
const stringifiedSourceConfig = JSON.stringify(previewRequest.source.query, null, 2);

View file

@ -6,11 +6,11 @@
import { useCallback, useMemo, useState } from 'react';
import { AggName } from '../../../../../../../common/types/aggregations';
import { dictionaryToArray } from '../../../../../../../common/types/common';
import { useToastNotifications } from '../../../../../app_dependencies';
import {
AggName,
DropDownLabel,
PivotAggsConfig,
PivotAggsConfigDict,

View file

@ -6,7 +6,7 @@
import { useEffect } from 'react';
import { getPreviewRequestBody } from '../../../../../common';
import { getPreviewTransformRequestBody } from '../../../../../common';
import { getDefaultStepDefineState } from '../common';
@ -26,7 +26,7 @@ export const useStepDefineForm = ({ overrides, onChange, searchItems }: StepDefi
const searchBar = useSearchBar(defaults, indexPattern);
const pivotConfig = usePivotConfig(defaults, indexPattern);
const previewRequest = getPreviewRequestBody(
const previewRequest = getPreviewTransformRequestBody(
indexPattern.title,
searchBar.state.pivotQuery,
pivotConfig.state.pivotGroupByArr,
@ -41,7 +41,7 @@ export const useStepDefineForm = ({ overrides, onChange, searchItems }: StepDefi
useEffect(() => {
if (!advancedSourceEditor.state.isAdvancedSourceEditorEnabled) {
const previewRequestUpdate = getPreviewRequestBody(
const previewRequestUpdate = getPreviewTransformRequestBody(
indexPattern.title,
searchBar.state.pivotQuery,
pivotConfig.state.pivotGroupByArr,

View file

@ -15,10 +15,11 @@ import { coreMock } from '../../../../../../../../../src/core/public/mocks';
import { dataPluginMock } from '../../../../../../../../../src/plugins/data/public/mocks';
const startMock = coreMock.createStart();
import { PIVOT_SUPPORTED_AGGS } from '../../../../../../common/types/pivot_aggs';
import {
PivotAggsConfigDict,
PivotGroupByConfigDict,
PIVOT_SUPPORTED_AGGS,
PIVOT_SUPPORTED_GROUP_BY_AGGS,
} from '../../../../common';
import { SearchItems } from '../../../../hooks/use_search_items';

View file

@ -22,6 +22,9 @@ import {
EuiText,
} from '@elastic/eui';
import { PivotAggDict } from '../../../../../../common/types/pivot_aggs';
import { PivotGroupByDict } from '../../../../../../common/types/pivot_group_by';
import { DataGrid } from '../../../../../shared_imports';
import {
@ -30,10 +33,8 @@ import {
} from '../../../../common/data_grid';
import {
getPreviewRequestBody,
PivotAggDict,
getPreviewTransformRequestBody,
PivotAggsConfigDict,
PivotGroupByDict,
PivotGroupByConfigDict,
PivotSupportedGroupByAggs,
PivotAggsConfig,
@ -87,7 +88,7 @@ export const StepDefineForm: FC<StepDefineFormProps> = React.memo((props) => {
toastNotifications,
};
const previewRequest = getPreviewRequestBody(
const previewRequest = getPreviewTransformRequestBody(
indexPattern.title,
pivotQuery,
pivotGroupByArr,

View file

@ -7,10 +7,11 @@
import React from 'react';
import { render, wait } from '@testing-library/react';
import { PIVOT_SUPPORTED_AGGS } from '../../../../../../common/types/pivot_aggs';
import {
PivotAggsConfig,
PivotGroupByConfig,
PIVOT_SUPPORTED_AGGS,
PIVOT_SUPPORTED_GROUP_BY_AGGS,
} from '../../../../common';
import { SearchItems } from '../../../../hooks/use_search_items';

View file

@ -18,7 +18,7 @@ import { useToastNotifications } from '../../../../app_dependencies';
import {
getPivotQuery,
getPivotPreviewDevConsoleStatement,
getPreviewRequestBody,
getPreviewTransformRequestBody,
isDefaultQuery,
isMatchAllQuery,
} from '../../../../common';
@ -44,7 +44,7 @@ export const StepDefineSummary: FC<Props> = ({
const pivotGroupByArr = dictionaryToArray(groupByList);
const pivotQuery = getPivotQuery(searchQuery);
const previewRequest = getPreviewRequestBody(
const previewRequest = getPreviewTransformRequestBody(
searchItems.indexPattern.title,
pivotQuery,
pivotGroupByArr,

View file

@ -11,24 +11,28 @@ import { i18n } from '@kbn/i18n';
import { EuiLink, EuiSwitch, EuiFieldText, EuiForm, EuiFormRow, EuiSelect } from '@elastic/eui';
import { KBN_FIELD_TYPES } from '../../../../../../../../../src/plugins/data/common';
import { toMountPoint } from '../../../../../../../../../src/plugins/kibana_react/public';
import { TransformId } from '../../../../../../common';
import {
isEsIndices,
isPostTransformsPreviewResponseSchema,
} from '../../../../../../common/api_schemas/type_guards';
import { TransformId, TransformPivotConfig } from '../../../../../../common/types/transform';
import { isValidIndexName } from '../../../../../../common/utils/es_utils';
import { getErrorMessage } from '../../../../../../common/utils/errors';
import { useAppDependencies, useToastNotifications } from '../../../../app_dependencies';
import { ToastNotificationText } from '../../../../components';
import { isHttpFetchError } from '../../../../common/request';
import { useDocumentationLinks } from '../../../../hooks/use_documentation_links';
import { SearchItems } from '../../../../hooks/use_search_items';
import { useApi } from '../../../../hooks/use_api';
import { StepDetailsTimeField } from './step_details_time_field';
import {
getPivotQuery,
getPreviewRequestBody,
getPreviewTransformRequestBody,
isTransformIdValid,
TransformPivotConfig,
} from '../../../../common';
import { EsIndexName, IndexPatternTitle } from './common';
import { delayValidator } from '../../../../common/validators';
@ -48,10 +52,12 @@ export interface StepDetailsExposedState {
indexPatternDateField?: string | undefined;
}
const defaultContinuousModeDelay = '60s';
export function getDefaultStepDetailsState(): StepDetailsExposedState {
return {
continuousModeDateField: '',
continuousModeDelay: '60s',
continuousModeDelay: defaultContinuousModeDelay,
createIndexPattern: true,
isContinuousModeEnabled: false,
transformId: '',
@ -72,7 +78,7 @@ export function applyTransformConfigToDetailsState(
const time = transformConfig.sync?.time;
if (time !== undefined) {
state.continuousModeDateField = time.field;
state.continuousModeDelay = time.delay;
state.continuousModeDelay = time?.delay ?? defaultContinuousModeDelay;
state.isContinuousModeEnabled = true;
}
}
@ -137,19 +143,20 @@ export const StepDetailsForm: FC<Props> = React.memo(
useEffect(() => {
// use an IIFE to avoid returning a Promise to useEffect.
(async function () {
try {
const { searchQuery, groupByList, aggList } = stepDefineState;
const pivotAggsArr = dictionaryToArray(aggList);
const pivotGroupByArr = dictionaryToArray(groupByList);
const pivotQuery = getPivotQuery(searchQuery);
const previewRequest = getPreviewRequestBody(
searchItems.indexPattern.title,
pivotQuery,
pivotGroupByArr,
pivotAggsArr
);
const { searchQuery, groupByList, aggList } = stepDefineState;
const pivotAggsArr = dictionaryToArray(aggList);
const pivotGroupByArr = dictionaryToArray(groupByList);
const pivotQuery = getPivotQuery(searchQuery);
const previewRequest = getPreviewTransformRequestBody(
searchItems.indexPattern.title,
pivotQuery,
pivotGroupByArr,
pivotAggsArr
);
const transformPreview = await api.getTransformsPreview(previewRequest);
const transformPreview = await api.getTransformsPreview(previewRequest);
if (isPostTransformsPreviewResponseSchema(transformPreview)) {
const properties = transformPreview.generated_dest_index.mappings.properties;
const datetimeColumns: string[] = Object.keys(properties).filter(
(col) => properties[col].type === 'date'
@ -157,43 +164,46 @@ export const StepDetailsForm: FC<Props> = React.memo(
setPreviewDateColumns(datetimeColumns);
setIndexPatternDateField(datetimeColumns[0]);
} catch (e) {
} else {
toastNotifications.addDanger({
title: i18n.translate('xpack.transform.stepDetailsForm.errorGettingTransformPreview', {
defaultMessage: 'An error occurred getting transform preview',
defaultMessage: 'An error occurred fetching the transform preview',
}),
text: toMountPoint(
<ToastNotificationText overlays={deps.overlays} text={getErrorMessage(e)} />
<ToastNotificationText
overlays={deps.overlays}
text={getErrorMessage(transformPreview)}
/>
),
});
}
try {
setTransformIds(
(await api.getTransforms()).transforms.map(
(transform: TransformPivotConfig) => transform.id
)
);
} catch (e) {
const resp = await api.getTransforms();
if (isHttpFetchError(resp)) {
toastNotifications.addDanger({
title: i18n.translate('xpack.transform.stepDetailsForm.errorGettingTransformList', {
defaultMessage: 'An error occurred getting the existing transform IDs:',
}),
text: toMountPoint(
<ToastNotificationText overlays={deps.overlays} text={getErrorMessage(e)} />
<ToastNotificationText overlays={deps.overlays} text={getErrorMessage(resp)} />
),
});
} else {
setTransformIds(resp.transforms.map((transform: TransformPivotConfig) => transform.id));
}
try {
setIndexNames((await api.getIndices()).map((index) => index.name));
} catch (e) {
const indices = await api.getEsIndices();
if (isEsIndices(indices)) {
setIndexNames(indices.map((index) => index.name));
} else {
toastNotifications.addDanger({
title: i18n.translate('xpack.transform.stepDetailsForm.errorGettingIndexNames', {
defaultMessage: 'An error occurred getting the existing index names:',
}),
text: toMountPoint(
<ToastNotificationText overlays={deps.overlays} text={getErrorMessage(e)} />
<ToastNotificationText overlays={deps.overlays} text={getErrorMessage(indices)} />
),
});
}

View file

@ -10,7 +10,9 @@ import { i18n } from '@kbn/i18n';
import { EuiSteps, EuiStepStatus } from '@elastic/eui';
import { getCreateRequestBody, TransformPivotConfig } from '../../../../common';
import { TransformPivotConfig } from '../../../../../../common/types/transform';
import { getCreateTransformRequestBody } from '../../../../common';
import { SearchItems } from '../../../../hooks/use_search_items';
import {
@ -149,7 +151,7 @@ export const Wizard: FC<WizardProps> = React.memo(({ cloneConfig, searchItems })
}
}, []);
const transformConfig = getCreateRequestBody(
const transformConfig = getCreateTransformRequestBody(
indexPattern.title,
stepDefineState,
stepDetailsState

View file

@ -7,7 +7,7 @@
import React, { FC } from 'react';
import { i18n } from '@kbn/i18n';
import { EuiToolTip } from '@elastic/eui';
import { TRANSFORM_STATE } from '../../../../../../common';
import { TransformState, TRANSFORM_STATE } from '../../../../../../common/constants';
import { createCapabilityFailureMessage } from '../../../../lib/authorization';
import { TransformListRow } from '../../../../common';
@ -18,8 +18,8 @@ export const deleteActionNameText = i18n.translate(
}
);
const transformCanNotBeDeleted = (item: TransformListRow) =>
![TRANSFORM_STATE.STOPPED, TRANSFORM_STATE.FAILED].includes(item.stats.state);
const transformCanNotBeDeleted = (i: TransformListRow) =>
!([TRANSFORM_STATE.STOPPED, TRANSFORM_STATE.FAILED] as TransformState[]).includes(i.stats.state);
export const isDeleteActionDisabled = (items: TransformListRow[], forceDisable: boolean) => {
const disabled = items.some(transformCanNotBeDeleted);

View file

@ -6,7 +6,7 @@
import React, { useContext, useMemo, useState } from 'react';
import { TRANSFORM_STATE } from '../../../../../../common';
import { TRANSFORM_STATE } from '../../../../../../common/constants';
import { TransformListAction, TransformListRow } from '../../../../common';
import { useDeleteIndexAndTargetIndex, useDeleteTransforms } from '../../../../hooks';
@ -55,7 +55,16 @@ export const useDeleteAction = (forceDisable: boolean) => {
const forceDelete = isBulkAction
? shouldForceDelete
: items[0] && items[0] && items[0].stats.state === TRANSFORM_STATE.FAILED;
deleteTransforms(items, shouldDeleteDestIndex, shouldDeleteDestIndexPattern, forceDelete);
deleteTransforms({
transformsInfo: items.map((i) => ({
id: i.config.id,
state: i.stats.state,
})),
deleteDestIndex: shouldDeleteDestIndex,
deleteDestIndexPattern: shouldDeleteDestIndexPattern,
forceDelete,
});
};
const openModal = (newItems: TransformListRow[]) => {

View file

@ -6,7 +6,9 @@
import React, { useContext, useMemo, useState } from 'react';
import { TransformListAction, TransformListRow, TransformPivotConfig } from '../../../../common';
import { TransformPivotConfig } from '../../../../../../common/types/transform';
import { TransformListAction, TransformListRow } from '../../../../common';
import { AuthorizationContext } from '../../../../lib/authorization';
import { editActionNameText, EditActionName } from './edit_action_name';

View file

@ -8,7 +8,7 @@ import React, { FC, useContext } from 'react';
import { i18n } from '@kbn/i18n';
import { EuiToolTip } from '@elastic/eui';
import { TRANSFORM_STATE } from '../../../../../../common';
import { TRANSFORM_STATE } from '../../../../../../common/constants';
import {
createCapabilityFailureMessage,

View file

@ -6,7 +6,7 @@
import React, { useContext, useMemo, useState } from 'react';
import { TRANSFORM_STATE } from '../../../../../../common';
import { TRANSFORM_STATE } from '../../../../../../common/constants';
import { AuthorizationContext } from '../../../../lib/authorization';
import { TransformListAction, TransformListRow } from '../../../../common';
@ -27,7 +27,7 @@ export const useStartAction = (forceDisable: boolean) => {
const startAndCloseModal = () => {
setModalVisible(false);
startTransforms(items);
startTransforms(items.map((i) => ({ id: i.id })));
};
const openModal = (newItems: TransformListRow[]) => {

View file

@ -8,7 +8,7 @@ import React, { FC, useContext } from 'react';
import { i18n } from '@kbn/i18n';
import { EuiToolTip } from '@elastic/eui';
import { TRANSFORM_STATE } from '../../../../../../common';
import { TRANSFORM_STATE } from '../../../../../../common/constants';
import { TransformListRow } from '../../../../common';
import {

View file

@ -6,7 +6,7 @@
import React, { useCallback, useContext, useMemo } from 'react';
import { TRANSFORM_STATE } from '../../../../../../common';
import { TRANSFORM_STATE } from '../../../../../../common/constants';
import { AuthorizationContext } from '../../../../lib/authorization';
import { TransformListAction, TransformListRow } from '../../../../common';
@ -20,9 +20,10 @@ export const useStopAction = (forceDisable: boolean) => {
const stopTransforms = useStopTransforms();
const clickHandler = useCallback((item: TransformListRow) => stopTransforms([item]), [
stopTransforms,
]);
const clickHandler = useCallback(
(i: TransformListRow) => stopTransforms([{ id: i.id, state: i.stats.state }]),
[stopTransforms]
);
const action: TransformListAction = useMemo(
() => ({

View file

@ -23,13 +23,12 @@ import {
EuiTitle,
} from '@elastic/eui';
import { isPostTransformsUpdateResponseSchema } from '../../../../../../common/api_schemas/type_guards';
import { TransformPivotConfig } from '../../../../../../common/types/transform';
import { getErrorMessage } from '../../../../../../common/utils/errors';
import {
refreshTransformList$,
TransformPivotConfig,
REFRESH_TRANSFORM_LIST_STATE,
} from '../../../../common';
import { refreshTransformList$, REFRESH_TRANSFORM_LIST_STATE } from '../../../../common';
import { useToastNotifications } from '../../../../app_dependencies';
import { useApi } from '../../../../hooks/use_api';
@ -58,19 +57,21 @@ export const EditTransformFlyout: FC<EditTransformFlyoutProps> = ({ closeFlyout,
const requestConfig = applyFormFieldsToTransformConfig(config, state.formFields);
const transformId = config.id;
try {
await api.updateTransform(transformId, requestConfig);
toastNotifications.addSuccess(
i18n.translate('xpack.transform.transformList.editTransformSuccessMessage', {
defaultMessage: 'Transform {transformId} updated.',
values: { transformId },
})
);
closeFlyout();
refreshTransformList$.next(REFRESH_TRANSFORM_LIST_STATE.REFRESH);
} catch (e) {
setErrorMessage(getErrorMessage(e));
const resp = await api.updateTransform(transformId, requestConfig);
if (!isPostTransformsUpdateResponseSchema(resp)) {
setErrorMessage(getErrorMessage(resp));
return;
}
toastNotifications.addSuccess(
i18n.translate('xpack.transform.transformList.editTransformSuccessMessage', {
defaultMessage: 'Transform {transformId} updated.',
values: { transformId },
})
);
closeFlyout();
refreshTransformList$.next(REFRESH_TRANSFORM_LIST_STATE.REFRESH);
}
const isUpdateButtonDisabled = !state.isFormValid || !state.isFormTouched;

View file

@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { TransformPivotConfig } from '../../../../common';
import { TransformPivotConfig } from '../../../../../../common/types/transform';
import {
applyFormFieldsToTransformConfig,
@ -86,9 +86,7 @@ describe('Transform: applyFormFieldsToTransformConfig()', () => {
});
test('should include previously nonexisting attributes', () => {
const transformConfigMock = getTransformConfigMock();
delete transformConfigMock.description;
delete transformConfigMock.frequency;
const { description, frequency, ...transformConfigMock } = getTransformConfigMock();
const updateConfig = applyFormFieldsToTransformConfig(transformConfigMock, {
description: getDescriptionFieldMock('the-new-description'),

View file

@ -9,7 +9,8 @@ import { useReducer } from 'react';
import { i18n } from '@kbn/i18n';
import { TransformPivotConfig } from '../../../../common';
import { PostTransformsUpdateRequestSchema } from '../../../../../../common/api_schemas/update_transforms';
import { TransformPivotConfig } from '../../../../../../common/types/transform';
// A Validator function takes in a value to check and returns an array of error messages.
// If no messages (empty array) get returned, the value is valid.
@ -118,53 +119,35 @@ interface Action {
value: string;
}
// Some attributes can have a value of `null` to trigger
// a reset to the default value, or in the case of `docs_per_second`
// `null` is used to disable throttling.
interface UpdateTransformPivotConfig {
description: string;
frequency: string;
settings: {
docs_per_second: number | null;
};
}
// Takes in the form configuration and returns a
// request object suitable to be sent to the
// transform update API endpoint.
export const applyFormFieldsToTransformConfig = (
config: TransformPivotConfig,
{ description, docsPerSecond, frequency }: EditTransformFlyoutFieldsState
): Partial<UpdateTransformPivotConfig> => {
const updateConfig: Partial<UpdateTransformPivotConfig> = {};
// set the values only if they changed from the default
// and actually differ from the previous value.
if (
!(config.frequency === undefined && frequency.value === '') &&
config.frequency !== frequency.value
) {
updateConfig.frequency = frequency.value;
}
if (
!(config.description === undefined && description.value === '') &&
config.description !== description.value
) {
updateConfig.description = description.value;
}
): PostTransformsUpdateRequestSchema => {
// if the input field was left empty,
// fall back to the default value of `null`
// which will disable throttling.
const docsPerSecondFormValue =
docsPerSecond.value !== '' ? parseInt(docsPerSecond.value, 10) : null;
const docsPerSecondConfigValue = config.settings?.docs_per_second ?? null;
if (docsPerSecondFormValue !== docsPerSecondConfigValue) {
updateConfig.settings = { docs_per_second: docsPerSecondFormValue };
}
return updateConfig;
return {
// set the values only if they changed from the default
// and actually differ from the previous value.
...(!(config.frequency === undefined && frequency.value === '') &&
config.frequency !== frequency.value
? { frequency: frequency.value }
: {}),
...(!(config.description === undefined && description.value === '') &&
config.description !== description.value
? { description: description.value }
: {}),
...(docsPerSecondFormValue !== docsPerSecondConfigValue
? { settings: { docs_per_second: docsPerSecondFormValue } }
: {}),
};
};
// Takes in a transform configuration and returns

View file

@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { TRANSFORM_STATE } from '../../../../../../common';
import { TRANSFORM_STATE } from '../../../../../../common/constants';
import mockTransformListRow from '../../../../common/__mocks__/transform_list_row.json';

View file

@ -9,11 +9,15 @@ import React, { useState } from 'react';
import { EuiSpacer, EuiBasicTable } from '@elastic/eui';
// @ts-ignore
import { formatDate } from '@elastic/eui/lib/services/format';
import { i18n } from '@kbn/i18n';
import theme from '@elastic/eui/dist/eui_theme_light.json';
import { i18n } from '@kbn/i18n';
import { isGetTransformsAuditMessagesResponseSchema } from '../../../../../../common/api_schemas/type_guards';
import { TransformMessage } from '../../../../../../common/types/messages';
import { useApi } from '../../../../hooks/use_api';
import { JobIcon } from '../../../../components/job_icon';
import { TransformMessage } from '../../../../../../common/types/messages';
import { useRefreshTransformList } from '../../../../common';
const TIME_FORMAT = 'YYYY-MM-DD HH:mm:ss';
@ -36,25 +40,16 @@ export const ExpandedRowMessagesPane: React.FC<Props> = ({ transformId }) => {
let concurrentLoads = 0;
return async function getMessages() {
try {
concurrentLoads++;
concurrentLoads++;
if (concurrentLoads > 1) {
return;
}
if (concurrentLoads > 1) {
return;
}
setIsLoading(true);
const messagesResp = await api.getTransformAuditMessages(transformId);
setIsLoading(false);
setMessages(messagesResp as any[]);
setIsLoading(true);
const messagesResp = await api.getTransformAuditMessages(transformId);
concurrentLoads--;
if (concurrentLoads > 0) {
concurrentLoads = 0;
getMessages();
}
} catch (error) {
if (!isGetTransformsAuditMessagesResponseSchema(messagesResp)) {
setIsLoading(false);
setErrorMessage(
i18n.translate(
@ -64,6 +59,17 @@ export const ExpandedRowMessagesPane: React.FC<Props> = ({ transformId }) => {
}
)
);
return;
}
setIsLoading(false);
setMessages(messagesResp as any[]);
concurrentLoads--;
if (concurrentLoads > 0) {
concurrentLoads = 0;
getMessages();
}
};
};

View file

@ -6,10 +6,11 @@
import React, { useMemo, FC } from 'react';
import { TransformPivotConfig } from '../../../../../../common/types/transform';
import { DataGrid } from '../../../../../shared_imports';
import { useToastNotifications } from '../../../../app_dependencies';
import { getPivotQuery, TransformPivotConfig } from '../../../../common';
import { getPivotQuery } from '../../../../common';
import { usePivotData } from '../../../../hooks/use_pivot_data';
import { SearchItems } from '../../../../hooks/use_search_items';

View file

@ -23,7 +23,7 @@ import {
EuiTitle,
} from '@elastic/eui';
import { TransformId } from '../../../../../../common';
import { TransformId } from '../../../../../../common/types/transform';
import {
useRefreshTransformList,
@ -189,7 +189,11 @@ export const TransformList: FC<Props> = ({
</EuiButtonEmpty>
</div>,
<div key="stopAction" className="transform__BulkActionItem">
<EuiButtonEmpty onClick={() => stopTransforms(transformSelection)}>
<EuiButtonEmpty
onClick={() =>
stopTransforms(transformSelection.map((t) => ({ id: t.id, state: t.stats.state })))
}
>
<StopActionName items={transformSelection} />
</EuiButtonEmpty>
</div>,

View file

@ -16,8 +16,8 @@ import {
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { TermClause, FieldClause, Value } from './common';
import { TRANSFORM_STATE } from '../../../../../../common';
import { TRANSFORM_MODE, TransformListRow } from '../../../../common';
import { TRANSFORM_MODE, TRANSFORM_STATE } from '../../../../../../common/constants';
import { TransformListRow } from '../../../../common';
import { getTaskStateBadge } from './use_columns';
const filters: SearchFilterConfig[] = [

View file

@ -7,9 +7,9 @@
import React, { FC } from 'react';
import { i18n } from '@kbn/i18n';
import { TRANSFORM_STATE } from '../../../../../../common';
import { TRANSFORM_MODE, TRANSFORM_STATE } from '../../../../../../common/constants';
import { TRANSFORM_MODE, TransformListRow } from '../../../../common';
import { TransformListRow } from '../../../../common';
import { StatsBar, TransformStatsBarStats } from '../stats_bar';

View file

@ -22,24 +22,21 @@ import {
RIGHT_ALIGNMENT,
} from '@elastic/eui';
import { TransformId, TRANSFORM_STATE } from '../../../../../../common';
import { TransformId } from '../../../../../../common/types/transform';
import { TransformStats } from '../../../../../../common/types/transform_stats';
import { TRANSFORM_STATE } from '../../../../../../common/constants';
import {
getTransformProgress,
TransformListRow,
TransformStats,
TRANSFORM_LIST_COLUMN,
} from '../../../../common';
import { getTransformProgress, TransformListRow, TRANSFORM_LIST_COLUMN } from '../../../../common';
import { useActions } from './use_actions';
enum STATE_COLOR {
aborting = 'warning',
failed = 'danger',
indexing = 'primary',
started = 'primary',
stopped = 'hollow',
stopping = 'hollow',
}
const STATE_COLOR = {
aborting: 'warning',
failed: 'danger',
indexing: 'primary',
started: 'primary',
stopped: 'hollow',
stopping: 'hollow',
} as const;
export const getTaskStateBadge = (
state: TransformStats['state'],

Some files were not shown because too many files have changed in this diff Show more