diff --git a/apps/studio/components/interfaces/Database/Replication/ReplicationPipelineStatus/PipelineDetailItem.tsx b/apps/studio/components/interfaces/Database/Replication/ReplicationPipelineStatus/PipelineDetailItem.tsx
new file mode 100644
index 00000000000..a831db7e4dd
--- /dev/null
+++ b/apps/studio/components/interfaces/Database/Replication/ReplicationPipelineStatus/PipelineDetailItem.tsx
@@ -0,0 +1,39 @@
+import { type ReactNode } from 'react'
+import { InfoTooltip } from 'ui-patterns/info-tooltip'
+
+import { DetailSubtext } from '../DetailSubtext'
+
+interface PipelineDetailItemProps {
+ label: string
+ /** A fixed explanation of what this field is. Never the current value's meaning. */
+ tooltip?: ReactNode
+ /** Explains what the current value means, when the value alone isn't enough. */
+ description?: ReactNode
+ children: ReactNode
+}
+
+/**
+ * One label and value inside a pipeline detail card. Shared so the configuration and health
+ * cards read as the same grid rather than two different treatments of the same idea.
+ */
+export const PipelineDetailItem = ({
+ label,
+ tooltip,
+ description,
+ children,
+}: PipelineDetailItemProps) => (
+
-
-
- During initial sync, tables can copy and stream independently before
- reconciling with the overall pipeline.
-
-
-
-
- {tablesWithLag.map((table) => (
-
-
-
- ))}
-
-
-
- >
- )}
-
+
)}
{!isPipelineLoading && !isStatusLoading && hasTableData && (
diff --git a/apps/studio/components/interfaces/Database/Replication/ReplicationPipelineStatus/ReplicationPipelineStatus.utils.tsx b/apps/studio/components/interfaces/Database/Replication/ReplicationPipelineStatus/ReplicationPipelineStatus.utils.tsx
index 928bb3dcd55..515273449fe 100644
--- a/apps/studio/components/interfaces/Database/Replication/ReplicationPipelineStatus/ReplicationPipelineStatus.utils.tsx
+++ b/apps/studio/components/interfaces/Database/Replication/ReplicationPipelineStatus/ReplicationPipelineStatus.utils.tsx
@@ -167,8 +167,7 @@ export const WAL_STATUS_META: Record = {
label: 'Reserved',
variant: 'success',
severity: 'normal',
- description:
- "Healthy. Your database is keeping the WAL files this pipeline's replication slot needs, and they are within the normal WAL size limit.",
+ description: 'Postgres will keep the WAL for every change until this pipeline sends it.',
tableDescription:
"Healthy. Your database is keeping the WAL files this table's replication slot needs, and they are within the normal WAL size limit.",
},
@@ -177,7 +176,7 @@ export const WAL_STATUS_META: Record = {
variant: 'warning',
severity: 'normal',
description:
- "Healthy, but growing. This pipeline's replication slot is holding on to more WAL than usual, but your database is still keeping everything it needs.",
+ 'The pipeline is behind. Postgres is retaining more WAL than usual, but nothing is discarded yet.',
tableDescription:
"Healthy, but growing. This table's replication slot is holding on to more WAL than usual, but your database is still keeping everything it needs.",
},
@@ -185,8 +184,7 @@ export const WAL_STATUS_META: Record = {
label: 'Unreserved',
variant: 'warning',
severity: 'warning',
- description:
- "At risk. Your database is no longer reserving all WAL files this pipeline's replication slot needs. If the pipeline does not catch up soon, those files may be removed.",
+ description: 'Postgres may discard WAL this pipeline has not sent yet.',
tableDescription:
"At risk. Your database is no longer reserving all WAL files this table's replication slot needs. If the pipeline does not catch up soon, those files may be removed.",
},
@@ -195,7 +193,7 @@ export const WAL_STATUS_META: Record = {
variant: 'destructive',
severity: 'critical',
description:
- "Broken. Some WAL files this pipeline's replication slot needs have already been removed. The pipeline can no longer continue from this slot. You can recreate a new pipeline, or set the invalidation behavior to recreate and restart the pipeline.",
+ 'Postgres already discarded WAL this pipeline needed. Replication cannot continue from here.',
tableDescription:
"Broken. Some WAL files this table's replication slot needs have already been removed. The pipeline can no longer continue from this slot. You can recreate a new pipeline, or set the invalidation behavior to recreate and restart the pipeline.",
},
@@ -203,8 +201,7 @@ export const WAL_STATUS_META: Record = {
label: 'Unknown',
variant: 'default',
severity: 'normal',
- description:
- "Unknown. Your database reported an unknown state for this pipeline's replication slot.",
+ description: 'Postgres did not report a recognized status for this pipeline’s slot.',
tableDescription:
"Unknown. Your database reported an unknown state for this table's replication slot.",
},
diff --git a/apps/studio/components/interfaces/Database/Replication/ReplicationPipelineStatus/SlotLagMetrics.test.tsx b/apps/studio/components/interfaces/Database/Replication/ReplicationPipelineStatus/SlotLagMetrics.test.tsx
deleted file mode 100644
index 859a6454129..00000000000
--- a/apps/studio/components/interfaces/Database/Replication/ReplicationPipelineStatus/SlotLagMetrics.test.tsx
+++ /dev/null
@@ -1,27 +0,0 @@
-import { screen } from '@testing-library/react'
-import { describe, expect, it } from 'vitest'
-
-import { SlotLagMetricsList } from './SlotLagMetrics'
-import { customRender } from '@/tests/lib/custom-render'
-
-const baseMetrics = {
- active: true,
- confirmed_flush_lsn_bytes: 0,
- restart_lsn_bytes: 0,
- reply_time_lag: 0,
-}
-
-describe('SlotLagMetricsList', () => {
- it('renders null safe WAL size as unlimited retention', () => {
- customRender()
-
- expect(screen.getByText('WAL retention remaining')).toBeInTheDocument()
- expect(screen.getByText('Unlimited')).toBeInTheDocument()
- })
-
- it('formats a numeric safe WAL size normally', () => {
- customRender()
-
- expect(screen.getByText('1 KB')).toBeInTheDocument()
- })
-})
diff --git a/apps/studio/components/interfaces/Database/Replication/ReplicationPipelineStatus/SlotLagMetrics.tsx b/apps/studio/components/interfaces/Database/Replication/ReplicationPipelineStatus/SlotLagMetrics.tsx
index 767322c381a..9a7e31028f9 100644
--- a/apps/studio/components/interfaces/Database/Replication/ReplicationPipelineStatus/SlotLagMetrics.tsx
+++ b/apps/studio/components/interfaces/Database/Replication/ReplicationPipelineStatus/SlotLagMetrics.tsx
@@ -1,13 +1,10 @@
import dayjs from 'dayjs'
-import { Info } from 'lucide-react'
import { type ReactNode } from 'react'
-import { Tooltip, TooltipContent, TooltipTrigger } from 'ui'
-import { SlotLagMetricKey, SlotLagMetrics } from './ReplicationPipelineStatus.types'
+import { SlotLagMetricKey } from './ReplicationPipelineStatus.types'
import { getFormattedLagValue } from './ReplicationPipelineStatus.utils'
-import { SlotConnectionIndicator, SlotStatusBadge } from './SlotStatus'
-interface SlotLagField {
+export interface SlotLagField {
key: SlotLagMetricKey
label: string
type: 'bytes' | 'duration'
@@ -20,12 +17,15 @@ interface SlotLagField {
getValueTooltip?: (value: number) => string
}
-const SLOT_LAG_FIELDS: SlotLagField[] = [
+export const SLOT_LAG_FIELDS: SlotLagField[] = [
{
key: 'confirmed_flush_lsn_bytes',
- label: 'Waiting to sync',
+ // Same label as the list column. Scoped to the main slot's ongoing change stream, so "Caught
+ // up" stays true while tables are still doing their initial copy.
+ label: 'Lag',
type: 'bytes',
- description: "Changes in your database the pipeline hasn't synced yet.",
+ description:
+ 'Changes still on their way to the destination, measured on the pipeline’s main slot. Tables in their initial sync use their own slots and aren’t counted.',
zeroLabel: 'Caught up',
},
{
@@ -44,7 +44,7 @@ const SLOT_LAG_FIELDS: SlotLagField[] = [
key: 'reply_time_lag',
label: 'Last check-in',
type: 'duration',
- description: 'Time since the pipeline last reported back to your database.',
+ description: 'Time since the pipeline last reported back to your database',
zeroLabel: 'Just now',
// reply_time_lag is "milliseconds ago", so the absolute time is now minus that, in local time.
getValueTooltip: (ms) => dayjs().subtract(ms, 'millisecond').format('MMM D, YYYY, h:mm:ss A'),
@@ -53,118 +53,8 @@ const SLOT_LAG_FIELDS: SlotLagField[] = [
// Resolves a field's value into a display string (+ optional precise detail), honoring the
// friendly zero/null labels before falling back to the formatted byte/duration value.
-const getFieldDisplay = (field: SlotLagField, value: number | null | undefined) => {
+export const getFieldDisplay = (field: SlotLagField, value: number | null | undefined) => {
if (value == null) return { display: field.nullLabel ?? 'n/a', detail: undefined }
if (field.zeroLabel && value === 0) return { display: field.zeroLabel, detail: undefined }
return getFormattedLagValue(field.type, value)
}
-
-export const SlotLagMetricsInline = ({
- tableName,
- metrics,
-}: {
- tableName: string
- metrics: SlotLagMetrics
-}) => {
- return (
-