From 07d9e7a304ba502d388f7c60f761302ad8abfec5 Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Tue, 25 Jan 2022 16:58:42 +0100 Subject: [PATCH 1/7] [ML] Adds reset action to transforms management. --- .../common/api_schemas/reset_transforms.ts | 32 +++++ .../common/api_schemas/type_guards.ts | 10 ++ .../transform/public/app/hooks/index.ts | 1 + .../transform/public/app/hooks/use_api.ts | 15 +++ .../public/app/hooks/use_reset_transform.tsx | 112 ++++++++++++++++++ .../components/authorization_provider.tsx | 3 + .../lib/authorization/components/common.ts | 7 ++ .../components/action_reset/index.ts | 10 ++ .../action_reset/reset_action_modal.tsx | 88 ++++++++++++++ .../action_reset/reset_action_name.tsx | 70 +++++++++++ .../action_reset/use_reset_action.tsx | 83 +++++++++++++ .../transform_list/transform_list.tsx | 21 ++++ .../components/transform_list/use_actions.tsx | 4 + .../server/routes/api/error_utils.ts | 11 +- .../transform/server/routes/api/transforms.ts | 89 ++++++++++++++ 15 files changed, 554 insertions(+), 2 deletions(-) create mode 100644 x-pack/plugins/transform/common/api_schemas/reset_transforms.ts create mode 100644 x-pack/plugins/transform/public/app/hooks/use_reset_transform.tsx create mode 100644 x-pack/plugins/transform/public/app/sections/transform_management/components/action_reset/index.ts create mode 100644 x-pack/plugins/transform/public/app/sections/transform_management/components/action_reset/reset_action_modal.tsx create mode 100644 x-pack/plugins/transform/public/app/sections/transform_management/components/action_reset/reset_action_name.tsx create mode 100644 x-pack/plugins/transform/public/app/sections/transform_management/components/action_reset/use_reset_action.tsx diff --git a/x-pack/plugins/transform/common/api_schemas/reset_transforms.ts b/x-pack/plugins/transform/common/api_schemas/reset_transforms.ts new file mode 100644 index 00000000000000..951f5ad87716a2 --- /dev/null +++ b/x-pack/plugins/transform/common/api_schemas/reset_transforms.ts @@ -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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { schema, TypeOf } from '@kbn/config-schema'; + +import { transformStateSchema, ResponseStatus } from './common'; + +export const resetTransformsRequestSchema = schema.object({ + /** + * Reset Transforms + */ + transformsInfo: schema.arrayOf( + schema.object({ + id: schema.string(), + state: transformStateSchema, + }) + ), +}); + +export type ResetTransformsRequestSchema = TypeOf; + +export interface ResetTransformStatus { + transformReset: ResponseStatus; +} + +export interface ResetTransformsResponseSchema { + [key: string]: ResetTransformStatus; +} diff --git a/x-pack/plugins/transform/common/api_schemas/type_guards.ts b/x-pack/plugins/transform/common/api_schemas/type_guards.ts index 6c572a195b65f4..3af08e57f71981 100644 --- a/x-pack/plugins/transform/common/api_schemas/type_guards.ts +++ b/x-pack/plugins/transform/common/api_schemas/type_guards.ts @@ -16,6 +16,7 @@ import { isPopulatedObject } from '../shared_imports'; import type { FieldHistogramsResponseSchema } from './field_histograms'; import type { GetTransformsAuditMessagesResponseSchema } from './audit_messages'; import type { DeleteTransformsResponseSchema } from './delete_transforms'; +import type { ResetTransformsResponseSchema } from './reset_transforms'; import type { StartTransformsResponseSchema } from './start_transforms'; import type { StopTransformsResponseSchema } from './stop_transforms'; import type { @@ -56,6 +57,15 @@ export const isDeleteTransformsResponseSchema = ( ); }; +export const isResetTransformsResponseSchema = ( + arg: unknown +): arg is ResetTransformsResponseSchema => { + return ( + isPopulatedObject(arg) && + Object.values(arg).every((d) => isPopulatedObject(d, ['transformReset'])) + ); +}; + export const isEsIndices = (arg: unknown): arg is EsIndex[] => { return Array.isArray(arg); }; diff --git a/x-pack/plugins/transform/public/app/hooks/index.ts b/x-pack/plugins/transform/public/app/hooks/index.ts index b050192fb9fb31..88333224f786b8 100644 --- a/x-pack/plugins/transform/public/app/hooks/index.ts +++ b/x-pack/plugins/transform/public/app/hooks/index.ts @@ -8,6 +8,7 @@ export { useApi } from './use_api'; export { useGetTransforms } from './use_get_transforms'; export { useDeleteTransforms, useDeleteIndexAndTargetIndex } from './use_delete_transform'; +export { useResetTransforms } from './use_reset_transform'; export { useStartTransforms } from './use_start_transform'; export { useStopTransforms } from './use_stop_transform'; export { useRequest } from './use_request'; diff --git a/x-pack/plugins/transform/public/app/hooks/use_api.ts b/x-pack/plugins/transform/public/app/hooks/use_api.ts index 21e37ca16c4de7..64ea34c9470cf2 100644 --- a/x-pack/plugins/transform/public/app/hooks/use_api.ts +++ b/x-pack/plugins/transform/public/app/hooks/use_api.ts @@ -22,6 +22,10 @@ import type { FieldHistogramsRequestSchema, FieldHistogramsResponseSchema, } from '../../../common/api_schemas/field_histograms'; +import type { + ResetTransformsRequestSchema, + ResetTransformsResponseSchema, +} from '../../../common/api_schemas/reset_transforms'; import type { StartTransformsRequestSchema, StartTransformsResponseSchema, @@ -157,6 +161,17 @@ export const useApi = () => { return e; } }, + async resetTransforms( + reqBody: ResetTransformsRequestSchema + ): Promise { + try { + return await http.post(`${API_BASE_PATH}reset_transforms`, { + body: JSON.stringify(reqBody), + }); + } catch (e) { + return e; + } + }, async startTransforms( reqBody: StartTransformsRequestSchema ): Promise { diff --git a/x-pack/plugins/transform/public/app/hooks/use_reset_transform.tsx b/x-pack/plugins/transform/public/app/hooks/use_reset_transform.tsx new file mode 100644 index 00000000000000..5c76276b346004 --- /dev/null +++ b/x-pack/plugins/transform/public/app/hooks/use_reset_transform.tsx @@ -0,0 +1,112 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { i18n } from '@kbn/i18n'; +import { toMountPoint } from '../../../../../../src/plugins/kibana_react/public'; +import type { + ResetTransformStatus, + ResetTransformsRequestSchema, +} from '../../../common/api_schemas/reset_transforms'; +import { isResetTransformsResponseSchema } from '../../../common/api_schemas/type_guards'; +import { getErrorMessage } from '../../../common/utils/errors'; +import { useAppDependencies, useToastNotifications } from '../app_dependencies'; +import { REFRESH_TRANSFORM_LIST_STATE, refreshTransformList$ } from '../common'; +import { ToastNotificationText } from '../components'; +import { useApi } from './use_api'; + +type SuccessCountField = keyof Omit; + +export const useResetTransforms = () => { + const { overlays, theme } = useAppDependencies(); + const toastNotifications = useToastNotifications(); + const api = useApi(); + + return async (reqBody: ResetTransformsRequestSchema) => { + const results = await api.resetTransforms(reqBody); + + if (!isResetTransformsResponseSchema(results)) { + toastNotifications.addDanger({ + title: i18n.translate('xpack.transform.transformList.resetTransformGenericErrorMessage', { + defaultMessage: 'An error occurred calling the API endpoint to reset transforms.', + }), + text: toMountPoint( + , + { theme$: theme.theme$ } + ), + }); + return; + } + + const isBulk = Object.keys(results).length > 1; + const successCount: Record = { + transformReset: 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]; + + // if we are only deleting one transform, show the success toast messages + if (!isBulk && status.transformReset) { + if (status.transformReset?.success) { + toastNotifications.addSuccess( + i18n.translate('xpack.transform.transformList.resetTransformSuccessMessage', { + defaultMessage: 'Request to reset transform {transformId} acknowledged.', + values: { transformId }, + }) + ); + } + } else { + (Object.keys(successCount) as SuccessCountField[]).forEach((key) => { + if (status[key]?.success) { + successCount[key] = successCount[key] + 1; + } + }); + } + if (status.transformReset?.error) { + const error = status.transformReset.error.reason; + toastNotifications.addDanger({ + title: i18n.translate('xpack.transform.transformList.resetTransformErrorMessage', { + defaultMessage: 'An error occurred resetting the transform {transformId}', + values: { transformId }, + }), + text: toMountPoint( + , + { theme$: theme.theme$ } + ), + }); + } + } + } + + // if we are deleting multiple transforms, combine the success messages + if (isBulk) { + if (successCount.transformReset > 0) { + toastNotifications.addSuccess( + i18n.translate('xpack.transform.transformList.bulkResetTransformSuccessMessage', { + defaultMessage: + 'Successfully reset {count} {count, plural, one {transform} other {transforms}}.', + values: { count: successCount.transformReset }, + }) + ); + } + } + + refreshTransformList$.next(REFRESH_TRANSFORM_LIST_STATE.REFRESH); + }; +}; diff --git a/x-pack/plugins/transform/public/app/lib/authorization/components/authorization_provider.tsx b/x-pack/plugins/transform/public/app/lib/authorization/components/authorization_provider.tsx index cc6313bf058c6a..cae840b86a8335 100644 --- a/x-pack/plugins/transform/public/app/lib/authorization/components/authorization_provider.tsx +++ b/x-pack/plugins/transform/public/app/lib/authorization/components/authorization_provider.tsx @@ -28,6 +28,7 @@ const initialCapabilities: Capabilities = { canStartStopTransform: false, canCreateTransformAlerts: false, canUseTransformAlerts: false, + canResetTransform: false, }; const initialValue: Authorization = { @@ -77,6 +78,8 @@ export const AuthorizationProvider = ({ privilegesEndpoint, children }: Props) = 'cluster:admin/transform/delete', ]); + value.capabilities.canResetTransform = hasPrivilege(['cluster', 'cluster:admin/transform/reset']); + value.capabilities.canPreviewTransform = hasPrivilege([ 'cluster', 'cluster:admin/transform/preview', diff --git a/x-pack/plugins/transform/public/app/lib/authorization/components/common.ts b/x-pack/plugins/transform/public/app/lib/authorization/components/common.ts index d430a4d059e5c9..659d5256439659 100644 --- a/x-pack/plugins/transform/public/app/lib/authorization/components/common.ts +++ b/x-pack/plugins/transform/public/app/lib/authorization/components/common.ts @@ -18,6 +18,7 @@ export interface Capabilities { canStartStopTransform: boolean; canCreateTransformAlerts: boolean; canUseTransformAlerts: boolean; + canResetTransform: boolean; } export type Privilege = [string, string]; @@ -91,6 +92,12 @@ export function createCapabilityFailureMessage( }); break; + case 'canResetTransform': + message = i18n.translate('xpack.transform.capability.noPermission.resetTransformTooltip', { + defaultMessage: 'You do not have permission to reset transforms.', + }); + break; + case 'noTransformNodes': message = i18n.translate('xpack.transform.capability.noPermission.noTransformNodesTooltip', { defaultMessage: 'There are no transform nodes available.', diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/action_reset/index.ts b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_reset/index.ts new file mode 100644 index 00000000000000..62f4d42715579f --- /dev/null +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_reset/index.ts @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { useResetAction } from './use_reset_action'; +export { ResetActionModal } from './reset_action_modal'; +export { isResetActionDisabled, ResetActionName } from './reset_action_name'; diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/action_reset/reset_action_modal.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_reset/reset_action_modal.tsx new file mode 100644 index 00000000000000..efe5a0c0b09fa4 --- /dev/null +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_reset/reset_action_modal.tsx @@ -0,0 +1,88 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { FC, useMemo } from 'react'; +import { i18n } from '@kbn/i18n'; +import { EUI_MODAL_CONFIRM_BUTTON, EuiConfirmModal, EuiFlexGroup, EuiSpacer } from '@elastic/eui'; +import { ResetAction } from './use_reset_action'; +import { isManagedTransform } from '../../../../common/managed_transforms_utils'; +import { ManagedTransformsWarningCallout } from '../managed_transforms_callout/managed_transforms_callout'; + +export const ResetActionModal: FC = ({ + closeModal, + resetAndCloseModal, + items, + shouldForceReset, +}) => { + const hasManagedTransforms = useMemo(() => items.some((t) => isManagedTransform(t)), [items]); + const isBulkAction = items.length > 1; + + const bulkResetModalTitle = i18n.translate('xpack.transform.transformList.bulkResetModalTitle', { + defaultMessage: 'Reset {count} {count, plural, one {transform} other {transforms}}?', + values: { count: items.length }, + }); + const resetModalTitle = i18n.translate('xpack.transform.transformList.resetModalTitle', { + defaultMessage: 'Reset {transformId}?', + values: { transformId: items[0] && items[0].config.id }, + }); + const bulkResetModalContent = ( + <> + + {hasManagedTransforms ? ( + <> + + + + ) : null} + + + ); + + const resetModalContent = ( + <> + + {hasManagedTransforms ? ( + <> + + + + ) : null} + + + ); + + return ( + + {isBulkAction ? bulkResetModalContent : resetModalContent} + + ); +}; diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/action_reset/reset_action_name.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_reset/reset_action_name.tsx new file mode 100644 index 00000000000000..8180ddaf26045d --- /dev/null +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_reset/reset_action_name.tsx @@ -0,0 +1,70 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { FC } from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiToolTip } from '@elastic/eui'; +import { TransformState, TRANSFORM_STATE } from '../../../../../../common/constants'; +import { createCapabilityFailureMessage } from '../../../../lib/authorization'; +import { TransformListRow } from '../../../../common'; + +export const resetActionNameText = i18n.translate( + 'xpack.transform.transformList.resetActionNameText', + { + defaultMessage: 'Reset', + } +); + +const transformCanNotBeReseted = (i: TransformListRow) => + !([TRANSFORM_STATE.STOPPED, TRANSFORM_STATE.FAILED] as TransformState[]).includes(i.stats.state); + +export const isResetActionDisabled = (items: TransformListRow[], forceDisable: boolean) => { + const disabled = items.some(transformCanNotBeReseted); + return forceDisable === true || disabled; +}; + +export interface ResetActionNameProps { + canResetTransform: boolean; + disabled: boolean; + isBulkAction: boolean; +} + +export const ResetActionName: FC = ({ + canResetTransform, + disabled, + isBulkAction, +}) => { + const bulkResetButtonDisabledText = i18n.translate( + 'xpack.transform.transformList.resetBulkActionDisabledToolTipContent', + { + defaultMessage: 'One or more selected transforms must be stopped in order to be reseted.', + } + ); + const resetButtonDisabledText = i18n.translate( + 'xpack.transform.transformList.resetActionDisabledToolTipContent', + { + defaultMessage: 'Stop the transform in order to reset it.', + } + ); + + if (disabled || !canResetTransform) { + let content; + if (disabled) { + content = isBulkAction ? bulkResetButtonDisabledText : resetButtonDisabledText; + } else { + content = createCapabilityFailureMessage('canResetTransform'); + } + + return ( + + <>{resetActionNameText} + + ); + } + + return <>{resetActionNameText}; +}; diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/action_reset/use_reset_action.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_reset/use_reset_action.tsx new file mode 100644 index 00000000000000..70164bc22a63cd --- /dev/null +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_reset/use_reset_action.tsx @@ -0,0 +1,83 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useContext, useMemo, useState } from 'react'; + +import { TRANSFORM_STATE } from '../../../../../../common/constants'; + +import { TransformListAction, TransformListRow } from '../../../../common'; +import { useResetTransforms } from '../../../../hooks'; +import { AuthorizationContext } from '../../../../lib/authorization'; + +import { resetActionNameText, isResetActionDisabled, ResetActionName } from './reset_action_name'; + +export type ResetAction = ReturnType; +export const useResetAction = (forceDisable: boolean) => { + const { canResetTransform } = useContext(AuthorizationContext).capabilities; + + const resetTransforms = useResetTransforms(); + + const [isModalVisible, setModalVisible] = useState(false); + const [items, setItems] = useState([]); + + const shouldForceReset = useMemo( + () => items.some((i: TransformListRow) => i.stats.state === TRANSFORM_STATE.FAILED), + [items] + ); + + const closeModal = () => setModalVisible(false); + + const resetAndCloseModal = () => { + setModalVisible(false); + + resetTransforms({ + transformsInfo: items.map((i) => ({ + id: i.config.id, + state: i.stats.state, + })), + }); + }; + + const openModal = (newItems: TransformListRow[]) => { + if (Array.isArray(newItems)) { + setItems(newItems); + setModalVisible(true); + } + }; + + const action: TransformListAction = useMemo( + () => ({ + name: (item: TransformListRow) => ( + + ), + enabled: (item: TransformListRow) => + !isResetActionDisabled([item], forceDisable) && canResetTransform, + description: resetActionNameText, + icon: 'refresh', + type: 'icon', + onClick: (item: TransformListRow) => openModal([item]), + 'data-test-subj': 'transformActionReset', + }), + [canResetTransform, forceDisable] + ); + + return { + action, + closeModal, + resetAndCloseModal, + isModalVisible, + items, + openModal, + shouldForceReset, + }; +}; diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/transform_list.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/transform_list.tsx index cdf0c14409fdd0..4c8447feae1ee2 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/transform_list.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/transform_list.tsx @@ -41,6 +41,12 @@ import { DeleteActionName, DeleteActionModal, } from '../action_delete'; +import { + isResetActionDisabled, + useResetAction, + ResetActionName, + ResetActionModal, +} from '../action_reset'; import { useStartAction, StartActionName, StartActionModal } from '../action_start'; import { StopActionName, useStopAction } from '../action_stop'; @@ -92,6 +98,7 @@ export const TransformList: FC = ({ const [isActionsMenuOpen, setIsActionsMenuOpen] = useState(false); const bulkStartAction = useStartAction(false, transformNodes); const bulkDeleteAction = useDeleteAction(false); + const bulkResetAction = useResetAction(false); const bulkStopAction = useStopAction(false); const [searchError, setSearchError] = useState(undefined); @@ -196,6 +203,19 @@ export const TransformList: FC = ({ , +
+ { + bulkResetAction.openModal(transformSelection); + }} + > + + +
,
bulkDeleteAction.openModal(transformSelection)}> = ({ {/* Bulk Action Modals */} {bulkStartAction.isModalVisible && } {bulkDeleteAction.isModalVisible && } + {bulkResetAction.isModalVisible && } {bulkStopAction.isModalVisible && } {/* Single Action Modals */} diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/use_actions.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/use_actions.tsx index 853c839a096faa..5d480003c7600f 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/use_actions.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/use_actions.tsx @@ -16,6 +16,7 @@ import { useDeleteAction, DeleteActionModal } from '../action_delete'; import { useDiscoverAction } from '../action_discover'; import { EditTransformFlyout } from '../edit_transform_flyout'; import { useEditAction } from '../action_edit'; +import { useResetAction, ResetActionModal } from '../action_reset'; import { useStartAction, StartActionModal } from '../action_start'; import { useStopAction } from '../action_stop'; import { useCreateAlertRuleAction } from '../action_create_alert'; @@ -35,6 +36,7 @@ export const useActions = ({ const deleteAction = useDeleteAction(forceDisable); const discoverAction = useDiscoverAction(forceDisable); const editAction = useEditAction(forceDisable, transformNodes); + const resetAction = useResetAction(forceDisable); const startAction = useStartAction(forceDisable, transformNodes); const stopAction = useStopAction(forceDisable); const createAlertRuleAction = useCreateAlertRuleAction(forceDisable); @@ -42,6 +44,7 @@ export const useActions = ({ return { modals: ( <> + {resetAction.isModalVisible && } {startAction.isModalVisible && } {stopAction.isModalVisible && } @@ -63,6 +66,7 @@ export const useActions = ({ editAction.action, cloneAction.action, deleteAction.action, + resetAction.action, ], }; }; diff --git a/x-pack/plugins/transform/server/routes/api/error_utils.ts b/x-pack/plugins/transform/server/routes/api/error_utils.ts index 7c4ac885766786..d9b892b7afcee3 100644 --- a/x-pack/plugins/transform/server/routes/api/error_utils.ts +++ b/x-pack/plugins/transform/server/routes/api/error_utils.ts @@ -13,6 +13,7 @@ import { ResponseError, CustomHttpResponseOptions } from 'src/core/server'; import { CommonResponseStatusSchema, TransformIdsSchema } from '../../../common/api_schemas/common'; import { DeleteTransformsResponseSchema } from '../../../common/api_schemas/delete_transforms'; +import { ResetTransformsResponseSchema } from '../../../common/api_schemas/reset_transforms'; const REQUEST_TIMEOUT = 'RequestTimeout'; @@ -21,7 +22,10 @@ export function isRequestTimeout(error: any) { } interface Params { - results: CommonResponseStatusSchema | DeleteTransformsResponseSchema; + results: + | CommonResponseStatusSchema + | DeleteTransformsResponseSchema + | ResetTransformsResponseSchema; id: string; items: TransformIdsSchema; action: string; @@ -60,7 +64,10 @@ export function fillResultsWithTimeouts({ results, id, items, action }: Params) ], }; - const newResults: CommonResponseStatusSchema | DeleteTransformsResponseSchema = {}; + const newResults: + | CommonResponseStatusSchema + | DeleteTransformsResponseSchema + | ResetTransformsResponseSchema = {}; return items.reduce((accumResults, currentVal) => { if (results[currentVal.id] === undefined) { diff --git a/x-pack/plugins/transform/server/routes/api/transforms.ts b/x-pack/plugins/transform/server/routes/api/transforms.ts index deef8898fdc661..1f2cdb5b3b3dfa 100644 --- a/x-pack/plugins/transform/server/routes/api/transforms.ts +++ b/x-pack/plugins/transform/server/routes/api/transforms.ts @@ -27,6 +27,11 @@ import { DeleteTransformsRequestSchema, DeleteTransformsResponseSchema, } from '../../../common/api_schemas/delete_transforms'; +import { + resetTransformsRequestSchema, + ResetTransformsRequestSchema, + ResetTransformsResponseSchema, +} from '../../../common/api_schemas/reset_transforms'; import { startTransformsRequestSchema, StartTransformsRequestSchema, @@ -316,6 +321,46 @@ export function registerTransformsRoutes(routeDependencies: RouteDependencies) { ) ); + /** + * @apiGroup Transforms + * + * @api {post} /api/transform/reset_transforms Post reset transforms + * @apiName ResetTransforms + * @apiDescription resets transforms + * + * @apiSchema (body) resetTransformsRequestSchema + */ + router.post( + { + path: addBasePath('reset_transforms'), + validate: { + body: resetTransformsRequestSchema, + }, + }, + license.guardApiRoute( + async (ctx, req, res) => { + try { + const body = await resetTransforms(req.body, ctx, res); + + if (body && body.status) { + if (body.status === 404) { + return res.notFound(); + } + if (body.status === 403) { + return res.forbidden(); + } + } + + return res.ok({ + body, + }); + } catch (e) { + return res.customError(wrapError(wrapEsError(e))); + } + } + ) + ); + /** * @apiGroup Transforms * @@ -547,6 +592,50 @@ async function deleteTransforms( return results; } +async function resetTransforms( + reqBody: ResetTransformsRequestSchema, + ctx: RequestHandlerContext, + response: KibanaResponseFactory +) { + const { transformsInfo } = reqBody; + + const results: ResetTransformsResponseSchema = {}; + + for (const transformInfo of transformsInfo) { + const transformReset: ResponseStatus = { success: false }; + const transformId = transformInfo.id; + + try { + try { + await ctx.core.elasticsearch.client.asCurrentUser.transform.resetTransform({ + transform_id: transformId, + }); + transformReset.success = true; + } catch (deleteTransformJobError) { + transformReset.error = deleteTransformJobError.meta.body.error; + if (deleteTransformJobError.statusCode === 403) { + return response.forbidden(); + } + } + + results[transformId] = { + transformReset, + }; + } catch (e) { + if (isRequestTimeout(e)) { + return fillResultsWithTimeouts({ + results, + id: transformInfo.id, + items: transformsInfo, + action: TRANSFORM_ACTIONS.DELETE, + }); + } + results[transformId] = { transformReset: { success: false, error: e.meta.body.error } }; + } + } + return results; +} + const previewTransformHandler: RequestHandler< undefined, undefined, From ee15a856a96dff72fb1b0c809cf70600b8c361fe Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Wed, 26 Jan 2022 12:34:28 +0100 Subject: [PATCH 2/7] [ML] Fix capabilities. Fix tests. --- x-pack/plugins/transform/common/constants.ts | 1 + .../components/transform_list/use_actions.test.tsx | 1 + x-pack/test/functional/services/transform/transform_table.ts | 1 + 3 files changed, 3 insertions(+) diff --git a/x-pack/plugins/transform/common/constants.ts b/x-pack/plugins/transform/common/constants.ts index d56b1ad56898ac..d2833f0e071fb3 100644 --- a/x-pack/plugins/transform/common/constants.ts +++ b/x-pack/plugins/transform/common/constants.ts @@ -55,6 +55,7 @@ export const APP_CLUSTER_PRIVILEGES = [ 'cluster:admin/transform/delete', 'cluster:admin/transform/preview', 'cluster:admin/transform/put', + 'cluster:admin/transform/reset', 'cluster:admin/transform/start', 'cluster:admin/transform/start_task', 'cluster:admin/transform/stop', diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/use_actions.test.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/use_actions.test.tsx index 20d2f784a4d8b3..68adbca5d662fc 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/use_actions.test.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/use_actions.test.tsx @@ -33,6 +33,7 @@ describe('Transform: Transform List Actions', () => { 'transformActionEdit', 'transformActionClone', 'transformActionDelete', + 'transformActionReset', ]); }); }); diff --git a/x-pack/test/functional/services/transform/transform_table.ts b/x-pack/test/functional/services/transform/transform_table.ts index 9e3ffcdfd8095d..6240ce1791464e 100644 --- a/x-pack/test/functional/services/transform/transform_table.ts +++ b/x-pack/test/functional/services/transform/transform_table.ts @@ -303,6 +303,7 @@ export function TransformTableProvider({ getService }: FtrProviderContext) { await testSubjects.existOrFail('transformActionDelete'); await testSubjects.existOrFail('transformActionDiscover'); await testSubjects.existOrFail('transformActionEdit'); + await testSubjects.existOrFail('transformActionReset'); if (isTransformRunning) { await testSubjects.missingOrFail('transformActionStart'); From f608ad7ebac2d9e95c73f676c0b640ab80c0e817 Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Wed, 26 Jan 2022 13:50:47 +0100 Subject: [PATCH 3/7] [ML] Fix translations. --- .../components/action_reset/reset_action_modal.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/action_reset/reset_action_modal.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_reset/reset_action_modal.tsx index efe5a0c0b09fa4..001d2dcad5ee63 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/action_reset/reset_action_modal.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_reset/reset_action_modal.tsx @@ -57,7 +57,7 @@ export const ResetActionModal: FC = ({ count={1} action={i18n.translate( 'xpack.transform.transformList.resetManagedTransformDescription', - { defaultMessage: 'deleting' } + { defaultMessage: 'resetting' } )} /> From 894b11700b2b7ced0b9494ac7b15e0b687872622 Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Wed, 26 Jan 2022 14:28:42 +0100 Subject: [PATCH 4/7] [ML] Extend functional tests. --- .../test/functional/apps/transform/index.ts | 1 + .../functional/apps/transform/resetting.ts | 143 ++++++++++++++++++ .../services/transform/transform_table.ts | 18 ++- 3 files changed, 161 insertions(+), 1 deletion(-) create mode 100644 x-pack/test/functional/apps/transform/resetting.ts diff --git a/x-pack/test/functional/apps/transform/index.ts b/x-pack/test/functional/apps/transform/index.ts index cb90271bf885fd..b716f5ecdc1b71 100644 --- a/x-pack/test/functional/apps/transform/index.ts +++ b/x-pack/test/functional/apps/transform/index.ts @@ -44,6 +44,7 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./editing')); loadTestFile(require.resolve('./feature_controls')); loadTestFile(require.resolve('./deleting')); + loadTestFile(require.resolve('./resetting')); loadTestFile(require.resolve('./starting')); }); } diff --git a/x-pack/test/functional/apps/transform/resetting.ts b/x-pack/test/functional/apps/transform/resetting.ts new file mode 100644 index 00000000000000..ea32c401f35517 --- /dev/null +++ b/x-pack/test/functional/apps/transform/resetting.ts @@ -0,0 +1,143 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { TRANSFORM_STATE } from '../../../../plugins/transform/common/constants'; + +import { FtrProviderContext } from '../../ftr_provider_context'; +import { getLatestTransformConfig, getPivotTransformConfig } from './index'; + +export default function ({ getService }: FtrProviderContext) { + const esArchiver = getService('esArchiver'); + const transform = getService('transform'); + + describe('resetting', function () { + const PREFIX = 'resetting'; + + const testDataList = [ + { + suiteTitle: 'batch transform with pivot configuration', + originalConfig: getPivotTransformConfig(PREFIX, false), + expected: { + messageText: 'reset transform.', + row: { + status: TRANSFORM_STATE.STOPPED, + type: 'pivot', + mode: 'batch', + progress: 100, + }, + }, + }, + { + suiteTitle: 'continuous transform with pivot configuration', + originalConfig: getPivotTransformConfig(PREFIX, true), + expected: { + messageText: 'reset transform.', + row: { + status: TRANSFORM_STATE.STOPPED, + type: 'pivot', + mode: 'continuous', + progress: undefined, + }, + }, + }, + { + suiteTitle: 'batch transform with latest configuration', + originalConfig: getLatestTransformConfig(PREFIX), + transformDescription: 'updated description', + transformDocsPerSecond: '1000', + transformFrequency: '10m', + expected: { + messageText: 'reset transform.', + row: { + status: TRANSFORM_STATE.STOPPED, + type: 'latest', + mode: 'batch', + progress: 100, + }, + }, + }, + ]; + + before(async () => { + await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/ml/ecommerce'); + await transform.testResources.createIndexPatternIfNeeded('ft_ecommerce', 'order_date'); + + for (const testData of testDataList) { + await transform.api.createAndRunTransform( + testData.originalConfig.id, + testData.originalConfig + ); + } + + await transform.testResources.setKibanaTimeZoneToUTC(); + await transform.securityUI.loginAsTransformPowerUser(); + }); + + after(async () => { + for (const testData of testDataList) { + await transform.testResources.deleteIndexPatternByTitle(testData.originalConfig.dest.index); + await transform.api.deleteIndices(testData.originalConfig.dest.index); + } + await transform.api.cleanTransformIndices(); + await transform.testResources.deleteIndexPatternByTitle('ft_ecommerce'); + }); + + for (const testData of testDataList) { + describe(`${testData.suiteTitle}`, function () { + it('reset transform', async () => { + await transform.testExecution.logTestStep('should load the home page'); + await transform.navigation.navigateTo(); + await transform.management.assertTransformListPageExists(); + + await transform.testExecution.logTestStep('should display the transforms table'); + await transform.management.assertTransformsTableExists(); + + if (testData.expected.row.mode === 'continuous') { + await transform.testExecution.logTestStep('should have the reset action disabled'); + await transform.table.assertTransformRowActionEnabled( + testData.originalConfig.id, + 'Reset', + false + ); + + await transform.testExecution.logTestStep('should stop the transform'); + await transform.table.clickTransformRowAction(testData.originalConfig.id, 'Stop'); + } + + await transform.testExecution.logTestStep('should display the stopped transform'); + await transform.table.assertTransformRowFields(testData.originalConfig.id, { + id: testData.originalConfig.id, + description: testData.originalConfig.description, + type: testData.expected.row.type, + status: testData.expected.row.status, + mode: testData.expected.row.mode, + progress: testData.expected.row.progress, + }); + + await transform.testExecution.logTestStep('should show the reset modal'); + await transform.table.assertTransformRowActionEnabled( + testData.originalConfig.id, + 'Reset', + true + ); + await transform.table.clickTransformRowAction(testData.originalConfig.id, 'Reset'); + await transform.table.assertTransformResetModalExists(); + + await transform.testExecution.logTestStep('should reset the transform'); + await transform.table.confirmResetTransform(); + + await transform.testExecution.logTestStep( + 'should display the messages tab and include a reset message' + ); + await transform.table.refreshTransformList(); + await transform.table.filterWithSearchString(testData.originalConfig.id, 1); + await transform.table.assertTransformExpandedRowMessages(testData.expected.messageText); + }); + }); + } + }); +} diff --git a/x-pack/test/functional/services/transform/transform_table.ts b/x-pack/test/functional/services/transform/transform_table.ts index 6240ce1791464e..67b2c374384059 100644 --- a/x-pack/test/functional/services/transform/transform_table.ts +++ b/x-pack/test/functional/services/transform/transform_table.ts @@ -9,7 +9,7 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../ftr_provider_context'; -type TransformRowActionName = 'Clone' | 'Delete' | 'Edit' | 'Start' | 'Stop' | 'Discover'; +type TransformRowActionName = 'Clone' | 'Delete' | 'Discover' | 'Edit' | 'Reset' | 'Start' | 'Stop'; export function TransformTableProvider({ getService }: FtrProviderContext) { const find = getService('find'); @@ -364,6 +364,14 @@ export function TransformTableProvider({ getService }: FtrProviderContext) { await testSubjects.missingOrFail('transformDeleteModal', { timeout: 60 * 1000 }); } + public async assertTransformResetModalExists() { + await testSubjects.existOrFail('transformResetModal', { timeout: 60 * 1000 }); + } + + public async assertTransformResetModalNotExists() { + await testSubjects.missingOrFail('transformResetModal', { timeout: 60 * 1000 }); + } + public async assertTransformStartModalExists() { await testSubjects.existOrFail('transformStartModal', { timeout: 60 * 1000 }); } @@ -380,6 +388,14 @@ export function TransformTableProvider({ getService }: FtrProviderContext) { }); } + public async confirmResetTransform() { + await retry.tryForTime(30 * 1000, async () => { + await this.assertTransformResetModalExists(); + await testSubjects.click('transformResetModal > confirmModalConfirmButton'); + await this.assertTransformResetModalNotExists(); + }); + } + public async assertTransformRowNotExists(transformId: string) { await retry.tryForTime(30 * 1000, async () => { // If after deletion, and there's no transform left From de12f96e283c65bbf9e5187bc1e3f8b78da05f53 Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Wed, 26 Jan 2022 14:37:15 +0100 Subject: [PATCH 5/7] [ML] Fix action name. --- x-pack/plugins/transform/server/routes/api/transforms.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/transform/server/routes/api/transforms.ts b/x-pack/plugins/transform/server/routes/api/transforms.ts index 1f2cdb5b3b3dfa..902fbb1d60bcad 100644 --- a/x-pack/plugins/transform/server/routes/api/transforms.ts +++ b/x-pack/plugins/transform/server/routes/api/transforms.ts @@ -67,9 +67,10 @@ import { isKeywordDuplicate } from '../../../common/utils/field_utils'; import { transformHealthServiceProvider } from '../../lib/alerting/transform_health_rule_type/transform_health_service'; enum TRANSFORM_ACTIONS { + DELETE = 'delete', + RESET = 'reset', STOP = 'stop', START = 'start', - DELETE = 'delete', } export function registerTransformsRoutes(routeDependencies: RouteDependencies) { @@ -627,7 +628,7 @@ async function resetTransforms( results, id: transformInfo.id, items: transformsInfo, - action: TRANSFORM_ACTIONS.DELETE, + action: TRANSFORM_ACTIONS.RESET, }); } results[transformId] = { transformReset: { success: false, error: e.meta.body.error } }; From 044b4da836f70000ec459b454e81ed6999957529 Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Thu, 27 Jan 2022 10:47:54 +0100 Subject: [PATCH 6/7] [ML] Fix comment. --- .../plugins/transform/public/app/hooks/use_reset_transform.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/transform/public/app/hooks/use_reset_transform.tsx b/x-pack/plugins/transform/public/app/hooks/use_reset_transform.tsx index 5c76276b346004..1687bd751ac19e 100644 --- a/x-pack/plugins/transform/public/app/hooks/use_reset_transform.tsx +++ b/x-pack/plugins/transform/public/app/hooks/use_reset_transform.tsx @@ -56,7 +56,7 @@ export const useResetTransforms = () => { if (results.hasOwnProperty(transformId)) { const status = results[transformId]; - // if we are only deleting one transform, show the success toast messages + // if we are only resetting one transform, show the success toast messages if (!isBulk && status.transformReset) { if (status.transformReset?.success) { toastNotifications.addSuccess( From ab14042dd362ce0cab87ad2a7e2061c43e2cc7cf Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Mon, 31 Jan 2022 16:25:41 +0100 Subject: [PATCH 7/7] [ML] Adds reset transform API tests. --- .../action_reset/reset_action_name.tsx | 2 +- .../transform/server/routes/api/transforms.ts | 6 +- .../api_integration/apis/transform/index.ts | 1 + .../apis/transform/reset_transforms.ts | 254 ++++++++++++++++++ .../test/functional/apps/transform/cloning.ts | 1 - 5 files changed, 259 insertions(+), 5 deletions(-) create mode 100644 x-pack/test/api_integration/apis/transform/reset_transforms.ts diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/action_reset/reset_action_name.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_reset/reset_action_name.tsx index 8180ddaf26045d..c6ea0f5f7270d6 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/action_reset/reset_action_name.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_reset/reset_action_name.tsx @@ -41,7 +41,7 @@ export const ResetActionName: FC = ({ const bulkResetButtonDisabledText = i18n.translate( 'xpack.transform.transformList.resetBulkActionDisabledToolTipContent', { - defaultMessage: 'One or more selected transforms must be stopped in order to be reseted.', + defaultMessage: 'One or more selected transforms must be stopped to be reset.', } ); const resetButtonDisabledText = i18n.translate( diff --git a/x-pack/plugins/transform/server/routes/api/transforms.ts b/x-pack/plugins/transform/server/routes/api/transforms.ts index 5851863303e6eb..e88f71f0e7f240 100644 --- a/x-pack/plugins/transform/server/routes/api/transforms.ts +++ b/x-pack/plugins/transform/server/routes/api/transforms.ts @@ -615,9 +615,9 @@ async function resetTransforms( transform_id: transformId, }); transformReset.success = true; - } catch (deleteTransformJobError) { - transformReset.error = deleteTransformJobError.meta.body.error; - if (deleteTransformJobError.statusCode === 403) { + } catch (resetTransformJobError) { + transformReset.error = resetTransformJobError.meta.body.error; + if (resetTransformJobError.statusCode === 403) { return response.forbidden(); } } diff --git a/x-pack/test/api_integration/apis/transform/index.ts b/x-pack/test/api_integration/apis/transform/index.ts index 351008f3f735ec..153bdcebc692ba 100644 --- a/x-pack/test/api_integration/apis/transform/index.ts +++ b/x-pack/test/api_integration/apis/transform/index.ts @@ -29,6 +29,7 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { }); loadTestFile(require.resolve('./delete_transforms')); + loadTestFile(require.resolve('./reset_transforms')); loadTestFile(require.resolve('./start_transforms')); loadTestFile(require.resolve('./stop_transforms')); loadTestFile(require.resolve('./transforms')); diff --git a/x-pack/test/api_integration/apis/transform/reset_transforms.ts b/x-pack/test/api_integration/apis/transform/reset_transforms.ts new file mode 100644 index 00000000000000..16d92ff133400d --- /dev/null +++ b/x-pack/test/api_integration/apis/transform/reset_transforms.ts @@ -0,0 +1,254 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; + +import { ResetTransformsRequestSchema } from '../../../../plugins/transform/common/api_schemas/reset_transforms'; +import { TRANSFORM_STATE } from '../../../../plugins/transform/common/constants'; + +import { COMMON_REQUEST_HEADERS } from '../../../functional/services/ml/common_api'; +import { USER } from '../../../functional/services/transform/security_common'; + +import { FtrProviderContext } from '../../ftr_provider_context'; + +import { asyncForEach, generateTransformConfig } from './common'; + +export default ({ getService }: FtrProviderContext) => { + const esArchiver = getService('esArchiver'); + const supertest = getService('supertestWithoutAuth'); + const transform = getService('transform'); + + const expected = { + complete: { + state: 'stopped', + pagesProcessed: 2, + lastCheckpoint: 1, + }, + reset: { + state: 'stopped', + pagesProcessed: 0, + lastCheckpoint: 0, + }, + }; + + describe('/api/transform/reset_transforms', function () { + before(async () => { + await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/ml/farequote'); + await transform.testResources.setKibanaTimeZoneToUTC(); + }); + + after(async () => { + await transform.api.cleanTransformIndices(); + }); + + describe('single transform reset', function () { + const transformId = 'transform-test-reset'; + + beforeEach(async () => { + await transform.api.createAndRunTransform( + transformId, + generateTransformConfig(transformId) + ); + await transform.api.waitForBatchTransformToComplete(transformId); + }); + + afterEach(async () => { + await transform.api.cleanTransformIndices(); + }); + + it('should reset transform by transformId', async () => { + // Check that batch transform finished running and assert stats. + const completeStats = await transform.api.getTransformStats(transformId); + expect(completeStats.state).to.eql(expected.complete.state); + expect(completeStats.stats.pages_processed).to.eql(expected.complete.pagesProcessed); + expect(completeStats.checkpointing.last.checkpoint).to.eql( + expected.complete.lastCheckpoint + ); + + const reqBody: ResetTransformsRequestSchema = { + transformsInfo: [{ id: transformId, state: TRANSFORM_STATE.STOPPED }], + }; + const { body } = await supertest + .post(`/api/transform/reset_transforms`) + .auth( + USER.TRANSFORM_POWERUSER, + transform.securityCommon.getPasswordForUser(USER.TRANSFORM_POWERUSER) + ) + .set(COMMON_REQUEST_HEADERS) + .send(reqBody) + .expect(200); + + // Check that transform was reset and assert stats. + expect(body[transformId].transformReset.success).to.eql(true); + const resetStats = await transform.api.getTransformStats(transformId); + expect(resetStats.state).to.eql(expected.reset.state); + expect(resetStats.stats.pages_processed).to.eql(expected.reset.pagesProcessed); + expect(resetStats.checkpointing.last.checkpoint).to.eql(expected.reset.lastCheckpoint); + }); + + it('should return 403 for unauthorized user', async () => { + // Check that batch transform finished running and assert stats. + const completeStats = await transform.api.getTransformStats(transformId); + expect(completeStats.state).to.eql(expected.complete.state); + expect(completeStats.stats.pages_processed).to.eql(expected.complete.pagesProcessed); + expect(completeStats.checkpointing.last.checkpoint).to.eql( + expected.complete.lastCheckpoint + ); + + const reqBody: ResetTransformsRequestSchema = { + transformsInfo: [{ id: transformId, state: TRANSFORM_STATE.STOPPED }], + }; + await supertest + .post(`/api/transform/reset_transforms`) + .auth( + USER.TRANSFORM_VIEWER, + transform.securityCommon.getPasswordForUser(USER.TRANSFORM_VIEWER) + ) + .set(COMMON_REQUEST_HEADERS) + .send(reqBody) + .expect(403); + + await transform.api.waitForTransformToExist(transformId); + // Check that transform was not reset by asserting unchanged stats. + const resetStats = await transform.api.getTransformStats(transformId); + expect(resetStats.state).to.eql(expected.complete.state); + expect(resetStats.stats.pages_processed).to.eql(expected.complete.pagesProcessed); + expect(resetStats.checkpointing.last.checkpoint).to.eql(expected.complete.lastCheckpoint); + }); + }); + + describe('single transform reset with invalid transformId', function () { + it('should return 200 with error in response if invalid transformId', async () => { + const reqBody: ResetTransformsRequestSchema = { + transformsInfo: [{ id: 'invalid_transform_id', state: TRANSFORM_STATE.STOPPED }], + }; + const { body } = await supertest + .post(`/api/transform/reset_transforms`) + .auth( + USER.TRANSFORM_POWERUSER, + transform.securityCommon.getPasswordForUser(USER.TRANSFORM_POWERUSER) + ) + .set(COMMON_REQUEST_HEADERS) + .send(reqBody) + .expect(200); + + expect(body.invalid_transform_id.transformReset.success).to.eql(false); + expect(body.invalid_transform_id.transformReset).to.have.property('error'); + }); + }); + + describe('bulk reset', function () { + const reqBody: ResetTransformsRequestSchema = { + transformsInfo: [ + { id: 'bulk_reset_test_1', state: TRANSFORM_STATE.STOPPED }, + { id: 'bulk_reset_test_2', state: TRANSFORM_STATE.STOPPED }, + ], + }; + + beforeEach(async () => { + await asyncForEach(reqBody.transformsInfo, async ({ id }: { id: string }, idx: number) => { + await transform.api.createAndRunTransform(id, generateTransformConfig(id)); + await transform.api.waitForBatchTransformToComplete(id); + }); + }); + + afterEach(async () => { + await transform.api.cleanTransformIndices(); + }); + + it('should reset multiple transforms by transformIds', async () => { + await asyncForEach( + reqBody.transformsInfo, + async ({ id: transformId }: { id: string }, idx: number) => { + // Check that batch transform finished running and assert stats. + const completeStats = await transform.api.getTransformStats(transformId); + expect(completeStats.state).to.eql(expected.complete.state); + expect(completeStats.stats.pages_processed).to.eql(expected.complete.pagesProcessed); + expect(completeStats.checkpointing.last.checkpoint).to.eql( + expected.complete.lastCheckpoint + ); + } + ); + + const { body } = await supertest + .post(`/api/transform/reset_transforms`) + .auth( + USER.TRANSFORM_POWERUSER, + transform.securityCommon.getPasswordForUser(USER.TRANSFORM_POWERUSER) + ) + .set(COMMON_REQUEST_HEADERS) + .send(reqBody) + .expect(200); + + await asyncForEach( + reqBody.transformsInfo, + async ({ id: transformId }: { id: string }, idx: number) => { + await transform.api.waitForTransformToExist(transformId); + // Check that transform was reset and assert stats. + expect(body[transformId].transformReset.success).to.eql(true); + const resetStats = await transform.api.getTransformStats(transformId); + expect(resetStats.state).to.eql(expected.reset.state); + expect(resetStats.stats.pages_processed).to.eql(expected.reset.pagesProcessed); + expect(resetStats.checkpointing.last.checkpoint).to.eql(expected.reset.lastCheckpoint); + } + ); + }); + + it('should reset multiple transforms by transformIds, even if one of the transformIds is invalid', async () => { + await asyncForEach( + reqBody.transformsInfo, + async ({ id: transformId }: { id: string }, idx: number) => { + // Check that batch transform finished running and assert stats. + const completeStats = await transform.api.getTransformStats(transformId); + expect(completeStats.state).to.eql(expected.complete.state); + expect(completeStats.stats.pages_processed).to.eql(expected.complete.pagesProcessed); + expect(completeStats.checkpointing.last.checkpoint).to.eql( + expected.complete.lastCheckpoint + ); + } + ); + + const invalidTransformId = 'invalid_transform_id'; + const { body } = await supertest + .post(`/api/transform/reset_transforms`) + .auth( + USER.TRANSFORM_POWERUSER, + transform.securityCommon.getPasswordForUser(USER.TRANSFORM_POWERUSER) + ) + .set(COMMON_REQUEST_HEADERS) + .send({ + ...reqBody, + transformsInfo: [ + ...reqBody.transformsInfo, + { id: invalidTransformId, state: TRANSFORM_STATE.STOPPED }, + ], + }) + .expect(200); + + await asyncForEach( + reqBody.transformsInfo, + async ({ id: transformId }: { id: string }, idx: number) => { + if (transformId !== invalidTransformId) { + await transform.api.waitForTransformToExist(transformId); + // Check that transform was reset and assert stats. + expect(body[transformId].transformReset.success).to.eql(true); + const resetStats = await transform.api.getTransformStats(transformId); + expect(resetStats.state).to.eql(expected.reset.state); + expect(resetStats.stats.pages_processed).to.eql(expected.reset.pagesProcessed); + expect(resetStats.checkpointing.last.checkpoint).to.eql( + expected.reset.lastCheckpoint + ); + } else { + expect(body[transformId].transformReset.success).to.eql(false); + expect(body[transformId].transformReset).to.have.property('error'); + } + } + ); + }); + }); + }); +}; diff --git a/x-pack/test/functional/apps/transform/cloning.ts b/x-pack/test/functional/apps/transform/cloning.ts index ec6d3977841cec..382f1b5ba75ab1 100644 --- a/x-pack/test/functional/apps/transform/cloning.ts +++ b/x-pack/test/functional/apps/transform/cloning.ts @@ -101,7 +101,6 @@ export default function ({ getService }: FtrProviderContext) { transformConfigWithRuntimeMapping.id, transformConfigWithRuntimeMapping ); - await transform.api.createAndRunTransform( transformConfigWithLatest.id, transformConfigWithLatest