From 40dbb31f2967ae705f75a1fd439042acf522f3bc Mon Sep 17 00:00:00 2001 From: Nick Grosenbacher Date: Wed, 19 Jul 2023 13:46:43 -0400 Subject: [PATCH 1/4] SWC-6492 - Add custom renderer for JSON columns - Add JSONTableCellRenderer which handles all JSON types - Custom component for each of primitives, arrays of primitives, objects with primitive values, and complex objects - Don't use ExpandableTableDataCell on JSON cells, since they have their own control. Still need to figure out how to properly handle overflow for JSON columns. - Add snapshot tests for a variety of JSON cases - Reorganize SynapseTableCell renderers --- packages/synapse-react-client/package.json | 1 + .../SynapseTable/ExpandCollapseButton.tsx | 25 + .../SynapseTable/ExpandableTableDataCell.tsx | 16 +- .../components/SynapseTable/SynapseTable.tsx | 38 +- .../SynapseTableCell}/EntityIdList.tsx | 6 +- .../EvaluationIdRenderer.tsx | 4 +- .../JSON/ComplexJSONRenderer.tsx | 56 ++ .../JSON/JSONArrayRenderer.tsx | 55 ++ .../JSON/JSONObjectRenderer.tsx | 91 ++++ .../JSON/JSONPrimitiveRenderer.tsx | 27 + .../JSON/JSONRendererUtils.ts | 21 + .../JSON/JSONTableCellRenderer.tsx | 40 ++ .../SynapseTableCell/JSON/index.ts | 5 + .../SynapseTableCell}/SynapseTableCell.tsx | 62 +-- .../SynapseTableCell}/UserIdList.tsx | 6 +- .../SynapseTable/SynapseTableCell/index.ts | 6 + .../SynapseTable/SynapseTableUtils.ts | 29 ++ .../stories/QueryWrapperPlotNav.stories.tsx | 10 + .../stories/SynapseTableCell.stories.tsx | 99 ++++ .../SynapseTable.integration.test.tsx | 2 +- .../JSON/JSONTableCellRenderer.test.tsx | 64 +++ .../JSONTableCellRenderer.test.tsx.snap | 487 ++++++++++++++++++ .../SynapseTable}/SynapseTableUtils.test.ts | 23 + .../test/containers/EntityIdList.test.tsx | 2 +- .../containers/EvaluationIdRenderer.test.tsx | 2 +- .../test/containers/UserIdList.test.tsx | 4 +- .../synapse-types/src/Table/ColumnType.ts | 1 + pnpm-lock.yaml | 4 +- 28 files changed, 1107 insertions(+), 79 deletions(-) create mode 100644 packages/synapse-react-client/src/components/SynapseTable/ExpandCollapseButton.tsx rename packages/synapse-react-client/src/components/{ => SynapseTable/SynapseTableCell}/EntityIdList.tsx (86%) rename packages/synapse-react-client/src/components/{ => SynapseTable/SynapseTableCell}/EvaluationIdRenderer.tsx (88%) create mode 100644 packages/synapse-react-client/src/components/SynapseTable/SynapseTableCell/JSON/ComplexJSONRenderer.tsx create mode 100644 packages/synapse-react-client/src/components/SynapseTable/SynapseTableCell/JSON/JSONArrayRenderer.tsx create mode 100644 packages/synapse-react-client/src/components/SynapseTable/SynapseTableCell/JSON/JSONObjectRenderer.tsx create mode 100644 packages/synapse-react-client/src/components/SynapseTable/SynapseTableCell/JSON/JSONPrimitiveRenderer.tsx create mode 100644 packages/synapse-react-client/src/components/SynapseTable/SynapseTableCell/JSON/JSONRendererUtils.ts create mode 100644 packages/synapse-react-client/src/components/SynapseTable/SynapseTableCell/JSON/JSONTableCellRenderer.tsx create mode 100644 packages/synapse-react-client/src/components/SynapseTable/SynapseTableCell/JSON/index.ts rename packages/synapse-react-client/src/components/{synapse_table_functions => SynapseTable/SynapseTableCell}/SynapseTableCell.tsx (84%) rename packages/synapse-react-client/src/components/{ => SynapseTable/SynapseTableCell}/UserIdList.tsx (84%) create mode 100644 packages/synapse-react-client/src/components/SynapseTable/SynapseTableCell/index.ts create mode 100644 packages/synapse-react-client/stories/SynapseTableCell.stories.tsx rename packages/synapse-react-client/test/{containers/table => components/SynapseTable}/SynapseTable.integration.test.tsx (99%) create mode 100644 packages/synapse-react-client/test/components/SynapseTable/SynapseTableCell/JSON/JSONTableCellRenderer.test.tsx create mode 100644 packages/synapse-react-client/test/components/SynapseTable/SynapseTableCell/JSON/__snapshots__/JSONTableCellRenderer.test.tsx.snap rename packages/synapse-react-client/test/{containers/table => components/SynapseTable}/SynapseTableUtils.test.ts (92%) diff --git a/packages/synapse-react-client/package.json b/packages/synapse-react-client/package.json index 96299668ca..9a359d16fb 100644 --- a/packages/synapse-react-client/package.json +++ b/packages/synapse-react-client/package.json @@ -85,6 +85,7 @@ "react-datetime": "^3.2.0", "react-error-boundary": "^3.1.4", "react-hot-toast": "2.2.0", + "react-inspector": "^6.0.2", "react-intersection-observer": "^9.5.2", "react-mailchimp-subscribe": "^2.1.3", "react-measure": "^2.5.2", diff --git a/packages/synapse-react-client/src/components/SynapseTable/ExpandCollapseButton.tsx b/packages/synapse-react-client/src/components/SynapseTable/ExpandCollapseButton.tsx new file mode 100644 index 0000000000..b411505183 --- /dev/null +++ b/packages/synapse-react-client/src/components/SynapseTable/ExpandCollapseButton.tsx @@ -0,0 +1,25 @@ +import React from 'react' +import IconSvg from '../IconSvg' + +type ExpandCollapseButtonProps = { + isExpanded: boolean +} & React.ButtonHTMLAttributes + +export default function ExpandCollapseButton(props: ExpandCollapseButtonProps) { + const { isExpanded, ...buttonProps } = props + return ( + + ) +} diff --git a/packages/synapse-react-client/src/components/SynapseTable/ExpandableTableDataCell.tsx b/packages/synapse-react-client/src/components/SynapseTable/ExpandableTableDataCell.tsx index 39baa63def..96dfb1ef4d 100644 --- a/packages/synapse-react-client/src/components/SynapseTable/ExpandableTableDataCell.tsx +++ b/packages/synapse-react-client/src/components/SynapseTable/ExpandableTableDataCell.tsx @@ -1,6 +1,6 @@ import React, { useRef, useState } from 'react' -import IconSvg from '../IconSvg/IconSvg' import useResizeObserver from '@react-hook/resize-observer' +import ExpandCollapseButton from './ExpandCollapseButton' export default function ExpandableTableDataCell( props: JSX.IntrinsicElements['td'], @@ -46,19 +46,11 @@ export default function ExpandableTableDataCell( ref={tdRef} > {isOverflowingWhenNotExpanded && ( - + /> )} {props.children} diff --git a/packages/synapse-react-client/src/components/SynapseTable/SynapseTable.tsx b/packages/synapse-react-client/src/components/SynapseTable/SynapseTable.tsx index b9bf1ba0a3..02d5504871 100644 --- a/packages/synapse-react-client/src/components/SynapseTable/SynapseTable.tsx +++ b/packages/synapse-react-client/src/components/SynapseTable/SynapseTable.tsx @@ -13,7 +13,6 @@ import { getUserProfileWithProfilePicAttached } from '../../utils/functions/getU import { SynapseContextType } from '../../utils/context/SynapseContext' import { ColumnModel, - ColumnType, ColumnTypeEnum, EntityHeader, FacetColumnRequest, @@ -36,7 +35,7 @@ import ModalDownload from '../ModalDownload/ModalDownload' import { QueryVisualizationContextType } from '../QueryVisualizationWrapper' import { QueryContextType } from '../QueryContext/QueryContext' import { Icon } from '../row_renderers/utils' -import { SynapseTableCell } from '../synapse_table_functions/SynapseTableCell' +import { SynapseTableCell } from './SynapseTableCell/SynapseTableCell' import { Checkbox } from '../widgets/Checkbox' import { EnumFacetFilter } from '../widgets/query-filter/EnumFacetFilter' import { @@ -48,6 +47,7 @@ import { getColumnIndicesWithType, getUniqueEntities, isFileViewOrDataset, + isSortableColumn, } from './SynapseTableUtils' import { TablePagination } from './TablePagination' import EntityIDColumnCopyIcon from './EntityIDColumnCopyIcon' @@ -550,19 +550,24 @@ export class SynapseTable extends React.Component< const columnLinkConfig = columnLinks.find(el => { return el.matchColumnName === columnName }) + const columnType = headers[colIndex].columnType + const shouldWrapInExpandable = columnType !== ColumnTypeEnum.JSON // JSON handles its own overflow const index = this.findSelectionIndex( this.state.sortedColumnSelection, columnName, ) const isBold = index === -1 ? '' : 'SRC-boldText' + const TableDataCellElement = shouldWrapInExpandable + ? ExpandableTableDataCell + : 'td' if (isColumnActive) { return ( - - + ) } return @@ -659,23 +664,6 @@ export class SynapseTable extends React.Component< return rowsFormatted } - public isSortableColumn(column: ColumnType) { - switch (column) { - case ColumnTypeEnum.USERID: - case ColumnTypeEnum.ENTITYID: - case ColumnTypeEnum.FILEHANDLEID: - case ColumnTypeEnum.STRING_LIST: - case ColumnTypeEnum.INTEGER_LIST: - case ColumnTypeEnum.BOOLEAN_LIST: - case ColumnTypeEnum.DATE_LIST: - case ColumnTypeEnum.USERID_LIST: - case ColumnTypeEnum.ENTITYID_LIST: - return false - default: - return true - } - } - private createTableHeader( headers: SelectColumn[], columnModels: ColumnModel[], @@ -745,7 +733,7 @@ export class SynapseTable extends React.Component< columnModel, lastQueryRequest, )} - {this.isSortableColumn(column.columnType) && ( + {isSortableColumn(column.columnType) && ( + /> )} {isEntityIDColumn && } diff --git a/packages/synapse-react-client/src/components/EntityIdList.tsx b/packages/synapse-react-client/src/components/SynapseTable/SynapseTableCell/EntityIdList.tsx similarity index 86% rename from packages/synapse-react-client/src/components/EntityIdList.tsx rename to packages/synapse-react-client/src/components/SynapseTable/SynapseTableCell/EntityIdList.tsx index 5d6301e506..142a9199ce 100644 --- a/packages/synapse-react-client/src/components/EntityIdList.tsx +++ b/packages/synapse-react-client/src/components/SynapseTable/SynapseTableCell/EntityIdList.tsx @@ -1,8 +1,8 @@ import React, { useEffect, useState } from 'react' import { useInView } from 'react-intersection-observer' -import { getEntityHeadersByIds } from '../synapse-client/SynapseClient' -import { useSynapseContext } from '../utils/context/SynapseContext' -import { EntityLink } from './EntityLink' +import { getEntityHeadersByIds } from '../../../synapse-client/SynapseClient' +import { useSynapseContext } from '../../../utils/context/SynapseContext' +import { EntityLink } from '../../EntityLink' export type EntityIdListProps = { entityIdList: string[] diff --git a/packages/synapse-react-client/src/components/EvaluationIdRenderer.tsx b/packages/synapse-react-client/src/components/SynapseTable/SynapseTableCell/EvaluationIdRenderer.tsx similarity index 88% rename from packages/synapse-react-client/src/components/EvaluationIdRenderer.tsx rename to packages/synapse-react-client/src/components/SynapseTable/SynapseTableCell/EvaluationIdRenderer.tsx index f0d93fbc89..89d6cafe37 100644 --- a/packages/synapse-react-client/src/components/EvaluationIdRenderer.tsx +++ b/packages/synapse-react-client/src/components/SynapseTable/SynapseTableCell/EvaluationIdRenderer.tsx @@ -1,7 +1,7 @@ import React, { useEffect, useState } from 'react' import { useInView } from 'react-intersection-observer' -import { getEvaluation } from '../synapse-client/SynapseClient' -import { useSynapseContext } from '../utils/context/SynapseContext' +import { getEvaluation } from '../../../synapse-client/SynapseClient' +import { useSynapseContext } from '../../../utils/context/SynapseContext' export type EvaluationIdRendererProps = { evaluationId: string diff --git a/packages/synapse-react-client/src/components/SynapseTable/SynapseTableCell/JSON/ComplexJSONRenderer.tsx b/packages/synapse-react-client/src/components/SynapseTable/SynapseTableCell/JSON/ComplexJSONRenderer.tsx new file mode 100644 index 0000000000..809bdb2c57 --- /dev/null +++ b/packages/synapse-react-client/src/components/SynapseTable/SynapseTableCell/JSON/ComplexJSONRenderer.tsx @@ -0,0 +1,56 @@ +import React, { useState } from 'react' +import { Typography } from '@mui/material' +import ExpandCollapseButton from '../../ExpandCollapseButton' +import { chromeLight, Inspector } from 'react-inspector' + +type ComplexJSONRendererProps = { + value: any +} + +/** + * Handles rendering any complex JSON value in a Synapse table cell. This generally acts as a fallback renderer if the + * JSON data doesn't have a simple structure handled by a different renderer. + * @param props + * @constructor + */ +export function ComplexJSONRenderer(props: ComplexJSONRendererProps) { + const { value } = props + const [expandAll, setExpandAll] = useState(false) + + // @ts-expect-error - the theme prop type provided by react-inspector is wrong + const theme: React.ComponentProps['theme'] = { + ...chromeLight, + BASE_BACKGROUND_COLOR: 'none', + } + + return ( + <> + + {expandAll ? 'Collapse' : 'Expand'} all + setExpandAll(v => !v)} + /> + + + + ) +} diff --git a/packages/synapse-react-client/src/components/SynapseTable/SynapseTableCell/JSON/JSONArrayRenderer.tsx b/packages/synapse-react-client/src/components/SynapseTable/SynapseTableCell/JSON/JSONArrayRenderer.tsx new file mode 100644 index 0000000000..251f8e5fa0 --- /dev/null +++ b/packages/synapse-react-client/src/components/SynapseTable/SynapseTableCell/JSON/JSONArrayRenderer.tsx @@ -0,0 +1,55 @@ +import { JSONPrimitiveType } from './JSONRendererUtils' +import React, { useState } from 'react' +import { Box, Collapse, Typography } from '@mui/material' +import { JSONPrimitiveRenderer } from './JSONPrimitiveRenderer' +import ExpandCollapseButton from '../../ExpandCollapseButton' + +type JSONArrayRendererProps = { value: JSONPrimitiveType[] } + +/** + * Handles rendering a JSON array of "primitive" types in a Synapse table cell. + * @param props + * @constructor + */ +export function JSONArrayRenderer(props: JSONArrayRendererProps) { + const { value } = props + const [expanded, setExpanded] = useState(false) + const showCollapseText = value.length > 1 + + if (value.length === 0) { + return ( + + {'empty array'} + + ) + } + if (value.length === 1) { + return + } + return ( + <> + {showCollapseText && ( + + {`${value.length.toLocaleString()} items`} + setExpanded(!expanded)} + /> + + )} + + {value.map((val: JSONPrimitiveType, index: number) => ( + + + + + + ))} + + + ) +} diff --git a/packages/synapse-react-client/src/components/SynapseTable/SynapseTableCell/JSON/JSONObjectRenderer.tsx b/packages/synapse-react-client/src/components/SynapseTable/SynapseTableCell/JSON/JSONObjectRenderer.tsx new file mode 100644 index 0000000000..a8af7fbca2 --- /dev/null +++ b/packages/synapse-react-client/src/components/SynapseTable/SynapseTableCell/JSON/JSONObjectRenderer.tsx @@ -0,0 +1,91 @@ +import React, { useState } from 'react' +import { Box, Collapse, Typography } from '@mui/material' +import ExpandCollapseButton from '../../ExpandCollapseButton' +import { JSONPrimitiveType } from './JSONRendererUtils' +import { JSONPrimitiveRenderer } from './JSONPrimitiveRenderer' + +const OBJECT_DEFAULT_NUM_KEYS_TO_SHOW = 3 +type JSONObjectKeyValuePairProps = { + keyName: string + value: JSONPrimitiveType +} + +function JSONObjectKeyValuePair(props: JSONObjectKeyValuePairProps) { + const { keyName, value } = props + + return ( + + + + + + + + + ) +} + +type JSONObjectRendererProps = { + value: Record +} + +/** + * Handles rendering a JSON object where all values are JSON primitives in a Synapse table cell. + * @param props + * @constructor + */ +export function JSONObjectRenderer(props: JSONObjectRendererProps) { + const { value } = props + const numKeys = Object.keys(value).length + const isCollapsible = numKeys > OBJECT_DEFAULT_NUM_KEYS_TO_SHOW + const [expanded, setExpanded] = useState(false) + + if (numKeys === 0) { + return ( + + {'empty object'} + + ) + } + + return ( + <> + {Object.entries(value) + // Render the first set of items unconditionally + .slice(0, OBJECT_DEFAULT_NUM_KEYS_TO_SHOW) + .map(([key, val]) => ( + + ))} + {/* Render the rest of the items in a Collapse */} + {isCollapsible && ( + <> + + {Object.entries(value) + .slice(OBJECT_DEFAULT_NUM_KEYS_TO_SHOW) + .map(([key, val]) => ( + + ))} + + + {expanded + ? '' + : `${( + numKeys - OBJECT_DEFAULT_NUM_KEYS_TO_SHOW + ).toLocaleString()} more`} + setExpanded(v => !v)} + /> + + + )} + + ) +} diff --git a/packages/synapse-react-client/src/components/SynapseTable/SynapseTableCell/JSON/JSONPrimitiveRenderer.tsx b/packages/synapse-react-client/src/components/SynapseTable/SynapseTableCell/JSON/JSONPrimitiveRenderer.tsx new file mode 100644 index 0000000000..639e4b3a3a --- /dev/null +++ b/packages/synapse-react-client/src/components/SynapseTable/SynapseTableCell/JSON/JSONPrimitiveRenderer.tsx @@ -0,0 +1,27 @@ +import { Typography } from '@mui/material' +import React from 'react' +import { JSONPrimitiveType } from './JSONRendererUtils' + +type JSONPrimitiveRendererProps = { value: JSONPrimitiveType } + +/** + * Handles rendering a JSON "primitive" value in a Synapse table cell. + * + * A JSON primitive value is a string, number, boolean, or null. + * @param props + * @constructor + */ +export function JSONPrimitiveRenderer(props: JSONPrimitiveRendererProps) { + const { value } = props + if (value === null) { + return ( + + {''} + + ) + } + if (typeof value === 'number') { + return <>{value.toLocaleString()} + } + return <>{String(value)} +} diff --git a/packages/synapse-react-client/src/components/SynapseTable/SynapseTableCell/JSON/JSONRendererUtils.ts b/packages/synapse-react-client/src/components/SynapseTable/SynapseTableCell/JSON/JSONRendererUtils.ts new file mode 100644 index 0000000000..6d55c9b938 --- /dev/null +++ b/packages/synapse-react-client/src/components/SynapseTable/SynapseTableCell/JSON/JSONRendererUtils.ts @@ -0,0 +1,21 @@ +export type JSONPrimitiveType = string | number | boolean | null + +/** + * returns true iff the value is a primitive that could have been parsed from JSON + * i.e. is null, string, number, or boolean + * + * This is false for objects and arrays, as well as JavaScript types that are not valid JSON, like + * undefined, NaN, Infinity, Symbol, and functions + * @param value + */ +export function isJSONPrimitive(value: unknown): value is JSONPrimitiveType { + return ( + value === null || ['string', 'number', 'boolean'].includes(typeof value) + ) +} + +export function isJSONObjectAllPrimitiveKeys( + value: object, +): value is Record { + return Object.values(value).every(v => isJSONPrimitive(v)) +} diff --git a/packages/synapse-react-client/src/components/SynapseTable/SynapseTableCell/JSON/JSONTableCellRenderer.tsx b/packages/synapse-react-client/src/components/SynapseTable/SynapseTableCell/JSON/JSONTableCellRenderer.tsx new file mode 100644 index 0000000000..ef07a432d3 --- /dev/null +++ b/packages/synapse-react-client/src/components/SynapseTable/SynapseTableCell/JSON/JSONTableCellRenderer.tsx @@ -0,0 +1,40 @@ +import React from 'react' +import { + isJSONObjectAllPrimitiveKeys, + isJSONPrimitive, +} from './JSONRendererUtils' +import { JSONPrimitiveRenderer } from './JSONPrimitiveRenderer' +import { JSONArrayRenderer } from './JSONArrayRenderer' +import { JSONObjectRenderer } from './JSONObjectRenderer' +import { ComplexJSONRenderer } from './ComplexJSONRenderer' + +export type JSONTableCellRendererProps = { + value: string | null +} + +export default function JSONTableCellRenderer( + props: JSONTableCellRendererProps, +) { + let value: unknown = props.value + try { + if (typeof value === 'string') { + value = JSON.parse(value) + } + } catch (e) { + console.warn('JSONTableCellRenderer: failed to parse JSON', e) + } + + if (value === null) { + return + } else if (Array.isArray(value) && value.every(isJSONPrimitive)) { + return + } else if ( + typeof value === 'object' && + // To render a simple flat table, ensure that we have no array/object values + isJSONObjectAllPrimitiveKeys(value) + ) { + return + } + + return +} diff --git a/packages/synapse-react-client/src/components/SynapseTable/SynapseTableCell/JSON/index.ts b/packages/synapse-react-client/src/components/SynapseTable/SynapseTableCell/JSON/index.ts new file mode 100644 index 0000000000..95c14009ad --- /dev/null +++ b/packages/synapse-react-client/src/components/SynapseTable/SynapseTableCell/JSON/index.ts @@ -0,0 +1,5 @@ +import JSONTableCellRenderer from './JSONTableCellRenderer' +import type { JSONTableCellRendererProps } from './JSONTableCellRenderer' + +export { JSONTableCellRenderer, JSONTableCellRendererProps } +export default JSONTableCellRenderer diff --git a/packages/synapse-react-client/src/components/synapse_table_functions/SynapseTableCell.tsx b/packages/synapse-react-client/src/components/SynapseTable/SynapseTableCell/SynapseTableCell.tsx similarity index 84% rename from packages/synapse-react-client/src/components/synapse_table_functions/SynapseTableCell.tsx rename to packages/synapse-react-client/src/components/SynapseTable/SynapseTableCell/SynapseTableCell.tsx index a0b0feae12..e9b5d24198 100644 --- a/packages/synapse-react-client/src/components/synapse_table_functions/SynapseTableCell.tsx +++ b/packages/synapse-react-client/src/components/SynapseTable/SynapseTableCell/SynapseTableCell.tsx @@ -1,13 +1,13 @@ import React from 'react' import dayjs from 'dayjs' -import { formatDate } from '../../utils/functions/DateFormatter' +import { formatDate } from '../../../utils/functions/DateFormatter' import { isDataset, isDatasetCollection, isEntityView, -} from '../../utils/functions/EntityTypeUtils' -import { PRODUCTION_ENDPOINT_CONFIG } from '../../utils/functions/getEndpoint' -import { AUTHENTICATED_USERS } from '../../utils/SynapseConstants' +} from '../../../utils/functions/EntityTypeUtils' +import { PRODUCTION_ENDPOINT_CONFIG } from '../../../utils/functions/getEndpoint' +import { AUTHENTICATED_USERS } from '../../../utils/SynapseConstants' import { ColumnModel, ColumnType, @@ -23,17 +23,18 @@ import { CardLink, ColumnSpecifiedLink, MarkdownLink, -} from '../CardContainerLogic' -import DirectDownload from '../DirectDownload' -import EntityIdList from '../EntityIdList' -import { EntityLink } from '../EntityLink' -import EvaluationIdRenderer from '../EvaluationIdRenderer' -import { SynapseCardLabel } from '../GenericCard' -import IconSvg, { IconName } from '../IconSvg/IconSvg' -import { useQueryContext } from '../QueryContext/QueryContext' -import { NOT_SET_DISPLAY_VALUE } from '../SynapseTable/SynapseTableConstants' -import UserCard from '../UserCard/UserCard' -import UserIdList from '../UserIdList' +} from '../../CardContainerLogic' +import DirectDownload from '../../DirectDownload' +import EntityIdList from './EntityIdList' +import { EntityLink } from '../../EntityLink' +import EvaluationIdRenderer from './EvaluationIdRenderer' +import { SynapseCardLabel } from '../../GenericCard' +import IconSvg, { IconName } from '../../IconSvg/IconSvg' +import { useQueryContext } from '../../QueryContext/QueryContext' +import { NOT_SET_DISPLAY_VALUE } from '../SynapseTableConstants' +import UserCard from '../../UserCard/UserCard' +import UserIdList from './UserIdList' +import JSONTableCellRenderer from './JSON/JSONTableCellRenderer' export type SynapseTableCellProps = { columnType: ColumnType @@ -50,20 +51,21 @@ export type SynapseTableCellProps = { rowVersionNumber?: number } -export const SynapseTableCell: React.FC = ({ - columnType, - columnValue, - isBold, - mapEntityIdToHeader, - mapUserIdToHeader, - columnLinkConfig, - columnName, - selectColumns, - columnModels, - rowData, - rowId, - rowVersionNumber, -}) => { +export function SynapseTableCell(props: SynapseTableCellProps) { + const { + columnType, + columnValue, + isBold, + mapEntityIdToHeader, + mapUserIdToHeader, + columnLinkConfig, + columnName, + selectColumns, + columnModels, + rowData, + rowId, + rowVersionNumber, + } = props const { entity } = useQueryContext() if (!columnValue) { @@ -255,6 +257,8 @@ export const SynapseTableCell: React.FC = ({ case ColumnTypeEnum.LARGETEXT: { return

{columnValue}

} + case ColumnTypeEnum.JSON: + return default: console.warn( `ColumnType ${columnType} has unspecified handler. Rendering the column value.`, diff --git a/packages/synapse-react-client/src/components/UserIdList.tsx b/packages/synapse-react-client/src/components/SynapseTable/SynapseTableCell/UserIdList.tsx similarity index 84% rename from packages/synapse-react-client/src/components/UserIdList.tsx rename to packages/synapse-react-client/src/components/SynapseTable/SynapseTableCell/UserIdList.tsx index 1478a5892a..3744812481 100644 --- a/packages/synapse-react-client/src/components/UserIdList.tsx +++ b/packages/synapse-react-client/src/components/SynapseTable/SynapseTableCell/UserIdList.tsx @@ -1,8 +1,8 @@ import React, { useEffect, useState } from 'react' import { useInView } from 'react-intersection-observer' -import { getUserProfiles } from '../synapse-client/SynapseClient' -import { useSynapseContext } from '../utils/context/SynapseContext' -import { UserCardSmall } from './UserCard/UserCardSmall' +import { getUserProfiles } from '../../../synapse-client/SynapseClient' +import { useSynapseContext } from '../../../utils/context/SynapseContext' +import { UserCardSmall } from '../../UserCard/UserCardSmall' export type UserIdListProps = { userIds: string[] diff --git a/packages/synapse-react-client/src/components/SynapseTable/SynapseTableCell/index.ts b/packages/synapse-react-client/src/components/SynapseTable/SynapseTableCell/index.ts new file mode 100644 index 0000000000..8c5773e57a --- /dev/null +++ b/packages/synapse-react-client/src/components/SynapseTable/SynapseTableCell/index.ts @@ -0,0 +1,6 @@ +import { SynapseTableCell } from './SynapseTableCell' +import type { SynapseTableCellProps } from './SynapseTableCell' + +export { SynapseTableCellProps, SynapseTableCell } + +export default SynapseTableCell diff --git a/packages/synapse-react-client/src/components/SynapseTable/SynapseTableUtils.ts b/packages/synapse-react-client/src/components/SynapseTable/SynapseTableUtils.ts index 2f8787592b..92fa5414a4 100644 --- a/packages/synapse-react-client/src/components/SynapseTable/SynapseTableUtils.ts +++ b/packages/synapse-react-client/src/components/SynapseTable/SynapseTableUtils.ts @@ -85,3 +85,32 @@ export const getFileColumnModelId = ( // else the file ID column was not found return undefined } + +export function isSortableColumn(column: ColumnType) { + switch (column) { + case ColumnTypeEnum.STRING: + case ColumnTypeEnum.DOUBLE: + case ColumnTypeEnum.BOOLEAN: + case ColumnTypeEnum.INTEGER: + case ColumnTypeEnum.DATE: + case ColumnTypeEnum.LINK: + case ColumnTypeEnum.MEDIUMTEXT: + case ColumnTypeEnum.LARGETEXT: + return true + case ColumnTypeEnum.USERID: + case ColumnTypeEnum.ENTITYID: + case ColumnTypeEnum.FILEHANDLEID: + case ColumnTypeEnum.STRING_LIST: + case ColumnTypeEnum.INTEGER_LIST: + case ColumnTypeEnum.BOOLEAN_LIST: + case ColumnTypeEnum.DATE_LIST: + case ColumnTypeEnum.USERID_LIST: + case ColumnTypeEnum.ENTITYID_LIST: + case ColumnTypeEnum.EVALUATIONID: + case ColumnTypeEnum.JSON: + return false + default: + console.warn(`Unhandled column type: ${column}`) + return false + } +} diff --git a/packages/synapse-react-client/stories/QueryWrapperPlotNav.stories.tsx b/packages/synapse-react-client/stories/QueryWrapperPlotNav.stories.tsx index c6a33f2f7d..817fefdbbf 100644 --- a/packages/synapse-react-client/stories/QueryWrapperPlotNav.stories.tsx +++ b/packages/synapse-react-client/stories/QueryWrapperPlotNav.stories.tsx @@ -392,3 +392,13 @@ export const People: Story = { }, }, } + +export const TableWithJSONColumns: Story = { + args: { + sql: 'SELECT * FROM syn52115635', + tableConfiguration: {}, + name: 'JSON Column Demo', + hideSqlEditorControl: false, + shouldDeepLink: false, + }, +} diff --git a/packages/synapse-react-client/stories/SynapseTableCell.stories.tsx b/packages/synapse-react-client/stories/SynapseTableCell.stories.tsx new file mode 100644 index 0000000000..5dba0ad426 --- /dev/null +++ b/packages/synapse-react-client/stories/SynapseTableCell.stories.tsx @@ -0,0 +1,99 @@ +import { Meta, StoryObj } from '@storybook/react' +import { SynapseTableCell } from '../src/components/SynapseTable/SynapseTableCell/SynapseTableCell' +import { ColumnTypeEnum } from '@sage-bionetworks/synapse-types' +import { QueryContextProvider, TargetEnum } from '../src' +import { mockTableEntity } from '../mocks/entity/mockTableEntity' + +const meta = { + title: 'Explore/SynapseTableCell', + component: SynapseTableCell, + tags: ['autodocs'], + decorators: [ + (Story: StoryObj) => ( + + + + ), + ], +} satisfies Meta +export default meta +type Story = StoryObj + +export const String: Story = { + args: { + columnType: ColumnTypeEnum.STRING, + columnValue: 'stringValue', + }, +} + +export const NoValue: Story = { + args: { + columnType: ColumnTypeEnum.STRING, + columnValue: null, + }, +} + +export const Integer: Story = { + args: { + columnType: ColumnTypeEnum.INTEGER, + columnValue: '1000', + }, +} + +export const EmptyJSONObject: Story = { + args: { + columnType: ColumnTypeEnum.JSON, + columnValue: JSON.stringify({}), + }, +} + +export const EmptyJSONArray: Story = { + args: { + columnType: ColumnTypeEnum.JSON, + columnValue: JSON.stringify([]), + }, +} + +export const JSONObject: Story = { + args: { + columnType: ColumnTypeEnum.JSON, + columnValue: JSON.stringify({ + string: 'hello', + numberInt: 2, + numberDbl: 1.75, + null: null, + boolean: false, + }), + }, +} + +export const JSONArray: Story = { + args: { + columnType: ColumnTypeEnum.JSON, + columnValue: JSON.stringify(['foo', 1000, null, true, false]), + }, +} + +export const ComplexJSONType: Story = { + args: { + columnType: ColumnTypeEnum.JSON, + columnValue: JSON.stringify([ + { + foo: [ + 'bar', + 'baz', + { + qux: ['quux', 'abc'], + lorem: 'ipsum', + }, + ], + }, + null, + 'baz', + ]), + }, +} diff --git a/packages/synapse-react-client/test/containers/table/SynapseTable.integration.test.tsx b/packages/synapse-react-client/test/components/SynapseTable/SynapseTable.integration.test.tsx similarity index 99% rename from packages/synapse-react-client/test/containers/table/SynapseTable.integration.test.tsx rename to packages/synapse-react-client/test/components/SynapseTable/SynapseTable.integration.test.tsx index d37fbeeff1..4c8d5ec9c9 100644 --- a/packages/synapse-react-client/test/containers/table/SynapseTable.integration.test.tsx +++ b/packages/synapse-react-client/test/components/SynapseTable/SynapseTable.integration.test.tsx @@ -13,7 +13,7 @@ import { import { SynapseTableCell, SynapseTableCellProps, -} from '../../../src/components/synapse_table_functions/SynapseTableCell' +} from '../../../src/components/SynapseTable/SynapseTableCell/SynapseTableCell' import SynapseTable, { SORT_STATE, SynapseTableProps, diff --git a/packages/synapse-react-client/test/components/SynapseTable/SynapseTableCell/JSON/JSONTableCellRenderer.test.tsx b/packages/synapse-react-client/test/components/SynapseTable/SynapseTableCell/JSON/JSONTableCellRenderer.test.tsx new file mode 100644 index 0000000000..7b8ccd15a8 --- /dev/null +++ b/packages/synapse-react-client/test/components/SynapseTable/SynapseTableCell/JSON/JSONTableCellRenderer.test.tsx @@ -0,0 +1,64 @@ +import React from 'react' +import JSONTableCellRenderer from '../../../../../src/components/SynapseTable/SynapseTableCell/JSON' +import { render } from '@testing-library/react' + +describe('JSONTableCellRenderer', () => { + it.each<[string, any]>([ + ['Empty array', []], + ['Empty object', {}], + ['Array of primitives', ['foo', 1000, -25.67, null, true, false]], + [ + 'Object of primitives', + { + string: 'foo', + numberInt: 1000, + numberFloat: -25.67, + null: null, + true: true, + false: false, + }, + ], + ['one item', ['foo']], + ['one null item', [null]], + ['null', null], + [ + 'complex array', + [ + { + foo: [ + 'bar', + 'baz', + { + qux: 'quux', + lorem: 'ipsum', + }, + ], + }, + null, + ['qwerty', 'asdf'], + ], + ], + [ + 'complex object', + { + foo: [ + 'bar', + 'baz', + { + qux: ['quux', 'abc'], + lorem: 'ipsum', + }, + ], + + null: null, + bar: 'baz', + zxcv: { + querty: 'asdf', + }, + }, + ], + ])('handles %p case', (_case, jsonObject) => { + const { container } = render() + expect(container).toMatchSnapshot() + }) +}) diff --git a/packages/synapse-react-client/test/components/SynapseTable/SynapseTableCell/JSON/__snapshots__/JSONTableCellRenderer.test.tsx.snap b/packages/synapse-react-client/test/components/SynapseTable/SynapseTableCell/JSON/__snapshots__/JSONTableCellRenderer.test.tsx.snap new file mode 100644 index 0000000000..6de7a41fde --- /dev/null +++ b/packages/synapse-react-client/test/components/SynapseTable/SynapseTableCell/JSON/__snapshots__/JSONTableCellRenderer.test.tsx.snap @@ -0,0 +1,487 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`JSONTableCellRenderer handles "Array of primitives" case 1`] = ` +
+ + 6 items + + +
+
+
+
+ + foo + +
+
+ + 1,000 + +
+
+ + -25.67 + +
+
+ + + <null value> + + +
+
+ + true + +
+
+ + false + +
+
+
+
+
+`; + +exports[`JSONTableCellRenderer handles "Empty array" case 1`] = ` +
+ + empty array + +
+`; + +exports[`JSONTableCellRenderer handles "Empty object" case 1`] = ` +
+ + empty object + +
+`; + +exports[`JSONTableCellRenderer handles "Object of primitives" case 1`] = ` +
+
+ + string + + + foo + +
+
+ + numberInt + + + 1,000 + +
+
+ + numberFloat + + + -25.67 + +
+
+
+
+
+ + null + + + + <null value> + + +
+
+ + true + + + true + +
+
+ + false + + + false + +
+
+
+
+ + 3 more + + +
+`; + +exports[`JSONTableCellRenderer handles "complex array" case 1`] = ` +
+ + Expand + all + + +
    +
+`; + +exports[`JSONTableCellRenderer handles "complex object" case 1`] = ` +
+ + Expand + all + + +
    +
+`; + +exports[`JSONTableCellRenderer handles "null" case 1`] = ` +
+ + <null value> + +
+`; + +exports[`JSONTableCellRenderer handles "one item" case 1`] = ` +
+ foo +
+`; + +exports[`JSONTableCellRenderer handles "one null item" case 1`] = ` +
+ + <null value> + +
+`; diff --git a/packages/synapse-react-client/test/containers/table/SynapseTableUtils.test.ts b/packages/synapse-react-client/test/components/SynapseTable/SynapseTableUtils.test.ts similarity index 92% rename from packages/synapse-react-client/test/containers/table/SynapseTableUtils.test.ts rename to packages/synapse-react-client/test/components/SynapseTable/SynapseTableUtils.test.ts index 1355955e42..6f1a03912d 100644 --- a/packages/synapse-react-client/test/containers/table/SynapseTableUtils.test.ts +++ b/packages/synapse-react-client/test/components/SynapseTable/SynapseTableUtils.test.ts @@ -2,6 +2,7 @@ import { getColumnIndicesWithType, getFileColumnModelId, getUniqueEntities, + isSortableColumn, } from '../../../src/components/SynapseTable/SynapseTableUtils' import { ColumnModel, @@ -293,4 +294,26 @@ describe('Synapse Table Utilities tests', () => { expect(getFileColumnModelId(columnModels)).toEqual(undefined) }) }) + + describe('isSortableColumn', () => { + it('throws a warning and returns false for an unknown type', () => { + const consoleWarnSpy = jest + .spyOn(console, 'warn') + .mockImplementation(() => {}) + + // Call under test + isSortableColumn('FAKE_COLUMN_TYPE' as ColumnTypeEnum) + + expect(consoleWarnSpy).toHaveBeenCalled() + consoleWarnSpy.mockRestore() + }) + + it('explicitly handles all known types', () => { + const consoleWarnSpy = jest.spyOn(console, 'warn') + Object.keys(ColumnTypeEnum).forEach(key => { + isSortableColumn(key) + }) + expect(consoleWarnSpy).not.toHaveBeenCalled() + }) + }) }) diff --git a/packages/synapse-react-client/test/containers/EntityIdList.test.tsx b/packages/synapse-react-client/test/containers/EntityIdList.test.tsx index 96d78dc56a..d5ba68f47f 100644 --- a/packages/synapse-react-client/test/containers/EntityIdList.test.tsx +++ b/packages/synapse-react-client/test/containers/EntityIdList.test.tsx @@ -2,7 +2,7 @@ import React from 'react' import { mockAllIsIntersecting } from 'react-intersection-observer/test-utils' import EntityIdList, { EntityIdListProps, -} from '../../src/components/EntityIdList' +} from '../../src/components/SynapseTable/SynapseTableCell/EntityIdList' import { act } from '@testing-library/react' import { MOCK_CONTEXT_VALUE, diff --git a/packages/synapse-react-client/test/containers/EvaluationIdRenderer.test.tsx b/packages/synapse-react-client/test/containers/EvaluationIdRenderer.test.tsx index f84dc37df8..376fadf849 100644 --- a/packages/synapse-react-client/test/containers/EvaluationIdRenderer.test.tsx +++ b/packages/synapse-react-client/test/containers/EvaluationIdRenderer.test.tsx @@ -2,7 +2,7 @@ import React from 'react' import { mockAllIsIntersecting } from 'react-intersection-observer/test-utils' import EvaluationIdRenderer, { EvaluationIdRendererProps, -} from '../../src/components/EvaluationIdRenderer' +} from '../../src/components/SynapseTable/SynapseTableCell/EvaluationIdRenderer' import { act } from '@testing-library/react' import { MOCK_CONTEXT_VALUE, diff --git a/packages/synapse-react-client/test/containers/UserIdList.test.tsx b/packages/synapse-react-client/test/containers/UserIdList.test.tsx index 651373a865..17bac2c443 100644 --- a/packages/synapse-react-client/test/containers/UserIdList.test.tsx +++ b/packages/synapse-react-client/test/containers/UserIdList.test.tsx @@ -1,6 +1,8 @@ import React from 'react' import { mockAllIsIntersecting } from 'react-intersection-observer/test-utils' -import UserIdList, { UserIdListProps } from '../../src/components/UserIdList' +import UserIdList, { + UserIdListProps, +} from '../../src/components/SynapseTable/SynapseTableCell/UserIdList' import { act } from '@testing-library/react' import { MOCK_CONTEXT_VALUE, diff --git a/packages/synapse-types/src/Table/ColumnType.ts b/packages/synapse-types/src/Table/ColumnType.ts index 7b7148207e..eb5fa4c268 100644 --- a/packages/synapse-types/src/Table/ColumnType.ts +++ b/packages/synapse-types/src/Table/ColumnType.ts @@ -18,6 +18,7 @@ export enum ColumnTypeEnum { USERID_LIST = 'USERID_LIST', ENTITYID_LIST = 'ENTITYID_LIST', EVALUATIONID = 'EVALUATIONID', + JSON = 'JSON', } // Allow an instance of the enum or any of the values in the num diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 623ce6fc07..18e783283c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -921,6 +921,9 @@ importers: react-hot-toast: specifier: 2.2.0 version: 2.2.0(csstype@3.1.2)(react-dom@18.2.0)(react@18.2.0) + react-inspector: + specifier: ^6.0.2 + version: 6.0.2(react@18.2.0) react-intersection-observer: specifier: ^9.5.2 version: 9.5.2(react@18.2.0) @@ -15712,7 +15715,6 @@ packages: react: ^16.8.4 || ^17.0.0 || ^18.0.0 dependencies: react: 18.2.0 - dev: true /react-intersection-observer@9.5.2(react@18.2.0): resolution: {integrity: sha512-EmoV66/yvksJcGa1rdW0nDNc4I1RifDWkT50gXSFnPLYQ4xUptuDD4V7k+Rj1OgVAlww628KLGcxPXFlOkkU/Q==} From fadfbe0091b7d9188bdccc70f1be3277ee3e5be0 Mon Sep 17 00:00:00 2001 From: Nick Grosenbacher Date: Thu, 20 Jul 2023 11:43:49 -0400 Subject: [PATCH 2/4] SWC-6492 - Add JSONRendererUtils tests and fix a few edge cases --- .../JSON/JSONRendererUtils.ts | 10 ++-- .../JSON/JSONRendererUtils.test.ts | 49 +++++++++++++++++++ 2 files changed, 56 insertions(+), 3 deletions(-) create mode 100644 packages/synapse-react-client/test/components/SynapseTable/SynapseTableCell/JSON/JSONRendererUtils.test.ts diff --git a/packages/synapse-react-client/src/components/SynapseTable/SynapseTableCell/JSON/JSONRendererUtils.ts b/packages/synapse-react-client/src/components/SynapseTable/SynapseTableCell/JSON/JSONRendererUtils.ts index 6d55c9b938..674edf37ff 100644 --- a/packages/synapse-react-client/src/components/SynapseTable/SynapseTableCell/JSON/JSONRendererUtils.ts +++ b/packages/synapse-react-client/src/components/SynapseTable/SynapseTableCell/JSON/JSONRendererUtils.ts @@ -9,9 +9,13 @@ export type JSONPrimitiveType = string | number | boolean | null * @param value */ export function isJSONPrimitive(value: unknown): value is JSONPrimitiveType { - return ( - value === null || ['string', 'number', 'boolean'].includes(typeof value) - ) + if (value === null) { + return true + } + if (typeof value === 'number') { + return !Number.isNaN(value) && Number.isFinite(value) + } + return ['string', 'boolean'].includes(typeof value) } export function isJSONObjectAllPrimitiveKeys( diff --git a/packages/synapse-react-client/test/components/SynapseTable/SynapseTableCell/JSON/JSONRendererUtils.test.ts b/packages/synapse-react-client/test/components/SynapseTable/SynapseTableCell/JSON/JSONRendererUtils.test.ts new file mode 100644 index 0000000000..e1a25900f3 --- /dev/null +++ b/packages/synapse-react-client/test/components/SynapseTable/SynapseTableCell/JSON/JSONRendererUtils.test.ts @@ -0,0 +1,49 @@ +import { + isJSONObjectAllPrimitiveKeys, + isJSONPrimitive, +} from '../../../../../src/components/SynapseTable/SynapseTableCell/JSON/JSONRendererUtils' + +describe('JSONRendererUtils', () => { + describe('isJSONPrimitive', () => { + it.each([ + ['string', 'foo', true], + ['number', 25, true], + ['boolean', false, true], + ['null', null, true], + ['object', { foo: 'bar' }, false], + ['array', ['foo', 'bar'], false], + ['function', () => {}, false], + ['undefined', undefined, false], + ['Infinity', Infinity, false], + ['NaN', NaN, false], + ['Symbol', Symbol('someSymbol'), false], + ])('%p case', (_case, value, expected) => { + expect(isJSONPrimitive(value)).toBe(expected) + }) + }) + + describe('isJSONObjectAllPrimitiveKeys', () => { + const allPrimitiveValues = { + string: 'foo', + number: 25, + boolean: false, + null: null, + } + it('should return true if all keys are primitive', () => { + expect(isJSONObjectAllPrimitiveKeys(allPrimitiveValues)).toBe(true) + }) + + it('should return true if there are no keys (empty object)', () => { + expect(isJSONObjectAllPrimitiveKeys({})).toBe(true) + }) + + it('should return false if any keys are not primitive', () => { + expect( + isJSONObjectAllPrimitiveKeys({ + ...allPrimitiveValues, + nonPrimitive: {}, + }), + ).toBe(false) + }) + }) +}) From 24541f5dd6d3f04946f970ce7f0c0d7089b4e4d4 Mon Sep 17 00:00:00 2001 From: Nick Grosenbacher Date: Thu, 20 Jul 2023 12:41:56 -0400 Subject: [PATCH 3/4] SWC-6492 - fix test missing assertion - handle all primitives with JSONPrimitiveRenderer even though the API seems to make this impossible. --- .../SynapseTableCell/JSON/JSONTableCellRenderer.tsx | 2 +- .../test/components/SynapseTable/SynapseTableUtils.test.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/synapse-react-client/src/components/SynapseTable/SynapseTableCell/JSON/JSONTableCellRenderer.tsx b/packages/synapse-react-client/src/components/SynapseTable/SynapseTableCell/JSON/JSONTableCellRenderer.tsx index ef07a432d3..78fc1a8153 100644 --- a/packages/synapse-react-client/src/components/SynapseTable/SynapseTableCell/JSON/JSONTableCellRenderer.tsx +++ b/packages/synapse-react-client/src/components/SynapseTable/SynapseTableCell/JSON/JSONTableCellRenderer.tsx @@ -24,7 +24,7 @@ export default function JSONTableCellRenderer( console.warn('JSONTableCellRenderer: failed to parse JSON', e) } - if (value === null) { + if (isJSONPrimitive(value) || value === null) { return } else if (Array.isArray(value) && value.every(isJSONPrimitive)) { return diff --git a/packages/synapse-react-client/test/components/SynapseTable/SynapseTableUtils.test.ts b/packages/synapse-react-client/test/components/SynapseTable/SynapseTableUtils.test.ts index 6f1a03912d..927c3bd20b 100644 --- a/packages/synapse-react-client/test/components/SynapseTable/SynapseTableUtils.test.ts +++ b/packages/synapse-react-client/test/components/SynapseTable/SynapseTableUtils.test.ts @@ -302,7 +302,7 @@ describe('Synapse Table Utilities tests', () => { .mockImplementation(() => {}) // Call under test - isSortableColumn('FAKE_COLUMN_TYPE' as ColumnTypeEnum) + expect(isSortableColumn('FAKE_COLUMN_TYPE' as ColumnTypeEnum)).toBe(false) expect(consoleWarnSpy).toHaveBeenCalled() consoleWarnSpy.mockRestore() From b0b2d8db2f1bbc4e43dbd3fabd8cc959a3ad3d8b Mon Sep 17 00:00:00 2001 From: Nick Grosenbacher Date: Mon, 24 Jul 2023 09:59:17 -0400 Subject: [PATCH 4/4] SWC-6492 - Change "empty" text to capitalize first - Fix misaligned text in rows (update margin/padding, use `Typography` to attempt to be consistent) - Add small amount of padding to ComplexJSONRenderer to prevent icon clipping --- .../JSON/ComplexJSONRenderer.tsx | 34 +-- .../JSON/JSONArrayRenderer.tsx | 6 +- .../JSON/JSONObjectRenderer.tsx | 6 +- .../SynapseTableCell/SynapseTableCell.tsx | 13 +- .../JSONTableCellRenderer.test.tsx.snap | 268 +++++++++--------- 5 files changed, 173 insertions(+), 154 deletions(-) diff --git a/packages/synapse-react-client/src/components/SynapseTable/SynapseTableCell/JSON/ComplexJSONRenderer.tsx b/packages/synapse-react-client/src/components/SynapseTable/SynapseTableCell/JSON/ComplexJSONRenderer.tsx index 809bdb2c57..51b83ad1eb 100644 --- a/packages/synapse-react-client/src/components/SynapseTable/SynapseTableCell/JSON/ComplexJSONRenderer.tsx +++ b/packages/synapse-react-client/src/components/SynapseTable/SynapseTableCell/JSON/ComplexJSONRenderer.tsx @@ -1,5 +1,5 @@ import React, { useState } from 'react' -import { Typography } from '@mui/material' +import { Box, Typography } from '@mui/material' import ExpandCollapseButton from '../../ExpandCollapseButton' import { chromeLight, Inspector } from 'react-inspector' @@ -36,21 +36,23 @@ export function ComplexJSONRenderer(props: ComplexJSONRendererProps) { onClick={() => setExpandAll(v => !v)} /> - + + + ) } diff --git a/packages/synapse-react-client/src/components/SynapseTable/SynapseTableCell/JSON/JSONArrayRenderer.tsx b/packages/synapse-react-client/src/components/SynapseTable/SynapseTableCell/JSON/JSONArrayRenderer.tsx index 251f8e5fa0..e68301d911 100644 --- a/packages/synapse-react-client/src/components/SynapseTable/SynapseTableCell/JSON/JSONArrayRenderer.tsx +++ b/packages/synapse-react-client/src/components/SynapseTable/SynapseTableCell/JSON/JSONArrayRenderer.tsx @@ -19,7 +19,7 @@ export function JSONArrayRenderer(props: JSONArrayRendererProps) { if (value.length === 0) { return ( - {'empty array'} + {'Empty array'} ) } @@ -31,7 +31,7 @@ export function JSONArrayRenderer(props: JSONArrayRendererProps) { {showCollapseText && ( {`${value.length.toLocaleString()} items`} {value.map((val: JSONPrimitiveType, index: number) => ( - + diff --git a/packages/synapse-react-client/src/components/SynapseTable/SynapseTableCell/JSON/JSONObjectRenderer.tsx b/packages/synapse-react-client/src/components/SynapseTable/SynapseTableCell/JSON/JSONObjectRenderer.tsx index a8af7fbca2..975e20c9ec 100644 --- a/packages/synapse-react-client/src/components/SynapseTable/SynapseTableCell/JSON/JSONObjectRenderer.tsx +++ b/packages/synapse-react-client/src/components/SynapseTable/SynapseTableCell/JSON/JSONObjectRenderer.tsx @@ -14,7 +14,7 @@ function JSONObjectKeyValuePair(props: JSONObjectKeyValuePairProps) { const { keyName, value } = props return ( - + @@ -46,7 +46,7 @@ export function JSONObjectRenderer(props: JSONObjectRendererProps) { if (numKeys === 0) { return ( - {'empty object'} + {'Empty object'} ) } @@ -71,7 +71,7 @@ export function JSONObjectRenderer(props: JSONObjectRendererProps) { {expanded ? '' diff --git a/packages/synapse-react-client/src/components/SynapseTable/SynapseTableCell/SynapseTableCell.tsx b/packages/synapse-react-client/src/components/SynapseTable/SynapseTableCell/SynapseTableCell.tsx index e9b5d24198..6a2dd6fa26 100644 --- a/packages/synapse-react-client/src/components/SynapseTable/SynapseTableCell/SynapseTableCell.tsx +++ b/packages/synapse-react-client/src/components/SynapseTable/SynapseTableCell/SynapseTableCell.tsx @@ -35,6 +35,7 @@ import { NOT_SET_DISPLAY_VALUE } from '../SynapseTableConstants' import UserCard from '../../UserCard/UserCard' import UserIdList from './UserIdList' import JSONTableCellRenderer from './JSON/JSONTableCellRenderer' +import { Typography } from '@mui/material' export type SynapseTableCellProps = { columnType: ColumnType @@ -255,7 +256,11 @@ export function SynapseTableCell(props: SynapseTableCellProps) { case ColumnTypeEnum.BOOLEAN: case ColumnTypeEnum.MEDIUMTEXT: case ColumnTypeEnum.LARGETEXT: { - return

{columnValue}

+ return ( + + {columnValue} + + ) } case ColumnTypeEnum.JSON: return @@ -263,7 +268,11 @@ export function SynapseTableCell(props: SynapseTableCellProps) { console.warn( `ColumnType ${columnType} has unspecified handler. Rendering the column value.`, ) - return

{columnValue}

+ return ( + + {columnValue} + + ) } // We can reach this if we don't get a mapping of IDs to entities or principals. // TODO: If we don't have a id:data mapping, we should render a component that can fetch the required data, rather than breaking from the case. diff --git a/packages/synapse-react-client/test/components/SynapseTable/SynapseTableCell/JSON/__snapshots__/JSONTableCellRenderer.test.tsx.snap b/packages/synapse-react-client/test/components/SynapseTable/SynapseTableCell/JSON/__snapshots__/JSONTableCellRenderer.test.tsx.snap index 6de7a41fde..f4047c9024 100644 --- a/packages/synapse-react-client/test/components/SynapseTable/SynapseTableCell/JSON/__snapshots__/JSONTableCellRenderer.test.tsx.snap +++ b/packages/synapse-react-client/test/components/SynapseTable/SynapseTableCell/JSON/__snapshots__/JSONTableCellRenderer.test.tsx.snap @@ -3,7 +3,7 @@ exports[`JSONTableCellRenderer handles "Array of primitives" case 1`] = `
6 items
`; @@ -128,7 +128,7 @@ exports[`JSONTableCellRenderer handles "Empty object" case 1`] = ` exports[`JSONTableCellRenderer handles "Object of primitives" case 1`] = `
3 more -
    -
+
    + +
+
`; @@ -373,90 +377,94 @@ exports[`JSONTableCellRenderer handles "complex object" case 1`] = ` -
    -