Skip to content
Open
1 change: 1 addition & 0 deletions components/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
"@mui/x-date-pickers": "^7.23.1",
"@perses-dev/core": "0.53.0",
"@perses-dev/spec": "0.2.0-beta.0",
"@tanstack/match-sorter-utils": "^8.19.4",
"@tanstack/react-table": "^8.20.5",
"@uiw/react-codemirror": "^4.19.1",
"date-fns": "^4.1.0",
Expand Down
38 changes: 31 additions & 7 deletions components/src/Table/Table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,20 +14,23 @@
import { Stack, useTheme } from '@mui/material';
import {
ColumnDef,
getCoreRowModel,
getExpandedRowModel,
getPaginationRowModel,
getSortedRowModel,
OnChangeFn,
Row,
RowSelectionState,
SortingState,
Table as TanstackTable,
getCoreRowModel,
getPaginationRowModel,
getSortedRowModel,
useReactTable,
VisibilityState,
} from '@tanstack/react-table';
import { ReactElement, useCallback, useMemo } from 'react';
import { ReactElement, useCallback, useMemo, useState } from 'react';
import { useFuzzySearch } from './hooks/useFuzzySearch';
import { TableCheckbox } from './TableCheckbox';
import { VirtualizedTable } from './VirtualizedTable';
import { DEFAULT_COLUMN_WIDTH, TableProps, persesColumnsToTanstackColumns } from './model/table-model';
import { DEFAULT_COLUMN_WIDTH, persesColumnsToTanstackColumns, TableProps } from './model/table-model';

const DEFAULT_GET_ROW_ID = (data: unknown, index: number): string => {
return `${index}`;
Expand Down Expand Up @@ -64,10 +67,20 @@ export function Table<TableData>({
pagination,
onPaginationChange,
rowSelectionVariant = 'standard',
getSubRows,
showSearch,
showColumnFilter,
hiddenColumns,
...otherProps
}: TableProps<TableData>): ReactElement {
const theme = useTheme();

const { globalFilter, setGlobalFilter, fuzzySearchOptions } = useFuzzySearch<TableData>(showSearch);

const [columnVisibility, setColumnVisibility] = useState<VisibilityState>(
hiddenColumns?.reduce((acc, columnId) => ({ ...acc, [columnId]: false }), {}) ?? {}
);

const handleRowSelectionChange: OnChangeFn<RowSelectionState> = (rowSelectionUpdater) => {
const newRowSelection =
typeof rowSelectionUpdater === 'function' ? rowSelectionUpdater(rowSelection) : rowSelectionUpdater;
Expand Down Expand Up @@ -177,7 +190,7 @@ export function Table<TableData>({
const table = useReactTable({
data,
columns: tableColumns,
getRowId,
getRowId: getRowId,
getCoreRowModel: getCoreRowModel(),
getSortedRowModel: getSortedRowModel(),
getPaginationRowModel: pagination ? getPaginationRowModel() : undefined,
Expand All @@ -187,12 +200,18 @@ export function Table<TableData>({
enableRowSelection: !!checkboxSelection,
onRowSelectionChange: handleRowSelectionChange,
onSortingChange: handleSortingChange,
onColumnVisibilityChange: setColumnVisibility,
getSubRows: getSubRows,
getExpandedRowModel: getSubRows ? getExpandedRowModel() : undefined,
...fuzzySearchOptions,
// For now, defaulting to sort by descending first. We can expose the ability
// to customize it if/when we have use cases for it.
sortDescFirst: true,
state: {
rowSelection,
sorting,
globalFilter: showSearch ? globalFilter : undefined,
columnVisibility,
...(pagination ? { pagination } : {}),
},
});
Expand All @@ -214,12 +233,17 @@ export function Table<TableData>({
defaultColumnHeight={defaultColumnHeight}
onRowClick={handleRowClick}
rows={table.getRowModel().rows}
columns={table.getAllFlatColumns()}
columns={table.getVisibleFlatColumns()}
headers={table.getHeaderGroups()}
cellConfigs={cellConfigs}
pagination={pagination}
onPaginationChange={onPaginationChange}
rowCount={table.getRowCount()}
showSearch={showSearch}
showColumnFilter={showColumnFilter}
globalFilter={globalFilter}
onGlobalFilterChange={setGlobalFilter}
allColumns={table.getAllColumns()}
/>
);
}
146 changes: 146 additions & 0 deletions components/src/Table/TableToolbar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
// Copyright The Perses Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

import { Checkbox, IconButton, InputAdornment, ListItemText, Menu, MenuItem, Stack } from '@mui/material';
import { Column } from '@tanstack/react-table';
import { ReactElement, useState } from 'react';
import Magnify from 'mdi-material-ui/Magnify';
import Close from 'mdi-material-ui/Close';
import ViewColumn from 'mdi-material-ui/ViewColumn';
import { TextField } from '../controls';

export interface TableToolbarProps<TableData> {
/**
* When `true`, a search input is rendered.
*/
showSearch?: boolean;

/**
* Current value of the global filter / search query.
*/
globalFilter: string;

/**
* Callback fired when the search query changes.
*/
onGlobalFilterChange: (value: string) => void;

/**
* When `true`, a "Columns" button is rendered that opens a column visibility dropdown.
*/
showColumnFilter?: boolean;

/**
* All columns from the table instance, used to build the visibility menu.
*/
columns: Array<Column<TableData>>;
/**
* The width of the toolbar, used to determine when to switch to a more compact layout.
*/
width: number | string;
}

export function TableToolbar<TableData>({
showSearch,
globalFilter,
onGlobalFilterChange,
showColumnFilter,
columns,
width,
}: TableToolbarProps<TableData>): ReactElement | null {
const [colMenuAnchor, setColMenuAnchor] = useState<null | HTMLElement>(null);
const colMenuOpen = Boolean(colMenuAnchor);

if (!showSearch && !showColumnFilter) {
return null;
}

return (
<Stack
direction="row"
gap={1}
alignItems="center"
justifyContent="flex-end"
width={width}
padding="0.5rem"
sx={{ backgroundColor: (theme) => theme.palette.background.default }}
>
{showSearch && (
<TextField
placeholder="Search…"
value={globalFilter}
onChange={onGlobalFilterChange}
variant="standard"
slotProps={{
htmlInput: { 'aria-label': 'search table' },
input: {
startAdornment: (
<InputAdornment position="start">
<Magnify fontSize="small" />
</InputAdornment>
),
endAdornment: globalFilter !== '' && (
<InputAdornment position="end">
<IconButton onClick={() => onGlobalFilterChange('')}>
<Close fontSize="small" />
</IconButton>
</InputAdornment>
),
},
}}
sx={{ flexGrow: 1 }}
/>
)}
{showColumnFilter && (
<>
<IconButton
onClick={(e) => setColMenuAnchor(e.currentTarget)}
aria-haspopup="listbox"
aria-expanded={colMenuOpen}
color="info"
>
<ViewColumn />
</IconButton>
<Menu
anchorEl={colMenuAnchor}
open={colMenuOpen}
onClose={() => setColMenuAnchor(null)}
slotProps={{ list: { dense: true } }}
>
{columns.map((column) => {
const header = column.columnDef.header;
const label = typeof header === 'string' ? header : column.id;
return (
<MenuItem
key={column.id}
disabled={!column.getCanHide()}
onClick={column.getCanHide() ? column.getToggleVisibilityHandler() : undefined}
dense
>
<Checkbox
checked={column.getIsVisible()}
disabled={!column.getCanHide()}
size="small"
disableRipple
sx={{ p: 0, mr: 1 }}
/>
<ListItemText primary={label} />
</MenuItem>
);
})}
</Menu>
</>
)}
</Stack>
);
}
Loading
Loading