ProjectAccessSection.tsx195 lines · main
1import Link from 'next/link'
2import {
3 Badge,
4 Button,
5 Card,
6 CardContent,
7 Table,
8 TableBody,
9 TableCell,
10 TableHead,
11 TableHeader,
12 TableRow,
13} from 'ui'
14import {
15 PageSection,
16 PageSectionContent,
17 PageSectionMeta,
18 PageSectionSummary,
19 PageSectionTitle,
20} from 'ui-patterns/PageSection'
21import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader'
22
23import { summarizeProjectAccess } from './General.utils'
24import AlertError from '@/components/ui/AlertError'
25import { useOrganizationRolesV2Query } from '@/data/organization-members/organization-roles-query'
26import { useOrganizationMembersQuery } from '@/data/organizations/organization-members-query'
27import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization'
28import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
29import { useProfile } from '@/lib/profile'
30
31export const ProjectAccessSection = () => {
32 const { data: project } = useSelectedProjectQuery()
33 const { data: organization } = useSelectedOrganizationQuery()
34 const { profile } = useProfile()
35
36 const isBranch = Boolean(project?.parent_project_ref)
37 const projectRef = project?.parent_project_ref ?? project?.ref
38
39 const {
40 data: organizationMembers = [],
41 error: organizationMembersError,
42 isPending: isLoadingOrganizationMembers,
43 isError: isErrorOrganizationMembers,
44 } = useOrganizationMembersQuery(
45 { slug: organization?.slug },
46 {
47 enabled: !!organization?.slug,
48 }
49 )
50 const {
51 data: organizationRoles,
52 error: organizationRolesError,
53 isPending: isLoadingOrganizationRoles,
54 isError: isErrorOrganizationRoles,
55 } = useOrganizationRolesV2Query(
56 { slug: organization?.slug },
57 {
58 enabled: !!organization?.slug,
59 }
60 )
61
62 const userMemberData = organizationMembers.find(
63 (member) => member.gotrue_id === profile?.gotrue_id
64 )
65 const orgScopedRoleIds = new Set(
66 (organizationRoles?.org_scoped_roles ?? []).map((role) => role.id)
67 )
68 const hasProjectScopedRoles = (organizationRoles?.project_scoped_roles ?? []).length > 0
69 const isOrgScopedRole = (userMemberData?.role_ids ?? []).some((roleId) =>
70 orgScopedRoleIds.has(roleId)
71 )
72 const hasLimitedVisibility = hasProjectScopedRoles && !isOrgScopedRole
73
74 const {
75 visibleMembers,
76 hiddenMembersCount,
77 projectMemberCount,
78 organizationMemberCount,
79 shouldShowOrgComparison,
80 hasOrganizationWideAccess,
81 } = summarizeProjectAccess({
82 organizationMembers,
83 roles: organizationRoles,
84 projectRef,
85 hasLimitedVisibility,
86 currentUserId: profile?.gotrue_id,
87 })
88
89 const isLoadingProjectAccess = isLoadingOrganizationMembers || isLoadingOrganizationRoles
90 const isErrorProjectAccess = isErrorOrganizationMembers || isErrorOrganizationRoles
91 const projectAccessError = organizationMembersError ?? organizationRolesError
92
93 if (isBranch) return null
94
95 const projectAccessTitle = hasLimitedVisibility
96 ? 'You have limited visibility in this organization'
97 : shouldShowOrgComparison && hasOrganizationWideAccess
98 ? 'Organization-wide access'
99 : 'Restricted project access'
100
101 const projectAccessDescription = hasLimitedVisibility
102 ? 'Your access is limited to specific projects, so you can’t see all members or settings.'
103 : shouldShowOrgComparison
104 ? hasOrganizationWideAccess
105 ? `All ${organizationMemberCount} organization members can access this project.`
106 : `${projectMemberCount} of ${organizationMemberCount} organization members can access this project.`
107 : `${projectMemberCount} project member${projectMemberCount === 1 ? '' : 's'} currently ${projectMemberCount === 1 ? 'has' : 'have'} access.`
108
109 return (
110 <PageSection>
111 <PageSectionMeta>
112 <PageSectionSummary>
113 <PageSectionTitle>Project access</PageSectionTitle>
114 </PageSectionSummary>
115 </PageSectionMeta>
116 <PageSectionContent>
117 {isErrorProjectAccess ? (
118 <AlertError error={projectAccessError} subject="Failed to retrieve project members" />
119 ) : (
120 <Card>
121 {isLoadingProjectAccess ? (
122 <CardContent>
123 <GenericSkeletonLoader />
124 </CardContent>
125 ) : (
126 <>
127 <CardContent className="flex flex-col gap-4">
128 <div className="flex flex-col @lg:flex-row @lg:items-center @lg:justify-between gap-3">
129 <div>
130 <p className="text-sm">{projectAccessTitle}</p>
131 <p className="text-sm text-foreground-light">{projectAccessDescription}</p>
132 </div>
133 {!!organization?.slug && (
134 <Button asChild type="default">
135 <Link href={`/org/${organization.slug}/team`}>
136 {hasLimitedVisibility ? 'View team' : 'Manage members'}
137 </Link>
138 </Button>
139 )}
140 </div>
141 </CardContent>
142
143 {visibleMembers.length > 0 ? (
144 <CardContent className="p-0">
145 <Table>
146 <TableHeader>
147 <TableRow>
148 <TableHead>Member</TableHead>
149 <TableHead className="w-[180px]">Role</TableHead>
150 </TableRow>
151 </TableHeader>
152 <TableBody>
153 {visibleMembers.map((member) => (
154 <TableRow key={member.id}>
155 <TableCell className="align-top">
156 <div className="flex items-center gap-2">
157 <p className="text-sm text-foreground break-all">{member.email}</p>
158 {member.id === profile?.gotrue_id && (
159 <Badge variant="default">You</Badge>
160 )}
161 </div>
162 </TableCell>
163 <TableCell className="align-top text-sm text-foreground-light w-[180px]">
164 {member.role ?? ''}
165 </TableCell>
166 </TableRow>
167 ))}
168 {hiddenMembersCount > 0 && (
169 <TableRow className="[&>td]:hover:bg-inherit">
170 <TableCell colSpan={2}>
171 <p className="text-sm text-foreground-lighter">
172 +{hiddenMembersCount} more project member
173 {hiddenMembersCount === 1 ? '' : 's'}
174 </p>
175 </TableCell>
176 </TableRow>
177 )}
178 </TableBody>
179 </Table>
180 </CardContent>
181 ) : (
182 <CardContent className="pt-0">
183 <p className="text-sm text-foreground-lighter">
184 No visible project members in your current access scope.
185 </p>
186 </CardContent>
187 )}
188 </>
189 )}
190 </Card>
191 )}
192 </PageSectionContent>
193 </PageSection>
194 )
195}