diff --git a/apps/studio/components/ui/ShimmerLine.tsx b/apps/studio/components/ui/ShimmerLine.tsx
index be6512f237bdb..229e7c78328a8 100644
--- a/apps/studio/components/ui/ShimmerLine.tsx
+++ b/apps/studio/components/ui/ShimmerLine.tsx
@@ -1,5 +1,9 @@
-const ShimmerLine = ({ active }: { active: boolean }) => {
- return active ?
: null
+import { cn } from 'ui'
+
+const ShimmerLine = ({ active, className }: { active: boolean; className?: string }) => {
+ return active ? (
+
+ ) : null
}
export default ShimmerLine
diff --git a/apps/studio/data/sql/execute-sql-query.ts b/apps/studio/data/sql/execute-sql-query.ts
index 08fab08e683a4..7182f5ae52cbf 100644
--- a/apps/studio/data/sql/execute-sql-query.ts
+++ b/apps/studio/data/sql/execute-sql-query.ts
@@ -113,6 +113,11 @@ export async function executeSql
(
query: `explain ${sql}`,
disable_statement_timeout: isStatementTimeoutDisabled,
},
+ params: {
+ ...options.params,
+ // @ts-expect-error: This is just a client side thing to identify queries better
+ query: { key: 'preflight-check' },
+ },
})
const parsedTree = !!costCheck ? createNodeTree(costCheck) : undefined
const summary = !!parsedTree ? calculateSummary(parsedTree) : undefined
diff --git a/apps/studio/pages/project/[ref]/advisors/performance.tsx b/apps/studio/pages/project/[ref]/advisors/performance.tsx
index daffc7b505c0a..491a852cebc78 100644
--- a/apps/studio/pages/project/[ref]/advisors/performance.tsx
+++ b/apps/studio/pages/project/[ref]/advisors/performance.tsx
@@ -1,18 +1,17 @@
-import { useMemo, useState } from 'react'
-
import { useParams } from 'common'
-import LintPageTabs from 'components/interfaces/Linter/LintPageTabs'
import { LINTER_LEVELS } from 'components/interfaces/Linter/Linter.constants'
import { lintInfoMap } from 'components/interfaces/Linter/Linter.utils'
import LinterDataGrid from 'components/interfaces/Linter/LinterDataGrid'
import LinterFilters from 'components/interfaces/Linter/LinterFilters'
import { LinterPageFooter } from 'components/interfaces/Linter/LinterPageFooter'
+import LintPageTabs from 'components/interfaces/Linter/LintPageTabs'
import AdvisorsLayout from 'components/layouts/AdvisorsLayout/AdvisorsLayout'
import DefaultLayout from 'components/layouts/DefaultLayout'
import { FormHeader } from 'components/ui/Forms/FormHeader'
import { Lint, useProjectLintsQuery } from 'data/lint/lint-query'
import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject'
import { DOCS_URL } from 'lib/constants'
+import { useMemo, useState } from 'react'
import type { NextPageWithLayout } from 'types'
import { LoadingLine } from 'ui'
diff --git a/apps/studio/pages/project/[ref]/advisors/security.tsx b/apps/studio/pages/project/[ref]/advisors/security.tsx
index 14c647177d387..f58aa695d3daa 100644
--- a/apps/studio/pages/project/[ref]/advisors/security.tsx
+++ b/apps/studio/pages/project/[ref]/advisors/security.tsx
@@ -1,18 +1,17 @@
-import { useMemo, useState } from 'react'
-
import { useParams } from 'common'
-import LintPageTabs from 'components/interfaces/Linter/LintPageTabs'
import { LINTER_LEVELS } from 'components/interfaces/Linter/Linter.constants'
import { lintInfoMap } from 'components/interfaces/Linter/Linter.utils'
import LinterDataGrid from 'components/interfaces/Linter/LinterDataGrid'
import LinterFilters from 'components/interfaces/Linter/LinterFilters'
import { LinterPageFooter } from 'components/interfaces/Linter/LinterPageFooter'
+import LintPageTabs from 'components/interfaces/Linter/LintPageTabs'
import AdvisorsLayout from 'components/layouts/AdvisorsLayout/AdvisorsLayout'
import DefaultLayout from 'components/layouts/DefaultLayout'
import { FormHeader } from 'components/ui/Forms/FormHeader'
import { Lint, useProjectLintsQuery } from 'data/lint/lint-query'
import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject'
import { DOCS_URL } from 'lib/constants'
+import { useMemo, useState } from 'react'
import type { NextPageWithLayout } from 'types'
import { LoadingLine } from 'ui'
@@ -66,7 +65,7 @@ const ProjectLints: NextPageWithLayout = () => {
return (
diff --git a/apps/studio/styles/main.scss b/apps/studio/styles/main.scss
index f7df5cab778ea..76473f85a60b3 100644
--- a/apps/studio/styles/main.scss
+++ b/apps/studio/styles/main.scss
@@ -21,6 +21,7 @@
--sidebar-accent-foreground: var(--foreground-default);
--sidebar-border: var(--border-default);
--sidebar-ring: 217.2 91.2% 59.8%;
+ --header-height: 3rem;
}
[data-theme='deep-dark'],
diff --git a/e2e/studio/features/filter-bar.spec.ts b/e2e/studio/features/filter-bar.spec.ts
new file mode 100644
index 0000000000000..279afffe8b369
--- /dev/null
+++ b/e2e/studio/features/filter-bar.spec.ts
@@ -0,0 +1,813 @@
+import { expect } from '@playwright/test'
+
+import { createTable, dropTable, query } from '../utils/db/index.js'
+import {
+ addFilter,
+ addFilterWithDropdownValue,
+ getFilterBarInput,
+ navigateToTable,
+ selectColumnFilter,
+ selectOperator,
+ selectOperatorByClick,
+ setupFilterBarPage,
+} from '../utils/filter-bar-helpers.js'
+import { test } from '../utils/test.js'
+import { toUrl } from '../utils/to-url.js'
+
+const tableNamePrefix = 'pw_filter_bar'
+
+function getDateValue(daysAgo: number): string {
+ const date = new Date(Date.now() - daysAgo * 86400000)
+ const year = date.getFullYear()
+ const month = String(date.getMonth() + 1).padStart(2, '0')
+ const day = String(date.getDate()).padStart(2, '0')
+ return `${year}-${month}-${day}`
+}
+
+test.describe('Filter Bar', () => {
+ test.beforeEach(async ({ page, ref }) => {
+ await setupFilterBarPage(page, ref, toUrl(`/project/${ref}/editor?schema=public`))
+ })
+
+ test.describe('Basic Filter Operations', () => {
+ test('selecting a column creates a filter condition', async ({ page, ref }) => {
+ const tableName = `${tableNamePrefix}_col_sel`
+ const columnName = 'name'
+
+ await createTable(tableName, columnName, [{ name: 'Alice' }])
+
+ try {
+ await setupFilterBarPage(page, ref, toUrl(`/project/${ref}/editor?schema=public`))
+ await navigateToTable(page, ref, tableName)
+
+ await selectColumnFilter(page, columnName)
+
+ await expect(page.getByTestId(`filter-condition-${columnName}`)).toBeVisible()
+ await expect(page.getByTestId(`filter-operator-${columnName}`)).toBeFocused()
+ } finally {
+ await dropTable(tableName)
+ }
+ })
+
+ test('entering value filters the grid', async ({ page, ref }) => {
+ const tableName = `${tableNamePrefix}_val_filt`
+ const columnName = 'name'
+
+ await createTable(tableName, columnName, [
+ { name: 'Alice' },
+ { name: 'Bob' },
+ { name: 'Charlie' },
+ ])
+
+ try {
+ await setupFilterBarPage(page, ref, toUrl(`/project/${ref}/editor?schema=public`))
+ await navigateToTable(page, ref, tableName)
+
+ await addFilter(page, ref, columnName, '=', 'Alice')
+
+ await expect(page.getByRole('gridcell', { name: 'Alice' })).toBeVisible()
+ await expect(page.getByRole('gridcell', { name: 'Bob' })).not.toBeVisible()
+ await expect(page.getByRole('gridcell', { name: 'Charlie' })).not.toBeVisible()
+ } finally {
+ await dropTable(tableName)
+ }
+ })
+
+ test('removing filter via X button restores all data', async ({ page, ref }) => {
+ const tableName = `${tableNamePrefix}_remove_x`
+ const columnName = 'name'
+
+ await createTable(tableName, columnName, [{ name: 'Alice' }, { name: 'Bob' }])
+
+ try {
+ await setupFilterBarPage(page, ref, toUrl(`/project/${ref}/editor?schema=public`))
+ await navigateToTable(page, ref, tableName)
+
+ await addFilter(page, ref, columnName, '=', 'Alice')
+ await expect(page.getByRole('gridcell', { name: 'Bob' })).not.toBeVisible()
+
+ await page.getByTestId(`filter-remove-${columnName}`).click()
+
+ await expect(page.getByTestId(`filter-condition-${columnName}`)).not.toBeVisible()
+ await expect(page.getByRole('gridcell', { name: 'Alice' })).toBeVisible()
+ await expect(page.getByRole('gridcell', { name: 'Bob' })).toBeVisible()
+ } finally {
+ await dropTable(tableName)
+ }
+ })
+
+ test('multiple filters can be added', async ({ page, ref }) => {
+ const tableName = `${tableNamePrefix}_multi`
+
+ await query(
+ `CREATE TABLE IF NOT EXISTS ${tableName} (
+ id bigint generated by default as identity primary key,
+ first_name text,
+ last_name text
+ )`
+ )
+ await query(
+ `INSERT INTO ${tableName} (first_name, last_name) VALUES ('Alice', 'Smith'), ('Bob', 'Smith'), ('Alice', 'Jones')`
+ )
+
+ try {
+ await setupFilterBarPage(page, ref, toUrl(`/project/${ref}/editor?schema=public`))
+ await navigateToTable(page, ref, tableName)
+
+ await addFilter(page, ref, 'first_name', '=', 'Alice')
+ await addFilter(page, ref, 'last_name', '=', 'Smith')
+
+ await expect(page.getByTestId('filter-condition-first_name')).toBeVisible()
+ await expect(page.getByTestId('filter-condition-last_name')).toBeVisible()
+ } finally {
+ await dropTable(tableName)
+ }
+ })
+ })
+
+ test.describe('Keyboard Navigation - Freeform Input', () => {
+ test('Enter selects column from dropdown', async ({ page, ref }) => {
+ const tableName = `${tableNamePrefix}_enter_col`
+ const columnName = 'name'
+
+ await createTable(tableName, columnName, [{ name: 'Alice' }])
+
+ try {
+ await setupFilterBarPage(page, ref, toUrl(`/project/${ref}/editor?schema=public`))
+ await navigateToTable(page, ref, tableName)
+
+ const freeformInput = getFilterBarInput(page)
+ await freeformInput.click()
+ await freeformInput.fill(columnName)
+ await expect(page.getByTestId(`filter-menu-item-${columnName}`)).toBeVisible()
+
+ await page.keyboard.press('Enter')
+
+ await expect(page.getByTestId(`filter-condition-${columnName}`)).toBeVisible()
+ } finally {
+ await dropTable(tableName)
+ }
+ })
+
+ test('Tab selects column from dropdown', async ({ page, ref }) => {
+ const tableName = `${tableNamePrefix}_tab_col`
+ const columnName = 'name'
+
+ await createTable(tableName, columnName, [{ name: 'Alice' }])
+
+ try {
+ await setupFilterBarPage(page, ref, toUrl(`/project/${ref}/editor?schema=public`))
+ await navigateToTable(page, ref, tableName)
+
+ const freeformInput = getFilterBarInput(page)
+ await freeformInput.click()
+ await freeformInput.fill(columnName)
+ await expect(page.getByTestId(`filter-menu-item-${columnName}`)).toBeVisible()
+
+ await page.keyboard.press('Tab')
+
+ await expect(page.getByTestId(`filter-condition-${columnName}`)).toBeVisible()
+ } finally {
+ await dropTable(tableName)
+ }
+ })
+
+ test('ArrowDown/ArrowUp navigates dropdown items', async ({ page, ref }) => {
+ const tableName = `${tableNamePrefix}_arrow_nav`
+
+ await query(
+ `CREATE TABLE IF NOT EXISTS ${tableName} (
+ id bigint generated by default as identity primary key,
+ alpha text,
+ beta text,
+ gamma text
+ )`
+ )
+ await query(`INSERT INTO ${tableName} (alpha, beta, gamma) VALUES ('a', 'b', 'g')`)
+
+ try {
+ await setupFilterBarPage(page, ref, toUrl(`/project/${ref}/editor?schema=public`))
+ await navigateToTable(page, ref, tableName)
+
+ const freeformInput = getFilterBarInput(page)
+ await freeformInput.click()
+
+ // Items: id (0), alpha (1), beta (2), gamma (3)
+ await page.keyboard.press('ArrowDown')
+ await page.keyboard.press('ArrowDown')
+ await page.keyboard.press('Enter')
+
+ await expect(page.getByTestId('filter-condition-beta')).toBeVisible()
+ } finally {
+ await dropTable(tableName)
+ }
+ })
+
+ test('Backspace on empty input highlights last filter, second Backspace deletes it', async ({
+ page,
+ ref,
+ }) => {
+ const tableName = `${tableNamePrefix}_bksp_hl_del`
+ const columnName = 'name'
+
+ await createTable(tableName, columnName, [{ name: 'Alice' }])
+
+ try {
+ await setupFilterBarPage(page, ref, toUrl(`/project/${ref}/editor?schema=public`))
+ await navigateToTable(page, ref, tableName)
+
+ await addFilter(page, ref, columnName, '=', 'Alice')
+
+ const freeformInput = getFilterBarInput(page)
+ await freeformInput.click()
+ await page.keyboard.press('Backspace')
+
+ await expect(page.getByTestId(`filter-condition-${columnName}`)).toHaveAttribute(
+ 'data-highlighted',
+ 'true'
+ )
+
+ await page.keyboard.press('Backspace')
+
+ await expect(page.getByTestId(`filter-condition-${columnName}`)).not.toBeVisible()
+ } finally {
+ await dropTable(tableName)
+ }
+ })
+
+ test('ArrowLeft highlights previous condition', async ({ page, ref }) => {
+ const tableName = `${tableNamePrefix}_arrowl_hl`
+ const columnName = 'name'
+
+ await createTable(tableName, columnName, [{ name: 'Alice' }])
+
+ try {
+ await setupFilterBarPage(page, ref, toUrl(`/project/${ref}/editor?schema=public`))
+ await navigateToTable(page, ref, tableName)
+
+ await addFilter(page, ref, columnName, '=', 'Alice')
+
+ const freeformInput = getFilterBarInput(page)
+ await freeformInput.click()
+ await page.keyboard.press('ArrowLeft')
+
+ await expect(page.getByTestId(`filter-condition-${columnName}`)).toHaveAttribute(
+ 'data-highlighted',
+ 'true'
+ )
+ } finally {
+ await dropTable(tableName)
+ }
+ })
+
+ test('ArrowRight clears highlight at end', async ({ page, ref }) => {
+ const tableName = `${tableNamePrefix}_arrowr_cl`
+ const columnName = 'name'
+
+ await createTable(tableName, columnName, [{ name: 'Alice' }])
+
+ try {
+ await setupFilterBarPage(page, ref, toUrl(`/project/${ref}/editor?schema=public`))
+ await navigateToTable(page, ref, tableName)
+
+ await addFilter(page, ref, columnName, '=', 'Alice')
+
+ const freeformInput = getFilterBarInput(page)
+ await freeformInput.click()
+ await page.keyboard.press('ArrowLeft')
+ await expect(page.getByTestId(`filter-condition-${columnName}`)).toHaveAttribute(
+ 'data-highlighted',
+ 'true'
+ )
+
+ await page.keyboard.press('ArrowRight')
+ await expect(page.getByTestId(`filter-condition-${columnName}`)).not.toHaveAttribute(
+ 'data-highlighted',
+ 'true'
+ )
+ } finally {
+ await dropTable(tableName)
+ }
+ })
+
+ test('Escape clears highlight', async ({ page, ref }) => {
+ const tableName = `${tableNamePrefix}_esc_cl`
+ const columnName = 'name'
+
+ await createTable(tableName, columnName, [{ name: 'Alice' }])
+
+ try {
+ await setupFilterBarPage(page, ref, toUrl(`/project/${ref}/editor?schema=public`))
+ await navigateToTable(page, ref, tableName)
+
+ await addFilter(page, ref, columnName, '=', 'Alice')
+
+ const freeformInput = getFilterBarInput(page)
+ await freeformInput.click()
+ await page.keyboard.press('ArrowLeft')
+ await expect(page.getByTestId(`filter-condition-${columnName}`)).toHaveAttribute(
+ 'data-highlighted',
+ 'true'
+ )
+
+ await page.keyboard.press('Escape')
+ await expect(page.getByTestId(`filter-condition-${columnName}`)).not.toHaveAttribute(
+ 'data-highlighted',
+ 'true'
+ )
+ } finally {
+ await dropTable(tableName)
+ }
+ })
+
+ test('Enter on highlighted condition focuses value input', async ({ page, ref }) => {
+ const tableName = `${tableNamePrefix}_enter_hl`
+ const columnName = 'name'
+
+ await createTable(tableName, columnName, [{ name: 'Alice' }])
+
+ try {
+ await setupFilterBarPage(page, ref, toUrl(`/project/${ref}/editor?schema=public`))
+ await navigateToTable(page, ref, tableName)
+
+ await addFilter(page, ref, columnName, '=', 'Alice')
+
+ const freeformInput = getFilterBarInput(page)
+ await freeformInput.click()
+ await page.keyboard.press('Backspace')
+ await expect(page.getByTestId(`filter-condition-${columnName}`)).toHaveAttribute(
+ 'data-highlighted',
+ 'true'
+ )
+
+ await page.keyboard.press('Enter')
+ await expect(page.getByTestId(`filter-value-${columnName}`)).toBeFocused()
+ } finally {
+ await dropTable(tableName)
+ }
+ })
+ })
+
+ test.describe('Keyboard Navigation - Operator Input', () => {
+ test('Enter selects operator and focuses value', async ({ page, ref }) => {
+ const tableName = `${tableNamePrefix}_op_enter`
+ const columnName = 'name'
+
+ await createTable(tableName, columnName, [{ name: 'Alice' }])
+
+ try {
+ await setupFilterBarPage(page, ref, toUrl(`/project/${ref}/editor?schema=public`))
+ await navigateToTable(page, ref, tableName)
+
+ await selectColumnFilter(page, columnName)
+ await page.keyboard.press('Enter')
+
+ await expect(page.getByTestId(`filter-value-${columnName}`)).toBeFocused()
+ } finally {
+ await dropTable(tableName)
+ }
+ })
+
+ test('Backspace on empty operator removes condition', async ({ page, ref }) => {
+ const tableName = `${tableNamePrefix}_op_bksp`
+ const columnName = 'name'
+
+ await createTable(tableName, columnName, [{ name: 'Alice' }])
+
+ try {
+ await setupFilterBarPage(page, ref, toUrl(`/project/${ref}/editor?schema=public`))
+ await navigateToTable(page, ref, tableName)
+
+ await selectColumnFilter(page, columnName)
+
+ const operatorInput = page.getByTestId(`filter-operator-${columnName}`)
+ await operatorInput.fill('')
+ await page.keyboard.press('Backspace')
+
+ await expect(page.getByTestId(`filter-condition-${columnName}`)).not.toBeVisible()
+ } finally {
+ await dropTable(tableName)
+ }
+ })
+ })
+
+ test.describe('Keyboard Navigation - Value Input', () => {
+ test('Enter on value moves focus to group freeform', async ({ page, ref }) => {
+ const tableName = `${tableNamePrefix}_val_enter`
+ const columnName = 'name'
+
+ await createTable(tableName, columnName, [{ name: 'Alice' }])
+
+ try {
+ await setupFilterBarPage(page, ref, toUrl(`/project/${ref}/editor?schema=public`))
+ await navigateToTable(page, ref, tableName)
+
+ await addFilter(page, ref, columnName, '=', 'Alice')
+
+ await expect(getFilterBarInput(page)).toBeFocused()
+ } finally {
+ await dropTable(tableName)
+ }
+ })
+
+ test('Backspace on empty value moves to operator', async ({ page, ref }) => {
+ const tableName = `${tableNamePrefix}_val_bksp`
+ const columnName = 'name'
+
+ await createTable(tableName, columnName, [{ name: 'Alice' }])
+
+ try {
+ await setupFilterBarPage(page, ref, toUrl(`/project/${ref}/editor?schema=public`))
+ await navigateToTable(page, ref, tableName)
+
+ await selectColumnFilter(page, columnName)
+ await selectOperator(page, columnName, '=')
+
+ await page.keyboard.press('Backspace')
+
+ await expect(page.getByTestId(`filter-operator-${columnName}`)).toBeFocused()
+ } finally {
+ await dropTable(tableName)
+ }
+ })
+
+ test('ArrowLeft at position 0 moves to previous condition value', async ({ page, ref }) => {
+ const tableName = `${tableNamePrefix}_val_arrowl`
+
+ await query(
+ `CREATE TABLE IF NOT EXISTS ${tableName} (
+ id bigint generated by default as identity primary key,
+ first_name text,
+ last_name text
+ )`
+ )
+ await query(
+ `INSERT INTO ${tableName} (first_name, last_name) VALUES ('Alice', 'Smith'), ('Bob', 'Jones')`
+ )
+
+ try {
+ await setupFilterBarPage(page, ref, toUrl(`/project/${ref}/editor?schema=public`))
+ await navigateToTable(page, ref, tableName)
+
+ await addFilter(page, ref, 'first_name', '=', 'Alice')
+ await addFilter(page, ref, 'last_name', '=', 'Smith')
+
+ const lastNameValue = page.getByTestId('filter-value-last_name')
+ await lastNameValue.click()
+ await page.keyboard.press('Home')
+
+ await page.keyboard.press('ArrowLeft')
+
+ await expect(page.getByTestId('filter-value-first_name')).toBeFocused()
+ } finally {
+ await dropTable(tableName)
+ }
+ })
+
+ test('ArrowRight at end moves to next condition or freeform', async ({ page, ref }) => {
+ const tableName = `${tableNamePrefix}_val_arrowr`
+ const columnName = 'name'
+
+ await createTable(tableName, columnName, [{ name: 'Alice' }])
+
+ try {
+ await setupFilterBarPage(page, ref, toUrl(`/project/${ref}/editor?schema=public`))
+ await navigateToTable(page, ref, tableName)
+
+ await addFilter(page, ref, columnName, '=', 'Alice')
+
+ // Use keyboard to focus value: highlight condition then Enter to avoid re-opening dropdown
+ const freeformInput = getFilterBarInput(page)
+ await freeformInput.click()
+ await page.keyboard.press('Backspace')
+ await page.keyboard.press('Enter')
+
+ await expect(page.getByTestId(`filter-value-${columnName}`)).toBeFocused()
+ await page.keyboard.press('End')
+ await page.keyboard.press('ArrowRight')
+
+ await expect(freeformInput).toBeFocused()
+ } finally {
+ await dropTable(tableName)
+ }
+ })
+
+ test('editing value in the middle preserves cursor position', async ({ page, ref }) => {
+ const tableName = `${tableNamePrefix}_cursor_pos`
+ const columnName = 'name'
+
+ await createTable(tableName, columnName, [{ name: 'test' }])
+
+ try {
+ await setupFilterBarPage(page, ref, toUrl(`/project/${ref}/editor?schema=public`))
+ await navigateToTable(page, ref, tableName)
+
+ await selectColumnFilter(page, columnName)
+ await selectOperator(page, columnName, '=')
+
+ const valueInput = page.getByTestId(`filter-value-${columnName}`)
+ await valueInput.fill('HelloWorld')
+
+ // Move cursor to middle (after "Hello") and insert text
+ await page.keyboard.press('Home')
+ for (let i = 0; i < 5; i++) {
+ await page.keyboard.press('ArrowRight')
+ }
+ await page.keyboard.type('_Middle_')
+
+ // Verify the text was inserted in the middle, not appended at the end
+ await expect(valueInput).toHaveValue('Hello_Middle_World')
+ } finally {
+ await dropTable(tableName)
+ }
+ })
+ })
+
+ test.describe('Boolean Column Filters', () => {
+ test('filtering by true shows only true rows', async ({ page, ref }) => {
+ const tableName = `${tableNamePrefix}_bool_true`
+
+ await query(
+ `CREATE TABLE IF NOT EXISTS ${tableName} (
+ id bigint generated by default as identity primary key,
+ is_active boolean
+ )`
+ )
+ await query(`INSERT INTO ${tableName} (is_active) VALUES (true), (false), (true)`)
+
+ try {
+ await setupFilterBarPage(page, ref, toUrl(`/project/${ref}/editor?schema=public`))
+ await navigateToTable(page, ref, tableName)
+
+ await addFilterWithDropdownValue(page, ref, 'is_active', '=', 'true')
+
+ const rows = page.locator('[role="row"]')
+ // Header row + 2 data rows with true
+ await expect(rows).toHaveCount(3)
+ } finally {
+ await dropTable(tableName)
+ }
+ })
+
+ test('filtering by false shows only false rows', async ({ page, ref }) => {
+ const tableName = `${tableNamePrefix}_bool_false`
+
+ await query(
+ `CREATE TABLE IF NOT EXISTS ${tableName} (
+ id bigint generated by default as identity primary key,
+ is_active boolean
+ )`
+ )
+ await query(`INSERT INTO ${tableName} (is_active) VALUES (true), (false), (true)`)
+
+ try {
+ await setupFilterBarPage(page, ref, toUrl(`/project/${ref}/editor?schema=public`))
+ await navigateToTable(page, ref, tableName)
+
+ await addFilterWithDropdownValue(page, ref, 'is_active', '=', 'false')
+
+ const rows = page.locator('[role="row"]')
+ // Header row + 1 data row with false
+ await expect(rows).toHaveCount(2)
+ } finally {
+ await dropTable(tableName)
+ }
+ })
+
+ test('not equal operator with boolean filters correctly', async ({ page, ref }) => {
+ const tableName = `${tableNamePrefix}_bool_neq`
+
+ await query(
+ `CREATE TABLE IF NOT EXISTS ${tableName} (
+ id bigint generated by default as identity primary key,
+ is_active boolean
+ )`
+ )
+ await query(`INSERT INTO ${tableName} (is_active) VALUES (true), (false), (true)`)
+
+ try {
+ await setupFilterBarPage(page, ref, toUrl(`/project/${ref}/editor?schema=public`))
+ await navigateToTable(page, ref, tableName)
+
+ await addFilterWithDropdownValue(page, ref, 'is_active', '<>', 'true')
+
+ const rows = page.locator('[role="row"]')
+ // Header row + 1 data row with false (not equal to true)
+ await expect(rows).toHaveCount(2)
+ } finally {
+ await dropTable(tableName)
+ }
+ })
+ })
+
+ test.describe('Date Column Filters', () => {
+ test('filtering by Today shows only today rows', async ({ page, ref }) => {
+ const tableName = `${tableNamePrefix}_date_today`
+ const todayValue = getDateValue(0)
+
+ await query(
+ `CREATE TABLE IF NOT EXISTS ${tableName} (
+ id bigint generated by default as identity primary key,
+ created_at date
+ )`
+ )
+ await query(
+ `INSERT INTO ${tableName} (created_at) VALUES
+ (CURRENT_DATE),
+ (CURRENT_DATE - INTERVAL '1 day'),
+ (CURRENT_DATE - INTERVAL '7 days')`
+ )
+
+ try {
+ await setupFilterBarPage(page, ref, toUrl(`/project/${ref}/editor?schema=public`))
+ await navigateToTable(page, ref, tableName)
+
+ await addFilterWithDropdownValue(page, ref, 'created_at', '=', todayValue)
+
+ const rows = page.locator('[role="row"]')
+ await expect(rows).toHaveCount(2)
+ } finally {
+ await dropTable(tableName)
+ }
+ })
+
+ test('filtering by Yesterday shows only yesterday rows', async ({ page, ref }) => {
+ const tableName = `${tableNamePrefix}_date_yest`
+ const yesterdayValue = getDateValue(1)
+
+ await query(
+ `CREATE TABLE IF NOT EXISTS ${tableName} (
+ id bigint generated by default as identity primary key,
+ created_at date
+ )`
+ )
+ await query(
+ `INSERT INTO ${tableName} (created_at) VALUES
+ (CURRENT_DATE),
+ (CURRENT_DATE - INTERVAL '1 day'),
+ (CURRENT_DATE - INTERVAL '7 days')`
+ )
+
+ try {
+ await setupFilterBarPage(page, ref, toUrl(`/project/${ref}/editor?schema=public`))
+ await navigateToTable(page, ref, tableName)
+
+ await addFilterWithDropdownValue(page, ref, 'created_at', '=', yesterdayValue)
+
+ const rows = page.locator('[role="row"]')
+ await expect(rows).toHaveCount(2)
+ } finally {
+ await dropTable(tableName)
+ }
+ })
+
+ test('filtering by Last 7 days with greater or equal operator', async ({ page, ref }) => {
+ const tableName = `${tableNamePrefix}_date_7d`
+ const last7DaysValue = getDateValue(7)
+
+ await query(
+ `CREATE TABLE IF NOT EXISTS ${tableName} (
+ id bigint generated by default as identity primary key,
+ created_at date
+ )`
+ )
+ await query(
+ `INSERT INTO ${tableName} (created_at) VALUES
+ (CURRENT_DATE),
+ (CURRENT_DATE - INTERVAL '3 days'),
+ (CURRENT_DATE - INTERVAL '7 days'),
+ (CURRENT_DATE - INTERVAL '30 days')`
+ )
+
+ try {
+ await setupFilterBarPage(page, ref, toUrl(`/project/${ref}/editor?schema=public`))
+ await navigateToTable(page, ref, tableName)
+
+ await addFilterWithDropdownValue(page, ref, 'created_at', '>=', last7DaysValue)
+
+ const rows = page.locator('[role="row"]')
+ await expect(rows).toHaveCount(4)
+ } finally {
+ await dropTable(tableName)
+ }
+ })
+
+ test('date less than operator filters correctly', async ({ page, ref }) => {
+ const tableName = `${tableNamePrefix}_date_lt`
+ const todayValue = getDateValue(0)
+
+ await query(
+ `CREATE TABLE IF NOT EXISTS ${tableName} (
+ id bigint generated by default as identity primary key,
+ created_at date
+ )`
+ )
+ await query(
+ `INSERT INTO ${tableName} (created_at) VALUES
+ (CURRENT_DATE),
+ (CURRENT_DATE - INTERVAL '1 day'),
+ (CURRENT_DATE - INTERVAL '7 days')`
+ )
+
+ try {
+ await setupFilterBarPage(page, ref, toUrl(`/project/${ref}/editor?schema=public`))
+ await navigateToTable(page, ref, tableName)
+
+ await addFilterWithDropdownValue(page, ref, 'created_at', '<', todayValue)
+
+ const rows = page.locator('[role="row"]')
+ await expect(rows).toHaveCount(3)
+ } finally {
+ await dropTable(tableName)
+ }
+ })
+ })
+
+ test.describe('Timestamp Column Filters', () => {
+ test('timestamp column shows date preset dropdown options', async ({ page, ref }) => {
+ const tableName = `${tableNamePrefix}_ts_today`
+ const todayValue = getDateValue(0)
+
+ await query(
+ `CREATE TABLE IF NOT EXISTS ${tableName} (
+ id bigint generated by default as identity primary key,
+ created_at timestamp
+ )`
+ )
+ await query(
+ `INSERT INTO ${tableName} (created_at) VALUES
+ (NOW()),
+ (NOW() - INTERVAL '1 day'),
+ (NOW() - INTERVAL '7 days')`
+ )
+
+ try {
+ await setupFilterBarPage(page, ref, toUrl(`/project/${ref}/editor?schema=public`))
+ await navigateToTable(page, ref, tableName)
+
+ await addFilterWithDropdownValue(page, ref, 'created_at', '>=', todayValue)
+
+ const rows = page.locator('[role="row"]')
+ await expect(rows).toHaveCount(2)
+ } finally {
+ await dropTable(tableName)
+ }
+ })
+ })
+
+ test.describe('Keyboard Navigation - Dropdown Values', () => {
+ test('ArrowDown/Enter navigates and selects dropdown value option', async ({ page, ref }) => {
+ const tableName = `${tableNamePrefix}_kb_dropdown`
+
+ await query(
+ `CREATE TABLE IF NOT EXISTS ${tableName} (
+ id bigint generated by default as identity primary key,
+ is_active boolean
+ )`
+ )
+ await query(`INSERT INTO ${tableName} (is_active) VALUES (true), (false)`)
+
+ try {
+ await setupFilterBarPage(page, ref, toUrl(`/project/${ref}/editor?schema=public`))
+ await navigateToTable(page, ref, tableName)
+
+ await selectColumnFilter(page, 'is_active')
+ await selectOperator(page, 'is_active', '=')
+
+ // Navigate to 'false' option (second item) and select with Enter
+ await page.keyboard.press('ArrowDown')
+ await page.keyboard.press('Enter')
+
+ await expect(page.getByTestId('filter-condition-is_active')).toBeVisible()
+ } finally {
+ await dropTable(tableName)
+ }
+ })
+
+ test('Tab selects dropdown value option', async ({ page, ref }) => {
+ const tableName = `${tableNamePrefix}_kb_tab`
+
+ await query(
+ `CREATE TABLE IF NOT EXISTS ${tableName} (
+ id bigint generated by default as identity primary key,
+ is_active boolean
+ )`
+ )
+ await query(`INSERT INTO ${tableName} (is_active) VALUES (true), (false)`)
+
+ try {
+ await setupFilterBarPage(page, ref, toUrl(`/project/${ref}/editor?schema=public`))
+ await navigateToTable(page, ref, tableName)
+
+ await selectColumnFilter(page, 'is_active')
+ await selectOperator(page, 'is_active', '=')
+
+ await page.keyboard.press('Tab')
+
+ await expect(page.getByTestId('filter-condition-is_active')).toBeVisible()
+ } finally {
+ await dropTable(tableName)
+ }
+ })
+ })
+})
diff --git a/e2e/studio/utils/filter-bar-helpers.ts b/e2e/studio/utils/filter-bar-helpers.ts
new file mode 100644
index 0000000000000..8a9edbbc1905e
--- /dev/null
+++ b/e2e/studio/utils/filter-bar-helpers.ts
@@ -0,0 +1,84 @@
+import { expect, Page } from '@playwright/test'
+
+import { createApiResponseWaiter, waitForTableToLoad } from './wait-for-response.js'
+
+const FILTER_BAR_KEY = 'supabase-ui-table-filter-bar'
+
+export async function enableFilterBar(page: Page) {
+ await page.evaluate((key) => {
+ localStorage.setItem(key, 'true')
+ }, FILTER_BAR_KEY)
+}
+
+export function getFilterBarInput(page: Page) {
+ return page.getByTestId('filter-bar-freeform-input')
+}
+
+export async function navigateToTable(page: Page, ref: string, tableName: string) {
+ const gridWaiter = createApiResponseWaiter(page, 'pg-meta', ref, 'query?key=table-rows-')
+ await page.getByRole('button', { name: `View ${tableName}`, exact: true }).click()
+ await page.waitForURL(/\/editor\/\d+\?schema=public$/)
+ await gridWaiter
+}
+
+export async function selectColumnFilter(page: Page, columnName: string) {
+ const freeformInput = getFilterBarInput(page)
+ await freeformInput.click()
+ await freeformInput.fill(columnName)
+ await expect(page.getByTestId(`filter-menu-item-${columnName}`)).toBeVisible()
+ await page.keyboard.press('Enter')
+ await expect(page.getByTestId(`filter-operator-${columnName}`)).toBeFocused()
+}
+
+export async function selectOperator(page: Page, columnName: string, operator: string) {
+ await expect(page.getByTestId(`filter-menu-item-${operator}`)).toBeVisible()
+ await page.keyboard.press('Enter')
+ await expect(page.getByTestId(`filter-value-${columnName}`)).toBeFocused()
+}
+
+export async function addFilter(
+ page: Page,
+ ref: string,
+ columnName: string,
+ operator: string,
+ value: string
+) {
+ await selectColumnFilter(page, columnName)
+ await selectOperator(page, columnName, operator)
+ const rowsWaiter = createApiResponseWaiter(page, 'pg-meta', ref, 'query?key=table-rows-')
+ const valueInput = page.getByTestId(`filter-value-${columnName}`)
+ await valueInput.fill(value)
+ await page.keyboard.press('Enter')
+ await rowsWaiter
+}
+
+export async function selectOperatorByClick(page: Page, columnName: string, operator: string) {
+ await expect(page.getByTestId(`filter-menu-item-${operator}`)).toBeVisible()
+ await page.getByTestId(`filter-menu-item-${operator}`).click()
+ await expect(page.getByTestId(`filter-value-${columnName}`)).toBeFocused()
+}
+
+export async function addFilterWithDropdownValue(
+ page: Page,
+ ref: string,
+ columnName: string,
+ operator: string,
+ optionValue: string
+) {
+ await selectColumnFilter(page, columnName)
+ await selectOperatorByClick(page, columnName, operator)
+ const rowsWaiter = createApiResponseWaiter(page, 'pg-meta', ref, 'query?key=table-rows-')
+ await expect(page.getByTestId(`filter-menu-item-${optionValue}`)).toBeVisible()
+ await page.getByTestId(`filter-menu-item-${optionValue}`).click()
+ await rowsWaiter
+}
+
+export async function setupFilterBarPage(page: Page, ref: string, editorUrl: string) {
+ const loadPromise = waitForTableToLoad(page, ref)
+ await page.goto(editorUrl)
+ await loadPromise
+ await enableFilterBar(page)
+ const reloadPromise = waitForTableToLoad(page, ref)
+ await page.reload({ waitUntil: 'networkidle' })
+ await reloadPromise
+}
diff --git a/packages/ui-patterns/src/FilterBar/CommandListItem.tsx b/packages/ui-patterns/src/FilterBar/CommandListItem.tsx
index d5f42c4a5bd08..38b2e9e88bbad 100644
--- a/packages/ui-patterns/src/FilterBar/CommandListItem.tsx
+++ b/packages/ui-patterns/src/FilterBar/CommandListItem.tsx
@@ -29,6 +29,7 @@ export function CommandListItem({
isHighlighted && 'bg-surface-300',
!isHighlighted && 'hover:bg-surface-200'
)}
+ data-testid={`filter-menu-item-${item.value}`}
>
{includeIcon && item.icon}
diff --git a/packages/ui-patterns/src/FilterBar/FilterCondition.tsx b/packages/ui-patterns/src/FilterBar/FilterCondition.tsx
index 7c685c437fd6c..f94ea8626db18 100644
--- a/packages/ui-patterns/src/FilterBar/FilterCondition.tsx
+++ b/packages/ui-patterns/src/FilterBar/FilterCondition.tsx
@@ -222,6 +222,8 @@ export function FilterCondition({
variant === 'pill' ? 'rounded border' : 'border-r',
isHighlighted && 'ring-2 ring-primary'
)}
+ data-testid={`filter-condition-${property.name}`}
+ data-highlighted={isHighlighted}
>
{condition.operator || ' '}
@@ -285,6 +288,7 @@ export function FilterCondition({
className="h-full border-none bg-transparent py-0 px-1 text-xs focus:outline-none focus:ring-0 focus:shadow-none focus-visible:ring-0 focus-visible:ring-offset-0 w-full absolute left-0 top-0"
disabled={isLoading}
aria-label={`Value for ${property.label}`}
+ data-testid={`filter-value-${property.name}`}
/>
{localValue || ' '}
@@ -343,6 +347,7 @@ export function FilterCondition({
className="group hover:text-foreground hover:!bg-surface-600 rounded-none px-1 h-auto py-0"
aria-label={`Remove ${property.label} filter`}
tabIndex={-1}
+ data-testid={`filter-remove-${property.name}`}
/>