Skip to content

Commit

Permalink
lazy populate the treeview for groups (keycloak#21520)
Browse files Browse the repository at this point in the history
* added lazy parameter

fixes: keycloak#19954

* changed to only have the parameter

* fixed merge errors

* removed the `lazy` and now add subgroups on select

* lint

* fixed prettier

* fixed nullpointer

* fixed member tab
  • Loading branch information
edewit authored Aug 4, 2023
1 parent 92bec02 commit 3396198
Show file tree
Hide file tree
Showing 8 changed files with 293 additions and 162 deletions.
31 changes: 7 additions & 24 deletions js/apps/admin-ui/src/groups/GroupTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,19 @@ import type GroupRepresentation from "@keycloak/keycloak-admin-client/lib/defs/g
import { SearchInput, ToolbarItem } from "@patternfly/react-core";
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { Link, useLocation, useNavigate } from "react-router-dom";
import { Link, useLocation } from "react-router-dom";

import { adminClient } from "../admin-client";
import { ListEmptyState } from "../components/list-empty-state/ListEmptyState";
import { KeycloakDataTable } from "../components/table-toolbar/KeycloakDataTable";
import { useAccess } from "../context/access/Access";
import { fetchAdminUI } from "../context/auth/admin-ui-endpoint";
import { useRealm } from "../context/realm-context/RealmContext";
import useToggle from "../utils/useToggle";
import { GroupsModal } from "./GroupsModal";
import { useSubGroups } from "./SubGroupsContext";
import { DeleteGroup } from "./components/DeleteGroup";
import { GroupToolbar } from "./components/GroupToolbar";
import { MoveDialog } from "./components/MoveDialog";
import { getLastId } from "./groupIdUtils";
import { toGroups } from "./routes/Groups";

type GroupTableProps = {
refresh: () => void;
Expand All @@ -30,7 +27,6 @@ export const GroupTable = ({
}: GroupTableProps) => {
const { t } = useTranslation("groups");

const { realm } = useRealm();
const [selectedRows, setSelectedRows] = useState<GroupRepresentation[]>([]);

const [rename, setRename] = useState<GroupRepresentation>();
Expand All @@ -44,7 +40,6 @@ export const GroupTable = ({
const refresh = () => setKey(key + 1);
const [search, setSearch] = useState<string>();

const navigate = useNavigate();
const location = useLocation();
const id = getLastId(location.pathname);

Expand All @@ -60,26 +55,18 @@ export const GroupTable = ({

let groupsData = undefined;
if (id) {
const group = await adminClient.groups.findOne({ id });
if (!group) {
throw new Error(t("common:notFound"));
}

groupsData = !search
? group.subGroups
: group.subGroups?.filter((g) => g.name?.includes(search));
groupsData = await fetchAdminUI<GroupRepresentation[]>(
"ui-ext/groups/subgroup",
{ ...params, id },
);
} else {
groupsData = await fetchAdminUI<GroupRepresentation[]>("ui-ext/groups", {
...params,
global: "false",
});
}

if (!groupsData) {
navigate(toGroups({ realm }));
}

return groupsData || [];
return groupsData;
};

return (
Expand Down Expand Up @@ -204,11 +191,7 @@ export const GroupTable = ({
displayKey: "groups:groupName",
cellRenderer: (group) =>
canViewDetails ? (
<Link
key={group.id}
to={`${location.pathname}/${group.id}`}
onClick={() => navigate(toGroups({ realm, id: group.id }))}
>
<Link key={group.id} to={`${location.pathname}/${group.id}`}>
{group.name}
</Link>
) : (
Expand Down
219 changes: 113 additions & 106 deletions js/apps/admin-ui/src/groups/GroupsSection.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type GroupRepresentation from "@keycloak/keycloak-admin-client/lib/defs/groupRepresentation";
import {
Button,
Drawer,
DrawerContent,
DrawerContentBody,
Expand All @@ -11,16 +12,18 @@ import {
Tab,
TabTitleText,
Tabs,
Tooltip,
} from "@patternfly/react-core";
import { AngleLeftIcon, TreeIcon } from "@patternfly/react-icons";
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { useLocation, useNavigate } from "react-router-dom";

import { adminClient } from "../admin-client";
import { GroupBreadCrumbs } from "../components/bread-crumb/GroupBreadCrumbs";
import { PermissionsTab } from "../components/permission-tab/PermissionTab";
import { ViewHeader } from "../components/view-header/ViewHeader";
import { useAccess } from "../context/access/Access";
import { fetchAdminUI } from "../context/auth/admin-ui-endpoint";
import { useRealm } from "../context/realm-context/RealmContext";
import helpUrls from "../help-urls";
import { useFetch } from "../utils/useFetch";
Expand Down Expand Up @@ -53,6 +56,7 @@ export default function GroupsSection() {
const location = useLocation();
const id = getLastId(location.pathname);

const [open, toggle] = useToggle(true);
const [key, setKey] = useState(0);
const refresh = () => setKey(key + 1);

Expand Down Expand Up @@ -82,7 +86,9 @@ export default function GroupsSection() {
for (const i of ids!) {
const group =
i !== "search"
? await adminClient.groups.findOne({ id: i })
? await fetchAdminUI<GroupRepresentation | undefined>(
"ui-ext/groups/" + i,
)
: { name: t("searchGroups"), id: "search" };
if (group) {
groups.push(group);
Expand Down Expand Up @@ -123,121 +129,122 @@ export default function GroupsSection() {
/>
)}
<PageSection variant={PageSectionVariants.light} className="pf-u-p-0">
<Drawer isInline isExpanded key={key}>
<Drawer isInline isExpanded={open} key={key} position="left">
<DrawerContent
panelContent={
<DrawerPanelContent isResizable defaultSize="80%" minSize="500px">
<DrawerPanelContent isResizable>
<DrawerHead>
<GroupBreadCrumbs />
<ViewHeader
titleKey={!id ? "groups:groups" : currentGroup()?.name!}
subKey={!id ? "groups:groupsDescription" : ""}
helpUrl={!id ? helpUrls.groupsUrl : ""}
divider={!id}
dropdownItems={
id && canManageGroup
? [
<DropdownItem
data-testid="renameGroupAction"
key="renameGroup"
onClick={() => setRename(currentGroup())}
>
{t("renameGroup")}
</DropdownItem>,
<DropdownItem
data-testid="deleteGroup"
key="deleteGroup"
onClick={toggleDeleteOpen}
>
{t("deleteGroup")}
</DropdownItem>,
]
: undefined
}
<GroupTree
refresh={refresh}
canViewDetails={canViewDetails}
/>
{subGroups.length > 0 && (
<Tabs
inset={{
default: "insetNone",
md: "insetSm",
xl: "insetLg",
"2xl": "inset2xl",
}}
activeKey={activeTab}
onSelect={(_, key) => setActiveTab(key as number)}
isBox
mountOnEnter
unmountOnExit
>
<Tab
data-testid="groups"
eventKey={0}
title={<TabTitleText>{t("childGroups")}</TabTitleText>}
>
<GroupTable
refresh={refresh}
canViewDetails={canViewDetails}
/>
</Tab>
{canViewMembers && (
<Tab
data-testid="members"
eventKey={1}
title={<TabTitleText>{t("members")}</TabTitleText>}
>
<Members />
</Tab>
)}
<Tab
data-testid="attributes"
eventKey={2}
title={
<TabTitleText>{t("common:attributes")}</TabTitleText>
}
>
<GroupAttributes />
</Tab>
{canManageRoles && (
<Tab
eventKey={3}
data-testid="role-mapping-tab"
title={
<TabTitleText>{t("roleMapping")}</TabTitleText>
}
</DrawerHead>
</DrawerPanelContent>
}
>
<DrawerContentBody>
<Tooltip content={open ? t("common:hide") : t("common:show")}>
<Button
aria-label={open ? t("common:hide") : t("common:show")}
variant="plain"
icon={open ? <AngleLeftIcon /> : <TreeIcon />}
onClick={toggle}
/>
</Tooltip>
<GroupBreadCrumbs />
<ViewHeader
titleKey={!id ? "groups:groups" : currentGroup()?.name!}
subKey={!id ? "groups:groupsDescription" : ""}
helpUrl={!id ? helpUrls.groupsUrl : ""}
divider={!id}
dropdownItems={
id && canManageGroup
? [
<DropdownItem
data-testid="renameGroupAction"
key="renameGroup"
onClick={() => setRename(currentGroup())}
>
<GroupRoleMapping
id={id!}
name={currentGroup()?.name!}
/>
</Tab>
)}
{canViewPermissions && (
<Tab
eventKey={4}
data-testid="permissionsTab"
title={
<TabTitleText>
{t("common:permissions")}
</TabTitleText>
}
{t("renameGroup")}
</DropdownItem>,
<DropdownItem
data-testid="deleteGroup"
key="deleteGroup"
onClick={toggleDeleteOpen}
>
<PermissionsTab id={id} type="groups" />
</Tab>
)}
</Tabs>
)}
{subGroups.length === 0 && (
{t("deleteGroup")}
</DropdownItem>,
]
: undefined
}
/>
{subGroups.length > 0 && (
<Tabs
inset={{
default: "insetNone",
md: "insetSm",
xl: "insetLg",
"2xl": "inset2xl",
}}
activeKey={activeTab}
onSelect={(_, key) => setActiveTab(key as number)}
isBox
mountOnEnter
unmountOnExit
>
<Tab
data-testid="groups"
eventKey={0}
title={<TabTitleText>{t("childGroups")}</TabTitleText>}
>
<GroupTable
refresh={refresh}
canViewDetails={canViewDetails}
/>
</Tab>
{canViewMembers && (
<Tab
data-testid="members"
eventKey={1}
title={<TabTitleText>{t("members")}</TabTitleText>}
>
<Members />
</Tab>
)}
</DrawerHead>
</DrawerPanelContent>
}
>
<DrawerContentBody>
<GroupTree refresh={refresh} canViewDetails={canViewDetails} />
<Tab
data-testid="attributes"
eventKey={2}
title={
<TabTitleText>{t("common:attributes")}</TabTitleText>
}
>
<GroupAttributes />
</Tab>
{canManageRoles && (
<Tab
eventKey={3}
data-testid="role-mapping-tab"
title={<TabTitleText>{t("roleMapping")}</TabTitleText>}
>
<GroupRoleMapping id={id!} name={currentGroup()?.name!} />
</Tab>
)}
{canViewPermissions && (
<Tab
eventKey={4}
data-testid="permissionsTab"
title={
<TabTitleText>{t("common:permissions")}</TabTitleText>
}
>
<PermissionsTab id={id} type="groups" />
</Tab>
)}
</Tabs>
)}
{subGroups.length === 0 && (
<GroupTable refresh={refresh} canViewDetails={canViewDetails} />
)}
</DrawerContentBody>
</DrawerContent>
</Drawer>
Expand Down
Loading

0 comments on commit 3396198

Please sign in to comment.