CircularProgress,
} from "@material-ui/core";
import { ArvadosTheme } from "common/custom-theme";
-import { CloseIcon, ImageIcon, InputIcon, ImageOffIcon, OutputIcon, MaximizeIcon, UnMaximizeIcon, InfoIcon } from "components/icon/icon";
+import { CloseIcon, InputIcon, OutputIcon, MaximizeIcon, UnMaximizeIcon, InfoIcon } from "components/icon/icon";
import { MPVPanelProps } from "components/multi-panel-view/multi-panel-view";
import {
BooleanCommandInputParameter,
import { Process } from "store/processes/process";
import { navigateTo } from "store/navigation/navigation-action";
import classNames from "classnames";
-import { DefaultCodeSnippet } from "components/default-code-snippet/default-code-snippet";
+import { DefaultVirtualCodeSnippet } from "components/default-code-snippet/default-virtual-code-snippet";
import { KEEP_URL_REGEX } from "models/resource";
import { FixedSizeList } from 'react-window';
import AutoSizer from "react-virtualized-auto-sizer";
+import { LinkProps } from "@material-ui/core/Link";
type CssRules =
| "card"
| "iconHeader"
| "tableWrapper"
| "paramTableRoot"
+ | "paramTableCellText"
| "mountsTableRoot"
- | "value"
+ | "jsonWrapper"
| "keepLink"
| "collectionLink"
| "secondaryVal"
| "emptyValue"
| "noBorderRow"
- | "symmetricTabs";
+ | "symmetricTabs"
+ | "wrapTooltip";
const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
card: {
},
// Card content
content: {
- height: `calc(100% - ${theme.spacing.unit * 7}px - ${theme.spacing.unit * 1.5}px)`,
+ height: `calc(100% - ${theme.spacing.unit * 6}px)`,
padding: theme.spacing.unit * 1.0,
paddingTop: 0,
"&:last-child": {
color: theme.customs.colors.greyD,
fontSize: "1.875rem",
},
- // Applies to each tab's content
+ // Applies to table tab and collection table content
tableWrapper: {
height: "auto",
- maxHeight: `calc(100% - ${theme.spacing.unit * 3}px)`,
+ maxHeight: `calc(100% - ${theme.spacing.unit * 6}px)`,
overflow: "auto",
// Use flexbox to keep scrolling at the virtual list level
display: "flex",
flexDirection: "column",
- alignItems: "start", // Prevents scroll bars at different levels in json tab
+ alignItems: "stretch", // Stretches output collection to full width
+
},
// Param table virtual list styles
overflow: "hidden",
},
// Column width overrides
+ "& th:nth-of-type(1), & td:nth-of-type(1)": {
+ flexGrow: 0.7,
+ },
"& th:nth-last-of-type(1), & td:nth-last-of-type(1)": {
flexGrow: 2,
},
flexDirection: "row",
alignItems: "center",
whiteSpace: "nowrap",
- '& pre': {
- margin: 0,
- overflow: "hidden",
- textOverflow: "ellipsis",
- },
},
},
},
+ // Param value cell typography styles
+ paramTableCellText: {
+ overflow: "hidden",
+ display: "flex",
+ // Every cell contents requires a wrapper for the ellipsis
+ // since adding ellipses to an anchor element parent results in misaligned tooltip
+ "& a, & span": {
+ overflow: "hidden",
+ textOverflow: "ellipsis",
+ },
+ '& pre': {
+ margin: 0,
+ overflow: "hidden",
+ textOverflow: "ellipsis",
+ },
+ },
mountsTableRoot: {
width: "100%",
"& thead th": {
paddingRight: "25px",
},
},
- // Cell typography styles
- value: {
- maxWidth: "100%",
- whiteSpace: "nowrap",
- overflow: "hidden",
- textOverflow: "ellipsis",
+ // JSON tab wrapper
+ jsonWrapper: {
+ height: `calc(100% - ${theme.spacing.unit * 6}px)`,
},
keepLink: {
color: theme.palette.primary.main,
flexBasis: "0",
},
},
+ wrapTooltip: {
+ maxWidth: "600px",
+ wordWrap: "break-word",
+ },
});
export enum ProcessIOCardType {
setSubProcTabState(value);
};
- const [showImagePreview, setShowImagePreview] = useState(false);
-
const PanelIcon = label === ProcessIOCardType.INPUT ? InputIcon : OutputIcon;
const mainProcess = !(process && process!.containerRequest.requestingContainerUuid);
const showParamTable = mainProcess || forceShowParams;
}
action={
<div>
- {mainProcess && (
- <Tooltip
- title={"Toggle Image Preview"}
- disableFocusListener
- >
- <IconButton
- data-cy="io-preview-image-toggle"
- onClick={() => {
- setShowImagePreview(!showImagePreview);
- }}
- >
- {showImagePreview ? <ImageIcon /> : <ImageOffIcon />}
- </IconButton>
- </Tooltip>
- )}
{doUnMaximizePanel && panelMaximized && (
<Tooltip
title={`Unmaximize ${panelName || "panel"}`}
<div className={classes.tableWrapper}>
<ProcessIOPreview
data={params}
- showImagePreview={showImagePreview}
valueLabel={forceShowParams ? "Default value" : "Value"}
/>
</div>
)}
{(mainProcTabState === 1 || !hasParams) && (
- <div className={classes.tableWrapper}>
+ <div className={classes.jsonWrapper}>
<ProcessIORaw data={raw} />
</div>
)}
{hasOutputCollecton && <Tab label="Collection" />}
{isRawLoaded && <Tab label="JSON" />}
</Tabs>
- <div className={classes.tableWrapper}>
- {subProcTabState === 0 && hasInputMounts && <ProcessInputMounts mounts={mounts || []} />}
- {subProcTabState === 0 && hasOutputCollecton && (
+ {subProcTabState === 0 && hasInputMounts && <ProcessInputMounts mounts={mounts || []} />}
+ {subProcTabState === 0 && hasOutputCollecton && (
+ <div className={classes.tableWrapper}>
<>
{outputUuid && (
<Typography className={classes.collectionLink}>
currentItemUuid={outputUuid}
/>
</>
- )}
- {isRawLoaded && (subProcTabState === 1 || (!hasInputMounts && !hasOutputCollecton)) && (
- <div className={classes.tableWrapper}>
- <ProcessIORaw data={raw} />
- </div>
- )}
- </div>
+ </div>
+ )}
+ {isRawLoaded && (subProcTabState === 1 || (!hasInputMounts && !hasOutputCollecton)) && (
+ <div className={classes.jsonWrapper}>
+ <ProcessIORaw data={raw} />
+ </div>
+ )}
</>
) : (
<Grid
interface ProcessIOPreviewDataProps {
data: ProcessIOParameter[];
- showImagePreview: boolean;
valueLabel: string;
}
type ProcessIOPreviewProps = ProcessIOPreviewDataProps & WithStyles<CssRules>;
const ProcessIOPreview = memo(
- withStyles(styles)(({ classes, data, showImagePreview, valueLabel }: ProcessIOPreviewProps) => {
+ withStyles(styles)(({ classes, data, valueLabel }: ProcessIOPreviewProps) => {
const showLabel = data.some((param: ProcessIOParameter) => param.label);
const hasMoreValues = (index: number) => (
- data[index+1] && !(data[index+1].id || data[index+1].label)
+ data[index+1] && !isMainRow(data[index+1])
+ );
+
+ const isMainRow = (param: ProcessIOParameter) => (
+ param &&
+ ((param.id || param.label) &&
+ !param.value.secondary)
);
const RenderRow = ({index, style}) => {
[classes.noBorderRow]: hasMoreValues(index),
};
- return <TableRow style={style} className={classNames(rowClasses)}>
+ return <TableRow
+ style={style}
+ className={classNames(rowClasses)}
+ data-cy={isMainRow(param) ? "process-io-param" : ""}>
<TableCell>
- <Typography className={classes.value}>
- {param.id}
- </Typography>
+ <Tooltip title={param.id}>
+ <Typography className={classes.paramTableCellText}>
+ <span>
+ {param.id}
+ </span>
+ </Typography>
+ </Tooltip>
</TableCell>
{showLabel && <TableCell>
- <Typography className={classes.value}>
- {param.label}
- </Typography>
+ <Tooltip title={param.label}>
+ <Typography className={classes.paramTableCellText}>
+ <span>
+ {param.label}
+ </span>
+ </Typography>
+ </Tooltip>
</TableCell>}
<TableCell>
<ProcessValuePreview
value={param.value}
- showImagePreview={showImagePreview}
/>
</TableCell>
<TableCell>
- <Typography className={classes.value}>
+ <Typography className={classes.paramTableCellText}>
+ {/** Collection is an anchor so doesn't require wrapper element */}
{param.value.collection}
</Typography>
</TableCell>
interface ProcessValuePreviewProps {
value: ProcessIOValue;
- showImagePreview: boolean;
}
-const ProcessValuePreview = withStyles(styles)(({ value, showImagePreview, classes }: ProcessValuePreviewProps & WithStyles<CssRules>) => (
- <Typography className={classNames(classes.value, value.secondary && classes.secondaryVal)}>
+const ProcessValuePreview = withStyles(styles)(({ value, classes }: ProcessValuePreviewProps & WithStyles<CssRules>) => (
+ <Typography className={classNames(classes.paramTableCellText, value.secondary && classes.secondaryVal)}>
{value.display}
</Typography>
));
}
const ProcessIORaw = withStyles(styles)(({ data }: ProcessIORawDataProps) => (
- <Paper elevation={0} style={{width: "100%"}}>
- <DefaultCodeSnippet
- lines={[JSON.stringify(data, null, 2)]}
+ <Paper elevation={0} style={{minWidth: "100%", height: "100%"}}>
+ <DefaultVirtualCodeSnippet
+ lines={JSON.stringify(data, null, 2).split('\n')}
linked
/>
</Paper>
case isPrimitiveOfType(input, CWLType.BOOLEAN):
const boolValue = (input as BooleanCommandInputParameter).value;
return boolValue !== undefined && !(Array.isArray(boolValue) && boolValue.length === 0)
- ? [{ display: renderPrimitiveValue(boolValue, false) }]
+ ? [{ display: <PrimitiveTooltip data={boolValue}>{renderPrimitiveValue(boolValue, false)}</PrimitiveTooltip> }]
: [{ display: <EmptyValue /> }];
case isPrimitiveOfType(input, CWLType.INT):
return intValue !== undefined &&
// Missing values are empty array
!(Array.isArray(intValue) && intValue.length === 0)
- ? [{ display: renderPrimitiveValue(intValue, false) }]
+ ? [{ display: <PrimitiveTooltip data={intValue}>{renderPrimitiveValue(intValue, false)}</PrimitiveTooltip> }]
: [{ display: <EmptyValue /> }];
case isPrimitiveOfType(input, CWLType.FLOAT):
case isPrimitiveOfType(input, CWLType.DOUBLE):
const floatValue = (input as FloatCommandInputParameter).value;
return floatValue !== undefined && !(Array.isArray(floatValue) && floatValue.length === 0)
- ? [{ display: renderPrimitiveValue(floatValue, false) }]
+ ? [{ display: <PrimitiveTooltip data={floatValue}>{renderPrimitiveValue(floatValue, false)}</PrimitiveTooltip> }]
: [{ display: <EmptyValue /> }];
case isPrimitiveOfType(input, CWLType.STRING):
const stringValue = (input as StringCommandInputParameter).value || undefined;
return stringValue !== undefined && !(Array.isArray(stringValue) && stringValue.length === 0)
- ? [{ display: renderPrimitiveValue(stringValue, false) }]
+ ? [{ display: <PrimitiveTooltip data={stringValue}>{renderPrimitiveValue(stringValue, false)}</PrimitiveTooltip> }]
: [{ display: <EmptyValue /> }];
case isPrimitiveOfType(input, CWLType.FILE):
case getEnumType(input) !== null:
const enumValue = (input as EnumCommandInputParameter).value;
- return enumValue !== undefined && enumValue ? [{ display: <pre>{enumValue}</pre> }] : [{ display: <EmptyValue /> }];
+ return enumValue !== undefined && enumValue ? [{ display: <PrimitiveTooltip data={enumValue}>{enumValue}</PrimitiveTooltip> }] : [{ display: <EmptyValue /> }];
case isArrayOfType(input, CWLType.STRING):
const strArray = (input as StringArrayCommandInputParameter).value || [];
- return strArray.length ? [{ display: <>{strArray.map(val => renderPrimitiveValue(val, true))}</> }] : [{ display: <EmptyValue /> }];
+ return strArray.length ? [{ display: <PrimitiveArrayTooltip data={strArray}>{strArray.map(val => renderPrimitiveValue(val, true))}</PrimitiveArrayTooltip> }] : [{ display: <EmptyValue /> }];
case isArrayOfType(input, CWLType.INT):
case isArrayOfType(input, CWLType.LONG):
const intArray = (input as IntArrayCommandInputParameter).value || [];
- return intArray.length ? [{ display: <>{intArray.map(val => renderPrimitiveValue(val, true))}</> }] : [{ display: <EmptyValue /> }];
+ return intArray.length ? [{ display: <PrimitiveArrayTooltip data={intArray}>{intArray.map(val => renderPrimitiveValue(val, true))}</PrimitiveArrayTooltip> }] : [{ display: <EmptyValue /> }];
case isArrayOfType(input, CWLType.FLOAT):
case isArrayOfType(input, CWLType.DOUBLE):
const floatArray = (input as FloatArrayCommandInputParameter).value || [];
- return floatArray.length ? [{ display: <>{floatArray.map(val => renderPrimitiveValue(val, true))}</> }] : [{ display: <EmptyValue /> }];
+ return floatArray.length ? [{ display: <PrimitiveArrayTooltip data={floatArray}>{floatArray.map(val => renderPrimitiveValue(val, true))}</PrimitiveArrayTooltip> }] : [{ display: <EmptyValue /> }];
case isArrayOfType(input, CWLType.FILE):
const fileArrayMainFiles = (input as FileArrayCommandInputParameter).value || [];
}
};
+interface PrimitiveTooltipProps {
+ data: boolean | number | string;
+}
+
+const PrimitiveTooltip = (props: React.PropsWithChildren<PrimitiveTooltipProps>) => (
+ <Tooltip title={typeof props.data !== 'object' ? String(props.data) : ""}>
+ <pre>{props.children}</pre>
+ </Tooltip>
+);
+
+interface PrimitiveArrayTooltipProps {
+ data: string[];
+}
+
+const PrimitiveArrayTooltip = (props: React.PropsWithChildren<PrimitiveArrayTooltipProps>) => (
+ <Tooltip title={props.data.join(', ')}>
+ <span>{props.children}</span>
+ </Tooltip>
+);
+
+
const renderPrimitiveValue = (value: any, asChip: boolean) => {
const isObject = typeof value === "object";
if (!isObject) {
style={{marginRight: "10px"}}
/>
) : (
- <pre key={value}>{String(value)}</pre>
+ <>{String(value)}</>
);
} else {
return asChip ? <UnsupportedValueChip /> : <UnsupportedValue />;
// Passing a pdh always returns a relative wb2 collection url
const pdhWbPath = getNavUrl(pdhUrl, auth);
return pdhUrl && pdhWbPath ? (
- <Tooltip title={"View collection in Workbench"}>
+ <Tooltip title={<>View collection in Workbench<br />{pdhUrl}</>}>
<RouterLink
to={pdhWbPath}
className={classes.keepLink}
const keepUrlPathNav = getKeepNavUrl(auth, res, pdh);
return keepUrlPathNav ? (
- <Tooltip title={"View in keep-web"}>
+ <Tooltip classes={{tooltip: classes.wrapTooltip}} title={<>View in keep-web<br />{keepUrlPath || "/"}</>}>
<a
className={classes.keepLink}
href={keepUrlPathNav}
target="_blank"
- rel="noopener"
+ rel="noopener noreferrer"
>
{keepUrlPath || "/"}
</a>
};
};
+type MuiLinkWithTooltipProps = WithStyles<CssRules> & React.PropsWithChildren<LinkProps>;
+
+const MuiLinkWithTooltip = withStyles(styles)((props: MuiLinkWithTooltipProps) => (
+ <Tooltip title={props.title} classes={{tooltip: props.classes.wrapTooltip}}>
+ <MuiLink {...props}>
+ {props.children}
+ </MuiLink>
+ </Tooltip>
+));
+
const fileToProcessIOValue = (file: File, secondary: boolean, auth: AuthState, pdh: string | undefined, mainFilePdh: string): ProcessIOValue => {
if (isExternalValue(file)) {
return { display: <UnsupportedValue /> };
if (isFileUrl(file.location)) {
return {
display: (
- <MuiLink
+ <MuiLinkWithTooltip
href={file.location}
target="_blank"
rel="noopener"
+ title={file.location}
>
{file.location}
- </MuiLink>
+ </MuiLinkWithTooltip>
),
secondary,
};