| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388 |
- import { zodResolver } from '@hookform/resolvers/zod'
- import { PermissionAction } from '@supabase/shared-types/out/constants'
- import { IS_PLATFORM, useParams } from 'common'
- import { useRouter } from 'next/router'
- import { useEffect, useMemo, useState } from 'react'
- import { SubmitHandler, useForm } from 'react-hook-form'
- import { toast } from 'sonner'
- import {
- Alert,
- AlertDescription,
- AlertTitle,
- Button,
- Card,
- CardContent,
- CardFooter,
- cn,
- copyToClipboard,
- CriticalIcon,
- Form,
- FormControl,
- FormField,
- Switch,
- Tabs_Shadcn_ as Tabs,
- TabsContent_Shadcn_ as TabsContent,
- TabsList_Shadcn_ as TabsList,
- TabsTrigger_Shadcn_ as TabsTrigger,
- } from 'ui'
- import { CodeBlock } from 'ui-patterns/CodeBlock'
- import { Input } from 'ui-patterns/DataInputs/Input'
- import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal'
- import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
- import { PageContainer } from 'ui-patterns/PageContainer'
- import {
- PageSection,
- PageSectionContent,
- PageSectionMeta,
- PageSectionSummary,
- PageSectionTitle,
- } from 'ui-patterns/PageSection'
- import z from 'zod'
- import CommandRender from '../CommandRender'
- import { INVOCATION_TABS } from './EdgeFunctionDetails.constants'
- import { generateCLICommands } from './EdgeFunctionDetails.utils'
- import { getKeys, useAPIKeysQuery } from '@/data/api-keys/api-keys-query'
- import { useProjectApiUrl } from '@/data/config/project-endpoint-query'
- import { useEdgeFunctionQuery } from '@/data/edge-functions/edge-function-query'
- import { useEdgeFunctionDeleteMutation } from '@/data/edge-functions/edge-functions-delete-mutation'
- import { useEdgeFunctionUpdateMutation } from '@/data/edge-functions/edge-functions-update-mutation'
- import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions'
- import { useIsFeatureEnabled } from '@/hooks/misc/useIsFeatureEnabled'
- const FormSchema = z.object({
- name: z.string().min(0, 'Name is required'),
- verify_jwt: z.boolean(),
- })
- export const EdgeFunctionDetails = () => {
- const router = useRouter()
- const { ref: projectRef, functionSlug } = useParams()
- const showAllEdgeFunctionInvocationExamples = useIsFeatureEnabled(
- 'edge_functions:show_all_edge_function_invocation_examples'
- )
- const invocationTabs = useMemo(() => {
- if (showAllEdgeFunctionInvocationExamples) return INVOCATION_TABS
- return INVOCATION_TABS.filter((tab) => tab.id === 'curl' || tab.id === 'briven-js')
- }, [showAllEdgeFunctionInvocationExamples])
- const [showKey, setShowKey] = useState(false)
- const [selectedTab, setSelectedTab] = useState(invocationTabs[0].id)
- const [showDeleteModal, setShowDeleteModal] = useState(false)
- const { can: canUpdateEdgeFunctionPermission } = useAsyncCheckPermissions(
- PermissionAction.FUNCTIONS_WRITE,
- '*'
- )
- const canUpdateEdgeFunction = IS_PLATFORM && canUpdateEdgeFunctionPermission
- const { can: canReadAPIKeys } = useAsyncCheckPermissions(PermissionAction.SECRETS_READ, '*')
- const { data: apiKeys } = useAPIKeysQuery({ projectRef }, { enabled: canReadAPIKeys })
- const { data: selectedFunction } = useEdgeFunctionQuery({ projectRef, slug: functionSlug })
- const { data: endpoint } = useProjectApiUrl({ projectRef })
- const functionUrl = `${endpoint}/functions/v1/${selectedFunction?.slug}`
- const { mutate: updateEdgeFunction, isPending: isUpdating } = useEdgeFunctionUpdateMutation()
- const { mutate: deleteEdgeFunction, isPending: isDeleting } = useEdgeFunctionDeleteMutation({
- onSuccess: () => {
- toast.success(`Successfully deleted "${selectedFunction?.name}"`)
- router.push(`/project/${projectRef}/functions`)
- },
- })
- const form = useForm({
- resolver: zodResolver(FormSchema as any),
- defaultValues: { name: '', verify_jwt: false },
- })
- const { anonKey, publishableKey } = getKeys(apiKeys)
- const apiKey = publishableKey?.api_key ?? anonKey?.api_key ?? '[YOUR ANON KEY]'
- const { managementCommands } = generateCLICommands({
- selectedFunction,
- functionUrl,
- anonKey: apiKey,
- })
- const onUpdateFunction: SubmitHandler<z.infer<typeof FormSchema>> = async (values: any) => {
- if (!projectRef) return console.error('Project ref is required')
- if (selectedFunction === undefined) return console.error('No edge function selected')
- updateEdgeFunction(
- {
- projectRef,
- slug: selectedFunction.slug,
- payload: values,
- },
- {
- onSuccess: () => {
- toast.success(`Successfully updated edge function`)
- },
- }
- )
- }
- const onConfirmDelete = async () => {
- if (!projectRef) return console.error('Project ref is required')
- if (selectedFunction === undefined) return console.error('No edge function selected')
- deleteEdgeFunction({ projectRef, slug: selectedFunction.slug })
- }
- useEffect(() => {
- if (selectedFunction) {
- form.reset({
- name: selectedFunction.name,
- verify_jwt: selectedFunction.verify_jwt,
- })
- }
- }, [selectedFunction])
- return (
- <PageContainer size="small">
- <PageSection>
- <PageSectionMeta>
- <PageSectionSummary>
- <PageSectionTitle>Function configuration</PageSectionTitle>
- </PageSectionSummary>
- </PageSectionMeta>
- <PageSectionContent>
- <Form {...form}>
- <form onSubmit={form.handleSubmit(onUpdateFunction)}>
- <Card>
- <CardContent>
- <FormField
- control={form.control}
- name="name"
- render={({ field }) => (
- <FormItemLayout
- label="Name"
- layout="flex-row-reverse"
- description="Your slug and endpoint URL will remain the same"
- >
- <FormControl>
- <Input {...field} className="w-64" disabled={!canUpdateEdgeFunction} />
- </FormControl>
- </FormItemLayout>
- )}
- />
- </CardContent>
- {IS_PLATFORM && (
- <>
- <CardContent>
- <FormField
- control={form.control}
- name="verify_jwt"
- render={({ field }) => (
- <FormItemLayout
- label="Verify JWT with legacy secret"
- layout="flex-row-reverse"
- description={
- <>
- <p className="mb-2">
- Requires a JWT signed{' '}
- <em className="text-foreground not-italic">
- only by the legacy secret
- </em>{' '}
- in the{' '}
- <code className="text-code-inline break-keep!">
- Authorization
- </code>{' '}
- header. The <code className="text-code-inline">anon</code> key
- satisfies this.
- </p>
- <p>
- Recommended: OFF with JWT and custom auth logic in your function
- code.
- </p>
- </>
- }
- >
- <FormControl>
- <Switch
- checked={field.value}
- onCheckedChange={field.onChange}
- disabled={!canUpdateEdgeFunction}
- />
- </FormControl>
- </FormItemLayout>
- )}
- />
- </CardContent>
- <CardFooter className="flex justify-end space-x-2">
- {form.formState.isDirty && (
- <Button type="default" onClick={() => form.reset()}>
- Cancel
- </Button>
- )}
- <Button
- type="primary"
- htmlType="submit"
- loading={isUpdating}
- disabled={!canUpdateEdgeFunction || !form.formState.isDirty}
- >
- Save changes
- </Button>
- </CardFooter>
- </>
- )}
- </Card>
- </form>
- </Form>
- </PageSectionContent>
- </PageSection>
- <PageSection>
- <PageSectionMeta>
- <PageSectionSummary>
- <PageSectionTitle>Invoke function</PageSectionTitle>
- </PageSectionSummary>
- </PageSectionMeta>
- <PageSectionContent>
- <Card>
- <CardContent className="px-0">
- <Tabs
- className="w-full"
- defaultValue="curl"
- value={selectedTab}
- onValueChange={setSelectedTab}
- >
- <TabsList className="flex flex-wrap gap-4 px-6">
- {invocationTabs.map((tab) => (
- <TabsTrigger key={tab.id} value={tab.id}>
- {tab.label}
- </TabsTrigger>
- ))}
- {selectedTab === 'curl' && (
- <Button
- type="default"
- className="ml-auto -translate-y-2 translate-x-3"
- onClick={() => setShowKey(!showKey)}
- >
- {showKey ? 'Hide' : 'Show'} anon key
- </Button>
- )}
- </TabsList>
- {invocationTabs.map((tab) => {
- const code = tab.code({
- showKey,
- functionUrl,
- functionName: selectedFunction?.name ?? '',
- apiKey,
- })
- return (
- <TabsContent key={tab.id} value={tab.id}>
- <CodeBlock
- value={code}
- wrapperClassName="[&>div]:top-0 [&>div]:right-3 px-6"
- className={cn(
- 'p-0 text-xs mt-0! border-none ',
- showKey ? '[&>code]:break-all' : '[&>code]:wrap-break-word'
- )}
- language={tab.language}
- wrapLines={false}
- hideLineNumbers={tab.hideLineNumbers}
- handleCopy={() => {
- copyToClipboard(
- tab.code({
- showKey: true,
- functionUrl,
- functionName: selectedFunction?.name ?? '',
- apiKey,
- })
- )
- }}
- />
- </TabsContent>
- )
- })}
- </Tabs>
- </CardContent>
- </Card>
- </PageSectionContent>
- </PageSection>
- {IS_PLATFORM && (
- <>
- <PageSection>
- <PageSectionMeta>
- <PageSectionSummary>
- <PageSectionTitle>Develop locally</PageSectionTitle>
- </PageSectionSummary>
- </PageSectionMeta>
- <PageSectionContent>
- <div className="rounded-sm border bg-surface-100 px-6 py-4 drop-shadow-xs">
- <div className="space-y-6">
- <CommandRender
- commands={[
- {
- command: `briven functions download ${selectedFunction?.slug}`,
- description: 'Download the function to your local machine',
- jsx: () => (
- <>
- <span className="text-brand">briven</span> functions download{' '}
- {selectedFunction?.slug}
- </>
- ),
- comment: '1. Download the function',
- },
- ]}
- />
- <CommandRender commands={[managementCommands[0]]} />
- <CommandRender commands={[managementCommands[1]]} />
- </div>
- </div>
- </PageSectionContent>
- </PageSection>
- <PageSection>
- <PageSectionMeta>
- <PageSectionSummary>
- <PageSectionTitle>Delete function</PageSectionTitle>
- </PageSectionSummary>
- </PageSectionMeta>
- <PageSectionContent>
- <Alert variant="destructive">
- <CriticalIcon />
- <AlertTitle>Once your function is deleted, it can no longer be restored</AlertTitle>
- <AlertDescription>
- Make sure you have made a backup if you want to restore your edge function
- </AlertDescription>
- <AlertDescription className="mt-3">
- <Button
- type="danger"
- disabled={!canUpdateEdgeFunction}
- loading={selectedFunction?.id === undefined}
- onClick={() => setShowDeleteModal(true)}
- >
- Delete edge function
- </Button>
- </AlertDescription>
- </Alert>
- </PageSectionContent>
- </PageSection>
- <ConfirmationModal
- visible={showDeleteModal}
- loading={isDeleting}
- variant="destructive"
- confirmLabel="Delete"
- confirmLabelLoading="Deleting"
- title={`Confirm to delete ${selectedFunction?.name}`}
- onCancel={() => setShowDeleteModal(false)}
- onConfirm={onConfirmDelete}
- alert={{
- base: { variant: 'destructive' },
- title: 'This action cannot be undone',
- description:
- 'Ensure that you have made a backup if you want to restore your edge function',
- }}
- />
- </>
- )}
- </PageContainer>
- )
- }
|