From 509d80c9ebaf525884f00ca542cc4adc20563b4f Mon Sep 17 00:00:00 2001 From: Jordi Enric <37541088+jordienr@users.noreply.github.com> Date: Tue, 25 Aug 2026 15:38:57 +0200 Subject: [PATCH] feat(studio): CLI deploy instructions for workers (FE-4191) (#49194) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## What The surface that shows you how to deploy a worker from the CLI, plus the product rename and the alpha framing. - **Compute → Workers.** `PRODUCT_NAME` and `CLI_NAME` now say `Workers` / `workers`, so the sidebar, page title, command menu, and every generated snippet match the CLI. One name, not two. - **`DeployWorkerDialog`** — scaffold / configure / push, with copyable `supabase workers` and `config.toml` snippets - **`WorkersEmptyState`** — an `EmptyStatePresentational` with a permission-gated deploy action - **`AlphaNotice`** on the list page, and a **New** badge on the sidebar entry (`Route.isNew`) - **`WorkerSnippetTabs`** — CLI, `config.toml`, and curl/JS/Python calls built from one worker shape. Reused by #49195. Snippet URLs resolve from the project's `app_config.endpoint` (`https:///workers/v1/`, the same shape as `/functions/v1/`), and fall back to `[YOUR WORKER URL]` before settings load rather than printing a wrong host. ## How to test Only on the **Mockamaster** project in staging — it is the one project in the alpha allow-list. 1. Staging dashboard → Mockamaster → **Workers** (the sidebar entry carries a **New** badge) 2. **Deploy a worker** → step through the tabs; every snippet should name the real worker URL, not a placeholder 3. Copy the cURL snippet and run it: expect `401`. Reaching a deployed worker needs a second allow-list (`WORKERS_ALLOWED_PROJECTS` in api-gateway `customer-router/wrangler.toml`), separate from the flag that unlocks the dashboard. 4. Open a project with no workers to see the empty state ## Tests `workerSnippets.test.ts` covers the generated output users copy: worker URL in all three call snippets, the `[YOUR WORKER URL]` fallback, anon vs service-role placeholder, empty-name fallback and trimming, runtime default, and every `config.toml` field. ## Unverified copy The dialog steps and the `supabase workers ` subcommands come from the original POC spec, not from the shipped CLI. Same for "Dockerfile, Node.js and Deno supported" in the empty state — only Deno is confirmed end to end. Worth a check by someone who knows the CLI surface. Closes FE-4191 --------- Co-authored-by: Francesco Sansalvadore --- apps/studio/components/interfaces/Sidebar.tsx | 9 ++ .../interfaces/Workers/DeployWorkerDialog.tsx | 67 +++++++++++ .../interfaces/Workers/WorkerCommandLine.tsx | 23 ++++ .../interfaces/Workers/WorkerSnippetTabs.tsx | 84 +++++++++++++ .../interfaces/Workers/WorkersEmptyState.tsx | 41 +++++++ .../interfaces/Workers/WorkersList.test.tsx | 4 +- .../interfaces/Workers/WorkersList.tsx | 9 +- .../interfaces/Workers/workerSnippets.test.ts | 81 +++++++++++++ .../interfaces/Workers/workerSnippets.ts | 111 ++++++++++++++++++ .../NavigationBar/NavigationBar.utils.tsx | 1 + .../layouts/WorkersLayout/WorkersLayout.tsx | 2 +- apps/studio/components/ui/ui.types.ts | 1 + apps/studio/lib/constants/workers.ts | 8 +- .../pages/project/[ref]/workers/index.tsx | 33 ++++-- .../project/[ref]/workers/index.test.tsx | 6 +- 15 files changed, 458 insertions(+), 22 deletions(-) create mode 100644 apps/studio/components/interfaces/Workers/DeployWorkerDialog.tsx create mode 100644 apps/studio/components/interfaces/Workers/WorkerCommandLine.tsx create mode 100644 apps/studio/components/interfaces/Workers/WorkerSnippetTabs.tsx create mode 100644 apps/studio/components/interfaces/Workers/WorkersEmptyState.tsx create mode 100644 apps/studio/components/interfaces/Workers/workerSnippets.test.ts create mode 100644 apps/studio/components/interfaces/Workers/workerSnippets.ts diff --git a/apps/studio/components/interfaces/Sidebar.tsx b/apps/studio/components/interfaces/Sidebar.tsx index bf6ce2c63da..3a5ff661715 100644 --- a/apps/studio/components/interfaces/Sidebar.tsx +++ b/apps/studio/components/interfaces/Sidebar.tsx @@ -7,6 +7,7 @@ import Link from 'next/link' import { useRouter } from 'next/router' import { ComponentProps, ComponentPropsWithoutRef, FC, ReactNode, useEffect } from 'react' import { + Badge, Button, cn, DropdownMenu, @@ -195,6 +196,14 @@ export function SideBarNavLink({ <> {route.icon} {route.label} + {route.isNew && ( + + New + + )} ) diff --git a/apps/studio/components/interfaces/Workers/DeployWorkerDialog.tsx b/apps/studio/components/interfaces/Workers/DeployWorkerDialog.tsx new file mode 100644 index 00000000000..5c90e6a8f28 --- /dev/null +++ b/apps/studio/components/interfaces/Workers/DeployWorkerDialog.tsx @@ -0,0 +1,67 @@ +import { + Dialog, + DialogContent, + DialogHeader, + DialogSection, + DialogSectionSeparator, + DialogTitle, +} from 'ui' + +import { EXAMPLE_WORKER } from './workerSnippets' +import { WorkerSnippetTabs } from './WorkerSnippetTabs' + +interface DeployWorkerDialogProps { + open: boolean + onOpenChange: (open: boolean) => void +} + +const STEPS = [ + { + title: 'Scaffold the worker', + description: 'Creates supabase/workers// with an entrypoint for the runtime you pick.', + }, + { + title: 'Configure it', + description: 'Runtime, size, access, and instance count live in supabase/config.toml.', + }, + { + title: 'Push it', + description: 'Builds the image and schedules it on a microVM in US West.', + }, +] + +export const DeployWorkerDialog = ({ open, onOpenChange }: DeployWorkerDialogProps) => ( + + + + Deploy a worker + + + +

+ Workers are deployed with the Supabase CLI. This dashboard is read-only during the private + alpha. +

+
    + {STEPS.map((step, index) => ( +
  1. + + {index + 1} + +
    +

    {step.title}

    +

    {step.description}

    +
    +
  2. + ))} +
+
+ + + + + + +
+
+) diff --git a/apps/studio/components/interfaces/Workers/WorkerCommandLine.tsx b/apps/studio/components/interfaces/Workers/WorkerCommandLine.tsx new file mode 100644 index 00000000000..def836b48c3 --- /dev/null +++ b/apps/studio/components/interfaces/Workers/WorkerCommandLine.tsx @@ -0,0 +1,23 @@ +import CopyButton from '@/components/ui/CopyButton' + +interface WorkerCommandLineProps { + command: string + comment?: string +} + +export const WorkerCommandLine = ({ command, comment }: WorkerCommandLineProps) => ( +
+ {comment &&

{`> ${comment}`}

} +
+ $ + {command} + +
+
+) diff --git a/apps/studio/components/interfaces/Workers/WorkerSnippetTabs.tsx b/apps/studio/components/interfaces/Workers/WorkerSnippetTabs.tsx new file mode 100644 index 00000000000..151050e8fd4 --- /dev/null +++ b/apps/studio/components/interfaces/Workers/WorkerSnippetTabs.tsx @@ -0,0 +1,84 @@ +import { useParams } from 'common' +import { useState } from 'react' +import { cn } from 'ui' + +import { buildWorkerSnippets, type WorkerSnippetInput } from './workerSnippets' +import CopyButton from '@/components/ui/CopyButton' +import { useProjectSettingsV2Query } from '@/data/config/project-settings-v2-query' + +export type WorkerSnippetTab = 'config' | 'cli' | 'curl' | 'js' | 'python' + +const TAB_LABEL: Record = { + config: 'config.toml', + cli: 'CLI', + curl: 'cURL', + js: 'JavaScript', + python: 'Python', +} + +interface WorkerSnippetTabsProps { + input: Omit + tabs?: [WorkerSnippetTab, ...WorkerSnippetTab[]] + className?: string +} + +export const WorkerSnippetTabs = ({ + input, + tabs = ['cli', 'curl'], + className, +}: WorkerSnippetTabsProps) => { + const { ref } = useParams() + const [active, setActive] = useState(tabs[0]) + + const { data: settings } = useProjectSettingsV2Query({ projectRef: ref }, { enabled: !!ref }) + const snippets = buildWorkerSnippets({ + ...input, + endpoint: settings?.app_config?.endpoint, + protocol: settings?.app_config?.protocol, + }) + const snippetByTab: Record = { + config: snippets.configToml, + cli: snippets.cli, + curl: snippets.curl, + js: snippets.javascript, + python: snippets.python, + } + const activeTab = tabs.includes(active) ? active : tabs[0] + const value = snippetByTab[activeTab] + + return ( +
+
+ {tabs.map((tab) => ( + + ))} +
+ +
+ +
+          {value}
+        
+
+
+ ) +} diff --git a/apps/studio/components/interfaces/Workers/WorkersEmptyState.tsx b/apps/studio/components/interfaces/Workers/WorkersEmptyState.tsx new file mode 100644 index 00000000000..e5b29c52723 --- /dev/null +++ b/apps/studio/components/interfaces/Workers/WorkersEmptyState.tsx @@ -0,0 +1,41 @@ +import { PermissionAction } from '@supabase/shared-types/out/constants' +import { BoxPlus } from 'icons' +import { Plus } from 'lucide-react' +import { EmptyStatePresentational } from 'ui-patterns/EmptyStatePresentational' + +import { ButtonTooltip } from '@/components/ui/ButtonTooltip' +import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions' + +interface WorkersEmptyStateProps { + onDeploy: () => void +} + +export const WorkersEmptyState = ({ onDeploy }: WorkersEmptyStateProps) => { + const { can: canDeployWorkers } = useAsyncCheckPermissions(PermissionAction.FUNCTIONS_WRITE, '*') + + return ( + + } + disabled={!canDeployWorkers} + onClick={onDeploy} + tooltip={{ + content: { + side: 'bottom', + text: canDeployWorkers + ? undefined + : 'You need additional permissions to deploy workers', + }, + }} + > + Deploy a worker + + + ) +} diff --git a/apps/studio/components/interfaces/Workers/WorkersList.test.tsx b/apps/studio/components/interfaces/Workers/WorkersList.test.tsx index 8dbdef26a66..e78acd3b302 100644 --- a/apps/studio/components/interfaces/Workers/WorkersList.test.tsx +++ b/apps/studio/components/interfaces/Workers/WorkersList.test.tsx @@ -1,6 +1,6 @@ import { screen, within } from '@testing-library/react' import userEvent from '@testing-library/user-event' -import { beforeEach, describe, expect, it } from 'vitest' +import { beforeEach, describe, expect, it, vi } from 'vitest' import type { Worker } from './Workers.types' import { WorkersList } from './WorkersList' @@ -19,7 +19,7 @@ const worker = (name: string, overrides: Partial = {}): Worker => ({ }) const renderList = (workers: Worker[]) => - customRender() + customRender() const rowNames = () => screen diff --git a/apps/studio/components/interfaces/Workers/WorkersList.tsx b/apps/studio/components/interfaces/Workers/WorkersList.tsx index 0d32d1d1866..7503d8c1533 100644 --- a/apps/studio/components/interfaces/Workers/WorkersList.tsx +++ b/apps/studio/components/interfaces/Workers/WorkersList.tsx @@ -1,9 +1,10 @@ -import { ChevronLeft, ChevronRight } from 'lucide-react' +import { ChevronLeft, ChevronRight, Terminal } from 'lucide-react' import Link from 'next/link' import { useRouter } from 'next/router' import { useState } from 'react' import { Badge, + Button, Card, CardFooter, Input, @@ -30,6 +31,7 @@ import { ButtonTooltip } from '@/components/ui/ButtonTooltip' interface WorkersListProps { projectRef: string workers: Worker[] + onDeploy: () => void } const STATE_FILTERS: { value: WorkerBuildState | 'all'; label: string }[] = [ @@ -53,7 +55,7 @@ const parseStateFilter = (value: string): WorkerBuildState | 'all' => const parseAccessFilter = (value: string): WorkerAccess | 'all' => ACCESS_FILTERS.find((option) => option.value === value)?.value ?? 'all' -export const WorkersList = ({ projectRef, workers }: WorkersListProps) => { +export const WorkersList = ({ projectRef, workers, onDeploy }: WorkersListProps) => { const router = useRouter() const [search, setSearch] = useState('') const [stateFilter, setStateFilter] = useState('all') @@ -125,6 +127,9 @@ export const WorkersList = ({ projectRef, workers }: WorkersListProps) => { {filtered.length} worker{filtered.length === 1 ? '' : 's'} + diff --git a/apps/studio/components/interfaces/Workers/workerSnippets.test.ts b/apps/studio/components/interfaces/Workers/workerSnippets.test.ts new file mode 100644 index 00000000000..283d93e8bd7 --- /dev/null +++ b/apps/studio/components/interfaces/Workers/workerSnippets.test.ts @@ -0,0 +1,81 @@ +import { describe, expect, it } from 'vitest' + +import { WORKERS_REGION } from './Workers.constants' +import { buildWorkerCliCommands, buildWorkerSnippets, EXAMPLE_WORKER } from './workerSnippets' + +const input = (overrides: Partial[0]> = {}) => ({ + ...EXAMPLE_WORKER, + endpoint: 'abcdefgh.supabase.co', + ...overrides, +}) + +describe('buildWorkerSnippets', () => { + it('points every call snippet at the worker URL', () => { + const { curl, javascript, python } = buildWorkerSnippets(input({ name: 'embed' })) + const url = 'https://abcdefgh.supabase.co/workers/v1/embed' + + expect(curl).toContain(`'${url}'`) + expect(javascript).toContain(`'${url}'`) + expect(python).toContain(`"${url}"`) + }) + + it('leaves a placeholder URL until the project settings resolve', () => { + const { curl } = buildWorkerSnippets(input({ endpoint: undefined })) + expect(curl).toContain('[YOUR WORKER URL]') + }) + + it('asks for the anon key on a public worker and the service role key on a private one', () => { + expect(buildWorkerSnippets(input({ access: 'public' })).curl).toContain('[YOUR ANON KEY]') + expect(buildWorkerSnippets(input({ access: 'private' })).curl).toContain( + '[YOUR SERVICE ROLE KEY]' + ) + expect(buildWorkerSnippets(input({ access: 'private' })).javascript).toContain( + '[YOUR SERVICE ROLE KEY]' + ) + }) + + it('falls back to a placeholder name when the worker has none', () => { + expect(buildWorkerSnippets(input({ name: ' ' })).cli).toContain('my-worker') + }) + + it('trims the name before interpolating it', () => { + expect(buildWorkerSnippets(input({ name: ' embed ' })).cli).toContain('new embed --runtime') + }) + + it('defaults the runtime when the API omits it', () => { + expect(buildWorkerSnippets(input({ runtime: undefined })).cli).toContain('--runtime node') + }) + + it('writes the worker spec into the config.toml block', () => { + const { configToml } = buildWorkerSnippets( + input({ + name: 'embed', + runtime: 'python', + size: '4gb-2vcpu', + access: 'private', + instances: 3, + }) + ) + + expect(configToml).toContain('[workers.embed]') + expect(configToml).toContain('runtime = "python"') + expect(configToml).toContain('size = "4gb-2vcpu" # 4 GB · 2 vCPU') + expect(configToml).toContain('access = "private"') + expect(configToml).toContain('instances = 3') + expect(configToml).toContain(WORKERS_REGION) + }) +}) + +describe('buildWorkerCliCommands', () => { + it('names the worker in every command', () => { + const commands = buildWorkerCliCommands('embed') + expect(commands).not.toHaveLength(0) + for (const { command } of commands) { + expect(command).toContain('embed') + } + }) + + it('falls back to a placeholder name when the worker has none', () => { + expect(buildWorkerCliCommands(' ')[0].command).toContain('my-worker') + }) +}) diff --git a/apps/studio/components/interfaces/Workers/workerSnippets.ts b/apps/studio/components/interfaces/Workers/workerSnippets.ts new file mode 100644 index 00000000000..cbcc6e2beb7 --- /dev/null +++ b/apps/studio/components/interfaces/Workers/workerSnippets.ts @@ -0,0 +1,111 @@ +import { WORKERS_REGION, workerUrl } from './Workers.constants' +import type { WorkerAccess } from './Workers.types' +import { formatSize } from './Workers.utils' +import { CLI_NAME } from '@/lib/constants/workers' + +export interface WorkerSnippetInput { + name: string + endpoint: string | undefined + protocol?: string + runtime: string | undefined + size: string + access: WorkerAccess + instances: number +} + +export interface WorkerSnippets { + configToml: string + cli: string + curl: string + javascript: string + python: string +} + +export const EXAMPLE_WORKER: Omit = { + name: 'my-worker', + runtime: 'node', + size: '2gb-1vcpu', + access: 'public', + instances: 1, +} + +const safeName = (name: string) => (name.trim().length > 0 ? name.trim() : 'my-worker') + +export function buildWorkerSnippets(input: WorkerSnippetInput): WorkerSnippets { + const name = safeName(input.name) + const runtime = input.runtime ?? 'node' + + const url = + workerUrl({ endpoint: input.endpoint, protocol: input.protocol, name }) ?? '[YOUR WORKER URL]' + + const cli = [ + `supabase ${CLI_NAME} new ${name} --runtime ${runtime}`, + `supabase ${CLI_NAME} push ${name}`, + ].join('\n') + + const configToml = [ + `# supabase/config.toml`, + ``, + `[${CLI_NAME}.${name}]`, + `runtime = "${runtime}"`, + `size = "${input.size}" # ${formatSize(input.size)}`, + `access = "${input.access}"`, + `instances = ${input.instances}`, + `# region is locked to ${WORKERS_REGION} at alpha`, + ].join('\n') + + const authHeader = + input.access === 'public' + ? ` -H 'Authorization: Bearer [YOUR ANON KEY]' \\` + : ` -H 'Authorization: Bearer [YOUR SERVICE ROLE KEY]' \\` + + const curl = [ + `curl -L -X POST '${url}' \\`, + authHeader, + ` -H 'Content-Type: application/json' \\`, + ` --data '{"name":"world"}'`, + ].join('\n') + + const keyPlaceholder = input.access === 'public' ? '[YOUR ANON KEY]' : '[YOUR SERVICE ROLE KEY]' + + const javascript = [ + `const res = await fetch('${url}', {`, + ` method: 'POST',`, + ` headers: {`, + ` Authorization: 'Bearer ${keyPlaceholder}',`, + ` 'Content-Type': 'application/json',`, + ` },`, + ` body: JSON.stringify({ name: 'world' }),`, + `})`, + `const data = await res.json()`, + ].join('\n') + + const python = [ + `import requests`, + ``, + `res = requests.post(`, + ` "${url}",`, + ` headers={"Authorization": "Bearer ${keyPlaceholder}"},`, + ` json={"name": "world"},`, + `)`, + `print(res.json())`, + ].join('\n') + + return { configToml, cli, curl, javascript, python } +} + +export interface WorkerCliCommand { + comment: string + command: string +} + +export function buildWorkerCliCommands(name: string): WorkerCliCommand[] { + const slug = safeName(name) + return [ + { comment: 'Recreate the source locally', command: `supabase ${CLI_NAME} pull ${slug}` }, + { comment: 'Run it locally on a port', command: `supabase ${CLI_NAME} serve ${slug}` }, + { comment: 'Deploy a new version', command: `supabase ${CLI_NAME} push ${slug}` }, + { comment: 'Stream logs', command: `supabase ${CLI_NAME} logs ${slug} --follow` }, + { comment: 'Delete the worker', command: `supabase ${CLI_NAME} delete ${slug}` }, + ] +} diff --git a/apps/studio/components/layouts/Navigation/NavigationBar/NavigationBar.utils.tsx b/apps/studio/components/layouts/Navigation/NavigationBar/NavigationBar.utils.tsx index b09833e41ad..fe5a74346a0 100644 --- a/apps/studio/components/layouts/Navigation/NavigationBar/NavigationBar.utils.tsx +++ b/apps/studio/components/layouts/Navigation/NavigationBar/NavigationBar.utils.tsx @@ -148,6 +148,7 @@ export const generateProductRoutes = ( disabled: !isProjectActive, icon: , link: ref && (isProjectBuilding ? buildingUrl : `/project/${ref}/workers`), + isNew: true, }, ] : []), diff --git a/apps/studio/components/layouts/WorkersLayout/WorkersLayout.tsx b/apps/studio/components/layouts/WorkersLayout/WorkersLayout.tsx index 6f97d9bc3cf..3931a524d21 100644 --- a/apps/studio/components/layouts/WorkersLayout/WorkersLayout.tsx +++ b/apps/studio/components/layouts/WorkersLayout/WorkersLayout.tsx @@ -85,7 +85,7 @@ const WorkersLayoutContent = ({ children, title }: PropsWithChildren + )} ) diff --git a/apps/studio/components/ui/ui.types.ts b/apps/studio/components/ui/ui.types.ts index 287f8fb0ba4..5342df1922a 100644 --- a/apps/studio/components/ui/ui.types.ts +++ b/apps/studio/components/ui/ui.types.ts @@ -10,6 +10,7 @@ export interface Route { disabled?: boolean linkElement?: ReactNode items?: any | Route[] + isNew?: boolean /** * Binds a registered keyboard shortcut to this route when set. The sidebar * entry shows the keybind on hover and jumps to `link` when the shortcut diff --git a/apps/studio/lib/constants/workers.ts b/apps/studio/lib/constants/workers.ts index 1ebb3122cf4..02e9fa1b0e1 100644 --- a/apps/studio/lib/constants/workers.ts +++ b/apps/studio/lib/constants/workers.ts @@ -1,5 +1,5 @@ -export const PRODUCT_NAME = 'Compute' -export const CLI_NAME = 'compute' +export const PRODUCT_NAME = 'Workers' +export const CLI_NAME = 'workers' export const WORKERS_CLI_DEPLOY = `supabase ${CLI_NAME} push` @@ -14,7 +14,7 @@ Add a block to \`supabase/config.toml\`: \`\`\`toml [${CLI_NAME}.embed] runtime = "python" # node | deno | bun | python | dockerfile -size = "2x1" # 2x1 (2GB/1vCPU) | 4x2 (4GB/2vCPU) — fixed at deploy +size = "2gb-1vcpu" # 2gb-1vcpu | 4gb-2vcpu — fixed at deploy access = "public" # public | private instances = 1 # 1..10 per deploy; 100 cap per project secrets = ["OPENAI_API_KEY"] # names only — values live in the Secrets API @@ -33,7 +33,7 @@ Config precedence: \`--flag\` > \`config.toml\` > interactive prompt > default. - Workers under \`supabase/${CLI_NAME}//\` are auto-discovered; the folder name is the slug. - \`entrypoint\` is inferred from the runtime (node index.js, deno run main.ts, python main.py, Dockerfile CMD). -- \`region\` is locked to us-west-1 at alpha — do not set it. +- \`region\` is locked to us-west-2 at alpha — do not set it. - Sizes are fixed at deploy time. To change size, delete the worker and redeploy. ## Manage diff --git a/apps/studio/pages/project/[ref]/workers/index.tsx b/apps/studio/pages/project/[ref]/workers/index.tsx index 7fca6b52418..af939bb9209 100644 --- a/apps/studio/pages/project/[ref]/workers/index.tsx +++ b/apps/studio/pages/project/[ref]/workers/index.tsx @@ -1,5 +1,6 @@ import { useQuery } from '@tanstack/react-query' import { useParams } from 'common' +import { useState } from 'react' import { Admonition } from 'ui-patterns/Admonition' import { PageContainer } from 'ui-patterns/PageContainer' import { @@ -12,14 +13,17 @@ import { import { PageSection, PageSectionContent } from 'ui-patterns/PageSection' import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader' +import { DeployWorkerDialog } from '@/components/interfaces/Workers/DeployWorkerDialog' import { isWorkersForbidden, isWorkersUnavailable, } from '@/components/interfaces/Workers/Workers.utils' +import { WorkersEmptyState } from '@/components/interfaces/Workers/WorkersEmptyState' import { WorkersList } from '@/components/interfaces/Workers/WorkersList' import { DefaultLayout } from '@/components/layouts/DefaultLayout' import { WorkersLayout } from '@/components/layouts/WorkersLayout/WorkersLayout' import { AlertError } from '@/components/ui/AlertError' +import { AlphaNotice } from '@/components/ui/AlphaNotice' import { NoPermission } from '@/components/ui/NoPermission' import { workersQueryOptions } from '@/data/workers/workers-query' import { PRODUCT_NAME } from '@/lib/constants/workers' @@ -27,6 +31,7 @@ import type { NextPageWithLayout } from '@/types' const WorkersPage: NextPageWithLayout = () => { const { ref } = useParams() + const [isDeployInstructionsOpen, setIsDeployInstructionsOpen] = useState(false) const { data: workers, error, @@ -46,7 +51,7 @@ const WorkersPage: NextPageWithLayout = () => { {PRODUCT_NAME} - Run backend workers in microVMs next to your database + Run fully managed compute in isolation next to your database @@ -54,7 +59,12 @@ const WorkersPage: NextPageWithLayout = () => { - + + + {isPending && } {isNotEnrolled && ( { description={`${PRODUCT_NAME} is in private alpha. Contact support to have this project added to the alpha.`} /> )} - {isMissingPermission && ( - - )} + {isMissingPermission && } {isUnexpectedError && } {isSuccess && workers.length === 0 && ( -

- No workers yet. Deploy your first worker with the Supabase CLI. -

+ setIsDeployInstructionsOpen(true)} /> )} {isSuccess && workers.length > 0 && ref && ( - + setIsDeployInstructionsOpen(true)} + /> )}
+ + ) } diff --git a/apps/studio/tests/pages/project/[ref]/workers/index.test.tsx b/apps/studio/tests/pages/project/[ref]/workers/index.test.tsx index b3715c657e4..44bd63aae07 100644 --- a/apps/studio/tests/pages/project/[ref]/workers/index.test.tsx +++ b/apps/studio/tests/pages/project/[ref]/workers/index.test.tsx @@ -86,7 +86,7 @@ describe('/project/[ref]/workers', () => { await renderWorkersPage() - expect(screen.getByText(/No workers yet/)).toBeVisible() + expect(screen.getByText('Deploy your first worker')).toBeVisible() expect(screen.queryByRole('table')).not.toBeInTheDocument() }) @@ -105,9 +105,7 @@ describe('/project/[ref]/workers', () => { await renderWorkersPage() expect( - screen.getByText( - `You need additional permissions to view this project's ${PRODUCT_NAME} workers` - ) + screen.getByText("You need additional permissions to view this project's workers") ).toBeVisible() expect(screen.queryByRole('table')).not.toBeInTheDocument() })