diff --git a/apps/studio/app/dashboard/v2/[projectRef]/page.tsx b/apps/studio/app/dashboard/v2/[projectRef]/page.tsx
deleted file mode 100644
index 17d7d1fad2a..00000000000
--- a/apps/studio/app/dashboard/v2/[projectRef]/page.tsx
+++ /dev/null
@@ -1,10 +0,0 @@
-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/project/[projectRef]/ProjectShell.tsx b/apps/studio/app/dashboard/v2/project/[projectRef]/ProjectShell.tsx
index 174d4db8f5e..132e9029fa4 100644
--- a/apps/studio/app/dashboard/v2/project/[projectRef]/ProjectShell.tsx
+++ b/apps/studio/app/dashboard/v2/project/[projectRef]/ProjectShell.tsx
@@ -5,6 +5,7 @@ import { useMemo } from 'react'
import { V2ParamsProvider } from '@/app/v2/V2ParamsContext'
import { Shell } from '@/components/v2/Shell'
+import { ProjectContextProvider } from '@/components/layouts/ProjectLayout/ProjectContext'
import { useOrganizationsQuery } from 'data/organizations/organizations-query'
import { useProjectDetailQuery } from 'data/projects/project-detail-query'
@@ -26,7 +27,9 @@ export function ProjectShell({ children }: { children: React.ReactNode }) {
return (
- {children}
+
+ {children}
+
)
}
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
index fbaafaea94a..da053727304 100644
--- 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
@@ -5,6 +5,7 @@ import { useParams, usePathname } from 'next/navigation'
import { cn } from 'ui'
import { useV2Params } from '@/app/v2/V2ParamsContext'
+import { StudioDataWorkspace } from '@/components/v2/data/StudioDataWorkspace'
const SUB_TABS = [
{ slug: 'data', label: 'Data' },
@@ -27,28 +28,30 @@ export default function TableDetailLayout({
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}
-
- )
- })}
+
+
+
+ {SUB_TABS.map((tab) => {
+ const href = `${base}/${tab.slug}`
+ const isActive = pathname === href || pathname?.startsWith(href + '/')
+ return (
+
+ {tab.label}
+
+ )
+ })}
+
+
{children}
- {children}
-
+
)
}
diff --git a/apps/studio/components/interfaces/TableGridEditor/TableGridEditor.tsx b/apps/studio/components/interfaces/TableGridEditor/TableGridEditor.tsx
index 8e00a44a60d..a3a816c10b6 100644
--- a/apps/studio/components/interfaces/TableGridEditor/TableGridEditor.tsx
+++ b/apps/studio/components/interfaces/TableGridEditor/TableGridEditor.tsx
@@ -1,7 +1,7 @@
import { PermissionAction } from '@supabase/shared-types/out/constants'
import Link from 'next/link'
-import { useRouter } from 'next/router'
import { useCallback } from 'react'
+import { useRouter } from 'next/compat/router'
import { useParams } from 'common'
import { SupabaseGrid } from 'components/grid/SupabaseGrid'
@@ -30,11 +30,13 @@ import { TableDefinition } from './TableDefinition'
export interface TableGridEditorProps {
isLoadingSelectedTable?: boolean
selectedTable?: Entity
+ variant?: 'pages' | 'v2'
}
export const TableGridEditor = ({
isLoadingSelectedTable = false,
selectedTable,
+ variant = 'pages',
}: TableGridEditorProps) => {
const router = useRouter()
const { ref: projectRef, id } = useParams()
@@ -63,26 +65,48 @@ export const TableGridEditor = ({
const onTableCreated = useCallback(
(table: { id: number }) => {
+ if (variant === 'v2') {
+ const base = projectRef ? `/dashboard/v2/project/${projectRef}` : ''
+ router.push(
+ `${base}/data/tables/${table.id}/data${
+ !!selectedSchema ? `?schema=${selectedSchema}` : ''
+ }`
+ )
+ return
+ }
+
router.push(
- `/project/${projectRef}/editor/${table.id}${!!selectedSchema ? `?schema=${selectedSchema}` : ''}`
+ `/project/${projectRef}/editor/${table.id}${
+ !!selectedSchema ? `?schema=${selectedSchema}` : ''
+ }`
)
},
- [projectRef, router]
+ [projectRef, router, selectedSchema, variant]
)
const onTableDeleted = useCallback(async () => {
- // For simplicity for now, we just open the first table within the same schema
- if (selectedTable) {
- // Close tab
- const tabId = createTabId(selectedTable.entity_type, { id: selectedTable.id })
- tabs.handleTabClose({
- id: tabId,
- router,
- editor: 'table',
- onClearDashboardHistory: () => setLastVisitedTable(undefined),
- })
+ if (!selectedTable) return
+
+ // For now, just close the deleted table tab.
+ // v2 uses v2 URLs, pages uses the legacy tab-close router logic.
+ const tabId = createTabId(selectedTable.entity_type, { id: selectedTable.id })
+
+ if (variant === 'v2') {
+ tabs.removeTab(tabId)
+ setLastVisitedTable(undefined)
+
+ const base = projectRef ? `/dashboard/v2/project/${projectRef}` : ''
+ router.push(`${base}/data/tables`)
+ return
}
- }, [router, selectedTable, tabs])
+
+ tabs.handleTabClose({
+ id: tabId,
+ router,
+ editor: 'table',
+ onClearDashboardHistory: () => setLastVisitedTable(undefined),
+ })
+ }, [projectRef, router, selectedTable, tabs, setLastVisitedTable, variant])
const { isSchemaLocked } = useIsProtectedSchema({ schema: selectedTable?.schema ?? '' })
@@ -126,7 +150,44 @@ export const TableGridEditor = ({
title={`Unable to find your table with ID ${id}`}
description="This table doesn't exist in your database"
>
- {!!tabId ? (
+ {variant === 'v2' ? (
+ !!tabId ? (
+
+ ) : openTabs.length > 0 ? (
+
+ ) : (
+
+ )
+ ) : !!tabId ? (
@@ -145,13 +145,13 @@ export function LeftActivityBar() {
},
{
id: 'data',
- icon:
,
+ icon:
,
label: 'Data',
href: `${base}/data/tables`,
},
{
id: 'obs',
- icon:
,
+ icon:
,
label: 'Observability',
badge: hasAdvisorWarnings,
href: `${base}/obs/logs`,
diff --git a/apps/studio/components/v2/BrowserPanel.tsx b/apps/studio/components/v2/BrowserPanel.tsx
index 522f0ea6d83..3671aae8ba7 100644
--- a/apps/studio/components/v2/BrowserPanel.tsx
+++ b/apps/studio/components/v2/BrowserPanel.tsx
@@ -184,7 +184,7 @@ export function BrowserPanel() {
const groups = isData ? DATA_GROUPS : isObs ? OBS_GROUPS : isSettings ? SETTINGS_GROUPS : []
return (
-
+
{title}
@@ -234,7 +234,7 @@ export function BrowserPanel() {
>
{item.label}
{'countKey' in item && (
-
+
{getCount(counts, String(item.countKey))}
)}
@@ -246,7 +246,7 @@ export function BrowserPanel() {
})}
{isData && (
-
+
+ Add module
diff --git a/apps/studio/components/v2/DataListGrid.tsx b/apps/studio/components/v2/DataListGrid.tsx
new file mode 100644
index 00000000000..cdd69f37106
--- /dev/null
+++ b/apps/studio/components/v2/DataListGrid.tsx
@@ -0,0 +1,192 @@
+'use client'
+
+import { ArrowDown, ArrowUp } from 'lucide-react'
+import { useCallback, useMemo, useState, type ReactNode } from 'react'
+import DataGrid, { type Column, Row } from 'react-data-grid'
+import { cn } from 'ui'
+import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader'
+
+export type DataListColumnDef = {
+ key: string
+ name: string
+ minWidth?: number
+ /** When set, clicking the header sorts by this accessor */
+ sortValue?: (row: TRow) => string | number | boolean | null | undefined
+ renderCell: (row: TRow) => ReactNode
+}
+
+export type DataListGridProps = {
+ /** Top chrome (schema picker, search, actions) — same row height as Table Editor header */
+ toolbar?: ReactNode
+ /** Optional tab strip under the toolbar (e.g. Data / Definition later) */
+ tabs?: ReactNode
+ columns: DataListColumnDef[]
+ rows: TRow[]
+ rowKeyGetter: (row: TRow) => string | number
+ isLoading?: boolean
+ emptyMessage?: ReactNode
+ onRowClick?: (row: TRow) => void
+ onRowDoubleClick?: (row: TRow) => void
+ /** Status text on the right of the toolbar (e.g. row count) */
+ toolbarTrailing?: ReactNode
+ className?: string
+}
+
+function compareSortValues(
+ a: string | number | boolean | null | undefined,
+ b: string | number | boolean | null | undefined
+): number {
+ if (a === b) return 0
+ if (a == null) return 1
+ if (b == null) return -1
+ if (typeof a === 'number' && typeof b === 'number') return a - b
+ return String(a).localeCompare(String(b), undefined, { sensitivity: 'base' })
+}
+
+/**
+ * Read-only list grid with the same shell classes as Table Editor / SupabaseGrid
+ * (`sb-grid`, header chrome). Intended for v2 Data layer list routes before a
+ * fuller generic DataView exists.
+ */
+export function DataListGrid({
+ toolbar,
+ tabs,
+ columns: columnDefs,
+ rows,
+ rowKeyGetter,
+ isLoading = false,
+ emptyMessage,
+ onRowClick,
+ onRowDoubleClick,
+ toolbarTrailing,
+ className,
+}: DataListGridProps) {
+ const [sortKey, setSortKey] = useState(null)
+ const [sortDir, setSortDir] = useState<'asc' | 'desc'>('asc')
+
+ const sortableColumn = useMemo(
+ () => columnDefs.find((c) => c.key === sortKey),
+ [columnDefs, sortKey]
+ )
+
+ const sortedRows = useMemo(() => {
+ if (!sortKey || !sortableColumn?.sortValue) return rows
+ const accessor = sortableColumn.sortValue
+ const next = [...rows].sort((ra, rb) => {
+ const cmp = compareSortValues(accessor(ra), accessor(rb))
+ return sortDir === 'asc' ? cmp : -cmp
+ })
+ return next
+ }, [rows, sortKey, sortableColumn, sortDir])
+
+ const onHeaderClick = useCallback(
+ (key: string) => {
+ const def = columnDefs.find((c) => c.key === key)
+ if (!def?.sortValue) return
+ if (sortKey !== key) {
+ setSortKey(key)
+ setSortDir('asc')
+ } else {
+ setSortDir((d) => (d === 'asc' ? 'desc' : 'asc'))
+ }
+ },
+ [columnDefs, sortKey]
+ )
+
+ const columns: Column[] = useMemo(
+ () =>
+ columnDefs.map((col) => ({
+ key: col.key,
+ name: col.name,
+ resizable: true,
+ minWidth: col.minWidth ?? 120,
+ sortable: false,
+ headerCellClass: 'sb-grid-column-header sb-grid-column-header--cursor',
+ renderHeaderCell: () => {
+ const active = sortKey === col.key
+ const canSort = Boolean(col.sortValue)
+ return (
+
+ )
+ },
+ renderCell: (props) => (
+ {col.renderCell(props.row)}
+ ),
+ })),
+ [columnDefs, sortKey, sortDir, onHeaderClick]
+ )
+
+ return (
+
+ {(toolbar != null || toolbarTrailing != null) && (
+
+
{toolbar}
+ {toolbarTrailing != null ? (
+
{toolbarTrailing}
+ ) : null}
+
+ )}
+ {tabs != null ? (
+
{tabs}
+ ) : null}
+
+
rowKeyGetter(row)}
+ rowClass={() =>
+ cn(
+ '[&>.rdg-cell]:flex [&>.rdg-cell]:items-center',
+ '[&>.rdg-cell]:border-box [&>.rdg-cell]:outline-none [&>.rdg-cell]:shadow-none',
+ (onRowClick || onRowDoubleClick) && 'cursor-pointer'
+ )
+ }
+ renderers={{
+ renderRow(_, props) {
+ return (
+ onRowClick?.(props.row)}
+ onDoubleClick={() => onRowDoubleClick?.(props.row)}
+ />
+ )
+ },
+ noRowsFallback: isLoading ? (
+
+
+
+ ) : (
+
+ {emptyMessage ?? 'No rows'}
+
+ ),
+ }}
+ />
+
+
+ )
+}
diff --git a/apps/studio/components/v2/EditorFrame.tsx b/apps/studio/components/v2/EditorFrame.tsx
index 7fb410b5dee..90d1a28fbd3 100644
--- a/apps/studio/components/v2/EditorFrame.tsx
+++ b/apps/studio/components/v2/EditorFrame.tsx
@@ -39,7 +39,12 @@ export function EditorFrame({ children }: { children: React.ReactNode }) {
{isDataCategory ? null : isHome ? null :
}
{isDataCategory &&
}
-
+
{children}
diff --git a/apps/studio/components/v2/Navigation/ProjectBranchSelector.tsx b/apps/studio/components/v2/Navigation/ProjectBranchSelector.tsx
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/apps/studio/components/v2/RightActivityBar.tsx b/apps/studio/components/v2/RightActivityBar.tsx
index a6017c3cc9e..31650c041de 100644
--- a/apps/studio/components/v2/RightActivityBar.tsx
+++ b/apps/studio/components/v2/RightActivityBar.tsx
@@ -3,7 +3,6 @@
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'
@@ -12,7 +11,7 @@ export function RightActivityBar() {
const { projectRef } = useV2Params()
return (
-