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
156 changes: 35 additions & 121 deletions apps/design-system/content/docs/fragments/filter-bar.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -85,130 +85,44 @@ interface FilterCondition {

### Component Props

| Prop | Type | Description |
| -------------------- | ------------------------------ | ---------------------------------------- |
| filterProperties | FilterProperty[] | Array of properties that can be filtered |
| filters | FilterGroup | Current filter state |
| onFilterChange | (filters: FilterGroup) => void | Callback when filters change |
| freeformText | string | Current free-form search text |
| onFreeformTextChange | (text: string) => void | Callback when free-form text changes |
| aiApiUrl | string? | Optional URL for AI-powered filtering |
| Prop | Type | Description |
| -------------------- | ------------------------------ | ----------------------------------------------- |
| filterProperties | FilterProperty[] | Array of properties that can be filtered |
| filters | FilterGroup | Current filter state |
| onFilterChange | (filters: FilterGroup) => void | Callback when filters change |
| freeformText | string | Current free-form search text |
| onFreeformTextChange | (text: string) => void | Callback when free-form text changes |
| actions | FilterBarAction[]? | Optional custom actions to show in the menu |
| isLoading | boolean? | If true, dims the bar while work is in progress |

## AI Integration
## Custom actions (e.g. AI)

The Filter Bar component supports AI-powered filtering through an optional API endpoint. When `aiApiUrl` is provided, the component will send natural language queries to be converted into structured filters.

### API Endpoint

The AI API endpoint should accept POST requests with the following structure:

```typescript
// Request body
interface AIFilterRequest {
prompt: string // Natural language query
filterProperties: FilterProperty[] // Available filter properties
}

// Response body
interface AIFilterResponse {
logicalOperator: 'AND' | 'OR'
conditions: (FilterCondition | FilterGroup)[]
}
```

### Example API Implementation

```typescript
import { generateObject } from 'ai'
import { openai } from '@ai-sdk/openai'
import { z } from 'zod'

// Define schemas for validation
const FilterProperty = z.object({
label: z.string(),
name: z.string(),
type: z.enum(['string', 'number', 'date', 'boolean']),
options: z.array(z.string()).optional(),
operators: z.array(z.string()).optional(),
})

const FilterCondition = z.object({
propertyName: z.string(),
value: z.union([z.string(), z.number(), z.boolean(), z.null()]),
operator: z.string(),
})

type FilterGroupType = {
logicalOperator: 'AND' | 'OR'
conditions: Array<z.infer<typeof FilterCondition> | FilterGroupType>
}

const FilterGroup: z.ZodType<FilterGroupType> = z.lazy(() =>
z.object({
logicalOperator: z.enum(['AND', 'OR']),
conditions: z.array(z.union([FilterCondition, FilterGroup])),
})
)

export async function POST(req: Request) {
const { prompt, filterProperties } = await req.json()
const filterPropertiesString = JSON.stringify(filterProperties)

try {
const { object } = await generateObject({
model: openai('gpt-4-mini'),
schema: FilterGroup,
prompt: `Generate a filter group based on the following prompt: "${prompt}".
Use only these filter properties: ${filterPropertiesString}.
Each property has its own set of valid operators defined in the operators field.
Return a filter group with a logical operator ('AND'/'OR') and an array of conditions.
Each condition can be either a filter condition or another filter group.
Filter conditions should have the structure: { propertyName: string, value: string | number | boolean | null, operator: string }.
Ensure that the generated filters use only the provided property names and their corresponding operators.`,
})

// Validate that all propertyNames exist in filterProperties
const validatePropertyNames = (group: FilterGroupType): boolean => {
return group.conditions.every((condition) => {
if ('logicalOperator' in condition) {
return validatePropertyNames(condition as FilterGroupType)
}
const property = filterProperties.find(
(p: z.infer<typeof FilterProperty>) => p.name === condition.propertyName
)
if (!property) return false
// Validate operator is valid for this property
return property.operators?.includes(condition.operator) ?? false
})
}

if (!validatePropertyNames(object)) {
throw new Error('Invalid property names or operators in generated filter')
}

// Zod will throw an error if the object doesn't match the schema
const validatedFilters = FilterGroup.parse(object)
return Response.json(validatedFilters)
} catch (error: any) {
console.error('Error in AI filtering:', error)
return Response.json({ error: error.message || 'AI filtering failed' }, { status: 500 })
}
}
```

### Usage with AI
You can append custom actions to the property menu. Each action receives the current free-form input value and the active group's path so you can plug in AI, saved queries, etc.

```tsx
export function FilterDemoWithAI() {
const [filters, setFilters] = useState<FilterGroup>(initialFilters)
const actions = [
{
value: 'ai-filter',
label: 'Filter by AI',
onSelect: async (inputValue, { path }) => {
const response = await fetch('/api/filter-ai', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ prompt: inputValue, path }),
})
const group = (await response.json()) as FilterGroup
// Replace your filter state at the provided path with the returned group
setFilters((prev) => updateGroupAtPath(prev, path, group))
},
},
]

return (
<FilterBar
filterProperties={filterProperties}
filters={filters}
onFilterChange={setFilters}
aiApiUrl="/api/filter-ai" // Enable AI filtering
/>
)
}
<FilterBar
filterProperties={filterProperties}
filters={filters}
onFilterChange={setFilters}
freeformText={freeformText}
onFreeformTextChange={setFreeformText}
actions={actions}
/>
```
48 changes: 10 additions & 38 deletions apps/design-system/registry/default/example/filter-bar-demo.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { format } from 'date-fns'
import { useState } from 'react'
import { Button, Button_Shadcn_, Calendar, Input_Shadcn_ } from 'ui'
import { DateRange } from 'react-day-picker'
import { Button, Calendar } from 'ui'
import { CustomOptionProps, FilterBar, FilterGroup } from 'ui-patterns'

function CustomDatePicker({ onChange, onCancel, search }: CustomOptionProps) {
const [date, setDate] = useState<any | undefined>(
const [date, setDate] = useState<DateRange | undefined>(
search
? {
from: new Date(search),
Expand Down Expand Up @@ -46,46 +47,18 @@ function CustomDatePicker({ onChange, onCancel, search }: CustomOptionProps) {
)
}

function CustomTimePicker({ onChange, onCancel, search }: CustomOptionProps) {
const [time, setTime] = useState(search || '')

return (
<div className="space-y-4">
<h3 className="text-lg font-medium">Select Time</h3>
<Input_Shadcn_ type="time" value={time} onChange={(e) => setTime(e.target.value)} />
<div className="flex justify-end gap-2">
<Button_Shadcn_ variant="outline" onClick={onCancel}>
Cancel
</Button_Shadcn_>
<Button_Shadcn_ onClick={() => onChange(time)}>Apply</Button_Shadcn_>
</div>
</div>
)
}

function CustomRangePicker({ onChange, onCancel, search }: CustomOptionProps) {
const [range, setRange] = useState(search || '')

return (
<div className="space-y-4">
<h3 className="text-lg font-medium">Select Range</h3>
<Input_Shadcn_ type="range" value={range} onChange={(e) => setRange(e.target.value)} />
<div className="flex justify-end gap-2">
<Button_Shadcn_ variant="outline" onClick={onCancel}>
Cancel
</Button_Shadcn_>
<Button_Shadcn_ onClick={() => onChange(range)}>Apply</Button_Shadcn_>
</div>
</div>
)
}

const filterProperties = [
{
label: 'Name',
name: 'name',
type: 'string' as const,
operators: ['=', '!=', 'CONTAINS', 'STARTS WITH', 'ENDS WITH'],
operators: [
{ value: '=', label: 'Equals' },
{ value: '!=', label: 'Not equals' },
{ value: 'CONTAINS', label: 'Contains' },
{ value: 'STARTS WITH', label: 'Starts with' },
{ value: 'ENDS WITH', label: 'Ends with' },
],
},
{
label: 'Status',
Expand Down Expand Up @@ -138,7 +111,6 @@ const filterProperties = [
</div>
),
},
triggerOnPropertyClick: true,
operators: ['=', '!=', '>', '<', '>=', '<='],
},
]
Expand Down
6 changes: 6 additions & 0 deletions apps/docs/content/guides/storage/s3/compatibility.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,12 @@ The S3 protocol is currently in Public Alpha. If you encounter any issues or hav

</Admonition>

<Admonition type="caution">

**S3 versioning is not supported.** Supabase Storage does not enable S3's versioning capabilities for buckets. Deleted objects are permanently removed and cannot be restored.

</Admonition>

## Implemented endpoints

The most commonly used endpoints are implemented, and more will be added. Implemented S3 endpoints are marked with ✅ in the following tables.
Expand Down
9 changes: 7 additions & 2 deletions apps/studio/components/grid/SupabaseGrid.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { DndProvider } from 'react-dnd'
import { HTML5Backend } from 'react-dnd-html5-backend'
import { createPortal } from 'react-dom'

import { useParams } from 'common'
import { useFlag, useParams } from 'common'
import { isMsSqlForeignTable } from 'data/table-editor/table-editor-types'
import { useTableRowsQuery } from 'data/table-rows/table-rows-query'
import { RoleImpersonationState } from 'lib/role-impersonation'
Expand All @@ -17,6 +17,7 @@ import { Shortcuts } from './components/common/Shortcuts'
import { Footer } from './components/footer/Footer'
import { Grid } from './components/grid/Grid'
import { Header, HeaderProps } from './components/header/Header'
import { HeaderNew } from './components/header/HeaderNew'
import { RowContextMenu } from './components/menu/RowContextMenu'
import { GridProps } from './types'

Expand Down Expand Up @@ -45,6 +46,8 @@ export const SupabaseGrid = ({
const gridRef = useRef<DataGridHandle>(null)
const [mounted, setMounted] = useState(false)

const newFilterBarEnabled = useFlag('tableEditorNewFilterBar')

const { filters } = useTableFilter()
const { sorts, onApplySorts } = useTableSort()

Expand Down Expand Up @@ -90,10 +93,12 @@ export const SupabaseGrid = ({

const rows = data?.rows ?? EMPTY_ARR

const HeaderComponent = newFilterBarEnabled ? HeaderNew : Header

return (
<DndProvider backend={HTML5Backend} context={window}>
<div className="sb-grid h-full flex flex-col">
<Header
<HeaderComponent
customHeader={customHeader}
isRefetching={isRefetching}
tableQueriesEnabled={tableQueriesEnabled}
Expand Down
Loading
Loading