diff --git a/apps/docs/public/humans.txt b/apps/docs/public/humans.txt index 964214306f51e..78de85cb5b91b 100644 --- a/apps/docs/public/humans.txt +++ b/apps/docs/public/humans.txt @@ -273,6 +273,7 @@ Sana Cordeaux Sasi Kanumuri Sara Read Satya Rohith Gannamanedi +Sean Geoghegan Sean Oliver Sean Romberg Sean Thompson diff --git a/apps/studio/TANSTACK_MIGRATION.md b/apps/studio/TANSTACK_MIGRATION.md index e41b2b124dde4..0fdd44ca2006c 100644 --- a/apps/studio/TANSTACK_MIGRATION.md +++ b/apps/studio/TANSTACK_MIGRATION.md @@ -122,6 +122,7 @@ These are the layout-only TanStack files. Most hold a single product layout comp - [x] A `routes/_app/org/$slug/index.tsx` ← `pages/org/[slug]/index.tsx` - [x] A `routes/_app/org/$slug/apps.tsx` ← `pages/org/[slug]/apps.tsx` - [x] A `routes/_app/org/$slug/audit.tsx` ← `pages/org/[slug]/audit.tsx` +- [x] A `routes/_app/org/$slug/audit-log-drains.tsx` ← `pages/org/[slug]/audit-log-drains.tsx` (wraps in OrganizationSettingsLayout inline) - [x] A `routes/_app/org/$slug/billing.tsx` ← `pages/org/[slug]/billing.tsx` - [x] A `routes/_app/org/$slug/documents.tsx` ← `pages/org/[slug]/documents.tsx` - [x] A `routes/_app/org/$slug/general.tsx` ← `pages/org/[slug]/general.tsx` @@ -170,6 +171,7 @@ These are the layout-only TanStack files. Most hold a single product layout comp - [x] A `routes/project/$ref/database/functions.tsx` ← `pages/project/[ref]/database/functions.tsx` - [x] A `routes/project/$ref/database/indexes.tsx` ← `pages/project/[ref]/database/indexes.tsx` - [x] A `routes/project/$ref/database/migrations.tsx` ← `pages/project/[ref]/database/migrations.tsx` +- [x] A `routes/project/$ref/database/policies.tsx` ← `pages/project/[ref]/database/policies.tsx` - [x] A `routes/project/$ref/database/roles.tsx` ← `pages/project/[ref]/database/roles.tsx` - [x] A `routes/project/$ref/database/settings.tsx` ← `pages/project/[ref]/database/settings.tsx` - [x] A `routes/project/$ref/database/types.tsx` ← `pages/project/[ref]/database/types.tsx` @@ -192,7 +194,6 @@ These are the layout-only TanStack files. Most hold a single product layout comp - [x] A `routes/project/$ref/auth/overview.tsx` ← `pages/project/[ref]/auth/overview.tsx` - [x] A `routes/project/$ref/auth/users.tsx` ← `pages/project/[ref]/auth/users.tsx` -- [x] A `routes/project/$ref/auth/policies.tsx` ← `pages/project/[ref]/auth/policies.tsx` - [x] A `routes/project/$ref/auth/providers.tsx` ← `pages/project/[ref]/auth/providers.tsx` (sets `skipAuthLayout: true`, wraps in `AuthProvidersLayout` directly) - [x] A `routes/project/$ref/auth/mfa.tsx` ← `pages/project/[ref]/auth/mfa.tsx` - [x] A `routes/project/$ref/auth/hooks.tsx` ← `pages/project/[ref]/auth/hooks.tsx` @@ -258,6 +259,7 @@ These are the layout-only TanStack files. Most hold a single product layout comp - [x] A `routes/project/$ref/logs/dedicated-pooler-logs.tsx` ← `pages/project/[ref]/logs/dedicated-pooler-logs.tsx` - [x] A `routes/project/$ref/logs/edge-functions-logs.tsx` ← `pages/project/[ref]/logs/edge-functions-logs.tsx` - [x] A `routes/project/$ref/logs/edge-logs.tsx` ← `pages/project/[ref]/logs/edge-logs.tsx` +- [x] A `routes/project/$ref/logs/multigres-logs.tsx` ← `pages/project/[ref]/logs/multigres-logs.tsx` - [x] A `routes/project/$ref/logs/pg-upgrade-logs.tsx` ← `pages/project/[ref]/logs/pg-upgrade-logs.tsx` - [x] A `routes/project/$ref/logs/pgcron-logs.tsx` ← `pages/project/[ref]/logs/pgcron-logs.tsx` - [x] A `routes/project/$ref/logs/pooler-logs.tsx` ← `pages/project/[ref]/logs/pooler-logs.tsx` @@ -278,6 +280,7 @@ These are the layout-only TanStack files. Most hold a single product layout comp - [x] A `routes/project/$ref/observability/auth.tsx` ← `pages/project/[ref]/observability/auth.tsx` - [x] A `routes/project/$ref/observability/database.tsx` ← `pages/project/[ref]/observability/database.tsx` - [x] A `routes/project/$ref/observability/api-overview.tsx` ← `pages/project/[ref]/observability/api-overview.tsx` +- [x] A `routes/project/$ref/observability/connections.tsx` ← `pages/project/[ref]/observability/connections.tsx` - [x] A `routes/project/$ref/observability/edge-functions.tsx` ← `pages/project/[ref]/observability/edge-functions.tsx` - [x] A `routes/project/$ref/observability/postgrest.tsx` ← `pages/project/[ref]/observability/postgrest.tsx` - [x] A `routes/project/$ref/observability/query-insights.tsx` ← `pages/project/[ref]/observability/query-insights.tsx` @@ -298,6 +301,7 @@ These are the layout-only TanStack files. Most hold a single product layout comp - [x] A `routes/project/$ref/settings/addons.tsx` ← `pages/project/[ref]/settings/addons.tsx` - [x] A `routes/project/$ref/settings/api.tsx` ← `pages/project/[ref]/settings/api.tsx` (sets `skipSettingsLayout: true` — page is a useEffect redirect) - [x] A `routes/project/$ref/settings/dashboard.tsx` ← `pages/project/[ref]/settings/dashboard.tsx` +- [x] A `routes/project/$ref/settings/code-configuration.tsx` ← `pages/project/[ref]/settings/code-configuration.tsx` - [x] A `routes/project/$ref/settings/infrastructure/index.tsx` ← `pages/project/[ref]/settings/infrastructure.tsx` - [x] A `routes/project/$ref/settings/infrastructure/replica/$replicaId.tsx` ← `pages/project/[ref]/settings/infrastructure/replica/[replicaId].tsx` - [x] A `routes/project/$ref/settings/integrations.tsx` ← `pages/project/[ref]/settings/integrations.tsx` @@ -322,7 +326,7 @@ These are the layout-only TanStack files. Most hold a single product layout comp - [x] A `routes/project/$ref/sql/index.tsx` ← `pages/project/[ref]/sql/index.tsx` - [x] A `routes/project/$ref/sql/$id.tsx` ← `pages/project/[ref]/sql/[id].tsx` - [x] A `routes/project/$ref/sql/templates.tsx` ← `pages/project/[ref]/sql/templates.tsx` -- [x] A `routes/project/$ref/sql/quickstarts.tsx` ← `pages/project/[ref]/sql/quickstarts.tsx` +- [x] A `routes/project/$ref/sql/examples.tsx` ← `pages/project/[ref]/sql/examples.tsx` ### Project shell — `/editor/*` @@ -335,6 +339,7 @@ These are the layout-only TanStack files. Most hold a single product layout comp - [x] A `routes/project/$ref/explorer/index.tsx` ← `pages/project/[ref]/explorer/index.tsx` - [x] A `routes/project/$ref/explorer/notebook/$id.tsx` ← `pages/project/[ref]/explorer/notebook/[id].tsx` - [x] A `routes/project/$ref/explorer/chat/$id.tsx` ← `pages/project/[ref]/explorer/chat/[id].tsx` +- [x] A `routes/project/$ref/explorer/query/$id.tsx` ← `pages/project/[ref]/explorer/query/[id].tsx` ### Auth shell — `/sign-in`, `/sign-up`, etc. @@ -356,6 +361,7 @@ These are the layout-only TanStack files. Most hold a single product layout comp - [x] A `routes/redeem.tsx` ← `pages/redeem.tsx` (RedeemCreditsLayout) - [x] A `routes/logout.tsx` ← `pages/logout.tsx` - [x] A `routes/maintenance.tsx` ← `pages/maintenance.tsx` +- [x] A `routes/verify-email.tsx` ← `pages/verify-email.tsx` ### Error pages (handled at root) diff --git a/apps/studio/components/interfaces/Workers/WorkerDetail/WorkerDetail.tsx b/apps/studio/components/interfaces/Workers/WorkerDetail/WorkerDetail.tsx index 88e8de4d71b45..cca4bac374e18 100644 --- a/apps/studio/components/interfaces/Workers/WorkerDetail/WorkerDetail.tsx +++ b/apps/studio/components/interfaces/Workers/WorkerDetail/WorkerDetail.tsx @@ -118,7 +118,7 @@ export const WorkerDetail = () => { {worker.imageVersion !== undefined && ( - Image {worker.imageVersion} + Version {worker.imageVersion} )} diff --git a/apps/studio/components/interfaces/Workers/WorkerDetail/WorkerOverviewTab.tsx b/apps/studio/components/interfaces/Workers/WorkerDetail/WorkerOverviewTab.tsx index 39ee28eb50900..dc38d030041a5 100644 --- a/apps/studio/components/interfaces/Workers/WorkerDetail/WorkerOverviewTab.tsx +++ b/apps/studio/components/interfaces/Workers/WorkerDetail/WorkerOverviewTab.tsx @@ -18,7 +18,7 @@ import { LISTENING_PORT, WORKERS_REGION_LABEL } from '../Workers.constants' import type { Worker } from '../Workers.types' import { formatSize, getRuntimeMeta } from '../Workers.utils' import { buildWorkerCliCommands } from '../workerSnippets' -import { WorkerSnippetTabs } from '../WorkerSnippetTabs' +import { WORKER_CALL_TABS, WorkerSnippetTabs } from '../WorkerSnippetTabs' import { CLI_NAME } from '@/lib/constants/workers' interface WorkerOverviewTabProps { @@ -156,7 +156,7 @@ export const WorkerOverviewTab = ({ worker }: WorkerOverviewTabProps) => { {worker.imageVersion !== undefined && ( - + {worker.imageVersion} @@ -215,9 +215,7 @@ export const WorkerOverviewTab = ({ worker }: WorkerOverviewTabProps) => { How to call - - Call the worker over its gateway URL. Pass your project API key as a bearer token. - + Call the worker over its gateway URL. @@ -229,7 +227,7 @@ export const WorkerOverviewTab = ({ worker }: WorkerOverviewTabProps) => { access: worker.access, instances: worker.declaredInstances, }} - tabs={['cli', 'js', 'python']} + tabs={WORKER_CALL_TABS} /> diff --git a/apps/studio/components/interfaces/Workers/WorkerSnippetTabs.tsx b/apps/studio/components/interfaces/Workers/WorkerSnippetTabs.tsx index 08f181338de2a..d97a36255e45d 100644 --- a/apps/studio/components/interfaces/Workers/WorkerSnippetTabs.tsx +++ b/apps/studio/components/interfaces/Workers/WorkerSnippetTabs.tsx @@ -7,12 +7,19 @@ import { buildWorkerSnippets, type WorkerSnippetInput } from './workerSnippets' import CopyButton from '@/components/ui/CopyButton' import { useProjectSettingsV2Query } from '@/data/config/project-settings-v2-query' -export type WorkerSnippetTab = 'ai' | 'config' | 'cli' | 'js' | 'python' +export type WorkerSnippetTab = 'ai' | 'config' | 'cli' | 'curl' | 'js' | 'python' + +export const WORKER_CALL_TABS = [ + 'curl', + 'js', + 'python', +] as const satisfies readonly WorkerSnippetTab[] const TAB_LABEL: Record = { ai: 'AI Prompt', config: 'config.toml', cli: 'CLI', + curl: 'cURL', js: 'JavaScript', python: 'Python', } @@ -21,13 +28,14 @@ const TAB_ICON: Record = { ai: Sparkles, config: FileCode, cli: Terminal, + curl: Terminal, js: FileCode, python: FileCode, } interface WorkerSnippetTabsProps { input: Omit - tabs?: [WorkerSnippetTab, ...WorkerSnippetTab[]] + tabs?: readonly [WorkerSnippetTab, ...WorkerSnippetTab[]] className?: string } @@ -45,6 +53,7 @@ export const WorkerSnippetTabs = ({ input, tabs = ['cli'], className }: WorkerSn ai: snippets.aiPrompt, config: snippets.configToml, cli: snippets.cli, + curl: snippets.curl, js: snippets.javascript, python: snippets.python, } diff --git a/apps/studio/components/interfaces/Workers/workerSnippets.test.ts b/apps/studio/components/interfaces/Workers/workerSnippets.test.ts index 7070d5f014db6..77f434a143731 100644 --- a/apps/studio/components/interfaces/Workers/workerSnippets.test.ts +++ b/apps/studio/components/interfaces/Workers/workerSnippets.test.ts @@ -11,16 +11,21 @@ const input = (overrides: Partial[0]> = { describe('buildWorkerSnippets', () => { it('points the invoke examples at the worker URL', () => { - const { javascript, python } = buildWorkerSnippets(input({ name: 'embed' })) + 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 invoke URL until the project settings resolve', () => { - const { javascript } = buildWorkerSnippets(input({ endpoint: undefined })) - expect(javascript).toContain('[YOUR WORKER URL]') + const { curl } = buildWorkerSnippets(input({ endpoint: undefined })) + expect(curl).toContain('[YOUR WORKER URL]') + }) + + it('does not require authorization for the CLI invoke example', () => { + expect(buildWorkerSnippets(input()).curl).not.toContain('Authorization') }) it('asks for the anon key to invoke a public worker and the service role key for a private one', () => { diff --git a/apps/studio/components/interfaces/Workers/workerSnippets.ts b/apps/studio/components/interfaces/Workers/workerSnippets.ts index de89c173050a4..7cc337d332c39 100644 --- a/apps/studio/components/interfaces/Workers/workerSnippets.ts +++ b/apps/studio/components/interfaces/Workers/workerSnippets.ts @@ -17,6 +17,7 @@ export interface WorkerSnippets { aiPrompt: string configToml: string cli: string + curl: string javascript: string python: string } @@ -46,6 +47,12 @@ export function buildWorkerSnippets(input: WorkerSnippetInput): WorkerSnippets { ...(input.access === 'private' ? [`# note: the CLI can only deploy public workers today`] : []), ].join('\n') + const curl = [ + `curl --request POST '${url}' \\`, + ` --header 'Content-Type: application/json' \\`, + ` --data '{"name":"world"}'`, + ].join('\n') + const configBlock = [ `[${CLI_NAME}.${name}]`, `runtime = "${runtime}"`, @@ -102,7 +109,7 @@ export function buildWorkerSnippets(input: WorkerSnippetInput): WorkerSnippets { `print(res.json())`, ].join('\n') - return { aiPrompt, configToml, cli, javascript, python } + return { aiPrompt, configToml, cli, curl, javascript, python } } export interface WorkerCliCommand { diff --git a/apps/studio/redirects.shared.test.ts b/apps/studio/redirects.shared.test.ts index 5667989470f00..fa08c3a640af0 100644 --- a/apps/studio/redirects.shared.test.ts +++ b/apps/studio/redirects.shared.test.ts @@ -157,3 +157,62 @@ describe('matchRedirect query/hash preservation', () => { ).toBeNull() }) }) + +describe('matchRedirect maintenance mode', () => { + it('sends every other path to /maintenance when enabled', () => { + expect( + matchRedirect({ + pathname: '/project/abc/editor', + search: {}, + isPlatform: true, + maintenanceMode: true, + }) + ).toEqual({ destination: '/maintenance', permanent: false }) + }) + + it('carries query and hash onto /maintenance', () => { + expect( + matchRedirect({ + pathname: '/project/abc/editor', + search: { a: '1' }, + isPlatform: true, + maintenanceMode: true, + hash: 'section', + }) + ).toEqual({ destination: '/maintenance?a=1#section', permanent: false }) + }) + + it('leaves /maintenance and /img reachable when enabled', () => { + expect( + matchRedirect({ + pathname: '/maintenance', + search: {}, + isPlatform: true, + maintenanceMode: true, + }) + ).toBeNull() + expect( + matchRedirect({ + pathname: '/img/supabase-logo.svg', + search: {}, + isPlatform: true, + maintenanceMode: true, + }) + ).toBeNull() + }) + + it('bounces /maintenance back to / when disabled', () => { + expect(matchRedirect({ pathname: '/maintenance', search: {}, isPlatform: true })).toEqual({ + destination: '/', + permanent: false, + }) + expect( + matchRedirect({ + pathname: '/maintenance', + search: {}, + isPlatform: true, + maintenanceMode: false, + }) + ).toEqual({ destination: '/', permanent: false }) + }) +}) diff --git a/apps/studio/routes/__root.tsx b/apps/studio/routes/__root.tsx index 4d1c42e7c4556..9c2e251ef9e81 100644 --- a/apps/studio/routes/__root.tsx +++ b/apps/studio/routes/__root.tsx @@ -139,6 +139,12 @@ const IS_NON_PROD_ENV = process.env.NEXT_PUBLIC_ENVIRONMENT === 'local' || process.env.NEXT_PUBLIC_ENVIRONMENT === 'staging' +// Mirrors the `MAINTENANCE_MODE` reads in `next.config.ts` and `vercel.ts`. +// The var is unprefixed, so vite.config.ts inlines it explicitly (see the +// define there) rather than it arriving via the NEXT_PUBLIC_ sweep — that +// keeps the toggle a single build-time env var across all three runtimes. +const IS_MAINTENANCE_MODE = process.env.MAINTENANCE_MODE === 'true' + // Keep dev-only components out of the production bundle. const IS_DEV_TOOLBAR_ENABLED = IS_NON_PROD_ENV @@ -329,6 +335,7 @@ export const Route = createRootRouteWithContext()({ pathname: location.pathname, search: location.search as Record, isPlatform: IS_PLATFORM, + maintenanceMode: IS_MAINTENANCE_MODE, hash: location.hash, }) if (!match) return diff --git a/apps/studio/vite.config.ts b/apps/studio/vite.config.ts index a238efe54a371..b7bd19442e17f 100644 --- a/apps/studio/vite.config.ts +++ b/apps/studio/vite.config.ts @@ -602,6 +602,19 @@ export default defineConfig(({ command, mode }) => { } } + // `MAINTENANCE_MODE` gates the "redirect everything to /maintenance" rule. + // It's deliberately unprefixed, and the other two consumers both read it at + // BUILD time: `next.config.ts` reads it in `redirects()`, which Next bakes + // into `routes-manifest.json` during `next build`, and `vercel.ts` reads it + // while emitting `vercel.json`. So flipping maintenance has always meant a + // rebuild/redeploy, never just a server restart. Inline it here on the same + // terms so the isomorphic `beforeLoad` in `routes/__root.tsx` — which + // mirrors those rules for the TanStack runtime — can read it on the client + // too, without self-hosters having to set a second, NEXT_PUBLIC_-prefixed + // var. Falls back to `''` (not left undefined) so the browser bundle never + // ends up with a bare `process.env` reference. + publicEnvDefines['process.env.MAINTENANCE_MODE'] = JSON.stringify(env.MAINTENANCE_MODE ?? '') + // Sentry init (lib/sentry-client-options.ts, reached via router.tsx) reads // these at runtime in the browser. When a var is unset it gets no define // entry above, which would leave a literal `process.env.*` in the built