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-24 14:07:49 +00:00
|
|
|
import {
|
|
|
|
AlertVariant,
|
|
|
|
Button,
|
|
|
|
Checkbox,
|
|
|
|
Dropdown,
|
|
|
|
DropdownItem,
|
|
|
|
KebabToggle,
|
|
|
|
ToolbarItem,
|
|
|
|
} from "@patternfly/react-core";
|
2023-05-03 13:51:02 +00:00
|
|
|
import { uniqBy } from "lodash-es";
|
|
|
|
import { useState } from "react";
|
|
|
|
import { useTranslation } from "react-i18next";
|
|
|
|
import { Link, useLocation } from "react-router-dom";
|
2021-03-16 12:37:57 +00:00
|
|
|
|
2023-05-03 13:51:02 +00:00
|
|
|
import { adminClient } from "../admin-client";
|
|
|
|
import { useAlerts } from "../components/alert/Alerts";
|
|
|
|
import { GroupPath } from "../components/group/GroupPath";
|
|
|
|
import { ListEmptyState } from "../components/list-empty-state/ListEmptyState";
|
2023-03-21 12:05:17 +00:00
|
|
|
import {
|
|
|
|
Action,
|
|
|
|
KeycloakDataTable,
|
|
|
|
} from "../components/table-toolbar/KeycloakDataTable";
|
2023-05-03 13:51:02 +00:00
|
|
|
import { useAccess } from "../context/access/Access";
|
2021-06-21 17:55:51 +00:00
|
|
|
import { useRealm } from "../context/realm-context/RealmContext";
|
2023-05-03 13:51:02 +00:00
|
|
|
import { toUser } from "../user/routes/User";
|
2021-03-16 12:37:57 +00:00
|
|
|
import { emptyFormatter } from "../util";
|
2021-03-24 14:07:49 +00:00
|
|
|
import { MemberModal } from "./MembersModal";
|
2023-05-03 13:51:02 +00:00
|
|
|
import { useSubGroups } from "./SubGroupsContext";
|
|
|
|
import { getLastId } from "./groupIdUtils";
|
2021-03-16 12:37:57 +00:00
|
|
|
|
|
|
|
type MembersOf = UserRepresentation & {
|
|
|
|
membership: GroupRepresentation[];
|
|
|
|
};
|
|
|
|
|
2023-03-21 10:36:20 +00:00
|
|
|
const MemberOfRenderer = (member: MembersOf) => {
|
|
|
|
return (
|
|
|
|
<>
|
|
|
|
{member.membership.map((group, index) => (
|
|
|
|
<>
|
|
|
|
<GroupPath key={group.id} group={group} />
|
|
|
|
{member.membership[index + 1] ? ", " : ""}
|
|
|
|
</>
|
|
|
|
))}
|
|
|
|
</>
|
|
|
|
);
|
|
|
|
};
|
|
|
|
|
|
|
|
const UserDetailLink = (user: MembersOf) => {
|
|
|
|
const { realm } = useRealm();
|
|
|
|
return (
|
|
|
|
<Link key={user.id} to={toUser({ realm, id: user.id!, tab: "settings" })}>
|
|
|
|
{user.username}
|
|
|
|
</Link>
|
|
|
|
);
|
|
|
|
};
|
|
|
|
|
2021-03-16 12:37:57 +00:00
|
|
|
export const Members = () => {
|
|
|
|
const { t } = useTranslation("groups");
|
2023-03-21 10:36:20 +00:00
|
|
|
|
2021-07-28 12:01:42 +00:00
|
|
|
const { addAlert, addError } = useAlerts();
|
2021-03-19 18:37:21 +00:00
|
|
|
const location = useLocation();
|
2021-03-16 12:37:57 +00:00
|
|
|
const id = getLastId(location.pathname);
|
|
|
|
const [includeSubGroup, setIncludeSubGroup] = useState(false);
|
2021-05-18 12:25:46 +00:00
|
|
|
const { currentGroup } = useSubGroups();
|
2021-03-24 14:07:49 +00:00
|
|
|
const [addMembers, setAddMembers] = useState(false);
|
|
|
|
const [isKebabOpen, setIsKebabOpen] = useState(false);
|
|
|
|
const [selectedRows, setSelectedRows] = useState<UserRepresentation[]>([]);
|
2022-05-09 10:44:37 +00:00
|
|
|
const { hasAccess } = useAccess();
|
|
|
|
|
|
|
|
const isManager =
|
|
|
|
hasAccess("manage-users") || currentGroup()!.access!.manageMembership;
|
2021-03-16 12:37:57 +00:00
|
|
|
|
|
|
|
const [key, setKey] = useState(0);
|
|
|
|
const refresh = () => setKey(new Date().getTime());
|
|
|
|
|
|
|
|
const getMembership = async (id: string) =>
|
|
|
|
await adminClient.users.listGroups({ id: id! });
|
|
|
|
|
|
|
|
const getSubGroups = (groups: GroupRepresentation[]) => {
|
|
|
|
let subGroups: GroupRepresentation[] = [];
|
|
|
|
for (const group of groups!) {
|
|
|
|
subGroups.push(group);
|
|
|
|
const subs = getSubGroups(group.subGroups!);
|
|
|
|
subGroups = subGroups.concat(subs);
|
|
|
|
}
|
|
|
|
return subGroups;
|
|
|
|
};
|
|
|
|
|
|
|
|
const loader = async (first?: number, max?: number) => {
|
|
|
|
let members = await adminClient.groups.listMembers({
|
|
|
|
id: id!,
|
|
|
|
first,
|
|
|
|
max,
|
|
|
|
});
|
|
|
|
|
|
|
|
if (includeSubGroup) {
|
2022-02-21 14:58:28 +00:00
|
|
|
const subGroups = getSubGroups(currentGroup()?.subGroups!);
|
2021-03-16 12:37:57 +00:00
|
|
|
for (const group of subGroups) {
|
|
|
|
members = members.concat(
|
2023-07-11 14:03:21 +00:00
|
|
|
await adminClient.groups.listMembers({ id: group.id! }),
|
2021-03-16 12:37:57 +00:00
|
|
|
);
|
|
|
|
}
|
2022-02-02 10:33:57 +00:00
|
|
|
members = uniqBy(members, (member) => member.username);
|
2021-03-16 12:37:57 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
const memberOfPromises = await Promise.all(
|
2023-07-11 14:03:21 +00:00
|
|
|
members.map((member) => getMembership(member.id!)),
|
2021-03-16 12:37:57 +00:00
|
|
|
);
|
|
|
|
return members.map((member: UserRepresentation, i) => {
|
|
|
|
return { ...member, membership: memberOfPromises[i] };
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
|
|
|
return (
|
2021-03-24 14:07:49 +00:00
|
|
|
<>
|
|
|
|
{addMembers && (
|
|
|
|
<MemberModal
|
|
|
|
groupId={id!}
|
|
|
|
onClose={() => {
|
|
|
|
setAddMembers(false);
|
|
|
|
refresh();
|
|
|
|
}}
|
|
|
|
/>
|
|
|
|
)}
|
|
|
|
<KeycloakDataTable
|
2021-12-07 12:56:25 +00:00
|
|
|
data-testid="members-table"
|
2021-05-18 12:25:46 +00:00
|
|
|
key={`${id}${key}${includeSubGroup}`}
|
2021-03-24 14:07:49 +00:00
|
|
|
loader={loader}
|
|
|
|
ariaLabelKey="groups:members"
|
|
|
|
isPaginated
|
|
|
|
canSelectAll
|
|
|
|
onSelect={(rows) => setSelectedRows([...rows])}
|
|
|
|
toolbarItem={
|
2022-05-09 10:44:37 +00:00
|
|
|
isManager && (
|
|
|
|
<>
|
|
|
|
<ToolbarItem>
|
|
|
|
<Button
|
|
|
|
data-testid="addMember"
|
|
|
|
variant="primary"
|
|
|
|
onClick={() => setAddMembers(true)}
|
|
|
|
>
|
|
|
|
{t("addMember")}
|
|
|
|
</Button>
|
|
|
|
</ToolbarItem>
|
|
|
|
<ToolbarItem>
|
|
|
|
<Checkbox
|
|
|
|
data-testid="includeSubGroupsCheck"
|
|
|
|
label={t("includeSubGroups")}
|
|
|
|
id="kc-include-sub-groups"
|
|
|
|
isChecked={includeSubGroup}
|
|
|
|
onChange={() => setIncludeSubGroup(!includeSubGroup)}
|
|
|
|
/>
|
|
|
|
</ToolbarItem>
|
|
|
|
<ToolbarItem>
|
|
|
|
<Dropdown
|
|
|
|
toggle={
|
|
|
|
<KebabToggle
|
|
|
|
onToggle={() => setIsKebabOpen(!isKebabOpen)}
|
|
|
|
isDisabled={selectedRows.length === 0}
|
|
|
|
/>
|
|
|
|
}
|
|
|
|
isOpen={isKebabOpen}
|
|
|
|
isPlain
|
|
|
|
dropdownItems={[
|
|
|
|
<DropdownItem
|
|
|
|
key="action"
|
|
|
|
component="button"
|
|
|
|
onClick={async () => {
|
|
|
|
try {
|
|
|
|
await Promise.all(
|
|
|
|
selectedRows.map((user) =>
|
|
|
|
adminClient.users.delFromGroup({
|
|
|
|
id: user.id!,
|
|
|
|
groupId: id!,
|
2023-07-11 14:03:21 +00:00
|
|
|
}),
|
|
|
|
),
|
2022-05-09 10:44:37 +00:00
|
|
|
);
|
|
|
|
setIsKebabOpen(false);
|
|
|
|
addAlert(
|
|
|
|
t("usersLeft", { count: selectedRows.length }),
|
2023-07-11 14:03:21 +00:00
|
|
|
AlertVariant.success,
|
2022-05-09 10:44:37 +00:00
|
|
|
);
|
|
|
|
} catch (error) {
|
|
|
|
addError("groups:usersLeftError", error);
|
|
|
|
}
|
2021-03-24 14:07:49 +00:00
|
|
|
|
2022-05-09 10:44:37 +00:00
|
|
|
refresh();
|
|
|
|
}}
|
|
|
|
>
|
|
|
|
{t("leave")}
|
|
|
|
</DropdownItem>,
|
|
|
|
]}
|
|
|
|
/>
|
|
|
|
</ToolbarItem>
|
|
|
|
</>
|
|
|
|
)
|
2021-03-24 14:07:49 +00:00
|
|
|
}
|
2022-05-09 10:44:37 +00:00
|
|
|
actions={
|
|
|
|
isManager
|
|
|
|
? [
|
|
|
|
{
|
|
|
|
title: t("leave"),
|
|
|
|
onRowClick: async (user) => {
|
|
|
|
try {
|
|
|
|
await adminClient.users.delFromGroup({
|
|
|
|
id: user.id!,
|
|
|
|
groupId: id!,
|
|
|
|
});
|
|
|
|
addAlert(
|
|
|
|
t("usersLeft", { count: 1 }),
|
2023-07-11 14:03:21 +00:00
|
|
|
AlertVariant.success,
|
2022-05-09 10:44:37 +00:00
|
|
|
);
|
|
|
|
} catch (error) {
|
|
|
|
addError("groups:usersLeftError", error);
|
|
|
|
}
|
2021-06-21 17:55:51 +00:00
|
|
|
|
2022-05-09 10:44:37 +00:00
|
|
|
return true;
|
|
|
|
},
|
2023-03-21 12:05:17 +00:00
|
|
|
} as Action<UserRepresentation>,
|
2022-05-09 10:44:37 +00:00
|
|
|
]
|
|
|
|
: []
|
|
|
|
}
|
2021-03-24 14:07:49 +00:00
|
|
|
columns={[
|
|
|
|
{
|
|
|
|
name: "username",
|
|
|
|
displayKey: "common:name",
|
2021-06-21 17:55:51 +00:00
|
|
|
cellRenderer: UserDetailLink,
|
2021-03-24 14:07:49 +00:00
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "email",
|
|
|
|
displayKey: "groups:email",
|
|
|
|
cellFormatters: [emptyFormatter()],
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "firstName",
|
|
|
|
displayKey: "groups:firstName",
|
|
|
|
cellFormatters: [emptyFormatter()],
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "lastName",
|
|
|
|
displayKey: "groups:lastName",
|
|
|
|
cellFormatters: [emptyFormatter()],
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "membership",
|
|
|
|
displayKey: "groups:membership",
|
|
|
|
cellRenderer: MemberOfRenderer,
|
|
|
|
},
|
|
|
|
]}
|
|
|
|
emptyState={
|
|
|
|
<ListEmptyState
|
|
|
|
message={t("users:noUsersFound")}
|
2022-05-09 10:44:37 +00:00
|
|
|
instructions={isManager ? t("users:emptyInstructions") : undefined}
|
|
|
|
primaryActionText={isManager ? t("addMember") : undefined}
|
2021-03-24 14:07:49 +00:00
|
|
|
onPrimaryAction={() => setAddMembers(true)}
|
2022-10-03 10:48:48 +00:00
|
|
|
secondaryActions={[
|
|
|
|
{
|
|
|
|
text: t("includeSubGroups"),
|
|
|
|
onClick: () => setIncludeSubGroup(true),
|
|
|
|
},
|
|
|
|
]}
|
2021-03-24 14:07:49 +00:00
|
|
|
/>
|
|
|
|
}
|
|
|
|
/>
|
|
|
|
</>
|
2021-03-16 12:37:57 +00:00
|
|
|
);
|
|
|
|
};
|