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