Merge pull request #401 from jenny-s51/usersInRole

Realm roles: add "users in role" tab
This commit is contained in:
mfrances17 2021-03-03 16:24:53 -05:00 committed by GitHub
commit 9c1efdcc3b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 273 additions and 44 deletions

View file

@ -4,6 +4,7 @@ import ModalUtils from "../support/util/ModalUtils";
import ListingPage from "../support/pages/admin_console/ListingPage"; import ListingPage from "../support/pages/admin_console/ListingPage";
import SidebarPage from "../support/pages/admin_console/SidebarPage"; import SidebarPage from "../support/pages/admin_console/SidebarPage";
import CreateRealmRolePage from "../support/pages/admin_console/manage/realm_roles/CreateRealmRolePage"; 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"; let itemId = "realm_role_crud";
const loginPage = new LoginPage(); const loginPage = new LoginPage();
@ -12,6 +13,7 @@ const modalUtils = new ModalUtils();
const sidebarPage = new SidebarPage(); const sidebarPage = new SidebarPage();
const listingPage = new ListingPage(); const listingPage = new ListingPage();
const createRealmRolePage = new CreateRealmRolePage(); const createRealmRolePage = new CreateRealmRolePage();
const associatedRolesPage = new AssociatedRolesPage();
describe("Realm roles test", function () { describe("Realm roles test", function () {
describe("Realm roles creation", function () { describe("Realm roles creation", function () {
@ -48,7 +50,7 @@ describe("Realm roles test", function () {
listingPage.searchItem(itemId).itemExist(itemId); listingPage.searchItem(itemId).itemExist(itemId);
// Update cy.wait(100);
// Delete // Delete
listingPage.deleteItem(itemId); listingPage.deleteItem(itemId);
@ -70,40 +72,15 @@ describe("Realm roles test", function () {
masthead.checkNotificationMessage("Role created"); masthead.checkNotificationMessage("Role created");
cy.wait(100);
// Add associated realm role // Add associated realm role
cy.get("#roles-actions-dropdown").last().click();
cy.get("#add-roles").click(); associatedRolesPage.addAssociatedRealmRole();
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);
// Add associated client role // Add associated client role
cy.get('[data-cy=add-role-button]').click(); associatedRolesPage.addAssociatedClientRole();
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);
}); });
}); });
}); });

View file

@ -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");
}
}

View file

@ -9,7 +9,7 @@ import {
Switch, Switch,
TextContent, TextContent,
} from "@patternfly/react-core"; } from "@patternfly/react-core";
import { Trans, useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { HelpIcon, ExternalLinkAltIcon } from "@patternfly/react-icons"; import { HelpIcon, ExternalLinkAltIcon } from "@patternfly/react-icons";
import "./help-header.css"; import "./help-header.css";

View file

@ -19,7 +19,7 @@ export type Action = {
export type ListEmptyStateProps = { export type ListEmptyStateProps = {
message: string; message: string;
instructions: string; instructions: React.ReactNode;
primaryActionText?: string; primaryActionText?: string;
onPrimaryAction?: MouseEventHandler<HTMLButtonElement>; onPrimaryAction?: MouseEventHandler<HTMLButtonElement>;
hasIcon?: boolean; hasIcon?: boolean;

View file

@ -41,7 +41,6 @@ export const ViewHeader = ({
actionsDropdownId, actionsDropdownId,
titleKey, titleKey,
badge, badge,
badgeId,
badgeIsRead, badgeIsRead,
subKey, subKey,
subKeyLinkProps, subKeyLinkProps,
@ -77,7 +76,10 @@ export const ViewHeader = ({
</LevelItem> </LevelItem>
{badge && ( {badge && (
<LevelItem> <LevelItem>
<Badge id={badgeId} isRead={badgeIsRead}> <Badge
data-testid="composite-role-badge"
isRead={badgeIsRead}
>
{badge} {badge}
</Badge> </Badge>
</LevelItem> </LevelItem>

View file

@ -177,7 +177,7 @@ export const AssociatedRolesModal = (props: AssociatedRolesModalProps) => {
actions={[ actions={[
<Button <Button
key="add" key="add"
id="add-associated-roles-button" data-testid="add-associated-roles-button"
variant="primary" variant="primary"
isDisabled={!selectedRows?.length} isDisabled={!selectedRows?.length}
onClick={() => { onClick={() => {
@ -206,7 +206,7 @@ export const AssociatedRolesModal = (props: AssociatedRolesModalProps) => {
searchTypeComponent={ searchTypeComponent={
<Dropdown <Dropdown
onSelect={() => onFilterDropdownSelect(filterType)} onSelect={() => onFilterDropdownSelect(filterType)}
data-cy="filter-type-dropdown" data-testid="filter-type-dropdown"
toggle={ toggle={
<DropdownToggle <DropdownToggle
id="toggle-id-9" id="toggle-id-9"
@ -220,7 +220,7 @@ export const AssociatedRolesModal = (props: AssociatedRolesModalProps) => {
isOpen={isFilterDropdownOpen} isOpen={isFilterDropdownOpen}
dropdownItems={[ dropdownItems={[
<DropdownItem <DropdownItem
data-cy="filter-type-dropdown-item" data-testid="filter-type-dropdown-item"
key="filter-type" key="filter-type"
> >
{filterType == "roles" {filterType == "roles"
@ -231,7 +231,7 @@ export const AssociatedRolesModal = (props: AssociatedRolesModalProps) => {
/> />
} }
canSelectAll canSelectAll
// isPaginated isPaginated
onSelect={(rows) => { onSelect={(rows) => {
setSelectedRows([...rows]); setSelectedRows([...rows]);
}} }}

View file

@ -184,13 +184,13 @@ export const AssociatedRolesTab = ({
<KeycloakDataTable <KeycloakDataTable
key={key} key={key}
loader={loader} loader={loader}
isPaginated
ariaLabelKey="roles:roleList" ariaLabelKey="roles:roleList"
searchPlaceholderKey="roles:searchFor" searchPlaceholderKey="roles:searchFor"
canSelectAll canSelectAll
onSelect={(rows) => { onSelect={(rows) => {
setSelectedRows([...rows]); setSelectedRows([...rows]);
}} }}
isPaginated
toolbarItem={ toolbarItem={
<> <>
<Checkbox <Checkbox
@ -204,7 +204,7 @@ export const AssociatedRolesTab = ({
className="kc-add-role-button" className="kc-add-role-button"
key="add-role-button" key="add-role-button"
onClick={() => toggleModal()} onClick={() => toggleModal()}
data-cy="add-role-button" data-testid="add-role-button"
> >
{t("addRole")} {t("addRole")}
</Button> </Button>

View file

@ -23,6 +23,7 @@ import { useRealm } from "../context/realm-context/RealmContext";
import { AssociatedRolesModal } from "./AssociatedRolesModal"; import { AssociatedRolesModal } from "./AssociatedRolesModal";
import { KeycloakTabs } from "../components/keycloak-tabs/KeycloakTabs"; import { KeycloakTabs } from "../components/keycloak-tabs/KeycloakTabs";
import { AssociatedRolesTab } from "./AssociatedRolesTab"; import { AssociatedRolesTab } from "./AssociatedRolesTab";
import { UsersInRoleTab } from "./UsersInRoleTab";
const arrayToAttributes = (attributeArray: KeyValueType[]) => { const arrayToAttributes = (attributeArray: KeyValueType[]) => {
const initValue: { [index: string]: string[] } = {}; const initValue: { [index: string]: string[] } = {};
@ -254,7 +255,6 @@ export const RealmRoleTabs = () => {
<ViewHeader <ViewHeader
titleKey={role?.name || t("createRole")} titleKey={role?.name || t("createRole")}
badge={additionalRoles.length > 0 ? t("composite") : ""} badge={additionalRoles.length > 0 ? t("composite") : ""}
badgeId="composite-role-badge"
badgeIsRead={true} badgeIsRead={true}
subKey={id ? "" : "roles:roleCreateExplain"} subKey={id ? "" : "roles:roleCreateExplain"}
actionsDropdownId="roles-actions-dropdown" actionsDropdownId="roles-actions-dropdown"
@ -287,7 +287,7 @@ export const RealmRoleTabs = () => {
</DropdownItem>, </DropdownItem>,
<DropdownItem <DropdownItem
key="toggle-modal" key="toggle-modal"
id="add-roles" data-testid="add-roles"
component="button" component="button"
onClick={() => toggleModal()} onClick={() => toggleModal()}
> >
@ -336,6 +336,12 @@ export const RealmRoleTabs = () => {
reset={() => form.reset(role)} reset={() => form.reset(role)}
/> />
</Tab> </Tab>
<Tab
eventKey="users-in-role"
title={<TabTitleText>{t("usersInRole")}</TabTitleText>}
>
<UsersInRoleTab data-cy="users-in-role-tab" />
</Tab>
</KeycloakTabs> </KeycloakTabs>
)} )}
{!id && ( {!id && (

View file

@ -24,5 +24,33 @@
--pf-c-form__group--m-action--MarginTop: calc( --pf-c-form__group--m-action--MarginTop: calc(
var(--pf-global--spacer--2xl) - var(--pf-global--spacer--sm) 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);
}

View 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>
</>
);
};

View file

@ -48,6 +48,24 @@
"roleRemoveAssociatedText": "This action will remove {{role}} from {{roleName}. All the associated roles of {{role}} will also be removed.", "roleRemoveAssociatedText": "This action will remove {{role}} from {{roleName}. All the associated roles of {{role}} will also be removed.",
"compositeRoleOff": "Composite role turned off", "compositeRoleOff": "Composite role turned off",
"associatedRolesRemoved": "Associated roles have been removed", "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"
} }
} }