blob: d4b2de6c2569268002aa6b462be677ae11a65859 [file] [log] [blame]
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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 React, { useCallback, useRef, ReactNode, HTMLProps, MutableRefObject } from 'react';
import {
useTable,
usePagination,
useSortBy,
useGlobalFilter,
PluginHook,
TableOptions,
FilterType,
IdType,
Row,
} from 'react-table';
import { matchSorter, rankings } from 'match-sorter';
import GlobalFilter, { GlobalFilterProps } from './components/GlobalFilter';
import SelectPageSize, { SelectPageSizeProps, SizeOption } from './components/SelectPageSize';
import SimplePagination from './components/Pagination';
import useSticky from './hooks/useSticky';
export interface DataTableProps<D extends object> extends TableOptions<D> {
tableClassName?: string;
searchInput?: boolean | GlobalFilterProps<D>['searchInput'];
selectPageSize?: boolean | SelectPageSizeProps['selectRenderer'];
pageSizeOptions?: SizeOption[]; // available page size options
maxPageItemCount?: number;
hooks?: PluginHook<D>[]; // any additional hooks
width?: string | number;
height?: string | number;
pageSize?: number;
noResults?: string | ((filterString: string) => ReactNode);
sticky?: boolean;
wrapperRef?: MutableRefObject<HTMLDivElement>;
}
export interface RenderHTMLCellProps extends HTMLProps<HTMLTableCellElement> {
cellContent: ReactNode;
}
// Be sure to pass our updateMyData and the skipReset option
export default function DataTable<D extends object>({
tableClassName,
columns,
data,
width: initialWidth = '100%',
height: initialHeight = 300,
pageSize: initialPageSize = 0,
initialState: initialState_ = {},
pageSizeOptions = [10, 25, 50, 100, 200],
maxPageItemCount = 9,
sticky: doSticky,
searchInput = true,
selectPageSize,
noResults: noResultsText = 'No data found',
hooks,
wrapperRef: userWrapperRef,
...moreUseTableOptions
}: DataTableProps<D>): JSX.Element {
const tableHooks: PluginHook<D>[] = [
useGlobalFilter,
useSortBy,
usePagination,
doSticky ? useSticky : [],
hooks || [],
].flat();
const sortByRef = useRef([]); // cache initial `sortby` so sorting doesn't trigger page reset
const pageSizeRef = useRef([initialPageSize, data.length]);
const hasPagination = initialPageSize > 0 && data.length > 0; // pageSize == 0 means no pagination
const hasGlobalControl = hasPagination || !!searchInput;
const initialState = {
...initialState_,
// zero length means all pages, the `usePagination` plugin does not
// understand pageSize = 0
sortBy: sortByRef.current,
pageSize: initialPageSize > 0 ? initialPageSize : data.length || 10,
};
const defaultWrapperRef = useRef<HTMLDivElement>(null);
const globalControlRef = useRef<HTMLDivElement>(null);
const paginationRef = useRef<HTMLDivElement>(null);
const wrapperRef = userWrapperRef || defaultWrapperRef;
const defaultGetTableSize = useCallback(() => {
if (wrapperRef.current) {
// `initialWidth` and `initialHeight` could be also parameters like `100%`
// `Number` reaturns `NaN` on them, then we fallback to computed size
const width = Number(initialWidth) || wrapperRef.current.clientWidth;
const height =
(Number(initialHeight) || wrapperRef.current.clientHeight) -
(globalControlRef.current?.clientHeight || 0) -
(paginationRef.current?.clientHeight || 0);
return { width, height };
}
return undefined;
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [initialHeight, initialWidth, wrapperRef, hasPagination, hasGlobalControl]);
const defaultGlobalFilter: FilterType<D> = useCallback(
(rows: Row<D>[], columnIds: IdType<D>[], filterValue: string) => {
// allow searching by "col1_value col2_value"
const joinedString = (row: Row<D>) => columnIds.map(x => row.values[x]).join(' ');
return matchSorter(rows, filterValue, {
keys: [...columnIds, joinedString],
threshold: rankings.ACRONYM,
}) as typeof rows;
},
[],
);
const {
getTableProps,
getTableBodyProps,
prepareRow,
headerGroups,
page,
pageCount,
gotoPage,
preGlobalFilteredRows,
setGlobalFilter,
setPageSize: setPageSize_,
wrapStickyTable,
state: { pageIndex, pageSize, globalFilter: filterValue, sticky = {} },
} = useTable<D>(
{
columns,
data,
initialState,
getTableSize: defaultGetTableSize,
globalFilter: defaultGlobalFilter,
...moreUseTableOptions,
},
...tableHooks,
);
// make setPageSize accept 0
const setPageSize = (size: number) => {
// keep the original size if data is empty
if (size || data.length !== 0) {
setPageSize_(size === 0 ? data.length : size);
}
};
const noResults =
typeof noResultsText === 'function' ? noResultsText(filterValue as string) : noResultsText;
if (!columns || columns.length === 0) {
return <div className="dt-no-results">{noResults}</div>;
}
const renderTable = () => (
<table {...getTableProps({ className: tableClassName })}>
<thead>
{headerGroups.map(headerGroup => {
const { key: headerGroupKey, ...headerGroupProps } = headerGroup.getHeaderGroupProps();
return (
<tr key={headerGroupKey || headerGroup.id} {...headerGroupProps}>
{headerGroup.headers.map(column =>
column.render('Header', {
key: column.id,
...column.getSortByToggleProps(),
}),
)}
</tr>
);
})}
</thead>
<tbody {...getTableBodyProps()}>
{page && page.length > 0 ? (
page.map(row => {
prepareRow(row);
const { key: rowKey, ...rowProps } = row.getRowProps();
return (
<tr key={rowKey || row.id} {...rowProps}>
{row.cells.map(cell => cell.render('Cell', { key: cell.column.id }))}
</tr>
);
})
) : (
<tr>
<td className="dt-no-results" colSpan={columns.length}>
{noResults}
</td>
</tr>
)}
</tbody>
</table>
);
// force upate the pageSize when it's been update from the initial state
if (
pageSizeRef.current[0] !== initialPageSize ||
// when initialPageSize stays as zero, but total number of records changed,
// we'd also need to update page size
(initialPageSize === 0 && pageSizeRef.current[1] !== data.length)
) {
pageSizeRef.current = [initialPageSize, data.length];
setPageSize(initialPageSize);
}
return (
<div ref={wrapperRef} style={{ width: initialWidth, height: initialHeight }}>
{hasGlobalControl ? (
<div ref={globalControlRef} className="form-inline dt-controls">
<div className="row">
<div className="col-sm-6">
{hasPagination ? (
<SelectPageSize
total={data.length}
current={pageSize}
options={pageSizeOptions}
selectRenderer={typeof selectPageSize === 'boolean' ? undefined : selectPageSize}
onChange={setPageSize}
/>
) : null}
</div>
{searchInput ? (
<div className="col-sm-6">
<GlobalFilter<D>
searchInput={typeof searchInput === 'boolean' ? undefined : searchInput}
preGlobalFilteredRows={preGlobalFilteredRows}
setGlobalFilter={setGlobalFilter}
filterValue={filterValue}
/>
</div>
) : null}
</div>
</div>
) : null}
{wrapStickyTable ? wrapStickyTable(renderTable) : renderTable()}
{hasPagination ? (
<SimplePagination
ref={paginationRef}
style={sticky.height ? undefined : { visibility: 'hidden' }}
maxPageItemCount={maxPageItemCount}
pageCount={pageCount}
currentPage={pageIndex}
onPageChange={gotoPage}
/>
) : null}
</div>
);
}