UNPKG

@keycloakify/keycloak-account-ui

Version:
279 lines 16.4 kB
var __rest = (this && this.__rest) || function (s, e) { var t = {}; for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0) t[p] = s[p]; if (s != null && typeof Object.getOwnPropertySymbols === "function") for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) { if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i])) t[p[i]] = s[p[i]]; } return t; }; import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime"; import { Button, ButtonVariant, ToolbarItem } from "@patternfly/react-core"; import { ActionsColumn, ExpandableRowContent, Table, TableVariant, Tbody, Td, Th, Thead, Tr, } from "@patternfly/react-table"; import { cloneDeep, differenceBy, get } from "lodash-es"; import { isValidElement, useEffect, useId, useMemo, useRef, useState, } from "react"; import { useTranslation } from "react-i18next"; import { useStoredState } from "../../../ui-shared/utils/useStoredState"; import { useFetch } from "../../../ui-shared/utils/useFetch"; import { ListEmptyState } from "../../../ui-shared/controls/table/ListEmptyState"; import { PaginatingTableToolbar } from "../../../ui-shared/controls/table/PaginatingTableToolbar"; import { SyncAltIcon } from "@patternfly/react-icons"; import { KeycloakSpinner } from "../../../ui-shared/controls/KeycloakSpinner"; const CellRenderer = ({ row }) => { const isRow = (c) => !!c && c.title !== undefined; return row.cells.map((c, i) => (_jsx(Td, { children: (isRow(c) ? c.title : c) }, `cell-${i}`))); }; function DataTable(_a) { var { columns, rows, actions, actionResolver, ariaLabelKey, onSelect, onCollapse, canSelectAll, isNotCompact, isRadio } = _a, props = __rest(_a, ["columns", "rows", "actions", "actionResolver", "ariaLabelKey", "onSelect", "onCollapse", "canSelectAll", "isNotCompact", "isRadio"]); const { t } = useTranslation(); const [selectedRows, setSelectedRows] = useState([]); const [expandedRows, setExpandedRows] = useState([]); const updateState = (rowIndex, isSelected) => { const items = [ ...(rowIndex === -1 ? Array(rows.length).fill(isSelected) : selectedRows), ]; items[rowIndex] = isSelected; setSelectedRows(items); }; useEffect(() => { if (canSelectAll) { const selectAllCheckbox = document.getElementsByName("check-all").item(0); if (selectAllCheckbox) { const checkbox = selectAllCheckbox; const selected = selectedRows.filter((r) => r === true); checkbox.indeterminate = selected.length < rows.length && selected.length > 0; } } }, [selectedRows]); return (_jsxs(Table, Object.assign({}, props, { variant: isNotCompact ? undefined : TableVariant.compact, "aria-label": t(ariaLabelKey), children: [_jsx(Thead, { children: _jsxs(Tr, { children: [onCollapse && _jsx(Th, {}), canSelectAll && (_jsx(Th, { select: !isRadio ? { onSelect: (_, isSelected, rowIndex) => { onSelect(isSelected, rowIndex); updateState(-1, isSelected); }, isSelected: selectedRows.filter((r) => r === true).length === rows.length, } : undefined })), columns.map((column) => { var _a; return (_jsx(Th, { className: (_a = column.transforms) === null || _a === void 0 ? void 0 : _a[0]().className, children: t(column.displayKey || column.name) }, column.displayKey)); })] }) }), !onCollapse ? (_jsx(Tbody, { children: rows.map((row, index) => (_jsxs(Tr, { isExpanded: expandedRows[index], children: [onSelect && (_jsx(Td, { select: { rowIndex: index, onSelect: (_, isSelected, rowIndex) => { onSelect(isSelected, rowIndex); updateState(rowIndex, isSelected); }, isSelected: selectedRows[index], variant: isRadio ? "radio" : "checkbox", } })), _jsx(CellRenderer, { row: row }), (actions || actionResolver) && (_jsx(Td, { isActionCell: true, children: _jsx(ActionsColumn, { items: actions || (actionResolver === null || actionResolver === void 0 ? void 0 : actionResolver(row, {})), extraData: { rowIndex: index } }) }))] }, index))) })) : (rows.map((row, index) => (_jsx(Tbody, { children: index % 2 === 0 ? (_jsxs(Tr, { children: [_jsx(Td, { expand: { isExpanded: !!expandedRows[index], rowIndex: index, expandId: `${index}`, onToggle: (_, rowIndex, isOpen) => { onCollapse(isOpen, rowIndex); const expand = [...expandedRows]; expand[index] = isOpen; setExpandedRows(expand); }, } }), _jsx(CellRenderer, { row: row })] })) : (_jsxs(Tr, { isExpanded: !!expandedRows[index - 1], children: [_jsx(Td, {}), _jsx(Td, { colSpan: columns.length, children: _jsx(ExpandableRowContent, { children: _jsx(CellRenderer, { row: row }) }) })] })) }, index))))] }))); } /** * 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: "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 {DataListProps} 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 {Field<T>} props.detailColumns - definition of the columns expandable columns * @param {Action[]} props.actions - the actions that appear on the row * @param {IActionsResolver} props.actionResolver Resolver for the given action * @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} */ export function KeycloakDataTable(_a) { var { ariaLabelKey, searchPlaceholderKey, isPaginated = false, onSelect, canSelectAll = false, isNotCompact, isRadio, detailColumns, isRowDisabled, loader, columns, actions, actionResolver, searchTypeComponent, toolbarItem, subToolbar, emptyState, icon, isSearching = false } = _a, props = __rest(_a, ["ariaLabelKey", "searchPlaceholderKey", "isPaginated", "onSelect", "canSelectAll", "isNotCompact", "isRadio", "detailColumns", "isRowDisabled", "loader", "columns", "actions", "actionResolver", "searchTypeComponent", "toolbarItem", "subToolbar", "emptyState", "icon", "isSearching"]); const { t } = useTranslation(); const [selected, setSelected] = useState([]); const [rows, setRows] = useState(); const [unPaginatedData, setUnPaginatedData] = useState(); const [loading, setLoading] = useState(false); const [defaultPageSize, setDefaultPageSize] = useStoredState(localStorage, "pageSize", 10); const [max, setMax] = useState(defaultPageSize); const [first, setFirst] = useState(0); const [search, setSearch] = useState(""); const prevSearch = useRef(); const [key, setKey] = useState(0); const prevKey = useRef(); const refresh = () => setKey(key + 1); const id = useId(); const renderCell = (columns, value) => { return columns.map((col) => { var _a; if ("cellFormatters" in col) { const v = get(value, col.name); return (_a = col.cellFormatters) === null || _a === void 0 ? void 0 : _a.reduce((s, f) => f(s), v); } if (col.cellRenderer) { const Component = col.cellRenderer; //@ts-ignore return { title: _jsx(Component, Object.assign({}, value)) }; } return get(value, col.name); }); }; const convertToColumns = (data) => { const isDetailColumnsEnabled = (value) => { var _a, _b; return (_b = (_a = detailColumns === null || detailColumns === void 0 ? void 0 : detailColumns[0]) === null || _a === void 0 ? void 0 : _a.enabled) === null || _b === void 0 ? void 0 : _b.call(_a, value); }; return data .map((value, index) => { const disabledRow = isRowDisabled ? isRowDisabled(value) : false; const row = [ { data: value, disableSelection: disabledRow, disableActions: disabledRow, selected: !!selected.find((v) => get(v, "id") === get(value, "id")), isOpen: isDetailColumnsEnabled(value) ? false : undefined, cells: renderCell(columns, value), }, ]; if (detailColumns) { row.push({ parent: index * 2, cells: isDetailColumnsEnabled(value) ? renderCell(detailColumns, value) : [], }); } return row; }) .flat(); }; const getNodeText = (node) => { if (["string", "number"].includes(typeof node)) { return node.toString(); } if (node instanceof Array) { return node.map(getNodeText).join(""); } if (typeof node === "object") { return getNodeText(isValidElement(node.title) ? node.title.props : Object.values(node)); } return ""; }; const filteredData = useMemo(() => search === "" || isPaginated ? undefined : convertToColumns(unPaginatedData || []) .filter((row) => row.cells.some((cell) => cell && getNodeText(cell) .toLowerCase() .includes(search.toLowerCase()))) .slice(first, first + max + 1), [search, first, max]); useFetch(async () => { setLoading(true); const newSearch = prevSearch.current === "" && search !== ""; if (newSearch) { setFirst(0); } prevSearch.current = search; return typeof loader === "function" ? key === prevKey.current && unPaginatedData ? unPaginatedData : await loader(newSearch ? 0 : first, max + 1, search) : loader; }, (data) => { prevKey.current = key; if (!isPaginated) { setUnPaginatedData(data); if (data.length > first) { data = data.slice(first, first + max + 1); } else { setFirst(0); } } const result = convertToColumns(data); setRows(result); setLoading(false); }, [ key, first, max, search, typeof loader !== "function" ? loader : undefined, ]); const convertAction = () => actions && cloneDeep(actions).map((action, index) => { delete action.onRowClick; action.onClick = async (_, rowIndex) => { const result = await actions[index].onRowClick((filteredData || rows)[rowIndex].data); if (result) { if (!isPaginated) { setSearch(""); } refresh(); } }; return action; }); const _onSelect = (isSelected, rowIndex) => { const data = filteredData || rows; if (rowIndex === -1) { setRows(data.map((row) => { row.selected = isSelected; return row; })); } else { data[rowIndex].selected = isSelected; setRows([...rows]); } // Keeps selected items when paginating const difference = differenceBy(selected, data.map((row) => row.data), "id"); // Selected rows are any rows previously selected from a different page, plus current page selections const selectedRows = [ ...difference, ...data.filter((row) => row.selected).map((row) => row.data), ]; setSelected(selectedRows); onSelect(selectedRows); }; const onCollapse = (isOpen, rowIndex) => { data[rowIndex].isOpen = isOpen; setRows([...data]); }; const data = filteredData || rows; const noData = !data || data.length === 0; const searching = search !== "" || isSearching; // if we use detail columns there are twice the number of rows const maxRows = detailColumns ? max * 2 : max; const rowLength = detailColumns ? ((data === null || data === void 0 ? void 0 : data.length) || 0) / 2 : (data === null || data === void 0 ? void 0 : data.length) || 0; return (_jsxs(_Fragment, { children: [(loading || !noData || searching) && (_jsxs(PaginatingTableToolbar, { id: id, count: rowLength, first: first, max: max, onNextClick: setFirst, onPreviousClick: setFirst, onPerPageSelect: (first, max) => { setFirst(first); setMax(max); setDefaultPageSize(max); }, inputGroupName: searchPlaceholderKey ? `${ariaLabelKey}input` : undefined, inputGroupOnEnter: setSearch, inputGroupPlaceholder: t(searchPlaceholderKey || ""), searchTypeComponent: searchTypeComponent, toolbarItem: _jsxs(_Fragment, { children: [toolbarItem, " ", _jsx(ToolbarItem, { variant: "separator" }), " ", _jsx(ToolbarItem, { children: _jsxs(Button, { variant: "link", onClick: refresh, children: [_jsx(SyncAltIcon, {}), " ", t("refresh")] }) })] }), subToolbar: subToolbar, children: [!loading && !noData && (_jsx(DataTable, Object.assign({}, props, { canSelectAll: canSelectAll, onSelect: onSelect ? _onSelect : undefined, onCollapse: detailColumns ? onCollapse : undefined, actions: convertAction(), actionResolver: actionResolver, rows: data.slice(0, maxRows), columns: columns, isNotCompact: isNotCompact, isRadio: isRadio, ariaLabelKey: ariaLabelKey }))), !loading && noData && searching && (_jsx(ListEmptyState, { hasIcon: true, icon: icon, isSearchVariant: true, message: t("noSearchResults"), instructions: t("noSearchResultsInstructions"), secondaryActions: !isSearching ? [ { text: t("clearAllFilters"), onClick: () => setSearch(""), type: ButtonVariant.link, }, ] : [] })), loading && _jsx(KeycloakSpinner, {})] })), !loading && noData && !searching && emptyState] })); } //# sourceMappingURL=KeycloakDataTable.js.map