ruvector/studio/components/layouts/EdgeFunctionsLayout/EdgeFunctionDetailsLayout.tsx
rUv 814f595995 feat(studio): Add complete RuVector Studio application
Major additions:
- Complete Next.js studio application with 1600+ components
- Docker support (Dockerfile.combined, docker-compose.yml)
- GCP deployment documentation and benchmarks
- SQL benchmark scripts for performance testing
- Sentry integration for monitoring
- Comprehensive test suite and mocks

Studio features:
- Dashboard and admin interfaces
- Data visualization components
- Authentication and user management
- API integration with RuVector backend
- Static data and public assets

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-06 23:04:48 +00:00

294 lines
9.8 KiB
TypeScript

import { PermissionAction } from '@supabase/shared-types/out/constants'
import { Download, FileArchive, Send } from 'lucide-react'
import { useRouter } from 'next/router'
import React, { 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'
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 { useAsyncCheckPermissions } from 'hooks/misc/useCheckPermissions'
import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization'
import { withAuth } from 'hooks/misc/withAuth'
import { DOCS_URL } from 'lib/constants'
import Link from 'next/link'
import {
BreadcrumbItem_Shadcn_ as BreadcrumbItem,
BreadcrumbLink_Shadcn_ as BreadcrumbLink,
BreadcrumbList_Shadcn_ as BreadcrumbList,
BreadcrumbSeparator_Shadcn_ as BreadcrumbSeparator,
Button,
NavMenu,
NavMenuItem,
Popover_Shadcn_,
PopoverContent_Shadcn_,
PopoverTrigger_Shadcn_,
Separator,
} from 'ui'
import { Input } from 'ui-patterns/DataInputs/Input'
import {
PageHeader,
PageHeaderAside,
PageHeaderBreadcrumb,
PageHeaderMeta,
PageHeaderNavigationTabs,
PageHeaderSummary,
PageHeaderTitle,
} from 'ui-patterns/PageHeader'
import { ProjectLayout } from '../ProjectLayout'
import EdgeFunctionsLayout from './EdgeFunctionsLayout'
interface EdgeFunctionDetailsLayoutProps {
title?: string
}
const EdgeFunctionDetailsLayout = ({
title,
children,
}: PropsWithChildren<EdgeFunctionDetailsLayoutProps>) => {
const router = useRouter()
const { data: org } = useSelectedOrganizationQuery()
const { functionSlug, ref } = useParams()
const { mutate: sendEvent } = useSendEventMutation()
const isNewAPIDocsEnabled = useIsAPIDocsSidePanelEnabled()
const { isLoading, can: canReadFunctions } = useAsyncCheckPermissions(
PermissionAction.FUNCTIONS_READ,
'*'
)
const [isOpen, setIsOpen] = useState(false)
const {
data: selectedFunction,
error,
isError,
} = useEdgeFunctionQuery({ projectRef: ref, slug: functionSlug })
const { data: functionBody = { version: 0, files: [] }, error: filesError } =
useEdgeFunctionBodyQuery(
{
projectRef: ref,
slug: functionSlug,
},
{
retry: false,
retryOnMount: true,
refetchOnWindowFocus: false,
staleTime: Infinity,
refetchOnMount: false,
refetchOnReconnect: false,
refetchInterval: false,
refetchIntervalInBackground: false,
}
)
const name = selectedFunction?.name || ''
const breadcrumbItems = [
{
label: 'Edge Functions',
href: `/project/${ref}/functions`,
},
{
label: functionSlug,
href: `/project/${ref}/functions/${functionSlug}`,
},
]
const navigationItems = functionSlug
? [
{
label: 'Overview',
href: `/project/${ref}/functions/${functionSlug}`,
},
{
label: 'Invocations',
href: `/project/${ref}/functions/${functionSlug}/invocations`,
},
{
label: 'Logs',
href: `/project/${ref}/functions/${functionSlug}/logs`,
},
{
label: 'Code',
href: `/project/${ref}/functions/${functionSlug}/code`,
},
{
label: 'Details',
href: `/project/${ref}/functions/${functionSlug}/details`,
},
]
: []
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 })
functionBody.files.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
if (!!functionSlug && isError && error.code === 404 && !cancel) {
toast('Edge function cannot be found in your project')
router.push(`/project/${ref}/functions`)
}
return () => {
cancel = true
}
}, [isError])
if (!isLoading && !canReadFunctions) {
return (
<ProjectLayout title={title || 'Edge Functions'} product="Edge Functions">
<NoPermission isFullPage resourceText="access your project's edge functions" />
</ProjectLayout>
)
}
return (
<EdgeFunctionsLayout>
<div className="w-full min-h-full flex flex-col items-stretch">
<PageHeader size="full">
{breadcrumbItems.length > 0 && (
<PageHeaderBreadcrumb>
<BreadcrumbList>
{breadcrumbItems.map((item, index) => (
<React.Fragment key={item.label || `breadcrumb-${index}`}>
<BreadcrumbItem>
{item.href ? (
<BreadcrumbLink asChild>
<Link href={item.href}>{item.label}</Link>
</BreadcrumbLink>
) : (
<span>{item.label}</span>
)}
</BreadcrumbItem>
{index < breadcrumbItems.length - 1 && <BreadcrumbSeparator />}
</React.Fragment>
))}
</BreadcrumbList>
</PageHeaderBreadcrumb>
)}
<PageHeaderMeta>
<PageHeaderSummary>
<PageHeaderTitle>{functionSlug ? name : 'Edge Functions'}</PageHeaderTitle>
</PageHeaderSummary>
<PageHeaderAside>
<div className="flex items-center space-x-2">
{isNewAPIDocsEnabled && (
<APIDocsButton
section={
functionSlug !== undefined
? ['edge-functions', functionSlug]
: ['edge-functions']
}
source="edge-functions"
/>
)}
<DocsButton href={`${DOCS_URL}/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_>
{!!functionSlug && (
<Button
type="default"
icon={<Send />}
onClick={() => {
setIsOpen(true)
sendEvent({
action: 'edge_function_test_side_panel_opened',
groups: {
project: ref ?? 'Unknown',
organization: org?.slug ?? 'Unknown',
},
})
}}
>
Test
</Button>
)}
</div>
</PageHeaderAside>
</PageHeaderMeta>
{navigationItems.length > 0 && (
<PageHeaderNavigationTabs>
<NavMenu>
{navigationItems.map((item) => {
const isActive = router.asPath.split('?')[0] === item.href
return (
<NavMenuItem key={item.label} active={isActive}>
<Link href={item.href}>{item.label}</Link>
</NavMenuItem>
)
})}
</NavMenu>
</PageHeaderNavigationTabs>
)}
</PageHeader>
{children}
<EdgeFunctionTesterSheet visible={isOpen} onClose={() => setIsOpen(false)} />
</div>
</EdgeFunctionsLayout>
)
}
export default withAuth(EdgeFunctionDetailsLayout)