EdgeFunctionDetails.tsx 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388
  1. import { zodResolver } from '@hookform/resolvers/zod'
  2. import { PermissionAction } from '@supabase/shared-types/out/constants'
  3. import { IS_PLATFORM, useParams } from 'common'
  4. import { useRouter } from 'next/router'
  5. import { useEffect, useMemo, useState } from 'react'
  6. import { SubmitHandler, useForm } from 'react-hook-form'
  7. import { toast } from 'sonner'
  8. import {
  9. Alert,
  10. AlertDescription,
  11. AlertTitle,
  12. Button,
  13. Card,
  14. CardContent,
  15. CardFooter,
  16. cn,
  17. copyToClipboard,
  18. CriticalIcon,
  19. Form,
  20. FormControl,
  21. FormField,
  22. Switch,
  23. Tabs_Shadcn_ as Tabs,
  24. TabsContent_Shadcn_ as TabsContent,
  25. TabsList_Shadcn_ as TabsList,
  26. TabsTrigger_Shadcn_ as TabsTrigger,
  27. } from 'ui'
  28. import { CodeBlock } from 'ui-patterns/CodeBlock'
  29. import { Input } from 'ui-patterns/DataInputs/Input'
  30. import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal'
  31. import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
  32. import { PageContainer } from 'ui-patterns/PageContainer'
  33. import {
  34. PageSection,
  35. PageSectionContent,
  36. PageSectionMeta,
  37. PageSectionSummary,
  38. PageSectionTitle,
  39. } from 'ui-patterns/PageSection'
  40. import z from 'zod'
  41. import CommandRender from '../CommandRender'
  42. import { INVOCATION_TABS } from './EdgeFunctionDetails.constants'
  43. import { generateCLICommands } from './EdgeFunctionDetails.utils'
  44. import { getKeys, useAPIKeysQuery } from '@/data/api-keys/api-keys-query'
  45. import { useProjectApiUrl } from '@/data/config/project-endpoint-query'
  46. import { useEdgeFunctionQuery } from '@/data/edge-functions/edge-function-query'
  47. import { useEdgeFunctionDeleteMutation } from '@/data/edge-functions/edge-functions-delete-mutation'
  48. import { useEdgeFunctionUpdateMutation } from '@/data/edge-functions/edge-functions-update-mutation'
  49. import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions'
  50. import { useIsFeatureEnabled } from '@/hooks/misc/useIsFeatureEnabled'
  51. const FormSchema = z.object({
  52. name: z.string().min(0, 'Name is required'),
  53. verify_jwt: z.boolean(),
  54. })
  55. export const EdgeFunctionDetails = () => {
  56. const router = useRouter()
  57. const { ref: projectRef, functionSlug } = useParams()
  58. const showAllEdgeFunctionInvocationExamples = useIsFeatureEnabled(
  59. 'edge_functions:show_all_edge_function_invocation_examples'
  60. )
  61. const invocationTabs = useMemo(() => {
  62. if (showAllEdgeFunctionInvocationExamples) return INVOCATION_TABS
  63. return INVOCATION_TABS.filter((tab) => tab.id === 'curl' || tab.id === 'briven-js')
  64. }, [showAllEdgeFunctionInvocationExamples])
  65. const [showKey, setShowKey] = useState(false)
  66. const [selectedTab, setSelectedTab] = useState(invocationTabs[0].id)
  67. const [showDeleteModal, setShowDeleteModal] = useState(false)
  68. const { can: canUpdateEdgeFunctionPermission } = useAsyncCheckPermissions(
  69. PermissionAction.FUNCTIONS_WRITE,
  70. '*'
  71. )
  72. const canUpdateEdgeFunction = IS_PLATFORM && canUpdateEdgeFunctionPermission
  73. const { can: canReadAPIKeys } = useAsyncCheckPermissions(PermissionAction.SECRETS_READ, '*')
  74. const { data: apiKeys } = useAPIKeysQuery({ projectRef }, { enabled: canReadAPIKeys })
  75. const { data: selectedFunction } = useEdgeFunctionQuery({ projectRef, slug: functionSlug })
  76. const { data: endpoint } = useProjectApiUrl({ projectRef })
  77. const functionUrl = `${endpoint}/functions/v1/${selectedFunction?.slug}`
  78. const { mutate: updateEdgeFunction, isPending: isUpdating } = useEdgeFunctionUpdateMutation()
  79. const { mutate: deleteEdgeFunction, isPending: isDeleting } = useEdgeFunctionDeleteMutation({
  80. onSuccess: () => {
  81. toast.success(`Successfully deleted "${selectedFunction?.name}"`)
  82. router.push(`/project/${projectRef}/functions`)
  83. },
  84. })
  85. const form = useForm({
  86. resolver: zodResolver(FormSchema as any),
  87. defaultValues: { name: '', verify_jwt: false },
  88. })
  89. const { anonKey, publishableKey } = getKeys(apiKeys)
  90. const apiKey = publishableKey?.api_key ?? anonKey?.api_key ?? '[YOUR ANON KEY]'
  91. const { managementCommands } = generateCLICommands({
  92. selectedFunction,
  93. functionUrl,
  94. anonKey: apiKey,
  95. })
  96. const onUpdateFunction: SubmitHandler<z.infer<typeof FormSchema>> = async (values: any) => {
  97. if (!projectRef) return console.error('Project ref is required')
  98. if (selectedFunction === undefined) return console.error('No edge function selected')
  99. updateEdgeFunction(
  100. {
  101. projectRef,
  102. slug: selectedFunction.slug,
  103. payload: values,
  104. },
  105. {
  106. onSuccess: () => {
  107. toast.success(`Successfully updated edge function`)
  108. },
  109. }
  110. )
  111. }
  112. const onConfirmDelete = async () => {
  113. if (!projectRef) return console.error('Project ref is required')
  114. if (selectedFunction === undefined) return console.error('No edge function selected')
  115. deleteEdgeFunction({ projectRef, slug: selectedFunction.slug })
  116. }
  117. useEffect(() => {
  118. if (selectedFunction) {
  119. form.reset({
  120. name: selectedFunction.name,
  121. verify_jwt: selectedFunction.verify_jwt,
  122. })
  123. }
  124. }, [selectedFunction])
  125. return (
  126. <PageContainer size="small">
  127. <PageSection>
  128. <PageSectionMeta>
  129. <PageSectionSummary>
  130. <PageSectionTitle>Function configuration</PageSectionTitle>
  131. </PageSectionSummary>
  132. </PageSectionMeta>
  133. <PageSectionContent>
  134. <Form {...form}>
  135. <form onSubmit={form.handleSubmit(onUpdateFunction)}>
  136. <Card>
  137. <CardContent>
  138. <FormField
  139. control={form.control}
  140. name="name"
  141. render={({ field }) => (
  142. <FormItemLayout
  143. label="Name"
  144. layout="flex-row-reverse"
  145. description="Your slug and endpoint URL will remain the same"
  146. >
  147. <FormControl>
  148. <Input {...field} className="w-64" disabled={!canUpdateEdgeFunction} />
  149. </FormControl>
  150. </FormItemLayout>
  151. )}
  152. />
  153. </CardContent>
  154. {IS_PLATFORM && (
  155. <>
  156. <CardContent>
  157. <FormField
  158. control={form.control}
  159. name="verify_jwt"
  160. render={({ field }) => (
  161. <FormItemLayout
  162. label="Verify JWT with legacy secret"
  163. layout="flex-row-reverse"
  164. description={
  165. <>
  166. <p className="mb-2">
  167. Requires a JWT signed{' '}
  168. <em className="text-foreground not-italic">
  169. only by the legacy secret
  170. </em>{' '}
  171. in the{' '}
  172. <code className="text-code-inline break-keep!">
  173. Authorization
  174. </code>{' '}
  175. header. The <code className="text-code-inline">anon</code> key
  176. satisfies this.
  177. </p>
  178. <p>
  179. Recommended: OFF with JWT and custom auth logic in your function
  180. code.
  181. </p>
  182. </>
  183. }
  184. >
  185. <FormControl>
  186. <Switch
  187. checked={field.value}
  188. onCheckedChange={field.onChange}
  189. disabled={!canUpdateEdgeFunction}
  190. />
  191. </FormControl>
  192. </FormItemLayout>
  193. )}
  194. />
  195. </CardContent>
  196. <CardFooter className="flex justify-end space-x-2">
  197. {form.formState.isDirty && (
  198. <Button type="default" onClick={() => form.reset()}>
  199. Cancel
  200. </Button>
  201. )}
  202. <Button
  203. type="primary"
  204. htmlType="submit"
  205. loading={isUpdating}
  206. disabled={!canUpdateEdgeFunction || !form.formState.isDirty}
  207. >
  208. Save changes
  209. </Button>
  210. </CardFooter>
  211. </>
  212. )}
  213. </Card>
  214. </form>
  215. </Form>
  216. </PageSectionContent>
  217. </PageSection>
  218. <PageSection>
  219. <PageSectionMeta>
  220. <PageSectionSummary>
  221. <PageSectionTitle>Invoke function</PageSectionTitle>
  222. </PageSectionSummary>
  223. </PageSectionMeta>
  224. <PageSectionContent>
  225. <Card>
  226. <CardContent className="px-0">
  227. <Tabs
  228. className="w-full"
  229. defaultValue="curl"
  230. value={selectedTab}
  231. onValueChange={setSelectedTab}
  232. >
  233. <TabsList className="flex flex-wrap gap-4 px-6">
  234. {invocationTabs.map((tab) => (
  235. <TabsTrigger key={tab.id} value={tab.id}>
  236. {tab.label}
  237. </TabsTrigger>
  238. ))}
  239. {selectedTab === 'curl' && (
  240. <Button
  241. type="default"
  242. className="ml-auto -translate-y-2 translate-x-3"
  243. onClick={() => setShowKey(!showKey)}
  244. >
  245. {showKey ? 'Hide' : 'Show'} anon key
  246. </Button>
  247. )}
  248. </TabsList>
  249. {invocationTabs.map((tab) => {
  250. const code = tab.code({
  251. showKey,
  252. functionUrl,
  253. functionName: selectedFunction?.name ?? '',
  254. apiKey,
  255. })
  256. return (
  257. <TabsContent key={tab.id} value={tab.id}>
  258. <CodeBlock
  259. value={code}
  260. wrapperClassName="[&>div]:top-0 [&>div]:right-3 px-6"
  261. className={cn(
  262. 'p-0 text-xs mt-0! border-none ',
  263. showKey ? '[&>code]:break-all' : '[&>code]:wrap-break-word'
  264. )}
  265. language={tab.language}
  266. wrapLines={false}
  267. hideLineNumbers={tab.hideLineNumbers}
  268. handleCopy={() => {
  269. copyToClipboard(
  270. tab.code({
  271. showKey: true,
  272. functionUrl,
  273. functionName: selectedFunction?.name ?? '',
  274. apiKey,
  275. })
  276. )
  277. }}
  278. />
  279. </TabsContent>
  280. )
  281. })}
  282. </Tabs>
  283. </CardContent>
  284. </Card>
  285. </PageSectionContent>
  286. </PageSection>
  287. {IS_PLATFORM && (
  288. <>
  289. <PageSection>
  290. <PageSectionMeta>
  291. <PageSectionSummary>
  292. <PageSectionTitle>Develop locally</PageSectionTitle>
  293. </PageSectionSummary>
  294. </PageSectionMeta>
  295. <PageSectionContent>
  296. <div className="rounded-sm border bg-surface-100 px-6 py-4 drop-shadow-xs">
  297. <div className="space-y-6">
  298. <CommandRender
  299. commands={[
  300. {
  301. command: `briven functions download ${selectedFunction?.slug}`,
  302. description: 'Download the function to your local machine',
  303. jsx: () => (
  304. <>
  305. <span className="text-brand">briven</span> functions download{' '}
  306. {selectedFunction?.slug}
  307. </>
  308. ),
  309. comment: '1. Download the function',
  310. },
  311. ]}
  312. />
  313. <CommandRender commands={[managementCommands[0]]} />
  314. <CommandRender commands={[managementCommands[1]]} />
  315. </div>
  316. </div>
  317. </PageSectionContent>
  318. </PageSection>
  319. <PageSection>
  320. <PageSectionMeta>
  321. <PageSectionSummary>
  322. <PageSectionTitle>Delete function</PageSectionTitle>
  323. </PageSectionSummary>
  324. </PageSectionMeta>
  325. <PageSectionContent>
  326. <Alert variant="destructive">
  327. <CriticalIcon />
  328. <AlertTitle>Once your function is deleted, it can no longer be restored</AlertTitle>
  329. <AlertDescription>
  330. Make sure you have made a backup if you want to restore your edge function
  331. </AlertDescription>
  332. <AlertDescription className="mt-3">
  333. <Button
  334. type="danger"
  335. disabled={!canUpdateEdgeFunction}
  336. loading={selectedFunction?.id === undefined}
  337. onClick={() => setShowDeleteModal(true)}
  338. >
  339. Delete edge function
  340. </Button>
  341. </AlertDescription>
  342. </Alert>
  343. </PageSectionContent>
  344. </PageSection>
  345. <ConfirmationModal
  346. visible={showDeleteModal}
  347. loading={isDeleting}
  348. variant="destructive"
  349. confirmLabel="Delete"
  350. confirmLabelLoading="Deleting"
  351. title={`Confirm to delete ${selectedFunction?.name}`}
  352. onCancel={() => setShowDeleteModal(false)}
  353. onConfirm={onConfirmDelete}
  354. alert={{
  355. base: { variant: 'destructive' },
  356. title: 'This action cannot be undone',
  357. description:
  358. 'Ensure that you have made a backup if you want to restore your edge function',
  359. }}
  360. />
  361. </>
  362. )}
  363. </PageContainer>
  364. )
  365. }