feat(studio): rebuild pipeline health summary (#50445)

## What kind of change does this PR introduce?

Studio UI improvement.

## What is the current behavior?

Pipeline health is presented as a dense custom metrics panel with
repeated connection information and per-table lag details mixed into the
pipeline summary.

## What is the new behavior?

Moves the pipeline-level slot status, lag, WAL retention, and last
check-in into a standard detail section. It removes repeated connection
content and keeps table-specific state with the replicated tables.

| Before | After |
| --- | --- |
| <img width="1816" height="274" alt="CleanShot 2026-09-16 at 13 34
43@2x"
src="https://github.com/user-attachments/assets/eceed5bb-8af2-4a3b-83a9-7849f1554fbe"
/> | <img width="1830" height="506" alt="CleanShot 2026-09-16 at 13 34
15@2x"
src="https://github.com/user-attachments/assets/498c4390-17e2-45e5-a923-cb4a7cfd5978"
/> |

_Note that the page spacing may feel a bit funny. This is handled in
https://github.com/supabase/supabase/pull/49630_

This is an independent slice extracted from #49630. The related review
series is #50443, #50444, this PR, #50446, then #49630.

## To test

1. Open `/project/<ref>/database/replication` and select a running
pipeline.
2. Confirm **Pipeline health** shows slot status, lag, WAL retention
remaining, and last check-in.
3. Confirm unlimited WAL retention is labelled **Unlimited** and a
caught-up pipeline is labelled **Caught up**.

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

## UI Improvements

- Added a dedicated Pipeline Health section summarizing WAL status, slot
status, and replication lag.
- Replaced the inline metrics layout with responsive detail cards and
clearer supporting descriptions.
- Added tooltips for lag values and relative reply times, including
precise timestamps.
- Updated lag labels and status indicators for improved clarity.
- Added concise explanations for reserved, extended, unreserved, lost,
and unknown WAL states.
- Improved presentation of pipeline details with optional contextual
descriptions.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
Danny White authored and GitHub committed 2026-09-17 14:52:46 +10:00
1 parent 3e2d54eccb
commit a5dcf3b57c
8 files changed
+204 -340

No files matched your search

@@ -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) => (
<div className="space-y-1">
<dt className="flex items-center gap-1.5 text-sm text-foreground-lighter">
{label}
{tooltip !== undefined && (
<InfoTooltip side="top" className="max-w-56">
{tooltip}
</InfoTooltip>
)}
</dt>
<dd className="text-sm">{children}</dd>
{description !== undefined && <DetailSubtext>{description}</DetailSubtext>}
</div>
)
export const PIPELINE_DETAIL_GRID_CLASS_NAME = 'grid grid-cols-1 gap-x-10 gap-y-6 md:grid-cols-2'
@@ -0,0 +1,36 @@
import { screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { describe, expect, it } from 'vitest'
import { PipelineHealthSection } from './PipelineHealthSection'
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('PipelineHealthSection', () => {
it('renders null safe WAL size as unlimited retention', () => {
customRender(<PipelineHealthSection metrics={{ ...baseMetrics, safe_wal_size_bytes: null }} />)
expect(screen.getByText('WAL retention remaining')).toBeInTheDocument()
expect(screen.getByText('Unlimited')).toBeInTheDocument()
})
it('formats a numeric safe WAL size normally', () => {
customRender(<PipelineHealthSection metrics={{ ...baseMetrics, safe_wal_size_bytes: 1024 }} />)
expect(screen.getByText('1 KB')).toBeInTheDocument()
})
it('shows the absolute last check-in time on hover', async () => {
customRender(<PipelineHealthSection metrics={{ ...baseMetrics, safe_wal_size_bytes: null }} />)
await userEvent.hover(screen.getByText('Just now'))
expect(await screen.findByRole('tooltip')).toHaveTextContent(/\w{3} \d{1,2}, \d{4}/)
})
})
@@ -0,0 +1,104 @@
import { type ReactNode } from 'react'
import { Card, CardContent, Tooltip, TooltipContent, TooltipTrigger } from 'ui'
import {
PageSection,
PageSectionContent,
PageSectionMeta,
PageSectionSummary,
PageSectionTitle,
} from 'ui-patterns/PageSection'
import { PIPELINE_DETAIL_GRID_CLASS_NAME, PipelineDetailItem } from './PipelineDetailItem'
import { type SlotLagMetrics as SlotLagMetricsType } from './ReplicationPipelineStatus.types'
import { getWalStatusMeta } from './ReplicationPipelineStatus.utils'
import { getFieldDisplay, SLOT_LAG_FIELDS } from './SlotLagMetrics'
import { SLOT_STATUS_TOOLTIP, SlotWalStatusValue } from './SlotStatus'
import { InlineLink } from '@/components/ui/InlineLink'
import { DOCS_URL } from '@/lib/constants'
interface PipelineHealthSectionProps {
/** Absent while the pipeline is stopped or failed and reports no slot metrics. */
metrics?: SlotLagMetricsType
/**
* Every notice about the pipeline's state, in priority order. This section is the single place
* they render, so a reader always finds "what needs my attention" at the top of Pipeline health.
*/
children?: ReactNode
}
export const PipelineHealthSection = ({ metrics, children }: PipelineHealthSectionProps) => {
const walStatusMeta = getWalStatusMeta(metrics?.wal_status)
return (
<PageSection>
<PageSectionMeta>
<PageSectionSummary>
<PageSectionTitle>Pipeline health</PageSectionTitle>
</PageSectionSummary>
</PageSectionMeta>
<PageSectionContent className="flex flex-col gap-y-4">
{children}
{metrics !== undefined && (
<Card>
<CardContent className="pb-5">
<dl className={PIPELINE_DETAIL_GRID_CLASS_NAME}>
<PipelineDetailItem
label="Slot status"
tooltip={SLOT_STATUS_TOOLTIP}
description={
<>
{walStatusMeta.description}{' '}
<InlineLink
className="text-foreground-lighter hover:text-foreground"
href={`${DOCS_URL}/guides/database/replication/pipelines-monitoring#slot-statuses`}
>
Learn more
</InlineLink>
</>
}
>
<SlotWalStatusValue status={metrics.wal_status} />
</PipelineDetailItem>
{SLOT_LAG_FIELDS.map((field) => {
const rawValue = metrics[field.key]
const { display, detail } = getFieldDisplay(field, rawValue)
const valueTooltip =
field.getValueTooltip && typeof rawValue === 'number'
? field.getValueTooltip(rawValue)
: undefined
return (
<PipelineDetailItem
key={field.key}
label={field.label}
tooltip={field.description}
>
{valueTooltip !== undefined ? (
<Tooltip>
<TooltipTrigger asChild>
<span className="w-fit cursor-default">{display}</span>
</TooltipTrigger>
<TooltipContent side="bottom" className="text-xs">
{valueTooltip}
</TooltipContent>
</Tooltip>
) : (
display
)}
{detail !== undefined && (
<span className="block text-xs text-foreground-lighter">{detail}</span>
)}
</PipelineDetailItem>
)
})}
</dl>
</CardContent>
</Card>
)}
</PageSectionContent>
</PageSection>
)
}
@@ -24,10 +24,8 @@ import { ErrorDetailsDialog } from '../ErrorDetailsDialog'
import { getStatusName } from '../Pipeline.utils'
import { PipelineStatusName, STATUS_REFRESH_FREQUENCY_MS } from '../Replication.constants'
import { RestartTableDialog } from '../RestartTableDialog'
import { SlotLagMetrics } from './ReplicationPipelineStatus.types'
import { PipelineHealthSection } from './PipelineHealthSection'
import { getDisabledStateConfig } from './ReplicationPipelineStatus.utils'
import { SlotLagMetricsInline, SlotLagMetricsList } from './SlotLagMetrics'
import { SlotConnectionIndicator, SlotStatusBadge, SlotStatusLegend } from './SlotStatus'
import { TableReplicationRow } from './TableReplicationRow'
import { AlertError } from '@/components/ui/AlertError'
import { DropdownMenuItemTooltip } from '@/components/ui/DropdownMenuItemTooltip'
@@ -122,11 +120,6 @@ export const ReplicationPipelineStatus = () => {
[tableStatuses, searchString]
)
const tablesWithLag = useMemo(
() => tableStatuses.filter((table) => Boolean(table.table_sync_lag)),
[tableStatuses]
)
const erroredTables = useMemo(
() => tableStatuses.filter((table) => table.state.name === 'error'),
[tableStatuses]
@@ -183,57 +176,13 @@ export const ReplicationPipelineStatus = () => {
)}
{applyLagMetrics && (
<div className="border border-default rounded-lg bg-surface-100 px-4 py-4 space-y-3">
<div className="flex flex-wrap items-start justify-between gap-x-4 gap-y-2">
<div>
<h4 className="text-sm font-semibold text-foreground">Pipeline metrics</h4>
<p className="text-xs text-foreground-light">
Live metrics on how this pipeline is doing right now.
</p>
</div>
<div className="flex items-center gap-x-2.5">
<SlotConnectionIndicator isActive={applyLagMetrics.active} />
<span className="h-3.5 w-px bg-border" />
<SlotStatusBadge status={applyLagMetrics.wal_status} />
<SlotStatusLegend />
</div>
</div>
<PipelineHealthSection metrics={applyLagMetrics}>
{isStatusError && (
<p className="text-xs text-warning-700">
Unable to refresh data. Showing the last values we received.
</p>
)}
<SlotLagMetricsList metrics={applyLagMetrics} />
{tablesWithLag.length > 0 && (
<>
<div className="border-t border-default/40" />
<div className="space-y-3 text-xs text-foreground">
<div className="flex items-start gap-2 rounded-md border border-default/50 bg-surface-200/60 px-3 py-2 text-foreground-light">
<Info size={14} className="mt-0.5" />
<span>
During initial sync, tables can copy and stream independently before
reconciling with the overall pipeline.
</span>
</div>
<div className="rounded-sm border border-default/50 bg-surface-200/40">
<ul className="divide-y divide-default/40">
{tablesWithLag.map((table) => (
<li key={table.id} className="px-3 py-2">
<SlotLagMetricsInline
tableName={`${table.schema}.${table.name}`}
metrics={table.table_sync_lag as SlotLagMetrics}
/>
</li>
))}
</ul>
</div>
</div>
</>
)}
</div>
</PipelineHealthSection>
)}
{!isPipelineLoading && !isStatusLoading && hasTableData && (
@@ -167,8 +167,7 @@ export const WAL_STATUS_META: Record<SlotWalStatus, WalStatusMeta> = {
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<SlotWalStatus, WalStatusMeta> = {
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<SlotWalStatus, WalStatusMeta> = {
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<SlotWalStatus, WalStatusMeta> = {
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<SlotWalStatus, WalStatusMeta> = {
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.",
},
@@ -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(<SlotLagMetricsList metrics={{ ...baseMetrics, safe_wal_size_bytes: null }} />)
expect(screen.getByText('WAL retention remaining')).toBeInTheDocument()
expect(screen.getByText('Unlimited')).toBeInTheDocument()
})
it('formats a numeric safe WAL size normally', () => {
customRender(<SlotLagMetricsList metrics={{ ...baseMetrics, safe_wal_size_bytes: 1024 }} />)
expect(screen.getByText('1 KB')).toBeInTheDocument()
})
})
@@ -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 (
<div className="flex flex-wrap items-center gap-x-3 gap-y-1.5 text-xs text-foreground">
<span className="truncate font-medium" title={tableName}>
{tableName}
</span>
<SlotConnectionIndicator isActive={metrics.active} context="table" />
<span className="h-3.5 w-px bg-border" />
{metrics.wal_status && <SlotStatusBadge status={metrics.wal_status} context="table" />}
<span className="h-3.5 w-px bg-border" />
<div className="flex flex-wrap items-center gap-x-3 gap-y-1.5 text-[11px] text-foreground-light">
{SLOT_LAG_FIELDS.map((field) => {
const { display } = getFieldDisplay(field, metrics[field.key])
return (
<span key={`${tableName}-${field.key}`} className="flex items-baseline gap-1">
<span className="uppercase tracking-wide text-[10px] text-foreground-lighter">
{field.label}
</span>
<span className="text-foreground">{display}</span>
</span>
)
})}
</div>
</div>
)
}
export const SlotLagMetricsList = ({
metrics,
size = 'default',
showMetricInfo = true,
}: {
metrics: SlotLagMetrics
size?: 'default' | 'compact'
showMetricInfo?: boolean
}) => {
const gridClasses =
size === 'default'
? 'grid-cols-1 sm:grid-cols-2 xl:grid-cols-3 gap-y-4 gap-x-6'
: 'grid-cols-2 gap-y-2 gap-x-4'
const labelClasses =
size === 'default' ? 'text-xs text-foreground-light' : 'text-[11px] text-foreground-lighter'
const valueClasses =
size === 'default'
? 'text-sm font-medium text-foreground'
: 'text-xs font-medium text-foreground'
return (
<dl className={`grid ${gridClasses}`}>
{SLOT_LAG_FIELDS.map((field) => {
const rawValue = metrics[field.key]
const { display, detail } = getFieldDisplay(field, rawValue)
const valueTooltip =
field.getValueTooltip && typeof rawValue === 'number'
? field.getValueTooltip(rawValue)
: undefined
return (
<div key={field.key} className="flex flex-col gap-0.5">
<dt className={labelClasses}>
<span className="inline-flex items-center gap-1">
{field.label}
{showMetricInfo && (
<Tooltip>
<TooltipTrigger asChild>
<button
type="button"
tabIndex={0}
aria-label={`What is ${field.label}`}
className="inline-flex h-4 w-4 items-center justify-center rounded-full bg-surface-200 text-foreground-lighter transition-colors hover:bg-surface-300 hover:text-foreground focus-ring"
>
<Info size={12} />
</button>
</TooltipTrigger>
<TooltipContent side="top" align="start" className="max-w-xs text-xs">
{field.description}
</TooltipContent>
</Tooltip>
)}
</span>
</dt>
<dd className={`flex flex-col ${valueClasses}`}>
{valueTooltip ? (
<Tooltip>
<TooltipTrigger asChild>
<span className="w-fit cursor-default">{display}</span>
</TooltipTrigger>
<TooltipContent side="bottom" className="text-xs">
{valueTooltip}
</TooltipContent>
</Tooltip>
) : (
<span>{display}</span>
)}
{detail && <span className="text-[11px] text-foreground-lighter">{detail}</span>}
</dd>
</div>
)
})}
</dl>
)
}
@@ -1,136 +1,12 @@
import { Info } from 'lucide-react'
import {
Badge,
cn,
Popover,
PopoverContent,
PopoverTrigger,
Tooltip,
TooltipContent,
TooltipTrigger,
} from 'ui'
import { StateDot } from '../StateDot'
import { SlotWalStatus } from './ReplicationPipelineStatus.types'
import { getWalStatusMeta, WAL_STATUS_LEGEND } from './ReplicationPipelineStatus.utils'
import { InlineLink } from '@/components/ui/InlineLink'
import { DOCS_URL } from '@/lib/constants'
import { getWalStatusMeta } from './ReplicationPipelineStatus.utils'
export type SlotStatusContext = 'pipeline' | 'table'
export const SLOT_STATUS_TOOLTIP =
'How safely your database is keeping the changes this pipeline’s main replication slot still needs'
const CONNECTION_TEXT: Record<SlotStatusContext, { active: string; inactive: string }> = {
pipeline: {
active: "This pipeline's replication slot is active and being used right now.",
inactive: "This pipeline's replication slot is not active right now.",
},
table: {
active: "This table's replication slot is active and being used right now.",
inactive: "This table's replication slot is not active right now.",
},
}
/**
* Colored badge for a slot's WAL status, with the plain-language meaning on hover.
* Pass `context="table"` in the per-table inline view to show table-specific descriptions.
*/
export const SlotStatusBadge = ({
status,
context = 'pipeline',
}: {
status?: SlotWalStatus | null
context?: SlotStatusContext
}) => {
/** How safely Postgres is keeping the changes the slot still needs. */
export const SlotWalStatusValue = ({ status }: { status?: SlotWalStatus | null }) => {
const meta = getWalStatusMeta(status)
const description = context === 'table' ? meta.tableDescription : meta.description
return (
<Tooltip>
<TooltipTrigger asChild>
<Badge variant={meta.variant} className="cursor-default">
{meta.label}
</Badge>
</TooltipTrigger>
<TooltipContent side="bottom" className="max-w-[260px]">
{description}
</TooltipContent>
</Tooltip>
)
}
/**
* Info button opening a legend that explains every possible slot status.
*/
export const SlotStatusLegend = () => {
return (
<Popover>
<PopoverTrigger asChild>
<button
type="button"
tabIndex={0}
aria-label="What do the slot statuses mean?"
className="inline-flex h-4 w-4 items-center justify-center rounded-full bg-surface-200 text-foreground-lighter transition-colors hover:bg-surface-300 hover:text-foreground focus-ring"
>
<Info size={12} />
</button>
</PopoverTrigger>
<PopoverContent side="bottom" align="center" className="w-[26rem] p-0">
<div className="px-4 py-3 border-b border-overlay">
<p className="text-sm text-foreground">Slot statuses</p>
<p className="text-xs text-foreground-light">
How safely your database is keeping the changes the pipeline still needs.
</p>
</div>
<ul className="flex flex-col divide-y divide-overlay">
{WAL_STATUS_LEGEND.map((meta) => (
<li key={meta.label} className="flex items-start gap-x-3 px-4 py-2.5">
<div className="flex h-5 w-28 shrink-0 items-center">
<Badge variant={meta.variant}>{meta.label}</Badge>
</div>
<span className="flex-1 text-xs leading-5 text-foreground-light">
{meta.description}
</span>
</li>
))}
</ul>
<div className="border-t border-overlay px-4 py-2.5">
<InlineLink
href={`${DOCS_URL}/guides/database/replication/pipelines-monitoring`}
className="text-xs text-foreground-light"
>
Learn more about monitoring replication
</InlineLink>
</div>
</PopoverContent>
</Popover>
)
}
/**
* Small dot + label indicating whether the slot has a live replication connection.
* Pass `context="table"` in the per-table inline view to show table-specific descriptions.
*/
export const SlotConnectionIndicator = ({
isActive,
context = 'pipeline',
}: {
isActive?: boolean
context?: SlotStatusContext
}) => {
const text = CONNECTION_TEXT[context]
return (
<Tooltip>
<TooltipTrigger asChild>
<span className="flex items-center gap-x-1.5 text-xs text-foreground-light cursor-default">
<span
className={cn(
'h-1.5 w-1.5 rounded-full shrink-0',
isActive ? 'bg-brand' : 'bg-foreground-muted'
)}
/>
{isActive ? 'Connected' : 'Not connected'}
</span>
</TooltipTrigger>
<TooltipContent side="bottom" className="max-w-[260px]">
{isActive ? text.active : text.inactive}
</TooltipContent>
</Tooltip>
)
return <StateDot variant={meta.variant}>{meta.label}</StateDot>
}