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