mirror of
https://github.com/supabase/supabase.git
synced 2026-05-06 22:18:00 +08:00
## What kind of change does this PR introduce? UI improvements ## What is the current behavior? - The database tables list and columns list use inconsistent page shells and table primitives - The child columns page has weaker information hierarchy and row actions than the parent tables page - Responsive column priority on the tables list does not reflect the most important data on smaller breakpoints - Table actions and counts are harder to scan than they should be ## What is the new behavior? - Both pages now use `PageLayout` with matching large-width content containers - `ColumnList` now uses the latest `ui` Table primitives instead of the legacy cleaned-up-later table - Both pages now show totals in a table footer - `ColumnList` now uses a tiny filter input, case-insensitive filtering, inline descriptions under the name, and a primary `Edit` button with overflow actions - `TableList` now has improved responsive column priority: - smallest breakpoint keeps `Rows` - `Columns` appears from `sm` - `Size` appears from `lg` - `Realtime Enabled` appears from `2xl` - `TableList` now uses `View columns` as the CTA, removes the ambiguous icon from that CTA, restores the entity icon from `sm` upwards only, and tightens the name column on the smallest breakpoint only - Boolean icon columns are right-aligned consistently, with the same Realtime icon tones applied to both `Realtime Enabled` and `Nullable` - The columns detail page now uses breadcrumbs for navigation back to Tables instead of an inline back button | Before | After | | --- | --- | | <img width="1728" height="997" alt="Tables Database Mallet Toolshed Supabase-0E0E3DE0-4EA1-407F-88D4-B85664D26D8E" src="https://github.com/user-attachments/assets/3a2e265c-394e-432c-8c29-12317b60fda8" /> | <img width="1728" height="997" alt="Tables Database Mallet Toolshed Supabase-C8FC339C-E9DA-4ADB-8458-C7EFF55F2AEC" src="https://github.com/user-attachments/assets/50c83a3f-a70c-4d09-a8c3-1eeaed68b68b" /> | | <img width="1728" height="997" alt="Tables Database Mallet Toolshed Supabase-FE9196A0-BEAF-4BA5-8A2C-06F934A62C38" src="https://github.com/user-attachments/assets/707a564a-e764-45ac-8470-8532e22d39bc" /> | <img width="1728" height="997" alt="Tables Database Mallet Toolshed Supabase-36E93C1E-7943-4C98-8119-CAF48E2FE5BA" src="https://github.com/user-attachments/assets/4cba5791-a4d7-4f43-aea0-8277b2ec5d28" /> | --------- Co-authored-by: Joshen Lim <joshenlimek@gmail.com>
115 lines
4.0 KiB
TypeScript
115 lines
4.0 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,
|
|
`tables?include_columns=true&included_schemas=${databaseSchema}`
|
|
)
|
|
}
|