diff --git a/apps/studio/components/interfaces/Functions/EdgeFunctionDetails/EdgeFunctionDetails.tsx b/apps/studio/components/interfaces/Functions/EdgeFunctionDetails/EdgeFunctionDetails.tsx index a88a5a16ebf..fdbab04ad27 100644 --- a/apps/studio/components/interfaces/Functions/EdgeFunctionDetails/EdgeFunctionDetails.tsx +++ b/apps/studio/components/interfaces/Functions/EdgeFunctionDetails/EdgeFunctionDetails.tsx @@ -52,7 +52,7 @@ const FormSchema = z.object({ verify_jwt: z.boolean(), }) -const EdgeFunctionDetails = () => { +export const EdgeFunctionDetails = () => { const router = useRouter() const { ref: projectRef, functionSlug } = useParams() const [showDeleteModal, setShowDeleteModal] = useState(false) @@ -365,5 +365,3 @@ const EdgeFunctionDetails = () => { ) } - -export default EdgeFunctionDetails diff --git a/apps/studio/components/interfaces/Functions/EdgeFunctionsListItem.tsx b/apps/studio/components/interfaces/Functions/EdgeFunctionsListItem.tsx index 3df7336638d..a0951552654 100644 --- a/apps/studio/components/interfaces/Functions/EdgeFunctionsListItem.tsx +++ b/apps/studio/components/interfaces/Functions/EdgeFunctionsListItem.tsx @@ -14,7 +14,7 @@ interface EdgeFunctionsListItemProps { function: EdgeFunctionsResponse } -const EdgeFunctionsListItem = ({ function: item }: EdgeFunctionsListItemProps) => { +export const EdgeFunctionsListItem = ({ function: item }: EdgeFunctionsListItemProps) => { const router = useRouter() const { ref } = useParams() const { project } = useProjectContext() @@ -46,7 +46,9 @@ const EdgeFunctionsListItem = ({ function: item }: EdgeFunctionsListItemProps) =
-

{endpoint}

+

+ {endpoint} +

+ + + + + + + +
+ + {/* AI Assistant Option */} +
+
+ +

AI Assistant

+
+

+ Let our AI assistant help you create functions. Perfect for kickstarting a function. +

+ +
+ + {/* Editor Option */} + {edgeFunctionCreate && ( +
+
+ +

Via Editor

+
+

+ Create and edit functions directly in the browser. Download to local at any time. +

+ +
+ )} + + + {edgeFunctionCreate && ( + <> + + Start with a template + + + {EDGE_FUNCTION_TEMPLATES.map((template) => ( + } + onClick={() => {}} + > + +

{template.name}

+

{template.description}

+ +
+ ))} +
+ + )} ) } - -export default FunctionsEmptyState diff --git a/apps/studio/components/interfaces/Functions/TerminalInstructions.tsx b/apps/studio/components/interfaces/Functions/TerminalInstructions.tsx index f74e36bbbb8..fa2b18867df 100644 --- a/apps/studio/components/interfaces/Functions/TerminalInstructions.tsx +++ b/apps/studio/components/interfaces/Functions/TerminalInstructions.tsx @@ -21,7 +21,7 @@ interface TerminalInstructionsProps extends ComponentPropsWithoutRef, TerminalInstructionsProps >(({ closable = false, removeBorder = false, ...props }, ref) => { @@ -158,5 +158,3 @@ const TerminalInstructions = forwardRef< }) TerminalInstructions.displayName = 'TerminalInstructions' - -export default TerminalInstructions diff --git a/apps/studio/components/interfaces/Functions/index.ts b/apps/studio/components/interfaces/Functions/index.ts deleted file mode 100644 index e90c28a844a..00000000000 --- a/apps/studio/components/interfaces/Functions/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export { default as EdgeFunctionDetails } from './EdgeFunctionDetails/EdgeFunctionDetails' -export { default as EdgeFunctionsListItem } from './EdgeFunctionsListItem' -export { default as FunctionsEmptyState } from './FunctionsEmptyState' -export { default as TerminalInstructions } from './TerminalInstructions' diff --git a/apps/studio/components/layouts/EdgeFunctionsLayout/EdgeFunctionsLayout.tsx b/apps/studio/components/layouts/EdgeFunctionsLayout/EdgeFunctionsLayout.tsx index 09918364b88..5fdd6772908 100644 --- a/apps/studio/components/layouts/EdgeFunctionsLayout/EdgeFunctionsLayout.tsx +++ b/apps/studio/components/layouts/EdgeFunctionsLayout/EdgeFunctionsLayout.tsx @@ -13,9 +13,7 @@ const EdgeFunctionsProductMenu = () => { const menuItems = [ { - name: 'Edge Functions', - key: 'functions', - url: `/project/${projectRef}/functions`, + title: 'Manage', items: [ { name: 'Functions', diff --git a/apps/studio/components/layouts/PageLayout/PageHeader.tsx b/apps/studio/components/layouts/PageLayout/PageHeader.tsx index 2ca4785d98b..5c0c55bbc26 100644 --- a/apps/studio/components/layouts/PageLayout/PageHeader.tsx +++ b/apps/studio/components/layouts/PageLayout/PageHeader.tsx @@ -2,6 +2,7 @@ import { useParams } from 'common' import { ChevronLeft } from 'lucide-react' import { Fragment, ReactNode } from 'react' +import Link from 'next/link' import { cn } from 'ui' import { Breadcrumb, @@ -59,14 +60,13 @@ export const PageHeader = ({ {item.element ? ( item.element ) : item.href ? ( - - {breadcrumbs.length === 1 && !isCompact && ( - - )} - {item.label} + + + {breadcrumbs.length === 1 && !isCompact && ( + + )} + {item.label} + ) : ( diff --git a/apps/studio/components/ui/FileExplorerAndEditor/FileExplorerAndEditor.tsx b/apps/studio/components/ui/FileExplorerAndEditor/FileExplorerAndEditor.tsx index 2c427ef5ab7..1aa0b54c8ec 100644 --- a/apps/studio/components/ui/FileExplorerAndEditor/FileExplorerAndEditor.tsx +++ b/apps/studio/components/ui/FileExplorerAndEditor/FileExplorerAndEditor.tsx @@ -1,16 +1,17 @@ -import React from 'react' import { Edit, File, Plus, Trash } from 'lucide-react' -import { Button } from 'ui' +import { useEffect, useState } from 'react' + import AIEditor from 'components/ui/AIEditor' import { - TreeView, - TreeViewItem, - flattenTree, + Button, ContextMenu_Shadcn_, - ContextMenuTrigger_Shadcn_, ContextMenuContent_Shadcn_, ContextMenuItem_Shadcn_, ContextMenuSeparator_Shadcn_, + ContextMenuTrigger_Shadcn_, + flattenTree, + TreeView, + TreeViewItem, } from 'ui' interface FileData { @@ -61,6 +62,18 @@ const FileExplorerAndEditor = ({ }: FileExplorerAndEditorProps) => { const selectedFile = files.find((f) => f.selected) ?? files[0] + const [treeData, setTreeData] = useState({ + name: '', + children: files.map((file) => ({ + id: file.id.toString(), + name: file.name, + metadata: { + isEditing: false, + originalId: file.id, + }, + })), + }) + const handleChange = (value: string) => { const updatedFiles = files.map((file) => file.id === selectedFile.id ? { ...file, content: value } : file @@ -132,20 +145,8 @@ const FileExplorerAndEditor = ({ setTreeData(updatedTreeData) } - const [treeData, setTreeData] = React.useState({ - name: '', - children: files.map((file) => ({ - id: file.id.toString(), - name: file.name, - metadata: { - isEditing: false, - originalId: file.id, - }, - })), - }) - // Update treeData when files change - React.useEffect(() => { + useEffect(() => { setTreeData({ name: '', children: files.map((file) => ({ diff --git a/apps/studio/data/edge-functions/edge-functions-deploy-mutation.ts b/apps/studio/data/edge-functions/edge-functions-deploy-mutation.ts index 2aa2c75b92d..7a0a2da951b 100644 --- a/apps/studio/data/edge-functions/edge-functions-deploy-mutation.ts +++ b/apps/studio/data/edge-functions/edge-functions-deploy-mutation.ts @@ -1,10 +1,10 @@ import { useMutation, UseMutationOptions, useQueryClient } from '@tanstack/react-query' import { toast } from 'sonner' +import { components } from 'api-types' import { handleError, post } from 'data/fetchers' import type { ResponseError } from 'types' import { edgeFunctionsKeys } from './keys' -import { components } from 'api-types' export type EdgeFunctionsDeployVariables = { projectRef: string @@ -61,7 +61,7 @@ export const useEdgeFunctionDeployMutation = ({ { async onSuccess(data, variables, context) { const { projectRef } = variables - await Promise.all([queryClient.invalidateQueries(edgeFunctionsKeys.list(projectRef))]) + await queryClient.invalidateQueries(edgeFunctionsKeys.list(projectRef)) await onSuccess?.(data, variables, context) }, async onError(data, variables, context) { diff --git a/apps/studio/pages/project/[ref]/functions/[functionSlug]/details.tsx b/apps/studio/pages/project/[ref]/functions/[functionSlug]/details.tsx index df1d37567b0..8033823a504 100644 --- a/apps/studio/pages/project/[ref]/functions/[functionSlug]/details.tsx +++ b/apps/studio/pages/project/[ref]/functions/[functionSlug]/details.tsx @@ -1,4 +1,4 @@ -import { EdgeFunctionDetails } from 'components/interfaces/Functions' +import { EdgeFunctionDetails } from 'components/interfaces/Functions/EdgeFunctionDetails/EdgeFunctionDetails' import DefaultLayout from 'components/layouts/DefaultLayout' import EdgeFunctionDetailsLayout from 'components/layouts/EdgeFunctionsLayout/EdgeFunctionDetailsLayout' import type { NextPageWithLayout } from 'types' diff --git a/apps/studio/pages/project/[ref]/functions/index.tsx b/apps/studio/pages/project/[ref]/functions/index.tsx index f153df25aa8..0b41e0a1cc8 100644 --- a/apps/studio/pages/project/[ref]/functions/index.tsx +++ b/apps/studio/pages/project/[ref]/functions/index.tsx @@ -2,15 +2,13 @@ import { ChevronDown, Code, ExternalLink, Terminal } from 'lucide-react' import { useRouter } from 'next/router' import { useParams } from 'common' -import { - EdgeFunctionsListItem, - FunctionsEmptyState, - TerminalInstructions, -} from 'components/interfaces/Functions' +import { EdgeFunctionsListItem } from 'components/interfaces/Functions/EdgeFunctionsListItem' +import { FunctionsEmptyState } from 'components/interfaces/Functions/FunctionsEmptyState' +import { TerminalInstructions } from 'components/interfaces/Functions/TerminalInstructions' import DefaultLayout from 'components/layouts/DefaultLayout' import EdgeFunctionsLayout from 'components/layouts/EdgeFunctionsLayout/EdgeFunctionsLayout' import { PageLayout } from 'components/layouts/PageLayout/PageLayout' -import { ScaffoldContainer } from 'components/layouts/Scaffold' +import { ScaffoldContainer, ScaffoldSection } from 'components/layouts/Scaffold' import Table from 'components/to-be-cleaned/Table' import AlertError from 'components/ui/AlertError' import { ButtonTooltip } from 'components/ui/ButtonTooltip' @@ -93,20 +91,16 @@ const EdgeFunctionsPage: NextPageWithLayout = () => { ) const secondaryActions = [ - ...(!hasFunctions - ? [ - , - ] - : []), , + , { secondaryActions={secondaryActions} > - {isLoading && ( -
- -
- )} + + {isLoading && } - {isError && } + {isError && } - {isSuccess && ( - <> - {hasFunctions ? ( -
+ {isSuccess && ( + <> + {hasFunctions ? ( @@ -176,12 +166,12 @@ const EdgeFunctionsPage: NextPageWithLayout = () => { } /> - - ) : ( - - )} - - )} + ) : ( + + )} + + )} + ) diff --git a/apps/studio/pages/project/[ref]/functions/new.tsx b/apps/studio/pages/project/[ref]/functions/new.tsx index ffa685c6475..9c1bd3d7c51 100644 --- a/apps/studio/pages/project/[ref]/functions/new.tsx +++ b/apps/studio/pages/project/[ref]/functions/new.tsx @@ -1,10 +1,10 @@ +import { zodResolver } from '@hookform/resolvers/zod' import { AlertCircle, Book, Check } from 'lucide-react' import { useRouter } from 'next/router' import { useEffect, useState } from 'react' -import { toast } from 'sonner' import { useForm } from 'react-hook-form' -import { yupResolver } from '@hookform/resolvers/yup' -import * as yup from 'yup' +import { toast } from 'sonner' +import * as z from 'zod' import { useParams } from 'common' import { EDGE_FUNCTION_TEMPLATES } from 'components/interfaces/Functions/Functions.templates' @@ -41,7 +41,6 @@ import { TooltipContent, TooltipTrigger, } from 'ui' -import { InfoTooltip } from 'ui-patterns/info-tooltip' // Array of adjectives and nouns for random function name generation const ADJECTIVES = [ @@ -73,11 +72,11 @@ const NOUNS = [ const FUNCTION_NAME_REGEX = /^[A-Za-z0-9_-]+$/ // Define form schema with yup -const formSchema = yup.object({ - functionName: yup +const FormSchema = z.object({ + functionName: z .string() - .required('Function name is required') - .matches(FUNCTION_NAME_REGEX, 'Only letters, numbers, hyphens, and underscores allowed'), + .min(1, 'Function name is required') + .regex(FUNCTION_NAME_REGEX, 'Only letters, numbers, hyphens, and underscores allowed'), }) // Generate a random function name @@ -94,11 +93,11 @@ const sanitizeFunctionName = (name: string): string => { } // Type for the form values -type FormValues = yup.InferType +type FormValues = z.infer const NewFunctionPage = () => { const router = useRouter() - const { ref } = useParams() + const { ref, template } = useParams() const project = useSelectedProject() const isOptedInToAI = useOrgOptedIntoAi() const includeSchemaMetadata = isOptedInToAI || !IS_PLATFORM @@ -119,15 +118,15 @@ const NewFunctionPage = () => { const [isPreviewingTemplate, setIsPreviewingTemplate] = useState(false) const [savedCode, setSavedCode] = useState('') - // Setup form with react-hook-form and yup resolver const form = useForm({ - resolver: yupResolver(formSchema), + resolver: zodResolver(FormSchema), defaultValues: { functionName: generateRandomFunctionName(), }, }) const { mutate: deployFunction, isLoading: isDeploying } = useEdgeFunctionDeployMutation({ + // [Joshen] To investigate: For some reason, the invalidation for list of edge functions isn't triggering onSuccess: () => { toast.success('Successfully deployed edge function') const functionName = form.getValues('functionName') @@ -218,6 +217,24 @@ const NewFunctionPage = () => { } }, [edgeFunctionCreate, ref, router]) + useEffect(() => { + if (template) { + const templateMeta = EDGE_FUNCTION_TEMPLATES.find((x) => x.value === template) + if (templateMeta) { + form.reset({ functionName: template }) + setFiles([ + { + id: 1, + name: 'index.ts', + selected: true, + content: templateMeta.content, + }, + ]) + } + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [template]) + return ( { Templates - +