Added Memberships Modal (#33433)
* added MembershipsModal and fixed minor css issues Signed-off-by: Agnieszka Gancarczyk <agagancarczyk@gmail.com> * added test Signed-off-by: Agnieszka Gancarczyk <agagancarczyk@gmail.com> * improved test Signed-off-by: Agnieszka Gancarczyk <agagancarczyk@gmail.com> --------- Signed-off-by: Agnieszka Gancarczyk <agagancarczyk@gmail.com>
This commit is contained in:
parent
9681bce5fa
commit
09e3784f84
8 changed files with 209 additions and 9 deletions
|
@ -394,6 +394,16 @@ describe("Group test", () => {
|
|||
.assertNotificationUserLeftTheGroup(1)
|
||||
.assertNoUsersFoundEmptyStateMessageExist(true);
|
||||
});
|
||||
|
||||
it("Show memberships from item bar", () => {
|
||||
sidebarPage.goToGroups();
|
||||
groupPage.goToGroupChildGroupsTab(predefinedGroups[0]);
|
||||
childGroupsTab.goToMembersTab();
|
||||
membersTab
|
||||
.showGroupMembershipsItem(users[3].username)
|
||||
.assertGroupItemExist(predefinedGroups[0], true)
|
||||
.cancelShowGroupMembershipsModal();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Breadcrumbs", () => {
|
||||
|
|
|
@ -54,6 +54,17 @@ export default class MembersTab extends GroupDetailPage {
|
|||
return this;
|
||||
}
|
||||
|
||||
public showGroupMembershipsItem(username: string) {
|
||||
listingPage.clickRowDetails(username);
|
||||
listingPage.clickDetailMenu("Show memberships");
|
||||
return this;
|
||||
}
|
||||
|
||||
public cancelShowGroupMembershipsModal() {
|
||||
modalUtils.cancelModal();
|
||||
return this;
|
||||
}
|
||||
|
||||
public clickCheckboxIncludeSubGroupUsers() {
|
||||
cy.findByTestId(this.#includeSubGroupsCheck).click();
|
||||
return this;
|
||||
|
|
|
@ -3273,3 +3273,7 @@ groupDuplicated=Group duplicated
|
|||
duplicateAGroup=Duplicate group
|
||||
couldNotFetchClientRoleMappings=Could not fetch client role mappings\: {{error}}
|
||||
duplicateGroupWarning=Duplication of groups with a large number of subgroups is not supported. Please ensure that the group you are duplicating does not have a large number of subgroups.
|
||||
showMemberships=Show memberships
|
||||
showMembershipsTitle={{username}} Group Memberships
|
||||
noGroupMembershipsText=This user is not a member of any groups.
|
||||
noGroupMemberships=No memberships
|
||||
|
|
|
@ -32,6 +32,8 @@ import { emptyFormatter } from "../util";
|
|||
import { MemberModal } from "./MembersModal";
|
||||
import { useSubGroups } from "./SubGroupsContext";
|
||||
import { getLastId } from "./groupIdUtils";
|
||||
import { MembershipsModal } from "./MembershipsModal";
|
||||
import useToggle from "../utils/useToggle";
|
||||
|
||||
const UserDetailLink = (user: UserRepresentation) => {
|
||||
const { realm } = useRealm();
|
||||
|
@ -50,9 +52,7 @@ const UserDetailLink = (user: UserRepresentation) => {
|
|||
|
||||
export const Members = () => {
|
||||
const { adminClient } = useAdminClient();
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { addAlert, addError } = useAlerts();
|
||||
const location = useLocation();
|
||||
const id = getLastId(location.pathname);
|
||||
|
@ -62,6 +62,8 @@ export const Members = () => {
|
|||
const [addMembers, setAddMembers] = useState(false);
|
||||
const [isKebabOpen, setIsKebabOpen] = useState(false);
|
||||
const [selectedRows, setSelectedRows] = useState<UserRepresentation[]>([]);
|
||||
const [selectedUser, setSelectedUser] = useState<UserRepresentation>();
|
||||
const [showMemberships, toggleShowMemberships] = useToggle();
|
||||
const { hasAccess } = useAccess();
|
||||
|
||||
useFetch(
|
||||
|
@ -162,6 +164,14 @@ export const Members = () => {
|
|||
}}
|
||||
/>
|
||||
)}
|
||||
{showMemberships && (
|
||||
<MembershipsModal
|
||||
onClose={() => {
|
||||
toggleShowMemberships();
|
||||
}}
|
||||
user={selectedUser!}
|
||||
/>
|
||||
)}
|
||||
<KeycloakDataTable
|
||||
data-testid="members-table"
|
||||
key={`${id}${key}${includeSubGroup}`}
|
||||
|
@ -242,8 +252,8 @@ export const Members = () => {
|
|||
</>
|
||||
)
|
||||
}
|
||||
actions={
|
||||
isManager
|
||||
actions={[
|
||||
...(isManager
|
||||
? [
|
||||
{
|
||||
title: t("leave"),
|
||||
|
@ -257,13 +267,19 @@ export const Members = () => {
|
|||
} catch (error) {
|
||||
addError("usersLeftError", error);
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
} as Action<UserRepresentation>,
|
||||
]
|
||||
: []
|
||||
}
|
||||
: []),
|
||||
{
|
||||
title: t("showMemberships"),
|
||||
onRowClick: (user) => {
|
||||
setSelectedUser(user);
|
||||
toggleShowMemberships();
|
||||
},
|
||||
} as Action<UserRepresentation>,
|
||||
]}
|
||||
columns={[
|
||||
{
|
||||
name: "username",
|
||||
|
|
157
js/apps/admin-ui/src/groups/MembershipsModal.tsx
Normal file
157
js/apps/admin-ui/src/groups/MembershipsModal.tsx
Normal file
|
@ -0,0 +1,157 @@
|
|||
import type GroupRepresentation from "@keycloak/keycloak-admin-client/lib/defs/groupRepresentation";
|
||||
import UserRepresentation from "js/libs/keycloak-admin-client/lib/defs/userRepresentation";
|
||||
import { Modal, ModalVariant } from "@patternfly/react-core";
|
||||
import {
|
||||
Button,
|
||||
ButtonVariant,
|
||||
Checkbox,
|
||||
Popover,
|
||||
} from "@patternfly/react-core";
|
||||
import { QuestionCircleIcon } from "@patternfly/react-icons";
|
||||
import { cellWidth } from "@patternfly/react-table";
|
||||
import { useHelp } from "@keycloak/keycloak-ui-shared";
|
||||
import { ListEmptyState } from "@keycloak/keycloak-ui-shared";
|
||||
import { KeycloakDataTable } from "@keycloak/keycloak-ui-shared";
|
||||
import { sortBy, uniqBy } from "lodash-es";
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useAdminClient } from "../admin-client";
|
||||
import { GroupPath } from "../components/group/GroupPath";
|
||||
|
||||
type CredentialDataDialogProps = {
|
||||
user: UserRepresentation;
|
||||
onClose: () => void;
|
||||
};
|
||||
|
||||
export const MembershipsModal = ({
|
||||
user,
|
||||
onClose,
|
||||
}: CredentialDataDialogProps) => {
|
||||
const { t } = useTranslation();
|
||||
const { adminClient } = useAdminClient();
|
||||
const [key, setKey] = useState(0);
|
||||
const refresh = () => setKey(key + 1);
|
||||
const [isDirectMembership, setDirectMembership] = useState(true);
|
||||
const { enabled } = useHelp();
|
||||
const alphabetize = (groupsList: GroupRepresentation[]) => {
|
||||
return sortBy(groupsList, (group) => group.path?.toUpperCase());
|
||||
};
|
||||
|
||||
const loader = async (first?: number, max?: number, search?: string) => {
|
||||
const params: { [name: string]: string | number } = {
|
||||
first: first!,
|
||||
max: max!,
|
||||
};
|
||||
|
||||
const searchParam = search || "";
|
||||
if (searchParam) {
|
||||
params.search = searchParam;
|
||||
}
|
||||
|
||||
const joinedUserGroups = await adminClient.users.listGroups({
|
||||
...params,
|
||||
id: user.id!,
|
||||
});
|
||||
|
||||
const indirect: GroupRepresentation[] = [];
|
||||
if (!isDirectMembership)
|
||||
joinedUserGroups.forEach((g) => {
|
||||
const paths = (
|
||||
g.path?.substring(1).match(/((~\/)|[^/])+/g) || []
|
||||
).slice(0, -1);
|
||||
|
||||
indirect.push(
|
||||
...paths.map((p) => ({
|
||||
name: p,
|
||||
path: g.path?.substring(0, g.path.indexOf(p) + p.length),
|
||||
})),
|
||||
);
|
||||
});
|
||||
|
||||
return alphabetize(uniqBy([...joinedUserGroups, ...indirect], "path"));
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
variant={ModalVariant.large}
|
||||
title={t("showMembershipsTitle", { username: user.username })}
|
||||
data-testid="showMembershipsDialog"
|
||||
isOpen
|
||||
onClose={onClose}
|
||||
actions={[
|
||||
<Button
|
||||
id="modal-cancel"
|
||||
data-testid="cancel"
|
||||
key="cancel"
|
||||
variant={ButtonVariant.primary}
|
||||
onClick={onClose}
|
||||
>
|
||||
{t("cancel")}
|
||||
</Button>,
|
||||
]}
|
||||
>
|
||||
<KeycloakDataTable
|
||||
key={key}
|
||||
loader={loader}
|
||||
className="keycloak_user-section_groups-table"
|
||||
isPaginated
|
||||
ariaLabelKey="roleList"
|
||||
searchPlaceholderKey="searchGroup"
|
||||
toolbarItem={
|
||||
<>
|
||||
<Checkbox
|
||||
label={t("directMembership")}
|
||||
key="direct-membership-check"
|
||||
id="kc-direct-membership-checkbox"
|
||||
onChange={() => {
|
||||
setDirectMembership(!isDirectMembership);
|
||||
refresh();
|
||||
}}
|
||||
isChecked={isDirectMembership}
|
||||
className="pf-v5-u-mt-sm"
|
||||
/>
|
||||
{enabled && (
|
||||
<Popover
|
||||
aria-label="Basic popover"
|
||||
position="bottom"
|
||||
bodyContent={<div>{t("whoWillAppearPopoverTextUsers")}</div>}
|
||||
>
|
||||
<Button
|
||||
variant="link"
|
||||
className="kc-who-will-appear-button"
|
||||
key="who-will-appear-button"
|
||||
icon={<QuestionCircleIcon />}
|
||||
>
|
||||
{t("whoWillAppearLinkTextUsers")}
|
||||
</Button>
|
||||
</Popover>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
columns={[
|
||||
{
|
||||
name: "groupMembership",
|
||||
displayKey: "groupMembership",
|
||||
cellRenderer: (group: GroupRepresentation) => group.name || "-",
|
||||
transforms: [cellWidth(40)],
|
||||
},
|
||||
{
|
||||
name: "path",
|
||||
displayKey: "path",
|
||||
cellRenderer: (group: GroupRepresentation) => (
|
||||
<GroupPath group={group} />
|
||||
),
|
||||
transforms: [cellWidth(45)],
|
||||
},
|
||||
]}
|
||||
emptyState={
|
||||
<ListEmptyState
|
||||
hasIcon
|
||||
message={t("noGroupMemberships")}
|
||||
instructions={t("noGroupMembershipsText")}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
};
|
|
@ -363,6 +363,7 @@ export const GroupTree = ({
|
|||
name="exact"
|
||||
isChecked={exact}
|
||||
onChange={(_event, value) => setExact(value)}
|
||||
className="pf-v5-u-mr-xs"
|
||||
/>
|
||||
</InputGroupItem>
|
||||
<InputGroupItem>
|
||||
|
|
|
@ -205,13 +205,14 @@ export const UserGroups = ({ user }: UserGroupsProps) => {
|
|||
refresh();
|
||||
}}
|
||||
isChecked={isDirectMembership}
|
||||
className="direct-membership-check"
|
||||
className="pf-v5-u-mt-sm"
|
||||
/>
|
||||
<Button
|
||||
onClick={() => leave(selectedGroups)}
|
||||
data-testid="leave-group-button"
|
||||
variant="link"
|
||||
isDisabled={selectedGroups.length === 0}
|
||||
className="pf-v5-u-ml-md"
|
||||
>
|
||||
{t("leave")}
|
||||
</Button>
|
||||
|
|
|
@ -26,7 +26,7 @@ button#kc-join-groups-button {
|
|||
}
|
||||
|
||||
.kc-who-will-appear-button {
|
||||
margin-left: var(--pf-v5-global--spacer--md);
|
||||
margin-left: var(--pf-v5-global--spacer--sm);
|
||||
}
|
||||
|
||||
.pf-v5-c-chip.kc-consents-chip::before {
|
||||
|
|
Loading…
Reference in a new issue