diff --git a/cypress/integration/realm_roles_test.spec.ts b/cypress/integration/realm_roles_test.spec.ts index 7e3df414dc..52ae73c177 100644 --- a/cypress/integration/realm_roles_test.spec.ts +++ b/cypress/integration/realm_roles_test.spec.ts @@ -70,6 +70,7 @@ describe("Realm roles test", function () { masthead.checkNotificationMessage("Role created"); + // Add associated realm role cy.get("#roles-actions-dropdown").last().click(); cy.get("#add-roles").click(); @@ -83,6 +84,24 @@ describe("Realm roles test", function () { cy.url().should("include", "/AssociatedRoles"); cy.get("#composite-role-badge").should("contain.text", "Composite"); + + // Add associated client role + + cy.get('[data-cy=add-role-button]').click(); + + cy.wait(100); + + cy.get('[data-cy=filter-type-dropdown]').click() + + cy.get('[data-cy=filter-type-dropdown-item]').click() + + cy.wait(2500); + + cy.get('[type="checkbox"]').eq(4).check({force: true}); + + cy.get("#add-associated-roles-button").contains("Add").click(); + + cy.wait(2500); }); }); }); diff --git a/src/components/table-toolbar/KeycloakDataTable.tsx b/src/components/table-toolbar/KeycloakDataTable.tsx index 3bd7cb76dc..60bec32546 100644 --- a/src/components/table-toolbar/KeycloakDataTable.tsx +++ b/src/components/table-toolbar/KeycloakDataTable.tsx @@ -89,6 +89,7 @@ export type DataListProps = { columns: Field[]; actions?: Action[]; actionResolver?: IActionsResolver; + searchTypeComponent?: ReactNode; toolbarItem?: ReactNode; emptyState?: ReactNode; }; @@ -126,6 +127,7 @@ export function KeycloakDataTable({ columns, actions, actionResolver, + searchTypeComponent, toolbarItem, emptyState, }: DataListProps) { @@ -262,6 +264,7 @@ export function KeycloakDataTable({ inputGroupOnChange={searchOnChange} inputGroupOnClick={refresh} inputGroupPlaceholder={t(searchPlaceholderKey)} + searchTypeComponent={searchTypeComponent} toolbarItem={toolbarItem} > {!loading && (emptyState === undefined || rows.length !== 0) && ( @@ -286,6 +289,7 @@ export function KeycloakDataTable({ inputGroupOnClick={() => {}} inputGroupPlaceholder={t(searchPlaceholderKey)} toolbarItem={toolbarItem} + searchTypeComponent={searchTypeComponent} > {(emptyState === undefined || rows.length !== 0) && ( void; onPreviousClick: (page: number) => void; onPerPageSelect: (max: number, first: number) => void; + searchTypeComponent?: React.ReactNode; toolbarItem?: React.ReactNode; children: React.ReactNode; inputGroupName?: string; @@ -31,6 +32,7 @@ export const PaginatingTableToolbar = ({ onNextClick, onPreviousClick, onPerPageSelect, + searchTypeComponent, toolbarItem, children, inputGroupName, @@ -59,6 +61,7 @@ export const PaginatingTableToolbar = ({ return ( {toolbarItem} diff --git a/src/components/table-toolbar/TableToolbar.tsx b/src/components/table-toolbar/TableToolbar.tsx index eaf0e0ec2b..9e03ccf78a 100644 --- a/src/components/table-toolbar/TableToolbar.tsx +++ b/src/components/table-toolbar/TableToolbar.tsx @@ -18,6 +18,7 @@ import { SearchIcon } from "@patternfly/react-icons"; import { useTranslation } from "react-i18next"; type TableToolbarProps = { + filterToolbarDropdown?: ReactNode; toolbarItem?: ReactNode; toolbarItemFooter?: ReactNode; children: ReactNode; @@ -32,6 +33,7 @@ type TableToolbarProps = { }; export const TableToolbar = ({ + filterToolbarDropdown, toolbarItem, toolbarItemFooter, children, @@ -50,6 +52,7 @@ export const TableToolbar = ({ {inputGroupName && ( + {filterToolbarDropdown} {searchTypeComponent} { + const [containerName, setContainerName] = useState(""); + + useEffect(() => { + adminClient.clients + .findOne({ id: containerId! }) + .then((client) => setContainerName(client.clientId as string)); + }, [containerId]); + + if (filterType === "roles") { + return <>{name}; + } + + if (filterType === "clients") { + return ( + <> + {containerId && ( + + )}{" "} + {name} + + ); + } + + return null; +}; export type AssociatedRolesModalProps = { open: boolean; @@ -37,16 +89,38 @@ export const AssociatedRolesModal = (props: AssociatedRolesModalProps) => { const [name, setName] = useState(""); const adminClient = useAdminClient(); const [selectedRows, setSelectedRows] = useState([]); + const [allClientRoles, setAllClientRoles] = useState( + [] + ); + + const [isFilterDropdownOpen, setIsFilterDropdownOpen] = useState(false); + const [filterType, setFilterType] = useState("roles"); + const tableRefresher = React.useRef<() => void>(); const { id } = useParams<{ id: string }>(); + const alphabetize = (rolesList: RoleRepresentation[]) => { + return rolesList.sort((r1, r2) => { + const r1Name = r1.name?.toUpperCase(); + const r2Name = r2.name?.toUpperCase(); + if (r1Name! < r2Name!) { + return -1; + } + if (r1Name! > r2Name!) { + return 1; + } + + return 0; + }); + }; + const loader = async () => { const allRoles = await adminClient.roles.find(); const existingAdditionalRoles = await adminClient.roles.getCompositeRoles({ id, }); - return allRoles.filter((role: RoleRepresentation) => { + return alphabetize(allRoles).filter((role: RoleRepresentation) => { return ( existingAdditionalRoles.find( (existing: RoleRepresentation) => existing.name === role.name @@ -55,6 +129,52 @@ export const AssociatedRolesModal = (props: AssociatedRolesModalProps) => { }); }; + const AliasRenderer = (role: RoleRepresentation) => { + return ( + <> + + + ); + }; + + const clientRolesLoader = async () => { + const clients = await adminClient.clients.find(); + + const clientIdArray = clients.map((client) => client.id); + + let rolesList: RoleRepresentation[] = []; + for (const id of clientIdArray) { + const clientRolesList = await adminClient.clients.listRoles({ + id: id as string, + }); + rolesList = [...rolesList, ...clientRolesList]; + } + const existingAdditionalRoles = await adminClient.roles.getCompositeRoles({ + id, + }); + + setAllClientRoles(rolesList); + console.log(allClientRoles); + + return alphabetize(rolesList).filter((role: RoleRepresentation) => { + return ( + existingAdditionalRoles.find( + (existing: RoleRepresentation) => existing.name === role.name + ) === undefined && role.name !== name + ); + }); + }; + + React.useEffect(() => { + tableRefresher.current && tableRefresher.current(); + }, [filterType]); + useEffect(() => { (async () => { if (id) { @@ -77,6 +197,24 @@ export const AssociatedRolesModal = (props: AssociatedRolesModalProps) => { }); }; + const onFilterDropdownToggle = () => { + setIsFilterDropdownOpen(!isFilterDropdownOpen); + }; + + const onFilterDropdownSelect = (filterType: string) => { + if (filterType == "roles") { + setFilterType("clients"); + } + if (filterType == "clients") { + setFilterType("roles"); + } + setIsFilterDropdownOpen(!isFilterDropdownOpen); + }; + + const setRefresher = (refresher: () => void) => { + tableRefresher.current = refresher; + }; + return ( { > onFilterDropdownSelect(filterType)} + data-cy="filter-type-dropdown" + toggle={ + } + > + Filter by {filterType} + + } + isOpen={isFilterDropdownOpen} + dropdownItems={[ + + {filterType == "roles" + ? t("filterByClients") + : t("filterByRoles")}{" "} + , + ]} + /> + } canSelectAll // isPaginated onSelect={(rows) => { @@ -121,15 +287,11 @@ export const AssociatedRolesModal = (props: AssociatedRolesModalProps) => { { name: "name", displayKey: "roles:roleName", - }, - { - name: "composite", - displayKey: "roles:composite", - cellFormatters: [boolFormatter()], + cellRenderer: AliasRenderer, }, { name: "description", - displayKey: "roles:description", + displayKey: "common:description", }, ]} emptyState={ diff --git a/src/realm-roles/AssociatedRolesTab.tsx b/src/realm-roles/AssociatedRolesTab.tsx index 37807af6c4..6eb2f046eb 100644 --- a/src/realm-roles/AssociatedRolesTab.tsx +++ b/src/realm-roles/AssociatedRolesTab.tsx @@ -136,6 +136,7 @@ export const AssociatedRolesTab = ({ className="kc-add-role-button" key="add-role-button" onClick={() => toggleModal()} + data-cy="add-role-button" > {t("addRole")} diff --git a/src/realm-roles/RealmRolesSection.tsx b/src/realm-roles/RealmRolesSection.tsx index dbddad6227..0878b3fcca 100644 --- a/src/realm-roles/RealmRolesSection.tsx +++ b/src/realm-roles/RealmRolesSection.tsx @@ -6,6 +6,7 @@ import { RolesList } from "./RolesList"; export const RealmRolesSection = () => { const adminClient = useAdminClient(); + const loader = async (first?: number, max?: number, search?: string) => { const params: { [name: string]: string | number } = { first: first!, diff --git a/src/realm-roles/messages.json b/src/realm-roles/messages.json index 28dfa7a3b0..f68c61cab6 100644 --- a/src/realm-roles/messages.json +++ b/src/realm-roles/messages.json @@ -13,6 +13,8 @@ "importRole": "Import role", "roleID": "Role ID", "homeURL": "Home URL", + "filterByClients": "Filter by clients", + "filterByRoles": "Filter by roles", "roleExplain": "Realm-level roles are a global namespace to define your roles.", "roleCreateExplain": "This is some description", "roleName": "Role name",