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
This commit is contained in:
parent
bbfccea940
commit
ce0ce6d59e
8 changed files with 345 additions and 77 deletions
11
src/App.tsx
11
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 }) => (
|
|||
<AccessContextProvider>
|
||||
<Help>
|
||||
<AlertProvider>
|
||||
<ServerInfoProvider>{children}</ServerInfoProvider>
|
||||
<ServerInfoProvider>
|
||||
<SubGroups>{children}</SubGroups>
|
||||
</ServerInfoProvider>
|
||||
</AlertProvider>
|
||||
</Help>
|
||||
</AccessContextProvider>
|
||||
|
@ -79,7 +82,11 @@ export const App = () => {
|
|||
<Switch>
|
||||
{routes(() => {}).map((route, i) => (
|
||||
<Route
|
||||
exact
|
||||
exact={
|
||||
route.matchOptions?.exact === undefined
|
||||
? true
|
||||
: route.matchOptions.exact
|
||||
}
|
||||
key={i}
|
||||
path={route.path}
|
||||
component={() => (
|
||||
|
|
51
src/components/bread-crumb/GroupBreadCrumbs.tsx
Normal file
51
src/components/bread-crumb/GroupBreadCrumbs.tsx
Normal file
|
@ -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 && (
|
||||
<Breadcrumb>
|
||||
<BreadcrumbItem key="home">
|
||||
<Link to={`/${realm}/groups`}>{t("groups")}</Link>
|
||||
</BreadcrumbItem>
|
||||
{subGroups.map((group, i) => (
|
||||
<BreadcrumbItem key={i} isActive={subGroups.length - 1 === i}>
|
||||
{subGroups.length - 1 !== i && (
|
||||
<Link
|
||||
to={location.pathname.substr(
|
||||
0,
|
||||
location.pathname.indexOf(group.id!) + group.id!.length
|
||||
)}
|
||||
onClick={() => remove(group)}
|
||||
>
|
||||
{group.name}
|
||||
</Link>
|
||||
)}
|
||||
{subGroups.length - 1 === i && <>{group.name}</>}
|
||||
</BreadcrumbItem>
|
||||
))}
|
||||
</Breadcrumb>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -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 = () => {
|
|||
))}
|
||||
</Breadcrumb>
|
||||
)}
|
||||
<GroupBreadCrumbs />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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 <GroupBreadCrumbs />;
|
||||
};
|
||||
|
||||
describe("Group BreadCrumbs tests", () => {
|
||||
it("couple of crumbs", () => {
|
||||
const crumbs = mount(
|
||||
<MemoryRouter initialEntries={["/groups"]}>
|
||||
<SubGroups>
|
||||
<GroupCrumbs />
|
||||
</SubGroups>
|
||||
</MemoryRouter>
|
||||
);
|
||||
|
||||
expect(crumbs.find(GroupCrumbs)).toMatchSnapshot();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,140 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Group BreadCrumbs tests couple of crumbs 1`] = `
|
||||
<GroupCrumbs>
|
||||
<GroupBreadCrumbs>
|
||||
<Breadcrumb>
|
||||
<nav
|
||||
aria-label="Breadcrumb"
|
||||
className="pf-c-breadcrumb"
|
||||
data-ouia-component-id="OUIA-Generated-Breadcrumb-1"
|
||||
data-ouia-component-type="PF4/Breadcrumb"
|
||||
data-ouia-safe={true}
|
||||
>
|
||||
<ol
|
||||
className="pf-c-breadcrumb__list"
|
||||
>
|
||||
<BreadcrumbItem
|
||||
key=".$home"
|
||||
showDivider={false}
|
||||
>
|
||||
<li
|
||||
className="pf-c-breadcrumb__item"
|
||||
>
|
||||
<Link
|
||||
to="//groups"
|
||||
>
|
||||
<LinkAnchor
|
||||
href="//groups"
|
||||
navigate={[Function]}
|
||||
>
|
||||
<a
|
||||
href="//groups"
|
||||
onClick={[Function]}
|
||||
>
|
||||
groups
|
||||
</a>
|
||||
</LinkAnchor>
|
||||
</Link>
|
||||
</li>
|
||||
</BreadcrumbItem>
|
||||
<BreadcrumbItem
|
||||
isActive={false}
|
||||
key=".1:$0"
|
||||
showDivider={true}
|
||||
>
|
||||
<li
|
||||
className="pf-c-breadcrumb__item"
|
||||
>
|
||||
<span
|
||||
className="pf-c-breadcrumb__item-divider"
|
||||
>
|
||||
<AngleRightIcon
|
||||
color="currentColor"
|
||||
noVerticalAlign={false}
|
||||
size="sm"
|
||||
>
|
||||
<svg
|
||||
aria-hidden={true}
|
||||
aria-labelledby={null}
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
role="img"
|
||||
style={
|
||||
Object {
|
||||
"verticalAlign": "-0.125em",
|
||||
}
|
||||
}
|
||||
viewBox="0 0 256 512"
|
||||
width="1em"
|
||||
>
|
||||
<path
|
||||
d="M224.3 273l-136 136c-9.4 9.4-24.6 9.4-33.9 0l-22.6-22.6c-9.4-9.4-9.4-24.6 0-33.9l96.4-96.4-96.4-96.4c-9.4-9.4-9.4-24.6 0-33.9L54.3 103c9.4-9.4 24.6-9.4 33.9 0l136 136c9.5 9.4 9.5 24.6.1 34z"
|
||||
/>
|
||||
</svg>
|
||||
</AngleRightIcon>
|
||||
</span>
|
||||
<Link
|
||||
onClick={[Function]}
|
||||
to=""
|
||||
>
|
||||
<LinkAnchor
|
||||
href="/"
|
||||
navigate={[Function]}
|
||||
onClick={[Function]}
|
||||
>
|
||||
<a
|
||||
href="/"
|
||||
onClick={[Function]}
|
||||
>
|
||||
first group
|
||||
</a>
|
||||
</LinkAnchor>
|
||||
</Link>
|
||||
</li>
|
||||
</BreadcrumbItem>
|
||||
<BreadcrumbItem
|
||||
isActive={true}
|
||||
key=".1:$1"
|
||||
showDivider={true}
|
||||
>
|
||||
<li
|
||||
className="pf-c-breadcrumb__item"
|
||||
>
|
||||
<span
|
||||
className="pf-c-breadcrumb__item-divider"
|
||||
>
|
||||
<AngleRightIcon
|
||||
color="currentColor"
|
||||
noVerticalAlign={false}
|
||||
size="sm"
|
||||
>
|
||||
<svg
|
||||
aria-hidden={true}
|
||||
aria-labelledby={null}
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
role="img"
|
||||
style={
|
||||
Object {
|
||||
"verticalAlign": "-0.125em",
|
||||
}
|
||||
}
|
||||
viewBox="0 0 256 512"
|
||||
width="1em"
|
||||
>
|
||||
<path
|
||||
d="M224.3 273l-136 136c-9.4 9.4-24.6 9.4-33.9 0l-22.6-22.6c-9.4-9.4-9.4-24.6 0-33.9l96.4-96.4-96.4-96.4c-9.4-9.4-9.4-24.6 0-33.9L54.3 103c9.4-9.4 24.6-9.4 33.9 0l136 136c9.5 9.4 9.5 24.6.1 34z"
|
||||
/>
|
||||
</svg>
|
||||
</AngleRightIcon>
|
||||
</span>
|
||||
active group
|
||||
</li>
|
||||
</BreadcrumbItem>
|
||||
</ol>
|
||||
</nav>
|
||||
</Breadcrumb>
|
||||
</GroupBreadCrumbs>
|
||||
</GroupCrumbs>
|
||||
`;
|
|
@ -21,63 +21,6 @@ exports[`BreadCrumbs tests couple of crumbs 1`] = `
|
|||
<li
|
||||
className="pf-c-breadcrumb__item"
|
||||
>
|
||||
<Link
|
||||
to="/master"
|
||||
>
|
||||
<LinkAnchor
|
||||
href="/master"
|
||||
navigate={[Function]}
|
||||
>
|
||||
<a
|
||||
href="/master"
|
||||
onClick={[Function]}
|
||||
>
|
||||
<span
|
||||
key="/master"
|
||||
>
|
||||
Home
|
||||
</span>
|
||||
</a>
|
||||
</LinkAnchor>
|
||||
</Link>
|
||||
</li>
|
||||
</BreadcrumbItem>
|
||||
<BreadcrumbItem
|
||||
isActive={false}
|
||||
key=".$1"
|
||||
showDivider={true}
|
||||
>
|
||||
<li
|
||||
className="pf-c-breadcrumb__item"
|
||||
>
|
||||
<span
|
||||
className="pf-c-breadcrumb__item-divider"
|
||||
>
|
||||
<AngleRightIcon
|
||||
color="currentColor"
|
||||
noVerticalAlign={false}
|
||||
size="sm"
|
||||
>
|
||||
<svg
|
||||
aria-hidden={true}
|
||||
aria-labelledby={null}
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
role="img"
|
||||
style={
|
||||
Object {
|
||||
"verticalAlign": "-0.125em",
|
||||
}
|
||||
}
|
||||
viewBox="0 0 256 512"
|
||||
width="1em"
|
||||
>
|
||||
<path
|
||||
d="M224.3 273l-136 136c-9.4 9.4-24.6 9.4-33.9 0l-22.6-22.6c-9.4-9.4-9.4-24.6 0-33.9l96.4-96.4-96.4-96.4c-9.4-9.4-9.4-24.6 0-33.9L54.3 103c9.4-9.4 24.6-9.4 33.9 0l136 136c9.5 9.4 9.5 24.6.1 34z"
|
||||
/>
|
||||
</svg>
|
||||
</AngleRightIcon>
|
||||
</span>
|
||||
<Link
|
||||
to="/master/clients"
|
||||
>
|
||||
|
@ -101,7 +44,7 @@ exports[`BreadCrumbs tests couple of crumbs 1`] = `
|
|||
</BreadcrumbItem>
|
||||
<BreadcrumbItem
|
||||
isActive={true}
|
||||
key=".$2"
|
||||
key=".$1"
|
||||
showDivider={true}
|
||||
>
|
||||
<li
|
||||
|
@ -145,5 +88,6 @@ exports[`BreadCrumbs tests couple of crumbs 1`] = `
|
|||
</ol>
|
||||
</nav>
|
||||
</Breadcrumb>
|
||||
<GroupBreadCrumbs />
|
||||
</PageBreadCrumbs>
|
||||
`;
|
||||
|
|
|
@ -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<SubGroupsProps>({
|
||||
subGroups: [],
|
||||
setSubGroups: () => {},
|
||||
clear: () => {},
|
||||
remove: () => {},
|
||||
});
|
||||
|
||||
export const SubGroups = ({ children }: { children: ReactNode }) => {
|
||||
const [subGroups, setSubGroups] = useState<GroupRepresentation[]>([]);
|
||||
|
||||
const clear = () => setSubGroups([]);
|
||||
const remove = (group: GroupRepresentation) =>
|
||||
setSubGroups(
|
||||
subGroups.slice(
|
||||
0,
|
||||
subGroups.findIndex((g) => g.id === group.id)
|
||||
)
|
||||
);
|
||||
return (
|
||||
<SubGroupContext.Provider
|
||||
value={{ subGroups, setSubGroups, clear, remove }}
|
||||
>
|
||||
{children}
|
||||
</SubGroupContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
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<GroupRepresentation[]>([]);
|
||||
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) => (
|
||||
<>
|
||||
<Link key={group.id} to={`${url}/${group.id}`}>
|
||||
<Link key={group.id} to={`${location.pathname}/${group.id}`}>
|
||||
{group.name}
|
||||
</Link>
|
||||
</>
|
||||
|
@ -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={
|
||||
<>
|
||||
<ToolbarItem>
|
||||
|
|
|
@ -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",
|
||||
},
|
||||
{
|
||||
|
|
Loading…
Reference in a new issue