mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 09:25:06 +03:00
feat(studio-local): functions management api - function details (#42322)
Functions page on self-hosted differs from Platform Adds the possibility to see function details page in Self-Host version. ## Summary by CodeRabbit * **New Features** * Edge function detail lookup added for self‑hosted deployments (new retrieval endpoint & store method). * Consistent navigation to function pages from the functions list. * **Improvements** * UI tabs, download, and test controls adapt to deployment type. * Region, JWT verification, local development, and delete controls shown only on the platform. * Edit/save/delete controls enable/disable correctly based on deployment and permissions. * Function details load reliably across deployments.
This commit is contained in:
1 parent
d1c6cbacbd
commit
8c3655cd71
7 files changed
+303
-220
No files matched your search
+156
-142
@@ -1,5 +1,6 @@
|
||||
import { zodResolver } from '@hookform/resolvers/zod'
|
||||
import { PermissionAction } from '@supabase/shared-types/out/constants'
|
||||
import { IS_PLATFORM, useParams } from 'common'
|
||||
import dayjs from 'dayjs'
|
||||
import { ExternalLink } from 'lucide-react'
|
||||
import Link from 'next/link'
|
||||
@@ -7,44 +8,30 @@ import { useRouter } from 'next/router'
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import { SubmitHandler, useForm } from 'react-hook-form'
|
||||
import { toast } from 'sonner'
|
||||
import z from 'zod'
|
||||
|
||||
import { useParams } from 'common'
|
||||
import { AlertError } from 'components/ui/AlertError'
|
||||
import { getKeys, useAPIKeysQuery } from 'data/api-keys/api-keys-query'
|
||||
import { useProjectSettingsV2Query } from 'data/config/project-settings-v2-query'
|
||||
import { useCustomDomainsQuery } from 'data/custom-domains/custom-domains-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'
|
||||
import { DOCS_URL } from 'lib/constants'
|
||||
import {
|
||||
Alert_Shadcn_,
|
||||
AlertDescription_Shadcn_,
|
||||
AlertTitle_Shadcn_,
|
||||
Alert_Shadcn_,
|
||||
Button,
|
||||
Card,
|
||||
CardContent,
|
||||
CardFooter,
|
||||
cn,
|
||||
CodeBlock,
|
||||
copyToClipboard,
|
||||
CriticalIcon,
|
||||
Form_Shadcn_,
|
||||
FormControl_Shadcn_,
|
||||
FormField_Shadcn_,
|
||||
Form_Shadcn_,
|
||||
Switch,
|
||||
Tabs_Shadcn_ as Tabs,
|
||||
TabsContent_Shadcn_ as TabsContent,
|
||||
TabsList_Shadcn_ as TabsList,
|
||||
TabsTrigger_Shadcn_ as TabsTrigger,
|
||||
cn,
|
||||
copyToClipboard,
|
||||
} from 'ui'
|
||||
import { GenericSkeletonLoader } from 'ui-patterns'
|
||||
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,
|
||||
@@ -53,9 +40,22 @@ import {
|
||||
PageSectionSummary,
|
||||
PageSectionTitle,
|
||||
} from 'ui-patterns/PageSection'
|
||||
import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
|
||||
import z from 'zod'
|
||||
|
||||
import CommandRender from '../CommandRender'
|
||||
import { INVOCATION_TABS } from './EdgeFunctionDetails.constants'
|
||||
import { generateCLICommands } from './EdgeFunctionDetails.utils'
|
||||
import AlertError from '@/components/ui/AlertError'
|
||||
import { getKeys, useAPIKeysQuery } from '@/data/api-keys/api-keys-query'
|
||||
import { useProjectSettingsV2Query } from '@/data/config/project-settings-v2-query'
|
||||
import { useCustomDomainsQuery } from '@/data/custom-domains/custom-domains-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'
|
||||
import { DOCS_URL } from '@/lib/constants'
|
||||
|
||||
const FormSchema = z.object({
|
||||
name: z.string().min(0, 'Name is required'),
|
||||
@@ -78,11 +78,13 @@ export const EdgeFunctionDetails = () => {
|
||||
const [selectedTab, setSelectedTab] = useState(invocationTabs[0].id)
|
||||
const [showDeleteModal, setShowDeleteModal] = useState(false)
|
||||
|
||||
const { can: canUpdateEdgeFunction } = useAsyncCheckPermissions(
|
||||
const { can: canUpdateEdgeFunctionPermission } = useAsyncCheckPermissions(
|
||||
PermissionAction.FUNCTIONS_WRITE,
|
||||
'*'
|
||||
)
|
||||
|
||||
const canUpdateEdgeFunction = IS_PLATFORM && canUpdateEdgeFunctionPermission
|
||||
|
||||
const { can: canReadAPIKeys } = useAsyncCheckPermissions(PermissionAction.SECRETS_READ, '*')
|
||||
const { data: apiKeys } = useAPIKeysQuery(
|
||||
{
|
||||
@@ -197,8 +199,12 @@ export const EdgeFunctionDetails = () => {
|
||||
/>
|
||||
</dd>
|
||||
|
||||
<dt className="text-sm text-foreground-light">Region</dt>
|
||||
<dd className="text-sm @lg:text-left">All functions are deployed globally</dd>
|
||||
{IS_PLATFORM && (
|
||||
<>
|
||||
<dt className="text-sm text-foreground-light">Region</dt>
|
||||
<dd className="text-sm @lg:text-left">All functions are deployed globally</dd>
|
||||
</>
|
||||
)}
|
||||
|
||||
<dt className="text-sm text-foreground-light">Created at</dt>
|
||||
<dd className="text-sm @lg:text-left">
|
||||
@@ -278,53 +284,58 @@ export const EdgeFunctionDetails = () => {
|
||||
)}
|
||||
/>
|
||||
</CardContent>
|
||||
<CardContent>
|
||||
<FormField_Shadcn_
|
||||
control={form.control}
|
||||
name="verify_jwt"
|
||||
render={({ field }) => (
|
||||
<FormItemLayout
|
||||
label="Verify JWT with legacy secret"
|
||||
layout="flex-row-reverse"
|
||||
description={
|
||||
<>
|
||||
Requires that a JWT signed{' '}
|
||||
<em className="text-brand not-italic">
|
||||
only by the legacy JWT secret
|
||||
</em>{' '}
|
||||
is present in the <code>Authorization</code> header. The easy to
|
||||
obtain <code>anon</code> key can be used to satisfy this
|
||||
requirement. Recommendation: OFF with JWT and additional
|
||||
authorization logic implemented inside your function's code.
|
||||
</>
|
||||
}
|
||||
{IS_PLATFORM && (
|
||||
<>
|
||||
<CardContent>
|
||||
<FormField_Shadcn_
|
||||
control={form.control}
|
||||
name="verify_jwt"
|
||||
render={({ field }) => (
|
||||
<FormItemLayout
|
||||
label="Verify JWT with legacy secret"
|
||||
layout="flex-row-reverse"
|
||||
description={
|
||||
<>
|
||||
Requires that a JWT signed{' '}
|
||||
<em className="text-brand not-italic">
|
||||
only by the legacy JWT secret
|
||||
</em>{' '}
|
||||
is present in the <code>Authorization</code> header. The easy to
|
||||
obtain <code>anon</code> key can be used to satisfy this
|
||||
requirement. Recommendation: OFF with JWT and additional
|
||||
authorization logic implemented inside your function's code.
|
||||
</>
|
||||
}
|
||||
>
|
||||
<FormControl_Shadcn_>
|
||||
<Switch
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
disabled={!canUpdateEdgeFunction}
|
||||
/>
|
||||
</FormControl_Shadcn_>
|
||||
</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}
|
||||
>
|
||||
<FormControl_Shadcn_>
|
||||
<Switch
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
disabled={!canUpdateEdgeFunction}
|
||||
/>
|
||||
</FormControl_Shadcn_>
|
||||
</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>
|
||||
Save changes
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</>
|
||||
)}
|
||||
</Card>
|
||||
</form>
|
||||
</Form_Shadcn_>
|
||||
@@ -401,81 +412,84 @@ export const EdgeFunctionDetails = () => {
|
||||
</PageSectionContent>
|
||||
</PageSection>
|
||||
|
||||
<PageSection>
|
||||
<PageSectionMeta>
|
||||
<PageSectionSummary>
|
||||
<PageSectionTitle>Develop locally</PageSectionTitle>
|
||||
</PageSectionSummary>
|
||||
</PageSectionMeta>
|
||||
<PageSectionContent>
|
||||
<div className="rounded border bg-surface-100 px-6 py-4 drop-shadow-sm">
|
||||
<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-600">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',
|
||||
}}
|
||||
/>
|
||||
{IS_PLATFORM && (
|
||||
<>
|
||||
<PageSection>
|
||||
<PageSectionMeta>
|
||||
<PageSectionSummary>
|
||||
<PageSectionTitle>Develop locally</PageSectionTitle>
|
||||
</PageSectionSummary>
|
||||
</PageSectionMeta>
|
||||
<PageSectionContent>
|
||||
<div className="rounded border bg-surface-100 px-6 py-4 drop-shadow-sm">
|
||||
<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-600">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',
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</PageSectionContent>
|
||||
</PageSection>
|
||||
</PageContainer>
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import { IS_PLATFORM } from 'common'
|
||||
import { useParams } from 'common/hooks'
|
||||
import dayjs from 'dayjs'
|
||||
import { Check, Copy } from 'lucide-react'
|
||||
import { useRouter } from 'next/router'
|
||||
import { useState } from 'react'
|
||||
|
||||
import { IS_PLATFORM } from 'common'
|
||||
import { useParams } from 'common/hooks'
|
||||
import { useProjectSettingsV2Query } from 'data/config/project-settings-v2-query'
|
||||
import { useCustomDomainsQuery } from 'data/custom-domains/custom-domains-query'
|
||||
import type { EdgeFunctionsResponse } from 'data/edge-functions/edge-functions-query'
|
||||
import { cn, copyToClipboard, TableCell, TableRow } from 'ui'
|
||||
import { TableCell, TableRow, copyToClipboard } from 'ui'
|
||||
import { TimestampInfo } from 'ui-patterns'
|
||||
import { createNavigationHandler } from 'lib/navigation'
|
||||
|
||||
import { useProjectSettingsV2Query } from '@/data/config/project-settings-v2-query'
|
||||
import { useCustomDomainsQuery } from '@/data/custom-domains/custom-domains-query'
|
||||
import type { EdgeFunctionsResponse } from '@/data/edge-functions/edge-functions-query'
|
||||
import { createNavigationHandler } from '@/lib/navigation'
|
||||
|
||||
interface EdgeFunctionsListItemProps {
|
||||
function: EdgeFunctionsResponse
|
||||
@@ -31,9 +31,10 @@ export const EdgeFunctionsListItem = ({ function: item }: EdgeFunctionsListItemP
|
||||
? `https://${customDomainData.customDomain.hostname}/functions/v1/${item.slug}`
|
||||
: `${protocol}://${endpoint}/functions/v1/${item.slug}`
|
||||
|
||||
const handleNavigation = IS_PLATFORM
|
||||
? createNavigationHandler(`/project/${ref}/functions/${item.slug}`, router)
|
||||
: undefined
|
||||
const handleNavigation = createNavigationHandler(
|
||||
`/project/${ref}/functions/${item.slug}${IS_PLATFORM ? '' : `/details`}`,
|
||||
router
|
||||
)
|
||||
|
||||
return (
|
||||
<TableRow
|
||||
@@ -42,7 +43,7 @@ export const EdgeFunctionsListItem = ({ function: item }: EdgeFunctionsListItemP
|
||||
onAuxClick={handleNavigation}
|
||||
onKeyDown={handleNavigation}
|
||||
tabIndex={0}
|
||||
className={cn({ 'cursor-pointer inset-focus': IS_PLATFORM })}
|
||||
className="cursor-pointer inset-focus"
|
||||
>
|
||||
<TableCell>
|
||||
<p className="text-sm text-foreground whitespace-nowrap py-2">{item.name}</p>
|
||||
|
||||
@@ -5,7 +5,7 @@ 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 { IS_PLATFORM, 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'
|
||||
@@ -105,22 +105,26 @@ const EdgeFunctionDetailsLayout = ({
|
||||
|
||||
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`,
|
||||
},
|
||||
...(IS_PLATFORM
|
||||
? [
|
||||
{
|
||||
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`,
|
||||
@@ -260,54 +264,58 @@ const EdgeFunctionDetailsLayout = ({
|
||||
/>
|
||||
)}
|
||||
<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">
|
||||
{IS_PLATFORM && (
|
||||
<>
|
||||
<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="text"
|
||||
className="w-min hover:bg-transparent"
|
||||
icon={<FileArchive />}
|
||||
onClick={downloadFunction}
|
||||
type="default"
|
||||
icon={<Send />}
|
||||
onClick={() => {
|
||||
setIsOpen(true)
|
||||
sendEvent({
|
||||
action: 'edge_function_test_side_panel_opened',
|
||||
groups: {
|
||||
project: ref ?? 'Unknown',
|
||||
organization: org?.slug ?? 'Unknown',
|
||||
},
|
||||
})
|
||||
}}
|
||||
>
|
||||
Download as ZIP
|
||||
Test
|
||||
</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>
|
||||
|
||||
@@ -41,7 +41,6 @@ export const useEdgeFunctionQuery = <TData = EdgeFunctionData>(
|
||||
useQuery<EdgeFunctionData, EdgeFunctionError, TData>({
|
||||
queryKey: edgeFunctionsKeys.detail(projectRef, slug),
|
||||
queryFn: ({ signal }) => getEdgeFunction({ projectRef, slug }, signal),
|
||||
enabled:
|
||||
IS_PLATFORM && enabled && typeof projectRef !== 'undefined' && typeof slug !== 'undefined',
|
||||
enabled: enabled && typeof projectRef !== 'undefined' && typeof slug !== 'undefined',
|
||||
...options,
|
||||
})
|
||||
@@ -18,6 +18,17 @@ export class FileSystemFunctionsArtifactStore {
|
||||
|
||||
return functionsArtifacts.filter((f) => f !== undefined)
|
||||
}
|
||||
|
||||
async getFunctionBySlug(slug: string): Promise<FunctionArtifact | undefined> {
|
||||
const dirEntries = await readdir(this.folderPath, { withFileTypes: true })
|
||||
|
||||
const functionFolder = dirEntries.find(
|
||||
(dir) => dir.isDirectory() && dir.name !== 'main' && dir.name === slug
|
||||
)
|
||||
if (!functionFolder) return
|
||||
|
||||
return parseFolderToFunctionArtifact(functionFolder)
|
||||
}
|
||||
}
|
||||
|
||||
async function parseFolderToFunctionArtifact(
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
import { type NextApiRequest, type NextApiResponse } from 'next'
|
||||
|
||||
import type { components } from 'api-types'
|
||||
import { uuidv4 } from 'lib/helpers'
|
||||
import apiWrapper from 'lib/api/apiWrapper'
|
||||
import { getFunctionsArtifactStore } from 'lib/api/self-hosted/functions'
|
||||
|
||||
export default function handlerWithErrorCatching(req: NextApiRequest, res: NextApiResponse) {
|
||||
return apiWrapper(req, res, handler, { withAuth: true })
|
||||
}
|
||||
|
||||
async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
const { method } = req
|
||||
|
||||
switch (method) {
|
||||
case 'GET':
|
||||
return handleGet(req, res)
|
||||
default:
|
||||
res.setHeader('Allow', ['GET'])
|
||||
res.status(405).json({ data: null, error: { message: `Method ${method} Not Allowed` } })
|
||||
}
|
||||
}
|
||||
|
||||
type EdgeFunctionsResponse = components['schemas']['FunctionResponse']
|
||||
|
||||
const handleGet = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
const slugParam = req.query.slug
|
||||
const slug = Array.isArray(slugParam) ? slugParam[0] : slugParam
|
||||
if (!slug)
|
||||
return res.status(404).json({ error: { message: `Missing function 'slug' parameter` } })
|
||||
|
||||
const store = getFunctionsArtifactStore()
|
||||
|
||||
const functionsArtifact = await store.getFunctionBySlug(slug)
|
||||
if (!functionsArtifact) return res.status(404).json({ error: { message: `Function not found` } })
|
||||
|
||||
const functionResponse = {
|
||||
id: uuidv4(),
|
||||
slug: functionsArtifact.slug,
|
||||
version: 1,
|
||||
name: functionsArtifact.slug,
|
||||
status: 'ACTIVE',
|
||||
entrypoint_path: functionsArtifact.entrypoint_path,
|
||||
created_at: functionsArtifact.created_at,
|
||||
updated_at: functionsArtifact.updated_at,
|
||||
} satisfies EdgeFunctionsResponse
|
||||
|
||||
return res.status(200).json(functionResponse)
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { PermissionAction } from '@supabase/shared-types/out/constants'
|
||||
import { useParams } from 'common'
|
||||
import { IS_PLATFORM, useParams } from 'common'
|
||||
import { useFlag } from 'common'
|
||||
import ReportWidget from 'components/interfaces/Reports/ReportWidget'
|
||||
import DefaultLayout from 'components/layouts/DefaultLayout'
|
||||
@@ -19,7 +19,7 @@ import maxBy from 'lodash/maxBy'
|
||||
import meanBy from 'lodash/meanBy'
|
||||
import sumBy from 'lodash/sumBy'
|
||||
import { useRouter } from 'next/router'
|
||||
import { useMemo, useState } from 'react'
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import type { ChartIntervals, NextPageWithLayout } from 'types'
|
||||
import {
|
||||
AlertDescription_Shadcn_,
|
||||
@@ -65,6 +65,7 @@ const CHART_INTERVALS: ChartIntervals[] = [
|
||||
const PageLayout: NextPageWithLayout = () => {
|
||||
const router = useRouter()
|
||||
const { ref: projectRef, functionSlug } = useParams()
|
||||
|
||||
const newChartsEnabled = useFlag('newEdgeFunctionOverviewCharts')
|
||||
const [interval, setInterval] = useState<string>('15min')
|
||||
const selectedInterval = CHART_INTERVALS.find((i) => i.key === interval) || CHART_INTERVALS[1]
|
||||
|
||||
Reference in new issue
Block a user