Add download edge functions CTA to edge function details header (#34501)

* Add download edge functions CTA to edge function details header

* Add CLI command

* Update copy

* Small tweak based on feedback
This commit is contained in:
Joshen Lim
2025-03-31 14:06:25 +08:00
committed by GitHub
parent c41afbb565
commit b9b748391d
4 changed files with 82 additions and 9 deletions

View File

@@ -1,9 +1,10 @@
import { PermissionAction } from '@supabase/shared-types/out/constants'
import { Send } from 'lucide-react'
import { Download, FileArchive, Send } from 'lucide-react'
import { useRouter } from 'next/router'
import { useEffect, useState, type PropsWithChildren } from 'react'
import { toast } from 'sonner'
import { BlobReader, BlobWriter, ZipWriter } from '@zip.js/zip.js'
import { useParams } from 'common'
import { useIsAPIDocsSidePanelEnabled } from 'components/interfaces/App/FeaturePreview/FeaturePreviewContext'
import { EdgeFunctionTesterSheet } from 'components/interfaces/Functions/EdgeFunctionDetails/EdgeFunctionTesterSheet'
@@ -11,13 +12,21 @@ import { PageLayout } from 'components/layouts/PageLayout/PageLayout'
import APIDocsButton from 'components/ui/APIDocsButton'
import { DocsButton } from 'components/ui/DocsButton'
import NoPermission from 'components/ui/NoPermission'
import { useEdgeFunctionBodyQuery } from 'data/edge-functions/edge-function-body-query'
import { useEdgeFunctionQuery } from 'data/edge-functions/edge-function-query'
import { useSendEventMutation } from 'data/telemetry/send-event-mutation'
import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions'
import { useSelectedOrganization } from 'hooks/misc/useSelectedOrganization'
import { withAuth } from 'hooks/misc/withAuth'
import { useFlag } from 'hooks/ui/useFlag'
import { Button } from 'ui'
import {
Button,
Popover_Shadcn_,
PopoverContent_Shadcn_,
PopoverTrigger_Shadcn_,
Separator,
} from 'ui'
import { Input } from 'ui-patterns/DataInputs/Input'
import ProjectLayout from '../ProjectLayout/ProjectLayout'
import EdgeFunctionsLayout from './EdgeFunctionsLayout'
@@ -49,7 +58,13 @@ const EdgeFunctionDetailsLayout = ({
isError,
} = useEdgeFunctionQuery({ projectRef: ref, slug: functionSlug })
const { data: functionFiles = [], error: filesError } = useEdgeFunctionBodyQuery({
projectRef: ref,
slug: functionSlug,
})
const name = selectedFunction?.name || ''
const cliCommand = `supabase functions download ${functionSlug}`
const breadcrumbItems = [
{
@@ -87,6 +102,29 @@ const EdgeFunctionDetailsLayout = ({
]
: []
const downloadFunction = async () => {
if (filesError) return toast.error('Failed to retrieve edge function files')
const zipFileWriter = new BlobWriter('application/zip')
const zipWriter = new ZipWriter(zipFileWriter, { bufferedWrite: true })
functionFiles.forEach((file) => {
const nameSections = file.name.split('/')
const slugIndex = nameSections.indexOf(functionSlug ?? '')
const fileName = nameSections.slice(slugIndex + 1).join('/')
const fileBlob = new Blob([file.content])
zipWriter.add(fileName, new BlobReader(fileBlob))
})
const blobURL = URL.createObjectURL(await zipWriter.close())
const link = document.createElement('a')
link.href = blobURL
link.setAttribute('download', `${functionSlug}.zip`)
document.body.appendChild(link)
link.click()
link.parentNode?.removeChild(link)
}
useEffect(() => {
let cancel = false
@@ -126,6 +164,37 @@ const EdgeFunctionDetailsLayout = ({
/>
)}
<DocsButton href="https://supabase.com/docs/guides/functions" />
<Popover_Shadcn_>
<PopoverTrigger_Shadcn_ asChild>
<Button type="default" icon={<Download />}>
Download
</Button>
</PopoverTrigger_Shadcn_>
<PopoverContent_Shadcn_ align="end" className="p-0">
<div className="p-3 flex flex-col gap-y-2">
<p className="text-xs text-foreground-light">Download via CLI</p>
<Input
copy
showCopyOnHover
readOnly
containerClassName=""
className="text-xs font-mono tracking-tighter"
value={`supabase functions download ${functionSlug}`}
/>
</div>
<Separator className="!bg-border-overlay" />
<div className="py-2 px-1">
<Button
type="text"
className="w-min hover:bg-transparent"
icon={<FileArchive />}
onClick={downloadFunction}
>
Download as ZIP
</Button>
</div>
</PopoverContent_Shadcn_>
</Popover_Shadcn_>
{edgeFunctionCreate && !!functionSlug && (
<Button
type="default"

View File

@@ -1,6 +1,6 @@
import { useQuery, UseQueryOptions } from '@tanstack/react-query'
import { handleError, constructHeaders } from 'data/fetchers'
import { IS_PLATFORM, BASE_PATH } from 'lib/constants'
import { constructHeaders, handleError } from 'data/fetchers'
import { BASE_PATH, IS_PLATFORM } from 'lib/constants'
import { ResponseError } from 'types'
import { edgeFunctionsKeys } from './keys'
@@ -46,7 +46,7 @@ export async function getEdgeFunctionBody(
}
const { files } = await parseResponse.json()
return files
return files as EdgeFunctionFile[]
} catch (error) {
console.error('Failed to parse edge function code:', error)
throw new Error(

View File

@@ -34,6 +34,7 @@ const CodePage = () => {
data: functionFiles,
isLoading: isLoadingFiles,
isError: isErrorLoadingFiles,
isSuccess: isSuccessLoadingFiles,
error: filesError,
} = useEdgeFunctionBodyQuery({
projectRef: ref,
@@ -118,16 +119,16 @@ const CodePage = () => {
// TODO (Saxon): Remove this once the flag is fully launched
useEffect(() => {
if (!edgeFunctionCreate) {
if (edgeFunctionCreate !== undefined && !edgeFunctionCreate) {
router.push(`/project/${ref}/functions`)
}
}, [edgeFunctionCreate, ref, router])
}, [edgeFunctionCreate])
useEffect(() => {
// Set files from API response when available
if (functionFiles) {
setFiles(
functionFiles.map((file: { name: string; content: string }, index: number) => ({
functionFiles.map((file, index: number) => ({
id: index + 1,
name: file.name,
content: file.content,

View File

@@ -14,6 +14,7 @@ export const HIDDEN_PLACEHOLDER = '**** **** **** ****'
export interface Props extends Omit<ComponentProps<typeof Input_Shadcn_>, 'size' | 'onCopy'> {
copy?: boolean
showCopyOnHover?: boolean
onCopy?: () => void
icon?: any
reveal?: boolean
@@ -29,6 +30,7 @@ const Input = forwardRef<
(
{
copy,
showCopyOnHover = false,
icon,
reveal = false,
actions,
@@ -69,7 +71,7 @@ const Input = forwardRef<
if (icon) inputClasses.push(__styles.with_icon)
return (
<div className={cn('relative', containerClassName)}>
<div className={cn('relative group', containerClassName)}>
<Input_Shadcn_
ref={ref}
{...props}
@@ -85,6 +87,7 @@ const Input = forwardRef<
<Button
size="tiny"
type="default"
className={cn(showCopyOnHover && 'opacity-0 group-hover:opacity-100 transition')}
icon={<Copy size={16} className="text-foreground-muted" />}
onClick={() => _onCopy(props.value)}
>