mirror of
https://github.com/supabase/supabase.git
synced 2026-05-08 09:50:33 -04:00
56de26fe22
This PR migrates the whole monorepo to use Tailwind v4: - Removed `@tailwindcss/container-queries` plugin since it's included by default in v4, - Bump all instances of Tailwind to v4. Made minimal changes to the shared config to remove non-supported features (`alpha` mentions), - Migrate all apps to be compatible with v4 configs, - Fix the `typography.css` import in 3 apps, - Add missing rules which were included by default in v3, - Run `pnpm dlx @tailwindcss/upgrade` on all apps, which renames a lot of classes - Rename all misnamed classes according to https://tailwindcss.com/docs/upgrade-guide#renamed-utilities in all apps. --------- Co-authored-by: Jordi Enric <jordi.err@gmail.com>
391 lines
14 KiB
TypeScript
391 lines
14 KiB
TypeScript
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_Shadcn_,
|
|
AlertDescription_Shadcn_,
|
|
AlertTitle_Shadcn_,
|
|
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 === 'supabase-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),
|
|
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: `supabase functions download ${selectedFunction?.slug}`,
|
|
description: 'Download the function to your local machine',
|
|
jsx: () => (
|
|
<>
|
|
<span className="text-brand">supabase</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_Shadcn_ variant="destructive">
|
|
<CriticalIcon />
|
|
<AlertTitle_Shadcn_>
|
|
Once your function is deleted, it can no longer be restored
|
|
</AlertTitle_Shadcn_>
|
|
<AlertDescription_Shadcn_>
|
|
Make sure you have made a backup if you want to restore your edge function
|
|
</AlertDescription_Shadcn_>
|
|
<AlertDescription_Shadcn_ className="mt-3">
|
|
<Button
|
|
type="danger"
|
|
disabled={!canUpdateEdgeFunction}
|
|
loading={selectedFunction?.id === undefined}
|
|
onClick={() => setShowDeleteModal(true)}
|
|
>
|
|
Delete edge function
|
|
</Button>
|
|
</AlertDescription_Shadcn_>
|
|
</Alert_Shadcn_>
|
|
</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>
|
|
)
|
|
}
|