remove use of deprecated table component (#29812)

* remove use of deprecated table component

Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com>

* added transformer

Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com>

* fix row click

Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com>

* removed useless name label

Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com>

* fix row click again

Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com>

* removed more useless name label

Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com>

* removed useless options

Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com>

* removed useless options

Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com>

* removed data-label

Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com>

* fix for action click

Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com>

* made indeterminate work again

Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com>

* fixed test

Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com>

---------

Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com>
This commit is contained in:
Erik Jan de Wit 2024-06-04 11:01:01 +02:00 committed by GitHub
parent e5123ea9de
commit d1756564a7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 163 additions and 83 deletions

View file

@ -777,7 +777,7 @@ describe("Clients test", () => {
commonPage.sidebar().waitForPageLoad(); commonPage.sidebar().waitForPageLoad();
rolesTab.goToAssociatedRolesTab(); rolesTab.goToAssociatedRolesTab();
cy.get('td[data-label="Name"]') cy.get("td")
.contains("manage-account") .contains("manage-account")
.parent() .parent()
.within(() => { .within(() => {

View file

@ -11,7 +11,7 @@ export default class RoleMappingTab {
#assignBtn = "assign"; #assignBtn = "assign";
#hideInheritedRolesBtn = "#hideInheritedRoles"; #hideInheritedRolesBtn = "#hideInheritedRoles";
#assignedRolesTable = "assigned-roles"; #assignedRolesTable = "assigned-roles";
#namesColumn = 'td[data-label="Name"]:visible'; #namesColumn = "td:visible";
#roleMappingTab = "role-mapping-tab"; #roleMappingTab = "role-mapping-tab";
#filterTypeDropdown = "filter-type-dropdown"; #filterTypeDropdown = "filter-type-dropdown";

View file

@ -27,7 +27,7 @@ export default class InitialAccessTokenTab extends CommonPage {
} }
getFirstId(callback: (id: string) => void) { getFirstId(callback: (id: string) => void) {
cy.get('tbody > tr:first-child > [data-label="ID"]') cy.get("tbody > tr:first-child > td:first-child")
.invoke("text") .invoke("text")
.then((text) => { .then((text) => {
callback(text); callback(text);

View file

@ -13,8 +13,7 @@ export default class GroupDetailPage extends GroupPage {
#attributesTab = "attributes"; #attributesTab = "attributes";
#roleMappingTab = "role-mapping-tab"; #roleMappingTab = "role-mapping-tab";
#permissionsTab = "permissionsTab"; #permissionsTab = "permissionsTab";
#memberNameColumn = #memberNameColumn = '[data-testid="members-table"] > tbody > tr';
'[data-testid="members-table"] > tbody > tr > [data-label="Name"]';
#addMembers = "addMember"; #addMembers = "addMember";
#memberUsernameColumn = 'tbody > tr > [data-label="Username"]'; #memberUsernameColumn = 'tbody > tr > [data-label="Username"]';
#actionDrpDwnItemRenameGroup = "renameGroupAction"; #actionDrpDwnItemRenameGroup = "renameGroupAction";

View file

@ -8,7 +8,7 @@ export default class AssociatedRolesPage {
#filterTypeDropdownItem = "clients"; #filterTypeDropdownItem = "clients";
#usersPage = "users-page"; #usersPage = "users-page";
#removeRolesButton = "unAssignRole"; #removeRolesButton = "unAssignRole";
#addRoleTable = '[aria-label="Roles"] td[data-label="Name"]'; #addRoleTable = '[aria-label="Roles"] td';
addAssociatedRealmRole(roleName: string) { addAssociatedRealmRole(roleName: string) {
cy.findByTestId(this.#actionDropdown).last().click(); cy.findByTestId(this.#actionDropdown).last().click();

View file

@ -85,7 +85,7 @@ export default class RealmSettingsPage extends CommonPage {
eventsUserSave = "save-user"; eventsUserSave = "save-user";
enableAdminEvents = "adminEventsEnabled"; enableAdminEvents = "adminEventsEnabled";
eventsAdminSave = "save-admin"; eventsAdminSave = "save-admin";
eventTypeColumn = 'tbody > tr > [data-label="Event saved type"]'; eventTypeColumn = "tbody > tr td";
filterSelectMenu = ".kc-filter-type-select"; filterSelectMenu = ".kc-filter-type-select";
passiveKeysOption = "PASSIVE-option"; passiveKeysOption = "PASSIVE-option";
disabledKeysOption = "DISABLED-option"; disabledKeysOption = "DISABLED-option";
@ -1270,12 +1270,12 @@ export default class RealmSettingsPage extends CommonPage {
} }
checkElementNotInList(name: string) { checkElementNotInList(name: string) {
cy.get('tbody [data-label="Name"]').should("not.contain.text", name); cy.get("tbody").should("not.contain.text", name);
return this; return this;
} }
checkElementInList(name: string) { checkElementInList(name: string) {
cy.get('tbody [data-label="Name"]').should("contain.text", name); cy.get("tbody").should("contain.text", name);
return this; return this;
} }

View file

@ -21,7 +21,7 @@ export default class UserProfile {
#newAttributeAnnotationBtn = "annotations-add-row"; #newAttributeAnnotationBtn = "annotations-add-row";
#newAttributeAnnotationKey = "annotations.0.key"; #newAttributeAnnotationKey = "annotations.0.key";
#newAttributeAnnotationValue = "annotations.0.value"; #newAttributeAnnotationValue = "annotations.0.value";
#validatorsList = 'tbody [data-label="name"]'; #validatorsList = "tbody";
#saveNewAttributeBtn = "attribute-create"; #saveNewAttributeBtn = "attribute-create";
#addValidatorBtn = "addValidator"; #addValidatorBtn = "addValidator";
#removeValidatorBtn = "deleteValidator"; #removeValidatorBtn = "deleteValidator";

View file

@ -3,7 +3,7 @@ export default class UserRegistration {
#defaultGroupTab = "#pf-tab-20-groups"; #defaultGroupTab = "#pf-tab-20-groups";
#addRoleBtn = "assignRole"; #addRoleBtn = "assignRole";
#addDefaultGroupBtn = "no-default-groups-empty-action"; #addDefaultGroupBtn = "no-default-groups-empty-action";
#namesColumn = 'tbody td[data-label="Name"]:visible'; #namesColumn = "tbody td:visible";
#addBtn = "assign"; #addBtn = "assign";
#filterTypeDropdown = "filter-type-dropdown"; #filterTypeDropdown = "filter-type-dropdown";

View file

@ -1,20 +1,24 @@
import { Button, ButtonVariant, ToolbarItem } from "@patternfly/react-core"; import { Button, ButtonVariant, ToolbarItem } from "@patternfly/react-core";
import type { SVGIconProps } from "@patternfly/react-icons/dist/js/createIcon"; import type { SVGIconProps } from "@patternfly/react-icons/dist/js/createIcon";
import { import {
ActionsColumn,
ExpandableRowContent,
IAction, IAction,
IActions, IActions,
IActionsResolver, IActionsResolver,
IFormatter, IFormatter,
IRow, IRow,
IRowCell,
ITransform, ITransform,
TableVariant,
} from "@patternfly/react-table";
import {
Table, Table,
TableBody,
TableHeader,
TableProps, TableProps,
} from "@patternfly/react-table/deprecated"; TableVariant,
Tbody,
Td,
Th,
Thead,
Tr,
} from "@patternfly/react-table";
import { cloneDeep, differenceBy, get } from "lodash-es"; import { cloneDeep, differenceBy, get } from "lodash-es";
import { import {
ComponentClass, ComponentClass,
@ -67,6 +71,18 @@ type DataTableProps<T> = {
isRadio?: boolean; isRadio?: boolean;
}; };
type CellRendererProps = {
row: IRow;
};
const CellRenderer = ({ row }: CellRendererProps) => {
const isRow = (c: ReactNode | IRowCell): c is IRowCell =>
!!c && (c as IRowCell).title !== undefined;
return row.cells!.map((c, i) => (
<Td key={`cell-${i}`}>{(isRow(c) ? c.title : c) as ReactNode}</Td>
));
};
function DataTable<T>({ function DataTable<T>({
columns, columns,
rows, rows,
@ -81,32 +97,129 @@ function DataTable<T>({
...props ...props
}: DataTableProps<T>) { }: DataTableProps<T>) {
const { t } = useTranslation(); const { t } = useTranslation();
const [selectedRows, setSelectedRows] = useState<boolean[]>([]);
const [expandedRows, setExpandedRows] = useState<boolean[]>([]);
const updateState = (rowIndex: number, isSelected: boolean) => {
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 as HTMLInputElement;
const selected = selectedRows.filter((r) => r === true);
checkbox.indeterminate =
selected.length < rows.length && selected.length > 0;
}
}
}, [selectedRows]);
return ( return (
<Table <Table
{...props} {...props}
variant={isNotCompact ? undefined : TableVariant.compact} variant={isNotCompact ? undefined : TableVariant.compact}
onSelect={
onSelect
? (_, isSelected, rowIndex) => onSelect(isSelected, rowIndex)
: undefined
}
onCollapse={
onCollapse
? (_, rowIndex, isOpen) => onCollapse(isOpen, rowIndex)
: undefined
}
selectVariant={isRadio ? "radio" : "checkbox"}
canSelectAll={canSelectAll}
cells={columns.map((column) => {
return { ...column, title: t(column.displayKey || column.name) };
})}
rows={rows as IRow[]}
actions={actions}
actionResolver={actionResolver}
aria-label={t(ariaLabelKey)} aria-label={t(ariaLabelKey)}
> >
<TableHeader /> <Thead>
<TableBody /> <Tr>
{onCollapse && <Th />}
{canSelectAll && (
<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) => (
<Th
key={column.displayKey}
aria-label={t(ariaLabelKey)}
className={column.transforms?.[0]().className}
>
{t(column.displayKey || column.name)}
</Th>
))}
</Tr>
</Thead>
{!onCollapse ? (
<Tbody>
{(rows as IRow[]).map((row, index) => (
<Tr key={index} isExpanded={expandedRows[index]}>
{onSelect && (
<Td
select={{
rowIndex: index,
onSelect: (_, isSelected, rowIndex) => {
onSelect!(isSelected, rowIndex);
updateState(rowIndex, isSelected);
},
isSelected: selectedRows[index],
variant: isRadio ? "radio" : "checkbox",
}}
/>
)}
<CellRenderer row={row} />
{(actions || actionResolver) && (
<Td isActionCell>
<ActionsColumn
items={actions || actionResolver?.(row, {})!}
extraData={{ rowIndex: index }}
/>
</Td>
)}
</Tr>
))}
</Tbody>
) : (
(rows as IRow[]).map((row, index) => (
<Tbody key={index}>
{index % 2 === 0 ? (
<Tr>
<Td
expand={{
isExpanded: !!expandedRows[index],
rowIndex: index,
expandId: `${index}`,
onToggle: (_, rowIndex, isOpen) => {
onCollapse(isOpen, rowIndex);
const expand = [...expandedRows];
expand[index] = isOpen;
setExpandedRows(expand);
},
}}
/>
<CellRenderer row={row} />
</Tr>
) : (
<Tr isExpanded={!!expandedRows[index - 1]}>
<Td />
<Td colSpan={columns.length}>
<ExpandableRowContent>
<CellRenderer row={row} />
</ExpandableRowContent>
</Td>
</Tr>
)}
</Tbody>
))
)}
</Table> </Table>
); );
} }
@ -229,6 +342,10 @@ export function KeycloakDataTable<T>({
const renderCell = (columns: (Field<T> | DetailField<T>)[], value: T) => { const renderCell = (columns: (Field<T> | DetailField<T>)[], value: T) => {
return columns.map((col) => { return columns.map((col) => {
if ("cellFormatters" in col) {
const v = get(value, col.name);
return col.cellFormatters?.reduce((s, f) => f(s), v);
}
if (col.cellRenderer) { if (col.cellRenderer) {
const Component = col.cellRenderer; const Component = col.cellRenderer;
//@ts-ignore //@ts-ignore
@ -302,22 +419,6 @@ export function KeycloakDataTable<T>({
[search, first, max], [search, first, max],
); );
useEffect(() => {
if (canSelectAll) {
const checkboxes = document
.getElementsByClassName("pf-v5-c-table__check")
.item(0);
if (checkboxes) {
const checkAllCheckbox = checkboxes.children!.item(
0,
)! as HTMLInputElement;
checkAllCheckbox.indeterminate =
selected.length > 0 &&
selected.length < (filteredData || rows)!.length;
}
}
}, [selected]);
useFetch( useFetch(
async () => { async () => {
setLoading(true); setLoading(true);

View file

@ -153,7 +153,6 @@ export const KeysListTab = ({ realmComponents }: KeysListTabProps) => {
} }
/> />
} }
canSelectAll
columns={[ columns={[
{ {
name: "algorithm", name: "algorithm",
@ -182,16 +181,14 @@ export const KeysListTab = ({ realmComponents }: KeysListTabProps) => {
{ {
name: "provider", name: "provider",
displayKey: "provider", displayKey: "provider",
cellRenderer: ({ provider }: KeyData) => provider || "", cellRenderer: ({ provider }: KeyData) => provider || "-",
cellFormatters: [emptyFormatter()],
transforms: [cellWidth(10)], transforms: [cellWidth(10)],
}, },
{ {
name: "validTo", name: "validTo",
displayKey: "validTo", displayKey: "validTo",
cellRenderer: ({ validTo }: KeyData) => cellRenderer: ({ validTo }: KeyData) =>
validTo ? formatDate(new Date(validTo)) : "", validTo ? formatDate(new Date(validTo)) : "-",
cellFormatters: [emptyFormatter()],
transforms: [cellWidth(10)], transforms: [cellWidth(10)],
}, },
{ {
@ -252,7 +249,6 @@ export const KeysListTab = ({ realmComponents }: KeysListTabProps) => {
); );
} else return ""; } else return "";
}, },
cellFormatters: [],
transforms: [cellWidth(20)], transforms: [cellWidth(20)],
}, },
]} ]}

View file

@ -11,7 +11,7 @@ import {
} from "@patternfly/react-core"; } from "@patternfly/react-core";
import { CubesIcon, InfoCircleIcon } from "@patternfly/react-icons"; import { CubesIcon, InfoCircleIcon } from "@patternfly/react-icons";
import { IRowData } from "@patternfly/react-table"; import { IRowData } from "@patternfly/react-table";
import { MouseEvent, ReactNode, useMemo, useState } from "react"; import { ReactNode, useMemo, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { Link, useMatch, useNavigate } from "react-router-dom"; import { Link, useMatch, useNavigate } from "react-router-dom";
import { useAdminClient } from "../admin-client"; import { useAdminClient } from "../admin-client";
@ -165,11 +165,7 @@ export default function SessionsTable({
}, },
}); });
async function onClickRevoke( async function onClickRevoke(rowData: IRowData) {
event: MouseEvent,
rowIndex: number,
rowData: IRowData,
) {
const session = rowData.data as UserSessionRepresentation; const session = rowData.data as UserSessionRepresentation;
await adminClient.realms.deleteSession({ await adminClient.realms.deleteSession({
realm, realm,
@ -180,11 +176,7 @@ export default function SessionsTable({
refresh(); refresh();
} }
async function onClickSignOut( async function onClickSignOut(rowData: IRowData) {
event: MouseEvent,
rowIndex: number,
rowData: IRowData,
) {
const session = rowData.data as UserSessionRepresentation; const session = rowData.data as UserSessionRepresentation;
await adminClient.realms.deleteSession({ await adminClient.realms.deleteSession({
realm, realm,
@ -230,14 +222,14 @@ export default function SessionsTable({
return [ return [
{ {
title: t("revoke"), title: t("revoke"),
onClick: onClickRevoke, onClick: () => onClickRevoke(rowData),
} as Action<UserSessionRepresentation>, } as Action<UserSessionRepresentation>,
]; ];
} }
return [ return [
{ {
title: t("signOut"), title: t("signOut"),
onClick: onClickSignOut, onClick: () => onClickSignOut(rowData),
} as Action<UserSessionRepresentation>, } as Action<UserSessionRepresentation>,
]; ];
}} }}

View file

@ -1,5 +1,6 @@
import type GroupRepresentation from "@keycloak/keycloak-admin-client/lib/defs/groupRepresentation"; import type GroupRepresentation from "@keycloak/keycloak-admin-client/lib/defs/groupRepresentation";
import type UserRepresentation from "@keycloak/keycloak-admin-client/lib/defs/userRepresentation"; import type UserRepresentation from "@keycloak/keycloak-admin-client/lib/defs/userRepresentation";
import { useHelp } from "@keycloak/keycloak-ui-shared";
import { import {
AlertVariant, AlertVariant,
Button, Button,
@ -12,7 +13,6 @@ import { cellWidth } from "@patternfly/react-table";
import { intersectionBy, sortBy, uniqBy } from "lodash-es"; import { intersectionBy, sortBy, uniqBy } from "lodash-es";
import { useState } from "react"; import { useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { useHelp } from "@keycloak/keycloak-ui-shared";
import { useAdminClient } from "../admin-client"; import { useAdminClient } from "../admin-client";
import { useAlerts } from "../components/alert/Alerts"; import { useAlerts } from "../components/alert/Alerts";
import { useConfirmDialog } from "../components/confirm-dialog/ConfirmDialog"; import { useConfirmDialog } from "../components/confirm-dialog/ConfirmDialog";
@ -21,7 +21,6 @@ import { GroupPickerDialog } from "../components/group/GroupPickerDialog";
import { ListEmptyState } from "../components/list-empty-state/ListEmptyState"; import { ListEmptyState } from "../components/list-empty-state/ListEmptyState";
import { KeycloakDataTable } from "../components/table-toolbar/KeycloakDataTable"; import { KeycloakDataTable } from "../components/table-toolbar/KeycloakDataTable";
import { useAccess } from "../context/access/Access"; import { useAccess } from "../context/access/Access";
import { emptyFormatter } from "../util";
type UserGroupsProps = { type UserGroupsProps = {
user: UserRepresentation; user: UserRepresentation;
@ -239,8 +238,7 @@ export const UserGroups = ({ user }: UserGroupsProps) => {
{ {
name: "groupMembership", name: "groupMembership",
displayKey: "groupMembership", displayKey: "groupMembership",
cellRenderer: (group: GroupRepresentation) => group.name || "", cellRenderer: (group: GroupRepresentation) => group.name || "-",
cellFormatters: [emptyFormatter()],
transforms: [cellWidth(40)], transforms: [cellWidth(40)],
}, },
{ {
@ -269,10 +267,9 @@ export const UserGroups = ({ user }: UserGroupsProps) => {
{t("leave")} {t("leave")}
</Button> </Button>
) : ( ) : (
"" "-"
); );
}, },
cellFormatters: [emptyFormatter()],
transforms: [cellWidth(20)], transforms: [cellWidth(20)],
}, },
]} ]}

View file

@ -194,7 +194,6 @@ export const UserIdentityProviderLinks = ({
{ {
name: "identityProvider", name: "identityProvider",
displayKey: "name", displayKey: "name",
cellFormatters: [emptyFormatter()],
cellRenderer: idpLinkRenderer, cellRenderer: idpLinkRenderer,
transforms: [cellWidth(20)], transforms: [cellWidth(20)],
}, },
@ -213,7 +212,6 @@ export const UserIdentityProviderLinks = ({
}, },
{ {
name: "", name: "",
cellFormatters: [emptyFormatter()],
cellRenderer: unlinkRenderer, cellRenderer: unlinkRenderer,
transforms: [cellWidth(20)], transforms: [cellWidth(20)],
}, },
@ -223,7 +221,6 @@ export const UserIdentityProviderLinks = ({
columns.splice(1, 0, { columns.splice(1, 0, {
name: "type", name: "type",
displayKey: "type", displayKey: "type",
cellFormatters: [emptyFormatter()],
cellRenderer: badgeRenderer1, cellRenderer: badgeRenderer1,
transforms: [cellWidth(10)], transforms: [cellWidth(10)],
}); });
@ -286,13 +283,11 @@ export const UserIdentityProviderLinks = ({
{ {
name: "type", name: "type",
displayKey: "type", displayKey: "type",
cellFormatters: [emptyFormatter()],
cellRenderer: badgeRenderer2, cellRenderer: badgeRenderer2,
transforms: [cellWidth(60)], transforms: [cellWidth(60)],
}, },
{ {
name: "", name: "",
cellFormatters: [emptyFormatter()],
cellRenderer: linkRenderer, cellRenderer: linkRenderer,
}, },
]} ]}