SchemaSelector.tsx 8.0 KB

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