FunctionList.tsx245 lines · main
1import { PermissionAction } from '@supabase/shared-types/out/constants'
2import { includes, noop, sortBy } from 'lodash'
3import { Copy, Edit, Edit2, FileText, MoreVertical, Trash } from 'lucide-react'
4import Link from 'next/link'
5import { useRouter } from 'next/router'
6import {
7 Button,
8 DropdownMenu,
9 DropdownMenuContent,
10 DropdownMenuItem,
11 DropdownMenuSeparator,
12 DropdownMenuTrigger,
13 TableCell,
14 TableRow,
15} from 'ui'
16
17import { getDatabaseTriggersHref } from './FunctionList.utils'
18import { SIDEBAR_KEYS } from '@/components/layouts/ProjectLayout/LayoutSidebar/LayoutSidebarProvider'
19import { ButtonTooltip } from '@/components/ui/ButtonTooltip'
20import type { DatabaseFunction } from '@/data/database-functions/database-functions-query'
21import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions'
22import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
23import { useAiAssistantStateSnapshot } from '@/state/ai-assistant-state'
24import { useSidebarManagerSnapshot } from '@/state/sidebar-manager-state'
25
26interface FunctionListProps {
27 schema: string
28 filterString: string
29 isLocked: boolean
30 returnTypeFilter: string[]
31 securityFilter: string[]
32 duplicateFunction: (fn: any) => void
33 editFunction: (fn: any) => void
34 deleteFunction: (fn: any) => void
35 functions: DatabaseFunction[]
36}
37
38const FunctionList = ({
39 schema,
40 filterString,
41 isLocked,
42 returnTypeFilter,
43 securityFilter,
44 duplicateFunction = noop,
45 editFunction = noop,
46 deleteFunction = noop,
47 functions,
48}: FunctionListProps) => {
49 const router = useRouter()
50 const { data: selectedProject } = useSelectedProjectQuery()
51 const aiSnap = useAiAssistantStateSnapshot()
52 const { openSidebar } = useSidebarManagerSnapshot()
53
54 const filteredFunctions = (functions ?? []).filter((x) => {
55 const matchesName = includes(x.name.toLowerCase(), filterString.toLowerCase())
56 const matchesReturnType =
57 returnTypeFilter.length === 0 || returnTypeFilter.includes(x.return_type)
58 const matchesSecurity =
59 securityFilter.length === 0 ||
60 (securityFilter.includes('definer') && x.security_definer) ||
61 (securityFilter.includes('invoker') && !x.security_definer)
62 return matchesName && matchesReturnType && matchesSecurity
63 })
64 const _functions = sortBy(
65 filteredFunctions.filter((x) => x.schema == schema),
66 (func) => func.name.toLocaleLowerCase()
67 )
68 const projectRef = selectedProject?.ref
69 const { can: canUpdateFunctions } = useAsyncCheckPermissions(
70 PermissionAction.TENANT_SQL_ADMIN_WRITE,
71 'functions'
72 )
73
74 if (_functions.length === 0 && filterString.length === 0) {
75 return (
76 <TableRow key={schema}>
77 <TableCell colSpan={5}>
78 <p className="text-sm text-foreground">No functions created yet</p>
79 <p className="text-sm text-foreground-light">
80 There are no functions found in the schema "{schema}"
81 </p>
82 </TableCell>
83 </TableRow>
84 )
85 }
86
87 if (_functions.length === 0 && filterString.length > 0) {
88 return (
89 <TableRow key={schema}>
90 <TableCell colSpan={5}>
91 <p className="text-sm text-foreground">No results found</p>
92 <p className="text-sm text-foreground-light">
93 Your search for "{filterString}" did not return any results
94 </p>
95 </TableCell>
96 </TableRow>
97 )
98 }
99
100 return (
101 <>
102 {_functions.map((x) => {
103 const isApiDocumentAvailable = schema == 'public' && x.return_type !== 'trigger'
104
105 return (
106 <TableRow key={x.id}>
107 <TableCell className="truncate">
108 <Button
109 type="text"
110 className="text-link-table-cell text-sm disabled:opacity-100 disabled:no-underline p-0 hover:bg-transparent title"
111 disabled={isLocked || !canUpdateFunctions}
112 onClick={() => editFunction(x)}
113 title={x.name}
114 >
115 {x.name}
116 </Button>
117 </TableCell>
118 <TableCell className="table-cell">
119 <p
120 title={x.argument_types}
121 className={`truncate ${x.argument_types ? 'text-foreground-light' : 'text-foreground-muted'}`}
122 >
123 {x.argument_types || '–'}
124 </p>
125 </TableCell>
126 <TableCell className="table-cell">
127 {x.return_type === 'trigger' ? (
128 <Link
129 href={getDatabaseTriggersHref(projectRef, x.name)}
130 className="truncate text-link"
131 title={x.return_type}
132 >
133 {x.return_type}
134 </Link>
135 ) : (
136 <p title={x.return_type} className="truncate text-foreground-light">
137 {x.return_type}
138 </p>
139 )}
140 </TableCell>
141 <TableCell className="table-cell">
142 <p className="truncate text-foreground-light">
143 {x.security_definer ? 'Definer' : 'Invoker'}
144 </p>
145 </TableCell>
146 <TableCell className="text-right">
147 {!isLocked && (
148 <div className="flex items-center justify-end">
149 {canUpdateFunctions ? (
150 <DropdownMenu>
151 <DropdownMenuTrigger asChild>
152 <Button
153 aria-label="More options"
154 type="default"
155 className="px-1"
156 icon={<MoreVertical />}
157 />
158 </DropdownMenuTrigger>
159 <DropdownMenuContent side="left" className="w-52">
160 {isApiDocumentAvailable && (
161 <DropdownMenuItem
162 className="space-x-2"
163 onClick={() => router.push(`/project/${projectRef}/api?rpc=${x.name}`)}
164 >
165 <FileText size={14} />
166 <p>Client API docs</p>
167 </DropdownMenuItem>
168 )}
169 <DropdownMenuSeparator />
170 <DropdownMenuItem className="space-x-2" onClick={() => editFunction(x)}>
171 <Edit2 size={14} />
172 <p>Edit function</p>
173 </DropdownMenuItem>
174 <DropdownMenuItem
175 className="space-x-2"
176 onClick={() => {
177 openSidebar(SIDEBAR_KEYS.AI_ASSISTANT)
178 aiSnap.newChat({
179 name: `Update function ${x.name}`,
180 initialInput: 'Update this function to do...',
181 suggestions: {
182 title:
183 'I can help you make a change to this function, here are a few example prompts to get you started:',
184 prompts: [
185 {
186 label: 'Rename Function',
187 description: 'Rename this function to ...',
188 },
189 {
190 label: 'Modify Function',
191 description: 'Modify this function so that it ...',
192 },
193 {
194 label: 'Add Trigger',
195 description:
196 'Add a trigger for this function that calls it when ...',
197 },
198 ],
199 },
200 sqlSnippets: [x.complete_statement],
201 })
202 }}
203 >
204 <Edit size={14} />
205 <p>Edit function with Assistant</p>
206 </DropdownMenuItem>
207 <DropdownMenuItem
208 className="space-x-2"
209 onClick={() => duplicateFunction(x)}
210 >
211 <Copy size={14} />
212 <p>Duplicate function</p>
213 </DropdownMenuItem>
214 <DropdownMenuSeparator />
215 <DropdownMenuItem className="space-x-2" onClick={() => deleteFunction(x)}>
216 <Trash size={14} className="text-destructive" />
217 <p>Delete function</p>
218 </DropdownMenuItem>
219 </DropdownMenuContent>
220 </DropdownMenu>
221 ) : (
222 <ButtonTooltip
223 disabled
224 type="default"
225 icon={<MoreVertical />}
226 className="px-1"
227 tooltip={{
228 content: {
229 side: 'left',
230 text: 'You need additional permissions to update functions',
231 },
232 }}
233 />
234 )}
235 </div>
236 )}
237 </TableCell>
238 </TableRow>
239 )
240 })}
241 </>
242 )
243}
244
245export default FunctionList