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