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} )