From ea909e998bd2e4110f985ad3fbdb7c9e6a96e41d Mon Sep 17 00:00:00 2001 From: Joshen Lim Date: Fri, 19 Jun 2026 17:27:44 +0800 Subject: [PATCH] Local only: skip loading destinations if ETL API is not set up (#47031) ## Context > [!IMPORTANT] > Changes in this PR only apply to the local environment - there should not be any changes to staging (nor production) Given that read replicas currently sit under database replication, the UI currently waits for replication destinations to load before rendering the page. However for local development, setting up of the ETL API isn't necessary nor applicable for everyone so this indirectly adds friction if we just want to work with read replicas. ## Changes involved - Opting to skip retrying fetching ETL related requests if the error returned is "replication API URL is not configured" - This is indicative that the local platform isn't set up for ETL yet - ^ Database replication page will hence not wait for ETL requests to succeed before finally rendering the UI - Node diagram will also then render properly (just read replicas) - Add a small admonition to visualize this image ## To test - Will only be able to test locally - basically just head over to the database replication page (unless you somehow already have ETL API set up locally) - But can also verify that there's no changes on staging preview ## Summary by CodeRabbit * **Bug Fixes** * Improved error messaging when ETL is not configured in local development environments * Enhanced error handling for replication API failures with better non-retryable error detection * **Improvements** * Refined replication diagram rendering based on destination setup state * Updated dropdown menu interactions for read replica management --- .../Database/Replication/Destinations.tsx | 14 ++++++++++++-- .../Replication/ReplicationDiagram/index.tsx | 18 +++++++++++++++--- apps/studio/data/replication/utils.ts | 18 +++++++++++++++++- 3 files changed, 44 insertions(+), 6 deletions(-) diff --git a/apps/studio/components/interfaces/Database/Replication/Destinations.tsx b/apps/studio/components/interfaces/Database/Replication/Destinations.tsx index d9e58ccf4d8..462cb4c7125 100644 --- a/apps/studio/components/interfaces/Database/Replication/Destinations.tsx +++ b/apps/studio/components/interfaces/Database/Replication/Destinations.tsx @@ -19,7 +19,7 @@ import { TableHeader, TableRow, } from 'ui' -import { GenericSkeletonLoader } from 'ui-patterns' +import { Admonition, GenericSkeletonLoader } from 'ui-patterns' import { Input } from 'ui-patterns/DataInputs/Input' import { REPLICA_STATUS } from '../../Settings/Infrastructure/InfrastructureConfiguration/InstanceConfiguration.constants' @@ -43,6 +43,7 @@ import { replicationKeys } from '@/data/replication/keys' import { fetchReplicationPipelineVersion } from '@/data/replication/pipeline-version-query' import { useReplicationPipelinesQuery } from '@/data/replication/pipelines-query' import { useReplicationSourcesQuery } from '@/data/replication/sources-query' +import { checkLocalETLNotSetUp } from '@/data/replication/utils' import { useIsFeatureEnabled } from '@/hooks/misc/useIsFeatureEnabled' import { DOCS_URL } from '@/lib/constants' import { SHORTCUT_IDS } from '@/state/shortcuts/registry' @@ -144,7 +145,9 @@ export const Destinations = () => { pipelines.length === 0 const isLoading = isDestinationsLoading || isDatabasesLoading - const hasErrorsFetchingData = isDestinationsError || isDatabasesError + + const isLocalETLNotSetUp = checkLocalETLNotSetUp(destinationsError) + const hasErrorsFetchingData = (!isLocalETLNotSetUp && isDestinationsError) || isDatabasesError const openDestinationPanel = () => { if (!newDestinationDefaultType) return @@ -270,6 +273,13 @@ export const Destinations = () => { /> )} + {isLocalETLNotSetUp && ( + + )} + {isLoading ? ( ) : hasReplicas || hasDestinations ? ( diff --git a/apps/studio/components/interfaces/Database/Replication/ReplicationDiagram/index.tsx b/apps/studio/components/interfaces/Database/Replication/ReplicationDiagram/index.tsx index f90e8741bff..83a9555cee7 100644 --- a/apps/studio/components/interfaces/Database/Replication/ReplicationDiagram/index.tsx +++ b/apps/studio/components/interfaces/Database/Replication/ReplicationDiagram/index.tsx @@ -12,6 +12,7 @@ import { timeout } from '@/lib/helpers' import '@xyflow/react/dist/style.css' import { SmoothstepEdge } from './Edges' +import { checkLocalETLNotSetUp } from '@/data/replication/utils' export const ReplicationDiagram = () => { return ( @@ -42,10 +43,17 @@ const ReplicationDiagramContent = () => { [databases, projectRef] ) - const { data, isSuccess: isSuccessDestinations } = useReplicationDestinationsQuery({ + const { + data, + error: destinationsError, + isSuccess: isSuccessDestinations, + isError: isErrorDestinations, + } = useReplicationDestinationsQuery({ projectRef, }) const destinations = useMemo(() => data?.destinations ?? [], [data]) + const isLocalETLNotSetUp = checkLocalETLNotSetUp(destinationsError) + const skipRenderingDestinations = isErrorDestinations && isLocalETLNotSetUp const nodes = useMemo(() => { return [ @@ -103,10 +111,14 @@ const ReplicationDiagramContent = () => { } useEffect(() => { - if (nodes.length > 0 && isSuccessDestinations && isSuccessReplicas) { + if ( + nodes.length > 0 && + (isSuccessDestinations || skipRenderingDestinations) && + isSuccessReplicas + ) { setReactFlow() } - }, [nodes, isSuccessDestinations, isSuccessReplicas]) + }, [nodes, isSuccessDestinations, skipRenderingDestinations, isSuccessReplicas]) return (
diff --git a/apps/studio/data/replication/utils.ts b/apps/studio/data/replication/utils.ts index bf493328fab..31289a682e7 100644 --- a/apps/studio/data/replication/utils.ts +++ b/apps/studio/data/replication/utils.ts @@ -1,6 +1,20 @@ import { MAX_RETRY_FAILURE_COUNT } from '@/data/query-client' import { ResponseError } from '@/types' +const isLocal = + process.env.NEXT_PUBLIC_ENVIRONMENT !== 'prod' && + process.env.NEXT_PUBLIC_ENVIRONMENT !== 'staging' + +export const checkLocalETLNotSetUp = (error: ResponseError | null) => { + if (error === null) return false + + const isETLAPINotRunning = + error.code === undefined && error.message.includes('API error happened') + const isETLNotSetUp = + error.code === 503 && error.message.includes('replication API URL is not configured') + return isLocal && (isETLAPINotRunning || isETLNotSetUp) +} + export const checkReplicationFeatureFlagRetry = ( failureCount: number, error: ResponseError @@ -10,7 +24,9 @@ export const checkReplicationFeatureFlagRetry = ( error.code === 503 && error.message.includes('feature flag is required') - if (isFeatureFlagRequiredError) { + const isLocalETLNotSetUp = checkLocalETLNotSetUp(error) + + if (isFeatureFlagRequiredError || isLocalETLNotSetUp) { return false }