mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -04:00
Revert "[ML] Transforms: Add ability to delete dest index & index pattern when deleting transform job" (#68793)
This commit is contained in:
parent
332a1386d0
commit
3197a00e79
24 changed files with 107 additions and 1165 deletions
|
@ -1,78 +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 {
|
||||
BoomResponse,
|
||||
extractErrorMessage,
|
||||
MLCustomHttpResponseOptions,
|
||||
MLResponseError,
|
||||
} from './errors';
|
||||
import { ResponseError } from 'kibana/server';
|
||||
|
||||
describe('ML - error message utils', () => {
|
||||
describe('extractErrorMessage', () => {
|
||||
test('returns just the error message', () => {
|
||||
const testMsg = 'Saved object [index-pattern/blahblahblah] not found';
|
||||
|
||||
const bodyWithNestedErrorMsg: MLCustomHttpResponseOptions<MLResponseError> = {
|
||||
body: {
|
||||
message: {
|
||||
msg: testMsg,
|
||||
},
|
||||
},
|
||||
statusCode: 404,
|
||||
};
|
||||
expect(extractErrorMessage(bodyWithNestedErrorMsg)).toBe(testMsg);
|
||||
|
||||
const bodyWithStringMsg: MLCustomHttpResponseOptions<MLResponseError> = {
|
||||
body: {
|
||||
msg: testMsg,
|
||||
},
|
||||
statusCode: 404,
|
||||
};
|
||||
expect(extractErrorMessage(bodyWithStringMsg)).toBe(testMsg);
|
||||
|
||||
const bodyWithStringMessage: MLCustomHttpResponseOptions<ResponseError> = {
|
||||
body: {
|
||||
message: testMsg,
|
||||
},
|
||||
statusCode: 404,
|
||||
};
|
||||
expect(extractErrorMessage(bodyWithStringMessage)).toBe(testMsg);
|
||||
|
||||
const bodyWithString: MLCustomHttpResponseOptions<ResponseError> = {
|
||||
body: testMsg,
|
||||
statusCode: 404,
|
||||
};
|
||||
expect(extractErrorMessage(bodyWithString)).toBe(testMsg);
|
||||
|
||||
const bodyWithError: MLCustomHttpResponseOptions<ResponseError> = {
|
||||
body: new Error(testMsg),
|
||||
statusCode: 404,
|
||||
};
|
||||
expect(extractErrorMessage(bodyWithError)).toBe(testMsg);
|
||||
|
||||
const bodyWithBoomError: MLCustomHttpResponseOptions<BoomResponse> = {
|
||||
statusCode: 404,
|
||||
body: {
|
||||
data: [],
|
||||
isBoom: true,
|
||||
isServer: false,
|
||||
output: {
|
||||
statusCode: 404,
|
||||
payload: {
|
||||
statusCode: 404,
|
||||
error: testMsg,
|
||||
message: testMsg,
|
||||
},
|
||||
headers: {},
|
||||
},
|
||||
},
|
||||
};
|
||||
expect(extractErrorMessage(bodyWithBoomError)).toBe(testMsg);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -4,7 +4,6 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { ResponseError, ResponseHeaders } from 'kibana/server';
|
||||
import { isErrorResponse } from '../types/errors';
|
||||
|
||||
export function getErrorMessage(error: any) {
|
||||
|
@ -18,77 +17,3 @@ export function getErrorMessage(error: any) {
|
|||
|
||||
return JSON.stringify(error);
|
||||
}
|
||||
|
||||
// Adding temporary types until Kibana ResponseError is updated
|
||||
|
||||
export interface BoomResponse {
|
||||
data: any;
|
||||
isBoom: boolean;
|
||||
isServer: boolean;
|
||||
output: {
|
||||
statusCode: number;
|
||||
payload: {
|
||||
statusCode: number;
|
||||
error: string;
|
||||
message: string;
|
||||
};
|
||||
headers: {};
|
||||
};
|
||||
}
|
||||
export type MLResponseError =
|
||||
| {
|
||||
message: {
|
||||
msg: string;
|
||||
};
|
||||
}
|
||||
| { msg: string };
|
||||
|
||||
export interface MLCustomHttpResponseOptions<
|
||||
T extends ResponseError | MLResponseError | BoomResponse
|
||||
> {
|
||||
/** HTTP message to send to the client */
|
||||
body?: T;
|
||||
/** HTTP Headers with additional information about response */
|
||||
headers?: ResponseHeaders;
|
||||
statusCode: number;
|
||||
}
|
||||
|
||||
export const extractErrorMessage = (
|
||||
error:
|
||||
| MLCustomHttpResponseOptions<MLResponseError | ResponseError | BoomResponse>
|
||||
| undefined
|
||||
| string
|
||||
): string => {
|
||||
// extract only the error message within the response error coming from Kibana, Elasticsearch, and our own ML messages
|
||||
|
||||
if (typeof error === 'string') {
|
||||
return error;
|
||||
}
|
||||
if (error?.body === undefined) return '';
|
||||
|
||||
if (typeof error.body === 'string') {
|
||||
return error.body;
|
||||
}
|
||||
if (
|
||||
typeof error.body === 'object' &&
|
||||
'output' in error.body &&
|
||||
error.body.output.payload.message
|
||||
) {
|
||||
return error.body.output.payload.message;
|
||||
}
|
||||
|
||||
if (typeof error.body === 'object' && 'msg' in error.body && typeof error.body.msg === 'string') {
|
||||
return error.body.msg;
|
||||
}
|
||||
|
||||
if (typeof error.body === 'object' && 'message' in error.body) {
|
||||
if (typeof error.body.message === 'string') {
|
||||
return error.body.message;
|
||||
}
|
||||
if (!(error.body.message instanceof Error) && typeof (error.body.message.msg === 'string')) {
|
||||
return error.body.message.msg;
|
||||
}
|
||||
}
|
||||
// If all else fail return an empty message instead of JSON.stringify
|
||||
return '';
|
||||
};
|
||||
|
|
|
@ -18,7 +18,6 @@ import {
|
|||
} from '@elastic/eui';
|
||||
import { IIndexPattern } from 'src/plugins/data/common';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import { extractErrorMessage } from '../../../../../../../common/util/errors';
|
||||
import {
|
||||
deleteAnalytics,
|
||||
deleteAnalyticsAndDestIndex,
|
||||
|
@ -30,6 +29,7 @@ import {
|
|||
} from '../../../../../capabilities/check_capabilities';
|
||||
import { useMlKibana } from '../../../../../contexts/kibana';
|
||||
import { isDataFrameAnalyticsRunning, DataFrameAnalyticsListRow } from './common';
|
||||
import { extractErrorMessage } from '../../../../../util/error_utils';
|
||||
|
||||
interface DeleteActionProps {
|
||||
item: DataFrameAnalyticsListRow;
|
||||
|
|
|
@ -4,7 +4,6 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { extractErrorMessage } from '../../../../../../../common/util/errors';
|
||||
import { getToastNotifications } from '../../../../../util/dependency_cache';
|
||||
import { ml } from '../../../../../services/ml_api_service';
|
||||
import { refreshAnalyticsList$, REFRESH_ANALYTICS_LIST_STATE } from '../../../../common';
|
||||
|
@ -12,6 +11,7 @@ import {
|
|||
isDataFrameAnalyticsFailed,
|
||||
DataFrameAnalyticsListRow,
|
||||
} from '../../components/analytics_list/common';
|
||||
import { extractErrorMessage } from '../../../../../util/error_utils';
|
||||
|
||||
export const deleteAnalytics = async (d: DataFrameAnalyticsListRow) => {
|
||||
const toastNotifications = getToastNotifications();
|
||||
|
|
32
x-pack/plugins/ml/public/application/util/error_utils.ts
Normal file
32
x-pack/plugins/ml/public/application/util/error_utils.ts
Normal file
|
@ -0,0 +1,32 @@
|
|||
/*
|
||||
* 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 { CustomHttpResponseOptions, ResponseError } from 'kibana/server';
|
||||
|
||||
export const extractErrorMessage = (
|
||||
error: CustomHttpResponseOptions<ResponseError> | undefined | string
|
||||
): string | undefined => {
|
||||
if (typeof error === 'string') {
|
||||
return error;
|
||||
}
|
||||
|
||||
if (error?.body) {
|
||||
if (typeof error.body === 'string') {
|
||||
return error.body;
|
||||
}
|
||||
if (typeof error.body === 'object' && 'message' in error.body) {
|
||||
if (typeof error.body.message === 'string') {
|
||||
return error.body.message;
|
||||
}
|
||||
// @ts-ignore
|
||||
if (typeof (error.body.message?.msg === 'string')) {
|
||||
// @ts-ignore
|
||||
return error.body.message?.msg;
|
||||
}
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
};
|
|
@ -14,7 +14,6 @@ export * from '../common/types/audit_message';
|
|||
|
||||
export * from '../common/util/anomaly_utils';
|
||||
export * from '../common/util/errors';
|
||||
|
||||
export * from '../common/util/validators';
|
||||
|
||||
export * from './application/formatters/metric_change_description';
|
||||
|
|
|
@ -38,20 +38,3 @@ export interface ResultData {
|
|||
export interface TransformEndpointResult {
|
||||
[key: string]: ResultData;
|
||||
}
|
||||
|
||||
export interface DeleteTransformEndpointRequest {
|
||||
transformsInfo: TransformEndpointRequest[];
|
||||
deleteDestIndex?: boolean;
|
||||
deleteDestIndexPattern?: boolean;
|
||||
}
|
||||
|
||||
export interface DeleteTransformStatus {
|
||||
transformDeleted: ResultData;
|
||||
destIndexDeleted?: ResultData;
|
||||
destIndexPatternDeleted?: ResultData;
|
||||
destinationIndex?: string | undefined;
|
||||
}
|
||||
|
||||
export interface DeleteTransformEndpointResult {
|
||||
[key: string]: DeleteTransformStatus;
|
||||
}
|
||||
|
|
|
@ -29,14 +29,9 @@ const MAX_SIMPLE_MESSAGE_LENGTH = 140;
|
|||
interface ToastNotificationTextProps {
|
||||
overlays: CoreStart['overlays'];
|
||||
text: any;
|
||||
previewTextLength?: number;
|
||||
}
|
||||
|
||||
export const ToastNotificationText: FC<ToastNotificationTextProps> = ({
|
||||
overlays,
|
||||
text,
|
||||
previewTextLength,
|
||||
}) => {
|
||||
export const ToastNotificationText: FC<ToastNotificationTextProps> = ({ overlays, text }) => {
|
||||
if (typeof text === 'string' && text.length <= MAX_SIMPLE_MESSAGE_LENGTH) {
|
||||
return text;
|
||||
}
|
||||
|
@ -51,9 +46,8 @@ export const ToastNotificationText: FC<ToastNotificationTextProps> = ({
|
|||
|
||||
const unformattedText = text.message ? text.message : text;
|
||||
const formattedText = typeof unformattedText === 'object' ? JSON.stringify(text, null, 2) : text;
|
||||
const textLength = previewTextLength ?? 140;
|
||||
const previewText = `${formattedText.substring(0, textLength)}${
|
||||
formattedText.length > textLength ? ' ...' : ''
|
||||
const previewText = `${formattedText.substring(0, 140)}${
|
||||
formattedText.length > 140 ? ' ...' : ''
|
||||
}`;
|
||||
|
||||
const openModal = () => {
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
|
||||
export { useApi } from './use_api';
|
||||
export { useGetTransforms } from './use_get_transforms';
|
||||
export { useDeleteTransforms, useDeleteIndexAndTargetIndex } from './use_delete_transform';
|
||||
export { useDeleteTransforms } from './use_delete_transform';
|
||||
export { useStartTransforms } from './use_start_transform';
|
||||
export { useStopTransforms } from './use_stop_transform';
|
||||
export { useRequest } from './use_request';
|
||||
|
|
|
@ -5,12 +5,7 @@
|
|||
*/
|
||||
|
||||
import { useMemo } from 'react';
|
||||
import {
|
||||
TransformId,
|
||||
TransformEndpointRequest,
|
||||
TransformEndpointResult,
|
||||
DeleteTransformEndpointResult,
|
||||
} from '../../../common';
|
||||
import { TransformEndpointRequest, TransformEndpointResult, TransformId } from '../../../common';
|
||||
import { API_BASE_PATH } from '../../../common/constants';
|
||||
|
||||
import { useAppDependencies } from '../app_dependencies';
|
||||
|
@ -45,12 +40,10 @@ export const useApi = () => {
|
|||
});
|
||||
},
|
||||
deleteTransforms(
|
||||
transformsInfo: TransformEndpointRequest[],
|
||||
deleteDestIndex: boolean | undefined,
|
||||
deleteDestIndexPattern: boolean | undefined
|
||||
): Promise<DeleteTransformEndpointResult> {
|
||||
transformsInfo: TransformEndpointRequest[]
|
||||
): Promise<TransformEndpointResult> {
|
||||
return http.post(`${API_BASE_PATH}delete_transforms`, {
|
||||
body: JSON.stringify({ transformsInfo, deleteDestIndex, deleteDestIndexPattern }),
|
||||
body: JSON.stringify(transformsInfo),
|
||||
});
|
||||
},
|
||||
getTransformsPreview(obj: PreviewRequestBody): Promise<GetTransformsResponse> {
|
||||
|
|
|
@ -4,257 +4,52 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import React from 'react';
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { toMountPoint } from '../../../../../../src/plugins/kibana_react/public';
|
||||
import {
|
||||
TransformEndpointRequest,
|
||||
DeleteTransformEndpointResult,
|
||||
DeleteTransformStatus,
|
||||
} from '../../../common';
|
||||
import { getErrorMessage, extractErrorMessage } from '../../shared_imports';
|
||||
|
||||
import { TransformEndpointRequest, TransformEndpointResult } from '../../../common';
|
||||
|
||||
import { getErrorMessage } from '../../shared_imports';
|
||||
|
||||
import { useAppDependencies, useToastNotifications } from '../app_dependencies';
|
||||
import { TransformListRow, refreshTransformList$, REFRESH_TRANSFORM_LIST_STATE } from '../common';
|
||||
import { ToastNotificationText } from '../components';
|
||||
|
||||
import { useApi } from './use_api';
|
||||
import { indexService } from '../services/es_index_service';
|
||||
|
||||
export const useDeleteIndexAndTargetIndex = (items: TransformListRow[]) => {
|
||||
const { http, savedObjects } = useAppDependencies();
|
||||
const toastNotifications = useToastNotifications();
|
||||
|
||||
const [deleteDestIndex, setDeleteDestIndex] = useState<boolean>(true);
|
||||
const [deleteIndexPattern, setDeleteIndexPattern] = useState<boolean>(true);
|
||||
const [userCanDeleteIndex, setUserCanDeleteIndex] = useState<boolean>(false);
|
||||
const [indexPatternExists, setIndexPatternExists] = useState<boolean>(false);
|
||||
const toggleDeleteIndex = useCallback(() => setDeleteDestIndex(!deleteDestIndex), [
|
||||
deleteDestIndex,
|
||||
]);
|
||||
const toggleDeleteIndexPattern = useCallback(() => setDeleteIndexPattern(!deleteIndexPattern), [
|
||||
deleteIndexPattern,
|
||||
]);
|
||||
|
||||
const checkIndexPatternExists = useCallback(
|
||||
async (indexName: string) => {
|
||||
try {
|
||||
if (await indexService.indexPatternExists(savedObjects.client, indexName)) {
|
||||
setIndexPatternExists(true);
|
||||
}
|
||||
} catch (e) {
|
||||
const error = extractErrorMessage(e);
|
||||
|
||||
toastNotifications.addDanger(
|
||||
i18n.translate(
|
||||
'xpack.transform.deleteTransform.errorWithCheckingIfIndexPatternExistsNotificationErrorMessage',
|
||||
{
|
||||
defaultMessage:
|
||||
'An error occurred checking if index pattern {indexPattern} exists: {error}',
|
||||
values: { indexPattern: indexName, error },
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
},
|
||||
[savedObjects.client, toastNotifications]
|
||||
);
|
||||
|
||||
const checkUserIndexPermission = useCallback(async () => {
|
||||
try {
|
||||
const userCanDelete = await indexService.canDeleteIndex(http);
|
||||
if (userCanDelete) {
|
||||
setUserCanDeleteIndex(true);
|
||||
}
|
||||
} catch (e) {
|
||||
toastNotifications.addDanger(
|
||||
i18n.translate(
|
||||
'xpack.transform.transformList.errorWithCheckingIfUserCanDeleteIndexNotificationErrorMessage',
|
||||
{
|
||||
defaultMessage: 'An error occurred checking if user can delete destination index',
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
}, [http, toastNotifications]);
|
||||
|
||||
useEffect(() => {
|
||||
checkUserIndexPermission();
|
||||
|
||||
if (items.length === 1) {
|
||||
const config = items[0].config;
|
||||
const destinationIndex = Array.isArray(config.dest.index)
|
||||
? config.dest.index[0]
|
||||
: config.dest.index;
|
||||
checkIndexPatternExists(destinationIndex);
|
||||
} else {
|
||||
setIndexPatternExists(true);
|
||||
}
|
||||
}, [checkIndexPatternExists, checkUserIndexPermission, items]);
|
||||
|
||||
return {
|
||||
userCanDeleteIndex,
|
||||
deleteDestIndex,
|
||||
indexPatternExists,
|
||||
deleteIndexPattern,
|
||||
toggleDeleteIndex,
|
||||
toggleDeleteIndexPattern,
|
||||
};
|
||||
};
|
||||
|
||||
type SuccessCountField = keyof Omit<DeleteTransformStatus, 'destinationIndex'>;
|
||||
|
||||
export const useDeleteTransforms = () => {
|
||||
const { overlays } = useAppDependencies();
|
||||
const toastNotifications = useToastNotifications();
|
||||
const api = useApi();
|
||||
|
||||
return async (
|
||||
transforms: TransformListRow[],
|
||||
shouldDeleteDestIndex: boolean,
|
||||
shouldDeleteDestIndexPattern: boolean
|
||||
) => {
|
||||
return async (transforms: TransformListRow[]) => {
|
||||
const transformsInfo: TransformEndpointRequest[] = transforms.map((tf) => ({
|
||||
id: tf.config.id,
|
||||
state: tf.stats.state,
|
||||
}));
|
||||
|
||||
try {
|
||||
const results: DeleteTransformEndpointResult = await api.deleteTransforms(
|
||||
transformsInfo,
|
||||
shouldDeleteDestIndex,
|
||||
shouldDeleteDestIndexPattern
|
||||
);
|
||||
const isBulk = Object.keys(results).length > 1;
|
||||
const successCount: Record<SuccessCountField, number> = {
|
||||
transformDeleted: 0,
|
||||
destIndexDeleted: 0,
|
||||
destIndexPatternDeleted: 0,
|
||||
};
|
||||
const results: TransformEndpointResult = await api.deleteTransforms(transformsInfo);
|
||||
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 },
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
if (results[transformId].success === true) {
|
||||
toastNotifications.addSuccess(
|
||||
i18n.translate('xpack.transform.transformList.deleteTransformSuccessMessage', {
|
||||
defaultMessage: 'Request to delete transform {transformId} acknowledged.',
|
||||
values: { transformId },
|
||||
})
|
||||
);
|
||||
} 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', {
|
||||
toastNotifications.addDanger(
|
||||
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 },
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -264,13 +59,7 @@ export const useDeleteTransforms = () => {
|
|||
title: i18n.translate('xpack.transform.transformList.deleteTransformGenericErrorMessage', {
|
||||
defaultMessage: 'An error occurred calling the API endpoint to delete transforms.',
|
||||
}),
|
||||
text: toMountPoint(
|
||||
<ToastNotificationText
|
||||
previewTextLength={50}
|
||||
overlays={overlays}
|
||||
text={getErrorMessage(e)}
|
||||
/>
|
||||
),
|
||||
text: toMountPoint(<ToastNotificationText overlays={overlays} text={getErrorMessage(e)} />),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
|
|
@ -12,14 +12,11 @@ import {
|
|||
EuiOverlayMask,
|
||||
EuiToolTip,
|
||||
EUI_MODAL_CONFIRM_BUTTON,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiSwitch,
|
||||
EuiSpacer,
|
||||
} from '@elastic/eui';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
|
||||
import { TRANSFORM_STATE } from '../../../../../../common';
|
||||
import { useDeleteTransforms, useDeleteIndexAndTargetIndex } from '../../../../hooks';
|
||||
|
||||
import { useDeleteTransforms } from '../../../../hooks';
|
||||
import {
|
||||
createCapabilityFailureMessage,
|
||||
AuthorizationContext,
|
||||
|
@ -38,25 +35,13 @@ export const DeleteAction: FC<DeleteActionProps> = ({ items, forceDisable }) =>
|
|||
|
||||
const { canDeleteTransform } = useContext(AuthorizationContext).capabilities;
|
||||
const deleteTransforms = useDeleteTransforms();
|
||||
const {
|
||||
userCanDeleteIndex,
|
||||
deleteDestIndex,
|
||||
indexPatternExists,
|
||||
deleteIndexPattern,
|
||||
toggleDeleteIndex,
|
||||
toggleDeleteIndexPattern,
|
||||
} = useDeleteIndexAndTargetIndex(items);
|
||||
|
||||
const [isModalVisible, setModalVisible] = useState(false);
|
||||
|
||||
const closeModal = () => setModalVisible(false);
|
||||
const deleteAndCloseModal = () => {
|
||||
setModalVisible(false);
|
||||
|
||||
const shouldDeleteDestIndex = userCanDeleteIndex && deleteDestIndex;
|
||||
const shouldDeleteDestIndexPattern =
|
||||
userCanDeleteIndex && indexPatternExists && deleteIndexPattern;
|
||||
deleteTransforms(items, shouldDeleteDestIndex, shouldDeleteDestIndexPattern);
|
||||
deleteTransforms(items);
|
||||
};
|
||||
const openModal = () => setModalVisible(true);
|
||||
|
||||
|
@ -86,96 +71,17 @@ export const DeleteAction: FC<DeleteActionProps> = ({ items, forceDisable }) =>
|
|||
defaultMessage: 'Delete {transformId}',
|
||||
values: { transformId: items[0] && items[0].config.id },
|
||||
});
|
||||
const bulkDeleteModalContent = (
|
||||
<>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.transform.transformList.bulkDeleteModalBody"
|
||||
defaultMessage="Are you sure you want to delete {count, plural, one {this} other {these}} {count} {count, plural, one {transform} other {transforms}}?"
|
||||
values={{ count: items.length }}
|
||||
/>
|
||||
</p>
|
||||
<EuiFlexGroup direction="column" gutterSize="none">
|
||||
<EuiFlexItem>
|
||||
{
|
||||
<EuiSwitch
|
||||
data-test-subj="transformBulkDeleteIndexSwitch"
|
||||
label={i18n.translate(
|
||||
'xpack.transform.actionDeleteTransform.bulkDeleteDestinationIndexTitle',
|
||||
{
|
||||
defaultMessage: 'Delete destination indices',
|
||||
}
|
||||
)}
|
||||
checked={deleteDestIndex}
|
||||
onChange={toggleDeleteIndex}
|
||||
/>
|
||||
}
|
||||
</EuiFlexItem>
|
||||
<EuiSpacer size="s" />
|
||||
<EuiFlexItem>
|
||||
{
|
||||
<EuiSwitch
|
||||
data-test-subj="transformBulkDeleteIndexPatternSwitch"
|
||||
label={i18n.translate(
|
||||
'xpack.transform.actionDeleteTransform.bulkDeleteDestIndexPatternTitle',
|
||||
{
|
||||
defaultMessage: 'Delete destination index patterns',
|
||||
}
|
||||
)}
|
||||
checked={deleteIndexPattern}
|
||||
onChange={toggleDeleteIndexPattern}
|
||||
/>
|
||||
}
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</>
|
||||
);
|
||||
|
||||
const deleteModalContent = (
|
||||
<>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.transform.transformList.deleteModalBody"
|
||||
defaultMessage="Are you sure you want to delete this transform?"
|
||||
/>
|
||||
</p>
|
||||
<EuiFlexGroup direction="column" gutterSize="none">
|
||||
<EuiFlexItem>
|
||||
{userCanDeleteIndex && (
|
||||
<EuiSwitch
|
||||
data-test-subj="transformDeleteIndexSwitch"
|
||||
label={i18n.translate(
|
||||
'xpack.transform.actionDeleteTransform.deleteDestinationIndexTitle',
|
||||
{
|
||||
defaultMessage: 'Delete destination index {destinationIndex}',
|
||||
values: { destinationIndex: items[0] && items[0].config.dest.index },
|
||||
}
|
||||
)}
|
||||
checked={deleteDestIndex}
|
||||
onChange={toggleDeleteIndex}
|
||||
/>
|
||||
)}
|
||||
</EuiFlexItem>
|
||||
{userCanDeleteIndex && indexPatternExists && (
|
||||
<EuiFlexItem>
|
||||
<EuiSpacer size="s" />
|
||||
<EuiSwitch
|
||||
data-test-subj="transformDeleteIndexPatternSwitch"
|
||||
label={i18n.translate(
|
||||
'xpack.transform.actionDeleteTransform.deleteDestIndexPatternTitle',
|
||||
{
|
||||
defaultMessage: 'Delete index pattern {destinationIndex}',
|
||||
values: { destinationIndex: items[0] && items[0].config.dest.index },
|
||||
}
|
||||
)}
|
||||
checked={deleteIndexPattern}
|
||||
onChange={toggleDeleteIndexPattern}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
</EuiFlexGroup>
|
||||
</>
|
||||
const bulkDeleteModalMessage = i18n.translate(
|
||||
'xpack.transform.transformList.bulkDeleteModalBody',
|
||||
{
|
||||
defaultMessage:
|
||||
"Are you sure you want to delete {count, plural, one {this} other {these}} {count} {count, plural, one {transform} other {transforms}}? The transform's destination index and optional Kibana index pattern will not be deleted.",
|
||||
values: { count: items.length },
|
||||
}
|
||||
);
|
||||
const deleteModalMessage = i18n.translate('xpack.transform.transformList.deleteModalBody', {
|
||||
defaultMessage: `Are you sure you want to delete this transform? The transform's destination index and optional Kibana index pattern will not be deleted.`,
|
||||
});
|
||||
|
||||
let deleteButton = (
|
||||
<EuiButtonEmpty
|
||||
|
@ -193,7 +99,7 @@ export const DeleteAction: FC<DeleteActionProps> = ({ items, forceDisable }) =>
|
|||
if (disabled || !canDeleteTransform) {
|
||||
let content;
|
||||
if (disabled) {
|
||||
content = isBulkAction ? bulkDeleteButtonDisabledText : deleteButtonDisabledText;
|
||||
content = isBulkAction === true ? bulkDeleteButtonDisabledText : deleteButtonDisabledText;
|
||||
} else {
|
||||
content = createCapabilityFailureMessage('canDeleteTransform');
|
||||
}
|
||||
|
@ -211,7 +117,7 @@ export const DeleteAction: FC<DeleteActionProps> = ({ items, forceDisable }) =>
|
|||
{isModalVisible && (
|
||||
<EuiOverlayMask>
|
||||
<EuiConfirmModal
|
||||
title={isBulkAction ? bulkDeleteModalTitle : deleteModalTitle}
|
||||
title={isBulkAction === true ? bulkDeleteModalTitle : deleteModalTitle}
|
||||
onCancel={closeModal}
|
||||
onConfirm={deleteAndCloseModal}
|
||||
cancelButtonText={i18n.translate(
|
||||
|
@ -229,7 +135,7 @@ export const DeleteAction: FC<DeleteActionProps> = ({ items, forceDisable }) =>
|
|||
defaultFocusedButton={EUI_MODAL_CONFIRM_BUTTON}
|
||||
buttonColor="danger"
|
||||
>
|
||||
{isBulkAction ? bulkDeleteModalContent : deleteModalContent}
|
||||
<p>{isBulkAction === true ? bulkDeleteModalMessage : deleteModalMessage}</p>
|
||||
</EuiConfirmModal>
|
||||
</EuiOverlayMask>
|
||||
)}
|
||||
|
|
|
@ -1,33 +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 { HttpSetup, SavedObjectsClientContract } from 'kibana/public';
|
||||
import { API_BASE_PATH } from '../../../common/constants';
|
||||
import { IIndexPattern } from '../../../../../../src/plugins/data/common/index_patterns';
|
||||
|
||||
export class IndexService {
|
||||
async canDeleteIndex(http: HttpSetup) {
|
||||
const privilege = await http.get(`${API_BASE_PATH}privileges`);
|
||||
if (!privilege) {
|
||||
return false;
|
||||
}
|
||||
return privilege.hasAllPrivileges;
|
||||
}
|
||||
|
||||
async indexPatternExists(savedObjectsClient: SavedObjectsClientContract, indexName: string) {
|
||||
const response = await savedObjectsClient.find<IIndexPattern>({
|
||||
type: 'index-pattern',
|
||||
perPage: 1,
|
||||
search: `"${indexName}"`,
|
||||
searchFields: ['title'],
|
||||
fields: ['title'],
|
||||
});
|
||||
const ip = response.savedObjects.find((obj) => obj.attributes.title === indexName);
|
||||
return ip !== undefined;
|
||||
}
|
||||
}
|
||||
|
||||
export const indexService = new IndexService();
|
|
@ -15,7 +15,6 @@ export {
|
|||
|
||||
export {
|
||||
getErrorMessage,
|
||||
extractErrorMessage,
|
||||
getDataGridSchemaFromKibanaFieldType,
|
||||
getFieldsFromKibanaIndexPattern,
|
||||
multiColumnSortFactory,
|
||||
|
|
|
@ -10,11 +10,7 @@ import { i18n } from '@kbn/i18n';
|
|||
|
||||
import { ResponseError, CustomHttpResponseOptions } from 'src/core/server';
|
||||
|
||||
import {
|
||||
TransformEndpointRequest,
|
||||
TransformEndpointResult,
|
||||
DeleteTransformEndpointResult,
|
||||
} from '../../../common';
|
||||
import { TransformEndpointRequest, TransformEndpointResult } from '../../../common';
|
||||
|
||||
const REQUEST_TIMEOUT = 'RequestTimeout';
|
||||
|
||||
|
@ -23,7 +19,7 @@ export function isRequestTimeout(error: any) {
|
|||
}
|
||||
|
||||
interface Params {
|
||||
results: TransformEndpointResult | DeleteTransformEndpointResult;
|
||||
results: TransformEndpointResult;
|
||||
id: string;
|
||||
items: TransformEndpointRequest[];
|
||||
action: string;
|
||||
|
@ -63,7 +59,7 @@ export function fillResultsWithTimeouts({ results, id, items, action }: Params)
|
|||
},
|
||||
};
|
||||
|
||||
const newResults: TransformEndpointResult | DeleteTransformEndpointResult = {};
|
||||
const newResults: TransformEndpointResult = {};
|
||||
|
||||
return items.reduce((accumResults, currentVal) => {
|
||||
if (results[currentVal.id] === undefined) {
|
||||
|
|
|
@ -14,17 +14,3 @@ export const schemaTransformId = {
|
|||
export interface SchemaTransformId {
|
||||
transformId: string;
|
||||
}
|
||||
|
||||
export const deleteTransformSchema = schema.object({
|
||||
/**
|
||||
* Delete Transform & Destination Index
|
||||
*/
|
||||
transformsInfo: schema.arrayOf(
|
||||
schema.object({
|
||||
id: schema.string(),
|
||||
state: schema.maybe(schema.string()),
|
||||
})
|
||||
),
|
||||
deleteDestIndex: schema.maybe(schema.boolean()),
|
||||
deleteDestIndexPattern: schema.maybe(schema.boolean()),
|
||||
});
|
||||
|
|
|
@ -5,12 +5,7 @@
|
|||
*/
|
||||
import { schema } from '@kbn/config-schema';
|
||||
|
||||
import {
|
||||
KibanaResponseFactory,
|
||||
RequestHandler,
|
||||
RequestHandlerContext,
|
||||
SavedObjectsClientContract,
|
||||
} from 'kibana/server';
|
||||
import { RequestHandler } from 'kibana/server';
|
||||
import { CallCluster } from 'src/legacy/core_plugins/elasticsearch';
|
||||
import { wrapEsError } from '../../../../../legacy/server/lib/create_router/error_wrappers';
|
||||
|
||||
|
@ -19,9 +14,6 @@ import {
|
|||
TransformEndpointResult,
|
||||
TransformId,
|
||||
TRANSFORM_STATE,
|
||||
DeleteTransformEndpointRequest,
|
||||
DeleteTransformStatus,
|
||||
ResultData,
|
||||
} from '../../../common';
|
||||
|
||||
import { RouteDependencies } from '../../types';
|
||||
|
@ -29,9 +21,8 @@ import { RouteDependencies } from '../../types';
|
|||
import { addBasePath } from '../index';
|
||||
|
||||
import { isRequestTimeout, fillResultsWithTimeouts, wrapError } from './error_utils';
|
||||
import { deleteTransformSchema, schemaTransformId, SchemaTransformId } from './schema';
|
||||
import { schemaTransformId, SchemaTransformId } from './schema';
|
||||
import { registerTransformsAuditMessagesRoutes } from './transforms_audit_messages';
|
||||
import { IIndexPattern } from '../../../../../../src/plugins/data/common/index_patterns';
|
||||
|
||||
enum TRANSFORM_ACTIONS {
|
||||
STOP = 'stop',
|
||||
|
@ -182,37 +173,15 @@ export function registerTransformsRoutes(routeDependencies: RouteDependencies) {
|
|||
{
|
||||
path: addBasePath('delete_transforms'),
|
||||
validate: {
|
||||
body: deleteTransformSchema,
|
||||
body: schema.maybe(schema.any()),
|
||||
},
|
||||
},
|
||||
license.guardApiRoute(async (ctx, req, res) => {
|
||||
const {
|
||||
transformsInfo,
|
||||
deleteDestIndex,
|
||||
deleteDestIndexPattern,
|
||||
} = req.body as DeleteTransformEndpointRequest;
|
||||
const transformsInfo = req.body as TransformEndpointRequest[];
|
||||
|
||||
try {
|
||||
const body = await deleteTransforms(
|
||||
transformsInfo,
|
||||
deleteDestIndex,
|
||||
deleteDestIndexPattern,
|
||||
ctx,
|
||||
license,
|
||||
res
|
||||
);
|
||||
|
||||
if (body && body.status) {
|
||||
if (body.status === 404) {
|
||||
return res.notFound();
|
||||
}
|
||||
if (body.status === 403) {
|
||||
return res.forbidden();
|
||||
}
|
||||
}
|
||||
|
||||
return res.ok({
|
||||
body,
|
||||
body: await deleteTransforms(transformsInfo, ctx.transform!.dataClient.callAsCurrentUser),
|
||||
});
|
||||
} catch (e) {
|
||||
return res.customError(wrapError(wrapEsError(e)));
|
||||
|
@ -269,51 +238,18 @@ const getTransforms = async (options: { transformId?: string }, callAsCurrentUse
|
|||
return await callAsCurrentUser('transform.getTransforms', options);
|
||||
};
|
||||
|
||||
async function getIndexPatternId(
|
||||
indexName: string,
|
||||
savedObjectsClient: SavedObjectsClientContract
|
||||
) {
|
||||
const response = await savedObjectsClient.find<IIndexPattern>({
|
||||
type: 'index-pattern',
|
||||
perPage: 1,
|
||||
search: `"${indexName}"`,
|
||||
searchFields: ['title'],
|
||||
fields: ['title'],
|
||||
});
|
||||
const ip = response.saved_objects.find((obj) => obj.attributes.title === indexName);
|
||||
return ip?.id;
|
||||
}
|
||||
|
||||
async function deleteDestIndexPatternById(
|
||||
indexPatternId: string,
|
||||
savedObjectsClient: SavedObjectsClientContract
|
||||
) {
|
||||
return await savedObjectsClient.delete('index-pattern', indexPatternId);
|
||||
}
|
||||
|
||||
async function deleteTransforms(
|
||||
transformsInfo: TransformEndpointRequest[],
|
||||
deleteDestIndex: boolean | undefined,
|
||||
deleteDestIndexPattern: boolean | undefined,
|
||||
ctx: RequestHandlerContext,
|
||||
license: RouteDependencies['license'],
|
||||
response: KibanaResponseFactory
|
||||
callAsCurrentUser: CallCluster
|
||||
) {
|
||||
const tempResults: TransformEndpointResult = {};
|
||||
const results: Record<string, DeleteTransformStatus> = {};
|
||||
const results: TransformEndpointResult = {};
|
||||
|
||||
for (const transformInfo of transformsInfo) {
|
||||
let destinationIndex: string | undefined;
|
||||
const transformDeleted: ResultData = { success: false };
|
||||
const destIndexDeleted: ResultData = { success: false };
|
||||
const destIndexPatternDeleted: ResultData = {
|
||||
success: false,
|
||||
};
|
||||
const transformId = transformInfo.id;
|
||||
try {
|
||||
if (transformInfo.state === TRANSFORM_STATE.FAILED) {
|
||||
try {
|
||||
await ctx.transform!.dataClient.callAsCurrentUser('transform.stopTransform', {
|
||||
await callAsCurrentUser('transform.stopTransform', {
|
||||
transformId,
|
||||
force: true,
|
||||
waitForCompletion: true,
|
||||
|
@ -321,7 +257,7 @@ async function deleteTransforms(
|
|||
} catch (e) {
|
||||
if (isRequestTimeout(e)) {
|
||||
return fillResultsWithTimeouts({
|
||||
results: tempResults,
|
||||
results,
|
||||
id: transformId,
|
||||
items: transformsInfo,
|
||||
action: TRANSFORM_ACTIONS.DELETE,
|
||||
|
@ -329,75 +265,9 @@ async function deleteTransforms(
|
|||
}
|
||||
}
|
||||
}
|
||||
// Grab destination index info to delete
|
||||
try {
|
||||
const transformConfigs = await getTransforms(
|
||||
{ transformId },
|
||||
ctx.transform!.dataClient.callAsCurrentUser
|
||||
);
|
||||
const transformConfig = transformConfigs.transforms[0];
|
||||
destinationIndex = Array.isArray(transformConfig.dest.index)
|
||||
? transformConfig.dest.index[0]
|
||||
: transformConfig.dest.index;
|
||||
} catch (getTransformConfigError) {
|
||||
transformDeleted.error = wrapError(getTransformConfigError);
|
||||
results[transformId] = {
|
||||
transformDeleted,
|
||||
destIndexDeleted,
|
||||
destIndexPatternDeleted,
|
||||
destinationIndex,
|
||||
};
|
||||
continue;
|
||||
}
|
||||
|
||||
// If user checks box to delete the destinationIndex associated with the job
|
||||
if (destinationIndex && deleteDestIndex) {
|
||||
try {
|
||||
// If user does have privilege to delete the index, then delete the index
|
||||
// if no permission then return 403 forbidden
|
||||
await ctx.transform!.dataClient.callAsCurrentUser('indices.delete', {
|
||||
index: destinationIndex,
|
||||
});
|
||||
destIndexDeleted.success = true;
|
||||
} catch (deleteIndexError) {
|
||||
destIndexDeleted.error = wrapError(deleteIndexError);
|
||||
}
|
||||
}
|
||||
|
||||
// Delete the index pattern if there's an index pattern that matches the name of dest index
|
||||
if (destinationIndex && deleteDestIndexPattern) {
|
||||
try {
|
||||
const indexPatternId = await getIndexPatternId(
|
||||
destinationIndex,
|
||||
ctx.core.savedObjects.client
|
||||
);
|
||||
if (indexPatternId) {
|
||||
await deleteDestIndexPatternById(indexPatternId, ctx.core.savedObjects.client);
|
||||
destIndexPatternDeleted.success = true;
|
||||
}
|
||||
} catch (deleteDestIndexPatternError) {
|
||||
destIndexPatternDeleted.error = wrapError(deleteDestIndexPatternError);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
await ctx.transform!.dataClient.callAsCurrentUser('transform.deleteTransform', {
|
||||
transformId,
|
||||
});
|
||||
transformDeleted.success = true;
|
||||
} catch (deleteTransformJobError) {
|
||||
transformDeleted.error = wrapError(deleteTransformJobError);
|
||||
if (transformDeleted.error.statusCode === 403) {
|
||||
return response.forbidden();
|
||||
}
|
||||
}
|
||||
|
||||
results[transformId] = {
|
||||
transformDeleted,
|
||||
destIndexDeleted,
|
||||
destIndexPatternDeleted,
|
||||
destinationIndex,
|
||||
};
|
||||
await callAsCurrentUser('transform.deleteTransform', { transformId });
|
||||
results[transformId] = { success: true };
|
||||
} catch (e) {
|
||||
if (isRequestTimeout(e)) {
|
||||
return fillResultsWithTimeouts({
|
||||
|
@ -407,7 +277,7 @@ async function deleteTransforms(
|
|||
action: TRANSFORM_ACTIONS.DELETE,
|
||||
});
|
||||
}
|
||||
results[transformId] = { transformDeleted: { success: false, error: JSON.stringify(e) } };
|
||||
results[transformId] = { success: false, error: JSON.stringify(e) };
|
||||
}
|
||||
}
|
||||
return results;
|
||||
|
|
|
@ -30,6 +30,5 @@ export default function ({ loadTestFile }) {
|
|||
loadTestFile(require.resolve('./ingest_manager'));
|
||||
loadTestFile(require.resolve('./endpoint'));
|
||||
loadTestFile(require.resolve('./ml'));
|
||||
loadTestFile(require.resolve('./transform'));
|
||||
});
|
||||
}
|
||||
|
|
|
@ -197,7 +197,7 @@ export default ({ getService }: FtrProviderContext) => {
|
|||
await ml.testResources.deleteIndexPattern(destinationIndex);
|
||||
});
|
||||
|
||||
it('should delete job, target index, and index pattern by id', async () => {
|
||||
it('deletes job, target index, and index pattern by id', async () => {
|
||||
const { body } = await supertest
|
||||
.delete(`/api/ml/data_frame/analytics/${analyticsId}`)
|
||||
.query({ deleteDestIndex: true, deleteDestIndexPattern: true })
|
||||
|
|
|
@ -1,318 +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 expect from '@kbn/expect';
|
||||
import { TransformEndpointRequest } from '../../../../plugins/transform/common';
|
||||
import { FtrProviderContext } from '../../ftr_provider_context';
|
||||
import { COMMON_REQUEST_HEADERS } from '../../../functional/services/ml/common';
|
||||
import { USER } from '../../../functional/services/transform/security_common';
|
||||
|
||||
async function asyncForEach(array: any[], callback: Function) {
|
||||
for (let index = 0; index < array.length; index++) {
|
||||
await callback(array[index], index, array);
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default ({ getService }: FtrProviderContext) => {
|
||||
const esArchiver = getService('esArchiver');
|
||||
const supertest = getService('supertestWithoutAuth');
|
||||
const transform = getService('transform');
|
||||
|
||||
function generateDestIndex(transformId: string): string {
|
||||
return `user-${transformId}`;
|
||||
}
|
||||
|
||||
async function createTransform(transformId: string, destinationIndex: string) {
|
||||
const config = {
|
||||
id: transformId,
|
||||
source: { index: ['farequote-*'] },
|
||||
pivot: {
|
||||
group_by: { airline: { terms: { field: 'airline' } } },
|
||||
aggregations: { '@timestamp.value_count': { value_count: { field: '@timestamp' } } },
|
||||
},
|
||||
dest: { index: destinationIndex },
|
||||
};
|
||||
|
||||
await transform.api.createTransform(config);
|
||||
}
|
||||
|
||||
describe('delete_transforms', function () {
|
||||
before(async () => {
|
||||
await esArchiver.loadIfNeeded('ml/farequote');
|
||||
await transform.testResources.setKibanaTimeZoneToUTC();
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await transform.api.cleanTransformIndices();
|
||||
});
|
||||
|
||||
describe('single transform deletion', function () {
|
||||
const transformId = 'test1';
|
||||
const destinationIndex = generateDestIndex(transformId);
|
||||
|
||||
beforeEach(async () => {
|
||||
await createTransform(transformId, destinationIndex);
|
||||
await transform.api.createIndices(destinationIndex);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await transform.api.deleteIndices(destinationIndex);
|
||||
});
|
||||
|
||||
it('should delete transform by transformId', async () => {
|
||||
const transformsInfo: TransformEndpointRequest[] = [{ id: transformId }];
|
||||
const { body } = await supertest
|
||||
.post(`/api/transform/delete_transforms`)
|
||||
.auth(
|
||||
USER.TRANSFORM_POWERUSER,
|
||||
transform.securityCommon.getPasswordForUser(USER.TRANSFORM_POWERUSER)
|
||||
)
|
||||
.set(COMMON_REQUEST_HEADERS)
|
||||
.send({
|
||||
transformsInfo,
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
expect(body[transformId].transformDeleted.success).to.eql(true);
|
||||
expect(body[transformId].destIndexDeleted.success).to.eql(false);
|
||||
expect(body[transformId].destIndexPatternDeleted.success).to.eql(false);
|
||||
await transform.api.waitForTransformNotToExist(transformId);
|
||||
await transform.api.waitForIndicesToExist(destinationIndex);
|
||||
});
|
||||
|
||||
it('should return 403 for unauthorized user', async () => {
|
||||
const transformsInfo: TransformEndpointRequest[] = [{ id: transformId }];
|
||||
await supertest
|
||||
.post(`/api/transform/delete_transforms`)
|
||||
.auth(
|
||||
USER.TRANSFORM_VIEWER,
|
||||
transform.securityCommon.getPasswordForUser(USER.TRANSFORM_VIEWER)
|
||||
)
|
||||
.set(COMMON_REQUEST_HEADERS)
|
||||
.send({
|
||||
transformsInfo,
|
||||
})
|
||||
.expect(403);
|
||||
await transform.api.waitForTransformToExist(transformId);
|
||||
await transform.api.waitForIndicesToExist(destinationIndex);
|
||||
});
|
||||
});
|
||||
|
||||
describe('single transform deletion with invalid transformId', function () {
|
||||
it('should return 200 with error in response if invalid transformId', async () => {
|
||||
const transformsInfo: TransformEndpointRequest[] = [{ id: 'invalid_transform_id' }];
|
||||
const { body } = await supertest
|
||||
.post(`/api/transform/delete_transforms`)
|
||||
.auth(
|
||||
USER.TRANSFORM_POWERUSER,
|
||||
transform.securityCommon.getPasswordForUser(USER.TRANSFORM_POWERUSER)
|
||||
)
|
||||
.set(COMMON_REQUEST_HEADERS)
|
||||
.send({
|
||||
transformsInfo,
|
||||
})
|
||||
.expect(200);
|
||||
expect(body.invalid_transform_id.transformDeleted.success).to.eql(false);
|
||||
expect(body.invalid_transform_id.transformDeleted).to.have.property('error');
|
||||
});
|
||||
});
|
||||
|
||||
describe('bulk deletion', function () {
|
||||
const transformsInfo: TransformEndpointRequest[] = [
|
||||
{ id: 'bulk_delete_test_1' },
|
||||
{ id: 'bulk_delete_test_2' },
|
||||
];
|
||||
const destinationIndices = transformsInfo.map((d) => generateDestIndex(d.id));
|
||||
|
||||
beforeEach(async () => {
|
||||
await asyncForEach(transformsInfo, async ({ id }: { id: string }, idx: number) => {
|
||||
await createTransform(id, destinationIndices[idx]);
|
||||
await transform.api.createIndices(destinationIndices[idx]);
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await asyncForEach(destinationIndices, async (destinationIndex: string) => {
|
||||
await transform.api.deleteIndices(destinationIndex);
|
||||
});
|
||||
});
|
||||
|
||||
it('should delete multiple transforms by transformIds', async () => {
|
||||
const { body } = await supertest
|
||||
.post(`/api/transform/delete_transforms`)
|
||||
.auth(
|
||||
USER.TRANSFORM_POWERUSER,
|
||||
transform.securityCommon.getPasswordForUser(USER.TRANSFORM_POWERUSER)
|
||||
)
|
||||
.set(COMMON_REQUEST_HEADERS)
|
||||
.send({
|
||||
transformsInfo,
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
await asyncForEach(
|
||||
transformsInfo,
|
||||
async ({ id: transformId }: { id: string }, idx: number) => {
|
||||
expect(body[transformId].transformDeleted.success).to.eql(true);
|
||||
expect(body[transformId].destIndexDeleted.success).to.eql(false);
|
||||
expect(body[transformId].destIndexPatternDeleted.success).to.eql(false);
|
||||
await transform.api.waitForTransformNotToExist(transformId);
|
||||
await transform.api.waitForIndicesToExist(destinationIndices[idx]);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it('should delete multiple transforms by transformIds, even if one of the transformIds is invalid', async () => {
|
||||
const invalidTransformId = 'invalid_transform_id';
|
||||
const { body } = await supertest
|
||||
.post(`/api/transform/delete_transforms`)
|
||||
.auth(
|
||||
USER.TRANSFORM_POWERUSER,
|
||||
transform.securityCommon.getPasswordForUser(USER.TRANSFORM_POWERUSER)
|
||||
)
|
||||
.set(COMMON_REQUEST_HEADERS)
|
||||
.send({
|
||||
transformsInfo: [
|
||||
{ id: transformsInfo[0].id },
|
||||
{ id: invalidTransformId },
|
||||
{ id: transformsInfo[1].id },
|
||||
],
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
await asyncForEach(
|
||||
transformsInfo,
|
||||
async ({ id: transformId }: { id: string }, idx: number) => {
|
||||
expect(body[transformId].transformDeleted.success).to.eql(true);
|
||||
expect(body[transformId].destIndexDeleted.success).to.eql(false);
|
||||
expect(body[transformId].destIndexPatternDeleted.success).to.eql(false);
|
||||
await transform.api.waitForTransformNotToExist(transformId);
|
||||
await transform.api.waitForIndicesToExist(destinationIndices[idx]);
|
||||
}
|
||||
);
|
||||
|
||||
expect(body[invalidTransformId].transformDeleted.success).to.eql(false);
|
||||
expect(body[invalidTransformId].transformDeleted).to.have.property('error');
|
||||
});
|
||||
});
|
||||
|
||||
describe('with deleteDestIndex setting', function () {
|
||||
const transformId = 'test2';
|
||||
const destinationIndex = generateDestIndex(transformId);
|
||||
|
||||
before(async () => {
|
||||
await createTransform(transformId, destinationIndex);
|
||||
await transform.api.createIndices(destinationIndex);
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await transform.api.deleteIndices(destinationIndex);
|
||||
});
|
||||
|
||||
it('should delete transform and destination index', async () => {
|
||||
const transformsInfo: TransformEndpointRequest[] = [{ id: transformId }];
|
||||
const { body } = await supertest
|
||||
.post(`/api/transform/delete_transforms`)
|
||||
.auth(
|
||||
USER.TRANSFORM_POWERUSER,
|
||||
transform.securityCommon.getPasswordForUser(USER.TRANSFORM_POWERUSER)
|
||||
)
|
||||
.set(COMMON_REQUEST_HEADERS)
|
||||
.send({
|
||||
transformsInfo,
|
||||
deleteDestIndex: true,
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
expect(body[transformId].transformDeleted.success).to.eql(true);
|
||||
expect(body[transformId].destIndexDeleted.success).to.eql(true);
|
||||
expect(body[transformId].destIndexPatternDeleted.success).to.eql(false);
|
||||
await transform.api.waitForTransformNotToExist(transformId);
|
||||
await transform.api.waitForIndicesNotToExist(destinationIndex);
|
||||
});
|
||||
});
|
||||
|
||||
describe('with deleteDestIndexPattern setting', function () {
|
||||
const transformId = 'test3';
|
||||
const destinationIndex = generateDestIndex(transformId);
|
||||
|
||||
before(async () => {
|
||||
await createTransform(transformId, destinationIndex);
|
||||
await transform.api.createIndices(destinationIndex);
|
||||
await transform.testResources.createIndexPatternIfNeeded(destinationIndex);
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await transform.api.deleteIndices(destinationIndex);
|
||||
await transform.testResources.deleteIndexPattern(destinationIndex);
|
||||
});
|
||||
|
||||
it('should delete transform and destination index pattern', async () => {
|
||||
const transformsInfo: TransformEndpointRequest[] = [{ id: transformId }];
|
||||
const { body } = await supertest
|
||||
.post(`/api/transform/delete_transforms`)
|
||||
.auth(
|
||||
USER.TRANSFORM_POWERUSER,
|
||||
transform.securityCommon.getPasswordForUser(USER.TRANSFORM_POWERUSER)
|
||||
)
|
||||
.set(COMMON_REQUEST_HEADERS)
|
||||
.send({
|
||||
transformsInfo,
|
||||
deleteDestIndex: false,
|
||||
deleteDestIndexPattern: true,
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
expect(body[transformId].transformDeleted.success).to.eql(true);
|
||||
expect(body[transformId].destIndexDeleted.success).to.eql(false);
|
||||
expect(body[transformId].destIndexPatternDeleted.success).to.eql(true);
|
||||
await transform.api.waitForTransformNotToExist(transformId);
|
||||
await transform.api.waitForIndicesToExist(destinationIndex);
|
||||
await transform.testResources.assertIndexPatternNotExist(destinationIndex);
|
||||
});
|
||||
});
|
||||
|
||||
describe('with deleteDestIndex & deleteDestIndexPattern setting', function () {
|
||||
const transformId = 'test4';
|
||||
const destinationIndex = generateDestIndex(transformId);
|
||||
|
||||
before(async () => {
|
||||
await createTransform(transformId, destinationIndex);
|
||||
await transform.api.createIndices(destinationIndex);
|
||||
await transform.testResources.createIndexPatternIfNeeded(destinationIndex);
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await transform.api.deleteIndices(destinationIndex);
|
||||
await transform.testResources.deleteIndexPattern(destinationIndex);
|
||||
});
|
||||
|
||||
it('should delete transform, destination index, & destination index pattern', async () => {
|
||||
const transformsInfo: TransformEndpointRequest[] = [{ id: transformId }];
|
||||
const { body } = await supertest
|
||||
.post(`/api/transform/delete_transforms`)
|
||||
.auth(
|
||||
USER.TRANSFORM_POWERUSER,
|
||||
transform.securityCommon.getPasswordForUser(USER.TRANSFORM_POWERUSER)
|
||||
)
|
||||
.set(COMMON_REQUEST_HEADERS)
|
||||
.send({
|
||||
transformsInfo,
|
||||
deleteDestIndex: true,
|
||||
deleteDestIndexPattern: true,
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
expect(body[transformId].transformDeleted.success).to.eql(true);
|
||||
expect(body[transformId].destIndexDeleted.success).to.eql(true);
|
||||
expect(body[transformId].destIndexPatternDeleted.success).to.eql(true);
|
||||
await transform.api.waitForTransformNotToExist(transformId);
|
||||
await transform.api.waitForIndicesNotToExist(destinationIndex);
|
||||
await transform.testResources.assertIndexPatternNotExist(destinationIndex);
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
|
@ -1,32 +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 { FtrProviderContext } from '../../ftr_provider_context';
|
||||
|
||||
export default function ({ getService, loadTestFile }: FtrProviderContext) {
|
||||
const esArchiver = getService('esArchiver');
|
||||
const transform = getService('transform');
|
||||
|
||||
describe('Machine Learning', function () {
|
||||
this.tags(['transform']);
|
||||
|
||||
before(async () => {
|
||||
await transform.securityCommon.createTransformRoles();
|
||||
await transform.securityCommon.createTransformUsers();
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await transform.securityCommon.cleanTransformUsers();
|
||||
await transform.securityCommon.cleanTransformRoles();
|
||||
|
||||
await esArchiver.unload('ml/farequote');
|
||||
|
||||
await transform.testResources.resetKibanaTimeZone();
|
||||
});
|
||||
|
||||
loadTestFile(require.resolve('./delete_transforms'));
|
||||
});
|
||||
}
|
|
@ -28,7 +28,6 @@ import { InfraLogSourceConfigurationProvider } from './infra_log_source_configur
|
|||
import { MachineLearningProvider } from './ml';
|
||||
import { IngestManagerProvider } from './ingest_manager';
|
||||
import { ResolverGeneratorProvider } from './resolver';
|
||||
import { TransformProvider } from './transform';
|
||||
|
||||
export const services = {
|
||||
...commonServices,
|
||||
|
@ -49,5 +48,4 @@ export const services = {
|
|||
ml: MachineLearningProvider,
|
||||
ingestManager: IngestManagerProvider,
|
||||
resolverGenerator: ResolverGeneratorProvider,
|
||||
transform: TransformProvider,
|
||||
};
|
||||
|
|
|
@ -1,23 +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 { FtrProviderContext } from '../../functional/ftr_provider_context';
|
||||
|
||||
import { TransformAPIProvider } from '../../functional/services/transform/api';
|
||||
import { TransformSecurityCommonProvider } from '../../functional/services/transform/security_common';
|
||||
import { MachineLearningTestResourcesProvider } from '../../functional/services/ml/test_resources';
|
||||
|
||||
export function TransformProvider(context: FtrProviderContext) {
|
||||
const api = TransformAPIProvider(context);
|
||||
const securityCommon = TransformSecurityCommonProvider(context);
|
||||
const testResources = MachineLearningTestResourcesProvider(context);
|
||||
|
||||
return {
|
||||
api,
|
||||
securityCommon,
|
||||
testResources,
|
||||
};
|
||||
}
|
|
@ -20,21 +20,6 @@ export function TransformAPIProvider({ getService }: FtrProviderContext) {
|
|||
const esSupertest = getService('esSupertest');
|
||||
|
||||
return {
|
||||
async createIndices(indices: string) {
|
||||
log.debug(`Creating indices: '${indices}'...`);
|
||||
if ((await es.indices.exists({ index: indices, allowNoIndices: false })) === true) {
|
||||
log.debug(`Indices '${indices}' already exist. Nothing to create.`);
|
||||
return;
|
||||
}
|
||||
|
||||
const createResponse = await es.indices.create({ index: indices });
|
||||
expect(createResponse)
|
||||
.to.have.property('acknowledged')
|
||||
.eql(true, 'Response for create request indices should be acknowledged.');
|
||||
|
||||
await this.waitForIndicesToExist(indices, `expected ${indices} to be created`);
|
||||
},
|
||||
|
||||
async deleteIndices(indices: string) {
|
||||
log.debug(`Deleting indices: '${indices}'...`);
|
||||
if ((await es.indices.exists({ index: indices, allowNoIndices: false })) === false) {
|
||||
|
@ -49,25 +34,11 @@ export function TransformAPIProvider({ getService }: FtrProviderContext) {
|
|||
.to.have.property('acknowledged')
|
||||
.eql(true, 'Response for delete request should be acknowledged');
|
||||
|
||||
await this.waitForIndicesNotToExist(indices, `expected indices '${indices}' to be deleted`);
|
||||
},
|
||||
|
||||
async waitForIndicesToExist(indices: string, errorMsg?: string) {
|
||||
await retry.tryForTime(30 * 1000, async () => {
|
||||
if ((await es.indices.exists({ index: indices, allowNoIndices: false })) === true) {
|
||||
return true;
|
||||
} else {
|
||||
throw new Error(errorMsg || `indices '${indices}' should exist`);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
async waitForIndicesNotToExist(indices: string, errorMsg?: string) {
|
||||
await retry.tryForTime(30 * 1000, async () => {
|
||||
await retry.waitForWithTimeout(`'${indices}' indices to be deleted`, 30 * 1000, async () => {
|
||||
if ((await es.indices.exists({ index: indices, allowNoIndices: false })) === false) {
|
||||
return true;
|
||||
} else {
|
||||
throw new Error(errorMsg || `indices '${indices}' should not exist`);
|
||||
throw new Error(`expected indices '${indices}' to be deleted`);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
@ -92,7 +63,9 @@ export function TransformAPIProvider({ getService }: FtrProviderContext) {
|
|||
|
||||
async getTransformState(transformId: string): Promise<TRANSFORM_STATE> {
|
||||
const stats = await this.getTransformStats(transformId);
|
||||
return stats.state;
|
||||
const state: TRANSFORM_STATE = stats.state;
|
||||
|
||||
return state;
|
||||
},
|
||||
|
||||
async waitForTransformState(transformId: string, expectedState: TRANSFORM_STATE) {
|
||||
|
@ -123,8 +96,8 @@ export function TransformAPIProvider({ getService }: FtrProviderContext) {
|
|||
});
|
||||
},
|
||||
|
||||
async getTransform(transformId: string, expectedCode = 200) {
|
||||
return await esSupertest.get(`/_transform/${transformId}`).expect(expectedCode);
|
||||
async getTransform(transformId: string) {
|
||||
return await esSupertest.get(`/_transform/${transformId}`).expect(200);
|
||||
},
|
||||
|
||||
async createTransform(transformConfig: TransformPivotConfig) {
|
||||
|
@ -132,27 +105,11 @@ export function TransformAPIProvider({ getService }: FtrProviderContext) {
|
|||
log.debug(`Creating transform with id '${transformId}'...`);
|
||||
await esSupertest.put(`/_transform/${transformId}`).send(transformConfig).expect(200);
|
||||
|
||||
await this.waitForTransformToExist(
|
||||
transformId,
|
||||
`expected transform '${transformId}' to be created`
|
||||
);
|
||||
},
|
||||
|
||||
async waitForTransformToExist(transformId: string, errorMsg?: string) {
|
||||
await retry.waitForWithTimeout(`'${transformId}' to exist`, 5 * 1000, async () => {
|
||||
if (await this.getTransform(transformId, 200)) {
|
||||
await retry.waitForWithTimeout(`'${transformId}' to be created`, 5 * 1000, async () => {
|
||||
if (await this.getTransform(transformId)) {
|
||||
return true;
|
||||
} else {
|
||||
throw new Error(errorMsg || `expected transform '${transformId}' to exist`);
|
||||
}
|
||||
});
|
||||
},
|
||||
async waitForTransformNotToExist(transformId: string, errorMsg?: string) {
|
||||
await retry.waitForWithTimeout(`'${transformId}' to exist`, 5 * 1000, async () => {
|
||||
if (await this.getTransform(transformId, 404)) {
|
||||
return true;
|
||||
} else {
|
||||
throw new Error(errorMsg || `expected transform '${transformId}' to not exist`);
|
||||
throw new Error(`expected transform '${transformId}' to be created`);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue