X-Git-Url: https://git.arvados.org/arvados.git/blobdiff_plain/b5c15775caf865d8fed5d4839d4a082f6518bba4..3afc1cd214cb3f53ec36b3d5b4c80bc5989093a0:/services/workbench2/src/views/process-panel/process-io-card.tsx diff --git a/services/workbench2/src/views/process-panel/process-io-card.tsx b/services/workbench2/src/views/process-panel/process-io-card.tsx index 5716340edc..9fce7e83d4 100644 --- a/services/workbench2/src/views/process-panel/process-io-card.tsx +++ b/services/workbench2/src/views/process-panel/process-io-card.tsx @@ -27,7 +27,7 @@ import { 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, @@ -65,8 +65,11 @@ import { ProcessOutputCollectionFiles } from "./process-output-collection-files" 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" @@ -76,21 +79,17 @@ type CssRules = | "avatar" | "iconHeader" | "tableWrapper" - | "tableRoot" - | "paramValue" + | "paramTableRoot" + | "paramTableCellText" + | "mountsTableRoot" + | "jsonWrapper" | "keepLink" | "collectionLink" - | "imagePreview" - | "valArray" | "secondaryVal" - | "secondaryRow" | "emptyValue" | "noBorderRow" | "symmetricTabs" - | "imagePlaceholder" - | "rowWithPreview" - | "labelColumn" - | "primaryRow"; + | "wrapTooltip"; const styles: StyleRulesCallback = (theme: ArvadosTheme) => ({ card: { @@ -108,26 +107,98 @@ const styles: StyleRulesCallback = (theme: ArvadosTheme) => ({ alignSelf: "flex-start", paddingTop: theme.spacing.unit * 0.5, }, + // 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": { paddingBottom: theme.spacing.unit * 1, }, }, + // Card title title: { overflow: "hidden", paddingTop: theme.spacing.unit * 0.5, color: theme.customs.colors.greyD, fontSize: "1.875rem", }, + // 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: "stretch", // Stretches output collection to full width + }, - tableRoot: { + + // Param table virtual list styles + paramTableRoot: { + display: "flex", + flexDirection: "column", + overflow: "hidden", + // Flex header + "& thead tr": { + alignItems: "end", + "& th": { + padding: "4px 25px 10px", + }, + }, + "& tbody": { + height: "100vh", // Must be constrained by panel maxHeight + }, + // Flex header/body rows + "& thead tr, & > tbody tr": { + display: "flex", + // Flex header/body cells + "& th, & td": { + flexGrow: 1, + flexShrink: 1, + flexBasis: 0, + 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, + }, + }, + // Flex body rows + "& tbody tr": { + height: "40px", + // Flex body cells + "& td": { + padding: "2px 25px 2px", + overflow: "hidden", + display: "flex", + flexDirection: "row", + alignItems: "center", + whiteSpace: "nowrap", + }, + }, + }, + // 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": { verticalAlign: "bottom", @@ -137,17 +208,18 @@ const styles: StyleRulesCallback = (theme: ArvadosTheme) => ({ paddingRight: "25px", }, }, - paramValue: { - display: "flex", - alignItems: "flex-start", - flexDirection: "column", + // JSON tab wrapper + jsonWrapper: { + height: `calc(100% - ${theme.spacing.unit * 6}px)`, }, keepLink: { color: theme.palette.primary.main, textDecoration: "none", + // Overflow wrap for mounts table overflowWrap: "break-word", cursor: "pointer", }, + // Output collection tab link collectionLink: { margin: "10px", "& a": { @@ -157,28 +229,9 @@ const styles: StyleRulesCallback = (theme: ArvadosTheme) => ({ cursor: "pointer", }, }, - imagePreview: { - maxHeight: "15em", - maxWidth: "15em", - marginBottom: theme.spacing.unit, - }, - valArray: { - display: "flex", - gap: "10px", - flexWrap: "wrap", - "& span": { - display: "inline", - }, - }, secondaryVal: { paddingLeft: "20px", }, - secondaryRow: { - height: "24px", - verticalAlign: "top", - position: "relative", - top: "-4px", - }, emptyValue: { color: theme.customs.colors.grey700, }, @@ -195,27 +248,9 @@ const styles: StyleRulesCallback = (theme: ArvadosTheme) => ({ flexBasis: "0", }, }, - imagePlaceholder: { - width: "60px", - height: "60px", - display: "flex", - alignItems: "center", - justifyContent: "center", - backgroundColor: "#cecece", - borderRadius: "10px", - }, - rowWithPreview: { - verticalAlign: "bottom", - }, - labelColumn: { - minWidth: "120px", - }, - primaryRow: { - height: "24px", - "& td": { - paddingTop: "2px", - paddingBottom: "2px", - }, + wrapTooltip: { + maxWidth: "600px", + wordWrap: "break-word", }, }); @@ -273,8 +308,6 @@ export const ProcessIOCard = withStyles(styles)( setSubProcTabState(value); }; - const [showImagePreview, setShowImagePreview] = useState(false); - const PanelIcon = label === ProcessIOCardType.INPUT ? InputIcon : OutputIcon; const mainProcess = !(process && process!.containerRequest.requestingContainerUuid); const showParamTable = mainProcess || forceShowParams; @@ -315,21 +348,6 @@ export const ProcessIOCard = withStyles(styles)( } action={
- {mainProcess && ( - - { - setShowImagePreview(!showImagePreview); - }} - > - {showImagePreview ? : } - - - )} {doUnMaximizePanel && panelMaximized && ( )} {/* Once loaded, either raw or params may still be empty - * Raw when all params are empty - * Params when raw is provided by containerRequest properties but workflow mount is absent for preview - */} + * Raw when all params are empty + * Params when raw is provided by containerRequest properties but workflow mount is absent for preview + */} {!loading && (hasRaw || hasParams) && ( <>
)} {(mainProcTabState === 1 || !hasParams) && ( -
+
)} @@ -470,9 +487,9 @@ export const ProcessIOCard = withStyles(styles)( {hasOutputCollecton && } {isRawLoaded && } -
- {subProcTabState === 0 && hasInputMounts && } - {subProcTabState === 0 && hasOutputCollecton && ( + {subProcTabState === 0 && hasInputMounts && } + {subProcTabState === 0 && hasOutputCollecton && ( +
<> {outputUuid && ( @@ -492,13 +509,13 @@ export const ProcessIOCard = withStyles(styles)( currentItemUuid={outputUuid} /> - )} - {isRawLoaded && (subProcTabState === 1 || (!hasInputMounts && !hasOutputCollecton)) && ( -
- -
- )} -
+
+ )} + {isRawLoaded && (subProcTabState === 1 || (!hasInputMounts && !hasOutputCollecton)) && ( +
+ +
+ )} ) : ( ; 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] && !isMainRow(data[index+1]) + ); + + const isMainRow = (param: ProcessIOParameter) => ( + param && + ((param.id || param.label) && + !param.value.secondary) + ); + + const RenderRow = ({index, style}) => { + const param = data[index]; + + const rowClasses = { + [classes.noBorderRow]: hasMoreValues(index), + }; + + return + + + + + {param.id} + + + + + {showLabel && + + + + {param.label} + + + + } + + + + + + {/** Collection is an anchor so doesn't require wrapper element */} + {param.value.collection} + + + ; + }; + return ( Name - {showLabel && Label} + {showLabel && Label} {valueLabel} Collection - {data.map((param: ProcessIOParameter) => { - const firstVal = param.value.length > 0 ? param.value[0] : undefined; - const rest = param.value.slice(1); - const mainRowClasses = { - [classes.noBorderRow]: rest.length > 0, - [classes.primaryRow]: true - }; - - return ( - - - {param.id} - {showLabel && {param.label}} - - {firstVal && ( - - )} - - - {firstVal?.collection} - - - {rest.map((val, i) => { - const rowClasses = { - [classes.noBorderRow]: i < rest.length - 1, - [classes.secondaryRow]: val.secondary, - [classes.primaryRow]: !val.secondary, - }; - return ( - - - {showLabel && } - - - - - {val.collection} - - - ); - })} - - ); - })} + + {({ height, width }) => + + {RenderRow} + + } +
); @@ -621,22 +647,11 @@ const ProcessIOPreview = memo( interface ProcessValuePreviewProps { value: ProcessIOValue; - showImagePreview: boolean; } -const ProcessValuePreview = withStyles(styles)(({ value, showImagePreview, classes }: ProcessValuePreviewProps & WithStyles) => ( - - {value.imageUrl && showImagePreview ? ( - Inline Preview - ) : ( - "" - )} - {value.imageUrl && !showImagePreview ? : ""} - {value.display} +const ProcessValuePreview = withStyles(styles)(({ value, classes }: ProcessValuePreviewProps & WithStyles) => ( + + {value.display} )); @@ -645,9 +660,9 @@ interface ProcessIORawDataProps { } const ProcessIORaw = withStyles(styles)(({ data }: ProcessIORawDataProps) => ( - - + @@ -664,7 +679,7 @@ const ProcessInputMounts = withStyles(styles)( auth: state.auth, }))(({ mounts, classes, auth }: ProcessInputMountsProps & { auth: AuthState }) => ( @@ -703,7 +718,7 @@ export const getIOParamDisplayValue = (auth: AuthState, input: CommandInputParam 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: {renderPrimitiveValue(boolValue, false)} }] : [{ display: }]; case isPrimitiveOfType(input, CWLType.INT): @@ -712,20 +727,20 @@ export const getIOParamDisplayValue = (auth: AuthState, input: CommandInputParam return intValue !== undefined && // Missing values are empty array !(Array.isArray(intValue) && intValue.length === 0) - ? [{ display: renderPrimitiveValue(intValue, false) }] + ? [{ display: {renderPrimitiveValue(intValue, false)} }] : [{ display: }]; 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: {renderPrimitiveValue(floatValue, false)} }] : [{ display: }]; 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: {renderPrimitiveValue(stringValue, false)} }] : [{ display: }]; case isPrimitiveOfType(input, CWLType.FILE): @@ -746,21 +761,21 @@ export const getIOParamDisplayValue = (auth: AuthState, input: CommandInputParam case getEnumType(input) !== null: const enumValue = (input as EnumCommandInputParameter).value; - return enumValue !== undefined && enumValue ? [{ display:
{enumValue}
}] : [{ display: }]; + return enumValue !== undefined && enumValue ? [{ display: {enumValue} }] : [{ display: }]; case isArrayOfType(input, CWLType.STRING): const strArray = (input as StringArrayCommandInputParameter).value || []; - return strArray.length ? [{ display: <>{strArray.map(val => renderPrimitiveValue(val, true))} }] : [{ display: }]; + return strArray.length ? [{ display: {strArray.map(val => renderPrimitiveValue(val, true))} }] : [{ display: }]; 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: }]; + return intArray.length ? [{ display: {intArray.map(val => renderPrimitiveValue(val, true))} }] : [{ display: }]; 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: }]; + return floatArray.length ? [{ display: {floatArray.map(val => renderPrimitiveValue(val, true))} }] : [{ display: }]; case isArrayOfType(input, CWLType.FILE): const fileArrayMainFiles = (input as FileArrayCommandInputParameter).value || []; @@ -788,6 +803,27 @@ export const getIOParamDisplayValue = (auth: AuthState, input: CommandInputParam } }; +interface PrimitiveTooltipProps { + data: boolean | number | string; +} + +const PrimitiveTooltip = (props: React.PropsWithChildren) => ( + +
{props.children}
+
+); + +interface PrimitiveArrayTooltipProps { + data: string[]; +} + +const PrimitiveArrayTooltip = (props: React.PropsWithChildren) => ( + + {props.children} + +); + + const renderPrimitiveValue = (value: any, asChip: boolean) => { const isObject = typeof value === "object"; if (!isObject) { @@ -795,9 +831,10 @@ const renderPrimitiveValue = (value: any, asChip: boolean) => { ) : ( -
{String(value)}
+ <>{String(value)} ); } else { return asChip ? : ; @@ -829,7 +866,7 @@ const KeepUrlBase = withStyles(styles)(({ auth, res, pdh, classes }: KeepUrlProp // Passing a pdh always returns a relative wb2 collection url const pdhWbPath = getNavUrl(pdhUrl, auth); return pdhUrl && pdhWbPath ? ( - + View collection in Workbench
{pdhUrl}}> + View in keep-web
{keepUrlPath || "/"}}> {keepUrlPath || "/"} @@ -923,6 +960,16 @@ const directoryToProcessIOValue = (directory: Directory, auth: AuthState, pdh?: }; }; +type MuiLinkWithTooltipProps = WithStyles & React.PropsWithChildren; + +const MuiLinkWithTooltip = withStyles(styles)((props: MuiLinkWithTooltipProps) => ( + + + {props.children} + + +)); + const fileToProcessIOValue = (file: File, secondary: boolean, auth: AuthState, pdh: string | undefined, mainFilePdh: string): ProcessIOValue => { if (isExternalValue(file)) { return { display: }; @@ -931,13 +978,14 @@ const fileToProcessIOValue = (file: File, secondary: boolean, auth: AuthState, p if (isFileUrl(file.location)) { return { display: ( - {file.location} - + ), secondary, }; @@ -979,9 +1027,3 @@ const UnsupportedValueChip = withStyles(styles)(({ classes }: WithStyles )); - -const ImagePlaceholder = withStyles(styles)(({ classes }: WithStyles) => ( - - - -));