| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195 |
- import { useParams } from 'common'
- import { ArrowRight, Check, ChevronRight, User, X } from 'lucide-react'
- import Link from 'next/link'
- import { useMemo } from 'react'
- import {
- Badge,
- cn,
- HoverCard,
- HoverCardContent,
- HoverCardTrigger,
- ScrollArea,
- TableCell,
- TableRow,
- } from 'ui'
- import { ShimmeringLoader } from 'ui-patterns/ShimmeringLoader'
- import { isInviteExpired } from '../Organization.utils'
- import { MemberActions } from './MemberActions'
- import PartnerIcon from '@/components/ui/PartnerIcon'
- import { ProfileImage } from '@/components/ui/ProfileImage'
- import { useOrganizationRolesV2Query } from '@/data/organization-members/organization-roles-query'
- import { OrganizationMember } from '@/data/organizations/organization-members-query'
- import { useOrgProjectsInfiniteQuery } from '@/data/projects/org-projects-infinite-query'
- import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization'
- import { useProfile } from '@/lib/profile'
- interface MemberRowProps {
- member: OrganizationMember
- }
- const MEMBER_ORIGIN_TO_MANAGED_BY = {
- vercel: 'vercel-marketplace',
- } as const
- export const MemberRow = ({ member }: MemberRowProps) => {
- const { slug } = useParams()
- const { profile } = useProfile()
- const { data: selectedOrganization } = useSelectedOrganizationQuery()
- const { data: roles, isPending: isLoadingRoles } = useOrganizationRolesV2Query({
- slug: selectedOrganization?.slug,
- })
- const hasProjectScopedRoles = (roles?.project_scoped_roles ?? []).length > 0
- const { data: projectsData } = useOrgProjectsInfiniteQuery({ slug })
- const orgProjects =
- useMemo(() => projectsData?.pages.flatMap((page) => page.projects), [projectsData?.pages]) || []
- const isInvitedUser = Boolean(member.invited_id)
- // Use generic avatar for all team members instead of attempting to fetch from GitHub
- const profileImageUrl = undefined
- return (
- <TableRow>
- <TableCell>
- <div className="flex items-center gap-x-4">
- <ProfileImage
- alt={member.primary_email ?? member.username ?? ''}
- src={profileImageUrl}
- className="border rounded-full w-[32px] h-[32px] md:w-[40px] md:h-[40px]"
- placeholder={
- <div
- className={cn(
- 'w-[32px] h-[32px] md:w-[40px] md:h-[40px]',
- 'bg-surface-100 border border-overlay rounded-full text-foreground-lighter flex items-center justify-center'
- )}
- >
- <User size={20} strokeWidth={1.5} />
- </div>
- }
- />
- <div className="flex item-center gap-x-3">
- <p className="text-foreground-light truncate">{member.primary_email}</p>
- <div className="flex items-center gap-x-2">
- {member.gotrue_id === profile?.gotrue_id && <Badge>You</Badge>}
- {isInvitedUser && member.invited_at && (
- <Badge variant={isInviteExpired(member.invited_at) ? 'destructive' : 'warning'}>
- {isInviteExpired(member.invited_at) ? 'Expired' : 'Invited'}
- </Badge>
- )}
- {member.is_sso_user && <Badge variant="default">SSO</Badge>}
- {(member.metadata as any)?.origin && (
- <PartnerIcon
- organization={{
- managed_by:
- MEMBER_ORIGIN_TO_MANAGED_BY[
- (member.metadata as any).origin as keyof typeof MEMBER_ORIGIN_TO_MANAGED_BY
- ] ?? 'briven',
- }}
- tooltipText="Managed by Vercel Marketplace."
- />
- )}
- </div>
- </div>
- </div>
- </TableCell>
- <TableCell>
- <div className="flex items-center gap-x-1.5">
- {member.mfa_enabled ? (
- <>
- <span className="text-foreground-lighter">Enabled</span>
- <Check className="text-brand" strokeWidth={2} size={16} />
- </>
- ) : (
- <>
- <span className="text-foreground-lighter">Disabled</span>
- <X className="text-foreground-muted" strokeWidth={1.5} size={16} />
- </>
- )}
- </div>
- </TableCell>
- <TableCell className="max-w-64">
- {isLoadingRoles ? (
- <ShimmeringLoader className="w-32" />
- ) : (
- member.role_ids.map((id) => {
- const orgScopedRole = (roles?.org_scoped_roles ?? []).find((role) => role.id === id)
- const projectScopedRole = (roles?.project_scoped_roles ?? []).find(
- (role) => role.id === id
- )
- const role = orgScopedRole || projectScopedRole
- const roleName = (role?.name ?? '').split('_')[0]
- const projectsApplied =
- role?.projects.length === 0
- ? (orgProjects?.map((p) => p.name) ?? [])
- : (role?.projects ?? [])
- .map(({ ref }) => orgProjects?.find((p) => p.ref === ref)?.name ?? '')
- .filter((x) => x.length > 0)
- return (
- <div key={`role-${id}`} className="flex items-center gap-x-2">
- <p className="text-foreground-light">{roleName}</p>
- {hasProjectScopedRoles && (
- <>
- <ChevronRight className="text-foreground-muted/50" size={14} />
- {projectsApplied.length === 1 ? (
- <span className="text-foreground-light truncate" title={projectsApplied[0]}>
- {projectsApplied[0]}
- </span>
- ) : (
- <HoverCard openDelay={200}>
- <HoverCardTrigger asChild>
- <span className="text-foreground-light">
- {role?.projects.length === 0
- ? 'Organization'
- : `${projectsApplied.length} project${projectsApplied.length > 1 ? 's' : ''}`}
- </span>
- </HoverCardTrigger>
- <HoverCardContent className="p-0">
- <p className="p-2 text-xs">
- {roleName} role applies to {projectsApplied.length} project
- {projectsApplied.length > 1 ? 's' : ''}
- </p>
- <div className="border-t flex flex-col py-1">
- <ScrollArea
- className={cn(projectsApplied.length > 5 ? 'h-[130px]' : '')}
- >
- {projectsApplied.map((name) => {
- const ref = orgProjects?.find((p) => p.name === name)?.ref
- return (
- <Link
- key={name}
- href={`/project/${ref}`}
- className="px-2 py-1 group hover:bg-surface-300 hover:text-foreground transition flex items-center justify-between"
- >
- <span className="text-xs truncate max-w-[60%]">{name}</span>
- <span className="text-xs text-foreground flex items-center gap-x-1 opacity-0 group-hover:opacity-100 transition">
- Go to project
- <ArrowRight size={14} />
- </span>
- </Link>
- )
- })}
- </ScrollArea>
- </div>
- </HoverCardContent>
- </HoverCard>
- )}
- </>
- )}
- </div>
- )
- })
- )}
- </TableCell>
- <TableCell>
- <MemberActions member={member} />
- </TableCell>
- </TableRow>
- )
- }
|