2022-01-13 17:06:32 +00:00
/* eslint-disable react/prop-types */
// disable this rule as it was throwing an error in Header and Cell component
// definitions for the selection row for some reason when we dont really need it.
2024-10-25 10:27:44 +00:00
import React , {
useMemo ,
useEffect ,
useCallback ,
useContext ,
useRef ,
} from "react" ;
2021-09-10 19:06:37 +00:00
import classnames from "classnames" ;
2021-10-18 19:14:24 +00:00
import {
2022-04-07 15:37:43 +00:00
Column ,
HeaderGroup ,
2021-10-18 19:14:24 +00:00
Row ,
2021-11-09 17:31:28 +00:00
useFilters ,
2022-04-07 15:37:43 +00:00
useGlobalFilter ,
usePagination ,
useRowSelect ,
useSortBy ,
useTable ,
2021-10-18 19:14:24 +00:00
} from "react-table" ;
2023-11-21 21:49:41 +00:00
import { kebabCase , noop } from "lodash" ;
2022-04-07 16:08:00 +00:00
import { useDebouncedCallback } from "use-debounce" ;
2021-07-26 17:07:27 +00:00
2022-04-22 16:45:35 +00:00
import useDeepEffect from "hooks/useDeepEffect" ;
2021-09-29 04:04:58 +00:00
import sort from "utilities/sort" ;
2021-11-15 20:11:22 +00:00
import { AppContext } from "context/app" ;
2021-04-04 12:45:24 +00:00
2021-10-18 19:14:24 +00:00
import Button from "components/buttons/Button" ;
2021-11-07 06:41:09 +00:00
import Spinner from "components/Spinner" ;
2025-03-20 16:40:43 +00:00
import Pagination from "components/Pagination" ;
2023-04-25 12:44:08 +00:00
import ActionButton from "./ActionButton" ;
import { IActionButtonProps } from "./ActionButton/ActionButton" ;
2021-04-04 12:45:24 +00:00
2022-04-07 19:12:38 +00:00
const baseClass = "data-table-block" ;
2021-04-04 12:45:24 +00:00
2021-07-10 17:29:27 +00:00
interface IDataTableProps {
2022-01-13 17:06:32 +00:00
columns : Column [ ] ;
2021-07-10 17:29:27 +00:00
data : any ;
2022-01-31 22:41:54 +00:00
filters? : Record < string , string | number | boolean > ;
2021-07-10 17:29:27 +00:00
isLoading : boolean ;
2021-08-03 19:42:48 +00:00
manualSortBy? : boolean ;
2021-07-10 17:29:27 +00:00
sortHeader : any ;
sortDirection : any ;
onSort : any ; // TODO: an event type
2021-09-10 19:06:37 +00:00
disableMultiRowSelect : boolean ;
2024-12-26 22:51:28 +00:00
keyboardSelectableRows? : boolean ;
2021-07-10 17:29:27 +00:00
showMarkAllPages : boolean ;
isAllPagesSelected : boolean ; // TODO: make dependent on showMarkAllPages
toggleAllPagesSelected? : any ; // TODO: an event type and make it dependent on showMarkAllPages
2024-06-14 17:12:56 +00:00
resultsTitle? : string ;
2021-07-10 17:29:27 +00:00
defaultPageSize : number ;
2023-04-24 13:24:28 +00:00
defaultPageIndex? : number ;
2024-10-25 10:27:44 +00:00
defaultSelectedRows? : Record < string , boolean > ;
2025-08-08 16:13:48 +00:00
/ * * D e f a u l t : t r u e ( s a m e a s u s e T a b l e d e f a u l t )
* False prevents unnecessary page resets when a column ordering changes
* e . g . when clicking on an action that modifies the data
* /
autoResetPage? : boolean ;
2023-05-11 19:25:56 +00:00
primarySelectAction? : IActionButtonProps ;
2021-07-26 17:07:27 +00:00
secondarySelectActions? : IActionButtonProps [ ] ;
2021-11-05 04:16:42 +00:00
isClientSidePagination? : boolean ;
2023-04-27 13:24:02 +00:00
onClientSidePaginationChange ? : ( pageIndex : number ) = > void ; // Used to set URL to correct path and include page query param
2021-11-09 17:31:28 +00:00
isClientSideFilter? : boolean ;
2022-11-02 18:18:08 +00:00
disableHighlightOnHover? : boolean ;
2021-11-09 17:31:28 +00:00
searchQuery? : string ;
searchQueryColumn? : string ;
selectedDropdownFilter? : string ;
2026-02-27 15:29:34 +00:00
/** Set to true to persist row selection across client-side filters and pagination */
2024-10-25 10:27:44 +00:00
persistSelectedRows? : boolean ;
2025-04-29 14:29:21 +00:00
/** Set to `true` to not display the footer section of the table */
hideFooter? : boolean ;
2021-12-22 17:07:12 +00:00
onSelectSingleRow ? : ( value : Row ) = > void ;
2024-03-04 13:14:50 +00:00
onClickRow ? : ( value : any ) = > void ;
2021-12-22 17:07:12 +00:00
onResultsCountChange ? : ( value : number ) = > void ;
2024-08-21 20:12:42 +00:00
/ * * O p t i o n a l h e l p t e x t t o r e n d e r o n b o t t o m - l e f t o f t h e t a b l e . H i d d e n w h e n t a b l e i s l o a d i n g a n d n o
* rows of data are present . * /
renderTableHelpText ? : ( ) = > JSX . Element | null ;
2022-01-31 22:24:20 +00:00
renderPagination ? : ( ) = > JSX . Element | null ;
2022-06-13 23:20:57 +00:00
setExportRows ? : ( rows : Row [ ] ) = > void ;
2024-10-09 15:09:38 +00:00
onClearSelection ? : ( ) = > void ;
2025-09-23 16:55:18 +00:00
suppressHeaderActions? : boolean ;
2021-07-10 17:29:27 +00:00
}
2022-08-18 13:51:31 +00:00
interface IHeaderGroup extends HeaderGroup {
title? : string ;
}
2024-05-02 19:30:44 +00:00
// This data table uses react-table for implementation. The relevant v7 documentation of the library
// can be found here https://react-table-v7-docs.netlify.app/docs/api/usetable
2021-07-10 17:29:27 +00:00
const DataTable = ( {
columns : tableColumns ,
data : tableData ,
2022-01-31 22:41:54 +00:00
filters : tableFilters ,
2021-07-10 17:29:27 +00:00
isLoading ,
2021-08-03 19:42:48 +00:00
manualSortBy = false ,
2021-07-10 17:29:27 +00:00
sortHeader ,
sortDirection ,
onSort ,
2021-09-10 19:06:37 +00:00
disableMultiRowSelect ,
2024-12-26 22:51:28 +00:00
keyboardSelectableRows ,
2021-07-10 17:29:27 +00:00
showMarkAllPages ,
isAllPagesSelected ,
toggleAllPagesSelected ,
2024-06-14 17:12:56 +00:00
resultsTitle = "results" ,
2021-07-10 17:29:27 +00:00
defaultPageSize ,
2023-04-24 13:24:28 +00:00
defaultPageIndex ,
2024-10-25 10:27:44 +00:00
defaultSelectedRows = { } ,
2025-08-08 16:13:48 +00:00
autoResetPage = true ,
2023-05-11 19:25:56 +00:00
primarySelectAction ,
2021-07-26 17:07:27 +00:00
secondarySelectActions ,
2021-11-05 04:16:42 +00:00
isClientSidePagination ,
2023-04-27 13:24:02 +00:00
onClientSidePaginationChange ,
2021-11-09 17:31:28 +00:00
isClientSideFilter ,
2022-11-02 18:18:08 +00:00
disableHighlightOnHover ,
2021-11-09 17:31:28 +00:00
searchQuery ,
searchQueryColumn ,
selectedDropdownFilter ,
2024-10-25 10:27:44 +00:00
persistSelectedRows = false ,
2025-04-29 14:29:21 +00:00
hideFooter = false ,
2021-12-22 17:07:12 +00:00
onSelectSingleRow ,
2024-03-04 13:14:50 +00:00
onClickRow ,
2021-11-09 17:31:28 +00:00
onResultsCountChange ,
2024-08-21 20:12:42 +00:00
renderTableHelpText ,
2022-01-31 22:24:20 +00:00
renderPagination ,
2022-06-13 23:20:57 +00:00
setExportRows ,
2024-10-09 15:09:38 +00:00
onClearSelection = noop ,
2025-09-23 16:55:18 +00:00
suppressHeaderActions ,
2021-07-26 17:07:27 +00:00
} : IDataTableProps ) : JSX . Element = > {
2024-10-25 10:27:44 +00:00
// used to track the initial mount of the component.
const isInitialRender = useRef ( true ) ;
2021-11-15 20:11:22 +00:00
const { isOnlyObserver } = useContext ( AppContext ) ;
2021-08-18 23:58:56 +00:00
2021-04-04 12:45:24 +00:00
const columns = useMemo ( ( ) = > {
return tableColumns ;
} , [ tableColumns ] ) ;
// The table data needs to be ordered by the order we received from the API.
const data = useMemo ( ( ) = > {
return tableData ;
} , [ tableData ] ) ;
Live query performance improvements (#11995)
## Addresses #11856
Improve performance of the rendering of live query results by:
- rendering the table on a set interval instead of with each new result
- preventing redundant rerenders of various sorts
Partial run, with memory leak:
<img width="2552" alt="partial run with memory leak, after smaller
optimizations, before debouncing queryResults"
src="https://github.com/fleetdm/fleet/assets/61553566/5288bffb-6940-43da-9083-59adb4a25916">
Full run after debounce, no memory leak (10x improvement of max JS heap
size):
<img width="2559" alt="full run after debounce, no memory leak"
src="https://github.com/fleetdm/fleet/assets/61553566/be056610-e7a5-4289-a433-1070cf016e83">
**NOTE** - there are further optimizations to try on this page, and the
debounce interval can potentially be shortened to improve UX. In
experimenting with that, it's not immediately clear what a good balance
of UX / performance is. Since the customer seems keen to solve this, I
think we should merge as-is and send them a demo build to confirm this
fixes their problem, then iterate once they've confirmed it does.
# Checklist for submitter
If some of the following don't apply, delete the relevant line.
- [x] Changes file added for user-visible changes in `changes/`
- [x] Manual QA for all new/changed functionality
---------
Co-authored-by: Lucas Rodriguez <lucas@fleetdm.com>
Co-authored-by: Jacob Shandling <jacob@fleetdm.com>
Co-authored-by: Sarah Gillespie <sarah@fleetdm.com>
2023-06-06 20:23:09 +00:00
const initialSortBy = useMemo ( ( ) = > {
return [ { id : sortHeader , desc : sortDirection === "desc" } ] ;
} , [ sortHeader , sortDirection ] ) ;
2025-08-12 17:13:37 +00:00
// Decide the page index value to pass to useTable
const controlledPageIndex =
isClientSidePagination && ! ! onClientSidePaginationChange
? defaultPageIndex ? ? 0
: undefined ; // undefined lets react-table manage internally (Keeps internal mode working)
2021-05-13 14:30:42 +00:00
const {
headerGroups ,
rows ,
prepareRow ,
selectedFlatRows ,
toggleAllRowsSelected ,
2021-07-10 17:29:27 +00:00
isAllRowsSelected ,
2021-05-13 14:30:42 +00:00
state : tableState ,
2021-10-18 19:14:24 +00:00
page , // Instead of using 'rows', we'll use page,
// which has only the rows for the active page
// The rest of these things are super handy, too ;)
canPreviousPage ,
canNextPage ,
2021-11-09 17:31:28 +00:00
// pageOptions,
// pageCount,
2023-05-08 13:17:19 +00:00
gotoPage ,
2021-10-18 19:14:24 +00:00
nextPage ,
previousPage ,
setPageSize ,
2022-04-07 15:37:43 +00:00
setFilter , // sets a specific column-level filter
setAllFilters , // sets all of the column-level filters; rows are included in filtered results only if each column filter return true
setGlobalFilter , // sets the global filter; this serves as a global free text search across all columns (excluding only those where `disableGlobalFilter: true`)
2021-05-13 14:30:42 +00:00
} = useTable (
2021-04-04 12:45:24 +00:00
{
columns ,
data ,
2026-02-27 15:29:34 +00:00
// Use a stable row ID when available (row.id), otherwise fall back to the index-based ID (default of react-table)
getRowId : ( row : any , index : number ) = >
row && row . id != null ? String ( row . id ) : String ( index ) ,
2021-04-04 12:45:24 +00:00
initialState : {
Live query performance improvements (#11995)
## Addresses #11856
Improve performance of the rendering of live query results by:
- rendering the table on a set interval instead of with each new result
- preventing redundant rerenders of various sorts
Partial run, with memory leak:
<img width="2552" alt="partial run with memory leak, after smaller
optimizations, before debouncing queryResults"
src="https://github.com/fleetdm/fleet/assets/61553566/5288bffb-6940-43da-9083-59adb4a25916">
Full run after debounce, no memory leak (10x improvement of max JS heap
size):
<img width="2559" alt="full run after debounce, no memory leak"
src="https://github.com/fleetdm/fleet/assets/61553566/be056610-e7a5-4289-a433-1070cf016e83">
**NOTE** - there are further optimizations to try on this page, and the
debounce interval can potentially be shortened to improve UX. In
experimenting with that, it's not immediately clear what a good balance
of UX / performance is. Since the customer seems keen to solve this, I
think we should merge as-is and send them a demo build to confirm this
fixes their problem, then iterate once they've confirmed it does.
# Checklist for submitter
If some of the following don't apply, delete the relevant line.
- [x] Changes file added for user-visible changes in `changes/`
- [x] Manual QA for all new/changed functionality
---------
Co-authored-by: Lucas Rodriguez <lucas@fleetdm.com>
Co-authored-by: Jacob Shandling <jacob@fleetdm.com>
Co-authored-by: Sarah Gillespie <sarah@fleetdm.com>
2023-06-06 20:23:09 +00:00
sortBy : initialSortBy ,
2023-04-24 13:24:28 +00:00
pageIndex : defaultPageIndex ,
2024-10-25 10:27:44 +00:00
selectedRowIds : defaultSelectedRows ,
2021-04-04 12:45:24 +00:00
} ,
2025-08-12 17:13:37 +00:00
// For onClientSidePaginationChange (URL-controlled mode) we inject pageIndex, otherwise leave undefined so it's internal
// NOTE: This specifically prevents quick flicker of incorrect page data for clientside pagination with
// external source of truth (URL bar) such as the self-service page when searching or changing categories
// TODO: Figure out flickering on self-service page internal sort buttons
state :
controlledPageIndex !== undefined
? { pageIndex : controlledPageIndex }
: undefined ,
2021-04-04 12:45:24 +00:00
disableMultiSort : true ,
2021-08-16 21:02:00 +00:00
disableSortRemove : true ,
2021-08-03 19:42:48 +00:00
manualSortBy ,
2025-08-08 16:13:48 +00:00
autoResetPage ,
2026-02-27 15:29:34 +00:00
// Resets row selection on pagination
autoResetSelectedRows : ! persistSelectedRows ,
2022-01-31 22:41:54 +00:00
// Expands the enumerated `filterTypes` for react-table
2022-04-04 16:33:02 +00:00
// (see https://github.com/TanStack/react-table/blob/alpha/packages/react-table/src/filterTypes.ts)
2022-01-31 22:41:54 +00:00
// with custom `filterTypes` defined for this `useTable` instance
2024-05-23 20:30:24 +00:00
filterTypes : useMemo (
2022-01-31 22:41:54 +00:00
( ) = > ( {
hasLength : (
// eslint-disable-next-line @typescript-eslint/no-shadow
rows : Row [ ] ,
columnIds : string [ ] ,
filterValue : boolean
) = > {
return ! filterValue
? rows
: rows ? . filter ( ( row ) = > {
return columnIds ? . some ( ( id ) = > row ? . values ? . [ id ] ? . length ) ;
} ) ;
} ,
} ) ,
[ ]
) ,
2022-06-13 23:20:57 +00:00
autoResetFilters : false ,
2022-01-31 22:41:54 +00:00
// Expands the enumerated `sortTypes` for react-table
// (see https://github.com/tannerlinsley/react-table/blob/master/src/sortTypes.js)
// with custom `sortTypes` defined for this `useTable` instance
2024-05-23 20:30:24 +00:00
sortTypes : useMemo (
2021-08-18 14:00:02 +00:00
( ) = > ( {
2022-06-08 19:01:38 +00:00
boolean : (
a : { values : Record < string , unknown > } ,
b : { values : Record < string , unknown > } ,
id : string
) = > sort . booleanAsc ( a . values [ id ] , b . values [ id ] ) ,
2022-04-13 16:08:37 +00:00
caseInsensitive : (
a : { values : Record < string , unknown > } ,
b : { values : Record < string , unknown > } ,
id : string
) = > sort . caseInsensitiveAsc ( a . values [ id ] , b . values [ id ] ) ,
2021-08-18 14:00:02 +00:00
2022-04-13 16:08:37 +00:00
dateStrings : (
a : { values : Record < string , string > } ,
b : { values : Record < string , string > } ,
id : string
) = > sort . dateStringsAsc ( a . values [ id ] , b . values [ id ] ) ,
2021-08-18 14:00:02 +00:00
2022-04-13 16:08:37 +00:00
hasLength : (
a : { values : Record < string , unknown [ ] > } ,
b : { values : Record < string , unknown [ ] > } ,
id : string
) = > {
return sort . hasLength ( a . values [ id ] , b . values [ id ] ) ;
2021-08-18 14:00:02 +00:00
} ,
2025-05-01 18:43:38 +00:00
hostPolicyStatus : (
a : { values : Record < string , unknown [ ] > } ,
b : { values : Record < string , unknown [ ] > } ,
id : string
) = > sort . hostPolicyStatus ( a . values [ id ] , b . values [ id ] ) ,
2021-08-18 14:00:02 +00:00
} ) ,
[ ]
) ,
2021-04-04 12:45:24 +00:00
} ,
2022-04-07 15:37:43 +00:00
useGlobalFilter , // order of these hooks matters; here we first apply the global filter (if any); this could be reversed depending on where we want to target performance
useFilters , // react-table applies column-level filters after first applying the global filter (if any)
2021-05-13 14:30:42 +00:00
useSortBy ,
2021-10-18 19:14:24 +00:00
usePagination ,
2021-05-13 14:30:42 +00:00
useRowSelect
2021-04-04 12:45:24 +00:00
) ;
2023-04-27 13:24:02 +00:00
const { sortBy , selectedRowIds , pageIndex } = tableState ;
2021-11-09 17:31:28 +00:00
2022-01-31 22:41:54 +00:00
useEffect ( ( ) = > {
if ( tableFilters ) {
2022-04-07 15:37:43 +00:00
const filtersToSet = tableFilters ;
const global = filtersToSet . global ;
setGlobalFilter ( global ) ;
delete filtersToSet . global ;
const allFilters = Object . entries ( filtersToSet ) . map ( ( [ id , value ] ) = > ( {
2022-01-31 22:41:54 +00:00
id ,
value ,
} ) ) ;
! ! allFilters . length && setAllFilters ( allFilters ) ;
2022-06-13 23:20:57 +00:00
setExportRows && setExportRows ( rows ) ;
2022-01-31 22:41:54 +00:00
}
} , [ tableFilters ] ) ;
2022-06-13 23:20:57 +00:00
useEffect ( ( ) = > {
setExportRows && setExportRows ( rows ) ;
2022-07-14 16:21:26 +00:00
} , [ tableState . filters , rows . length ] ) ;
2022-06-13 23:20:57 +00:00
2021-11-09 17:31:28 +00:00
// Listen for changes to filters if clientSideFilter is enabled
const setDebouncedClientFilter = useDebouncedCallback (
( column : string , query : string ) = > {
setFilter ( column , query ) ;
} ,
300
) ;
useEffect ( ( ) = > {
if ( isClientSideFilter && onResultsCountChange ) {
onResultsCountChange ( rows . length ) ;
}
} , [ isClientSideFilter , onResultsCountChange , rows . length ] ) ;
useEffect ( ( ) = > {
2024-10-25 10:27:44 +00:00
if ( ! isInitialRender . current && isClientSideFilter && searchQueryColumn ) {
2021-11-09 17:31:28 +00:00
setDebouncedClientFilter ( searchQueryColumn , searchQuery || "" ) ;
}
2024-10-25 10:27:44 +00:00
// we only want to reset the selected rows if we are not persisting them
// across table data filters
if ( ! isInitialRender . current && ! persistSelectedRows ) {
toggleAllRowsSelected ( false ) ; // Resets row selection on query change (client-side)
}
isInitialRender . current = false ;
2021-11-09 17:31:28 +00:00
} , [ searchQuery , searchQueryColumn ] ) ;
useEffect ( ( ) = > {
if ( isClientSideFilter && selectedDropdownFilter ) {
2023-11-03 11:40:19 +00:00
toggleAllRowsSelected ( false ) ; // Resets row selection on filter change (client-side)
2021-11-09 17:31:28 +00:00
selectedDropdownFilter === "all"
? setDebouncedClientFilter ( "platforms" , "" )
: setDebouncedClientFilter ( "platforms" , selectedDropdownFilter ) ;
}
} , [ selectedDropdownFilter ] ) ;
2021-04-04 12:45:24 +00:00
2025-08-08 16:13:48 +00:00
// track previous sort state
const prevSort = useRef < { id? : string ; desc? : boolean } > ( {
id : undefined ,
desc : undefined , // desc as in descending
} ) ;
2021-05-13 14:30:42 +00:00
// This is used to listen for changes to sort. If there is a change
// Then the sortHandler change is fired.
2021-04-04 12:45:24 +00:00
useEffect ( ( ) = > {
const column = sortBy [ 0 ] ;
2025-08-08 16:13:48 +00:00
const prev = prevSort . current ;
const newId = column ? . id ;
const newDesc = column ? . desc ;
2021-04-04 12:45:24 +00:00
if ( column !== undefined ) {
2021-04-14 16:52:15 +00:00
if (
column . id !== sortHeader ||
column . desc !== ( sortDirection === "desc" )
) {
2021-04-04 12:45:24 +00:00
onSort ( column . id , column . desc ) ;
}
} else {
onSort ( undefined ) ;
}
2025-08-08 16:13:48 +00:00
// Only reset to page 0 if sort column/direction actually changes
// Prevents unnecessary page resets when a column ordering changes
// e.g. when clicking on an action that modifies the data
const hasSortChanged =
( ! prev && ( newId || newDesc !== undefined ) ) ||
( prev && ( prev . id !== newId || prev . desc !== newDesc ) ) ;
if ( isClientSidePagination && hasSortChanged ) {
gotoPage ( 0 ) ; // Just this, no defaultPageIndex/etc!
}
prevSort . current = column
? { id : newId , desc : newDesc }
: { id : undefined , desc : undefined } ;
} , [ sortBy , sortHeader , onSort , sortDirection , isClientSidePagination ] ) ;
2025-08-12 17:13:37 +00:00
/ * * F o r o n C l i e n t S i d e P a g i n a t i o n C h a n g e o n l y :
* Prevents bug where URL page + table page mismatch
2025-08-08 16:13:48 +00:00
* Whenever defaultPageIndex ( the value from props , e . g . queryParams . page ) changes ,
* ensure we call gotoPage so react - table reflects the correct visible page .
* /
useEffect ( ( ) = > {
if (
isClientSidePagination &&
2025-08-12 17:13:37 +00:00
! ! onClientSidePaginationChange &&
2025-08-08 16:13:48 +00:00
typeof defaultPageIndex === "number" &&
pageIndex !== defaultPageIndex
) {
gotoPage ( defaultPageIndex ) ;
2023-05-08 13:17:19 +00:00
}
2025-08-12 17:13:37 +00:00
} , [
isClientSidePagination ,
onClientSidePaginationChange ,
defaultPageIndex ,
gotoPage ,
pageIndex ,
] ) ;
2021-05-13 14:30:42 +00:00
2021-07-10 17:29:27 +00:00
useEffect ( ( ) = > {
if ( isAllPagesSelected ) {
toggleAllRowsSelected ( true ) ;
}
2021-11-09 17:31:28 +00:00
} , [ isAllPagesSelected , toggleAllRowsSelected ] ) ;
useEffect ( ( ) = > {
2025-03-20 16:40:43 +00:00
setPageSize ( defaultPageSize ) ;
2021-11-09 17:31:28 +00:00
} , [ setPageSize ] ) ;
2021-07-10 17:29:27 +00:00
useDeepEffect ( ( ) = > {
if (
Object . keys ( selectedRowIds ) . length < rows . length &&
toggleAllPagesSelected
) {
toggleAllPagesSelected ( false ) ;
}
} , [ tableState . selectedRowIds , toggleAllPagesSelected ] ) ;
const onToggleAllPagesClick = useCallback ( ( ) = > {
toggleAllPagesSelected ( ) ;
} , [ toggleAllPagesSelected ] ) ;
2021-05-13 14:30:42 +00:00
const onClearSelectionClick = useCallback ( ( ) = > {
2024-10-09 15:09:38 +00:00
onClearSelection ( ) ;
toggleAllRowsSelected ? . ( false ) ;
toggleAllPagesSelected ? . ( false ) ;
} , [ onClearSelection , toggleAllPagesSelected , toggleAllRowsSelected ] ) ;
2021-04-04 12:45:24 +00:00
2024-03-04 13:14:50 +00:00
const onSelectRowClick = useCallback (
2024-03-13 19:09:16 +00:00
( row : any ) = > {
2021-09-10 19:06:37 +00:00
if ( disableMultiRowSelect ) {
row . toggleRowSelected ( ) ;
onSelectSingleRow && onSelectSingleRow ( row ) ;
toggleAllRowsSelected ( false ) ;
}
} ,
2021-11-09 17:31:28 +00:00
[ disableMultiRowSelect , onSelectSingleRow , toggleAllRowsSelected ]
2021-09-10 19:06:37 +00:00
) ;
2022-08-18 13:51:31 +00:00
const renderColumnHeader = ( column : IHeaderGroup ) = > {
2022-01-13 17:06:32 +00:00
return (
< div className = "column-header" >
2023-11-21 21:49:41 +00:00
{ column . render ( "Header" ) }
2022-01-13 17:06:32 +00:00
{ column . Filter && column . render ( "Filter" ) }
< / div >
) ;
} ;
2021-08-03 20:09:01 +00:00
const renderSelectedCount = ( ) : JSX . Element = > {
2024-10-25 10:27:44 +00:00
const selectedCount = Object . entries ( selectedRowIds ) . filter (
( [ , value ] ) = > value
) . length ;
2021-08-03 20:09:01 +00:00
return (
< p >
2021-10-04 20:20:56 +00:00
< span >
2024-10-25 10:27:44 +00:00
{ selectedCount }
2021-10-04 20:20:56 +00:00
{ isAllPagesSelected && "+" }
< / span > { " " }
selected
2021-08-03 20:09:01 +00:00
< / p >
) ;
} ;
const renderAreAllSelected = ( ) : JSX . Element | null = > {
2021-07-10 17:29:27 +00:00
if ( isAllPagesSelected ) {
return < p > All matching { resultsTitle } are selected < / p > ;
}
if ( isAllRowsSelected ) {
return < p > All { resultsTitle } on this page are selected < / p > ;
}
2021-08-03 20:09:01 +00:00
return null ;
2021-07-10 17:29:27 +00:00
} ;
2021-07-26 17:07:27 +00:00
const renderActionButton = (
actionButtonProps : IActionButtonProps
) : JSX . Element = > {
2025-04-28 23:32:41 +00:00
const key = kebabCase ( actionButtonProps . name ) ;
2021-07-26 17:07:27 +00:00
return (
2025-04-28 23:32:41 +00:00
< div className = { ` ${ baseClass } __ ${ key } ` } >
< ActionButton { ... { key , ...actionButtonProps }} / >
2021-07-26 17:07:27 +00:00
< / div >
) ;
} ;
const renderPrimarySelectAction = ( ) : JSX . Element | null = > {
const targetIds = selectedFlatRows . map ( ( row : any ) = > row . original . id ) ;
const buttonText =
2023-05-11 19:25:56 +00:00
typeof primarySelectAction ? . buttonText === "function"
? primarySelectAction ? . buttonText ( targetIds )
: primarySelectAction ? . buttonText ;
2021-07-26 17:07:27 +00:00
const name = buttonText ? kebabCase ( buttonText ) : "primary-select-action" ;
2021-07-29 19:47:04 +00:00
2021-07-26 17:07:27 +00:00
const actionProps = {
name ,
buttonText : buttonText || "" ,
2025-04-28 23:32:41 +00:00
onClick : primarySelectAction?.onClick || noop ,
2021-07-26 17:07:27 +00:00
targetIds ,
2023-05-11 19:25:56 +00:00
variant : primarySelectAction?.variant ,
2023-05-30 21:16:47 +00:00
iconSvg : primarySelectAction?.iconSvg ,
2021-07-26 17:07:27 +00:00
} ;
return ! buttonText ? null : renderActionButton ( actionProps ) ;
} ;
const renderSecondarySelectActions = ( ) : JSX . Element [ ] | null = > {
if ( secondarySelectActions ) {
const targetIds = selectedFlatRows . map ( ( row : any ) = > row . original . id ) ;
const buttons = secondarySelectActions . map ( ( actionProps ) = > {
actionProps = { . . . actionProps , targetIds } ;
return renderActionButton ( actionProps ) ;
} ) ;
return buttons ;
}
return null ;
2021-07-15 17:01:52 +00:00
} ;
2021-07-10 17:29:27 +00:00
const shouldRenderToggleAllPages =
Object . keys ( selectedRowIds ) . length >= defaultPageSize &&
showMarkAllPages &&
! isAllPagesSelected ;
2021-07-26 17:07:27 +00:00
2021-11-05 04:16:42 +00:00
const pageOrRows = isClientSidePagination ? page : rows ;
2021-10-18 19:14:24 +00:00
2021-11-15 20:11:22 +00:00
const tableStyles = classnames ( {
"data-table__table" : true ,
2024-08-21 20:12:42 +00:00
"data-table__no-rows" : ! rows . length ,
2021-11-15 20:11:22 +00:00
"is-observer" : isOnlyObserver ,
} ) ;
2025-09-23 16:55:18 +00:00
const renderHeaderWithActions = ( ) = > (
< thead className = "active-selection" >
< tr { ...headerGroups [ 0 ] .getHeaderGroupProps ( ) } >
< th
className = "active-selection__checkbox"
{ . . . headerGroups [ 0 ] . headers [ 0 ] . getHeaderProps (
headerGroups [ 0 ] . headers [ 0 ] . getSortByToggleProps ( {
title : null ,
} )
) }
>
{ headerGroups [ 0 ] . headers [ 0 ] . render ( "Header" ) }
< / th >
< th className = "active-selection__container" >
< div className = "active-selection__inner" >
{ renderSelectedCount ( ) }
< div className = "active-selection__inner-left" >
{ secondarySelectActions && renderSecondarySelectActions ( ) }
< / div >
< div className = "active-selection__inner-right" >
{ primarySelectAction && renderPrimarySelectAction ( ) }
< / div >
{ toggleAllPagesSelected && renderAreAllSelected ( ) }
{ shouldRenderToggleAllPages && (
< Button
onClick = { onToggleAllPagesClick }
2025-09-29 17:10:41 +00:00
variant = "inverse"
2025-09-23 16:55:18 +00:00
className = "light-text"
2025-09-29 17:10:41 +00:00
size = "small"
2025-09-23 16:55:18 +00:00
>
< > Select all matching { resultsTitle } < / >
< / Button >
) }
2025-09-29 17:10:41 +00:00
< Button
onClick = { onClearSelectionClick }
variant = "inverse"
size = "small"
>
2025-09-23 16:55:18 +00:00
Clear selection
< / Button >
< / div >
< / th >
< / tr >
< / thead >
) ;
2025-10-15 22:03:54 +00:00
const shouldShowFooter =
// footer is not explicitly hidden
! hideFooter &&
// and any of:
// table is client-side paginated with more than 1 page of rows
( ( isClientSidePagination && ( canNextPage || canPreviousPage ) ) ||
// table's pagination is externally controlled
2026-03-09 20:23:44 +00:00
renderPagination ? . ( ) != null ||
2025-10-15 22:03:54 +00:00
// there is help text and at least 1 row of data
2026-03-09 20:23:44 +00:00
( renderTableHelpText ? . ( ) != null && ! ! rows ? . length ) ) ;
2025-10-15 22:03:54 +00:00
2021-04-04 12:45:24 +00:00
return (
< div className = { baseClass } >
2021-10-18 17:42:54 +00:00
{ isLoading && (
2024-02-23 14:57:18 +00:00
< div className = "loading-overlay" >
2021-10-18 17:42:54 +00:00
< Spinner / >
< / div >
) }
2024-02-23 14:57:18 +00:00
< div className = "data-table data-table__wrapper" >
2021-11-15 20:11:22 +00:00
< table className = { tableStyles } >
2025-09-23 16:55:18 +00:00
{ ! suppressHeaderActions &&
Object . keys ( selectedRowIds ) . length !== 0 &&
renderHeaderWithActions ( ) }
2021-04-04 12:45:24 +00:00
< thead >
2021-04-14 16:52:15 +00:00
{ headerGroups . map ( ( headerGroup ) = > (
2021-04-04 12:45:24 +00:00
< tr { ...headerGroup.getHeaderGroupProps ( ) } >
2022-06-14 22:57:43 +00:00
{ headerGroup . headers . map ( ( column ) = > {
return (
< th
className = { column . id ? ` ${ column . id } __header ` : "" }
2025-09-29 17:10:41 +00:00
{ . . . column . getHeaderProps ( ) }
2022-06-14 22:57:43 +00:00
>
2025-09-29 17:10:41 +00:00
{ column . canSort ? (
< Button
variant = "unstyled"
{ . . . column . getSortByToggleProps ( { title : null } ) }
aria - label = { ` Sort by ${ column . Header } ${
column . isSortedDesc ? "descending" : "ascending"
} ` }
tabIndex = { 0 }
className = "sortable-header"
>
{ renderColumnHeader ( column ) }
{ /* add arrow/icon as needed */ }
< / Button >
) : (
renderColumnHeader ( column )
) }
2022-06-14 22:57:43 +00:00
< / th >
) ;
} ) }
2021-04-04 12:45:24 +00:00
< / tr >
) ) }
< / thead >
< tbody >
2023-02-17 18:25:28 +00:00
{ pageOrRows . map ( ( row : Row ) = > {
2021-04-04 12:45:24 +00:00
prepareRow ( row ) ;
2021-09-10 19:06:37 +00:00
const rowStyles = classnames ( {
"single-row" : disableMultiRowSelect ,
2022-11-02 18:18:08 +00:00
"disable-highlight" : disableHighlightOnHover ,
2025-02-04 19:05:22 +00:00
"clickable-row" : ! ! onClickRow ,
2021-09-10 19:06:37 +00:00
} ) ;
2021-04-04 12:45:24 +00:00
return (
2021-09-10 19:06:37 +00:00
< tr
className = { rowStyles }
{ . . . row . getRowProps ( {
// @ts-ignore // TS complains about prop not existing
onClick : ( ) = > {
2024-03-04 13:14:50 +00:00
( onSelectRowClick &&
2022-06-10 18:29:45 +00:00
disableMultiRowSelect &&
2024-03-04 13:14:50 +00:00
onSelectRowClick ( row ) ) ||
2025-02-04 19:05:22 +00:00
( disableMultiRowSelect &&
onClickRow &&
onClickRow ( row ) ) ;
2021-09-10 19:06:37 +00:00
} ,
2024-12-18 15:12:27 +00:00
// For accessibility when tabable
onKeyDown : ( e : KeyboardEvent ) = > {
if ( e . key === "Enter" ) {
2025-04-10 19:42:10 +00:00
e . stopPropagation ( ) ;
2024-12-18 15:12:27 +00:00
( onSelectRowClick &&
disableMultiRowSelect &&
onSelectRowClick ( row ) ) ||
2025-02-04 19:05:22 +00:00
( disableMultiRowSelect &&
onClickRow &&
onClickRow ( row ) ) ;
2024-12-18 15:12:27 +00:00
}
} ,
2021-09-10 19:06:37 +00:00
} ) }
2024-12-18 15:12:27 +00:00
// Can tab onto an entire row if a child element does not have the same onClick functionality as clicking the whole row
2024-12-26 22:51:28 +00:00
tabIndex = { keyboardSelectableRows ? 0 : - 1 }
2021-09-10 19:06:37 +00:00
>
2025-02-04 19:05:22 +00:00
{ row . cells . map ( ( cell : any , index : number ) = > {
// Only allow row click behavior on first cell
// if the first cell is not a checkbox
const cellProps = cell . getCellProps ( ) ;
2025-03-20 20:36:59 +00:00
const multiRowSelectEnabled = ! disableMultiRowSelect ;
2025-02-04 19:05:22 +00:00
2021-04-04 12:45:24 +00:00
return (
2021-11-12 22:45:53 +00:00
< td
2022-09-07 16:58:53 +00:00
key = { cell . column . id }
2021-11-12 22:45:53 +00:00
className = {
cell . column . id ? ` ${ cell . column . id } __cell ` : ""
}
2025-03-20 20:36:59 +00:00
style = {
multiRowSelectEnabled ? { cursor : "initial" } : { }
}
2025-02-04 19:05:22 +00:00
{ . . . cellProps }
2021-11-12 22:45:53 +00:00
>
{ cell . render ( "Cell" ) }
< / td >
2021-04-04 12:45:24 +00:00
) ;
} ) }
< / tr >
) ;
2021-04-14 16:52:15 +00:00
} ) }
2021-04-04 12:45:24 +00:00
< / tbody >
< / table >
< / div >
2025-10-15 22:03:54 +00:00
{ shouldShowFooter && (
2025-04-29 14:29:21 +00:00
< div className = { ` ${ baseClass } __footer ` } >
{ renderTableHelpText && ! ! rows ? . length && (
< div className = { ` ${ baseClass } __table-help-text ` } >
{ renderTableHelpText ( ) }
< / div >
) }
{ isClientSidePagination ? (
< Pagination
disablePrev = { ! canPreviousPage }
disableNext = { ! canNextPage }
onPrevPage = { ( ) = > {
2026-02-27 15:29:34 +00:00
! persistSelectedRows && toggleAllRowsSelected ( false ) ; // Resets row selection on pagination (client-side)
2025-08-12 17:13:37 +00:00
onClientSidePaginationChange
? onClientSidePaginationChange ( pageIndex - 1 )
: previousPage ( ) ;
2025-04-29 14:29:21 +00:00
} }
onNextPage = { ( ) = > {
2026-02-27 15:29:34 +00:00
! persistSelectedRows && toggleAllRowsSelected ( false ) ; // Resets row selection on pagination (client-side)
2025-08-12 17:13:37 +00:00
onClientSidePaginationChange
? onClientSidePaginationChange ( pageIndex + 1 )
: nextPage ( ) ;
2025-04-29 14:29:21 +00:00
} }
hidePagination = { ! canPreviousPage && ! canNextPage }
/ >
) : (
renderPagination && renderPagination ( )
) }
< / div >
) }
2021-04-04 12:45:24 +00:00
< / div >
) ;
} ;
export default DataTable ;