2020-12-07 19:23:18 +00:00
import React , { ReactNode , useEffect , useState } from "react" ;
import { useTranslation } from "react-i18next" ;
import {
IAction ,
IActions ,
IFormatter ,
Table ,
TableBody ,
TableHeader ,
TableVariant ,
} from "@patternfly/react-table" ;
import { PaginatingTableToolbar } from "./PaginatingTableToolbar" ;
import { Spinner } from "@patternfly/react-core" ;
import { TableToolbar } from "./TableToolbar" ;
import _ from "lodash" ;
type Row < T > = {
data : T ;
cells : ( keyof T ) [ ] ;
} ;
type DataTableProps < T > = {
ariaLabelKey : string ;
columns : Field < T > [ ] ;
rows : Row < T > [ ] ;
actions? : IActions ;
} ;
function DataTable < T > ( {
columns ,
rows ,
actions ,
ariaLabelKey ,
} : DataTableProps < T > ) {
const { t } = useTranslation ( ) ;
return (
< Table
variant = { TableVariant . compact }
cells = { columns . map ( ( column ) = > {
return { . . . column , title : t ( column . displayKey || column . name ) } ;
} ) }
rows = { rows }
actions = { actions }
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 [ ] > ;
isPaginated? : boolean ;
ariaLabelKey : string ;
searchPlaceholderKey : string ;
columns : Field < T > [ ] ;
actions? : Action < T > [ ] ;
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.
* }
* ] }
* @param { Object } props - The properties .
* @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
* @param { ReactNode } props . toolbarItem - Toolbar items that appear on the top of the table @see ToolbarItem
* @param { ReactNode } props . emptyState - ReactNode show when the list is empty could be any component but best to use @see ListEmptyState
* /
export function KeycloakDataTable < T > ( {
2020-12-07 19:23:18 +00:00
ariaLabelKey ,
searchPlaceholderKey ,
isPaginated = false ,
loader ,
columns ,
actions ,
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 ( "" ) ;
const load = async ( ) = > {
setLoading ( true ) ;
const data = await loader ( first , max , search ) ;
setRows (
data ! . map ( ( value ) = > {
return {
data : value ,
cells : columns.map ( ( col ) = > {
if ( col . cellRenderer ) {
return col . cellRenderer ( value ) ;
}
return ( value as any ) [ col . name ] ;
} ) ,
} ;
} )
) ;
setLoading ( false ) ;
} ;
useEffect ( ( ) = > {
load ( ) ;
} , [ first , max ] ) ;
const filter = ( search : string ) = > {
setFilteredData (
rows ! . filter ( ( row ) = >
row . cells . some (
( cell ) = >
cell && cell . toString ( ) . toLowerCase ( ) . includes ( search . toLowerCase ( ) )
)
)
) ;
} ;
const convertAction = ( ) = >
actions &&
_ . cloneDeep ( actions ) . map ( ( action : Action < T > , index : number ) = > {
delete action . onRowClick ;
action . onClick = async ( _ , rowIndex ) = > {
const result = await actions [ index ] . onRowClick ! ( rows ! [ rowIndex ] . data ) ;
if ( result ) {
load ( ) ;
}
} ;
return action ;
} ) ;
const searchOnChange = ( value : string ) = > {
if ( isPaginated ) {
setSearch ( value ) ;
} else {
filter ( value ) ;
}
} ;
const Loading = ( ) = > (
< div className = "pf-u-text-align-center" >
< Spinner / >
< / div >
) ;
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 }
inputGroupOnClick = { load }
inputGroupPlaceholder = { t ( searchPlaceholderKey ) }
toolbarItem = { toolbarItem }
>
{ ! loading && ( emptyState === undefined || rows . length !== 0 ) && (
< DataTable
actions = { convertAction ( ) }
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 }
>
{ ( emptyState === undefined || rows . length !== 0 ) && (
< DataTable
actions = { convertAction ( ) }
rows = { filteredData || rows }
columns = { columns }
ariaLabelKey = { ariaLabelKey }
/ >
) }
{ rows . length === 0 && emptyState }
< / TableToolbar >
) }
< / >
) ;
}