| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546 |
- 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<string, string> = {
- 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 (
- <>
- <TableRow>
- <TableCell className="min-w-[120px]">
- <div className="flex items-center gap-x-3">
- <div className="w-5 flex justify-center items-center">
- <Table2 size={16} />
- </div>
- <p>{table.name}</p>
- </div>
- </TableCell>
- {!HIDE_REPLICATION_USER_FLOW && !!hasReplication && (
- <TableCell colSpan={table.isConnected ? 1 : 2} className="min-w-[150px]">
- <div className="flex items-center">
- <Tooltip>
- <TooltipTrigger asChild>
- <div className="flex items-center gap-x-2">
- {isLoadingPipelineStatus ? (
- <Loader2 size={12} className="animate-spin text-foreground-lighter" />
- ) : isPipelineRunning ? (
- <DotPing
- animate={isReplicating}
- variant={isReplicating ? 'primary' : 'default'}
- />
- ) : null}
- <span className="text-foreground-lighter capitalize">
- {replicationStatusLabel}
- </span>
- </div>
- </TooltipTrigger>
- {isPipelineRunning && (
- <TooltipContent side="bottom">
- {isReplicating
- ? `Table data is currently replicating${!!inferredPostgresTable ? ` from ${inferredPostgresTable.schema}.${inferredPostgresTable.name}` : ''}`
- : !isTableUnderReplicationPublication
- ? 'Replication is disabled for this table'
- : undefined}
- </TooltipContent>
- )}
- </Tooltip>
- </div>
- </TableCell>
- )}
- {!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
- <TableCell className="text-right flex flex-row items-center gap-x-2 justify-end">
- <>
- <DropdownMenu>
- <DropdownMenuTrigger asChild>
- <Button type="default" className="w-7" icon={<MoreVertical />} />
- </DropdownMenuTrigger>
- <DropdownMenuContent side="bottom" align="end" className="w-fit min-w-[180px]">
- {!!publication && (
- <>
- {!!inferredPostgresTable && (
- <DropdownMenuItem asChild className="flex items-center gap-x-2">
- <Link
- href={`/project/${projectRef}/database/replication/${pipeline?.id}?search=${inferredPostgresTable.schema}.${inferredPostgresTable.name}`}
- >
- <Eye size={12} className="text-foreground-lighter" />
- <p>View replication</p>
- </Link>
- </DropdownMenuItem>
- )}
- {isTableUnderReplicationPublication ? (
- <DropdownMenuItem
- className="flex items-center gap-x-2"
- onClick={() => setShowStopReplicationModal(true)}
- >
- <Pause size={12} className="text-foreground-lighter" />
- <p>Disable replication</p>
- </DropdownMenuItem>
- ) : (
- <DropdownMenuItem
- className="flex items-center gap-x-2"
- onClick={() => setShowStartReplicationModal(true)}
- >
- <Play size={12} className="text-foreground-lighter" />
- <p>Enable replication</p>
- </DropdownMenuItem>
- )}
- <DropdownMenuSeparator />
- </>
- )}
- <DropdownMenuItemTooltip
- disabled={isReplicating}
- className="flex items-center gap-x-2"
- onClick={() => setShowRemoveTableModal(true)}
- tooltip={{
- content: {
- side: 'left',
- text: 'Stop replication on this table before removing',
- },
- }}
- >
- <Trash size={12} className="text-foreground-lighter" />
- <p>Delete table</p>
- </DropdownMenuItemTooltip>
- </DropdownMenuContent>
- </DropdownMenu>
- </>
- </TableCell>
- ) : (
- // [Joshen] These are for purely Analytics Bucket context
- <TableCell className="text-right flex flex-row items-center gap-x-2 justify-end">
- <DropdownMenu>
- <DropdownMenuTrigger asChild>
- <Button
- loading={isDeletingNamespaceTable}
- type="default"
- className="w-7"
- icon={<MoreVertical />}
- />
- </DropdownMenuTrigger>
- <DropdownMenuContent align="end" className="w-fit min-w-[180px]">
- <DropdownMenuItem
- className="flex items-center gap-x-2"
- onClick={() => setShowRemoveTableModal(true)}
- >
- <Trash size={12} className="text-foreground-lighter" />
- <p>Delete table</p>
- </DropdownMenuItem>
- </DropdownMenuContent>
- </DropdownMenu>
- </TableCell>
- )}
- </TableRow>
- {/* [Joshen] Render each foreign table associated to the namespace table as its own row */}
- {connectedForeignTables?.map((x) => (
- <TableRow key={x.id}>
- <TableCell className="pl-6">
- <div className="flex items-center gap-x-2 rounded-sm">
- <div className="w-4 h-5 rounded-bl-lg border-l-2 border-b-2 border-control -translate-y-2" />
- <div
- className={cn(
- 'flex items-center justify-center text-xs h-4 w-4 rounded-[2px] font-bold',
- 'text-warning-600/80 dark:text-yellow-900 bg-yellow-500'
- )}
- >
- F
- </div>
- <p>
- {x.schema}.{x.name}
- </p>
- </div>
- </TableCell>
- <TableCell className="flex flex-row justify-end gap-x-2">
- <InsertDataDialog table={table.name} fdwTable={x} />
- <DropdownMenu>
- <DropdownMenuTrigger asChild>
- <Button type="default" className="w-7" icon={<MoreVertical />} />
- </DropdownMenuTrigger>
- <DropdownMenuContent className="w-fit min-w-[180px]" align="end">
- <DropdownMenuItem asChild className="flex items-center gap-x-2">
- <Link
- href={`/project/${projectRef}/sql/new?content=${encodeURIComponent(`select * from ${schema}.${table.name};`)}`}
- >
- <SqlEditor size={12} className="text-foreground-lighter" />
- <p>Query in SQL Editor</p>
- </Link>
- </DropdownMenuItem>
- <DropdownMenuItem asChild className="flex items-center gap-x-2">
- <Link href={`/project/${projectRef}/editor/${x.id}?schema=${x.schema}`}>
- <TableEditor size={12} className="text-foreground-lighter" />
- <p>View in Table Editor</p>
- </Link>
- </DropdownMenuItem>
- </DropdownMenuContent>
- </DropdownMenu>
- </TableCell>
- </TableRow>
- ))}
- {/* [Joshen] If the iceberg table doesn't have a corresponding namespace table, but the namespace itself already has some tables connected */}
- {connectedForeignTablesInNamespace.length > 0 && connectedForeignTables.length === 0 && (
- <TableRow>
- <TableCell className="pl-6">
- <Tooltip>
- <TooltipTrigger>
- <div className="flex items-center gap-x-2 rounded-sm">
- <div className="w-4 h-4 rounded-bl-lg border-l-2 border-b-2 border-control -translate-y-1.5" />
- <div
- className={cn(
- 'flex items-center justify-center text-xs h-4 w-4 rounded-[2px]',
- 'font-bold border border-dashed border-control text-foreground-lighter'
- )}
- >
- ?
- </div>
- <p className="text-foreground-lighter">No matching foreign table in schema</p>
- </div>
- </TooltipTrigger>
- <TooltipContent side="right">
- Update the schema tables if you'd like to query this table from Postgres
- </TooltipContent>
- </Tooltip>
- </TableCell>
- <TableCell className="flex flex-row justify-end"></TableCell>
- </TableRow>
- )}
- <ConfirmationModal
- size="medium"
- variant="warning"
- visible={showStopReplicationModal}
- loading={isUpdatingReplication}
- title="Confirm to disable replication for table"
- confirmLabel="Disable replication"
- onCancel={() => setShowStopReplicationModal(false)}
- onConfirm={() => onConfirmStopReplication()}
- >
- <p className="text-sm text-foreground-light">
- Data within the "{table.name}" table will stop replicating. However do note that,
- re-enabling replication on this table will clear and re-sync all data in it. Are you sure?
- </p>
- </ConfirmationModal>
- <ConfirmationModal
- size="medium"
- variant="warning"
- visible={showStartReplicationModal}
- loading={isUpdatingReplication}
- title="Enable replication for table"
- confirmLabel="Enable replication"
- onCancel={() => setShowStartReplicationModal(false)}
- onConfirm={() => onConfirmStartReplication()}
- >
- <p className="text-sm text-foreground-light">
- Re-enabling replication on the "{table.name}" table will clear and re-sync all data in it.
- Are you sure?
- </p>
- </ConfirmationModal>
- <ConfirmationModal
- size="medium"
- variant="warning"
- visible={showRemoveTableModal}
- loading={isRemovingTable}
- title={`Confirm to delete table "${table.name}"`}
- description="This action cannot be undone."
- confirmLabel="Delete table"
- onCancel={() => setShowRemoveTableModal(false)}
- onConfirm={() => {
- if (HIDE_REPLICATION_USER_FLOW) {
- onConfirmRemoveNamespaceTable()
- } else {
- onConfirmRemoveTable()
- }
- }}
- >
- <p className="text-sm text-foreground-light">
- Data from this Iceberg table will be permanently lost. Are you sure?
- </p>
- </ConfirmationModal>
- </>
- )
- }
|