mirror of https://github.com/portainer/portainer
288 lines
7.5 KiB
TypeScript
288 lines
7.5 KiB
TypeScript
import {
|
|
Table as TableInstance,
|
|
TableState,
|
|
useReactTable,
|
|
Row,
|
|
Column,
|
|
getCoreRowModel,
|
|
getPaginationRowModel,
|
|
getFilteredRowModel,
|
|
getSortedRowModel,
|
|
getFacetedRowModel,
|
|
getFacetedUniqueValues,
|
|
getFacetedMinMaxValues,
|
|
getExpandedRowModel,
|
|
TableOptions,
|
|
} from '@tanstack/react-table';
|
|
import { ReactNode, useMemo } from 'react';
|
|
import clsx from 'clsx';
|
|
import _ from 'lodash';
|
|
|
|
import { AutomationTestingProps } from '@/types';
|
|
|
|
import { IconProps } from '@@/Icon';
|
|
|
|
import { DatatableHeader } from './DatatableHeader';
|
|
import { DatatableFooter } from './DatatableFooter';
|
|
import { defaultGetRowId } from './defaultGetRowId';
|
|
import { Table } from './Table';
|
|
import { useGoToHighlightedRow } from './useGoToHighlightedRow';
|
|
import { BasicTableSettings, DefaultType } from './types';
|
|
import { DatatableContent } from './DatatableContent';
|
|
import { createSelectColumn } from './select-column';
|
|
import { TableRow } from './TableRow';
|
|
import { type TableState as GlobalTableState } from './useTableState';
|
|
|
|
export type PaginationProps =
|
|
| {
|
|
isServerSidePagination?: false;
|
|
totalCount?: never;
|
|
page?: never;
|
|
onPageChange?: never;
|
|
}
|
|
| {
|
|
isServerSidePagination: true;
|
|
totalCount: number;
|
|
page: number;
|
|
onPageChange(page: number): void;
|
|
};
|
|
|
|
export interface Props<D extends DefaultType> extends AutomationTestingProps {
|
|
dataset: D[];
|
|
columns: TableOptions<D>['columns'];
|
|
renderTableSettings?(instance: TableInstance<D>): ReactNode;
|
|
renderTableActions?(selectedRows: D[]): ReactNode;
|
|
disableSelect?: boolean;
|
|
getRowId?(row: D): string;
|
|
isRowSelectable?(row: Row<D>): boolean;
|
|
emptyContentLabel?: string;
|
|
title?: string;
|
|
titleIcon?: IconProps['icon'];
|
|
initialTableState?: Partial<TableState>;
|
|
isLoading?: boolean;
|
|
description?: ReactNode;
|
|
highlightedItemId?: string;
|
|
settingsManager: GlobalTableState<BasicTableSettings>;
|
|
renderRow?(row: Row<D>, highlightedItemId?: string): ReactNode;
|
|
getRowCanExpand?(row: Row<D>): boolean;
|
|
noWidget?: boolean;
|
|
extendTableOptions?: (options: TableOptions<D>) => TableOptions<D>;
|
|
}
|
|
|
|
export function Datatable<D extends DefaultType>({
|
|
columns,
|
|
dataset,
|
|
renderTableSettings = () => null,
|
|
renderTableActions = () => null,
|
|
disableSelect,
|
|
getRowId = defaultGetRowId,
|
|
isRowSelectable = () => true,
|
|
title,
|
|
titleIcon,
|
|
emptyContentLabel,
|
|
initialTableState = {},
|
|
isLoading,
|
|
description,
|
|
settingsManager: settings,
|
|
renderRow = defaultRenderRow,
|
|
highlightedItemId,
|
|
noWidget,
|
|
getRowCanExpand,
|
|
'data-cy': dataCy,
|
|
onPageChange = () => {},
|
|
page,
|
|
totalCount = dataset.length,
|
|
isServerSidePagination = false,
|
|
extendTableOptions = (value) => value,
|
|
}: Props<D> & PaginationProps) {
|
|
const pageCount = useMemo(
|
|
() => Math.ceil(totalCount / settings.pageSize),
|
|
[settings.pageSize, totalCount]
|
|
);
|
|
|
|
const enableRowSelection = getIsSelectionEnabled(
|
|
disableSelect,
|
|
isRowSelectable
|
|
);
|
|
|
|
const allColumns = useMemo(
|
|
() => _.compact([!disableSelect && createSelectColumn<D>(), ...columns]),
|
|
[disableSelect, columns]
|
|
);
|
|
|
|
const tableInstance = useReactTable<D>(
|
|
extendTableOptions({
|
|
columns: allColumns,
|
|
data: dataset,
|
|
initialState: {
|
|
pagination: {
|
|
pageSize: settings.pageSize,
|
|
pageIndex: page || 0,
|
|
},
|
|
sorting: settings.sortBy ? [settings.sortBy] : [],
|
|
globalFilter: {
|
|
search: settings.search,
|
|
...initialTableState.globalFilter,
|
|
},
|
|
|
|
...initialTableState,
|
|
},
|
|
defaultColumn: {
|
|
enableColumnFilter: false,
|
|
enableHiding: true,
|
|
sortingFn: 'alphanumeric',
|
|
},
|
|
enableRowSelection,
|
|
autoResetExpanded: false,
|
|
globalFilterFn: defaultGlobalFilterFn,
|
|
getRowId,
|
|
getCoreRowModel: getCoreRowModel(),
|
|
getFilteredRowModel: getFilteredRowModel(),
|
|
getPaginationRowModel: getPaginationRowModel(),
|
|
getFacetedRowModel: getFacetedRowModel(),
|
|
getFacetedUniqueValues: getFacetedUniqueValues(),
|
|
getFacetedMinMaxValues: getFacetedMinMaxValues(),
|
|
getExpandedRowModel: getExpandedRowModel(),
|
|
getRowCanExpand,
|
|
getColumnCanGlobalFilter,
|
|
...(isServerSidePagination
|
|
? { manualPagination: true, pageCount }
|
|
: {
|
|
getSortedRowModel: getSortedRowModel(),
|
|
}),
|
|
})
|
|
);
|
|
|
|
const tableState = tableInstance.getState();
|
|
|
|
useGoToHighlightedRow(
|
|
isServerSidePagination,
|
|
tableState.pagination.pageSize,
|
|
tableInstance.getCoreRowModel().rows,
|
|
handlePageChange,
|
|
highlightedItemId
|
|
);
|
|
|
|
const selectedRowModel = tableInstance.getSelectedRowModel();
|
|
const selectedItems = selectedRowModel.rows.map((row) => row.original);
|
|
|
|
return (
|
|
<Table.Container noWidget={noWidget}>
|
|
<DatatableHeader
|
|
onSearchChange={handleSearchBarChange}
|
|
searchValue={settings.search}
|
|
title={title}
|
|
titleIcon={titleIcon}
|
|
description={description}
|
|
renderTableActions={() => renderTableActions(selectedItems)}
|
|
renderTableSettings={() => renderTableSettings(tableInstance)}
|
|
/>
|
|
|
|
<DatatableContent<D>
|
|
tableInstance={tableInstance}
|
|
renderRow={(row) => renderRow(row, highlightedItemId)}
|
|
emptyContentLabel={emptyContentLabel}
|
|
isLoading={isLoading}
|
|
onSortChange={handleSortChange}
|
|
data-cy={dataCy}
|
|
/>
|
|
|
|
<DatatableFooter
|
|
onPageChange={handlePageChange}
|
|
onPageSizeChange={handlePageSizeChange}
|
|
page={typeof page === 'number' ? page : tableState.pagination.pageIndex}
|
|
pageSize={tableState.pagination.pageSize}
|
|
pageCount={tableInstance.getPageCount()}
|
|
totalSelected={selectedItems.length}
|
|
/>
|
|
</Table.Container>
|
|
);
|
|
|
|
function handleSearchBarChange(search: string) {
|
|
tableInstance.setGlobalFilter({ search });
|
|
settings.setSearch(search);
|
|
}
|
|
|
|
function handlePageChange(page: number) {
|
|
tableInstance.setPageIndex(page);
|
|
onPageChange(page);
|
|
}
|
|
|
|
function handleSortChange(colId: string, desc: boolean) {
|
|
settings.setSortBy(colId, desc);
|
|
}
|
|
|
|
function handlePageSizeChange(pageSize: number) {
|
|
tableInstance.setPageSize(pageSize);
|
|
settings.setPageSize(pageSize);
|
|
}
|
|
}
|
|
|
|
function defaultRenderRow<D extends DefaultType>(
|
|
row: Row<D>,
|
|
highlightedItemId?: string
|
|
) {
|
|
return (
|
|
<TableRow<D>
|
|
cells={row.getVisibleCells()}
|
|
className={clsx({
|
|
active: highlightedItemId === row.id,
|
|
})}
|
|
/>
|
|
);
|
|
}
|
|
|
|
function getIsSelectionEnabled<D extends DefaultType>(
|
|
disabledSelect?: boolean,
|
|
isRowSelectable?: Props<D>['isRowSelectable']
|
|
) {
|
|
if (disabledSelect) {
|
|
return false;
|
|
}
|
|
|
|
if (isRowSelectable) {
|
|
return isRowSelectable;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
export function defaultGlobalFilterFn<D, TFilter extends { search: string }>(
|
|
row: Row<D>,
|
|
columnId: string,
|
|
filterValue: null | TFilter
|
|
): boolean {
|
|
const value = row.getValue(columnId);
|
|
|
|
if (filterValue === null || !filterValue.search) {
|
|
return true;
|
|
}
|
|
|
|
if (value == null) {
|
|
return false;
|
|
}
|
|
|
|
const filterValueLower = filterValue.search.toLowerCase();
|
|
|
|
if (
|
|
typeof value === 'string' ||
|
|
typeof value === 'number' ||
|
|
typeof value === 'boolean'
|
|
) {
|
|
return value.toString().toLowerCase().includes(filterValueLower);
|
|
}
|
|
|
|
if (Array.isArray(value)) {
|
|
return value.some((item) => item.toLowerCase().includes(filterValueLower));
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
function getColumnCanGlobalFilter<D>(column: Column<D, unknown>): boolean {
|
|
if (column.id === 'select') {
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|