mirror of
https://github.com/ruvnet/RuVector.git
synced 2026-05-24 22:15:18 +00:00
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>
294 lines
9.8 KiB
TypeScript
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)
|