Skip to content

Commit 4027894

Browse files
heisbrotaliamerj
andauthored
Feature/groups page (#498)
* move our group membership from the settings menu, into the Team menu * add action to the table and new group page * update group page and return group settings to settings menu * new update * fix bug * group action: add peer to group * group action: add user to group * Update wording, redirect to group page after creation * Add better table loading skeleton * Adjust group name cell * Update wording * Update sort order * Refactor * Merge main * Fix button height * Fix resources table * Adjust table loading skeleton * Adjust table loading skeleton * Add loading to tab triggers * Update meta * Update group location * Fix rename * Refactor group details * Fix linked peers * Fix group usage * Fix incrementing peer count * Prevent renaming to already existing group * Fix group name click * Update group nav * Make group table cells clickable * Fix breadcrumbs * Update wording * Add confirmation before removing users from group * Add permissions * Add initial group for network routes * Add acl and routing peer groups --------- Co-authored-by: aliamerj <[email protected]>
1 parent af90792 commit 4027894

File tree

63 files changed

+3362
-706
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

63 files changed

+3362
-706
lines changed

src/app/(dashboard)/access-control/page.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ export default function AccessControlPage() {
3333
<div className={"p-default py-6"}>
3434
<Breadcrumbs>
3535
<Breadcrumbs.Item
36-
href={"/policies"}
36+
href={"/access-control"}
3737
label={"Access Control"}
3838
icon={<AccessControlIcon size={14} />}
3939
/>

src/app/(dashboard)/dns/nameservers/page.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ export default function NameServers() {
3232
<div className={"p-default py-6"}>
3333
<Breadcrumbs>
3434
<Breadcrumbs.Item
35-
href={"/dns"}
35+
href={"/dns/nameservers"}
3636
label={"DNS"}
3737
icon={<DNSIcon size={13} />}
3838
/>
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { globalMetaTitle } from "@utils/meta";
2+
import type { Metadata } from "next";
3+
import BlankLayout from "@/layouts/BlankLayout";
4+
5+
export const metadata: Metadata = {
6+
title: `Group - ${globalMetaTitle}`,
7+
};
8+
export default BlankLayout;

src/app/(dashboard)/group/page.tsx

Lines changed: 297 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,297 @@
1+
"use client";
2+
3+
import Breadcrumbs from "@components/Breadcrumbs";
4+
import FullTooltip from "@components/FullTooltip";
5+
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@components/Tabs";
6+
import FullScreenLoading from "@components/ui/FullScreenLoading";
7+
import { GroupBadgeIcon } from "@components/ui/GroupBadgeIcon";
8+
import { PageNotFound } from "@components/ui/PageNotFound";
9+
import { RestrictedAccess } from "@components/ui/RestrictedAccess";
10+
import useRedirect from "@hooks/useRedirect";
11+
import useFetchApi from "@utils/api";
12+
import { cn, singularize } from "@utils/helpers";
13+
import { FolderGit2Icon, Layers3Icon, PencilIcon } from "lucide-react";
14+
import { useSearchParams } from "next/navigation";
15+
import React, { useState } from "react";
16+
import AccessControlIcon from "@/assets/icons/AccessControlIcon";
17+
import DNSIcon from "@/assets/icons/DNSIcon";
18+
import NetworkRoutesIcon from "@/assets/icons/NetworkRoutesIcon";
19+
import PeerIcon from "@/assets/icons/PeerIcon";
20+
import SetupKeysIcon from "@/assets/icons/SetupKeysIcon";
21+
import TeamIcon from "@/assets/icons/TeamIcon";
22+
import { GroupProvider, useGroupContext } from "@/contexts/GroupProvider";
23+
import { usePermissions } from "@/contexts/PermissionsProvider";
24+
import RoutesProvider from "@/contexts/RoutesProvider";
25+
import { Group, GROUP_TOOLTIP_TEXT } from "@/interfaces/Group";
26+
import PageContainer from "@/layouts/PageContainer";
27+
import { GroupNameserversSection } from "@/modules/groups/details/GroupNameserversSection";
28+
import { GroupNetworkRoutesSection } from "@/modules/groups/details/GroupNetworkRoutesSection";
29+
import { GroupPeersSection } from "@/modules/groups/details/GroupPeersSection";
30+
import { GroupPoliciesSection } from "@/modules/groups/details/GroupPoliciesSection";
31+
import { GroupResourcesSection } from "@/modules/groups/details/GroupResourcesSection";
32+
import { GroupSetupKeysSection } from "@/modules/groups/details/GroupSetupKeysSection";
33+
import { GroupUsersSection } from "@/modules/groups/details/GroupUsersSection";
34+
import useGroupDetails from "@/modules/groups/details/useGroupDetails";
35+
36+
export default function GroupPage() {
37+
const queryParameter = useSearchParams();
38+
const { isRestricted } = usePermissions();
39+
const groupId = queryParameter.get("id");
40+
const {
41+
data: group,
42+
isLoading,
43+
error,
44+
} = useFetchApi<Group>(`/groups/${groupId}`, true);
45+
46+
useRedirect("/groups", false, !groupId || isRestricted);
47+
48+
if (isRestricted) {
49+
return (
50+
<PageContainer>
51+
<RestrictedAccess page={"Group Information"} />
52+
</PageContainer>
53+
);
54+
}
55+
56+
if (error)
57+
return (
58+
<PageNotFound
59+
title={error?.message}
60+
description={
61+
"The group you are attempting to access cannot be found. It may have been deleted, or you may not have permission to view it. Please verify the URL or return to the dashboard."
62+
}
63+
/>
64+
);
65+
66+
return group && !isLoading ? (
67+
<PageContainer>
68+
<RoutesProvider>
69+
<GroupProvider group={group} isDetailPage={true}>
70+
<div className={"p-default py-6 pb-0 w-full mb-[6px]"}>
71+
<Breadcrumbs>
72+
<Breadcrumbs.Item
73+
href={"/groups"}
74+
label={"Groups"}
75+
icon={<FolderGit2Icon size={14} />}
76+
/>
77+
<Breadcrumbs.Item label={group.name} active />
78+
</Breadcrumbs>
79+
<GroupDetailsName />
80+
</div>
81+
<GroupOverviewTabs group={group} />
82+
</GroupProvider>
83+
</RoutesProvider>
84+
</PageContainer>
85+
) : (
86+
<FullScreenLoading />
87+
);
88+
}
89+
90+
const GroupDetailsName = () => {
91+
const { group, isJWTGroup, isAllowedToRename, openGroupRenameModal } =
92+
useGroupContext();
93+
const { permission } = usePermissions();
94+
95+
return (
96+
<div className={"w-full"}>
97+
<h1 className={"flex items-center gap-3 w-full whitespace-nowrap"}>
98+
<GroupBadgeIcon id={group?.id} issued={group?.issued} size={20} />
99+
{group.name}
100+
{group.name !== "All" && permission?.groups?.update && (
101+
<div>
102+
<FullTooltip
103+
content={
104+
<div className={"text-xs max-w-xs"}>
105+
{isJWTGroup
106+
? GROUP_TOOLTIP_TEXT.RENAME.JWT
107+
: GROUP_TOOLTIP_TEXT.RENAME.INTEGRATION}
108+
</div>
109+
}
110+
interactive={false}
111+
disabled={isAllowedToRename}
112+
className={"w-full block"}
113+
>
114+
<div
115+
className={cn(
116+
"flex h-8 w-8 items-center justify-center gap-2 dark:text-neutral-300 text-neutral-500 hover:text-neutral-100 transition-all hover:bg-nb-gray-800/60 rounded-md cursor-pointer",
117+
!isAllowedToRename &&
118+
"opacity-40 cursor-not-allowed pointer-events-none",
119+
)}
120+
onClick={openGroupRenameModal}
121+
>
122+
<PencilIcon size={16} />
123+
</div>
124+
</FullTooltip>
125+
</div>
126+
)}
127+
</h1>
128+
</div>
129+
);
130+
};
131+
132+
const validAllGroupTabs = [
133+
"policies",
134+
"resources",
135+
"network-routes",
136+
"nameservers",
137+
];
138+
const validOtherGroupTabs = ["users", "peers", "setup-keys"];
139+
140+
const GroupOverviewTabs = ({ group }: { group: Group }) => {
141+
const searchParams = useSearchParams();
142+
143+
const getInitialTab = () => {
144+
const isAllGroup = group.name === "All";
145+
const tabParam = searchParams.get("tab");
146+
const validTabs = isAllGroup
147+
? validAllGroupTabs
148+
: [...validAllGroupTabs, ...validOtherGroupTabs];
149+
if (tabParam === null) return isAllGroup ? "policies" : "users";
150+
if (isAllGroup) {
151+
return validTabs.includes(tabParam) ? tabParam : "policies";
152+
}
153+
return validTabs.includes(tabParam) ? tabParam : "users";
154+
};
155+
156+
const [tab, setTab] = useState(getInitialTab());
157+
const groupDetails = useGroupDetails(group?.id || "");
158+
159+
const peersCount = groupDetails?.peers_count || 0;
160+
const usersCount = groupDetails?.users?.length || 0;
161+
const policiesCount = groupDetails?.policies?.length || 0;
162+
const resourcesCount = groupDetails?.resources_count || 0;
163+
const routesCount = groupDetails?.routes?.length || 0;
164+
const nameserversCount = groupDetails?.nameservers?.length || 0;
165+
const setupKeysCount = groupDetails?.setupKeys?.length || 0;
166+
167+
return (
168+
<Tabs
169+
defaultValue={tab}
170+
onValueChange={(v) => setTab(v)}
171+
value={tab}
172+
className={"pt-2 pb-0 mb-0"}
173+
>
174+
<TabsList justify={"start"} className={"px-8"}>
175+
{group.name !== "All" && (
176+
<TabsTrigger
177+
value={"users"}
178+
className={groupDetails === null ? "animate-pulse" : ""}
179+
>
180+
<TeamIcon
181+
size={12}
182+
className={
183+
"fill-nb-gray-500 group-data-[state=active]/trigger:fill-netbird transition-all"
184+
}
185+
/>
186+
{singularize("Users", usersCount)}
187+
</TabsTrigger>
188+
)}
189+
190+
{group.name !== "All" && (
191+
<TabsTrigger
192+
value={"peers"}
193+
className={groupDetails === null ? "animate-pulse" : ""}
194+
>
195+
<PeerIcon
196+
size={12}
197+
className={
198+
"fill-nb-gray-500 group-data-[state=active]/trigger:fill-netbird transition-all"
199+
}
200+
/>
201+
{singularize("Peers", peersCount)}
202+
</TabsTrigger>
203+
)}
204+
205+
<TabsTrigger
206+
value={"policies"}
207+
className={groupDetails === null ? "animate-pulse" : ""}
208+
>
209+
<AccessControlIcon
210+
size={12}
211+
className={
212+
"fill-nb-gray-500 group-data-[state=active]/trigger:fill-netbird transition-all"
213+
}
214+
/>
215+
{singularize("Policies", policiesCount)}
216+
</TabsTrigger>
217+
218+
<TabsTrigger
219+
value={"resources"}
220+
className={groupDetails === null ? "animate-pulse" : ""}
221+
>
222+
<Layers3Icon size={14} />
223+
{singularize("Resources", resourcesCount)}
224+
</TabsTrigger>
225+
226+
<TabsTrigger
227+
value={"network-routes"}
228+
className={groupDetails === null ? "animate-pulse" : ""}
229+
>
230+
<NetworkRoutesIcon
231+
size={12}
232+
className={
233+
"fill-nb-gray-500 group-data-[state=active]/trigger:fill-netbird transition-all"
234+
}
235+
/>
236+
{singularize("Network Routes", routesCount)}
237+
</TabsTrigger>
238+
239+
<TabsTrigger
240+
value={"nameservers"}
241+
className={groupDetails === null ? "animate-pulse" : ""}
242+
>
243+
<DNSIcon
244+
size={12}
245+
className={
246+
"fill-nb-gray-500 group-data-[state=active]/trigger:fill-netbird transition-all"
247+
}
248+
/>
249+
{singularize("Nameservers", nameserversCount)}
250+
</TabsTrigger>
251+
252+
{group.name !== "All" && (
253+
<TabsTrigger
254+
value={"setup-keys"}
255+
className={groupDetails === null ? "animate-pulse" : ""}
256+
>
257+
<SetupKeysIcon
258+
size={12}
259+
className={
260+
"fill-nb-gray-500 group-data-[state=active]/trigger:fill-netbird transition-all"
261+
}
262+
/>
263+
{singularize("Setup Keys", setupKeysCount)}
264+
</TabsTrigger>
265+
)}
266+
</TabsList>
267+
268+
<TabsContent value={"users"} className={"pb-8"}>
269+
<GroupUsersSection users={groupDetails?.users} />
270+
</TabsContent>
271+
272+
<TabsContent value={"peers"} className={"pb-8"}>
273+
<GroupPeersSection peers={groupDetails?.peersOfGroup} />
274+
</TabsContent>
275+
276+
<TabsContent value={"policies"} className={"pb-8"}>
277+
<GroupPoliciesSection policies={groupDetails?.policies} />
278+
</TabsContent>
279+
280+
<TabsContent value={"resources"} className={"pb-8"}>
281+
<GroupResourcesSection resources={groupDetails?.networkResources} />
282+
</TabsContent>
283+
284+
<TabsContent value={"network-routes"} className={"pb-8"}>
285+
<GroupNetworkRoutesSection routes={groupDetails?.routes} />
286+
</TabsContent>
287+
288+
<TabsContent value={"nameservers"} className={"pb-8"}>
289+
<GroupNameserversSection nameserverGroups={groupDetails?.nameservers} />
290+
</TabsContent>
291+
292+
<TabsContent value={"setup-keys"} className={"pb-8"}>
293+
<GroupSetupKeysSection setupKeys={groupDetails?.setupKeys} />
294+
</TabsContent>
295+
</Tabs>
296+
);
297+
};
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { globalMetaTitle } from "@utils/meta";
2+
import type { Metadata } from "next";
3+
import BlankLayout from "@/layouts/BlankLayout";
4+
5+
export const metadata: Metadata = {
6+
title: `Groups - ${globalMetaTitle}`,
7+
};
8+
export default BlankLayout;
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
"use client";
2+
3+
import Paragraph from "@components/Paragraph";
4+
import SkeletonTable from "@components/skeletons/SkeletonTable";
5+
import { RestrictedAccess } from "@components/ui/RestrictedAccess";
6+
import { usePortalElement } from "@hooks/usePortalElement";
7+
import { ExternalLinkIcon, FolderGit2Icon } from "lucide-react";
8+
import React, { lazy, Suspense } from "react";
9+
import Breadcrumbs from "@/components/Breadcrumbs";
10+
import InlineLink from "@/components/InlineLink";
11+
import { usePermissions } from "@/contexts/PermissionsProvider";
12+
import PageContainer from "@/layouts/PageContainer";
13+
14+
const GroupsTable = lazy(() => import("@/modules/groups/table/GroupsTable"));
15+
16+
export default function GroupsPage() {
17+
const { permission } = usePermissions();
18+
const { ref: headingRef, portalTarget } =
19+
usePortalElement<HTMLHeadingElement>();
20+
21+
return (
22+
<PageContainer>
23+
<div className={"p-default py-6"}>
24+
<Breadcrumbs>
25+
<Breadcrumbs.Item
26+
href={"/groups"}
27+
label={"Groups"}
28+
icon={<FolderGit2Icon size={14} />}
29+
active
30+
/>
31+
</Breadcrumbs>
32+
<h1 ref={headingRef}>Groups</h1>
33+
<Paragraph>
34+
Here is the overview of the groups of your organization. You can
35+
delete the unused ones.
36+
</Paragraph>
37+
<Paragraph>
38+
Learn more about{" "}
39+
<InlineLink
40+
href={"https://docs.netbird.io/how-to/manage-network-access"}
41+
target={"_blank"}
42+
>
43+
Groups
44+
<ExternalLinkIcon size={12} />
45+
</InlineLink>
46+
in our documentation.
47+
</Paragraph>
48+
</div>
49+
<RestrictedAccess hasAccess={permission.groups.read} page={"Groups"}>
50+
<Suspense fallback={<SkeletonTable />}>
51+
<GroupsTable headingTarget={portalTarget} />
52+
</Suspense>
53+
</RestrictedAccess>
54+
</PageContainer>
55+
);
56+
}

0 commit comments

Comments
 (0)