-
Notifications
You must be signed in to change notification settings - Fork 89
move group membership from the settings menu, into the Team menu #492
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
aliamerj
wants to merge
24
commits into
netbirdio:main
Choose a base branch
from
aliamerj:change-groups-location
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from 3 commits
Commits
Show all changes
24 commits
Select commit
Hold shift + click to select a range
f6930c1
move our group membership from the settings menu, into the Team menu
aliamerj bac47d7
add action to the table and new group page
aliamerj 924185e
update group page and return group settings to settings menu
aliamerj fdbd649
new update
aliamerj e6f8c7c
fix bug
aliamerj f457c4d
group action: add peer to group
aliamerj 984825e
group action: add user to group
aliamerj 85ee6f4
Update wording, redirect to group page after creation
heisbrot 204f18f
Add better table loading skeleton
heisbrot 1b7e883
Adjust group name cell
heisbrot 76ca81c
Update wording
heisbrot fdca5bb
Update sort order
heisbrot a871dc2
Refactor
heisbrot 5ee0d89
Merge remote-tracking branch 'origin/main' into change-groups-location
heisbrot 919842a
Merge main
heisbrot 0cbf3ff
Fix button height
heisbrot 05cbff0
Fix resources table
heisbrot 3536725
Adjust table loading skeleton
heisbrot cbe33ae
Adjust table loading skeleton
heisbrot 44ffa10
Add loading to tab triggers
heisbrot de3e794
Update meta
heisbrot 671070f
Update group location
heisbrot 107f8f2
Fix rename
heisbrot 98439ff
Refactor group details
heisbrot File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| import { globalMetaTitle } from "@utils/meta"; | ||
| import type { Metadata } from "next"; | ||
| import BlankLayout from "@/layouts/BlankLayout"; | ||
|
|
||
| export const metadata: Metadata = { | ||
| title: `Group - Groups - ${globalMetaTitle}`, | ||
| }; | ||
| export default BlankLayout; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,351 @@ | ||
| "use client"; | ||
| import { notify } from "@components/Notification"; | ||
| import Breadcrumbs from "@components/Breadcrumbs"; | ||
| import Button from "@components/Button"; | ||
| import Card from "@components/Card"; | ||
| import { Tabs, TabsContent, TabsList, TabsTrigger } from "@components/Tabs"; | ||
| import FullScreenLoading from "@components/ui/FullScreenLoading"; | ||
| import { PageNotFound } from "@components/ui/PageNotFound"; | ||
| import { RestrictedAccess } from "@components/ui/RestrictedAccess"; | ||
| import TextWithTooltip from "@components/ui/TextWithTooltip"; | ||
| import useRedirect from "@hooks/useRedirect"; | ||
| import useFetchApi, { useApiCall } from "@utils/api"; | ||
| import { | ||
| ExternalLinkIcon, | ||
| FolderGit2Icon, | ||
| KeyIcon, | ||
| KeyRoundIcon, | ||
| MonitorSmartphoneIcon, | ||
| PencilIcon, | ||
| ServerIcon, | ||
| UserCheckIcon, | ||
| UsersIcon, | ||
| } from "lucide-react"; | ||
| import { useSearchParams } from "next/navigation"; | ||
| import React, { useMemo, useState } from "react"; | ||
| import { useSWRConfig } from "swr"; | ||
| import { usePermissions } from "@/contexts/PermissionsProvider"; | ||
| import RoutesProvider from "@/contexts/RoutesProvider"; | ||
| import type { Group } from "@/interfaces/Group"; | ||
| import PageContainer from "@/layouts/PageContainer"; | ||
| import { SetupKey } from "@/interfaces/SetupKey"; | ||
| import { EditGroupNameModal } from "@/modules/groups/EditGroupNameModal"; | ||
| import { useGroupIdentification } from "@/modules/groups/useGroupIdentification"; | ||
|
|
||
| export default function GroupPage() { | ||
| const queryParameter = useSearchParams(); | ||
| const { isRestricted } = usePermissions(); | ||
| const groupId = queryParameter.get("id"); | ||
| const { | ||
| data: group, | ||
| isLoading, | ||
| error, | ||
| } = useFetchApi<Group>("/groups/" + groupId, true); | ||
|
|
||
| useRedirect("/team/groups", false, !groupId || isRestricted); | ||
|
|
||
| if (isRestricted) { | ||
| return ( | ||
| <PageContainer> | ||
| <RestrictedAccess page={"Group Information"} /> | ||
| </PageContainer> | ||
| ); | ||
| } | ||
|
|
||
| if (error) | ||
| return ( | ||
| <PageNotFound | ||
| title={error?.message} | ||
| description={ | ||
| "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." | ||
| } | ||
| /> | ||
| ); | ||
|
|
||
| return group && !isLoading ? ( | ||
| <GroupOverview group={group} /> | ||
| ) : ( | ||
| <FullScreenLoading /> | ||
| ); | ||
| } | ||
|
|
||
| function GroupOverview({ group }: { group: Group }) { | ||
| return ( | ||
| <PageContainer> | ||
| <RoutesProvider> | ||
| <div className={"p-default py-6 pb-0"}> | ||
| <Breadcrumbs> | ||
| <Breadcrumbs.Item | ||
| href={"/team/groups"} | ||
| label={"Groups"} | ||
| icon={<FolderGit2Icon size={13} />} | ||
| /> | ||
| <Breadcrumbs.Item label={group.name} active /> | ||
| </Breadcrumbs> | ||
| <GroupGeneralInformation group={group} /> | ||
| </div> | ||
| <GroupOverviewTabs group={group} /> | ||
| </RoutesProvider> | ||
| </PageContainer> | ||
| ); | ||
| } | ||
|
|
||
|
|
||
| const GroupGeneralInformation = ({ group }: { group: Group }) => { | ||
| const { mutate } = useSWRConfig(); | ||
| const [showEditNameModal, setShowEditNameModal] = useState(false); | ||
| const groupRequest = useApiCall<SetupKey>("/groups/" + group.id); | ||
| const { permission } = usePermissions(); | ||
| const { isRegularGroup, isJWTGroup } = useGroupIdentification({ | ||
| id: group?.id, | ||
| issued: group?.issued, | ||
| }); | ||
| const updatePermission = useMemo(() => { | ||
| //todo : @Eduard can you check the logic here for group rename | ||
| let rename = true; | ||
|
|
||
| // Rename logic | ||
| if (permission.groups.update) rename = false; | ||
| if (isJWTGroup) rename = true; // maybe JWT groups can't be renamed? | ||
| if (!isRegularGroup) rename = true; | ||
|
|
||
| return { rename }; | ||
| }, [permission, isJWTGroup, isRegularGroup]); | ||
|
|
||
| const onGroupNameUpdate = (name: string) => { | ||
| notify({ | ||
| title: "Group: " + group.name, | ||
| description: "Group was successfully Rename.", | ||
heisbrot marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| promise: groupRequest.put({ name: name }).then(() => { | ||
| mutate("/groups/" + group.id); | ||
| }), | ||
| loadingMessage: "Renaming the group...", | ||
| }); | ||
| setShowEditNameModal(false); | ||
| }; | ||
|
|
||
| return ( | ||
| <> | ||
| {showEditNameModal && ( | ||
| <EditGroupNameModal | ||
| initialName={group.name} | ||
| open={showEditNameModal} | ||
| onOpenChange={setShowEditNameModal} | ||
| onSuccess={onGroupNameUpdate} | ||
| /> | ||
| )} | ||
| <div className={"flex justify-between max-w-6xl items-start"}> | ||
| <div> | ||
| <div className={"flex items-center gap-3"}> | ||
| <h1 className={"flex items-center gap-3"}> | ||
| <FolderGit2Icon size={20} className={"mb-[3px] shrink-0"} /> | ||
| <TextWithTooltip text={group.name} maxChars={30} /> | ||
| {updatePermission && ( | ||
| <div | ||
| className={ | ||
| "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" | ||
| } | ||
| onClick={() => setShowEditNameModal(true)} | ||
| > | ||
| <PencilIcon size={16} /> | ||
| </div> | ||
| )} | ||
| </h1> | ||
| </div> | ||
| </div> | ||
| </div> | ||
|
|
||
| <div | ||
| className={ | ||
| "flex-wrap xl:flex-nowrap flex gap-10 w-full mt-5 max-w-6xl items-start" | ||
| } | ||
| > | ||
| <GroupInformationCard group={group} /> | ||
| </div> | ||
| </> | ||
| ); | ||
| }; | ||
|
|
||
| function GroupInformationCard({ group }: { group: Group }) { | ||
| return ( | ||
| <Card className={"w-full xl:w-1/2"}> | ||
| <Card.List> | ||
| <Card.ListItem | ||
| copy | ||
| copyText={group.id} | ||
| label={ | ||
| <> | ||
| <KeyIcon size={16} /> | ||
| Group ID | ||
| </> | ||
| } | ||
| value={group.id} | ||
| /> | ||
|
|
||
| <Card.ListItem | ||
| label={ | ||
| <> | ||
| <UsersIcon size={16} /> | ||
| Group Name | ||
| </> | ||
| } | ||
| value={group.name} | ||
| /> | ||
|
|
||
| <Card.ListItem | ||
| label={ | ||
| <> | ||
| <UserCheckIcon size={16} /> | ||
| Total Peers | ||
| </> | ||
| } | ||
| value={group.peers_count?.toString() ?? "0"} | ||
| /> | ||
|
|
||
| <Card.ListItem | ||
| label={ | ||
| <> | ||
| <ServerIcon size={16} /> | ||
| Total Resources | ||
| </> | ||
| } | ||
| value={group.resources_count?.toString() ?? "0"} | ||
| /> | ||
|
|
||
| <Card.ListItem | ||
| label={ | ||
| <> | ||
| <KeyRoundIcon size={16} /> | ||
| Issued By | ||
| </> | ||
| } | ||
| value={group.issued} | ||
| /> | ||
| </Card.List> | ||
| </Card> | ||
| ); | ||
| } | ||
|
|
||
| const GroupOverviewTabs = ({ group }: { group: Group }) => { | ||
| const [tab, setTab] = useState("peers"); | ||
|
|
||
| return ( | ||
| <Tabs | ||
| defaultValue={tab} | ||
| onValueChange={(v) => setTab(v)} | ||
| value={tab} | ||
| className={"pt-10 pb-0 mb-0"} | ||
| > | ||
| <TabsList justify={"start"} className={"px-8"}> | ||
| <TabsTrigger value={"peers"}> | ||
| <UsersIcon size={16} /> | ||
| Peers ({group.peers_count}) | ||
| </TabsTrigger> | ||
|
|
||
| <TabsTrigger value={"resources"}> | ||
| <ServerIcon size={16} /> | ||
| Resources ({group.resources_count}) | ||
| </TabsTrigger> | ||
| </TabsList> | ||
|
|
||
| <TabsContent value={"peers"} className={"pb-8"}> | ||
| <GroupPeersSection group={group} /> | ||
| </TabsContent> | ||
|
|
||
| <TabsContent value={"resources"} className={"pb-8"}> | ||
| <GroupResourcesSection group={group} /> | ||
| </TabsContent> | ||
| </Tabs> | ||
| ); | ||
| }; | ||
|
|
||
| // Peer List Component for the Peers tab | ||
| const GroupPeersSection = ({ group }: { group: Group }) => { | ||
| return ( | ||
| <div className="p-default"> | ||
| <div className="mb-6"> | ||
| <h3 className="text-lg font-medium">Peers in this Group</h3> | ||
| <p className="text-nb-gray-300 text-sm"> | ||
| List of all peers that are members of this group | ||
| </p> | ||
| </div> | ||
|
|
||
| <Card> | ||
| <Card.List> | ||
| {group.peers && group.peers.length > 0 ? ( | ||
| group.peers.map((peer) => { | ||
| //todo : @Eduard can you check this | ||
heisbrot marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| if (typeof peer === "string" || !peer?.id) return null; | ||
| return (<Card.ListItem | ||
| key={peer.id} | ||
| label={ | ||
| <div className="flex items-center gap-2"> | ||
| <MonitorSmartphoneIcon size={14} /> | ||
| <span>{peer.name}</span> | ||
| </div> | ||
| } | ||
| value={ | ||
| <div className="flex items-center gap-2"> | ||
| <span className="text-xs text-nb-gray-400">ID: {peer.id}</span> | ||
| </div> | ||
| } | ||
| /> | ||
| ) | ||
| })) : ( | ||
| <div className="p-8 text-center text-nb-gray-400"> | ||
| <UsersIcon size={32} className="mx-auto mb-2 opacity-50" /> | ||
| <p>No peers assigned to this group</p> | ||
| </div> | ||
| )} | ||
| </Card.List> | ||
| </Card> | ||
| </div> | ||
| ); | ||
| }; | ||
| // Resources List Component for the Resources tab | ||
| const GroupResourcesSection = ({ group }: { group: Group }) => { | ||
| return ( | ||
| <div className="p-default"> | ||
| <div className="mb-6"> | ||
| <h3 className="text-lg font-medium">Resources in this Group</h3> | ||
| <p className="text-nb-gray-300 text-sm"> | ||
| List of all resources that are accessible through this group | ||
| </p> | ||
| </div> | ||
|
|
||
| <Card> | ||
| <Card.List> | ||
| {group.resources && group.resources.length > 0 ? ( | ||
| group.resources.map((resource) => { | ||
| //todo : @Eduard can you check this | ||
heisbrot marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| if (typeof resource === "string" || !resource?.id) return null; | ||
| return (<Card.ListItem | ||
| key={resource.id} | ||
| label={ | ||
| <div className="flex items-center gap-2"> | ||
| <ServerIcon size={14} /> | ||
| <span className="capitalize">{resource.type}</span> | ||
| </div> | ||
| } | ||
| value={ | ||
| <div className="flex items-center gap-2"> | ||
| <span className="text-xs text-nb-gray-400">ID: {resource.id}</span> | ||
| <Button variant='secondary' size="sm"> | ||
| <ExternalLinkIcon size={12} /> | ||
| </Button> | ||
| </div> | ||
| } | ||
| /> | ||
| ) | ||
| }) | ||
| ) : ( | ||
| <div className="p-8 text-center text-nb-gray-400"> | ||
| <ServerIcon size={32} className="mx-auto mb-2 opacity-50" /> | ||
| <p>No resources assigned to this group</p> | ||
| </div> | ||
| )} | ||
| </Card.List> | ||
| </Card> | ||
| </div> | ||
| ); | ||
| }; | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| import { globalMetaTitle } from "@utils/meta"; | ||
| import type { Metadata } from "next"; | ||
| import BlankLayout from "@/layouts/BlankLayout"; | ||
|
|
||
| export const metadata: Metadata = { | ||
| title: `Group - Team - ${globalMetaTitle}`, | ||
| }; | ||
| export default BlankLayout; |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.