229 lines
6.6 KiB
TypeScript
229 lines
6.6 KiB
TypeScript
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;
|
|
};
|
|
|
|
/**
|
|
* 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>({
|
|
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>
|
|
)}
|
|
</>
|
|
);
|
|
}
|