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, InputIcon, 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 { DefaultVirtualCodeSnippet } from "components/default-code-snippet/default-virtual-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"
94 const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
99 paddingTop: theme.spacing.unit,
103 fontSize: "1.875rem",
104 color: theme.customs.colors.greyL,
107 alignSelf: "flex-start",
108 paddingTop: theme.spacing.unit * 0.5,
112 height: `calc(100% - ${theme.spacing.unit * 6}px)`,
113 padding: theme.spacing.unit * 1.0,
116 paddingBottom: theme.spacing.unit * 1,
122 paddingTop: theme.spacing.unit * 0.5,
123 color: theme.customs.colors.greyD,
124 fontSize: "1.875rem",
126 // Applies to table tab's content
129 maxHeight: `calc(100% - ${theme.spacing.unit * 6}px)`,
131 // Use flexbox to keep scrolling at the virtual list level
133 flexDirection: "column",
134 alignItems: "start", // Prevents scroll bars at different levels in json tab
137 // Param table virtual list styles
140 flexDirection: "column",
146 padding: "4px 25px 10px",
150 height: "100vh", // Must be constrained by panel maxHeight
152 // Flex header/body rows
153 "& thead tr, & > tbody tr": {
155 // Flex header/body cells
162 // Column width overrides
163 "& th:nth-of-type(1), & td:nth-of-type(1)": {
166 "& th:nth-last-of-type(1), & td:nth-last-of-type(1)": {
175 padding: "2px 25px 2px",
178 flexDirection: "row",
179 alignItems: "center",
180 whiteSpace: "nowrap",
184 // Param value cell typography styles
185 paramTableCellText: {
188 // Every cell contents requires a wrapper for the ellipsis
189 // since adding ellipses to an anchor element parent results in misaligned tooltip
192 textOverflow: "ellipsis",
197 textOverflow: "ellipsis",
203 verticalAlign: "bottom",
204 paddingBottom: "10px",
207 paddingRight: "25px",
212 height: `calc(100% - ${theme.spacing.unit * 6}px)`,
215 color: theme.palette.primary.main,
216 textDecoration: "none",
217 // Overflow wrap for mounts table
218 overflowWrap: "break-word",
221 // Output collection tab link
225 color: theme.palette.primary.main,
226 textDecoration: "none",
227 overflowWrap: "break-word",
235 color: theme.customs.colors.grey700,
239 borderBottom: "none",
241 paddingBottom: "2px",
252 wordWrap: "break-word",
256 export enum ProcessIOCardType {
257 INPUT = "Input Parameters",
258 OUTPUT = "Output Parameters",
260 export interface ProcessIOCardDataProps {
262 label: ProcessIOCardType;
263 params: ProcessIOParameter[] | null;
265 mounts?: InputCollectionMount[];
267 forceShowParams?: boolean;
270 export interface ProcessIOCardActionProps {
271 navigateTo: (uuid: string) => void;
274 const mapDispatchToProps = (dispatch: Dispatch): ProcessIOCardActionProps => ({
275 navigateTo: uuid => dispatch<any>(navigateTo(uuid)),
278 type ProcessIOCardProps = ProcessIOCardDataProps & ProcessIOCardActionProps & WithStyles<CssRules> & MPVPanelProps;
280 export const ProcessIOCard = withStyles(styles)(
300 }: ProcessIOCardProps) => {
301 const [mainProcTabState, setMainProcTabState] = useState(0);
302 const [subProcTabState, setSubProcTabState] = useState(0);
303 const handleMainProcTabChange = (event: React.MouseEvent<HTMLElement>, value: number) => {
304 setMainProcTabState(value);
306 const handleSubProcTabChange = (event: React.MouseEvent<HTMLElement>, value: number) => {
307 setSubProcTabState(value);
310 const PanelIcon = label === ProcessIOCardType.INPUT ? InputIcon : OutputIcon;
311 const mainProcess = !(process && process!.containerRequest.requestingContainerUuid);
312 const showParamTable = mainProcess || forceShowParams;
314 const loading = raw === null || raw === undefined || params === null;
316 const hasRaw = !!(raw && Object.keys(raw).length > 0);
317 const hasParams = !!(params && params.length > 0);
318 // isRawLoaded allows subprocess panel to display raw even if it's {}
319 const isRawLoaded = !!(raw && Object.keys(raw).length >= 0);
322 const hasInputMounts = !!(label === ProcessIOCardType.INPUT && mounts && mounts.length);
323 const hasOutputCollecton = !!(label === ProcessIOCardType.OUTPUT && outputUuid);
324 // Subprocess should not show loading if hasOutputCollection or hasInputMounts
325 const subProcessLoading = loading && !hasOutputCollecton && !hasInputMounts;
329 className={classes.card}
330 data-cy="process-io-card"
333 className={classes.header}
335 content: classes.title,
336 avatar: classes.avatar,
338 avatar={<PanelIcon className={classes.iconHeader} />}
350 {doUnMaximizePanel && panelMaximized && (
352 title={`Unmaximize ${panelName || "panel"}`}
355 <IconButton onClick={doUnMaximizePanel}>
360 {doMaximizePanel && !panelMaximized && (
362 title={`Maximize ${panelName || "panel"}`}
365 <IconButton onClick={doMaximizePanel}>
372 title={`Close ${panelName || "panel"}`}
376 disabled={panelMaximized}
377 onClick={doHidePanel}
386 <CardContent className={classes.content}>
389 {/* raw is undefined until params are loaded */}
400 {/* Once loaded, either raw or params may still be empty
401 * Raw when all params are empty
402 * Params when raw is provided by containerRequest properties but workflow mount is absent for preview
404 {!loading && (hasRaw || hasParams) && (
407 value={mainProcTabState}
408 onChange={handleMainProcTabChange}
410 className={classes.symmetricTabs}
412 {/* params will be empty on processes without workflow definitions in mounts, so we only show raw */}
413 {hasParams && <Tab label="Parameters" />}
414 {!forceShowParams && <Tab label="JSON" />}
415 {hasOutputCollecton && <Tab label="Collection" />}
417 {mainProcTabState === 0 && params && hasParams && (
418 <div className={classes.tableWrapper}>
421 valueLabel={forceShowParams ? "Default value" : "Value"}
425 {(mainProcTabState === 1 || !hasParams) && (
426 <div className={classes.jsonWrapper}>
427 <ProcessIORaw data={raw} />
430 {mainProcTabState === 2 && hasOutputCollecton && (
433 <Typography className={classes.collectionLink}>
434 Output Collection:{" "}
436 className={classes.keepLink}
438 navigateTo(outputUuid || "");
445 <ProcessOutputCollectionFiles
447 currentItemUuid={outputUuid}
454 {!loading && !hasRaw && !hasParams && (
461 <DefaultView messages={["No parameters found"]} />
468 {subProcessLoading ? (
477 ) : !subProcessLoading && (hasInputMounts || hasOutputCollecton || isRawLoaded) ? (
480 value={subProcTabState}
481 onChange={handleSubProcTabChange}
483 className={classes.symmetricTabs}
485 {hasInputMounts && <Tab label="Collections" />}
486 {hasOutputCollecton && <Tab label="Collection" />}
487 {isRawLoaded && <Tab label="JSON" />}
489 <div className={classes.tableWrapper}>
490 {subProcTabState === 0 && hasInputMounts && <ProcessInputMounts mounts={mounts || []} />}
491 {subProcTabState === 0 && hasOutputCollecton && (
494 <Typography className={classes.collectionLink}>
495 Output Collection:{" "}
497 className={classes.keepLink}
499 navigateTo(outputUuid || "");
506 <ProcessOutputCollectionFiles
508 currentItemUuid={outputUuid}
512 {isRawLoaded && (subProcTabState === 1 || (!hasInputMounts && !hasOutputCollecton)) && (
513 <div className={classes.jsonWrapper}>
514 <ProcessIORaw data={raw} />
526 <DefaultView messages={["No data to display"]} />
538 export type ProcessIOValue = {
539 display: ReactElement<any, any>;
541 collection?: ReactElement<any, any>;
545 export type ProcessIOParameter = {
548 value: ProcessIOValue;
551 interface ProcessIOPreviewDataProps {
552 data: ProcessIOParameter[];
556 type ProcessIOPreviewProps = ProcessIOPreviewDataProps & WithStyles<CssRules>;
558 const ProcessIOPreview = memo(
559 withStyles(styles)(({ classes, data, valueLabel }: ProcessIOPreviewProps) => {
560 const showLabel = data.some((param: ProcessIOParameter) => param.label);
562 const hasMoreValues = (index: number) => (
563 data[index+1] && !isMainRow(data[index+1])
566 const isMainRow = (param: ProcessIOParameter) => (
568 ((param.id || param.label) &&
569 !param.value.secondary)
572 const RenderRow = ({index, style}) => {
573 const param = data[index];
576 [classes.noBorderRow]: hasMoreValues(index),
581 className={classNames(rowClasses)}
582 data-cy={isMainRow(param) ? "process-io-param" : ""}>
584 <Tooltip title={param.id}>
585 <Typography className={classes.paramTableCellText}>
592 {showLabel && <TableCell>
593 <Tooltip title={param.label}>
594 <Typography className={classes.paramTableCellText}>
607 <Typography className={classes.paramTableCellText}>
608 {/** Collection is an anchor so doesn't require wrapper element */}
609 {param.value.collection}
617 className={classes.paramTableRoot}
618 aria-label="Process IO Preview"
622 <TableCell>Name</TableCell>
623 {showLabel && <TableCell>Label</TableCell>}
624 <TableCell>{valueLabel}</TableCell>
625 <TableCell>Collection</TableCell>
630 {({ height, width }) =>
633 itemCount={data.length}
647 interface ProcessValuePreviewProps {
648 value: ProcessIOValue;
651 const ProcessValuePreview = withStyles(styles)(({ value, classes }: ProcessValuePreviewProps & WithStyles<CssRules>) => (
652 <Typography className={classNames(classes.paramTableCellText, value.secondary && classes.secondaryVal)}>
657 interface ProcessIORawDataProps {
658 data: ProcessIOParameter[];
661 const ProcessIORaw = withStyles(styles)(({ data }: ProcessIORawDataProps) => (
662 <Paper elevation={0} style={{minWidth: "100%", height: "100%"}}>
663 <DefaultVirtualCodeSnippet
664 lines={JSON.stringify(data, null, 2).split('\n')}
670 interface ProcessInputMountsDataProps {
671 mounts: InputCollectionMount[];
674 type ProcessInputMountsProps = ProcessInputMountsDataProps & WithStyles<CssRules>;
676 const ProcessInputMounts = withStyles(styles)(
677 connect((state: RootState) => ({
679 }))(({ mounts, classes, auth }: ProcessInputMountsProps & { auth: AuthState }) => (
681 className={classes.mountsTableRoot}
682 aria-label="Process Input Mounts"
686 <TableCell>Path</TableCell>
687 <TableCell>Portable Data Hash</TableCell>
691 {mounts.map(mount => (
692 <TableRow key={mount.path}>
694 <pre>{mount.path}</pre>
698 to={getNavUrl(mount.pdh, auth)}
699 className={classes.keepLink}
711 type FileWithSecondaryFiles = {
712 secondaryFiles: File[];
715 export const getIOParamDisplayValue = (auth: AuthState, input: CommandInputParameter | CommandOutputParameter, pdh?: string): ProcessIOValue[] => {
717 case isPrimitiveOfType(input, CWLType.BOOLEAN):
718 const boolValue = (input as BooleanCommandInputParameter).value;
719 return boolValue !== undefined && !(Array.isArray(boolValue) && boolValue.length === 0)
720 ? [{ display: <PrimitiveTooltip data={boolValue}>{renderPrimitiveValue(boolValue, false)}</PrimitiveTooltip> }]
721 : [{ display: <EmptyValue /> }];
723 case isPrimitiveOfType(input, CWLType.INT):
724 case isPrimitiveOfType(input, CWLType.LONG):
725 const intValue = (input as IntCommandInputParameter).value;
726 return intValue !== undefined &&
727 // Missing values are empty array
728 !(Array.isArray(intValue) && intValue.length === 0)
729 ? [{ display: <PrimitiveTooltip data={intValue}>{renderPrimitiveValue(intValue, false)}</PrimitiveTooltip> }]
730 : [{ display: <EmptyValue /> }];
732 case isPrimitiveOfType(input, CWLType.FLOAT):
733 case isPrimitiveOfType(input, CWLType.DOUBLE):
734 const floatValue = (input as FloatCommandInputParameter).value;
735 return floatValue !== undefined && !(Array.isArray(floatValue) && floatValue.length === 0)
736 ? [{ display: <PrimitiveTooltip data={floatValue}>{renderPrimitiveValue(floatValue, false)}</PrimitiveTooltip> }]
737 : [{ display: <EmptyValue /> }];
739 case isPrimitiveOfType(input, CWLType.STRING):
740 const stringValue = (input as StringCommandInputParameter).value || undefined;
741 return stringValue !== undefined && !(Array.isArray(stringValue) && stringValue.length === 0)
742 ? [{ display: <PrimitiveTooltip data={stringValue}>{renderPrimitiveValue(stringValue, false)}</PrimitiveTooltip> }]
743 : [{ display: <EmptyValue /> }];
745 case isPrimitiveOfType(input, CWLType.FILE):
746 const mainFile = (input as FileCommandInputParameter).value;
747 // secondaryFiles: File[] is not part of CommandOutputParameter so we cast to access secondaryFiles
748 const secondaryFiles = (mainFile as unknown as FileWithSecondaryFiles)?.secondaryFiles || [];
749 const files = [...(mainFile && !(Array.isArray(mainFile) && mainFile.length === 0) ? [mainFile] : []), ...secondaryFiles];
750 const mainFilePdhUrl = mainFile ? getResourcePdhUrl(mainFile, pdh) : "";
752 ? files.map((file, i) => fileToProcessIOValue(file, i > 0, auth, pdh, i > 0 ? mainFilePdhUrl : ""))
753 : [{ display: <EmptyValue /> }];
755 case isPrimitiveOfType(input, CWLType.DIRECTORY):
756 const directory = (input as DirectoryCommandInputParameter).value;
757 return directory !== undefined && !(Array.isArray(directory) && directory.length === 0)
758 ? [directoryToProcessIOValue(directory, auth, pdh)]
759 : [{ display: <EmptyValue /> }];
761 case getEnumType(input) !== null:
762 const enumValue = (input as EnumCommandInputParameter).value;
763 return enumValue !== undefined && enumValue ? [{ display: <PrimitiveTooltip data={enumValue}>{enumValue}</PrimitiveTooltip> }] : [{ display: <EmptyValue /> }];
765 case isArrayOfType(input, CWLType.STRING):
766 const strArray = (input as StringArrayCommandInputParameter).value || [];
767 return strArray.length ? [{ display: <PrimitiveArrayTooltip data={strArray}>{strArray.map(val => renderPrimitiveValue(val, true))}</PrimitiveArrayTooltip> }] : [{ display: <EmptyValue /> }];
769 case isArrayOfType(input, CWLType.INT):
770 case isArrayOfType(input, CWLType.LONG):
771 const intArray = (input as IntArrayCommandInputParameter).value || [];
772 return intArray.length ? [{ display: <PrimitiveArrayTooltip data={intArray}>{intArray.map(val => renderPrimitiveValue(val, true))}</PrimitiveArrayTooltip> }] : [{ display: <EmptyValue /> }];
774 case isArrayOfType(input, CWLType.FLOAT):
775 case isArrayOfType(input, CWLType.DOUBLE):
776 const floatArray = (input as FloatArrayCommandInputParameter).value || [];
777 return floatArray.length ? [{ display: <PrimitiveArrayTooltip data={floatArray}>{floatArray.map(val => renderPrimitiveValue(val, true))}</PrimitiveArrayTooltip> }] : [{ display: <EmptyValue /> }];
779 case isArrayOfType(input, CWLType.FILE):
780 const fileArrayMainFiles = (input as FileArrayCommandInputParameter).value || [];
781 const firstMainFilePdh = fileArrayMainFiles.length > 0 && fileArrayMainFiles[0] ? getResourcePdhUrl(fileArrayMainFiles[0], pdh) : "";
783 // Convert each main and secondaryFiles into array of ProcessIOValue preserving ordering
784 let fileArrayValues: ProcessIOValue[] = [];
785 for (let i = 0; i < fileArrayMainFiles.length; i++) {
786 const secondaryFiles = (fileArrayMainFiles[i] as unknown as FileWithSecondaryFiles)?.secondaryFiles || [];
787 fileArrayValues.push(
788 // Pass firstMainFilePdh to secondary files and every main file besides the first to hide pdh if equal
789 ...(fileArrayMainFiles[i] ? [fileToProcessIOValue(fileArrayMainFiles[i], false, auth, pdh, i > 0 ? firstMainFilePdh : "")] : []),
790 ...secondaryFiles.map(file => fileToProcessIOValue(file, true, auth, pdh, firstMainFilePdh))
794 return fileArrayValues.length ? fileArrayValues : [{ display: <EmptyValue /> }];
796 case isArrayOfType(input, CWLType.DIRECTORY):
797 const directories = (input as DirectoryArrayCommandInputParameter).value || [];
798 return directories.length ? directories.map(directory => directoryToProcessIOValue(directory, auth, pdh)) : [{ display: <EmptyValue /> }];
801 return [{ display: <UnsupportedValue /> }];
805 interface PrimitiveTooltipProps {
806 data: boolean | number | string;
809 const PrimitiveTooltip = (props: React.PropsWithChildren<PrimitiveTooltipProps>) => (
810 <Tooltip title={typeof props.data !== 'object' ? String(props.data) : ""}>
811 <pre>{props.children}</pre>
815 interface PrimitiveArrayTooltipProps {
819 const PrimitiveArrayTooltip = (props: React.PropsWithChildren<PrimitiveArrayTooltipProps>) => (
820 <Tooltip title={props.data.join(', ')}>
821 <span>{props.children}</span>
826 const renderPrimitiveValue = (value: any, asChip: boolean) => {
827 const isObject = typeof value === "object";
832 label={String(value)}
833 style={{marginRight: "10px"}}
839 return asChip ? <UnsupportedValueChip /> : <UnsupportedValue />;
844 * @returns keep url without keep: prefix
846 const getKeepUrl = (file: File | Directory, pdh?: string): string => {
847 const isKeepUrl = file.location?.startsWith("keep:") || false;
848 const keepUrl = isKeepUrl ? file.location?.replace("keep:", "") : pdh ? `${pdh}/${file.location}` : file.location;
849 return keepUrl || "";
852 interface KeepUrlProps {
854 res: File | Directory;
858 const getResourcePdhUrl = (res: File | Directory, pdh?: string): string => {
859 const keepUrl = getKeepUrl(res, pdh);
860 return keepUrl ? keepUrl.split("/").slice(0, 1)[0] : "";
863 const KeepUrlBase = withStyles(styles)(({ auth, res, pdh, classes }: KeepUrlProps & WithStyles<CssRules>) => {
864 const pdhUrl = getResourcePdhUrl(res, pdh);
865 // Passing a pdh always returns a relative wb2 collection url
866 const pdhWbPath = getNavUrl(pdhUrl, auth);
867 return pdhUrl && pdhWbPath ? (
868 <Tooltip title={<>View collection in Workbench<br />{pdhUrl}</>}>
871 className={classes.keepLink}
881 const KeepUrlPath = withStyles(styles)(({ auth, res, pdh, classes }: KeepUrlProps & WithStyles<CssRules>) => {
882 const keepUrl = getKeepUrl(res, pdh);
883 const keepUrlParts = keepUrl ? keepUrl.split("/") : [];
884 const keepUrlPath = keepUrlParts.length > 1 ? keepUrlParts.slice(1).join("/") : "";
886 const keepUrlPathNav = getKeepNavUrl(auth, res, pdh);
887 return keepUrlPathNav ? (
888 <Tooltip classes={{tooltip: classes.wrapTooltip}} title={<>View in keep-web<br />{keepUrlPath || "/"}</>}>
890 className={classes.keepLink}
891 href={keepUrlPathNav}
893 rel="noopener noreferrer"
903 const getKeepNavUrl = (auth: AuthState, file: File | Directory, pdh?: string): string => {
904 let 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 getImageUrl = (auth: AuthState, file: File, pdh?: string): string => {
913 const keepUrl = getKeepUrl(file, pdh);
914 return getInlineFileUrl(
915 `${auth.config.keepWebServiceUrl}/c=${keepUrl}?api_token=${auth.apiToken}`,
916 auth.config.keepWebServiceUrl,
917 auth.config.keepWebInlineServiceUrl
921 const isFileImage = (basename?: string): boolean => {
922 return basename ? (mime.getType(basename) || "").startsWith("image/") : false;
925 const isFileUrl = (location?: string): boolean =>
926 !!location && !KEEP_URL_REGEX.exec(location) && (location.startsWith("http://") || location.startsWith("https://"));
928 const normalizeDirectoryLocation = (directory: Directory): Directory => {
929 if (!directory.location) {
934 location: (directory.location || "").endsWith("/") ? directory.location : directory.location + "/",
938 const directoryToProcessIOValue = (directory: Directory, auth: AuthState, pdh?: string): ProcessIOValue => {
939 if (isExternalValue(directory)) {
940 return { display: <UnsupportedValue /> };
943 const normalizedDirectory = normalizeDirectoryLocation(directory);
948 res={normalizedDirectory}
955 res={normalizedDirectory}
962 type MuiLinkWithTooltipProps = WithStyles<CssRules> & React.PropsWithChildren<LinkProps>;
964 const MuiLinkWithTooltip = withStyles(styles)((props: MuiLinkWithTooltipProps) => (
965 <Tooltip title={props.title} classes={{tooltip: props.classes.wrapTooltip}}>
972 const fileToProcessIOValue = (file: File, secondary: boolean, auth: AuthState, pdh: string | undefined, mainFilePdh: string): ProcessIOValue => {
973 if (isExternalValue(file)) {
974 return { display: <UnsupportedValue /> };
977 if (isFileUrl(file.location)) {
984 title={file.location}
987 </MuiLinkWithTooltip>
993 const resourcePdh = getResourcePdhUrl(file, pdh);
1003 imageUrl: isFileImage(file.basename) ? getImageUrl(auth, file, pdh) : undefined,
1005 resourcePdh !== mainFilePdh ? (
1017 const isExternalValue = (val: any) => Object.keys(val).includes("$import") || Object.keys(val).includes("$include");
1019 export const EmptyValue = withStyles(styles)(({ classes }: WithStyles<CssRules>) => <span className={classes.emptyValue}>No value</span>);
1021 const UnsupportedValue = withStyles(styles)(({ classes }: WithStyles<CssRules>) => <span className={classes.emptyValue}>Cannot display value</span>);
1023 const UnsupportedValueChip = withStyles(styles)(({ classes }: WithStyles<CssRules>) => (
1026 label={"Cannot display value"}