RoleImpersonationPopover.tsx101 lines · main
1import { RoleImpersonationSelector } from '.'
2import { ChevronDown, User as IconUser } from 'lucide-react'
3import { useState } from 'react'
4import { Button, cn, Popover, PopoverContent, PopoverTrigger } from 'ui'
5
6import { getAvatarUrl, getDisplayName } from '../Auth/Users/Users.utils'
7import type { User } from '@/data/auth/users-infinite-query'
8import { useRoleImpersonationStateSnapshot } from '@/state/role-impersonation-state'
9
10export 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
18export 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
74const 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
92const 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}