20251: Fix flaky collection file browser by using race-free state update callback
[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 = (containerRequestUuid: string) =>
29     async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository): Promise<Process | undefined> => {
30         let containerRequest: ContainerRequestResource | undefined = undefined;
31         try {
32             containerRequest = await services.containerRequestService.get(containerRequestUuid);
33             dispatch<any>(updateResources([containerRequest]));
34         } catch {
35             return undefined;
36         }
37
38         if (containerRequest.outputUuid) {
39             try {
40                 const collection = await services.collectionService.get(containerRequest.outputUuid, false);
41                 dispatch<any>(updateResources([collection]));
42             } catch {}
43         }
44
45         if (containerRequest.containerUuid) {
46             let container: ContainerResource | undefined = undefined;
47             try {
48                 container = await services.containerService.get(containerRequest.containerUuid, false);
49                 dispatch<any>(updateResources([container]));
50             } catch {}
51
52             try{
53                 if (container && container.runtimeUserUuid) {
54                     const runtimeUser = await services.userService.get(container.runtimeUserUuid, false);
55                     dispatch<any>(updateResources([runtimeUser]));
56                 }
57             } catch {}
58
59             return { containerRequest, container };
60         }
61         return { containerRequest };
62     };
63
64 export const loadContainers = (containerUuids: string[], loadMounts: boolean = true) =>
65     async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
66         let args: any = {
67             filters: new FilterBuilder().addIn('uuid', containerUuids).getFilters(),
68             limit: containerUuids.length,
69          };
70         if (!loadMounts) {
71             args.select = containerFieldsNoMounts;
72         }
73         const { items } = await services.containerService.list(args);
74         dispatch<any>(updateResources(items));
75         return items;
76     };
77
78 // Until the api supports unselecting fields, we need a list of all other fields to omit mounts
79 const containerFieldsNoMounts = [
80     "auth_uuid",
81     "command",
82     "container_image",
83     "cost",
84     "created_at",
85     "cwd",
86     "environment",
87     "etag",
88     "exit_code",
89     "finished_at",
90     "gateway_address",
91     "href",
92     "interactive_session_started",
93     "kind",
94     "lock_count",
95     "locked_by_uuid",
96     "log",
97     "modified_at",
98     "modified_by_client_uuid",
99     "modified_by_user_uuid",
100     "output_path",
101     "output_properties",
102     "output_storage_classes",
103     "output",
104     "owner_uuid",
105     "priority",
106     "progress",
107     "runtime_auth_scopes",
108     "runtime_constraints",
109     "runtime_status",
110     "runtime_user_uuid",
111     "scheduling_parameters",
112     "started_at",
113     "state",
114     "uuid",
115 ]
116
117 export const cancelRunningWorkflow = (uuid: string) =>
118     async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
119         try {
120             const process = await services.containerRequestService.update(uuid, { priority: 0 });
121             dispatch<any>(updateResources([process]));
122             if (process.containerUuid) {
123                 const container = await services.containerService.get(process.containerUuid, false);
124                 dispatch<any>(updateResources([container]));
125             }
126             return process;
127         } catch (e) {
128             throw new Error('Could not cancel the process.');
129         }
130     };
131
132 export const resumeOnHoldWorkflow = (uuid: string) =>
133     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) =>
148     async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
149         try {
150             const process = await services.containerRequestService.update(uuid, { state: ContainerRequestState.COMMITTED });
151             if (process) {
152                 dispatch<any>(updateResources([process]));
153                 dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'Process started', hideDuration: 2000, kind: SnackbarKind.SUCCESS }));
154             } else {
155                 dispatch<any>(snackbarActions.OPEN_SNACKBAR({ message: `Failed to start process`, kind: SnackbarKind.ERROR }));
156             }
157         } catch (e) {
158             dispatch<any>(snackbarActions.OPEN_SNACKBAR({ message: `Failed to start process`, kind: SnackbarKind.ERROR }));
159         }
160     };
161
162 export const reRunProcess = (processUuid: string, workflowUuid: string) =>
163     (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
164         const process = getResource<any>(processUuid)(getState().resources);
165         const workflows = getState().runProcessPanel.searchWorkflows;
166         const workflow = workflows.find(workflow => workflow.uuid === workflowUuid);
167         if (workflow && process) {
168             const mainWf = getWorkflow(process.mounts[MOUNT_PATH_CWL_WORKFLOW]);
169             if (mainWf) { mainWf.inputs = getInputs(process); }
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) { return undefined; }
203     const mountInput = data.mounts?.[MOUNT_PATH_CWL_INPUT]?.content;
204     const propsInput = data.properties?.cwl_input;
205     if (!mountInput && !propsInput) { return {}; }
206     return (mountInput || propsInput);
207 }
208
209 export const getInputs = (data: any): CommandInputParameter[] => {
210     // Definitions from mounts are needed so we return early if missing
211     if (!data || !data.mounts || !data.mounts[MOUNT_PATH_CWL_WORKFLOW]) { return []; }
212     const content  = getRawInputs(data) as any;
213     // Only escape if content is falsy to allow displaying definitions if no inputs are present
214     // (Don't check raw content length)
215     if (!content) { return []; }
216
217     const inputs = getWorkflowInputs(data.mounts[MOUNT_PATH_CWL_WORKFLOW].content);
218     return inputs ? inputs.map(
219         (it: any) => (
220             {
221                 type: it.type,
222                 id: it.id,
223                 label: it.label,
224                 default: content[it.id],
225                 value: content[it.id.split('/').pop()] || [],
226                 doc: it.doc
227             }
228         )
229     ) : [];
230 };
231
232 /*
233  * Fetches raw outputs from containerRequest properties
234  * Assumes containerRequest is loaded
235  */
236 export const getRawOutputs = (data: any): CommandInputParameter[] | undefined => {
237     if (!data || !data.properties || !data.properties.cwl_output) { return undefined; }
238     return (data.properties.cwl_output);
239 }
240
241 export type InputCollectionMount = {
242     path: string;
243     pdh: string;
244 }
245
246 export const getInputCollectionMounts = (data: any): InputCollectionMount[] => {
247     if (!data || !data.mounts) { return []; }
248     return Object.keys(data.mounts)
249         .map(key => ({
250             ...data.mounts[key],
251             path: key,
252         }))
253         .filter(mount => mount.kind === 'collection' &&
254                 mount.portable_data_hash &&
255                 mount.path)
256         .map(mount => ({
257             path: mount.path,
258             pdh: mount.portable_data_hash,
259         }));
260 };
261
262 export const getOutputParameters = (data: any): CommandOutputParameter[] => {
263     if (!data || !data.mounts || !data.mounts[MOUNT_PATH_CWL_WORKFLOW]) { return []; }
264     const outputs = getWorkflowOutputs(data.mounts[MOUNT_PATH_CWL_WORKFLOW].content);
265     return outputs ? outputs.map(
266         (it: any) => (
267             {
268                 type: it.type,
269                 id: it.id,
270                 label: it.label,
271                 doc: it.doc
272             }
273         )
274     ) : [];
275 };
276
277 export const openRemoveProcessDialog = (uuid: string) =>
278     (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
279         dispatch(dialogActions.OPEN_DIALOG({
280             id: REMOVE_PROCESS_DIALOG,
281             data: {
282                 title: 'Remove process permanently',
283                 text: 'Are you sure you want to remove this process?',
284                 confirmButtonLabel: 'Remove',
285                 uuid
286             }
287         }));
288     };
289
290 export const REMOVE_PROCESS_DIALOG = 'removeProcessDialog';
291
292 export const removeProcessPermanently = (uuid: string) =>
293     async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
294         dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'Removing ...', kind: SnackbarKind.INFO }));
295         await services.containerRequestService.delete(uuid);
296         dispatch(projectPanelActions.REQUEST_ITEMS());
297         dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'Removed.', hideDuration: 2000, kind: SnackbarKind.SUCCESS }));
298     };