mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 09:25:06 +03:00
fix: Better error handling for filters (#43027)
## I have read the [CONTRIBUTING.md](https://github.com/supabase/supabase/blob/master/CONTRIBUTING.md) file. YES/NO ## What kind of change does this PR introduce? Update error handling for filter related errors on the table editor
This commit is contained in:
1 parent
e561196f07
commit
41a7b34985
4 files changed
+175
-21
No files matched your search
@@ -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 <ForeignTableMissingVaultKeyError />
|
||||
} else if (isInvalidSyntaxError) {
|
||||
return <InvalidSyntaxError error={error} />
|
||||
} else if (hasFilterRelatedError) {
|
||||
return <FilterError removeAllFilters={removeAllFilters} />
|
||||
} else if (isInvalidOrderingOperatorError) {
|
||||
return <InvalidOrderingOperatorError error={error} />
|
||||
}
|
||||
@@ -94,27 +109,18 @@ const ForeignTableMissingVaultKeyError = () => {
|
||||
)
|
||||
}
|
||||
|
||||
const InvalidSyntaxError = ({ error }: { error: ResponseError }) => {
|
||||
const { onApplyFilters } = useTableFilter()
|
||||
|
||||
const FilterError = ({ removeAllFilters }: { removeAllFilters: () => void }) => {
|
||||
return (
|
||||
<Admonition
|
||||
type="warning"
|
||||
type="note"
|
||||
className="pointer-events-auto"
|
||||
title="Invalid input syntax provided in filter value(s)"
|
||||
title="No results found — check your filter values"
|
||||
>
|
||||
<p className="!mb-0">
|
||||
Unable to retrieve results as the provided value in your filter(s) doesn't match it's column
|
||||
data type.
|
||||
<p className="!mb-4">
|
||||
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.
|
||||
</p>
|
||||
<p className="!mb-2">
|
||||
Verify that your filter values are correct before applying the filters again.
|
||||
</p>
|
||||
<p className="text-sm text-foreground-lighter prose max-w-full !mb-4">
|
||||
Error: <code className="text-code-inline">{error.message}</code>
|
||||
</p>
|
||||
|
||||
<Button type="default" onClick={() => onApplyFilters([])}>
|
||||
<Button type="default" onClick={removeAllFilters}>
|
||||
Remove filters
|
||||
</Button>
|
||||
</Admonition>
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
@@ -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))
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in new issue
Block a user