2023-05-03 13:51:02 +00:00
|
|
|
import type GroupRepresentation from "@keycloak/keycloak-admin-client/lib/defs/groupRepresentation";
|
|
|
|
import type UserRepresentation from "@keycloak/keycloak-admin-client/lib/defs/userRepresentation";
|
2021-03-23 19:02:27 +00:00
|
|
|
import {
|
|
|
|
AlertVariant,
|
|
|
|
Button,
|
|
|
|
ButtonVariant,
|
|
|
|
Checkbox,
|
2021-04-22 20:37:10 +00:00
|
|
|
Popover,
|
2021-03-23 19:02:27 +00:00
|
|
|
} from "@patternfly/react-core";
|
2021-07-21 09:30:18 +00:00
|
|
|
import { QuestionCircleIcon } from "@patternfly/react-icons";
|
2021-03-23 19:02:27 +00:00
|
|
|
import { cellWidth } from "@patternfly/react-table";
|
2023-05-30 11:35:28 +00:00
|
|
|
import { intersectionBy, sortBy, uniqBy } from "lodash-es";
|
|
|
|
import { useState } from "react";
|
2021-07-21 09:30:18 +00:00
|
|
|
import { useTranslation } from "react-i18next";
|
2023-05-03 13:51:02 +00:00
|
|
|
import { useHelp } from "ui-shared";
|
|
|
|
|
|
|
|
import { adminClient } from "../admin-client";
|
2021-07-21 09:30:18 +00:00
|
|
|
import { useAlerts } from "../components/alert/Alerts";
|
|
|
|
import { useConfirmDialog } from "../components/confirm-dialog/ConfirmDialog";
|
2021-06-21 17:55:51 +00:00
|
|
|
import { GroupPath } from "../components/group/GroupPath";
|
2021-07-21 09:30:18 +00:00
|
|
|
import { GroupPickerDialog } from "../components/group/GroupPickerDialog";
|
|
|
|
import { ListEmptyState } from "../components/list-empty-state/ListEmptyState";
|
|
|
|
import { KeycloakDataTable } from "../components/table-toolbar/KeycloakDataTable";
|
2022-08-25 20:25:34 +00:00
|
|
|
import { useAccess } from "../context/access/Access";
|
2023-05-03 13:51:02 +00:00
|
|
|
import { emptyFormatter } from "../util";
|
2021-04-14 18:19:39 +00:00
|
|
|
|
2021-09-02 13:44:31 +00:00
|
|
|
type UserGroupsProps = {
|
|
|
|
user: UserRepresentation;
|
2021-04-14 18:19:39 +00:00
|
|
|
};
|
2021-03-23 19:02:27 +00:00
|
|
|
|
2021-09-02 13:44:31 +00:00
|
|
|
export const UserGroups = ({ user }: UserGroupsProps) => {
|
2023-09-08 13:17:17 +00:00
|
|
|
const { t } = useTranslation();
|
2021-07-28 12:01:42 +00:00
|
|
|
const { addAlert, addError } = useAlerts();
|
2021-03-23 19:02:27 +00:00
|
|
|
const [key, setKey] = useState(0);
|
2023-05-30 11:35:28 +00:00
|
|
|
const refresh = () => setKey(key + 1);
|
2021-03-23 19:02:27 +00:00
|
|
|
|
2021-11-03 13:45:37 +00:00
|
|
|
const [selectedGroups, setSelectedGroups] = useState<GroupRepresentation[]>(
|
2023-07-11 14:03:21 +00:00
|
|
|
[],
|
2021-11-03 13:45:37 +00:00
|
|
|
);
|
2021-03-23 19:02:27 +00:00
|
|
|
|
2021-04-01 18:47:05 +00:00
|
|
|
const [isDirectMembership, setDirectMembership] = useState(true);
|
2021-04-05 17:55:17 +00:00
|
|
|
const [directMembershipList, setDirectMembershipList] = useState<
|
|
|
|
GroupRepresentation[]
|
|
|
|
>([]);
|
2021-03-23 19:02:27 +00:00
|
|
|
const [open, setOpen] = useState(false);
|
|
|
|
|
2021-07-21 09:30:18 +00:00
|
|
|
const { enabled } = useHelp();
|
2021-04-22 20:37:10 +00:00
|
|
|
|
2022-08-25 20:25:34 +00:00
|
|
|
const { hasAccess } = useAccess();
|
|
|
|
const isManager = hasAccess("manage-users");
|
|
|
|
|
2021-04-01 18:47:05 +00:00
|
|
|
const alphabetize = (groupsList: GroupRepresentation[]) => {
|
2021-11-03 13:45:37 +00:00
|
|
|
return sortBy(groupsList, (group) => group.path?.toUpperCase());
|
2021-04-01 18:47:05 +00:00
|
|
|
};
|
|
|
|
|
2021-03-23 19:02:27 +00:00
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
2021-05-06 19:21:10 +00:00
|
|
|
const joinedUserGroups = await adminClient.users.listGroups({
|
|
|
|
...params,
|
2021-09-02 13:44:31 +00:00
|
|
|
id: user.id!,
|
2021-05-06 19:21:10 +00:00
|
|
|
});
|
2021-04-01 18:47:05 +00:00
|
|
|
|
2023-05-30 11:35:28 +00:00
|
|
|
setDirectMembershipList([...joinedUserGroups]);
|
|
|
|
|
|
|
|
const indirect: GroupRepresentation[] = [];
|
|
|
|
if (!isDirectMembership)
|
|
|
|
joinedUserGroups.forEach((g) => {
|
|
|
|
const paths = g.path?.substring(1).split("/").slice(0, -1) || [];
|
|
|
|
indirect.push(
|
|
|
|
...paths.map((p) => ({
|
|
|
|
name: p,
|
|
|
|
path: g.path?.substring(0, g.path.indexOf(p) + p.length),
|
2023-07-11 14:03:21 +00:00
|
|
|
})),
|
2023-05-30 11:35:28 +00:00
|
|
|
);
|
|
|
|
});
|
2021-04-01 18:47:05 +00:00
|
|
|
|
2023-05-30 11:35:28 +00:00
|
|
|
return alphabetize(uniqBy([...joinedUserGroups, ...indirect], "path"));
|
2021-03-23 19:02:27 +00:00
|
|
|
};
|
|
|
|
|
2021-04-14 18:19:39 +00:00
|
|
|
const toggleModal = () => {
|
|
|
|
setOpen(!open);
|
|
|
|
};
|
|
|
|
|
2021-03-23 19:02:27 +00:00
|
|
|
const [toggleDeleteDialog, DeleteConfirm] = useConfirmDialog({
|
2021-06-16 11:35:03 +00:00
|
|
|
titleKey: t("leaveGroup", {
|
2021-11-03 13:45:37 +00:00
|
|
|
count: selectedGroups.length,
|
|
|
|
name: selectedGroups[0]?.name,
|
2021-03-23 19:02:27 +00:00
|
|
|
}),
|
2021-06-16 11:35:03 +00:00
|
|
|
messageKey: t("leaveGroupConfirmDialog", {
|
2021-11-03 13:45:37 +00:00
|
|
|
count: selectedGroups.length,
|
|
|
|
groupname: selectedGroups[0]?.name,
|
2021-09-02 13:44:31 +00:00
|
|
|
username: user.username,
|
2021-03-23 19:02:27 +00:00
|
|
|
}),
|
2021-06-16 11:35:03 +00:00
|
|
|
continueButtonLabel: "leave",
|
2021-03-23 19:02:27 +00:00
|
|
|
continueButtonVariant: ButtonVariant.danger,
|
|
|
|
onConfirm: async () => {
|
|
|
|
try {
|
2021-11-03 13:45:37 +00:00
|
|
|
await Promise.all(
|
|
|
|
selectedGroups.map((group) =>
|
|
|
|
adminClient.users.delFromGroup({
|
|
|
|
id: user.id!,
|
|
|
|
groupId: group.id!,
|
2023-07-11 14:03:21 +00:00
|
|
|
}),
|
|
|
|
),
|
2021-11-03 13:45:37 +00:00
|
|
|
);
|
2023-05-30 11:35:28 +00:00
|
|
|
|
2021-06-16 11:35:03 +00:00
|
|
|
addAlert(t("removedGroupMembership"), AlertVariant.success);
|
2021-03-23 19:02:27 +00:00
|
|
|
} catch (error) {
|
2021-07-28 12:01:42 +00:00
|
|
|
addError("users:removedGroupMembershipError", error);
|
2021-03-23 19:02:27 +00:00
|
|
|
}
|
2023-05-30 11:35:28 +00:00
|
|
|
refresh();
|
2021-03-23 19:02:27 +00:00
|
|
|
},
|
|
|
|
});
|
|
|
|
|
2021-11-03 13:45:37 +00:00
|
|
|
const leave = (group: GroupRepresentation[]) => {
|
|
|
|
setSelectedGroups(group);
|
2021-03-23 19:02:27 +00:00
|
|
|
toggleDeleteDialog();
|
|
|
|
};
|
|
|
|
|
2021-04-19 19:53:28 +00:00
|
|
|
const addGroups = async (groups: GroupRepresentation[]): Promise<void> => {
|
2023-05-30 11:35:28 +00:00
|
|
|
try {
|
|
|
|
await Promise.all(
|
|
|
|
groups.map((group) =>
|
|
|
|
adminClient.users.addToGroup({
|
|
|
|
id: user.id!,
|
|
|
|
groupId: group.id!,
|
2023-07-11 14:03:21 +00:00
|
|
|
}),
|
|
|
|
),
|
2023-05-30 11:35:28 +00:00
|
|
|
);
|
|
|
|
|
|
|
|
addAlert(t("addedGroupMembership"), AlertVariant.success);
|
|
|
|
} catch (error) {
|
|
|
|
addError("users:addedGroupMembershipError", error);
|
|
|
|
}
|
|
|
|
refresh();
|
2021-04-14 18:19:39 +00:00
|
|
|
};
|
|
|
|
|
2021-03-23 19:02:27 +00:00
|
|
|
return (
|
|
|
|
<>
|
2021-06-23 10:42:14 +00:00
|
|
|
<DeleteConfirm />
|
|
|
|
{open && (
|
|
|
|
<GroupPickerDialog
|
2021-09-02 13:44:31 +00:00
|
|
|
id={user.id}
|
2021-06-23 10:42:14 +00:00
|
|
|
type="selectMany"
|
|
|
|
text={{
|
2021-09-02 13:44:31 +00:00
|
|
|
title: t("joinGroupsFor", { username: user.username }),
|
2021-06-23 10:42:14 +00:00
|
|
|
ok: "users:join",
|
|
|
|
}}
|
2022-08-25 20:25:34 +00:00
|
|
|
canBrowse={isManager}
|
2021-06-23 10:42:14 +00:00
|
|
|
onClose={() => setOpen(false)}
|
2023-05-30 11:35:28 +00:00
|
|
|
onConfirm={async (groups = []) => {
|
|
|
|
await addGroups(groups);
|
2021-06-23 10:42:14 +00:00
|
|
|
setOpen(false);
|
|
|
|
}}
|
|
|
|
/>
|
|
|
|
)}
|
|
|
|
<KeycloakDataTable
|
|
|
|
key={key}
|
|
|
|
loader={loader}
|
|
|
|
className="keycloak_user-section_groups-table"
|
|
|
|
isPaginated
|
|
|
|
ariaLabelKey="roles:roleList"
|
|
|
|
searchPlaceholderKey="groups:searchGroup"
|
|
|
|
canSelectAll
|
2021-11-03 13:45:37 +00:00
|
|
|
onSelect={(groups) =>
|
|
|
|
isDirectMembership
|
|
|
|
? setSelectedGroups(groups)
|
|
|
|
: setSelectedGroups(
|
2023-07-11 14:03:21 +00:00
|
|
|
intersectionBy(groups, directMembershipList, "id"),
|
2021-11-03 13:45:37 +00:00
|
|
|
)
|
|
|
|
}
|
|
|
|
isRowDisabled={(group) =>
|
|
|
|
!isDirectMembership &&
|
|
|
|
directMembershipList.every((item) => item.id !== group.id)
|
|
|
|
}
|
2021-06-23 10:42:14 +00:00
|
|
|
toolbarItem={
|
|
|
|
<>
|
|
|
|
<Button
|
|
|
|
className="kc-join-group-button"
|
|
|
|
onClick={toggleModal}
|
|
|
|
data-testid="add-group-button"
|
2022-04-21 15:03:48 +00:00
|
|
|
isDisabled={!user.access?.manageGroupMembership}
|
2021-06-23 10:42:14 +00:00
|
|
|
>
|
|
|
|
{t("joinGroup")}
|
|
|
|
</Button>
|
|
|
|
<Checkbox
|
|
|
|
label={t("directMembership")}
|
|
|
|
key="direct-membership-check"
|
|
|
|
id="kc-direct-membership-checkbox"
|
2023-05-30 11:35:28 +00:00
|
|
|
onChange={() => {
|
|
|
|
setDirectMembership(!isDirectMembership);
|
|
|
|
refresh();
|
|
|
|
}}
|
2021-06-23 10:42:14 +00:00
|
|
|
isChecked={isDirectMembership}
|
|
|
|
className="direct-membership-check"
|
|
|
|
/>
|
2021-11-03 13:45:37 +00:00
|
|
|
<Button
|
|
|
|
onClick={() => leave(selectedGroups)}
|
|
|
|
data-testid="leave-group-button"
|
|
|
|
variant="link"
|
|
|
|
isDisabled={selectedGroups.length === 0}
|
|
|
|
>
|
|
|
|
{t("leave")}
|
|
|
|
</Button>
|
|
|
|
|
2021-06-23 10:42:14 +00:00
|
|
|
{enabled && (
|
|
|
|
<Popover
|
|
|
|
aria-label="Basic popover"
|
|
|
|
position="bottom"
|
2023-09-08 13:17:17 +00:00
|
|
|
bodyContent={<div>{t("whoWillAppearPopoverTextUsers")}</div>}
|
2021-03-23 19:02:27 +00:00
|
|
|
>
|
2021-06-23 10:42:14 +00:00
|
|
|
<Button
|
|
|
|
variant="link"
|
|
|
|
className="kc-who-will-appear-button"
|
|
|
|
key="who-will-appear-button"
|
|
|
|
icon={<QuestionCircleIcon />}
|
2021-04-22 20:37:10 +00:00
|
|
|
>
|
2023-09-08 13:17:17 +00:00
|
|
|
{t("whoWillAppearLinkTextUsers")}
|
2021-06-23 10:42:14 +00:00
|
|
|
</Button>
|
|
|
|
</Popover>
|
|
|
|
)}
|
|
|
|
</>
|
|
|
|
}
|
|
|
|
columns={[
|
|
|
|
{
|
|
|
|
name: "groupMembership",
|
|
|
|
displayKey: "users:groupMembership",
|
2023-02-09 16:31:16 +00:00
|
|
|
cellRenderer: (group: GroupRepresentation) => group.name || "",
|
2021-06-23 10:42:14 +00:00
|
|
|
cellFormatters: [emptyFormatter()],
|
|
|
|
transforms: [cellWidth(40)],
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "path",
|
|
|
|
displayKey: "users:path",
|
2023-02-09 16:31:16 +00:00
|
|
|
cellRenderer: (group: GroupRepresentation) => (
|
|
|
|
<GroupPath group={group} />
|
|
|
|
),
|
2021-06-23 10:42:14 +00:00
|
|
|
transforms: [cellWidth(45)],
|
|
|
|
},
|
|
|
|
|
|
|
|
{
|
|
|
|
name: "",
|
2023-02-09 16:31:16 +00:00
|
|
|
cellRenderer: (group: GroupRepresentation) => {
|
|
|
|
const canLeaveGroup =
|
|
|
|
directMembershipList.some((item) => item.id === group.id) ||
|
|
|
|
directMembershipList.length === 0 ||
|
|
|
|
isDirectMembership;
|
|
|
|
return canLeaveGroup ? (
|
|
|
|
<Button
|
|
|
|
data-testid={`leave-${group.name}`}
|
|
|
|
onClick={() => leave([group])}
|
|
|
|
variant="link"
|
|
|
|
isDisabled={!user.access?.manageGroupMembership}
|
|
|
|
>
|
|
|
|
{t("leave")}
|
|
|
|
</Button>
|
|
|
|
) : (
|
|
|
|
""
|
|
|
|
);
|
|
|
|
},
|
2021-06-23 10:42:14 +00:00
|
|
|
cellFormatters: [emptyFormatter()],
|
|
|
|
transforms: [cellWidth(20)],
|
|
|
|
},
|
|
|
|
]}
|
|
|
|
emptyState={
|
2023-05-30 11:35:28 +00:00
|
|
|
<ListEmptyState
|
|
|
|
hasIcon
|
|
|
|
message={t("noGroups")}
|
|
|
|
instructions={t("noGroupsText")}
|
|
|
|
primaryActionText={t("joinGroup")}
|
|
|
|
onPrimaryAction={toggleModal}
|
|
|
|
/>
|
2021-06-23 10:42:14 +00:00
|
|
|
}
|
|
|
|
/>
|
2021-03-23 19:02:27 +00:00
|
|
|
</>
|
|
|
|
);
|
|
|
|
};
|