import saveAs from 'file-saver' import { Clipboard, Copy, Download, Edit, Lock, MoreVertical, Trash } from 'lucide-react' import Link from 'next/link' import Papa from 'papaparse' import { toast } from 'sonner' import { IS_PLATFORM, useParams } from 'common' import { MAX_EXPORT_ROW_COUNT, MAX_EXPORT_ROW_COUNT_MESSAGE, } from 'components/grid/components/header/Header' import { parseSupaTable } from 'components/grid/SupabaseGrid.utils' import { formatTableRowsToSQL, getEntityLintDetails, } from 'components/interfaces/TableGridEditor/TableEntity.utils' import { EntityTypeIcon } from 'components/ui/EntityTypeIcon' import type { ItemRenderer } from 'components/ui/InfiniteList' import { InlineLink } from 'components/ui/InlineLink' import { getTableDefinition } from 'data/database/table-definition-query' import { ENTITY_TYPE } from 'data/entity-types/entity-type-constants' import { Entity } from 'data/entity-types/entity-types-infinite-query' import { useProjectLintsQuery } from 'data/lint/lint-query' import { EditorTablePageLink } from 'data/prefetchers/project.$ref.editor.$id' import { getTableEditor } from 'data/table-editor/table-editor-query' import { isTableLike } from 'data/table-editor/table-editor-types' import { fetchAllTableRows } from 'data/table-rows/table-rows-query' import { useQuerySchemaState } from 'hooks/misc/useSchemaQueryState' import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' import { formatSql } from 'lib/formatSql' import { useTableEditorStateSnapshot } from 'state/table-editor' import { createTabId, useTabsStateSnapshot } from 'state/tabs' import { Badge, Button, cn, copyToClipboard, DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuTrigger, Tooltip, TooltipContent, TooltipTrigger, TreeViewItemVariant, } from 'ui' export interface EntityListItemProps { id: number | string projectRef: string isLocked: boolean isActive?: boolean onExportCLI: () => void } // [jordi] Used to determine the entity is a table and not a view or other unsupported entity type function isTableLikeEntityListItem(entity: { type?: string }) { return entity?.type === ENTITY_TYPE.TABLE || entity?.type === ENTITY_TYPE.PARTITIONED_TABLE } const EntityListItem: ItemRenderer = ({ id, projectRef, item: entity, isLocked, isActive: _isActive, onExportCLI, }) => { const { data: project } = useSelectedProjectQuery() const snap = useTableEditorStateSnapshot() const { selectedSchema } = useQuerySchemaState() const tabId = createTabId(entity.type, { id: entity.id }) const tabs = useTabsStateSnapshot() const isPreview = tabs.previewTabId === tabId const isOpened = Object.values(tabs.tabsMap).some((tab) => tab.metadata?.tableId === entity.id) const isActive = Number(id) === entity.id const canEdit = isActive && !isLocked const { data: lints = [] } = useProjectLintsQuery({ projectRef: project?.ref, }) const tableHasLints: boolean = getEntityLintDetails( entity.name, 'rls_disabled_in_public', ['ERROR'], lints, selectedSchema ).hasLint const viewHasLints: boolean = getEntityLintDetails( entity.name, 'security_definer_view', ['ERROR', 'WARN'], lints, selectedSchema ).hasLint const materializedViewHasLints: boolean = getEntityLintDetails( entity.name, 'materialized_view_in_api', ['ERROR', 'WARN'], lints, selectedSchema ).hasLint const foreignTableHasLints: boolean = getEntityLintDetails( entity.name, 'foreign_table_in_api', ['ERROR', 'WARN'], lints, selectedSchema ).hasLint const formatTooltipText = (entityType: string) => { return Object.entries(ENTITY_TYPE) .find(([, value]) => value === entityType)?.[0] ?.toLowerCase() ?.split('_') ?.join(' ') } const exportTableAsCSV = async () => { if (IS_PLATFORM && !project?.connectionString) { return console.error('Connection string is required') } const toastId = toast.loading(`Exporting ${entity.name} as CSV...`) try { const table = await getTableEditor({ id: entity.id, projectRef, connectionString: project?.connectionString, }) if (isTableLike(table) && table.live_rows_estimate > MAX_EXPORT_ROW_COUNT) { return toast.error(
{MAX_EXPORT_ROW_COUNT_MESSAGE}
, { id: toastId } ) } const supaTable = table && parseSupaTable(table) if (!supaTable) { return toast.error(`Failed to export table: ${entity.name}`, { id: toastId }) } const rows = await fetchAllTableRows({ projectRef, connectionString: project?.connectionString, table: supaTable, }) const formattedRows = rows.map((row) => { const formattedRow = row Object.keys(row).map((column) => { if (typeof row[column] === 'object' && row[column] !== null) formattedRow[column] = JSON.stringify(formattedRow[column]) }) return formattedRow }) if (formattedRows.length > 0) { const csv = Papa.unparse(formattedRows, { columns: supaTable.columns.map((column) => column.name), }) const csvData = new Blob([csv], { type: 'text/csv;charset=utf-8;' }) saveAs(csvData, `${entity!.name}_rows.csv`) } toast.success(`Successfully exported ${entity.name} as CSV`, { id: toastId }) } catch (error: any) { toast.error(`Failed to export table: ${error.message}`, { id: toastId }) } } const exportTableAsSQL = async () => { if (IS_PLATFORM && !project?.connectionString) { return console.error('Connection string is required') } const toastId = toast.loading(`Exporting ${entity.name} as SQL...`) try { const table = await getTableEditor({ id: entity.id, projectRef, connectionString: project?.connectionString, }) if (isTableLike(table) && table.live_rows_estimate > MAX_EXPORT_ROW_COUNT) { return toast.error(
{MAX_EXPORT_ROW_COUNT_MESSAGE}
, { id: toastId } ) } const supaTable = table && parseSupaTable(table) if (!supaTable) { return toast.error(`Failed to export table: ${entity.name}`, { id: toastId }) } const rows = await fetchAllTableRows({ projectRef, connectionString: project?.connectionString, table: supaTable, }) const formattedRows = rows.map((row) => { const formattedRow = row Object.keys(row).map((column) => { if (typeof row[column] === 'object' && row[column] !== null) formattedRow[column] = JSON.stringify(formattedRow[column]) }) return formattedRow }) if (formattedRows.length > 0) { const sqlStatements = formatTableRowsToSQL(supaTable, formattedRows) const sqlData = new Blob([sqlStatements], { type: 'text/sql;charset=utf-8;' }) saveAs(sqlData, `${entity!.name}_rows.sql`) } toast.success(`Successfully exported ${entity.name} as SQL`, { id: toastId }) } catch (error: any) { toast.error(`Failed to export table: ${error.message}`, { id: toastId }) } } return ( { e.preventDefault() const tabId = createTabId(entity.type, { id: entity.id }) tabs.makeTabPermanent(tabId) }} > <> {isActive &&
} {formatTooltipText(entity.type)}
{entity.name}
{canEdit && (