Merge pull request #401 from jenny-s51/usersInRole
Realm roles: add "users in role" tab
This commit is contained in:
commit
9c1efdcc3b
11 changed files with 273 additions and 44 deletions
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
}
|
|
@ -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";
|
||||
|
|
|
@ -19,7 +19,7 @@ export type Action = {
|
|||
|
||||
export type ListEmptyStateProps = {
|
||||
message: string;
|
||||
instructions: string;
|
||||
instructions: React.ReactNode;
|
||||
primaryActionText?: string;
|
||||
onPrimaryAction?: MouseEventHandler<HTMLButtonElement>;
|
||||
hasIcon?: boolean;
|
||||
|
|
|
@ -41,7 +41,6 @@ export const ViewHeader = ({
|
|||
actionsDropdownId,
|
||||
titleKey,
|
||||
badge,
|
||||
badgeId,
|
||||
badgeIsRead,
|
||||
subKey,
|
||||
subKeyLinkProps,
|
||||
|
@ -77,7 +76,10 @@ export const ViewHeader = ({
|
|||
</LevelItem>
|
||||
{badge && (
|
||||
<LevelItem>
|
||||
<Badge id={badgeId} isRead={badgeIsRead}>
|
||||
<Badge
|
||||
data-testid="composite-role-badge"
|
||||
isRead={badgeIsRead}
|
||||
>
|
||||
{badge}
|
||||
</Badge>
|
||||
</LevelItem>
|
||||
|
|
|
@ -177,7 +177,7 @@ export const AssociatedRolesModal = (props: AssociatedRolesModalProps) => {
|
|||
actions={[
|
||||
<Button
|
||||
key="add"
|
||||
id="add-associated-roles-button"
|
||||
data-testid="add-associated-roles-button"
|
||||
variant="primary"
|
||||
isDisabled={!selectedRows?.length}
|
||||
onClick={() => {
|
||||
|
@ -206,7 +206,7 @@ export const AssociatedRolesModal = (props: AssociatedRolesModalProps) => {
|
|||
searchTypeComponent={
|
||||
<Dropdown
|
||||
onSelect={() => onFilterDropdownSelect(filterType)}
|
||||
data-cy="filter-type-dropdown"
|
||||
data-testid="filter-type-dropdown"
|
||||
toggle={
|
||||
<DropdownToggle
|
||||
id="toggle-id-9"
|
||||
|
@ -220,7 +220,7 @@ export const AssociatedRolesModal = (props: AssociatedRolesModalProps) => {
|
|||
isOpen={isFilterDropdownOpen}
|
||||
dropdownItems={[
|
||||
<DropdownItem
|
||||
data-cy="filter-type-dropdown-item"
|
||||
data-testid="filter-type-dropdown-item"
|
||||
key="filter-type"
|
||||
>
|
||||
{filterType == "roles"
|
||||
|
@ -231,7 +231,7 @@ export const AssociatedRolesModal = (props: AssociatedRolesModalProps) => {
|
|||
/>
|
||||
}
|
||||
canSelectAll
|
||||
// isPaginated
|
||||
isPaginated
|
||||
onSelect={(rows) => {
|
||||
setSelectedRows([...rows]);
|
||||
}}
|
||||
|
|
|
@ -184,13 +184,13 @@ export const AssociatedRolesTab = ({
|
|||
<KeycloakDataTable
|
||||
key={key}
|
||||
loader={loader}
|
||||
isPaginated
|
||||
ariaLabelKey="roles:roleList"
|
||||
searchPlaceholderKey="roles:searchFor"
|
||||
canSelectAll
|
||||
onSelect={(rows) => {
|
||||
setSelectedRows([...rows]);
|
||||
}}
|
||||
isPaginated
|
||||
toolbarItem={
|
||||
<>
|
||||
<Checkbox
|
||||
|
@ -204,7 +204,7 @@ export const AssociatedRolesTab = ({
|
|||
className="kc-add-role-button"
|
||||
key="add-role-button"
|
||||
onClick={() => toggleModal()}
|
||||
data-cy="add-role-button"
|
||||
data-testid="add-role-button"
|
||||
>
|
||||
{t("addRole")}
|
||||
</Button>
|
||||
|
|
|
@ -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 = () => {
|
|||
<ViewHeader
|
||||
titleKey={role?.name || t("createRole")}
|
||||
badge={additionalRoles.length > 0 ? t("composite") : ""}
|
||||
badgeId="composite-role-badge"
|
||||
badgeIsRead={true}
|
||||
subKey={id ? "" : "roles:roleCreateExplain"}
|
||||
actionsDropdownId="roles-actions-dropdown"
|
||||
|
@ -287,7 +287,7 @@ export const RealmRoleTabs = () => {
|
|||
</DropdownItem>,
|
||||
<DropdownItem
|
||||
key="toggle-modal"
|
||||
id="add-roles"
|
||||
data-testid="add-roles"
|
||||
component="button"
|
||||
onClick={() => toggleModal()}
|
||||
>
|
||||
|
@ -336,6 +336,12 @@ export const RealmRoleTabs = () => {
|
|||
reset={() => form.reset(role)}
|
||||
/>
|
||||
</Tab>
|
||||
<Tab
|
||||
eventKey="users-in-role"
|
||||
title={<TabTitleText>{t("usersInRole")}</TabTitleText>}
|
||||
>
|
||||
<UsersInRoleTab data-cy="users-in-role-tab" />
|
||||
</Tab>
|
||||
</KeycloakTabs>
|
||||
)}
|
||||
{!id && (
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
134
src/realm-roles/UsersInRoleTab.tsx
Normal file
134
src/realm-roles/UsersInRoleTab.tsx
Normal file
|
@ -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 (
|
||||
<>
|
||||
<PageSection data-testid="users-page" variant="light">
|
||||
<KeycloakDataTable
|
||||
isPaginated
|
||||
loader={loader}
|
||||
ariaLabelKey="roles:roleList"
|
||||
searchPlaceholderKey=""
|
||||
toolbarItem={
|
||||
enabled && (
|
||||
<Popover
|
||||
aria-label="Basic popover"
|
||||
position="bottom"
|
||||
bodyContent={
|
||||
<div>
|
||||
{t("roles:whoWillAppearPopoverText")}
|
||||
<Button
|
||||
className="kc-groups-link"
|
||||
variant="link"
|
||||
onClick={() => history.push(`/${realm}/groups`)}
|
||||
>
|
||||
{t("groups")}
|
||||
</Button>
|
||||
{t("or")}
|
||||
<Button
|
||||
className="kc-users-link"
|
||||
variant="link"
|
||||
onClick={() => history.push(`/${realm}/users`)}
|
||||
>
|
||||
{t("users")}.
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
footerContent={t("roles:whoWillAppearPopoverFooterText")}
|
||||
>
|
||||
<Button
|
||||
variant="link"
|
||||
className="kc-who-will-appear-button"
|
||||
key="who-will-appear-button"
|
||||
icon={<QuestionCircleIcon />}
|
||||
>
|
||||
{t("roles:whoWillAppearLinkText")}
|
||||
</Button>
|
||||
</Popover>
|
||||
)
|
||||
}
|
||||
emptyState={
|
||||
<ListEmptyState
|
||||
hasIcon={true}
|
||||
message={t("noDirectUsers")}
|
||||
instructions={
|
||||
<div>
|
||||
{t("noUsersEmptyStateDescription")}
|
||||
<Button
|
||||
className="kc-groups-link-empty-state"
|
||||
variant="link"
|
||||
onClick={() => history.push(`/${realm}/groups`)}
|
||||
>
|
||||
{t("groups")}
|
||||
</Button>
|
||||
{t("or")}
|
||||
<Button
|
||||
className="kc-users-link-empty-state"
|
||||
variant="link"
|
||||
onClick={() => history.push(`/${realm}/users`)}
|
||||
>
|
||||
{t("users")}
|
||||
</Button>
|
||||
{t("noUsersEmptyStateDescriptionContinued")}
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
}
|
||||
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()],
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</PageSection>
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue