13327: Tests should be mostly passing
[arvados.git] / services / workbench2 / src / views / process-panel / process-details-attributes.tsx
1 // Copyright (C) The Arvados Authors. All rights reserved.
2 //
3 // SPDX-License-Identifier: AGPL-3.0
4
5 import React from "react";
6 import { CustomStyleRulesCallback } from 'common/custom-theme';
7 import { Grid, Typography } from "@mui/material";
8 import withStyles from '@mui/styles/withStyles';
9 import { Dispatch } from 'redux';
10 import { formatCost, formatDate } from "common/formatters";
11 import { resourceLabel } from "common/labels";
12 import { DetailsAttribute } from "components/details-attribute/details-attribute";
13 import { ResourceKind } from "models/resource";
14 import { CollectionName, ContainerRunTime, ResourceWithName } from "views-components/data-explorer/renderers";
15 import { getProcess, getProcessStatus, ProcessProperties } from "store/processes/process";
16 import { RootState } from "store/store";
17 import { connect } from "react-redux";
18 import { ProcessResource, MOUNT_PATH_CWL_WORKFLOW } from "models/process";
19 import { ContainerResource, ContainerState } from "models/container";
20 import { navigateToOutput, openWorkflow, loadContainerStatus } from "store/process-panel/process-panel-actions";
21 import { ArvadosTheme } from "common/custom-theme";
22 import { ProcessRuntimeStatus } from "views-components/process-runtime-status/process-runtime-status";
23 import { getPropertyChip } from "views-components/resource-properties-form/property-chip";
24 import { ContainerRequestResource, ContainerRequestState } from "models/container-request";
25 import { filterResources } from "store/resources/resources";
26 import { JSONMount } from 'models/mount-types';
27 import { getCollectionUrl } from 'models/collection';
28 import { useAsyncInterval } from 'common/use-async-interval';
29 import { Link } from "react-router-dom";
30 import { getResourceUrl } from "routes/routes";
31 import WarningIcon from '@mui/icons-material/Warning';
32
33 type CssRules = 'link' | 'propertyTag';
34
35 const styles: CustomStyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
36     link: {
37         fontSize: '0.875rem',
38         color: theme.palette.primary.main,
39         '&:hover': {
40             cursor: 'pointer'
41         }
42     },
43     propertyTag: {
44         marginRight: theme.spacing(0.5),
45         marginBottom: theme.spacing(0.5)
46     },
47 });
48
49 const mapStateToProps = (state: RootState, props: { request: ProcessResource, container?: ContainerResource }) => {
50     const process = getProcess(props.request.uuid)(state.resources);
51
52     let workflowCollection = "";
53     let workflowPath = "";
54     let schedulingStatus = "";
55     if (process?.containerRequest?.mounts && process.containerRequest.mounts[MOUNT_PATH_CWL_WORKFLOW]) {
56         const wf = process.containerRequest.mounts[MOUNT_PATH_CWL_WORKFLOW] as JSONMount;
57
58         if (process?.container &&
59             state.processPanel.containerStatus?.uuid === process?.container?.uuid)
60         {
61             schedulingStatus = state.processPanel.containerStatus.schedulingStatus;
62         }
63
64         if (wf.content["$graph"] &&
65             wf.content["$graph"].length > 0 &&
66             wf.content["$graph"][0] &&
67             wf.content["$graph"][0]["steps"] &&
68             wf.content["$graph"][0]["steps"][0]) {
69
70             const REGEX = /keep:([0-9a-f]{32}\+\d+)\/(.*)/;
71             const pdh = wf.content["$graph"][0]["steps"][0].run.match(REGEX);
72             if (pdh) {
73                 workflowCollection = pdh[1];
74                 workflowPath = pdh[2];
75             }
76         }
77     }
78
79     return {
80         container: process?.container,
81         workflowCollection,
82         workflowPath,
83         schedulingStatus,
84         subprocesses: filterResources((resource: ContainerRequestResource) =>
85             (resource.kind === ResourceKind.CONTAINER_REQUEST &&
86              resource.requestingContainerUuid === process?.containerRequest.containerUuid)
87         )(state.resources),
88     };
89 };
90
91 interface ProcessDetailsAttributesActionProps {
92     navigateToOutput: (resource: ContainerRequestResource) => void;
93     openWorkflow: (uuid: string) => void;
94     pollSchedulingStatus: (uuid: string) => void;
95 }
96
97 const mapDispatchToProps = (dispatch: Dispatch): ProcessDetailsAttributesActionProps => ({
98     navigateToOutput: (resource) => dispatch<any>(navigateToOutput(resource)),
99     openWorkflow: (uuid) => dispatch<any>(openWorkflow(uuid)),
100     pollSchedulingStatus: (uuid) => dispatch<any>(loadContainerStatus(uuid)),
101 });
102
103 const isProcessScheduling = (container?: ContainerResource): boolean => (
104     container?.state === ContainerState.QUEUED || container?.state === ContainerState.LOCKED
105 );
106
107 export const ProcessDetailsAttributes = withStyles(styles, { withTheme: true })(
108     connect(mapStateToProps, mapDispatchToProps)(
109         (props: {
110             request: ProcessResource,
111             container?: ContainerResource,
112             subprocesses: ContainerRequestResource[],
113             workflowCollection,
114             workflowPath,
115             schedulingStatus,
116             twoCol?: boolean,
117             hideProcessPanelRedundantFields?: boolean,
118             pollSchedulingStatus: (processUuid: string) => Promise<void>,
119             classes: Record<CssRules, string>
120         } & ProcessDetailsAttributesActionProps) => {
121             const containerRequest = props.request;
122             const container = props.container;
123             const subprocesses = props.subprocesses;
124             const classes = props.classes;
125             const mdSize = props.twoCol ? 6 : 12;
126             const workflowCollection = props.workflowCollection;
127             const workflowPath = props.workflowPath;
128             const filteredPropertyKeys = Object.keys(containerRequest.properties)
129                                                .filter(k => (typeof containerRequest.properties[k] !== 'object'));
130             const hasTotalCost = containerRequest && containerRequest.cumulativeCost > 0;
131             const totalCostNotReady = container && container.cost > 0 && container.state === "Running" && containerRequest && containerRequest.cumulativeCost === 0 && subprocesses.length > 0;
132             let schedulingStatus = props.schedulingStatus;
133             const resubmittedUrl = containerRequest && getResourceUrl(containerRequest.properties[ProcessProperties.FAILED_CONTAINER_RESUBMITTED]);
134
135             useAsyncInterval(() => (
136                 props.pollSchedulingStatus(containerRequest.uuid)
137             ), isProcessScheduling(container) ? 5000 : null);
138
139             if (containerRequest.state === ContainerRequestState.UNCOMMITTED) {
140                 schedulingStatus = "In draft state, not ready to run";
141             }
142
143             return <Grid container>
144             <Grid item xs={12}>
145                 <ProcessRuntimeStatus runtimeStatus={container?.runtimeStatus} containerCount={containerRequest.containerCount} schedulingStatus={schedulingStatus} />
146             </Grid>
147             {!props.hideProcessPanelRedundantFields && <Grid item xs={12} md={mdSize}>
148                 <DetailsAttribute label='Type' value={resourceLabel(ResourceKind.PROCESS)} />
149             </Grid>}
150             {resubmittedUrl && <Grid item xs={12}>
151                 <Typography>
152                     <WarningIcon />
153                     This process failed but was automatically resubmitted.  <Link to={resubmittedUrl}> Click here to go to the resubmitted process.</Link>
154                 </Typography>
155             </Grid>}
156             <Grid item xs={12} md={mdSize}>
157                 <DetailsAttribute label='Container request UUID' linkToUuid={containerRequest.uuid} value={containerRequest.uuid} />
158             </Grid>
159             <Grid item xs={12} md={mdSize}>
160                 <DetailsAttribute label='Docker image locator'
161                                   linkToUuid={containerRequest.containerImage} value={containerRequest.containerImage} />
162             </Grid>
163             <Grid item xs={12} md={mdSize}>
164                 <DetailsAttribute
165                     label='Owner' linkToUuid={containerRequest.ownerUuid}
166                     uuidEnhancer={(uuid: string) => <ResourceWithName uuid={uuid} />} />
167             </Grid>
168             <Grid item xs={12} md={mdSize}>
169                 <DetailsAttribute label='Container UUID' value={containerRequest.containerUuid} />
170             </Grid>
171             {!props.hideProcessPanelRedundantFields && <Grid item xs={12} md={mdSize}>
172                 <DetailsAttribute label='Status' value={getProcessStatus({ containerRequest, container })} />
173             </Grid>}
174             <Grid item xs={12} md={mdSize}>
175                 <DetailsAttribute label='Created at' value={formatDate(containerRequest.createdAt)} />
176             </Grid>
177             <Grid item xs={12} md={mdSize}>
178                 <DetailsAttribute label='Started at' value={container ? formatDate(container.startedAt) : "(none)"} />
179             </Grid>
180             <Grid item xs={12} md={mdSize}>
181                 <DetailsAttribute label='Finished at' value={container ? formatDate(container.finishedAt) : "(none)"} />
182             </Grid>
183             <Grid item xs={12} md={mdSize}>
184                 <DetailsAttribute label='Container run time'>
185                     <ContainerRunTime uuid={containerRequest.uuid} />
186                 </DetailsAttribute>
187             </Grid>
188             {(containerRequest && containerRequest.modifiedByUserUuid) && <Grid item xs={12} md={mdSize} data-cy="process-details-attributes-modifiedby-user">
189                 <DetailsAttribute
190                     label='Submitted by' linkToUuid={containerRequest.modifiedByUserUuid}
191                     uuidEnhancer={(uuid: string) => <ResourceWithName uuid={uuid} />} />
192             </Grid>}
193             {(container && container.runtimeUserUuid && container.runtimeUserUuid !== containerRequest.modifiedByUserUuid) && <Grid item xs={12} md={mdSize} data-cy="process-details-attributes-runtime-user">
194                 <DetailsAttribute
195                     label='Run as' linkToUuid={container.runtimeUserUuid}
196                     uuidEnhancer={(uuid: string) => <ResourceWithName uuid={uuid} />} />
197             </Grid>}
198             <Grid item xs={12} md={mdSize}>
199                 <DetailsAttribute label='Requesting container UUID' value={containerRequest.requestingContainerUuid || "(none)"} />
200             </Grid>
201             <Grid item xs={6}>
202                 <DetailsAttribute label='Output collection' />
203                 {containerRequest.outputUuid && <span onClick={() => props.navigateToOutput(containerRequest!)}>
204                     <CollectionName className={classes.link} uuid={containerRequest.outputUuid} />
205                 </span>}
206             </Grid>
207             {container && <Grid item xs={12} md={mdSize}>
208                 <DetailsAttribute label='Cost' value={
209                 `${hasTotalCost ? formatCost(containerRequest.cumulativeCost) + ' total, ' : (totalCostNotReady ? 'total pending completion, ' : '')}${container.cost > 0 ? formatCost(container.cost) : 'not available'} for this container`
210                 } />
211             </Grid>}
212             {container && workflowCollection && <Grid item xs={12} md={mdSize}>
213                 <DetailsAttribute label='Workflow code' link={getCollectionUrl(workflowCollection)} value={workflowPath} />
214             </Grid>}
215             {containerRequest.properties.template_uuid &&
216              <Grid item xs={12} md={mdSize}>
217                  <span onClick={() => props.openWorkflow(containerRequest.properties.template_uuid)}>
218                      <DetailsAttribute classValue={classes.link}
219                                        label='Workflow' value={containerRequest.properties.workflowName} />
220                  </span>
221              </Grid>}
222             <Grid item xs={12} md={mdSize}>
223                 <DetailsAttribute label='Priority' value={containerRequest.priority} />
224             </Grid>
225             {/*
226                 NOTE: The property list should be kept at the bottom, because it spans
227                 the entire available width, without regards of the twoCol prop.
228               */}
229             <Grid item xs={12} md={12}>
230                 <DetailsAttribute label='Properties' />
231                 {filteredPropertyKeys.length > 0
232                                              ? filteredPropertyKeys.map(k =>
233                                                  Array.isArray(containerRequest.properties[k])
234                                                  ? containerRequest.properties[k].map((v: string) =>
235                                                      getPropertyChip(k, v, undefined, classes.propertyTag))
236                                                  : getPropertyChip(k, containerRequest.properties[k], undefined, classes.propertyTag))
237                                              : <div>No properties</div>}
238             </Grid>
239             </Grid>;
240         }
241     )
242 );