Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 18 additions & 21 deletions .github/workflows/dashboard-pr-reminder.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,29 +18,26 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
with:
sparse-checkout: |
scripts
- name: Find Dashboard PRs older than 24 hours
id: find-prs
uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0
- uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
name: Install pnpm
with:
script: |
const findStalePRs = require('./scripts/actions/find-stale-dashboard-prs.js');
return await findStalePRs({ github, context, core });
run_install: false

- name: Send Slack notification
if: fromJSON(steps.find-prs.outputs.count) > 0
uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_DASHBOARD_WEBHOOK_URL }}
STALE_PRS_JSON: ${{ steps.find-prs.outputs.stale_prs }}
- name: Use Node.js
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
script: |
const sendSlackNotification = require('./scripts/actions/send-slack-pr-notification.js');
const stalePRs = JSON.parse(process.env.STALE_PRS_JSON);
const webhookUrl = process.env.SLACK_WEBHOOK_URL;
await sendSlackNotification(stalePRs, webhookUrl);
node-version-file: '.nvmrc'
cache: 'pnpm'

- name: No stale PRs found
if: fromJSON(steps.find-prs.outputs.count) == 0
run: |
echo "✓ No Dashboard PRs older than 24 hours found"
- name: Install deps
run: pnpm install --frozen-lockfile

- name: Find stale Dashboard PRs and notify Slack
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_DASHBOARD_WEBHOOK_URL }}
run: pnpm tsx scripts/actions/find-stale-dashboard-prs.ts | pnpm tsx scripts/actions/send-slack-pr-notification.ts
2 changes: 1 addition & 1 deletion apps/design-system/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
"next-contentlayer2": "0.4.6",
"next-themes": "^0.3.0",
"react": "catalog:",
"react-data-grid": "7.0.0-beta.41",
"react-data-grid": "7.0.0-beta.47",
"react-day-picker": "^9.11.1",
"react-dom": "catalog:",
"react-hook-form": "^7.45.0",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ export default function DataGridDemo() {
headerCellClass: 'border-default border-r border-b',
renderCell: ({ row }) => {
// eslint-disable-next-line react-hooks/rules-of-hooks
const [isRowSelected, onRowSelectionChange] = useRowSelection()
const { isRowSelected, onRowSelectionChange } = useRowSelection()

return (
<div className="flex items-center justify-center h-full">
Expand All @@ -43,7 +43,6 @@ export default function DataGridDemo() {
e.stopPropagation()
onRowSelectionChange({
row,
type: 'ROW',
checked: !isRowSelected,
isShiftClick: e.shiftKey,
})
Expand Down
5 changes: 2 additions & 3 deletions apps/studio/components/grid/components/grid/AddColumn.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { Plus } from 'lucide-react'
import type { CalculatedColumn } from 'react-data-grid'

import { useTableEditorStateSnapshot } from 'state/table-editor'
import { Button } from 'ui'

import { ADD_COLUMN_KEY } from '../../constants'
import { DefaultFormatter } from '../formatter/DefaultFormatter'
import { useTableEditorStateSnapshot } from '@/state/table-editor'

export const AddColumn: CalculatedColumn<any, any> = {
key: ADD_COLUMN_KEY,
Expand All @@ -15,7 +15,6 @@ export const AddColumn: CalculatedColumn<any, any> = {
resizable: false,
sortable: false,
frozen: false,
isLastFrozenColumn: false,
renderHeaderCell() {
return <AddColumnHeader aria-label="Add New Row" />
},
Expand Down
2 changes: 1 addition & 1 deletion apps/studio/components/grid/components/grid/Grid.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -203,7 +203,7 @@ export const Grid = memo(
// Compute rowClass function to style pending add/delete rows
const computedRowClass = useMemo(() => {
return (row: SupaRow) => {
const classes: string[] = []
const classes: string[] = ['[&>.rdg-cell]:flex', '[&>.rdg-cell]:items-center']

// Call the original rowClass if provided
if (rowClass) {
Expand Down
48 changes: 27 additions & 21 deletions apps/studio/components/grid/components/grid/GridError.tsx
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -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
Expand All @@ -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')
Expand All @@ -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} />
}
Expand Down Expand Up @@ -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>
Expand Down
Original file line number Diff line number Diff line change
@@ -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)
})
})
14 changes: 14 additions & 0 deletions apps/studio/components/grid/components/grid/GridError.utils.ts
Original file line number Diff line number Diff line change
@@ -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))
}
19 changes: 9 additions & 10 deletions apps/studio/components/grid/components/grid/SelectColumn.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,15 @@ import {
RenderCellProps,
RenderGroupCellProps,
RenderHeaderCellProps,
useHeaderRowSelection,
useRowSelection,
} from 'react-data-grid'

import { ButtonTooltip } from 'components/ui/ButtonTooltip'
import { useTableEditorStateSnapshot } from 'state/table-editor'
import { useTableEditorTableStateSnapshot } from 'state/table-editor-table'
import { SELECT_COLUMN_KEY } from '../../constants'
import type { SupaRow } from '../../types'
import { ButtonTooltip } from '@/components/ui/ButtonTooltip'
import { useTableEditorStateSnapshot } from '@/state/table-editor'
import { useTableEditorTableStateSnapshot } from '@/state/table-editor-table'

export const SelectColumn: CalculatedColumn<any, any> = {
key: SELECT_COLUMN_KEY,
Expand All @@ -23,33 +24,32 @@ export const SelectColumn: CalculatedColumn<any, any> = {
resizable: false,
sortable: false,
frozen: true,
isLastFrozenColumn: false,
renderHeaderCell: (props: RenderHeaderCellProps<unknown>) => {
// [Joshen] formatter is actually a valid React component, so we can use hooks here
// eslint-disable-next-line react-hooks/rules-of-hooks
const [isRowSelected, onRowSelectionChange] = useRowSelection()
const { isRowSelected, onRowSelectionChange } = useHeaderRowSelection()

return (
<SelectCellHeader
aria-label="Select All"
tabIndex={props.tabIndex}
value={isRowSelected}
onChange={(checked) => onRowSelectionChange({ type: 'HEADER', checked })}
onChange={(checked) => onRowSelectionChange({ checked })}
/>
)
},
renderCell: (props: RenderCellProps<SupaRow>) => {
// [Alaister] formatter is actually a valid React component, so we can use hooks here
// eslint-disable-next-line react-hooks/rules-of-hooks
const [isRowSelected, onRowSelectionChange] = useRowSelection()
const { isRowSelected, onRowSelectionChange } = useRowSelection()
return (
<SelectCellFormatter
aria-label="Select"
tabIndex={props.tabIndex}
value={isRowSelected}
row={props.row}
onChange={(checked, isShiftClick) => {
onRowSelectionChange({ type: 'ROW', row: props.row, checked, isShiftClick })
onRowSelectionChange({ row: props.row, checked, isShiftClick })
}}
// Stop propagation to prevent row selection
onClick={stopPropagation}
Expand All @@ -59,15 +59,14 @@ export const SelectColumn: CalculatedColumn<any, any> = {
renderGroupCell: (props: RenderGroupCellProps<SupaRow>) => {
// [Alaister] groupFormatter is actually a valid React component, so we can use hooks here
// eslint-disable-next-line react-hooks/rules-of-hooks
const [isRowSelected, onRowSelectionChange] = useRowSelection()
const { isRowSelected, onRowSelectionChange } = useRowSelection()
return (
<SelectCellFormatter
aria-label="Select Group"
tabIndex={props.tabIndex}
value={isRowSelected}
onChange={(checked) => {
onRowSelectionChange({
type: 'ROW',
row: props.row,
checked,
isShiftClick: false,
Expand Down
Loading
Loading