diff --git a/studio/components/interfaces/Functions/EdgeFunctionDetails.tsx b/studio/components/interfaces/Functions/EdgeFunctionDetails.tsx new file mode 100644 index 00000000000..c93c1fefb66 --- /dev/null +++ b/studio/components/interfaces/Functions/EdgeFunctionDetails.tsx @@ -0,0 +1,332 @@ +import dayjs from 'dayjs' +import { useRouter } from 'next/router' +import { FC, useState, useEffect } from 'react' +import * as Tooltip from '@radix-ui/react-tooltip' +import { PermissionAction } from '@supabase/shared-types/out/constants' +import { + Alert, + IconGlobe, + IconTerminal, + IconMinimize2, + IconMaximize2, + IconCheck, + IconClipboard, + Button, + Modal, +} from 'ui' + +import { useStore, useParams, checkPermissions } from 'hooks' +import Panel from 'components/ui/Panel' +import CommandRender from './CommandRender' +import { useProjectApiQuery } from 'data/config/project-api-query' +import { useEdgeFunctionDeleteMutation } from 'data/edge-functions/edge-functions-delete-mutation' + +interface Props {} + +// [Joshen] Next - additional configs: Verify jwt + import maps + +const EdgeFunctionDetails: FC = () => { + const router = useRouter() + const { functions, ui } = useStore() + const { ref: projectRef, id } = useParams() + const [isCopied, setIsCopied] = useState(false) + const [showDeleteModal, setShowDeleteModal] = useState(false) + const [showInstructions, setShowInstructions] = useState(false) + const [selectedFunction, setSelectedFunction] = useState(null) + + const canDeleteEdgeFunction = checkPermissions(PermissionAction.FUNCTIONS_WRITE, '*') + const { mutateAsync: deleteEdgeFunction, isLoading: isDeleting } = useEdgeFunctionDeleteMutation() + + useEffect(() => { + setSelectedFunction(functions.byId(id)) + }, [functions.isLoaded, ui.selectedProject]) + + const { data: settings } = useProjectApiQuery({ projectRef }) + + // Get the API service + const apiService = settings?.autoApiService + const anonKey = apiService?.service_api_keys.find((x) => x.name === 'anon key') + ? apiService.defaultApiKey + : undefined + + const endpoint = apiService?.app_config.endpoint ?? '' + const endpointSections = endpoint.split('.') + const functionsEndpoint = [ + ...endpointSections.slice(0, 1), + 'functions', + ...endpointSections.slice(1), + ].join('.') + const functionUrl = `${apiService?.protocol}://${functionsEndpoint}/${selectedFunction?.slug}` + + const onConfirmDelete = async () => { + if (!projectRef) return console.error('Project ref is required') + if (selectedFunction === undefined) return console.error('No edge function selected') + + try { + await deleteEdgeFunction({ projectRef, slug: selectedFunction.slug }) + ui.setNotification({ + category: 'success', + message: `Successfully deleted "${selectedFunction.name}"`, + }) + router.push(`/project/${projectRef}/functions`) + } catch (error: any) { + ui.setNotification({ + category: 'error', + message: `Failed to delete function: ${error.message}`, + }) + } + } + + const managementCommands: any = [ + { + command: `supabase functions deploy ${selectedFunction?.slug}`, + description: 'This will overwrite the deployed function with your new function', + jsx: () => { + return ( + <> + supabase functions deploy{' '} + {selectedFunction?.slug} + + ) + }, + comment: 'Deploy a new version', + }, + { + command: `supabase functions delete ${selectedFunction?.slug}`, + description: 'This will remove the function and all the logs associated with it', + jsx: () => { + return ( + <> + supabase functions delete{' '} + {selectedFunction?.slug} + + ) + }, + comment: 'Delete the function', + }, + ] + + const secretCommands: any = [ + { + command: `supabase secrets list`, + description: 'This will list all the secrets for your project', + jsx: () => { + return ( + <> + supabase secrets list + + ) + }, + comment: 'View all secrets', + }, + { + command: `supabase secrets set NAME1=VALUE1 NAME2=VALUE2`, + description: 'This will set secrets for your project', + jsx: () => { + return ( + <> + supabase secrets set NAME1=VALUE1 NAME2=VALUE2 + + ) + }, + comment: 'Set secrets for your project', + }, + { + command: `supabase secrets unset NAME1 NAME2 `, + description: 'This will delete secrets for your project', + jsx: () => { + return ( + <> + supabase secrets unset NAME1 NAME2 + + ) + }, + comment: 'Unset secrets for your project', + }, + ] + + const invokeCommands: any = [ + { + command: `curl -L -X POST '${functionUrl}' -H 'Authorization: Bearer ${ + anonKey ?? '[YOUR ANON KEY]' + }' --data '{"name":"Functions"}'`, + description: 'Invokes the hello function', + jsx: () => { + return ( + <> + curl -L -X POST 'https://{functionsEndpoint}/ + {selectedFunction?.slug}' -H 'Authorization: Bearer [YOUR ANON KEY]'{' '} + {`--data '{"name":"Functions"}'`} + + ) + }, + comment: 'Invoke your function', + }, + ] + + return ( + <> +
+
+
+

Function Name

+

{selectedFunction?.name}

+
+ +
+

Endpoint URL

+

{functionUrl}

+ +
+ +
+

Created At

+

+ {selectedFunction?.created_at && + dayjs(selectedFunction.created_at).format('dddd, MMMM D, YYYY h:mm A')} +

+
+ +
+

Last Updated At

+

+ {selectedFunction?.updated_at && + dayjs(selectedFunction.updated_at).format('dddd, MMMM D, YYYY h:mm A')} +

+
+ +
+ Deployments +
{selectedFunction?.version}
+
+ +
+ Regions +
+
+ + Earth +
+ All functions are deployed globally +
+
+
+ +
+
+
+
+ +
+

Command line access

+
+
setShowInstructions(!showInstructions)}> + {showInstructions ? ( + + ) : ( + + )} +
+
+ +
Deployment management
+ +
Invoke
+ +
Secrets management
+ +
+ + Delete Edge Function

}> + + +

+ Make sure you have made a backup if you want to restore your edge function +

+ + + + + {!canDeleteEdgeFunction && ( + + +
+ + You need additional permissions to delete an edge function + +
+
+ )} +
+
+
+
+
+ + Confirm to delete {selectedFunction?.name}} + visible={showDeleteModal} + loading={isDeleting} + onCancel={() => setShowDeleteModal(false)} + onConfirm={onConfirmDelete} + > +
+ + + Ensure that you have made a backup if you want to restore your edge function + + +
+
+ + ) +} + +export default EdgeFunctionDetails diff --git a/studio/components/interfaces/Functions/EdgeFunctionsListItem.tsx b/studio/components/interfaces/Functions/EdgeFunctionsListItem.tsx new file mode 100644 index 00000000000..0d9ce1f727b --- /dev/null +++ b/studio/components/interfaces/Functions/EdgeFunctionsListItem.tsx @@ -0,0 +1,103 @@ +import dayjs from 'dayjs' +import { FC, useState } from 'react' +import { useRouter } from 'next/router' +import { observer } from 'mobx-react-lite' +import { IconCheck, IconClipboard } from 'ui' +import * as Tooltip from '@radix-ui/react-tooltip' + +import { useParams, useStore } from 'hooks' +import Table from 'components/to-be-cleaned/Table' +import { EdgeFunctionsResponse } from 'data/edge-functions/edge-functions-query' + +interface Props { + function: EdgeFunctionsResponse +} + +const EdgeFunctionsListItem: FC = ({ function: item }) => { + const router = useRouter() + const { ui } = useStore() + const { ref } = useParams() + const [isCopied, setIsCopied] = useState(false) + + // get the .co or .net TLD from the restUrl + const restUrl = ui.selectedProject?.restUrl + const restUrlTld = new URL(restUrl as string).hostname.split('.').pop() + const functionUrl = `https://${ref}.functions.supabase.${restUrlTld}/${item.slug}` + + return ( + { + router.push(`/project/${ref}/functions/${item.id}/details`) + }} + > + +
+

{item.name}

+
+
+ +
+

{functionUrl}

+ +
+
+ +

{dayjs(item.created_at).format('DD MMM, YYYY HH:mm')}

+
+ + + +
+

{dayjs(item.updated_at).fromNow()}

+
+
+ + +
+ + Last updated on {dayjs(item.updated_at).format('DD MMM, YYYY HH:mm')} + +
+
+
+
+ +

{item.version}

+
+
+ ) +} + +export default observer(EdgeFunctionsListItem) diff --git a/studio/components/interfaces/Functions/FunctionsListItem.tsx b/studio/components/interfaces/Functions/FunctionsListItem.tsx deleted file mode 100644 index f2b9045d417..00000000000 --- a/studio/components/interfaces/Functions/FunctionsListItem.tsx +++ /dev/null @@ -1,62 +0,0 @@ -import dayjs from 'dayjs' -import { FC } from 'react' -import { useRouter } from 'next/router' -import { observer } from 'mobx-react-lite' - -import { useStore } from 'hooks' -import Table from 'components/to-be-cleaned/Table' - -interface Props { - function: any -} - -const FunctionsListItem: FC = ({ function: item }) => { - const { ui } = useStore() - const is_functionConfirmed = item.email_confirmed_at || item.phone_confirmed_at - - const router = useRouter() - const ref = ui?.selectedProject?.ref - - // get the .co or .net TLD from the restUrl - const restUrl = ui.selectedProject?.restUrl - const restUrlTld = new URL(restUrl as string).hostname.split('.').pop() - - return ( - { - router.push(`/project/${ref}/functions/${item.id}`) - }} - > - -
- {item.name} -
-
- -
- {`https://${ref}.functions.supabase.${restUrlTld}/${item.slug}`} - {`/${item.name}`} -
-
- - - {dayjs(item.created_at).format('DD MMM, YYYY HH:mm')} - - - - - {dayjs(item.updated_at).format('DD MMM, YYYY HH:mm')} - - - - v{item.version} - - - {item.status} - -
- ) -} - -export default observer(FunctionsListItem) diff --git a/studio/components/interfaces/Functions/FunctionsNav.tsx b/studio/components/interfaces/Functions/FunctionsNav.tsx index dfe6bd5ab13..af24e28d29f 100644 --- a/studio/components/interfaces/Functions/FunctionsNav.tsx +++ b/studio/components/interfaces/Functions/FunctionsNav.tsx @@ -1,8 +1,7 @@ import { Tabs } from 'ui' import { useRouter } from 'next/router' -// @ts-ignore -const FunctionsNav = ({ item }) => { +const FunctionsNav = ({ item }: any) => { const router = useRouter() const activeRoute = router.pathname.split('/')[5] const { ref } = router.query @@ -17,9 +16,8 @@ const FunctionsNav = ({ item }) => { router.push(`/project/${ref}/functions/${item.id}/${e === 'metrics' ? '' : e}`) } > - - {/* */} + diff --git a/studio/components/interfaces/Functions/TerminalInstructions.tsx b/studio/components/interfaces/Functions/TerminalInstructions.tsx index f8f30b523ab..16a2e2eea21 100644 --- a/studio/components/interfaces/Functions/TerminalInstructions.tsx +++ b/studio/components/interfaces/Functions/TerminalInstructions.tsx @@ -10,19 +10,16 @@ import { Commands } from './Functions.types' interface Props { closable?: boolean + removeBorder?: boolean } -const TerminalInstructions: FC = ({ closable = false }) => { +const TerminalInstructions: FC = ({ closable = false, removeBorder = false }) => { const router = useRouter() const { ref: projectRef } = useParams() - const [showInstructions, setShowInstructions] = useState(!closable) - const { data: tokens } = useAccessTokensQuery() + const { data: settings } = useProjectApiQuery({ projectRef }) - const { data: settings } = useProjectApiQuery({ - projectRef, - }) const apiService = settings?.autoApiService const anonKey = apiService?.service_api_keys.find((x) => x.name === 'anon key') ? apiService.defaultApiKey @@ -86,7 +83,9 @@ const TerminalInstructions: FC = ({ closable = false }) => { return (
@@ -107,7 +106,7 @@ const TerminalInstructions: FC = ({ closable = false }) => {
)}
-
+
diff --git a/studio/components/interfaces/Functions/index.ts b/studio/components/interfaces/Functions/index.ts index b096ff5e604..003d5feaca6 100644 --- a/studio/components/interfaces/Functions/index.ts +++ b/studio/components/interfaces/Functions/index.ts @@ -1,5 +1,6 @@ -import FunctionsListItem from './FunctionsListItem' +import EdgeFunctionDetails from './EdgeFunctionDetails' +import EdgeFunctionsListItem from './EdgeFunctionsListItem' import FunctionsEmptyState from './FunctionsEmptyState' import TerminalInstructions from './TerminalInstructions' -export { FunctionsListItem, FunctionsEmptyState, TerminalInstructions } +export { EdgeFunctionDetails, EdgeFunctionsListItem, FunctionsEmptyState, TerminalInstructions } diff --git a/studio/components/layouts/FunctionsLayout.tsx b/studio/components/layouts/FunctionsLayout.tsx index 511d401da95..0dc242cd836 100644 --- a/studio/components/layouts/FunctionsLayout.tsx +++ b/studio/components/layouts/FunctionsLayout.tsx @@ -1,14 +1,15 @@ import Link from 'next/link' import { useRouter } from 'next/router' -import { FC, useEffect, ReactNode } from 'react' +import { FC, useEffect, ReactNode, useState } from 'react' import { observer } from 'mobx-react-lite' -import { IconCode } from 'ui' +import { Button, IconExternalLink, IconCode, Modal, IconTerminal } from 'ui' -import { checkPermissions, useStore, withAuth } from 'hooks' +import { checkPermissions, useParams, useStore, withAuth } from 'hooks' import FunctionsNav from '../interfaces/Functions/FunctionsNav' import BaseLayout from 'components/layouts' import NoPermission from 'components/ui/NoPermission' import { PermissionAction } from '@supabase/shared-types/out/constants' +import { TerminalInstructions } from 'components/interfaces/Functions' interface Props { title?: string @@ -16,10 +17,10 @@ interface Props { } const FunctionsLayout: FC = ({ title, children }) => { - const { functions, ui } = useStore() const router = useRouter() - - const { id, ref } = router.query + const { id, ref } = useParams() + const { functions, ui } = useStore() + const [showTerminalInstructions, setShowTerminalInstructions] = useState(false) useEffect(() => { if (ui.selectedProjectRef) functions.load() @@ -87,7 +88,7 @@ const FunctionsLayout: FC = ({ title, children }) => { ].join(' ')} >
-
+
= ({ title, children }) => {
-
- -

- Edge Functions -

- -

Beta

+
+
+ +

+ Edge Functions +

+ +

Beta

+ {name && ( +
+ + + + + +
{name}
+
+ )} +
+
+ + + + + + +
- - {name && ( - <> - - - - - -
{name}
- - )}
{item && } @@ -140,6 +161,24 @@ const FunctionsLayout: FC = ({ title, children }) => {
)} + + setShowTerminalInstructions(false)} + header={

Deploying an edge function to your project

} + customFooter={ +
+ +
+ } + > +
+ +
+
) } diff --git a/studio/data/edge-functions/edge-functions-delete-mutation.ts b/studio/data/edge-functions/edge-functions-delete-mutation.ts new file mode 100644 index 00000000000..d642758cd9c --- /dev/null +++ b/studio/data/edge-functions/edge-functions-delete-mutation.ts @@ -0,0 +1,46 @@ +import { useMutation, UseMutationOptions, useQueryClient } from '@tanstack/react-query' +import { delete_ } from 'lib/common/fetch' +import { API_ADMIN_URL } from 'lib/constants' +import { edgeFunctionsKeys } from './keys' + +export type EdgeFunctionsDeleteVariables = { + projectRef: string + slug: string +} + +export async function deleteEdgeFunction({ projectRef, slug }: EdgeFunctionsDeleteVariables) { + if (!projectRef) throw new Error('projectRef is required') + + const response = await delete_(`${API_ADMIN_URL}/projects/${projectRef}/functions/${slug}`, {}) + if (response.error) { + throw response.error + } + + return response +} + +type EdgeFunctionsDeleteData = Awaited> + +export const useEdgeFunctionDeleteMutation = ({ + onSuccess, + ...options +}: Omit< + UseMutationOptions, + 'mutationFn' +> = {}) => { + const queryClient = useQueryClient() + + return useMutation( + (vars) => deleteEdgeFunction(vars), + { + async onSuccess(data, variables, context) { + const { projectRef } = variables + await queryClient.invalidateQueries(edgeFunctionsKeys.list(projectRef), { + refetchType: 'all', + }) + await onSuccess?.(data, variables, context) + }, + ...options, + } + ) +} diff --git a/studio/data/edge-functions/edge-functions-query.ts b/studio/data/edge-functions/edge-functions-query.ts new file mode 100644 index 00000000000..28bc3f8c649 --- /dev/null +++ b/studio/data/edge-functions/edge-functions-query.ts @@ -0,0 +1,57 @@ +import { useQuery, useQueryClient, UseQueryOptions } from '@tanstack/react-query' +import { get } from 'lib/common/fetch' +import { API_ADMIN_URL } from 'lib/constants' +import { useCallback } from 'react' +import { edgeFunctionsKeys } from './keys' + +export type EdgeFunctionsVariables = { projectRef?: string } + +export type EdgeFunctionsResponse = { + id: string + name: string + slug: string + status: string + version: number + created_at: number + updated_at: number + verify_jwt: boolean + import_map: boolean +} + +export async function getEdgeFunctions( + { projectRef }: EdgeFunctionsVariables, + signal?: AbortSignal +) { + if (!projectRef) throw new Error('projectRef is required') + + const response = await get(`${API_ADMIN_URL}/projects/${projectRef}/functions`, { + signal, + }) + if (response.error) throw response.error + return response as EdgeFunctionsResponse[] +} + +export type EdgeFunctionsData = Awaited> +export type EdgeFunctionsError = unknown + +export const useEdgeFunctionsQuery = ( + { projectRef }: EdgeFunctionsVariables, + { enabled = true, ...options }: UseQueryOptions = {} +) => + useQuery( + edgeFunctionsKeys.list(projectRef), + ({ signal }) => getEdgeFunctions({ projectRef }, signal), + { enabled: enabled && typeof projectRef !== 'undefined', ...options } + ) + +export const useEdgeFunctionsPrefetch = ({ projectRef }: EdgeFunctionsVariables) => { + const client = useQueryClient() + + return useCallback(() => { + if (projectRef) { + client.prefetchQuery(edgeFunctionsKeys.list(projectRef), ({ signal }) => + getEdgeFunctions({ projectRef }, signal) + ) + } + }, [projectRef]) +} diff --git a/studio/data/edge-functions/keys.ts b/studio/data/edge-functions/keys.ts new file mode 100644 index 00000000000..dacd8d4a8dd --- /dev/null +++ b/studio/data/edge-functions/keys.ts @@ -0,0 +1,3 @@ +export const edgeFunctionsKeys = { + list: (projectRef: string | undefined) => ['projects', projectRef, 'edge-functions'] as const, +} diff --git a/studio/pages/_app.tsx b/studio/pages/_app.tsx index d97675d69ef..f542d30ca75 100644 --- a/studio/pages/_app.tsx +++ b/studio/pages/_app.tsx @@ -16,6 +16,7 @@ import dayjs from 'dayjs' import customParseFormat from 'dayjs/plugin/customParseFormat' import timezone from 'dayjs/plugin/timezone' import utc from 'dayjs/plugin/utc' +import relativeTime from 'dayjs/plugin/relativeTime' // @ts-ignore import Prism from 'prism-react-renderer/prism' @@ -44,6 +45,7 @@ import useAutoAuthRedirect from 'hooks/misc/useAutoAuthRedirect' dayjs.extend(customParseFormat) dayjs.extend(utc) dayjs.extend(timezone) +dayjs.extend(relativeTime) dart(Prism) diff --git a/studio/pages/project/[ref]/functions/[id]/details.tsx b/studio/pages/project/[ref]/functions/[id]/details.tsx index 613833e4f31..debcd5ebe99 100644 --- a/studio/pages/project/[ref]/functions/[id]/details.tsx +++ b/studio/pages/project/[ref]/functions/[id]/details.tsx @@ -1,244 +1,21 @@ -import dayjs from 'dayjs' -import { useEffect, useState } from 'react' import { observer } from 'mobx-react-lite' -import { IconGlobe, IconTerminal } from 'ui' import { PermissionAction } from '@supabase/shared-types/out/constants' import { NextPageWithLayout } from 'types' -import { checkPermissions, useParams, useStore } from 'hooks' -import { useProjectApiQuery } from 'data/config/project-api-query' +import { checkPermissions, useParams } from 'hooks' import FunctionsLayout from 'components/layouts/FunctionsLayout' -import CommandRender from 'components/interfaces/Functions/CommandRender' import NoPermission from 'components/ui/NoPermission' +import { EdgeFunctionDetails } from 'components/interfaces/Functions' const PageLayout: NextPageWithLayout = () => { - const { ref: projectRef, id } = useParams() - - const { functions, ui } = useStore() - - const [selectedFunction, setSelectedFunction] = useState(null) - - useEffect(() => { - setSelectedFunction(functions.byId(id)) - }, [functions.isLoaded, ui.selectedProject]) - - const { data: settings } = useProjectApiQuery({ - projectRef, - }) - - // get the .co or .net TLD from the restUrl - const restUrl = ui.selectedProject?.restUrl - const restUrlTld = new URL(restUrl as string).hostname.split('.').pop() - - const managementCommands: any = [ - { - command: `supabase functions deploy ${selectedFunction?.slug}`, - description: 'This will overwrite the deployed function with your new function', - jsx: () => { - return ( - <> - supabase functions deploy{' '} - {selectedFunction?.slug} - - ) - }, - comment: 'Deploy a new version', - }, - { - command: `supabase functions delete ${selectedFunction?.slug}`, - description: 'This will remove the function and all the logs associated with it', - jsx: () => { - return ( - <> - supabase functions delete{' '} - {selectedFunction?.slug} - - ) - }, - comment: 'Delete the function', - }, - ] - - const secretCommands: any = [ - { - command: `supabase secrets list`, - description: 'This will list all the secrets for your project', - jsx: () => { - return ( - <> - supabase secrets list - - ) - }, - comment: 'View all secrets', - }, - { - command: `supabase secrets set NAME1=VALUE1 NAME2=VALUE2`, - description: 'This will set secrets for your project', - jsx: () => { - return ( - <> - supabase secrets set NAME1=VALUE1 NAME2=VALUE2 - - ) - }, - comment: 'Set secrets for your project', - }, - { - command: `supabase secrets unset NAME1 NAME2 `, - description: 'This will delete secrets for your project', - jsx: () => { - return ( - <> - supabase secrets unset NAME1 NAME2 - - ) - }, - comment: 'Unset secrets for your project', - }, - ] - - // Get the API service - const apiService = settings?.autoApiService - const anonKey = apiService?.service_api_keys.find((x) => x.name === 'anon key') - ? apiService.defaultApiKey - : undefined - - const endpoint = apiService?.app_config.endpoint ?? '' - const endpointSections = endpoint.split('.') - const functionsEndpoint = [ - ...endpointSections.slice(0, 1), - 'functions', - ...endpointSections.slice(1), - ].join('.') - - const invokeCommands: any = [ - { - command: `curl -L -X POST 'https://${projectRef}.functions.supabase.${restUrlTld}/${ - selectedFunction?.slug - }' -H 'Authorization: Bearer ${anonKey ?? '[YOUR ANON KEY]'}' --data '{"name":"Functions"}'`, - description: 'Invokes the hello function', - jsx: () => { - return ( - <> - curl -L -X POST 'https://{functionsEndpoint}/ - {selectedFunction?.slug}' -H 'Authorization: Bearer [YOUR ANON KEY]'{' '} - {`--data '{"name":"Functions"}'`} - - ) - }, - comment: 'Invoke your function', - }, - ] + const { id } = useParams() const canReadFunction = checkPermissions(PermissionAction.FUNCTIONS_READ, id as string) if (!canReadFunction) { return } - return ( -
-
-
-
-
- Function Name -
{selectedFunction?.name}
-
- -
- Status -
-
-
- {selectedFunction?.status} -
- - - - -
-
-
-
-
-
- -
- Endpoint URL -
- {`https://${projectRef}.functions.supabase.co/${selectedFunction?.slug}`} -
-
- -
- Created At -
- {selectedFunction?.created_at && - dayjs(selectedFunction.created_at).format('dddd, MMMM D, YYYY h:mm A')} -
-
- -
- Updated At -
- {selectedFunction?.updated_at && - dayjs(selectedFunction.updated_at).format('dddd, MMMM D, YYYY h:mm A')} -
-
- -
- Version -
v{selectedFunction?.version}
-
- -
- Regions -
-
- - Earth -
- All functions are deployed globally -
-
-
-
- -
-
-
-
- -
-

Command line access

-
- -
Deployment management
- -
Invoke
- -
Secrets management
- -
-
-
- ) + return } PageLayout.getLayout = (page) => {page} diff --git a/studio/pages/project/[ref]/functions/index.tsx b/studio/pages/project/[ref]/functions/index.tsx index 0532afd7a8f..e78b41e0996 100644 --- a/studio/pages/project/[ref]/functions/index.tsx +++ b/studio/pages/project/[ref]/functions/index.tsx @@ -1,59 +1,53 @@ import { observer } from 'mobx-react-lite' +import { useParams } from 'hooks' import { NextPageWithLayout } from 'types' -import { useStore } from 'hooks' -import FunctionsLayout from 'components/layouts/FunctionsLayout' - import Table from 'components/to-be-cleaned/Table' -import { Function } from 'components/interfaces/Functions/Functions.types' -import { - FunctionsListItem, - FunctionsEmptyState, - TerminalInstructions, -} from 'components/interfaces/Functions' - -const EdgeFunctionsList = ({ functions }: { functions: Function[] }) => { - return ( - <> -
-
- {`${functions.length} function${ - functions.length > 1 ? 's' : '' - } deployed`} -
-
- - Name - URL - Created - Last updated - Version - Status - - } - body={ - <> - {functions.length > 0 && - functions.map((item: any) => )} - - } - /> - - - - ) -} +import FunctionsLayout from 'components/layouts/FunctionsLayout' +import { EdgeFunctionsListItem, FunctionsEmptyState } from 'components/interfaces/Functions' +import ShimmeringLoader from 'components/ui/ShimmeringLoader' +import { useEdgeFunctionsQuery } from 'data/edge-functions/edge-functions-query' const PageLayout: NextPageWithLayout = () => { - const { functions } = useStore() - const hasFunctions = functions.list().length > 0 + const { ref } = useParams() + const { isLoading, data: functions } = useEdgeFunctionsQuery({ projectRef: ref }) + + const hasFunctions = (functions ?? []).length > 0 + + if (isLoading || functions === undefined) { + return ( +
+ + + +
+ ) + } return hasFunctions ? ( -
- - +
+
+ {`${functions.length} function${ + functions.length > 1 ? 's' : '' + } deployed`} +
+
+ Name + URL + Created + Last updated + Deployments + + } + body={ + <> + {functions.length > 0 && + functions.map((item: any) => )} + + } + /> ) : (