diff --git a/x-pack/plugins/ml/public/application/model_management/force_stop_dialog.tsx b/x-pack/plugins/ml/public/application/model_management/force_stop_dialog.tsx index ee6bac7ebf6e64d..07b69cb07fd18d0 100644 --- a/x-pack/plugins/ml/public/application/model_management/force_stop_dialog.tsx +++ b/x-pack/plugins/ml/public/application/model_management/force_stop_dialog.tsx @@ -5,33 +5,63 @@ * 2.0. */ -import React, { FC } from 'react'; -import { EuiConfirmModal } from '@elastic/eui'; +import React, { type FC, useState } from 'react'; +import { EuiCheckboxGroup, EuiCheckboxGroupOption, EuiConfirmModal } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; import { i18n } from '@kbn/i18n'; import type { OverlayStart, ThemeServiceStart } from '@kbn/core/public'; import { toMountPoint, wrapWithTheme } from '@kbn/kibana-react-plugin/public'; +import { isPopulatedObject } from '@kbn/ml-is-populated-object'; import type { ModelItem } from './models_list'; interface ForceStopModelConfirmDialogProps { model: ModelItem; onCancel: () => void; - onConfirm: () => void; + onConfirm: (deploymentIds: string[]) => void; } +/** + * Confirmation is required when there are multiple model deployments + * or associated pipelines. + */ export const StopModelDeploymentsConfirmDialog: FC = ({ model, onConfirm, onCancel, }) => { + const [checkboxIdToSelectedMap, setCheckboxIdToSelectedMap] = useState>( + {} + ); + + const options: EuiCheckboxGroupOption[] = model.deployment_ids.map((deploymentId) => { + return { + id: deploymentId, + label: deploymentId, + }; + }); + + const onChange = (id: string) => { + setCheckboxIdToSelectedMap((prev) => { + return { + ...prev, + [id]: !prev[id], + }; + }); + }; + + const selectedDeploymentIds = Object.keys(checkboxIdToSelectedMap).filter( + (id) => checkboxIdToSelectedMap[id] + ); + return ( 1 && selectedDeploymentIds.length === 0} > - -
    - {Object.keys(model.pipelines!) - .sort() - .map((pipelineName) => { - return
  • {pipelineName}
  • ; - })} -
+ {model.deployment_ids.length > 1 ? ( + + ), + }} + options={options} + idToSelectedMap={checkboxIdToSelectedMap} + onChange={onChange} + /> + ) : null} + + {isPopulatedObject(model.pipelines) ? ( + <> + +
    + {Object.keys(model.pipelines!) + .sort() + .map((pipelineName) => { + return
  • {pipelineName}
  • ; + })} +
+ + ) : null}
); }; export const getUserConfirmationProvider = - (overlays: OverlayStart, theme: ThemeServiceStart) => async (forceStopModel: ModelItem) => { + (overlays: OverlayStart, theme: ThemeServiceStart) => + async (forceStopModel: ModelItem): Promise => { return new Promise(async (resolve, reject) => { try { const modalSession = overlays.openModal( @@ -68,11 +121,11 @@ export const getUserConfirmationProvider = model={forceStopModel} onCancel={() => { modalSession.close(); - resolve(false); + reject(); }} - onConfirm={() => { + onConfirm={(deploymentIds: string[]) => { modalSession.close(); - resolve(true); + resolve(deploymentIds); }} />, theme.theme$ @@ -80,7 +133,7 @@ export const getUserConfirmationProvider = ) ); } catch (e) { - resolve(false); + reject(); } }); }; diff --git a/x-pack/plugins/ml/public/application/model_management/model_actions.tsx b/x-pack/plugins/ml/public/application/model_management/model_actions.tsx index 6927122ece3c858..1b249224cd9ded8 100644 --- a/x-pack/plugins/ml/public/application/model_management/model_actions.tsx +++ b/x-pack/plugins/ml/public/application/model_management/model_actions.tsx @@ -275,14 +275,18 @@ export function useModelActions({ const requireForceStop = isPopulatedObject(item.pipelines); const hasMultipleDeployments = item.deployment_ids.length > 1; + let deploymentIds: string[] = [item.model_id]; if (requireForceStop || hasMultipleDeployments) { - const hasUserApproved = await getUserConfirmation(item); - if (!hasUserApproved) return; + try { + deploymentIds = await getUserConfirmation(item); + } catch (error) { + return; + } } try { onLoading(true); - await trainedModelsApiService.stopModelAllocation(item.model_id, { + await trainedModelsApiService.stopModelAllocation(deploymentIds, { force: requireForceStop, }); displaySuccessToast( 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 d57e81f4c05c127..2975982eccad074 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 @@ -142,11 +142,11 @@ export function trainedModelsApiProvider(httpService: HttpService) { }); }, - stopModelAllocation(modelId: string, options: { force: boolean } = { force: false }) { + stopModelAllocation(deploymentsIds: string[], options: { force: boolean } = { force: false }) { const force = options?.force; return httpService.http<{ acknowledge: boolean }>({ - path: `${apiBasePath}/trained_models/${modelId}/deployment/_stop`, + path: `${apiBasePath}/trained_models/${deploymentsIds.join(',')}/deployment/_stop`, method: 'POST', query: { force }, });