This commit is contained in:
Danny White committed 2026-07-09 16:55:18 -04:00
1 parent d8d312fee9
commit 849d8526af
22 files changed
+787 -343

No files matched your search

@@ -1,3 +1,4 @@
import { useParams } from 'common'
import { ArrowUpRight } from 'lucide-react'
import Link from 'next/link'
import { parseAsInteger, parseAsStringEnum, useQueryState } from 'nuqs'
@@ -23,6 +24,8 @@ import { DestinationForm } from './DestinationForm'
import { DestinationType } from './DestinationPanel.types'
import { DestinationTypeSelection } from './DestinationTypeSelection'
import { ReadReplicaForm } from './ReadReplicaForm'
import { WarehouseDestinationForm } from './WarehouseDestinationForm'
import { useWarehouseProjectState } from '@/components/interfaces/Database/Warehouse/warehouseDemoStore'
import { DocsButton } from '@/components/ui/DocsButton'
import { useCheckEntitlements } from '@/hooks/misc/useCheckEntitlements'
import { DOCS_URL } from '@/lib/constants'
@@ -32,6 +35,8 @@ interface DestinationPanelProps {
}
export const DestinationPanel = ({ onSuccessCreateReadReplica }: DestinationPanelProps) => {
const { ref: projectRef } = useParams()
const warehouseState = useWarehouseProjectState(projectRef)
const enablePgReplicate = useIsETLPrivateAlpha()
const { hasAccess: hasETLReplicationAccess } = useCheckEntitlements('replication.etl')
@@ -39,6 +44,7 @@ export const DestinationPanel = ({ onSuccessCreateReadReplica }: DestinationPane
'destinationType',
parseAsStringEnum<DestinationType>([
'Read Replica',
'Warehouse',
'BigQuery',
'Analytics Bucket',
'DuckLake',
@@ -94,74 +100,85 @@ export const DestinationPanel = ({ onSuccessCreateReadReplica }: DestinationPane
}
}, [edit, invalidExistingDestination, setEdit])
useEffect(() => {
if (urlDestinationType === 'Warehouse' && warehouseState.enabled) {
setDestinationType(null)
}
}, [urlDestinationType, warehouseState.enabled, setDestinationType])
return (
<>
<Sheet open={visible} onOpenChange={onClose}>
<SheetContent size="lg" showClose={false}>
<div className="flex flex-col h-full" tabIndex={-1}>
<SheetHeader>
<SheetTitle>{editMode ? 'Edit destination' : 'Add destination'}</SheetTitle>
<SheetDescription>
{editMode
? 'Update the configuration for this destination.'
: 'A destination can be a read replica or an external destination that receives replicated data in near real time.'}
</SheetDescription>
</SheetHeader>
<Sheet
open={visible}
onOpenChange={(open) => {
if (!open) onClose()
}}
>
<SheetContent size="lg" showClose={false}>
<div className="flex flex-col h-full" tabIndex={-1}>
<SheetHeader>
<SheetTitle>{editMode ? 'Edit destination' : 'Add destination'}</SheetTitle>
<SheetDescription>
{editMode
? 'Update the configuration for this destination.'
: 'A destination can be a read replica or an external destination that receives replicated data in near real time.'}
</SheetDescription>
</SheetHeader>
<DestinationTypeSelection />
<DestinationTypeSelection />
<DialogSectionSeparator />
<DialogSectionSeparator />
{destinationType === 'Read Replica' ? (
<ReadReplicaForm onClose={onClose} onSuccess={() => onSuccessCreateReadReplica?.()} />
) : !enablePgReplicate ? (
<SheetSection>
<div className={cn('border rounded-md p-6 flex flex-col gap-y-4')}>
<div className="flex flex-col gap-y-1">
<h4>Request Pipelines access</h4>
<p className="text-sm text-foreground-light">
Pipelines is in <span className="text-foreground">alpha</span> and being
rolled out gradually. Request access below to join the waitlist. Read replicas
are available now.
</p>
</div>
<div className="flex gap-x-2">
<Button
asChild
variant="secondary"
iconRight={<ArrowUpRight size={16} strokeWidth={1.5} />}
>
<Link
target="_blank"
rel="noreferrer"
href="https://forms.supabase.com/pg_replicate"
>
Request alpha access
</Link>
</Button>
<DocsButton href={`${DOCS_URL}/guides/database/replication#pipelines`} />
</div>
{destinationType === 'Read Replica' ? (
<ReadReplicaForm onClose={onClose} onSuccess={() => onSuccessCreateReadReplica?.()} />
) : destinationType === 'Warehouse' ? (
<WarehouseDestinationForm onClose={onClose} />
) : !enablePgReplicate ? (
<SheetSection>
<div className={cn('border rounded-md p-6 flex flex-col gap-y-4')}>
<div className="flex flex-col gap-y-1">
<h4>Request Pipelines access</h4>
<p className="text-sm text-foreground-light">
Pipelines is in <span className="text-foreground">alpha</span> and being rolled
out gradually. Request access below to join the waitlist. Read replicas are
available now.
</p>
</div>
</SheetSection>
) : replicationNotEnabled ? (
<SheetSection>
<EnablePipelinesCallout
className="p-6!"
type={destinationType}
hasAccess={hasETLReplicationAccess}
/>
</SheetSection>
) : (
<DestinationForm
visible={visible}
selectedType={destinationType ?? 'Read Replica'}
existingDestination={existingDestination}
onClose={onClose}
<div className="flex gap-x-2">
<Button
asChild
variant="secondary"
iconRight={<ArrowUpRight size={16} strokeWidth={1.5} />}
>
<Link
target="_blank"
rel="noreferrer"
href="https://forms.supabase.com/pg_replicate"
>
Request alpha access
</Link>
</Button>
<DocsButton href={`${DOCS_URL}/guides/database/replication#pipelines`} />
</div>
</div>
</SheetSection>
) : replicationNotEnabled ? (
<SheetSection>
<EnablePipelinesCallout
className="p-6!"
type={destinationType}
hasAccess={hasETLReplicationAccess}
/>
)}
</div>
</SheetContent>
</Sheet>
</>
</SheetSection>
) : (
<DestinationForm
visible={visible}
selectedType={destinationType ?? 'Read Replica'}
existingDestination={existingDestination}
onClose={onClose}
/>
)}
</div>
</SheetContent>
</Sheet>
)
}
@@ -1,5 +1,6 @@
export type DestinationType =
| 'Read Replica'
| 'Warehouse'
| 'BigQuery'
| 'Analytics Bucket'
| 'DuckLake'
@@ -32,6 +32,14 @@ vi.mock('@/hooks/misc/useIsFeatureEnabled', () => ({
useIsFeatureEnabled: () => ({ infrastructureReadReplicas: true }),
}))
vi.mock('@/hooks/misc/useIsWarehouseEnabled', () => ({
useIsWarehouseEnabled: () => false,
}))
vi.mock('@/components/interfaces/Database/Warehouse/warehouseDemoStore', () => ({
useWarehouseProjectState: () => ({ enabled: false }),
}))
// Background queries from useDestinationInformation (sources + pipelines fire
// even in create mode). Prevent retries so unmatched handlers fail fast.
vi.mock('@/data/replication/utils', () => ({
@@ -125,7 +133,7 @@ describe('DestinationTypeSelection', () => {
fireEvent.click(await screen.findByRole('combobox'))
fireEvent.click(await screen.findByText('BigQuery'))
expect(await screen.findByText(/This destination type is in alpha/)).toBeInTheDocument()
expect(await screen.findByText(/In alpha and may change/)).toBeInTheDocument()
})
test('disables the selector in edit mode so the destination type cannot be changed', async () => {
@@ -1,6 +1,8 @@
import { useParams } from 'common'
import { AnalyticsBucket, BigQuery, Database } from 'icons'
import { Snowflake } from 'lucide-react'
import { Snowflake, Warehouse } from 'lucide-react'
import { parseAsInteger, parseAsStringEnum, useQueryState } from 'nuqs'
import type { ElementType } from 'react'
import {
Badge,
Select,
@@ -21,14 +23,16 @@ import {
useIsETLSnowflakePrivateAlpha,
} from '../useIsETLPrivateAlpha'
import { DestinationType } from './DestinationPanel.types'
import { useWarehouseProjectState } from '@/components/interfaces/Database/Warehouse/warehouseDemoStore'
import { InlineLink } from '@/components/ui/InlineLink'
import { useIsFeatureEnabled } from '@/hooks/misc/useIsFeatureEnabled'
import { useIsWarehouseEnabled } from '@/hooks/misc/useIsWarehouseEnabled'
interface DestinationTypeOption {
value: DestinationType
label: string
description: string
icon: typeof Database
icon: ElementType<{ size?: number; className?: string }>
isAlpha: boolean
enabled: boolean
}
@@ -38,17 +42,34 @@ interface DestinationTypeGroup {
options: DestinationTypeOption[]
}
const LUCIDE_DESTINATION_TYPES = new Set<DestinationType>(['Warehouse', 'Snowflake'])
function DestinationOptionIcon({ option }: { option: DestinationTypeOption }) {
const Icon = option.icon
const className = 'shrink-0 text-foreground-light'
if (LUCIDE_DESTINATION_TYPES.has(option.value)) {
return <Icon size={20} strokeWidth={1.5} className={className} />
}
return <Icon size={20} className={className} />
}
export const DestinationTypeSelection = () => {
const { ref: projectRef } = useParams()
const etlEnableBigQuery = useIsETLBigQueryPrivateAlpha()
const etlEnableIceberg = useIsETLIcebergPrivateAlpha()
const etlEnableDucklake = useIsETLDucklakePrivateAlpha()
const etlEnableSnowflake = useIsETLSnowflakePrivateAlpha()
const isWarehouseFeatureEnabled = useIsWarehouseEnabled()
const warehouseState = useWarehouseProjectState(projectRef)
const { infrastructureReadReplicas } = useIsFeatureEnabled(['infrastructure:read_replicas'])
const [urlDestinationType, setDestinationType] = useQueryState(
'destinationType',
parseAsStringEnum<DestinationType>([
'Read Replica',
'Warehouse',
'BigQuery',
'Analytics Bucket',
'DuckLake',
@@ -91,6 +112,17 @@ export const DestinationTypeSelection = () => {
{
label: 'Pipelines',
options: [
{
value: 'Warehouse',
label: 'Warehouse',
description: 'Replicate to a managed Warehouse endpoint for analytics',
icon: Warehouse,
isAlpha: true,
enabled: isOptionVisible(
'Warehouse',
isWarehouseFeatureEnabled && !warehouseState.enabled
),
},
{
value: 'Analytics Bucket',
label: 'Analytics Bucket',
@@ -146,8 +178,7 @@ export const DestinationTypeSelection = () => {
description={
selectedOption?.isAlpha && (
<span className="block text-sm text-foreground-light mb-1">
This destination type is in alpha and may be unstable or introduce breaking changes
while we iterate based on customer feedback.{' '}
In alpha and may change.{' '}
<InlineLink href="https://github.com/orgs/supabase/discussions/39416">
Leave feedback
</InlineLink>
@@ -163,7 +194,7 @@ export const DestinationTypeSelection = () => {
<SelectTrigger className="h-auto py-2">
{selectedOption ? (
<div className="flex items-center gap-x-3 text-left">
<selectedOption.icon size={20} className="shrink-0 text-foreground-light" />
<DestinationOptionIcon option={selectedOption} />
<div className="flex items-center gap-x-2">
<span className="text-sm text-foreground">{selectedOption.label}</span>
{selectedOption.isAlpha && <Badge variant="warning">Alpha</Badge>}
@@ -181,7 +212,7 @@ export const DestinationTypeSelection = () => {
{group.options.map((option) => (
<SelectItem key={option.value} value={option.value} className="py-2">
<div className="flex items-center gap-x-3">
<option.icon size={20} className="shrink-0 text-foreground-light" />
<DestinationOptionIcon option={option} />
<div className="flex flex-col gap-y-0.5">
<div className="flex items-center gap-x-2">
<span className="text-foreground">{option.label}</span>
@@ -0,0 +1,89 @@
import { useParams } from 'common'
import { useState } from 'react'
import { toast } from 'sonner'
import { Button, Checkbox, Label, SheetFooter, SheetSection } from 'ui'
import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
import { enableWarehouseProject } from '@/components/interfaces/Database/Warehouse/warehouseDemoStore'
import { useDefaultWarehouseSchemas } from '@/components/interfaces/Database/Warehouse/WarehouseSchemaScope'
interface WarehouseDestinationFormProps {
onClose: () => void
}
export function WarehouseDestinationForm({ onClose }: WarehouseDestinationFormProps) {
const { ref: projectRef } = useParams()
const defaultSchemas = useDefaultWarehouseSchemas()
const [selectedSchemas, setSelectedSchemas] = useState<string[]>(() => defaultSchemas)
const [isEnabling, setIsEnabling] = useState(false)
const selectedSet = new Set(selectedSchemas)
const onToggleSchema = (schema: string, checked: boolean) => {
setSelectedSchemas((prev) => (checked ? [...prev, schema] : prev.filter((s) => s !== schema)))
}
const onEnable = async () => {
if (!projectRef || selectedSchemas.length === 0) return
setIsEnabling(true)
enableWarehouseProject(projectRef, selectedSchemas)
setIsEnabling(false)
toast.success('Warehouse replication started')
onClose()
}
return (
<>
<SheetSection className="grow overflow-auto">
<div className="flex flex-col gap-y-4 p-5">
<p className="text-sm text-foreground-light">
Replicate your Postgres data to a separate Warehouse endpoint. Tables keep the same
schema and table names (for example, <code>public.events</code>).
</p>
<div>
<p className="text-sm text-foreground mb-2">Replicated schemas</p>
<p className="text-sm text-foreground-light mb-3">
Choose which schemas to replicate. You can change this later.
</p>
<div className="flex flex-col gap-y-2 max-h-64 overflow-y-auto">
{defaultSchemas.map((schema) => (
<FormItemLayout
key={schema}
isReactForm={false}
layout="flex"
label={schema}
className="items-center"
>
<Checkbox
id={`destination-warehouse-schema-${schema}`}
checked={selectedSet.has(schema)}
onCheckedChange={(checked) => onToggleSchema(schema, checked === true)}
/>
<Label htmlFor={`destination-warehouse-schema-${schema}`} className="sr-only">
Include schema {schema}
</Label>
</FormItemLayout>
))}
</div>
<Button
variant="default"
onClick={() => setSelectedSchemas(defaultSchemas)}
className="mt-3"
>
Select all
</Button>
</div>
</div>
</SheetSection>
<SheetFooter>
<Button variant="default" onClick={onClose}>
Cancel
</Button>
<Button loading={isEnabling} disabled={selectedSchemas.length === 0} onClick={onEnable}>
Enable Warehouse
</Button>
</SheetFooter>
</>
)
}
@@ -38,6 +38,7 @@ import {
} from './useIsETLPrivateAlpha'
import { useWarehouseProjectState } from '@/components/interfaces/Database/Warehouse/warehouseDemoStore'
import { WarehouseDestinationRow } from '@/components/interfaces/Database/Warehouse/WarehouseDestinationRow'
import { WarehouseManageSheet } from '@/components/interfaces/Database/Warehouse/WarehouseManageSheet'
import { AlertError } from '@/components/ui/AlertError'
import { DocsButton } from '@/components/ui/DocsButton'
import { Shortcut } from '@/components/ui/Shortcut'
@@ -67,17 +68,22 @@ export const Destinations = () => {
const hasManagedWarehouse = isWarehouseFeatureEnabled && warehouseState.enabled
const { infrastructureReadReplicas } = useIsFeatureEnabled(['infrastructure:read_replicas'])
const newDestinationDefaultType = infrastructureReadReplicas
? 'Read Replica'
: etlEnableBigQuery
? 'BigQuery'
: etlEnableIceberg
? 'Analytics Bucket'
: etlEnableDucklake
? 'DuckLake'
: etlEnableSnowflake
? 'Snowflake'
: null
const newDestinationDefaultType: DestinationType | null =
isWarehouseFeatureEnabled && !warehouseState.enabled
? 'Warehouse'
: infrastructureReadReplicas
? 'Read Replica'
: etlEnableBigQuery
? 'BigQuery'
: etlEnableIceberg
? 'Analytics Bucket'
: etlEnableDucklake
? 'DuckLake'
: etlEnableSnowflake
? 'Snowflake'
: isWarehouseFeatureEnabled
? 'Warehouse'
: null
const prefetchedRef = useRef(false)
const searchInputRef = useRef<HTMLInputElement>(null)
@@ -89,6 +95,7 @@ export const Destinations = () => {
'destinationType',
parseAsStringEnum<DestinationType>([
'Read Replica',
'Warehouse',
'BigQuery',
'Analytics Bucket',
'DuckLake',
@@ -361,7 +368,7 @@ export const Destinations = () => {
<EmptyStatePresentational
icon={Database}
title="Add a replication destination"
description="Deploy a Read Replica to reduce latency and isolate workloads, or connect a Pipelines destination for analytics."
description="Replicate to another region, an analytics destination, or Warehouse."
className="mt-4"
>
<Button
@@ -378,6 +385,7 @@ export const Destinations = () => {
</div>
<DestinationPanel onSuccessCreateReadReplica={() => setStatusRefetchInterval(5000)} />
<WarehouseManageSheet />
<DisablePipelinesDialog
open={showDisablePipelinesDialog}
@@ -10,7 +10,7 @@ All platform behavior is mocked via `warehouseDemoStore` (localStorage). No real
## In scope
- **Replication page** — `WarehouseProjectCard` (enable/disable, schema scope, observability)
- **Replication page** — enable via **Add destination → Warehouse**; manage via separate **Manage replication** sheet (`?warehouseManage=true`)
- **Destinations table** — read-only managed row (`DuckLake` / `supabase_warehouse`)
- **Table list** — read-only **Replicated** column
- **SQL Editor** — Postgres / Warehouse target toggle + mock "Served by: Warehouse" footer
@@ -43,7 +43,7 @@ Enable simulates `provisioning` → `backfilling` → `streaming` phase progress
## QA checklist
1. Flag off → no Warehouse UI
2. Flag on, disabled → Replication card with Enable CTA only
2. Flag on, disabled → empty state **Add destination** opens sheet with Warehouse as default type
3. Enable → observability metrics, schema checkboxes, Destinations row, Replicated column, SQL toggle, Connect Warehouse tab
4. Uncheck schema → tables in that schema show Replicated = No
5. Disable → confirm destructive modal, all Warehouse UI hidden
@@ -1,9 +1,13 @@
import { useParams } from 'common'
import { Database, Minus } from 'lucide-react'
import { TableCell, TableRow, Tooltip, TooltipContent, TooltipTrigger } from 'ui'
import Link from 'next/link'
import { useState } from 'react'
import { Button, TableCell, TableRow, Tooltip, TooltipContent, TooltipTrigger } from 'ui'
import { DeleteDestination } from '../Replication/DeleteDestination'
import { MANAGED_WAREHOUSE_PUBLICATION_TOOLTIP } from './managedWarehouse.resources'
import { useWarehouseProjectState } from './warehouseDemoStore'
import { disableWarehouseProject, useWarehouseProjectState } from './warehouseDemoStore'
import { WarehouseDestinationRowMenu } from './WarehouseDestinationRowMenu'
import {
formatReplicationLagSeconds,
getWarehouseDestinationStatusLabel,
@@ -13,52 +17,94 @@ import { WarehouseSyncChip } from './WarehouseSyncChip'
export function WarehouseDestinationRow() {
const { ref: projectRef } = useParams()
const state = useWarehouseProjectState(projectRef)
const [showDeleteDestinationForm, setShowDeleteDestinationForm] = useState(false)
const [isDeleting, setIsDeleting] = useState(false)
if (!state.enabled || !state.pipelineId) return null
const statusLabel = getWarehouseDestinationStatusLabel(state.replicationPhase)
const destinationName = `DuckLake (Pipeline ID: ${state.pipelineId})`
const statusLabel = getWarehouseDestinationStatusLabel(
state.replicationPhase,
state.pipelineStatus
)
const lag = formatReplicationLagSeconds(state.lagSeconds)
const isCaughtUp = state.lagSeconds === 0 || state.lagSeconds === 2
const isPipelineStopped =
state.pipelineStatus === 'stopped' && state.replicationPhase === 'streaming'
const showLag =
!isPipelineStopped &&
(state.replicationPhase === 'streaming' || state.replicationPhase === 'backfilling')
const onDeleteDestination = async () => {
if (!projectRef) return
setIsDeleting(true)
disableWarehouseProject(projectRef)
setIsDeleting(false)
setShowDeleteDestinationForm(false)
}
return (
<TableRow>
<TableCell>
<Database size={18} className="text-foreground-light" />
</TableCell>
<TableCell className="max-w-[180px]">
<div>
<p>DuckLake (Pipeline ID: {state.pipelineId})</p>
<p className="text-foreground-lighter">{state.destinationName}</p>
</div>
</TableCell>
<TableCell>
<WarehouseSyncChip phase={state.replicationPhase} />
{statusLabel === 'Failed' && (
<p className="text-xs text-foreground-lighter mt-1">Managed Warehouse pipeline</p>
)}
</TableCell>
<TableCell>
{state.replicationPhase === 'streaming' || state.replicationPhase === 'backfilling' ? (
isCaughtUp ? (
<p className="text-foreground-light">Caught up</p>
<>
<TableRow>
<TableCell>
<Database size={18} className="text-foreground-light" />
</TableCell>
<TableCell className="max-w-[180px]">
<div>
<p>{destinationName}</p>
<p className="text-foreground-lighter">{state.destinationName}</p>
</div>
</TableCell>
<TableCell>
<WarehouseSyncChip phase={state.replicationPhase} pipelineStatus={state.pipelineStatus} />
{statusLabel === 'Failed' && (
<p className="text-xs text-foreground-lighter mt-1">Managed Warehouse pipeline</p>
)}
</TableCell>
<TableCell>
{showLag ? (
isCaughtUp ? (
<p className="text-foreground-light">Caught up</p>
) : (
<p className="text-foreground-light">{lag}</p>
)
) : (
<p className="text-foreground-light">{lag}</p>
)
) : (
<Minus size={18} className="text-foreground-lighter" />
)}
</TableCell>
<TableCell>
<Tooltip>
<TooltipTrigger asChild>
<span className="text-foreground-light">{state.publicationName}</span>
</TooltipTrigger>
<TooltipContent side="bottom" className="max-w-xs">
{MANAGED_WAREHOUSE_PUBLICATION_TOOLTIP}
</TooltipContent>
</Tooltip>
</TableCell>
<TableCell />
</TableRow>
<Minus size={18} className="text-foreground-lighter" />
)}
</TableCell>
<TableCell>
<Tooltip>
<TooltipTrigger asChild>
<span className="text-foreground-light">{state.publicationName}</span>
</TooltipTrigger>
<TooltipContent side="bottom" className="max-w-xs">
{MANAGED_WAREHOUSE_PUBLICATION_TOOLTIP}
</TooltipContent>
</Tooltip>
</TableCell>
<TableCell>
<div className="flex items-center justify-end gap-x-2">
<Button asChild variant="default" className="relative">
<Link href={`/project/${projectRef}/database/replication/${state.pipelineId}`}>
View pipeline
</Link>
</Button>
<WarehouseDestinationRowMenu
replicationPhase={state.replicationPhase}
pipelineStatus={state.pipelineStatus}
onDeleteClick={() => setShowDeleteDestinationForm(true)}
/>
</div>
</TableCell>
</TableRow>
<DeleteDestination
visible={showDeleteDestinationForm}
setVisible={setShowDeleteDestinationForm}
onDelete={onDeleteDestination}
isLoading={isDeleting}
name={destinationName}
/>
</>
)
}
@@ -0,0 +1,106 @@
import { useParams } from 'common'
import { Edit, MoreVertical, Pause, Play, RotateCcw, Trash } from 'lucide-react'
import { parseAsBoolean, useQueryState } from 'nuqs'
import { toast } from 'sonner'
import {
Button,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from 'ui'
import type { ReplicationPhase, WarehousePipelineStatus } from './warehouseDemoStore'
import {
restartWarehousePipeline,
startWarehousePipeline,
stopWarehousePipeline,
} from './warehouseDemoStore'
interface WarehouseDestinationRowMenuProps {
replicationPhase: ReplicationPhase
pipelineStatus: WarehousePipelineStatus
onDeleteClick: () => void
}
export function WarehouseDestinationRowMenu({
replicationPhase,
pipelineStatus,
onDeleteClick,
}: WarehouseDestinationRowMenuProps) {
const { ref: projectRef } = useParams()
const [, setShowManage] = useQueryState('warehouseManage', parseAsBoolean.withDefault(false))
const isTransitional = replicationPhase === 'provisioning' || replicationPhase === 'backfilling'
const isStreaming = replicationPhase === 'streaming'
const isFailed = replicationPhase === 'failed'
const isRunning = pipelineStatus === 'running'
const isStopped = pipelineStatus === 'stopped'
const showStopAndRestart = !isTransitional && isStreaming && isRunning
const showStart = !isTransitional && isStreaming && isStopped
const showFailedActions = isFailed
const onRestartPipeline = () => {
if (!projectRef) return
restartWarehousePipeline(projectRef)
toast.success('Warehouse pipeline restarted')
}
const onStopPipeline = () => {
if (!projectRef) return
stopWarehousePipeline(projectRef)
toast.success('Warehouse pipeline stopped')
}
const onStartPipeline = () => {
if (!projectRef) return
startWarehousePipeline(projectRef)
toast.success('Warehouse pipeline started')
}
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="default" className="px-1.5" icon={<MoreVertical />} />
</DropdownMenuTrigger>
<DropdownMenuContent side="bottom" align="end" className="w-52">
{(showStopAndRestart || showFailedActions) && (
<>
<DropdownMenuItem className="space-x-2" onClick={onRestartPipeline}>
<RotateCcw size={14} />
<p>Restart pipeline</p>
</DropdownMenuItem>
{showStopAndRestart && (
<DropdownMenuItem className="space-x-2" onClick={onStopPipeline}>
<Pause size={14} />
<p>Stop pipeline</p>
</DropdownMenuItem>
)}
<DropdownMenuSeparator />
</>
)}
{showStart && (
<>
<DropdownMenuItem className="space-x-2" onClick={onStartPipeline}>
<Play size={14} />
<p>Start pipeline</p>
</DropdownMenuItem>
<DropdownMenuSeparator />
</>
)}
<DropdownMenuItem className="space-x-2" onClick={() => setShowManage(true)}>
<Edit size={14} />
<p>Edit destination</p>
</DropdownMenuItem>
<DropdownMenuItem className="space-x-2" onClick={onDeleteClick}>
<Trash size={14} />
<p>Delete destination</p>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
)
}
@@ -17,9 +17,14 @@ import { disableWarehouseProject } from './warehouseDemoStore'
interface WarehouseDisableModalProps {
open: boolean
onOpenChange: (open: boolean) => void
onDisabled?: () => void
}
export function WarehouseDisableModal({ open, onOpenChange }: WarehouseDisableModalProps) {
export function WarehouseDisableModal({
open,
onOpenChange,
onDisabled,
}: WarehouseDisableModalProps) {
const { ref: projectRef } = useParams()
const [isDisabling, setIsDisabling] = useState(false)
@@ -29,6 +34,7 @@ export function WarehouseDisableModal({ open, onOpenChange }: WarehouseDisableMo
disableWarehouseProject(projectRef)
setIsDisabling(false)
onOpenChange(false)
onDisabled?.()
}
return (
@@ -1,103 +0,0 @@
import { useParams } from 'common'
import { useEffect, useState } from 'react'
import {
Button,
Checkbox,
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogSection,
DialogSectionSeparator,
DialogTitle,
Label,
} from 'ui'
import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
import { enableWarehouseProject } from './warehouseDemoStore'
import { useDefaultWarehouseSchemas } from './WarehouseSchemaScope'
interface WarehouseEnableModalProps {
open: boolean
onOpenChange: (open: boolean) => void
}
export function WarehouseEnableModal({ open, onOpenChange }: WarehouseEnableModalProps) {
const { ref: projectRef } = useParams()
const defaultSchemas = useDefaultWarehouseSchemas()
const [selectedSchemas, setSelectedSchemas] = useState<string[]>(defaultSchemas)
const [isEnabling, setIsEnabling] = useState(false)
useEffect(() => {
if (open) setSelectedSchemas(defaultSchemas)
}, [open, defaultSchemas])
const selectedSet = new Set(selectedSchemas)
const onToggle = (schema: string, checked: boolean) => {
setSelectedSchemas((prev) => (checked ? [...prev, schema] : prev.filter((s) => s !== schema)))
}
const onEnable = async () => {
if (!projectRef || selectedSchemas.length === 0) return
setIsEnabling(true)
enableWarehouseProject(projectRef, selectedSchemas)
setIsEnabling(false)
onOpenChange(false)
}
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent size="medium">
<DialogHeader>
<DialogTitle>Enable Warehouse</DialogTitle>
</DialogHeader>
<DialogSection>
<p className="text-sm text-foreground-light">
Create an analytical replica of your Postgres data on a separate Warehouse endpoint.
Tables use the same schema and table names (for example, <code>public.events</code>).
</p>
<p className="text-sm text-foreground-light mt-3">
Select which schemas to replicate. You can change this later from the Warehouse card.
</p>
<div className="flex flex-col gap-y-2 max-h-64 overflow-y-auto mt-4">
{defaultSchemas.map((schema) => (
<FormItemLayout
key={schema}
isReactForm={false}
layout="flex"
label={schema}
className="items-center"
>
<Checkbox
id={`enable-warehouse-schema-${schema}`}
checked={selectedSet.has(schema)}
onCheckedChange={(checked) => onToggle(schema, checked === true)}
/>
<Label htmlFor={`enable-warehouse-schema-${schema}`} className="sr-only">
Include schema {schema}
</Label>
</FormItemLayout>
))}
</div>
<Button
variant="default"
onClick={() => setSelectedSchemas(defaultSchemas)}
className="mt-3"
>
Select all
</Button>
</DialogSection>
<DialogSectionSeparator />
<DialogFooter>
<Button variant="default" onClick={() => onOpenChange(false)}>
Cancel
</Button>
<Button loading={isEnabling} disabled={selectedSchemas.length === 0} onClick={onEnable}>
Enable Warehouse
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
)
}
@@ -0,0 +1,96 @@
import { useParams } from 'common'
import { parseAsBoolean, useQueryState } from 'nuqs'
import { useState } from 'react'
import {
Button,
DialogSectionSeparator,
Sheet,
SheetContent,
SheetDescription,
SheetFooter,
SheetHeader,
SheetSection,
SheetTitle,
} from 'ui'
import { useWarehouseProjectState } from './warehouseDemoStore'
import { WarehouseDisableModal } from './WarehouseDisableModal'
import { WarehouseSchemaScope } from './WarehouseSchemaScope'
export function WarehouseManageSheet() {
const { ref: projectRef } = useParams()
const warehouseState = useWarehouseProjectState(projectRef)
const [showManage, setShowManage] = useQueryState(
'warehouseManage',
parseAsBoolean.withDefault(false)
)
const [, setShowConnect] = useQueryState('showConnect', parseAsBoolean.withDefault(false))
const [, setConnectTab] = useQueryState('connectTab')
const [showDisableModal, setShowDisableModal] = useState(false)
const onClose = () => setShowManage(false)
const onDisableSuccess = () => {
onClose()
}
if (!warehouseState.enabled) return null
return (
<>
<Sheet
open={showManage}
onOpenChange={(open) => {
if (!open) onClose()
}}
>
<SheetContent size="lg" showClose={false}>
<div className="flex flex-col h-full" tabIndex={-1}>
<SheetHeader>
<SheetTitle>Edit destination</SheetTitle>
<SheetDescription>Update which schemas are replicated to Warehouse.</SheetDescription>
</SheetHeader>
<DialogSectionSeparator />
<SheetSection className="grow overflow-auto">
<div className="flex flex-col gap-y-4 p-5">
<WarehouseSchemaScope
disabled={
warehouseState.replicationPhase === 'provisioning' ||
warehouseState.replicationPhase === 'backfilling'
}
/>
</div>
</SheetSection>
<SheetFooter>
<Button variant="default" onClick={onClose}>
Close
</Button>
<Button
variant="default"
onClick={() => {
setConnectTab('warehouse')
setShowConnect(true)
}}
>
Connect
</Button>
<Button variant="danger" onClick={() => setShowDisableModal(true)}>
Disable Warehouse
</Button>
</SheetFooter>
</div>
</SheetContent>
</Sheet>
<WarehouseDisableModal
open={showDisableModal}
onOpenChange={setShowDisableModal}
onDisabled={onDisableSuccess}
/>
</>
)
}
@@ -24,7 +24,10 @@ export function WarehouseObservabilityPanel() {
className="[&_span]:text-sm"
/>
<div className="mt-2">
<WarehouseSyncChip phase={state.replicationPhase} />
<WarehouseSyncChip
phase={state.replicationPhase}
pipelineStatus={state.pipelineStatus}
/>
</div>
</div>
<div className="px-4 py-3">
@@ -1,84 +0,0 @@
import { useParams } from 'common'
import { parseAsBoolean, useQueryState } from 'nuqs'
import { useState } from 'react'
import { Button, Card, CardContent } from 'ui'
import { useWarehouseProjectState } from './warehouseDemoStore'
import { WarehouseDisableModal } from './WarehouseDisableModal'
import { WarehouseEnableModal } from './WarehouseEnableModal'
import { WarehouseObservabilityPanel } from './WarehouseObservabilityPanel'
import { WarehouseSchemaScope } from './WarehouseSchemaScope'
import { WarehouseSyncChip } from './WarehouseSyncChip'
import { useIsWarehouseEnabled } from '@/hooks/misc/useIsWarehouseEnabled'
export function WarehouseProjectCard() {
const { ref: projectRef } = useParams()
const isWarehouseFeatureEnabled = useIsWarehouseEnabled()
const warehouseState = useWarehouseProjectState(projectRef)
const [showEnableModal, setShowEnableModal] = useState(false)
const [showDisableModal, setShowDisableModal] = useState(false)
const [, setShowConnect] = useQueryState('showConnect', parseAsBoolean.withDefault(false))
const [, setConnectTab] = useQueryState('connectTab')
if (!isWarehouseFeatureEnabled) return null
return (
<>
<Card className="mb-6">
<CardContent className="p-6 flex flex-col gap-y-4">
<div className="flex flex-col sm:flex-row sm:items-start sm:justify-between gap-4">
<div className="flex flex-col gap-y-1">
<div className="flex items-center gap-x-2">
<h4 className="text-foreground text-base">Warehouse</h4>
{warehouseState.enabled && (
<WarehouseSyncChip phase={warehouseState.replicationPhase} />
)}
</div>
<p className="text-sm text-foreground-light max-w-2xl">
{warehouseState.enabled
? 'Analytical replica of your Postgres data on a separate Warehouse endpoint. Query the same schema and table names against the Warehouse host.'
: 'Enable an analytical replica of your Postgres data on a separate Warehouse endpoint for analytics workloads.'}
</p>
</div>
<div className="flex items-center gap-x-2 shrink-0">
{warehouseState.enabled ? (
<>
<Button
variant="default"
onClick={() => {
setConnectTab('warehouse')
setShowConnect(true)
}}
>
Connect
</Button>
<Button variant="danger" onClick={() => setShowDisableModal(true)}>
Disable Warehouse
</Button>
</>
) : (
<Button onClick={() => setShowEnableModal(true)}>Enable Warehouse</Button>
)}
</div>
</div>
{warehouseState.enabled && (
<>
<WarehouseObservabilityPanel />
<WarehouseSchemaScope
disabled={
warehouseState.replicationPhase === 'provisioning' ||
warehouseState.replicationPhase === 'backfilling'
}
/>
</>
)}
</CardContent>
</Card>
<WarehouseEnableModal open={showEnableModal} onOpenChange={setShowEnableModal} />
<WarehouseDisableModal open={showDisableModal} onOpenChange={setShowDisableModal} />
</>
)
}
@@ -0,0 +1,132 @@
import { useParams } from 'common'
import { ChevronLeft } from 'lucide-react'
import Link from 'next/link'
import { useMemo } from 'react'
import { Badge, Button, Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from 'ui'
import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader'
import { isTableReplicated, useWarehouseProjectState } from './warehouseDemoStore'
import { WarehouseObservabilityPanel } from './WarehouseObservabilityPanel'
import { formatReplicationLagSeconds } from './warehouseReplication.utils'
import { WarehouseSyncChip } from './WarehouseSyncChip'
import { useTablesQuery } from '@/data/tables/tables-query'
export function WarehouseReplicationPipelineStatus() {
const { ref: projectRef } = useParams()
const state = useWarehouseProjectState(projectRef)
const { data: tables, isPending: isTablesLoading } = useTablesQuery(
{ projectRef },
{ enabled: !!projectRef && state.enabled }
)
const replicatedTables = useMemo(() => {
if (!tables) return []
return tables
.filter((table) => isTableReplicated(projectRef, table.schema, table.name))
.sort((a, b) => `${a.schema}.${a.name}`.localeCompare(`${b.schema}.${b.name}`))
}, [tables, projectRef])
if (!state.enabled || !state.pipelineId) return null
const destinationName = `DuckLake (Pipeline ID: ${state.pipelineId})`
const isPipelineStopped =
state.pipelineStatus === 'stopped' && state.replicationPhase === 'streaming'
const tableStatusLabel =
state.replicationPhase === 'failed'
? 'Error'
: state.replicationPhase === 'backfilling'
? 'Backfilling'
: isPipelineStopped
? 'Paused'
: state.replicationPhase === 'streaming'
? 'Replicating'
: 'Pending'
return (
<div className="flex flex-col gap-y-4">
<div className="flex items-center justify-between">
<div className="flex items-center gap-x-3">
<Button asChild variant="outline" icon={<ChevronLeft />} style={{ padding: '5px' }}>
<Link href={`/project/${projectRef}/database/replication`} />
</Button>
<div className="flex items-center gap-x-3">
<h3 className="text-xl font-semibold">{destinationName}</h3>
<WarehouseSyncChip
phase={state.replicationPhase}
pipelineStatus={state.pipelineStatus}
/>
</div>
</div>
</div>
<WarehouseObservabilityPanel />
<div className="border border-default rounded-lg">
<div className="px-4 py-3 border-b border-default">
<h4 className="text-sm font-semibold text-foreground">Table replication</h4>
<p className="text-xs text-foreground-light">
Tables in replicated schemas use the same schema and table names on the Warehouse
endpoint.
</p>
</div>
{isTablesLoading ? (
<div className="p-4">
<GenericSkeletonLoader />
</div>
) : replicatedTables.length === 0 ? (
<p className="px-4 py-6 text-sm text-foreground-light">No replicated tables found.</p>
) : (
<Table>
<TableHeader>
<TableRow>
<TableHead>Table</TableHead>
<TableHead>Status</TableHead>
<TableHead>Replication lag</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{replicatedTables.map((table) => (
<TableRow key={`${table.schema}.${table.name}`}>
<TableCell>
{table.schema}.{table.name}
</TableCell>
<TableCell>
<Badge
variant={
tableStatusLabel === 'Error'
? 'destructive'
: tableStatusLabel === 'Backfilling' || tableStatusLabel === 'Pending'
? 'warning'
: tableStatusLabel === 'Replicating'
? 'success'
: 'default'
}
>
{tableStatusLabel}
</Badge>
</TableCell>
<TableCell>
{isPipelineStopped || state.replicationPhase === 'provisioning'
? '—'
: formatReplicationLagSeconds(state.lagSeconds)}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
)}
</div>
</div>
)
}
export function isWarehouseMockPipelineId(
projectRef: string | undefined,
pipelineId: string | undefined,
warehouseState: ReturnType<typeof useWarehouseProjectState>
): boolean {
if (!projectRef || !pipelineId || !warehouseState.enabled) return false
return warehouseState.pipelineId === pipelineId
}
@@ -10,6 +10,7 @@ import {
useWarehouseProjectState,
} from './warehouseDemoStore'
import { useSchemasQuery } from '@/data/database/schemas-query'
import { EMPTY_ARR } from '@/lib/void'
interface WarehouseSchemaScopeProps {
disabled?: boolean
@@ -17,12 +18,12 @@ interface WarehouseSchemaScopeProps {
export function WarehouseSchemaScope({ disabled = false }: WarehouseSchemaScopeProps) {
const { ref: projectRef } = useParams()
const { data: schemas = [] } = useSchemasQuery({ projectRef })
const { data: schemas } = useSchemasQuery({ projectRef })
const warehouseState = useWarehouseProjectState(projectRef)
const replicableSchemas = useMemo(
() =>
schemas
(schemas ?? EMPTY_ARR)
.map((s) => s.name)
.filter(isReplicableSchema)
.sort(),
@@ -80,6 +81,9 @@ export function WarehouseSchemaScope({ disabled = false }: WarehouseSchemaScopeP
export function useDefaultWarehouseSchemas(): string[] {
const { ref: projectRef } = useParams()
const { data: schemas = [] } = useSchemasQuery({ projectRef })
return useMemo(() => getDefaultIncludedSchemas(schemas.map((s) => s.name)), [schemas])
const { data: schemas } = useSchemasQuery({ projectRef })
return useMemo(
() => getDefaultIncludedSchemas((schemas ?? EMPTY_ARR).map((s) => s.name)),
[schemas]
)
}
@@ -1,23 +1,26 @@
import { Badge } from 'ui'
import type { ReplicationPhase } from './warehouseDemoStore'
import type { ReplicationPhase, WarehousePipelineStatus } from './warehouseDemoStore'
import { getWarehouseDestinationStatusLabel } from './warehouseReplication.utils'
interface WarehouseSyncChipProps {
phase: ReplicationPhase
pipelineStatus?: WarehousePipelineStatus
}
export function WarehouseSyncChip({ phase }: WarehouseSyncChipProps) {
const label = getWarehouseDestinationStatusLabel(phase)
export function WarehouseSyncChip({ phase, pipelineStatus = 'running' }: WarehouseSyncChipProps) {
const label = getWarehouseDestinationStatusLabel(phase, pipelineStatus)
const variant =
phase === 'failed'
? 'destructive'
: phase === 'backfilling' || phase === 'provisioning'
? 'warning'
: phase === 'streaming'
? 'success'
: 'default'
pipelineStatus === 'stopped' && phase === 'streaming'
? 'default'
: phase === 'failed'
? 'destructive'
: phase === 'backfilling' || phase === 'provisioning'
? 'warning'
: phase === 'streaming'
? 'success'
: 'default'
return <Badge variant={variant}>{label}</Badge>
}
@@ -5,4 +5,4 @@ export function isSupabaseManagedWarehousePublicationName(name: string): boolean
}
export const MANAGED_WAREHOUSE_PUBLICATION_TOOLTIP =
'Managed by Supabase for Warehouse replication. Configure schemas on the Warehouse card above.'
'Managed by Supabase for Warehouse replication. Use Manage to update replicated schemas.'
@@ -6,9 +6,12 @@ import { INTERNAL_SCHEMAS } from '@/hooks/useProtectedSchemas'
export type ReplicationPhase = 'idle' | 'provisioning' | 'backfilling' | 'streaming' | 'failed'
export type WarehousePipelineStatus = 'running' | 'stopped'
export interface WarehouseProjectState {
enabled: boolean
replicationPhase: ReplicationPhase
pipelineStatus: WarehousePipelineStatus
lagSeconds: number | null
includedSchemas: string[]
pipelineId: string | null
@@ -21,6 +24,7 @@ export interface WarehouseProjectState {
const DEFAULT_STATE: WarehouseProjectState = {
enabled: false,
replicationPhase: 'idle',
pipelineStatus: 'stopped',
lagSeconds: null,
includedSchemas: [],
pipelineId: null,
@@ -71,7 +75,14 @@ export function getDefaultIncludedSchemas(availableSchemas: string[]): string[]
}
function getProjectState(projectRef: string): WarehouseProjectState {
return warehouseDemoStore.projects[projectRef] ?? { ...DEFAULT_STATE }
const raw = warehouseDemoStore.projects[projectRef]
if (!raw) return { ...DEFAULT_STATE }
return {
...DEFAULT_STATE,
...raw,
pipelineStatus:
raw.pipelineStatus ?? (raw.replicationPhase === 'streaming' ? 'running' : 'stopped'),
}
}
function setProjectState(projectRef: string, next: WarehouseProjectState): void {
@@ -81,7 +92,14 @@ function setProjectState(projectRef: string, next: WarehouseProjectState): void
export function useWarehouseProjectState(projectRef: string | undefined) {
const snap = useSnapshot(warehouseDemoStore)
if (!projectRef) return { ...DEFAULT_STATE }
return snap.projects[projectRef] ?? { ...DEFAULT_STATE }
const raw = snap.projects[projectRef]
if (!raw) return { ...DEFAULT_STATE }
return {
...DEFAULT_STATE,
...raw,
pipelineStatus:
raw.pipelineStatus ?? (raw.replicationPhase === 'streaming' ? 'running' : 'stopped'),
}
}
export function isWarehouseProjectEnabled(projectRef: string | undefined): boolean {
@@ -129,6 +147,7 @@ function simulatePhaseProgression(projectRef: string): void {
setProjectState(projectRef, {
...backfilling,
replicationPhase: 'streaming',
pipelineStatus: 'running',
lagSeconds: 2,
})
}, 6000)
@@ -140,6 +159,7 @@ export function enableWarehouseProject(projectRef: string, includedSchemas: stri
setProjectState(projectRef, {
enabled: true,
replicationPhase: 'provisioning',
pipelineStatus: 'running',
lagSeconds: null,
includedSchemas,
pipelineId,
@@ -156,6 +176,45 @@ export function disableWarehouseProject(projectRef: string): void {
setProjectState(projectRef, { ...DEFAULT_STATE })
}
export function stopWarehousePipeline(projectRef: string): void {
const current = getProjectState(projectRef)
if (!current.enabled || current.replicationPhase !== 'streaming') return
setProjectState(projectRef, { ...current, pipelineStatus: 'stopped' })
}
export function startWarehousePipeline(projectRef: string): void {
const current = getProjectState(projectRef)
if (!current.enabled || current.pipelineStatus !== 'stopped') return
setProjectState(projectRef, {
...current,
pipelineStatus: 'running',
lagSeconds: current.lagSeconds ?? 2,
})
}
export function restartWarehousePipeline(projectRef: string): void {
const current = getProjectState(projectRef)
if (!current.enabled) return
clearPhaseTimer()
setProjectState(projectRef, {
...current,
pipelineStatus: 'running',
replicationPhase: 'backfilling',
lagSeconds: 30,
})
phaseTimer = setTimeout(() => {
const state = getProjectState(projectRef)
if (!state.enabled || state.replicationPhase !== 'backfilling') return
setProjectState(projectRef, {
...state,
replicationPhase: 'streaming',
pipelineStatus: 'running',
lagSeconds: 2,
})
}, 4000)
}
export function updateWarehouseIncludedSchemas(
projectRef: string,
includedSchemas: string[]
@@ -178,6 +237,7 @@ export function updateWarehouseIncludedSchemas(
setProjectState(projectRef, {
...state,
replicationPhase: 'streaming',
pipelineStatus: 'running',
lagSeconds: 2,
})
}, 4000)
@@ -1,4 +1,8 @@
import type { ReplicationPhase, WarehouseProjectState } from './warehouseDemoStore'
import type {
ReplicationPhase,
WarehousePipelineStatus,
WarehouseProjectState,
} from './warehouseDemoStore'
export function formatReplicationPhase(phase: ReplicationPhase): string {
switch (phase) {
@@ -25,7 +29,11 @@ export function formatReplicationLagSeconds(lagSeconds: number | null): string {
return `${(lagSeconds / 3600).toFixed(1)}h`
}
export function getWarehouseDestinationStatusLabel(phase: ReplicationPhase): string {
export function getWarehouseDestinationStatusLabel(
phase: ReplicationPhase,
pipelineStatus: WarehousePipelineStatus = 'running'
): string {
if (pipelineStatus === 'stopped' && phase === 'streaming') return 'Stopped'
switch (phase) {
case 'provisioning':
return 'Starting'
@@ -58,5 +66,10 @@ export function buildWarehouseConnectionString({
}
export function isWarehouseReplicationHealthy(state: WarehouseProjectState): boolean {
return state.enabled && state.replicationPhase === 'streaming' && (state.lagSeconds ?? 0) < 120
return (
state.enabled &&
state.replicationPhase === 'streaming' &&
state.pipelineStatus === 'running' &&
(state.lagSeconds ?? 0) < 120
)
}
@@ -4,18 +4,29 @@ import { useContext, useEffect } from 'react'
import { ReplicationPipelineStatus } from '@/components/interfaces/Database/Replication/ReplicationPipelineStatus/ReplicationPipelineStatus'
import { useIsETLPrivateAlpha } from '@/components/interfaces/Database/Replication/useIsETLPrivateAlpha'
import { useWarehouseProjectState } from '@/components/interfaces/Database/Warehouse/warehouseDemoStore'
import {
isWarehouseMockPipelineId,
WarehouseReplicationPipelineStatus,
} from '@/components/interfaces/Database/Warehouse/WarehouseReplicationPipelineStatus'
import DatabaseLayout from '@/components/layouts/DatabaseLayout/DatabaseLayout'
import { DefaultLayout } from '@/components/layouts/DefaultLayout'
import { ScaffoldContainer, ScaffoldSection } from '@/components/layouts/Scaffold'
import { FormHeader } from '@/components/ui/Forms/FormHeader'
import { useIsWarehouseEnabled } from '@/hooks/misc/useIsWarehouseEnabled'
import { PipelineRequestStatusProvider } from '@/state/replication-pipeline-request-status'
import type { NextPageWithLayout } from '@/types'
const DatabaseReplicationPage: NextPageWithLayout = () => {
const router = useRouter()
const { ref: projectRef } = useParams()
const pipelineId = router.query.pipelineId as string | undefined
const { hasLoaded } = useContext(FeatureFlagContext)
const enablePgReplicate = useIsETLPrivateAlpha()
const isWarehouseFeatureEnabled = useIsWarehouseEnabled()
const warehouseState = useWarehouseProjectState(projectRef)
const isWarehouseMockPipeline =
isWarehouseFeatureEnabled && isWarehouseMockPipelineId(projectRef, pipelineId, warehouseState)
useEffect(() => {
if (hasLoaded && !enablePgReplicate) {
@@ -31,7 +42,11 @@ const DatabaseReplicationPage: NextPageWithLayout = () => {
<ScaffoldSection>
<div className="col-span-12">
<FormHeader title="Replication" />
<ReplicationPipelineStatus />
{isWarehouseMockPipeline ? (
<WarehouseReplicationPipelineStatus />
) : (
<ReplicationPipelineStatus />
)}
</div>
</ScaffoldSection>
</ScaffoldContainer>
@@ -2,7 +2,6 @@ import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader'
import { Destinations } from '@/components/interfaces/Database/Replication/Destinations'
import { ReplicationDiagram } from '@/components/interfaces/Database/Replication/ReplicationDiagram'
import { WarehouseProjectCard } from '@/components/interfaces/Database/Warehouse/WarehouseProjectCard'
import DatabaseLayout from '@/components/layouts/DatabaseLayout/DatabaseLayout'
import { DefaultLayout } from '@/components/layouts/DefaultLayout'
import { ScaffoldContainer, ScaffoldSection } from '@/components/layouts/Scaffold'
@@ -43,8 +42,7 @@ const DatabaseReplicationPage: NextPageWithLayout = () => {
<h3 className="text-foreground text-xl prose">Replication</h3>
</div>
<p className="prose text-sm max-w-full">
Deploy Read Replicas across multiple regions, or use Pipelines to replicate database
changes to analytics destinations.
Replicate to another region, an analytics destination, or Warehouse.
</p>
</div>
</ScaffoldSection>
@@ -56,11 +54,6 @@ const DatabaseReplicationPage: NextPageWithLayout = () => {
</ScaffoldContainer>
) : (
<>
<ScaffoldContainer>
<ScaffoldSection isFullWidth>
<WarehouseProjectCard />
</ScaffoldSection>
</ScaffoldContainer>
<ReplicationDiagram />
<ScaffoldContainer>
<ScaffoldSection isFullWidth className="pt-6!">