ProjectAccessSection.tsx 7.2 KB

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