From ce0ce6d59ec2d78163bb43492d5c05c78eff411b Mon Sep 17 00:00:00 2001 From: Erik Jan de Wit Date: Tue, 12 Jan 2021 15:00:44 +0100 Subject: [PATCH] Sub group navigation in groups section (#274) * list sub group of a group using the id * introduced group bread crumb component to render this builds up the groups as you click to subgroups * added deeplinking * fixed route config types * fixed new realm prefix * added null checks for if group not found * changed labes to groups --- src/App.tsx | 11 +- .../bread-crumb/GroupBreadCrumbs.tsx | 51 +++++++ .../bread-crumb/PageBreadCrumbs.tsx | 2 + .../__tests__/GroupBreadCrumbs.test.tsx | 32 ++++ .../GroupBreadCrumbs.test.tsx.snap | 140 ++++++++++++++++++ .../PageBreadCrumbs.test.tsx.snap | 60 +------- src/groups/GroupsSection.tsx | 117 +++++++++++++-- src/route-config.ts | 9 +- 8 files changed, 345 insertions(+), 77 deletions(-) create mode 100644 src/components/bread-crumb/GroupBreadCrumbs.tsx create mode 100644 src/components/bread-crumb/__tests__/GroupBreadCrumbs.test.tsx create mode 100644 src/components/bread-crumb/__tests__/__snapshots__/GroupBreadCrumbs.test.tsx.snap diff --git a/src/App.tsx b/src/App.tsx index 1827fb0b77..ef84f11b19 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -18,6 +18,7 @@ import { AccessContextProvider, useAccess } from "./context/access/Access"; import { routes, RouteDef } from "./route-config"; import { PageBreadCrumbs } from "./components/bread-crumb/PageBreadCrumbs"; import { ForbiddenSection } from "./ForbiddenSection"; +import { SubGroups } from "./groups/GroupsSection"; import { useRealm } from "./context/realm-context/RealmContext"; import { useAdminClient, asyncStateFetch } from "./context/auth/AdminClient"; @@ -28,7 +29,9 @@ const AppContexts = ({ children }: { children: ReactNode }) => ( - {children} + + {children} + @@ -79,7 +82,11 @@ export const App = () => { {routes(() => {}).map((route, i) => ( ( diff --git a/src/components/bread-crumb/GroupBreadCrumbs.tsx b/src/components/bread-crumb/GroupBreadCrumbs.tsx new file mode 100644 index 0000000000..31e9a1263d --- /dev/null +++ b/src/components/bread-crumb/GroupBreadCrumbs.tsx @@ -0,0 +1,51 @@ +import React, { useEffect } from "react"; +import { Link, useHistory } from "react-router-dom"; +import { useTranslation } from "react-i18next"; +import { Breadcrumb, BreadcrumbItem } from "@patternfly/react-core"; + +import { useSubGroups } from "../../groups/GroupsSection"; +import { useRealm } from "../../context/realm-context/RealmContext"; + +export const GroupBreadCrumbs = () => { + const { t } = useTranslation(); + const { clear, remove, subGroups } = useSubGroups(); + const { realm } = useRealm(); + + const history = useHistory(); + + useEffect(() => { + return history.listen(({ pathname }) => { + if (pathname.indexOf("/groups") === -1 || pathname.endsWith("/groups")) { + clear(); + } + }); + }, [history]); + + return ( + <> + {subGroups.length !== 0 && ( + + + {t("groups")} + + {subGroups.map((group, i) => ( + + {subGroups.length - 1 !== i && ( + remove(group)} + > + {group.name} + + )} + {subGroups.length - 1 === i && <>{group.name}} + + ))} + + )} + + ); +}; diff --git a/src/components/bread-crumb/PageBreadCrumbs.tsx b/src/components/bread-crumb/PageBreadCrumbs.tsx index 4d030a6c5c..7d51b8e1ee 100644 --- a/src/components/bread-crumb/PageBreadCrumbs.tsx +++ b/src/components/bread-crumb/PageBreadCrumbs.tsx @@ -6,6 +6,7 @@ import { Breadcrumb, BreadcrumbItem } from "@patternfly/react-core"; import { useRealm } from "../../context/realm-context/RealmContext"; import { routes } from "../../route-config"; +import { GroupBreadCrumbs } from "./GroupBreadCrumbs"; export const PageBreadCrumbs = () => { const { t } = useTranslation(); @@ -25,6 +26,7 @@ export const PageBreadCrumbs = () => { ))} )} + ); }; diff --git a/src/components/bread-crumb/__tests__/GroupBreadCrumbs.test.tsx b/src/components/bread-crumb/__tests__/GroupBreadCrumbs.test.tsx new file mode 100644 index 0000000000..6852d66aad --- /dev/null +++ b/src/components/bread-crumb/__tests__/GroupBreadCrumbs.test.tsx @@ -0,0 +1,32 @@ +import React, { useEffect } from "react"; +import { mount } from "enzyme"; +import { MemoryRouter } from "react-router-dom"; + +import { GroupBreadCrumbs } from "../GroupBreadCrumbs"; +import { SubGroups, useSubGroups } from "../../../groups/GroupsSection"; + +const GroupCrumbs = () => { + const { setSubGroups } = useSubGroups(); + useEffect(() => { + setSubGroups([ + { id: "1", name: "first group" }, + { id: "123", name: "active group" }, + ]); + }, []); + + return ; +}; + +describe("Group BreadCrumbs tests", () => { + it("couple of crumbs", () => { + const crumbs = mount( + + + + + + ); + + expect(crumbs.find(GroupCrumbs)).toMatchSnapshot(); + }); +}); diff --git a/src/components/bread-crumb/__tests__/__snapshots__/GroupBreadCrumbs.test.tsx.snap b/src/components/bread-crumb/__tests__/__snapshots__/GroupBreadCrumbs.test.tsx.snap new file mode 100644 index 0000000000..950c611b34 --- /dev/null +++ b/src/components/bread-crumb/__tests__/__snapshots__/GroupBreadCrumbs.test.tsx.snap @@ -0,0 +1,140 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Group BreadCrumbs tests couple of crumbs 1`] = ` + + + + + + + +`; diff --git a/src/components/bread-crumb/__tests__/__snapshots__/PageBreadCrumbs.test.tsx.snap b/src/components/bread-crumb/__tests__/__snapshots__/PageBreadCrumbs.test.tsx.snap index 6f70afe222..f5e308e969 100644 --- a/src/components/bread-crumb/__tests__/__snapshots__/PageBreadCrumbs.test.tsx.snap +++ b/src/components/bread-crumb/__tests__/__snapshots__/PageBreadCrumbs.test.tsx.snap @@ -21,63 +21,6 @@ exports[`BreadCrumbs tests couple of crumbs 1`] = `
  • - - - - - Home - - - - -
  • - - -
  • - - - - - - - @@ -101,7 +44,7 @@ exports[`BreadCrumbs tests couple of crumbs 1`] = `
  • + `; diff --git a/src/groups/GroupsSection.tsx b/src/groups/GroupsSection.tsx index 0a6ff837d9..5188ef08a9 100644 --- a/src/groups/GroupsSection.tsx +++ b/src/groups/GroupsSection.tsx @@ -1,4 +1,11 @@ -import React, { useState } from "react"; +import React, { + createContext, + ReactNode, + useContext, + useEffect, + useState, +} from "react"; +import { Link, useHistory, useLocation } from "react-router-dom"; import { useTranslation } from "react-i18next"; import { Button, @@ -21,12 +28,58 @@ import { useAlerts } from "../components/alert/Alerts"; import { KeycloakDataTable } from "../components/table-toolbar/KeycloakDataTable"; import "./GroupsSection.css"; -import { Link, useRouteMatch } from "react-router-dom"; +import { useRealm } from "../context/realm-context/RealmContext"; type GroupTableData = GroupRepresentation & { membersLength?: number; }; +type SubGroupsProps = { + subGroups: GroupRepresentation[]; + setSubGroups: (group: GroupRepresentation[]) => void; + clear: () => void; + remove: (group: GroupRepresentation) => void; +}; + +const SubGroupContext = createContext({ + subGroups: [], + setSubGroups: () => {}, + clear: () => {}, + remove: () => {}, +}); + +export const SubGroups = ({ children }: { children: ReactNode }) => { + const [subGroups, setSubGroups] = useState([]); + + const clear = () => setSubGroups([]); + const remove = (group: GroupRepresentation) => + setSubGroups( + subGroups.slice( + 0, + subGroups.findIndex((g) => g.id === group.id) + ) + ); + return ( + + {children} + + ); +}; + +export const useSubGroups = () => useContext(SubGroupContext); + +const getId = (pathname: string) => { + const pathParts = pathname.substr(1).split("/"); + return pathParts.length > 1 ? pathParts.splice(2) : undefined; +}; + +const getLastId = (pathname: string) => { + const pathParts = getId(pathname); + return pathParts ? pathParts[pathParts.length - 1] : undefined; +}; + export const GroupsSection = () => { const { t } = useTranslation("groups"); const adminClient = useAdminClient(); @@ -34,8 +87,14 @@ export const GroupsSection = () => { const [createGroupName, setCreateGroupName] = useState(""); const [isCreateModalOpen, setIsCreateModalOpen] = useState(false); const [selectedRows, setSelectedRows] = useState([]); + const { subGroups, setSubGroups } = useSubGroups(); const { addAlert } = useAlerts(); - const { url } = useRouteMatch(); + const { realm } = useRealm(); + const history = useHistory(); + + const location = useLocation(); + const id = getLastId(location.pathname); + const [key, setKey] = useState(""); const refresh = () => setKey(`${new Date().getTime()}`); @@ -45,17 +104,47 @@ export const GroupsSection = () => { }; const loader = async () => { - const groupsData = await adminClient.groups.find(); + let groupsData; + if (!id) { + groupsData = await adminClient.groups.find(); + } else { + const ids = getId(location.pathname); + const isNavigationStateInValid = ids && ids.length !== subGroups.length; + if (isNavigationStateInValid) { + const groups = []; + for (const i of ids!) { + const group = await adminClient.groups.findOne({ id: i }); + if (group) groups.push(group); + } + setSubGroups(groups); + groupsData = groups.pop()?.subGroups!; + } else { + const group = await adminClient.groups.findOne({ id }); + if (group) { + setSubGroups([...subGroups, group]); + groupsData = group.subGroups!; + } + } + } - const memberPromises = groupsData.map((group) => getMembers(group.id!)); - const memberData = await Promise.all(memberPromises); - const updatedObject = groupsData.map((group: GroupTableData, i) => { - group.membersLength = memberData[i]; - return group; - }); - return updatedObject; + if (groupsData) { + const memberPromises = groupsData.map((group) => getMembers(group.id!)); + const memberData = await Promise.all(memberPromises); + return groupsData.map((group: GroupTableData, i) => { + group.membersLength = memberData[i]; + return group; + }); + } else { + history.push(`/${realm}/groups`); + } + + return []; }; + useEffect(() => { + refresh(); + }, [id]); + const handleModalToggle = () => { setIsCreateModalOpen(!isCreateModalOpen); }; @@ -85,7 +174,7 @@ export const GroupsSection = () => { const GroupNameCell = (group: GroupTableData) => ( <> - + {group.name} @@ -107,8 +196,8 @@ export const GroupsSection = () => { onSelect={(rows) => setSelectedRows([...rows])} canSelectAll={false} loader={loader} - ariaLabelKey="client-scopes:clientScopeList" - searchPlaceholderKey="client-scopes:searchFor" + ariaLabelKey="groups:groups" + searchPlaceholderKey="groups:searchForGroups" toolbarItem={ <> diff --git a/src/route-config.ts b/src/route-config.ts index c7a67c0b17..35068280b9 100644 --- a/src/route-config.ts +++ b/src/route-config.ts @@ -26,13 +26,13 @@ import { BreadcrumbsRoute } from "use-react-router-breadcrumbs"; import { RealmRoleTabs } from "./realm-roles/RealmRoleTabs"; export type RouteDef = BreadcrumbsRoute & { - component: () => JSX.Element; access: AccessType; + component: () => JSX.Element; }; type RoutesFn = (t: TFunction) => RouteDef[]; -export const routes: RoutesFn = (t) => [ +export const routes: RoutesFn = (t: TFunction) => [ { path: "/:realm/add-realm", component: NewRealmForm, @@ -126,7 +126,10 @@ export const routes: RoutesFn = (t) => [ { path: "/:realm/groups", component: GroupsSection, - breadcrumb: t("groups"), + breadcrumb: null, + matchOptions: { + exact: false, + }, access: "query-groups", }, {