Files
supabase/e2e/studio/utils/wait-for-response.ts
T
Charis 4ae803c3d7 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 -->
2026-06-02 16:41:52 -04:00

125 lines
4.3 KiB
TypeScript

import { Page } from '@playwright/test'
type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE'
/**
* Waits for a API response for a specific endpoint before continuing the playwright test.
* @param page - Playwright page object
* @param basePath - Base path of API endpoint to wait for (e.g. 'pg-meta', 'platform/projects', etc.)
* @param ref - Project reference
* @param action - Action path of API endpoint to wait for (e.g. 'types', 'triggers', 'content', etc.)
* @param options - Optional object which checks more scenarios
*/
export async function waitForApiResponse(
page: Page,
basePath: string,
ref: string,
action: string,
options?: Options
): Promise<void> {
return createApiResponseWaiter(page, basePath, ref, action, options)
}
function buildUrlMatcher(basePath: string, ref: string, action: string, method?: HttpMethod) {
// Normalize inputs and build a tolerant matcher that works across environments
const trimmedBasePath = basePath.replace(/^\/+|\/+$/g, '')
const refAlternatives = [ref, 'default']
const [actionPath, actionQuery] = action.split('?')
const trimmedActionPath = actionPath.replace(/^\/+/, '')
const expectedSearchParams = new URLSearchParams(actionQuery ?? '')
return (response: any) => {
const url = new URL(response.url())
const requestMethod = response.request().method()
// Must include base path and one of the ref alternatives
const hasBasePath = url.pathname.includes(`/${trimmedBasePath}/`)
const hasRef = refAlternatives.some((r) => url.pathname.includes(`/${r}/`))
const hasActionPath =
trimmedActionPath.length === 0 || url.pathname.includes(`/${trimmedActionPath}`)
const hasExpectedSearchParams = [...expectedSearchParams.entries()].every(([key, value]) =>
url.searchParams.getAll(key).some((actualValue) => actualValue.includes(value))
)
const urlMatches = hasBasePath && hasRef && hasActionPath && hasExpectedSearchParams
if (method) return urlMatches && requestMethod === method
return urlMatches
}
}
/**
* Starts listening for a specific API response and returns a promise you can await later.
* Use this to avoid races by creating the waiter BEFORE triggering navigation/clicks.
*
* Example:
* const wait = createApiResponseWaiter(page, 'pg-meta', ref, 'query?key=schemas')
* await page.goto(...)
* await wait
*/
export function createApiResponseWaiter(
page: Page,
basePath: string,
ref: string,
action: string,
options?: Options
): Promise<void> {
const matcher = buildUrlMatcher(basePath, ref, action, options?.method)
return page
.waitForResponse(matcher, { timeout: options?.timeout ?? 30_000 })
.then(() => {})
.catch((error) => {
const trimmedBasePath = basePath.replace(/^\/+|\/+$/g, '')
const message = `Error waiting for response: ${error}. Method: ${options?.method}, URL contains: ${trimmedBasePath}/(default|${ref})/${action}`
if (options?.soft) {
console.warn(`[soft-wait] ${message}`)
const fallback = options?.fallbackWaitMs ?? 0
if (fallback > 0) {
return page.waitForTimeout(fallback).then(() => {})
}
return
} else {
console.error(message)
throw error
}
})
}
type Options = {
method?: HttpMethod
timeout?: number
// When true, do not throw on timeout/error; optionally wait fallbackWaitMs and continue
soft?: boolean
fallbackWaitMs?: number
}
export async function waitForTableToLoad(page: Page, ref: string, schema?: string) {
const tableSchema = schema || 'public'
return await waitForApiResponse(page, 'pg-meta', ref, `query?key=entity-types-${tableSchema}-`)
}
export async function waitForGridDataToLoad(page: Page, ref: string) {
return await waitForApiResponse(page, 'pg-meta', ref, 'query?key=table-rows-')
}
export async function waitForDatabaseToLoad(page: Page, ref: string, schema?: string) {
const databaseSchema = schema || 'public'
return await waitForApiResponse(
page,
'pg-meta',
ref,
`query?key=project:default-schema:${databaseSchema}-infinite_tables`
)
}
export async function waitForSchemaVisualizerToLoad(page: Page, ref: string, schema?: string) {
const databaseSchema = schema || 'public'
return await waitForApiResponse(
page,
'pg-meta',
ref,
`query?key=project:default-schema:${schema ?? 'public'}-infinite_tables`
)
}