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