mirror of
https://github.com/supabase/supabase.git
synced 2026-10-18 15:49:09 +03:00
Merge pull request #12542 from supabase/chore/update-edge-functions-ui
Chore/update edge functions UI
This commit is contained in:
13 files changed
+675
-386
No files matched your search
@@ -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<Props> = () => {
|
||||
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<any>(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 (
|
||||
<>
|
||||
<span className="text-brand-1100">supabase</span> 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 (
|
||||
<>
|
||||
<span className="text-brand-1100">supabase</span> 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 (
|
||||
<>
|
||||
<span className="text-brand-1100">supabase</span> secrets list
|
||||
</>
|
||||
)
|
||||
},
|
||||
comment: 'View all secrets',
|
||||
},
|
||||
{
|
||||
command: `supabase secrets set NAME1=VALUE1 NAME2=VALUE2`,
|
||||
description: 'This will set secrets for your project',
|
||||
jsx: () => {
|
||||
return (
|
||||
<>
|
||||
<span className="text-brand-1100">supabase</span> 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 (
|
||||
<>
|
||||
<span className="text-brand-1100">supabase</span> 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 (
|
||||
<>
|
||||
<span className="text-brand-1100">curl</span> -L -X POST 'https://{functionsEndpoint}/
|
||||
{selectedFunction?.slug}' -H 'Authorization: Bearer [YOUR ANON KEY]'{' '}
|
||||
{`--data '{"name":"Functions"}'`}
|
||||
</>
|
||||
)
|
||||
},
|
||||
comment: 'Invoke your function',
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="space-y-8 pb-16">
|
||||
<div className="space-y-6 rounded border bg-scale-100 px-10 py-8 drop-shadow-sm dark:bg-scale-300">
|
||||
<div className="flex items-center">
|
||||
<p className="text-sm text-scale-1000 w-[130px]">Function Name</p>
|
||||
<p className="text-sm text-scale-1200">{selectedFunction?.name}</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center">
|
||||
<p className="text-sm text-scale-1000 w-[130px]">Endpoint URL</p>
|
||||
<p className="text-sm text-scale-1200">{functionUrl}</p>
|
||||
<button
|
||||
type="button"
|
||||
className="text-scale-900 hover:text-scale-1200 transition ml-2"
|
||||
onClick={(event: any) => {
|
||||
function onCopy(value: any) {
|
||||
setIsCopied(true)
|
||||
navigator.clipboard.writeText(value).then()
|
||||
setTimeout(function () {
|
||||
setIsCopied(false)
|
||||
}, 3000)
|
||||
}
|
||||
event.stopPropagation()
|
||||
onCopy(functionUrl)
|
||||
}}
|
||||
>
|
||||
{isCopied ? (
|
||||
<div className="text-brand-900">
|
||||
<IconCheck size={14} strokeWidth={3} />
|
||||
</div>
|
||||
) : (
|
||||
<div className="relative">
|
||||
<div className="block">
|
||||
<IconClipboard size={14} strokeWidth={1.5} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center">
|
||||
<p className="text-sm text-scale-1000 w-[130px]">Created At</p>
|
||||
<p className="text-sm text-scale-1200">
|
||||
{selectedFunction?.created_at &&
|
||||
dayjs(selectedFunction.created_at).format('dddd, MMMM D, YYYY h:mm A')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center">
|
||||
<p className="text-sm text-scale-1000 w-[130px]">Last Updated At</p>
|
||||
<p className="text-sm text-scale-1200">
|
||||
{selectedFunction?.updated_at &&
|
||||
dayjs(selectedFunction.updated_at).format('dddd, MMMM D, YYYY h:mm A')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center">
|
||||
<span className="text-sm text-scale-1000 w-[130px]">Deployments</span>
|
||||
<div className="text-sm text-scale-1200">{selectedFunction?.version}</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-start">
|
||||
<span className="text-sm text-scale-1000 w-[130px]">Regions</span>
|
||||
<div className="col-span-2 flex flex-col gap-1">
|
||||
<div className="flex items-center gap-2 text-sm text-scale-1200">
|
||||
<IconGlobe size={16} strokeWidth={1.5} />
|
||||
<span>Earth</span>
|
||||
</div>
|
||||
<span className="text-sm text-scale-1000">All functions are deployed globally</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="space-y-6 rounded border bg-scale-100 px-8 py-6 drop-shadow-sm dark:bg-scale-300 transition-all overflow-hidden"
|
||||
style={{ maxHeight: showInstructions ? 800 : 80 }}
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="flex h-8 w-8 items-center justify-center rounded border bg-scale-1200 p-2 text-scale-100 dark:bg-scale-100 dark:text-scale-1200">
|
||||
<IconTerminal strokeWidth={2} />
|
||||
</div>
|
||||
<h4>Command line access</h4>
|
||||
</div>
|
||||
<div className="cursor-pointer" onClick={() => setShowInstructions(!showInstructions)}>
|
||||
{showInstructions ? (
|
||||
<IconMinimize2 size={14} strokeWidth={1.5} />
|
||||
) : (
|
||||
<IconMaximize2 size={14} strokeWidth={1.5} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h5 className="text-base">Deployment management</h5>
|
||||
<CommandRender commands={managementCommands} />
|
||||
<h5 className="text-base">Invoke </h5>
|
||||
<CommandRender commands={invokeCommands} />
|
||||
<h5 className="text-base">Secrets management</h5>
|
||||
<CommandRender commands={secretCommands} />
|
||||
</div>
|
||||
|
||||
<Panel title={<p>Delete Edge Function</p>}>
|
||||
<Panel.Content>
|
||||
<Alert
|
||||
withIcon
|
||||
variant="danger"
|
||||
title="Once your function is deleted, it can no longer be restored"
|
||||
>
|
||||
<p className="mb-3">
|
||||
Make sure you have made a backup if you want to restore your edge function
|
||||
</p>
|
||||
<Tooltip.Root delayDuration={0}>
|
||||
<Tooltip.Trigger>
|
||||
<Button
|
||||
type="danger"
|
||||
disabled={!canDeleteEdgeFunction}
|
||||
loading={selectedFunction?.id === undefined}
|
||||
onClick={() => setShowDeleteModal(true)}
|
||||
>
|
||||
Delete edge function
|
||||
</Button>
|
||||
</Tooltip.Trigger>
|
||||
{!canDeleteEdgeFunction && (
|
||||
<Tooltip.Content side="bottom">
|
||||
<Tooltip.Arrow className="radix-tooltip-arrow" />
|
||||
<div
|
||||
className={[
|
||||
'rounded bg-scale-100 py-1 px-2 leading-none shadow',
|
||||
'border border-scale-200',
|
||||
].join(' ')}
|
||||
>
|
||||
<span className="text-xs text-scale-1200">
|
||||
You need additional permissions to delete an edge function
|
||||
</span>
|
||||
</div>
|
||||
</Tooltip.Content>
|
||||
)}
|
||||
</Tooltip.Root>
|
||||
</Alert>
|
||||
</Panel.Content>
|
||||
</Panel>
|
||||
</div>
|
||||
|
||||
<Modal
|
||||
size="small"
|
||||
alignFooter="right"
|
||||
header={<h3>Confirm to delete {selectedFunction?.name}</h3>}
|
||||
visible={showDeleteModal}
|
||||
loading={isDeleting}
|
||||
onCancel={() => setShowDeleteModal(false)}
|
||||
onConfirm={onConfirmDelete}
|
||||
>
|
||||
<div className="py-6">
|
||||
<Modal.Content>
|
||||
<Alert withIcon variant="warning" title="This action cannot be undone">
|
||||
Ensure that you have made a backup if you want to restore your edge function
|
||||
</Alert>
|
||||
</Modal.Content>
|
||||
</div>
|
||||
</Modal>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default EdgeFunctionDetails
|
||||
@@ -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<Props> = ({ 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 (
|
||||
<Table.tr
|
||||
key={item.id}
|
||||
onClick={() => {
|
||||
router.push(`/project/${ref}/functions/${item.id}/details`)
|
||||
}}
|
||||
>
|
||||
<Table.td className="">
|
||||
<div className="flex items-center gap-2">
|
||||
<p className="text-sm text-scale-1200">{item.name}</p>
|
||||
</div>
|
||||
</Table.td>
|
||||
<Table.td className="">
|
||||
<div className="text-xs text-scale-1100 flex gap-2 items-center truncate">
|
||||
<p className="font-mono truncate hidden md:inline">{functionUrl}</p>
|
||||
<button
|
||||
type="button"
|
||||
className="text-scale-900 hover:text-scale-1200 transition"
|
||||
onClick={(event: any) => {
|
||||
function onCopy(value: any) {
|
||||
setIsCopied(true)
|
||||
navigator.clipboard.writeText(value).then()
|
||||
setTimeout(function () {
|
||||
setIsCopied(false)
|
||||
}, 3000)
|
||||
}
|
||||
event.stopPropagation()
|
||||
onCopy(functionUrl)
|
||||
}}
|
||||
>
|
||||
{isCopied ? (
|
||||
<div className="text-brand-900">
|
||||
<IconCheck size={14} strokeWidth={3} />
|
||||
</div>
|
||||
) : (
|
||||
<div className="relative">
|
||||
<div className="block">
|
||||
<IconClipboard size={14} strokeWidth={1.5} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</Table.td>
|
||||
<Table.td className="hidden 2xl:table-cell">
|
||||
<p className="text-scale-1100">{dayjs(item.created_at).format('DD MMM, YYYY HH:mm')}</p>
|
||||
</Table.td>
|
||||
<Table.td className="lg:table-cell">
|
||||
<Tooltip.Root delayDuration={0}>
|
||||
<Tooltip.Trigger>
|
||||
<div className="flex items-center space-x-2">
|
||||
<p className="text-sm text-scale-1000">{dayjs(item.updated_at).fromNow()}</p>
|
||||
</div>
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content side="bottom">
|
||||
<Tooltip.Arrow className="radix-tooltip-arrow" />
|
||||
<div
|
||||
className={[
|
||||
'rounded bg-scale-100 py-1 px-2 leading-none shadow',
|
||||
'border border-scale-200',
|
||||
].join(' ')}
|
||||
>
|
||||
<span className="text-xs text-scale-1200">
|
||||
Last updated on {dayjs(item.updated_at).format('DD MMM, YYYY HH:mm')}
|
||||
</span>
|
||||
</div>
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
</Table.td>
|
||||
<Table.td className="lg:table-cell">
|
||||
<p className="text-scale-1100">{item.version}</p>
|
||||
</Table.td>
|
||||
</Table.tr>
|
||||
)
|
||||
}
|
||||
|
||||
export default observer(EdgeFunctionsListItem)
|
||||
@@ -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<Props> = ({ 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 (
|
||||
<Table.tr
|
||||
key={item.id}
|
||||
onClick={() => {
|
||||
router.push(`/project/${ref}/functions/${item.id}`)
|
||||
}}
|
||||
>
|
||||
<Table.td className="">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm text-scale-1200">{item.name}</span>
|
||||
</div>
|
||||
</Table.td>
|
||||
<Table.td className="">
|
||||
<div className="text-xs text-scale-900 flex gap-2 items-center truncate">
|
||||
<span className="font-mono truncate hidden md:inline">{`https://${ref}.functions.supabase.${restUrlTld}/${item.slug}`}</span>
|
||||
<span className="font-mono truncate md:hidden">{`/${item.name}`}</span>
|
||||
</div>
|
||||
</Table.td>
|
||||
<Table.td className="hidden 2xl:table-cell">
|
||||
<span className="text-scale-1100">
|
||||
{dayjs(item.created_at).format('DD MMM, YYYY HH:mm')}
|
||||
</span>
|
||||
</Table.td>
|
||||
<Table.td className="hidden lg:table-cell">
|
||||
<span className="text-scale-1100">
|
||||
{dayjs(item.updated_at).format('DD MMM, YYYY HH:mm')}
|
||||
</span>
|
||||
</Table.td>
|
||||
<Table.td className="hidden 2xl:table-cell">
|
||||
<span className="text-scale-1100">v{item.version}</span>
|
||||
</Table.td>
|
||||
<Table.td className="2xl:table-cell text-right">
|
||||
<span className="text-scale-1100">{item.status}</span>
|
||||
</Table.td>
|
||||
</Table.tr>
|
||||
)
|
||||
}
|
||||
|
||||
export default observer(FunctionsListItem)
|
||||
@@ -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}`)
|
||||
}
|
||||
>
|
||||
<Tabs.Panel id="metrics" label="Metrics" />
|
||||
<Tabs.Panel id="details" label="Details" />
|
||||
{/* <Tabs.Panel id="triggers" label="Triggers" /> */}
|
||||
<Tabs.Panel id="metrics" label="Metrics" />
|
||||
<Tabs.Panel id="invocations" label="Invocations" />
|
||||
<Tabs.Panel id="logs" label="Logs" />
|
||||
</Tabs>
|
||||
|
||||
@@ -10,19 +10,16 @@ import { Commands } from './Functions.types'
|
||||
|
||||
interface Props {
|
||||
closable?: boolean
|
||||
removeBorder?: boolean
|
||||
}
|
||||
|
||||
const TerminalInstructions: FC<Props> = ({ closable = false }) => {
|
||||
const TerminalInstructions: FC<Props> = ({ 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<Props> = ({ closable = false }) => {
|
||||
|
||||
return (
|
||||
<div
|
||||
className="col-span-7 overflow-hidden transition-all border rounded shadow bg-scale-100 dark:bg-scale-300"
|
||||
className={`col-span-7 overflow-hidden transition-all rounded bg-scale-100 dark:bg-scale-300 ${
|
||||
removeBorder ? '' : 'border shadow'
|
||||
}`}
|
||||
style={{ maxHeight: showInstructions ? 500 : 80 }}
|
||||
>
|
||||
<div className="px-8 py-6 space-y-6">
|
||||
@@ -107,7 +106,7 @@ const TerminalInstructions: FC<Props> = ({ closable = false }) => {
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<CommandRender commands={commands} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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 }
|
||||
@@ -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<Props> = ({ 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<Props> = ({ title, children }) => {
|
||||
].join(' ')}
|
||||
>
|
||||
<div className="item-center flex flex-col justify-between gap-y-4 xl:flex-row">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex items-center gap-3 w-full">
|
||||
<div
|
||||
className={[
|
||||
'h-6 w-6 rounded border border-brand-600 bg-brand-300',
|
||||
@@ -97,35 +98,55 @@ const FunctionsLayout: FC<Props> = ({ title, children }) => {
|
||||
<IconCode size={14} strokeWidth={3} />
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-4">
|
||||
<Link href={`/project/${ref}/functions`}>
|
||||
<h1 className="cursor-pointer text-2xl text-scale-1200 transition-colors hover:text-scale-1100">
|
||||
Edge Functions
|
||||
</h1>
|
||||
</Link>
|
||||
<p className="mt-1 text-scale-1000">Beta</p>
|
||||
<div className="flex items-center justify-between w-full">
|
||||
<div className="flex items-center space-x-4">
|
||||
<Link href={`/project/${ref}/functions`}>
|
||||
<h1 className="cursor-pointer text-2xl text-scale-1200 transition-colors hover:text-scale-1100">
|
||||
Edge Functions
|
||||
</h1>
|
||||
</Link>
|
||||
<p className="mt-1 text-scale-1000">Beta</p>
|
||||
{name && (
|
||||
<div className="mt-1.5 flex items-center space-x-4">
|
||||
<span className="text-scale-1000">
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
width="16"
|
||||
height="16"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
fill="none"
|
||||
shapeRendering="geometricPrecision"
|
||||
>
|
||||
<path d="M16 3.549L7.12 20.600"></path>
|
||||
</svg>
|
||||
</span>
|
||||
<h5 className="text-lg text-scale-1200">{name}</h5>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Button
|
||||
type="default"
|
||||
icon={<IconTerminal size={14} strokeWidth={1.5} />}
|
||||
onClick={() => setShowTerminalInstructions(true)}
|
||||
>
|
||||
Terminal Instructions
|
||||
</Button>
|
||||
<Link href="https://supabase.com/docs/guides/functions">
|
||||
<a target="_link">
|
||||
<Button
|
||||
type="default"
|
||||
icon={<IconExternalLink size={14} strokeWidth={1.5} />}
|
||||
>
|
||||
Documentation
|
||||
</Button>
|
||||
</a>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{name && (
|
||||
<>
|
||||
<span className="text-scale-800 dark:text-scale-700">
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
width="16"
|
||||
height="16"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
fill="none"
|
||||
shapeRendering="geometricPrecision"
|
||||
>
|
||||
<path d="M16 3.549L7.12 20.600"></path>
|
||||
</svg>
|
||||
</span>
|
||||
<h5 className="text-2xl text-scale-1200">{name}</h5>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{item && <FunctionsNav item={item} />}
|
||||
@@ -140,6 +161,24 @@ const FunctionsLayout: FC<Props> = ({ title, children }) => {
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Modal
|
||||
size="xlarge"
|
||||
visible={showTerminalInstructions}
|
||||
onCancel={() => setShowTerminalInstructions(false)}
|
||||
header={<h3>Deploying an edge function to your project</h3>}
|
||||
customFooter={
|
||||
<div className="w-full flex items-center justify-end">
|
||||
<Button type="primary" size="tiny" onClick={() => setShowTerminalInstructions(false)}>
|
||||
Confirm
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div className="py-4">
|
||||
<TerminalInstructions removeBorder />
|
||||
</div>
|
||||
</Modal>
|
||||
</BaseLayout>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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<ReturnType<typeof deleteEdgeFunction>>
|
||||
|
||||
export const useEdgeFunctionDeleteMutation = ({
|
||||
onSuccess,
|
||||
...options
|
||||
}: Omit<
|
||||
UseMutationOptions<EdgeFunctionsDeleteData, unknown, EdgeFunctionsDeleteVariables>,
|
||||
'mutationFn'
|
||||
> = {}) => {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
return useMutation<EdgeFunctionsDeleteData, unknown, EdgeFunctionsDeleteVariables>(
|
||||
(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,
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -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<ReturnType<typeof getEdgeFunctions>>
|
||||
export type EdgeFunctionsError = unknown
|
||||
|
||||
export const useEdgeFunctionsQuery = <TData = EdgeFunctionsData>(
|
||||
{ projectRef }: EdgeFunctionsVariables,
|
||||
{ enabled = true, ...options }: UseQueryOptions<EdgeFunctionsData, EdgeFunctionsError, TData> = {}
|
||||
) =>
|
||||
useQuery<EdgeFunctionsData, EdgeFunctionsError, TData>(
|
||||
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])
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export const edgeFunctionsKeys = {
|
||||
list: (projectRef: string | undefined) => ['projects', projectRef, 'edge-functions'] as const,
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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<any>(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 (
|
||||
<>
|
||||
<span className="text-brand-1100">supabase</span> 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 (
|
||||
<>
|
||||
<span className="text-brand-1100">supabase</span> 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 (
|
||||
<>
|
||||
<span className="text-brand-1100">supabase</span> secrets list
|
||||
</>
|
||||
)
|
||||
},
|
||||
comment: 'View all secrets',
|
||||
},
|
||||
{
|
||||
command: `supabase secrets set NAME1=VALUE1 NAME2=VALUE2`,
|
||||
description: 'This will set secrets for your project',
|
||||
jsx: () => {
|
||||
return (
|
||||
<>
|
||||
<span className="text-brand-1100">supabase</span> 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 (
|
||||
<>
|
||||
<span className="text-brand-1100">supabase</span> 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 (
|
||||
<>
|
||||
<span className="text-brand-1100">curl</span> -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 <NoPermission isFullPage resourceText="access this edge function's details" />
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="grid gap-y-4 lg:grid-cols-2 lg:gap-x-8">
|
||||
<div>
|
||||
<div
|
||||
className="
|
||||
space-y-6 rounded border bg-scale-100 px-10 py-8 drop-shadow-sm
|
||||
dark:bg-scale-300
|
||||
"
|
||||
>
|
||||
<div className="w-full space-y-4">
|
||||
<div className="grid grid-cols-3">
|
||||
<span className="mb-1 block text-sm text-scale-1000">Function Name</span>
|
||||
<div className="text-sm text-scale-1200">{selectedFunction?.name}</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-3">
|
||||
<span className="mb-1 block text-sm text-scale-1000">Status</span>
|
||||
<div className="col-span-2 flex flex-col gap-2">
|
||||
<div className="flex">
|
||||
<div
|
||||
className="
|
||||
lowercasefirst-letter flex flex-row items-center gap-3 rounded-full bg-brand-300
|
||||
px-3 py-0.5 text-base lowercase text-brand-900 dark:bg-brand-100
|
||||
"
|
||||
>
|
||||
{selectedFunction?.status}
|
||||
<div className="relative h-2 w-2">
|
||||
<span className="flex h-2 w-2">
|
||||
<span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-brand-800 opacity-75"></span>
|
||||
<span className="relative inline-flex h-2 w-2 rounded-full bg-brand-900"></span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-3">
|
||||
<span className="cols-span-1 mb-1 block text-sm text-scale-1000">Endpoint URL</span>
|
||||
<div className="col-span-2">
|
||||
<span className="w-full break-words text-sm text-scale-1200">{`https://${projectRef}.functions.supabase.co/${selectedFunction?.slug}`}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-3">
|
||||
<span className="mb-1 block text-sm text-scale-1000">Created At</span>
|
||||
<div className="col-span-2 text-sm text-scale-1200">
|
||||
{selectedFunction?.created_at &&
|
||||
dayjs(selectedFunction.created_at).format('dddd, MMMM D, YYYY h:mm A')}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-3">
|
||||
<span className="mb-1 block text-sm text-scale-1000">Updated At</span>
|
||||
<div className="col-span-2 text-sm text-scale-1200">
|
||||
{selectedFunction?.updated_at &&
|
||||
dayjs(selectedFunction.updated_at).format('dddd, MMMM D, YYYY h:mm A')}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-3">
|
||||
<span className="mb-1 block text-sm text-scale-1000">Version</span>
|
||||
<div className="col-span-2 text-sm text-scale-1200">v{selectedFunction?.version}</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-3">
|
||||
<span className="mb-1 block text-sm text-scale-1000">Regions</span>
|
||||
<div className="col-span-2 flex flex-col gap-1">
|
||||
<div className="flex items-center gap-2 text-sm text-scale-1200">
|
||||
<IconGlobe />
|
||||
<span>Earth</span>
|
||||
</div>
|
||||
<span className="text-sm text-scale-1000">All functions are deployed globally</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div
|
||||
className="space-y-6 rounded
|
||||
border bg-scale-100 px-10 py-8
|
||||
drop-shadow-sm
|
||||
dark:bg-scale-300"
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex h-8 w-8 items-center justify-center rounded border bg-scale-1200 p-2 text-scale-100 dark:bg-scale-100 dark:text-scale-1200">
|
||||
<IconTerminal strokeWidth={2} />
|
||||
</div>
|
||||
<h4>Command line access</h4>
|
||||
</div>
|
||||
|
||||
<h5 className="text-base">Deployment management</h5>
|
||||
<CommandRender commands={managementCommands} />
|
||||
<h5 className="text-base">Invoke </h5>
|
||||
<CommandRender commands={invokeCommands} />
|
||||
<h5 className="text-base">Secrets management</h5>
|
||||
<CommandRender commands={secretCommands} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
return <EdgeFunctionDetails />
|
||||
}
|
||||
|
||||
PageLayout.getLayout = (page) => <FunctionsLayout>{page}</FunctionsLayout>
|
||||
|
||||
@@ -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 (
|
||||
<>
|
||||
<div className="flex flex-col gap-3 py-6">
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-sm text-scale-900">{`${functions.length} function${
|
||||
functions.length > 1 ? 's' : ''
|
||||
} deployed`}</span>
|
||||
</div>
|
||||
<div>
|
||||
<Table
|
||||
head={
|
||||
<>
|
||||
<Table.th>Name</Table.th>
|
||||
<Table.th>URL</Table.th>
|
||||
<Table.th className="hidden lg:table-cell">Created</Table.th>
|
||||
<Table.th className="hidden 2xl:table-cell">Last updated</Table.th>
|
||||
<Table.th className="hidden 2xl:table-cell">Version</Table.th>
|
||||
<Table.th className="text-right">Status</Table.th>
|
||||
</>
|
||||
}
|
||||
body={
|
||||
<>
|
||||
{functions.length > 0 &&
|
||||
functions.map((item: any) => <FunctionsListItem key={item.id} function={item} />)}
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
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 (
|
||||
<div className="space-y-2 w-full py-6">
|
||||
<ShimmeringLoader />
|
||||
<ShimmeringLoader className="w-3/4" />
|
||||
<ShimmeringLoader className="w-1/2" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return hasFunctions ? (
|
||||
<div className="py-6">
|
||||
<TerminalInstructions closable />
|
||||
<EdgeFunctionsList functions={functions.list()} />
|
||||
<div className="py-6 space-y-4">
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-sm text-scale-900">{`${functions.length} function${
|
||||
functions.length > 1 ? 's' : ''
|
||||
} deployed`}</span>
|
||||
</div>
|
||||
<Table
|
||||
head={
|
||||
<>
|
||||
<Table.th>Name</Table.th>
|
||||
<Table.th>URL</Table.th>
|
||||
<Table.th className="hidden 2xl:table-cell">Created</Table.th>
|
||||
<Table.th className="lg:table-cell">Last updated</Table.th>
|
||||
<Table.th className="lg:table-cell">Deployments</Table.th>
|
||||
</>
|
||||
}
|
||||
body={
|
||||
<>
|
||||
{functions.length > 0 &&
|
||||
functions.map((item: any) => <EdgeFunctionsListItem key={item.id} function={item} />)}
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<FunctionsEmptyState />
|
||||
|
||||
Reference in new issue
Block a user