diff --git a/cypress/integration/realm_roles_test.spec.ts b/cypress/integration/realm_roles_test.spec.ts index a13294ea89..60da90447e 100644 --- a/cypress/integration/realm_roles_test.spec.ts +++ b/cypress/integration/realm_roles_test.spec.ts @@ -4,6 +4,7 @@ import ModalUtils from "../support/util/ModalUtils"; import ListingPage from "../support/pages/admin_console/ListingPage"; import SidebarPage from "../support/pages/admin_console/SidebarPage"; import CreateRealmRolePage from "../support/pages/admin_console/manage/realm_roles/CreateRealmRolePage"; +import AssociatedRolesPage from "../support/pages/admin_console/manage/realm_roles/AssociatedRolesPage"; let itemId = "realm_role_crud"; const loginPage = new LoginPage(); @@ -12,6 +13,7 @@ const modalUtils = new ModalUtils(); const sidebarPage = new SidebarPage(); const listingPage = new ListingPage(); const createRealmRolePage = new CreateRealmRolePage(); +const associatedRolesPage = new AssociatedRolesPage(); describe("Realm roles test", function () { describe("Realm roles creation", function () { @@ -48,7 +50,7 @@ describe("Realm roles test", function () { listingPage.searchItem(itemId).itemExist(itemId); - // Update + cy.wait(100); // Delete listingPage.deleteItem(itemId); @@ -70,40 +72,15 @@ describe("Realm roles test", function () { masthead.checkNotificationMessage("Role created"); + cy.wait(100); + // Add associated realm role - cy.get("#roles-actions-dropdown").last().click(); - cy.get("#add-roles").click(); - - cy.wait(100); - - cy.get('[type="checkbox"]').eq(1).check(); - - cy.get("#add-associated-roles-button").contains("Add").click(); - - cy.url().should("include", "/AssociatedRoles"); - - cy.get("#composite-role-badge").should("contain.text", "Composite"); - - cy.wait(100); + associatedRolesPage.addAssociatedRealmRole(); // 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(40).check({force: true}); - - cy.get("#add-associated-roles-button").contains("Add").click(); - - cy.wait(2500); + associatedRolesPage.addAssociatedClientRole(); }); }); }); diff --git a/cypress/support/pages/admin_console/manage/realm_roles/AssociatedRolesPage.ts b/cypress/support/pages/admin_console/manage/realm_roles/AssociatedRolesPage.ts new file mode 100644 index 0000000000..1d8fe1dbd2 --- /dev/null +++ b/cypress/support/pages/admin_console/manage/realm_roles/AssociatedRolesPage.ts @@ -0,0 +1,64 @@ +export default class AssociatedRolesPage { + actionDropdown: string; + addRolesDropdownItem: string; + addRoleToolbarButton: string; + checkbox: string; + addAssociatedRolesModalButton: string; + compositeRoleBadge: string; + filterTypeDropdown: string; + filterTypeDropdownItem: string; + usersPage: string; + + constructor() { + this.actionDropdown = "[data-testid=action-dropdown]"; + this.addRolesDropdownItem = "[data-testid=add-roles]"; + this.addRoleToolbarButton = "[data-testid=add-role-button]"; + this.checkbox = "[type=checkbox]"; + this.addAssociatedRolesModalButton = + "[data-testid=add-associated-roles-button]"; + this.compositeRoleBadge = "[data-testid=composite-role-badge]"; + this.filterTypeDropdown = "[data-testid=filter-type-dropdown]"; + this.filterTypeDropdownItem = "[data-testid=filter-type-dropdown-item]"; + this.usersPage = "[data-testid=users-page]"; + } + + addAssociatedRealmRole() { + cy.get(this.actionDropdown).last().click(); + + cy.get(this.addRolesDropdownItem).click(); + + cy.wait(100); + + cy.get(this.checkbox).eq(1).check(); + + cy.get(this.addAssociatedRolesModalButton).contains("Add").click(); + + cy.url().should("include", "/AssociatedRoles"); + + cy.get(this.compositeRoleBadge).should("contain.text", "Composite"); + + cy.wait(2500); + + return this; + } + + addAssociatedClientRole() { + cy.get(this.addRoleToolbarButton).click(); + + cy.wait(100); + + cy.get(this.filterTypeDropdown).click(); + + cy.get(this.filterTypeDropdownItem).click(); + + cy.wait(2500); + + cy.get(this.checkbox).eq(40).check({ force: true }); + + cy.get(this.addAssociatedRolesModalButton).contains("Add").click(); + + cy.wait(2500); + + cy.contains("Users in role").click().get(this.usersPage).should("exist"); + } +} diff --git a/src/components/help-enabler/HelpHeader.tsx b/src/components/help-enabler/HelpHeader.tsx index 9848a12db8..d42cda08a9 100644 --- a/src/components/help-enabler/HelpHeader.tsx +++ b/src/components/help-enabler/HelpHeader.tsx @@ -9,7 +9,7 @@ import { Switch, TextContent, } from "@patternfly/react-core"; -import { Trans, useTranslation } from "react-i18next"; +import { useTranslation } from "react-i18next"; import { HelpIcon, ExternalLinkAltIcon } from "@patternfly/react-icons"; import "./help-header.css"; diff --git a/src/components/list-empty-state/ListEmptyState.tsx b/src/components/list-empty-state/ListEmptyState.tsx index 3a805477d6..cc095f1c05 100644 --- a/src/components/list-empty-state/ListEmptyState.tsx +++ b/src/components/list-empty-state/ListEmptyState.tsx @@ -19,7 +19,7 @@ export type Action = { export type ListEmptyStateProps = { message: string; - instructions: string; + instructions: React.ReactNode; primaryActionText?: string; onPrimaryAction?: MouseEventHandler; hasIcon?: boolean; diff --git a/src/components/view-header/ViewHeader.tsx b/src/components/view-header/ViewHeader.tsx index 4f25de5a8c..2cc6b281a6 100644 --- a/src/components/view-header/ViewHeader.tsx +++ b/src/components/view-header/ViewHeader.tsx @@ -41,7 +41,6 @@ export const ViewHeader = ({ actionsDropdownId, titleKey, badge, - badgeId, badgeIsRead, subKey, subKeyLinkProps, @@ -77,7 +76,10 @@ export const ViewHeader = ({ {badge && ( - + {badge} diff --git a/src/realm-roles/AssociatedRolesModal.tsx b/src/realm-roles/AssociatedRolesModal.tsx index e72a8adec8..45a0121109 100644 --- a/src/realm-roles/AssociatedRolesModal.tsx +++ b/src/realm-roles/AssociatedRolesModal.tsx @@ -177,7 +177,7 @@ export const AssociatedRolesModal = (props: AssociatedRolesModalProps) => { actions={[ diff --git a/src/realm-roles/RealmRoleTabs.tsx b/src/realm-roles/RealmRoleTabs.tsx index 68d1e1df54..7bd9dca9b2 100644 --- a/src/realm-roles/RealmRoleTabs.tsx +++ b/src/realm-roles/RealmRoleTabs.tsx @@ -23,6 +23,7 @@ import { useRealm } from "../context/realm-context/RealmContext"; import { AssociatedRolesModal } from "./AssociatedRolesModal"; import { KeycloakTabs } from "../components/keycloak-tabs/KeycloakTabs"; import { AssociatedRolesTab } from "./AssociatedRolesTab"; +import { UsersInRoleTab } from "./UsersInRoleTab"; const arrayToAttributes = (attributeArray: KeyValueType[]) => { const initValue: { [index: string]: string[] } = {}; @@ -254,7 +255,6 @@ export const RealmRoleTabs = () => { 0 ? t("composite") : ""} - badgeId="composite-role-badge" badgeIsRead={true} subKey={id ? "" : "roles:roleCreateExplain"} actionsDropdownId="roles-actions-dropdown" @@ -287,7 +287,7 @@ export const RealmRoleTabs = () => { , toggleModal()} > @@ -336,6 +336,12 @@ export const RealmRoleTabs = () => { reset={() => form.reset(role)} /> + {t("usersInRole")}} + > + + )} {!id && ( diff --git a/src/realm-roles/RealmRolesSection.css b/src/realm-roles/RealmRolesSection.css index a45446edc6..b45ea91f84 100644 --- a/src/realm-roles/RealmRolesSection.css +++ b/src/realm-roles/RealmRolesSection.css @@ -24,5 +24,33 @@ --pf-c-form__group--m-action--MarginTop: calc( var(--pf-global--spacer--2xl) - var(--pf-global--spacer--sm) ); - } + +.kc-who-will-appear-button { + padding-left: 0px; +} + +.pf-c-button.pf-m-link.kc-groups-link { + font-size: var(--pf-global--FontSize--sm); + padding-left: 0px; + padding-right: var(--pf-global--spacer--xs); + padding-top: 0px; +} + +.pf-c-button.pf-m-link.kc-users-link { + font-size: var(--pf-global--FontSize--sm); + padding-left: var(--pf-global--spacer--xs); + padding-right: var(--pf-global--spacer--xs); + padding-top: 0px; +} + +.pf-c-button.pf-m-link.kc-groups-link-empty-state { + padding-left: var(--pf-global--spacer--xs); + padding-right: var(--pf-global--spacer--xs); +} + +.pf-c-button.pf-m-link.kc-users-link-empty-state { + padding-left: var(--pf-global--spacer--xs); + padding-right: var(--pf-global--spacer--xs); +} + diff --git a/src/realm-roles/UsersInRoleTab.tsx b/src/realm-roles/UsersInRoleTab.tsx new file mode 100644 index 0000000000..4a88611b33 --- /dev/null +++ b/src/realm-roles/UsersInRoleTab.tsx @@ -0,0 +1,134 @@ +import React, { useContext } from "react"; +import { useHistory, useParams } from "react-router-dom"; +import { useTranslation } from "react-i18next"; +import { Button, PageSection, Popover } from "@patternfly/react-core"; +import { ListEmptyState } from "../components/list-empty-state/ListEmptyState"; +import { KeycloakDataTable } from "../components/table-toolbar/KeycloakDataTable"; +import { boolFormatter, emptyFormatter } from "../util"; +import { useAdminClient } from "../context/auth/AdminClient"; +import { QuestionCircleIcon } from "@patternfly/react-icons"; +import { useRealm } from "../context/realm-context/RealmContext"; +import { HelpContext } from "../components/help-enabler/HelpHeader"; + +export const UsersInRoleTab = () => { + const history = useHistory(); + const { realm } = useRealm(); + + const { t } = useTranslation("roles"); + + const { id } = useParams<{ id: string }>(); + + const adminClient = useAdminClient(); + + const loader = async (first?: number, max?: number) => { + const role = await adminClient.roles.findOneById({ id: id }); + const usersWithRole = await adminClient.roles.findUsersWithRole({ + name: role.name!, + first: first!, + max: max!, + } as any); + return usersWithRole; + }; + + const { enabled } = useContext(HelpContext); + + return ( + <> + + + {t("roles:whoWillAppearPopoverText")} + + {t("or")} + + + } + footerContent={t("roles:whoWillAppearPopoverFooterText")} + > + + + ) + } + emptyState={ + + {t("noUsersEmptyStateDescription")} + + {t("or")} + + {t("noUsersEmptyStateDescriptionContinued")} + + } + /> + } + columns={[ + { + name: "username", + displayKey: "roles:userName", + cellFormatters: [emptyFormatter()], + }, + { + name: "email", + displayKey: "roles:email", + cellFormatters: [emptyFormatter()], + }, + { + name: "lastName", + displayKey: "roles:lastName", + cellFormatters: [emptyFormatter()], + }, + { + name: "firstName", + displayKey: "roles:firstName", + cellFormatters: [boolFormatter(), emptyFormatter()], + }, + ]} + /> + + + ); +}; diff --git a/src/realm-roles/messages.json b/src/realm-roles/messages.json index 623dca94ea..02af1031e6 100644 --- a/src/realm-roles/messages.json +++ b/src/realm-roles/messages.json @@ -48,6 +48,24 @@ "roleRemoveAssociatedText": "This action will remove {{role}} from {{roleName}. All the associated roles of {{role}} will also be removed.", "compositeRoleOff": "Composite role turned off", "associatedRolesRemoved": "Associated roles have been removed", - "compositesRemovedAlertDescription": "All the associated roles have been removed" + "compositesRemovedAlertDescription": "All the associated roles have been removed", + "whoWillAppearLinkText": "Who will appear in this user list?", + "whoWillAppearPopoverText": "This tab shows only the users who are assigned directly to this role. To see users who are assigned this role as an associated role or through a group, go to", + "whoWillAppearPopoverFooterText": "Users who have this role as an effective role cannot be added on this tab.", + "usersInRole": "Users in role", + "addUser": "Add user", + "removeUser": "Remove users", + "removeUserText": "Do you want to remove {{numSelected}} users?. These users will no longer have permissions of the role {{role}} and the associated roles of it.", + "noDirectUsers": "No direct users", + "noUsersEmptyStateDescription": "Only the users with this role directly assigned will appear under this tab. If you need to find users assigned to this role, go to", + "noUsersEmptyStateDescriptionContinued": "to find them. Users that already have this role as an effective role cannot be added here.", + "id": "ID", + "groups": "Groups", + "or": "or", + "users": "Users", + "userName": "Username", + "email": "Email", + "lastName": "Last name", + "firstName": "First name" } }