ui-kit
ui-kit copied to clipboard
[RFC]: DataGrid Component
What's the problem?
We already have the useDataGrid query hook working on the ui-kit, however, we don’t have a component that consumes it, also in order to implement the new SQL Playground in Console, we’ll require to have a DataGrid component for the result visualization.
What are the requirements?
Following the same pattern we have for other components, the DataGrid
component should provide a higher-level API that enables customers to setup a basic DataGrid
easily but also enable exposing a lower-level API to access the inner table components directly, hopefully the library we use enables us to export its API directly similar to what we do on Chart.js with chartConfigProps
.
We already have the useDataGrid
hook to query the data so the most important concern is how we display it and enable customizing it.
For v1, we can probably focus on getting a basic version out while we provide data visualization features that we also require on Console
- Default state should render a table with basic styles, with a column to index rows (example from Snowflake)
- Columns should be resizable in order to enable viewing larger elements
- Cells should be selectable and open a drawer with the cell’s data, in case of containing a JSON, it should be formatted. This drawer should also be customizable and provide an option to disable it
- Rows should enable/disable showing a cell with the row’s index to the left
- Clicking on a row’s index should select the row and open a drawer with the row’s data. This drawer should also be customizable and provide an option to disable it
- DataGrid should include built-in pagination
- Clicking on a column header should change sorting, clicking once should set to
ASC
, clicking twice should set toDESC
and clicking a third time should reset, sorting should be made client-side for static mode and server-side for connected mode.
Requirements for table component:
The DataGrid component should return a table with the received data from DataGrid API,
- Should enable column resize
- Rows should listen to click events and send row data
- Cells should listen to click events and send cell data
- Should enable pagination
- Should be based on
<table />
elements for better a11y - Should be fully customizable
- Should enable sorting by column
- Should enable showing/hiding row numbers
- Should enable showing/hiding headers
- Should enable showing a drawer on column/cell click
- Should support virtualized scrolling
- Good performance with thousands of rows or more.
- Sticky columns
- Download query data as .csv
- Enable keyboard interactions (selecting whole table, copying a row/cell)
In pro of following consistency with the way we implement charts, helper components, etc we should probably opt for finding a library that would best fit our needs
Candidates:
Tanstack Table (React Table)
Tanstack Table provides a headless React hook that makes it easy to implement a Table component, this is similar to what we do today with base-ui’s Select component
✅ Should be based on <table />
elements for better a11y (you can base it either on div elements or table)
✅ Should be fully customizable (as it’s a headless hook we have full control on the table styles’ customization)
✅ Should enable sorting by column
✅ Should enable showing/hiding row numbers
✅ Should enable showing/hiding headers
✅ Should enable showing a drawer on column/cell click
✅ Should be a lightweight library (752kb npm, 15kb with tree-shaking)
✅ Should be a well maintained library (last commit yesterday)
Pros | Cons |
---|---|
React-Table is a popular library with 23.6k stars on https://github.com/tanstack/table | Pronounced learning curve as it is not a component itself but a headless hook |
Maintained by Tanstack which is the same team that maintains React-Query | |
As it is a headless hook, that enables us to implement future features even if those are not directly supported by React-Table | |
Extensive documentation with many examples |
react-data-grid
react-data-grid provides a react component for building data grids, react-data-grid
is currently in beta
❌ Should enable pagination (does not provide built-in support for pagination)
⚠️ Should be based on <table />
elements for better a11y (based on div elements)
⚠️ Should be fully customizable
- react-data-grid exports a high level component that receives a
renderers
prop, this one enables overriding theirCheckbox
component,Row
,SortStatus
,noRowsFallback
, while this provides a certain level of customization some things seems to be missing (e.g header)
✅ Should enable sorting by column
✅ Should enable showing/hiding row numbers
❌ Should enable showing/hiding headers
✅ Should enable showing a drawer on column/cell click
⚠️ Should be a lightweight library (752kb might be not so lightweight for a headless hook library)
✅ Should be a well maintained library (last commit 4 days ago)
Other options are either paid or not as good as Tanstack Table, we decided to proceed with Tanstack Table
Detailed design
The component should receive a set of headers
and rows
for static mode, as well as other properties we already set in static mode for other components.
<DataGrid headers={[]} rows={[]} ...othercommonprops />
Subcomponents should also include a className
and style
prop in order to enable styles customization.
Loader state
For loader we probably want to do something different to just a loading rectangle, we probably want to try something similar to console, cells in loading state and headers shown for static mode, for connected mode cells should be in loading state as well
Requirements implementation
Based on the decision of using Tanstack Table for the Table, we should rely on their examples on how to implement the features we require, here are some of the guides provided by Tanstack table:
- Basic
- Resizing
- Sorting
- Server-side pagination
- Infinite scroll
- Sticky column (for index column)
Some features however we will have to implement on our own: drawer, exporting to .csv
Interactivity and customization
It’s necessary to enable handling most of interactivity via props, for this we will require:
- Props to handle click events on Cells, Rows and Headers
- Prop to disable/enable the drawer
- Prop to customize drawer content
We can follow our current pattern of using fallbacks/overrides in order to provide the best level of customization, we can have RowOverride
, CellOverride
, DrawerOverride
, HeaderOverride
and PaginationOverride
Rows
For rows we would have the rowOverride
but also a top-level prop in order to hide the indexes
Prop | Type | Description |
---|---|---|
rowOverride | Element | ({props: RowProps, RowComponent: Row, CellComponent: Cell}) => Element | Overrides the row component |
indexColumn | 'left' | 'right' | 'hide' | Sets the position of the index column to either left or right or disables it. |
RowProps
Prop | Type | Description |
---|---|---|
onIndexClick | (rowData: string[] | Element[]) => void | Callback called when an index (row) is clicked |
cellValues | string[] | Element[] | Values that will fill each Cell |
className | string | Classname to be applied to the Row container |
styles | CSSStyles | CSS Styles to be applied to the Row container |
Cells
Prop | Type | Description |
---|---|---|
cellOverride | Element | (props: CellProps, CellComponent: Cell) => Element | Override for the row component |
CellProps
Prop | Type | Description |
---|---|---|
onCellClick | (cellValue: string | Element) => void |
Callback called when an cell is clicked |
cellValue | string | Element |
The value of the cell |
className | string | Classname to be applied to the Cell container |
styles | CSSStyles | CSS Styles to be applied to the Cell container |
onIndexClick
and onCellclick
should act as middlewares rather than overriding our setup for opening the drawer, however, if the drawer is disabled those should act as overrides
Drawer
For Drawer, besides of having a drawerOverride
component we should have a top-level disableDrawer
prop so customers can easily just disable it when they require it without having to add a fallback with an empty result.
Prop | Type | Description |
---|---|---|
disableDrawer | boolean | When true, will disable the drawer |
drawerOverride | Element | (props: DrawerProps, Component: Drawer) => Element | The override for the drawer component |
DrawerProps
For v1, it would be enough that the Drawer just shows the data in the cell/row but in order to enable customization we’ll need to enable an onClose
prop
Prop | Type | Description |
---|---|---|
onClose | () => void | Callback called when the close button in the drawer is clicked |
displayValue | string | Element |
Value that the Drawer would display |
className | string | Classname to be applied to the Drawer container |
style | CSSStyles | CSS Styles to be applied to the Drawer container |
Pagination
Regarding pagination, we can provide high-level customization as well as low-level customization, for this, we most likely will want to change the rows per page options for now and then customers can fully customize the component with an override prop
Prop | Type | Description |
---|---|---|
pageSizeOptions | number[] | The page sizes options for pagination |
paginationOverride | Element | (props: PaginationProps, Component: Pagination) => Element | The override for the pagination component |
paginationBehavior | 'infinite' | 'paginated' | Determines whether the pagination should be performed as a virtualized infinite or a paginated way |
The Pagination
component should then be uncontrolled, so customers can easily call the setter and default value from props in their custom pagination component as well as the onNext
and onBack
PaginationProps
Prop | Type | Description |
---|---|---|
setPageSize | (pageSize: number) => void | Setter to be called when the pageSize changes |
defaultPageSize | number | The default pageSize for the paginator, should be one of pageSizeOptions otherwise should throw a warning |
onNext | () => void | Callback to be called when the “next” button is clicked |
onBack | () => void | Callback to be called when the “back” button is clicked |
currentPage | number | The current page index |
setCurrentPage | (page: number) => void | Setter to change the current page |
hasNext | boolean | Whether a next page is available or not |
hasPrevious | boolean | Whether a previous page is available or not |
className | string | Classname to be applied to the Pagination container |
style | CSSStyles | CSS Styles to be applied to the Pagination container |
Then we would have Header
which we could split in HeaderRow
and HeaderCell
those would be very similar to Row
and Cell
but should be distinct as it’s a common use case to customize the header different to the other rows/cells, additionally we should have a top-level prop to hide the header
Prop | Type | Description |
---|---|---|
hideHeader | boolean | If true, hides the header |
Drawbacks
- In order to provide a good level of customization it could require a lot of work, as DataGrid has a lot of components where we should enable customization.
Alternatives
- Customers already have a
useDataGrid
hook that they could use in their own tables
Adoption strategy
-
DataGrid
would be a new component, however, customers could be already using theuseDataGrid
hook, we could encourage them to start using theDataGrid
component, however this is not a breaking change nor it has to be mandatory to migrate fromuseDataGrid
toDataGrid
component.
Unresolved questions
- ~~What component library to use, probably the best option is Tanstack Table, there are not so many good libraries for our use case~~
- Is this too much for a v1? It is important to provide our customers with enough customization for their use cases which adds a fair amount of work and based on the library we select we may require more or less work on implementing the required features, should we shrink requirements for v1?
- Pagination in static mode. In the current design we only consider connected mode for this, although customers could implement pagination on their own using the override, should we also care about supporting pagination in static mode for v1?
- Should we enable downloading query data as .csv?