Chore/add instructions for psql export table editor (#37249)
* Midway * midway * update warning * Nit * Nittt * Add comment
This commit is contained in:
129
apps/studio/components/grid/components/header/ExportDialog.tsx
Normal file
129
apps/studio/components/grid/components/header/ExportDialog.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
@@ -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)} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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])
|
||||
|
||||
Reference in New Issue
Block a user