diff --git a/apps/studio/app/dashboard/v2/[projectRef]/page.tsx b/apps/studio/app/dashboard/v2/[projectRef]/page.tsx
new file mode 100644
index 00000000000..17d7d1fad2a
--- /dev/null
+++ b/apps/studio/app/dashboard/v2/[projectRef]/page.tsx
@@ -0,0 +1,10 @@
+import { redirect } from 'next/navigation'
+
+export default async function V2ProjectShortcutPage({
+ params,
+}: {
+ params: Promise<{ projectRef: string }>
+}) {
+ const { projectRef } = await params
+ redirect(`/dashboard/v2/project/${projectRef}`)
+}
diff --git a/apps/studio/app/dashboard/v2/org/[orgSlug]/page.tsx b/apps/studio/app/dashboard/v2/org/[orgSlug]/page.tsx
new file mode 100644
index 00000000000..ec05a7b19fb
--- /dev/null
+++ b/apps/studio/app/dashboard/v2/org/[orgSlug]/page.tsx
@@ -0,0 +1,58 @@
+'use client'
+
+import Link from 'next/link'
+import { useParams } from 'next/navigation'
+import { useOrgProjectsInfiniteQuery } from 'data/projects/org-projects-infinite-query'
+import { useOrganizationsQuery } from 'data/organizations/organizations-query'
+import { ShimmeringLoader } from 'ui-patterns/ShimmeringLoader'
+
+import { V2OrgParamsProvider } from '@/app/v2/V2ParamsContext'
+
+function OrgDashboardContent() {
+ const params = useParams()
+ const orgSlug = params?.orgSlug as string | undefined
+
+ const { data: org } = useOrganizationsQuery({
+ select: (orgs) => orgs?.find((o) => o.slug === orgSlug),
+ })
+ const { data: projectsData, isPending } = useOrgProjectsInfiniteQuery(
+ { slug: orgSlug, limit: 50 },
+ { enabled: Boolean(orgSlug) }
+ )
+ const projects = projectsData?.pages?.flatMap((p) => p.projects) ?? []
+
+ if (!orgSlug) {
+ return
Select an organization.
+ }
+
+ return (
+
+
{org?.name ?? orgSlug}
+
Projects
+ {isPending ? (
+
+ ) : (
+
+ {projects.map((proj) => (
+ -
+
+ {proj.name}
+
+
+ ))}
+
+ )}
+
+ )
+}
+
+export default function V2OrgPage() {
+ return (
+
+
+
+ )
+}
diff --git a/apps/studio/app/dashboard/v2/project/[projectRef]/ProjectShell.tsx b/apps/studio/app/dashboard/v2/project/[projectRef]/ProjectShell.tsx
new file mode 100644
index 00000000000..174d4db8f5e
--- /dev/null
+++ b/apps/studio/app/dashboard/v2/project/[projectRef]/ProjectShell.tsx
@@ -0,0 +1,32 @@
+'use client'
+
+import { useParams } from 'next/navigation'
+import { useMemo } from 'react'
+
+import { V2ParamsProvider } from '@/app/v2/V2ParamsContext'
+import { Shell } from '@/components/v2/Shell'
+import { useOrganizationsQuery } from 'data/organizations/organizations-query'
+import { useProjectDetailQuery } from 'data/projects/project-detail-query'
+
+export function ProjectShell({ children }: { children: React.ReactNode }) {
+ const params = useParams()
+ const projectRef = params?.projectRef as string | undefined
+
+ const { data: project } = useProjectDetailQuery(
+ { ref: projectRef },
+ { enabled: Boolean(projectRef) }
+ )
+ const { data: organizations } = useOrganizationsQuery()
+
+ const orgSlug = useMemo(() => {
+ if (!project?.organization_id || !organizations) return undefined
+ const org = organizations.find((o) => o.id === project.organization_id)
+ return org?.slug
+ }, [project?.organization_id, organizations])
+
+ return (
+
+ {children}
+
+ )
+}
diff --git a/apps/studio/app/dashboard/v2/project/[projectRef]/data/buckets/page.tsx b/apps/studio/app/dashboard/v2/project/[projectRef]/data/buckets/page.tsx
new file mode 100644
index 00000000000..72ec5997b4f
--- /dev/null
+++ b/apps/studio/app/dashboard/v2/project/[projectRef]/data/buckets/page.tsx
@@ -0,0 +1,5 @@
+import { V2PlaceholderView } from '@/components/v2/views/V2PlaceholderView'
+
+export default function DataBucketsPage() {
+ return
+}
diff --git a/apps/studio/app/dashboard/v2/project/[projectRef]/data/channels/page.tsx b/apps/studio/app/dashboard/v2/project/[projectRef]/data/channels/page.tsx
new file mode 100644
index 00000000000..90f8a6e6f81
--- /dev/null
+++ b/apps/studio/app/dashboard/v2/project/[projectRef]/data/channels/page.tsx
@@ -0,0 +1,5 @@
+import { V2PlaceholderView } from '@/components/v2/views/V2PlaceholderView'
+
+export default function DataChannelsPage() {
+ return
+}
diff --git a/apps/studio/app/dashboard/v2/project/[projectRef]/data/edge-functions/page.tsx b/apps/studio/app/dashboard/v2/project/[projectRef]/data/edge-functions/page.tsx
new file mode 100644
index 00000000000..df8dcfe2125
--- /dev/null
+++ b/apps/studio/app/dashboard/v2/project/[projectRef]/data/edge-functions/page.tsx
@@ -0,0 +1,5 @@
+import { V2PlaceholderView } from '@/components/v2/views/V2PlaceholderView'
+
+export default function DataEdgeFunctionsPage() {
+ return
+}
diff --git a/apps/studio/app/dashboard/v2/project/[projectRef]/data/extensions/page.tsx b/apps/studio/app/dashboard/v2/project/[projectRef]/data/extensions/page.tsx
new file mode 100644
index 00000000000..ae8745bd11f
--- /dev/null
+++ b/apps/studio/app/dashboard/v2/project/[projectRef]/data/extensions/page.tsx
@@ -0,0 +1,5 @@
+import { V2PlaceholderView } from '@/components/v2/views/V2PlaceholderView'
+
+export default function DataExtensionsPage() {
+ return
+}
diff --git a/apps/studio/app/dashboard/v2/project/[projectRef]/data/functions/page.tsx b/apps/studio/app/dashboard/v2/project/[projectRef]/data/functions/page.tsx
new file mode 100644
index 00000000000..86d8e57617c
--- /dev/null
+++ b/apps/studio/app/dashboard/v2/project/[projectRef]/data/functions/page.tsx
@@ -0,0 +1,5 @@
+import { V2PlaceholderView } from '@/components/v2/views/V2PlaceholderView'
+
+export default function DataFunctionsPage() {
+ return
+}
diff --git a/apps/studio/app/dashboard/v2/project/[projectRef]/data/indexes/page.tsx b/apps/studio/app/dashboard/v2/project/[projectRef]/data/indexes/page.tsx
new file mode 100644
index 00000000000..ef6d011283e
--- /dev/null
+++ b/apps/studio/app/dashboard/v2/project/[projectRef]/data/indexes/page.tsx
@@ -0,0 +1,5 @@
+import { V2PlaceholderView } from '@/components/v2/views/V2PlaceholderView'
+
+export default function DataIndexesPage() {
+ return
+}
diff --git a/apps/studio/app/dashboard/v2/project/[projectRef]/data/oauth-apps/page.tsx b/apps/studio/app/dashboard/v2/project/[projectRef]/data/oauth-apps/page.tsx
new file mode 100644
index 00000000000..27a3d7d38c5
--- /dev/null
+++ b/apps/studio/app/dashboard/v2/project/[projectRef]/data/oauth-apps/page.tsx
@@ -0,0 +1,5 @@
+import { V2PlaceholderView } from '@/components/v2/views/V2PlaceholderView'
+
+export default function DataOAuthAppsPage() {
+ return
+}
diff --git a/apps/studio/app/dashboard/v2/project/[projectRef]/data/providers/page.tsx b/apps/studio/app/dashboard/v2/project/[projectRef]/data/providers/page.tsx
new file mode 100644
index 00000000000..2c9b9b2175c
--- /dev/null
+++ b/apps/studio/app/dashboard/v2/project/[projectRef]/data/providers/page.tsx
@@ -0,0 +1,5 @@
+import { V2PlaceholderView } from '@/components/v2/views/V2PlaceholderView'
+
+export default function DataProvidersPage() {
+ return
+}
diff --git a/apps/studio/app/dashboard/v2/project/[projectRef]/data/publications/page.tsx b/apps/studio/app/dashboard/v2/project/[projectRef]/data/publications/page.tsx
new file mode 100644
index 00000000000..96688665f86
--- /dev/null
+++ b/apps/studio/app/dashboard/v2/project/[projectRef]/data/publications/page.tsx
@@ -0,0 +1,5 @@
+import { V2PlaceholderView } from '@/components/v2/views/V2PlaceholderView'
+
+export default function DataPublicationsPage() {
+ return
+}
diff --git a/apps/studio/app/dashboard/v2/project/[projectRef]/data/roles/page.tsx b/apps/studio/app/dashboard/v2/project/[projectRef]/data/roles/page.tsx
new file mode 100644
index 00000000000..bd6617a547f
--- /dev/null
+++ b/apps/studio/app/dashboard/v2/project/[projectRef]/data/roles/page.tsx
@@ -0,0 +1,5 @@
+import { V2PlaceholderView } from '@/components/v2/views/V2PlaceholderView'
+
+export default function DataRolesPage() {
+ return
+}
diff --git a/apps/studio/app/dashboard/v2/project/[projectRef]/data/tables/[tableId]/data/page.tsx b/apps/studio/app/dashboard/v2/project/[projectRef]/data/tables/[tableId]/data/page.tsx
new file mode 100644
index 00000000000..dc96fc53e5d
--- /dev/null
+++ b/apps/studio/app/dashboard/v2/project/[projectRef]/data/tables/[tableId]/data/page.tsx
@@ -0,0 +1,5 @@
+import { V2TableDetailView } from '@/components/v2/views/V2TableDetailView'
+
+export default function TableDataPage() {
+ return
+}
diff --git a/apps/studio/app/dashboard/v2/project/[projectRef]/data/tables/[tableId]/indexes/page.tsx b/apps/studio/app/dashboard/v2/project/[projectRef]/data/tables/[tableId]/indexes/page.tsx
new file mode 100644
index 00000000000..48aaeb8ec08
--- /dev/null
+++ b/apps/studio/app/dashboard/v2/project/[projectRef]/data/tables/[tableId]/indexes/page.tsx
@@ -0,0 +1,5 @@
+import { V2TableDetailView } from '@/components/v2/views/V2TableDetailView'
+
+export default function TableIndexesPage() {
+ return
+}
diff --git a/apps/studio/app/dashboard/v2/project/[projectRef]/data/tables/[tableId]/layout.tsx b/apps/studio/app/dashboard/v2/project/[projectRef]/data/tables/[tableId]/layout.tsx
new file mode 100644
index 00000000000..fbaafaea94a
--- /dev/null
+++ b/apps/studio/app/dashboard/v2/project/[projectRef]/data/tables/[tableId]/layout.tsx
@@ -0,0 +1,54 @@
+'use client'
+
+import Link from 'next/link'
+import { useParams, usePathname } from 'next/navigation'
+import { cn } from 'ui'
+
+import { useV2Params } from '@/app/v2/V2ParamsContext'
+
+const SUB_TABS = [
+ { slug: 'data', label: 'Data' },
+ { slug: 'schema', label: 'Schema' },
+ { slug: 'policies', label: 'Policies' },
+ { slug: 'indexes', label: 'Indexes' },
+ { slug: 'settings', label: 'Settings' },
+]
+
+export default function TableDetailLayout({
+ children,
+}: {
+ children: React.ReactNode
+}) {
+ const params = useParams()
+ const pathname = usePathname()
+ const { projectRef } = useV2Params()
+ const tableId = params?.tableId as string
+
+ const base = projectRef ? `/dashboard/v2/project/${projectRef}/data/tables/${tableId}` : ''
+
+ return (
+
+
+ {SUB_TABS.map((tab) => {
+ const href = `${base}/${tab.slug}`
+ const isActive = pathname === href || pathname?.startsWith(href + '/')
+ return (
+
+ {tab.label}
+
+ )
+ })}
+
+
{children}
+
+ )
+}
diff --git a/apps/studio/app/dashboard/v2/project/[projectRef]/data/tables/[tableId]/page.tsx b/apps/studio/app/dashboard/v2/project/[projectRef]/data/tables/[tableId]/page.tsx
new file mode 100644
index 00000000000..ad763a45ee7
--- /dev/null
+++ b/apps/studio/app/dashboard/v2/project/[projectRef]/data/tables/[tableId]/page.tsx
@@ -0,0 +1,10 @@
+import { redirect } from 'next/navigation'
+
+export default async function TableDetailPage({
+ params,
+}: {
+ params: Promise<{ projectRef: string; tableId: string }>
+}) {
+ const { projectRef, tableId } = await params
+ redirect(`/dashboard/v2/project/${projectRef}/data/tables/${tableId}/data`)
+}
diff --git a/apps/studio/app/dashboard/v2/project/[projectRef]/data/tables/[tableId]/policies/page.tsx b/apps/studio/app/dashboard/v2/project/[projectRef]/data/tables/[tableId]/policies/page.tsx
new file mode 100644
index 00000000000..db18ae596a1
--- /dev/null
+++ b/apps/studio/app/dashboard/v2/project/[projectRef]/data/tables/[tableId]/policies/page.tsx
@@ -0,0 +1,5 @@
+import { V2TableDetailView } from '@/components/v2/views/V2TableDetailView'
+
+export default function TablePoliciesPage() {
+ return
+}
diff --git a/apps/studio/app/dashboard/v2/project/[projectRef]/data/tables/[tableId]/schema/page.tsx b/apps/studio/app/dashboard/v2/project/[projectRef]/data/tables/[tableId]/schema/page.tsx
new file mode 100644
index 00000000000..b80f6301261
--- /dev/null
+++ b/apps/studio/app/dashboard/v2/project/[projectRef]/data/tables/[tableId]/schema/page.tsx
@@ -0,0 +1,5 @@
+import { V2TableDetailView } from '@/components/v2/views/V2TableDetailView'
+
+export default function TableSchemaPage() {
+ return
+}
diff --git a/apps/studio/app/dashboard/v2/project/[projectRef]/data/tables/[tableId]/settings/page.tsx b/apps/studio/app/dashboard/v2/project/[projectRef]/data/tables/[tableId]/settings/page.tsx
new file mode 100644
index 00000000000..0bc59d9b30c
--- /dev/null
+++ b/apps/studio/app/dashboard/v2/project/[projectRef]/data/tables/[tableId]/settings/page.tsx
@@ -0,0 +1,5 @@
+import { V2TableDetailView } from '@/components/v2/views/V2TableDetailView'
+
+export default function TableSettingsPage() {
+ return
+}
diff --git a/apps/studio/app/dashboard/v2/project/[projectRef]/data/tables/page.tsx b/apps/studio/app/dashboard/v2/project/[projectRef]/data/tables/page.tsx
new file mode 100644
index 00000000000..10084a7aeda
--- /dev/null
+++ b/apps/studio/app/dashboard/v2/project/[projectRef]/data/tables/page.tsx
@@ -0,0 +1,5 @@
+import { V2TablesList } from '@/components/v2/views/V2TablesList'
+
+export default function DataTablesPage() {
+ return
+}
diff --git a/apps/studio/app/dashboard/v2/project/[projectRef]/data/types/page.tsx b/apps/studio/app/dashboard/v2/project/[projectRef]/data/types/page.tsx
new file mode 100644
index 00000000000..b59e212011d
--- /dev/null
+++ b/apps/studio/app/dashboard/v2/project/[projectRef]/data/types/page.tsx
@@ -0,0 +1,5 @@
+import { V2PlaceholderView } from '@/components/v2/views/V2PlaceholderView'
+
+export default function DataTypesPage() {
+ return
+}
diff --git a/apps/studio/app/dashboard/v2/project/[projectRef]/data/users/page.tsx b/apps/studio/app/dashboard/v2/project/[projectRef]/data/users/page.tsx
new file mode 100644
index 00000000000..f3a410d2e77
--- /dev/null
+++ b/apps/studio/app/dashboard/v2/project/[projectRef]/data/users/page.tsx
@@ -0,0 +1,5 @@
+import { V2PlaceholderView } from '@/components/v2/views/V2PlaceholderView'
+
+export default function DataUsersPage() {
+ return
+}
diff --git a/apps/studio/app/dashboard/v2/project/[projectRef]/layout.tsx b/apps/studio/app/dashboard/v2/project/[projectRef]/layout.tsx
new file mode 100644
index 00000000000..99d16fa2341
--- /dev/null
+++ b/apps/studio/app/dashboard/v2/project/[projectRef]/layout.tsx
@@ -0,0 +1,9 @@
+import { ProjectShell } from './ProjectShell'
+
+export default function ProjectLayout({
+ children,
+}: {
+ children: React.ReactNode
+}) {
+ return {children}
+}
diff --git a/apps/studio/app/dashboard/v2/project/[projectRef]/obs/alerts/page.tsx b/apps/studio/app/dashboard/v2/project/[projectRef]/obs/alerts/page.tsx
new file mode 100644
index 00000000000..38bf2643be8
--- /dev/null
+++ b/apps/studio/app/dashboard/v2/project/[projectRef]/obs/alerts/page.tsx
@@ -0,0 +1,5 @@
+import { V2PlaceholderView } from '@/components/v2/views/V2PlaceholderView'
+
+export default function ObsAlertsPage() {
+ return
+}
diff --git a/apps/studio/app/dashboard/v2/project/[projectRef]/obs/logs/api/page.tsx b/apps/studio/app/dashboard/v2/project/[projectRef]/obs/logs/api/page.tsx
new file mode 100644
index 00000000000..c6407030269
--- /dev/null
+++ b/apps/studio/app/dashboard/v2/project/[projectRef]/obs/logs/api/page.tsx
@@ -0,0 +1,5 @@
+import { V2PlaceholderView } from '@/components/v2/views/V2PlaceholderView'
+
+export default function ObsLogsApiPage() {
+ return
+}
diff --git a/apps/studio/app/dashboard/v2/project/[projectRef]/obs/logs/auth/page.tsx b/apps/studio/app/dashboard/v2/project/[projectRef]/obs/logs/auth/page.tsx
new file mode 100644
index 00000000000..6228bfc536f
--- /dev/null
+++ b/apps/studio/app/dashboard/v2/project/[projectRef]/obs/logs/auth/page.tsx
@@ -0,0 +1,5 @@
+import { V2PlaceholderView } from '@/components/v2/views/V2PlaceholderView'
+
+export default function ObsLogsAuthPage() {
+ return
+}
diff --git a/apps/studio/app/dashboard/v2/project/[projectRef]/obs/logs/page.tsx b/apps/studio/app/dashboard/v2/project/[projectRef]/obs/logs/page.tsx
new file mode 100644
index 00000000000..fc548badbb0
--- /dev/null
+++ b/apps/studio/app/dashboard/v2/project/[projectRef]/obs/logs/page.tsx
@@ -0,0 +1,5 @@
+import { V2PlaceholderView } from '@/components/v2/views/V2PlaceholderView'
+
+export default function ObsLogsPage() {
+ return
+}
diff --git a/apps/studio/app/dashboard/v2/project/[projectRef]/obs/logs/postgres/page.tsx b/apps/studio/app/dashboard/v2/project/[projectRef]/obs/logs/postgres/page.tsx
new file mode 100644
index 00000000000..45c70583532
--- /dev/null
+++ b/apps/studio/app/dashboard/v2/project/[projectRef]/obs/logs/postgres/page.tsx
@@ -0,0 +1,5 @@
+import { V2PlaceholderView } from '@/components/v2/views/V2PlaceholderView'
+
+export default function ObsLogsPostgresPage() {
+ return
+}
diff --git a/apps/studio/app/dashboard/v2/project/[projectRef]/obs/metrics/connections/page.tsx b/apps/studio/app/dashboard/v2/project/[projectRef]/obs/metrics/connections/page.tsx
new file mode 100644
index 00000000000..30cdf6e0a2c
--- /dev/null
+++ b/apps/studio/app/dashboard/v2/project/[projectRef]/obs/metrics/connections/page.tsx
@@ -0,0 +1,5 @@
+import { V2PlaceholderView } from '@/components/v2/views/V2PlaceholderView'
+
+export default function ObsMetricsConnectionsPage() {
+ return
+}
diff --git a/apps/studio/app/dashboard/v2/project/[projectRef]/obs/metrics/cpu/page.tsx b/apps/studio/app/dashboard/v2/project/[projectRef]/obs/metrics/cpu/page.tsx
new file mode 100644
index 00000000000..513055b5969
--- /dev/null
+++ b/apps/studio/app/dashboard/v2/project/[projectRef]/obs/metrics/cpu/page.tsx
@@ -0,0 +1,5 @@
+import { V2PlaceholderView } from '@/components/v2/views/V2PlaceholderView'
+
+export default function ObsMetricsCpuPage() {
+ return
+}
diff --git a/apps/studio/app/dashboard/v2/project/[projectRef]/obs/metrics/disk/page.tsx b/apps/studio/app/dashboard/v2/project/[projectRef]/obs/metrics/disk/page.tsx
new file mode 100644
index 00000000000..2301a7e4dd3
--- /dev/null
+++ b/apps/studio/app/dashboard/v2/project/[projectRef]/obs/metrics/disk/page.tsx
@@ -0,0 +1,5 @@
+import { V2PlaceholderView } from '@/components/v2/views/V2PlaceholderView'
+
+export default function ObsMetricsDiskPage() {
+ return
+}
diff --git a/apps/studio/app/dashboard/v2/project/[projectRef]/obs/metrics/memory/page.tsx b/apps/studio/app/dashboard/v2/project/[projectRef]/obs/metrics/memory/page.tsx
new file mode 100644
index 00000000000..5fe6a1620f4
--- /dev/null
+++ b/apps/studio/app/dashboard/v2/project/[projectRef]/obs/metrics/memory/page.tsx
@@ -0,0 +1,5 @@
+import { V2PlaceholderView } from '@/components/v2/views/V2PlaceholderView'
+
+export default function ObsMetricsMemoryPage() {
+ return
+}
diff --git a/apps/studio/app/dashboard/v2/project/[projectRef]/page.tsx b/apps/studio/app/dashboard/v2/project/[projectRef]/page.tsx
new file mode 100644
index 00000000000..5fce7ef4e58
--- /dev/null
+++ b/apps/studio/app/dashboard/v2/project/[projectRef]/page.tsx
@@ -0,0 +1,10 @@
+import { redirect } from 'next/navigation'
+
+export default async function V2ProjectPage({
+ params,
+}: {
+ params: Promise<{ projectRef: string }>
+}) {
+ const { projectRef } = await params
+ redirect(`/dashboard/v2/project/${projectRef}/data/tables`)
+}
diff --git a/apps/studio/app/dashboard/v2/project/[projectRef]/settings/api-keys/page.tsx b/apps/studio/app/dashboard/v2/project/[projectRef]/settings/api-keys/page.tsx
new file mode 100644
index 00000000000..8c62569a565
--- /dev/null
+++ b/apps/studio/app/dashboard/v2/project/[projectRef]/settings/api-keys/page.tsx
@@ -0,0 +1,5 @@
+import { V2PlaceholderView } from '@/components/v2/views/V2PlaceholderView'
+
+export default function SettingsApiKeysPage() {
+ return
+}
diff --git a/apps/studio/app/dashboard/v2/project/[projectRef]/settings/auth/page.tsx b/apps/studio/app/dashboard/v2/project/[projectRef]/settings/auth/page.tsx
new file mode 100644
index 00000000000..e90d25ad538
--- /dev/null
+++ b/apps/studio/app/dashboard/v2/project/[projectRef]/settings/auth/page.tsx
@@ -0,0 +1,5 @@
+import { V2PlaceholderView } from '@/components/v2/views/V2PlaceholderView'
+
+export default function SettingsAuthPage() {
+ return
+}
diff --git a/apps/studio/app/dashboard/v2/project/[projectRef]/settings/billing/page.tsx b/apps/studio/app/dashboard/v2/project/[projectRef]/settings/billing/page.tsx
new file mode 100644
index 00000000000..7f01294d722
--- /dev/null
+++ b/apps/studio/app/dashboard/v2/project/[projectRef]/settings/billing/page.tsx
@@ -0,0 +1,5 @@
+import { V2PlaceholderView } from '@/components/v2/views/V2PlaceholderView'
+
+export default function SettingsBillingPage() {
+ return
+}
diff --git a/apps/studio/app/dashboard/v2/project/[projectRef]/settings/branches/page.tsx b/apps/studio/app/dashboard/v2/project/[projectRef]/settings/branches/page.tsx
new file mode 100644
index 00000000000..9e4addbe973
--- /dev/null
+++ b/apps/studio/app/dashboard/v2/project/[projectRef]/settings/branches/page.tsx
@@ -0,0 +1,5 @@
+import { V2PlaceholderView } from '@/components/v2/views/V2PlaceholderView'
+
+export default function SettingsBranchesPage() {
+ return
+}
diff --git a/apps/studio/app/dashboard/v2/project/[projectRef]/settings/compute/page.tsx b/apps/studio/app/dashboard/v2/project/[projectRef]/settings/compute/page.tsx
new file mode 100644
index 00000000000..e08b892a0a7
--- /dev/null
+++ b/apps/studio/app/dashboard/v2/project/[projectRef]/settings/compute/page.tsx
@@ -0,0 +1,5 @@
+import { V2PlaceholderView } from '@/components/v2/views/V2PlaceholderView'
+
+export default function SettingsComputePage() {
+ return
+}
diff --git a/apps/studio/app/dashboard/v2/project/[projectRef]/settings/functions/page.tsx b/apps/studio/app/dashboard/v2/project/[projectRef]/settings/functions/page.tsx
new file mode 100644
index 00000000000..c6183b6674f
--- /dev/null
+++ b/apps/studio/app/dashboard/v2/project/[projectRef]/settings/functions/page.tsx
@@ -0,0 +1,5 @@
+import { V2PlaceholderView } from '@/components/v2/views/V2PlaceholderView'
+
+export default function SettingsFunctionsPage() {
+ return
+}
diff --git a/apps/studio/app/dashboard/v2/project/[projectRef]/settings/general/page.tsx b/apps/studio/app/dashboard/v2/project/[projectRef]/settings/general/page.tsx
new file mode 100644
index 00000000000..e4f3696280d
--- /dev/null
+++ b/apps/studio/app/dashboard/v2/project/[projectRef]/settings/general/page.tsx
@@ -0,0 +1,5 @@
+import { V2PlaceholderView } from '@/components/v2/views/V2PlaceholderView'
+
+export default function SettingsGeneralPage() {
+ return
+}
diff --git a/apps/studio/app/dashboard/v2/project/[projectRef]/settings/members/page.tsx b/apps/studio/app/dashboard/v2/project/[projectRef]/settings/members/page.tsx
new file mode 100644
index 00000000000..8665362dd2e
--- /dev/null
+++ b/apps/studio/app/dashboard/v2/project/[projectRef]/settings/members/page.tsx
@@ -0,0 +1,5 @@
+import { V2PlaceholderView } from '@/components/v2/views/V2PlaceholderView'
+
+export default function SettingsMembersPage() {
+ return
+}
diff --git a/apps/studio/app/dashboard/v2/project/[projectRef]/settings/merge-requests/page.tsx b/apps/studio/app/dashboard/v2/project/[projectRef]/settings/merge-requests/page.tsx
new file mode 100644
index 00000000000..7f2da09c9aa
--- /dev/null
+++ b/apps/studio/app/dashboard/v2/project/[projectRef]/settings/merge-requests/page.tsx
@@ -0,0 +1,5 @@
+import { V2PlaceholderView } from '@/components/v2/views/V2PlaceholderView'
+
+export default function SettingsMergeRequestsPage() {
+ return
+}
diff --git a/apps/studio/app/dashboard/v2/project/[projectRef]/settings/network/page.tsx b/apps/studio/app/dashboard/v2/project/[projectRef]/settings/network/page.tsx
new file mode 100644
index 00000000000..8fec1e08546
--- /dev/null
+++ b/apps/studio/app/dashboard/v2/project/[projectRef]/settings/network/page.tsx
@@ -0,0 +1,5 @@
+import { V2PlaceholderView } from '@/components/v2/views/V2PlaceholderView'
+
+export default function SettingsNetworkPage() {
+ return
+}
diff --git a/apps/studio/app/dashboard/v2/project/[projectRef]/settings/realtime/page.tsx b/apps/studio/app/dashboard/v2/project/[projectRef]/settings/realtime/page.tsx
new file mode 100644
index 00000000000..5dd95007e14
--- /dev/null
+++ b/apps/studio/app/dashboard/v2/project/[projectRef]/settings/realtime/page.tsx
@@ -0,0 +1,5 @@
+import { V2PlaceholderView } from '@/components/v2/views/V2PlaceholderView'
+
+export default function SettingsRealtimePage() {
+ return
+}
diff --git a/apps/studio/app/dashboard/v2/project/[projectRef]/settings/storage/page.tsx b/apps/studio/app/dashboard/v2/project/[projectRef]/settings/storage/page.tsx
new file mode 100644
index 00000000000..ed0eb19cf5f
--- /dev/null
+++ b/apps/studio/app/dashboard/v2/project/[projectRef]/settings/storage/page.tsx
@@ -0,0 +1,5 @@
+import { V2PlaceholderView } from '@/components/v2/views/V2PlaceholderView'
+
+export default function SettingsStoragePage() {
+ return
+}
diff --git a/apps/studio/app/layout.tsx b/apps/studio/app/layout.tsx
new file mode 100644
index 00000000000..6f7fe9e284f
--- /dev/null
+++ b/apps/studio/app/layout.tsx
@@ -0,0 +1,46 @@
+import type { Metadata } from 'next'
+
+import { AppRouterProviders } from '@/app/v2/AppRouterProviders'
+import { genFaviconData } from 'common/MetaFavicons/app-router'
+import 'react-data-grid/lib/styles.css'
+import 'styles/code.scss'
+import 'styles/contextMenu.scss'
+import 'styles/editor.scss'
+import 'styles/focus.scss'
+import 'styles/graphiql-base.scss'
+import 'styles/grid.scss'
+import 'styles/main.scss'
+import 'styles/markdown-preview.scss'
+import 'styles/monaco.scss'
+import 'styles/react-data-grid-logs.scss'
+import 'styles/reactflow.scss'
+import 'styles/storage.scss'
+import 'styles/stripe.scss'
+import 'styles/toast.scss'
+import 'styles/typography.scss'
+import 'styles/ui.scss'
+import 'ui-patterns/ShimmeringLoader/index.css'
+import 'ui/build/css/themes/dark.css'
+import 'ui/build/css/themes/light.css'
+
+const BASE_PATH = process.env.NEXT_PUBLIC_BASE_PATH ?? ''
+
+export const metadata: Metadata = {
+ applicationName: 'Supabase Studio',
+ title: 'Supabase',
+ icons: genFaviconData(BASE_PATH),
+}
+
+export default function RootLayout({
+ children,
+}: {
+ children: React.ReactNode
+}) {
+ return (
+
+
+ {children}
+
+
+ )
+}
diff --git a/apps/studio/app/v2/AppRouterProviders.tsx b/apps/studio/app/v2/AppRouterProviders.tsx
new file mode 100644
index 00000000000..562ca655fab
--- /dev/null
+++ b/apps/studio/app/v2/AppRouterProviders.tsx
@@ -0,0 +1,35 @@
+'use client'
+
+import { QueryClientProvider } from '@tanstack/react-query'
+import { ThemeProvider } from 'common'
+import { useRootQueryClient } from 'data/query-client'
+import { customFont, sourceCodePro } from 'fonts'
+import { AuthProvider } from 'lib/auth'
+import { ReactNode } from 'react'
+import { TooltipProvider } from 'ui'
+
+export function AppRouterProviders({ children }: { children: ReactNode }) {
+ const queryClient = useRootQueryClient()
+
+ return (
+
+
+
+
+
+ {children}
+
+
+
+
+ )
+}
diff --git a/apps/studio/app/v2/V2ParamsContext.tsx b/apps/studio/app/v2/V2ParamsContext.tsx
new file mode 100644
index 00000000000..0ac8e901c54
--- /dev/null
+++ b/apps/studio/app/v2/V2ParamsContext.tsx
@@ -0,0 +1,51 @@
+'use client'
+
+import { useParams } from 'next/navigation'
+import { createContext, useContext, type ReactNode } from 'react'
+
+export interface V2Params {
+ orgSlug: string | undefined
+ projectRef: string | undefined
+}
+
+const V2ParamsContext = createContext({
+ orgSlug: undefined,
+ projectRef: undefined,
+})
+
+/** Use in project scope: params have projectRef; orgSlug is passed in after being derived from project. */
+export function V2ParamsProvider({
+ children,
+ projectRef,
+ orgSlug,
+}: {
+ children: ReactNode
+ projectRef: string | undefined
+ orgSlug: string | undefined
+}) {
+ return (
+
+ {children}
+
+ )
+}
+
+/** Use in org scope: params have orgSlug only. */
+export function V2OrgParamsProvider({ children }: { children: ReactNode }) {
+ const params = useParams()
+ const orgSlug = params?.orgSlug as string | undefined
+
+ return (
+
+ {children}
+
+ )
+}
+
+export function useV2Params(): V2Params {
+ const ctx = useContext(V2ParamsContext)
+ if (!ctx) {
+ return { orgSlug: undefined, projectRef: undefined }
+ }
+ return ctx
+}
diff --git a/apps/studio/app/v2/layout.tsx b/apps/studio/app/v2/layout.tsx
new file mode 100644
index 00000000000..0cc63f94090
--- /dev/null
+++ b/apps/studio/app/v2/layout.tsx
@@ -0,0 +1,3 @@
+export default function V2Layout({ children }: { children: React.ReactNode }) {
+ return <>{children}>
+}
diff --git a/apps/studio/components/ui/AIAssistantPanel/AIAssistant.tsx b/apps/studio/components/ui/AIAssistantPanel/AIAssistant.tsx
index 9916de1310f..2cfd425d715 100644
--- a/apps/studio/components/ui/AIAssistantPanel/AIAssistant.tsx
+++ b/apps/studio/components/ui/AIAssistantPanel/AIAssistant.tsx
@@ -3,7 +3,7 @@ import { useChat } from '@ai-sdk/react'
import { lastAssistantMessageIsCompleteWithToolCalls } from 'ai'
import { AnimatePresence, motion } from 'framer-motion'
import { Eraser, Pencil, X } from 'lucide-react'
-import { useRouter } from 'next/router'
+import { useRouter } from 'next/compat/router'
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { LOCAL_STORAGE_KEYS, useFlag } from 'common'
@@ -117,7 +117,7 @@ export const AIAssistant = ({ className }: AIAssistantProps) => {
const { mutateAsync: rateMessage } = useRateMessageMutation()
- const isInSQLEditor = router.pathname.includes('/sql/[id]')
+ const isInSQLEditor = router?.pathname?.includes('/sql/') ?? false
const snippet = snippets[entityId ?? '']
const snippetContent = snippet?.snippet?.content?.sql
diff --git a/apps/studio/components/ui/AIAssistantPanel/DisplayBlockRenderer.tsx b/apps/studio/components/ui/AIAssistantPanel/DisplayBlockRenderer.tsx
index 6ca37f7c1f1..67584ff0a78 100644
--- a/apps/studio/components/ui/AIAssistantPanel/DisplayBlockRenderer.tsx
+++ b/apps/studio/components/ui/AIAssistantPanel/DisplayBlockRenderer.tsx
@@ -1,6 +1,6 @@
import { PermissionAction } from '@supabase/shared-types/out/constants'
import { useQueryClient } from '@tanstack/react-query'
-import { useRouter } from 'next/router'
+import { useRouter } from 'next/compat/router'
import { useRef, useState, type DragEvent, type PropsWithChildren } from 'react'
import { useParams } from 'common'
@@ -90,8 +90,8 @@ export const DisplayBlockRenderer = ({
const [rows, setRows] = useState(
Array.isArray(initialResults) ? initialResults : undefined
)
- const isReportsPage = router.pathname.endsWith('/reports/[id]')
- const isHomePage = router.pathname === '/project/[ref]'
+ const isReportsPage = router?.pathname?.includes('/reports/') ?? false
+ const isHomePage = router?.pathname?.includes('/project/') ?? false
const isDraggableToReports = canCreateSQLSnippet && (isReportsPage || isHomePage)
const label = initialArgs.label || 'SQL Results'
const [isWriteQuery, setIsWriteQuery] = useState(initialArgs.isWriteQuery || false)
diff --git a/apps/studio/components/ui/AIAssistantPanel/ModelSelector.tsx b/apps/studio/components/ui/AIAssistantPanel/ModelSelector.tsx
index d7c66b64d13..016b57c0c68 100644
--- a/apps/studio/components/ui/AIAssistantPanel/ModelSelector.tsx
+++ b/apps/studio/components/ui/AIAssistantPanel/ModelSelector.tsx
@@ -1,7 +1,7 @@
import { Check, ChevronsUpDown } from 'lucide-react'
import { useState } from 'react'
import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization'
-import { useRouter } from 'next/router'
+import { useRouter } from 'next/compat/router'
import {
Badge,
Button,
diff --git a/apps/studio/components/ui/HelpPanel/HelpOptionsList.tsx b/apps/studio/components/ui/HelpPanel/HelpOptionsList.tsx
index 278f3558462..5036c6b6c98 100644
--- a/apps/studio/components/ui/HelpPanel/HelpOptionsList.tsx
+++ b/apps/studio/components/ui/HelpPanel/HelpOptionsList.tsx
@@ -2,7 +2,7 @@ import type { SupportFormUrlKeys } from 'components/interfaces/Support/SupportFo
import { SupportLink } from 'components/interfaces/Support/SupportLink'
import { DOCS_URL } from 'lib/constants'
import { Activity, BookOpen, Mail, Wrench } from 'lucide-react'
-import { useRouter } from 'next/router'
+import { useRouter } from 'next/compat/router'
import SVG from 'react-inlinesvg'
import { AiIconAnimation, ButtonGroup, ButtonGroupItem } from 'ui'
@@ -32,7 +32,7 @@ export const HelpOptionsList = ({
size = 'tiny',
}: HelpOptionsListProps) => {
const router = useRouter()
- const basePath = router.basePath ?? ''
+ const basePath = router?.basePath ?? ''
const ids = HELP_OPTION_IDS.filter((id) => !excludeIds.includes(id))
diff --git a/apps/studio/components/ui/HelpPanel/HelpPanel.tsx b/apps/studio/components/ui/HelpPanel/HelpPanel.tsx
index 264450d190d..898ac9c190b 100644
--- a/apps/studio/components/ui/HelpPanel/HelpPanel.tsx
+++ b/apps/studio/components/ui/HelpPanel/HelpPanel.tsx
@@ -4,7 +4,7 @@ import { SIDEBAR_KEYS } from 'components/layouts/ProjectLayout/LayoutSidebar/Lay
import { ButtonTooltip } from 'components/ui/ButtonTooltip'
import { X } from 'lucide-react'
import Image from 'next/image'
-import { useRouter } from 'next/router'
+import { useRouter } from 'next/compat/router'
import SVG from 'react-inlinesvg'
import { useAiAssistantStateSnapshot } from 'state/ai-assistant-state'
import { useSidebarManagerSnapshot } from 'state/sidebar-manager-state'
@@ -26,6 +26,7 @@ export const HelpPanel = ({
const snap = useAiAssistantStateSnapshot()
const { openSidebar, closeSidebar } = useSidebarManagerSnapshot()
const router = useRouter()
+ const basePath = router?.basePath ?? ''
const __styles = styleHandler('popover')
@@ -76,7 +77,7 @@ export const HelpPanel = ({
>
}
+ icon={}
>
Join us on Discord
diff --git a/apps/studio/components/v2/ActivityBar.tsx b/apps/studio/components/v2/ActivityBar.tsx
new file mode 100644
index 00000000000..b2a17a6ee98
--- /dev/null
+++ b/apps/studio/components/v2/ActivityBar.tsx
@@ -0,0 +1,145 @@
+'use client'
+
+import { useProjectLintsQuery } from 'data/lint/lint-query'
+import { Database, LayoutDashboard, Settings } from 'lucide-react'
+import Link from 'next/link'
+import { usePathname } from 'next/navigation'
+import { cn, Tooltip, TooltipContent, TooltipTrigger } from 'ui'
+
+import { useV2Params } from '@/app/v2/V2ParamsContext'
+
+export type ActivityId = 'data' | 'obs' | 'settings'
+
+interface ActivityBarProps {
+ side: 'left' | 'right'
+ activeId: string | null
+ onSelect?: (id: string) => void
+ items: Array<{
+ id: string
+ icon: React.ReactNode
+ label: string
+ badge?: boolean
+ href?: string
+ }>
+ /** For right bar, tooltips show on the left of the icon */
+ tooltipSide?: 'left' | 'right'
+ bottomContent?: React.ReactNode
+}
+
+export function ActivityBar({
+ side,
+ activeId,
+ onSelect,
+ items,
+ tooltipSide = 'right',
+ bottomContent,
+}: ActivityBarProps) {
+ return (
+
+ )
+}
+
+export function LeftActivityBar() {
+ const pathname = usePathname()
+ const { projectRef } = useV2Params()
+ const activeId: ActivityId | null = pathname?.includes('/data/')
+ ? 'data'
+ : pathname?.includes('/obs/')
+ ? 'obs'
+ : pathname?.includes('/settings/')
+ ? 'settings'
+ : null
+
+ const { data: lints } = useProjectLintsQuery({ projectRef })
+ const hasAdvisorWarnings = (lints?.length ?? 0) > 0
+
+ const base = projectRef ? `/dashboard/v2/project/${projectRef}` : '#'
+
+ return (
+ ,
+ label: 'Data',
+ href: `${base}/data/tables`,
+ },
+ {
+ id: 'obs',
+ icon: ,
+ label: 'Observability',
+ badge: hasAdvisorWarnings,
+ href: `${base}/obs/logs`,
+ },
+ {
+ id: 'settings',
+ icon: ,
+ label: 'Settings',
+ href: `${base}/settings/general`,
+ },
+ ]}
+ tooltipSide="right"
+ />
+ )
+}
diff --git a/apps/studio/components/v2/AdvisorsPanel.tsx b/apps/studio/components/v2/AdvisorsPanel.tsx
new file mode 100644
index 00000000000..d36a7291572
--- /dev/null
+++ b/apps/studio/components/v2/AdvisorsPanel.tsx
@@ -0,0 +1,53 @@
+'use client'
+
+import Link from 'next/link'
+import { useV2Params } from '@/app/v2/V2ParamsContext'
+import { useProjectLintsQuery } from 'data/lint/lint-query'
+import { ShimmeringLoader } from 'ui-patterns/ShimmeringLoader'
+
+export function AdvisorsPanel() {
+ const { projectRef } = useV2Params()
+ const { data: lints, isPending, isError } = useProjectLintsQuery({ projectRef })
+
+ if (isPending) return
+ if (isError) return Failed to load advisors.
+
+ const list = lints ?? []
+ if (list.length === 0) {
+ return (
+
+ No advisor warnings. Your project looks good.
+
+ )
+ }
+
+ return (
+
+ {list.slice(0, 20).map((lint, i) => (
+
+
{lint.name}
+
{lint.description}
+
+ {lint.level}
+
+
+ ))}
+
+ )
+}
diff --git a/apps/studio/components/v2/AssistantPanel.tsx b/apps/studio/components/v2/AssistantPanel.tsx
new file mode 100644
index 00000000000..8aa9ce917f7
--- /dev/null
+++ b/apps/studio/components/v2/AssistantPanel.tsx
@@ -0,0 +1,32 @@
+'use client'
+
+import { usePathname } from 'next/navigation'
+import { useState } from 'react'
+import { Button, Input } from 'ui'
+
+export function AssistantPanel() {
+ const pathname = usePathname()
+ const viewName = pathname?.split('/').filter(Boolean).slice(-2).join(' / ') ?? 'Dashboard'
+ const [input, setInput] = useState('')
+
+ return (
+
+
Context: {viewName}
+
+
Welcome. I can help you with your database, auth, storage, and more.
+
Ask a question or describe what you want to do.
+
+
+ setInput(e.target.value)}
+ placeholder="Ask anything..."
+ className="text-sm flex-1"
+ />
+
+
+
+ )
+}
diff --git a/apps/studio/components/v2/BrowserPanel.tsx b/apps/studio/components/v2/BrowserPanel.tsx
new file mode 100644
index 00000000000..522f0ea6d83
--- /dev/null
+++ b/apps/studio/components/v2/BrowserPanel.tsx
@@ -0,0 +1,257 @@
+'use client'
+
+import { ChevronRight } from 'lucide-react'
+import Link from 'next/link'
+import { usePathname } from 'next/navigation'
+import { cn } from 'ui'
+
+import { useV2DataCounts } from './useV2DataCounts'
+import { useV2Params } from '@/app/v2/V2ParamsContext'
+import { useV2DashboardStore } from '@/stores/v2-dashboard'
+
+const DATA_GROUPS = [
+ {
+ id: 'data-database',
+ label: 'Database',
+ items: [
+ { href: 'tables', label: 'Tables', countKey: 'tables' },
+ { href: 'functions', label: 'Functions', countKey: 'functions' },
+ { href: 'types', label: 'Enumerated types', countKey: 'types' },
+ { href: 'roles', label: 'Roles', countKey: 'roles' },
+ { href: 'extensions', label: 'Extensions', countKey: 'extensions' },
+ { href: 'indexes', label: 'Indexes', countKey: 'indexes' },
+ { href: 'publications', label: 'Publications', countKey: 'publications' },
+ ],
+ },
+ {
+ id: 'data-auth',
+ label: 'Auth',
+ items: [
+ { href: 'users', label: 'Users', countKey: 'users' },
+ { href: 'providers', label: 'Providers', countKey: 'providers' },
+ { href: 'oauth-apps', label: 'OAuth apps', countKey: 'oauthApps' },
+ ],
+ },
+ {
+ id: 'data-storage',
+ label: 'Storage',
+ items: [{ href: 'buckets', label: 'Buckets', countKey: 'buckets' }],
+ },
+ {
+ id: 'data-edge-functions',
+ label: 'Edge functions',
+ items: [{ href: 'edge-functions', label: 'Functions', countKey: 'edgeFunctions' }],
+ },
+ {
+ id: 'data-realtime',
+ label: 'Realtime',
+ items: [{ href: 'channels', label: 'Channels', countKey: 'channels' }],
+ },
+]
+
+const OBS_GROUPS = [
+ {
+ id: 'obs-logs',
+ label: 'Log streams',
+ items: [
+ { href: 'logs', label: 'All logs' },
+ { href: 'logs/api', label: 'API gateway' },
+ { href: 'logs/postgres', label: 'Postgres' },
+ { href: 'logs/auth', label: 'Auth' },
+ ],
+ },
+ {
+ id: 'obs-metrics',
+ label: 'Metrics',
+ items: [
+ { href: 'metrics/connections', label: 'Connections' },
+ { href: 'metrics/cpu', label: 'CPU' },
+ { href: 'metrics/memory', label: 'Memory' },
+ { href: 'metrics/disk', label: 'Disk' },
+ ],
+ },
+ {
+ id: 'obs-alerts',
+ label: 'Alerts',
+ items: [{ href: 'alerts', label: 'Active alerts' }],
+ },
+]
+
+const SETTINGS_GROUPS = [
+ {
+ id: 'settings-project',
+ label: 'Project',
+ items: [
+ { href: 'general', label: 'General' },
+ { href: 'compute', label: 'Compute & disk' },
+ { href: 'network', label: 'Network' },
+ { href: 'api-keys', label: 'API keys' },
+ ],
+ },
+ {
+ id: 'settings-branches',
+ label: 'Branches',
+ items: [
+ { href: 'branches', label: 'Branches' },
+ { href: 'merge-requests', label: 'Merge requests' },
+ ],
+ },
+ {
+ id: 'settings-modules',
+ label: 'Modules',
+ items: [
+ { href: 'auth', label: 'Auth config' },
+ { href: 'storage', label: 'Storage config' },
+ { href: 'functions', label: 'Functions config' },
+ { href: 'realtime', label: 'Realtime config' },
+ ],
+ },
+ {
+ id: 'settings-org',
+ label: 'Organization',
+ items: [
+ { href: 'members', label: 'Members' },
+ { href: 'billing', label: 'Billing' },
+ ],
+ },
+]
+
+type GroupStatus = 'green' | 'yellow' | 'red'
+
+function getMockGroupStatus(groupId: string): GroupStatus {
+ const statuses: GroupStatus[] = ['green', 'yellow', 'red']
+ const hash = groupId.split('').reduce((acc, ch) => acc + ch.charCodeAt(0), 0)
+ return statuses[hash % statuses.length]
+}
+
+function getStatusClass(status: GroupStatus): string {
+ switch (status) {
+ case 'green':
+ return 'bg-emerald-500'
+ case 'yellow':
+ return 'bg-amber-500'
+ case 'red':
+ return 'bg-red-500'
+ default:
+ return 'bg-muted-foreground'
+ }
+}
+
+function getCount(counts: ReturnType, key: string): number | string {
+ switch (key) {
+ case 'tables':
+ return counts.tables
+ case 'functions':
+ return counts.functions
+ case 'types':
+ return counts.types
+ case 'roles':
+ return counts.roles
+ case 'extensions':
+ return counts.extensions
+ case 'indexes':
+ return '—'
+ case 'publications':
+ return counts.publications
+ case 'users':
+ return counts.users
+ case 'providers':
+ case 'oauthApps':
+ case 'channels':
+ return '—'
+ case 'buckets':
+ return counts.buckets
+ case 'edgeFunctions':
+ return counts.edgeFunctions
+ default:
+ return 0
+ }
+}
+
+export function BrowserPanel() {
+ const pathname = usePathname()
+ const { orgSlug, projectRef } = useV2Params()
+ const { expandedGroups, toggleGroup } = useV2DashboardStore()
+ const counts = useV2DataCounts(projectRef)
+
+ const base = projectRef ? `/dashboard/v2/project/${projectRef}` : ''
+ const isData = pathname?.includes('/data/')
+ const isObs = pathname?.includes('/obs/')
+ const isSettings = pathname?.includes('/settings/')
+
+ const title = isData ? 'Data' : isObs ? 'Observability' : isSettings ? 'Settings' : ''
+
+ const groups = isData ? DATA_GROUPS : isObs ? OBS_GROUPS : isSettings ? SETTINGS_GROUPS : []
+
+ return (
+
+
+ {title}
+
+
+ {groups.map((group) => {
+ const isExpanded = expandedGroups[group.id] !== false
+ const status = getMockGroupStatus(group.id)
+ return (
+
+
+ {isExpanded &&
+ group.items.map((item) => {
+ const itemHref = isData
+ ? `${base}/data/${item.href}`
+ : isObs
+ ? `${base}/obs/${item.href}`
+ : `${base}/settings/${item.href}`
+ const isActive = pathname === itemHref || pathname?.startsWith(itemHref + '/')
+ return (
+
+ {item.label}
+ {'countKey' in item && (
+
+ {getCount(counts, String(item.countKey))}
+
+ )}
+
+ )
+ })}
+
+ )
+ })}
+ {isData && (
+
+
+ + Add module
+
+
+ )}
+
+
+ )
+}
diff --git a/apps/studio/components/v2/EditorFrame.tsx b/apps/studio/components/v2/EditorFrame.tsx
new file mode 100644
index 00000000000..391c52c17fb
--- /dev/null
+++ b/apps/studio/components/v2/EditorFrame.tsx
@@ -0,0 +1,44 @@
+'use client'
+
+import { usePathname } from 'next/navigation'
+import { cn } from 'ui'
+
+import { useV2Params } from '@/app/v2/V2ParamsContext'
+
+import { TabBar } from './TabBar'
+import { StaticTitle } from './StaticTitle'
+import { V2EditorFooter } from './V2EditorFooter'
+
+export function EditorFrame({ children }: { children: React.ReactNode }) {
+ const pathname = usePathname()
+ const { projectRef } = useV2Params()
+
+ const isDataCategory =
+ pathname?.includes('/data/tables') ||
+ pathname?.includes('/data/functions') ||
+ pathname?.includes('/data/types') ||
+ pathname?.includes('/data/roles') ||
+ pathname?.includes('/data/extensions') ||
+ pathname?.includes('/data/indexes') ||
+ pathname?.includes('/data/publications') ||
+ pathname?.includes('/data/users') ||
+ pathname?.includes('/data/providers') ||
+ pathname?.includes('/data/oauth-apps') ||
+ pathname?.includes('/data/buckets') ||
+ pathname?.includes('/data/edge-functions') ||
+ pathname?.includes('/data/channels')
+
+ return (
+
+ {isDataCategory ? (
+
+ ) : (
+
+ )}
+
+ {children}
+
+
+
+ )
+}
diff --git a/apps/studio/components/v2/RightActivityBar.tsx b/apps/studio/components/v2/RightActivityBar.tsx
new file mode 100644
index 00000000000..a6017c3cc9e
--- /dev/null
+++ b/apps/studio/components/v2/RightActivityBar.tsx
@@ -0,0 +1,27 @@
+'use client'
+
+import { AdvisorButton } from 'components/layouts/AppLayout/AdvisorButton'
+import { AssistantButton } from 'components/layouts/AppLayout/AssistantButton'
+import { InlineEditorButton } from 'components/layouts/AppLayout/InlineEditorButton'
+
+import { HelpButton } from 'components/ui/HelpPanel/HelpButton'
+
+import { useV2Params } from '@/app/v2/V2ParamsContext'
+
+export function RightActivityBar() {
+ const { projectRef } = useV2Params()
+
+ return (
+
+ )
+}
diff --git a/apps/studio/components/v2/RightPanel.tsx b/apps/studio/components/v2/RightPanel.tsx
new file mode 100644
index 00000000000..d4464db1c76
--- /dev/null
+++ b/apps/studio/components/v2/RightPanel.tsx
@@ -0,0 +1,43 @@
+'use client'
+
+import { usePathname } from 'next/navigation'
+import { X } from 'lucide-react'
+import { Button, cn } from 'ui'
+
+import { useV2DashboardStore } from '@/stores/v2-dashboard'
+
+import { AssistantPanel } from './AssistantPanel'
+import { SqlPanel } from './SqlPanel'
+import { AdvisorsPanel } from './AdvisorsPanel'
+
+export function RightPanel() {
+ const rightPanel = useV2DashboardStore((s) => s.rightPanel)
+ const closeRightPanel = useV2DashboardStore((s) => s.closeRightPanel)
+
+ if (!rightPanel) return null
+
+ const titles = { chat: 'Assistant', sql: 'SQL', adv: 'Advisors' }
+ const title = titles[rightPanel]
+
+ return (
+
+
+ {title}
+
+
+
+ {rightPanel === 'chat' &&
}
+ {rightPanel === 'sql' &&
}
+ {rightPanel === 'adv' &&
}
+
+
+ )
+}
diff --git a/apps/studio/components/v2/Shell.tsx b/apps/studio/components/v2/Shell.tsx
new file mode 100644
index 00000000000..7a80cd85fcb
--- /dev/null
+++ b/apps/studio/components/v2/Shell.tsx
@@ -0,0 +1,49 @@
+'use client'
+
+import { MobileSheetProvider } from 'components/layouts/Navigation/NavigationBar/MobileSheetContext'
+import { LayoutSidebar } from 'components/layouts/ProjectLayout/LayoutSidebar'
+import { ResizableHandle, ResizablePanel, ResizablePanelGroup, SidebarProvider } from 'ui'
+
+import { LeftActivityBar } from './ActivityBar'
+import { BrowserPanel } from './BrowserPanel'
+import { EditorFrame } from './EditorFrame'
+import { RightActivityBar } from './RightActivityBar'
+import { TopBar } from './TopBar'
+import { V2LayoutSidebarProvider } from './V2LayoutSidebarProvider'
+import { V2DashboardProvider } from '@/stores/v2-dashboard'
+
+export function Shell({ children }: { children: React.ReactNode }) {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {children}
+
+
+
+
+
+
+
+
+
+
+
+ )
+}
diff --git a/apps/studio/components/v2/SqlPanel.tsx b/apps/studio/components/v2/SqlPanel.tsx
new file mode 100644
index 00000000000..021c512963f
--- /dev/null
+++ b/apps/studio/components/v2/SqlPanel.tsx
@@ -0,0 +1,58 @@
+'use client'
+
+import { useState } from 'react'
+import { Button } from 'ui'
+
+import { useV2Params } from '@/app/v2/V2ParamsContext'
+import { useExecuteSqlMutation } from 'data/sql/execute-sql-mutation'
+import { useProjectDetailQuery } from 'data/projects/project-detail-query'
+
+export function SqlPanel() {
+ const { projectRef } = useV2Params()
+ const [sql, setSql] = useState('SELECT 1')
+ const { data: project } = useProjectDetailQuery(
+ { ref: projectRef },
+ { enabled: Boolean(projectRef) }
+ )
+ const { mutate: executeSql, isPending, data, error } = useExecuteSqlMutation()
+
+ const handleRun = () => {
+ if (!projectRef || !project?.connectionString) return
+ executeSql({
+ projectRef,
+ connectionString: project.connectionString,
+ sql,
+ })
+ }
+
+ return (
+
+ )
+}
diff --git a/apps/studio/components/v2/StaticTitle.tsx b/apps/studio/components/v2/StaticTitle.tsx
new file mode 100644
index 00000000000..dcb81adce84
--- /dev/null
+++ b/apps/studio/components/v2/StaticTitle.tsx
@@ -0,0 +1,16 @@
+'use client'
+
+import { usePathname } from 'next/navigation'
+
+export function StaticTitle() {
+ const pathname = usePathname()
+ const segment = pathname?.split('/').filter(Boolean).slice(-2) ?? []
+ const title = segment[segment.length - 1] ?? 'Overview'
+ const formatted = title.replace(/-/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase())
+
+ return (
+
+
{formatted}
+
+ )
+}
diff --git a/apps/studio/components/v2/TabBar.tsx b/apps/studio/components/v2/TabBar.tsx
new file mode 100644
index 00000000000..01827e51f2b
--- /dev/null
+++ b/apps/studio/components/v2/TabBar.tsx
@@ -0,0 +1,92 @@
+'use client'
+
+import Link from 'next/link'
+import { usePathname } from 'next/navigation'
+import { List, Plus, X } from 'lucide-react'
+import { cn } from 'ui'
+
+import { useV2Params } from '@/app/v2/V2ParamsContext'
+import { useV2DashboardStore } from '@/stores/v2-dashboard'
+
+const CATEGORY_LABELS: Record = {
+ tables: 'Tables',
+ functions: 'Functions',
+ types: 'Enumerated types',
+ roles: 'Roles',
+ extensions: 'Extensions',
+ indexes: 'Indexes',
+ publications: 'Publications',
+ users: 'Users',
+ providers: 'Providers',
+ 'oauth-apps': 'OAuth apps',
+ buckets: 'Buckets',
+ 'edge-functions': 'Edge functions',
+ channels: 'Channels',
+}
+
+export function TabBar() {
+ const pathname = usePathname()
+ const { projectRef } = useV2Params()
+ const { detailTabs, removeDetailTab } = useV2DashboardStore()
+
+ const base = projectRef ? `/dashboard/v2/project/${projectRef}` : ''
+ const pathParts = pathname?.replace(/^\/v2\/[^/]+\/[^/]+/, '').split('/').filter(Boolean) ?? []
+ const activity = pathParts[0]
+ const category = pathParts[1]
+ const categoryLabel = category ? CATEGORY_LABELS[category] ?? category : 'Data'
+ const categoryListHref = category ? `${base}/data/${category}` : `${base}/data/tables`
+
+ const activeTabPath = pathname ?? ''
+
+ return (
+
+
+
+
{categoryLabel}
+
+
+ {detailTabs.map((tab) => {
+ const isActive = activeTabPath === tab.path
+ return (
+
+
+ {tab.label}
+
+
+
+ )
+ })}
+
+ )
+}
diff --git a/apps/studio/components/v2/TopBar.tsx b/apps/studio/components/v2/TopBar.tsx
new file mode 100644
index 00000000000..d52a8690739
--- /dev/null
+++ b/apps/studio/components/v2/TopBar.tsx
@@ -0,0 +1,48 @@
+'use client'
+
+import { useOrganizationsQuery } from 'data/organizations/organizations-query'
+import { useProjectDetailQuery } from 'data/projects/project-detail-query'
+import { useV2Params } from '@/app/v2/V2ParamsContext'
+import { Button, cn } from 'ui'
+import { ShimmeringLoader } from 'ui-patterns/ShimmeringLoader'
+
+import { V2BranchSelector } from './V2BranchSelector'
+import { V2OrgProjectSelector } from './V2OrgProjectSelector'
+
+export function TopBar() {
+ const { orgSlug, projectRef } = useV2Params()
+ const { data: org } = useOrganizationsQuery({
+ enabled: true,
+ select: (data) => data.find((o) => o.slug === orgSlug),
+ })
+ const { data: project, isPending: loadingProject } = useProjectDetailQuery(
+ { ref: projectRef },
+ { enabled: Boolean(projectRef) }
+ )
+
+ return (
+
+ )
+}
diff --git a/apps/studio/components/v2/V2BranchSelector.tsx b/apps/studio/components/v2/V2BranchSelector.tsx
new file mode 100644
index 00000000000..c539b2e47fb
--- /dev/null
+++ b/apps/studio/components/v2/V2BranchSelector.tsx
@@ -0,0 +1,119 @@
+'use client'
+
+import { useRouter } from 'next/navigation'
+import { useState } from 'react'
+import {
+ Badge,
+ Button,
+ Command_Shadcn_,
+ CommandGroup_Shadcn_,
+ CommandItem_Shadcn_,
+ CommandList_Shadcn_,
+ Popover_Shadcn_,
+ PopoverContent_Shadcn_,
+ PopoverTrigger_Shadcn_,
+} from 'ui'
+import { GitBranch } from 'lucide-react'
+
+import { useV2Params } from '@/app/v2/V2ParamsContext'
+import { useBranchesQuery } from 'data/branches/branches-query'
+import { useProjectDetailQuery } from 'data/projects/project-detail-query'
+import { IS_PLATFORM } from 'lib/constants'
+
+export function V2BranchSelector() {
+ const router = useRouter()
+ const { projectRef } = useV2Params()
+ const [open, setOpen] = useState(false)
+
+ const { data: project } = useProjectDetailQuery(
+ { ref: projectRef },
+ { enabled: Boolean(projectRef) }
+ )
+ const parentRef = project?.parent_project_ref ?? projectRef
+ const { data: branches } = useBranchesQuery(
+ { projectRef: parentRef },
+ { enabled: open && IS_PLATFORM && Boolean(parentRef) }
+ )
+
+ const selectedBranch = branches?.find((b) => b.project_ref === projectRef)
+ const branchName = selectedBranch?.name ?? 'main'
+ const isDefault = selectedBranch?.is_default ?? true
+
+ const handleSelectBranch = (ref: string) => {
+ if (!orgSlug) return
+ setOpen(false)
+ router.push(`/dashboard/v2/project/${ref}/data/tables`)
+ }
+
+ const handleManageBranches = () => {
+ if (!orgSlug || !projectRef) return
+ setOpen(false)
+ router.push(`/dashboard/v2/project/${projectRef}/settings/branches`)
+ }
+
+ if (!IS_PLATFORM) {
+ return (
+
+
+ main
+
+ )
+ }
+
+ return (
+
+
+
+
+
+
+
+
+ {branches?.map((branch) => (
+ handleSelectBranch(branch.project_ref)}
+ >
+
+ {branch.name}
+ {branch.is_default && (
+
+ prod
+
+ )}
+
+ ))}
+
+
+
+ Manage branches
+
+ {
+ setOpen(false)
+ // Create branch - could open modal; for now navigate to branches
+ handleManageBranches()
+ }}
+ >
+ + Create branch
+
+
+
+
+
+
+ )
+}
diff --git a/apps/studio/components/v2/V2EditorFooter.tsx b/apps/studio/components/v2/V2EditorFooter.tsx
new file mode 100644
index 00000000000..e158c00d3bd
--- /dev/null
+++ b/apps/studio/components/v2/V2EditorFooter.tsx
@@ -0,0 +1,20 @@
+'use client'
+
+import { useV2Params } from '@/app/v2/V2ParamsContext'
+import { useProjectDetailQuery } from 'data/projects/project-detail-query'
+
+export function V2EditorFooter() {
+ const { projectRef } = useV2Params()
+ const { data: project } = useProjectDetailQuery(
+ { ref: projectRef },
+ { enabled: Boolean(projectRef) }
+ )
+
+ return (
+
+ )
+}
diff --git a/apps/studio/components/v2/V2LayoutSidebarProvider.tsx b/apps/studio/components/v2/V2LayoutSidebarProvider.tsx
new file mode 100644
index 00000000000..6c9f70cc33f
--- /dev/null
+++ b/apps/studio/components/v2/V2LayoutSidebarProvider.tsx
@@ -0,0 +1,52 @@
+'use client'
+
+import dynamic from 'next/dynamic'
+import { type PropsWithChildren } from 'react'
+
+import { useV2Params } from '@/app/v2/V2ParamsContext'
+import { SIDEBAR_KEYS } from 'components/layouts/ProjectLayout/LayoutSidebar/LayoutSidebarProvider'
+import { getSupportLinkQueryParams } from 'components/ui/HelpPanel/HelpPanel.utils'
+import { useRegisterSidebar, useSidebarManagerSnapshot } from 'state/sidebar-manager-state'
+
+const HelpPanel = dynamic(() =>
+ import('components/ui/HelpPanel/HelpPanel').then((m) => m.HelpPanel)
+)
+
+const AdvisorPanel = dynamic(() =>
+ import('components/ui/AdvisorPanel/AdvisorPanel').then((m) => m.AdvisorPanel)
+)
+const AIAssistant = dynamic(() =>
+ import('components/ui/AIAssistantPanel/AIAssistant').then((m) => m.AIAssistant)
+)
+const SqlPanel = dynamic(() =>
+ import('components/v2/SqlPanel').then((m) => m.SqlPanel)
+)
+
+export const V2LayoutSidebarProvider = ({ children }: PropsWithChildren) => {
+ const { projectRef } = useV2Params()
+ const hasProject = Boolean(projectRef)
+ const { closeSidebar } = useSidebarManagerSnapshot()
+ const { orgSlug } = useV2Params()
+
+ const supportLinkQueryParams = getSupportLinkQueryParams(undefined, { slug: orgSlug }, projectRef)
+
+ useRegisterSidebar(SIDEBAR_KEYS.AI_ASSISTANT, () => , {}, 'i', hasProject)
+ useRegisterSidebar(SIDEBAR_KEYS.EDITOR_PANEL, () => , {}, 'e', hasProject)
+ useRegisterSidebar(SIDEBAR_KEYS.ADVISOR_PANEL, () => , {}, undefined, true)
+ useRegisterSidebar(
+ SIDEBAR_KEYS.HELP_PANEL,
+ () => (
+ closeSidebar(SIDEBAR_KEYS.HELP_PANEL)}
+ projectRef={projectRef}
+ supportLinkQueryParams={supportLinkQueryParams}
+ />
+ ),
+ {},
+ undefined,
+ true
+ )
+
+ return <>{children}>
+}
+
diff --git a/apps/studio/components/v2/V2OrgProjectSelector.tsx b/apps/studio/components/v2/V2OrgProjectSelector.tsx
new file mode 100644
index 00000000000..f75001bb5e5
--- /dev/null
+++ b/apps/studio/components/v2/V2OrgProjectSelector.tsx
@@ -0,0 +1,101 @@
+'use client'
+
+import { useRouter } from 'next/navigation'
+import { useState } from 'react'
+import {
+ Button,
+ Command_Shadcn_,
+ CommandGroup_Shadcn_,
+ CommandInput_Shadcn_,
+ CommandItem_Shadcn_,
+ CommandList_Shadcn_,
+ Popover_Shadcn_,
+ PopoverContent_Shadcn_,
+ PopoverTrigger_Shadcn_,
+} from 'ui'
+
+import { useV2Params } from '@/app/v2/V2ParamsContext'
+import { useOrganizationsQuery } from 'data/organizations/organizations-query'
+import { useOrgProjectsInfiniteQuery } from 'data/projects/org-projects-infinite-query'
+import { useProjectDetailQuery } from 'data/projects/project-detail-query'
+import { ChevronsUpDown } from 'lucide-react'
+
+export function V2OrgProjectSelector() {
+ const router = useRouter()
+ const { orgSlug, projectRef } = useV2Params()
+ const [open, setOpen] = useState(false)
+
+ const { data: orgs } = useOrganizationsQuery()
+ const selectedOrg = orgs?.find((o) => o.slug === orgSlug)
+ const { data: project } = useProjectDetailQuery(
+ { ref: projectRef },
+ { enabled: Boolean(projectRef) }
+ )
+ const { data: projectsData } = useOrgProjectsInfiniteQuery(
+ { slug: orgSlug, limit: 50 },
+ { enabled: open && Boolean(orgSlug) }
+ )
+ const projects = projectsData?.pages?.flatMap((p) => p.projects) ?? []
+
+ const handleSelectProject = (ref: string) => {
+ if (!orgSlug) return
+ setOpen(false)
+ router.push(`/dashboard/v2/project/${ref}/data/tables`)
+ }
+
+ const handleSelectOrg = (slug: string) => {
+ setOpen(false)
+ router.push(`/dashboard/v2/org/${slug}`)
+ }
+
+ return (
+
+
+
+
+
+
+
+
+
+ {orgs?.map((org) => (
+ handleSelectOrg(org.slug)}
+ >
+ {org.name}
+
+ ))}
+
+ {orgSlug && (
+
+ {projects.map((proj) => (
+ handleSelectProject(proj.ref)}
+ >
+ {proj.name}
+
+ ))}
+
+ )}
+
+
+
+
+ )
+}
diff --git a/apps/studio/components/v2/useV2DataCounts.ts b/apps/studio/components/v2/useV2DataCounts.ts
new file mode 100644
index 00000000000..2a95e388f9b
--- /dev/null
+++ b/apps/studio/components/v2/useV2DataCounts.ts
@@ -0,0 +1,70 @@
+'use client'
+
+import { useUsersCountQuery } from 'data/auth/users-count-query'
+import { useDatabaseExtensionsQuery } from 'data/database-extensions/database-extensions-query'
+import { useDatabaseFunctionsQuery } from 'data/database-functions/database-functions-query'
+import { useDatabasePublicationsQuery } from 'data/database-publications/database-publications-query'
+import { useDatabaseRolesQuery } from 'data/database-roles/database-roles-query'
+import { useEdgeFunctionsQuery } from 'data/edge-functions/edge-functions-query'
+import { useEnumeratedTypesQuery } from 'data/enumerated-types/enumerated-types-query'
+import { useProjectDetailQuery } from 'data/projects/project-detail-query'
+import { useTablesQuery } from 'data/tables/tables-query'
+import { usePaginatedBucketsQuery } from 'data/storage/buckets-query'
+
+export function useV2DataCounts(projectRef: string | undefined) {
+ const { data: project } = useProjectDetailQuery(
+ { ref: projectRef },
+ { enabled: Boolean(projectRef) }
+ )
+ const conn = project?.connectionString
+
+ const { data: tables } = useTablesQuery(
+ { projectRef, connectionString: conn, schema: 'public' },
+ { enabled: Boolean(projectRef) }
+ )
+ const { data: usersCount } = useUsersCountQuery(
+ { projectRef },
+ { enabled: Boolean(projectRef) }
+ )
+ const { data: bucketsPages } = usePaginatedBucketsQuery(
+ { projectRef },
+ { enabled: Boolean(projectRef) }
+ )
+ const bucketsList = bucketsPages?.pages?.flatMap((p) => p) ?? []
+ const { data: edgeFunctions } = useEdgeFunctionsQuery(
+ { projectRef },
+ { enabled: Boolean(projectRef) }
+ )
+ const { data: extensions } = useDatabaseExtensionsQuery(
+ { projectRef, connectionString: conn },
+ { enabled: Boolean(projectRef) }
+ )
+ const { data: roles } = useDatabaseRolesQuery(
+ { projectRef, connectionString: conn },
+ { enabled: Boolean(projectRef) }
+ )
+ const { data: publications } = useDatabasePublicationsQuery(
+ { projectRef, connectionString: conn },
+ { enabled: Boolean(projectRef) }
+ )
+ const { data: types } = useEnumeratedTypesQuery(
+ { projectRef, connectionString: conn },
+ { enabled: Boolean(projectRef) }
+ )
+ const { data: dbFunctions } = useDatabaseFunctionsQuery(
+ { projectRef, connectionString: conn },
+ { enabled: Boolean(projectRef) }
+ )
+
+ return {
+ tables: Array.isArray(tables) ? tables.length : 0,
+ users: usersCount?.count ?? 0,
+ buckets: bucketsList.length,
+ edgeFunctions: edgeFunctions?.length ?? 0,
+ extensions: extensions?.length ?? 0,
+ roles: roles?.length ?? 0,
+ publications: publications?.length ?? 0,
+ types: types?.length ?? 0,
+ functions: dbFunctions?.length ?? 0,
+ }
+}
diff --git a/apps/studio/components/v2/views/V2PlaceholderView.tsx b/apps/studio/components/v2/views/V2PlaceholderView.tsx
new file mode 100644
index 00000000000..cbbd9fa3114
--- /dev/null
+++ b/apps/studio/components/v2/views/V2PlaceholderView.tsx
@@ -0,0 +1,10 @@
+'use client'
+
+export function V2PlaceholderView({ title }: { title: string }) {
+ return (
+
+
{title}
+
This view will show real data from the API. Placeholder for the v2 prototype.
+
+ )
+}
diff --git a/apps/studio/components/v2/views/V2TableDetailView.tsx b/apps/studio/components/v2/views/V2TableDetailView.tsx
new file mode 100644
index 00000000000..0d8bf274808
--- /dev/null
+++ b/apps/studio/components/v2/views/V2TableDetailView.tsx
@@ -0,0 +1,48 @@
+'use client'
+
+import { useParams } from 'next/navigation'
+import { ShimmeringLoader } from 'ui-patterns/ShimmeringLoader'
+
+import { useV2Params } from '@/app/v2/V2ParamsContext'
+import { useProjectDetailQuery } from 'data/projects/project-detail-query'
+import { useTableEditorQuery } from 'data/table-editor/table-editor-query'
+
+export function V2TableDetailView({ subTab }: { subTab: string }) {
+ const params = useParams()
+ const { projectRef } = useV2Params()
+ const tableId = params?.tableId as string
+ const id = tableId ? Number(tableId) : undefined
+
+ const { data: project } = useProjectDetailQuery(
+ { ref: projectRef },
+ { enabled: Boolean(projectRef) }
+ )
+ const { data: table, isPending, isError } = useTableEditorQuery(
+ {
+ projectRef,
+ connectionString: project?.connectionString,
+ id,
+ },
+ { enabled: Boolean(projectRef) && typeof id === 'number' && !Number.isNaN(id) }
+ )
+
+ if (isError) {
+ return (
+ Failed to load table.
+ )
+ }
+ if (isPending || !table) {
+ return
+ }
+
+ return (
+
+
+ {table.schema}.{table.name} — {subTab}
+
+
+ Content for {subTab} will use existing components (e.g. data grid, schema editor, policy editor).
+
+
+ )
+}
diff --git a/apps/studio/components/v2/views/V2TablesList.tsx b/apps/studio/components/v2/views/V2TablesList.tsx
new file mode 100644
index 00000000000..4f19c494706
--- /dev/null
+++ b/apps/studio/components/v2/views/V2TablesList.tsx
@@ -0,0 +1,125 @@
+'use client'
+
+import Link from 'next/link'
+import { useMemo, useState } from 'react'
+import {
+ Select_Shadcn_,
+ SelectContent_Shadcn_,
+ SelectItem_Shadcn_,
+ SelectTrigger_Shadcn_,
+ SelectValue_Shadcn_,
+ Input,
+} from 'ui'
+import { ShimmeringLoader } from 'ui-patterns/ShimmeringLoader'
+
+import { useV2Params } from '@/app/v2/V2ParamsContext'
+import { useV2DashboardStore } from '@/stores/v2-dashboard'
+import { useSchemasQuery } from 'data/database/schemas-query'
+import { useProjectDetailQuery } from 'data/projects/project-detail-query'
+import { useTablesQuery } from 'data/tables/tables-query'
+
+export function V2TablesList() {
+ const { projectRef } = useV2Params()
+ const addDetailTab = useV2DashboardStore((s) => s.addDetailTab)
+ const [schema, setSchema] = useState('public')
+ const [search, setSearch] = useState('')
+
+ const { data: project } = useProjectDetailQuery(
+ { ref: projectRef },
+ { enabled: Boolean(projectRef) }
+ )
+ const { data: schemas } = useSchemasQuery(
+ { projectRef, connectionString: project?.connectionString },
+ { enabled: Boolean(projectRef) }
+ )
+ const { data: tables, isPending, isError } = useTablesQuery(
+ {
+ projectRef,
+ connectionString: project?.connectionString,
+ schema,
+ includeColumns: false,
+ },
+ { enabled: Boolean(projectRef) }
+ )
+
+ const filtered = useMemo(() => {
+ if (!Array.isArray(tables)) return []
+ if (!search.trim()) return tables
+ const q = search.toLowerCase()
+ return tables.filter((t) => t.name.toLowerCase().includes(q))
+ }, [tables, search])
+
+ const base = projectRef ? `/dashboard/v2/project/${projectRef}` : ''
+
+ const handleRowClick = (tableId: string, tableName: string) => {
+ const path = `${base}/data/tables/${tableId}`
+ addDetailTab({ id: tableId, label: tableName, path })
+ }
+
+ if (isError) {
+ return (
+ Failed to load tables.
+ )
+ }
+
+ return (
+
+
+
+
+
+
+
+ {schemas?.map((s) => (
+
+ {s.name}
+
+ ))}
+
+
+ setSearch(e.target.value)}
+ className="max-w-[200px] h-8 text-xs"
+ />
+
+ {Array.isArray(tables) ? tables.length : 0} tables
+
+
+
+ {isPending ? (
+
+ ) : (
+
+
+
+ | Name |
+ Schema |
+
+
+
+ {filtered.map((table) => (
+
+ |
+ handleRowClick(String(table.id), table.name)}
+ className="text-foreground hover:underline"
+ >
+ {table.name}
+
+ |
+ {table.schema ?? schema} |
+
+ ))}
+
+
+ )}
+
+
+ )
+}
diff --git a/apps/studio/data/telemetry/send-event-mutation.ts b/apps/studio/data/telemetry/send-event-mutation.ts
index 68ad02967b4..cec12f07677 100644
--- a/apps/studio/data/telemetry/send-event-mutation.ts
+++ b/apps/studio/data/telemetry/send-event-mutation.ts
@@ -4,7 +4,6 @@ import { sendTelemetryEvent } from 'common'
import { TelemetryEvent } from 'common/telemetry-constants'
import { handleError } from 'data/fetchers'
import { API_URL } from 'lib/constants'
-import { useRouter } from 'next/router'
import type { ResponseError, UseCustomMutationOptions } from 'types'
interface SendEventVariables {
@@ -30,11 +29,10 @@ export const useSendEventMutation = ({
UseCustomMutationOptions,
'mutationFn'
> = {}) => {
- const router = useRouter()
-
return useMutation({
mutationFn: (event) => {
- return sendEvent({ event, pathname: router.pathname })
+ const pathname = typeof window !== 'undefined' ? window.location.pathname : undefined
+ return sendEvent({ event, pathname })
},
async onSuccess(data, variables, context) {
await onSuccess?.(data, variables, context)
diff --git a/apps/studio/lib/telemetry/track.ts b/apps/studio/lib/telemetry/track.ts
index ec1755bc9a3..152e7ac3a3c 100644
--- a/apps/studio/lib/telemetry/track.ts
+++ b/apps/studio/lib/telemetry/track.ts
@@ -3,7 +3,6 @@ import { TelemetryEvent, TelemetryGroups } from 'common/telemetry-constants'
import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization'
import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject'
import { API_URL } from 'lib/constants'
-import { useRouter } from 'next/router'
import { useCallback } from 'react'
type EventMap = {
@@ -29,7 +28,8 @@ type HasProperties = EventMap[A] extends { properties:
export const useTrack = () => {
const { data: project } = useSelectedProjectQuery()
const { data: org } = useSelectedOrganizationQuery()
- const router = useRouter()
+ // Avoid Pages Router dependency in App Router.
+ const pathname = typeof window !== 'undefined' ? window.location.pathname : undefined
const track = useCallback(
(
@@ -52,9 +52,9 @@ export const useTrack = () => {
...(groups && { groups }),
} as EventMap[A]
- sendTelemetryEvent(API_URL, event, router.pathname)
+ sendTelemetryEvent(API_URL, event, pathname)
},
- [project?.ref, org?.slug, router.pathname]
+ [project?.ref, org?.slug, pathname]
)
return track
diff --git a/apps/studio/stores/v2-dashboard.tsx b/apps/studio/stores/v2-dashboard.tsx
new file mode 100644
index 00000000000..d02b4f4b09a
--- /dev/null
+++ b/apps/studio/stores/v2-dashboard.tsx
@@ -0,0 +1,128 @@
+'use client'
+
+import {
+ createContext,
+ useCallback,
+ useContext,
+ useMemo,
+ useState,
+ type ReactNode,
+} from 'react'
+
+export type RightPanelType = 'chat' | 'sql' | 'adv' | null
+
+export interface DetailTab {
+ id: string
+ label: string
+ path: string
+}
+
+const defaultExpandedGroups: Record = {
+ 'data-database': true,
+ 'data-auth': true,
+ 'data-storage': true,
+ 'data-edge-functions': true,
+ 'data-realtime': true,
+ 'obs-logs': true,
+ 'obs-metrics': true,
+ 'obs-alerts': true,
+ 'settings-project': true,
+ 'settings-branches': true,
+ 'settings-modules': true,
+ 'settings-org': true,
+}
+
+interface V2DashboardState {
+ detailTabs: DetailTab[]
+ expandedGroups: Record
+ rightPanel: RightPanelType
+
+ addDetailTab: (tab: DetailTab) => void
+ removeDetailTab: (id: string) => void
+ setDetailTabs: (tabs: DetailTab[]) => void
+ toggleGroup: (groupId: string) => void
+ setExpandedGroup: (groupId: string, expanded: boolean) => void
+ toggleRightPanel: (panel: 'chat' | 'sql' | 'adv') => void
+ closeRightPanel: () => void
+}
+
+const V2DashboardContext = createContext(null)
+
+export function V2DashboardProvider({ children }: { children: ReactNode }) {
+ const [detailTabs, setDetailTabsState] = useState([])
+ const [expandedGroups, setExpandedGroups] = useState>(defaultExpandedGroups)
+ const [rightPanel, setRightPanel] = useState(null)
+
+ const addDetailTab = useCallback((tab: DetailTab) => {
+ setDetailTabsState((state) =>
+ state.some((t) => t.id === tab.id) ? state : [...state, tab]
+ )
+ }, [])
+
+ const removeDetailTab = useCallback((id: string) => {
+ setDetailTabsState((state) => state.filter((t) => t.id !== id))
+ }, [])
+
+ const setDetailTabs = useCallback((tabs: DetailTab[]) => {
+ setDetailTabsState(tabs)
+ }, [])
+
+ const toggleGroup = useCallback((groupId: string) => {
+ setExpandedGroups((state) => ({
+ ...state,
+ [groupId]: !state[groupId],
+ }))
+ }, [])
+
+ const setExpandedGroup = useCallback((groupId: string, expanded: boolean) => {
+ setExpandedGroups((state) => ({ ...state, [groupId]: expanded }))
+ }, [])
+
+ const toggleRightPanel = useCallback((panel: 'chat' | 'sql' | 'adv') => {
+ setRightPanel((current) => (current === panel ? null : panel))
+ }, [])
+
+ const closeRightPanel = useCallback(() => setRightPanel(null), [])
+
+ const value = useMemo(
+ () => ({
+ detailTabs,
+ expandedGroups,
+ rightPanel,
+ addDetailTab,
+ removeDetailTab,
+ setDetailTabs,
+ toggleGroup,
+ setExpandedGroup,
+ toggleRightPanel,
+ closeRightPanel,
+ }),
+ [
+ detailTabs,
+ expandedGroups,
+ rightPanel,
+ addDetailTab,
+ removeDetailTab,
+ setDetailTabs,
+ toggleGroup,
+ setExpandedGroup,
+ toggleRightPanel,
+ closeRightPanel,
+ ]
+ )
+
+ return (
+ {children}
+ )
+}
+
+export function useV2DashboardStore(): V2DashboardState
+export function useV2DashboardStore(selector: (s: V2DashboardState) => T): T
+export function useV2DashboardStore(selector?: (s: V2DashboardState) => T) {
+ const ctx = useContext(V2DashboardContext)
+ if (!ctx) {
+ throw new Error('useV2DashboardStore must be used within V2DashboardProvider')
+ }
+ if (selector) return selector(ctx)
+ return ctx
+}
diff --git a/packages/ui/src/components/shadcn/ui/resizable.tsx b/packages/ui/src/components/shadcn/ui/resizable.tsx
index d8ed45b9ac1..d52186852c0 100644
--- a/packages/ui/src/components/shadcn/ui/resizable.tsx
+++ b/packages/ui/src/components/shadcn/ui/resizable.tsx
@@ -1,5 +1,6 @@
'use client'
+import { useEffect, useState } from 'react'
import { GripVertical } from 'lucide-react'
import * as ResizablePrimitive from 'react-resizable-panels'
@@ -22,7 +23,8 @@ const serverCompatibleLocalStorage = {
},
}
-const ResizablePanelGroupWithPersistence = ({
+/** Only mounts after hydration so `useDefaultLayout` never reads localStorage during SSR / first paint. */
+const ResizablePanelGroupPersisted = ({
className,
autoSaveId,
defaultLayout: defaultLayoutProp,
@@ -45,6 +47,42 @@ const ResizablePanelGroupWithPersistence = ({
)
}
+const ResizablePanelGroupWithPersistence = ({
+ className,
+ autoSaveId,
+ defaultLayout: defaultLayoutProp,
+ onLayoutChanged: onLayoutChangedProp,
+ ...props
+}: ResizablePrimitive.GroupProps & { autoSaveId: string }) => {
+ const [hasMounted, setHasMounted] = useState(false)
+
+ useEffect(() => {
+ setHasMounted(true)
+ }, [])
+
+ if (!hasMounted) {
+ return (
+
+ )
+ }
+
+ return (
+
+ )
+}
+
const ResizablePanelGroup = ({
className,
autoSaveId,