import saveAs from 'file-saver' import { Clipboard, Copy, Download, Edit, Lock, MoreHorizontal, Trash, Unlock } from 'lucide-react' import Link from 'next/link' import Papa from 'papaparse' import { toast } from 'sonner' import { useSnapshot } from 'valtio' import { IS_PLATFORM } 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 { useIsTableEditorTabsEnabled } from 'components/interfaces/App/FeaturePreview/FeaturePreviewContext' import { formatTableRowsToSQL, getEntityLintDetails, } from 'components/interfaces/TableGridEditor/TableEntity.utils' import { EntityTypeIcon } from 'components/ui/EntityTypeIcon' import type { ItemRenderer } from 'components/ui/InfiniteList' 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 { copyToClipboard } from 'lib/helpers' import { useTableEditorStateSnapshot } from 'state/table-editor' import { createTabId, getTabsStore, makeTabPermanent } from 'state/tabs' import { cn, DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuTrigger, Tooltip, TooltipContent, TooltipTrigger, TreeViewItemVariant, } from 'ui' import { useProjectContext } from '../ProjectLayout/ProjectContext' export interface EntityListItemProps { id: number | string projectRef: string isLocked: boolean isActive?: boolean } const EntityListItem: ItemRenderer = ({ id, projectRef, item: entity, isLocked, isActive: _isActive, }) => { const { project } = useProjectContext() const snap = useTableEditorStateSnapshot() const { selectedSchema } = useQuerySchemaState() // For tabs preview flag logic const isTableEditorTabsEnabled = useIsTableEditorTabsEnabled() const tabId = createTabId(entity.type, { id: entity.id }) const tabStore = getTabsStore(projectRef) const isPreview = isTableEditorTabsEnabled ? tabStore.previewTabId === tabId : false const tabs = useSnapshot(tabStore) 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 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 }) makeTabPermanent(projectRef, tabId) }} > <> {isActive &&
} {formatTooltipText(entity.type)}
{entity.name}
{canEdit && ( { e.stopPropagation() copyToClipboard(entity.name) }} > Copy name {entity.type === ENTITY_TYPE.TABLE && ( <> { e.stopPropagation() snap.onEditTable() }} > Edit table { e.stopPropagation() snap.onDuplicateTable() }} > Duplicate table View policies Export data { e.stopPropagation() exportTableAsCSV() }} > Export table as CSV { e.stopPropagation() exportTableAsSQL() }} > Export table as SQL { e.stopPropagation() snap.onDeleteTable() }} > Delete table )} )} ) } const EntityTooltipTrigger = ({ entity, isActive, tableHasLints, viewHasLints, materializedViewHasLints, }: { entity: Entity isActive: boolean tableHasLints: boolean viewHasLints: boolean materializedViewHasLints: boolean }) => { let tooltipContent = '' switch (entity.type) { case ENTITY_TYPE.TABLE: if (tableHasLints) { tooltipContent = 'RLS disabled' } break case ENTITY_TYPE.VIEW: if (viewHasLints) { tooltipContent = 'Security definer view' } break case ENTITY_TYPE.MATERIALIZED_VIEW: if (materializedViewHasLints) { tooltipContent = 'Security definer view' } break case ENTITY_TYPE.FOREIGN_TABLE: tooltipContent = 'RLS is not enforced on foreign tables' break default: break } if (tooltipContent) { return ( {tooltipContent} ) } return null } export default EntityListItem