Files
supabase/apps/studio/components/layouts/TableEditorLayout/ExportAllRows.progress.tsx
Charis 8e705ecdbc fix(export all rows): use cursor pagination if possible (#40536)
Exporting all rows (in CSV, SQL, or JSON format) currently uses offset pagination, which can cause performance problems if the table is large. There is also a correctness problem if the table is being actively updated as the export happens, because the relative row offsets could shift between queries.

Now that composite filters are available in postgres-meta, we can change to using cursor pagination on the primary key (or any non-null unique keys) wherever possible. Where this is not possible, the user will be shown a confirmation dialog explaining the possible performance impact.

---------

Co-authored-by: Ali Waseem <waseema393@gmail.com>
Co-authored-by: Joshen Lim <joshenlimek@gmail.com>
2025-12-08 13:39:10 -05:00

105 lines
2.6 KiB
TypeScript

import { useCallback, useRef, type ReactNode } from 'react'
import { toast } from 'sonner'
import { SonnerProgress } from 'ui'
export const useProgressToasts = () => {
const toastIdsRef = useRef(new Map<number, string | number>())
const startProgressTracker = useCallback(
({
id,
name,
trackPercentage = false,
}: {
id: number
name: string
trackPercentage?: boolean
}) => {
if (toastIdsRef.current.has(id)) return
if (trackPercentage) {
toastIdsRef.current.set(
id,
toast(<SonnerProgress progress={0} message={`Exporting ${name}...`} />, {
closeButton: false,
duration: Infinity,
})
)
} else {
toastIdsRef.current.set(id, toast.loading(`Exporting ${name}...`))
}
},
[]
)
const trackPercentageProgress = useCallback(
({
id,
name,
value,
totalRows,
}: {
id: number
name: string
value: number
totalRows: number
}) => {
const savedToastId = toastIdsRef.current.get(id)
const progress = Math.min((value / totalRows) * 100, 100)
const newToastId = toast(
<SonnerProgress progress={progress} message={`Exporting ${name}...`} />,
{
id: savedToastId,
closeButton: false,
duration: Infinity,
}
)
if (!savedToastId) toastIdsRef.current.set(id, newToastId)
},
[]
)
const stopTrackerWithError = useCallback(
(id: number, name: string, customMessage?: ReactNode) => {
const savedToastId = toastIdsRef.current.get(id)
if (savedToastId) {
toast.dismiss(savedToastId)
toastIdsRef.current.delete(id)
}
toast.error(customMessage ?? `There was an error exporting ${name}`)
},
[]
)
const dismissTrackerSilently = useCallback((id: number) => {
const savedToastId = toastIdsRef.current.get(id)
if (savedToastId) {
toast.dismiss(savedToastId)
toastIdsRef.current.delete(id)
}
}, [])
const markTrackerComplete = useCallback((id: number, totalRows: number) => {
const savedToastId = toastIdsRef.current.get(id)
const deleteSavedToastId = () => toastIdsRef.current.delete(id)
toast.success(`Successfully exported ${totalRows} rows`, {
id: savedToastId,
duration: 4000,
onAutoClose: deleteSavedToastId,
onDismiss: deleteSavedToastId,
})
}, [])
return {
startProgressTracker,
trackPercentageProgress,
stopTrackerWithError,
dismissTrackerSilently,
markTrackerComplete,
}
}