ExposedSchemaSelector.tsx 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168
  1. import { Check, ChevronsUpDown } from 'lucide-react'
  2. import { useMemo, useState } from 'react'
  3. import {
  4. Button,
  5. cn,
  6. Command,
  7. CommandEmpty,
  8. CommandGroup,
  9. CommandInput,
  10. CommandItem,
  11. CommandList,
  12. Popover,
  13. PopoverContent,
  14. PopoverTrigger,
  15. ScrollArea,
  16. } from 'ui'
  17. import { ShimmeringLoader } from 'ui-patterns/ShimmeringLoader'
  18. import { useSchemasQuery } from '@/data/database/schemas-query'
  19. import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
  20. import { INTERNAL_SCHEMAS } from '@/hooks/useProtectedSchemas'
  21. import { pluralize } from '@/lib/helpers'
  22. interface ExposedSchemaSelectorProps {
  23. disabled?: boolean
  24. selectedSchemas: string[]
  25. onToggleSchema: (schema: string) => void
  26. }
  27. export const ExposedSchemaSelector = ({
  28. disabled = false,
  29. selectedSchemas,
  30. onToggleSchema,
  31. }: ExposedSchemaSelectorProps) => {
  32. const [open, setOpen] = useState(false)
  33. const { data: project } = useSelectedProjectQuery()
  34. const {
  35. data: allSchemas,
  36. isPending,
  37. isError,
  38. isSuccess,
  39. } = useSchemasQuery({
  40. projectRef: project?.ref,
  41. connectionString: project?.connectionString,
  42. })
  43. const schemas = useMemo(
  44. () =>
  45. (allSchemas ?? [])
  46. .filter((s) => {
  47. if (s.name === 'graphql_public') return true
  48. return !INTERNAL_SCHEMAS.includes(s.name)
  49. })
  50. .sort((a, b) => a.name.localeCompare(b.name)),
  51. [allSchemas]
  52. )
  53. const missingExposedSchema = useMemo(
  54. () => selectedSchemas.filter((schema) => !schemas.some((s) => s.name === schema)),
  55. [schemas, selectedSchemas]
  56. )
  57. const selectedSet = useMemo(() => new Set(selectedSchemas), [selectedSchemas])
  58. const selectedCount = schemas.filter((s) => selectedSet.has(s.name)).length
  59. return (
  60. <Popover open={open} onOpenChange={setOpen} modal={false}>
  61. <PopoverTrigger asChild>
  62. <Button
  63. size="small"
  64. disabled={disabled}
  65. type="default"
  66. className="w-full [&>span]:w-full pr-1! space-x-1"
  67. iconRight={<ChevronsUpDown className="text-foreground-muted" strokeWidth={2} size={14} />}
  68. >
  69. <div className="w-full flex gap-1">
  70. <p className="text-foreground-lighter">
  71. {isSuccess
  72. ? `${selectedCount} of ${schemas.length} ${pluralize(schemas.length, 'schema')} exposed`
  73. : 'Loading schemas...'}
  74. </p>
  75. </div>
  76. </Button>
  77. </PopoverTrigger>
  78. <PopoverContent
  79. className="p-0 min-w-[200px] pointer-events-auto"
  80. side="bottom"
  81. align="start"
  82. sameWidthAsTrigger
  83. >
  84. <Command>
  85. <CommandInput className="text-xs" placeholder="Find schema..." />
  86. <CommandList>
  87. <CommandGroup>
  88. {isPending ? (
  89. <>
  90. <div className="px-2 py-1">
  91. <ShimmeringLoader className="py-2" />
  92. </div>
  93. <div className="px-2 py-1 w-4/5">
  94. <ShimmeringLoader className="py-2" />
  95. </div>
  96. </>
  97. ) : isError ? (
  98. <div className="flex items-center py-3 justify-center">
  99. <p className="text-xs text-foreground-lighter">Failed to retrieve schemas</p>
  100. </div>
  101. ) : (
  102. <>
  103. <CommandEmpty>
  104. <p className="text-xs text-center text-foreground-lighter py-3">
  105. No schemas found
  106. </p>
  107. </CommandEmpty>
  108. <ScrollArea className={schemas.length > 7 ? 'h-[210px]' : ''}>
  109. {missingExposedSchema.map((schema) => (
  110. <CommandItem
  111. key={schema}
  112. value={schema}
  113. className="cursor-pointer w-full"
  114. onSelect={() => {
  115. onToggleSchema(schema)
  116. }}
  117. >
  118. <div className="w-full flex flex-col">
  119. <div className="w-full flex items-center gap-x-2">
  120. <Check size={16} className="text-brand shrink-0" />
  121. <span className="truncate">{schema}</span>
  122. </div>
  123. <span className="pl-6 text-destructive text-[11px]">
  124. This schema does not exist
  125. </span>
  126. </div>
  127. </CommandItem>
  128. ))}
  129. {schemas.map((schema) => {
  130. const isExposed = selectedSet.has(schema.name)
  131. return (
  132. <CommandItem
  133. key={schema.id}
  134. value={schema.name}
  135. className="cursor-pointer w-full"
  136. onSelect={() => {
  137. onToggleSchema(schema.name)
  138. }}
  139. >
  140. <div
  141. className={cn('w-full flex items-center gap-x-2', !isExposed && 'ml-6')}
  142. >
  143. {isExposed && <Check size={16} className="text-brand shrink-0" />}
  144. <span className="truncate">{schema.name}</span>
  145. </div>
  146. </CommandItem>
  147. )
  148. })}
  149. </ScrollArea>
  150. </>
  151. )}
  152. </CommandGroup>
  153. </CommandList>
  154. </Command>
  155. </PopoverContent>
  156. </Popover>
  157. )
  158. }