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