mirror of
https://github.com/supabase/supabase.git
synced 2026-06-15 08:05:21 +08:00
## Problem The `_Shadcn_` suffix isn't needed anymore on `Command` components ## Solution - Remove the `_Shadcn_` suffix - Simplify UI package exports - Apply prettier <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Refactor** * Simplified command component imports and exports across the UI library by removing internal naming aliases and adopting direct component references. Updated the public UI package barrel export to use wildcard re-exports for cleaner API surface. <!-- review_stack_entry_start --> [](https://app.coderabbit.ai/change-stack/supabase/supabase/pull/46153?utm_source=github_walkthrough&utm_medium=github&utm_campaign=change_stack) <!-- review_stack_entry_end --> <!-- end of auto-generated comment: release notes by coderabbit.ai -->
145 lines
4.8 KiB
TypeScript
145 lines
4.8 KiB
TypeScript
import { keepPreviousData } from '@tanstack/react-query'
|
|
import { useDebounce } from '@uidotdev/usehooks'
|
|
import { Check, ChevronsUpDown } from 'lucide-react'
|
|
import { useMemo, useState } from 'react'
|
|
import { toast } from 'sonner'
|
|
import {
|
|
Button,
|
|
cn,
|
|
Command,
|
|
CommandEmpty,
|
|
CommandGroup,
|
|
CommandInput,
|
|
CommandItem,
|
|
CommandList,
|
|
Popover,
|
|
PopoverContent,
|
|
PopoverTrigger,
|
|
ScrollArea,
|
|
} from 'ui'
|
|
import { Admonition, GenericSkeletonLoader } from 'ui-patterns'
|
|
import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
|
|
|
|
import { User, useUsersInfiniteQuery } from '@/data/auth/users-infinite-query'
|
|
import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
|
|
import { useRoleImpersonationStateSnapshot } from '@/state/role-impersonation-state'
|
|
import { ResponseError } from '@/types'
|
|
|
|
export const UserSelector = () => {
|
|
const { data: project } = useSelectedProjectQuery()
|
|
const state = useRoleImpersonationStateSnapshot()
|
|
|
|
const [open, setOpen] = useState(false)
|
|
const [searchText, setSearchText] = useState('')
|
|
|
|
const debouncedSearchText = useDebounce(searchText, 300)
|
|
|
|
const { data, error, isSuccess, isPending, isError } = useUsersInfiniteQuery(
|
|
{
|
|
projectRef: project?.ref,
|
|
connectionString: project?.connectionString,
|
|
keywords: debouncedSearchText.trim().toLocaleLowerCase(),
|
|
},
|
|
{ placeholderData: keepPreviousData }
|
|
)
|
|
const users = useMemo(() => data?.pages.flatMap((page) => page.result) ?? [], [data?.pages])
|
|
|
|
const impersonatingUser =
|
|
state.role?.type === 'postgrest' &&
|
|
state.role.role === 'authenticated' &&
|
|
state.role.userType === 'native'
|
|
? state.role.user
|
|
: undefined
|
|
|
|
const onSelectUser = async (user: User) => {
|
|
try {
|
|
await state.setRole({
|
|
type: 'postgrest',
|
|
role: 'authenticated',
|
|
userType: 'native',
|
|
user,
|
|
aal: 'aal1',
|
|
})
|
|
} catch (error) {
|
|
toast.error(`Failed to impersonate user: ${(error as ResponseError).message}`)
|
|
}
|
|
}
|
|
|
|
return (
|
|
<FormItemLayout isReactForm={false} label="Select which user to test as">
|
|
<Popover open={open} onOpenChange={setOpen} modal>
|
|
<PopoverTrigger asChild>
|
|
<Button
|
|
block
|
|
type="default"
|
|
role="combobox"
|
|
size="small"
|
|
aria-expanded={open}
|
|
className={cn('justify-between', !impersonatingUser && 'text-foreground-lighter')}
|
|
iconRight={<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />}
|
|
>
|
|
{impersonatingUser?.email ?? 'Select a user'}
|
|
</Button>
|
|
</PopoverTrigger>
|
|
<PopoverContent sameWidthAsTrigger className="p-0" side="bottom" align="start">
|
|
<Command shouldFilter={false}>
|
|
<CommandInput
|
|
showResetIcon
|
|
placeholder="Search for a user"
|
|
className="text-xs"
|
|
value={searchText}
|
|
onValueChange={setSearchText}
|
|
/>
|
|
|
|
{isError ? (
|
|
<Admonition showIcon={false} type="warning" className="border-0 rounded-none text-xs">
|
|
Failed to fetch users: {error.message}
|
|
</Admonition>
|
|
) : (
|
|
<CommandEmpty>No user found</CommandEmpty>
|
|
)}
|
|
|
|
<CommandList>
|
|
{isPending && (
|
|
<div className="p-2">
|
|
<GenericSkeletonLoader />
|
|
</div>
|
|
)}
|
|
|
|
{isSuccess && (
|
|
<CommandGroup>
|
|
<ScrollArea className={users.length > 7 ? 'h-full md:h-[210px]' : ''}>
|
|
{users.map((user) => {
|
|
return (
|
|
<CommandItem
|
|
key={user.id}
|
|
value={user.email}
|
|
className="cursor-pointer w-full"
|
|
onSelect={() => {
|
|
onSelectUser(user)
|
|
setOpen(false)
|
|
}}
|
|
>
|
|
<div className="w-full flex items-center justify-between">
|
|
<p className="space-x-3">
|
|
<span className="text-foreground-light">{user.email}</span>
|
|
<code className="text-code-inline text-foreground-lighter!">
|
|
{user.id?.slice(0, 8)}
|
|
</code>
|
|
</p>
|
|
{impersonatingUser?.id === user.id && <Check size={16} />}
|
|
</div>
|
|
</CommandItem>
|
|
)
|
|
})}
|
|
</ScrollArea>
|
|
</CommandGroup>
|
|
)}
|
|
</CommandList>
|
|
</Command>
|
|
</PopoverContent>
|
|
</Popover>
|
|
</FormItemLayout>
|
|
)
|
|
}
|