RoleImpersonationPopover.tsx101 lines · main
| 1 | import { RoleImpersonationSelector } from '.' |
| 2 | import { ChevronDown, User as IconUser } from 'lucide-react' |
| 3 | import { useState } from 'react' |
| 4 | import { Button, cn, Popover, PopoverContent, PopoverTrigger } from 'ui' |
| 5 | |
| 6 | import { getAvatarUrl, getDisplayName } from '../Auth/Users/Users.utils' |
| 7 | import type { User } from '@/data/auth/users-infinite-query' |
| 8 | import { useRoleImpersonationStateSnapshot } from '@/state/role-impersonation-state' |
| 9 | |
| 10 | export interface RoleImpersonationPopoverProps { |
| 11 | header?: string |
| 12 | serviceRoleLabel?: string |
| 13 | variant?: 'regular' | 'connected-on-right' | 'connected-on-left' | 'connected-on-both' |
| 14 | align?: 'center' | 'start' | 'end' |
| 15 | disallowAuthenticatedOption?: boolean |
| 16 | } |
| 17 | |
| 18 | export const RoleImpersonationPopover = ({ |
| 19 | header, |
| 20 | serviceRoleLabel, |
| 21 | variant = 'regular', |
| 22 | align = 'end', |
| 23 | disallowAuthenticatedOption = false, |
| 24 | }: RoleImpersonationPopoverProps) => { |
| 25 | const state = useRoleImpersonationStateSnapshot() |
| 26 | |
| 27 | const [isOpen, setIsOpen] = useState(false) |
| 28 | |
| 29 | const currentRole = state.role?.role ?? serviceRoleLabel ?? 'postgres' |
| 30 | |
| 31 | return ( |
| 32 | <Popover open={isOpen} onOpenChange={setIsOpen}> |
| 33 | <PopoverTrigger asChild> |
| 34 | <Button |
| 35 | size="tiny" |
| 36 | type="default" |
| 37 | className={cn( |
| 38 | 'h-[26px] pr-3 gap-0', |
| 39 | variant === 'connected-on-right' && 'rounded-r-none border-r-0', |
| 40 | variant === 'connected-on-left' && 'rounded-l-none border-l-0', |
| 41 | variant === 'connected-on-both' && 'rounded-none border-x-0' |
| 42 | )} |
| 43 | > |
| 44 | <div className="flex items-center gap-1"> |
| 45 | <span className="text-foreground-muted">Role</span> |
| 46 | <span>{currentRole}</span> |
| 47 | {state.role?.type === 'postgrest' && state.role.role === 'authenticated' && ( |
| 48 | <> |
| 49 | {state.role.userType === 'native' && state.role.user ? ( |
| 50 | <UserRoleButtonSection user={state.role.user} /> |
| 51 | ) : state.role.userType === 'external' && state.role.externalAuth ? ( |
| 52 | <ExternalAuthButtonSection sub={state.role.externalAuth.sub} /> |
| 53 | ) : null} |
| 54 | <span className="text-xs text-foreground-lighter font-light"> |
| 55 | {state.role.aal === 'aal2' ? 'AAL2' : 'AAL1'} |
| 56 | </span> |
| 57 | </> |
| 58 | )} |
| 59 | <ChevronDown className="text-muted" strokeWidth={1} size={12} /> |
| 60 | </div> |
| 61 | </Button> |
| 62 | </PopoverTrigger> |
| 63 | <PopoverContent className="p-0 overflow-hidden w-min" side="bottom" align={align}> |
| 64 | <RoleImpersonationSelector |
| 65 | header={header} |
| 66 | serviceRoleLabel={serviceRoleLabel} |
| 67 | disallowAuthenticatedOption={disallowAuthenticatedOption} |
| 68 | /> |
| 69 | </PopoverContent> |
| 70 | </Popover> |
| 71 | ) |
| 72 | } |
| 73 | |
| 74 | const UserRoleButtonSection = ({ user }: { user: User }) => { |
| 75 | const avatarUrl = getAvatarUrl(user) |
| 76 | const displayName = getDisplayName(user, user.email ?? user.phone ?? user.id ?? 'Unknown') |
| 77 | |
| 78 | return ( |
| 79 | <div className="flex gap-1 items-center pl-0.5 pr-1.5 h-[21px] bg-surface-200 rounded-full overflow-hidden"> |
| 80 | {avatarUrl ? ( |
| 81 | <img className="rounded-full w-[18px] h-[18px]" src={avatarUrl} alt={displayName} /> |
| 82 | ) : ( |
| 83 | <div className="rounded-full w-[18px] h-[18px] bg-surface-100 border flex items-center justify-center text-light"> |
| 84 | <IconUser size={12} strokeWidth={2} /> |
| 85 | </div> |
| 86 | )} |
| 87 | <span className="truncate max-w-[84px]">{displayName}</span> |
| 88 | </div> |
| 89 | ) |
| 90 | } |
| 91 | |
| 92 | const ExternalAuthButtonSection = ({ sub }: { sub: string }) => { |
| 93 | return ( |
| 94 | <div className="flex gap-1 items-center pl-0.5 pr-1.5 h-[21px] bg-surface-200 rounded-full overflow-hidden"> |
| 95 | <div className="rounded-full w-[18px] h-[18px] bg-surface-100 border flex items-center justify-center text-light"> |
| 96 | <IconUser size={12} strokeWidth={2} /> |
| 97 | </div> |
| 98 | <span className="truncate max-w-[84px]">{sub}</span> |
| 99 | </div> |
| 100 | ) |
| 101 | } |