mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 09:25:06 +03:00
Slight tuning of edge function page (#43798)
<img width="1543" height="981" alt="image" src="https://github.com/user-attachments/assets/8e6c37c5-15c7-4bc4-be87-477e36a62a65" /> Switches from two to one column layout for details page and removes an inconsistent background colour in header. Moves critical detail information from page into header (url and deploy dates/counts)
This commit is contained in:
1 parent
48b96e8a79
commit
8499d30e98
4 files changed
+272
-320
No files matched your search
@@ -1,3 +1,4 @@
|
||||
import { IS_PLATFORM } from 'common'
|
||||
import { Circle, Code, Minus, Plus, Wind } from 'lucide-react'
|
||||
import Link from 'next/link'
|
||||
import { basename } from 'path'
|
||||
@@ -111,7 +112,7 @@ const FunctionDiff = ({
|
||||
<CardHeader>
|
||||
<CardTitle>
|
||||
<Link
|
||||
href={`/project/${currentBranchRef}/functions/${functionSlug}`}
|
||||
href={`/project/${currentBranchRef}/functions/${functionSlug}${IS_PLATFORM ? '' : '/details'}`}
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
<Code strokeWidth={1.5} size={16} className="text-foreground-muted" />
|
||||
|
||||
+206
-312
@@ -1,9 +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'
|
||||
import { useRouter } from 'next/router'
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import { SubmitHandler, useForm } from 'react-hook-form'
|
||||
@@ -29,7 +26,6 @@ import {
|
||||
TabsList_Shadcn_ as TabsList,
|
||||
TabsTrigger_Shadcn_ as TabsTrigger,
|
||||
} 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'
|
||||
@@ -46,7 +42,6 @@ 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 { useProjectApiUrl } from '@/data/config/project-endpoint-query'
|
||||
import { useEdgeFunctionQuery } from '@/data/edge-functions/edge-function-query'
|
||||
@@ -54,7 +49,6 @@ import { useEdgeFunctionDeleteMutation } from '@/data/edge-functions/edge-functi
|
||||
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'),
|
||||
@@ -87,13 +81,7 @@ export const EdgeFunctionDetails = () => {
|
||||
const { can: canReadAPIKeys } = useAsyncCheckPermissions(PermissionAction.SECRETS_READ, '*')
|
||||
const { data: apiKeys } = useAPIKeysQuery({ projectRef }, { enabled: canReadAPIKeys })
|
||||
|
||||
const {
|
||||
data: selectedFunction,
|
||||
error,
|
||||
isPending: isLoading,
|
||||
isError,
|
||||
isSuccess,
|
||||
} = useEdgeFunctionQuery({ projectRef, slug: functionSlug })
|
||||
const { data: selectedFunction } = useEdgeFunctionQuery({ projectRef, slug: functionSlug })
|
||||
|
||||
const { data: endpoint } = useProjectApiUrl({ projectRef })
|
||||
const functionUrl = `${endpoint}/functions/v1/${selectedFunction?.slug}`
|
||||
@@ -114,10 +102,6 @@ export const EdgeFunctionDetails = () => {
|
||||
const { anonKey, publishableKey } = getKeys(apiKeys)
|
||||
const apiKey = publishableKey?.api_key ?? anonKey?.api_key ?? '[YOUR ANON KEY]'
|
||||
|
||||
const hasImportMap = useMemo(
|
||||
() => selectedFunction?.import_map || selectedFunction?.import_map_path,
|
||||
[selectedFunction]
|
||||
)
|
||||
const { managementCommands } = generateCLICommands({
|
||||
selectedFunction,
|
||||
functionUrl,
|
||||
@@ -158,112 +142,58 @@ export const EdgeFunctionDetails = () => {
|
||||
}, [selectedFunction])
|
||||
|
||||
return (
|
||||
<PageContainer size="full">
|
||||
<PageSection orientation="horizontal">
|
||||
<PageSectionSummary className="gap-6 !self-start">
|
||||
<PageSectionTitle>Details</PageSectionTitle>
|
||||
{isLoading && <GenericSkeletonLoader />}
|
||||
{isError && (
|
||||
<AlertError
|
||||
error={error}
|
||||
subject="Failed to retrieve edge function details"
|
||||
layout="vertical"
|
||||
/>
|
||||
)}
|
||||
{isSuccess && (
|
||||
<dl className="grid grid-cols-1 @xl:grid-cols-[auto_1fr] gap-y-2 [&>dd]:mb-3 @xl:[&>dd]:mb-0 @xl:gap-y-4 gap-x-10">
|
||||
<dt className="text-sm text-foreground-light">Slug</dt>
|
||||
<dd className="text-sm @lg:text-left">{selectedFunction?.slug}</dd>
|
||||
|
||||
<dt className="text-sm text-foreground-light">Endpoint URL</dt>
|
||||
<dd className="text-sm @lg:text-left">
|
||||
<Input
|
||||
copy
|
||||
readOnly
|
||||
size="small"
|
||||
className="font-mono input-mono"
|
||||
value={functionUrl}
|
||||
/>
|
||||
</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">
|
||||
{dayjs(selectedFunction?.created_at ?? 0).format('dddd, MMMM D, YYYY h:mm A')}
|
||||
</dd>
|
||||
|
||||
<dt className="text-sm text-foreground-light">Last updated at</dt>
|
||||
<dd className="text-sm @lg:text-left">
|
||||
{dayjs(selectedFunction?.updated_at ?? 0).format('dddd, MMMM D, YYYY h:mm A')}
|
||||
</dd>
|
||||
|
||||
<dt className="text-sm text-foreground-light">Deployments</dt>
|
||||
<dd className="text-sm @lg:text-left">{selectedFunction?.version ?? 0}</dd>
|
||||
|
||||
<dt className="text-sm text-foreground-light">Import Maps</dt>
|
||||
<dd className="text-sm @lg:text-left">
|
||||
<p>
|
||||
Import maps are{' '}
|
||||
<span className={cn(hasImportMap ? 'text-brand' : 'text-amber-900')}>
|
||||
{hasImportMap ? 'used' : 'not used'}
|
||||
</span>{' '}
|
||||
for this function
|
||||
</p>
|
||||
<p className="text-foreground-light mt-1">
|
||||
Import maps allow the use of bare specifiers in functions instead of explicit
|
||||
import URLs
|
||||
</p>
|
||||
<div className="mt-4">
|
||||
<Button
|
||||
asChild
|
||||
type="default"
|
||||
size="tiny"
|
||||
icon={<ExternalLink strokeWidth={1.5} />}
|
||||
>
|
||||
<Link
|
||||
href={`${DOCS_URL}/guides/functions/dependencies`}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
More about import maps
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</dd>
|
||||
</dl>
|
||||
)}
|
||||
</PageSectionSummary>
|
||||
<PageContainer size="small">
|
||||
<PageSection>
|
||||
<PageSectionMeta>
|
||||
<PageSectionSummary>
|
||||
<PageSectionTitle>Function configuration</PageSectionTitle>
|
||||
</PageSectionSummary>
|
||||
</PageSectionMeta>
|
||||
<PageSectionContent>
|
||||
<PageSection className="pt-0">
|
||||
<PageSectionMeta>
|
||||
<PageSectionSummary>
|
||||
<PageSectionTitle>Function configuration</PageSectionTitle>
|
||||
</PageSectionSummary>
|
||||
</PageSectionMeta>
|
||||
<PageSectionContent>
|
||||
<Form_Shadcn_ {...form}>
|
||||
<form onSubmit={form.handleSubmit(onUpdateFunction)}>
|
||||
<Card>
|
||||
<Form_Shadcn_ {...form}>
|
||||
<form onSubmit={form.handleSubmit(onUpdateFunction)}>
|
||||
<Card>
|
||||
<CardContent>
|
||||
<FormField_Shadcn_
|
||||
control={form.control}
|
||||
name="name"
|
||||
render={({ field }) => (
|
||||
<FormItemLayout
|
||||
label="Name"
|
||||
layout="flex-row-reverse"
|
||||
description="Your slug and endpoint URL will remain the same"
|
||||
>
|
||||
<FormControl_Shadcn_>
|
||||
<Input {...field} className="w-64" disabled={!canUpdateEdgeFunction} />
|
||||
</FormControl_Shadcn_>
|
||||
</FormItemLayout>
|
||||
)}
|
||||
/>
|
||||
</CardContent>
|
||||
{IS_PLATFORM && (
|
||||
<>
|
||||
<CardContent>
|
||||
<FormField_Shadcn_
|
||||
control={form.control}
|
||||
name="name"
|
||||
name="verify_jwt"
|
||||
render={({ field }) => (
|
||||
<FormItemLayout
|
||||
label="Name"
|
||||
label="Verify JWT with legacy secret"
|
||||
layout="flex-row-reverse"
|
||||
description="Your slug and endpoint URL will remain the same"
|
||||
description={
|
||||
<>
|
||||
Requires a JWT signed{' '}
|
||||
<em className="text-brand not-italic">only by the legacy secret</em>{' '}
|
||||
in the <code>Authorization</code> header. The <code>anon</code> key
|
||||
satisfies this. Recommended: OFF with JWT and custom auth logic in
|
||||
your function code.
|
||||
</>
|
||||
}
|
||||
>
|
||||
<FormControl_Shadcn_>
|
||||
<Input
|
||||
{...field}
|
||||
className="w-64"
|
||||
<Switch
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
disabled={!canUpdateEdgeFunction}
|
||||
/>
|
||||
</FormControl_Shadcn_>
|
||||
@@ -271,214 +201,178 @@ export const EdgeFunctionDetails = () => {
|
||||
)}
|
||||
/>
|
||||
</CardContent>
|
||||
{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}
|
||||
>
|
||||
Save changes
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</>
|
||||
)}
|
||||
</Card>
|
||||
</form>
|
||||
</Form_Shadcn_>
|
||||
</PageSectionContent>
|
||||
</PageSection>
|
||||
<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>
|
||||
</>
|
||||
)}
|
||||
</Card>
|
||||
</form>
|
||||
</Form_Shadcn_>
|
||||
</PageSectionContent>
|
||||
</PageSection>
|
||||
|
||||
<PageSection>
|
||||
<PageSectionMeta>
|
||||
<PageSectionSummary>
|
||||
<PageSectionTitle>Invoke function</PageSectionTitle>
|
||||
</PageSectionSummary>
|
||||
</PageSectionMeta>
|
||||
<PageSectionContent>
|
||||
<Card>
|
||||
<CardContent className="px-0">
|
||||
<Tabs
|
||||
className="w-full"
|
||||
defaultValue="curl"
|
||||
value={selectedTab}
|
||||
onValueChange={setSelectedTab}
|
||||
>
|
||||
<TabsList className="flex flex-wrap gap-4 px-6">
|
||||
{invocationTabs.map((tab) => (
|
||||
<TabsTrigger key={tab.id} value={tab.id}>
|
||||
{tab.label}
|
||||
</TabsTrigger>
|
||||
))}
|
||||
{selectedTab === 'curl' && (
|
||||
<Button
|
||||
type="default"
|
||||
className="ml-auto -translate-y-2 translate-x-3"
|
||||
onClick={() => setShowKey(!showKey)}
|
||||
>
|
||||
{showKey ? 'Hide' : 'Show'} anon key
|
||||
</Button>
|
||||
)}
|
||||
</TabsList>
|
||||
{invocationTabs.map((tab) => {
|
||||
const code = tab.code({
|
||||
showKey,
|
||||
functionUrl,
|
||||
functionName: selectedFunction?.name ?? '',
|
||||
apiKey,
|
||||
})
|
||||
|
||||
return (
|
||||
<TabsContent key={tab.id} value={tab.id} className="mt-4 px-6">
|
||||
<CodeBlock
|
||||
value={code}
|
||||
className={cn(
|
||||
'p-0 text-xs !mt-0 border-none [&>code]:!whitespace-pre-wrap',
|
||||
showKey ? '[&>code]:break-all' : '[&>code]:break-words'
|
||||
)}
|
||||
language={tab.language}
|
||||
wrapLines={true}
|
||||
hideLineNumbers={tab.hideLineNumbers}
|
||||
handleCopy={() => {
|
||||
copyToClipboard(
|
||||
tab.code({
|
||||
showKey: true,
|
||||
functionUrl,
|
||||
functionName: selectedFunction?.name ?? '',
|
||||
apiKey,
|
||||
})
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</TabsContent>
|
||||
)
|
||||
})}
|
||||
</Tabs>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</PageSectionContent>
|
||||
</PageSection>
|
||||
|
||||
{IS_PLATFORM && (
|
||||
<>
|
||||
<PageSection>
|
||||
<PageSectionMeta>
|
||||
<PageSectionSummary>
|
||||
<PageSectionTitle>Invoke function</PageSectionTitle>
|
||||
<PageSectionTitle>Develop locally</PageSectionTitle>
|
||||
</PageSectionSummary>
|
||||
</PageSectionMeta>
|
||||
<PageSectionContent>
|
||||
<Card>
|
||||
<CardContent className="px-0">
|
||||
<Tabs
|
||||
className="w-full"
|
||||
defaultValue="curl"
|
||||
value={selectedTab}
|
||||
onValueChange={setSelectedTab}
|
||||
>
|
||||
<TabsList className="flex flex-wrap gap-4 px-6">
|
||||
{invocationTabs.map((tab) => (
|
||||
<TabsTrigger key={tab.id} value={tab.id}>
|
||||
{tab.label}
|
||||
</TabsTrigger>
|
||||
))}
|
||||
{selectedTab === 'curl' && (
|
||||
<Button
|
||||
type="default"
|
||||
className="ml-auto -translate-y-2 translate-x-3"
|
||||
onClick={() => setShowKey(!showKey)}
|
||||
>
|
||||
{showKey ? 'Hide' : 'Show'} anon key
|
||||
</Button>
|
||||
)}
|
||||
</TabsList>
|
||||
{invocationTabs.map((tab) => {
|
||||
const code = tab.code({
|
||||
showKey,
|
||||
functionUrl,
|
||||
functionName: selectedFunction?.name ?? '',
|
||||
apiKey,
|
||||
})
|
||||
|
||||
return (
|
||||
<TabsContent key={tab.id} value={tab.id} className="mt-4 px-6">
|
||||
<CodeBlock
|
||||
value={code}
|
||||
className={cn(
|
||||
'p-0 text-xs !mt-0 border-none [&>code]:!whitespace-pre-wrap',
|
||||
showKey ? '[&>code]:break-all' : '[&>code]:break-words'
|
||||
)}
|
||||
language={tab.language}
|
||||
wrapLines={true}
|
||||
hideLineNumbers={tab.hideLineNumbers}
|
||||
handleCopy={() => {
|
||||
copyToClipboard(
|
||||
tab.code({
|
||||
showKey: true,
|
||||
functionUrl,
|
||||
functionName: selectedFunction?.name ?? '',
|
||||
apiKey,
|
||||
})
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</TabsContent>
|
||||
)
|
||||
})}
|
||||
</Tabs>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<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>
|
||||
|
||||
{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>
|
||||
<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',
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</PageContainer>
|
||||
)
|
||||
}
|
||||
@@ -6,14 +6,17 @@ import { EdgeFunctionTesterSheet } from 'components/interfaces/Functions/EdgeFun
|
||||
import { APIDocsButton } from 'components/ui/APIDocsButton'
|
||||
import { DocsButton } from 'components/ui/DocsButton'
|
||||
import NoPermission from 'components/ui/NoPermission'
|
||||
import { useProjectApiUrl } from 'data/config/project-endpoint-query'
|
||||
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 dayjs from 'dayjs'
|
||||
import relativeTime from 'dayjs/plugin/relativeTime'
|
||||
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 { Download, FileArchive, Send } from 'lucide-react'
|
||||
import { Clock, Download, FileArchive, Send } from 'lucide-react'
|
||||
import Link from 'next/link'
|
||||
import { useRouter } from 'next/router'
|
||||
import React, { useEffect, useState, type PropsWithChildren } from 'react'
|
||||
@@ -24,6 +27,9 @@ import {
|
||||
BreadcrumbList_Shadcn_ as BreadcrumbList,
|
||||
BreadcrumbSeparator_Shadcn_ as BreadcrumbSeparator,
|
||||
Button,
|
||||
HoverCard,
|
||||
HoverCardContent,
|
||||
HoverCardTrigger,
|
||||
NavMenu,
|
||||
NavMenuItem,
|
||||
Popover_Shadcn_,
|
||||
@@ -36,6 +42,7 @@ import {
|
||||
PageHeader,
|
||||
PageHeaderAside,
|
||||
PageHeaderBreadcrumb,
|
||||
PageHeaderDescription,
|
||||
PageHeaderMeta,
|
||||
PageHeaderNavigationTabs,
|
||||
PageHeaderSummary,
|
||||
@@ -45,6 +52,8 @@ import {
|
||||
import { ProjectLayout } from '../ProjectLayout'
|
||||
import EdgeFunctionsLayout from './EdgeFunctionsLayout'
|
||||
|
||||
dayjs.extend(relativeTime)
|
||||
|
||||
interface EdgeFunctionDetailsLayoutProps {
|
||||
title: string
|
||||
}
|
||||
@@ -65,12 +74,14 @@ const EdgeFunctionDetailsLayout = ({
|
||||
)
|
||||
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
const [isTimestampHoverCardOpen, setIsTimestampHoverCardOpen] = useState(false)
|
||||
|
||||
const {
|
||||
data: selectedFunction,
|
||||
error,
|
||||
isError,
|
||||
} = useEdgeFunctionQuery({ projectRef: ref, slug: functionSlug })
|
||||
const { data: endpoint } = useProjectApiUrl({ projectRef: ref })
|
||||
|
||||
const { data: functionBody = { version: 0, files: [] }, error: filesError } =
|
||||
useEdgeFunctionBodyQuery(
|
||||
@@ -91,6 +102,14 @@ const EdgeFunctionDetailsLayout = ({
|
||||
)
|
||||
|
||||
const name = selectedFunction?.name || ''
|
||||
const functionUrl =
|
||||
endpoint && selectedFunction?.slug ? `${endpoint}/functions/v1/${selectedFunction.slug}` : ''
|
||||
const createdRelative = selectedFunction?.created_at
|
||||
? dayjs(selectedFunction.created_at).fromNow()
|
||||
: undefined
|
||||
const updatedRelative = selectedFunction?.updated_at
|
||||
? dayjs(selectedFunction.updated_at).fromNow()
|
||||
: undefined
|
||||
const browserTitle = {
|
||||
entity: functionSlug ? name || functionSlug : undefined,
|
||||
section: title,
|
||||
@@ -125,14 +144,14 @@ const EdgeFunctionDetailsLayout = ({
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
label: 'Details',
|
||||
href: `/project/${ref}/functions/${functionSlug}/details`,
|
||||
},
|
||||
{
|
||||
label: 'Code',
|
||||
href: `/project/${ref}/functions/${functionSlug}/code`,
|
||||
},
|
||||
{
|
||||
label: 'Settings',
|
||||
href: `/project/${ref}/functions/${functionSlug}/details`,
|
||||
},
|
||||
]
|
||||
: []
|
||||
|
||||
@@ -228,7 +247,7 @@ const EdgeFunctionDetailsLayout = ({
|
||||
return (
|
||||
<EdgeFunctionsLayout title={title} browserTitle={browserTitle}>
|
||||
<div className="w-full min-h-full flex flex-col items-stretch">
|
||||
<PageHeader size="full" className="sticky top-0 z-10 bg-background">
|
||||
<PageHeader size="full" className="sticky top-0 z-10 bg-surface-75">
|
||||
{breadcrumbItems.length > 0 && (
|
||||
<PageHeaderBreadcrumb>
|
||||
<BreadcrumbList>
|
||||
@@ -253,6 +272,44 @@ const EdgeFunctionDetailsLayout = ({
|
||||
<PageHeaderMeta>
|
||||
<PageHeaderSummary>
|
||||
<PageHeaderTitle>{functionSlug ? name : 'Edge Functions'}</PageHeaderTitle>
|
||||
<PageHeaderDescription className="flex flex-row flex-wrap items-center gap-x-4 gap-y-1 !text-sm">
|
||||
<span className="flex items-center gap-2">{functionUrl}</span>
|
||||
<HoverCard
|
||||
openDelay={250}
|
||||
closeDelay={100}
|
||||
open={isTimestampHoverCardOpen}
|
||||
onOpenChange={setIsTimestampHoverCardOpen}
|
||||
>
|
||||
<HoverCardTrigger asChild>
|
||||
<button type="button" className="flex items-center gap-2 group">
|
||||
<Clock size={16} strokeWidth={1.5} className="text-foreground-lighter" />
|
||||
<span className="transition text-foreground-light group-hover:text-foreground underline decoration-dotted decoration-foreground-muted underline-offset-4">
|
||||
{updatedRelative ?? 'Deploy status unavailable'}
|
||||
</span>
|
||||
</button>
|
||||
</HoverCardTrigger>
|
||||
<HoverCardContent side="bottom" align="start" className="w-[320px] p-0">
|
||||
{createdRelative && (
|
||||
<div className="px-4 py-2 space-y-1">
|
||||
<h3 className="heading-meta text-foreground-light">Created</h3>
|
||||
<p className="text-foreground">{createdRelative}</p>
|
||||
</div>
|
||||
)}
|
||||
{updatedRelative && (
|
||||
<div className="px-4 py-2 space-y-1">
|
||||
<h3 className="heading-meta text-foreground-light">Last deployed</h3>
|
||||
<p className="text-foreground">{updatedRelative}</p>
|
||||
</div>
|
||||
)}
|
||||
{selectedFunction?.version !== undefined && (
|
||||
<div className="px-4 py-2 space-y-1">
|
||||
<h3 className="heading-meta text-foreground-light">Deployments</h3>
|
||||
<p className="text-foreground">{selectedFunction.version}</p>
|
||||
</div>
|
||||
)}
|
||||
</HoverCardContent>
|
||||
</HoverCard>
|
||||
</PageHeaderDescription>
|
||||
</PageHeaderSummary>
|
||||
|
||||
<PageHeaderAside>
|
||||
|
||||
@@ -7,7 +7,7 @@ const PageLayout: NextPageWithLayout = () => <EdgeFunctionDetails />
|
||||
|
||||
PageLayout.getLayout = (page) => (
|
||||
<DefaultLayout>
|
||||
<EdgeFunctionDetailsLayout title="Details">{page}</EdgeFunctionDetailsLayout>
|
||||
<EdgeFunctionDetailsLayout title="Settings">{page}</EdgeFunctionDetailsLayout>
|
||||
</DefaultLayout>
|
||||
)
|
||||
|
||||
|
||||
Reference in new issue
Block a user