[ML] Transform: Enable force delete if one of the transforms failed (#69472)

Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
This commit is contained in:
Quynh Nguyen 2020-06-23 15:05:44 -05:00 committed by GitHub
parent 1158be9264
commit 22d09a3bbd
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 77 additions and 45 deletions

View file

@ -43,6 +43,7 @@ export interface DeleteTransformEndpointRequest {
transformsInfo: TransformEndpointRequest[];
deleteDestIndex?: boolean;
deleteDestIndexPattern?: boolean;
forceDelete?: boolean;
}
export interface DeleteTransformStatus {

View file

@ -47,10 +47,16 @@ export const useApi = () => {
deleteTransforms(
transformsInfo: TransformEndpointRequest[],
deleteDestIndex: boolean | undefined,
deleteDestIndexPattern: boolean | undefined
deleteDestIndexPattern: boolean | undefined,
forceDelete: boolean
): Promise<DeleteTransformEndpointResult> {
return http.post(`${API_BASE_PATH}delete_transforms`, {
body: JSON.stringify({ transformsInfo, deleteDestIndex, deleteDestIndexPattern }),
body: JSON.stringify({
transformsInfo,
deleteDestIndex,
deleteDestIndexPattern,
forceDelete,
}),
});
},
getTransformsPreview(obj: PreviewRequestBody): Promise<GetTransformsResponse> {

View file

@ -8,13 +8,13 @@ import React, { useCallback, useEffect, useState } from 'react';
import { i18n } from '@kbn/i18n';
import { toMountPoint } from '../../../../../../src/plugins/kibana_react/public';
import {
TransformEndpointRequest,
DeleteTransformEndpointResult,
DeleteTransformStatus,
TransformEndpointRequest,
} from '../../../common';
import { getErrorMessage, extractErrorMessage } from '../../shared_imports';
import { extractErrorMessage, getErrorMessage } from '../../shared_imports';
import { useAppDependencies, useToastNotifications } from '../app_dependencies';
import { TransformListRow, refreshTransformList$, REFRESH_TRANSFORM_LIST_STATE } from '../common';
import { REFRESH_TRANSFORM_LIST_STATE, refreshTransformList$, TransformListRow } from '../common';
import { ToastNotificationText } from '../components';
import { useApi } from './use_api';
import { indexService } from '../services/es_index_service';
@ -27,13 +27,13 @@ export const useDeleteIndexAndTargetIndex = (items: TransformListRow[]) => {
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 {
@ -79,6 +79,7 @@ export const useDeleteIndexAndTargetIndex = (items: TransformListRow[]) => {
useEffect(() => {
checkUserIndexPermission();
// if user only deleting one transform
if (items.length === 1) {
const config = items[0].config;
const destinationIndex = Array.isArray(config.dest.index)
@ -110,7 +111,8 @@ export const useDeleteTransforms = () => {
return async (
transforms: TransformListRow[],
shouldDeleteDestIndex: boolean,
shouldDeleteDestIndexPattern: boolean
shouldDeleteDestIndexPattern: boolean,
shouldForceDelete = false
) => {
const transformsInfo: TransformEndpointRequest[] = transforms.map((tf) => ({
id: tf.config.id,
@ -121,7 +123,8 @@ export const useDeleteTransforms = () => {
const results: DeleteTransformEndpointResult = await api.deleteTransforms(
transformsInfo,
shouldDeleteDestIndex,
shouldDeleteDestIndexPattern
shouldDeleteDestIndexPattern,
shouldForceDelete
);
const isBulk = Object.keys(results).length > 1;
const successCount: Record<SuccessCountField, number> = {

View file

@ -4,25 +4,25 @@
* you may not use this file except in compliance with the Elastic License.
*/
import React, { Fragment, FC, useContext, useState } from 'react';
import React, { FC, Fragment, useContext, useMemo, useState } from 'react';
import { i18n } from '@kbn/i18n';
import {
EUI_MODAL_CONFIRM_BUTTON,
EuiButtonEmpty,
EuiConfirmModal,
EuiOverlayMask,
EuiToolTip,
EUI_MODAL_CONFIRM_BUTTON,
EuiFlexGroup,
EuiFlexItem,
EuiSwitch,
EuiOverlayMask,
EuiSpacer,
EuiSwitch,
EuiToolTip,
} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import { TRANSFORM_STATE } from '../../../../../../common';
import { useDeleteTransforms, useDeleteIndexAndTargetIndex } from '../../../../hooks';
import { useDeleteIndexAndTargetIndex, useDeleteTransforms } from '../../../../hooks';
import {
createCapabilityFailureMessage,
AuthorizationContext,
createCapabilityFailureMessage,
} from '../../../../lib/authorization';
import { TransformListRow } from '../../../../common';
@ -31,11 +31,17 @@ interface DeleteActionProps {
forceDisable?: boolean;
}
const transformCanNotBeDeleted = (i: TransformListRow) =>
![TRANSFORM_STATE.STOPPED, TRANSFORM_STATE.FAILED].includes(i.stats.state);
export const DeleteAction: FC<DeleteActionProps> = ({ items, forceDisable }) => {
const isBulkAction = items.length > 1;
const disabled = items.some((i: TransformListRow) => i.stats.state !== TRANSFORM_STATE.STOPPED);
const disabled = items.some(transformCanNotBeDeleted);
const shouldForceDelete = useMemo(
() => items.some((i: TransformListRow) => i.stats.state === TRANSFORM_STATE.FAILED),
[items]
);
const { canDeleteTransform } = useContext(AuthorizationContext).capabilities;
const deleteTransforms = useDeleteTransforms();
const {
@ -56,7 +62,12 @@ export const DeleteAction: FC<DeleteActionProps> = ({ items, forceDisable }) =>
const shouldDeleteDestIndex = userCanDeleteIndex && deleteDestIndex;
const shouldDeleteDestIndexPattern =
userCanDeleteIndex && indexPatternExists && deleteIndexPattern;
deleteTransforms(items, shouldDeleteDestIndex, shouldDeleteDestIndexPattern);
// if we are deleting multiple transforms, then force delete all if at least one item has failed
// else, force delete only when the item user picks has failed
const forceDelete = isBulkAction
? shouldForceDelete
: items[0] && items[0] && items[0].stats.state === TRANSFORM_STATE.FAILED;
deleteTransforms(items, shouldDeleteDestIndex, shouldDeleteDestIndexPattern, forceDelete);
};
const openModal = () => setModalVisible(true);
@ -89,11 +100,19 @@ export const DeleteAction: FC<DeleteActionProps> = ({ items, forceDisable }) =>
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 }}
/>
{shouldForceDelete ? (
<FormattedMessage
id="xpack.transform.transformList.bulkForceDeleteModalBody"
defaultMessage="Are you sure you want to force delete {count, plural, one {this} other {these}} {count} {count, plural, one {transform} other {transforms}}? The {count, plural, one {transform} other {transforms}} will be deleted regardless of {count, plural, one {its} other {their}} current state."
values={{ count: items.length }}
/>
) : (
<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>
@ -134,10 +153,17 @@ export const DeleteAction: FC<DeleteActionProps> = ({ items, forceDisable }) =>
const deleteModalContent = (
<>
<p>
<FormattedMessage
id="xpack.transform.transformList.deleteModalBody"
defaultMessage="Are you sure you want to delete this transform?"
/>
{items[0] && items[0] && items[0].stats.state === TRANSFORM_STATE.FAILED ? (
<FormattedMessage
id="xpack.transform.transformList.forceDeleteModalBody"
defaultMessage="Are you sure you want to force delete this transform? The transform will be deleted regardless of its current state."
/>
) : (
<FormattedMessage
id="xpack.transform.transformList.deleteModalBody"
defaultMessage="Are you sure you want to delete this transform?"
/>
)}
</p>
<EuiFlexGroup direction="column" gutterSize="none">
<EuiFlexItem>

View file

@ -83,11 +83,14 @@ export const elasticsearchJsPlugin = (Client: any, config: any, components: any)
transform.deleteTransform = ca({
urls: [
{
fmt: '/_transform/<%=transformId%>',
fmt: '/_transform/<%=transformId%>?&force=<%=force%>',
req: {
transformId: {
type: 'string',
},
force: {
type: 'boolean',
},
},
},
],

View file

@ -27,4 +27,5 @@ export const deleteTransformSchema = schema.object({
),
deleteDestIndex: schema.maybe(schema.boolean()),
deleteDestIndexPattern: schema.maybe(schema.boolean()),
forceDelete: schema.maybe(schema.boolean()),
});

View file

@ -190,6 +190,7 @@ export function registerTransformsRoutes(routeDependencies: RouteDependencies) {
transformsInfo,
deleteDestIndex,
deleteDestIndexPattern,
forceDelete,
} = req.body as DeleteTransformEndpointRequest;
try {
@ -197,6 +198,7 @@ export function registerTransformsRoutes(routeDependencies: RouteDependencies) {
transformsInfo,
deleteDestIndex,
deleteDestIndexPattern,
forceDelete,
ctx,
license,
res
@ -295,39 +297,28 @@ async function deleteTransforms(
transformsInfo: TransformEndpointRequest[],
deleteDestIndex: boolean | undefined,
deleteDestIndexPattern: boolean | undefined,
shouldForceDelete: boolean = false,
ctx: RequestHandlerContext,
license: RouteDependencies['license'],
response: KibanaResponseFactory
) {
const tempResults: TransformEndpointResult = {};
const results: Record<string, DeleteTransformStatus> = {};
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;
// force delete only if the transform has failed
let needToForceDelete = false;
try {
if (transformInfo.state === TRANSFORM_STATE.FAILED) {
try {
await ctx.transform!.dataClient.callAsCurrentUser('transform.stopTransform', {
transformId,
force: true,
waitForCompletion: true,
} as StopOptions);
} catch (e) {
if (isRequestTimeout(e)) {
return fillResultsWithTimeouts({
results: tempResults,
id: transformId,
items: transformsInfo,
action: TRANSFORM_ACTIONS.DELETE,
});
}
}
needToForceDelete = true;
}
// Grab destination index info to delete
try {
@ -383,6 +374,7 @@ async function deleteTransforms(
try {
await ctx.transform!.dataClient.callAsCurrentUser('transform.deleteTransform', {
transformId,
force: shouldForceDelete && needToForceDelete,
});
transformDeleted.success = true;
} catch (deleteTransformJobError) {