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