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