RoleImpersonationPopover.tsx 3.9 KB

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