[ML] Transforms: Adds _schedule_now action to transform list. (#153545)

- Adds `_schedule_now` action to transform list.
- Fixes bulk actions to be correctly disabled when not available.
This commit is contained in:
Walter Rafelsberger 2023-03-26 12:18:01 +02:00 committed by GitHub
parent 0ed1f63799
commit 0cc560fbb6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
24 changed files with 604 additions and 16 deletions

View file

@ -0,0 +1,14 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { TypeOf } from '@kbn/config-schema';
import { transformIdsSchema, CommonResponseStatusSchema } from './common';
export const scheduleNowTransformsRequestSchema = transformIdsSchema;
export type ScheduleNowTransformsRequestSchema = TypeOf<typeof scheduleNowTransformsRequestSchema>;
export type ScheduleNowTransformsResponseSchema = CommonResponseStatusSchema;

View file

@ -21,6 +21,7 @@ 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 { ScheduleNowTransformsResponseSchema } from './schedule_now_transforms';
import type {
GetTransformNodesResponseSchema,
GetTransformsResponseSchema,
@ -144,3 +145,9 @@ export const isStopTransformsResponseSchema = (
): arg is StopTransformsResponseSchema => {
return isGenericSuccessResponseSchema(arg);
};
export const isScheduleNowTransformsResponseSchema = (
arg: unknown
): arg is ScheduleNowTransformsResponseSchema => {
return isGenericSuccessResponseSchema(arg);
};

View file

@ -58,6 +58,7 @@ export const APP_CLUSTER_PRIVILEGES = [
'cluster:admin/transform/preview',
'cluster:admin/transform/put',
'cluster:admin/transform/reset',
'cluster:admin/transform/schedule_now',
'cluster:admin/transform/start',
'cluster:admin/transform/start_task',
'cluster:admin/transform/stop',
@ -84,7 +85,7 @@ export const APP_CREATE_TRANSFORM_CLUSTER_PRIVILEGES = [
export const APP_INDEX_PRIVILEGES = ['monitor'];
// reflects https://github.com/elastic/elasticsearch/blob/master/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/transform/transforms/TransformStats.java#L250
// reflects https://github.com/elastic/elasticsearch/blob/master/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/transform/transforms/TransformStats.java#L214
export const TRANSFORM_STATE = {
ABORTING: 'aborting',
FAILED: 'failed',

View file

@ -9,6 +9,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 { useScheduleNowTransforms } from './use_schedule_now_transform';
export { useStartTransforms } from './use_start_transform';
export { useStopTransforms } from './use_stop_transform';
export { useRequest } from './use_request';

View file

@ -34,6 +34,10 @@ import type {
StopTransformsRequestSchema,
StopTransformsResponseSchema,
} from '../../../common/api_schemas/stop_transforms';
import type {
ScheduleNowTransformsRequestSchema,
ScheduleNowTransformsResponseSchema,
} from '../../../common/api_schemas/schedule_now_transforms';
import type {
GetTransformNodesResponseSchema,
GetTransformsResponseSchema,
@ -195,6 +199,17 @@ export const useApi = () => {
return e;
}
},
async scheduleNowTransforms(
transformsInfo: ScheduleNowTransformsRequestSchema
): Promise<ScheduleNowTransformsResponseSchema | IHttpFetchError> {
try {
return await http.post(`${API_BASE_PATH}schedule_now_transforms`, {
body: JSON.stringify(transformsInfo),
});
} catch (e) {
return e;
}
},
async getTransformAuditMessages(
transformId: TransformId,
sortField: string,

View file

@ -0,0 +1,84 @@
/*
* 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 '@kbn/kibana-react-plugin/public';
import type { ScheduleNowTransformsRequestSchema } from '../../../common/api_schemas/schedule_now_transforms';
import { isScheduleNowTransformsResponseSchema } from '../../../common/api_schemas/type_guards';
import { getErrorMessage } from '../../../common/utils/errors';
import { useAppDependencies, useToastNotifications } from '../app_dependencies';
import { refreshTransformList$, REFRESH_TRANSFORM_LIST_STATE } from '../common';
import { ToastNotificationText } from '../components';
import { useApi } from './use_api';
export const useScheduleNowTransforms = () => {
const { overlays, theme } = useAppDependencies();
const toastNotifications = useToastNotifications();
const api = useApi();
return async (transformsInfo: ScheduleNowTransformsRequestSchema) => {
const results = await api.scheduleNowTransforms(transformsInfo);
if (!isScheduleNowTransformsResponseSchema(results)) {
toastNotifications.addDanger({
title: i18n.translate(
'xpack.transform.stepCreateForm.scheduleNowTransformResponseSchemaErrorMessage',
{
defaultMessage:
'An error occurred calling the request to schedule the transform to process data instantly.',
}
),
text: toMountPoint(
<ToastNotificationText
overlays={overlays}
theme={theme}
text={getErrorMessage(results)}
/>,
{ theme$: theme.theme$ }
),
});
return;
}
for (const transformId in results) {
// hasOwnProperty check to ensure only properties on object itself, and not its prototypes
if (results.hasOwnProperty(transformId)) {
const result = results[transformId];
if (result.success === true) {
toastNotifications.addSuccess(
i18n.translate('xpack.transform.transformList.scheduleNowTransformSuccessMessage', {
defaultMessage:
'Request to schedule transform {transformId} to process data instantly acknowledged.',
values: { transformId },
})
);
} else {
toastNotifications.addError(new Error(JSON.stringify(result.error!.caused_by, null, 2)), {
title: i18n.translate(
'xpack.transform.transformList.scheduleNowTransformErrorMessage',
{
defaultMessage:
'An error occurred scheduling transform {transformId} to process data instantly.',
values: { transformId },
}
),
toastMessage: result.error!.reason,
});
}
}
}
refreshTransformList$.next(REFRESH_TRANSFORM_LIST_STATE.REFRESH);
};
};

View file

@ -25,6 +25,7 @@ const initialCapabilities: Capabilities = {
canDeleteTransform: false,
canPreviewTransform: false,
canCreateTransform: false,
canScheduleNowTransform: false,
canStartStopTransform: false,
canCreateTransformAlerts: false,
canUseTransformAlerts: false,
@ -94,6 +95,11 @@ export const AuthorizationProvider = ({ privilegesEndpoint, children }: Props) =
value.capabilities.canUseTransformAlerts = value.capabilities.canGetTransform;
value.capabilities.canScheduleNowTransform = hasPrivilege([
'cluster',
'cluster:admin/transform/schedule_now',
]);
return (
<AuthorizationContext.Provider value={{ ...value }}>{children}</AuthorizationContext.Provider>
);

View file

@ -15,6 +15,7 @@ export interface Capabilities {
canDeleteTransform: boolean;
canPreviewTransform: boolean;
canCreateTransform: boolean;
canScheduleNowTransform: boolean;
canStartStopTransform: boolean;
canCreateTransformAlerts: boolean;
canUseTransformAlerts: boolean;
@ -78,6 +79,15 @@ export function createCapabilityFailureMessage(
}
);
break;
case 'canScheduleNowTransform':
message = i18n.translate(
'xpack.transform.capability.noPermission.scheduleNowTransformTooltip',
{
defaultMessage:
'You do not have permission to schedule transforms to process data instantly.',
}
);
break;
case 'canStartStopTransform':
message = i18n.translate(
'xpack.transform.capability.noPermission.startOrStopTransformTooltip',

View file

@ -0,0 +1,9 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
export { useScheduleNowAction } from './use_schedule_now_action';
export { isScheduleNowActionDisabled, ScheduleNowActionName } from './schedule_now_action_name';

View file

@ -0,0 +1,102 @@
/*
* 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, useContext } from 'react';
import { i18n } from '@kbn/i18n';
import { EuiToolTip } from '@elastic/eui';
import {
createCapabilityFailureMessage,
AuthorizationContext,
} from '../../../../lib/authorization';
import { TransformListRow, isCompletedBatchTransform } from '../../../../common';
export const scheduleNowActionNameText = i18n.translate(
'xpack.transform.transformList.scheduleNowActionNameText',
{
defaultMessage: 'Schedule now',
}
);
export const isScheduleNowActionDisabled = (
items: TransformListRow[],
canScheduleNowTransform: boolean,
transformNodes: number
) => {
// Disable schedule-now for batch transforms which have completed.
const completedBatchTransform = items.some((i: TransformListRow) => isCompletedBatchTransform(i));
return (
!canScheduleNowTransform ||
completedBatchTransform ||
items.length === 0 ||
transformNodes === 0
);
};
export interface ScheduleNowActionNameProps {
items: TransformListRow[];
forceDisable?: boolean;
transformNodes: number;
}
export const ScheduleNowActionName: FC<ScheduleNowActionNameProps> = ({
items,
forceDisable,
transformNodes,
}) => {
const { canScheduleNowTransform } = useContext(AuthorizationContext).capabilities;
const isBulkAction = items.length > 1;
// Disable schedule-now for batch transforms which have completed.
const completedBatchTransform = items.some((i: TransformListRow) => isCompletedBatchTransform(i));
let completedBatchTransformMessage;
if (isBulkAction === true) {
completedBatchTransformMessage = i18n.translate(
'xpack.transform.transformList.cannotScheduleNowCompleteBatchTransformBulkActionToolTip',
{
defaultMessage:
'One or more transforms are completed batch transforms and cannot be scheduled to process data instantly.',
}
);
} else {
completedBatchTransformMessage = i18n.translate(
'xpack.transform.transformList.cannotScheduleNowCompleteBatchTransformToolTip',
{
defaultMessage:
'{transformId} is a completed batch transform and cannot be scheduled to process data instantly.',
values: { transformId: items[0] && items[0].config.id },
}
);
}
const actionIsDisabled = isScheduleNowActionDisabled(
items,
canScheduleNowTransform,
transformNodes
);
let content: string = i18n.translate('xpack.transform.transformList.scheduleNowToolTip', {
defaultMessage:
'Schedule the transform to instantly process data without waiting for the configured interval between checks for changes in the source indices.',
});
if (actionIsDisabled && items.length > 0) {
if (!canScheduleNowTransform) {
content = createCapabilityFailureMessage('canScheduleNowTransform');
} else if (completedBatchTransform) {
content = completedBatchTransformMessage;
}
}
return (
<EuiToolTip position="top" content={content}>
<>{scheduleNowActionNameText}</>
</EuiToolTip>
);
};

View file

@ -0,0 +1,53 @@
/*
* 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 } from 'react';
import { TRANSFORM_STATE } from '../../../../../../common/constants';
import { AuthorizationContext } from '../../../../lib/authorization';
import { TransformListAction, TransformListRow } from '../../../../common';
import { useScheduleNowTransforms } from '../../../../hooks';
import {
isScheduleNowActionDisabled,
scheduleNowActionNameText,
ScheduleNowActionName,
} from './schedule_now_action_name';
export type ScheduleNowAction = ReturnType<typeof useScheduleNowAction>;
export const useScheduleNowAction = (forceDisable: boolean, transformNodes: number) => {
const { canScheduleNowTransform } = useContext(AuthorizationContext).capabilities;
const scheduleNowTransforms = useScheduleNowTransforms();
const action: TransformListAction = useMemo(
() => ({
name: (item: TransformListRow) => (
<ScheduleNowActionName
items={[item]}
forceDisable={forceDisable}
transformNodes={transformNodes}
/>
),
available: (item: TransformListRow) => item.stats.state === TRANSFORM_STATE.STARTED,
enabled: (item: TransformListRow) =>
!isScheduleNowActionDisabled([item], canScheduleNowTransform, transformNodes),
description: scheduleNowActionNameText,
icon: 'play',
type: 'icon',
onClick: (item: TransformListRow) => scheduleNowTransforms([{ id: item.id }]),
'data-test-subj': 'transformActionScheduleNow',
}),
[canScheduleNowTransform, forceDisable, scheduleNowTransforms, transformNodes]
);
return {
action,
scheduleNowTransforms,
};
};

View file

@ -7,4 +7,4 @@
export { useStartAction } from './use_start_action';
export { StartActionModal } from './start_action_modal';
export { StartActionName } from './start_action_name';
export { isStartActionDisabled, StartActionName } from './start_action_name';

View file

@ -76,7 +76,7 @@ export const StartActionName: FC<StartActionNameProps> = ({
}
);
completedBatchTransformMessage = i18n.translate(
'xpack.transform.transformList.completeBatchTransformBulkActionToolTip',
'xpack.transform.transformList.cannotRestartCompleteBatchTransformBulkActionToolTip',
{
defaultMessage:
'One or more transforms are completed batch transforms and cannot be restarted.',
@ -91,7 +91,7 @@ export const StartActionName: FC<StartActionNameProps> = ({
}
);
completedBatchTransformMessage = i18n.translate(
'xpack.transform.transformList.completeBatchTransformToolTip',
'xpack.transform.transformList.cannotRestartCompleteBatchTransformToolTip',
{
defaultMessage: '{transformId} is a completed batch transform and cannot be restarted.',
values: { transformId: items[0] && items[0].config.id },

View file

@ -6,4 +6,4 @@
*/
export { useStopAction } from './use_stop_action';
export { StopActionName } from './stop_action_name';
export { isStopActionDisabled, StopActionName } from './stop_action_name';

View file

@ -47,8 +47,18 @@ import {
ResetActionName,
ResetActionModal,
} from '../action_reset';
import { useStartAction, StartActionName, StartActionModal } from '../action_start';
import { StopActionName, useStopAction } from '../action_stop';
import {
isStartActionDisabled,
useStartAction,
StartActionName,
StartActionModal,
} from '../action_start';
import {
isScheduleNowActionDisabled,
useScheduleNowAction,
ScheduleNowActionName,
} from '../action_schedule_now';
import { isStopActionDisabled, StopActionName, useStopAction } from '../action_stop';
import { useColumns } from './use_columns';
import { ExpandedRow } from './expanded_row';
@ -100,6 +110,7 @@ export const TransformList: FC<TransformListProps> = ({
const bulkDeleteAction = useDeleteAction(false);
const bulkResetAction = useResetAction(false);
const bulkStopAction = useStopAction(false);
const bulkScheduleNowAction = useScheduleNowAction(false, transformNodes);
const { capabilities } = useContext(AuthorizationContext);
const disabled =
@ -171,15 +182,41 @@ export const TransformList: FC<TransformListProps> = ({
const bulkActionMenuItems = [
<div key="startAction" className="transform__BulkActionItem">
<EuiButtonEmpty onClick={() => bulkStartAction.openModal(transformSelection)}>
<EuiButtonEmpty
onClick={() => bulkStartAction.openModal(transformSelection)}
disabled={isStartActionDisabled(
transformSelection,
capabilities.canStartStopTransform,
transformNodes
)}
>
<StartActionName items={transformSelection} transformNodes={transformNodes} />
</EuiButtonEmpty>
</div>,
<div key="scheduleNowAction" className="transform__BulkActionItem">
<EuiButtonEmpty
onClick={() =>
bulkScheduleNowAction.scheduleNowTransforms(transformSelection.map((i) => ({ id: i.id })))
}
disabled={isScheduleNowActionDisabled(
transformSelection,
capabilities.canScheduleNowTransform,
transformNodes
)}
>
<ScheduleNowActionName items={transformSelection} transformNodes={transformNodes} />
</EuiButtonEmpty>
</div>,
<div key="stopAction" className="transform__BulkActionItem">
<EuiButtonEmpty
onClick={() => {
bulkStopAction.openModal(transformSelection);
}}
disabled={isStopActionDisabled(
transformSelection,
capabilities.canStartStopTransform,
false
)}
>
<StopActionName items={transformSelection} />
</EuiButtonEmpty>
@ -189,6 +226,7 @@ export const TransformList: FC<TransformListProps> = ({
onClick={() => {
bulkResetAction.openModal(transformSelection);
}}
disabled={isResetActionDisabled(transformSelection, false)}
>
<ResetActionName
canResetTransform={capabilities.canResetTransform}
@ -198,7 +236,10 @@ export const TransformList: FC<TransformListProps> = ({
</EuiButtonEmpty>
</div>,
<div key="deleteAction" className="transform__BulkActionItem">
<EuiButtonEmpty onClick={() => bulkDeleteAction.openModal(transformSelection)}>
<EuiButtonEmpty
onClick={() => bulkDeleteAction.openModal(transformSelection)}
disabled={isDeleteActionDisabled(transformSelection, false)}
>
<DeleteActionName
canDeleteTransform={capabilities.canDeleteTransform}
disabled={isDeleteActionDisabled(transformSelection, false)}

View file

@ -28,6 +28,7 @@ describe('Transform: Transform List Actions', () => {
expect(actions.map((a: any) => a['data-test-subj'])).toStrictEqual([
'transformActionDiscover',
'transformActionCreateAlertRule',
'transformActionScheduleNow',
'transformActionStart',
'transformActionStop',
'transformActionEdit',

View file

@ -17,6 +17,7 @@ import { useDiscoverAction } from '../action_discover';
import { EditTransformFlyout } from '../edit_transform_flyout';
import { useEditAction } from '../action_edit';
import { useResetAction, ResetActionModal } from '../action_reset';
import { useScheduleNowAction } from '../action_schedule_now';
import { useStartAction, StartActionModal } from '../action_start';
import { useStopAction } from '../action_stop';
import { useCreateAlertRuleAction } from '../action_create_alert';
@ -37,6 +38,7 @@ export const useActions = ({
const discoverAction = useDiscoverAction(forceDisable);
const editAction = useEditAction(forceDisable, transformNodes);
const resetAction = useResetAction(forceDisable);
const scheduleNowAction = useScheduleNowAction(forceDisable, transformNodes);
const startAction = useStartAction(forceDisable, transformNodes);
const stopAction = useStopAction(forceDisable);
const createAlertRuleAction = useCreateAlertRuleAction(forceDisable);
@ -55,6 +57,7 @@ export const useActions = ({
actions: [
discoverAction.action,
createAlertRuleAction.action,
scheduleNowAction.action,
startAction.action,
stopAction.action,
editAction.action,

View file

@ -42,6 +42,11 @@ import {
StopTransformsRequestSchema,
StopTransformsResponseSchema,
} from '../../../common/api_schemas/stop_transforms';
import {
scheduleNowTransformsRequestSchema,
ScheduleNowTransformsRequestSchema,
ScheduleNowTransformsResponseSchema,
} from '../../../common/api_schemas/schedule_now_transforms';
import {
postTransformsUpdateRequestSchema,
PostTransformsUpdateRequestSchema,
@ -68,6 +73,7 @@ import { transformHealthServiceProvider } from '../../lib/alerting/transform_hea
enum TRANSFORM_ACTIONS {
DELETE = 'delete',
RESET = 'reset',
SCHEDULE_NOW = 'schedule_now',
STOP = 'stop',
START = 'start',
}
@ -439,6 +445,27 @@ export function registerTransformsRoutes(routeDependencies: RouteDependencies) {
license.guardApiRoute<undefined, undefined, StopTransformsRequestSchema>(stopTransformsHandler)
);
/**
* @apiGroup Transforms
*
* @api {post} /api/transform/schedule_now_transforms Schedules transforms now
* @apiName PostScheduleNowTransforms
* @apiDescription Schedules transforms now
*
* @apiSchema (body) scheduleNowTransformsRequestSchema
*/
router.post<undefined, undefined, ScheduleNowTransformsRequestSchema>(
{
path: addBasePath('schedule_now_transforms'),
validate: {
body: scheduleNowTransformsRequestSchema,
},
},
license.guardApiRoute<undefined, undefined, ScheduleNowTransformsRequestSchema>(
scheduleNowTransformsHandler
)
);
/**
* @apiGroup Transforms
*
@ -786,3 +813,50 @@ async function stopTransforms(
}
return results;
}
const scheduleNowTransformsHandler: RequestHandler<
undefined,
undefined,
ScheduleNowTransformsRequestSchema
> = async (ctx, req, res) => {
const transformsInfo = req.body;
try {
const esClient = (await ctx.core).elasticsearch.client;
return res.ok({
body: await scheduleNowTransforms(transformsInfo, esClient.asCurrentUser),
});
} catch (e) {
return res.customError(wrapError(wrapEsError(e)));
}
};
async function scheduleNowTransforms(
transformsInfo: ScheduleNowTransformsRequestSchema,
esClient: ElasticsearchClient
) {
const results: ScheduleNowTransformsResponseSchema = {};
for (const transformInfo of transformsInfo) {
const transformId = transformInfo.id;
try {
await esClient.transport.request({
method: 'POST',
path: `_transform/${transformId}/_schedule_now`,
});
results[transformId] = { success: true };
} catch (e) {
if (isRequestTimeout(e)) {
return fillResultsWithTimeouts({
results,
id: transformId,
items: transformsInfo,
action: TRANSFORM_ACTIONS.SCHEDULE_NOW,
});
}
results[transformId] = { success: false, error: e.meta.body.error };
}
}
return results;
}

View file

@ -36139,7 +36139,7 @@
"xpack.transform.transformList.bulkResetTransformSuccessMessage": "Réinitialisation réussie de {count} {count, plural, one {transformation} other {transformations}}.",
"xpack.transform.transformList.bulkStartModalTitle": "Démarrer {count} {count, plural, one {transformation} other {transformations}} ?",
"xpack.transform.transformList.bulkStopModalTitle": "Arrêter {count} {count, plural, one {transformation} other {transformations}} ?",
"xpack.transform.transformList.completeBatchTransformToolTip": "{transformId} est une transformation par lots terminée et ne peut pas être redémarrée.",
"xpack.transform.transformList.cannotRestartCompleteBatchTransformToolTip": "{transformId} est une transformation par lots terminée et ne peut pas être redémarrée.",
"xpack.transform.transformList.deleteModalTitle": "Supprimer {transformId} ?",
"xpack.transform.transformList.deleteTransformErrorMessage": "Une erreur s'est produite lors de la suppression de la transformation {transformId}",
"xpack.transform.transformList.deleteTransformSuccessMessage": "La requête pour supprimer la transformation {transformId} a été reconnue.",
@ -36431,7 +36431,7 @@
"xpack.transform.transformHealth.yellowLabel": "Dégradé",
"xpack.transform.transformList.alertingRules.screenReaderDescription": "Cette colonne affiche une icône lorsqu'il existe des règles d'alerte associées à une transformation",
"xpack.transform.transformList.cloneActionNameText": "Cloner",
"xpack.transform.transformList.completeBatchTransformBulkActionToolTip": "Une ou plusieurs transformations sont des transformations par lots terminées et ne peuvent pas être redémarrées.",
"xpack.transform.transformList.cannotRestartCompleteBatchTransformBulkActionToolTip": "Une ou plusieurs transformations sont des transformations par lots terminées et ne peuvent pas être redémarrées.",
"xpack.transform.transformList.createAlertRuleNameText": "Créer une règle d'alerte",
"xpack.transform.transformList.createTransformButton": "Créer une transformation",
"xpack.transform.transformList.deleteActionDisabledToolTipContent": "Arrêtez la transformation pour pouvoir la supprimer.",

View file

@ -36118,7 +36118,7 @@
"xpack.transform.transformList.bulkResetTransformSuccessMessage": "{count}個の{count, plural, other {変換}}が正常にリセットされました。",
"xpack.transform.transformList.bulkStartModalTitle": "{count}個の{count, plural, other {変換}}を開始しますか?",
"xpack.transform.transformList.bulkStopModalTitle": "{count}個のを{count, plural, other {変換}}停止しますか?",
"xpack.transform.transformList.completeBatchTransformToolTip": "{transformId} は完了済みの一斉変換で、再度開始できません。",
"xpack.transform.transformList.cannotRestartCompleteBatchTransformToolTip": "{transformId} は完了済みの一斉変換で、再度開始できません。",
"xpack.transform.transformList.deleteModalTitle": "{transformId}を削除しますか?",
"xpack.transform.transformList.deleteTransformErrorMessage": "変換 {transformId} の削除中にエラーが発生しました",
"xpack.transform.transformList.deleteTransformSuccessMessage": "変換 {transformId} の削除リクエストが受け付けられました。",
@ -36410,7 +36410,7 @@
"xpack.transform.transformHealth.yellowLabel": "劣化",
"xpack.transform.transformList.alertingRules.screenReaderDescription": "アラートルールがが変換に関連付けられているときには、この列にアイコンが表示されます",
"xpack.transform.transformList.cloneActionNameText": "クローンを作成",
"xpack.transform.transformList.completeBatchTransformBulkActionToolTip": "1 つまたは複数の変換が完了済みの一斉変換で、再度開始できません。",
"xpack.transform.transformList.cannotRestartCompleteBatchTransformBulkActionToolTip": "1 つまたは複数の変換が完了済みの一斉変換で、再度開始できません。",
"xpack.transform.transformList.createAlertRuleNameText": "アラートルールを作成",
"xpack.transform.transformList.createTransformButton": "変換の作成",
"xpack.transform.transformList.deleteActionDisabledToolTipContent": "削除するにはデータフレームジョブを停止してください。",

View file

@ -36134,7 +36134,7 @@
"xpack.transform.transformList.bulkResetTransformSuccessMessage": "已成功重置 {count} 个{count, plural, other {转换}}。",
"xpack.transform.transformList.bulkStartModalTitle": "启动 {count} 个{count, plural, other {转换}}",
"xpack.transform.transformList.bulkStopModalTitle": "停止 {count} 个{count, plural, other {转换}}",
"xpack.transform.transformList.completeBatchTransformToolTip": "{transformId} 为已完成批量转换,无法重新启动。",
"xpack.transform.transformList.cannotRestartCompleteBatchTransformToolTip": "{transformId} 为已完成批量转换,无法重新启动。",
"xpack.transform.transformList.deleteModalTitle": "删除 {transformId}",
"xpack.transform.transformList.deleteTransformErrorMessage": "删除转换 {transformId} 时发生错误",
"xpack.transform.transformList.deleteTransformSuccessMessage": "删除转换 {transformId} 的请求已确认。",
@ -36426,7 +36426,7 @@
"xpack.transform.transformHealth.yellowLabel": "已降级",
"xpack.transform.transformList.alertingRules.screenReaderDescription": "存在与转换关联的告警规则时,此列显示图标",
"xpack.transform.transformList.cloneActionNameText": "克隆",
"xpack.transform.transformList.completeBatchTransformBulkActionToolTip": "一个或多个转换为已完成批量转换,无法重新启动。",
"xpack.transform.transformList.cannotRestartCompleteBatchTransformBulkActionToolTip": "一个或多个转换为已完成批量转换,无法重新启动。",
"xpack.transform.transformList.createAlertRuleNameText": "创建告警规则",
"xpack.transform.transformList.createTransformButton": "创建转换",
"xpack.transform.transformList.deleteActionDisabledToolTipContent": "停止数据帧作业,以便将其删除。",

View file

@ -17,7 +17,10 @@ export function generateDestIndex(transformId: string): string {
return `user-${transformId}`;
}
export function generateTransformConfig(transformId: string): PutTransformsRequestSchema {
export function generateTransformConfig(
transformId: string,
continuous = false
): PutTransformsRequestSchema {
const destinationIndex = generateDestIndex(transformId);
return {
@ -27,5 +30,6 @@ export function generateTransformConfig(transformId: string): PutTransformsReque
aggregations: { '@timestamp.value_count': { value_count: { field: '@timestamp' } } },
},
dest: { index: destinationIndex },
...(continuous ? { sync: { time: { field: '@timestamp', delay: '60s' } } } : {}),
};
}

View file

@ -32,6 +32,7 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) {
loadTestFile(require.resolve('./reset_transforms'));
loadTestFile(require.resolve('./start_transforms'));
loadTestFile(require.resolve('./stop_transforms'));
loadTestFile(require.resolve('./schedule_now_transforms'));
loadTestFile(require.resolve('./transforms'));
loadTestFile(require.resolve('./transforms_nodes'));
loadTestFile(require.resolve('./transforms_preview'));

View file

@ -0,0 +1,162 @@
/*
* 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 { ScheduleNowTransformsRequestSchema } from '@kbn/transform-plugin/common/api_schemas/schedule_now_transforms';
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, generateDestIndex, generateTransformConfig } from './common';
export default ({ getService }: FtrProviderContext) => {
const esArchiver = getService('esArchiver');
const supertest = getService('supertestWithoutAuth');
const transform = getService('transform');
async function createTransform(transformId: string) {
const config = generateTransformConfig(transformId, true);
await transform.api.createTransform(transformId, config);
}
describe('/api/transform/schedule_now_transforms', function () {
before(async () => {
await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/ml/farequote');
await transform.testResources.setKibanaTimeZoneToUTC();
});
describe('single transform _schedule_now', function () {
const transformId = 'transform-test-schedule-now';
const destinationIndex = generateDestIndex(transformId);
beforeEach(async () => {
await createTransform(transformId);
await transform.api.startTransform(transformId);
});
afterEach(async () => {
await transform.api.stopTransform(transformId);
await transform.api.cleanTransformIndices();
await transform.api.deleteIndices(destinationIndex);
});
it('should schedule the transform by transformId', async () => {
const reqBody: ScheduleNowTransformsRequestSchema = [{ id: transformId }];
const { body, status } = await supertest
.post(`/api/transform/schedule_now_transforms`)
.auth(
USER.TRANSFORM_POWERUSER,
transform.securityCommon.getPasswordForUser(USER.TRANSFORM_POWERUSER)
)
.set(COMMON_REQUEST_HEADERS)
.send(reqBody);
transform.api.assertResponseStatusCode(200, status, body);
expect(body[transformId].success).to.eql(true);
expect(typeof body[transformId].error).to.eql('undefined');
});
it('should return 200 with success:false for unauthorized user', async () => {
const reqBody: ScheduleNowTransformsRequestSchema = [{ id: transformId }];
const { body, status } = await supertest
.post(`/api/transform/schedule_now_transforms`)
.auth(
USER.TRANSFORM_VIEWER,
transform.securityCommon.getPasswordForUser(USER.TRANSFORM_VIEWER)
)
.set(COMMON_REQUEST_HEADERS)
.send(reqBody);
transform.api.assertResponseStatusCode(200, status, body);
expect(body[transformId].success).to.eql(false);
expect(typeof body[transformId].error).to.eql('object');
});
});
describe('single transform schedule with invalid transformId', function () {
it('should return 200 with error in response if invalid transformId', async () => {
const reqBody: ScheduleNowTransformsRequestSchema = [{ id: 'invalid_transform_id' }];
const { body, status } = await supertest
.post(`/api/transform/schedule_now_transforms`)
.auth(
USER.TRANSFORM_POWERUSER,
transform.securityCommon.getPasswordForUser(USER.TRANSFORM_POWERUSER)
)
.set(COMMON_REQUEST_HEADERS)
.send(reqBody);
transform.api.assertResponseStatusCode(200, status, body);
expect(body.invalid_transform_id.success).to.eql(false);
expect(body.invalid_transform_id).to.have.property('error');
});
});
describe('bulk schedule', function () {
const reqBody: ScheduleNowTransformsRequestSchema = [
{ id: 'bulk_schedule_now_test_1' },
{ id: 'bulk_schedule_now_test_2' },
];
const destinationIndices = reqBody.map((d) => generateDestIndex(d.id));
beforeEach(async () => {
await asyncForEach(reqBody, async ({ id }: { id: string }, idx: number) => {
await createTransform(id);
await transform.api.startTransform(id);
});
});
afterEach(async () => {
await asyncForEach(reqBody, async ({ id }: { id: string }, idx: number) => {
await transform.api.stopTransform(id);
});
await transform.api.cleanTransformIndices();
await asyncForEach(destinationIndices, async (destinationIndex: string) => {
await transform.api.deleteIndices(destinationIndex);
});
});
it('should schedule multiple transforms by transformIds', async () => {
const { body, status } = await supertest
.post(`/api/transform/schedule_now_transforms`)
.auth(
USER.TRANSFORM_POWERUSER,
transform.securityCommon.getPasswordForUser(USER.TRANSFORM_POWERUSER)
)
.set(COMMON_REQUEST_HEADERS)
.send(reqBody);
transform.api.assertResponseStatusCode(200, status, body);
await asyncForEach(reqBody, async ({ id: transformId }: { id: string }, idx: number) => {
expect(body[transformId].success).to.eql(true);
});
});
it('should schedule multiple transforms by transformIds, even if one of the transformIds is invalid', async () => {
const invalidTransformId = 'invalid_transform_id';
const { body, status } = await supertest
.post(`/api/transform/schedule_now_transforms`)
.auth(
USER.TRANSFORM_POWERUSER,
transform.securityCommon.getPasswordForUser(USER.TRANSFORM_POWERUSER)
)
.set(COMMON_REQUEST_HEADERS)
.send([{ id: reqBody[0].id }, { id: invalidTransformId }, { id: reqBody[1].id }]);
transform.api.assertResponseStatusCode(200, status, body);
await asyncForEach(reqBody, async ({ id: transformId }: { id: string }, idx: number) => {
expect(body[transformId].success).to.eql(true);
});
expect(body[invalidTransformId].success).to.eql(false);
expect(body[invalidTransformId]).to.have.property('error');
});
});
});
};