15397: Remove default_owner_uuid from SDKs and workbench.
[arvados.git] / services / workbench2 / src / views / process-panel / process-io-card.tsx
1 // Copyright (C) The Arvados Authors. All rights reserved.
2 //
3 // SPDX-License-Identifier: AGPL-3.0
4
5 import React, { ReactElement, memo } from "react";
6 import { Dispatch } from "redux";
7 import {
8     StyleRulesCallback,
9     WithStyles,
10     withStyles,
11     Card,
12     CardHeader,
13     IconButton,
14     CardContent,
15     Tooltip,
16     Typography,
17     Table,
18     TableHead,
19     TableBody,
20     TableRow,
21     TableCell,
22     Paper,
23     Grid,
24     Chip,
25     CircularProgress,
26 } from "@material-ui/core";
27 import { ArvadosTheme } from "common/custom-theme";
28 import { CloseIcon, InputIcon, OutputIcon, MaximizeIcon, UnMaximizeIcon, InfoIcon } from "components/icon/icon";
29 import { MPVPanelProps } from "components/multi-panel-view/multi-panel-view";
30 import {
31     BooleanCommandInputParameter,
32     CommandInputParameter,
33     CWLType,
34     Directory,
35     DirectoryArrayCommandInputParameter,
36     DirectoryCommandInputParameter,
37     EnumCommandInputParameter,
38     FileArrayCommandInputParameter,
39     FileCommandInputParameter,
40     FloatArrayCommandInputParameter,
41     FloatCommandInputParameter,
42     IntArrayCommandInputParameter,
43     IntCommandInputParameter,
44     isArrayOfType,
45     isPrimitiveOfType,
46     StringArrayCommandInputParameter,
47     StringCommandInputParameter,
48     getEnumType,
49 } from "models/workflow";
50 import { CommandOutputParameter } from "cwlts/mappings/v1.0/CommandOutputParameter";
51 import { File } from "models/workflow";
52 import { getInlineFileUrl } from "views-components/context-menu/actions/helpers";
53 import { AuthState } from "store/auth/auth-reducer";
54 import mime from "mime";
55 import { DefaultView } from "components/default-view/default-view";
56 import { getNavUrl } from "routes/routes";
57 import { Link as RouterLink } from "react-router-dom";
58 import { Link as MuiLink } from "@material-ui/core";
59 import { InputCollectionMount } from "store/processes/processes-actions";
60 import { connect } from "react-redux";
61 import { RootState } from "store/store";
62 import { ProcessOutputCollectionFiles } from "./process-output-collection-files";
63 import { Process } from "store/processes/process";
64 import { navigateTo } from "store/navigation/navigation-action";
65 import classNames from "classnames";
66 import { DefaultVirtualCodeSnippet } from "components/default-code-snippet/default-virtual-code-snippet";
67 import { KEEP_URL_REGEX } from "models/resource";
68 import { FixedSizeList } from 'react-window';
69 import AutoSizer from "react-virtualized-auto-sizer";
70 import { LinkProps } from "@material-ui/core/Link";
71 import { ConditionalTabs } from "components/conditional-tabs/conditional-tabs";
72
73 type CssRules =
74     | "card"
75     | "content"
76     | "title"
77     | "header"
78     | "avatar"
79     | "iconHeader"
80     | "tableWrapper"
81     | "paramTableRoot"
82     | "paramTableCellText"
83     | "mountsTableRoot"
84     | "jsonWrapper"
85     | "keepLink"
86     | "collectionLink"
87     | "secondaryVal"
88     | "emptyValue"
89     | "noBorderRow"
90     | "symmetricTabs"
91     | "wrapTooltip";
92
93 const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
94     card: {
95         height: "100%",
96     },
97     header: {
98         paddingTop: theme.spacing.unit,
99         paddingBottom: 0,
100     },
101     iconHeader: {
102         fontSize: "1.875rem",
103         color: theme.customs.colors.greyL,
104     },
105     avatar: {
106         alignSelf: "flex-start",
107         paddingTop: theme.spacing.unit * 0.5,
108     },
109     // Card content
110     content: {
111         height: `calc(100% - ${theme.spacing.unit * 6}px)`,
112         padding: theme.spacing.unit * 1.0,
113         paddingTop: 0,
114         "&:last-child": {
115             paddingBottom: theme.spacing.unit * 1,
116         },
117     },
118     // Card title
119     title: {
120         overflow: "hidden",
121         paddingTop: theme.spacing.unit * 0.5,
122         color: theme.customs.colors.greyD,
123         fontSize: "1.875rem",
124     },
125     // Applies to table tab and collection table content
126     tableWrapper: {
127         height: "auto",
128         maxHeight: `calc(100% - ${theme.spacing.unit * 6}px)`,
129         overflow: "auto",
130         // Use flexbox to keep scrolling at the virtual list level
131         display: "flex",
132         flexDirection: "column",
133         alignItems: "stretch", // Stretches output collection to full width
134
135     },
136
137     // Param table virtual list styles
138     paramTableRoot: {
139         display: "flex",
140         flexDirection: "column",
141         overflow: "hidden",
142         // Flex header
143         "& thead tr": {
144             alignItems: "end",
145             "& th": {
146                 padding: "4px 25px 10px",
147             },
148         },
149         "& tbody": {
150             height: "100vh", // Must be constrained by panel maxHeight
151         },
152         // Flex header/body rows
153         "& thead tr, & > tbody tr": {
154             display: "flex",
155             // Flex header/body cells
156             "& th, & td": {
157                 flexGrow: 1,
158                 flexShrink: 1,
159                 flexBasis: 0,
160                 overflow: "hidden",
161             },
162             // Column width overrides
163             "& th:nth-of-type(1), & td:nth-of-type(1)": {
164                 flexGrow: 0.7,
165             },
166             "& th:nth-last-of-type(1), & td:nth-last-of-type(1)": {
167                 flexGrow: 2,
168             },
169         },
170         // Flex body rows
171         "& tbody tr": {
172             height: "40px",
173             // Flex body cells
174             "& td": {
175                 padding: "2px 25px 2px",
176                 overflow: "hidden",
177                 display: "flex",
178                 flexDirection: "row",
179                 alignItems: "center",
180                 whiteSpace: "nowrap",
181             },
182         },
183     },
184     // Param value cell typography styles
185     paramTableCellText: {
186         overflow: "hidden",
187         display: "flex",
188         // Every cell contents requires a wrapper for the ellipsis
189         // since adding ellipses to an anchor element parent results in misaligned tooltip
190         "& a, & span": {
191             overflow: "hidden",
192             textOverflow: "ellipsis",
193         },
194         '& pre': {
195             margin: 0,
196             overflow: "hidden",
197             textOverflow: "ellipsis",
198         },
199     },
200     mountsTableRoot: {
201         width: "100%",
202         "& thead th": {
203             verticalAlign: "bottom",
204             paddingBottom: "10px",
205         },
206         "& td, & th": {
207             paddingRight: "25px",
208         },
209     },
210     // JSON tab wrapper
211     jsonWrapper: {
212         height: `calc(100% - ${theme.spacing.unit * 6}px)`,
213     },
214     keepLink: {
215         color: theme.palette.primary.main,
216         textDecoration: "none",
217         // Overflow wrap for mounts table
218         overflowWrap: "break-word",
219         cursor: "pointer",
220     },
221     // Output collection tab link
222     collectionLink: {
223         margin: "10px",
224         "& a": {
225             color: theme.palette.primary.main,
226             textDecoration: "none",
227             overflowWrap: "break-word",
228             cursor: "pointer",
229         },
230     },
231     secondaryVal: {
232         paddingLeft: "20px",
233     },
234     emptyValue: {
235         color: theme.customs.colors.grey700,
236     },
237     noBorderRow: {
238         "& td": {
239             borderBottom: "none",
240             paddingTop: "2px",
241             paddingBottom: "2px",
242         },
243         height: "24px",
244     },
245     symmetricTabs: {
246         "& button": {
247             flexBasis: "0",
248         },
249     },
250     wrapTooltip: {
251         maxWidth: "600px",
252         wordWrap: "break-word",
253     },
254 });
255
256 export enum ProcessIOCardType {
257     INPUT = "Input Parameters",
258     OUTPUT = "Output Parameters",
259 }
260 export interface ProcessIOCardDataProps {
261     process?: Process;
262     label: ProcessIOCardType;
263     params: ProcessIOParameter[] | null;
264     raw: any;
265     mounts?: InputCollectionMount[];
266     outputUuid?: string;
267     forceShowParams?: boolean;
268 }
269
270 type ProcessIOCardProps = ProcessIOCardDataProps & WithStyles<CssRules> & MPVPanelProps;
271
272 export const ProcessIOCard = withStyles(styles)(
273     ({
274         classes,
275         label,
276         params,
277         raw,
278         mounts,
279         outputUuid,
280         doHidePanel,
281         doMaximizePanel,
282         doUnMaximizePanel,
283         panelMaximized,
284         panelName,
285         process,
286         forceShowParams,
287     }: ProcessIOCardProps) => {
288         const PanelIcon = label === ProcessIOCardType.INPUT ? InputIcon : OutputIcon;
289         const mainProcess = !(process && process!.containerRequest.requestingContainerUuid);
290         const showParamTable = mainProcess || forceShowParams;
291
292         const loading = raw === null || raw === undefined || params === null;
293
294         const hasRaw = !!(raw && Object.keys(raw).length > 0);
295         const hasParams = !!(params && params.length > 0);
296         // isRawLoaded allows subprocess panel to display raw even if it's {}
297         const isRawLoaded = !!(raw && Object.keys(raw).length >= 0);
298
299         // Subprocess
300         const hasInputMounts = !!(label === ProcessIOCardType.INPUT && mounts && mounts.length);
301         const hasOutputCollecton = !!(label === ProcessIOCardType.OUTPUT && outputUuid);
302         // Subprocess should not show loading if hasOutputCollection or hasInputMounts
303         const subProcessLoading = loading && !hasOutputCollecton && !hasInputMounts;
304
305         return (
306             <Card
307                 className={classes.card}
308                 data-cy="process-io-card"
309             >
310                 <CardHeader
311                     className={classes.header}
312                     classes={{
313                         content: classes.title,
314                         avatar: classes.avatar,
315                     }}
316                     avatar={<PanelIcon className={classes.iconHeader} />}
317                     title={
318                         <Typography
319                             noWrap
320                             variant="h6"
321                             color="inherit"
322                         >
323                             {label}
324                         </Typography>
325                     }
326                     action={
327                         <div>
328                             {doUnMaximizePanel && panelMaximized && (
329                                 <Tooltip
330                                     title={`Unmaximize ${panelName || "panel"}`}
331                                     disableFocusListener
332                                 >
333                                     <IconButton onClick={doUnMaximizePanel}>
334                                         <UnMaximizeIcon />
335                                     </IconButton>
336                                 </Tooltip>
337                             )}
338                             {doMaximizePanel && !panelMaximized && (
339                                 <Tooltip
340                                     title={`Maximize ${panelName || "panel"}`}
341                                     disableFocusListener
342                                 >
343                                     <IconButton onClick={doMaximizePanel}>
344                                         <MaximizeIcon />
345                                     </IconButton>
346                                 </Tooltip>
347                             )}
348                             {doHidePanel && (
349                                 <Tooltip
350                                     title={`Close ${panelName || "panel"}`}
351                                     disableFocusListener
352                                 >
353                                     <IconButton
354                                         disabled={panelMaximized}
355                                         onClick={doHidePanel}
356                                     >
357                                         <CloseIcon />
358                                     </IconButton>
359                                 </Tooltip>
360                             )}
361                         </div>
362                     }
363                 />
364                 <CardContent className={classes.content}>
365                     {showParamTable ? (
366                         <>
367                             {/* raw is undefined until params are loaded */}
368                             {loading && (
369                                 <Grid
370                                     container
371                                     item
372                                     alignItems="center"
373                                     justify="center"
374                                 >
375                                     <CircularProgress />
376                                 </Grid>
377                             )}
378                             {/* Once loaded, either raw or params may still be empty
379                                 *   Raw when all params are empty
380                                 *   Params when raw is provided by containerRequest properties but workflow mount is absent for preview
381                                 */}
382                             {!loading && (hasRaw || hasParams) && (
383                                 <ConditionalTabs
384                                     variant="fullWidth"
385                                     className={classes.symmetricTabs}
386                                     tabs={[
387                                         {
388                                             // params will be empty on processes without workflow definitions in mounts, so we only show raw
389                                             show: hasParams,
390                                             label: "Parameters",
391                                             content: <ProcessIOPreview
392                                                     data={params || []}
393                                                     valueLabel={forceShowParams ? "Default value" : "Value"}
394                                             />,
395                                         },
396                                         {
397                                             show: !forceShowParams,
398                                             label: "JSON",
399                                             content: <ProcessIORaw data={raw} />,
400                                         },
401                                         {
402                                             show: hasOutputCollecton,
403                                             label: "Collection",
404                                             content: <ProcessOutputCollection outputUuid={outputUuid} />,
405                                         },
406                                     ]}
407                                 />
408                             )}
409                             {!loading && !hasRaw && !hasParams && (
410                                 <Grid
411                                     container
412                                     item
413                                     alignItems="center"
414                                     justify="center"
415                                 >
416                                     <DefaultView messages={["No parameters found"]} />
417                                 </Grid>
418                             )}
419                         </>
420                     ) : (
421                         // Subprocess
422                         <>
423                             {subProcessLoading ? (
424                                 <Grid
425                                     container
426                                     item
427                                     alignItems="center"
428                                     justify="center"
429                                 >
430                                     <CircularProgress />
431                                 </Grid>
432                             ) : !subProcessLoading && (hasInputMounts || hasOutputCollecton || isRawLoaded) ? (
433                                 <ConditionalTabs
434                                     variant="fullWidth"
435                                     className={classes.symmetricTabs}
436                                     tabs={[
437                                         {
438                                             show: hasInputMounts,
439                                             label: "Collections",
440                                             content: <ProcessInputMounts mounts={mounts || []} />,
441                                         },
442                                         {
443                                             show: hasOutputCollecton,
444                                             label: "Collection",
445                                             content: <ProcessOutputCollection outputUuid={outputUuid} />,
446                                         },
447                                         {
448                                             show: isRawLoaded,
449                                             label: "JSON",
450                                             content: <ProcessIORaw data={raw} />,
451                                         },
452                                     ]}
453                                 />
454                             ) : (
455                                 <Grid
456                                     container
457                                     item
458                                     alignItems="center"
459                                     justify="center"
460                                 >
461                                     <DefaultView messages={["No data to display"]} />
462                                 </Grid>
463                             )}
464                         </>
465                     )}
466                 </CardContent>
467             </Card>
468         );
469     }
470 );
471
472 export type ProcessIOValue = {
473     display: ReactElement<any, any>;
474     imageUrl?: string;
475     collection?: ReactElement<any, any>;
476     secondary?: boolean;
477 };
478
479 export type ProcessIOParameter = {
480     id: string;
481     label: string;
482     value: ProcessIOValue;
483 };
484
485 interface ProcessIOPreviewDataProps {
486     data: ProcessIOParameter[];
487     valueLabel: string;
488     hidden?: boolean;
489 }
490
491 type ProcessIOPreviewProps = ProcessIOPreviewDataProps & WithStyles<CssRules>;
492
493 const ProcessIOPreview = memo(
494     withStyles(styles)(({ data, valueLabel, hidden, classes }: ProcessIOPreviewProps) => {
495         const showLabel = data.some((param: ProcessIOParameter) => param.label);
496
497         const hasMoreValues = (index: number) => (
498             data[index+1] && !isMainRow(data[index+1])
499         );
500
501         const isMainRow = (param: ProcessIOParameter) => (
502             param &&
503             ((param.id || param.label) &&
504             !param.value.secondary)
505         );
506
507         const RenderRow = ({index, style}) => {
508             const param = data[index];
509
510             const rowClasses = {
511                 [classes.noBorderRow]: hasMoreValues(index),
512             };
513
514             return <TableRow
515                 style={style}
516                 className={classNames(rowClasses)}
517                 data-cy={isMainRow(param) ? "process-io-param" : ""}>
518                 <TableCell>
519                     <Tooltip title={param.id}>
520                         <Typography className={classes.paramTableCellText}>
521                             <span>
522                                 {param.id}
523                             </span>
524                         </Typography>
525                     </Tooltip>
526                 </TableCell>
527                 {showLabel && <TableCell>
528                     <Tooltip title={param.label}>
529                         <Typography className={classes.paramTableCellText}>
530                             <span>
531                                 {param.label}
532                             </span>
533                         </Typography>
534                     </Tooltip>
535                 </TableCell>}
536                 <TableCell>
537                     <ProcessValuePreview
538                         value={param.value}
539                     />
540                 </TableCell>
541                 <TableCell>
542                     <Typography className={classes.paramTableCellText}>
543                         {/** Collection is an anchor so doesn't require wrapper element */}
544                         {param.value.collection}
545                     </Typography>
546                 </TableCell>
547             </TableRow>;
548         };
549
550         return <div className={classes.tableWrapper} hidden={hidden}>
551             <Table
552                 className={classes.paramTableRoot}
553                 aria-label="Process IO Preview"
554             >
555                 <TableHead>
556                     <TableRow>
557                         <TableCell>Name</TableCell>
558                         {showLabel && <TableCell>Label</TableCell>}
559                         <TableCell>{valueLabel}</TableCell>
560                         <TableCell>Collection</TableCell>
561                     </TableRow>
562                 </TableHead>
563                 <TableBody>
564                     <AutoSizer>
565                         {({ height, width }) =>
566                             <FixedSizeList
567                                 height={height}
568                                 itemCount={data.length}
569                                 itemSize={40}
570                                 width={width}
571                             >
572                                 {RenderRow}
573                             </FixedSizeList>
574                         }
575                     </AutoSizer>
576                 </TableBody>
577             </Table>
578         </div>;
579     })
580 );
581
582 interface ProcessValuePreviewProps {
583     value: ProcessIOValue;
584 }
585
586 const ProcessValuePreview = withStyles(styles)(({ value, classes }: ProcessValuePreviewProps & WithStyles<CssRules>) => (
587     <Typography className={classNames(classes.paramTableCellText, value.secondary && classes.secondaryVal)}>
588         {value.display}
589     </Typography>
590 ));
591
592 interface ProcessIORawDataProps {
593     data: ProcessIOParameter[];
594     hidden?: boolean;
595 }
596
597 const ProcessIORaw = withStyles(styles)(({ data, hidden, classes }: ProcessIORawDataProps & WithStyles<CssRules>) => (
598     <div className={classes.jsonWrapper} hidden={hidden}>
599         <Paper elevation={0} style={{minWidth: "100%", height: "100%"}}>
600             <DefaultVirtualCodeSnippet
601                 lines={JSON.stringify(data, null, 2).split('\n')}
602                 linked
603             />
604         </Paper>
605     </div>
606 ));
607
608 interface ProcessInputMountsDataProps {
609     mounts: InputCollectionMount[];
610     hidden?: boolean;
611 }
612
613 type ProcessInputMountsProps = ProcessInputMountsDataProps & WithStyles<CssRules>;
614
615 const ProcessInputMounts = withStyles(styles)(
616     connect((state: RootState) => ({
617         auth: state.auth,
618     }))(({ mounts, hidden, classes, auth }: ProcessInputMountsProps & { auth: AuthState }) => (
619         <Table
620             className={classes.mountsTableRoot}
621             aria-label="Process Input Mounts"
622             hidden={hidden}
623         >
624             <TableHead>
625                 <TableRow>
626                     <TableCell>Path</TableCell>
627                     <TableCell>Portable Data Hash</TableCell>
628                 </TableRow>
629             </TableHead>
630             <TableBody>
631                 {mounts.map(mount => (
632                     <TableRow key={mount.path}>
633                         <TableCell>
634                             <pre>{mount.path}</pre>
635                         </TableCell>
636                         <TableCell>
637                             <RouterLink
638                                 to={getNavUrl(mount.pdh, auth)}
639                                 className={classes.keepLink}
640                             >
641                                 {mount.pdh}
642                             </RouterLink>
643                         </TableCell>
644                     </TableRow>
645                 ))}
646             </TableBody>
647         </Table>
648     ))
649 );
650
651 export interface ProcessOutputCollectionActionProps {
652     navigateTo: (uuid: string) => void;
653 }
654
655 const mapNavigateToProps = (dispatch: Dispatch): ProcessOutputCollectionActionProps => ({
656     navigateTo: uuid => dispatch<any>(navigateTo(uuid)),
657 });
658
659 type ProcessOutputCollectionProps = {outputUuid: string | undefined, hidden?: boolean} & ProcessOutputCollectionActionProps &  WithStyles<CssRules>;
660
661 const ProcessOutputCollection = withStyles(styles)(connect(null, mapNavigateToProps)(({ outputUuid, hidden, navigateTo, classes }: ProcessOutputCollectionProps) => (
662     <div className={classes.tableWrapper} hidden={hidden}>
663         <>
664             {outputUuid && (
665                 <Typography className={classes.collectionLink}>
666                     Output Collection:{" "}
667                     <MuiLink
668                         className={classes.keepLink}
669                         onClick={() => {
670                             navigateTo(outputUuid || "");
671                         }}
672                     >
673                         {outputUuid}
674                     </MuiLink>
675                 </Typography>
676             )}
677             <ProcessOutputCollectionFiles
678                 isWritable={false}
679                 currentItemUuid={outputUuid}
680             />
681         </>
682     </div>
683 )));
684
685 type FileWithSecondaryFiles = {
686     secondaryFiles: File[];
687 };
688
689 export const getIOParamDisplayValue = (auth: AuthState, input: CommandInputParameter | CommandOutputParameter, pdh?: string): ProcessIOValue[] => {
690     switch (true) {
691         case isPrimitiveOfType(input, CWLType.BOOLEAN):
692             const boolValue = (input as BooleanCommandInputParameter).value;
693             return boolValue !== undefined && !(Array.isArray(boolValue) && boolValue.length === 0)
694                 ? [{ display: <PrimitiveTooltip data={boolValue}>{renderPrimitiveValue(boolValue, false)}</PrimitiveTooltip> }]
695                 : [{ display: <EmptyValue /> }];
696
697         case isPrimitiveOfType(input, CWLType.INT):
698         case isPrimitiveOfType(input, CWLType.LONG):
699             const intValue = (input as IntCommandInputParameter).value;
700             return intValue !== undefined &&
701                 // Missing values are empty array
702                 !(Array.isArray(intValue) && intValue.length === 0)
703                 ? [{ display: <PrimitiveTooltip data={intValue}>{renderPrimitiveValue(intValue, false)}</PrimitiveTooltip> }]
704                 : [{ display: <EmptyValue /> }];
705
706         case isPrimitiveOfType(input, CWLType.FLOAT):
707         case isPrimitiveOfType(input, CWLType.DOUBLE):
708             const floatValue = (input as FloatCommandInputParameter).value;
709             return floatValue !== undefined && !(Array.isArray(floatValue) && floatValue.length === 0)
710                 ? [{ display: <PrimitiveTooltip data={floatValue}>{renderPrimitiveValue(floatValue, false)}</PrimitiveTooltip> }]
711                 : [{ display: <EmptyValue /> }];
712
713         case isPrimitiveOfType(input, CWLType.STRING):
714             const stringValue = (input as StringCommandInputParameter).value || undefined;
715             return stringValue !== undefined && !(Array.isArray(stringValue) && stringValue.length === 0)
716                 ? [{ display: <PrimitiveTooltip data={stringValue}>{renderPrimitiveValue(stringValue, false)}</PrimitiveTooltip> }]
717                 : [{ display: <EmptyValue /> }];
718
719         case isPrimitiveOfType(input, CWLType.FILE):
720             const mainFile = (input as FileCommandInputParameter).value;
721             // secondaryFiles: File[] is not part of CommandOutputParameter so we cast to access secondaryFiles
722             const secondaryFiles = (mainFile as unknown as FileWithSecondaryFiles)?.secondaryFiles || [];
723             const files = [...(mainFile && !(Array.isArray(mainFile) && mainFile.length === 0) ? [mainFile] : []), ...secondaryFiles];
724             const mainFilePdhUrl = mainFile ? getResourcePdhUrl(mainFile, pdh) : "";
725             return files.length
726                 ? files.map((file, i) => fileToProcessIOValue(file, i > 0, auth, pdh, i > 0 ? mainFilePdhUrl : ""))
727                 : [{ display: <EmptyValue /> }];
728
729         case isPrimitiveOfType(input, CWLType.DIRECTORY):
730             const directory = (input as DirectoryCommandInputParameter).value;
731             return directory !== undefined && !(Array.isArray(directory) && directory.length === 0)
732                 ? [directoryToProcessIOValue(directory, auth, pdh)]
733                 : [{ display: <EmptyValue /> }];
734
735         case getEnumType(input) !== null:
736             const enumValue = (input as EnumCommandInputParameter).value;
737             return enumValue !== undefined && enumValue ? [{ display: <PrimitiveTooltip data={enumValue}>{enumValue}</PrimitiveTooltip> }] : [{ display: <EmptyValue /> }];
738
739         case isArrayOfType(input, CWLType.STRING):
740             const strArray = (input as StringArrayCommandInputParameter).value || [];
741             return strArray.length ? [{ display: <PrimitiveArrayTooltip data={strArray}>{strArray.map(val => renderPrimitiveValue(val, true))}</PrimitiveArrayTooltip> }] : [{ display: <EmptyValue /> }];
742
743         case isArrayOfType(input, CWLType.INT):
744         case isArrayOfType(input, CWLType.LONG):
745             const intArray = (input as IntArrayCommandInputParameter).value || [];
746             return intArray.length ? [{ display: <PrimitiveArrayTooltip data={intArray}>{intArray.map(val => renderPrimitiveValue(val, true))}</PrimitiveArrayTooltip> }] : [{ display: <EmptyValue /> }];
747
748         case isArrayOfType(input, CWLType.FLOAT):
749         case isArrayOfType(input, CWLType.DOUBLE):
750             const floatArray = (input as FloatArrayCommandInputParameter).value || [];
751             return floatArray.length ? [{ display: <PrimitiveArrayTooltip data={floatArray}>{floatArray.map(val => renderPrimitiveValue(val, true))}</PrimitiveArrayTooltip> }] : [{ display: <EmptyValue /> }];
752
753         case isArrayOfType(input, CWLType.FILE):
754             const fileArrayMainFiles = (input as FileArrayCommandInputParameter).value || [];
755             const firstMainFilePdh = fileArrayMainFiles.length > 0 && fileArrayMainFiles[0] ? getResourcePdhUrl(fileArrayMainFiles[0], pdh) : "";
756
757             // Convert each main and secondaryFiles into array of ProcessIOValue preserving ordering
758             let fileArrayValues: ProcessIOValue[] = [];
759             for (let i = 0; i < fileArrayMainFiles.length; i++) {
760                 const secondaryFiles = (fileArrayMainFiles[i] as unknown as FileWithSecondaryFiles)?.secondaryFiles || [];
761                 fileArrayValues.push(
762                     // Pass firstMainFilePdh to secondary files and every main file besides the first to hide pdh if equal
763                     ...(fileArrayMainFiles[i] ? [fileToProcessIOValue(fileArrayMainFiles[i], false, auth, pdh, i > 0 ? firstMainFilePdh : "")] : []),
764                     ...secondaryFiles.map(file => fileToProcessIOValue(file, true, auth, pdh, firstMainFilePdh))
765                 );
766             }
767
768             return fileArrayValues.length ? fileArrayValues : [{ display: <EmptyValue /> }];
769
770         case isArrayOfType(input, CWLType.DIRECTORY):
771             const directories = (input as DirectoryArrayCommandInputParameter).value || [];
772             return directories.length ? directories.map(directory => directoryToProcessIOValue(directory, auth, pdh)) : [{ display: <EmptyValue /> }];
773
774         default:
775             return [{ display: <UnsupportedValue /> }];
776     }
777 };
778
779 interface PrimitiveTooltipProps {
780     data: boolean | number | string;
781 }
782
783 const PrimitiveTooltip = (props: React.PropsWithChildren<PrimitiveTooltipProps>) => (
784     <Tooltip title={typeof props.data !== 'object' ? String(props.data) : ""}>
785         <pre>{props.children}</pre>
786     </Tooltip>
787 );
788
789 interface PrimitiveArrayTooltipProps {
790     data: string[];
791 }
792
793 const PrimitiveArrayTooltip = (props: React.PropsWithChildren<PrimitiveArrayTooltipProps>) => (
794     <Tooltip title={props.data.join(', ')}>
795         <span>{props.children}</span>
796     </Tooltip>
797 );
798
799
800 const renderPrimitiveValue = (value: any, asChip: boolean) => {
801     const isObject = typeof value === "object";
802     if (!isObject) {
803         return asChip ? (
804             <Chip
805                 key={value}
806                 label={String(value)}
807                 style={{marginRight: "10px"}}
808             />
809         ) : (
810             <>{String(value)}</>
811         );
812     } else {
813         return asChip ? <UnsupportedValueChip /> : <UnsupportedValue />;
814     }
815 };
816
817 /*
818  * @returns keep url without keep: prefix
819  */
820 const getKeepUrl = (file: File | Directory, pdh?: string): string => {
821     const isKeepUrl = file.location?.startsWith("keep:") || false;
822     const keepUrl = isKeepUrl ? file.location?.replace("keep:", "") : pdh ? `${pdh}/${file.location}` : file.location;
823     return keepUrl || "";
824 };
825
826 interface KeepUrlProps {
827     auth: AuthState;
828     res: File | Directory;
829     pdh?: string;
830 }
831
832 const getResourcePdhUrl = (res: File | Directory, pdh?: string): string => {
833     const keepUrl = getKeepUrl(res, pdh);
834     return keepUrl ? keepUrl.split("/").slice(0, 1)[0] : "";
835 };
836
837 const KeepUrlBase = withStyles(styles)(({ auth, res, pdh, classes }: KeepUrlProps & WithStyles<CssRules>) => {
838     const pdhUrl = getResourcePdhUrl(res, pdh);
839     // Passing a pdh always returns a relative wb2 collection url
840     const pdhWbPath = getNavUrl(pdhUrl, auth);
841     return pdhUrl && pdhWbPath ? (
842         <Tooltip title={<>View collection in Workbench<br />{pdhUrl}</>}>
843             <RouterLink
844                 to={pdhWbPath}
845                 className={classes.keepLink}
846             >
847                 {pdhUrl}
848             </RouterLink>
849         </Tooltip>
850     ) : (
851         <></>
852     );
853 });
854
855 const KeepUrlPath = withStyles(styles)(({ auth, res, pdh, classes }: KeepUrlProps & WithStyles<CssRules>) => {
856     const keepUrl = getKeepUrl(res, pdh);
857     const keepUrlParts = keepUrl ? keepUrl.split("/") : [];
858     const keepUrlPath = keepUrlParts.length > 1 ? keepUrlParts.slice(1).join("/") : "";
859
860     const keepUrlPathNav = getKeepNavUrl(auth, res, pdh);
861     return keepUrlPathNav ? (
862         <Tooltip classes={{tooltip: classes.wrapTooltip}} title={<>View in keep-web<br />{keepUrlPath || "/"}</>}>
863             <a
864                 className={classes.keepLink}
865                 href={keepUrlPathNav}
866                 target="_blank"
867                 rel="noopener noreferrer"
868             >
869                 {keepUrlPath || "/"}
870             </a>
871         </Tooltip>
872     ) : (
873         <EmptyValue />
874     );
875 });
876
877 const getKeepNavUrl = (auth: AuthState, file: File | Directory, pdh?: string): string => {
878     let keepUrl = getKeepUrl(file, pdh);
879     return getInlineFileUrl(
880         `${auth.config.keepWebServiceUrl}/c=${keepUrl}?api_token=${auth.apiToken}`,
881         auth.config.keepWebServiceUrl,
882         auth.config.keepWebInlineServiceUrl
883     );
884 };
885
886 const getImageUrl = (auth: AuthState, file: File, pdh?: string): string => {
887     const keepUrl = getKeepUrl(file, pdh);
888     return getInlineFileUrl(
889         `${auth.config.keepWebServiceUrl}/c=${keepUrl}?api_token=${auth.apiToken}`,
890         auth.config.keepWebServiceUrl,
891         auth.config.keepWebInlineServiceUrl
892     );
893 };
894
895 const isFileImage = (basename?: string): boolean => {
896     return basename ? (mime.getType(basename) || "").startsWith("image/") : false;
897 };
898
899 const isFileUrl = (location?: string): boolean =>
900     !!location && !KEEP_URL_REGEX.exec(location) && (location.startsWith("http://") || location.startsWith("https://"));
901
902 const normalizeDirectoryLocation = (directory: Directory): Directory => {
903     if (!directory.location) {
904         return directory;
905     }
906     return {
907         ...directory,
908         location: (directory.location || "").endsWith("/") ? directory.location : directory.location + "/",
909     };
910 };
911
912 const directoryToProcessIOValue = (directory: Directory, auth: AuthState, pdh?: string): ProcessIOValue => {
913     if (isExternalValue(directory)) {
914         return { display: <UnsupportedValue /> };
915     }
916
917     const normalizedDirectory = normalizeDirectoryLocation(directory);
918     return {
919         display: (
920             <KeepUrlPath
921                 auth={auth}
922                 res={normalizedDirectory}
923                 pdh={pdh}
924             />
925         ),
926         collection: (
927             <KeepUrlBase
928                 auth={auth}
929                 res={normalizedDirectory}
930                 pdh={pdh}
931             />
932         ),
933     };
934 };
935
936 type MuiLinkWithTooltipProps = WithStyles<CssRules> & React.PropsWithChildren<LinkProps>;
937
938 const MuiLinkWithTooltip = withStyles(styles)((props: MuiLinkWithTooltipProps) => (
939     <Tooltip title={props.title} classes={{tooltip: props.classes.wrapTooltip}}>
940         <MuiLink {...props}>
941             {props.children}
942         </MuiLink>
943     </Tooltip>
944 ));
945
946 const fileToProcessIOValue = (file: File, secondary: boolean, auth: AuthState, pdh: string | undefined, mainFilePdh: string): ProcessIOValue => {
947     if (isExternalValue(file)) {
948         return { display: <UnsupportedValue /> };
949     }
950
951     if (isFileUrl(file.location)) {
952         return {
953             display: (
954                 <MuiLinkWithTooltip
955                     href={file.location}
956                     target="_blank"
957                     rel="noopener"
958                     title={file.location}
959                 >
960                     {file.location}
961                 </MuiLinkWithTooltip>
962             ),
963             secondary,
964         };
965     }
966
967     const resourcePdh = getResourcePdhUrl(file, pdh);
968     return {
969         display: (
970             <KeepUrlPath
971                 auth={auth}
972                 res={file}
973                 pdh={pdh}
974             />
975         ),
976         secondary,
977         imageUrl: isFileImage(file.basename) ? getImageUrl(auth, file, pdh) : undefined,
978         collection:
979             resourcePdh !== mainFilePdh ? (
980                 <KeepUrlBase
981                     auth={auth}
982                     res={file}
983                     pdh={pdh}
984                 />
985             ) : (
986                 <></>
987             ),
988     };
989 };
990
991 const isExternalValue = (val: any) => Object.keys(val).includes("$import") || Object.keys(val).includes("$include");
992
993 export const EmptyValue = withStyles(styles)(({ classes }: WithStyles<CssRules>) => <span className={classes.emptyValue}>No value</span>);
994
995 const UnsupportedValue = withStyles(styles)(({ classes }: WithStyles<CssRules>) => <span className={classes.emptyValue}>Cannot display value</span>);
996
997 const UnsupportedValueChip = withStyles(styles)(({ classes }: WithStyles<CssRules>) => (
998     <Chip
999         icon={<InfoIcon />}
1000         label={"Cannot display value"}
1001     />
1002 ));