health status improvements

This commit is contained in:
Danny White committed 2026-07-02 17:18:01 -06:00
1 parent c8f514ae13
commit bc6be4364f
12 files changed
+361 -143

No files matched your search

@@ -58,6 +58,7 @@ import {
isWarehouseSchema,
} from '../Warehouse/warehouseNaming.utils'
import { getWarehouseTableStatusSortRank } from '../Warehouse/warehouseReplication.utils'
import { WarehouseStatusText } from '../Warehouse/WarehouseSyncChip'
import {
buildTableDetailUrl,
getActiveWarehouseSchemas,
@@ -88,7 +89,14 @@ import type { SafePostgresTable } from '@/lib/postgres-types'
import { SHORTCUT_IDS } from '@/state/shortcuts/registry'
import { useShortcut } from '@/state/shortcuts/useShortcut'
type TableListSortColumn = 'name' | 'columns' | 'rows' | 'storage' | 'realtime' | 'warehouse'
type TableListSortColumn =
| 'name'
| 'columns'
| 'rows'
| 'rls'
| 'storage'
| 'realtime'
| 'warehouse'
type TableListSort = `${TableListSortColumn}:asc` | `${TableListSortColumn}:desc`
interface TableListProps {
@@ -145,6 +153,34 @@ export const TableList = ({
const tables = tablesData?.pages.flat() ?? []
const sourceSchemaForSizes = isWarehouseSchema(selectedSchema)
? getSourceSchemaName(selectedSchema)
: undefined
const { data: sourceTablesForSizesData } = useInfiniteTablesQuery(
{
projectRef: project?.ref,
connectionString: project?.connectionString,
schema: sourceSchemaForSizes,
includeColumns: false,
pageSize: 50,
nameFilter: debouncedFilterString,
},
{
enabled: Boolean(project?.ref && sourceSchemaForSizes),
}
)
const postgresSizeByTableName = useMemo(() => {
const sizes = new Map<string, string>()
for (const table of sourceTablesForSizesData?.pages.flat() ?? []) {
if (table.size) {
sizes.set(table.name, table.size)
}
}
return sizes
}, [sourceTablesForSizesData?.pages])
const [sentinelRef, sentinelEntry] = useIntersectionObserver({
threshold: 0,
rootMargin: '200px 0px 200px 0px',
@@ -265,6 +301,9 @@ export const TableList = ({
const getBytes = (entity: (typeof entities)[number]) =>
'bytes' in entity && typeof entity.bytes === 'number' ? entity.bytes : undefined
const isRlsEnabled = (entity: (typeof entities)[number]) =>
entity.type === ENTITY_TYPE.TABLE && 'rls_enabled' in entity && entity.rls_enabled === true
const getWarehouseSortRank = (entity: (typeof entities)[number]) => {
if (entity.type !== ENTITY_TYPE.TABLE) return -1
@@ -295,6 +334,8 @@ export const TableList = ({
} else {
comparison = a.rows - b.rows
}
} else if (sortColumn === 'rls') {
comparison = Number(isRlsEnabled(a)) - Number(isRlsEnabled(b))
} else if (sortColumn === 'storage') {
const bytesA = getBytes(a)
const bytesB = getBytes(b)
@@ -525,6 +566,11 @@ export const TableList = ({
Rows (Est)
</TableHeadSort>
</TableHead>
<TableHead key="rls" aria-sort={getAriaSort('rls')}>
<TableHeadSort column="rls" currentSort={sort} onSortChange={handleSortChange}>
RLS
</TableHeadSort>
</TableHead>
<TableHead key="storage" aria-sort={getAriaSort('storage')}>
<TableHeadSort
column="storage"
@@ -559,7 +605,7 @@ export const TableList = ({
<>
{entities.length === 0 && filterString.length === 0 && (
<TableRow key={selectedSchema}>
<TableCell colSpan={8}>
<TableCell colSpan={9}>
{visibleTypes.length === 0 ? (
<>
<p className="text-sm text-foreground">
@@ -594,7 +640,7 @@ export const TableList = ({
)}
{entities.length === 0 && filterString.length > 0 && (
<TableRow key={selectedSchema}>
<TableCell colSpan={8}>
<TableCell colSpan={9}>
<p className="text-sm text-foreground">No results found</p>
<p className="text-sm text-foreground-light">
Your search for "{filterString}" did not return any results
@@ -674,11 +720,36 @@ export const TableList = ({
<p className="text-foreground-muted">—</p>
)}
</TableCell>
<TableCell>
{x.type === ENTITY_TYPE.TABLE && 'rls_enabled' in x ? (
x.rls_enabled ? (
<span className="inline-flex items-center gap-1.5 text-sm text-foreground-light">
<span
aria-hidden
className="size-1.5 shrink-0 rounded-full bg-brand"
/>
<WarehouseStatusText
text="Enabled"
tooltip="Row Level Security is enabled. Row access is controlled by policies."
/>
</span>
) : (
<p className="text-foreground-muted">—</p>
)
) : (
<p className="text-foreground-muted">—</p>
)}
</TableCell>
<TableCell>
{x.type === ENTITY_TYPE.TABLE ? (
<TableListWarehouseStorageCell
tableKey={`${getSourceSchemaName(selectedSchema)}.${x.name}`}
tableSize={x.size}
linkedPostgresSize={
isWarehouseSchema(selectedSchema)
? postgresSizeByTableName.get(x.name)
: undefined
}
isWarehouseSchemaView={isWarehouseSchema(selectedSchema)}
/>
) : (
@@ -830,7 +901,7 @@ export const TableList = ({
</TableBody>
<TableFooter className="font-normal">
<TableRow ref={sentinelRef} className="border-b-0">
<TableCell colSpan={8} className="text-foreground-muted hover:bg-inherit">
<TableCell colSpan={9} className="text-foreground-muted hover:bg-inherit">
{isFetchingNextTablesPage
? 'Loading more tables…'
: `${footerCount} ${footerCount === 1 ? 'table' : 'tables'}${
@@ -1,68 +1,43 @@
import { cn, Tooltip, TooltipContent, TooltipTrigger } from 'ui'
import { useSnapshot } from 'valtio'
import {
resolveWarehouseTableState,
useProjectReplication,
warehouseDemoStore,
type WarehouseProjectReplicationStatus,
type WarehouseTableState,
} from './warehouseDemoStore'
import { WarehouseLinkedTableStatus } from './WarehouseLinkedTableStatus'
import { getSourceTableKey, isWarehouseSchema } from './warehouseNaming.utils'
import {
getSourceTableKey,
getWarehouseCopyTooltip,
isWarehouseSchema,
} from './warehouseNaming.utils'
import { getReplicationLagDisplay } from './warehouseReplication.utils'
getReplicationLagDisplay,
getWarehouseLinkedTableStatus,
} from './warehouseReplication.utils'
import { WarehouseStatusText } from './WarehouseSyncChip'
function getTableEditorDataSourceSuffix(
schema: string,
table: string,
warehouseTables: Record<string, WarehouseTableState>,
projectReplication: WarehouseProjectReplicationStatus | null
): {
label: string
labelTooltip: string
lagSuffix?: string
lagTooltip?: string
lagTone?: 'warning' | 'destructive'
} | null {
if (isWarehouseSchema(schema)) {
const sourceTableKey = getSourceTableKey(schema, table)
const tableState = warehouseTables[sourceTableKey]
const labelTooltip = getWarehouseCopyTooltip(sourceTableKey)
export const WAREHOUSE_TABLE_EDITOR_TOOLTIP =
'This is a linked Warehouse table. Rows replicate from Postgres. Recent writes may not appear here yet.'
if (!projectReplication) {
return {
label: 'Linked Warehouse table',
labelTooltip,
}
}
const footerTextSizeClassName = 'text-xs'
const footerLabelClassName = 'text-xs text-foreground-light'
const lagDisplay = getReplicationLagDisplay(projectReplication, tableState?.copyStatus)
function shouldShowWarehouseEditorStatus(
tableKey: string,
isWarehouseView: boolean,
projectReplication: ReturnType<typeof useProjectReplication>,
storedState: (typeof warehouseDemoStore.tables)[string] | undefined
): boolean {
const state = resolveWarehouseTableState(tableKey, storedState, { isWarehouseView })
return {
label: 'Linked Warehouse table',
labelTooltip,
lagSuffix: lagDisplay.compactSuffix,
lagTooltip: lagDisplay.compactSuffix !== undefined ? lagDisplay.tooltip : undefined,
lagTone:
lagDisplay.tone === 'warning'
? 'warning'
: lagDisplay.tone === 'destructive'
? 'destructive'
: undefined,
}
}
if (state.mode !== 'has_warehouse_copy' || !state.copyStatus) return false
const tableKey = getSourceTableKey(schema, table)
if (warehouseTables[tableKey]?.mode === 'has_warehouse_copy') {
return {
label: 'Postgres (live)',
labelTooltip: 'Live Postgres rows',
}
}
const lagDisplay = projectReplication
? getReplicationLagDisplay(projectReplication, state.copyStatus)
: null
const linkedStatus = getWarehouseLinkedTableStatus(lagDisplay, state.copyStatus)
return null
if (!linkedStatus) return false
if (linkedStatus.type === 'copy' && linkedStatus.copyStatus === 'live') return false
return true
}
interface TableEditorDataSourceSuffixProps {
@@ -73,42 +48,80 @@ interface TableEditorDataSourceSuffixProps {
export function TableEditorDataSourceSuffix({ schema, table }: TableEditorDataSourceSuffixProps) {
const warehouseSnap = useSnapshot(warehouseDemoStore)
const projectReplication = useProjectReplication()
const suffix = getTableEditorDataSourceSuffix(
schema,
table,
warehouseSnap.tables,
projectReplication
if (isWarehouseSchema(schema)) {
const tableKey = getSourceTableKey(schema, table)
const showStatus = shouldShowWarehouseEditorStatus(
tableKey,
true,
projectReplication,
warehouseSnap.tables[tableKey]
)
return (
<>
<span className="mx-1.5">·</span>
<span
className={`inline-flex min-w-0 items-center gap-1 truncate ${footerTextSizeClassName}`}
>
<WarehouseStatusText
text="Linked Warehouse table"
tooltip={WAREHOUSE_TABLE_EDITOR_TOOLTIP}
className={footerLabelClassName}
/>
{showStatus ? (
<>
<span className="shrink-0 text-foreground-muted">·</span>
<WarehouseLinkedTableStatus
tableKey={tableKey}
isWarehouseView
className={footerTextSizeClassName}
showLeadingIndicator={false}
/>
</>
) : null}
</span>
</>
)
}
const tableKey = getSourceTableKey(schema, table)
const storedState = warehouseSnap.tables[tableKey]
const state = resolveWarehouseTableState(tableKey, storedState, { isWarehouseView: false })
if (state.mode !== 'has_warehouse_copy') {
return null
}
const showStatus = shouldShowWarehouseEditorStatus(
tableKey,
false,
projectReplication,
storedState
)
if (!suffix) return null
const lagClassName =
suffix.lagTone === 'destructive'
? 'text-destructive'
: suffix.lagTone === 'warning'
? 'text-warning'
: 'text-foreground-lighter'
return (
<>
<span className="mx-1.5">·</span>
<span className="inline-flex min-w-0 items-baseline truncate text-foreground">
<Tooltip>
<TooltipTrigger asChild>
<span className="cursor-default truncate">{suffix.label}</span>
</TooltipTrigger>
<TooltipContent className="max-w-xs">{suffix.labelTooltip}</TooltipContent>
</Tooltip>
{suffix.lagSuffix !== undefined && suffix.lagTooltip !== undefined && (
<Tooltip>
<TooltipTrigger asChild>
<span className={cn('ml-1 shrink-0 cursor-help', lagClassName)}>
({suffix.lagSuffix})
</span>
</TooltipTrigger>
<TooltipContent className="max-w-xs">{suffix.lagTooltip}</TooltipContent>
</Tooltip>
)}
<span
className={`inline-flex min-w-0 items-center gap-1 truncate ${footerTextSizeClassName}`}
>
<WarehouseStatusText
text="Postgres (live)"
tooltip="Live Postgres rows"
className={footerLabelClassName}
/>
{showStatus ? (
<>
<span className="shrink-0 text-foreground-muted">·</span>
<WarehouseLinkedTableStatus
tableKey={tableKey}
isWarehouseView={false}
className={footerTextSizeClassName}
showLeadingIndicator={false}
/>
</>
) : null}
</span>
</>
)
@@ -1,8 +1,7 @@
import {
formatWarehouseSize,
getWarehouseLensSizeTooltip,
getWarehouseLinkedStorageTooltip,
getWarehouseStorageDisplay,
getWarehouseStorageTooltip,
resolveWarehouseTableState,
useWarehouseTableState,
} from './warehouseDemoStore'
@@ -11,12 +10,15 @@ import { WarehouseStatusText } from './WarehouseSyncChip'
interface TableListWarehouseStorageCellProps {
tableKey: string
tableSize?: string
/** Postgres heap size when viewing the warehouse schema lens. */
linkedPostgresSize?: string
isWarehouseSchemaView: boolean
}
export function TableListWarehouseStorageCell({
tableKey,
tableSize,
linkedPostgresSize,
isWarehouseSchemaView,
}: TableListWarehouseStorageCellProps) {
const storedState = useWarehouseTableState(tableKey)
@@ -34,18 +36,15 @@ export function TableListWarehouseStorageCell({
const storageDisplay = getWarehouseStorageDisplay(
state,
isWarehouseSchemaView ? undefined : tableSize
isWarehouseSchemaView ? linkedPostgresSize : tableSize,
isWarehouseSchemaView ? tableSize : undefined
)
const sizeLabel = isWarehouseSchemaView
? (tableSize ?? formatWarehouseSize(state.warehouseSizeBytes))
: storageDisplay?.postgresSize
const sizeTooltip = isWarehouseSchemaView
? sizeLabel
? getWarehouseLensSizeTooltip(sizeLabel, tableKey)
: undefined
: storageDisplay
? getWarehouseStorageTooltip(storageDisplay)
: undefined
const sizeTooltip = storageDisplay
? getWarehouseLinkedStorageTooltip(storageDisplay, isWarehouseSchemaView)
: undefined
if (!sizeLabel) {
return <span className="text-sm text-foreground-muted">—</span>
@@ -27,10 +27,61 @@ interface WarehouseProjectReplicationStatus {
}
```
Platform may also expose `lag_ms` on linked-table responses; prefer **bytes** for user-facing copy (time-based flush lag is often NULL for logical slots). Studio only surfaces lag amounts when `replication_health` is `behind` or `critical`.
Platform may also expose `lag_ms` on linked-table responses; prefer **bytes** for operator surfaces (Observability). Everyday Studio UI uses `replication_health` buckets only — do not show lag amounts in table list, detail, storage panel, or table editor.
Consumed by **Observability → Warehouse** (mock sparklines in prototype; real monitor API later).
## Replication health and Studio display
Studio derives a **single amalgamated status** per linked table from two inputs:
| Input | Scope | API field |
| ------------------- | ----------- | --------------------------------------------------------------------------------------------------- |
| Table copy progress | Per table | `warehouse_copy_status` |
| Replication health | Per project | `replication_health` (derived from `replication_lag_bytes`, `pipeline_status`, `replication_phase`) |
Implementation: `warehouseReplication.utils.ts` (`resolveReplicationHealth`, `getWarehouseLinkedTableStatus`).
### Project `replication_health` buckets
Derived from WAL backlog (`replication_lag_bytes`) and pipeline state. Thresholds are an implementation detail; users never see byte amounts outside Observability.
| Bucket | Condition (prototype) | User-facing label |
| ---------- | ---------------------------------------------- | --------------------------------- |
| `healthy` | Lag &lt; 50 MB, pipeline live, phase streaming | **Live** (when copy is also live) |
| `behind` | 50 MB ≤ lag &lt; 500 MB | **Catching up** |
| `critical` | Lag ≥ 500 MB | **Degraded** |
| `error` | Pipeline or phase error | **Error** |
`initial_sync` phase maps to **Catching up** at project scope.
### Table `warehouse_copy_status` labels
| Copy status | User-facing label | Leading indicator |
| ------------- | ----------------- | --------------------------------------- |
| `backfilling` | **Backfilling** | Spinner |
| `live` | **Live** | Pulsing dot |
| `error` | **Error** | Red dot (hidden in Table Editor footer) |
### Amalgamation rules (one status shown)
1. **Table copy wins** for `backfilling` or `error` on the table.
2. Otherwise, if project replication is not caught up, **project health supersedes Live** (`Catching up`, `Degraded`, `Error`).
3. Otherwise **Live** when copy is `live` and project is healthy.
### Where labels appear
| Surface | What users see |
| ------------------------------------------ | --------------------------------------------------------------------------- |
| Tables list → Warehouse column | Amalgamated status |
| Table detail header / Storage panel Status | Same |
| Table Editor footer | Expectation tooltip on label; amalgamated status only when not healthy Live |
| Observability | Byte lag, sparklines, operator detail |
### Table Editor expectation copy
Warehouse lens footer always explains that rows replicate from Postgres and recent writes may not appear yet. This sets expectations; it does not replace health status when replication is unhealthy.
## Proposed fields on table metadata
Returned by table editor / catalog queries (`getTableEditor`, table list, etc.):
@@ -13,6 +13,7 @@ interface WarehouseLinkedTableStatusProps {
tableKey: string
isWarehouseView?: boolean
className?: string
showLeadingIndicator?: boolean
}
/** Single amalgamated Warehouse link status (table copy + project replication). */
@@ -20,6 +21,7 @@ export function WarehouseLinkedTableStatus({
tableKey,
isWarehouseView = false,
className,
showLeadingIndicator = true,
}: WarehouseLinkedTableStatusProps) {
const storedState = useWarehouseTableState(tableKey)
const projectReplication = useProjectReplication()
@@ -40,6 +42,7 @@ export function WarehouseLinkedTableStatus({
copyStatus={linkedTableStatus.copyStatus}
appearance="inline"
className={className}
showLeadingIndicator={showLeadingIndicator}
/>
)
}
@@ -52,17 +52,22 @@ interface WarehouseCopyStatusProps {
className?: string
dotSize?: 'default' | 'lg'
tooltip?: string
/** When false, omits the leading dot/spinner (e.g. compact Table Editor footer). */
showLeadingIndicator?: boolean
}
function CopyStatusLeadingIndicator({
copyStatus,
mode,
dotSize = 'default',
showLeadingIndicator = true,
}: {
copyStatus: CopyStatus
mode: WarehouseCopyStatusAppearance
dotSize?: 'default' | 'lg'
showLeadingIndicator?: boolean
}) {
if (!showLeadingIndicator) return null
if (copyStatus === 'backfilling') {
return (
<Loader2
@@ -94,12 +99,19 @@ function CopyStatusLeadingIndicator({
return <Icon className="size-3 shrink-0" aria-hidden="true" />
}
const inlineCopyStatusClassNames: Record<CopyStatus, string> = {
backfilling: 'text-foreground-light hover:text-foreground',
live: 'text-foreground-light hover:text-foreground',
error: 'text-destructive hover:text-destructive-600',
}
export function WarehouseCopyStatus({
copyStatus,
appearance = 'badge',
className,
dotSize = 'default',
tooltip,
showLeadingIndicator = true,
}: WarehouseCopyStatusProps) {
const label = COPY_STATUS_LABELS[copyStatus]
const statusTooltip = tooltip ?? COPY_STATUS_TOOLTIPS[copyStatus]
@@ -111,13 +123,19 @@ export function WarehouseCopyStatus({
<TooltipTrigger asChild>
<span
className={cn(
'group inline-flex cursor-default items-center gap-1 text-sm text-foreground-light transition-colors hover:text-foreground',
'group inline-flex cursor-default items-center gap-1 text-sm transition-colors',
inlineCopyStatusClassNames[copyStatus],
className
)}
data-copy-status={copyStatus}
aria-label={`Warehouse link sync status: ${label}`}
>
<CopyStatusLeadingIndicator copyStatus={copyStatus} mode="inline" dotSize={dotSize} />
<CopyStatusLeadingIndicator
copyStatus={copyStatus}
mode="inline"
dotSize={dotSize}
showLeadingIndicator={showLeadingIndicator}
/>
<span className="border-b border-dotted border-current/40 transition-colors group-hover:border-current/70">
{label}
</span>
@@ -4,7 +4,6 @@ import Link from 'next/link'
import { parseAsBoolean, parseAsString, useQueryState } from 'nuqs'
import { useState, type ReactNode } from 'react'
import {
Badge,
Button,
DropdownMenu,
DropdownMenuContent,
@@ -116,16 +115,17 @@ function TableCopyRow({
detailUrl?: string
isCurrent?: boolean
}) {
const labelTooltip = isCurrent ? `This table. ${tooltip}` : tooltip
return (
<MetaRow
label={
<MetaRowLabel tooltip={tooltip}>
<MetaRowLabel tooltip={labelTooltip}>
<span>{label}</span>
</MetaRowLabel>
}
>
<div className="flex items-center justify-end gap-1.5">
{isCurrent && <Badge variant="default">This</Badge>}
<code className="text-code-inline break-all">{name}</code>
{detailUrl !== undefined && (
<RowIconLink
@@ -102,19 +102,20 @@ export function hasWarehouseTables(): boolean {
export function resolveWarehouseTableState(
tableKey: string,
storedState: WarehouseTableState,
storedState: WarehouseTableState | undefined,
{ isWarehouseView }: { isWarehouseView: boolean }
): WarehouseTableState {
const hasWarehouseCopy = storedState.mode === 'has_warehouse_copy' || isWarehouseView
if (!hasWarehouseCopy) return storedState
const resolved = storedState ?? { mode: 'postgres' }
const hasWarehouseCopy = resolved.mode === 'has_warehouse_copy' || isWarehouseView
if (!hasWarehouseCopy) return resolved
return {
mode: 'has_warehouse_copy',
copyStatus: storedState.copyStatus ?? DEFAULT_WAREHOUSE_COPY_FIELDS.copyStatus,
copyStatus: resolved.copyStatus ?? DEFAULT_WAREHOUSE_COPY_FIELDS.copyStatus,
warehouseSizeBytes:
storedState.warehouseSizeBytes ?? DEFAULT_WAREHOUSE_COPY_FIELDS.warehouseSizeBytes,
lastSyncedAt: storedState.lastSyncedAt,
copyName: storedState.copyName ?? getWarehouseQualifiedTableName(tableKey),
resolved.warehouseSizeBytes ?? DEFAULT_WAREHOUSE_COPY_FIELDS.warehouseSizeBytes,
lastSyncedAt: resolved.lastSyncedAt,
copyName: resolved.copyName ?? getWarehouseQualifiedTableName(tableKey),
}
}
@@ -285,37 +286,52 @@ export interface WarehouseStorageDisplay {
export function getWarehouseStorageDisplay(
state: Pick<WarehouseTableState, 'mode' | 'warehouseSizeBytes'> | undefined,
postgresSize?: string
postgresSize?: string,
warehouseSize?: string
): WarehouseStorageDisplay | null {
const mode = state?.mode ?? 'postgres'
if (mode === 'postgres') return null
const warehouseCopySize = postgresSize ?? formatWarehouseSize(state?.warehouseSizeBytes)
const resolvedPostgres = postgresSize ?? null
const warehouseCopySize =
warehouseSize ?? postgresSize ?? formatWarehouseSize(state?.warehouseSizeBytes)
return {
postgresSize: postgresSize ?? null,
postgresSize: resolvedPostgres,
warehouseCopySize,
}
}
export function getWarehouseStorageTooltip(display: WarehouseStorageDisplay): string {
if (display.postgresSize) {
return `Postgres: ${display.postgresSize} · Warehouse: ${display.warehouseCopySize}`
/** Linked-table storage tooltip. Order matches the active lens (postgres or warehouse). */
export function getWarehouseLinkedStorageTooltip(
display: WarehouseStorageDisplay,
isWarehouseView: boolean
): string {
const { postgresSize, warehouseCopySize } = display
if (postgresSize) {
return isWarehouseView
? `Warehouse: ${warehouseCopySize} · Postgres: ${postgresSize}`
: `Postgres: ${postgresSize} · Warehouse: ${warehouseCopySize}`
}
return `Warehouse: ${display.warehouseCopySize}`
return `Warehouse: ${warehouseCopySize}`
}
export function getWarehouseStorageTooltip(display: WarehouseStorageDisplay): string {
return getWarehouseLinkedStorageTooltip(display, false)
}
/** @deprecated Use getWarehouseLinkedStorageTooltip with a full WarehouseStorageDisplay */
export function getWarehouseLensSizeTooltip(
warehouseSize: string,
sourceTableKey: string,
_sourceTableKey: string,
postgresSize?: string | null
): string {
if (postgresSize) {
return `Postgres: ${postgresSize} · Warehouse: ${warehouseSize}`
}
return `Warehouse: ${warehouseSize}. Postgres source: ${sourceTableKey}`
return getWarehouseLinkedStorageTooltip(
{ postgresSize: postgresSize ?? null, warehouseCopySize: warehouseSize },
true
)
}
/** @deprecated Prefer postgres size in UI with getWarehouseStorageTooltip on a Linked hint. */
@@ -35,7 +35,9 @@ export function resolveReplicationHealth(
export interface ReplicationLagDisplay {
health: ReplicationHealth
headline: string
/** Bytes formatted for Observability only — not shown in everyday Studio surfaces. */
lagAmount?: string
/** Short user-facing health label (no lag amounts). */
compactSuffix?: string
tooltip: string
tone: 'default' | 'warning' | 'destructive'
@@ -49,7 +51,7 @@ export function getReplicationLagDisplay(
return {
health: 'error',
headline: 'Sync error',
compactSuffix: 'Sync error',
compactSuffix: 'Error',
tooltip: 'This linked Warehouse table could not stay in sync with Postgres.',
tone: 'destructive',
}
@@ -59,13 +61,10 @@ export function getReplicationLagDisplay(
const lagFormatted = getFormattedLagValue('bytes', replication.replicationLagBytes).display
if (health === 'error') {
const hasSignificantLag =
replication.replicationLagBytes >= REPLICATION_LAG_BEHIND_THRESHOLD_BYTES
return {
health,
headline: 'Replication error',
compactSuffix: hasSignificantLag ? `${lagFormatted} behind` : 'Replication error',
compactSuffix: 'Error',
tooltip: 'Warehouse replication pipeline encountered an error. Check replication logs.',
tone: 'destructive',
}
@@ -75,6 +74,7 @@ export function getReplicationLagDisplay(
return {
health: 'healthy',
headline: 'Initial sync',
compactSuffix: 'Catching up',
tooltip: 'Running the first full sync for this project’s Warehouse pipeline.',
tone: 'default',
}
@@ -103,8 +103,9 @@ export function getReplicationLagDisplay(
health,
headline: 'Replication behind',
lagAmount: lagFormatted,
compactSuffix: `${lagFormatted} behind`,
tooltip: `Warehouse is ${lagFormatted} behind Postgres for all tables in this project.`,
compactSuffix: 'Catching up',
tooltip:
'Warehouse replication is catching up. Recent writes may not appear in linked tables yet.',
tone: 'warning',
}
}
@@ -113,8 +114,8 @@ export function getReplicationLagDisplay(
health,
headline: 'Severely behind',
lagAmount: lagFormatted,
compactSuffix: `${lagFormatted} behind`,
tooltip: `Warehouse replication is severely behind (${lagFormatted}). Query results may be stale.`,
compactSuffix: 'Degraded',
tooltip: 'Warehouse replication is degraded. Query results may be stale.',
tone: 'destructive',
}
}
@@ -18,8 +18,8 @@ import {
const REPLICATION_PRESETS: { preset: WarehouseDemoReplicationPreset; label: string }[] = [
{ preset: 'healthy', label: 'Healthy (in sync)' },
{ preset: 'behind', label: 'Behind (warning)' },
{ preset: 'critical', label: 'Critical (severely behind)' },
{ preset: 'behind', label: 'Catching up (behind)' },
{ preset: 'critical', label: 'Degraded (critical)' },
{ preset: 'pipeline_error', label: 'Pipeline error' },
{ preset: 'copy_error', label: 'Table link sync error' },
]
@@ -36,7 +36,7 @@ import { buildTableDetailSqlEditorUrl } from '@/components/interfaces/Database/T
import { TableDetailSplitLinkButton } from '@/components/interfaces/Database/Tables/TableDetailSplitLinkButton'
import {
formatWarehouseSize,
getWarehouseLensSizeTooltip,
getWarehouseLinkedStorageTooltip,
getWarehouseStorageDisplay,
getWarehouseStorageTooltip,
resolveWarehouseTableState,
@@ -62,6 +62,7 @@ import { useDatabasePublicationsQuery } from '@/data/database-publications/datab
import { ENTITY_TYPE } from '@/data/entity-types/entity-type-constants'
import { useTableEditorQuery } from '@/data/table-editor/table-editor-query'
import { isTableLike } from '@/data/table-editor/table-editor-types'
import { useTablesQuery } from '@/data/tables/tables-query'
import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions'
import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
import { useTableDetailWarehouseView } from '@/hooks/misc/useTableDetailWarehouseView'
@@ -116,6 +117,26 @@ export function TableDetailLayout({
const { isWarehouseDetailView } = useTableDetailWarehouseView(selectedTable?.schema)
const sourceSchemaForSizes =
selectedTable !== undefined && isWarehouseDetailView
? getSourceSchemaName(selectedTable.schema)
: undefined
const { data: sourceSchemaTables } = useTablesQuery(
{
projectRef: project?.ref,
connectionString: project?.connectionString,
schema: sourceSchemaForSizes,
includeColumns: false,
},
{ enabled: Boolean(project?.ref && sourceSchemaForSizes) }
)
const linkedPostgresSize =
selectedTable !== undefined && isWarehouseDetailView
? sourceSchemaTables?.find((table) => table.name === selectedTable.name)?.size
: undefined
const { data: publications } = useDatabasePublicationsQuery({
projectRef: project?.ref,
connectionString: project?.connectionString,
@@ -187,10 +208,13 @@ export function TableDetailLayout({
isTable && isWarehouseDetailView
? (selectedTable.size ?? formatWarehouseSize(warehouseState?.warehouseSizeBytes))
: undefined
const warehouseSizeTooltip =
warehouseSizeLabel && tableKey
? getWarehouseLensSizeTooltip(warehouseSizeLabel, tableKey)
: undefined
const warehouseLensStorageDisplay =
isTable && isWarehouseDetailView && warehouseState?.mode === 'has_warehouse_copy'
? getWarehouseStorageDisplay(warehouseState, linkedPostgresSize, warehouseSizeLabel)
: null
const warehouseSizeTooltip = warehouseLensStorageDisplay
? getWarehouseLinkedStorageTooltip(warehouseLensStorageDisplay, true)
: undefined
const navigationItems =
id && ref
@@ -40,6 +40,10 @@ import {
getWarehouseCopyTooltip,
isWarehouseSchema,
} from '@/components/interfaces/Database/Warehouse/warehouseNaming.utils'
import {
buildTableDetailUrl,
WAREHOUSE_TABLE_DETAIL_VIEW,
} from '@/components/interfaces/Database/Warehouse/warehouseTableEditor.utils'
import { getEntityLintDetails } from '@/components/interfaces/TableGridEditor/TableEntity.utils'
import { EntityTypeIcon } from '@/components/ui/EntityTypeIcon'
import { InlineLink } from '@/components/ui/InlineLink'
@@ -99,7 +103,9 @@ export const EntityListItem = ({
const isPreview = tabs.previewTabId === tabId
const isActive = Number(id) === entity.id
const canEdit = isActive && !isLocked
const isWarehouseEntity = isWarehouseSchema(entity.schema)
const showOverflowMenu = isActive && (!isLocked || isWarehouseEntity)
const canMutateTable = isActive && !isLocked && !isWarehouseEntity
const { filters } = useTableFilter()
const roleImpersonationState = useRoleImpersonationStateSnapshot()
@@ -163,7 +169,6 @@ export const EntityListItem = ({
const apiAccessData = apiAccessMap?.[entity.name]
const isWarehouseEntity = isWarehouseSchema(entity.schema)
const entityIconType = isWarehouseEntity ? ENTITY_TYPE.WAREHOUSE_TABLE : entity.type
const formatTooltipText = (entityType: string) => {
@@ -252,7 +257,7 @@ export const EntityListItem = ({
/>
</div>
{canEdit && (
{showOverflowMenu && (
<DropdownMenu>
<DropdownMenuTrigger
asChild
@@ -357,7 +362,24 @@ export const EntityListItem = ({
</DropdownMenuItem>
)}
{entity.type === ENTITY_TYPE.TABLE && (
{isWarehouseEntity && isTableLikeEntityListItem(entity) && (
<>
<DropdownMenuSeparator />
<DropdownMenuItem key="warehouse-settings" className="space-x-2" asChild>
<Link
href={buildTableDetailUrl(projectRef, entity.id, {
view: WAREHOUSE_TABLE_DETAIL_VIEW,
section: 'storage',
})}
>
<Settings size={12} className="shrink-0" />
<span>Edit settings</span>
</Link>
</DropdownMenuItem>
</>
)}
{entity.type === ENTITY_TYPE.TABLE && canMutateTable && (
<>
<DropdownMenuSeparator />