SchemaSelector.tsx233 lines · main
1import { PermissionAction } from '@supabase/shared-types/out/constants'
2import { Check, ChevronsUpDown, Plus } from 'lucide-react'
3import { ComponentPropsWithoutRef, forwardRef, useState } from 'react'
4import {
5 Alert,
6 AlertDescription,
7 AlertTitle,
8 Button,
9 Command,
10 CommandEmpty,
11 CommandGroup,
12 CommandInput,
13 CommandItem,
14 CommandList,
15 CommandSeparator,
16 Popover,
17 PopoverContent,
18 PopoverTrigger,
19 ScrollArea,
20 Skeleton,
21} from 'ui'
22
23import { useSchemasQuery } from '@/data/database/schemas-query'
24import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions'
25import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
26
27type SchemaSelectorProps = Omit<ComponentPropsWithoutRef<'div'>, 'onSelect'> & {
28 disabled?: boolean
29 size?: 'tiny' | 'small'
30 showError?: boolean
31 selectedSchemaName?: string
32 placeholderLabel?: string
33 supportSelectAll?: boolean
34 excludedSchemas?: string[]
35 stopScrollPropagation?: boolean
36 onSelectSchema: (name: string) => void
37 onSelectCreateSchema?: () => void
38 align?: 'start' | 'end'
39 open?: boolean
40 onOpenChange?: (open: boolean) => void
41}
42
43export const SchemaSelector = forwardRef<HTMLDivElement, SchemaSelectorProps>(
44 (
45 {
46 className,
47 disabled = false,
48 size = 'tiny',
49 showError = true,
50 selectedSchemaName,
51 placeholderLabel = 'Choose a schema...',
52 supportSelectAll = false,
53 excludedSchemas = [],
54 stopScrollPropagation = false,
55 onSelectSchema,
56 onSelectCreateSchema,
57 align = 'start',
58 open: openProp,
59 onOpenChange,
60 ...rest
61 },
62 ref
63 ) => {
64 const [internalOpen, setInternalOpen] = useState(false)
65 const isControlled = openProp !== undefined
66 const open = isControlled ? openProp : internalOpen
67 const setOpen = (next: boolean) => {
68 if (!isControlled) setInternalOpen(next)
69 onOpenChange?.(next)
70 }
71 const { can: canCreateSchemas } = useAsyncCheckPermissions(
72 PermissionAction.TENANT_SQL_ADMIN_WRITE,
73 'schemas'
74 )
75
76 const { data: project } = useSelectedProjectQuery()
77 const {
78 data,
79 isPending: isSchemasLoading,
80 isSuccess: isSchemasSuccess,
81 isError: isSchemasError,
82 error: schemasError,
83 refetch: refetchSchemas,
84 } = useSchemasQuery({
85 projectRef: project?.ref,
86 connectionString: project?.connectionString,
87 })
88
89 const schemas = (data || [])
90 .filter((schema) => !excludedSchemas.includes(schema.name))
91 .sort((a, b) => a.name.localeCompare(b.name))
92
93 return (
94 <div ref={ref} className={className} {...rest}>
95 {isSchemasLoading && (
96 <Button
97 type="default"
98 key="schema-selector-skeleton"
99 className="w-full [&>span]:w-full"
100 size={size}
101 disabled
102 >
103 <Skeleton className="w-full h-3 bg-foreground-muted" />
104 </Button>
105 )}
106
107 {showError && isSchemasError && (
108 <Alert variant="warning" className="px-3! py-3!">
109 <AlertTitle className="text-xs text-amber-900">Failed to load schemas</AlertTitle>
110 <AlertDescription className="text-xs mb-2 wrap-break-word">
111 Error: {(schemasError as any)?.message}
112 </AlertDescription>
113 <Button type="default" size="tiny" onClick={() => refetchSchemas()}>
114 Reload schemas
115 </Button>
116 </Alert>
117 )}
118
119 {isSchemasSuccess && (
120 <Popover open={open} onOpenChange={setOpen} modal={false}>
121 <PopoverTrigger asChild>
122 <Button
123 size={size}
124 disabled={disabled}
125 type="default"
126 data-testid="schema-selector"
127 className={`w-full [&>span]:w-full pr-1! space-x-1`}
128 iconRight={
129 <ChevronsUpDown className="text-foreground-muted" strokeWidth={2} size={14} />
130 }
131 >
132 {selectedSchemaName ? (
133 <div className="w-full flex gap-1">
134 <p className="text-foreground-lighter">schema</p>
135 <p className="text-foreground">
136 {selectedSchemaName === '*' ? 'All schemas' : selectedSchemaName}
137 </p>
138 </div>
139 ) : (
140 <div className="w-full flex gap-1">
141 <p className="text-foreground-lighter">{placeholderLabel}</p>
142 </div>
143 )}
144 </Button>
145 </PopoverTrigger>
146 <PopoverContent
147 className="p-0 min-w-[200px] pointer-events-auto"
148 side="bottom"
149 align={align}
150 sameWidthAsTrigger
151 >
152 <Command>
153 <CommandInput className="text-xs" placeholder="Find schema..." />
154 <CommandList
155 onWheel={stopScrollPropagation ? (event) => event.stopPropagation() : undefined}
156 >
157 <CommandEmpty>No schemas found</CommandEmpty>
158 <CommandGroup>
159 <ScrollArea className={(schemas || []).length > 7 ? 'h-[210px]' : ''}>
160 {supportSelectAll && (
161 <CommandItem
162 key="select-all"
163 className="cursor-pointer flex items-center justify-between space-x-2 w-full"
164 onSelect={() => {
165 onSelectSchema('*')
166 setOpen(false)
167 }}
168 onClick={() => {
169 onSelectSchema('*')
170 setOpen(false)
171 }}
172 >
173 <span>All schemas</span>
174 {selectedSchemaName === '*' && (
175 <Check className="text-brand" strokeWidth={2} size={16} />
176 )}
177 </CommandItem>
178 )}
179 {schemas.map((schema) => (
180 <CommandItem
181 key={schema.id}
182 className="cursor-pointer flex items-center justify-between space-x-2 w-full"
183 onSelect={() => {
184 onSelectSchema(schema.name)
185 setOpen(false)
186 }}
187 onClick={() => {
188 onSelectSchema(schema.name)
189 setOpen(false)
190 }}
191 >
192 <span>{schema.name}</span>
193 {selectedSchemaName === schema.name && (
194 <Check className="text-brand" strokeWidth={2} size={16} />
195 )}
196 </CommandItem>
197 ))}
198 </ScrollArea>
199 </CommandGroup>
200 {onSelectCreateSchema !== undefined && canCreateSchemas && (
201 <>
202 <CommandSeparator />
203 <CommandGroup>
204 <CommandItem
205 className="cursor-pointer flex items-center gap-x-2 w-full"
206 onSelect={() => {
207 onSelectCreateSchema()
208 setOpen(false)
209 }}
210 onClick={() => {
211 onSelectCreateSchema()
212 setOpen(false)
213 }}
214 >
215 <Plus size={12} />
216 Create a new schema
217 </CommandItem>
218 </CommandGroup>
219 </>
220 )}
221 </CommandList>
222 </Command>
223 </PopoverContent>
224 </Popover>
225 )}
226 </div>
227 )
228 }
229)
230
231SchemaSelector.displayName = 'SchemaSelector'
232
233export default SchemaSelector