diff --git a/apps/studio/components/interfaces/Database/Replication/DestinationRow.test.tsx b/apps/studio/components/interfaces/Database/Replication/DestinationRow.test.tsx
index 24d0c73b802..78fab0b282e 100644
--- a/apps/studio/components/interfaces/Database/Replication/DestinationRow.test.tsx
+++ b/apps/studio/components/interfaces/Database/Replication/DestinationRow.test.tsx
@@ -4,6 +4,7 @@ import userEvent from '@testing-library/user-event'
import { platformComponents as components } from 'api-types'
import { mockAnimationsApi } from 'jsdom-testing-mocks'
import { HttpResponse } from 'msw'
+import { Table, TableBody } from 'ui'
import { describe, expect, test, vi } from 'vitest'
import { DestinationRow as DestinationRowComponent } from './DestinationRow'
@@ -68,12 +69,13 @@ const addDestinationMock = () =>
}),
})
-const addPipelinesMock = () =>
+const addPipelinesMock = (waitForResponse?: Promise) =>
addAPIMock({
method: 'get',
path: '/platform/replication/:ref/pipelines',
- response: () =>
- HttpResponse.json({
+ response: async () => {
+ await waitForResponse
+ return HttpResponse.json({
pipelines: [
{
id: PIPELINE_ID,
@@ -89,7 +91,8 @@ const addPipelinesMock = () =>
},
},
],
- }),
+ })
+ },
})
const addPipelineStatusMock = (statusName: ReplicationPipelineStatusResponse['status']['name']) =>
@@ -145,6 +148,38 @@ describe('DestinationRow', () => {
addVersionMock()
}
+ test('shows the destination before its pipeline details load', async () => {
+ let finishPipelineRequest = () => {}
+ const pendingPipeline = new Promise((resolve) => {
+ finishPipelineRequest = resolve
+ })
+ addSourcesMock()
+ addDestinationMock()
+ addPipelinesMock(pendingPipeline)
+ addPipelineStatusMock('started')
+ addReplicationStatusMock(0)
+ addVersionMock()
+
+ customRender(
+
+ )
+
+ const row = (await screen.findByText('My BigQuery Destination')).closest('tr')
+ expect(row).toBeInTheDocument()
+ expect(row).not.toHaveAttribute('tabindex')
+ expect(screen.queryByRole('button', { name: 'Pipeline options' })).not.toBeInTheDocument()
+
+ await act(async () => finishPipelineRequest())
+
+ expect(await screen.findByText('supabase_realtime')).toBeInTheDocument()
+ expect(row).toHaveAttribute('tabindex', '0')
+ expect(screen.getByRole('button', { name: 'Pipeline options' })).toBeInTheDocument()
+ })
+
test('waits for asynchronous shutdown before deleting the pipeline', async () => {
addAllMocks()
routerMock.setCurrentUrl('/project/default/database/pipelines')
diff --git a/apps/studio/components/interfaces/Database/Replication/DestinationRow.tsx b/apps/studio/components/interfaces/Database/Replication/DestinationRow.tsx
index 7766862c6da..d9c70607358 100644
--- a/apps/studio/components/interfaces/Database/Replication/DestinationRow.tsx
+++ b/apps/studio/components/interfaces/Database/Replication/DestinationRow.tsx
@@ -3,7 +3,7 @@ import { ChevronRight, Minus } from 'lucide-react'
import { useRouter } from 'next/router'
import { useState } from 'react'
import { toast } from 'sonner'
-import { TableCell, TableRow } from 'ui'
+import { cn, TableCell, TableRow } from 'ui'
import { ShimmeringLoader } from 'ui-patterns/ShimmeringLoader'
import { DeleteDestination } from './DeleteDestination'
@@ -50,9 +50,8 @@ export const DestinationRow = ({ destinationId }: DestinationRowProps) => {
error: pipelineError,
isPending: isPipelineLoading,
isError: isPipelineError,
- isSuccess: isPipelineSuccess,
} = pipelineFetcher
- const destinationName = destination?.name ?? ''
+ const destinationName = destination?.name ?? pipeline?.destination_name ?? ''
const {
error: pipelineStatusError,
@@ -134,8 +133,7 @@ export const DestinationRow = ({ destinationId }: DestinationRowProps) => {
}
}
- // Five distinct states, so early returns rather than a ternary chain. The row only renders once
- // a pipeline exists, so there is no "no pipeline" case to handle here.
+ // Five distinct states, so early returns rather than a ternary chain.
const renderLag = () => {
if (isReplicationStatusLoading) return
if (isInitialSyncRunning)
@@ -161,39 +159,41 @@ export const DestinationRow = ({ destinationId }: DestinationRowProps) => {
)}
- {isPipelineSuccess && pipeline && (
+ {(isPipelineLoading || pipeline) && (
{type ? : null}
- {isPipelineLoading ? (
+ {isPipelineLoading && !destinationName ? (
) : (
{destinationName || type}
-
- #{pipeline?.id}
- ·
- {type}
- {hasTableErrors && (
- <>
- ·
-
- {errorCount} table error{errorCount === 1 ? '' : 's'}
-
- >
- )}
-
+ {pipeline && (
+
+ #{pipeline.id}
+ ·
+ {type}
+ {hasTableErrors && (
+ <>
+ ·
+
+ {errorCount} table error{errorCount === 1 ? '' : 's'}
+
+ >
+ )}
+
+ )}
)}
@@ -232,32 +232,38 @@ export const DestinationRow = ({ destinationId }: DestinationRowProps) => {
-
event.stopPropagation()}
- onAuxClick={(event) => event.stopPropagation()}
- onKeyDown={(event) => event.stopPropagation()}
- >
- setShowDeleteDestinationForm(true)}
- hasUpdate={hasUpdate}
- onUpdateClick={() => setShowUpdateVersionModal(true)}
- />
-
-
-
- Go to pipeline details
-
+ {pipeline ? (
+ <>
+
event.stopPropagation()}
+ onAuxClick={(event) => event.stopPropagation()}
+ onKeyDown={(event) => event.stopPropagation()}
+ >
+ setShowDeleteDestinationForm(true)}
+ hasUpdate={hasUpdate}
+ onUpdateClick={() => setShowUpdateVersionModal(true)}
+ />
+
+
+
+ Go to pipeline details
+
+ >
+ ) : (
+
+ )}
diff --git a/apps/studio/components/interfaces/Database/Replication/Destinations.tsx b/apps/studio/components/interfaces/Database/Replication/Destinations.tsx
index a65f24d008b..2e21f98ddc3 100644
--- a/apps/studio/components/interfaces/Database/Replication/Destinations.tsx
+++ b/apps/studio/components/interfaces/Database/Replication/Destinations.tsx
@@ -22,7 +22,7 @@ import {
} from 'ui'
import { Input } from 'ui-patterns/DataInputs/Input'
import { EmptyStatePresentational } from 'ui-patterns/EmptyStatePresentational'
-import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader'
+import { GenericTableLoader } from 'ui-patterns/ShimmeringLoader'
import { DestinationPanel } from './DestinationPanel/DestinationPanel'
import { DestinationType } from './DestinationPanel/DestinationPanel.types'
@@ -363,7 +363,9 @@ export const Destinations = () => {
)}
- {isDestinationsLoading && }
+ {isDestinationsLoading && (
+
+ )}
{!isDestinationsLoading && hasDestinations && (
diff --git a/apps/studio/components/interfaces/Database/Replication/PipelineStatePill.tsx b/apps/studio/components/interfaces/Database/Replication/PipelineStatePill.tsx
index 86f5de20ae2..b5245b3ea66 100644
--- a/apps/studio/components/interfaces/Database/Replication/PipelineStatePill.tsx
+++ b/apps/studio/components/interfaces/Database/Replication/PipelineStatePill.tsx
@@ -81,7 +81,7 @@ export const PipelineStatePill = ({
{shouldShowError ? 'Unknown' : label}
-
+
{tooltipMessage}
{isError && isRequestPending && ` Unable to refresh status: ${error?.message}.`}
diff --git a/apps/studio/components/interfaces/Database/Replication/ReplicationPipelineLayout.tsx b/apps/studio/components/interfaces/Database/Replication/ReplicationPipelineLayout.tsx
index efc35800f6c..eea85b96784 100644
--- a/apps/studio/components/interfaces/Database/Replication/ReplicationPipelineLayout.tsx
+++ b/apps/studio/components/interfaces/Database/Replication/ReplicationPipelineLayout.tsx
@@ -244,7 +244,7 @@ export const ReplicationPipelineLayout = ({ children }: PropsWithChildren) => {
-
+
{isPipelineIdentityLoading ? (
diff --git a/apps/studio/components/interfaces/Database/Replication/RowMenu.tsx b/apps/studio/components/interfaces/Database/Replication/RowMenu.tsx
index 1246f26ad9f..2629cb1e7fc 100644
--- a/apps/studio/components/interfaces/Database/Replication/RowMenu.tsx
+++ b/apps/studio/components/interfaces/Database/Replication/RowMenu.tsx
@@ -166,7 +166,7 @@ export const RowMenu = ({
/>
{hasUpdate && (
)}
@@ -184,12 +184,16 @@ export const RowMenu = ({
{hasUpdate && (
<>
onUpdateClick?.()}
disabled={isPipelineRequestPending}
>
Update available
+
>
diff --git a/apps/studio/components/interfaces/Database/Replication/UpdateVersionModal.tsx b/apps/studio/components/interfaces/Database/Replication/UpdateVersionModal.tsx
index 2aa7531d2e8..5629d7239fc 100644
--- a/apps/studio/components/interfaces/Database/Replication/UpdateVersionModal.tsx
+++ b/apps/studio/components/interfaces/Database/Replication/UpdateVersionModal.tsx
@@ -83,24 +83,20 @@ export const UpdateVersionModal = ({ visible, pipeline, onClose }: UpdateVersion
? 'A newer pipeline version is available with improvements and bug fixes. The pipeline will restart and continue from where it left off.'
: 'A newer pipeline version is available with improvements and bug fixes.'}
-
-
-
-
- Current
-
- {isLoadingVersion ? 'Loading…' : (currentVersionName ?? 'Unknown')}
-
-
-
- New
-
- {isLoadingVersion ? 'Loading…' : (newVersionName ?? 'Unknown')}
-
-
-
-
-
+
+
+
Current
+
+ {isLoadingVersion ? 'Loading…' : (currentVersionName ?? 'Unknown')}
+
+
+
+
New
+
+ {isLoadingVersion ? 'Loading…' : (newVersionName ?? 'Unknown')}
+
+
+
)
diff --git a/apps/studio/pages/project/[ref]/database/pipelines/index.tsx b/apps/studio/pages/project/[ref]/database/pipelines/index.tsx
index 6c030934ed4..fa6223730fb 100644
--- a/apps/studio/pages/project/[ref]/database/pipelines/index.tsx
+++ b/apps/studio/pages/project/[ref]/database/pipelines/index.tsx
@@ -1,3 +1,4 @@
+import { useParams } from 'common'
import { MessageSquare } from 'lucide-react'
import { Button } from 'ui'
import { PageContainer } from 'ui-patterns/PageContainer'
@@ -10,7 +11,7 @@ import {
PageHeaderTitle,
} from 'ui-patterns/PageHeader'
import { PageSection, PageSectionContent } from 'ui-patterns/PageSection'
-import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader'
+import { GenericTableLoader, ShimmeringLoader } from 'ui-patterns/ShimmeringLoader'
import { Destinations } from '@/components/interfaces/Database/Replication/Destinations'
import { PIPELINES_FEEDBACK_URL } from '@/components/interfaces/Database/Replication/Replication.constants'
@@ -22,17 +23,16 @@ import { DocsButton } from '@/components/ui/DocsButton'
import { UnknownInterface } from '@/components/ui/UnknownInterface'
import { useHighAvailability } from '@/hooks/misc/useHighAvailability'
import { useIsFeatureEnabled } from '@/hooks/misc/useIsFeatureEnabled'
-import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
import { DOCS_URL } from '@/lib/constants'
import type { NextPageWithLayout } from '@/types'
const DatabasePipelinesPage: NextPageWithLayout = () => {
- const { data: selectedProject, isPending } = useSelectedProjectQuery()
- const { isHighAvailability } = useHighAvailability()
+ const { ref: projectRef } = useParams()
+ const { isHighAvailability, isPending } = useHighAvailability()
const showPgReplicate = useIsFeatureEnabled('database:replication')
if (!showPgReplicate) {
- return
+ return
}
if (isHighAvailability) {
@@ -81,16 +81,41 @@ const DatabasePipelinesPage: NextPageWithLayout = () => {
- {isPending ? (
-
- ) : (
-
-
-
-
-
-
- )}
+
+
+ {isPending ? (
+ <>
+
+ Loading pipelines
+
+
+
+
+
+
+ >
+ ) : (
+ <>
+
+
+ >
+ )}
+
+
>
)