mirror of
https://github.com/supabase/supabase.git
synced 2026-10-08 10:55:06 +03:00
## I have read the [CONTRIBUTING.md](https://github.com/supabase/supabase/blob/master/CONTRIBUTING.md) file. YES ## What kind of change does this PR introduce? Performance improvement / feature ## What is the current behavior? The Schema Designer fetches all tables in a single request via `useTablesQuery`. For schemas with 400+ tables this blocks first paint on a large payload. ## What is the new behavior? `SchemaGraph` uses `useInfiniteTablesQuery` (pageSize: 100) so the first 100 tables paint immediately. A "Load more tables" button appears above the legend whenever more pages remain, letting users load the rest on demand. ## Additional context <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Added a "Find table…" selector and keyboard shortcut to quickly locate and focus tables; supports incremental loading and debounced name search (with literal wildcard handling). * Schema Graph shows a bottom "Load more tables" control with loading state and preserves view after loading more. * **Refactor** * Table listing switched to infinite/paginated retrieval and improved "no tables" logic; server-side name filtering supported. * **Tests** * E2E tests add a schema-visualizer wait helper and update flows to support the paginated visualizer. <!-- review_stack_entry_start --> [](https://app.coderabbit.ai/change-stack/supabase/supabase/pull/46402?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.6 KiB
TypeScript
145 lines
4.6 KiB
TypeScript
import { Loader2, Search } from 'lucide-react'
|
|
import { ComponentPropsWithoutRef, forwardRef, useMemo, useState } from 'react'
|
|
import {
|
|
Button,
|
|
Command,
|
|
CommandEmpty,
|
|
CommandGroup,
|
|
CommandInput,
|
|
CommandItem,
|
|
CommandList,
|
|
Popover,
|
|
PopoverContent,
|
|
PopoverTrigger,
|
|
ScrollArea,
|
|
} from 'ui'
|
|
|
|
import { useInfiniteTablesQuery } from '@/data/tables/tables-query'
|
|
import { useDebouncedValue } from '@/hooks/misc/useDebouncedValue'
|
|
import type { SafePostgresTable } from '@/lib/postgres-types'
|
|
|
|
type FindTableSelectorProps = Omit<ComponentPropsWithoutRef<'div'>, 'onSelect'> & {
|
|
projectRef?: string
|
|
connectionString?: string | null
|
|
schema?: string
|
|
disabled?: boolean
|
|
size?: 'tiny' | 'small'
|
|
open: boolean
|
|
onOpenChange: (open: boolean) => void
|
|
onSelect: (table: SafePostgresTable) => void
|
|
}
|
|
|
|
export const FindTableSelector = forwardRef<HTMLDivElement, FindTableSelectorProps>(
|
|
(
|
|
{
|
|
className,
|
|
projectRef,
|
|
connectionString,
|
|
schema,
|
|
disabled = false,
|
|
size = 'tiny',
|
|
open,
|
|
onOpenChange,
|
|
onSelect,
|
|
...rest
|
|
},
|
|
ref
|
|
) => {
|
|
const [search, setSearch] = useState('')
|
|
const debouncedSearch = useDebouncedValue(search, 300)
|
|
const nameFilter = debouncedSearch.trim() || undefined
|
|
|
|
const { data, isFetching, hasNextPage, isFetchingNextPage, fetchNextPage } =
|
|
useInfiniteTablesQuery(
|
|
{
|
|
projectRef,
|
|
connectionString,
|
|
schema,
|
|
includeColumns: false,
|
|
pageSize: 50,
|
|
nameFilter,
|
|
},
|
|
{ enabled: open }
|
|
)
|
|
|
|
const tables = useMemo(() => data?.pages.flat() ?? [], [data])
|
|
|
|
const handleOpenChange = (next: boolean) => {
|
|
if (!next) setSearch('')
|
|
onOpenChange(next)
|
|
}
|
|
|
|
return (
|
|
<div ref={ref} className={className} {...rest}>
|
|
<Popover open={open} onOpenChange={handleOpenChange} modal={false}>
|
|
<PopoverTrigger asChild>
|
|
<Button
|
|
size={size}
|
|
type="default"
|
|
disabled={disabled}
|
|
data-testid="find-table-selector"
|
|
icon={<Search size={14} strokeWidth={1.5} className="text-foreground-muted" />}
|
|
>
|
|
<span className="text-foreground-lighter">Find table…</span>
|
|
</Button>
|
|
</PopoverTrigger>
|
|
<PopoverContent className="p-0 w-[260px] pointer-events-auto" side="bottom" align="start">
|
|
<Command shouldFilter={false}>
|
|
<CommandInput
|
|
className="text-xs"
|
|
placeholder="Find table…"
|
|
value={search}
|
|
onValueChange={setSearch}
|
|
/>
|
|
<CommandList>
|
|
{isFetching && tables.length === 0 ? (
|
|
<div className="flex items-center justify-center gap-x-2 py-6 text-xs text-foreground-light">
|
|
<Loader2 className="animate-spin" size={14} />
|
|
<span>Loading tables</span>
|
|
</div>
|
|
) : (
|
|
<>
|
|
<CommandEmpty>No tables found</CommandEmpty>
|
|
<CommandGroup>
|
|
<ScrollArea className={tables.length > 7 ? 'h-[210px]' : ''}>
|
|
{tables.map((table) => (
|
|
<CommandItem
|
|
key={table.id}
|
|
value={String(table.id)}
|
|
className="cursor-pointer flex items-center justify-between space-x-2 w-full"
|
|
onSelect={() => {
|
|
onSelect(table)
|
|
handleOpenChange(false)
|
|
}}
|
|
>
|
|
<span>{table.name}</span>
|
|
</CommandItem>
|
|
))}
|
|
{hasNextPage && (
|
|
<div className="px-2 py-1.5">
|
|
<Button
|
|
block
|
|
size="tiny"
|
|
type="default"
|
|
loading={isFetchingNextPage}
|
|
onClick={() => fetchNextPage()}
|
|
>
|
|
Load more
|
|
</Button>
|
|
</div>
|
|
)}
|
|
</ScrollArea>
|
|
</CommandGroup>
|
|
</>
|
|
)}
|
|
</CommandList>
|
|
</Command>
|
|
</PopoverContent>
|
|
</Popover>
|
|
</div>
|
|
)
|
|
}
|
|
)
|
|
|
|
FindTableSelector.displayName = 'FindTableSelector'
|