mirror of
https://github.com/supabase/supabase.git
synced 2026-10-17 23:29:08 +03:00
health status improvements
This commit is contained in:
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'}${
|
||||
|
||||
+96
-83
@@ -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>
|
||||
</>
|
||||
)
|
||||
|
||||
+9
-10
@@ -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 < 50 MB, pipeline live, phase streaming | **Live** (when copy is also live) |
|
||||
| `behind` | 50 MB ≤ lag < 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 />
|
||||
|
||||
|
||||
Reference in new issue
Block a user