keycloak-scim/src/user/UserGroups.tsx

389 lines
11 KiB
TypeScript
Raw Normal View History

2021-04-22 20:37:10 +00:00
import React, { useContext, useEffect, useState } from "react";
import { useParams } from "react-router-dom";
import { useTranslation } from "react-i18next";
import {
AlertVariant,
Button,
ButtonVariant,
Checkbox,
PageSection,
2021-04-22 20:37:10 +00:00
Popover,
} from "@patternfly/react-core";
import { ListEmptyState } from "../components/list-empty-state/ListEmptyState";
import { KeycloakDataTable } from "../components/table-toolbar/KeycloakDataTable";
import { useAlerts } from "../components/alert/Alerts";
import { useConfirmDialog } from "../components/confirm-dialog/ConfirmDialog";
import { emptyFormatter } from "../util";
import { asyncStateFetch, useAdminClient } from "../context/auth/AdminClient";
import GroupRepresentation from "keycloak-admin/lib/defs/groupRepresentation";
import { cellWidth } from "@patternfly/react-table";
import { useErrorHandler } from "react-error-boundary";
import _ from "lodash";
import UserRepresentation from "keycloak-admin/lib/defs/userRepresentation";
import { JoinGroupDialog } from "./JoinGroupDialog";
2021-04-22 20:37:10 +00:00
import { HelpContext } from "../components/help-enabler/HelpHeader";
import { QuestionCircleIcon } from "@patternfly/react-icons";
type GroupTableData = GroupRepresentation & {
membersLength?: number;
};
export type UserFormProps = {
username?: string;
loader?: (
first?: number,
max?: number,
search?: string
) => Promise<UserRepresentation[]>;
addGroup?: (newGroup: GroupRepresentation) => void;
};
export const UserGroups = () => {
const { t } = useTranslation("roles");
const { addAlert } = useAlerts();
const [key, setKey] = useState(0);
const refresh = () => setKey(new Date().getTime());
const handleError = useErrorHandler();
const [selectedGroup, setSelectedGroup] = useState<GroupRepresentation>();
const [list, setList] = useState(false);
const [listGroups, setListGroups] = useState(true);
const [search, setSearch] = useState("");
const [username, setUsername] = useState("");
const [isDirectMembership, setDirectMembership] = useState(true);
const [directMembershipList, setDirectMembershipList] = useState<
GroupRepresentation[]
>([]);
const [open, setOpen] = useState(false);
2021-04-22 20:37:10 +00:00
const { enabled } = useContext(HelpContext);
const adminClient = useAdminClient();
const { id } = useParams<{ id: string }>();
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 user = await adminClient.users.findOne({ id });
setUsername(user.username!);
const searchParam = search || "";
if (searchParam) {
params.search = searchParam;
setSearch(searchParam);
}
if (!searchParam && !listGroups && !list) {
return [];
}
const joinedGroups = await adminClient.users.listGroups({ ...params, id });
const allCreatedGroups = await adminClient.groups.find();
const getAllPaths = joinedGroups.reduce(
(acc: string[], cur) => (cur.path && acc.push(cur.path), acc),
[]
);
const parentGroupNames: string[] = [];
const allGroupMembership: string[] = [];
const slicedGroups: string[] = [];
const rootLevelGroups: GroupRepresentation[] = [...allCreatedGroups];
let allPaths: GroupRepresentation[] = [];
const getAllSubgroupPaths = (
o: any,
f: any,
context: GroupRepresentation[]
): GroupRepresentation[] => {
f(o, context);
if (typeof o !== "object") return context;
if (Array.isArray(o))
return o.forEach((e) => getAllSubgroupPaths(e, f, context)), context;
for (const prop in o) getAllSubgroupPaths(o[prop], f, context);
return context;
};
const arr = getAllSubgroupPaths(
rootLevelGroups,
(x: GroupRepresentation, context: GroupRepresentation[][]) => {
if (x !== undefined && x.subGroups) context.push(x.subGroups);
},
[]
);
const allSubgroups: GroupRepresentation[] = [].concat(...(arr as any));
allPaths = [...rootLevelGroups, ...allSubgroups];
getAllPaths.forEach((item) => {
const paths = item.split("/");
const groups: string[] = [];
paths.reduce((acc, value) => {
const path = acc + "/" + value;
groups.push(path);
return path;
}, "");
for (let i = 1; i < groups.length; i++) {
slicedGroups.push(groups[i].substring(1));
}
});
allGroupMembership.push(...slicedGroups);
allPaths.forEach((item) => {
if (item.subGroups!.length !== 0) {
allPaths.push(...item!.subGroups!);
}
});
allPaths = allPaths.filter((group) =>
allGroupMembership.includes(group.path as any)
);
const topLevelGroups = allCreatedGroups.filter((value) =>
parentGroupNames.includes(value.name!)
);
const subgroupArray: any[] = [];
topLevelGroups.forEach((group) => subgroupArray.push(group.subGroups));
const directMembership = joinedGroups.filter(
(value) => !topLevelGroups.includes(value)
);
setDirectMembershipList(directMembership);
const filterDupesfromGroups = allPaths.filter(
(thing, index, self) =>
index === self.findIndex((t) => t.name === thing.name)
);
if (isDirectMembership) {
return alphabetize(directMembership);
}
return alphabetize(filterDupesfromGroups);
};
useEffect(() => {
return asyncStateFetch(
() => {
return Promise.resolve(adminClient.users.listGroups({ id }));
},
(response) => {
setListGroups(!!(response && response.length > 0));
},
handleError
);
}, []);
useEffect(() => {
refresh();
}, [isDirectMembership]);
const AliasRenderer = (group: GroupRepresentation) => {
return <>{group.name}</>;
};
const JoinGroupButtonRenderer = (group: GroupRepresentation) => {
return (
<>
<Button onClick={() => joinGroup(group)} variant="link">
{t("users:joinGroup")}
</Button>
</>
);
};
const toggleModal = () => {
setOpen(!open);
};
const joinGroup = (group: GroupRepresentation) => {
setSelectedGroup(group);
toggleModal();
};
const [toggleDeleteDialog, DeleteConfirm] = useConfirmDialog({
titleKey: t("users:leaveGroup", {
name: selectedGroup?.name,
}),
messageKey: t("users:leaveGroupConfirmDialog", {
groupname: selectedGroup?.name,
username: username,
}),
continueButtonLabel: "users:leave",
continueButtonVariant: ButtonVariant.danger,
onConfirm: async () => {
try {
await adminClient.users.delFromGroup({
id,
groupId: selectedGroup!.id!,
});
refresh();
addAlert(t("users:removedGroupMembership"), AlertVariant.success);
} catch (error) {
addAlert(
t("users:removedGroupMembershipError", { error }),
AlertVariant.danger
);
}
},
});
const leave = (group: GroupRepresentation) => {
setSelectedGroup(group);
toggleDeleteDialog();
};
const LeaveButtonRenderer = (group: GroupRepresentation) => {
if (
directMembershipList.some((item) => item.id === group.id) ||
directMembershipList.length === 0 ||
isDirectMembership
) {
return (
<>
<Button
data-testid={`leave-${group.name}`}
onClick={() => leave(group)}
variant="link"
>
{t("users:Leave")}
</Button>
</>
);
} else {
return <> </>;
}
};
const addGroups = async (groups: GroupRepresentation[]): Promise<void> => {
const newGroups = groups;
newGroups.forEach(async (group) => {
try {
await adminClient.users.addToGroup({
id: id,
groupId: group.id!,
});
setList(true);
refresh();
addAlert(t("users:addedGroupMembership"), AlertVariant.success);
} catch (error) {
addAlert(
t("users:addedGroupMembershipError", { error }),
AlertVariant.danger
);
}
});
};
return (
<>
<PageSection variant="light">
<DeleteConfirm />
{open && (
<JoinGroupDialog
open={open}
onClose={() => setOpen(!open)}
onConfirm={addGroups}
toggleDialog={() => toggleModal()}
username={username}
/>
)}
<KeycloakDataTable
key={key}
loader={loader}
isPaginated
ariaLabelKey="roles:roleList"
searchPlaceholderKey="groups:searchGroup"
canSelectAll
onSelect={() => {}}
toolbarItem={
<>
<Button
className="kc-join-group-button"
key="join-group-button"
onClick={toggleModal}
data-testid="add-group-button"
>
{t("users:joinGroup")}
</Button>
{JoinGroupButtonRenderer}
<Checkbox
label={t("users:directMembership")}
key="direct-membership-check"
id="kc-direct-membership-checkbox"
onChange={() => setDirectMembership(!isDirectMembership)}
isChecked={isDirectMembership}
/>
2021-04-22 20:37:10 +00:00
{enabled && (
<Popover
aria-label="Basic popover"
position="bottom"
bodyContent={<div>{t("users:whoWillAppearPopoverText")}</div>}
>
<Button
variant="link"
className="kc-who-will-appear-button"
key="who-will-appear-button"
icon={<QuestionCircleIcon />}
>
{t("users:whoWillAppearLinkText")}
</Button>
</Popover>
)}
</>
}
columns={[
{
name: "groupMembership",
displayKey: "users:groupMembership",
cellRenderer: AliasRenderer,
cellFormatters: [emptyFormatter()],
transforms: [cellWidth(40)],
},
{
name: "path",
displayKey: "users:Path",
cellFormatters: [emptyFormatter()],
transforms: [cellWidth(45)],
},
{
name: "",
cellRenderer: LeaveButtonRenderer,
cellFormatters: [emptyFormatter()],
transforms: [cellWidth(20)],
},
]}
emptyState={
!search ? (
<ListEmptyState
hasIcon={true}
message={t("users:noGroups")}
instructions={t("users:noGroupsText")}
/>
) : (
""
)
}
/>
</PageSection>
</>
);
};