update edge function design

This commit is contained in:
Saxon Fletcher committed 2026-05-21 12:03:32 +10:00
1 parent fc3ae47b63
commit 9c838f7629
15 files changed
+807 -692

No files matched your search

@@ -30,6 +30,13 @@ 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 {
PageHeader,
PageHeaderDescription,
PageHeaderMeta,
PageHeaderSummary,
PageHeaderTitle,
} from 'ui-patterns/PageHeader'
import {
PageSection,
PageSectionContent,
@@ -142,247 +149,262 @@ export const EdgeFunctionDetails = () => {
}, [selectedFunction])
return (
<PageContainer size="small">
<PageSection>
<PageSectionMeta>
<PageSectionSummary>
<PageSectionTitle>Function configuration</PageSectionTitle>
</PageSectionSummary>
</PageSectionMeta>
<PageSectionContent>
<Form {...form}>
<form onSubmit={form.handleSubmit(onUpdateFunction)}>
<Card>
<CardContent>
<FormField
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>
<Input {...field} className="w-64" disabled={!canUpdateEdgeFunction} />
</FormControl>
</FormItemLayout>
)}
/>
</CardContent>
{IS_PLATFORM && (
<>
<CardContent>
<FormField
control={form.control}
name="verify_jwt"
render={({ field }) => (
<FormItemLayout
label="Verify JWT with legacy secret"
layout="flex-row-reverse"
description={
<>
<p className="mb-2">
Requires a JWT signed{' '}
<em className="text-foreground not-italic">
only by the legacy secret
</em>{' '}
in the{' '}
<code className="text-code-inline break-keep!">
Authorization
</code>{' '}
header. The <code className="text-code-inline">anon</code> key
satisfies this.
</p>
<p>
Recommended: OFF with JWT and custom auth logic in your function
code.
</p>
</>
}
>
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
disabled={!canUpdateEdgeFunction}
/>
</FormControl>
</FormItemLayout>
)}
/>
</CardContent>
<>
<PageHeader size="small" className="pb-12">
<PageHeaderMeta>
<PageHeaderSummary>
<PageHeaderTitle>Settings</PageHeaderTitle>
<PageHeaderDescription>
Configure function behavior, security, and deployment options.
</PageHeaderDescription>
</PageHeaderSummary>
</PageHeaderMeta>
</PageHeader>
<CardFooter className="flex justify-end space-x-2">
{form.formState.isDirty && (
<Button type="default" onClick={() => form.reset()}>
Cancel
</Button>
<PageContainer size="small">
<PageSection className="pt-0">
<PageSectionMeta>
<PageSectionSummary>
<PageSectionTitle>Function configuration</PageSectionTitle>
</PageSectionSummary>
</PageSectionMeta>
<PageSectionContent>
<Form {...form}>
<form onSubmit={form.handleSubmit(onUpdateFunction)}>
<Card>
<CardContent>
<FormField
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>
<Input {...field} className="w-64" disabled={!canUpdateEdgeFunction} />
</FormControl>
</FormItemLayout>
)}
<Button
type="primary"
htmlType="submit"
loading={isUpdating}
disabled={!canUpdateEdgeFunction || !form.formState.isDirty}
>
Save changes
</Button>
</CardFooter>
</>
)}
</Card>
</form>
</Form>
</PageSectionContent>
</PageSection>
/>
</CardContent>
{IS_PLATFORM && (
<>
<CardContent>
<FormField
control={form.control}
name="verify_jwt"
render={({ field }) => (
<FormItemLayout
label="Verify JWT with legacy secret"
layout="flex-row-reverse"
description={
<>
<p className="mb-2">
Requires a JWT signed{' '}
<em className="text-foreground not-italic">
only by the legacy secret
</em>{' '}
in the{' '}
<code className="text-code-inline break-keep!">
Authorization
</code>{' '}
header. The <code className="text-code-inline">anon</code> key
satisfies this.
</p>
<p>
Recommended: OFF with JWT and custom auth logic in your function
code.
</p>
</>
}
>
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
disabled={!canUpdateEdgeFunction}
/>
</FormControl>
</FormItemLayout>
)}
/>
</CardContent>
<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}>
<CodeBlock
value={code}
wrapperClassName="[&>div]:top-0 [&>div]:right-3 px-6"
className={cn(
'p-0 text-xs mt-0! border-none ',
showKey ? '[&>code]:break-all' : '[&>code]:wrap-break-word'
<CardFooter className="flex justify-end space-x-2">
{form.formState.isDirty && (
<Button type="default" onClick={() => form.reset()}>
Cancel
</Button>
)}
language={tab.language}
wrapLines={false}
hideLineNumbers={tab.hideLineNumbers}
handleCopy={() => {
copyToClipboard(
tab.code({
showKey: true,
functionUrl,
functionName: selectedFunction?.name ?? '',
apiKey,
})
)
}}
/>
</TabsContent>
)
})}
</Tabs>
</CardContent>
</Card>
</PageSectionContent>
</PageSection>
<Button
type="primary"
htmlType="submit"
loading={isUpdating}
disabled={!canUpdateEdgeFunction || !form.formState.isDirty}
>
Save changes
</Button>
</CardFooter>
</>
)}
</Card>
</form>
</Form>
</PageSectionContent>
</PageSection>
{IS_PLATFORM && (
<>
<PageSection>
<PageSectionMeta>
<PageSectionSummary>
<PageSectionTitle>Develop locally</PageSectionTitle>
</PageSectionSummary>
</PageSectionMeta>
<PageSectionContent>
<div className="rounded-sm border bg-surface-100 px-6 py-4 drop-shadow-xs">
<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">supabase</span> functions download{' '}
{selectedFunction?.slug}
</>
),
comment: '1. Download the function',
},
]}
/>
<CommandRender commands={[managementCommands[0]]} />
<CommandRender commands={[managementCommands[1]]} />
<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}>
<CodeBlock
value={code}
wrapperClassName="[&>div]:top-0 [&>div]:right-3 px-6"
className={cn(
'p-0 text-xs mt-0! border-none ',
showKey ? '[&>code]:break-all' : '[&>code]:wrap-break-word'
)}
language={tab.language}
wrapLines={false}
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>Develop locally</PageSectionTitle>
</PageSectionSummary>
</PageSectionMeta>
<PageSectionContent>
<div className="rounded-sm border bg-surface-100 px-6 py-4 drop-shadow-xs">
<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">supabase</span> functions download{' '}
{selectedFunction?.slug}
</>
),
comment: '1. Download the function',
},
]}
/>
<CommandRender commands={[managementCommands[0]]} />
<CommandRender commands={[managementCommands[1]]} />
</div>
</div>
</div>
</PageSectionContent>
</PageSection>
<PageSection>
<PageSectionMeta>
<PageSectionSummary>
<PageSectionTitle>Delete function</PageSectionTitle>
</PageSectionSummary>
</PageSectionMeta>
<PageSectionContent>
<Alert variant="destructive">
<CriticalIcon />
<AlertTitle>Once your function is deleted, it can no longer be restored</AlertTitle>
<AlertDescription>
Make sure you have made a backup if you want to restore your edge function
</AlertDescription>
<AlertDescription className="mt-3">
<Button
type="danger"
disabled={!canUpdateEdgeFunction}
loading={selectedFunction?.id === undefined}
onClick={() => setShowDeleteModal(true)}
>
Delete edge function
</Button>
</AlertDescription>
</Alert>
</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>
</PageSectionContent>
</PageSection>
<PageSection>
<PageSectionMeta>
<PageSectionSummary>
<PageSectionTitle>Delete function</PageSectionTitle>
</PageSectionSummary>
</PageSectionMeta>
<PageSectionContent>
<Alert variant="destructive">
<CriticalIcon />
<AlertTitle>
Once your function is deleted, it can no longer be restored
</AlertTitle>
<AlertDescription>
Make sure you have made a backup if you want to restore your edge function
</AlertDescription>
<AlertDescription className="mt-3">
<Button
type="danger"
disabled={!canUpdateEdgeFunction}
loading={selectedFunction?.id === undefined}
onClick={() => setShowDeleteModal(true)}
>
Delete edge function
</Button>
</AlertDescription>
</Alert>
</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>
</>
)
}
@@ -1,16 +1,23 @@
import type { ComponentProps } from 'react'
import { useMemo } from 'react'
import { Button } from 'ui'
import { Chart, ChartActions, ChartLoadingState, ChartMetric } from 'ui-patterns/Chart'
import { PageContainer } from 'ui-patterns/PageContainer'
import {
Chart,
ChartActions,
ChartCard,
ChartContent,
ChartHeader,
ChartLoadingState,
ChartMetric,
} from 'ui-patterns/Chart'
import {
PageSection,
PageSectionAside,
PageSectionContent,
PageSectionMeta,
PageSectionSummary,
PageSectionTitle,
} from 'ui-patterns/PageSection'
import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader'
import { EdgeFunctionChartEmptyState } from './EdgeFunctionChartEmptyState'
import { EdgeFunctionInvocationsChart } from './EdgeFunctionInvocationsChart'
@@ -21,8 +28,6 @@ import {
getSegmentedButtonClassName,
} from './EdgeFunctionOverview.utils'
import type { InvocationChartDatum, InvocationUpdateAnnotation } from './EdgeFunctionOverview.utils'
import { toAlertError } from './EdgeFunctionRecentErrors.utils'
import AlertError from '@/components/ui/AlertError'
import type { ChartIntervals } from '@/types'
interface EdgeFunctionInvocationsSectionProps {
@@ -33,9 +38,6 @@ interface EdgeFunctionInvocationsSectionProps {
totalInvocationCount: number
totalErrorCount: number
totalWarningCount: number
isLoadingFunction: boolean
isErrorFunction: boolean
functionError?: unknown
isLoadingChart: boolean
isErrorChart: boolean
chartErrorMessage?: string
@@ -52,9 +54,6 @@ export const EdgeFunctionInvocationsSection = ({
totalInvocationCount,
totalErrorCount,
totalWarningCount,
isLoadingFunction,
isErrorFunction,
functionError,
isLoadingChart,
isErrorChart,
chartErrorMessage,
@@ -67,89 +66,72 @@ export const EdgeFunctionInvocationsSection = ({
() => getChartEmptyStateCopy('invocations', isErrorChart, chartErrorMessage),
[chartErrorMessage, isErrorChart]
)
const invocationTitle = isLoadingChart
? 'Invocations'
: `${totalInvocationCount.toLocaleString('en-US')} total invocations`
return (
<PageSection className="bg-surface-100/50 border-b pb-10 pt-8">
<PageSectionContent>
<PageContainer size="full">
<div className="flex flex-col gap-5">
<PageSectionMeta className="items-center!">
<PageSectionSummary>
<div className="flex flex-wrap items-start gap-x-8 gap-y-4">
<ChartMetric
label="Total Invocations"
value={totalInvocationCount}
status="default"
tooltip="Total number of invocations"
/>
<ChartMetric
label="5xx Rate"
value={formatRate(totalErrorCount, totalInvocationCount)}
status="negative"
tooltip="Share of invocations that returned a 5xx status code"
/>
<ChartMetric
label="4xx Rate"
value={formatRate(totalWarningCount, totalInvocationCount)}
status="warning"
tooltip="Share of invocations that returned a 4xx status code"
/>
</div>
</PageSectionSummary>
<PageSectionAside className="flex-wrap @xl:self-center">
<div className="flex items-center">
{EDGE_FUNCTION_CHART_INTERVALS.map((item, index) => {
return (
<Button
key={`function-filter-${item.key}`}
type={interval === item.key ? 'secondary' : 'default'}
onClick={() => onIntervalChange(item.key)}
className={getSegmentedButtonClassName(
index,
EDGE_FUNCTION_CHART_INTERVALS.length
)}
>
{item.label}
</Button>
)
})}
</div>
<ChartActions actions={actions} />
</PageSectionAside>
</PageSectionMeta>
<div>
{isLoadingFunction && <GenericSkeletonLoader />}
{isErrorFunction && (
<AlertError
error={toAlertError(functionError)}
subject="Failed to retrieve edge function details"
layout="vertical"
/>
)}
</div>
<div>
<Chart isLoading={isLoadingChart}>
{isLoadingChart ? (
<ChartLoadingState />
) : isErrorChart || chartData.length === 0 ? (
<EdgeFunctionChartEmptyState
title={emptyStateCopy.title}
description={emptyStateCopy.description}
/>
) : (
<EdgeFunctionInvocationsChart
chartData={chartData}
dateTimeFormat={dateTimeFormat}
onChartClick={onChartClick}
updateAnnotation={updateAnnotation}
/>
)}
</Chart>
</div>
<PageSection className="pt-0">
<PageSectionMeta>
<PageSectionSummary>
<PageSectionTitle>{invocationTitle}</PageSectionTitle>
</PageSectionSummary>
<PageSectionAside className="flex-wrap @xl:self-center">
<div className="flex items-center">
{EDGE_FUNCTION_CHART_INTERVALS.map((item, index) => (
<Button
key={`function-filter-${item.key}`}
type={interval === item.key ? 'secondary' : 'default'}
onClick={() => onIntervalChange(item.key)}
className={getSegmentedButtonClassName(index, EDGE_FUNCTION_CHART_INTERVALS.length)}
>
{item.label}
</Button>
))}
</div>
</PageContainer>
{actions && actions.length > 0 ? <ChartActions actions={actions} /> : null}
</PageSectionAside>
</PageSectionMeta>
<PageSectionContent>
<Chart isLoading={isLoadingChart}>
<ChartCard>
<ChartHeader align="start">
<div className="flex flex-wrap gap-x-8 gap-y-4">
<ChartMetric
label="5xx Rate"
value={formatRate(totalErrorCount, totalInvocationCount)}
status="negative"
tooltip="Share of invocations that returned a 5xx status code"
/>
<ChartMetric
label="4xx Rate"
value={formatRate(totalWarningCount, totalInvocationCount)}
status="warning"
tooltip="Share of invocations that returned a 4xx status code"
/>
</div>
</ChartHeader>
<ChartContent
isEmpty={isErrorChart || chartData.length === 0}
emptyState={
<EdgeFunctionChartEmptyState
title={emptyStateCopy.title}
description={emptyStateCopy.description}
/>
}
loadingState={<ChartLoadingState />}
>
<div className="h-40">
<EdgeFunctionInvocationsChart
chartData={chartData}
dateTimeFormat={dateTimeFormat}
onChartClick={onChartClick}
updateAnnotation={updateAnnotation}
/>
</div>
</ChartContent>
</ChartCard>
</Chart>
</PageSectionContent>
</PageSection>
)
@@ -3,6 +3,7 @@ import { IS_PLATFORM, useParams } from 'common'
import { ExternalLink } from 'lucide-react'
import { useRouter } from 'next/router'
import { useEffect, useMemo, useState } from 'react'
import { PageContainer } from 'ui-patterns/PageContainer'
import { EdgeFunctionInvocationsSection } from './EdgeFunctionInvocationsSection'
import {
@@ -17,6 +18,7 @@ import {
toEdgeFunctionChartData,
} from './EdgeFunctionOverview.utils'
import type { EdgeFunctionChartRawDatum } from './EdgeFunctionOverview.utils'
import { EdgeFunctionOverviewHeader } from './EdgeFunctionOverviewHeader'
import { EdgeFunctionPerformanceSection } from './EdgeFunctionPerformanceSection'
import { EdgeFunctionRecentErrors } from './EdgeFunctionRecentErrors'
import { EdgeFunctionUsageSection } from './EdgeFunctionUsageSection'
@@ -40,12 +42,7 @@ export const EdgeFunctionOverview = () => {
const selectedInterval =
EDGE_FUNCTION_CHART_INTERVALS.find((item) => item.key === interval) ||
EDGE_FUNCTION_CHART_INTERVALS[1]
const {
data: selectedFunction,
error: functionError,
isPending: isLoadingFunction,
isError: isErrorFunction,
} = useEdgeFunctionQuery({
const { data: selectedFunction } = useEdgeFunctionQuery({
projectRef,
slug: functionSlug,
})
@@ -189,61 +186,61 @@ export const EdgeFunctionOverview = () => {
return (
<>
<EdgeFunctionInvocationsSection
interval={interval}
onIntervalChange={setInterval}
selectedInterval={selectedInterval}
actions={invocationActions}
totalInvocationCount={totalInvocationCount}
totalErrorCount={totalErrorCount}
totalWarningCount={totalWarningCount}
isLoadingFunction={isLoadingFunction}
isErrorFunction={isErrorFunction}
functionError={functionError}
isLoadingChart={combinedStatsResults.isLoading}
isErrorChart={isErrorCombinedStats}
chartErrorMessage={combinedStatsError?.message ?? 'Unknown error'}
chartData={invocationChartData}
onChartClick={() => {
router.push(
`/project/${projectRef}/functions/${functionSlug}/${
isUnifiedLogsEnabled ? 'logs' : 'invocations'
}${isUnifiedLogsEnabled ? '' : `?its=${startDate.toISOString()}`}`
)
}}
updateAnnotation={invocationUpdateAnnotation}
/>
<EdgeFunctionOverviewHeader />
<PageContainer size="default">
<EdgeFunctionInvocationsSection
interval={interval}
onIntervalChange={setInterval}
selectedInterval={selectedInterval}
actions={invocationActions}
totalInvocationCount={totalInvocationCount}
totalErrorCount={totalErrorCount}
totalWarningCount={totalWarningCount}
isLoadingChart={combinedStatsResults.isLoading}
isErrorChart={isErrorCombinedStats}
chartErrorMessage={combinedStatsError?.message ?? 'Unknown error'}
chartData={invocationChartData}
onChartClick={() => {
router.push(
`/project/${projectRef}/functions/${functionSlug}/${
isUnifiedLogsEnabled ? 'logs' : 'invocations'
}${isUnifiedLogsEnabled ? '' : `?its=${startDate.toISOString()}`}`
)
}}
updateAnnotation={invocationUpdateAnnotation}
/>
<EdgeFunctionRecentErrors
functionId={id}
functionSlug={functionSlug as string}
projectRef={projectRef as string}
updatedAt={selectedFunction?.updated_at}
/>
<EdgeFunctionRecentErrors
functionId={id}
functionSlug={functionSlug as string}
projectRef={projectRef as string}
updatedAt={selectedFunction?.updated_at}
/>
<EdgeFunctionPerformanceSection
data={chartData}
dateTimeFormat={dateTimeFormat}
isLoading={combinedStatsResults.isLoading}
isError={isErrorCombinedStats}
errorMessage={combinedStatsError?.message ?? 'Unknown error'}
averageExecutionTime={averageExecutionTime}
maxExecutionTime={maxExecutionTime}
/>
<EdgeFunctionPerformanceSection
data={chartData}
dateTimeFormat={dateTimeFormat}
isLoading={combinedStatsResults.isLoading}
isError={isErrorCombinedStats}
errorMessage={combinedStatsError?.message ?? 'Unknown error'}
averageExecutionTime={averageExecutionTime}
maxExecutionTime={maxExecutionTime}
/>
<EdgeFunctionUsageSection
data={chartData}
dateTimeFormat={dateTimeFormat}
isLoading={combinedStatsResults.isLoading}
isError={isErrorCombinedStats}
errorMessage={combinedStatsError?.message ?? 'Unknown error'}
averageCpuTime={averageCpuTime}
maxCpuTime={maxCpuTime}
averageMemoryUsage={averageMemoryUsage}
totalHeapMemory={totalHeapMemory}
totalExternalMemory={totalExternalMemory}
totalMemoryByType={totalMemoryByType}
/>
<EdgeFunctionUsageSection
data={chartData}
dateTimeFormat={dateTimeFormat}
isLoading={combinedStatsResults.isLoading}
isError={isErrorCombinedStats}
errorMessage={combinedStatsError?.message ?? 'Unknown error'}
averageCpuTime={averageCpuTime}
maxCpuTime={maxCpuTime}
averageMemoryUsage={averageMemoryUsage}
totalHeapMemory={totalHeapMemory}
totalExternalMemory={totalExternalMemory}
totalMemoryByType={totalMemoryByType}
/>
</PageContainer>
</>
)
}
@@ -1,5 +1,5 @@
import dayjs from 'dayjs'
import { describe, expect, it } from 'vitest'
import { describe, expect, it, vi } from 'vitest'
import {
EDGE_FUNCTION_CHART_INTERVALS,
@@ -7,6 +7,7 @@ import {
formatMetric,
formatRate,
formatReferenceDelta,
formatShortFromNow,
getBucketedTimeRange,
getChartEmptyStateCopy,
getChartTimeRangeLabels,
@@ -196,6 +197,17 @@ describe('EdgeFunctionOverview.utils', () => {
expect(rollingEnd.toISOString()).toBe('2026-03-20T10:37:00.000Z')
})
it('formats short relative timestamps', () => {
vi.useFakeTimers()
vi.setSystemTime(new Date('2026-03-20T10:00:00.000Z'))
expect(formatShortFromNow('2026-03-20T09:28:00.000Z')).toBe('32m ago')
expect(formatShortFromNow('2026-03-20T09:59:30.000Z')).toBe('30s ago')
expect(formatShortFromNow('2026-03-19T10:00:00.000Z')).toBe('1d ago')
vi.useRealTimers()
})
it('formats metric, rate, and reference deltas consistently', () => {
expect(formatMetric(12.34, 'MB')).toBe('12.3MB')
expect(formatMetric(1234, 'ms')).toBe('1,234ms')
@@ -278,6 +278,32 @@ export const formatMetric = (value?: number, unit?: string) => {
return unit ? `${formatted}${unit}` : formatted
}
export const formatShortFromNow = (input: string | number | Date): string => {
const then = dayjs(input)
if (!then.isValid()) return ''
const seconds = Math.max(0, dayjs().diff(then, 'second'))
if (seconds < 60) return `${seconds}s ago`
const minutes = Math.floor(seconds / 60)
if (minutes < 60) return `${minutes}m ago`
const hours = Math.floor(minutes / 60)
if (hours < 24) return `${hours}h ago`
const days = Math.floor(hours / 24)
if (days < 7) return `${days}d ago`
const weeks = Math.floor(days / 7)
if (weeks < 5) return `${weeks}w ago`
const months = Math.floor(days / 30)
if (months < 12) return `${months}mo ago`
const years = Math.floor(days / 365)
return `${years}y ago`
}
export const formatRate = (count: number, total: number) =>
new Intl.NumberFormat('en-US', {
style: 'percent',
@@ -0,0 +1,115 @@
import { useParams } from 'common'
import dayjs from 'dayjs'
import relativeTime from 'dayjs/plugin/relativeTime'
import { Clock } from 'lucide-react'
import { useState } from 'react'
import { HoverCard, HoverCardContent, HoverCardTrigger } from 'ui'
import { TimestampInfo } from 'ui-patterns'
import {
PageHeader,
PageHeaderDescription,
PageHeaderMeta,
PageHeaderSummary,
PageHeaderTitle,
} from 'ui-patterns/PageHeader'
import { formatShortFromNow } from './EdgeFunctionOverview.utils'
import CopyButton from '@/components/ui/CopyButton'
import { ShortcutTooltip } from '@/components/ui/ShortcutTooltip'
import { useProjectApiUrl } from '@/data/config/project-endpoint-query'
import { useEdgeFunctionQuery } from '@/data/edge-functions/edge-function-query'
import { SHORTCUT_IDS } from '@/state/shortcuts/registry'
dayjs.extend(relativeTime)
export const EdgeFunctionOverviewHeader = () => {
const { ref: projectRef, functionSlug } = useParams()
const [isTimestampHoverCardOpen, setIsTimestampHoverCardOpen] = useState(false)
const { data: selectedFunction } = useEdgeFunctionQuery({
projectRef,
slug: functionSlug,
})
const { data: endpoint } = useProjectApiUrl({ projectRef })
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 updatedShortRelative = selectedFunction?.updated_at
? formatShortFromNow(selectedFunction.updated_at)
: undefined
return (
<PageHeader size="small" className="pb-12">
<PageHeaderMeta>
<PageHeaderSummary>
<PageHeaderTitle>{name || functionSlug}</PageHeaderTitle>
<PageHeaderDescription className="flex flex-row flex-wrap items-center gap-x-4 gap-y-1 text-sm!">
{functionUrl ? (
<div className="flex items-center gap-x-2">
<span className="flex items-center gap-2">{functionUrl}</span>
<ShortcutTooltip shortcutId={SHORTCUT_IDS.FUNCTION_DETAIL_COPY_URL} side="bottom">
<CopyButton iconOnly type="text" text={functionUrl} />
</ShortcutTooltip>
</div>
) : null}
<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">
{updatedShortRelative ?? 'Deploy status unavailable'}
</span>
</button>
</HoverCardTrigger>
<HoverCardContent side="bottom" align="start" className="w-40 p-0">
{createdRelative && (
<div className="px-4 py-2 space-y-1">
<h3 className="heading-meta text-foreground-light">Created</h3>
{!!selectedFunction && (
<TimestampInfo
className="text-sm"
label={createdRelative}
utcTimestamp={selectedFunction.created_at}
/>
)}
</div>
)}
{updatedRelative && (
<div className="px-4 py-2 space-y-1">
<h3 className="heading-meta text-foreground-light">Last deployed</h3>
{!!selectedFunction && (
<TimestampInfo
className="text-sm"
label={updatedRelative}
utcTimestamp={selectedFunction.updated_at}
/>
)}
</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-sm text-foreground">{selectedFunction.version}</p>
</div>
)}
</HoverCardContent>
</HoverCard>
</PageHeaderDescription>
</PageHeaderSummary>
</PageHeaderMeta>
</PageHeader>
)
}
@@ -1,6 +1,5 @@
import { useMemo } from 'react'
import { ChartMetric } from 'ui-patterns/Chart'
import { PageContainer } from 'ui-patterns/PageContainer'
import {
PageSection,
PageSectionContent,
@@ -59,45 +58,37 @@ export const EdgeFunctionPerformanceSection = ({
return (
<PageSection>
<PageSectionMeta>
<PageSectionSummary>
<PageSectionTitle>Performance</PageSectionTitle>
</PageSectionSummary>
</PageSectionMeta>
<PageSectionContent>
<PageContainer size="full">
<div className="flex flex-col gap-6">
<PageSectionMeta>
<PageSectionSummary>
<PageSectionTitle>Performance</PageSectionTitle>
</PageSectionSummary>
</PageSectionMeta>
<div className="grid grid-cols-1 gap-6 lg:grid-cols-2">
<EdgeFunctionTimeSeriesChartCard
className="lg:col-span-2"
data={data}
dateTimeFormat={dateTimeFormat}
isLoading={isLoading}
isError={isError}
emptyTitle={emptyStateCopy.title}
emptyDescription={emptyStateCopy.description}
metrics={metrics}
dataKey="max_execution_time"
dataKeys={['avg_execution_time', 'max_execution_time']}
config={EXECUTION_TIME_CHART_CONFIG}
tooltipDetails={tooltipDetails}
referenceLines={[
{
y: averageExecutionTime,
label: 'average',
stroke: 'hsl(var(--foreground-default))',
strokeWidth: 1.5,
},
]}
yAxisProps={{
width: 64,
tickFormatter: (value: number) => `${Math.round(value)}ms`,
}}
/>
</div>
</div>
</PageContainer>
<EdgeFunctionTimeSeriesChartCard
data={data}
dateTimeFormat={dateTimeFormat}
isLoading={isLoading}
isError={isError}
emptyTitle={emptyStateCopy.title}
emptyDescription={emptyStateCopy.description}
metrics={metrics}
dataKey="max_execution_time"
dataKeys={['avg_execution_time', 'max_execution_time']}
config={EXECUTION_TIME_CHART_CONFIG}
tooltipDetails={tooltipDetails}
referenceLines={[
{
y: averageExecutionTime,
label: 'average',
stroke: 'hsl(var(--foreground-default))',
strokeWidth: 1.5,
},
]}
yAxisProps={{
width: 64,
tickFormatter: (value: number) => `${Math.round(value)}ms`,
}}
/>
</PageSectionContent>
</PageSection>
)
@@ -13,7 +13,6 @@ import {
TableHeader,
TableRow,
} from 'ui'
import { PageContainer } from 'ui-patterns/PageContainer'
import {
PageSection,
PageSectionAside,
@@ -196,175 +195,169 @@ export const EdgeFunctionRecentErrors = ({
return (
<PageSection>
<PageSectionMeta>
<PageSectionSummary>
<PageSectionTitle>Errors since last deploy</PageSectionTitle>
</PageSectionSummary>
<PageSectionAside>
<Button
type="default"
size="tiny"
icon={<ExternalLink size={14} />}
onClick={() => router.push(`/project/${projectRef}/functions/${functionSlug}/logs`)}
>
View logs
</Button>
</PageSectionAside>
</PageSectionMeta>
<PageSectionContent>
<PageContainer size="full">
<div className="flex flex-col gap-6">
<PageSectionMeta>
<PageSectionSummary>
<PageSectionTitle>Errors since last deploy</PageSectionTitle>
</PageSectionSummary>
<PageSectionAside>
<Button
type="default"
size="tiny"
icon={<ExternalLink size={14} />}
onClick={() =>
router.push(`/project/${projectRef}/functions/${functionSlug}/logs`)
}
>
View logs
</Button>
</PageSectionAside>
</PageSectionMeta>
{recentErrorInvocationsError || functionRuntimeLogsError ? (
<AlertError
error={queryError}
subject="Failed to retrieve edge function errors since the last deploy"
/>
) : isLoadingRecentErrorInvocations ||
isLoadingFunctionRuntimeLogs ||
isLoadingSinceLastDeployInvocationCount ? (
<GenericSkeletonLoader />
) : recentErrorGroups.length === 0 ? (
<div className="rounded-md border border-dashed px-5 py-6 text-sm text-foreground-light">
<div className="flex items-start gap-3">
{emptyStateIcon}
<div>{emptyStateMessage}</div>
</div>
</div>
) : (
<Card className="p-0 overflow-hidden">
<Table>
<TableHeader>
<TableRow>
<TableHead>Error</TableHead>
<TableHead>Count</TableHead>
<TableHead>Last Seen</TableHead>
<TableHead>Method</TableHead>
<TableHead>Status</TableHead>
<TableHead>Duration</TableHead>
<TableHead className="text-right">Troubleshoot</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{recentErrorGroups.map((group) => {
const displayMessage = getDisplayErrorMessage(group)
const docsUrl = buildTroubleshootingDocsUrl({
statusCode: group.lastStatusCode,
})
return (
<Fragment key={group.message}>
<TableRow key={`${group.message}-summary`}>
<TableCell className="max-w-[420px]">
<span
className="block truncate whitespace-nowrap text-foreground"
title={displayMessage}
>
{displayMessage}
</span>
</TableCell>
<TableCell className="text-foreground-light">{group.count}</TableCell>
<TableCell className="text-foreground-light">
{formatLogTimestamp(group.lastSeen, 'relative')}
</TableCell>
<TableCell className="text-foreground-light">
{group.lastMethod ?? '-'}
</TableCell>
<TableCell>
{group.lastStatusCode ? (
<Badge
variant={getStatusBadgeVariant(group.lastStatusCode)}
className="font-mono"
>
{group.lastStatusCode}
</Badge>
) : (
<Badge variant="destructive" className="font-mono">
Error
</Badge>
)}
</TableCell>
<TableCell className="text-foreground-light">
{group.executionTime ?? '-'}
</TableCell>
<TableCell className="text-right">
<div className="flex justify-end">
<AiAssistantDropdown
label="Ask Assistant"
size="tiny"
buildPrompt={() => buildGroupAssistantPrompt(group, functionSlug)}
onOpenAssistant={() => handleOpenAssistant(group)}
additionalDropdownItems={[
{
label: 'View troubleshooting guide',
icon: <BookOpen size={14} />,
onClick: () =>
window.open(docsUrl, '_blank', 'noopener,noreferrer'),
},
]}
/>
</div>
</TableCell>
</TableRow>
<TableRow key={`${group.message}-logs`} className="hover:bg-transparent">
<TableCell colSpan={7} className="p-0">
<div className="max-h-64 overflow-auto bg-surface-75 font-mono text-xs">
{group.logs.length === 0 ? (
<div className="px-4 py-3 text-foreground-lighter">
No related runtime logs found for this error group.
</div>
) : (
group.logs.map((log, index) => {
const isError = log.level === 'error'
return (
<div
key={log.key}
className={cn(
'flex items-start gap-3 px-4 py-2',
index !== 0 && 'border-t border-default',
isError && 'bg-destructive-200/40'
)}
>
<span className="shrink-0 tabular-nums text-foreground-muted">
{formatLogTimestamp(log.lastSeen, 'time')}
</span>
<Badge
variant={isError ? 'destructive' : 'default'}
className="shrink-0"
>
{log.level}
</Badge>
{log.count > 1 && (
<span className="shrink-0 text-foreground-muted tabular-nums">
×{log.count}
</span>
)}
<span
className={cn(
'flex-1 wrap-break-word whitespace-pre-wrap',
isError ? 'text-destructive' : 'text-foreground-light'
)}
>
{formatSingleLineMessage(log.message)}
</span>
</div>
)
})
)}
</div>
</TableCell>
</TableRow>
</Fragment>
)
})}
</TableBody>
</Table>
</Card>
)}
{recentErrorInvocationsError || functionRuntimeLogsError ? (
<AlertError
error={queryError}
subject="Failed to retrieve edge function errors since the last deploy"
/>
) : isLoadingRecentErrorInvocations ||
isLoadingFunctionRuntimeLogs ||
isLoadingSinceLastDeployInvocationCount ? (
<GenericSkeletonLoader />
) : recentErrorGroups.length === 0 ? (
<div className="rounded-md border border-dashed px-5 py-6 text-sm text-foreground-light">
<div className="flex items-start gap-3">
{emptyStateIcon}
<div>{emptyStateMessage}</div>
</div>
</div>
</PageContainer>
) : (
<Card className="p-0 overflow-hidden">
<Table>
<TableHeader>
<TableRow>
<TableHead>Error</TableHead>
<TableHead>Count</TableHead>
<TableHead>Last Seen</TableHead>
<TableHead>Method</TableHead>
<TableHead>Status</TableHead>
<TableHead>Duration</TableHead>
<TableHead className="text-right">Troubleshoot</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{recentErrorGroups.map((group) => {
const displayMessage = getDisplayErrorMessage(group)
const docsUrl = buildTroubleshootingDocsUrl({
statusCode: group.lastStatusCode,
})
return (
<Fragment key={group.message}>
<TableRow key={`${group.message}-summary`}>
<TableCell className="max-w-[420px]">
<span
className="block truncate whitespace-nowrap text-foreground"
title={displayMessage}
>
{displayMessage}
</span>
</TableCell>
<TableCell className="text-foreground-light">{group.count}</TableCell>
<TableCell className="text-foreground-light">
{formatLogTimestamp(group.lastSeen, 'relative')}
</TableCell>
<TableCell className="text-foreground-light">
{group.lastMethod ?? '-'}
</TableCell>
<TableCell>
{group.lastStatusCode ? (
<Badge
variant={getStatusBadgeVariant(group.lastStatusCode)}
className="font-mono"
>
{group.lastStatusCode}
</Badge>
) : (
<Badge variant="destructive" className="font-mono">
Error
</Badge>
)}
</TableCell>
<TableCell className="text-foreground-light">
{group.executionTime ?? '-'}
</TableCell>
<TableCell className="text-right">
<div className="flex justify-end">
<AiAssistantDropdown
label="Ask Assistant"
size="tiny"
buildPrompt={() => buildGroupAssistantPrompt(group, functionSlug)}
onOpenAssistant={() => handleOpenAssistant(group)}
additionalDropdownItems={[
{
label: 'View troubleshooting guide',
icon: <BookOpen size={14} />,
onClick: () =>
window.open(docsUrl, '_blank', 'noopener,noreferrer'),
},
]}
/>
</div>
</TableCell>
</TableRow>
<TableRow key={`${group.message}-logs`} className="hover:bg-transparent">
<TableCell colSpan={7} className="p-0">
<div className="max-h-64 overflow-auto bg-surface-75 font-mono text-xs">
{group.logs.length === 0 ? (
<div className="px-4 py-3 text-foreground-lighter">
No related runtime logs found for this error group.
</div>
) : (
group.logs.map((log, index) => {
const isError = log.level === 'error'
return (
<div
key={log.key}
className={cn(
'flex items-start gap-3 px-4 py-2',
index !== 0 && 'border-t border-default',
isError && 'bg-destructive-200/40'
)}
>
<span className="shrink-0 tabular-nums text-foreground-muted">
{formatLogTimestamp(log.lastSeen, 'time')}
</span>
<Badge
variant={isError ? 'destructive' : 'default'}
className="shrink-0"
>
{log.level}
</Badge>
{log.count > 1 && (
<span className="shrink-0 text-foreground-muted tabular-nums">
×{log.count}
</span>
)}
<span
className={cn(
'flex-1 wrap-break-word whitespace-pre-wrap',
isError ? 'text-destructive' : 'text-foreground-light'
)}
>
{formatSingleLineMessage(log.message)}
</span>
</div>
)
})
)}
</div>
</TableCell>
</TableRow>
</Fragment>
)
})}
</TableBody>
</Table>
</Card>
)}
</PageSectionContent>
</PageSection>
)
@@ -1,6 +1,5 @@
import { useMemo } from 'react'
import { ChartMetric } from 'ui-patterns/Chart'
import { PageContainer } from 'ui-patterns/PageContainer'
import {
PageSection,
PageSectionContent,
@@ -90,68 +89,63 @@ export const EdgeFunctionUsageSection = ({
return (
<PageSection>
<PageSectionMeta>
<PageSectionSummary>
<PageSectionTitle>Usage</PageSectionTitle>
</PageSectionSummary>
</PageSectionMeta>
<PageSectionContent>
<PageContainer size="full">
<div className="flex flex-col gap-6">
<PageSectionMeta>
<PageSectionSummary>
<PageSectionTitle>Usage</PageSectionTitle>
</PageSectionSummary>
</PageSectionMeta>
<div className="flex flex-col gap-6">
<EdgeFunctionTimeSeriesChartCard
data={data}
dateTimeFormat={dateTimeFormat}
isLoading={isLoading}
isError={isError}
emptyTitle={cpuEmptyStateCopy.title}
emptyDescription={cpuEmptyStateCopy.description}
metrics={cpuMetrics}
dataKey="max_cpu_time_used"
config={CPU_TIME_CHART_CONFIG}
tooltipDetails={cpuTooltipDetails}
referenceLines={[
{
y: averageCpuTime,
label: 'average',
stroke: 'hsl(var(--foreground-default))',
strokeWidth: 1.5,
},
]}
yAxisProps={{
width: 64,
tickFormatter: (value: number) => `${Math.round(value)}ms`,
}}
/>
<div className="grid grid-cols-1 gap-6 lg:grid-cols-2">
<EdgeFunctionTimeSeriesChartCard
data={data}
dateTimeFormat={dateTimeFormat}
isLoading={isLoading}
isError={isError}
emptyTitle={cpuEmptyStateCopy.title}
emptyDescription={cpuEmptyStateCopy.description}
metrics={cpuMetrics}
dataKey="max_cpu_time_used"
config={CPU_TIME_CHART_CONFIG}
tooltipDetails={cpuTooltipDetails}
referenceLines={[
{
y: averageCpuTime,
label: 'average',
stroke: 'hsl(var(--foreground-default))',
strokeWidth: 1.5,
},
]}
yAxisProps={{
width: 64,
tickFormatter: (value: number) => `${Math.round(value)}ms`,
}}
/>
<EdgeFunctionTimeSeriesChartCard
data={data}
dateTimeFormat={dateTimeFormat}
isLoading={isLoading}
isError={isError}
emptyTitle={memoryEmptyStateCopy.title}
emptyDescription={memoryEmptyStateCopy.description}
metrics={memoryMetrics}
dataKey="avg_memory_used"
config={MEMORY_CHART_CONFIG}
tooltipDetails={memoryTooltipDetails}
referenceLines={[
{
y: averageMemoryUsage,
label: 'average',
stroke: 'hsl(var(--foreground-default))',
strokeWidth: 1.5,
},
]}
yAxisProps={{
width: 64,
tickFormatter: (value: number) => `${Number(value).toFixed(1)}MB`,
}}
/>
</div>
</div>
</PageContainer>
<EdgeFunctionTimeSeriesChartCard
data={data}
dateTimeFormat={dateTimeFormat}
isLoading={isLoading}
isError={isError}
emptyTitle={memoryEmptyStateCopy.title}
emptyDescription={memoryEmptyStateCopy.description}
metrics={memoryMetrics}
dataKey="avg_memory_used"
config={MEMORY_CHART_CONFIG}
tooltipDetails={memoryTooltipDetails}
referenceLines={[
{
y: averageMemoryUsage,
label: 'average',
stroke: 'hsl(var(--foreground-default))',
strokeWidth: 1.5,
},
]}
yAxisProps={{
width: 64,
tickFormatter: (value: number) => `${Number(value).toFixed(1)}MB`,
}}
/>
</div>
</PageSectionContent>
</PageSection>
)
@@ -6,10 +6,9 @@ import { useEffect, useMemo, useState } from 'react'
import { toast } from 'sonner'
import { LogoLoader } from 'ui'
import { getEdgeFunctionDetailsPageLayout } from './layout'
import { DeployEdgeFunctionWarningModal } from '@/components/interfaces/EdgeFunctions/DeployEdgeFunctionWarningModal'
import { formatFunctionBodyToFiles } from '@/components/interfaces/EdgeFunctions/EdgeFunctions.utils'
import { DefaultLayout } from '@/components/layouts/DefaultLayout'
import EdgeFunctionDetailsLayout from '@/components/layouts/EdgeFunctionsLayout/EdgeFunctionDetailsLayout'
import { PreventNavigationOnUnsavedChanges } from '@/components/ui-patterns/Dialogs/PreventNavigationOnUnsavedChanges'
import { ButtonTooltip } from '@/components/ui/ButtonTooltip'
import { FileExplorerAndEditor } from '@/components/ui/FileExplorerAndEditor'
@@ -241,12 +240,6 @@ const CodePage = () => {
)
}
CodePage.getLayout = (page: React.ReactNode) => {
return (
<DefaultLayout>
<EdgeFunctionDetailsLayout title="Code">{page}</EdgeFunctionDetailsLayout>
</DefaultLayout>
)
}
CodePage.getLayout = getEdgeFunctionDetailsPageLayout
export default CodePage
@@ -1,14 +1,9 @@
import { getEdgeFunctionDetailsPageLayout } from './layout'
import { EdgeFunctionDetails } from '@/components/interfaces/Functions/EdgeFunctionDetails/EdgeFunctionDetails'
import DefaultLayout from '@/components/layouts/DefaultLayout'
import EdgeFunctionDetailsLayout from '@/components/layouts/EdgeFunctionsLayout/EdgeFunctionDetailsLayout'
import type { NextPageWithLayout } from '@/types'
const PageLayout: NextPageWithLayout = () => <EdgeFunctionDetails />
PageLayout.getLayout = (page) => (
<DefaultLayout>
<EdgeFunctionDetailsLayout title="Settings">{page}</EdgeFunctionDetailsLayout>
</DefaultLayout>
)
PageLayout.getLayout = getEdgeFunctionDetailsPageLayout
export default PageLayout
@@ -10,11 +10,10 @@ import { Alert, AlertDescription, AlertTitle, Button, LogoLoader, WarningIcon }
import { PageContainer } from 'ui-patterns/PageContainer'
import { PageSection, PageSectionContent } from 'ui-patterns/PageSection'
import { getEdgeFunctionDetailsPageLayout } from './layout'
import { EdgeFunctionOverview } from '@/components/interfaces/Functions/EdgeFunctionOverview/EdgeFunctionOverview'
import { EdgeFunctionRecentInvocations } from '@/components/interfaces/Functions/EdgeFunctionRecentInvocations'
import ReportWidget from '@/components/interfaces/Reports/ReportWidget'
import DefaultLayout from '@/components/layouts/DefaultLayout'
import EdgeFunctionDetailsLayout from '@/components/layouts/EdgeFunctionsLayout/EdgeFunctionDetailsLayout'
import AreaChart from '@/components/ui/Charts/AreaChart'
import StackedBarChart from '@/components/ui/Charts/StackedBarChart'
import NoPermission from '@/components/ui/NoPermission'
@@ -172,7 +171,7 @@ const LegacyEdgeFunctionOverview = () => {
</span>
</div>
<div>
<div className="grid grid-cols-1 md:grid-cols-2 md:gap-4 lg:grid-cols-2 lg:gap-8">
<div className="flex flex-col gap-8">
<ReportWidget
title="Execution time"
tooltip="Average execution time of function invocations"
@@ -440,10 +439,6 @@ const PageLayout: NextPageWithLayout = () => {
return <LegacyEdgeFunctionOverview />
}
PageLayout.getLayout = (page) => (
<DefaultLayout>
<EdgeFunctionDetailsLayout title="Overview">{page}</EdgeFunctionDetailsLayout>
</DefaultLayout>
)
PageLayout.getLayout = getEdgeFunctionDetailsPageLayout
export default PageLayout
@@ -1,8 +1,7 @@
import { useParams } from 'common'
import { getEdgeFunctionDetailsPageLayout } from './layout'
import { LogsPreviewer } from '@/components/interfaces/Settings/Logs/LogsPreviewer'
import DefaultLayout from '@/components/layouts/DefaultLayout'
import EdgeFunctionDetailsLayout from '@/components/layouts/EdgeFunctionsLayout/EdgeFunctionDetailsLayout'
import { useEdgeFunctionQuery } from '@/data/edge-functions/edge-function-query'
import type { NextPageWithLayout } from '@/types'
@@ -27,10 +26,6 @@ export const LogPage: NextPageWithLayout = () => {
)
}
LogPage.getLayout = (page) => (
<DefaultLayout>
<EdgeFunctionDetailsLayout title="Invocations">{page}</EdgeFunctionDetailsLayout>
</DefaultLayout>
)
LogPage.getLayout = getEdgeFunctionDetailsPageLayout
export default LogPage
@@ -0,0 +1,10 @@
import type { ReactNode } from 'react'
import DefaultLayout from '@/components/layouts/DefaultLayout'
import EdgeFunctionDetailsLayout from '@/components/layouts/EdgeFunctionsLayout/EdgeFunctionDetailsLayout'
export const getEdgeFunctionDetailsPageLayout = (page: ReactNode) => (
<DefaultLayout>
<EdgeFunctionDetailsLayout>{page}</EdgeFunctionDetailsLayout>
</DefaultLayout>
)
@@ -1,8 +1,7 @@
import { useParams } from 'common'
import { getEdgeFunctionDetailsPageLayout } from './layout'
import { LogsPreviewer } from '@/components/interfaces/Settings/Logs/LogsPreviewer'
import DefaultLayout from '@/components/layouts/DefaultLayout'
import EdgeFunctionDetailsLayout from '@/components/layouts/EdgeFunctionsLayout/EdgeFunctionDetailsLayout'
import { useEdgeFunctionQuery } from '@/data/edge-functions/edge-function-query'
import type { NextPageWithLayout } from '@/types'
@@ -28,10 +27,6 @@ export const LogPage: NextPageWithLayout = () => {
)
}
LogPage.getLayout = (page) => (
<DefaultLayout>
<EdgeFunctionDetailsLayout title="Logs">{page}</EdgeFunctionDetailsLayout>
</DefaultLayout>
)
LogPage.getLayout = getEdgeFunctionDetailsPageLayout
export default LogPage