mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 09:25:06 +03:00
feat(studio): paginate Tables list with server-side search (#46514)
## 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? Feature / performance improvement. ## What is the current behavior? The Database > Tables list renders every table for the selected schema in a single `useTablesQuery({ includeColumns: true })` fetch. ## What is the new behavior? - Database > Tables list uses `useInfiniteTablesQuery` with a 50-row page size, streaming pages as the user scrolls. - An `IntersectionObserver` sentinel attached to the footer status row triggers `fetchNextPage()` while `hasNextPage` is true; the same row doubles as the table count / loading indicator. - Search is debounced (300 ms) and passed as `nameFilter` to the hook, which forwards it to pg-meta's `getTablesPaginatedSql`. The backend returns only matching rows, so search works across the whole schema rather than only the loaded pages. ## Additional context <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Debounced search filtering for tables * Infinite-scroll pagination with sentinel-driven loading * Footer shows “Loading more tables…” and dynamic table counts * **Bug Fixes** * Ensure table list refresh after create/duplicate by invalidating infinite-list cache * **Tests** * Updated end-to-end waits and helpers to match the new paginated table-loading API <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
1 parent
0da841321f
commit
4ae803c3d7
4 files changed
+51
-27
No files matched your search
@@ -1,11 +1,12 @@
|
||||
import { PermissionAction } from '@supabase/shared-types/out/constants'
|
||||
import { useDebounce, useIntersectionObserver } from '@uidotdev/usehooks'
|
||||
import { useParams } from 'common'
|
||||
import { noop } from 'lodash'
|
||||
import { Check, Copy, Edit, Eye, Filter, MoreVertical, Plus, Search, Trash, X } from 'lucide-react'
|
||||
import Link from 'next/link'
|
||||
import { useRouter } from 'next/router'
|
||||
import { parseAsString, useQueryState } from 'nuqs'
|
||||
import { useRef, useState } from 'react'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
@@ -47,7 +48,7 @@ import { ENTITY_TYPE } from '@/data/entity-types/entity-type-constants'
|
||||
import { useForeignTablesQuery } from '@/data/foreign-tables/foreign-tables-query'
|
||||
import { useMaterializedViewsQuery } from '@/data/materialized-views/materialized-views-query'
|
||||
import { usePrefetchEditorTablePage } from '@/data/prefetchers/project.$ref.editor.$id'
|
||||
import { useTablesQuery } from '@/data/tables/tables-query'
|
||||
import { useInfiniteTablesQuery } from '@/data/tables/tables-query'
|
||||
import { useViewsQuery } from '@/data/views/views-query'
|
||||
import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions'
|
||||
import { useQuerySchemaState } from '@/hooks/misc/useSchemaQueryState'
|
||||
@@ -80,6 +81,7 @@ export const TableList = ({
|
||||
const { selectedSchema, setSelectedSchema } = useQuerySchemaState()
|
||||
|
||||
const [filterString, setFilterString] = useQueryState('search', parseAsString.withDefault(''))
|
||||
const debouncedFilterString = useDebounce(filterString, 300)
|
||||
const [visibleTypes, setVisibleTypes] = useState<string[]>(Object.values(ENTITY_TYPE))
|
||||
const [schemaSelectorOpen, setSchemaSelectorOpen] = useState(false)
|
||||
const searchInputRef = useRef<HTMLInputElement>(null)
|
||||
@@ -90,27 +92,40 @@ export const TableList = ({
|
||||
)
|
||||
|
||||
const {
|
||||
data: tables,
|
||||
data: tablesData,
|
||||
error: tablesError,
|
||||
isError: isErrorTables,
|
||||
isPending: isLoadingTables,
|
||||
isSuccess: isSuccessTables,
|
||||
} = useTablesQuery(
|
||||
{
|
||||
projectRef: project?.ref,
|
||||
connectionString: project?.connectionString,
|
||||
schema: selectedSchema,
|
||||
sortByProperty: 'name',
|
||||
includeColumns: true,
|
||||
},
|
||||
{
|
||||
select(tables) {
|
||||
return filterString.length === 0
|
||||
? tables
|
||||
: tables.filter((table) => table.name.toLowerCase().includes(filterString.toLowerCase()))
|
||||
},
|
||||
hasNextPage: hasNextTablesPage,
|
||||
isFetchingNextPage: isFetchingNextTablesPage,
|
||||
fetchNextPage: fetchNextTablesPage,
|
||||
} = useInfiniteTablesQuery({
|
||||
projectRef: project?.ref,
|
||||
connectionString: project?.connectionString,
|
||||
schema: selectedSchema,
|
||||
includeColumns: true,
|
||||
pageSize: 50,
|
||||
nameFilter: debouncedFilterString,
|
||||
})
|
||||
|
||||
const tables = tablesData?.pages.flat() ?? []
|
||||
|
||||
const [sentinelRef, sentinelEntry] = useIntersectionObserver({
|
||||
threshold: 0,
|
||||
rootMargin: '200px 0px 200px 0px',
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
if (sentinelEntry?.isIntersecting && hasNextTablesPage && !isFetchingNextTablesPage) {
|
||||
fetchNextTablesPage()
|
||||
}
|
||||
)
|
||||
}, [
|
||||
sentinelEntry?.isIntersecting,
|
||||
hasNextTablesPage,
|
||||
isFetchingNextTablesPage,
|
||||
fetchNextTablesPage,
|
||||
])
|
||||
|
||||
const {
|
||||
data: views,
|
||||
@@ -589,9 +604,13 @@ export const TableList = ({
|
||||
</>
|
||||
</TableBody>
|
||||
<TableFooter className="font-normal">
|
||||
<TableRow className="border-b-0 [&>td]:hover:bg-inherit">
|
||||
<TableCell colSpan={7} className="text-foreground-muted">
|
||||
{entities.length} {entities.length === 1 ? 'table' : 'tables'}
|
||||
<TableRow ref={sentinelRef} className="border-b-0">
|
||||
<TableCell colSpan={7} className="text-foreground-muted hover:bg-inherit">
|
||||
{isFetchingNextTablesPage
|
||||
? 'Loading more tables…'
|
||||
: `${entities.length} ${entities.length === 1 ? 'table' : 'tables'}${
|
||||
hasNextTablesPage ? ' loaded' : ''
|
||||
}`}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableFooter>
|
||||
|
||||
@@ -715,6 +715,9 @@ export const SidePanelEditor = ({
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: tableKeys.list(project?.ref, table.schema, includeColumns),
|
||||
}),
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: tableKeys.infiniteListPrefix(project?.ref, table.schema),
|
||||
}),
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: entityTypeKeys.list(project?.ref),
|
||||
}),
|
||||
@@ -782,6 +785,9 @@ export const SidePanelEditor = ({
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: tableKeys.list(project?.ref, table.schema, includeColumns),
|
||||
}),
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: tableKeys.infiniteListPrefix(project?.ref, table.schema),
|
||||
}),
|
||||
queryClient.invalidateQueries({ queryKey: entityTypeKeys.list(project?.ref) }),
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: privilegeKeys.tablePrivilegesList(project?.ref),
|
||||
|
||||
@@ -232,7 +232,7 @@ test.describe('Database', () => {
|
||||
page,
|
||||
'pg-meta',
|
||||
ref,
|
||||
'tables?include_columns=true&included_schemas=public'
|
||||
'query?key=project:default-schema:public-infinite_tables'
|
||||
)
|
||||
await page.goto(toUrl(`/project/${env.PROJECT_REF}/database/tables?schema=public`))
|
||||
await wait
|
||||
@@ -256,7 +256,7 @@ test.describe('Database', () => {
|
||||
page,
|
||||
'pg-meta',
|
||||
ref,
|
||||
'tables?include_columns=true&included_schemas=auth'
|
||||
'query?key=project:default-schema:auth-infinite_tables'
|
||||
)
|
||||
await page.getByRole('option', { name: 'auth' }).click()
|
||||
await authSchemaWait
|
||||
@@ -293,7 +293,7 @@ test.describe('Database', () => {
|
||||
page,
|
||||
'pg-meta',
|
||||
ref,
|
||||
'tables?include_columns=true&included_schemas=public'
|
||||
'query?key=project:default-schema:public-infinite_tables'
|
||||
)
|
||||
await page.goto(toUrl(`/project/${env.PROJECT_REF}/database/tables?schema=public`))
|
||||
// Wait for database tables to be populated
|
||||
@@ -313,7 +313,6 @@ test.describe('Database', () => {
|
||||
|
||||
// validate table creation
|
||||
await createTableWait
|
||||
await waitForDatabaseToLoad(page, ref)
|
||||
await expect(page.getByRole('dialog')).not.toBeVisible()
|
||||
|
||||
await expect(page.getByText(databaseTableNameNew, { exact: true })).toBeVisible()
|
||||
@@ -435,7 +434,7 @@ test.describe('Database', () => {
|
||||
page,
|
||||
'pg-meta',
|
||||
ref,
|
||||
'tables?include_columns=true&included_schemas=public'
|
||||
'query?key=project:default-schema:public-infinite_tables'
|
||||
)
|
||||
await page.goto(toUrl(`/project/${env.PROJECT_REF}/database/tables?schema=public`))
|
||||
|
||||
|
||||
@@ -109,7 +109,7 @@ export async function waitForDatabaseToLoad(page: Page, ref: string, schema?: st
|
||||
page,
|
||||
'pg-meta',
|
||||
ref,
|
||||
`tables?include_columns=true&included_schemas=${databaseSchema}`
|
||||
`query?key=project:default-schema:${databaseSchema}-infinite_tables`
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user