MembersView.tsx163 lines · main
1import { useParams } from 'common'
2import { partition } from 'lodash'
3import { AlertCircle } from 'lucide-react'
4import { useMemo } from 'react'
5import {
6 Card,
7 Loading,
8 Table,
9 TableBody,
10 TableCell,
11 TableFooter,
12 TableHead,
13 TableHeader,
14 TableRow,
15} from 'ui'
16import { Admonition } from 'ui-patterns'
17import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader'
18
19import { MemberRow } from './MemberRow'
20import AlertError from '@/components/ui/AlertError'
21import { useOrganizationRolesV2Query } from '@/data/organization-members/organization-roles-query'
22import { useOrganizationMembersQuery } from '@/data/organizations/organization-members-query'
23import { useProfile } from '@/lib/profile'
24
25export interface MembersViewProps {
26 searchString: string
27}
28
29const MembersView = ({ searchString }: MembersViewProps) => {
30 const { slug } = useParams()
31 const { profile } = useProfile()
32
33 const {
34 data: members = [],
35 error: membersError,
36 isPending: isLoadingMembers,
37 isError: isErrorMembers,
38 isSuccess: isSuccessMembers,
39 } = useOrganizationMembersQuery({ slug })
40 const {
41 data: roles,
42 error: rolesError,
43 isSuccess: isSuccessRoles,
44 isError: isErrorRoles,
45 } = useOrganizationRolesV2Query({
46 slug,
47 })
48
49 const filteredMembers = useMemo(() => {
50 return !searchString
51 ? members
52 : members.filter((member) => {
53 if (member.invited_at) {
54 return member.primary_email?.includes(searchString)
55 }
56 if (member.gotrue_id) {
57 return (
58 member.username.includes(searchString) || member.primary_email?.includes(searchString)
59 )
60 }
61 return false
62 })
63 }, [members, searchString])
64
65 const [[user], _otherMembers] = partition(
66 filteredMembers,
67 (m) => m.gotrue_id === profile?.gotrue_id
68 )
69
70 const userMember = members.find((m) => m.gotrue_id === profile?.gotrue_id)
71 const orgScopedRoleIds = (roles?.org_scoped_roles ?? []).map((r) => r.id)
72 const isOrgScopedRole = orgScopedRoleIds.includes(userMember?.role_ids?.[0] ?? -1)
73
74 // [Joshen] Temp wait on API level changes but I think it makes sense to hide invites for
75 // project scoped users since they can't see other members to begin with. Not a security issue nonetheless
76 const otherMembers = isOrgScopedRole
77 ? _otherMembers
78 : _otherMembers.filter((x) => !('invited_id' in x))
79 const sortedMembers = otherMembers.sort((a, b) =>
80 (a.primary_email ?? '').localeCompare(b.primary_email ?? '')
81 )
82
83 return (
84 <>
85 {isLoadingMembers && <GenericSkeletonLoader />}
86
87 {isErrorMembers && (
88 <AlertError error={membersError} subject="Failed to retrieve organization members" />
89 )}
90
91 {isErrorRoles && (
92 <AlertError error={rolesError} subject="Failed to retrieve organization roles" />
93 )}
94
95 {isSuccessMembers && (
96 <div className="rounded-sm w-full overflow-hidden overflow-x-scroll">
97 <Card>
98 <Loading active={!filteredMembers}>
99 <Table>
100 <TableHeader>
101 <TableRow>
102 <TableHead key="header-user">Member</TableHead>
103 <TableHead key="header-mfa">MFA</TableHead>
104 <TableHead key="header-role">Role</TableHead>
105 <TableHead key="header-action" />
106 </TableRow>
107 </TableHeader>
108
109 <TableBody>
110 {[
111 ...(isSuccessRoles && isSuccessMembers && !isOrgScopedRole
112 ? [
113 <TableRow key="project-scope-notice">
114 <TableCell colSpan={12} className="p-0!">
115 <Admonition
116 type="note"
117 title="You have limited visibility in this organization"
118 description="Your access is limited to specific projects, so you can’t see all members or settings."
119 className="border-0 rounded-none"
120 />
121 </TableCell>
122 </TableRow>,
123 ]
124 : []),
125 ...(!!user ? [<MemberRow key={user.gotrue_id} member={user} />] : []),
126 ...sortedMembers.map((member) => (
127 <MemberRow key={member.gotrue_id} member={member} />
128 )),
129 ...(searchString.length > 0 && filteredMembers.length === 0
130 ? [
131 <TableRow key="no-results" className="bg-panel-secondary-light">
132 <TableCell colSpan={12}>
133 <div className="flex items-center space-x-3 opacity-75">
134 <AlertCircle size={16} strokeWidth={2} />
135 <p className="text-foreground-light">
136 No members matched the search query "{searchString}"
137 </p>
138 </div>
139 </TableCell>
140 </TableRow>,
141 ]
142 : []),
143 ]}
144 </TableBody>
145 <TableFooter className="font-normal">
146 <TableRow className="border-b-0 [&>td]:hover:bg-inherit">
147 <TableCell colSpan={4} className="text-foreground-muted">
148 {searchString
149 ? `${filteredMembers.length} of ${members.length} ${members.length === 1 ? 'member' : 'members'}`
150 : `${members.length || 0} ${members.length === 1 ? 'member' : 'members'}`}
151 </TableCell>
152 </TableRow>
153 </TableFooter>
154 </Table>
155 </Loading>
156 </Card>
157 </div>
158 )}
159 </>
160 )
161}
162
163export default MembersView