Skip to content

Commit 9523272

Browse files
authored
Chore/add instructions for psql export table editor (supabase#37249)
* Midway * midway * update warning * Nit * Nittt * Add comment
1 parent e23607b commit 9523272

File tree

4 files changed

+196
-63
lines changed

4 files changed

+196
-63
lines changed
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
import { useParams } from 'common'
2+
import { getConnectionStrings } from 'components/interfaces/Connect/DatabaseSettings.utils'
3+
import { useReadReplicasQuery } from 'data/read-replicas/replicas-query'
4+
import { pluckObjectFields } from 'lib/helpers'
5+
import { useState } from 'react'
6+
import { useTableEditorTableStateSnapshot } from 'state/table-editor-table'
7+
import {
8+
Button,
9+
cn,
10+
CodeBlock,
11+
Dialog,
12+
DialogContent,
13+
DialogFooter,
14+
DialogHeader,
15+
DialogSection,
16+
DialogSectionSeparator,
17+
DialogTitle,
18+
Tabs_Shadcn_,
19+
TabsContent_Shadcn_,
20+
TabsList_Shadcn_,
21+
TabsTrigger_Shadcn_,
22+
} from 'ui'
23+
import { Admonition } from 'ui-patterns'
24+
25+
interface ExportDialogProps {
26+
open: boolean
27+
onOpenChange: (open: boolean) => void
28+
}
29+
30+
export const ExportDialog = ({ open, onOpenChange }: ExportDialogProps) => {
31+
const { ref: projectRef } = useParams()
32+
const snap = useTableEditorTableStateSnapshot()
33+
const [selectedTab, setSelectedTab] = useState<string>('csv')
34+
35+
const { data: databases } = useReadReplicasQuery({ projectRef })
36+
const primaryDatabase = (databases ?? []).find((db) => db.identifier === projectRef)
37+
const DB_FIELDS = ['db_host', 'db_name', 'db_port', 'db_user', 'inserted_at']
38+
const emptyState = { db_user: '', db_host: '', db_port: '', db_name: '' }
39+
40+
const connectionInfo = pluckObjectFields(primaryDatabase || emptyState, DB_FIELDS)
41+
const { db_host, db_port, db_user, db_name } = connectionInfo
42+
43+
const connectionStrings = getConnectionStrings({
44+
connectionInfo,
45+
metadata: { projectRef },
46+
// [Joshen] We don't need any pooler details for this context, we only want direct
47+
poolingInfo: { connectionString: '', db_host: '', db_name: '', db_port: 0, db_user: '' },
48+
})
49+
50+
const outputName = `${snap.table.name}_rows`
51+
52+
const csvExportCommand = `
53+
${connectionStrings.direct.psql} -c "COPY (SELECT * FROM "${snap.table.schema}"."${snap.table.name}") TO STDOUT WITH CSV HEADER DELIMITER ',';" > ${outputName}.csv
54+
`.trim()
55+
56+
const sqlExportCommand = `
57+
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
58+
`.trim()
59+
60+
return (
61+
<Dialog open={open} onOpenChange={onOpenChange}>
62+
<DialogContent>
63+
<DialogHeader>
64+
<DialogTitle>Export table data via CLI</DialogTitle>
65+
</DialogHeader>
66+
67+
<DialogSectionSeparator />
68+
69+
<DialogSection className="flex flex-col gap-y-4">
70+
<p className="text-sm">
71+
We highly recommend using <code>{selectedTab === 'csv' ? 'psql' : 'pg_dump'}</code> to
72+
export your table data, in particular if your table is relatively large. This can be
73+
done via the following command that you can run in your terminal:
74+
</p>
75+
76+
<Tabs_Shadcn_ value={selectedTab} onValueChange={setSelectedTab}>
77+
<TabsList_Shadcn_ className="gap-x-3">
78+
<TabsTrigger_Shadcn_ value="csv">As CSV</TabsTrigger_Shadcn_>
79+
<TabsTrigger_Shadcn_ value="sql">As SQL</TabsTrigger_Shadcn_>
80+
</TabsList_Shadcn_>
81+
<TabsContent_Shadcn_ value="csv">
82+
<CodeBlock
83+
hideLineNumbers
84+
wrapperClassName={cn('[&_pre]:px-4 [&_pre]:py-3')}
85+
language="bash"
86+
value={csvExportCommand}
87+
className="[&_code]:text-[12px] [&_code]:text-foreground"
88+
/>
89+
</TabsContent_Shadcn_>
90+
<TabsContent_Shadcn_ value="sql">
91+
<CodeBlock
92+
hideLineNumbers
93+
wrapperClassName={cn('[&_pre]:px-4 [&_pre]:py-3')}
94+
language="bash"
95+
value={sqlExportCommand}
96+
className="[&_code]:text-[12px] [&_code]:text-foreground"
97+
/>
98+
</TabsContent_Shadcn_>
99+
</Tabs_Shadcn_>
100+
101+
<p className="text-sm">
102+
You will be prompted for your database password, and the output file{' '}
103+
<code>
104+
{outputName}.{selectedTab}
105+
</code>{' '}
106+
will be saved in the current directory that your terminal is in.
107+
</p>
108+
109+
{selectedTab === 'sql' && (
110+
<Admonition
111+
type="note"
112+
title="The pg_dump version needs to match your Postgres version"
113+
>
114+
<p className="!leading-normal">
115+
If you run into a server version mismatch error, you will need to update{' '}
116+
<code>pg_dump</code> before running the command.
117+
</p>
118+
</Admonition>
119+
)}
120+
</DialogSection>
121+
<DialogFooter>
122+
<Button type="default" onClick={() => onOpenChange(false)}>
123+
Close
124+
</Button>
125+
</DialogFooter>
126+
</DialogContent>
127+
</Dialog>
128+
)
129+
}

apps/studio/components/grid/components/header/Header.tsx

Lines changed: 67 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import {
3535
Separator,
3636
SonnerProgress,
3737
} from 'ui'
38+
import { ExportDialog } from './ExportDialog'
3839
import { FilterPopover } from './filter/FilterPopover'
3940
import { SortPopover } from './sort/SortPopover'
4041
// [Joshen] CSV exports require this guard as a fail-safe if the table is
@@ -230,6 +231,7 @@ const RowHeader = () => {
230231
const { sorts } = useTableSort()
231232

232233
const [isExporting, setIsExporting] = useState(false)
234+
const [showExportModal, setShowExportModal] = useState(false)
233235

234236
const { data } = useTableRowsQuery({
235237
projectRef: project?.ref,
@@ -441,61 +443,73 @@ const RowHeader = () => {
441443
})
442444

443445
return (
444-
<div className="flex items-center gap-x-2">
445-
{snap.editable && (
446-
<ButtonTooltip
447-
type="default"
448-
size="tiny"
449-
icon={<Trash />}
450-
onClick={onRowsDelete}
451-
disabled={snap.allRowsSelected && isImpersonatingRole}
452-
tooltip={{
453-
content: {
454-
side: 'bottom',
455-
text:
456-
snap.allRowsSelected && isImpersonatingRole
457-
? 'Table truncation is not supported when impersonating a role'
458-
: undefined,
459-
},
460-
}}
461-
>
462-
{snap.allRowsSelected
463-
? `Delete all rows in table`
464-
: snap.selectedRows.size > 1
465-
? `Delete ${snap.selectedRows.size} rows`
466-
: `Delete ${snap.selectedRows.size} row`}
467-
</ButtonTooltip>
468-
)}
469-
<DropdownMenu>
470-
<DropdownMenuTrigger asChild>
471-
<Button
446+
<>
447+
<div className="flex items-center gap-x-2">
448+
{snap.editable && (
449+
<ButtonTooltip
472450
type="default"
473451
size="tiny"
474-
iconRight={<ChevronDown />}
475-
loading={isExporting}
476-
disabled={isExporting}
452+
icon={<Trash />}
453+
onClick={onRowsDelete}
454+
disabled={snap.allRowsSelected && isImpersonatingRole}
455+
tooltip={{
456+
content: {
457+
side: 'bottom',
458+
text:
459+
snap.allRowsSelected && isImpersonatingRole
460+
? 'Table truncation is not supported when impersonating a role'
461+
: undefined,
462+
},
463+
}}
477464
>
478-
Export
479-
</Button>
480-
</DropdownMenuTrigger>
481-
<DropdownMenuContent className="w-40">
482-
<DropdownMenuItem onClick={onRowsExportCSV}>
483-
<span className="text-foreground-light">Export to CSV</span>
484-
</DropdownMenuItem>
485-
<DropdownMenuItem onClick={onRowsExportSQL}>Export to SQL</DropdownMenuItem>
486-
</DropdownMenuContent>
487-
</DropdownMenu>
488-
489-
{!snap.allRowsSelected && totalRows > allRows.length && (
490-
<>
491-
<div className="h-6 ml-0.5">
492-
<Separator orientation="vertical" />
493-
</div>
494-
<Button type="text" onClick={() => onSelectAllRows()}>
495-
Select all rows in table
496-
</Button>
497-
</>
498-
)}
499-
</div>
465+
{snap.allRowsSelected
466+
? `Delete all rows in table`
467+
: snap.selectedRows.size > 1
468+
? `Delete ${snap.selectedRows.size} rows`
469+
: `Delete ${snap.selectedRows.size} row`}
470+
</ButtonTooltip>
471+
)}
472+
<DropdownMenu>
473+
<DropdownMenuTrigger asChild>
474+
<Button
475+
type="default"
476+
size="tiny"
477+
iconRight={<ChevronDown />}
478+
loading={isExporting}
479+
disabled={isExporting}
480+
>
481+
Export
482+
</Button>
483+
</DropdownMenuTrigger>
484+
<DropdownMenuContent className={snap.allRowsSelected ? 'w-52' : 'w-40'}>
485+
<DropdownMenuItem onClick={onRowsExportCSV}>Export as CSV</DropdownMenuItem>
486+
<DropdownMenuItem onClick={onRowsExportSQL}>Export as SQL</DropdownMenuItem>
487+
{/* [Joshen] Should make this available for all cases, but that'll involve updating
488+
the Dialog's SQL output to be dynamic based on any filters applied */}
489+
{snap.allRowsSelected && (
490+
<DropdownMenuItem className="group" onClick={() => setShowExportModal(true)}>
491+
<div>
492+
<p className="group-hover:text-foreground">Export via CLI</p>
493+
<p className="text-foreground-lighter">Recommended for large tables</p>
494+
</div>
495+
</DropdownMenuItem>
496+
)}
497+
</DropdownMenuContent>
498+
</DropdownMenu>
499+
500+
{!snap.allRowsSelected && totalRows > allRows.length && (
501+
<>
502+
<div className="h-6 ml-0.5">
503+
<Separator orientation="vertical" />
504+
</div>
505+
<Button type="text" onClick={() => onSelectAllRows()}>
506+
Select all rows in table
507+
</Button>
508+
</>
509+
)}
510+
</div>
511+
512+
<ExportDialog open={showExportModal} onOpenChange={() => setShowExportModal(false)} />
513+
</>
500514
)
501515
}

apps/studio/components/interfaces/TableGridEditor/TableGridEditor.tsx

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,7 @@ import {
1212
isTableLike,
1313
isView,
1414
} from 'data/table-editor/table-editor-types'
15-
import { useGetTables } from 'data/tables/tables-query'
1615
import { useCheckPermissions } from 'hooks/misc/useCheckPermissions'
17-
import { useQuerySchemaState } from 'hooks/misc/useSchemaQueryState'
18-
import { useSelectedProject } from 'hooks/misc/useSelectedProject'
1916
import { useUrlState } from 'hooks/ui/useUrlState'
2017
import { PROTECTED_SCHEMAS } from 'lib/constants/schemas'
2118
import { useAppStateSnapshot } from 'state/app-state'
@@ -37,12 +34,10 @@ export const TableGridEditor = ({
3734
selectedTable,
3835
}: TableGridEditorProps) => {
3936
const router = useRouter()
40-
const project = useSelectedProject()
4137
const appSnap = useAppStateSnapshot()
4238
const { ref: projectRef, id } = useParams()
4339

4440
const tabs = useTabsStateSnapshot()
45-
const { selectedSchema } = useQuerySchemaState()
4641

4742
useLoadTableEditorStateFromLocalStorageIntoUrl({
4843
projectRef,
@@ -57,11 +52,6 @@ export const TableGridEditor = ({
5752
const tabId = !!id ? tabs.openTabs.find((x) => x.endsWith(id)) : undefined
5853
const openTabs = tabs.openTabs.filter((x) => !x.startsWith('sql'))
5954

60-
const getTables = useGetTables({
61-
projectRef: project?.ref,
62-
connectionString: project?.connectionString,
63-
})
64-
6555
const onClearDashboardHistory = useCallback(() => {
6656
if (projectRef) appSnap.setDashboardHistory(projectRef, 'editor', undefined)
6757
}, [appSnap, projectRef])

data.sql

Whitespace-only changes.

0 commit comments

Comments
 (0)