2020-12-07 19:23:18 +00:00
import React , { ReactNode , useEffect , useState } from "react" ;
import { useTranslation } from "react-i18next" ;
2021-02-17 21:12:25 +00:00
import { useErrorHandler } from "react-error-boundary" ;
2020-12-07 19:23:18 +00:00
import {
IAction ,
IActions ,
2021-01-26 19:39:01 +00:00
IActionsResolver ,
2020-12-07 19:23:18 +00:00
IFormatter ,
Table ,
TableBody ,
TableHeader ,
TableVariant ,
} from "@patternfly/react-table" ;
import { Spinner } from "@patternfly/react-core" ;
import _ from "lodash" ;
2020-12-14 08:57:05 +00:00
import { PaginatingTableToolbar } from "./PaginatingTableToolbar" ;
import { TableToolbar } from "./TableToolbar" ;
2021-01-11 18:56:19 +00:00
import { asyncStateFetch } from "../../context/auth/AdminClient" ;
2020-12-14 08:57:05 +00:00
2020-12-07 19:23:18 +00:00
type Row < T > = {
data : T ;
2020-12-14 08:57:05 +00:00
selected : boolean ;
cells : ( keyof T | JSX . Element ) [ ] ;
2020-12-07 19:23:18 +00:00
} ;
type DataTableProps < T > = {
ariaLabelKey : string ;
columns : Field < T > [ ] ;
rows : Row < T > [ ] ;
actions? : IActions ;
2021-01-26 19:39:01 +00:00
actionResolver? : IActionsResolver ;
2020-12-14 08:57:05 +00:00
onSelect ? : ( isSelected : boolean , rowIndex : number ) = > void ;
canSelectAll : boolean ;
2020-12-07 19:23:18 +00:00
} ;
function DataTable < T > ( {
columns ,
rows ,
actions ,
2021-01-26 19:39:01 +00:00
actionResolver ,
2020-12-07 19:23:18 +00:00
ariaLabelKey ,
2020-12-14 08:57:05 +00:00
onSelect ,
canSelectAll ,
2020-12-07 19:23:18 +00:00
} : DataTableProps < T > ) {
const { t } = useTranslation ( ) ;
return (
< Table
variant = { TableVariant . compact }
2020-12-14 08:57:05 +00:00
onSelect = {
onSelect
? ( _ , isSelected , rowIndex ) = > onSelect ( isSelected , rowIndex )
: undefined
}
canSelectAll = { canSelectAll }
2020-12-07 19:23:18 +00:00
cells = { columns . map ( ( column ) = > {
return { . . . column , title : t ( column . displayKey || column . name ) } ;
} ) }
rows = { rows }
actions = { actions }
2021-01-26 19:39:01 +00:00
actionResolver = { actionResolver }
2020-12-07 19:23:18 +00:00
aria - label = { t ( ariaLabelKey ) }
>
< TableHeader / >
< TableBody / >
< / Table >
) ;
}
export type Field < T > = {
name : string ;
displayKey? : string ;
cellFormatters? : IFormatter [ ] ;
cellRenderer ? : ( row : T ) = > ReactNode ;
} ;
export type Action < T > = IAction & {
onRowClick ? : ( row : T ) = > Promise < boolean > | void ;
} ;
export type DataListProps < T > = {
loader : ( first? : number , max? : number , search? : string ) = > Promise < T [ ] > ;
2020-12-14 08:57:05 +00:00
onSelect ? : ( value : T [ ] ) = > void ;
canSelectAll? : boolean ;
2020-12-07 19:23:18 +00:00
isPaginated? : boolean ;
ariaLabelKey : string ;
searchPlaceholderKey : string ;
columns : Field < T > [ ] ;
actions? : Action < T > [ ] ;
2021-01-26 19:39:01 +00:00
actionResolver? : IActionsResolver ;
2021-02-23 13:36:37 +00:00
searchTypeComponent? : ReactNode ;
2020-12-07 19:23:18 +00:00
toolbarItem? : ReactNode ;
emptyState? : ReactNode ;
} ;
2020-12-11 10:18:29 +00:00
/ * *
* A generic component that can be used to show the initial list most sections have . Takes care of the loading of the date and filtering .
* All you have to define is how the columns are displayed .
* @example
* < KeycloakDataTable columns = { [
* {
* name : "clientId" , //name of the field from the array of object the loader returns to display in this column
* displayKey : "clients:clientID" , //i18n key to use to lookup the name of the column header
* cellRenderer : ClientDetailLink , //optionally you can use a component to render the column when you don't want just the content of the field, the whole row / entire object is passed in.
* }
* ] }
2021-01-12 13:16:35 +00:00
* @param { DataListProps } props - The properties .
2020-12-11 10:18:29 +00:00
* @param { string } props . ariaLabelKey - The aria label key i18n key to lookup the label
* @param { string } props . searchPlaceholderKey - The i18n key to lookup the placeholder for the search box
* @param { boolean } props . isPaginated - if true the the loader will be called with first , max and search and a pager will be added in the header
* @param { ( first? : number , max? : number , search? : string ) = > Promise < T [ ] > } props . loader - loader function that will fetch the data to display first , max and search are only applicable when isPaginated = true
* @param { Field < T > } props . columns - definition of the columns
* @param { Action [ ] } props . actions - the actions that appear on the row
2021-01-26 19:39:01 +00:00
* @param { IActionsResolver } props . actionResolver Resolver for the given action
2021-01-12 13:16:35 +00:00
* @param { ReactNode } props . toolbarItem - Toolbar items that appear on the top of the table { @link ToolbarItem }
* @param { ReactNode } props . emptyState - ReactNode show when the list is empty could be any component but best to use { @link ListEmptyState }
2020-12-11 10:18:29 +00:00
* /
export function KeycloakDataTable < T > ( {
2020-12-07 19:23:18 +00:00
ariaLabelKey ,
searchPlaceholderKey ,
isPaginated = false ,
2020-12-14 08:57:05 +00:00
onSelect ,
canSelectAll = false ,
2020-12-07 19:23:18 +00:00
loader ,
columns ,
actions ,
2021-01-26 19:39:01 +00:00
actionResolver ,
2021-02-23 13:36:37 +00:00
searchTypeComponent ,
2020-12-07 19:23:18 +00:00
toolbarItem ,
emptyState ,
} : DataListProps < T > ) {
const { t } = useTranslation ( ) ;
const [ rows , setRows ] = useState < Row < T > [ ] > ( ) ;
const [ filteredData , setFilteredData ] = useState < Row < T > [ ] > ( ) ;
const [ loading , setLoading ] = useState ( false ) ;
const [ max , setMax ] = useState ( 10 ) ;
const [ first , setFirst ] = useState ( 0 ) ;
const [ search , setSearch ] = useState ( "" ) ;
2021-01-05 13:39:27 +00:00
const [ key , setKey ] = useState ( 0 ) ;
const refresh = ( ) = > setKey ( new Date ( ) . getTime ( ) ) ;
2021-02-17 21:12:25 +00:00
const handleError = useErrorHandler ( ) ;
2020-12-07 19:23:18 +00:00
useEffect ( ( ) = > {
2021-01-11 18:56:19 +00:00
return asyncStateFetch (
2021-01-05 13:39:27 +00:00
async ( ) = > {
setLoading ( true ) ;
const data = await loader ( first , max , search ) ;
const result = data ! . map ( ( value ) = > {
return {
data : value ,
selected : false ,
cells : columns.map ( ( col ) = > {
if ( col . cellRenderer ) {
return col . cellRenderer ( value ) ;
}
2021-01-12 13:16:35 +00:00
return _ . get ( value , col . name ) ;
2021-01-05 13:39:27 +00:00
} ) ,
} ;
} ) ;
return result ;
} ,
( result ) = > {
setRows ( result ) ;
2021-01-29 10:33:18 +00:00
setFilteredData ( result ) ;
2021-01-05 13:39:27 +00:00
setLoading ( false ) ;
2021-02-17 21:12:25 +00:00
} ,
handleError
2021-01-05 13:39:27 +00:00
) ;
} , [ key , first , max ] ) ;
2020-12-07 19:23:18 +00:00
2020-12-14 08:57:05 +00:00
const getNodeText = ( node : keyof T | JSX . Element ) : string = > {
if ( [ "string" , "number" ] . includes ( typeof node ) ) {
return node ! . toString ( ) ;
}
if ( node instanceof Array ) {
return node . map ( getNodeText ) . join ( "" ) ;
}
if ( typeof node === "object" && node ) {
return getNodeText ( node . props . children ) ;
}
return "" ;
} ;
2020-12-07 19:23:18 +00:00
const filter = ( search : string ) = > {
setFilteredData (
rows ! . filter ( ( row ) = >
row . cells . some (
( cell ) = >
2020-12-14 08:57:05 +00:00
cell &&
getNodeText ( cell ) . toLowerCase ( ) . includes ( search . toLowerCase ( ) )
2020-12-07 19:23:18 +00:00
)
)
) ;
} ;
const convertAction = ( ) = >
actions &&
_ . cloneDeep ( actions ) . map ( ( action : Action < T > , index : number ) = > {
delete action . onRowClick ;
action . onClick = async ( _ , rowIndex ) = > {
2021-01-29 10:33:18 +00:00
const result = await actions [ index ] . onRowClick ! (
( filteredData || rows ) ! [ rowIndex ] . data
) ;
2020-12-07 19:23:18 +00:00
if ( result ) {
2021-01-05 13:39:27 +00:00
refresh ( ) ;
2020-12-07 19:23:18 +00:00
}
} ;
return action ;
} ) ;
const searchOnChange = ( value : string ) = > {
if ( isPaginated ) {
setSearch ( value ) ;
} else {
filter ( value ) ;
}
} ;
const Loading = ( ) = > (
< div className = "pf-u-text-align-center" >
< Spinner / >
< / div >
) ;
2020-12-14 08:57:05 +00:00
const _onSelect = ( isSelected : boolean , rowIndex : number ) = > {
if ( rowIndex === - 1 ) {
setRows (
rows ! . map ( ( row ) = > {
row . selected = isSelected ;
return row ;
} )
) ;
} else {
rows ! [ rowIndex ] . selected = isSelected ;
setRows ( [ . . . rows ! ] ) ;
}
onSelect ! ( rows ! . filter ( ( row ) = > row . selected ) . map ( ( row ) = > row . data ) ) ;
} ;
2020-12-07 19:23:18 +00:00
return (
< >
{ ! rows && < Loading / > }
{ rows && isPaginated && (
< PaginatingTableToolbar
count = { rows . length }
first = { first }
max = { max }
onNextClick = { setFirst }
onPreviousClick = { setFirst }
onPerPageSelect = { ( first , max ) = > {
setFirst ( first ) ;
setMax ( max ) ;
} }
inputGroupName = { ` ${ ariaLabelKey } input ` }
inputGroupOnChange = { searchOnChange }
2021-01-05 13:39:27 +00:00
inputGroupOnClick = { refresh }
2020-12-07 19:23:18 +00:00
inputGroupPlaceholder = { t ( searchPlaceholderKey ) }
2021-02-23 13:36:37 +00:00
searchTypeComponent = { searchTypeComponent }
2020-12-07 19:23:18 +00:00
toolbarItem = { toolbarItem }
>
{ ! loading && ( emptyState === undefined || rows . length !== 0 ) && (
< DataTable
2020-12-14 08:57:05 +00:00
canSelectAll = { canSelectAll }
onSelect = { onSelect ? _onSelect : undefined }
2020-12-07 19:23:18 +00:00
actions = { convertAction ( ) }
2021-01-26 19:39:01 +00:00
actionResolver = { actionResolver }
2020-12-07 19:23:18 +00:00
rows = { rows }
columns = { columns }
ariaLabelKey = { ariaLabelKey }
/ >
) }
{ ! loading && rows . length === 0 && emptyState }
{ loading && < Loading / > }
< / PaginatingTableToolbar >
) }
{ rows && ! isPaginated && (
< TableToolbar
inputGroupName = { ` ${ ariaLabelKey } input ` }
inputGroupOnChange = { searchOnChange }
inputGroupOnClick = { ( ) = > { } }
inputGroupPlaceholder = { t ( searchPlaceholderKey ) }
toolbarItem = { toolbarItem }
2021-02-23 13:36:37 +00:00
searchTypeComponent = { searchTypeComponent }
2020-12-07 19:23:18 +00:00
>
{ ( emptyState === undefined || rows . length !== 0 ) && (
< DataTable
2020-12-14 08:57:05 +00:00
canSelectAll = { canSelectAll }
onSelect = { onSelect ? _onSelect : undefined }
2020-12-07 19:23:18 +00:00
actions = { convertAction ( ) }
2021-01-26 19:39:01 +00:00
actionResolver = { actionResolver }
2020-12-07 19:23:18 +00:00
rows = { filteredData || rows }
columns = { columns }
ariaLabelKey = { ariaLabelKey }
/ >
) }
{ rows . length === 0 && emptyState }
< / TableToolbar >
) }
< / >
) ;
}