mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 09:25:06 +03:00
chore(studio): database tables UI improvements (#44163)
## 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>
This commit is contained in:
1 parent
10e9cdd1f4
commit
64c37ca72b
6 files changed
+355
-294
No files matched your search
@@ -89,12 +89,14 @@ test.describe('Database', () => {
|
||||
// validates table and column exists
|
||||
await expect(page.getByText(databaseTableName, { exact: true })).toBeVisible()
|
||||
// test we can edit the column
|
||||
await page.getByText(`${databaseTableName} actions`).click()
|
||||
|
||||
await page.getByText(`${databaseTableName} actions`).click()
|
||||
await expect(page.getByRole('menuitem', { name: 'Edit table' })).toBeVisible()
|
||||
await page.getByRole('menuitem', { name: 'Edit table' }).click({ force: true })
|
||||
await expect(page.getByRole('menuitem', { name: 'Edit table' })).not.toBeVisible()
|
||||
const tableActionsButton = page.getByRole('button', {
|
||||
name: `${databaseTableName} actions`,
|
||||
})
|
||||
await tableActionsButton.click()
|
||||
const editTableMenuItem = page.getByRole('menuitem', { name: 'Edit table' })
|
||||
await expect(editTableMenuItem).toBeVisible()
|
||||
await editTableMenuItem.press('Enter')
|
||||
await expect(editTableMenuItem).not.toBeVisible()
|
||||
const dialog = page.getByRole('dialog')
|
||||
await expect(dialog).toBeVisible()
|
||||
await expect(dialog.getByText('timestamptz')).toBeVisible()
|
||||
@@ -104,10 +106,10 @@ test.describe('Database', () => {
|
||||
await expect(page.getByRole('dialog')).not.toBeVisible()
|
||||
|
||||
// test the schema view has been refreshed
|
||||
await page.getByText(`${databaseTableName} actions`).click()
|
||||
await expect(page.getByRole('menuitem', { name: 'Edit table' })).toBeVisible()
|
||||
await page.getByRole('menuitem', { name: 'Edit table' }).click()
|
||||
await expect(page.getByRole('menuitem', { name: 'Edit table' })).not.toBeVisible()
|
||||
await tableActionsButton.click()
|
||||
await expect(editTableMenuItem).toBeVisible()
|
||||
await editTableMenuItem.press('Enter')
|
||||
await expect(editTableMenuItem).not.toBeVisible()
|
||||
await expect(page.getByRole('dialog')).toBeVisible()
|
||||
// FIXME: For some reason, the dialog is not stable and rerenders, sometimes preventing the description to be filled
|
||||
await page.waitForTimeout(500)
|
||||
@@ -115,15 +117,19 @@ test.describe('Database', () => {
|
||||
await page.getByRole('button', { name: 'Cancel' }).click()
|
||||
await expect(page.getByRole('dialog')).not.toBeVisible()
|
||||
|
||||
await page.getByText(`${databaseTableName} actions`).click()
|
||||
await expect(page.getByRole('menuitem', { name: 'Copy name' })).toBeVisible()
|
||||
await page.getByRole('menuitem', { name: 'Copy name' }).click()
|
||||
await expect(page.getByRole('menuitem', { name: 'Copy name' })).not.toBeVisible()
|
||||
await tableActionsButton.click()
|
||||
const copyTableNameMenuItem = page.getByRole('menuitem', { name: 'Copy name' })
|
||||
await expect(copyTableNameMenuItem).toBeVisible()
|
||||
await copyTableNameMenuItem.press('Enter')
|
||||
await expect(copyTableNameMenuItem).not.toBeVisible()
|
||||
await expectClipboardValue({ page, value: databaseTableName, exact: true })
|
||||
|
||||
await page.getByText(`${databaseTableName} actions`).click()
|
||||
await expect(page.getByRole('menuitem', { name: 'View in Table Editor' })).toBeVisible()
|
||||
await page.getByRole('menuitem', { name: 'View in Table Editor' }).click()
|
||||
await tableActionsButton.click()
|
||||
const viewInTableEditorMenuItem = page.getByRole('menuitem', {
|
||||
name: 'View in Table Editor',
|
||||
})
|
||||
await expect(viewInTableEditorMenuItem).toBeVisible()
|
||||
await viewInTableEditorMenuItem.press('Enter')
|
||||
await page.waitForURL(/.*\/editor\/\d+/)
|
||||
await expect(page.getByRole('tab', { name: databaseTableName })).toBeVisible()
|
||||
})
|
||||
@@ -152,11 +158,13 @@ test.describe('Database', () => {
|
||||
await expect(page.getByText(databaseTableName, { exact: true })).toBeVisible()
|
||||
await expect(page.getByText(databaseColumnName, { exact: true })).toBeVisible()
|
||||
// test we can edit the column
|
||||
await page
|
||||
.getByText(`${databaseTableName} ${databaseColumnName} actions`)
|
||||
.click({ force: true })
|
||||
await expect(page.getByRole('menuitem', { name: 'Edit column' })).toBeVisible()
|
||||
await page.getByRole('menuitem', { name: 'Edit column' }).click()
|
||||
const columnActionsButton = page.getByRole('button', {
|
||||
name: `${databaseTableName} ${databaseColumnName} actions`,
|
||||
})
|
||||
await columnActionsButton.click()
|
||||
const editColumnMenuItem = page.getByRole('menuitem', { name: 'Edit column' })
|
||||
await expect(editColumnMenuItem).toBeVisible()
|
||||
await editColumnMenuItem.press('Enter')
|
||||
await page.getByLabel('Description').fill('Bazinga')
|
||||
await page.getByRole('button', { name: 'Save' }).click()
|
||||
await expect(
|
||||
@@ -165,20 +173,17 @@ test.describe('Database', () => {
|
||||
await expect(page.getByRole('dialog')).not.toBeVisible()
|
||||
|
||||
// test the schema view has been refreshed
|
||||
await page
|
||||
.getByText(`${databaseTableName} ${databaseColumnName} actions`)
|
||||
.click({ force: true })
|
||||
await expect(page.getByRole('menuitem', { name: 'Edit column' })).toBeVisible()
|
||||
await page.getByRole('menuitem', { name: 'Edit column' }).click()
|
||||
await columnActionsButton.click()
|
||||
await expect(editColumnMenuItem).toBeVisible()
|
||||
await editColumnMenuItem.press('Enter')
|
||||
await expect(page.getByLabel('Description')).toHaveValue('Bazinga')
|
||||
await page.getByRole('button', { name: 'Cancel' }).click()
|
||||
await expect(page.getByRole('dialog')).not.toBeVisible()
|
||||
|
||||
await page
|
||||
.getByText(`${databaseTableName} ${databaseColumnName} actions`)
|
||||
.click({ force: true })
|
||||
await expect(page.getByRole('menuitem', { name: 'Copy name' })).toBeVisible()
|
||||
await page.getByRole('menuitem', { name: 'Copy name' }).click()
|
||||
await columnActionsButton.click()
|
||||
const copyColumnNameMenuItem = page.getByRole('menuitem', { name: 'Copy name' })
|
||||
await expect(copyColumnNameMenuItem).toBeVisible()
|
||||
await copyColumnNameMenuItem.press('Enter')
|
||||
await expectClipboardValue({ page, value: databaseColumnName, exact: true })
|
||||
})
|
||||
})
|
||||
@@ -208,11 +213,13 @@ test.describe('Database', () => {
|
||||
await expect(page.getByRole('button', { name: 'New table' })).toBeVisible()
|
||||
|
||||
// validates database name is present and has accurate number of columns
|
||||
const tableRow = page.getByRole('row', {
|
||||
name: `${databaseTableName} No description`,
|
||||
})
|
||||
const tableRow = page
|
||||
.getByRole('row')
|
||||
.filter({ has: page.getByText(databaseTableName, { exact: true }) })
|
||||
.first()
|
||||
await expect(tableRow).toContainText(databaseTableName)
|
||||
await expect(tableRow).toContainText('3 columns')
|
||||
await expect(tableRow.getByRole('cell').filter({ hasText: /^3$/ }).first()).toBeVisible()
|
||||
await expect(tableRow.getByRole('link', { name: 'View columns' })).toBeVisible()
|
||||
|
||||
// change schema -> auth
|
||||
await page.getByTestId('schema-selector').click()
|
||||
@@ -364,14 +371,20 @@ test.describe('Database', () => {
|
||||
}
|
||||
)
|
||||
|
||||
const databaseWait = createApiResponseWaiter(
|
||||
page,
|
||||
'pg-meta',
|
||||
ref,
|
||||
'tables?include_columns=true&included_schemas=public'
|
||||
)
|
||||
await page.goto(toUrl(`/project/${env.PROJECT_REF}/database/tables?schema=public`))
|
||||
|
||||
// Wait for database tables to be populated
|
||||
await waitForDatabaseToLoad(page, ref)
|
||||
await databaseWait
|
||||
|
||||
// navigate to table columns
|
||||
const databaseRow = page.getByRole('row', { name: databaseTableName })
|
||||
await databaseRow.getByRole('link', { name: '3 columns' }).click()
|
||||
await databaseRow.getByRole('link', { name: 'View columns' }).click()
|
||||
await page.waitForURL(/.*\/database\/tables\/\d+/)
|
||||
|
||||
// validate and display everything correctly
|
||||
@@ -407,8 +420,7 @@ test.describe('Database', () => {
|
||||
await expect(columnDatabase2Row).toContainText('numeric')
|
||||
|
||||
// update table column
|
||||
await columnDatabase2Row.getByRole('button').click()
|
||||
await page.getByRole('button', { name: 'Edit column' }).click()
|
||||
await columnDatabase2Row.getByRole('button', { name: 'Edit' }).click()
|
||||
await page.getByLabel('name').fill(databaseColumnName3)
|
||||
const columnUpdateWait = createApiResponseWaiter(
|
||||
page,
|
||||
@@ -430,8 +442,8 @@ test.describe('Database', () => {
|
||||
|
||||
// delete table column
|
||||
const columnDatabase3Row = page.getByRole('row', { name: databaseColumnName3 })
|
||||
await columnDatabase3Row.getByRole('button').click()
|
||||
await page.getByRole('button', { name: 'Delete column' }).click()
|
||||
await columnDatabase3Row.getByRole('button').last().click()
|
||||
await page.getByRole('menuitem', { name: 'Delete column' }).click()
|
||||
await page.getByRole('checkbox', { name: 'Drop column with cascade?' }).check()
|
||||
const columnDeleteWait = createApiResponseWaiter(
|
||||
page,
|
||||
|
||||
@@ -24,19 +24,25 @@ function buildUrlMatcher(basePath: string, ref: string, action: string, method?:
|
||||
// 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 = response.url()
|
||||
const url = new URL(response.url())
|
||||
const requestMethod = response.request().method()
|
||||
|
||||
// Must include base path and one of the ref alternatives
|
||||
const hasBasePath = url.includes(`${trimmedBasePath}/`)
|
||||
const hasRef = refAlternatives.some((r) => url.includes(`/${r}/`))
|
||||
const hasBasePath = url.pathname.includes(`/${trimmedBasePath}/`)
|
||||
const hasRef = refAlternatives.some((r) => url.pathname.includes(`/${r}/`))
|
||||
|
||||
// Action match should be tolerant to extra query params ordering
|
||||
const hasAction = url.includes(action)
|
||||
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 && hasAction
|
||||
const urlMatches = hasBasePath && hasRef && hasActionPath && hasExpectedSearchParams
|
||||
if (method) return urlMatches && requestMethod === method
|
||||
return urlMatches
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user