FunctionsEmptyState.tsx403 lines · main
| 1 | // @ts-nocheck |
| 2 | import { useParams } from 'common' |
| 3 | import { Code, Play, Terminal } from 'lucide-react' |
| 4 | import Link from 'next/link' |
| 5 | import { useRouter } from 'next/router' |
| 6 | import { parseAsString, useQueryState } from 'nuqs' |
| 7 | import { useMemo } from 'react' |
| 8 | import { |
| 9 | AiIconAnimation, |
| 10 | Button, |
| 11 | Card, |
| 12 | CardContent, |
| 13 | CardHeader, |
| 14 | CardTitle, |
| 15 | cn, |
| 16 | Dialog, |
| 17 | DialogContent, |
| 18 | DialogDescription, |
| 19 | DialogHeader, |
| 20 | DialogSection, |
| 21 | DialogTitle, |
| 22 | DialogTrigger, |
| 23 | Separator, |
| 24 | } from 'ui' |
| 25 | import { CodeBlock } from 'ui-patterns/CodeBlock' |
| 26 | |
| 27 | import { EDGE_FUNCTION_TEMPLATES } from './Functions.templates' |
| 28 | import { SIDEBAR_KEYS } from '@/components/layouts/ProjectLayout/LayoutSidebar/LayoutSidebarProvider' |
| 29 | import { ScaffoldSectionTitle } from '@/components/layouts/Scaffold' |
| 30 | import { DocsButton } from '@/components/ui/DocsButton' |
| 31 | import { ResourceItem } from '@/components/ui/Resource/ResourceItem' |
| 32 | import { ResourceList } from '@/components/ui/Resource/ResourceList' |
| 33 | import { useSendEventMutation } from '@/data/telemetry/send-event-mutation' |
| 34 | import { useIsFeatureEnabled } from '@/hooks/misc/useIsFeatureEnabled' |
| 35 | import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization' |
| 36 | import { DOCS_URL } from '@/lib/constants' |
| 37 | import { useAiAssistantStateSnapshot } from '@/state/ai-assistant-state' |
| 38 | import { useSidebarManagerSnapshot } from '@/state/sidebar-manager-state' |
| 39 | |
| 40 | export const FunctionsEmptyState = () => { |
| 41 | const { ref } = useParams() |
| 42 | const router = useRouter() |
| 43 | const aiSnap = useAiAssistantStateSnapshot() |
| 44 | const { openSidebar } = useSidebarManagerSnapshot() |
| 45 | |
| 46 | const { mutate: sendEvent } = useSendEventMutation() |
| 47 | const { data: org } = useSelectedOrganizationQuery() |
| 48 | const [, setCreateMethod] = useQueryState('create', parseAsString) |
| 49 | |
| 50 | const showStripeExample = useIsFeatureEnabled('edge_functions:show_stripe_example') |
| 51 | const templates = useMemo(() => { |
| 52 | if (showStripeExample) { |
| 53 | return EDGE_FUNCTION_TEMPLATES |
| 54 | } |
| 55 | |
| 56 | // Filter out Stripe template |
| 57 | return EDGE_FUNCTION_TEMPLATES.filter((template) => template.value !== 'stripe-webhook') |
| 58 | }, [showStripeExample]) |
| 59 | |
| 60 | return ( |
| 61 | <> |
| 62 | <Card> |
| 63 | <CardHeader> |
| 64 | <CardTitle>Deploy your first edge function</CardTitle> |
| 65 | </CardHeader> |
| 66 | <CardContent className="p-0 grid grid-cols-[repeat(auto-fit,minmax(240px,1fr))] divide-y md:divide-y-0 md:divide-x divide-default items-stretch"> |
| 67 | {/* Editor Option */} |
| 68 | <div className="p-8"> |
| 69 | <div className="flex items-center gap-2"> |
| 70 | <Code strokeWidth={1.5} size={20} /> |
| 71 | <h4 className="text-base text-foreground">Via Editor</h4> |
| 72 | </div> |
| 73 | <p className="text-sm text-foreground-light mb-4 mt-1"> |
| 74 | Create and edit functions directly in the browser. Download to local at any time. |
| 75 | </p> |
| 76 | <Button |
| 77 | type="default" |
| 78 | onClick={() => { |
| 79 | router.push(`/project/${ref}/functions/new`) |
| 80 | sendEvent({ |
| 81 | action: 'edge_function_via_editor_button_clicked', |
| 82 | properties: { origin: 'no_functions_block' }, |
| 83 | groups: { project: ref ?? 'Unknown', organization: org?.slug ?? 'Unknown' }, |
| 84 | }) |
| 85 | }} |
| 86 | > |
| 87 | Open Editor |
| 88 | </Button> |
| 89 | </div> |
| 90 | |
| 91 | {/* AI Assistant Option */} |
| 92 | <div className="p-8"> |
| 93 | <div className="flex items-center gap-2"> |
| 94 | <AiIconAnimation size={20} /> |
| 95 | <h4 className="text-base text-foreground">AI Assistant</h4> |
| 96 | </div> |
| 97 | <p className="text-sm text-foreground-light mb-4 mt-1"> |
| 98 | Let our AI assistant help you create functions. Perfect for kickstarting a function. |
| 99 | </p> |
| 100 | <Button |
| 101 | type="default" |
| 102 | onClick={() => { |
| 103 | openSidebar(SIDEBAR_KEYS.AI_ASSISTANT) |
| 104 | aiSnap.newChat({ |
| 105 | name: 'Create new edge function', |
| 106 | initialInput: 'Create a new edge function that ...', |
| 107 | suggestions: { |
| 108 | title: |
| 109 | 'I can help you create a new edge function. Here are a few example prompts to get you started:', |
| 110 | prompts: [ |
| 111 | { |
| 112 | label: 'Stripe Payments', |
| 113 | description: |
| 114 | 'Create a new edge function that processes payments with Stripe', |
| 115 | }, |
| 116 | { |
| 117 | label: 'Email with Resend', |
| 118 | description: 'Create a new edge function that sends emails with Resend', |
| 119 | }, |
| 120 | { |
| 121 | label: 'PDF Generator', |
| 122 | description: |
| 123 | 'Create a new edge function that generates PDFs from HTML templates', |
| 124 | }, |
| 125 | ], |
| 126 | }, |
| 127 | }) |
| 128 | sendEvent({ |
| 129 | action: 'edge_function_ai_assistant_button_clicked', |
| 130 | properties: { origin: 'no_functions_block' }, |
| 131 | groups: { project: ref ?? 'Unknown', organization: org?.slug ?? 'Unknown' }, |
| 132 | }) |
| 133 | }} |
| 134 | > |
| 135 | Open Assistant |
| 136 | </Button> |
| 137 | </div> |
| 138 | |
| 139 | {/* CLI Option */} |
| 140 | <div className="p-8"> |
| 141 | <div className="flex items-center gap-2"> |
| 142 | <Terminal strokeWidth={1.5} size={20} /> |
| 143 | <h4 className="text-base text-foreground">Via CLI</h4> |
| 144 | </div> |
| 145 | <p className="text-sm text-foreground-light mb-4 mt-1"> |
| 146 | Create and deploy functions using the Briven CLI. Ideal for local development and |
| 147 | version control. |
| 148 | </p> |
| 149 | |
| 150 | <Button |
| 151 | type="default" |
| 152 | onClick={() => { |
| 153 | setCreateMethod('cli') |
| 154 | sendEvent({ |
| 155 | action: 'edge_function_via_cli_button_clicked', |
| 156 | properties: { origin: 'no_functions_block' }, |
| 157 | groups: { project: ref ?? 'Unknown', organization: org?.slug ?? 'Unknown' }, |
| 158 | }) |
| 159 | }} |
| 160 | > |
| 161 | View CLI Instructions |
| 162 | </Button> |
| 163 | </div> |
| 164 | </CardContent> |
| 165 | </Card> |
| 166 | <ScaffoldSectionTitle className="text-xl mb-4 mt-12"> |
| 167 | Start with a template |
| 168 | </ScaffoldSectionTitle> |
| 169 | <ResourceList> |
| 170 | {templates.map((template) => ( |
| 171 | <ResourceItem |
| 172 | key={template.name} |
| 173 | media={<Code strokeWidth={1.5} size={16} className="translate-y-[-9px]" />} |
| 174 | onClick={() => { |
| 175 | sendEvent({ |
| 176 | action: 'edge_function_template_clicked', |
| 177 | properties: { templateName: template.name, origin: 'functions_page' }, |
| 178 | groups: { project: ref ?? 'Unknown', organization: org?.slug ?? 'Unknown' }, |
| 179 | }) |
| 180 | }} |
| 181 | > |
| 182 | <Link href={`/project/${ref}/functions/new?template=${template.value}`}> |
| 183 | <p>{template.name}</p> |
| 184 | <p className="text-sm text-foreground-lighter">{template.description}</p> |
| 185 | </Link> |
| 186 | </ResourceItem> |
| 187 | ))} |
| 188 | </ResourceList> |
| 189 | </> |
| 190 | ) |
| 191 | } |
| 192 | |
| 193 | export const FunctionsInstructionsLocal = () => { |
| 194 | const showStripeExample = useIsFeatureEnabled('edge_functions:show_stripe_example') |
| 195 | const templates = useMemo(() => { |
| 196 | if (showStripeExample) { |
| 197 | return EDGE_FUNCTION_TEMPLATES |
| 198 | } |
| 199 | |
| 200 | // Filter out Stripe template |
| 201 | return EDGE_FUNCTION_TEMPLATES.filter((template) => template.value !== 'stripe-webhook') |
| 202 | }, [showStripeExample]) |
| 203 | |
| 204 | return ( |
| 205 | <> |
| 206 | <div className="flex flex-col gap-y-4"> |
| 207 | <Card> |
| 208 | <CardHeader> |
| 209 | <CardTitle>Developing Edge Functions with the CLI</CardTitle> |
| 210 | </CardHeader> |
| 211 | <CardContent |
| 212 | className={cn( |
| 213 | 'p-0 flex flex-col', |
| 214 | '2xl:grid 2xl:grid-cols-[repeat(auto-fit,minmax(240px,1fr))] 2xl:divide-y-0 2xl:divide-x', |
| 215 | 'divide-y divide-default items-stretch' |
| 216 | )} |
| 217 | > |
| 218 | <div className="p-8"> |
| 219 | <div className="flex items-center gap-2"> |
| 220 | <Code size={20} /> |
| 221 | <h4 className="text-base text-foreground">Create an Edge Function</h4> |
| 222 | </div> |
| 223 | <p className="text-sm text-foreground-light mt-1 mb-4 prose [&>code]:text-xs text-sm max-w-full"> |
| 224 | Create a new edge function called <code>hello-world</code> in your project via the |
| 225 | Briven CLI. |
| 226 | </p> |
| 227 | <div className="mb-4"> |
| 228 | <CodeBlock |
| 229 | language="bash" |
| 230 | hideLineNumbers={true} |
| 231 | className={cn( |
| 232 | 'px-3.5 max-w-full prose dark:prose-dark [&>code]:m-0 2xl:min-h-28' |
| 233 | )} |
| 234 | value="briven functions new hello-world" |
| 235 | /> |
| 236 | </div> |
| 237 | <DocsButton |
| 238 | href={`${DOCS_URL}/guides/functions/local-quickstart#create-an-edge-function`} |
| 239 | /> |
| 240 | </div> |
| 241 | |
| 242 | <div className="p-8"> |
| 243 | <div className="flex items-center gap-2"> |
| 244 | <Play size={20} /> |
| 245 | <h4 className="text-base text-foreground">Run Edge Functions</h4> |
| 246 | </div> |
| 247 | <p className="text-sm text-foreground-light mt-1 mb-4 prose [&>code]:text-xs text-sm max-w-full"> |
| 248 | You can run your Edge Function locally using <code>briven functions serve</code>. |
| 249 | </p> |
| 250 | <div className="mb-4"> |
| 251 | <CodeBlock |
| 252 | language="bash" |
| 253 | hideLineNumbers={true} |
| 254 | className={cn( |
| 255 | 'px-3.5 max-w-full prose dark:prose-dark [&>code]:m-0 2xl:min-h-28' |
| 256 | )} |
| 257 | value={` |
| 258 | briven start # start the briven stack |
| 259 | briven functions serve # start the Functions watcher`.trim()} |
| 260 | /> |
| 261 | </div> |
| 262 | <DocsButton |
| 263 | href={`${DOCS_URL}/guides/functions/local-quickstart#running-edge-functions-locally`} |
| 264 | /> |
| 265 | </div> |
| 266 | |
| 267 | <div className="p-8"> |
| 268 | <div className="flex items-center gap-2"> |
| 269 | <Terminal strokeWidth={1.5} size={20} /> |
| 270 | <h4 className="text-base text-foreground">Invoke Edge Functions</h4> |
| 271 | </div> |
| 272 | <p className="text-sm text-foreground-light mt-1 mb-4"> |
| 273 | While serving your local Edge Functions, you can invoke it using cURL or one of the |
| 274 | client libraries. |
| 275 | </p> |
| 276 | <div className="mb-4"> |
| 277 | <CodeBlock |
| 278 | language="bash" |
| 279 | hideLineNumbers={true} |
| 280 | className={cn( |
| 281 | 'px-3.5 max-w-full prose dark:prose-dark [&>code]:m-0 2xl:min-h-28' |
| 282 | )} |
| 283 | value={` |
| 284 | curl --request POST 'http://localhost:54321/functions/v1/hello-world' \\ |
| 285 | --header 'Authorization: Bearer BRIVEN_ANON_KEY' \\ |
| 286 | --header 'Content-Type: application/json' \\ |
| 287 | --data '{ "name":"Functions" }'`.trim()} |
| 288 | /> |
| 289 | </div> |
| 290 | <DocsButton |
| 291 | href={`${DOCS_URL}/guides/functions/local-quickstart#invoking-edge-functions-locally`} |
| 292 | /> |
| 293 | </div> |
| 294 | </CardContent> |
| 295 | </Card> |
| 296 | |
| 297 | <Card> |
| 298 | <CardHeader> |
| 299 | <CardTitle>Self-hosted Briven</CardTitle> |
| 300 | </CardHeader> |
| 301 | <CardContent className="p-0 grid grid-cols-[repeat(auto-fit,minmax(240px,1fr))] divide-y md:divide-y-0 md:divide-x divide-default items-stretch"> |
| 302 | <div className="p-8"> |
| 303 | <p className="text-sm text-foreground-light mt-1 mb-4"> |
| 304 | Edge Functions are available in self-hosted Briven via Briven Edge Runtime. |
| 305 | Unlike the platform, functions must be added manually — place each function at{' '} |
| 306 | <code className="text-code-inline"> |
| 307 | volumes/functions/<function-name>/index.ts |
| 308 | </code>{' '} |
| 309 | and restart the <code className="text-code-inline">functions</code> service to pick |
| 310 | up changes. See the guide to learn more about configuration, secrets, and routing. |
| 311 | </p> |
| 312 | <DocsButton href={`${DOCS_URL}/guides/self-hosting/self-hosted-functions`} /> |
| 313 | </div> |
| 314 | </CardContent> |
| 315 | </Card> |
| 316 | |
| 317 | <ScaffoldSectionTitle className="text-xl mt-12">Explore our templates</ScaffoldSectionTitle> |
| 318 | <ResourceList> |
| 319 | {templates.map((template) => ( |
| 320 | <Dialog key={template.name}> |
| 321 | <DialogTrigger asChild> |
| 322 | <ResourceItem |
| 323 | key={template.name} |
| 324 | media={<Code strokeWidth={1.5} size={16} className="translate-y-[-9px]" />} |
| 325 | > |
| 326 | <div> |
| 327 | <p>{template.name}</p> |
| 328 | <p className="text-sm text-foreground-lighter">{template.description}</p> |
| 329 | </div> |
| 330 | </ResourceItem> |
| 331 | </DialogTrigger> |
| 332 | <DialogContent size="large"> |
| 333 | <DialogHeader> |
| 334 | <DialogTitle>{template.name}</DialogTitle> |
| 335 | <DialogDescription>{template.description}</DialogDescription> |
| 336 | </DialogHeader> |
| 337 | <Separator /> |
| 338 | <DialogSection className="p-0!"> |
| 339 | <CodeBlock |
| 340 | language="ts" |
| 341 | hideLineNumbers={true} |
| 342 | className={cn( |
| 343 | 'border-0 rounded-none px-3.5 max-w-full prose dark:prose-dark [&>code]:m-0 max-h-[420px]' |
| 344 | )} |
| 345 | value={template.content} |
| 346 | /> |
| 347 | </DialogSection> |
| 348 | </DialogContent> |
| 349 | </Dialog> |
| 350 | ))} |
| 351 | </ResourceList> |
| 352 | </div> |
| 353 | </> |
| 354 | ) |
| 355 | } |
| 356 | |
| 357 | export const FunctionsSecretsEmptyStateLocal = () => { |
| 358 | return ( |
| 359 | <> |
| 360 | <Card> |
| 361 | <CardHeader> |
| 362 | <CardTitle>Local development & CLI</CardTitle> |
| 363 | </CardHeader> |
| 364 | <CardContent> |
| 365 | <div className="text-sm text-foreground-light mb-4"> |
| 366 | <p className="mb-2">Secrets can be loaded in two ways:</p> |
| 367 | <ul className="list-disc pl-6 space-y-1"> |
| 368 | <li> |
| 369 | Place a <code className="text-code-inline">.env</code> file at{' '} |
| 370 | <code className="text-code-inline">briven/functions/.env</code> — picked up |
| 371 | automatically on <code className="text-code-inline">briven start</code>. |
| 372 | </li> |
| 373 | <li> |
| 374 | Pass <code className="text-code-inline">--env-file</code> to{' '} |
| 375 | <code className="text-code-inline">briven functions serve</code>, e.g.{' '} |
| 376 | <code className="text-code-inline"> |
| 377 | briven functions serve --env-file ./path/to/.env-file |
| 378 | </code> |
| 379 | </li> |
| 380 | </ul> |
| 381 | </div> |
| 382 | <DocsButton href={`${DOCS_URL}/guides/functions/secrets#using-the-cli`} /> |
| 383 | </CardContent> |
| 384 | </Card> |
| 385 | |
| 386 | <Card> |
| 387 | <CardHeader> |
| 388 | <CardTitle>Self-Hosted Briven</CardTitle> |
| 389 | </CardHeader> |
| 390 | <CardContent> |
| 391 | <p className="text-sm text-foreground-light mb-4"> |
| 392 | Configure secrets in your <code className="text-code-inline">.env</code> file and{' '} |
| 393 | <code className="text-code-inline">docker-compose.yml</code> under the{' '} |
| 394 | <code className="text-code-inline">functions</code> service. |
| 395 | </p> |
| 396 | <DocsButton |
| 397 | href={`${DOCS_URL}/guides/self-hosting/self-hosted-functions#custom-environment-variables`} |
| 398 | /> |
| 399 | </CardContent> |
| 400 | </Card> |
| 401 | </> |
| 402 | ) |
| 403 | } |