Merge branch '21540-priority-update-deadlock' refs #21540
[arvados.git] / services / workbench2 / src / store / processes / processes-actions.ts
1 // Copyright (C) The Arvados Authors. All rights reserved.
2 //
3 // SPDX-License-Identifier: AGPL-3.0
4
5 import { Dispatch } from "redux";
6 import { RootState } from "store/store";
7 import { ServiceRepository } from "services/services";
8 import { updateResources } from "store/resources/resources-actions";
9 import { dialogActions } from "store/dialog/dialog-actions";
10 import { snackbarActions, SnackbarKind } from "store/snackbar/snackbar-actions";
11 import { projectPanelRunActions } from "store/project-panel/project-panel-action-bind";
12 import { navigateTo, navigateToRunProcess } from "store/navigation/navigation-action";
13 import { goToStep, runProcessPanelActions } from "store/run-process-panel/run-process-panel-actions";
14 import { getResource } from "store/resources/resources";
15 import { initialize } from "redux-form";
16 import { RUN_PROCESS_BASIC_FORM, RunProcessBasicFormData } from "store/run-process-panel/run-process-panel-actions";
17 import { RunProcessAdvancedFormData, RUN_PROCESS_ADVANCED_FORM } from "store/run-process-panel/run-process-panel-actions";
18 import { MOUNT_PATH_CWL_WORKFLOW, MOUNT_PATH_CWL_INPUT } from "models/process";
19 import { CommandInputParameter, getWorkflow, getWorkflowInputs, getWorkflowOutputs, WorkflowInputsData } from "models/workflow";
20 import { ProjectResource } from "models/project";
21 import { UserResource } from "models/user";
22 import { CommandOutputParameter } from "cwlts/mappings/v1.0/CommandOutputParameter";
23 import { ContainerRequestResource, ContainerRequestState } from "models/container-request";
24 import { FilterBuilder } from "services/api/filter-builder";
25 import { selectedToArray } from "components/multiselect-toolbar/MultiselectToolbar";
26 import { Resource, ResourceKind } from "models/resource";
27 import { ContextMenuResource } from "store/context-menu/context-menu-actions";
28 import { CommonResourceServiceError, getCommonResourceServiceError } from "services/common-service/common-resource-service";
29 import { getProcessPanelCurrentUuid } from "store/process-panel/process-panel";
30 import { getProjectPanelCurrentUuid } from "store/project-panel/project-panel";
31
32 export const loadContainers =
33     (containerUuids: string[], loadMounts: boolean = true) =>
34     async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
35         let args: any = {
36             filters: new FilterBuilder().addIn("uuid", containerUuids).getFilters(),
37             limit: containerUuids.length,
38         };
39         if (!loadMounts) {
40             args.select = containerFieldsNoMounts;
41         }
42         const { items } = await services.containerService.list(args);
43         dispatch<any>(updateResources(items));
44         return items;
45     };
46
47 // Until the api supports unselecting fields, we need a list of all other fields to omit mounts
48 export const containerFieldsNoMounts = [
49     "auth_uuid",
50     "command",
51     "container_image",
52     "cost",
53     "created_at",
54     "cwd",
55     "environment",
56     "etag",
57     "exit_code",
58     "finished_at",
59     "gateway_address",
60     "interactive_session_started",
61     "kind",
62     "lock_count",
63     "locked_by_uuid",
64     "log",
65     "modified_at",
66     "modified_by_user_uuid",
67     "output_path",
68     "output_properties",
69     "output_storage_classes",
70     "output",
71     "owner_uuid",
72     "priority",
73     "progress",
74     "runtime_auth_scopes",
75     "runtime_constraints",
76     "runtime_status",
77     "runtime_user_uuid",
78     "scheduling_parameters",
79     "started_at",
80     "state",
81     "subrequests_cost",
82     "uuid",
83 ];
84
85 export const cancelRunningWorkflow = (uuid: string) => async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
86     try {
87         const process = await services.containerRequestService.update(uuid, { priority: 0 });
88         dispatch<any>(updateResources([process]));
89         if (process.containerUuid) {
90             const container = await services.containerService.get(process.containerUuid, false);
91             dispatch<any>(updateResources([container]));
92         }
93         return process;
94     } catch (e) {
95         throw new Error("Could not cancel the process.");
96     }
97 };
98
99 export const resumeOnHoldWorkflow = (uuid: string) => async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
100     try {
101         const process = await services.containerRequestService.update(uuid, { priority: 500 });
102         dispatch<any>(updateResources([process]));
103         if (process.containerUuid) {
104             const container = await services.containerService.get(process.containerUuid, false);
105             dispatch<any>(updateResources([container]));
106         }
107         return process;
108     } catch (e) {
109         throw new Error("Could not resume the process.");
110     }
111 };
112
113 export const startWorkflow = (uuid: string) => async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
114     try {
115         const process = await services.containerRequestService.update(uuid, { state: ContainerRequestState.COMMITTED });
116         if (process) {
117             dispatch<any>(updateResources([process]));
118             dispatch(snackbarActions.OPEN_SNACKBAR({ message: "Process started", hideDuration: 2000, kind: SnackbarKind.SUCCESS }));
119         } else {
120             dispatch<any>(snackbarActions.OPEN_SNACKBAR({ message: `Failed to start process`, kind: SnackbarKind.ERROR }));
121         }
122     } catch (e) {
123         dispatch<any>(snackbarActions.OPEN_SNACKBAR({ message: `Failed to start process`, kind: SnackbarKind.ERROR }));
124     }
125 };
126
127 export const reRunProcess =
128     (processUuid: string, workflowUuid: string) => (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
129         const process = getResource<any>(processUuid)(getState().resources);
130         const workflows = getState().runProcessPanel.searchWorkflows;
131         const workflow = workflows.find(workflow => workflow.uuid === workflowUuid);
132         if (workflow && process) {
133             const mainWf = getWorkflow(process.mounts[MOUNT_PATH_CWL_WORKFLOW]);
134             if (mainWf) {
135                 mainWf.inputs = getInputs(process);
136             }
137             const stringifiedDefinition = JSON.stringify(process.mounts[MOUNT_PATH_CWL_WORKFLOW].content);
138             const newWorkflow = { ...workflow, definition: stringifiedDefinition };
139
140             const owner = getResource<ProjectResource | UserResource>(workflow.ownerUuid)(getState().resources);
141             const basicInitialData: RunProcessBasicFormData = { name: `Copy of: ${process.name}`, owner };
142             dispatch<any>(initialize(RUN_PROCESS_BASIC_FORM, basicInitialData));
143
144             const advancedInitialData: RunProcessAdvancedFormData = {
145                 description: process.description,
146                 output: process.outputName,
147                 runtime: process.schedulingParameters.max_run_time,
148                 ram: process.runtimeConstraints.ram,
149                 vcpus: process.runtimeConstraints.vcpus,
150                 keep_cache_ram: process.runtimeConstraints.keep_cache_ram,
151                 acr_container_image: process.containerImage,
152             };
153             dispatch<any>(initialize(RUN_PROCESS_ADVANCED_FORM, advancedInitialData));
154
155             dispatch<any>(navigateToRunProcess);
156             dispatch<any>(goToStep(1));
157             dispatch(runProcessPanelActions.SET_STEP_CHANGED(true));
158             dispatch(runProcessPanelActions.SET_SELECTED_WORKFLOW(newWorkflow));
159         } else {
160             dispatch<any>(snackbarActions.OPEN_SNACKBAR({ message: `You can't re-run this process`, kind: SnackbarKind.ERROR }));
161         }
162     };
163
164 /*
165  * Fetches raw inputs from containerRequest mounts with fallback to properties
166  * Returns undefined if containerRequest not loaded
167  * Returns {} if inputs not found in mounts or props
168  */
169 export const getRawInputs = (data: any): WorkflowInputsData | undefined => {
170     if (!data) {
171         return undefined;
172     }
173     const mountInput = data.mounts?.[MOUNT_PATH_CWL_INPUT]?.content;
174     const propsInput = data.properties?.cwl_input;
175     if (!mountInput && !propsInput) {
176         return {};
177     }
178     return mountInput || propsInput;
179 };
180
181 export const getInputs = (data: any): CommandInputParameter[] => {
182     // Definitions from mounts are needed so we return early if missing
183     if (!data || !data.mounts || !data.mounts[MOUNT_PATH_CWL_WORKFLOW]) {
184         return [];
185     }
186     const content = getRawInputs(data) as any;
187     // Only escape if content is falsy to allow displaying definitions if no inputs are present
188     // (Don't check raw content length)
189     if (!content) {
190         return [];
191     }
192
193     const inputs = getWorkflowInputs(data.mounts[MOUNT_PATH_CWL_WORKFLOW].content);
194     return inputs
195         ? inputs.map((it: any) => ({
196               type: it.type,
197               id: it.id,
198               label: it.label,
199               default: content[it.id],
200               value: content[it.id.split("/").pop()] || [],
201               doc: it.doc,
202           }))
203         : [];
204 };
205
206 /*
207  * Fetches raw outputs from containerRequest properties
208  * Assumes containerRequest is loaded
209  */
210 export const getRawOutputs = (data: any): any | undefined => {
211     if (!data || !data.properties || !data.properties.cwl_output) {
212         return undefined;
213     }
214     return data.properties.cwl_output;
215 };
216
217 export type InputCollectionMount = {
218     path: string;
219     pdh: string;
220 };
221
222 export const getInputCollectionMounts = (data: any): InputCollectionMount[] => {
223     if (!data || !data.mounts) {
224         return [];
225     }
226     return Object.keys(data.mounts)
227         .map(key => ({
228             ...data.mounts[key],
229             path: key,
230         }))
231         .filter(mount => mount.kind === "collection" && mount.portable_data_hash && mount.path)
232         .map(mount => ({
233             path: mount.path,
234             pdh: mount.portable_data_hash,
235         }));
236 };
237
238 export const getOutputParameters = (data: any): CommandOutputParameter[] => {
239     if (!data || !data.mounts || !data.mounts[MOUNT_PATH_CWL_WORKFLOW]) {
240         return [];
241     }
242     const outputs = getWorkflowOutputs(data.mounts[MOUNT_PATH_CWL_WORKFLOW].content);
243     return outputs
244         ? outputs.map((it: any) => ({
245               type: it.type,
246               id: it.id,
247               label: it.label,
248               doc: it.doc,
249           }))
250         : [];
251 };
252
253 export const openRemoveProcessDialog =
254     (resource: ContextMenuResource, numOfProcesses: Number) => (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
255         const confirmationText =
256             numOfProcesses === 1
257                 ? "Are you sure you want to remove this process?"
258                 : `Are you sure you want to remove these ${numOfProcesses} processes?`;
259         const titleText = numOfProcesses === 1 ? "Remove process permanently" : "Remove processes permanently";
260
261         dispatch(
262             dialogActions.OPEN_DIALOG({
263                 id: REMOVE_PROCESS_DIALOG,
264                 data: {
265                     title: titleText,
266                     text: confirmationText,
267                     confirmButtonLabel: "Remove",
268                     uuid: resource.uuid,
269                     resource,
270                 },
271             })
272         );
273     };
274
275 export const REMOVE_PROCESS_DIALOG = "removeProcessDialog";
276
277 export const removeProcessPermanently = (uuid: string) => async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
278     const currentProcessPanelUuid = getProcessPanelCurrentUuid(getState().router);
279     const currentProjectUuid = getProjectPanelCurrentUuid(getState());
280     const resource = getState().dialog.removeProcessDialog.data.resource;
281     const checkedList = getState().multiselect.checkedList;
282
283     const uuidsToRemove: string[] = resource.fromContextMenu ? [resource.uuid] : selectedToArray(checkedList);
284
285     //if no items in checkedlist, default to normal context menu behavior
286     if (!uuidsToRemove.length) uuidsToRemove.push(uuid);
287
288     const processesToRemove = uuidsToRemove
289         .map(uuid => getResource(uuid)(getState().resources) as Resource)
290         .filter(resource => resource.kind === ResourceKind.PROCESS);
291
292     await Promise.allSettled(processesToRemove.map(process => services.containerRequestService.delete(process.uuid, false)))
293         .then(res => {
294             const failed = res.filter((promiseResult): promiseResult is PromiseRejectedResult => promiseResult.status === 'rejected');
295             const succeeded = res.filter((promiseResult): promiseResult is PromiseFulfilledResult<ContainerRequestResource> => promiseResult.status === 'fulfilled');
296
297             // Get error kinds
298             const accessDeniedError = failed.filter((promiseResult) => {
299                 return getCommonResourceServiceError(promiseResult.reason) === CommonResourceServiceError.PERMISSION_ERROR_FORBIDDEN;
300             });
301             const genericError = failed.filter((promiseResult) => {
302                 return getCommonResourceServiceError(promiseResult.reason) !== CommonResourceServiceError.PERMISSION_ERROR_FORBIDDEN;
303             });
304
305             // Show grouped errors for access or generic error
306             if (accessDeniedError.length) {
307                 if (accessDeniedError.length > 1) {
308                     dispatch(snackbarActions.OPEN_SNACKBAR({ message: `Access denied: ${accessDeniedError.length} items`, hideDuration: 2000, kind: SnackbarKind.ERROR }));
309                 } else {
310                     dispatch(snackbarActions.OPEN_SNACKBAR({ message: `Access denied`, hideDuration: 2000, kind: SnackbarKind.ERROR }));
311                 }
312             }
313             if (genericError.length) {
314                 if (genericError.length > 1) {
315                     dispatch(snackbarActions.OPEN_SNACKBAR({ message: `Deletion failed: ${genericError.length} items`, hideDuration: 2000, kind: SnackbarKind.ERROR }));
316                 } else {
317                     dispatch(snackbarActions.OPEN_SNACKBAR({ message: `Deletion failed`, hideDuration: 2000, kind: SnackbarKind.ERROR }));
318                 }
319             }
320             if (succeeded.length) {
321                 if (succeeded.length > 1) {
322                     dispatch(snackbarActions.OPEN_SNACKBAR({ message: `Removed ${succeeded.length} items`, hideDuration: 2000, kind: SnackbarKind.SUCCESS }));
323                 } else {
324                     dispatch(snackbarActions.OPEN_SNACKBAR({ message: "Removed", hideDuration: 2000, kind: SnackbarKind.SUCCESS }));
325                 }
326             }
327
328             // If currently viewing any of the deleted runs, navigate to home
329             if (currentProcessPanelUuid) {
330                 const currentProcessDeleted = succeeded.find((promiseResult) => promiseResult.value.uuid === currentProcessPanelUuid);
331                 if (currentProcessDeleted) {
332                     dispatch<any>(navigateTo(currentProcessDeleted.value.ownerUuid));
333                 }
334             }
335
336             // If currently viewing the parent project of any of the deleted runs, refresh project runs tab
337             if (currentProjectUuid && succeeded.find((promiseResult) => promiseResult.value.ownerUuid === currentProjectUuid)) {
338                 dispatch(projectPanelRunActions.REQUEST_ITEMS());
339             }
340         });
341 };