mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[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:
parent
0ed1f63799
commit
0cc560fbb6
24 changed files with 604 additions and 16 deletions
|
@ -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;
|
|
@ -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);
|
||||
};
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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);
|
||||
};
|
||||
};
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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';
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -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,
|
||||
};
|
||||
};
|
|
@ -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';
|
||||
|
|
|
@ -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 },
|
||||
|
|
|
@ -6,4 +6,4 @@
|
|||
*/
|
||||
|
||||
export { useStopAction } from './use_stop_action';
|
||||
export { StopActionName } from './stop_action_name';
|
||||
export { isStopActionDisabled, StopActionName } from './stop_action_name';
|
||||
|
|
|
@ -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)}
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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.",
|
||||
|
|
|
@ -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": "削除するにはデータフレームジョブを停止してください。",
|
||||
|
|
|
@ -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": "停止数据帧作业,以便将其删除。",
|
||||
|
|
|
@ -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' } } } : {}),
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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'));
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
Loading…
Add table
Add a link
Reference in a new issue