add ability to leave multiple groups (#1431)

* add ability to leave multiple groups

fixes: #496

* With v21.0.0 a new JSON format v4 was introduced

* Update src/user/UserGroups.tsx

Co-authored-by: Jon Koops <jonkoops@gmail.com>

* Update src/user/UserGroups.tsx

Co-authored-by: Jon Koops <jonkoops@gmail.com>

* filter selected groups based on direct membership

* fixed test

* remove all the groups

Co-authored-by: Jon Koops <jonkoops@gmail.com>
This commit is contained in:
Erik Jan de Wit 2021-11-03 14:45:37 +01:00 committed by GitHub
parent d1e45dabde
commit 405d3310f3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 73 additions and 34 deletions

View file

@ -62,6 +62,9 @@ describe("Users test", () => {
beforeEach(() => {
keycloakBefore();
loginPage.logIn();
cy.intercept(
"/auth/admin/realms/master/components?type=org.keycloak.storage.UserStorageProvider"
).as("brute-force");
sidebarPage.goToUsers();
});
@ -90,8 +93,6 @@ describe("Users test", () => {
const groupsListCopy = groupsList.slice(0, 1);
console.log(groupsList);
groupsListCopy.forEach((element) => {
cy.findByTestId(`${element}-check`).click();
});
@ -106,6 +107,7 @@ describe("Users test", () => {
});
it("User details test", () => {
cy.wait("@brute-force");
listingPage.searchItem(itemId).itemExist(itemId);
listingPage.goToItemDetails(itemId);
@ -115,10 +117,12 @@ describe("Users test", () => {
masthead.checkNotificationMessage("The user has been saved");
sidebarPage.goToUsers();
cy.wait("@brute-force");
listingPage.searchItem(itemId).itemExist(itemId);
});
it("User attributes test", () => {
cy.wait("@brute-force");
listingPage.searchItem(itemId).itemExist(itemId);
listingPage.goToItemDetails(itemId);
@ -132,6 +136,7 @@ describe("Users test", () => {
});
it("User attributes with multiple values test", () => {
cy.wait("@brute-force");
listingPage.searchItem(itemId).itemExist(itemId);
listingPage.goToItemDetails(itemId);
@ -159,6 +164,7 @@ describe("Users test", () => {
});
it("Add user to groups test", () => {
cy.wait("@brute-force");
// Go to user groups
listingPage.searchItem(itemId).itemExist(itemId);
listingPage.goToItemDetails(itemId);
@ -176,15 +182,17 @@ describe("Users test", () => {
});
it("Leave group test", () => {
cy.wait("@brute-force");
listingPage.searchItem(itemId).itemExist(itemId);
listingPage.goToItemDetails(itemId);
// Go to user groups
userGroupsPage.goToGroupsTab();
cy.contains("Leave").click();
cy.findByTestId(`leave-${groupsList[0]}`).click();
cy.findByTestId("modalConfirm").click();
});
it("Go to user consents test", () => {
cy.wait("@brute-force");
listingPage.searchItem(itemId).itemExist(itemId);
listingPage.goToItemDetails(itemId);

View file

@ -11,8 +11,8 @@ export default {
assignedType: "Assigned type",
displayOrder: "Display order",
type: "Type",
deleteClientScope: "Delete client scope {{name}}",
deleteClientScope_plural: "Delete {{count}} client scopes",
deleteClientScope_one: "Delete client scope {{name}}",
deleteClientScope_other: "Delete {{count}} client scopes",
deleteConfirm: "Are you sure you want to delete this client scope",
changeTypeTo: "Change type to",
changeTypeIntro: "{{count}} selected client scopes will be changed to",

View file

@ -38,8 +38,8 @@ export default {
assignRole: "Assign role",
unAssignRole: "Unassign",
removeMappingTitle: "Remove mapping?",
removeMappingConfirm: "Are you sure you want to remove this mapping?",
removeMappingConfirm_plural:
removeMappingConfirm_one: "Are you sure you want to remove this mapping?",
removeMappingConfirm_other:
"Are you sure you want to remove {{count}} mappings",
clientScopeSearch: {
name: "Name",

View file

@ -10,11 +10,11 @@ export default {
renameGroup: "Rename group",
deleteGroup: "Delete group",
leave: "Leave group",
usersLeft: "{{count}} user left the group",
usersLeft_plural: "{{count}} users left the group",
usersLeft_one: "{{count}} user left the group",
usersLeft_other: "{{count}} users left the group",
usersLeftError: "Could not remove users from the group: {{error}}",
usersAdded: "{{count}} user added to the group",
usersAdded_plural: "{{count}} users added to the group",
usersAdded_one: "{{count}} user added to the group",
usersAdded_other: "{{count}} users added to the group",
usersAddedError: "Could not add users to the group: {{error}}",
search: "Search",
members: "Members",
@ -52,12 +52,12 @@ export default {
noGroupsInThisSubGroup: "No groups in this sub group",
noGroupsInThisSubGroupInstructions:
"You haven't created any groups in this sub group.",
deleteConfirmTitle: "Delete group?",
deleteConfirmTitle_plural: "Delete groups?",
deleteConfirm: "Are you sure you want to delete this group",
deleteConfirm_plural: "Are you sure you want to delete this groups.",
groupDeleted: "Group deleted",
groupDeleted_plural: "Groups deleted",
deleteConfirmTitle_one: "Delete group?",
deleteConfirmTitle_other: "Delete groups?",
deleteConfirm_one: "Are you sure you want to delete this group",
deleteConfirm_other: "Are you sure you want to delete this groups.",
groupDeleted_one: "Group deleted",
groupDeleted_other: "Groups deleted",
groupDeleteError: "Error deleting group {error}",
attributes: "Attributes",
groupUpdated: "Group updated",

View file

@ -9,7 +9,7 @@ import { QuestionCircleIcon } from "@patternfly/react-icons";
import { cellWidth } from "@patternfly/react-table";
import type GroupRepresentation from "@keycloak/keycloak-admin-client/lib/defs/groupRepresentation";
import type UserRepresentation from "@keycloak/keycloak-admin-client/lib/defs/userRepresentation";
import _ from "lodash";
import { intersectionBy, sortBy } from "lodash";
import React, { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { useAlerts } from "../components/alert/Alerts";
@ -32,7 +32,9 @@ export const UserGroups = ({ user }: UserGroupsProps) => {
const [key, setKey] = useState(0);
const refresh = () => setKey(new Date().getTime());
const [selectedGroup, setSelectedGroup] = useState<GroupRepresentation>();
const [selectedGroups, setSelectedGroups] = useState<GroupRepresentation[]>(
[]
);
const [search, setSearch] = useState("");
const [isDirectMembership, setDirectMembership] = useState(true);
@ -45,7 +47,7 @@ export const UserGroups = ({ user }: UserGroupsProps) => {
const adminClient = useAdminClient();
const alphabetize = (groupsList: GroupRepresentation[]) => {
return _.sortBy(groupsList, (group) => group.path?.toUpperCase());
return sortBy(groupsList, (group) => group.path?.toUpperCase());
};
const loader = async (first?: number, max?: number, search?: string) => {
@ -170,20 +172,26 @@ export const UserGroups = ({ user }: UserGroupsProps) => {
const [toggleDeleteDialog, DeleteConfirm] = useConfirmDialog({
titleKey: t("leaveGroup", {
name: selectedGroup?.name,
count: selectedGroups.length,
name: selectedGroups[0]?.name,
}),
messageKey: t("leaveGroupConfirmDialog", {
groupname: selectedGroup?.name,
count: selectedGroups.length,
groupname: selectedGroups[0]?.name,
username: user.username,
}),
continueButtonLabel: "leave",
continueButtonVariant: ButtonVariant.danger,
onConfirm: async () => {
try {
await adminClient.users.delFromGroup({
id: user.id!,
groupId: selectedGroup!.id!,
});
await Promise.all(
selectedGroups.map((group) =>
adminClient.users.delFromGroup({
id: user.id!,
groupId: group.id!,
})
)
);
refresh();
addAlert(t("removedGroupMembership"), AlertVariant.success);
} catch (error) {
@ -192,8 +200,8 @@ export const UserGroups = ({ user }: UserGroupsProps) => {
},
});
const leave = (group: GroupRepresentation) => {
setSelectedGroup(group);
const leave = (group: GroupRepresentation[]) => {
setSelectedGroups(group);
toggleDeleteDialog();
};
@ -206,7 +214,7 @@ export const UserGroups = ({ user }: UserGroupsProps) => {
canLeaveGroup && (
<Button
data-testid={`leave-${group.name}`}
onClick={() => leave(group)}
onClick={() => leave([group])}
variant="link"
>
{t("leave")}
@ -249,6 +257,7 @@ export const UserGroups = ({ user }: UserGroupsProps) => {
onConfirm={(groups) => {
addGroups(groups);
setOpen(false);
refresh();
}}
/>
)}
@ -260,11 +269,21 @@ export const UserGroups = ({ user }: UserGroupsProps) => {
ariaLabelKey="roles:roleList"
searchPlaceholderKey="groups:searchGroup"
canSelectAll
onSelect={(groups) =>
isDirectMembership
? setSelectedGroups(groups)
: setSelectedGroups(
intersectionBy(groups, directMembershipList, "id")
)
}
isRowDisabled={(group) =>
!isDirectMembership &&
directMembershipList.every((item) => item.id !== group.id)
}
toolbarItem={
<>
<Button
className="kc-join-group-button"
key="join-group-button"
onClick={toggleModal}
data-testid="add-group-button"
>
@ -278,6 +297,15 @@ export const UserGroups = ({ user }: UserGroupsProps) => {
isChecked={isDirectMembership}
className="direct-membership-check"
/>
<Button
onClick={() => leave(selectedGroups)}
data-testid="leave-group-button"
variant="link"
isDisabled={selectedGroups.length === 0}
>
{t("leave")}
</Button>
{enabled && (
<Popover
aria-label="Basic popover"

View file

@ -19,9 +19,12 @@ export default {
selectGroups: "Select groups to join",
searchForGroups: "Search for groups",
leave: "Leave",
leaveGroup: "Leave group {{name}}?",
leaveGroupConfirmDialog:
leaveGroup_one: "Leave group {{name}}?",
leaveGroup_other: "Leave groups?",
leaveGroupConfirmDialog_one:
"Are you sure you want to remove {{username}} from the group {{groupname}}?",
leaveGroupConfirmDialog_other:
"Are you sure you want to remove {{username}} from the {{count}} selected groups?",
directMembership: "Direct membership",
groupMembership: "Group membership",
addedGroupMembership: "Added group membership",
@ -56,9 +59,9 @@ export default {
deleteConfirm: "Delete user?",
deleteConfirmCurrentUser:
"Are you sure you want to permanently delete this user",
deleteConfirmDialog:
deleteConfirmDialog_one:
"Are you sure you want to permanently delete {{count}} selected user",
deleteConfirmDialog_plural:
deleteConfirmDialog_other:
"Are you sure you want to permanently delete {{count}} selected users",
userID: "User ID",
userCreated: "The user has been created",