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:
@@ -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"
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)}
|
||||
>
|
||||
|
||||
Reference in New Issue
Block a user