|
| 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 | +}; |
0 commit comments