Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions apps/studio/components/grid/SupabaseGrid.utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,13 @@ import { CalculatedColumn, CellKeyboardEvent } from 'react-data-grid'

import type { Filter, SavedState } from 'components/grid/types'
import { Entity, isTableLike } from 'data/table-editor/table-editor-types'
import { useSearchParams } from 'next/navigation'
import { parseAsBoolean, parseAsNativeArrayOf, parseAsString, useQueryStates } from 'nuqs'
import { copyToClipboard } from 'ui'
import { FilterOperatorOptions } from './components/header/filter/Filter.constants'
import { STORAGE_KEY_PREFIX } from './constants'
import type { Sort, SupaColumn, SupaTable } from './types'
import { formatClipboardValue } from './utils/common'
import { parseAsNativeArrayOf, parseAsBoolean, parseAsString, useQueryStates } from 'nuqs'
import { useSearchParams } from 'next/navigation'

export const LOAD_TAB_FROM_CACHE_PARAM = 'loadFromCache'

Expand Down Expand Up @@ -67,6 +67,7 @@ export function filtersToUrlParams(filters: Filter[]) {
export function parseSupaTable(table: Entity): SupaTable {
const columns = table.columns
const primaryKeys = isTableLike(table) ? table.primary_keys : []
const uniqueIndexes = isTableLike(table) ? table.unique_indexes : []
const relationships = isTableLike(table) ? table.relationships : []

const supaColumns: SupaColumn[] = columns.map((column) => {
Expand Down Expand Up @@ -116,8 +117,14 @@ export function parseSupaTable(table: Entity): SupaTable {
name: table.name,
comment: table.comment,
schema: table.schema,
type: table.entity_type,
columns: supaColumns,
estimateRowCount: isTableLike(table) ? table.live_rows_estimate : 0,
primaryKey: primaryKeys?.length > 0 ? primaryKeys.map((col) => col.name) : undefined,
uniqueIndexes:
!!uniqueIndexes && uniqueIndexes.length > 0
? uniqueIndexes.map(({ columns }) => columns)
: undefined,
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,9 +68,9 @@ export const ExportDialog = ({
const queryChains = !table ? undefined : getAllTableRowsSql({ table, sorts, filters })
const query = !!queryChains
? ignoreRoleImpersonation
? queryChains.toSql()
? queryChains.sql.toSql()
: wrapWithRoleImpersonation(
queryChains.toSql(),
queryChains.sql.toSql(),
roleImpersonationState as RoleImpersonationState
)
: ''
Expand Down
194 changes: 57 additions & 137 deletions apps/studio/components/grid/components/header/Header.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { PermissionAction } from '@supabase/shared-types/out/constants'
import saveAs from 'file-saver'
import { ArrowUp, ChevronDown, FileText, Trash } from 'lucide-react'
import Link from 'next/link'
import { ReactNode, useState } from 'react'
Expand All @@ -10,9 +9,14 @@ import { useTableFilter } from 'components/grid/hooks/useTableFilter'
import { useTableSort } from 'components/grid/hooks/useTableSort'
import { GridHeaderActions } from 'components/interfaces/TableGridEditor/GridHeaderActions'
import { formatTableRowsToSQL } from 'components/interfaces/TableGridEditor/TableEntity.utils'
import {
useExportAllRowsAsCsv,
useExportAllRowsAsJson,
useExportAllRowsAsSql,
} from 'components/layouts/TableEditorLayout/ExportAllRows'
import { ButtonTooltip } from 'components/ui/ButtonTooltip'
import { useTableRowsCountQuery } from 'data/table-rows/table-rows-count-query'
import { fetchAllTableRows, useTableRowsQuery } from 'data/table-rows/table-rows-query'
import { useTableRowsQuery } from 'data/table-rows/table-rows-query'
import { useSendEventMutation } from 'data/telemetry/send-event-mutation'
import { useAsyncCheckPermissions } from 'hooks/misc/useCheckPermissions'
import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization'
Expand All @@ -34,12 +38,12 @@ import {
DropdownMenuItem,
DropdownMenuTrigger,
Separator,
SonnerProgress,
} from 'ui'
import { ExportDialog } from './ExportDialog'
import { FilterPopover } from './filter/FilterPopover'
import { formatRowsForCSV } from './Header.utils'
import { SortPopover } from './sort/SortPopover'

// [Joshen] CSV exports require this guard as a fail-safe if the table is
// just too large for a browser to keep all the rows in memory before
// exporting. Either that or export as multiple CSV sheets with max n rows each
Expand Down Expand Up @@ -312,170 +316,82 @@ const RowHeader = ({ tableQueriesEnabled = true }: RowHeaderProps) => {
toast.success('Copied rows to clipboard')
}

const exportParams = snap.allRowsSelected
? ({ type: 'fetch_all', filters, sorts } as const)
: ({
type: 'provided_rows',
table: snap.table,
rows: allRows.filter((x) => snap.selectedRows.has(x.idx)),
} as const)

const { exportCsv, confirmationModal: exportCsvConfirmationModal } = useExportAllRowsAsCsv(
project
? {
enabled: true,
projectRef: project.ref,
connectionString: project?.connectionString ?? null,
entity: snap.table,
totalRows,
...exportParams,
}
: { enabled: false }
)
const onRowsExportCSV = async () => {
setIsExporting(true)

if (snap.allRowsSelected && totalRows > MAX_EXPORT_ROW_COUNT) {
toast.error(
<div className="prose text-sm text-foreground">{MAX_EXPORT_ROW_COUNT_MESSAGE}</div>
)
return setIsExporting(false)
}

if (!project) {
toast.error('Project is required')
return setIsExporting(false)
}

const toastId = snap.allRowsSelected
? toast(
<SonnerProgress progress={0} message={`Exporting all rows from ${snap.table.name}`} />,
{
closeButton: false,
duration: Infinity,
}
)
: toast.loading(
`Exporting ${snap.selectedRows.size} row${snap.selectedRows.size > 1 ? 's' : ''} from ${snap.table.name}`
)

const rows = snap.allRowsSelected
? await fetchAllTableRows({
projectRef: project.ref,
connectionString: project.connectionString,
table: snap.table,
filters,
sorts,
roleImpersonationState: roleImpersonationState as RoleImpersonationState,
progressCallback: (value: number) => {
const progress = Math.min((value / totalRows) * 100, 100)
toast(
<SonnerProgress
progress={progress}
message={`Exporting all rows from ${snap.table.name}`}
/>,
{
id: toastId,
closeButton: false,
duration: Infinity,
}
)
},
})
: allRows.filter((x) => snap.selectedRows.has(x.idx))

if (rows.length === 0) {
toast.dismiss(toastId)
toast.error('Export failed, please try exporting again')
setIsExporting(false)
return
}
exportCsv()

const csv = formatRowsForCSV({
rows,
columns: snap.table!.columns.map((column) => column.name),
})
const csvData = new Blob([csv], { type: 'text/csv;charset=utf-8;' })
toast.success(`Downloaded ${rows.length} rows as CSV`, {
id: toastId,
closeButton: true,
duration: 4000,
})
saveAs(csvData, `${snap.table!.name}_rows.csv`)
setIsExporting(false)
}

const { exportSql, confirmationModal: exportSqlConfirmationModal } = useExportAllRowsAsSql(
project
? {
enabled: true,
projectRef: project.ref,
connectionString: project?.connectionString ?? null,
entity: snap.table,
...exportParams,
}
: { enabled: false }
)
const onRowsExportSQL = async () => {
setIsExporting(true)

if (snap.allRowsSelected && totalRows > MAX_EXPORT_ROW_COUNT) {
toast.error(
<div className="prose text-sm text-foreground">{MAX_EXPORT_ROW_COUNT_MESSAGE}</div>
)
return setIsExporting(false)
}

if (!project) {
toast.error('Project is required')
return setIsExporting(false)
}

if (snap.allRowsSelected && totalRows === 0) {
toast.error('Export failed, please try exporting again')
return setIsExporting(false)
}

const toastId = snap.allRowsSelected
? toast(
<SonnerProgress progress={0} message={`Exporting all rows from ${snap.table.name}`} />,
{
closeButton: false,
duration: Infinity,
}
)
: toast.loading(
`Exporting ${snap.selectedRows.size} row${snap.selectedRows.size > 1 ? 's' : ''} from ${snap.table.name}`
)

const rows = snap.allRowsSelected
? await fetchAllTableRows({
projectRef: project.ref,
connectionString: project.connectionString,
table: snap.table,
filters,
sorts,
roleImpersonationState: roleImpersonationState as RoleImpersonationState,
progressCallback: (value: number) => {
const progress = Math.min((value / totalRows) * 100, 100)
toast(
<SonnerProgress
progress={progress}
message={`Exporting all rows from ${snap.table.name}`}
/>,
{
id: toastId,
closeButton: false,
duration: Infinity,
}
)
},
})
: allRows.filter((x) => snap.selectedRows.has(x.idx))

if (rows.length === 0) {
toast.error('Export failed, please exporting try again')
setIsExporting(false)
return
}
exportSql()

const sqlStatements = formatTableRowsToSQL(snap.table, rows)
const sqlData = new Blob([sqlStatements], { type: 'text/sql;charset=utf-8;' })
toast.success(`Downloading ${rows.length} rows as SQL`, {
id: toastId,
closeButton: true,
duration: 4000,
})
saveAs(sqlData, `${snap.table!.name}_rows.sql`)
setIsExporting(false)
}

const { exportJson, confirmationModal: exportJsonConfirmationModal } = useExportAllRowsAsJson(
project
? {
enabled: true,
projectRef: project.ref,
connectionString: project?.connectionString ?? null,
entity: snap.table,
...exportParams,
}
: { enabled: false }
)
const onRowsExportJSON = async () => {
if (!project) {
return toast.error('Project is required')
}

setIsExporting(true)
const toastId = toast.loading(
`Exporting ${snap.selectedRows.size} row${snap.selectedRows.size > 1 ? 's' : ''} from ${snap.table.name}`
)
const rows = allRows.filter((x) => snap.selectedRows.has(x.idx))
const sqlData = new Blob([JSON.stringify(rows)], { type: 'text/sql;charset=utf-8;' })
toast.success(`Downloading ${rows.length} rows as JSON`, {
id: toastId,
closeButton: true,
duration: 4000,
})
saveAs(sqlData, `${snap.table!.name}_rows.json`)

exportJson()

setIsExporting(false)
}
Expand Down Expand Up @@ -600,6 +516,10 @@ const RowHeader = ({ tableQueriesEnabled = true }: RowHeaderProps) => {
open={showExportModal}
onOpenChange={() => setShowExportModal(false)}
/>

{exportCsvConfirmationModal}
{exportSqlConfirmationModal}
{exportJsonConfirmationModal}
</>
)
}
6 changes: 5 additions & 1 deletion apps/studio/components/grid/types/table.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { GridForeignKey } from './base'
import type { ENTITY_TYPE } from 'data/entity-types/entity-type-constants'
import type { Dictionary } from 'types'
import { GridForeignKey } from './base'

export interface SupaColumn {
readonly dataType: string
Expand All @@ -20,11 +21,14 @@ export interface SupaColumn {

export interface SupaTable {
readonly id: number
readonly type: ENTITY_TYPE
readonly columns: SupaColumn[]
readonly name: string
readonly schema?: string | null
readonly comment?: string | null
readonly estimateRowCount: number
readonly primaryKey?: string[]
readonly uniqueIndexes?: string[][]
}

export interface SupaRow extends Dictionary<any> {
Expand Down
Loading
Loading