2021-03-23 19:02:27 +00:00
|
|
|
import React, { useEffect, useState } from "react";
|
|
|
|
import { useParams } from "react-router-dom";
|
|
|
|
import { useTranslation } from "react-i18next";
|
|
|
|
import {
|
|
|
|
AlertVariant,
|
|
|
|
Button,
|
|
|
|
ButtonVariant,
|
|
|
|
Checkbox,
|
|
|
|
PageSection,
|
|
|
|
} 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";
|
2021-04-01 18:47:05 +00:00
|
|
|
import _ from "lodash";
|
2021-04-14 18:19:39 +00:00
|
|
|
import UserRepresentation from "keycloak-admin/lib/defs/userRepresentation";
|
|
|
|
import { JoinGroupDialog } from "./JoinGroupDialog";
|
|
|
|
|
|
|
|
type GroupTableData = GroupRepresentation & {
|
|
|
|
membersLength?: number;
|
|
|
|
};
|
|
|
|
|
|
|
|
export type UserFormProps = {
|
|
|
|
username?: string;
|
|
|
|
loader?: (
|
|
|
|
first?: number,
|
|
|
|
max?: number,
|
|
|
|
search?: string
|
|
|
|
) => Promise<UserRepresentation[]>;
|
|
|
|
addGroup?: (newGroup: GroupRepresentation) => void;
|
|
|
|
};
|
2021-03-23 19:02:27 +00:00
|
|
|
|
|
|
|
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>();
|
2021-04-14 18:19:39 +00:00
|
|
|
const [list, setList] = useState(false);
|
2021-03-23 19:02:27 +00:00
|
|
|
const [listGroups, setListGroups] = useState(true);
|
2021-04-14 18:19:39 +00:00
|
|
|
|
2021-03-23 19:02:27 +00:00
|
|
|
const [search, setSearch] = useState("");
|
|
|
|
const [username, setUsername] = useState("");
|
|
|
|
|
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);
|
|
|
|
|
|
|
|
const adminClient = useAdminClient();
|
|
|
|
const { id } = useParams<{ id: string }>();
|
2021-04-01 18:47:05 +00:00
|
|
|
const alphabetize = (groupsList: GroupRepresentation[]) => {
|
|
|
|
return _.sortBy(groupsList, (group) => group.path?.toUpperCase());
|
|
|
|
};
|
|
|
|
|
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 user = await adminClient.users.findOne({ id });
|
|
|
|
setUsername(user.username!);
|
|
|
|
|
|
|
|
const searchParam = search || "";
|
|
|
|
if (searchParam) {
|
|
|
|
params.search = searchParam;
|
|
|
|
setSearch(searchParam);
|
|
|
|
}
|
|
|
|
|
2021-04-14 18:19:39 +00:00
|
|
|
if (!searchParam && !listGroups && !list) {
|
2021-03-23 19:02:27 +00:00
|
|
|
return [];
|
|
|
|
}
|
|
|
|
|
2021-04-01 18:47:05 +00:00
|
|
|
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)
|
|
|
|
);
|
|
|
|
|
2021-04-05 17:55:17 +00:00
|
|
|
setDirectMembershipList(directMembership);
|
2021-04-14 18:19:39 +00:00
|
|
|
|
2021-04-01 18:47:05 +00:00
|
|
|
const filterDupesfromGroups = allPaths.filter(
|
|
|
|
(thing, index, self) =>
|
|
|
|
index === self.findIndex((t) => t.name === thing.name)
|
|
|
|
);
|
|
|
|
|
|
|
|
if (isDirectMembership) {
|
|
|
|
return alphabetize(directMembership);
|
|
|
|
}
|
|
|
|
|
|
|
|
return alphabetize(filterDupesfromGroups);
|
2021-03-23 19:02:27 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
return asyncStateFetch(
|
|
|
|
() => {
|
|
|
|
return Promise.resolve(adminClient.users.listGroups({ id }));
|
|
|
|
},
|
|
|
|
(response) => {
|
|
|
|
setListGroups(!!(response && response.length > 0));
|
|
|
|
},
|
|
|
|
handleError
|
|
|
|
);
|
2021-04-14 18:19:39 +00:00
|
|
|
}, []);
|
2021-03-23 19:02:27 +00:00
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
refresh();
|
|
|
|
}, [isDirectMembership]);
|
|
|
|
|
|
|
|
const AliasRenderer = (group: GroupRepresentation) => {
|
|
|
|
return <>{group.name}</>;
|
|
|
|
};
|
|
|
|
|
2021-04-14 18:19:39 +00:00
|
|
|
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();
|
|
|
|
};
|
2021-03-23 19:02:27 +00:00
|
|
|
|
|
|
|
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();
|
|
|
|
};
|
|
|
|
|
2021-04-05 17:55:17 +00:00
|
|
|
const LeaveButtonRenderer = (group: GroupRepresentation) => {
|
|
|
|
if (
|
|
|
|
directMembershipList.some((item) => item.id === group.id) ||
|
2021-04-14 18:19:39 +00:00
|
|
|
directMembershipList.length === 0 ||
|
|
|
|
isDirectMembership
|
2021-04-05 17:55:17 +00:00
|
|
|
) {
|
|
|
|
return (
|
|
|
|
<>
|
2021-04-14 18:19:39 +00:00
|
|
|
<Button
|
|
|
|
data-testid={`leave-${group.name}`}
|
|
|
|
onClick={() => leave(group)}
|
|
|
|
variant="link"
|
|
|
|
>
|
2021-04-05 17:55:17 +00:00
|
|
|
{t("users:Leave")}
|
|
|
|
</Button>
|
|
|
|
</>
|
|
|
|
);
|
|
|
|
} else {
|
|
|
|
return <> </>;
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2021-04-14 18:19:39 +00:00
|
|
|
const addGroup = async (group: GroupRepresentation): Promise<void> => {
|
|
|
|
const newGroup = group;
|
|
|
|
|
|
|
|
try {
|
|
|
|
await adminClient.users.addToGroup({
|
|
|
|
id: id,
|
|
|
|
groupId: newGroup.id!,
|
|
|
|
});
|
|
|
|
setList(true);
|
|
|
|
refresh();
|
|
|
|
addAlert(t("users:addedGroupMembership"), AlertVariant.success);
|
|
|
|
} catch (error) {
|
|
|
|
addAlert(
|
|
|
|
t("users:addedGroupMembershipError", { error }),
|
|
|
|
AlertVariant.danger
|
|
|
|
);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2021-03-23 19:02:27 +00:00
|
|
|
return (
|
|
|
|
<>
|
|
|
|
<PageSection variant="light">
|
|
|
|
<DeleteConfirm />
|
2021-04-14 18:19:39 +00:00
|
|
|
{open && (
|
|
|
|
<JoinGroupDialog
|
|
|
|
open={open}
|
|
|
|
onClose={() => setOpen(!open)}
|
|
|
|
onConfirm={addGroup}
|
|
|
|
toggleDialog={() => toggleModal()}
|
|
|
|
username={username}
|
|
|
|
/>
|
|
|
|
)}
|
2021-03-23 19:02:27 +00:00
|
|
|
<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"
|
2021-04-14 18:19:39 +00:00
|
|
|
onClick={toggleModal}
|
2021-03-23 19:02:27 +00:00
|
|
|
data-testid="add-group-button"
|
|
|
|
>
|
|
|
|
{t("users:joinGroup")}
|
|
|
|
</Button>
|
2021-04-14 18:19:39 +00:00
|
|
|
{JoinGroupButtonRenderer}
|
2021-03-23 19:02:27 +00:00
|
|
|
<Checkbox
|
|
|
|
label={t("users:directMembership")}
|
|
|
|
key="direct-membership-check"
|
|
|
|
id="kc-direct-membership-checkbox"
|
|
|
|
onChange={() => setDirectMembership(!isDirectMembership)}
|
|
|
|
isChecked={isDirectMembership}
|
|
|
|
/>
|
|
|
|
</>
|
|
|
|
}
|
|
|
|
columns={[
|
|
|
|
{
|
|
|
|
name: "groupMembership",
|
|
|
|
displayKey: "users:groupMembership",
|
|
|
|
cellRenderer: AliasRenderer,
|
|
|
|
cellFormatters: [emptyFormatter()],
|
|
|
|
transforms: [cellWidth(40)],
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "path",
|
|
|
|
displayKey: "users:Path",
|
|
|
|
cellFormatters: [emptyFormatter()],
|
|
|
|
transforms: [cellWidth(45)],
|
|
|
|
},
|
2021-04-05 17:55:17 +00:00
|
|
|
|
2021-03-23 19:02:27 +00:00
|
|
|
{
|
|
|
|
name: "",
|
|
|
|
cellRenderer: LeaveButtonRenderer,
|
|
|
|
cellFormatters: [emptyFormatter()],
|
|
|
|
transforms: [cellWidth(20)],
|
|
|
|
},
|
|
|
|
]}
|
|
|
|
emptyState={
|
|
|
|
!search ? (
|
|
|
|
<ListEmptyState
|
|
|
|
hasIcon={true}
|
|
|
|
message={t("users:noGroups")}
|
|
|
|
instructions={t("users:noGroupsText")}
|
|
|
|
/>
|
|
|
|
) : (
|
|
|
|
""
|
|
|
|
)
|
|
|
|
}
|
|
|
|
/>
|
|
|
|
</PageSection>
|
|
|
|
</>
|
|
|
|
);
|
|
|
|
};
|