Files
supabase/apps/studio/components/interfaces/Settings/API/ExposedSchemaSelector.tsx
Gildas Garcia 0283d92505 fix: allow users to remove missing schemas from the data API (#46169)
## Problem

Exposed schemas (from the Data API) that have been deleted manually (for
instance, using the SQL editor) are still present in the Data API
setting, crashing Postgrest clients. However, the Data API setting does
not show them anymore preventing users from fixing the issue.

## Solution

- Detect missing schemas and show users an error message
- Make missing schema visually distinct
- Allow users to unselect them and update the Data API settings

## How to test

- Create a new schema
- Verify that it is exposed (expose it if necessary) in the Data API
settings
- Delete the schema using the SQL editor
- Verify that you can unselect it from the Data API settings

## Screenshots

<img width="470" height="243" alt="image"
src="https://github.com/user-attachments/assets/2a1f7dc5-c9a9-4779-9f26-1d3e0f66fb8f"
/>


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Bug Fixes**
* Dropdown for exposed API schemas now shows selected schemas that no
longer exist and labels them "This schema does not exist", allowing
users to toggle them.
* "Exposed schemas" field now shows an inline validation/error message
when selected schemas are unavailable to help resolve configuration
issues.

<!-- review_stack_entry_start -->

[![Review Change
Stack](https://storage.googleapis.com/coderabbit_public_assets/review-stack-in-coderabbit-ui.svg)](https://app.coderabbit.ai/change-stack/supabase/supabase/pull/46169?utm_source=github_walkthrough&utm_medium=github&utm_campaign=change_stack)

<!-- review_stack_entry_end -->
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-05-20 17:33:03 +02:00

169 lines
5.6 KiB
TypeScript

import { Check, ChevronsUpDown } from 'lucide-react'
import { useMemo, useState } from 'react'
import {
Button,
cn,
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
Popover,
PopoverContent,
PopoverTrigger,
ScrollArea,
} from 'ui'
import { ShimmeringLoader } from 'ui-patterns/ShimmeringLoader'
import { useSchemasQuery } from '@/data/database/schemas-query'
import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
import { INTERNAL_SCHEMAS } from '@/hooks/useProtectedSchemas'
import { pluralize } from '@/lib/helpers'
interface ExposedSchemaSelectorProps {
disabled?: boolean
selectedSchemas: string[]
onToggleSchema: (schema: string) => void
}
export const ExposedSchemaSelector = ({
disabled = false,
selectedSchemas,
onToggleSchema,
}: ExposedSchemaSelectorProps) => {
const [open, setOpen] = useState(false)
const { data: project } = useSelectedProjectQuery()
const {
data: allSchemas,
isPending,
isError,
isSuccess,
} = useSchemasQuery({
projectRef: project?.ref,
connectionString: project?.connectionString,
})
const schemas = useMemo(
() =>
(allSchemas ?? [])
.filter((s) => {
if (s.name === 'graphql_public') return true
return !INTERNAL_SCHEMAS.includes(s.name)
})
.sort((a, b) => a.name.localeCompare(b.name)),
[allSchemas]
)
const missingExposedSchema = useMemo(
() => selectedSchemas.filter((schema) => !schemas.some((s) => s.name === schema)),
[schemas, selectedSchemas]
)
const selectedSet = useMemo(() => new Set(selectedSchemas), [selectedSchemas])
const selectedCount = schemas.filter((s) => selectedSet.has(s.name)).length
return (
<Popover open={open} onOpenChange={setOpen} modal={false}>
<PopoverTrigger asChild>
<Button
size="small"
disabled={disabled}
type="default"
className="w-full [&>span]:w-full pr-1! space-x-1"
iconRight={<ChevronsUpDown className="text-foreground-muted" strokeWidth={2} size={14} />}
>
<div className="w-full flex gap-1">
<p className="text-foreground-lighter">
{isSuccess
? `${selectedCount} of ${schemas.length} ${pluralize(schemas.length, 'schema')} exposed`
: 'Loading schemas...'}
</p>
</div>
</Button>
</PopoverTrigger>
<PopoverContent
className="p-0 min-w-[200px] pointer-events-auto"
side="bottom"
align="start"
sameWidthAsTrigger
>
<Command>
<CommandInput className="text-xs" placeholder="Find schema..." />
<CommandList>
<CommandGroup>
{isPending ? (
<>
<div className="px-2 py-1">
<ShimmeringLoader className="py-2" />
</div>
<div className="px-2 py-1 w-4/5">
<ShimmeringLoader className="py-2" />
</div>
</>
) : isError ? (
<div className="flex items-center py-3 justify-center">
<p className="text-xs text-foreground-lighter">Failed to retrieve schemas</p>
</div>
) : (
<>
<CommandEmpty>
<p className="text-xs text-center text-foreground-lighter py-3">
No schemas found
</p>
</CommandEmpty>
<ScrollArea className={schemas.length > 7 ? 'h-[210px]' : ''}>
{missingExposedSchema.map((schema) => (
<CommandItem
key={schema}
value={schema}
className="cursor-pointer w-full"
onSelect={() => {
onToggleSchema(schema)
}}
>
<div className="w-full flex flex-col">
<div className="w-full flex items-center gap-x-2">
<Check size={16} className="text-brand shrink-0" />
<span className="truncate">{schema}</span>
</div>
<span className="pl-6 text-destructive text-[11px]">
This schema does not exist
</span>
</div>
</CommandItem>
))}
{schemas.map((schema) => {
const isExposed = selectedSet.has(schema.name)
return (
<CommandItem
key={schema.id}
value={schema.name}
className="cursor-pointer w-full"
onSelect={() => {
onToggleSchema(schema.name)
}}
>
<div
className={cn('w-full flex items-center gap-x-2', !isExposed && 'ml-6')}
>
{isExposed && <Check size={16} className="text-brand shrink-0" />}
<span className="truncate">{schema.name}</span>
</div>
</CommandItem>
)
})}
</ScrollArea>
</>
)}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
)
}