From 8aec972f647aafbeb69a9f70b40f19d11ff62df0 Mon Sep 17 00:00:00 2001 From: Dima Arnautov Date: Thu, 25 Nov 2021 10:54:30 +0100 Subject: [PATCH] [ML] Support `force` stop deployment (#118563) * support force stop * fix i18n, revert tests * update text * remove "Force" from the confirmation modals --- .../close_job_confirm/close_job_confirm.tsx | 4 +- .../action_stop/stop_action_modal.tsx | 2 +- .../services/ml_api_service/trained_models.ts | 5 +- .../models_management/force_stop_dialog.tsx | 89 +++++++++++++++++++ .../models_management/models_list.tsx | 16 +++- .../ml/server/routes/trained_models.ts | 3 + 6 files changed, 113 insertions(+), 6 deletions(-) create mode 100644 x-pack/plugins/ml/public/application/trained_models/models_management/force_stop_dialog.tsx diff --git a/x-pack/plugins/ml/public/application/components/model_snapshots/close_job_confirm/close_job_confirm.tsx b/x-pack/plugins/ml/public/application/components/model_snapshots/close_job_confirm/close_job_confirm.tsx index 14c3dc3fc7a7a..da1bb825768e2 100644 --- a/x-pack/plugins/ml/public/application/components/model_snapshots/close_job_confirm/close_job_confirm.tsx +++ b/x-pack/plugins/ml/public/application/components/model_snapshots/close_job_confirm/close_job_confirm.tsx @@ -43,10 +43,10 @@ export const CloseJobConfirm: FC = ({ confirmButtonText={ combinedJobState === COMBINED_JOB_STATE.OPEN_AND_RUNNING ? i18n.translate('xpack.ml.modelSnapshotTable.closeJobConfirm.stopAndClose.button', { - defaultMessage: 'Force stop and close', + defaultMessage: 'Stop and close', }) : i18n.translate('xpack.ml.modelSnapshotTable.closeJobConfirm.close.button', { - defaultMessage: 'Force close', + defaultMessage: 'Close', }) } defaultFocusedButton="confirm" diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_stop/stop_action_modal.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_stop/stop_action_modal.tsx index e9939299d7ca0..6f0ee0afebe42 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_stop/stop_action_modal.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_stop/stop_action_modal.tsx @@ -31,7 +31,7 @@ export const StopActionModal: FC = ({ closeModal, item, forceStopAnd confirmButtonText={i18n.translate( 'xpack.ml.dataframe.analyticsList.forceStopModalStartButton', { - defaultMessage: 'Force stop', + defaultMessage: 'Stop', } )} defaultFocusedButton={EUI_MODAL_CONFIRM_BUTTON} diff --git a/x-pack/plugins/ml/public/application/services/ml_api_service/trained_models.ts b/x-pack/plugins/ml/public/application/services/ml_api_service/trained_models.ts index 822c5059982e7..381528e1055d6 100644 --- a/x-pack/plugins/ml/public/application/services/ml_api_service/trained_models.ts +++ b/x-pack/plugins/ml/public/application/services/ml_api_service/trained_models.ts @@ -135,10 +135,13 @@ export function trainedModelsApiProvider(httpService: HttpService) { }); }, - stopModelAllocation(modelId: string) { + stopModelAllocation(modelId: string, options: { force: boolean } = { force: false }) { + const force = options?.force; + return httpService.http<{ acknowledge: boolean }>({ path: `${apiBasePath}/trained_models/${modelId}/deployment/_stop`, method: 'POST', + query: { force }, }); }, }; diff --git a/x-pack/plugins/ml/public/application/trained_models/models_management/force_stop_dialog.tsx b/x-pack/plugins/ml/public/application/trained_models/models_management/force_stop_dialog.tsx new file mode 100644 index 0000000000000..6d2a3880180eb --- /dev/null +++ b/x-pack/plugins/ml/public/application/trained_models/models_management/force_stop_dialog.tsx @@ -0,0 +1,89 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { FC } from 'react'; +import { EuiConfirmModal } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import type { OverlayStart } from 'kibana/public'; +import type { ModelItem } from './models_list'; +import { toMountPoint } from '../../../../../../../src/plugins/kibana_react/public'; + +interface ForceStopModelConfirmDialogProps { + model: ModelItem; + onCancel: () => void; + onConfirm: () => void; +} + +export const ForceStopModelConfirmDialog: FC = ({ + model, + onConfirm, + onCancel, +}) => { + return ( + + } + onCancel={onCancel} + onConfirm={onConfirm} + cancelButtonText={ + + } + confirmButtonText={ + + } + buttonColor="danger" + > + +
    + {Object.keys(model.pipelines!) + .sort() + .map((pipelineName) => { + return
  • {pipelineName}
  • ; + })} +
+
+ ); +}; + +export const getUserConfirmationProvider = + (overlays: OverlayStart) => async (forceStopModel: ModelItem) => { + return new Promise(async (resolve, reject) => { + try { + const modalSession = overlays.openModal( + toMountPoint( + { + modalSession.close(); + resolve(false); + }} + onConfirm={() => { + modalSession.close(); + resolve(true); + }} + /> + ) + ); + } catch (e) { + resolve(false); + } + }); + }; diff --git a/x-pack/plugins/ml/public/application/trained_models/models_management/models_list.tsx b/x-pack/plugins/ml/public/application/trained_models/models_management/models_list.tsx index 0283e470bc383..c80ff808aa539 100644 --- a/x-pack/plugins/ml/public/application/trained_models/models_management/models_list.tsx +++ b/x-pack/plugins/ml/public/application/trained_models/models_management/models_list.tsx @@ -53,6 +53,7 @@ import { useFieldFormatter } from '../../contexts/kibana/use_field_formatter'; import { FIELD_FORMAT_IDS } from '../../../../../../../src/plugins/field_formats/common'; import { useRefresh } from '../../routing/use_refresh'; import { DEPLOYMENT_STATE } from '../../../../common/constants/trained_models'; +import { getUserConfirmationProvider } from './force_stop_dialog'; type Stats = Omit; @@ -80,6 +81,7 @@ export const ModelsList: FC = () => { const { services: { application: { navigateToUrl, capabilities }, + overlays, }, } = useMlKibana(); const urlLocator = useMlLocator()!; @@ -110,6 +112,8 @@ export const ModelsList: FC = () => { {} ); + const getUserConfirmation = useMemo(() => getUserConfirmationProvider(overlays), []); + const navigateToPath = useNavigateToPath(); const isBuiltInModel = useCallback( @@ -418,13 +422,21 @@ export const ModelsList: FC = () => { available: (item) => item.model_type === 'pytorch', enabled: (item) => !isLoading && - !isPopulatedObject(item.pipelines) && isPopulatedObject(item.stats?.deployment_stats) && item.stats?.deployment_stats?.state !== DEPLOYMENT_STATE.STOPPING, onClick: async (item) => { + const requireForceStop = isPopulatedObject(item.pipelines); + + if (requireForceStop) { + const hasUserApproved = await getUserConfirmation(item); + if (!hasUserApproved) return; + } + try { setIsLoading(true); - await trainedModelsApiService.stopModelAllocation(item.model_id); + await trainedModelsApiService.stopModelAllocation(item.model_id, { + force: requireForceStop, + }); displaySuccessToast( i18n.translate('xpack.ml.trainedModels.modelsList.stopSuccess', { defaultMessage: 'Deployment for "{modelId}" has been stopped successfully.', diff --git a/x-pack/plugins/ml/server/routes/trained_models.ts b/x-pack/plugins/ml/server/routes/trained_models.ts index e7696861153ff..e213efc203704 100644 --- a/x-pack/plugins/ml/server/routes/trained_models.ts +++ b/x-pack/plugins/ml/server/routes/trained_models.ts @@ -16,6 +16,7 @@ import { modelsProvider } from '../models/data_frame_analytics'; import { TrainedModelConfigResponse } from '../../common/types/trained_models'; import { memoryOverviewServiceProvider } from '../models/memory_overview'; import { mlLog } from '../lib/log'; +import { forceQuerySchema } from './schemas/anomaly_detectors_schema'; export function trainedModelsRoutes({ router, routeGuard }: RouteInitialization) { /** @@ -266,6 +267,7 @@ export function trainedModelsRoutes({ router, routeGuard }: RouteInitialization) path: '/api/ml/trained_models/{modelId}/deployment/_stop', validate: { params: modelIdSchema, + query: forceQuerySchema, }, options: { tags: ['access:ml:canGetDataFrameAnalytics'], @@ -276,6 +278,7 @@ export function trainedModelsRoutes({ router, routeGuard }: RouteInitialization) const { modelId } = request.params; const { body } = await mlClient.stopTrainedModelDeployment({ model_id: modelId, + force: request.query.force ?? false, }); return response.ok({ body,