diff --git a/apps/studio/components/grid/components/grid/GridError.tsx b/apps/studio/components/grid/components/grid/GridError.tsx index a44ca11a8f6..fa8def1d371 100644 --- a/apps/studio/components/grid/components/grid/GridError.tsx +++ b/apps/studio/components/grid/components/grid/GridError.tsx @@ -1,14 +1,18 @@ import { useParams } from 'common' import { useTableFilter } from 'components/grid/hooks/useTableFilter' +import { useTableFilterNew } from 'components/grid/hooks/useTableFilterNew' import { useTableSort } from 'components/grid/hooks/useTableSort' import AlertError from 'components/ui/AlertError' import { InlineLink } from 'components/ui/InlineLink' import { ENTITY_TYPE } from 'data/entity-types/entity-type-constants' import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' +import { useCallback } from 'react' import { useTableEditorTableStateSnapshot } from 'state/table-editor-table' import { Button } from 'ui' import { Admonition } from 'ui-patterns' +import { isFilterRelatedError } from './GridError.utils' +import { useIsTableFilterBarEnabled } from '@/components/interfaces/App/FeaturePreview/FeaturePreviewContext' import { HighCostError } from '@/components/ui/HighQueryCost' import { COST_THRESHOLD_ERROR } from '@/data/sql/execute-sql-query' import { useTableEditorStateSnapshot } from '@/state/table-editor' @@ -18,12 +22,22 @@ export const GridError = ({ error }: { error?: ResponseError | null }) => { const { id: _id } = useParams() const tableId = _id ? Number(_id) : undefined - const { filters } = useTableFilter() + const newFilterBarEnabled = useIsTableFilterBarEnabled() + const { filters: oldFilters, clearFilters: clearOldFilters } = useTableFilter() + const { filters: newFilters, clearFilters: clearNewFilters } = useTableFilterNew() const { sorts } = useTableSort() const snap = useTableEditorTableStateSnapshot() const tableEditorSnap = useTableEditorStateSnapshot() + const removeAllFilters = useCallback(() => { + if (newFilterBarEnabled) { + clearNewFilters() + } else { + clearOldFilters() + } + }, [clearOldFilters, clearNewFilters, newFilterBarEnabled]) + if (!error) return null const tableEntityType = snap.originalTable?.entity_type @@ -32,8 +46,9 @@ export const GridError = ({ error }: { error?: ResponseError | null }) => { const isForeignTableMissingVaultKeyError = isForeignTable && error?.message?.includes('query vault failed') - const isInvalidSyntaxError = - filters.length > 0 && error?.message?.includes('invalid input syntax') + const hasActiveFilters = oldFilters.length > 0 || newFilters.length > 0 + + const hasFilterRelatedError = hasActiveFilters && isFilterRelatedError(error?.message) const isInvalidOrderingOperatorError = sorts.length > 0 && error?.message?.includes('identify an ordering operator') @@ -55,8 +70,8 @@ export const GridError = ({ error }: { error?: ResponseError | null }) => { ) } else if (isForeignTableMissingVaultKeyError) { return - } else if (isInvalidSyntaxError) { - return + } else if (hasFilterRelatedError) { + return } else if (isInvalidOrderingOperatorError) { return } @@ -94,27 +109,18 @@ const ForeignTableMissingVaultKeyError = () => { ) } -const InvalidSyntaxError = ({ error }: { error: ResponseError }) => { - const { onApplyFilters } = useTableFilter() - +const FilterError = ({ removeAllFilters }: { removeAllFilters: () => void }) => { return ( -

- Unable to retrieve results as the provided value in your filter(s) doesn't match it's column - data type. +

+ One or more of your filters may have a value or operator that doesn't match the column's + data type. Try updating or removing the filter.

-

- Verify that your filter values are correct before applying the filters again. -

-

- Error: {error.message} -

- -
diff --git a/apps/studio/components/grid/components/grid/GridError.utils.test.ts b/apps/studio/components/grid/components/grid/GridError.utils.test.ts new file mode 100644 index 00000000000..5a1095dd782 --- /dev/null +++ b/apps/studio/components/grid/components/grid/GridError.utils.test.ts @@ -0,0 +1,87 @@ +import { describe, expect, test } from 'vitest' + +import { isFilterRelatedError } from './GridError.utils' + +describe('isFilterRelatedError', () => { + test('returns false for null or undefined', () => { + expect(isFilterRelatedError(null)).toBe(false) + expect(isFilterRelatedError(undefined)).toBe(false) + }) + + test('returns false for empty string', () => { + expect(isFilterRelatedError('')).toBe(false) + }) + + test('returns false for unrelated error messages', () => { + expect(isFilterRelatedError('connection refused')).toBe(false) + expect(isFilterRelatedError('permission denied for table users')).toBe(false) + expect(isFilterRelatedError('relation "users" does not exist')).toBe(false) + expect(isFilterRelatedError('Query cost exceeds threshold')).toBe(false) + }) + + test('detects invalid input syntax errors', () => { + expect( + isFilterRelatedError( + 'Failed to run sql query: ERROR: 22P02: invalid input syntax for type inet: "192.168.3"' + ) + ).toBe(true) + expect( + isFilterRelatedError( + 'Failed to run sql query: ERROR: 22P02: invalid input syntax for type integer: "abc"' + ) + ).toBe(true) + expect( + isFilterRelatedError( + 'Failed to run sql query: ERROR: 22P02: invalid input syntax for type uuid: "not-a-uuid"' + ) + ).toBe(true) + }) + + test('detects operator does not exist errors', () => { + expect( + isFilterRelatedError( + 'Failed to run sql query: ERROR: 42883: operator does not exist: text > integer' + ) + ).toBe(true) + }) + + test('detects collation errors', () => { + expect( + isFilterRelatedError( + 'Failed to run sql query: ERROR: could not determine which collation to use for string comparison' + ) + ).toBe(true) + }) + + test('detects invalid enum value errors', () => { + expect( + isFilterRelatedError( + 'Failed to run sql query: ERROR: 22P02: invalid input value for enum status: "badvalue"' + ) + ).toBe(true) + }) + + test('detects malformed array literal errors', () => { + expect( + isFilterRelatedError( + 'Failed to run sql query: ERROR: 22P02: malformed array literal: "not-an-array"' + ) + ).toBe(true) + }) + + test('detects invalid byte sequence errors', () => { + expect( + isFilterRelatedError( + 'Failed to run sql query: ERROR: 22021: invalid byte sequence for encoding "UTF8"' + ) + ).toBe(true) + }) + + test('detects syntax errors from invalid IS operator values', () => { + expect( + isFilterRelatedError( + 'Failed to run sql query: ERROR: 42601: syntax error at or near "sdfsdf"' + ) + ).toBe(true) + }) +}) diff --git a/apps/studio/components/grid/components/grid/GridError.utils.ts b/apps/studio/components/grid/components/grid/GridError.utils.ts new file mode 100644 index 00000000000..cc1331c74c9 --- /dev/null +++ b/apps/studio/components/grid/components/grid/GridError.utils.ts @@ -0,0 +1,14 @@ +const FILTER_ERROR_PATTERNS = [ + 'invalid input syntax', + 'operator does not exist', + 'could not determine which collation', + 'invalid input value for enum', + 'malformed array literal', + 'invalid byte sequence', + 'syntax error', +] + +export function isFilterRelatedError(errorMessage: string | undefined | null): boolean { + if (!errorMessage) return false + return FILTER_ERROR_PATTERNS.some((pattern) => errorMessage.includes(pattern)) +} diff --git a/e2e/studio/features/filter-bar.spec.ts b/e2e/studio/features/filter-bar.spec.ts index 09fc74545ae..86597c9e758 100644 --- a/e2e/studio/features/filter-bar.spec.ts +++ b/e2e/studio/features/filter-bar.spec.ts @@ -13,6 +13,7 @@ import { } from '../utils/filter-bar-helpers.js' import { test } from '../utils/test.js' import { toUrl } from '../utils/to-url.js' +import { createApiResponseWaiter } from '../utils/wait-for-response.js' const tableNamePrefix = 'pw_filter_bar' @@ -811,4 +812,50 @@ test.describe('Filter Bar', () => { } }) }) + + test.describe('Filter Error Feedback', () => { + test('invalid filter value shows friendly error and remove button clears filters', async ({ + page, + ref, + }) => { + const tableName = `${tableNamePrefix}_err_fb` + + await createTable(tableName, 'name', [{ name: 'Alice' }, { name: 'Bob' }]) + + try { + await setupFilterBarPage(page, ref, toUrl(`/project/${ref}/editor?schema=public`)) + await navigateToTable(page, ref, tableName) + + // Apply a filter using the 'is' operator with an invalid value + // 'is' only accepts null/not null/true/false, so 'badvalue' triggers a syntax error + await selectColumnFilter(page, 'name') + await selectOperatorByClick(page, 'name', 'is') + + const valueInput = page.getByTestId('filter-value-name') + const rowsWaiter = createApiResponseWaiter(page, 'pg-meta', ref, 'query?key=table-rows-') + await valueInput.fill('badvalue') + await page.keyboard.press('Enter') + await rowsWaiter + + // Should show the friendly filter error, not the scary general error + await expect(page.getByText('No results found — check your filter values')).toBeVisible({ + timeout: 10000, + }) + await expect(page.getByText("doesn't match the column's data type")).toBeVisible() + + const removeButton = page.getByRole('button', { name: 'Remove filters' }) + await expect(removeButton).toBeVisible() + + // Clicking "Remove filters" should clear filters and restore data + await removeButton.click() + + // Filter pill should be gone and data should be visible again + await expect(page.getByTestId('filter-condition-name')).not.toBeVisible({ timeout: 10000 }) + await expect(page.getByRole('gridcell', { name: 'Alice' })).toBeVisible({ timeout: 10000 }) + await expect(page.getByRole('gridcell', { name: 'Bob' })).toBeVisible() + } finally { + await dropTable(tableName) + } + }) + }) })