improve data fetching and tabs persistence

This commit is contained in:
Francesco Sansalvadore committed 2026-03-24 13:05:59 +01:00
1 parent 8319e13984
commit 22eaef3c16
9 files changed
+97 -35

No files matched your search

@@ -26,7 +26,7 @@ export default function TableDetailLayout({ children }: { children: React.ReactN
return (
<StudioDataWorkspace projectRef={projectRef} id={tableId}>
<div className="flex flex-col h-full">
<div className="flex items-center border-b border-border shrink-0 min-h-[36px] h-[var(--header-height)]">
<div className="flex items-center border-b border-border shrink-0 min-h-[36px]">
{SUB_TABS.map((tab) => {
const href = `${base}/${tab.slug}`
const isActive = pathname === href || pathname?.startsWith(href + '/')
@@ -8,7 +8,7 @@ import { isForeignTable, isTable } from 'data/table-editor/table-editor-types'
import { useTableRowsCountQuery } from 'data/table-rows/table-rows-count-query'
import { useTableRowsQuery } from 'data/table-rows/table-rows-query'
import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject'
import { RoleImpersonationState } from 'lib/role-impersonation'
import type { RoleImpersonationState } from 'lib/role-impersonation'
import { AlertCircle, ArrowLeft, ArrowRight, HelpCircle, Loader2 } from 'lucide-react'
import { useEffect, useState } from 'react'
import { useRoleImpersonationStateSnapshot } from 'state/role-impersonation-state'
@@ -54,7 +54,7 @@ type PaginationProps = {
export const Pagination = ({ enableForeignRowsQuery = true }: PaginationProps) => {
const { id: _id } = useParams()
const id = _id ? Number(_id) : undefined
const routeTableId = _id ? Number(_id) : undefined
const { sorts } = useTableSort()
const { filters } = useTableFilter()
@@ -63,11 +63,12 @@ export const Pagination = ({ enableForeignRowsQuery = true }: PaginationProps) =
const tableEditorSnap = useTableEditorStateSnapshot()
const snap = useTableEditorTableStateSnapshot()
const roleImpersonationState = useRoleImpersonationStateSnapshot()
const tableId = routeTableId ?? snap.table.id
const { data: selectedTable } = useTableEditorQuery({
projectRef: project?.ref,
connectionString: project?.connectionString,
id,
id: tableId,
})
const isForeignTableSelected = isForeignTable(selectedTable)
@@ -90,7 +91,7 @@ export const Pagination = ({ enableForeignRowsQuery = true }: PaginationProps) =
} = useTableRowsCountQuery(
{
projectRef: project?.ref,
tableId: snap.table.id,
tableId,
filters,
enforceExactCount: snap.enforceExactCount,
roleImpersonationState: roleImpersonationState as RoleImpersonationState,
@@ -102,19 +103,18 @@ export const Pagination = ({ enableForeignRowsQuery = true }: PaginationProps) =
)
const count = data?.count ?? 0
const hasCountData = count >= 0
const isEstimateCount = data?.is_estimate ?? false
const countString = data?.is_estimate ? formatEstimatedCount(count) : count.toLocaleString()
const maxPages = Math.ceil(count / tableEditorSnap.rowsPerPage)
const totalPages = count > 0 ? maxPages : 1
const preflightCheck = !tableEditorSnap.tablesToIgnorePreflightCheck.includes(id ?? -1)
const preflightCheck = !tableEditorSnap.tablesToIgnorePreflightCheck.includes(tableId ?? -1)
// [Joshen] This is only applicable for foreign tables, as we use the number of rows on the page to determine
// if we've reached the last page (and hence disable the next button)
const { data: rowsData, isPending: isLoadingRows } = useTableRowsQuery(
{
projectRef: project?.ref,
tableId: id,
tableId,
sorts,
filters,
page: snap.page,
@@ -126,6 +126,11 @@ export const Pagination = ({ enableForeignRowsQuery = true }: PaginationProps) =
enabled: isForeignTableSelected && enableForeignRowsQuery,
}
)
const isCountQueryEnabled =
!isForeignTableSelected &&
typeof project?.ref !== 'undefined' &&
typeof tableId !== 'undefined'
const isCountLoading = isCountQueryEnabled && isLoading
const isLastPage = (rowsData?.rows ?? []).length < tableEditorSnap.rowsPerPage
const onPreviousPage = () => {
@@ -173,7 +178,7 @@ export const Pagination = ({ enableForeignRowsQuery = true }: PaginationProps) =
const onRowsPerPageChange = (value: string | number) => {
const rowsPerPage = Number(value)
tableEditorSnap.setRowsPerPage(isNaN(rowsPerPage) ? 100 : rowsPerPage)
tableEditorSnap.setRowsPerPage(Number.isNaN(rowsPerPage) ? 100 : rowsPerPage)
snap.setPage(1)
}
@@ -188,8 +193,7 @@ export const Pagination = ({ enableForeignRowsQuery = true }: PaginationProps) =
if (isError && snap.enforceExactCount && error?.code === 408) {
snap.setEnforceExactCount(false)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isError, snap.enforceExactCount, error?.code])
}, [isError, snap.enforceExactCount, snap.setEnforceExactCount, error?.code])
// [Joshen] One to revisit if we can consolidate this and the main return statement
if (isForeignTableSelected) {
@@ -243,7 +247,7 @@ export const Pagination = ({ enableForeignRowsQuery = true }: PaginationProps) =
icon={<ArrowLeft />}
type="outline"
className="px-1.5"
disabled={page <= 1 || isLoading}
disabled={page <= 1 || isCountLoading}
onClick={onPreviousPage}
/>
@@ -284,7 +288,7 @@ export const Pagination = ({ enableForeignRowsQuery = true }: PaginationProps) =
<RowCountSelector onRowsPerPageChange={onRowsPerPageChange} />
</div>
{isLoading ? (
{isCountLoading ? (
<Button type="text" className="w-7" icon={<Loader2 size={12} className="animate-spin" />} />
) : isError ? (
<Tooltip>
+1 -1
View File
@@ -163,7 +163,7 @@ export function BrowserPanel({ onCollapse }: { onCollapse?: () => void }) {
return (
<div className="w-full h-full flex flex-col border-r border-border bg-dash-sidebar">
<div className="flex items-center justify-between h-[var(--header-height)] pl-3 pr-2 py-1.5 border-b border-border shrink-0">
<div className="flex items-center justify-between pl-3 pr-2 py-1.5 border-b border-border shrink-0">
<span className="text-sm font-medium text-foreground">{title}</span>
{onCollapse && (
<Tooltip delayDuration={0}>
+2 -2
View File
@@ -80,7 +80,7 @@ export function DataTabBar() {
}
return (
<div className="flex items-center border-b border-border bg-background shrink-0 min-h-[36px] h-[var(--header-height)]">
<div className="flex items-center border-b border-border bg-background shrink-0 min-h-[36px]">
<div className="min-w-0 flex-1 overflow-x-auto h-full">
<div className="flex items-center w-max min-w-full h-full">
{visibleTabs.map((tab) => {
@@ -123,7 +123,7 @@ export function DataTabBar() {
<button
type="button"
onClick={() => router.push(chooserPath)}
className="shrink-0 h-full px-3 border-l border-border text-foreground-lighter hover:text-foreground hover:bg-sidebar-accent/50 transition-colors"
className="shrink-0 h-full aspect-square flex items-center justify-center border-l border-border text-foreground-lighter hover:text-foreground hover:bg-sidebar-accent/50 transition-colors"
aria-label="Open data chooser"
>
<Plus className="h-4 w-4" />
+1 -7
View File
@@ -15,32 +15,26 @@ const BADGE_CONFIG: Record<
list: {
label: 'List',
icon: <List className="h-3 w-3" strokeWidth={1.5} />,
className: 'bg-muted text-foreground-lighter',
},
db: {
label: 'Database',
icon: <Database className="h-3 w-3" strokeWidth={1.5} />,
className: 'text-foreground-lighter',
},
auth: {
label: 'Auth',
icon: <Auth className="h-3 w-3" strokeWidth={1.5} />,
className: 'text-foreground-lighter',
},
st: {
label: 'Storage',
icon: <Storage className="h-3 w-3" strokeWidth={1.5} />,
className: 'text-foreground-lighter',
},
fn: {
label: 'Edge Functions',
icon: <EdgeFunctions className="h-3 w-3" strokeWidth={1.5} />,
className: 'text-foreground-lighter',
},
rt: {
label: 'Realtime',
icon: <Realtime className="h-3 w-3" strokeWidth={1.5} />,
className: 'text-foreground-lighter',
},
}
@@ -59,7 +53,7 @@ export function TypeBadge({ domain, type, className }: TypeBadgeProps) {
<TooltipTrigger asChild>
<span
className={cn(
'inline-flex items-center justify-center rounded p-[3px] shrink-0',
'inline-flex items-center justify-center rounded shrink-0',
badgeClass,
className
)}
@@ -30,7 +30,10 @@ export function V2TableDataGrid({
}) {
const isQueueOperationsEnabled = useIsQueueOperationsEnabled()
const queryClient = useQueryClient()
useProjectDetailQuery({ ref: projectRef }, { enabled: Boolean(projectRef) })
const { data: project } = useProjectDetailQuery(
{ ref: projectRef },
{ enabled: Boolean(projectRef) }
)
const tableEditorSnap = useTableEditorStateSnapshot()
const snap = useTableEditorTableStateSnapshot()
const preflightCheck = !tableEditorSnap.tablesToIgnorePreflightCheck.includes(tableId ?? -1)
@@ -46,7 +49,12 @@ export function V2TableDataGrid({
const tableQueriesEnabled = msSqlWarning.warning === null
const rowsQueryEnabled =
Boolean(projectRef && tableId !== undefined && !Number.isNaN(tableId)) && tableQueriesEnabled
Boolean(
projectRef &&
project?.connectionString &&
tableId !== undefined &&
!Number.isNaN(tableId)
) && tableQueriesEnabled
const {
data,
@@ -58,6 +66,7 @@ export function V2TableDataGrid({
} = useTableRowsQuery(
{
projectRef,
connectionString: project?.connectionString,
tableId,
sorts,
filters,
@@ -1,6 +1,5 @@
'use client'
import { TableGridEditor } from 'components/interfaces/TableGridEditor/TableGridEditor'
import { useProjectDetailQuery } from 'data/projects/project-detail-query'
import { useTableEditorQuery } from 'data/table-editor/table-editor-query'
import { useParams } from 'next/navigation'
@@ -9,6 +8,7 @@ import { ShimmeringLoader } from 'ui-patterns/ShimmeringLoader'
import { useV2Params } from '@/app/v2/V2ParamsContext'
import { useV2DashboardStore } from '@/stores/v2-dashboard'
import { TableGridEditor } from 'components/interfaces/TableGridEditor/TableGridEditor'
export function V2TableDetailView({ subTab }: { subTab: string }) {
const params = useParams()
@@ -4,17 +4,17 @@ import { getTableRowsSql } from '@supabase/pg-meta/src/query/table-row-query'
import { useQuery, useQueryClient, type QueryClient } from '@tanstack/react-query'
import { IS_PLATFORM } from 'common'
import { parseSupaTable } from 'components/grid/SupabaseGrid.utils'
import { Filter, Sort, SupaRow, SupaTable } from 'components/grid/types'
import type { Filter, Sort, SupaRow, SupaTable } from 'components/grid/types'
import { ENTITY_TYPE } from 'data/entity-types/entity-type-constants'
import { prefetchTableEditor } from 'data/table-editor/table-editor-query'
import { isMsSqlForeignTable } from 'data/table-editor/table-editor-types'
import { RoleImpersonationState, wrapWithRoleImpersonation } from 'lib/role-impersonation'
import { wrapWithRoleImpersonation, type RoleImpersonationState } from 'lib/role-impersonation'
import { isRoleImpersonationEnabled } from 'state/role-impersonation-state'
import { ResponseError, UseCustomQueryOptions } from 'types'
import { ResponseError, type UseCustomQueryOptions } from 'types'
import { handleError } from '../fetchers'
import { useConnectionStringForReadOps } from '../read-replicas/replicas-query'
import { executeSql, ExecuteSqlError } from '../sql/execute-sql-query'
import { executeSql, type ExecuteSqlError } from '../sql/execute-sql-query'
import { tableRowKeys } from './keys'
import { formatFilterValue } from './utils'
import { timeout } from '@/lib/helpers'
@@ -389,28 +389,37 @@ async function getTableRows(
}
export const useTableRowsQuery = <TData = TableRowsData>(
{ projectRef, tableId, ...args }: Omit<TableRowsVariables, 'queryClient' | 'connectionString'>,
{
projectRef,
tableId,
connectionString: connectionStringOverride,
...args
}: Omit<TableRowsVariables, 'queryClient'>,
{ enabled = true, ...options }: UseCustomQueryOptions<TableRowsData, TableRowsError, TData> = {}
) => {
const queryClient = useQueryClient()
const { connectionString, identifier: readReplicaIdentifier } = useConnectionStringForReadOps()
const resolvedConnectionString = connectionStringOverride ?? connectionString
// [Joshen] Exclude preflightCheck from query key
const { preflightCheck, ...othersArgs } = args
const { preflightCheck: _preflightCheck, ...othersArgs } = args
return useQuery<TableRowsData, TableRowsError, TData>({
queryKey: tableRowKeys.tableRows(projectRef, {
table: { id: tableId },
readReplicaIdentifier,
readReplicaIdentifier: connectionStringOverride ? undefined : readReplicaIdentifier,
...othersArgs,
}),
queryFn: ({ signal }) =>
getTableRows({ queryClient, projectRef, connectionString, tableId, ...args }, signal),
getTableRows(
{ queryClient, projectRef, connectionString: resolvedConnectionString, tableId, ...args },
signal
),
enabled:
enabled &&
typeof projectRef !== 'undefined' &&
typeof tableId !== 'undefined' &&
(!IS_PLATFORM || typeof connectionString !== 'undefined'),
(!IS_PLATFORM || typeof resolvedConnectionString !== 'undefined'),
...options,
})
}
+46
View File
@@ -66,6 +66,7 @@ export const CATEGORY_LABELS: Record<string, string> = {
}
const RECENT_ITEMS_STORAGE_KEY = 'v2-recent-items'
const DATA_TABS_STORAGE_KEY = 'v2-data-tabs'
const defaultExpandedGroups: Record<string, boolean> = {
'obs-logs': true,
@@ -131,6 +132,41 @@ export function V2DashboardProvider({ children }: { children: ReactNode }) {
}
}, [])
// Load persisted data tabs from localStorage once
useEffect(() => {
if (typeof window === 'undefined') return
try {
const raw = localStorage.getItem(DATA_TABS_STORAGE_KEY)
if (!raw) return
const parsed = JSON.parse(raw) as unknown
if (!Array.isArray(parsed)) return
const normalized = parsed
.filter((x) => x && typeof x === 'object')
.map((x) => x as Partial<DataTab>)
.filter(
(x) =>
typeof x.id === 'string' &&
typeof x.label === 'string' &&
typeof x.type === 'string' &&
typeof x.category === 'string' &&
typeof x.domain === 'string' &&
typeof x.path === 'string'
)
.map((x) => ({
id: x.id as string,
label: x.label as string,
type: x.type as DataTabType,
category: x.category as string,
domain: x.domain as DataTabDomain,
path: x.path as string,
}))
setDataTabs(normalized)
} catch {
// Ignore corrupted localStorage
}
}, [])
// Persist recents to localStorage on change
useEffect(() => {
if (typeof window === 'undefined') return
@@ -141,6 +177,16 @@ export function V2DashboardProvider({ children }: { children: ReactNode }) {
}
}, [recentItems])
// Persist open data tabs to localStorage on change
useEffect(() => {
if (typeof window === 'undefined') return
try {
localStorage.setItem(DATA_TABS_STORAGE_KEY, JSON.stringify(dataTabs))
} catch {
// Ignore write failures
}
}, [dataTabs])
const addRecentItem = useCallback((item: Omit<RecentItem, 'timestamp'>) => {
setRecentItems((prev) => {
const without = prev.filter((x) => x.id !== item.id)