mirror of
https://github.com/supabase/supabase.git
synced 2026-10-06 01:45:10 +03:00
## Context Part of dashboard scalability project Opting to use the connection string of the project's read replica (if available) for read queries on the database. Trialing with the Table Editor as a first pass - changes involved will opt to use replica connection string for `useTableRowsQuery`, `useTableRowsCountQuery`, and `useForeignKeyConstraintsQuery` There's definitely optimizations to be done for deciding which replica to use - but am starting off with a rather naive logic to prioritize replicas in the same region as the project. ## Changes involved - We're no longer passing `connectionString` as a param into the affected hooks, the `connectionString` is derived from within those hooks instead - Change is feature flagged, so things should be status quo if flag is off (use primary database's connection string) - Added `useConnectionStringForReadOps` hook which returns the replica's connection string if (Otherwise defaults to primary database connection string) - Feature flag is on - Project has a replica available ## To test - [ ] Verify that the table editor works as expected for a project that has read replicas (There shouldn't be any change really) - [ ] Also just double check that updating cells in the table editor works as well (There's no change there, we're using the primary DB's connection string for mutation ops) - [ ] ^ Same thing for a project that doesn't have read replicas - [ ] ^ Same thing for local / self-host
172 lines
5.5 KiB
TypeScript
172 lines
5.5 KiB
TypeScript
import { keepPreviousData, useQueryClient } from '@tanstack/react-query'
|
|
import { useParams } from 'common'
|
|
import { isMsSqlForeignTable } from 'data/table-editor/table-editor-types'
|
|
import { useTableRowsQuery } from 'data/table-rows/table-rows-query'
|
|
import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject'
|
|
import { RoleImpersonationState } from 'lib/role-impersonation'
|
|
import { EMPTY_ARR } from 'lib/void'
|
|
import { PropsWithChildren, useEffect, useRef, useState } from 'react'
|
|
import { DataGridHandle } from 'react-data-grid'
|
|
import { DndProvider } from 'react-dnd'
|
|
import { HTML5Backend } from 'react-dnd-html5-backend'
|
|
import { createPortal } from 'react-dom'
|
|
import { useRoleImpersonationStateSnapshot } from 'state/role-impersonation-state'
|
|
import { useTableEditorStateSnapshot } from 'state/table-editor'
|
|
import { QueuedOperation } from 'state/table-editor-operation-queue.types'
|
|
import { useTableEditorTableStateSnapshot } from 'state/table-editor-table'
|
|
|
|
import {
|
|
useIsQueueOperationsEnabled,
|
|
useIsTableFilterBarEnabled,
|
|
} from '../interfaces/App/FeaturePreview/FeaturePreviewContext'
|
|
import { Shortcuts } from './components/common/Shortcuts'
|
|
import { Footer } from './components/footer/Footer'
|
|
import { Grid } from './components/grid/Grid'
|
|
import { Header, HeaderProps } from './components/header/Header'
|
|
import { HeaderNew } from './components/header/HeaderNew'
|
|
import { RowContextMenu } from './components/menu/RowContextMenu'
|
|
import { useTableFilter } from './hooks/useTableFilter'
|
|
import { useTableSort } from './hooks/useTableSort'
|
|
import { validateMsSqlSorting } from './MsSqlValidation'
|
|
import { GridProps } from './types'
|
|
import { reapplyOptimisticUpdates } from './utils/queueOperationUtils'
|
|
|
|
export const SupabaseGrid = ({
|
|
customHeader,
|
|
gridProps,
|
|
children,
|
|
}: PropsWithChildren<
|
|
Pick<HeaderProps, 'customHeader'> & {
|
|
gridProps?: GridProps
|
|
}
|
|
>) => {
|
|
const { id: _id } = useParams()
|
|
const tableId = _id ? Number(_id) : undefined
|
|
|
|
const isQueueOperationsEnabled = useIsQueueOperationsEnabled()
|
|
|
|
const queryClient = useQueryClient()
|
|
const { data: project } = useSelectedProjectQuery()
|
|
const tableEditorSnap = useTableEditorStateSnapshot()
|
|
const snap = useTableEditorTableStateSnapshot()
|
|
const preflightCheck = !tableEditorSnap.tablesToIgnorePreflightCheck.includes(tableId ?? -1)
|
|
|
|
const gridRef = useRef<DataGridHandle>(null)
|
|
const [mounted, setMounted] = useState(false)
|
|
|
|
const newFilterBarEnabled = useIsTableFilterBarEnabled()
|
|
|
|
const { filters } = useTableFilter()
|
|
const { sorts, onApplySorts } = useTableSort()
|
|
|
|
const roleImpersonationState = useRoleImpersonationStateSnapshot()
|
|
|
|
const msSqlWarning = isMsSqlForeignTable(snap.originalTable)
|
|
? validateMsSqlSorting({ filters, sorts, table: snap.originalTable })
|
|
: { warning: null }
|
|
const tableQueriesEnabled = msSqlWarning.warning === null
|
|
|
|
const {
|
|
data,
|
|
error,
|
|
isSuccess,
|
|
isError,
|
|
isPending: isLoading,
|
|
isRefetching,
|
|
dataUpdatedAt,
|
|
} = useTableRowsQuery(
|
|
{
|
|
projectRef: project?.ref,
|
|
tableId,
|
|
sorts,
|
|
filters,
|
|
page: snap.page,
|
|
preflightCheck,
|
|
limit: tableEditorSnap.rowsPerPage,
|
|
roleImpersonationState: roleImpersonationState as RoleImpersonationState,
|
|
},
|
|
{
|
|
placeholderData: keepPreviousData,
|
|
enabled: tableQueriesEnabled,
|
|
retry: (_, error: any) => {
|
|
const doesNotExistError = error && error.message?.includes('does not exist')
|
|
if (doesNotExistError) onApplySorts([])
|
|
return false
|
|
},
|
|
}
|
|
)
|
|
|
|
useEffect(() => {
|
|
if (!mounted) setMounted(true)
|
|
}, [])
|
|
|
|
// Re-apply optimistic updates when table data is loaded/refetched
|
|
// This ensures pending changes remain visible when switching tabs or after data refresh
|
|
// Skip re-applying during save to avoid race condition where refetch completes before queue clears
|
|
const isSaving = tableEditorSnap.operationQueue.status === 'saving'
|
|
useEffect(() => {
|
|
if (
|
|
isSuccess &&
|
|
project?.ref &&
|
|
tableId &&
|
|
isQueueOperationsEnabled &&
|
|
tableEditorSnap.hasPendingOperations &&
|
|
!isSaving
|
|
) {
|
|
reapplyOptimisticUpdates({
|
|
queryClient,
|
|
projectRef: project.ref,
|
|
tableId,
|
|
operations: tableEditorSnap.operationQueue.operations as readonly QueuedOperation[],
|
|
})
|
|
}
|
|
}, [
|
|
isSuccess,
|
|
dataUpdatedAt,
|
|
project?.ref,
|
|
tableId,
|
|
isQueueOperationsEnabled,
|
|
tableEditorSnap.hasPendingOperations,
|
|
tableEditorSnap.operationQueue.operations,
|
|
queryClient,
|
|
isSaving,
|
|
])
|
|
|
|
const rows = data?.rows ?? EMPTY_ARR
|
|
|
|
const HeaderComponent = newFilterBarEnabled ? HeaderNew : Header
|
|
|
|
return (
|
|
<DndProvider backend={HTML5Backend} context={window}>
|
|
<div className="sb-grid h-full flex flex-col">
|
|
<HeaderComponent
|
|
customHeader={customHeader}
|
|
isRefetching={isRefetching}
|
|
tableQueriesEnabled={tableQueriesEnabled}
|
|
/>
|
|
|
|
{msSqlWarning.warning !== null && <msSqlWarning.Component />}
|
|
|
|
{children || (
|
|
<>
|
|
<Grid
|
|
ref={gridRef}
|
|
{...gridProps}
|
|
rows={rows}
|
|
error={error}
|
|
isDisabled={!tableQueriesEnabled}
|
|
isLoading={isLoading}
|
|
isSuccess={isSuccess}
|
|
isError={isError}
|
|
/>
|
|
<Footer enableForeignRowsQuery={tableQueriesEnabled} />
|
|
<Shortcuts gridRef={gridRef} rows={rows} />
|
|
</>
|
|
)}
|
|
|
|
{mounted && createPortal(<RowContextMenu rows={rows} />, document.body)}
|
|
</div>
|
|
</DndProvider>
|
|
)
|
|
}
|