Skip to content

Commit

Permalink
[ML] Transforms: Adds _schedule_now action to transform list. (#153545
Browse files Browse the repository at this point in the history
)

- Adds `_schedule_now` action to transform list.
- Fixes bulk actions to be correctly disabled when not available.
  • Loading branch information
walterra authored Mar 26, 2023
1 parent 0ed1f63 commit 0cc560f
Show file tree
Hide file tree
Showing 24 changed files with 604 additions and 16 deletions.
Original file line number Diff line number Diff line change
@@ -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;
7 changes: 7 additions & 0 deletions x-pack/plugins/transform/common/api_schemas/type_guards.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -144,3 +145,9 @@ export const isStopTransformsResponseSchema = (
): arg is StopTransformsResponseSchema => {
return isGenericSuccessResponseSchema(arg);
};

export const isScheduleNowTransformsResponseSchema = (
arg: unknown
): arg is ScheduleNowTransformsResponseSchema => {
return isGenericSuccessResponseSchema(arg);
};
3 changes: 2 additions & 1 deletion x-pack/plugins/transform/common/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand All @@ -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',
Expand Down
1 change: 1 addition & 0 deletions x-pack/plugins/transform/public/app/hooks/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
15 changes: 15 additions & 0 deletions x-pack/plugins/transform/public/app/hooks/use_api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
};
};
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ const initialCapabilities: Capabilities = {
canDeleteTransform: false,
canPreviewTransform: false,
canCreateTransform: false,
canScheduleNowTransform: false,
canStartStopTransform: false,
canCreateTransformAlerts: false,
canUseTransformAlerts: false,
Expand Down Expand Up @@ -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>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export interface Capabilities {
canDeleteTransform: boolean;
canPreviewTransform: boolean;
canCreateTransform: boolean;
canScheduleNowTransform: boolean;
canStartStopTransform: boolean;
canCreateTransformAlerts: boolean;
canUseTransformAlerts: boolean;
Expand Down Expand Up @@ -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',
Expand Down
Original file line number Diff line number Diff line change
@@ -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';
Original file line number Diff line number Diff line change
@@ -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>
);
};
Original file line number Diff line number Diff line change
@@ -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,
};
};
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Loading

0 comments on commit 0cc560f

Please sign in to comment.