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