diff --git a/apps/studio/components/grid/components/header/ExportDialog.tsx b/apps/studio/components/grid/components/header/ExportDialog.tsx new file mode 100644 index 0000000000..83d6f275d7 --- /dev/null +++ b/apps/studio/components/grid/components/header/ExportDialog.tsx @@ -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('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 ( + + + + Export table data via CLI + + + + + +

+ We highly recommend using {selectedTab === 'csv' ? 'psql' : 'pg_dump'} 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: +

+ + + + As CSV + As SQL + + + + + + + + + +

+ You will be prompted for your database password, and the output file{' '} + + {outputName}.{selectedTab} + {' '} + will be saved in the current directory that your terminal is in. +

+ + {selectedTab === 'sql' && ( + +

+ If you run into a server version mismatch error, you will need to update{' '} + pg_dump before running the command. +

+
+ )} +
+ + + +
+
+ ) +} diff --git a/apps/studio/components/grid/components/header/Header.tsx b/apps/studio/components/grid/components/header/Header.tsx index 55847f98d3..67d4d2886e 100644 --- a/apps/studio/components/grid/components/header/Header.tsx +++ b/apps/studio/components/grid/components/header/Header.tsx @@ -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 ( -
- {snap.editable && ( - } - 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`} - - )} - - - - - - - Export to CSV - - Export to SQL - - + {snap.allRowsSelected + ? `Delete all rows in table` + : snap.selectedRows.size > 1 + ? `Delete ${snap.selectedRows.size} rows` + : `Delete ${snap.selectedRows.size} row`} + + )} + + + + + + Export as CSV + Export as SQL + {/* [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 && ( + setShowExportModal(true)}> +
+

Export via CLI

+

Recommended for large tables

+
+
+ )} +
+
- {!snap.allRowsSelected && totalRows > allRows.length && ( - <> -
- -
- - - )} -
+ {!snap.allRowsSelected && totalRows > allRows.length && ( + <> +
+ +
+ + + )} + + + setShowExportModal(false)} /> + ) } diff --git a/apps/studio/components/interfaces/TableGridEditor/TableGridEditor.tsx b/apps/studio/components/interfaces/TableGridEditor/TableGridEditor.tsx index a29a158fc0..fe294b3616 100644 --- a/apps/studio/components/interfaces/TableGridEditor/TableGridEditor.tsx +++ b/apps/studio/components/interfaces/TableGridEditor/TableGridEditor.tsx @@ -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]) diff --git a/data.sql b/data.sql new file mode 100644 index 0000000000..e69de29bb2