From 2f44c60ed70e77063c31026dfde9ea93ecb82fd4 Mon Sep 17 00:00:00 2001 From: David Lyon Date: Tue, 18 Jun 2024 11:53:24 -0700 Subject: [PATCH 1/2] Add column select dropdown, no defaults --- src/common/components/Table.tsx | 141 ++++++++++++++---- .../data_products/GenomeAttribs.tsx | 129 +++++++++------- .../data_products/SampleAttribs.tsx | 25 +++- 3 files changed, 203 insertions(+), 92 deletions(-) diff --git a/src/common/components/Table.tsx b/src/common/components/Table.tsx index 7d5df23c..5fa57fa4 100644 --- a/src/common/components/Table.tsx +++ b/src/common/components/Table.tsx @@ -1,4 +1,4 @@ -import { useMemo } from 'react'; +import { useId, useMemo, useState } from 'react'; import { createColumnHelper, ColumnDef, @@ -22,9 +22,17 @@ import classes from './Table.module.scss'; import { Button } from './Button'; import { CheckBox } from './CheckBox'; import { Loader } from './Loader'; -import { HeatMapRow } from '../api/collectionsApi'; -import { Tooltip } from '@mui/material'; - +import { ColumnMeta, HeatMapRow } from '../api/collectionsApi'; +import { + Checkbox, + FormControl, + InputLabel, + ListItemText, + MenuItem, + OutlinedInput, + Select, + Tooltip, +} from '@mui/material'; /* See also: https://tanstack.com/table/v8/docs/api/core/column-def#meta This supports passing arbitrary data into the table. @@ -362,6 +370,8 @@ export const useTableColumns = ({ accessors[id] = (rowData) => rowData[index]; }); + const [columnVisibility, setColumnVisibility] = useState({}); + const fieldsOrdered = fields .filter(({ id }) => !exclude.includes(id)) .sort((a, b) => { @@ -378,30 +388,41 @@ export const useTableColumns = ({ } }); - return useMemo( - () => { - const columns = createColumnHelper(); - return fieldsOrdered.map((field) => - columns.accessor(accessors[field.id], { - header: field.displayName ?? field.id.replace(/_/g, ' ').trim(), - id: field.id, - meta: field.options, - cell: - field.render || - ((cell: CellContext) => { - const val = cell.getValue(); - if (typeof val === 'string') return cell.getValue(); - if (typeof val === 'number') - return (cell.getValue() as number).toLocaleString(); - return cell.getValue(); - }), - }) - ); - }, - // We only want to remake the columns if fieldNames or fieldsOrdered have new values - // eslint-disable-next-line react-hooks/exhaustive-deps - [JSON.stringify(fields), JSON.stringify(fieldsOrdered)] - ); + return { + columnVisibility, + setColumnVisibility, + ...useMemo( + () => { + const columns = createColumnHelper(); + setColumnVisibility((columnVisibility) => ({ + ...Object.fromEntries(fieldsOrdered.map((col) => [col.id, true])), + ...columnVisibility, + })); + return { + columns: fieldsOrdered, + columnDefs: fieldsOrdered.map((field) => + columns.accessor(accessors[field.id], { + header: field.displayName ?? field.id.replace(/_/g, ' ').trim(), + id: field.id, + meta: field.options, + cell: + field.render || + ((cell: CellContext) => { + const val = cell.getValue(); + if (typeof val === 'string') return cell.getValue(); + if (typeof val === 'number') + return (cell.getValue() as number).toLocaleString(); + return cell.getValue(); + }), + }) + ), + }; + }, + // We only want to remake the columns if fieldNames or fieldsOrdered have new values + // eslint-disable-next-line react-hooks/exhaustive-deps + [JSON.stringify(fields), JSON.stringify(fieldsOrdered)] + ), + }; }; /** @@ -423,3 +444,67 @@ export const usePageBounds = ( lastRow, }; }; + +export const ColumnSelect = ({ + columnVisibility, + setColumnVisibility, + columnMeta, +}: { + columnVisibility: { [k: string]: boolean | undefined }; + setColumnVisibility: React.Dispatch< + React.SetStateAction<{ [k: string]: boolean | undefined }> + >; + columnMeta: { [k: string]: ColumnMeta } | undefined; +}) => { + const id = useId(); + const visible = Object.entries(columnVisibility) + .filter(([id, visible]) => visible) + .map(([id]) => id); + return ( + <> + + Columns + + + + ); +}; diff --git a/src/features/collections/data_products/GenomeAttribs.tsx b/src/features/collections/data_products/GenomeAttribs.tsx index 882da437..1bc9a113 100644 --- a/src/features/collections/data_products/GenomeAttribs.tsx +++ b/src/features/collections/data_products/GenomeAttribs.tsx @@ -14,6 +14,7 @@ import { getSelection, } from '../../../common/api/collectionsApi'; import { + ColumnSelect, Pagination, Table, usePageBounds, @@ -196,65 +197,67 @@ export const GenomeAttribs: FC<{ data?.fields.findIndex((f) => f.name === '__match__') ?? -1; const idIndex = data?.fields.findIndex((f) => f.name === 'kbase_id') ?? -1; - const columns = useTableColumns({ - fields: data?.fields.map((field) => ({ - id: field.name, - displayName: columnMeta?.[field.name]?.display_name ?? field.name, - options: { - textAlign: ['float', 'int'].includes( - columnMeta?.[field.name]?.type ?? '' - ) - ? 'right' - : 'left', - }, - render: - field.name === 'kbase_id' - ? (cell) => { - // GTDB IDs are not (yet?) UPAs - if (collection_id === 'GTDB') return cell.getValue(); - const upa = (cell.getValue() as string).replace(/_/g, '/'); - return ( - - {upa} - - ); - } - : // HARDCODED Special rendering for the `classification` column - field.name === 'classification' - ? (cell) => { - return ( - - ({ + id: field.name, + displayName: columnMeta?.[field.name]?.display_name ?? field.name, + options: { + textAlign: ['float', 'int'].includes( + columnMeta?.[field.name]?.type ?? '' + ) + ? 'right' + : 'left', + }, + render: + field.name === 'kbase_id' + ? (cell) => { + // GTDB IDs are not (yet?) UPAs + if (collection_id === 'GTDB') return cell.getValue(); + const upa = (cell.getValue() as string).replace(/_/g, '/'); + return ( + - {cell.getValue() as string} - - - ); - } - : undefined, - })), - // HARDCODED the field order parameter and the hidden fields parameter hardcode overrides for which columns will appear and in what order - order: ['kbase_display_name', 'kbase_id', 'genome_size'], - exclude: ['__match__', '__sel__'], - }); + {upa} + + ); + } + : // HARDCODED Special rendering for the `classification` column + field.name === 'classification' + ? (cell) => { + return ( + + + {cell.getValue() as string} + + + ); + } + : undefined, + })), + // HARDCODED the field order parameter and the hidden fields parameter hardcode overrides for which columns will appear and in what order + order: ['kbase_display_name', 'kbase_id', 'genome_size'], + exclude: ['__match__', '__sel__'], + } + ); const table = useReactTable({ data: data?.table || [], getRowId: (row) => String(row[idIndex]), - columns: columns, + columns: columnDefs, getCoreRowModel: getCoreRowModel(), getSortedRowModel: getSortedRowModel(), @@ -272,10 +275,13 @@ export const GenomeAttribs: FC<{ enableRowSelection: true, onRowSelectionChange: setSelection, + onColumnVisibilityChange: setColumnVisibility, + state: { sorting, pagination, rowSelection: selection, + columnVisibility, }, }); @@ -340,10 +346,17 @@ export const GenomeAttribs: FC<{ justifyContent="space-between" alignItems="center" > - - Showing {formatNumber(firstRow)} - {formatNumber(lastRow)} of{' '} - {formatNumber(count || 0)} genomes - + + + Showing {formatNumber(firstRow)} - {formatNumber(lastRow)} of{' '} + {formatNumber(count || 0)} samples + + + ({ - data: data?.table || [], - getRowId: (row) => rowId(row), - columns: useTableColumns({ + const { columnDefs, columnVisibility, setColumnVisibility } = useTableColumns( + { fields: data?.fields.map((field) => ({ id: field.name, displayName: columnMeta?.[field.name]?.display_name, @@ -295,7 +294,13 @@ export const SampleAttribs: FC<{ 'genome_count', ], exclude: ['__match__', '__sel__'], - }), + } + ); + + const table = useReactTable({ + data: data?.table || [], + getRowId: (row) => rowId(row), + columns: columnDefs, getCoreRowModel: getCoreRowModel(), getSortedRowModel: getSortedRowModel(), @@ -313,10 +318,13 @@ export const SampleAttribs: FC<{ enableRowSelection: false, onRowSelectionChange: setSelectionFromSamples, + onColumnVisibilityChange: setColumnVisibility, + state: { sorting, pagination, rowSelection: sampleSelection, + columnVisibility, }, }); @@ -378,11 +386,16 @@ export const SampleAttribs: FC<{ justifyContent="space-between" alignItems="center" > - + Showing {formatNumber(firstRow)} - {formatNumber(lastRow)} of{' '} {formatNumber(countData?.count || 0)} samples + {context !== 'samples.all' ? ( Date: Mon, 24 Jun 2024 11:09:25 -0700 Subject: [PATCH 2/2] fix tests --- src/common/components/Table.test.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/common/components/Table.test.tsx b/src/common/components/Table.test.tsx index 6de0f8b2..ae8109d5 100644 --- a/src/common/components/Table.test.tsx +++ b/src/common/components/Table.test.tsx @@ -388,7 +388,7 @@ test('useTableColumns hook makes appropriate headers from string lists', () => { exclude: ['b', 'z'], order: ['c', 'a', 'q'], }); - useEffect(() => colSpy(cols), [cols]); + useEffect(() => colSpy(cols.columnDefs), [cols.columnDefs]); return <>; }; @@ -413,7 +413,7 @@ test('Empty useTableColumns hook returns empty column list', () => { const colSpy = jest.fn(); const Wrapper = () => { const cols = useTableColumns({}); - useEffect(() => colSpy(cols), [cols]); + useEffect(() => colSpy(cols.columnDefs), [cols.columnDefs]); return <>; };