Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 13 commits into from
Mar 26, 2023
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(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you add a tooltip to this button to provide clarification as to what this action does as @qn895 suggested?

Something like Instantly run the transform to process data without waiting for the configured interval between checks for changes in the source indices. ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added a tooltip in a92b1ae.

'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) => (
qn895 marked this conversation as resolved.
Show resolved Hide resolved
<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