@keycloakify/keycloak-account-ui
Version:
Repackaged Keycloak Account UI
279 lines • 16.4 kB
JavaScript
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