From 7353782724d837be316f0f6f87471ed176ab4bfd Mon Sep 17 00:00:00 2001 From: Ali Waseem Date: Sat, 3 Oct 2026 14:34:14 -0600 Subject: [PATCH] fix(studio): show compute waitlist notice when project is not enrolled (#51224) Resolves FUNC-941 ## What When a project isn't enrolled in Compute, show a short notice with a link to the waitlist (https://supabase.com/compute) instead of a generic error. ## Why The compute API returns its 404 as `{ "error": { "code", "message" } }`. `handleError` only reads a top-level `message`, so it dropped the status code, `isComputeUnavailable` never matched, and users saw "API error happened while trying to communicate with the server." The query now checks `response.status === 404` directly. ## Testing - [ ] Project not enrolled in Compute: the waitlist notice shows - [ ] Enrolled project: the instance list loads as before --- apps/studio/data/compute/compute-query.ts | 7 +++-- .../pages/project/[ref]/compute/index.tsx | 18 ++++++++++-- .../project/[ref]/compute/index.test.tsx | 28 +++++++++++++++---- 3 files changed, 43 insertions(+), 10 deletions(-) diff --git a/apps/studio/data/compute/compute-query.ts b/apps/studio/data/compute/compute-query.ts index f372e317254..cd6f58ab981 100644 --- a/apps/studio/data/compute/compute-query.ts +++ b/apps/studio/data/compute/compute-query.ts @@ -4,7 +4,7 @@ import { computeRefetchInterval, parseComputeInstance } from './compute.utils' import { computeKeys } from './keys' import { get, handleError } from '@/data/fetchers' import { IS_PLATFORM } from '@/lib/constants' -import type { ResponseError } from '@/types' +import { ResponseError } from '@/types' export type ComputeVariables = { projectRef?: string } export type ComputeError = ResponseError @@ -12,11 +12,14 @@ export type ComputeError = ResponseError async function getComputeInstances({ projectRef }: ComputeVariables, signal?: AbortSignal) { if (!projectRef) throw new Error('projectRef is required') - const { data, error } = await get('/v2/projects/{ref}/compute', { + const { data, error, response } = await get('/v2/projects/{ref}/compute', { params: { path: { ref: projectRef } }, signal, }) + if (response.status === 404) { + throw new ResponseError('Compute is not available for this project', 404) + } if (error) return handleError(error) return data.data.map((instance) => parseComputeInstance(instance)) } diff --git a/apps/studio/pages/project/[ref]/compute/index.tsx b/apps/studio/pages/project/[ref]/compute/index.tsx index d035f401cf1..38fc9a1786d 100644 --- a/apps/studio/pages/project/[ref]/compute/index.tsx +++ b/apps/studio/pages/project/[ref]/compute/index.tsx @@ -1,6 +1,6 @@ import { useQuery } from '@tanstack/react-query' import { useParams } from 'common' -import { RefreshCw } from 'lucide-react' +import { ExternalLink, RefreshCw } from 'lucide-react' import { useState } from 'react' import { Button } from 'ui' import { Admonition } from 'ui-patterns/Admonition' @@ -79,8 +79,20 @@ const ComputePage: NextPageWithLayout = () => { {isNotEnrolled && ( }> + + Join waitlist + + + } /> )} {isMissingPermission && ( diff --git a/apps/studio/tests/pages/project/[ref]/compute/index.test.tsx b/apps/studio/tests/pages/project/[ref]/compute/index.test.tsx index b0daea83a8a..3742f8c4a46 100644 --- a/apps/studio/tests/pages/project/[ref]/compute/index.test.tsx +++ b/apps/studio/tests/pages/project/[ref]/compute/index.test.tsx @@ -1,14 +1,15 @@ import { QueryClient } from '@tanstack/react-query' import { fireEvent, screen } from '@testing-library/react' import type { components } from 'api-types' -import { HttpResponse } from 'msw' +import { http, HttpResponse } from 'msw' import { beforeEach, describe, expect, it } from 'vitest' import { computeQueryOptions } from '@/data/compute/compute-query' +import { API_URL } from '@/lib/constants' import { PRODUCT_NAME } from '@/lib/constants/compute' import ComputePage from '@/pages/project/[ref]/compute/index' import { customRender } from '@/tests/lib/custom-render' -import { addAPIMock, type APIErrorBody } from '@/tests/lib/msw' +import { addAPIMock, mswServer, type APIErrorBody } from '@/tests/lib/msw' import { routerMock } from '@/tests/lib/route-mock' type ListComputeInstancesResponse = components['schemas']['V2ListComputeInstancesResponse_Output'] @@ -101,12 +102,29 @@ describe('/project/[ref]/compute', () => { expect(await screen.findByRole('link', { name: 'embed' })).toBeVisible() }) - it('explains that a project outside the alpha is not enrolled', async () => { - mockComputeInstancesListFailure(404) + it('points a project outside the alpha to the waitlist', async () => { + mswServer.use( + http.get(`${API_URL}/v2/projects/:ref/compute`, () => + HttpResponse.json( + { + error: { + code: 'not_found.compute.not_enabled', + message: 'Compute is not available for this project', + }, + }, + { status: 404 } + ) + ) + ) await renderComputePage() - expect(screen.getByText(`${PRODUCT_NAME} is not enabled for this project`)).toBeVisible() + expect(screen.getByText(`You don't have access to ${PRODUCT_NAME} yet`)).toBeVisible() + expect(screen.getByRole('link', { name: 'Join waitlist' })).toHaveAttribute( + 'href', + 'https://supabase.com/compute' + ) + expect(screen.queryByText('Failed to retrieve compute instances')).not.toBeInTheDocument() expect(screen.queryByRole('table')).not.toBeInTheDocument() })