mirror of
https://github.com/supabase/supabase.git
synced 2026-10-15 14:19:11 +03:00
improve data fetching and tabs persistence
This commit is contained in:
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>
|
||||
|
||||
@@ -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}>
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in new issue
Block a user