22083: Store "failedToLoadOutputCollection" state
[arvados.git] / services / workbench2 / src / store / process-panel / process-panel-actions.ts
1 // Copyright (C) The Arvados Authors. All rights reserved.
2 //
3 // SPDX-License-Identifier: AGPL-3.0
4
5 import { unionize, ofType, UnionOf } from "common/unionize";
6 import { getInputs,
7          getOutputParameters,
8          getRawInputs,
9          getRawOutputs
10 } from "store/processes/processes-actions";
11 import { Dispatch } from "redux";
12 import { Process, ProcessStatus } from "store/processes/process";
13 import { RootState } from "store/store";
14 import { ServiceRepository } from "services/services";
15 import { navigateTo } from "store/navigation/navigation-action";
16 import { snackbarActions } from "store/snackbar/snackbar-actions";
17 import { SnackbarKind } from "../snackbar/snackbar-actions";
18 import { loadSubprocessPanel, subprocessPanelActions } from "../subprocess-panel/subprocess-panel-actions";
19 import { initProcessLogsPanel, processLogsPanelActions } from "store/process-logs-panel/process-logs-panel-actions";
20 import { CollectionFile } from "models/collection-file";
21 import { ContainerRequestResource } from "models/container-request";
22 import { CommandOutputParameter } from "cwlts/mappings/v1.0/CommandOutputParameter";
23 import { CommandInputParameter, getIOParamId, WorkflowInputsData } from "models/workflow";
24 import { getIOParamDisplayValue, ProcessIOParameter } from "views/process-panel/process-io-card";
25 import { OutputDetails, NodeInstanceType, NodeInfo, UsageReport } from "./process-panel";
26 import { AuthState } from "store/auth/auth-reducer";
27 import { ContextMenuResource } from "store/context-menu/context-menu-actions";
28 import { OutputDataUpdate } from "./process-panel-reducer";
29 import { updateResources } from "store/resources/resources-actions";
30 import { ContainerResource } from "models/container";
31 import { FilterBuilder } from "services/api/filter-builder";
32
33 export const processPanelActions = unionize({
34     RESET_PROCESS_PANEL: ofType<{}>(),
35     SET_PROCESS_PANEL_CONTAINER_REQUEST_UUID: ofType<string>(),
36     SET_PROCESS_PANEL_FILTERS: ofType<string[]>(),
37     TOGGLE_PROCESS_PANEL_FILTER: ofType<string>(),
38     SET_INPUT_RAW: ofType<WorkflowInputsData | null>(),
39     SET_INPUT_PARAMS: ofType<ProcessIOParameter[] | null>(),
40     SET_OUTPUT_DATA: ofType<OutputDataUpdate | null>(),
41     SET_OUTPUT_DEFINITIONS: ofType<CommandOutputParameter[]>(),
42     SET_OUTPUT_PARAMS: ofType<ProcessIOParameter[] | null>(),
43     SET_NODE_INFO: ofType<NodeInfo>(),
44     SET_USAGE_REPORT: ofType<UsageReport>(),
45 });
46
47 export type ProcessPanelAction = UnionOf<typeof processPanelActions>;
48
49 export const toggleProcessPanelFilter = processPanelActions.TOGGLE_PROCESS_PANEL_FILTER;
50
51 export const loadProcess =
52     (containerRequestUuid: string) =>
53     async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository): Promise<Process | undefined> => {
54         let containerRequest: ContainerRequestResource | undefined = undefined;
55         let container: ContainerResource | undefined = undefined;
56
57         try {
58             const containerRequestResult = await services.groupsService.contents(
59                 '', {
60                     filters: new FilterBuilder().addIsA('uuid', 'arvados#containerRequest')
61                                                  .addEqual('uuid', containerRequestUuid)
62                                                  .getFilters(),
63                     include: ["container_uuid"]
64             });
65             if (containerRequestResult.items.length === 1) {
66                 containerRequest = containerRequestResult.items[0] as ContainerRequestResource;
67                 dispatch<any>(updateResources(containerRequestResult.items));
68
69                 if (containerRequestResult.included?.length === 1) {
70                     container = containerRequestResult.included[0] as ContainerResource;
71                     dispatch<any>(updateResources(containerRequestResult.included));
72                 }
73             }
74         } catch (e) {
75             if (!containerRequest) {
76                 dispatch(
77                     snackbarActions.OPEN_SNACKBAR({
78                         message: e.message,
79                         hideDuration: 2000,
80                         kind: SnackbarKind.ERROR,
81                     })
82                 );
83             }
84         }
85
86         if (!containerRequest) {
87             return undefined;
88         }
89
90         if (!container && containerRequest.containerUuid) {
91             // Get the container the old fashioned way
92             try {
93                 container = await services.containerService.get(containerRequest.containerUuid, false);
94                 dispatch<any>(updateResources([container]));
95             } catch {}
96         }
97
98         if (container && container.runtimeUserUuid) {
99             try {
100                 const runtimeUser = await services.userService.get(container.runtimeUserUuid, false);
101                 dispatch<any>(updateResources([runtimeUser]));
102             } catch {}
103         }
104
105         if (containerRequest.outputUuid) {
106             try {
107                 const collection = await services.collectionService.get(containerRequest.outputUuid, false);
108                 dispatch<any>(updateResources([collection]));
109             } catch {}
110         }
111
112         return { containerRequest, container };
113     };
114
115 export const loadProcessPanel = (uuid: string) => async (dispatch: Dispatch, getState: () => RootState): Promise<Process | undefined> => {
116     // Reset subprocess data explorer if navigating to new process
117     //  Avoids resetting pagination when refreshing same process
118     if (getState().processPanel.containerRequestUuid !== uuid) {
119         dispatch(subprocessPanelActions.CLEAR());
120     }
121     dispatch(processPanelActions.RESET_PROCESS_PANEL());
122     dispatch(processLogsPanelActions.RESET_PROCESS_LOGS_PANEL());
123     dispatch<ProcessPanelAction>(processPanelActions.SET_PROCESS_PANEL_CONTAINER_REQUEST_UUID(uuid));
124     const process = await dispatch<any>(loadProcess(uuid));
125     dispatch(initProcessPanelFilters);
126     dispatch<any>(initProcessLogsPanel(uuid));
127     dispatch<any>(loadSubprocessPanel());
128     return process;
129 };
130
131 export const navigateToOutput = (resource: ContextMenuResource | ContainerRequestResource) => async (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
132     try {
133         await services.collectionService.get(resource.outputUuid || '');
134         dispatch<any>(navigateTo(resource.outputUuid || ''));
135     } catch {
136         dispatch(snackbarActions.OPEN_SNACKBAR({ message: "Output collection was trashed or deleted.", hideDuration: 4000, kind: SnackbarKind.WARNING }));
137     }
138 };
139
140 export const loadInputs =
141     (containerRequest: ContainerRequestResource) => async (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
142         dispatch<ProcessPanelAction>(processPanelActions.SET_INPUT_RAW(getRawInputs(containerRequest)));
143         dispatch<ProcessPanelAction>(processPanelActions.SET_INPUT_PARAMS(formatInputData(getInputs(containerRequest), getState().auth)));
144     };
145
146 export const loadOutputs =
147     (containerRequest: ContainerRequestResource) => async (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
148         const noOutputs: OutputDetails = { raw: {} };
149
150         if (!containerRequest.outputUuid) {
151             dispatch<ProcessPanelAction>(processPanelActions.SET_OUTPUT_DATA({
152                 uuid: containerRequest.uuid,
153                 payload: noOutputs
154             }));
155             return;
156         }
157         let propsOutputs: any = undefined;
158         try {
159             propsOutputs = getRawOutputs(containerRequest);
160             const filesPromise = services.collectionService.files(containerRequest.outputUuid);
161             const collectionPromise = services.collectionService.get(containerRequest.outputUuid);
162             const [files, collection] = await Promise.all([filesPromise, collectionPromise]);
163
164             // If has propsOutput, skip fetching cwl.output.json
165             if (propsOutputs !== undefined) {
166                 dispatch<ProcessPanelAction>(
167                     processPanelActions.SET_OUTPUT_DATA({
168                         uuid: containerRequest.uuid,
169                         payload: {
170                             raw: propsOutputs,
171                             pdh: collection.portableDataHash,
172                         },
173                     })
174                 );
175             } else {
176                 // Fetch outputs from keep
177                 const outputFile = files.find(file => file.name === "cwl.output.json") as CollectionFile | undefined;
178                 let outputData = outputFile ? await services.collectionService.getFileContents(outputFile) : undefined;
179                 if (outputData && (outputData = JSON.parse(outputData)) && collection.portableDataHash) {
180                     dispatch<ProcessPanelAction>(
181                         processPanelActions.SET_OUTPUT_DATA({
182                             uuid: containerRequest.uuid,
183                             payload: {
184                                 raw: outputData,
185                                 pdh: collection.portableDataHash,
186                             },
187                         })
188                     );
189                 } else {
190                     dispatch<ProcessPanelAction>(processPanelActions.SET_OUTPUT_DATA({ uuid: containerRequest.uuid, payload: noOutputs }));
191                 }
192             }
193         } catch {
194             dispatch<ProcessPanelAction>(processPanelActions.SET_OUTPUT_DATA({ uuid: containerRequest.uuid, payload: { raw: propsOutputs, failedToLoadOutputCollection: true } }));
195         }
196     };
197
198 export const loadNodeJson =
199     (containerRequest: ContainerRequestResource) => async (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
200         const noLog = { nodeInfo: null };
201         if (!containerRequest.logUuid) {
202             dispatch<ProcessPanelAction>(processPanelActions.SET_NODE_INFO(noLog));
203             return;
204         }
205         try {
206             const filesPromise = services.collectionService.files(containerRequest.logUuid);
207             const collectionPromise = services.collectionService.get(containerRequest.logUuid);
208             const [files] = await Promise.all([filesPromise, collectionPromise]);
209
210             // Fetch node.json from keep
211             const nodeFile = files.find(file => file.name === "node.json") as CollectionFile | undefined;
212             let nodeData = nodeFile ? await services.collectionService.getFileContents(nodeFile) : undefined;
213             if (nodeData && (nodeData = JSON.parse(nodeData))) {
214                 dispatch<ProcessPanelAction>(
215                     processPanelActions.SET_NODE_INFO({
216                         nodeInfo: nodeData as NodeInstanceType,
217                     })
218                 );
219             } else {
220                 dispatch<ProcessPanelAction>(processPanelActions.SET_NODE_INFO(noLog));
221             }
222
223             const usageReportFile = files.find(file => file.name === "usage_report.html") as CollectionFile | null;
224             dispatch<ProcessPanelAction>(processPanelActions.SET_USAGE_REPORT({ usageReport: usageReportFile }));
225         } catch {
226             dispatch<ProcessPanelAction>(processPanelActions.SET_NODE_INFO(noLog));
227             dispatch<ProcessPanelAction>(processPanelActions.SET_USAGE_REPORT({ usageReport: null }));
228         }
229     };
230
231 export const loadOutputDefinitions =
232     (containerRequest: ContainerRequestResource) => async (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
233         if (containerRequest && containerRequest.mounts) {
234             dispatch<ProcessPanelAction>(processPanelActions.SET_OUTPUT_DEFINITIONS(getOutputParameters(containerRequest)));
235         }
236     };
237
238 export const updateOutputParams = () => async (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
239     const outputDefinitions = getState().processPanel.outputDefinitions;
240     const outputData = getState().processPanel.outputData;
241
242     if (outputData && outputData.raw) {
243         dispatch<ProcessPanelAction>(
244             processPanelActions.SET_OUTPUT_PARAMS(formatOutputData(outputDefinitions, outputData.raw, outputData.pdh, getState().auth))
245         );
246     }
247 };
248
249 export const openWorkflow = (uuid: string) => (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
250     dispatch<any>(navigateTo(uuid));
251 };
252
253 export const initProcessPanelFilters = processPanelActions.SET_PROCESS_PANEL_FILTERS([
254     ProcessStatus.QUEUED,
255     ProcessStatus.COMPLETED,
256     ProcessStatus.FAILED,
257     ProcessStatus.RUNNING,
258     ProcessStatus.ONHOLD,
259     ProcessStatus.FAILING,
260     ProcessStatus.WARNING,
261     ProcessStatus.CANCELLED,
262 ]);
263
264 export const formatInputData = (inputs: CommandInputParameter[], auth: AuthState): ProcessIOParameter[] => {
265     return inputs.flatMap((input): ProcessIOParameter[] => {
266         const processValues = getIOParamDisplayValue(auth, input);
267         return processValues.map((thisValue, i) => ({
268             id: i === 0 ? getIOParamId(input) : "",
269             label: i === 0 ? input.label || "" : "",
270             value: thisValue,
271         }));
272     });
273 };
274
275 export const formatOutputData = (
276     definitions: CommandOutputParameter[],
277     values: any,
278     pdh: string | undefined,
279     auth: AuthState
280 ): ProcessIOParameter[] => {
281     return definitions.flatMap((output): ProcessIOParameter[] => {
282         const processValues = getIOParamDisplayValue(auth, Object.assign(output, { value: values[getIOParamId(output)] || [] }), pdh);
283         return processValues.map((thisValue, i) => ({
284             id: i === 0 ? getIOParamId(output) : "",
285             label: i === 0 ? output.label || "" : "",
286             value: thisValue,
287         }));
288     });
289 };