1 // Copyright (C) The Arvados Authors. All rights reserved.
3 // SPDX-License-Identifier: AGPL-3.0
5 import React, { ReactElement, memo, useState } from "react";
6 import { Dispatch } from "redux";
28 } from "@material-ui/core";
29 import { ArvadosTheme } from "common/custom-theme";
30 import { CloseIcon, ImageIcon, InputIcon, ImageOffIcon, OutputIcon, MaximizeIcon, UnMaximizeIcon, InfoIcon } from "components/icon/icon";
31 import { MPVPanelProps } from "components/multi-panel-view/multi-panel-view";
33 BooleanCommandInputParameter,
34 CommandInputParameter,
37 DirectoryArrayCommandInputParameter,
38 DirectoryCommandInputParameter,
39 EnumCommandInputParameter,
40 FileArrayCommandInputParameter,
41 FileCommandInputParameter,
42 FloatArrayCommandInputParameter,
43 FloatCommandInputParameter,
44 IntArrayCommandInputParameter,
45 IntCommandInputParameter,
48 StringArrayCommandInputParameter,
49 StringCommandInputParameter,
51 } from "models/workflow";
52 import { CommandOutputParameter } from "cwlts/mappings/v1.0/CommandOutputParameter";
53 import { File } from "models/workflow";
54 import { getInlineFileUrl } from "views-components/context-menu/actions/helpers";
55 import { AuthState } from "store/auth/auth-reducer";
56 import mime from "mime";
57 import { DefaultView } from "components/default-view/default-view";
58 import { getNavUrl } from "routes/routes";
59 import { Link as RouterLink } from "react-router-dom";
60 import { Link as MuiLink } from "@material-ui/core";
61 import { InputCollectionMount } from "store/processes/processes-actions";
62 import { connect } from "react-redux";
63 import { RootState } from "store/store";
64 import { ProcessOutputCollectionFiles } from "./process-output-collection-files";
65 import { Process } from "store/processes/process";
66 import { navigateTo } from "store/navigation/navigation-action";
67 import classNames from "classnames";
68 import { DefaultCodeSnippet } from "components/default-code-snippet/default-code-snippet";
69 import { KEEP_URL_REGEX } from "models/resource";
70 import { FixedSizeList } from 'react-window';
71 import AutoSizer from "react-virtualized-auto-sizer";
72 import { LinkProps } from "@material-ui/core/Link";
83 | "paramTableCellText"
93 const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
98 paddingTop: theme.spacing.unit,
102 fontSize: "1.875rem",
103 color: theme.customs.colors.greyL,
106 alignSelf: "flex-start",
107 paddingTop: theme.spacing.unit * 0.5,
111 height: `calc(100% - ${theme.spacing.unit * 6}px)`,
112 padding: theme.spacing.unit * 1.0,
115 paddingBottom: theme.spacing.unit * 1,
121 paddingTop: theme.spacing.unit * 0.5,
122 color: theme.customs.colors.greyD,
123 fontSize: "1.875rem",
125 // Applies to each tab's content
128 maxHeight: `calc(100% - ${theme.spacing.unit * 6}px)`,
130 // Use flexbox to keep scrolling at the virtual list level
132 flexDirection: "column",
133 alignItems: "start", // Prevents scroll bars at different levels in json tab
136 // Param table virtual list styles
139 flexDirection: "column",
145 padding: "4px 25px 10px",
149 height: "100vh", // Must be constrained by panel maxHeight
151 // Flex header/body rows
152 "& thead tr, & > tbody tr": {
154 // Flex header/body cells
161 // Column width overrides
162 "& th:nth-of-type(1), & td:nth-of-type(1)": {
165 "& th:nth-last-of-type(1), & td:nth-last-of-type(1)": {
174 padding: "2px 25px 2px",
177 flexDirection: "row",
178 alignItems: "center",
179 whiteSpace: "nowrap",
183 // Param value cell typography styles
184 paramTableCellText: {
187 // Every cell contents requires a wrapper for the ellipsis
188 // since adding ellipses to an anchor element parent results in misaligned tooltip
191 textOverflow: "ellipsis",
196 textOverflow: "ellipsis",
202 verticalAlign: "bottom",
203 paddingBottom: "10px",
206 paddingRight: "25px",
210 color: theme.palette.primary.main,
211 textDecoration: "none",
212 // Overflow wrap for mounts table
213 overflowWrap: "break-word",
216 // Output collection tab link
220 color: theme.palette.primary.main,
221 textDecoration: "none",
222 overflowWrap: "break-word",
230 color: theme.customs.colors.grey700,
234 borderBottom: "none",
236 paddingBottom: "2px",
247 wordWrap: "break-word",
251 export enum ProcessIOCardType {
252 INPUT = "Input Parameters",
253 OUTPUT = "Output Parameters",
255 export interface ProcessIOCardDataProps {
257 label: ProcessIOCardType;
258 params: ProcessIOParameter[] | null;
260 mounts?: InputCollectionMount[];
262 forceShowParams?: boolean;
265 export interface ProcessIOCardActionProps {
266 navigateTo: (uuid: string) => void;
269 const mapDispatchToProps = (dispatch: Dispatch): ProcessIOCardActionProps => ({
270 navigateTo: uuid => dispatch<any>(navigateTo(uuid)),
273 type ProcessIOCardProps = ProcessIOCardDataProps & ProcessIOCardActionProps & WithStyles<CssRules> & MPVPanelProps;
275 export const ProcessIOCard = withStyles(styles)(
295 }: ProcessIOCardProps) => {
296 const [mainProcTabState, setMainProcTabState] = useState(0);
297 const [subProcTabState, setSubProcTabState] = useState(0);
298 const handleMainProcTabChange = (event: React.MouseEvent<HTMLElement>, value: number) => {
299 setMainProcTabState(value);
301 const handleSubProcTabChange = (event: React.MouseEvent<HTMLElement>, value: number) => {
302 setSubProcTabState(value);
305 const PanelIcon = label === ProcessIOCardType.INPUT ? InputIcon : OutputIcon;
306 const mainProcess = !(process && process!.containerRequest.requestingContainerUuid);
307 const showParamTable = mainProcess || forceShowParams;
309 const loading = raw === null || raw === undefined || params === null;
311 const hasRaw = !!(raw && Object.keys(raw).length > 0);
312 const hasParams = !!(params && params.length > 0);
313 // isRawLoaded allows subprocess panel to display raw even if it's {}
314 const isRawLoaded = !!(raw && Object.keys(raw).length >= 0);
317 const hasInputMounts = !!(label === ProcessIOCardType.INPUT && mounts && mounts.length);
318 const hasOutputCollecton = !!(label === ProcessIOCardType.OUTPUT && outputUuid);
319 // Subprocess should not show loading if hasOutputCollection or hasInputMounts
320 const subProcessLoading = loading && !hasOutputCollecton && !hasInputMounts;
324 className={classes.card}
325 data-cy="process-io-card"
328 className={classes.header}
330 content: classes.title,
331 avatar: classes.avatar,
333 avatar={<PanelIcon className={classes.iconHeader} />}
345 {doUnMaximizePanel && panelMaximized && (
347 title={`Unmaximize ${panelName || "panel"}`}
350 <IconButton onClick={doUnMaximizePanel}>
355 {doMaximizePanel && !panelMaximized && (
357 title={`Maximize ${panelName || "panel"}`}
360 <IconButton onClick={doMaximizePanel}>
367 title={`Close ${panelName || "panel"}`}
371 disabled={panelMaximized}
372 onClick={doHidePanel}
381 <CardContent className={classes.content}>
384 {/* raw is undefined until params are loaded */}
395 {/* Once loaded, either raw or params may still be empty
396 * Raw when all params are empty
397 * Params when raw is provided by containerRequest properties but workflow mount is absent for preview
399 {!loading && (hasRaw || hasParams) && (
402 value={mainProcTabState}
403 onChange={handleMainProcTabChange}
405 className={classes.symmetricTabs}
407 {/* params will be empty on processes without workflow definitions in mounts, so we only show raw */}
408 {hasParams && <Tab label="Parameters" />}
409 {!forceShowParams && <Tab label="JSON" />}
410 {hasOutputCollecton && <Tab label="Collection" />}
412 {mainProcTabState === 0 && params && hasParams && (
413 <div className={classes.tableWrapper}>
416 valueLabel={forceShowParams ? "Default value" : "Value"}
420 {(mainProcTabState === 1 || !hasParams) && (
421 <div className={classes.tableWrapper}>
422 <ProcessIORaw data={raw} />
425 {mainProcTabState === 2 && hasOutputCollecton && (
428 <Typography className={classes.collectionLink}>
429 Output Collection:{" "}
431 className={classes.keepLink}
433 navigateTo(outputUuid || "");
440 <ProcessOutputCollectionFiles
442 currentItemUuid={outputUuid}
449 {!loading && !hasRaw && !hasParams && (
456 <DefaultView messages={["No parameters found"]} />
463 {subProcessLoading ? (
472 ) : !subProcessLoading && (hasInputMounts || hasOutputCollecton || isRawLoaded) ? (
475 value={subProcTabState}
476 onChange={handleSubProcTabChange}
478 className={classes.symmetricTabs}
480 {hasInputMounts && <Tab label="Collections" />}
481 {hasOutputCollecton && <Tab label="Collection" />}
482 {isRawLoaded && <Tab label="JSON" />}
484 <div className={classes.tableWrapper}>
485 {subProcTabState === 0 && hasInputMounts && <ProcessInputMounts mounts={mounts || []} />}
486 {subProcTabState === 0 && hasOutputCollecton && (
489 <Typography className={classes.collectionLink}>
490 Output Collection:{" "}
492 className={classes.keepLink}
494 navigateTo(outputUuid || "");
501 <ProcessOutputCollectionFiles
503 currentItemUuid={outputUuid}
507 {isRawLoaded && (subProcTabState === 1 || (!hasInputMounts && !hasOutputCollecton)) && (
508 <div className={classes.tableWrapper}>
509 <ProcessIORaw data={raw} />
521 <DefaultView messages={["No data to display"]} />
533 export type ProcessIOValue = {
534 display: ReactElement<any, any>;
536 collection?: ReactElement<any, any>;
540 export type ProcessIOParameter = {
543 value: ProcessIOValue;
546 interface ProcessIOPreviewDataProps {
547 data: ProcessIOParameter[];
551 type ProcessIOPreviewProps = ProcessIOPreviewDataProps & WithStyles<CssRules>;
553 const ProcessIOPreview = memo(
554 withStyles(styles)(({ classes, data, valueLabel }: ProcessIOPreviewProps) => {
555 const showLabel = data.some((param: ProcessIOParameter) => param.label);
557 const hasMoreValues = (index: number) => (
558 data[index+1] && !isMainRow(data[index+1])
561 const isMainRow = (param: ProcessIOParameter) => (param && (param.id || param.label && !param.value.secondary));
563 const RenderRow = ({index, style}) => {
564 const param = data[index];
567 [classes.noBorderRow]: hasMoreValues(index),
572 className={classNames(rowClasses)}
573 data-cy={isMainRow(param) ? "process-io-param" : ""}>
575 <Tooltip title={param.id}>
576 <Typography className={classes.paramTableCellText}>
583 {showLabel && <TableCell>
584 <Tooltip title={param.label}>
585 <Typography className={classes.paramTableCellText}>
598 <Typography className={classes.paramTableCellText}>
599 {/** Collection is an anchor so doesn't require wrapper element */}
600 {param.value.collection}
608 className={classes.paramTableRoot}
609 aria-label="Process IO Preview"
613 <TableCell>Name</TableCell>
614 {showLabel && <TableCell>Label</TableCell>}
615 <TableCell>{valueLabel}</TableCell>
616 <TableCell>Collection</TableCell>
621 {({ height, width }) =>
624 itemCount={data.length}
638 interface ProcessValuePreviewProps {
639 value: ProcessIOValue;
642 const ProcessValuePreview = withStyles(styles)(({ value, classes }: ProcessValuePreviewProps & WithStyles<CssRules>) => (
643 <Typography className={classNames(classes.paramTableCellText, value.secondary && classes.secondaryVal)}>
648 interface ProcessIORawDataProps {
649 data: ProcessIOParameter[];
652 const ProcessIORaw = withStyles(styles)(({ data }: ProcessIORawDataProps) => (
653 <Paper elevation={0} style={{minWidth: "100%"}}>
655 lines={[JSON.stringify(data, null, 2)]}
661 interface ProcessInputMountsDataProps {
662 mounts: InputCollectionMount[];
665 type ProcessInputMountsProps = ProcessInputMountsDataProps & WithStyles<CssRules>;
667 const ProcessInputMounts = withStyles(styles)(
668 connect((state: RootState) => ({
670 }))(({ mounts, classes, auth }: ProcessInputMountsProps & { auth: AuthState }) => (
672 className={classes.mountsTableRoot}
673 aria-label="Process Input Mounts"
677 <TableCell>Path</TableCell>
678 <TableCell>Portable Data Hash</TableCell>
682 {mounts.map(mount => (
683 <TableRow key={mount.path}>
685 <pre>{mount.path}</pre>
689 to={getNavUrl(mount.pdh, auth)}
690 className={classes.keepLink}
702 type FileWithSecondaryFiles = {
703 secondaryFiles: File[];
706 export const getIOParamDisplayValue = (auth: AuthState, input: CommandInputParameter | CommandOutputParameter, pdh?: string): ProcessIOValue[] => {
708 case isPrimitiveOfType(input, CWLType.BOOLEAN):
709 const boolValue = (input as BooleanCommandInputParameter).value;
710 return boolValue !== undefined && !(Array.isArray(boolValue) && boolValue.length === 0)
711 ? [{ display: <PrimitiveTooltip data={boolValue}>{renderPrimitiveValue(boolValue, false)}</PrimitiveTooltip> }]
712 : [{ display: <EmptyValue /> }];
714 case isPrimitiveOfType(input, CWLType.INT):
715 case isPrimitiveOfType(input, CWLType.LONG):
716 const intValue = (input as IntCommandInputParameter).value;
717 return intValue !== undefined &&
718 // Missing values are empty array
719 !(Array.isArray(intValue) && intValue.length === 0)
720 ? [{ display: <PrimitiveTooltip data={intValue}>{renderPrimitiveValue(intValue, false)}</PrimitiveTooltip> }]
721 : [{ display: <EmptyValue /> }];
723 case isPrimitiveOfType(input, CWLType.FLOAT):
724 case isPrimitiveOfType(input, CWLType.DOUBLE):
725 const floatValue = (input as FloatCommandInputParameter).value;
726 return floatValue !== undefined && !(Array.isArray(floatValue) && floatValue.length === 0)
727 ? [{ display: <PrimitiveTooltip data={floatValue}>{renderPrimitiveValue(floatValue, false)}</PrimitiveTooltip> }]
728 : [{ display: <EmptyValue /> }];
730 case isPrimitiveOfType(input, CWLType.STRING):
731 const stringValue = (input as StringCommandInputParameter).value || undefined;
732 return stringValue !== undefined && !(Array.isArray(stringValue) && stringValue.length === 0)
733 ? [{ display: <PrimitiveTooltip data={stringValue}>{renderPrimitiveValue(stringValue, false)}</PrimitiveTooltip> }]
734 : [{ display: <EmptyValue /> }];
736 case isPrimitiveOfType(input, CWLType.FILE):
737 const mainFile = (input as FileCommandInputParameter).value;
738 // secondaryFiles: File[] is not part of CommandOutputParameter so we cast to access secondaryFiles
739 const secondaryFiles = (mainFile as unknown as FileWithSecondaryFiles)?.secondaryFiles || [];
740 const files = [...(mainFile && !(Array.isArray(mainFile) && mainFile.length === 0) ? [mainFile] : []), ...secondaryFiles];
741 const mainFilePdhUrl = mainFile ? getResourcePdhUrl(mainFile, pdh) : "";
743 ? files.map((file, i) => fileToProcessIOValue(file, i > 0, auth, pdh, i > 0 ? mainFilePdhUrl : ""))
744 : [{ display: <EmptyValue /> }];
746 case isPrimitiveOfType(input, CWLType.DIRECTORY):
747 const directory = (input as DirectoryCommandInputParameter).value;
748 return directory !== undefined && !(Array.isArray(directory) && directory.length === 0)
749 ? [directoryToProcessIOValue(directory, auth, pdh)]
750 : [{ display: <EmptyValue /> }];
752 case getEnumType(input) !== null:
753 const enumValue = (input as EnumCommandInputParameter).value;
754 return enumValue !== undefined && enumValue ? [{ display: <PrimitiveTooltip data={enumValue}>{enumValue}</PrimitiveTooltip> }] : [{ display: <EmptyValue /> }];
756 case isArrayOfType(input, CWLType.STRING):
757 const strArray = (input as StringArrayCommandInputParameter).value || [];
758 return strArray.length ? [{ display: <PrimitiveArrayTooltip data={strArray}>{strArray.map(val => renderPrimitiveValue(val, true))}</PrimitiveArrayTooltip> }] : [{ display: <EmptyValue /> }];
760 case isArrayOfType(input, CWLType.INT):
761 case isArrayOfType(input, CWLType.LONG):
762 const intArray = (input as IntArrayCommandInputParameter).value || [];
763 return intArray.length ? [{ display: <PrimitiveArrayTooltip data={intArray}>{intArray.map(val => renderPrimitiveValue(val, true))}</PrimitiveArrayTooltip> }] : [{ display: <EmptyValue /> }];
765 case isArrayOfType(input, CWLType.FLOAT):
766 case isArrayOfType(input, CWLType.DOUBLE):
767 const floatArray = (input as FloatArrayCommandInputParameter).value || [];
768 return floatArray.length ? [{ display: <PrimitiveArrayTooltip data={floatArray}>{floatArray.map(val => renderPrimitiveValue(val, true))}</PrimitiveArrayTooltip> }] : [{ display: <EmptyValue /> }];
770 case isArrayOfType(input, CWLType.FILE):
771 const fileArrayMainFiles = (input as FileArrayCommandInputParameter).value || [];
772 const firstMainFilePdh = fileArrayMainFiles.length > 0 && fileArrayMainFiles[0] ? getResourcePdhUrl(fileArrayMainFiles[0], pdh) : "";
774 // Convert each main and secondaryFiles into array of ProcessIOValue preserving ordering
775 let fileArrayValues: ProcessIOValue[] = [];
776 for (let i = 0; i < fileArrayMainFiles.length; i++) {
777 const secondaryFiles = (fileArrayMainFiles[i] as unknown as FileWithSecondaryFiles)?.secondaryFiles || [];
778 fileArrayValues.push(
779 // Pass firstMainFilePdh to secondary files and every main file besides the first to hide pdh if equal
780 ...(fileArrayMainFiles[i] ? [fileToProcessIOValue(fileArrayMainFiles[i], false, auth, pdh, i > 0 ? firstMainFilePdh : "")] : []),
781 ...secondaryFiles.map(file => fileToProcessIOValue(file, true, auth, pdh, firstMainFilePdh))
785 return fileArrayValues.length ? fileArrayValues : [{ display: <EmptyValue /> }];
787 case isArrayOfType(input, CWLType.DIRECTORY):
788 const directories = (input as DirectoryArrayCommandInputParameter).value || [];
789 return directories.length ? directories.map(directory => directoryToProcessIOValue(directory, auth, pdh)) : [{ display: <EmptyValue /> }];
792 return [{ display: <UnsupportedValue /> }];
796 interface PrimitiveTooltipProps {
797 data: boolean | number | string;
800 const PrimitiveTooltip = (props: React.PropsWithChildren<PrimitiveTooltipProps>) => (
801 <Tooltip title={typeof props.data !== 'object' ? String(props.data) : ""}>
802 <pre>{props.children}</pre>
806 interface PrimitiveArrayTooltipProps {
810 const PrimitiveArrayTooltip = (props: React.PropsWithChildren<PrimitiveArrayTooltipProps>) => (
811 <Tooltip title={props.data.join(', ')}>
812 <span>{props.children}</span>
817 const renderPrimitiveValue = (value: any, asChip: boolean) => {
818 const isObject = typeof value === "object";
823 label={String(value)}
824 style={{marginRight: "10px"}}
830 return asChip ? <UnsupportedValueChip /> : <UnsupportedValue />;
835 * @returns keep url without keep: prefix
837 const getKeepUrl = (file: File | Directory, pdh?: string): string => {
838 const isKeepUrl = file.location?.startsWith("keep:") || false;
839 const keepUrl = isKeepUrl ? file.location?.replace("keep:", "") : pdh ? `${pdh}/${file.location}` : file.location;
840 return keepUrl || "";
843 interface KeepUrlProps {
845 res: File | Directory;
849 const getResourcePdhUrl = (res: File | Directory, pdh?: string): string => {
850 const keepUrl = getKeepUrl(res, pdh);
851 return keepUrl ? keepUrl.split("/").slice(0, 1)[0] : "";
854 const KeepUrlBase = withStyles(styles)(({ auth, res, pdh, classes }: KeepUrlProps & WithStyles<CssRules>) => {
855 const pdhUrl = getResourcePdhUrl(res, pdh);
856 // Passing a pdh always returns a relative wb2 collection url
857 const pdhWbPath = getNavUrl(pdhUrl, auth);
858 return pdhUrl && pdhWbPath ? (
859 <Tooltip title={<>View collection in Workbench<br />{pdhUrl}</>}>
862 className={classes.keepLink}
872 const KeepUrlPath = withStyles(styles)(({ auth, res, pdh, classes }: KeepUrlProps & WithStyles<CssRules>) => {
873 const keepUrl = getKeepUrl(res, pdh);
874 const keepUrlParts = keepUrl ? keepUrl.split("/") : [];
875 const keepUrlPath = keepUrlParts.length > 1 ? keepUrlParts.slice(1).join("/") : "";
877 const keepUrlPathNav = getKeepNavUrl(auth, res, pdh);
878 return keepUrlPathNav ? (
879 <Tooltip classes={{tooltip: classes.wrapTooltip}} title={<>View in keep-web<br />{keepUrlPath || "/"}</>}>
881 className={classes.keepLink}
882 href={keepUrlPathNav}
894 const getKeepNavUrl = (auth: AuthState, file: File | Directory, pdh?: string): string => {
895 let keepUrl = getKeepUrl(file, pdh);
896 return getInlineFileUrl(
897 `${auth.config.keepWebServiceUrl}/c=${keepUrl}?api_token=${auth.apiToken}`,
898 auth.config.keepWebServiceUrl,
899 auth.config.keepWebInlineServiceUrl
903 const getImageUrl = (auth: AuthState, file: File, pdh?: string): string => {
904 const keepUrl = getKeepUrl(file, pdh);
905 return getInlineFileUrl(
906 `${auth.config.keepWebServiceUrl}/c=${keepUrl}?api_token=${auth.apiToken}`,
907 auth.config.keepWebServiceUrl,
908 auth.config.keepWebInlineServiceUrl
912 const isFileImage = (basename?: string): boolean => {
913 return basename ? (mime.getType(basename) || "").startsWith("image/") : false;
916 const isFileUrl = (location?: string): boolean =>
917 !!location && !KEEP_URL_REGEX.exec(location) && (location.startsWith("http://") || location.startsWith("https://"));
919 const normalizeDirectoryLocation = (directory: Directory): Directory => {
920 if (!directory.location) {
925 location: (directory.location || "").endsWith("/") ? directory.location : directory.location + "/",
929 const directoryToProcessIOValue = (directory: Directory, auth: AuthState, pdh?: string): ProcessIOValue => {
930 if (isExternalValue(directory)) {
931 return { display: <UnsupportedValue /> };
934 const normalizedDirectory = normalizeDirectoryLocation(directory);
939 res={normalizedDirectory}
946 res={normalizedDirectory}
953 type MuiLinkWithTooltipProps = WithStyles<CssRules> & React.PropsWithChildren<LinkProps>;
955 const MuiLinkWithTooltip = withStyles(styles)((props: MuiLinkWithTooltipProps) => (
956 <Tooltip title={props.title} classes={{tooltip: props.classes.wrapTooltip}}>
963 const fileToProcessIOValue = (file: File, secondary: boolean, auth: AuthState, pdh: string | undefined, mainFilePdh: string): ProcessIOValue => {
964 if (isExternalValue(file)) {
965 return { display: <UnsupportedValue /> };
968 if (isFileUrl(file.location)) {
975 title={file.location}
978 </MuiLinkWithTooltip>
984 const resourcePdh = getResourcePdhUrl(file, pdh);
994 imageUrl: isFileImage(file.basename) ? getImageUrl(auth, file, pdh) : undefined,
996 resourcePdh !== mainFilePdh ? (
1008 const isExternalValue = (val: any) => Object.keys(val).includes("$import") || Object.keys(val).includes("$include");
1010 export const EmptyValue = withStyles(styles)(({ classes }: WithStyles<CssRules>) => <span className={classes.emptyValue}>No value</span>);
1012 const UnsupportedValue = withStyles(styles)(({ classes }: WithStyles<CssRules>) => <span className={classes.emptyValue}>Cannot display value</span>);
1014 const UnsupportedValueChip = withStyles(styles)(({ classes }: WithStyles<CssRules>) => (
1017 label={"Cannot display value"}