Chore/add instructions for psql export table editor (#37249)

* Midway

* midway

* update warning

* Nit

* Nittt

* Add comment
This commit is contained in:
Joshen Lim
2025-07-22 15:37:48 +08:00
committed by GitHub
parent e23607b099
commit 9523272dbf
4 changed files with 195 additions and 62 deletions

View File

@@ -0,0 +1,129 @@
import { useParams } from 'common'
import { getConnectionStrings } from 'components/interfaces/Connect/DatabaseSettings.utils'
import { useReadReplicasQuery } from 'data/read-replicas/replicas-query'
import { pluckObjectFields } from 'lib/helpers'
import { useState } from 'react'
import { useTableEditorTableStateSnapshot } from 'state/table-editor-table'
import {
Button,
cn,
CodeBlock,
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogSection,
DialogSectionSeparator,
DialogTitle,
Tabs_Shadcn_,
TabsContent_Shadcn_,
TabsList_Shadcn_,
TabsTrigger_Shadcn_,
} from 'ui'
import { Admonition } from 'ui-patterns'
interface ExportDialogProps {
open: boolean
onOpenChange: (open: boolean) => void
}
export const ExportDialog = ({ open, onOpenChange }: ExportDialogProps) => {
const { ref: projectRef } = useParams()
const snap = useTableEditorTableStateSnapshot()
const [selectedTab, setSelectedTab] = useState<string>('csv')
const { data: databases } = useReadReplicasQuery({ projectRef })
const primaryDatabase = (databases ?? []).find((db) => db.identifier === projectRef)
const DB_FIELDS = ['db_host', 'db_name', 'db_port', 'db_user', 'inserted_at']
const emptyState = { db_user: '', db_host: '', db_port: '', db_name: '' }
const connectionInfo = pluckObjectFields(primaryDatabase || emptyState, DB_FIELDS)
const { db_host, db_port, db_user, db_name } = connectionInfo
const connectionStrings = getConnectionStrings({
connectionInfo,
metadata: { projectRef },
// [Joshen] We don't need any pooler details for this context, we only want direct
poolingInfo: { connectionString: '', db_host: '', db_name: '', db_port: 0, db_user: '' },
})
const outputName = `${snap.table.name}_rows`
const csvExportCommand = `
${connectionStrings.direct.psql} -c "COPY (SELECT * FROM "${snap.table.schema}"."${snap.table.name}") TO STDOUT WITH CSV HEADER DELIMITER ',';" > ${outputName}.csv
`.trim()
const sqlExportCommand = `
pg_dump -h ${db_host} -p ${db_port} -d ${db_name} -U ${db_user} --table="${snap.table.schema}.${snap.table.name}" --data-only --column-inserts > ${outputName}.sql
`.trim()
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent>
<DialogHeader>
<DialogTitle>Export table data via CLI</DialogTitle>
</DialogHeader>
<DialogSectionSeparator />
<DialogSection className="flex flex-col gap-y-4">
<p className="text-sm">
We highly recommend using <code>{selectedTab === 'csv' ? 'psql' : 'pg_dump'}</code> to
export your table data, in particular if your table is relatively large. This can be
done via the following command that you can run in your terminal:
</p>
<Tabs_Shadcn_ value={selectedTab} onValueChange={setSelectedTab}>
<TabsList_Shadcn_ className="gap-x-3">
<TabsTrigger_Shadcn_ value="csv">As CSV</TabsTrigger_Shadcn_>
<TabsTrigger_Shadcn_ value="sql">As SQL</TabsTrigger_Shadcn_>
</TabsList_Shadcn_>
<TabsContent_Shadcn_ value="csv">
<CodeBlock
hideLineNumbers
wrapperClassName={cn('[&_pre]:px-4 [&_pre]:py-3')}
language="bash"
value={csvExportCommand}
className="[&_code]:text-[12px] [&_code]:text-foreground"
/>
</TabsContent_Shadcn_>
<TabsContent_Shadcn_ value="sql">
<CodeBlock
hideLineNumbers
wrapperClassName={cn('[&_pre]:px-4 [&_pre]:py-3')}
language="bash"
value={sqlExportCommand}
className="[&_code]:text-[12px] [&_code]:text-foreground"
/>
</TabsContent_Shadcn_>
</Tabs_Shadcn_>
<p className="text-sm">
You will be prompted for your database password, and the output file{' '}
<code>
{outputName}.{selectedTab}
</code>{' '}
will be saved in the current directory that your terminal is in.
</p>
{selectedTab === 'sql' && (
<Admonition
type="note"
title="The pg_dump version needs to match your Postgres version"
>
<p className="!leading-normal">
If you run into a server version mismatch error, you will need to update{' '}
<code>pg_dump</code> before running the command.
</p>
</Admonition>
)}
</DialogSection>
<DialogFooter>
<Button type="default" onClick={() => onOpenChange(false)}>
Close
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
)
}

View File

@@ -35,6 +35,7 @@ import {
Separator,
SonnerProgress,
} from 'ui'
import { ExportDialog } from './ExportDialog'
import { FilterPopover } from './filter/FilterPopover'
import { SortPopover } from './sort/SortPopover'
// [Joshen] CSV exports require this guard as a fail-safe if the table is
@@ -230,6 +231,7 @@ const RowHeader = () => {
const { sorts } = useTableSort()
const [isExporting, setIsExporting] = useState(false)
const [showExportModal, setShowExportModal] = useState(false)
const { data } = useTableRowsQuery({
projectRef: project?.ref,
@@ -441,61 +443,73 @@ const RowHeader = () => {
})
return (
<div className="flex items-center gap-x-2">
{snap.editable && (
<ButtonTooltip
type="default"
size="tiny"
icon={<Trash />}
onClick={onRowsDelete}
disabled={snap.allRowsSelected && isImpersonatingRole}
tooltip={{
content: {
side: 'bottom',
text:
snap.allRowsSelected && isImpersonatingRole
? 'Table truncation is not supported when impersonating a role'
: undefined,
},
}}
>
{snap.allRowsSelected
? `Delete all rows in table`
: snap.selectedRows.size > 1
? `Delete ${snap.selectedRows.size} rows`
: `Delete ${snap.selectedRows.size} row`}
</ButtonTooltip>
)}
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
<>
<div className="flex items-center gap-x-2">
{snap.editable && (
<ButtonTooltip
type="default"
size="tiny"
iconRight={<ChevronDown />}
loading={isExporting}
disabled={isExporting}
icon={<Trash />}
onClick={onRowsDelete}
disabled={snap.allRowsSelected && isImpersonatingRole}
tooltip={{
content: {
side: 'bottom',
text:
snap.allRowsSelected && isImpersonatingRole
? 'Table truncation is not supported when impersonating a role'
: undefined,
},
}}
>
Export
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-40">
<DropdownMenuItem onClick={onRowsExportCSV}>
<span className="text-foreground-light">Export to CSV</span>
</DropdownMenuItem>
<DropdownMenuItem onClick={onRowsExportSQL}>Export to SQL</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
{snap.allRowsSelected
? `Delete all rows in table`
: snap.selectedRows.size > 1
? `Delete ${snap.selectedRows.size} rows`
: `Delete ${snap.selectedRows.size} row`}
</ButtonTooltip>
)}
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
type="default"
size="tiny"
iconRight={<ChevronDown />}
loading={isExporting}
disabled={isExporting}
>
Export
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className={snap.allRowsSelected ? 'w-52' : 'w-40'}>
<DropdownMenuItem onClick={onRowsExportCSV}>Export as CSV</DropdownMenuItem>
<DropdownMenuItem onClick={onRowsExportSQL}>Export as SQL</DropdownMenuItem>
{/* [Joshen] Should make this available for all cases, but that'll involve updating
the Dialog's SQL output to be dynamic based on any filters applied */}
{snap.allRowsSelected && (
<DropdownMenuItem className="group" onClick={() => setShowExportModal(true)}>
<div>
<p className="group-hover:text-foreground">Export via CLI</p>
<p className="text-foreground-lighter">Recommended for large tables</p>
</div>
</DropdownMenuItem>
)}
</DropdownMenuContent>
</DropdownMenu>
{!snap.allRowsSelected && totalRows > allRows.length && (
<>
<div className="h-6 ml-0.5">
<Separator orientation="vertical" />
</div>
<Button type="text" onClick={() => onSelectAllRows()}>
Select all rows in table
</Button>
</>
)}
</div>
{!snap.allRowsSelected && totalRows > allRows.length && (
<>
<div className="h-6 ml-0.5">
<Separator orientation="vertical" />
</div>
<Button type="text" onClick={() => onSelectAllRows()}>
Select all rows in table
</Button>
</>
)}
</div>
<ExportDialog open={showExportModal} onOpenChange={() => setShowExportModal(false)} />
</>
)
}

View File

@@ -12,10 +12,7 @@ import {
isTableLike,
isView,
} from 'data/table-editor/table-editor-types'
import { useGetTables } from 'data/tables/tables-query'
import { useCheckPermissions } from 'hooks/misc/useCheckPermissions'
import { useQuerySchemaState } from 'hooks/misc/useSchemaQueryState'
import { useSelectedProject } from 'hooks/misc/useSelectedProject'
import { useUrlState } from 'hooks/ui/useUrlState'
import { PROTECTED_SCHEMAS } from 'lib/constants/schemas'
import { useAppStateSnapshot } from 'state/app-state'
@@ -37,12 +34,10 @@ export const TableGridEditor = ({
selectedTable,
}: TableGridEditorProps) => {
const router = useRouter()
const project = useSelectedProject()
const appSnap = useAppStateSnapshot()
const { ref: projectRef, id } = useParams()
const tabs = useTabsStateSnapshot()
const { selectedSchema } = useQuerySchemaState()
useLoadTableEditorStateFromLocalStorageIntoUrl({
projectRef,
@@ -57,11 +52,6 @@ export const TableGridEditor = ({
const tabId = !!id ? tabs.openTabs.find((x) => x.endsWith(id)) : undefined
const openTabs = tabs.openTabs.filter((x) => !x.startsWith('sql'))
const getTables = useGetTables({
projectRef: project?.ref,
connectionString: project?.connectionString,
})
const onClearDashboardHistory = useCallback(() => {
if (projectRef) appSnap.setDashboardHistory(projectRef, 'editor', undefined)
}, [appSnap, projectRef])

0
data.sql Normal file
View File