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)} - /> -
- - + {pipeline ? ( + <> +
event.stopPropagation()} + onAuxClick={(event) => event.stopPropagation()} + onKeyDown={(event) => event.stopPropagation()} + > + setShowDeleteDestinationForm(true)} + hasUpdate={hasUpdate} + onUpdateClick={() => setShowUpdateVersionModal(true)} + /> +
+ + + + ) : ( + + )}
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 +

+ + + + ) : ( + <> + + + + )} +
+
)