import { useParams } from 'common' import { SqlEditor, TableEditor } from 'icons' import { uniq } from 'lodash' import { Eye, Loader2, MoreVertical, Pause, Play, Table2, Trash } from 'lucide-react' import Link from 'next/link' import { useMemo, useState } from 'react' import { toast } from 'sonner' import { Button, cn, DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger, TableCell, TableRow, Tooltip, TooltipContent, TooltipTrigger, } from 'ui' import { ConfirmationModal } from 'ui-patterns/Dialogs/ConfirmationModal' import { HIDE_REPLICATION_USER_FLOW } from '../AnalyticsBucketDetails.constants' import { getAnalyticsBucketFDWServerName, getNamespaceTableNameFromPostgresTableName, } from '../AnalyticsBucketDetails.utils' import { useAnalyticsBucketAssociatedEntities } from '../useAnalyticsBucketAssociatedEntities' import { useAnalyticsBucketWrapperInstance } from '../useAnalyticsBucketWrapperInstance' import { InsertDataDialog } from './InsertDataDialog' import { inferPostgresTableFromNamespaceTable } from './NamespaceWithTables.utils' import { convertKVStringArrayToJson, formatWrapperTables, } from '@/components/interfaces/Integrations/Wrappers/Wrappers.utils' import { getDecryptedParameters } from '@/components/interfaces/Storage/Storage.utils' import { DotPing } from '@/components/ui/DotPing' import { DropdownMenuItemTooltip } from '@/components/ui/DropdownMenuItemTooltip' import { useFDWDropForeignTableMutation } from '@/data/fdw/fdw-drop-foreign-table-mutation' import { useFDWUpdateMutation } from '@/data/fdw/fdw-update-mutation' import { useReplicationPipelineStatusQuery } from '@/data/replication/pipeline-status-query' import { useUpdatePublicationMutation } from '@/data/replication/publication-update-mutation' import { useStartPipelineMutation } from '@/data/replication/start-pipeline-mutation' import { useReplicationTablesQuery } from '@/data/replication/tables-query' import { useIcebergNamespaceTableDeleteMutation } from '@/data/storage/iceberg-namespace-table-delete-mutation' import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject' interface TableRowComponentProps { table: { id: number; name: string; isConnected: boolean } schema: string namespace: string } export const TableRowComponent = ({ table, schema, namespace }: TableRowComponentProps) => { const { ref: projectRef, bucketId } = useParams() const { data: project } = useSelectedProjectQuery() const [showStopReplicationModal, setShowStopReplicationModal] = useState(false) const [showStartReplicationModal, setShowStartReplicationModal] = useState(false) const [showRemoveTableModal, setShowRemoveTableModal] = useState(false) const [isUpdatingReplication, setIsUpdatingReplication] = useState(false) const [isRemovingTable, setIsRemovingTable] = useState(false) const { sourceId, publication, pipeline, icebergWrapper } = useAnalyticsBucketAssociatedEntities({ projectRef, bucketId, }) const { data, isPending: isLoadingPipelineStatus } = useReplicationPipelineStatusQuery({ projectRef, pipelineId: pipeline?.id, }) const pipelineStatus = data?.status.name const { data: tables } = useReplicationTablesQuery({ projectRef, sourceId }) const { data: wrapperInstance, meta: wrapperMeta } = useAnalyticsBucketWrapperInstance({ bucketId: bucketId, }) const { mutateAsync: updateFDW } = useFDWUpdateMutation() const { mutateAsync: dropForeignTable } = useFDWDropForeignTableMutation() const { mutateAsync: deleteNamespaceTable, isPending: isDeletingNamespaceTable } = useIcebergNamespaceTableDeleteMutation({ onError: () => {} }) const { mutateAsync: updatePublication } = useUpdatePublicationMutation() const { mutateAsync: startPipeline } = useStartPipelineMutation() const inferredPostgresTable = inferPostgresTableFromNamespaceTable({ publication, tableName: table.name, }) const isTableUnderReplicationPublication = !!inferredPostgresTable const hasReplication = !!pipeline && !!publication const isPipelineRunning = pipelineStatus === 'started' const isReplicating = isTableUnderReplicationPublication && isPipelineRunning // [Joshen] Considers both the replication pipeline status + if the table is in the replication publication const replicationStatusLabel = useMemo(() => { if (hasReplication) { if (isLoadingPipelineStatus) { return 'Checking' } else if (!isPipelineRunning) { return '-' } else if (isTableUnderReplicationPublication) { return 'Running' } else { return 'Disabled' } } }, [ hasReplication, isLoadingPipelineStatus, isPipelineRunning, isTableUnderReplicationPublication, ]) const onConfirmStopReplication = async () => { if (!projectRef) return console.error('Project ref is required') if (!bucketId) return console.error('Bucket ID is required') if (!sourceId) return toast.error('Source ID is required') if (!publication) return toast.error('Unable to find existing publication') if (!pipeline) return toast.error('Unable to find existing pipeline') try { setIsUpdatingReplication(true) // [Joshen ALPHA] Assumption here is that all the namespace tables have _changelog as suffix // May need to update if that assumption falls short (e.g for those dealing with iceberg APIs directly) const updatedTables = publication.tables.filter( (x) => table.name !== getNamespaceTableNameFromPostgresTableName(x) ) await updatePublication({ projectRef, sourceId, publicationName: publication.name, tables: updatedTables, }) await startPipeline({ projectRef, pipelineId: pipeline.id }) setShowStopReplicationModal(false) toast.success('Successfully disabled replication for table! Pipeline is being restarted.') } catch (error: any) { toast.error(`Failed to disable replication for table: ${error.message}`) } finally { setIsUpdatingReplication(false) } } const onConfirmStartReplication = async () => { if (!projectRef) return console.error('Project ref is required') if (!bucketId) return console.error('Bucket ID is required') if (!sourceId) return toast.error('Source ID is required') if (!publication) return toast.error('Unable to find existing publication') if (!pipeline) return toast.error('Unable to find existing pipeline') // [Joshen ALPHA] This has potential to be flaky - we should see how we can get the table name and schema better const pgTable = tables?.find( (t) => getNamespaceTableNameFromPostgresTableName(t) === table.name ) if (!pgTable) return toast.error('Unable to find corresponding Postgres table') try { setIsUpdatingReplication(true) const updatedTables = publication.tables.concat([ { schema: pgTable.schema, name: pgTable.name }, ]) await updatePublication({ projectRef, sourceId, publicationName: publication.name, tables: updatedTables, }) await startPipeline({ projectRef, pipelineId: pipeline.id }) setShowStartReplicationModal(false) toast.success('Successfully enabled replication for table! Pipeline is being restarted.') } catch (error: any) { toast.error(`Failed to enable replication for table: ${error.message}`) } finally { setIsUpdatingReplication(false) } } // [Joshen] For ETL replication context const onConfirmRemoveTable = async () => { if (!bucketId) return console.error('Bucket ID is required') if (!wrapperInstance || !wrapperMeta) return toast.error('Unable to find wrapper') try { setIsRemovingTable(true) // [Joshen] Update FDW instance only if table is in FDW instance's tables // e.g for a namespace table that was added outside of the dashboard, it wouldn't be const isTableInWrapperInstance = wrapperInstance.tables.some((x) => x.name === table.name) if (isTableInWrapperInstance) { const serverName = getAnalyticsBucketFDWServerName(bucketId) const serverOptions = await getDecryptedParameters({ ref: project?.ref, connectionString: project?.connectionString ?? undefined, wrapper: wrapperInstance, wrapperMeta, }) const formValues: Record = { wrapper_name: wrapperInstance.name, server_name: wrapperInstance.server_name, ...serverOptions, } const targetSchemas = (formValues['briven_target_schema'] || '') .split(',') .map((s) => s.trim()) const wrapperTables = formatWrapperTables(wrapperInstance, wrapperMeta).filter( (x) => x.table_name !== table.name ) // [Joshen] Once Ivan's PR goes through, swap these out to just use useFDWDropForeignTableMutation // https://github.com/briven/briven/pull/40206 await updateFDW({ projectRef: project?.ref, connectionString: project?.connectionString, wrapper: wrapperInstance, wrapperMeta, formState: { ...formValues, server_name: serverName, briven_target_schema: uniq([...targetSchemas]) .filter(Boolean) .join(','), }, tables: wrapperTables, }) } const wrapperValues = convertKVStringArrayToJson(wrapperInstance?.server_options ?? []) await deleteNamespaceTable({ projectRef, warehouse: wrapperValues.warehouse, namespace: namespace, table: table.name, }) toast.success('Successfully removed table!') setShowRemoveTableModal(false) } catch (error: any) { toast.error(`Failed to remove table: ${error.message}`) } finally { setIsRemovingTable(false) } } const connectedForeignTablesInNamespace = (icebergWrapper?.tables ?? []).filter((x) => x.options[0].includes(`table=${namespace}.`) ) const connectedForeignTables = (icebergWrapper?.tables ?? []).filter( (x) => x.options[0] === `table=${namespace}.${table.name}` ) // [Joshen] For purely Analytics Bucket context const onConfirmRemoveNamespaceTable = async () => { try { setIsRemovingTable(true) const wrapperValues = convertKVStringArrayToJson(wrapperInstance?.server_options ?? []) await deleteNamespaceTable({ projectRef, warehouse: wrapperValues.warehouse, namespace: namespace, table: table.name, }) await Promise.all( connectedForeignTables.map((x) => dropForeignTable({ projectRef, connectionString: project?.connectionString, schemaName: x.schema, tableName: x.name, }) ) ) toast.success(`Successfully removed table "${table.name}"!`) } catch (error: any) { toast.error(`Failed to remove table: ${error.message}`) } finally { setIsRemovingTable(false) } } return ( <>

{table.name}

{!HIDE_REPLICATION_USER_FLOW && !!hasReplication && (
{isLoadingPipelineStatus ? ( ) : isPipelineRunning ? ( ) : null} {replicationStatusLabel}
{isPipelineRunning && ( {isReplicating ? `Table data is currently replicating${!!inferredPostgresTable ? ` from ${inferredPostgresTable.schema}.${inferredPostgresTable.name}` : ''}` : !isTableUnderReplicationPublication ? 'Replication is disabled for this table' : undefined} )}
)} {!HIDE_REPLICATION_USER_FLOW && table.isConnected ? ( // [Joshen] These are if there's the context of replication which we're currently not doing // May need to clean up if we decided to move forward de-coupling replication and Analytics Buckets <>