mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 09:25:06 +03:00
**Stack 5.1/6** of the TanStack Start migration (#46424). The original S5 (174 files) was over CodeRabbit's 150-file review cap, so it's split into 5.1 + 5.2 by product. Stacked on **#47113** (S4). > [!NOTE] > Thin route wrappers rendering the existing pages-router components via compat shims. Next-safe (full Next build run). The TanStack app isn't functional end-to-end until 5.2 + the matrix flip. ## What's in this PR - **Data-cluster project routes:** database, editor, sql, storage, realtime, branches. - **Top-level / onboarding routes:** `authorize`, `join`, `logout`, `redeem`, `verify-email`, `claim-project`, aws-marketplace, Vercel/GitHub integration entrypoints; `_app`/`_auth` layout shells; `/org/_` + `/project/_` catch-alls. - **Supporting edits:** hoist `BranchesPageWrapper` out of `getLayout`, `ConnectStepsSection` `import.meta.glob`, `api/server.js`. - `routeTree.gen.ts` regenerated for the routes present so far. ## Verification On top of S1–S4: `studio` typecheck ✓, lint (0 errors) ✓, **Next build ✓ (181/181 pages)**. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Refactor** * Restructured application routing infrastructure for improved code organization and maintainability. * Extracted and refactored layout wrapper components for enhanced reusability across different sections of the application. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Alaister Young <10985857+alaister@users.noreply.github.com> Co-authored-by: Ivan Vasilov <vasilov.ivan@gmail.com>
This commit is contained in:
80 files changed
+3112
-153
No files matched your search
@@ -0,0 +1,25 @@
|
||||
// STUDIO_FRAMEWORK gates whether this function actually serves the TanStack
|
||||
// SSR handler. Vercel auto-detects every file under /api as a Function
|
||||
// regardless of the framework preset (vercel.com/docs/functions), so we
|
||||
// can't keep this file from being deployed in the Next.js prod build — we
|
||||
// just make it inert when the env var is unset.
|
||||
const isTanstack = process.env.STUDIO_FRAMEWORK === 'tanstack'
|
||||
|
||||
// Computed path keeps `dist/server/server.js` out of Vercel's function
|
||||
// bundler's static analysis. In the Next.js prod deploy the `dist/` tree
|
||||
// doesn't exist, but Vercel still bundles this file because it lives under
|
||||
// `api/`. With the .join() the bundler treats the import as runtime-only
|
||||
// and the missing dist/ isn't a build error. In TanStack mode the SSR
|
||||
// bundle is shipped into the function via the `functions['api/server.js']
|
||||
// .includeFiles` config in vercel.ts.
|
||||
const tanstackEntry = ['..', 'dist', 'server', 'server.js'].join('/')
|
||||
|
||||
const handler = isTanstack
|
||||
? (await import(tanstackEntry)).default
|
||||
: { fetch: () => new Response('Not Found', { status: 404 }) }
|
||||
|
||||
// Vercel's Web API handler convention: export an object with `fetch(request)`.
|
||||
// TanStack's server build is already shaped that way — default-export it
|
||||
// verbatim and Vercel hands us a real Web Request.
|
||||
// eslint-disable-next-line no-restricted-exports
|
||||
export default handler
|
||||
@@ -1,12 +1,11 @@
|
||||
import { useParams } from 'common'
|
||||
import dynamic from 'next/dynamic'
|
||||
import Link from 'next/link'
|
||||
import { useMemo, useRef } from 'react'
|
||||
import { useMemo, useRef, type ComponentType } from 'react'
|
||||
import { Button } from 'ui'
|
||||
import { Admonition } from 'ui-patterns/admonition'
|
||||
import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader'
|
||||
|
||||
import { connectSchema } from './connect.schema'
|
||||
import type {
|
||||
ConnectionStringPooler,
|
||||
ConnectState,
|
||||
@@ -129,6 +128,27 @@ function useConnectionStringPooler(deploymentMode: DeploymentMode): ConnectionSt
|
||||
)
|
||||
}
|
||||
|
||||
// Vite needs `import.meta.glob` to statically discover the step content
|
||||
// modules because the `${filePath}` template can span multiple directory
|
||||
// segments (`flask/supabasepy`, `steps/shadcn/explore`, ...) which Vite's
|
||||
// dynamic-import-vars plugin can't analyze. Skip the glob on the SSR bundle
|
||||
// — Vite replaces `import.meta.env.SSR` at build time and tree-shakes the
|
||||
// call so the 37 content modules stay out of the server graph (pulling them
|
||||
// in reshuffles chunks enough to surface latent circular-dep bugs in
|
||||
// unrelated modules). Next/webpack doesn't know about `import.meta.glob`
|
||||
// either; the try/catch lets that branch fall through to the webpack-friendly
|
||||
// `import()` below.
|
||||
let contentModules: Record<string, () => Promise<unknown>> = {}
|
||||
if (!import.meta.env?.SSR) {
|
||||
try {
|
||||
contentModules = import.meta.glob('./content/**/content.{tsx,ts}')
|
||||
} catch {
|
||||
// webpack build: import.meta.glob is undefined, keep empty map
|
||||
}
|
||||
}
|
||||
|
||||
type StepContentModule = { default: ComponentType<StepContentProps> }
|
||||
|
||||
/**
|
||||
* Dynamically loads and renders a content component from the content directory.
|
||||
* All step content uses this unified loader - no built-in component registry needed.
|
||||
@@ -151,7 +171,16 @@ function StepContent({
|
||||
|
||||
// Dynamically import the content component
|
||||
const ContentComponent = useMemo(() => {
|
||||
return dynamic<StepContentProps>(() => import(`./content/${filePath}/content`), {
|
||||
const viteLoader =
|
||||
contentModules[`./content/${filePath}/content.tsx`] ??
|
||||
contentModules[`./content/${filePath}/content.ts`]
|
||||
|
||||
const loader = viteLoader
|
||||
? (viteLoader as () => Promise<StepContentModule>)
|
||||
: () =>
|
||||
import(/* @vite-ignore */ `./content/${filePath}/content`) as Promise<StepContentModule>
|
||||
|
||||
return dynamic<StepContentProps>(loader, {
|
||||
loading: () => (
|
||||
<div className="p-4 min-h-[200px]">
|
||||
<GenericSkeletonLoader />
|
||||
@@ -196,11 +225,6 @@ export function ConnectStepsSection({ steps, state, projectKeys }: ConnectStepsS
|
||||
|
||||
const showSelfHostedMcpNotice = deploymentMode.isSelfHosted && state.mode === 'mcp'
|
||||
|
||||
const customPrompt = useMemo(
|
||||
() => connectSchema.modes.find((m) => m.id === state.mode)?.prompt,
|
||||
[state.mode]
|
||||
)
|
||||
|
||||
if (steps.length === 0) return null
|
||||
|
||||
return (
|
||||
@@ -244,7 +268,7 @@ export function ConnectStepsSection({ steps, state, projectKeys }: ConnectStepsS
|
||||
/>
|
||||
)}
|
||||
|
||||
<CopyPromptAdmonition stepsContainerRef={stepsContainerRef} customPrompt={customPrompt} />
|
||||
<CopyPromptAdmonition stepsContainerRef={stepsContainerRef} />
|
||||
|
||||
<div className="mt-6" ref={stepsContainerRef}>
|
||||
{steps.map((step, index) => (
|
||||
|
||||
@@ -3,6 +3,7 @@ import Head from 'next/head'
|
||||
import { useRouter } from 'next/router'
|
||||
import { cn } from 'ui'
|
||||
|
||||
import { parseCatchAllRoute } from '@/compat/next/router'
|
||||
import {
|
||||
Header,
|
||||
LoadingCardView,
|
||||
@@ -20,7 +21,10 @@ import { buildStudioPageTitle } from '@/lib/page-title'
|
||||
const GenericOrganizationPage: NextPage = () => {
|
||||
const router = useRouter()
|
||||
const { appTitle } = useCustomContent(['app:title'])
|
||||
const { routeSlug, ...queryParams } = router.query
|
||||
// Normalise the catch-all path across Next (`routeSlug: string[]`) and
|
||||
// TanStack (`_splat: string`) so downstream URL building (buildOrgUrl)
|
||||
// keeps working — see parseCatchAllRoute.
|
||||
const { segments: routeSlug, queryParams } = parseCatchAllRoute(router.query, 'routeSlug')
|
||||
const queryString =
|
||||
Object.keys(queryParams).length > 0
|
||||
? new URLSearchParams(queryParams as Record<string, string>).toString()
|
||||
|
||||
@@ -3,7 +3,7 @@ import { useParams } from 'common'
|
||||
import { partition } from 'lodash'
|
||||
import { MessageCircle } from 'lucide-react'
|
||||
import { useRouter } from 'next/router'
|
||||
import { useState } from 'react'
|
||||
import { useState, type PropsWithChildren } from 'react'
|
||||
import { toast } from 'sonner'
|
||||
import { Button } from 'ui'
|
||||
|
||||
@@ -189,73 +189,74 @@ const BranchesPage: NextPageWithLayout = () => {
|
||||
)
|
||||
}
|
||||
|
||||
BranchesPage.getLayout = (page) => {
|
||||
const BranchesPageWrapper = () => {
|
||||
const snap = useAppStateSnapshot()
|
||||
const { can: canCreateBranches } = useAsyncCheckPermissions(
|
||||
PermissionAction.CREATE,
|
||||
'preview_branches',
|
||||
{
|
||||
resource: { is_default: false },
|
||||
}
|
||||
)
|
||||
// Hoisted out of `getLayout` so the TanStack route can import it
|
||||
// directly. Same shape and identical body as before — accepts the page
|
||||
// content as `children` instead of capturing it from a closure.
|
||||
export const BranchesPageWrapper = ({ children }: PropsWithChildren) => {
|
||||
const snap = useAppStateSnapshot()
|
||||
const { can: canCreateBranches } = useAsyncCheckPermissions(
|
||||
PermissionAction.CREATE,
|
||||
'preview_branches',
|
||||
{
|
||||
resource: { is_default: false },
|
||||
}
|
||||
)
|
||||
|
||||
const primaryActions = (
|
||||
<ButtonTooltip
|
||||
variant="primary"
|
||||
disabled={!canCreateBranches}
|
||||
onClick={() => snap.setShowCreateBranchModal(true)}
|
||||
tooltip={{
|
||||
content: {
|
||||
side: 'bottom',
|
||||
text: !canCreateBranches
|
||||
? 'You need additional permissions to create branches'
|
||||
: undefined,
|
||||
},
|
||||
}}
|
||||
const primaryActions = (
|
||||
<ButtonTooltip
|
||||
variant="primary"
|
||||
disabled={!canCreateBranches}
|
||||
onClick={() => snap.setShowCreateBranchModal(true)}
|
||||
tooltip={{
|
||||
content: {
|
||||
side: 'bottom',
|
||||
text: !canCreateBranches
|
||||
? 'You need additional permissions to create branches'
|
||||
: undefined,
|
||||
},
|
||||
}}
|
||||
>
|
||||
Create branch
|
||||
</ButtonTooltip>
|
||||
)
|
||||
|
||||
const secondaryActions = (
|
||||
<div className="flex items-center gap-x-2">
|
||||
<Button
|
||||
asChild
|
||||
variant="text"
|
||||
icon={<MessageCircle className="text-muted" strokeWidth={1} />}
|
||||
>
|
||||
Create branch
|
||||
</ButtonTooltip>
|
||||
)
|
||||
|
||||
const secondaryActions = (
|
||||
<div className="flex items-center gap-x-2">
|
||||
<Button
|
||||
asChild
|
||||
variant="text"
|
||||
icon={<MessageCircle className="text-muted" strokeWidth={1} />}
|
||||
<a
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
href="https://github.com/orgs/supabase/discussions/18937"
|
||||
>
|
||||
<a
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
href="https://github.com/orgs/supabase/discussions/18937"
|
||||
>
|
||||
Branching feedback
|
||||
</a>
|
||||
</Button>
|
||||
<DocsButton href={`${DOCS_URL}/guides/platform/branching`} />
|
||||
</div>
|
||||
)
|
||||
|
||||
return (
|
||||
<PageLayout
|
||||
title="Branches"
|
||||
subtitle="Manage your database preview branches and deployments"
|
||||
primaryActions={primaryActions}
|
||||
secondaryActions={secondaryActions}
|
||||
>
|
||||
{page}
|
||||
</PageLayout>
|
||||
)
|
||||
}
|
||||
Branching feedback
|
||||
</a>
|
||||
</Button>
|
||||
<DocsButton href={`${DOCS_URL}/guides/platform/branching`} />
|
||||
</div>
|
||||
)
|
||||
|
||||
return (
|
||||
<DefaultLayout>
|
||||
<BranchLayout>
|
||||
<BranchesPageWrapper />
|
||||
</BranchLayout>
|
||||
</DefaultLayout>
|
||||
<PageLayout
|
||||
title="Branches"
|
||||
subtitle="Manage your database preview branches and deployments"
|
||||
primaryActions={primaryActions}
|
||||
secondaryActions={secondaryActions}
|
||||
>
|
||||
{children}
|
||||
</PageLayout>
|
||||
)
|
||||
}
|
||||
|
||||
BranchesPage.getLayout = (page) => (
|
||||
<DefaultLayout>
|
||||
<BranchLayout>
|
||||
<BranchesPageWrapper>{page}</BranchesPageWrapper>
|
||||
</BranchLayout>
|
||||
</DefaultLayout>
|
||||
)
|
||||
|
||||
export default BranchesPage
|
||||
@@ -300,7 +300,7 @@ const MergeRequestsPage: NextPageWithLayout = () => {
|
||||
)
|
||||
}
|
||||
|
||||
const MergeRequestsPageWrapper = ({ children }: PropsWithChildren<{}>) => {
|
||||
export const MergeRequestsPageWrapper = ({ children }: PropsWithChildren<{}>) => {
|
||||
const router = useRouter()
|
||||
const { ref } = useParams()
|
||||
const { data: project } = useSelectedProjectQuery()
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
} from 'ui'
|
||||
import { ShimmeringLoader } from 'ui-patterns/ShimmeringLoader'
|
||||
|
||||
import { parseCatchAllRoute } from '@/compat/next/router'
|
||||
import {
|
||||
Header,
|
||||
LoadingCardView,
|
||||
@@ -35,7 +36,10 @@ import { withAuth } from '@/hooks/misc/withAuth'
|
||||
const GenericProjectPage: NextPage = () => {
|
||||
const router = useRouter()
|
||||
const { slug } = useParams()
|
||||
const { routeSlug, ...queryParams } = router.query
|
||||
// Normalise the catch-all path across Next (`routeSlug: string[]`) and
|
||||
// TanStack (`_splat: string`) so downstream URL building
|
||||
// (urlRewriterFactory) keeps working — see parseCatchAllRoute.
|
||||
const { segments: routeSlug, queryParams } = parseCatchAllRoute(router.query, 'routeSlug')
|
||||
|
||||
const { lastVisitedOrganization } = useLastVisitedOrganization()
|
||||
|
||||
|
||||
+1765
-79
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,33 @@
|
||||
import { createFileRoute, Outlet, useMatches } from '@tanstack/react-router'
|
||||
|
||||
import { AppLayout } from '@/components/layouts/AppLayout/AppLayout'
|
||||
import { DefaultLayout } from '@/components/layouts/DefaultLayout'
|
||||
|
||||
export const Route = createFileRoute('/_app')({
|
||||
component: AppShell,
|
||||
})
|
||||
|
||||
function AppShell() {
|
||||
// useMatches() without `select` returns a fresh array on every router
|
||||
// tick — even when the matched leaf hasn't changed — and re-renders the
|
||||
// entire shell subtree. Use the `select` form so we only re-render when
|
||||
// the values we actually consume change.
|
||||
const headerTitle = useMatches({
|
||||
select: (matches) =>
|
||||
(matches[matches.length - 1]?.staticData as { defaultLayoutHeaderTitle?: string } | undefined)
|
||||
?.defaultLayoutHeaderTitle,
|
||||
})
|
||||
const hideMobileMenu = useMatches({
|
||||
select: (matches) =>
|
||||
(matches[matches.length - 1]?.staticData as { hideMobileMenu?: boolean } | undefined)
|
||||
?.hideMobileMenu,
|
||||
})
|
||||
|
||||
return (
|
||||
<AppLayout>
|
||||
<DefaultLayout headerTitle={headerTitle} hideMobileMenu={hideMobileMenu}>
|
||||
<Outlet />
|
||||
</DefaultLayout>
|
||||
</AppLayout>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { createFileRoute, Outlet } from '@tanstack/react-router'
|
||||
|
||||
import { AuthenticationLayout } from '@/components/layouts/AuthenticationLayout'
|
||||
|
||||
export const Route = createFileRoute('/_auth')({
|
||||
component: AuthShell,
|
||||
})
|
||||
|
||||
function AuthShell() {
|
||||
return (
|
||||
<AuthenticationLayout>
|
||||
<Outlet />
|
||||
</AuthenticationLayout>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
|
||||
import APIAuthorizationPage from '@/pages/authorize'
|
||||
|
||||
export const Route = createFileRoute('/authorize')({
|
||||
component: Authorize,
|
||||
})
|
||||
|
||||
function Authorize() {
|
||||
return <APIAuthorizationPage dehydratedState={undefined} />
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
|
||||
import AwsMarketplaceOnboardingPage from '@/pages/aws-marketplace-onboarding'
|
||||
|
||||
export const Route = createFileRoute('/aws-marketplace-onboarding')({
|
||||
component: AwsMarketplaceOnboardingRoute,
|
||||
})
|
||||
|
||||
function AwsMarketplaceOnboardingRoute() {
|
||||
return <AwsMarketplaceOnboardingPage dehydratedState={undefined} />
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
import Head from 'next/head'
|
||||
|
||||
import { useCustomContent } from '@/hooks/custom-content/useCustomContent'
|
||||
import ClaimProjectPage from '@/pages/claim-project'
|
||||
|
||||
export const Route = createFileRoute('/claim-project')({
|
||||
component: ClaimProject,
|
||||
})
|
||||
|
||||
function ClaimProject() {
|
||||
const { appTitle } = useCustomContent(['app:title'])
|
||||
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>{`Claim project | ${appTitle ?? 'Supabase'}`}</title>
|
||||
</Head>
|
||||
<main className="flex flex-col w-full min-h-screen overflow-y-auto">
|
||||
<ClaimProjectPage dehydratedState={undefined} />
|
||||
</main>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import { createFileRoute, redirect } from '@tanstack/react-router'
|
||||
|
||||
import { IS_PLATFORM } from '@/lib/constants'
|
||||
|
||||
// `/` is never rendered — it always redirects. Mirrors the Next.js
|
||||
// `redirects()` rules in next.config.ts: platform sends users to `/org`
|
||||
// (or `/new/new-project` when deep-linked with `?next=new-project`),
|
||||
// self-hosted sends them straight to `/project/default`.
|
||||
export const Route = createFileRoute('/')({
|
||||
validateSearch: (search: Record<string, unknown>) => ({
|
||||
next: typeof search.next === 'string' ? search.next : undefined,
|
||||
}),
|
||||
beforeLoad: ({ search }) => {
|
||||
// `href` instead of `to` because these targets aren't in the TanStack
|
||||
// routeTree yet — they're still on the Next.js pages side during the
|
||||
// migration. Swap to `to` once `/org`, `/new/new-project`, and
|
||||
// `/project/default` are migrated.
|
||||
if (IS_PLATFORM) {
|
||||
if (search.next === 'new-project') throw redirect({ href: '/new/new-project' })
|
||||
throw redirect({ href: '/org' })
|
||||
}
|
||||
throw redirect({ href: '/project/default' })
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,13 @@
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
|
||||
import GitHubIntegrationAuthorize from '@/pages/integrations/github/authorize'
|
||||
|
||||
export const Route = createFileRoute('/integrations/github/authorize')({
|
||||
component: GitHubAuthorizeRoute,
|
||||
})
|
||||
|
||||
// Placed at top-level — the Next page has no getLayout (renders bare),
|
||||
// so adding AppLayout/DefaultLayout via _app would be a behaviour change.
|
||||
function GitHubAuthorizeRoute() {
|
||||
return <GitHubIntegrationAuthorize />
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import { createFileRoute, Outlet } from '@tanstack/react-router'
|
||||
|
||||
import VercelIntegrationWindowLayout from '@/components/layouts/IntegrationsLayout/VercelIntegrationWindowLayout'
|
||||
|
||||
export const Route = createFileRoute('/integrations/vercel')({
|
||||
component: VercelIntegrationShell,
|
||||
})
|
||||
|
||||
// Placed at top-level rather than under _app — Next getLayout for all
|
||||
// three leaves only wraps in VercelIntegrationWindowLayout (no AppLayout
|
||||
// or DefaultLayout), so adding _app's chrome here would be a behaviour
|
||||
// change.
|
||||
function VercelIntegrationShell() {
|
||||
return (
|
||||
<VercelIntegrationWindowLayout>
|
||||
<Outlet />
|
||||
</VercelIntegrationWindowLayout>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
|
||||
import VercelIntegration from '@/pages/integrations/vercel/[slug]/deploy-button/new-project'
|
||||
|
||||
export const Route = createFileRoute('/integrations/vercel/$slug/deploy-button/new-project')({
|
||||
component: VercelDeployButtonNewProjectRoute,
|
||||
})
|
||||
|
||||
function VercelDeployButtonNewProjectRoute() {
|
||||
return <VercelIntegration dehydratedState={undefined} />
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
|
||||
import VercelIntegration from '@/pages/integrations/vercel/[slug]/marketplace/choose-project'
|
||||
|
||||
export const Route = createFileRoute('/integrations/vercel/$slug/marketplace/choose-project')({
|
||||
component: VercelChooseProjectRoute,
|
||||
})
|
||||
|
||||
function VercelChooseProjectRoute() {
|
||||
return <VercelIntegration dehydratedState={undefined} />
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
|
||||
import VercelIntegration from '@/pages/integrations/vercel/install'
|
||||
|
||||
export const Route = createFileRoute('/integrations/vercel/install')({
|
||||
component: VercelInstallRoute,
|
||||
})
|
||||
|
||||
function VercelInstallRoute() {
|
||||
return <VercelIntegration dehydratedState={undefined} />
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
import { cn } from 'ui'
|
||||
|
||||
import JoinOrganizationPage from '@/pages/join'
|
||||
|
||||
export const Route = createFileRoute('/join')({
|
||||
component: Join,
|
||||
})
|
||||
|
||||
function Join() {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'flex h-full min-h-screen bg-studio',
|
||||
'w-full flex-col place-items-center',
|
||||
'items-center justify-center gap-8 px-5'
|
||||
)}
|
||||
>
|
||||
<JoinOrganizationPage dehydratedState={undefined} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
|
||||
import LogoutPage from '@/pages/logout'
|
||||
|
||||
export const Route = createFileRoute('/logout')({
|
||||
component: Logout,
|
||||
})
|
||||
|
||||
function Logout() {
|
||||
return <LogoutPage dehydratedState={undefined} />
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
import { cn } from 'ui'
|
||||
|
||||
import MaintenancePage from '@/pages/maintenance'
|
||||
|
||||
export const Route = createFileRoute('/maintenance')({
|
||||
component: Maintenance,
|
||||
})
|
||||
|
||||
function Maintenance() {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'flex h-full min-h-screen bg-studio',
|
||||
'w-full flex-col place-items-center',
|
||||
'items-center justify-center gap-8 px-5'
|
||||
)}
|
||||
>
|
||||
<MaintenancePage dehydratedState={undefined} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
|
||||
import { DefaultLayout } from '@/components/layouts/DefaultLayout'
|
||||
import { PageLayout } from '@/components/layouts/PageLayout/PageLayout'
|
||||
import Wizard from '@/pages/new/[slug]'
|
||||
|
||||
export const Route = createFileRoute('/new/$slug')({
|
||||
component: NewProjectRoute,
|
||||
})
|
||||
|
||||
// Placed at top-level rather than under _app — the Next getLayout omits
|
||||
// AppLayout, and uses PageLayout (not WizardLayout) inside DefaultLayout.
|
||||
function NewProjectRoute() {
|
||||
return (
|
||||
<DefaultLayout hideMobileMenu headerTitle="New project">
|
||||
<PageLayout>
|
||||
<Wizard dehydratedState={undefined} />
|
||||
</PageLayout>
|
||||
</DefaultLayout>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
|
||||
import GenericOrganizationPage from '@/pages/org/_/[[...routeSlug]]'
|
||||
|
||||
export const Route = createFileRoute('/org/_/$')({
|
||||
component: OrgWildcardSplatRoute,
|
||||
})
|
||||
|
||||
// Matches `/org/_/...`. TanStack exposes the trailing path as the
|
||||
// `_splat` param; the underlying Next page normalises it back to the
|
||||
// `routeSlug: string[]` shape it originally consumed.
|
||||
//
|
||||
// Naming delta: `org.[_].$.tsx` (path-as-filename) rather than
|
||||
// `org/[_]/$.tsx` (nested directory) — see `org.[_].tsx` for the full
|
||||
// rationale; in short, the directory + index.tsx form trips a
|
||||
// router-generator bug that strips the escaped `_` segment when the
|
||||
// last segment isn't escaped.
|
||||
function OrgWildcardSplatRoute() {
|
||||
return <GenericOrganizationPage />
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
|
||||
import GenericOrganizationPage from '@/pages/org/_/[[...routeSlug]]'
|
||||
|
||||
export const Route = createFileRoute('/org/_')({
|
||||
component: OrgWildcardIndexRoute,
|
||||
})
|
||||
|
||||
// Matches `/org/_` exactly. The companion splat route at
|
||||
// `org.[_].$.tsx` handles `/org/_/...`.
|
||||
//
|
||||
// `[_]` is TanStack's escape syntax for a literal underscore in the URL
|
||||
// (otherwise an unescaped `_` segment is treated as a pathless layout).
|
||||
//
|
||||
// Naming delta: this is `org.[_].tsx`, not `org/[_]/index.tsx`, to side-
|
||||
// step a router-generator bug — when an index.tsx's path has a bracket-
|
||||
// escaped *parent* segment, `originalRoutePath` gets wiped wholesale and
|
||||
// the escape info is lost (getRouteNodes.js:132 sets originalRoutePath
|
||||
// to `/`). The path-as-filename form keeps the last segment non-index,
|
||||
// so the bug branch is skipped.
|
||||
function OrgWildcardIndexRoute() {
|
||||
return <GenericOrganizationPage />
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
|
||||
import GenericProjectPage from '@/pages/project/_/[[...routeSlug]]'
|
||||
|
||||
export const Route = createFileRoute('/project/_/$')({
|
||||
component: ProjectWildcardSplatRoute,
|
||||
})
|
||||
|
||||
// Matches `/project/_/...`. TanStack exposes the trailing path as the
|
||||
// `_splat` param; the underlying Next page normalises it back to the
|
||||
// `routeSlug: string[]` shape it originally consumed.
|
||||
//
|
||||
// Naming delta: `project.[_].$.tsx` (path-as-filename) rather than
|
||||
// `project/[_]/$.tsx` (nested directory) — see `org.[_].tsx` for the
|
||||
// full rationale; in short, the directory + index.tsx form trips a
|
||||
// router-generator bug that strips the escaped `_` segment when the
|
||||
// last segment isn't escaped.
|
||||
function ProjectWildcardSplatRoute() {
|
||||
return <GenericProjectPage />
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
|
||||
import GenericProjectPage from '@/pages/project/_/[[...routeSlug]]'
|
||||
|
||||
export const Route = createFileRoute('/project/_')({
|
||||
component: ProjectWildcardIndexRoute,
|
||||
})
|
||||
|
||||
// Matches `/project/_` exactly. The companion splat route at
|
||||
// `project.[_].$.tsx` handles `/project/_/...`. See `org.[_].tsx` for
|
||||
// why this is named with the path-as-filename form rather than
|
||||
// `project/[_]/index.tsx`.
|
||||
function ProjectWildcardIndexRoute() {
|
||||
return <GenericProjectPage />
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { createFileRoute, Outlet } from '@tanstack/react-router'
|
||||
|
||||
import { DefaultLayout } from '@/components/layouts/DefaultLayout'
|
||||
|
||||
export const Route = createFileRoute('/project/$ref')({
|
||||
component: ProjectShell,
|
||||
})
|
||||
|
||||
function ProjectShell() {
|
||||
return (
|
||||
<DefaultLayout>
|
||||
<Outlet />
|
||||
</DefaultLayout>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import { createFileRoute, Outlet } from '@tanstack/react-router'
|
||||
|
||||
import BranchLayout from '@/components/layouts/BranchLayout/BranchLayout'
|
||||
|
||||
export const Route = createFileRoute('/project/$ref/branches')({
|
||||
component: BranchesShell,
|
||||
})
|
||||
|
||||
// BranchLayout sibling-file shell. Each leaf renders its own per-page
|
||||
// PageLayout (different titles + primary/secondary actions); we keep
|
||||
// that wrapping in the leaf route files via the page-side
|
||||
// `*PageWrapper` exports rather than trying to share PageLayout in the
|
||||
// shell — the shell deliberately stops at BranchLayout.
|
||||
function BranchesShell() {
|
||||
return (
|
||||
<BranchLayout>
|
||||
<Outlet />
|
||||
</BranchLayout>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
|
||||
import BranchesPage, { BranchesPageWrapper } from '@/pages/project/[ref]/branches/index'
|
||||
|
||||
export const Route = createFileRoute('/project/$ref/branches/')({
|
||||
component: BranchesIndexRoute,
|
||||
})
|
||||
|
||||
function BranchesIndexRoute() {
|
||||
return (
|
||||
<BranchesPageWrapper>
|
||||
<BranchesPage dehydratedState={undefined} />
|
||||
</BranchesPageWrapper>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
|
||||
import MergeRequestsPage, {
|
||||
MergeRequestsPageWrapper,
|
||||
} from '@/pages/project/[ref]/branches/merge-requests'
|
||||
|
||||
export const Route = createFileRoute('/project/$ref/branches/merge-requests')({
|
||||
component: MergeRequestsRoute,
|
||||
})
|
||||
|
||||
function MergeRequestsRoute() {
|
||||
return (
|
||||
<MergeRequestsPageWrapper>
|
||||
<MergeRequestsPage dehydratedState={undefined} />
|
||||
</MergeRequestsPageWrapper>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import { createFileRoute, Outlet, useMatches } from '@tanstack/react-router'
|
||||
|
||||
import DatabaseLayout from '@/components/layouts/DatabaseLayout/DatabaseLayout'
|
||||
|
||||
export const Route = createFileRoute('/project/$ref/database')({
|
||||
component: DatabaseShell,
|
||||
})
|
||||
|
||||
function DatabaseShell() {
|
||||
// useMatches() returns a fresh array reference on every router tick, which
|
||||
// re-renders the shell (and its subtree, e.g. DatabaseLayout → ProjectLayout
|
||||
// → the route content) even when the leaf staticData hasn't changed. The
|
||||
// `select` form lets TanStack compare the selected value (a string) and
|
||||
// only re-render when it actually changes — required to keep heavy children
|
||||
// like ReactFlow stable, otherwise their internal compose-refs chain runs
|
||||
// setRef → setState every render and trips React's max-update-depth.
|
||||
const title = useMatches({
|
||||
select: (matches) => {
|
||||
// Walk up from the leaf to the nearest match that declares a title.
|
||||
// Section layouts like `database/triggers.tsx` set `databaseLayoutTitle`
|
||||
// while their leaf children (`triggers/data`, `triggers/event`, the
|
||||
// index) don't, so a leaf-only read would drop the title to ''.
|
||||
for (let i = matches.length - 1; i >= 0; i--) {
|
||||
const data = matches[i]?.staticData as { databaseLayoutTitle?: string } | undefined
|
||||
if (typeof data?.databaseLayoutTitle === 'string') return data.databaseLayoutTitle
|
||||
}
|
||||
return ''
|
||||
},
|
||||
})
|
||||
|
||||
return (
|
||||
<DatabaseLayout title={title}>
|
||||
<Outlet />
|
||||
</DatabaseLayout>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
|
||||
import DatabasePhysicalBackups from '@/pages/project/[ref]/database/backups/pitr'
|
||||
|
||||
export const Route = createFileRoute('/project/$ref/database/backups/pitr')({
|
||||
component: DatabaseBackupsPitrRoute,
|
||||
staticData: {
|
||||
databaseLayoutTitle: 'Backups',
|
||||
},
|
||||
})
|
||||
|
||||
function DatabaseBackupsPitrRoute() {
|
||||
return <DatabasePhysicalBackups dehydratedState={undefined} />
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
|
||||
import RestoreToNewProjectPage from '@/pages/project/[ref]/database/backups/restore-to-new-project'
|
||||
|
||||
export const Route = createFileRoute('/project/$ref/database/backups/restore-to-new-project')({
|
||||
component: DatabaseBackupsRestoreToNewProjectRoute,
|
||||
staticData: {
|
||||
databaseLayoutTitle: 'Backups',
|
||||
},
|
||||
})
|
||||
|
||||
function DatabaseBackupsRestoreToNewProjectRoute() {
|
||||
return <RestoreToNewProjectPage dehydratedState={undefined} />
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
|
||||
import DatabaseScheduledBackups from '@/pages/project/[ref]/database/backups/scheduled'
|
||||
|
||||
export const Route = createFileRoute('/project/$ref/database/backups/scheduled')({
|
||||
component: DatabaseBackupsScheduledRoute,
|
||||
staticData: {
|
||||
databaseLayoutTitle: 'Backups',
|
||||
},
|
||||
})
|
||||
|
||||
function DatabaseBackupsScheduledRoute() {
|
||||
return <DatabaseScheduledBackups dehydratedState={undefined} />
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
|
||||
import PrivilegesPage from '@/pages/project/[ref]/database/column-privileges'
|
||||
|
||||
export const Route = createFileRoute('/project/$ref/database/column-privileges')({
|
||||
component: DatabaseColumnPrivilegesRoute,
|
||||
staticData: {
|
||||
databaseLayoutTitle: 'Column Privileges',
|
||||
},
|
||||
})
|
||||
|
||||
function DatabaseColumnPrivilegesRoute() {
|
||||
return <PrivilegesPage dehydratedState={undefined} />
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
|
||||
import DatabaseExtensions from '@/pages/project/[ref]/database/extensions'
|
||||
|
||||
export const Route = createFileRoute('/project/$ref/database/extensions')({
|
||||
component: DatabaseExtensionsRoute,
|
||||
staticData: {
|
||||
databaseLayoutTitle: 'Extensions',
|
||||
},
|
||||
})
|
||||
|
||||
function DatabaseExtensionsRoute() {
|
||||
return <DatabaseExtensions dehydratedState={undefined} />
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
|
||||
import DatabaseFunctionsPage from '@/pages/project/[ref]/database/functions'
|
||||
|
||||
export const Route = createFileRoute('/project/$ref/database/functions')({
|
||||
component: DatabaseFunctionsRoute,
|
||||
staticData: {
|
||||
databaseLayoutTitle: 'Functions',
|
||||
},
|
||||
})
|
||||
|
||||
function DatabaseFunctionsRoute() {
|
||||
return <DatabaseFunctionsPage dehydratedState={undefined} />
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
|
||||
import IndexesPage from '@/pages/project/[ref]/database/indexes'
|
||||
|
||||
export const Route = createFileRoute('/project/$ref/database/indexes')({
|
||||
component: DatabaseIndexesRoute,
|
||||
staticData: {
|
||||
databaseLayoutTitle: 'Indexes',
|
||||
},
|
||||
})
|
||||
|
||||
function DatabaseIndexesRoute() {
|
||||
return <IndexesPage dehydratedState={undefined} />
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
|
||||
import MigrationsPage from '@/pages/project/[ref]/database/migrations'
|
||||
|
||||
export const Route = createFileRoute('/project/$ref/database/migrations')({
|
||||
component: DatabaseMigrationsRoute,
|
||||
staticData: {
|
||||
databaseLayoutTitle: 'Migrations',
|
||||
},
|
||||
})
|
||||
|
||||
function DatabaseMigrationsRoute() {
|
||||
return <MigrationsPage dehydratedState={undefined} />
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
|
||||
import DatabasePublications from '@/pages/project/[ref]/database/publications/[id]'
|
||||
|
||||
export const Route = createFileRoute('/project/$ref/database/publications/$id')({
|
||||
component: DatabasePublicationDetailRoute,
|
||||
staticData: {
|
||||
databaseLayoutTitle: 'Publications',
|
||||
},
|
||||
})
|
||||
|
||||
function DatabasePublicationDetailRoute() {
|
||||
return <DatabasePublications dehydratedState={undefined} />
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
|
||||
import DatabasePublications from '@/pages/project/[ref]/database/publications/index'
|
||||
|
||||
export const Route = createFileRoute('/project/$ref/database/publications/')({
|
||||
component: DatabasePublicationsIndexRoute,
|
||||
staticData: {
|
||||
databaseLayoutTitle: 'Publications',
|
||||
},
|
||||
})
|
||||
|
||||
function DatabasePublicationsIndexRoute() {
|
||||
return <DatabasePublications dehydratedState={undefined} />
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
|
||||
import DatabaseReplicationPage from '@/pages/project/[ref]/database/replication/[pipelineId]'
|
||||
|
||||
export const Route = createFileRoute('/project/$ref/database/replication/$pipelineId')({
|
||||
component: DatabaseReplicationPipelineRoute,
|
||||
staticData: {
|
||||
databaseLayoutTitle: 'Replication',
|
||||
},
|
||||
})
|
||||
|
||||
function DatabaseReplicationPipelineRoute() {
|
||||
return <DatabaseReplicationPage dehydratedState={undefined} />
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
|
||||
import DatabaseReplicationPage from '@/pages/project/[ref]/database/replication/index'
|
||||
|
||||
export const Route = createFileRoute('/project/$ref/database/replication/')({
|
||||
component: DatabaseReplicationIndexRoute,
|
||||
staticData: {
|
||||
databaseLayoutTitle: 'Replication',
|
||||
},
|
||||
})
|
||||
|
||||
function DatabaseReplicationIndexRoute() {
|
||||
return <DatabaseReplicationPage dehydratedState={undefined} />
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
|
||||
import DatabaseReadReplicaPage from '@/pages/project/[ref]/database/replication/replica/[replicaId]'
|
||||
|
||||
export const Route = createFileRoute('/project/$ref/database/replication/replica/$replicaId')({
|
||||
component: DatabaseReplicationReplicaRoute,
|
||||
staticData: {
|
||||
databaseLayoutTitle: 'Replication',
|
||||
},
|
||||
})
|
||||
|
||||
function DatabaseReplicationReplicaRoute() {
|
||||
return <DatabaseReadReplicaPage dehydratedState={undefined} />
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
|
||||
import DatabaseRoles from '@/pages/project/[ref]/database/roles'
|
||||
|
||||
export const Route = createFileRoute('/project/$ref/database/roles')({
|
||||
component: DatabaseRolesRoute,
|
||||
staticData: {
|
||||
databaseLayoutTitle: 'Roles',
|
||||
},
|
||||
})
|
||||
|
||||
function DatabaseRolesRoute() {
|
||||
return <DatabaseRoles dehydratedState={undefined} />
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
|
||||
import SchemasPage from '@/pages/project/[ref]/database/schemas'
|
||||
|
||||
export const Route = createFileRoute('/project/$ref/database/schemas')({
|
||||
component: SchemasRoute,
|
||||
staticData: {
|
||||
databaseLayoutTitle: 'Schema Visualizer',
|
||||
},
|
||||
})
|
||||
|
||||
function SchemasRoute() {
|
||||
return <SchemasPage dehydratedState={undefined} />
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
|
||||
import DatabaseSettings from '@/pages/project/[ref]/database/settings'
|
||||
|
||||
export const Route = createFileRoute('/project/$ref/database/settings')({
|
||||
component: DatabaseSettingsRoute,
|
||||
staticData: {
|
||||
databaseLayoutTitle: 'Settings',
|
||||
},
|
||||
})
|
||||
|
||||
function DatabaseSettingsRoute() {
|
||||
return <DatabaseSettings dehydratedState={undefined} />
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
|
||||
import DatabaseTables from '@/pages/project/[ref]/database/tables/[id]'
|
||||
|
||||
export const Route = createFileRoute('/project/$ref/database/tables/$id')({
|
||||
component: DatabaseTableDetailRoute,
|
||||
staticData: {
|
||||
databaseLayoutTitle: 'Tables',
|
||||
},
|
||||
})
|
||||
|
||||
function DatabaseTableDetailRoute() {
|
||||
return <DatabaseTables dehydratedState={undefined} />
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
|
||||
import DatabaseTables from '@/pages/project/[ref]/database/tables/index'
|
||||
|
||||
export const Route = createFileRoute('/project/$ref/database/tables/')({
|
||||
component: DatabaseTablesIndexRoute,
|
||||
staticData: {
|
||||
databaseLayoutTitle: 'Tables',
|
||||
},
|
||||
})
|
||||
|
||||
function DatabaseTablesIndexRoute() {
|
||||
return <DatabaseTables dehydratedState={undefined} />
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import { PermissionAction } from '@supabase/shared-types/out/constants'
|
||||
import { createFileRoute, Outlet } from '@tanstack/react-router'
|
||||
|
||||
import { PageLayout } from '@/components/layouts/PageLayout/PageLayout'
|
||||
import { NoPermission } from '@/components/ui/NoPermission'
|
||||
import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions'
|
||||
|
||||
export const Route = createFileRoute('/project/$ref/database/triggers')({
|
||||
component: TriggersShell,
|
||||
staticData: {
|
||||
databaseLayoutTitle: 'Triggers',
|
||||
},
|
||||
})
|
||||
|
||||
function TriggersShell() {
|
||||
const { ref } = Route.useParams()
|
||||
const { can: canReadTriggers, isSuccess: isPermissionsLoaded } = useAsyncCheckPermissions(
|
||||
PermissionAction.TENANT_SQL_ADMIN_READ,
|
||||
'triggers'
|
||||
)
|
||||
|
||||
if (isPermissionsLoaded && !canReadTriggers) {
|
||||
return <NoPermission isFullPage resourceText="view database triggers" />
|
||||
}
|
||||
|
||||
return (
|
||||
<PageLayout
|
||||
title="Database Triggers"
|
||||
subtitle="Execute actions automatically when database events occur"
|
||||
size="large"
|
||||
navigationItems={[
|
||||
{ label: 'Data', href: `/project/${ref}/database/triggers/data` },
|
||||
{ label: 'Event', href: `/project/${ref}/database/triggers/event` },
|
||||
]}
|
||||
>
|
||||
<Outlet />
|
||||
</PageLayout>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
|
||||
import TriggersDataPage from '@/pages/project/[ref]/database/triggers/data'
|
||||
|
||||
export const Route = createFileRoute('/project/$ref/database/triggers/data')({
|
||||
component: TriggersDataRoute,
|
||||
})
|
||||
|
||||
function TriggersDataRoute() {
|
||||
return <TriggersDataPage dehydratedState={undefined} />
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
|
||||
import TriggersSchemaPage from '@/pages/project/[ref]/database/triggers/event'
|
||||
|
||||
export const Route = createFileRoute('/project/$ref/database/triggers/event')({
|
||||
component: TriggersEventRoute,
|
||||
})
|
||||
|
||||
function TriggersEventRoute() {
|
||||
return <TriggersSchemaPage dehydratedState={undefined} />
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
|
||||
import DatabaseTriggersIndexPage from '@/pages/project/[ref]/database/triggers/index'
|
||||
|
||||
export const Route = createFileRoute('/project/$ref/database/triggers/')({
|
||||
component: DatabaseTriggersIndexRoute,
|
||||
})
|
||||
|
||||
function DatabaseTriggersIndexRoute() {
|
||||
return <DatabaseTriggersIndexPage dehydratedState={undefined} />
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
|
||||
import DatabaseEnumeratedTypes from '@/pages/project/[ref]/database/types'
|
||||
|
||||
export const Route = createFileRoute('/project/$ref/database/types')({
|
||||
component: DatabaseTypesRoute,
|
||||
staticData: {
|
||||
databaseLayoutTitle: 'Enumerated Types',
|
||||
},
|
||||
})
|
||||
|
||||
function DatabaseTypesRoute() {
|
||||
return <DatabaseEnumeratedTypes dehydratedState={undefined} />
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import { createFileRoute, Outlet } from '@tanstack/react-router'
|
||||
|
||||
import { EditorBaseLayout } from '@/components/layouts/editors/EditorBaseLayout'
|
||||
import { TableEditorLayout } from '@/components/layouts/TableEditorLayout/TableEditorLayout'
|
||||
import { TableEditorMenu } from '@/components/layouts/TableEditorLayout/TableEditorMenu'
|
||||
|
||||
export const Route = createFileRoute('/project/$ref/editor')({
|
||||
component: TableEditorShell,
|
||||
})
|
||||
|
||||
// All three editor leaves (index, $id, new) share identical layout
|
||||
// wrapping in the Next pages router (DefaultLayout > EditorBaseLayout >
|
||||
// TableEditorLayout). DefaultLayout is provided by the parent
|
||||
// `routes/project/$ref.tsx` shell, so this file only adds the
|
||||
// editor-specific layers.
|
||||
//
|
||||
// Note on the ProjectLayoutWithAuth chain: EditorBaseLayout always
|
||||
// wraps in <ProjectLayoutWithAuth>. TableEditorLayout *also* wraps in
|
||||
// one — but only on its no-permission path; the happy path returns
|
||||
// `<>{children}<SaveQueueActionBar/></>` so no double-wrap occurs in
|
||||
// normal use. Mirrors current Next behaviour exactly.
|
||||
function TableEditorShell() {
|
||||
return (
|
||||
<EditorBaseLayout
|
||||
productMenu={<TableEditorMenu />}
|
||||
product="Table Editor"
|
||||
productMenuClassName="overflow-y-hidden"
|
||||
>
|
||||
<TableEditorLayout>
|
||||
<Outlet />
|
||||
</TableEditorLayout>
|
||||
</EditorBaseLayout>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
|
||||
import TableEditorPage from '@/pages/project/[ref]/editor/[id]'
|
||||
|
||||
export const Route = createFileRoute('/project/$ref/editor/$id')({
|
||||
component: TableEditorDetailRoute,
|
||||
})
|
||||
|
||||
function TableEditorDetailRoute() {
|
||||
return <TableEditorPage dehydratedState={undefined} />
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
|
||||
import TableEditorPage from '@/pages/project/[ref]/editor/index'
|
||||
|
||||
export const Route = createFileRoute('/project/$ref/editor/')({
|
||||
component: TableEditorIndexRoute,
|
||||
})
|
||||
|
||||
function TableEditorIndexRoute() {
|
||||
return <TableEditorPage dehydratedState={undefined} />
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
|
||||
import EditorNewPage from '@/pages/project/[ref]/editor/new'
|
||||
|
||||
export const Route = createFileRoute('/project/$ref/editor/new')({
|
||||
component: TableEditorNewRoute,
|
||||
})
|
||||
|
||||
function TableEditorNewRoute() {
|
||||
return <EditorNewPage dehydratedState={undefined} />
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
|
||||
import { ProjectLayoutWithAuth } from '@/components/layouts/ProjectLayout'
|
||||
import MergePage from '@/pages/project/[ref]/merge'
|
||||
|
||||
export const Route = createFileRoute('/project/$ref/merge')({
|
||||
component: ProjectMergeRoute,
|
||||
})
|
||||
|
||||
function ProjectMergeRoute() {
|
||||
return (
|
||||
<ProjectLayoutWithAuth>
|
||||
<MergePage dehydratedState={undefined} />
|
||||
</ProjectLayoutWithAuth>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { createFileRoute, Outlet, useMatches } from '@tanstack/react-router'
|
||||
|
||||
import RealtimeLayout from '@/components/layouts/RealtimeLayout/RealtimeLayout'
|
||||
|
||||
export const Route = createFileRoute('/project/$ref/realtime')({
|
||||
component: RealtimeShell,
|
||||
})
|
||||
|
||||
function RealtimeShell() {
|
||||
// `select` form: only re-render when the selected title changes. See
|
||||
// routes/_app.tsx for the full rationale.
|
||||
const title = useMatches({
|
||||
select: (matches) =>
|
||||
(matches[matches.length - 1]?.staticData as { realtimeLayoutTitle?: string } | undefined)
|
||||
?.realtimeLayoutTitle ?? '',
|
||||
})
|
||||
|
||||
return (
|
||||
<RealtimeLayout title={title}>
|
||||
<Outlet />
|
||||
</RealtimeLayout>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
|
||||
import InspectorPage from '@/pages/project/[ref]/realtime/inspector'
|
||||
|
||||
export const Route = createFileRoute('/project/$ref/realtime/inspector')({
|
||||
component: RealtimeInspectorRoute,
|
||||
staticData: {
|
||||
realtimeLayoutTitle: 'Realtime Inspector',
|
||||
},
|
||||
})
|
||||
|
||||
function RealtimeInspectorRoute() {
|
||||
return <InspectorPage dehydratedState={undefined} />
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
|
||||
import RealtimePoliciesPage from '@/pages/project/[ref]/realtime/policies'
|
||||
|
||||
export const Route = createFileRoute('/project/$ref/realtime/policies')({
|
||||
component: RealtimePoliciesRoute,
|
||||
staticData: {
|
||||
realtimeLayoutTitle: 'Policies',
|
||||
},
|
||||
})
|
||||
|
||||
function RealtimePoliciesRoute() {
|
||||
return <RealtimePoliciesPage dehydratedState={undefined} />
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
|
||||
import RealtimeSettingsPage from '@/pages/project/[ref]/realtime/settings'
|
||||
|
||||
export const Route = createFileRoute('/project/$ref/realtime/settings')({
|
||||
component: RealtimeSettingsRoute,
|
||||
staticData: {
|
||||
realtimeLayoutTitle: 'Realtime Settings',
|
||||
},
|
||||
})
|
||||
|
||||
function RealtimeSettingsRoute() {
|
||||
return <RealtimeSettingsPage dehydratedState={undefined} />
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import { createFileRoute, Outlet } from '@tanstack/react-router'
|
||||
|
||||
import { EditorBaseLayout } from '@/components/layouts/editors/EditorBaseLayout'
|
||||
import SQLEditorLayout from '@/components/layouts/SQLEditorLayout/SQLEditorLayout'
|
||||
import { SQLEditorMenu } from '@/components/layouts/SQLEditorLayout/SQLEditorMenu'
|
||||
|
||||
export const Route = createFileRoute('/project/$ref/sql')({
|
||||
component: SQLEditorShell,
|
||||
})
|
||||
|
||||
// Twin of routes/project/$ref/editor.tsx — all four sql/* leaves share
|
||||
// identical layout wrapping (`<DefaultLayout><EditorBaseLayout productMenu>
|
||||
// <SQLEditorLayout>{page}</SQLEditorLayout></EditorBaseLayout>`), so the
|
||||
// shell hardcodes the props with no staticData overrides. DefaultLayout
|
||||
// is provided by the parent `routes/project/$ref.tsx` shell.
|
||||
//
|
||||
// EditorBaseLayout wraps in <ProjectLayoutWithAuth>; SQLEditorLayout adds
|
||||
// its own `withAuth` HOC but doesn't add another ProjectLayout — so no
|
||||
// double-render concerns, only doubled auth check (a no-op when already
|
||||
// authenticated).
|
||||
function SQLEditorShell() {
|
||||
return (
|
||||
<EditorBaseLayout productMenu={<SQLEditorMenu />} product="SQL Editor">
|
||||
<SQLEditorLayout>
|
||||
<Outlet />
|
||||
</SQLEditorLayout>
|
||||
</EditorBaseLayout>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
|
||||
import SqlEditor from '@/pages/project/[ref]/sql/[id]'
|
||||
|
||||
export const Route = createFileRoute('/project/$ref/sql/$id')({
|
||||
component: SQLEditorDetailRoute,
|
||||
})
|
||||
|
||||
function SQLEditorDetailRoute() {
|
||||
return <SqlEditor dehydratedState={undefined} />
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
|
||||
import SqlExamples from '@/pages/project/[ref]/sql/examples'
|
||||
|
||||
export const Route = createFileRoute('/project/$ref/sql/examples')({
|
||||
component: SqlExamplesRoute,
|
||||
})
|
||||
|
||||
function SqlExamplesRoute() {
|
||||
return <SqlExamples dehydratedState={undefined} />
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
|
||||
import SQLEditorIndexPage from '@/pages/project/[ref]/sql/index'
|
||||
|
||||
export const Route = createFileRoute('/project/$ref/sql/')({
|
||||
component: SQLEditorIndexRoute,
|
||||
})
|
||||
|
||||
function SQLEditorIndexRoute() {
|
||||
return <SQLEditorIndexPage dehydratedState={undefined} />
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
|
||||
import SqlTemplates from '@/pages/project/[ref]/sql/templates'
|
||||
|
||||
export const Route = createFileRoute('/project/$ref/sql/templates')({
|
||||
component: SQLTemplatesRoute,
|
||||
})
|
||||
|
||||
function SQLTemplatesRoute() {
|
||||
return <SqlTemplates dehydratedState={undefined} />
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
import { createFileRoute, Outlet, useMatches } from '@tanstack/react-router'
|
||||
|
||||
import { StorageBucketsLayout } from '@/components/layouts/StorageLayout/StorageBucketsLayout'
|
||||
import StorageLayout from '@/components/layouts/StorageLayout/StorageLayout'
|
||||
|
||||
export const Route = createFileRoute('/project/$ref/storage')({
|
||||
component: StorageShell,
|
||||
})
|
||||
|
||||
type StorageStaticData = {
|
||||
storageLayoutTitle?: string
|
||||
// Most storage leaves wrap in <StorageBucketsLayout> (the bucket-tab
|
||||
// sub-nav). Bucket detail pages (files/analytics/vectors $bucketId) skip
|
||||
// it and render a full-bleed bucket UI under StorageLayout only.
|
||||
skipStorageBucketsLayout?: boolean
|
||||
// Inner-layout overrides — used by /storage/s3 which passes its own
|
||||
// title and hides the subtitle. Most leaves leave these undefined.
|
||||
storageBucketsLayoutTitle?: string
|
||||
storageBucketsLayoutHideSubtitle?: boolean
|
||||
}
|
||||
|
||||
function StorageShell() {
|
||||
// `select` form across each field — only re-render when the specific
|
||||
// value changes. See routes/_app.tsx for the full rationale.
|
||||
const title = useMatches({
|
||||
select: (matches) =>
|
||||
(matches[matches.length - 1]?.staticData as StorageStaticData | undefined)
|
||||
?.storageLayoutTitle ?? '',
|
||||
})
|
||||
const skipBuckets = useMatches({
|
||||
select: (matches) =>
|
||||
(matches[matches.length - 1]?.staticData as StorageStaticData | undefined)
|
||||
?.skipStorageBucketsLayout ?? false,
|
||||
})
|
||||
const bucketsTitle = useMatches({
|
||||
select: (matches) =>
|
||||
(matches[matches.length - 1]?.staticData as StorageStaticData | undefined)
|
||||
?.storageBucketsLayoutTitle,
|
||||
})
|
||||
const bucketsHideSubtitle = useMatches({
|
||||
select: (matches) =>
|
||||
(matches[matches.length - 1]?.staticData as StorageStaticData | undefined)
|
||||
?.storageBucketsLayoutHideSubtitle ?? false,
|
||||
})
|
||||
|
||||
if (skipBuckets) {
|
||||
return (
|
||||
<StorageLayout title={title}>
|
||||
<Outlet />
|
||||
</StorageLayout>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<StorageLayout title={title}>
|
||||
<StorageBucketsLayout title={bucketsTitle} hideSubtitle={bucketsHideSubtitle}>
|
||||
<Outlet />
|
||||
</StorageBucketsLayout>
|
||||
</StorageLayout>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
|
||||
import AnalyticsBucketPage from '@/pages/project/[ref]/storage/analytics/buckets/[bucketId]'
|
||||
|
||||
export const Route = createFileRoute('/project/$ref/storage/analytics/buckets/$bucketId')({
|
||||
component: StorageAnalyticsBucketRoute,
|
||||
staticData: {
|
||||
storageLayoutTitle: 'Buckets',
|
||||
skipStorageBucketsLayout: true,
|
||||
},
|
||||
})
|
||||
|
||||
function StorageAnalyticsBucketRoute() {
|
||||
return <AnalyticsBucketPage dehydratedState={undefined} />
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
|
||||
import StorageAnalyticsPage from '@/pages/project/[ref]/storage/analytics/index'
|
||||
|
||||
export const Route = createFileRoute('/project/$ref/storage/analytics/')({
|
||||
component: StorageAnalyticsIndexRoute,
|
||||
staticData: {
|
||||
storageLayoutTitle: 'Analytics',
|
||||
},
|
||||
})
|
||||
|
||||
function StorageAnalyticsIndexRoute() {
|
||||
return <StorageAnalyticsPage dehydratedState={undefined} />
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
|
||||
import BucketPage from '@/pages/project/[ref]/storage/files/buckets/[bucketId]'
|
||||
|
||||
export const Route = createFileRoute('/project/$ref/storage/files/buckets/$bucketId')({
|
||||
component: StorageFilesBucketRoute,
|
||||
// Bucket detail pages skip StorageBucketsLayout — they render the
|
||||
// bucket UI full-bleed under StorageLayout only.
|
||||
staticData: {
|
||||
storageLayoutTitle: 'Buckets',
|
||||
skipStorageBucketsLayout: true,
|
||||
},
|
||||
})
|
||||
|
||||
function StorageFilesBucketRoute() {
|
||||
return <BucketPage dehydratedState={undefined} />
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
|
||||
import StorageFilesPage from '@/pages/project/[ref]/storage/files/index'
|
||||
|
||||
export const Route = createFileRoute('/project/$ref/storage/files/')({
|
||||
component: StorageFilesIndexRoute,
|
||||
staticData: {
|
||||
storageLayoutTitle: 'Files',
|
||||
},
|
||||
})
|
||||
|
||||
function StorageFilesIndexRoute() {
|
||||
return <StorageFilesPage dehydratedState={undefined} />
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
|
||||
import FilesPoliciesPage from '@/pages/project/[ref]/storage/files/policies'
|
||||
|
||||
export const Route = createFileRoute('/project/$ref/storage/files/policies')({
|
||||
component: StorageFilesPoliciesRoute,
|
||||
staticData: {
|
||||
storageLayoutTitle: 'Policies',
|
||||
},
|
||||
})
|
||||
|
||||
function StorageFilesPoliciesRoute() {
|
||||
return <FilesPoliciesPage dehydratedState={undefined} />
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
|
||||
import FilesSettingsPage from '@/pages/project/[ref]/storage/files/settings'
|
||||
|
||||
export const Route = createFileRoute('/project/$ref/storage/files/settings')({
|
||||
component: StorageFilesSettingsRoute,
|
||||
staticData: {
|
||||
storageLayoutTitle: 'Settings',
|
||||
},
|
||||
})
|
||||
|
||||
function StorageFilesSettingsRoute() {
|
||||
return <FilesSettingsPage dehydratedState={undefined} />
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
|
||||
import S3SettingsPage from '@/pages/project/[ref]/storage/s3'
|
||||
|
||||
export const Route = createFileRoute('/project/$ref/storage/s3')({
|
||||
component: StorageS3Route,
|
||||
staticData: {
|
||||
storageLayoutTitle: 'S3 Configuration',
|
||||
storageBucketsLayoutTitle: 'S3 Configuration',
|
||||
storageBucketsLayoutHideSubtitle: true,
|
||||
},
|
||||
})
|
||||
|
||||
function StorageS3Route() {
|
||||
return <S3SettingsPage dehydratedState={undefined} />
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
|
||||
import VectorsBucketPage from '@/pages/project/[ref]/storage/vectors/buckets/[bucketId]'
|
||||
|
||||
export const Route = createFileRoute('/project/$ref/storage/vectors/buckets/$bucketId')({
|
||||
component: StorageVectorsBucketRoute,
|
||||
staticData: {
|
||||
storageLayoutTitle: 'Buckets',
|
||||
skipStorageBucketsLayout: true,
|
||||
},
|
||||
})
|
||||
|
||||
function StorageVectorsBucketRoute() {
|
||||
return <VectorsBucketPage dehydratedState={undefined} />
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
|
||||
import StorageVectorsPage from '@/pages/project/[ref]/storage/vectors/index'
|
||||
|
||||
export const Route = createFileRoute('/project/$ref/storage/vectors/')({
|
||||
component: StorageVectorsIndexRoute,
|
||||
staticData: {
|
||||
storageLayoutTitle: 'Vectors',
|
||||
},
|
||||
})
|
||||
|
||||
function StorageVectorsIndexRoute() {
|
||||
return <StorageVectorsPage dehydratedState={undefined} />
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
|
||||
import RedeemCreditsPage from '@/pages/redeem'
|
||||
|
||||
export const Route = createFileRoute('/redeem')({
|
||||
component: RedeemRoute,
|
||||
})
|
||||
|
||||
function RedeemRoute() {
|
||||
return <RedeemCreditsPage dehydratedState={undefined} />
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
|
||||
import VerifyEmailPage from '@/pages/verify-email'
|
||||
|
||||
export const Route = createFileRoute('/verify-email')({
|
||||
component: VerifyEmailRoute,
|
||||
})
|
||||
|
||||
function VerifyEmailRoute() {
|
||||
return <VerifyEmailPage dehydratedState={undefined} />
|
||||
}
|
||||
Reference in new issue
Block a user