FunctionSelector.tsx196 lines · main
1import { useParams } from 'common'
2import { uniqBy } from 'lodash'
3import { Check, ChevronsUpDown, Plus } from 'lucide-react'
4import Link from 'next/link'
5import { useRouter } from 'next/router'
6import { useState } from 'react'
7import {
8 Alert,
9 AlertDescription,
10 AlertTitle,
11 Button,
12 Command,
13 CommandEmpty,
14 CommandGroup,
15 CommandInput,
16 CommandItem,
17 CommandList,
18 CommandSeparator,
19 Popover,
20 PopoverContent,
21 PopoverTrigger,
22 ScrollArea,
23} from 'ui'
24
25import {
26 DatabaseFunctionsData,
27 useDatabaseFunctionsQuery,
28} from '@/data/database-functions/database-functions-query'
29import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
30
31type DatabaseFunction = DatabaseFunctionsData[number]
32
33interface FunctionSelectorProps {
34 className?: string
35 size?: 'tiny' | 'small'
36 showError?: boolean
37 schema?: string
38 value: string
39 onChange: (value: string) => void
40 disabled?: boolean
41 stopScrollPropagation?: boolean
42 // used to filter the functions by a criteria
43 filterFunction?: (func: DatabaseFunction) => boolean
44 noResultsLabel?: React.ReactNode
45}
46
47const FunctionSelector = ({
48 className,
49 size = 'tiny',
50 showError = true,
51 disabled = false,
52 schema,
53 value,
54 onChange,
55 stopScrollPropagation = false,
56 filterFunction = () => true,
57 noResultsLabel = <span>No functions found in this schema.</span>,
58}: FunctionSelectorProps) => {
59 const router = useRouter()
60 const { ref } = useParams()
61 const { data: project } = useSelectedProjectQuery()
62 const [open, setOpen] = useState(false)
63
64 const {
65 data,
66 error,
67 isPending: isLoading,
68 isError,
69 isSuccess,
70 refetch,
71 } = useDatabaseFunctionsQuery({
72 projectRef: project?.ref,
73 connectionString: project?.connectionString,
74 })
75
76 const filteredFunctions = (data ?? [])
77 .filter((func) => schema && func.schema === schema)
78 .filter(filterFunction)
79 const functions = uniqBy(filteredFunctions, (func) => func.name)
80
81 return (
82 <div className={className}>
83 {isLoading && (
84 <Button type="default" className="justify-start" block size={size} loading>
85 Loading functions...
86 </Button>
87 )}
88
89 {showError && isError && (
90 <Alert variant="warning" className="px-3! py-3!">
91 <AlertTitle className="text-xs text-amber-900">Failed to load functions</AlertTitle>
92
93 <AlertDescription className="text-xs mb-2">Error: {error.message}</AlertDescription>
94
95 <Button type="default" size="tiny" onClick={() => refetch()}>
96 Reload functions
97 </Button>
98 </Alert>
99 )}
100
101 {isSuccess && (
102 <Popover open={open} onOpenChange={setOpen} modal={false}>
103 <PopoverTrigger asChild>
104 <Button
105 size={size}
106 disabled={!!disabled}
107 type="default"
108 className={`w-full [&>span]:w-full ${size === 'small' ? 'py-1.5' : ''}`}
109 iconRight={
110 <ChevronsUpDown className="text-foreground-muted" strokeWidth={2} size={14} />
111 }
112 >
113 {value ? (
114 <div className="w-full flex gap-1">
115 <p className="text-foreground-lighter">function:</p>
116 <p className="text-foreground">{value}</p>
117 </div>
118 ) : (
119 <div className="w-full flex gap-1">
120 <p className="text-foreground-lighter">Select a function</p>
121 </div>
122 )}
123 </Button>
124 </PopoverTrigger>
125 <PopoverContent className="p-0" side="bottom" align="start" sameWidthAsTrigger>
126 <Command>
127 <CommandInput placeholder="Search functions..." />
128 <CommandList
129 onWheel={stopScrollPropagation ? (event) => event.stopPropagation() : undefined}
130 >
131 <CommandEmpty>No functions found</CommandEmpty>
132 <CommandGroup>
133 <ScrollArea className={(functions || []).length > 7 ? 'h-[210px]' : ''}>
134 {!functions.length && (
135 <CommandItem
136 key="no-function-found"
137 disabled={true}
138 className="flex items-center justify-between space-x-2 w-full"
139 >
140 {noResultsLabel}
141 </CommandItem>
142 )}
143 {functions.map((func) => (
144 <CommandItem
145 key={func.id}
146 value={func.name.replaceAll('"', '')}
147 className="cursor-pointer flex items-center justify-between space-x-2 w-full"
148 onSelect={() => {
149 onChange(func.name)
150 setOpen(false)
151 }}
152 onClick={() => {
153 onChange(func.name)
154 setOpen(false)
155 }}
156 >
157 <span>{func.name}</span>
158 {value === func.name && (
159 <Check className="text-brand" size={14} strokeWidth={2} />
160 )}
161 </CommandItem>
162 ))}
163 </ScrollArea>
164 </CommandGroup>
165 <CommandSeparator />
166 <CommandGroup>
167 <CommandItem
168 className="cursor-pointer w-full"
169 onSelect={() => {
170 setOpen(false)
171 router.push(`/project/${ref}/database/functions`)
172 }}
173 onClick={() => setOpen(false)}
174 >
175 <Link
176 href={`/project/${ref}/database/functions`}
177 onClick={() => {
178 setOpen(false)
179 }}
180 className="w-full flex items-center gap-2"
181 >
182 <Plus size={14} strokeWidth={1.5} />
183 <p>New function</p>
184 </Link>
185 </CommandItem>
186 </CommandGroup>
187 </CommandList>
188 </Command>
189 </PopoverContent>
190 </Popover>
191 )}
192 </div>
193 )
194}
195
196export default FunctionSelector