From 8499d30e98eaab330a484247482b9e52206826e7 Mon Sep 17 00:00:00 2001 From: Saxon Fletcher Date: Thu, 19 Mar 2026 15:55:19 +1000 Subject: [PATCH] Slight tuning of edge function page (#43798) image 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) --- .../EdgeFunctionsDiffPanel.tsx | 3 +- .../EdgeFunctionDetails.tsx | 518 +++++++----------- .../EdgeFunctionDetailsLayout.tsx | 69 ++- .../functions/[functionSlug]/details.tsx | 2 +- 4 files changed, 272 insertions(+), 320 deletions(-) diff --git a/apps/studio/components/interfaces/BranchManagement/EdgeFunctionsDiffPanel.tsx b/apps/studio/components/interfaces/BranchManagement/EdgeFunctionsDiffPanel.tsx index ff91c7bd837..ff945fd0382 100644 --- a/apps/studio/components/interfaces/BranchManagement/EdgeFunctionsDiffPanel.tsx +++ b/apps/studio/components/interfaces/BranchManagement/EdgeFunctionsDiffPanel.tsx @@ -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 = ({ diff --git a/apps/studio/components/interfaces/Functions/EdgeFunctionDetails/EdgeFunctionDetails.tsx b/apps/studio/components/interfaces/Functions/EdgeFunctionDetails/EdgeFunctionDetails.tsx index 3da5c1a0ada..2fc17538c5c 100644 --- a/apps/studio/components/interfaces/Functions/EdgeFunctionDetails/EdgeFunctionDetails.tsx +++ b/apps/studio/components/interfaces/Functions/EdgeFunctionDetails/EdgeFunctionDetails.tsx @@ -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 ( - - - - Details - {isLoading && } - {isError && ( - - )} - {isSuccess && ( -
-
Slug
-
{selectedFunction?.slug}
- -
Endpoint URL
-
- -
- - {IS_PLATFORM && ( - <> -
Region
-
All functions are deployed globally
- - )} - -
Created at
-
- {dayjs(selectedFunction?.created_at ?? 0).format('dddd, MMMM D, YYYY h:mm A')} -
- -
Last updated at
-
- {dayjs(selectedFunction?.updated_at ?? 0).format('dddd, MMMM D, YYYY h:mm A')} -
- -
Deployments
-
{selectedFunction?.version ?? 0}
- -
Import Maps
-
-

- Import maps are{' '} - - {hasImportMap ? 'used' : 'not used'} - {' '} - for this function -

-

- Import maps allow the use of bare specifiers in functions instead of explicit - import URLs -

-
- -
-
-
- )} -
+ + + + + Function configuration + + - - - - Function configuration - - - - -
- + + + + + ( + + + + + + )} + /> + + {IS_PLATFORM && ( + <> ( + Requires a JWT signed{' '} + only by the legacy secret{' '} + in the Authorization header. The anon key + satisfies this. Recommended: OFF with JWT and custom auth logic in + your function code. + + } > - @@ -271,214 +201,178 @@ export const EdgeFunctionDetails = () => { )} /> - {IS_PLATFORM && ( - <> - - ( - - Requires that a JWT signed{' '} - - only by the legacy JWT secret - {' '} - is present in the Authorization header. The easy to - obtain anon key can be used to satisfy this - requirement. Recommendation: OFF with JWT and additional - authorization logic implemented inside your function's code. - - } - > - - - - - )} - /> - - - {form.formState.isDirty && ( - - )} - - - - )} - - - -
-
+ + {form.formState.isDirty && ( + + )} + + + + )} + + + +
+
+ + + + Invoke function + + + + + + + + {invocationTabs.map((tab) => ( + + {tab.label} + + ))} + {selectedTab === 'curl' && ( + + )} + + {invocationTabs.map((tab) => { + const code = tab.code({ + showKey, + functionUrl, + functionName: selectedFunction?.name ?? '', + apiKey, + }) + + return ( + + 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, + }) + ) + }} + /> + + ) + })} + + + + + + + {IS_PLATFORM && ( + <> - Invoke function + Develop locally - - - - - {invocationTabs.map((tab) => ( - - {tab.label} - - ))} - {selectedTab === 'curl' && ( - - )} - - {invocationTabs.map((tab) => { - const code = tab.code({ - showKey, - functionUrl, - functionName: selectedFunction?.name ?? '', - apiKey, - }) - - return ( - - 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, - }) - ) - }} - /> - - ) - })} - - - +
+
+ ( + <> + supabase functions download{' '} + {selectedFunction?.slug} + + ), + comment: '1. Download the function', + }, + ]} + /> + + +
+
- - {IS_PLATFORM && ( - <> - - - - Develop locally - - - -
-
- ( - <> - supabase functions download{' '} - {selectedFunction?.slug} - - ), - comment: '1. Download the function', - }, - ]} - /> - - -
-
-
-
- - - - Delete function - - - - - - - Once your function is deleted, it can no longer be restored - - - Make sure you have made a backup if you want to restore your edge function - - - - - - - - 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', - }} - /> - - )} - -
+ + + + Delete function + + + + + + + Once your function is deleted, it can no longer be restored + + + Make sure you have made a backup if you want to restore your edge function + + + + + + + + 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', + }} + /> + + )}
) } diff --git a/apps/studio/components/layouts/EdgeFunctionsLayout/EdgeFunctionDetailsLayout.tsx b/apps/studio/components/layouts/EdgeFunctionsLayout/EdgeFunctionDetailsLayout.tsx index 0dcdb9b6359..ea7c04f68a0 100644 --- a/apps/studio/components/layouts/EdgeFunctionsLayout/EdgeFunctionDetailsLayout.tsx +++ b/apps/studio/components/layouts/EdgeFunctionsLayout/EdgeFunctionDetailsLayout.tsx @@ -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 (
- + {breadcrumbItems.length > 0 && ( @@ -253,6 +272,44 @@ const EdgeFunctionDetailsLayout = ({ {functionSlug ? name : 'Edge Functions'} + + {functionUrl} + + + + + + {createdRelative && ( +
+

Created

+

{createdRelative}

+
+ )} + {updatedRelative && ( +
+

Last deployed

+

{updatedRelative}

+
+ )} + {selectedFunction?.version !== undefined && ( +
+

Deployments

+

{selectedFunction.version}

+
+ )} +
+
+
diff --git a/apps/studio/pages/project/[ref]/functions/[functionSlug]/details.tsx b/apps/studio/pages/project/[ref]/functions/[functionSlug]/details.tsx index d64f95d72d3..0328bb721c1 100644 --- a/apps/studio/pages/project/[ref]/functions/[functionSlug]/details.tsx +++ b/apps/studio/pages/project/[ref]/functions/[functionSlug]/details.tsx @@ -7,7 +7,7 @@ const PageLayout: NextPageWithLayout = () => PageLayout.getLayout = (page) => ( - {page} + {page} )