mirror of
https://github.com/supabase/supabase.git
synced 2026-06-18 05:33:50 +08:00
## Problem The `_Shadcn_` suffix isn't needed anymore on `HoverCard` components ## Solution Remove it. No other changes <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Refactor** * Standardized hover-card component usage across the apps and design system for consistent behavior and markup. * No user-facing changes — hover previews, tooltips, snippet/template previews, and code hover panels retain the same appearance and interactions. <!-- review_stack_entry_start --> [](https://app.coderabbit.ai/change-stack/supabase/supabase/pull/45987) <!-- review_stack_entry_end --> <!-- end of auto-generated comment: release notes by coderabbit.ai -->
196 lines
7.9 KiB
TypeScript
196 lines
7.9 KiB
TypeScript
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
|
|
] ?? 'supabase',
|
|
}}
|
|
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>
|
|
)
|
|
}
|