Joshen/fe 2453 post rollout dashboard clean up (#43325)

## Context

Just cleaning up the `unifiedReplication` feature flag now that this is
fully rolled out

Also addresses some bugs that I came across while cleaning up,
specifically for the new home page
- Add enabled features flag check for read replicas in database
replication
- Infra diagram, read replicas
- View connection string CTA to open connect UI with the right database
selected
- Replaced all other actions with a "Manage replica" CTA which links to
the replica details page
This commit is contained in:
Joshen Lim authored and GitHub committed 2026-03-04 15:28:01 +08:00
1 parent ddd94a9c24
commit a2b40468d0
15 files changed
+133 -1073

No files matched your search

-3
View File
@@ -87,7 +87,6 @@
"components/interfaces/Settings/Database/ConnectionPooling/ConnectionPooling.tsx": 1,
"components/interfaces/Settings/Database/SSLConfiguration.tsx": 1,
"components/interfaces/Settings/General/ComplianceConfig/ProjectComplianceMode.tsx": 1,
"components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/DeployNewReplicaPanel.tsx": 1,
"components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/InstanceConfiguration.tsx": 1,
"components/interfaces/Settings/Logs/LogTable.tsx": 2,
"components/interfaces/Settings/Logs/LogsPreviewer.tsx": 2,
@@ -948,7 +947,6 @@
"components/interfaces/Settings/General/CustomDomainConfig/DNSRecord.tsx": 1,
"components/interfaces/Settings/General/Infrastructure/PauseProjectButton.tsx": 1,
"components/interfaces/Settings/General/Infrastructure/RestartServerButton.tsx": 1,
"components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/DeployNewReplicaPanel.tsx": 1,
"components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/DropAllReplicasConfirmationModal.tsx": 1,
"components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/MapView.tsx": 1,
"components/interfaces/Settings/Integrations/GithubIntegration/GitHubIntegrationConnectionForm.tsx": 1,
@@ -1110,7 +1108,6 @@
"components/interfaces/Organization/Usage/UsageBarChart.tsx": 1,
"components/interfaces/ProjectAPIDocs/FirstLevelNav.tsx": 1,
"components/interfaces/Settings/General/TransferProjectPanel/TransferProjectButton.tsx": 1,
"components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/DeployNewReplicaPanel.tsx": 1,
"components/interfaces/Settings/Logs/LogSelection.tsx": 1,
"components/interfaces/Settings/Logs/LogTable.tsx": 3,
"components/interfaces/Settings/Logs/LogsFormatters.tsx": 2,
@@ -1,232 +0,0 @@
import { ArrowRight, ArrowUpRight, Circle, Database, Plus } from 'lucide-react'
import { useTheme } from 'next-themes'
import Link from 'next/link'
import { useMemo } from 'react'
import ReactFlow, { Background, Handle, Position, ReactFlowProvider } from 'reactflow'
import 'reactflow/dist/style.css'
import { useParams } from 'common'
import { BASE_PATH } from 'lib/constants'
import { Button, Card, CardContent } from 'ui'
import { NODE_WIDTH } from '../../Settings/Infrastructure/InfrastructureConfiguration/InstanceConfiguration.constants'
const STATIC_NODES = [
{
id: '1',
type: 'primary',
data: {
label: 'Primary Database',
region: 'East US (Ohio)',
provider: 'AWS',
regionIcon: 'us-east-1',
},
position: { x: 825, y: 0 },
},
{
id: '2',
type: 'replica',
data: {
label: 'Iceberg',
details: '3 tables',
regionIcon: 'us-west-1',
},
position: { x: 875, y: 110 },
},
{
id: '3',
type: 'replica',
data: {
label: 'BigQuery',
details: '5 tables',
regionIcon: 'us-west-1',
},
position: { x: 875, y: 200 },
},
{
id: '4',
type: 'blank',
position: { x: 875, y: 290 },
data: {},
},
{
id: '5',
type: 'cta',
position: { x: 125, y: 20 },
data: {},
},
]
const STATIC_EDGES = [
{ id: 'e1-2', source: '1', target: '2', type: 'smoothstep', animated: true },
{ id: 'e1-3', source: '1', target: '3', type: 'smoothstep', animated: true },
{ id: 'e1-4', source: '1', target: '4', type: 'smoothstep', animated: true },
]
export const ReplicationComingSoon = () => {
return (
<ReactFlowProvider>
<ReplicationStaticMockup />
</ReactFlowProvider>
)
}
const ReplicationStaticMockup = () => {
const { ref: projectRef = '_' } = useParams()
const nodes = useMemo(() => STATIC_NODES, [])
const edges = useMemo(() => STATIC_EDGES, [])
const { resolvedTheme } = useTheme()
const backgroundPatternColor =
resolvedTheme === 'dark' && projectRef !== '_'
? 'rgba(255, 255, 255, 0.3)'
: 'rgba(0, 0, 0, 0.4)'
const nodeTypes = useMemo(
() => ({
primary: PrimaryNode,
replica: ReplicaNode,
blank: BlankNode,
cta: () => CTANode({ projectRef }),
}),
[projectRef]
)
return (
<div className="relative border-t h-full w-full">
<ReactFlow
fitView
fitViewOptions={{ minZoom: 0.9, maxZoom: 0.9 }}
className="instance-configuration"
zoomOnPinch={false}
zoomOnScroll={false}
nodesDraggable={true}
nodesConnectable={false}
zoomOnDoubleClick={false}
edgesFocusable={false}
edgesUpdatable={false}
nodes={nodes}
edges={edges}
nodeTypes={nodeTypes}
proOptions={{ hideAttribution: true }}
>
<Background color={backgroundPatternColor} />
</ReactFlow>
</div>
)
}
const PrimaryNode = ({
data,
}: {
data: { label: string; region: string; provider: string; regionIcon: string }
}) => {
return (
<div className="flex flex-col rounded bg-surface-100 border border-default">
<div className="flex items-start justify-between p-3" style={{ width: NODE_WIDTH / 2 + 55 }}>
<div className="flex gap-x-3">
<div className="w-8 h-8 bg-brand-500 border border-brand-600 rounded-md flex items-center justify-center">
<Database size={16} />
</div>
<div className="flex flex-col gap-y-0.5">
<p className="text-sm">{data.label}</p>
<p className="flex items-center gap-x-1">
<span className="text-sm text-foreground-light">{data.region}</span>
</p>
<p className="flex items-center gap-x-1">
<span className="text-sm text-foreground-light">{data.provider}</span>
</p>
</div>
</div>
<div className="flex items-center gap-2">
<img
alt="region icon"
className="w-6 rounded-sm mt-0.5"
src={`${BASE_PATH}/img/regions/${data.regionIcon}.svg`}
/>
<Circle size={10} className="bg-brand-500 stroke-none rounded-full" />
</div>
</div>
<Handle type="source" position={Position.Left} className="opacity-25" />
</div>
)
}
const ReplicaNode = ({
data,
}: {
data: { label: string; details: string; regionIcon: string }
}) => {
return (
<div className="flex flex-col rounded bg-surface-100 border border-default px-2">
<div className="flex items-start justify-between p-3" style={{ width: NODE_WIDTH / 2 - 10 }}>
<div className="flex gap-x-3">
<div className="flex flex-col gap-y-0.5">
<p>{data.label}</p>
<p className="flex items-center gap-x-1">
<span className="text-sm text-foreground-light">{data.details}</span>
</p>
</div>
</div>
<div className="flex items-center gap-2">
<img
alt="region icon"
className="w-6 rounded-sm mt-0.5"
src={`${BASE_PATH}/img/regions/${data.regionIcon}.svg`}
/>
<Circle size={10} className="bg-brand-500 stroke-none rounded-full" />
</div>
</div>
<Handle type="target" position={Position.Left} className="opacity-25" />
</div>
)
}
const BlankNode = () => {
return (
<div className="flex flex-col rounded bg-surface-100 border border-default px-1">
<div className="flex items-start justify-between p-3" style={{ width: NODE_WIDTH / 2 }}>
<div className="flex gap-x-3">
<div className="flex items-center gap-1">
<Plus size={16} />
<span className="text-sm">Add new</span>
</div>
</div>
<Handle type="target" position={Position.Left} className="opacity-25" />
</div>
</div>
)
}
const CTANode = ({ projectRef }: { projectRef: string }) => {
return (
<Card className="w-[570px] p-6">
<CardContent>
<h2 className="text-lg mb-2">Stream database changes to external destinations</h2>
<p className="text-foreground-light mb-2">
Automatically replicate your data to external data warehouses and analytics platforms in
real-time. No manual exports, no lag.
</p>
<p className="text-foreground-light">
We are currently in <span className="text-foreground">private alpha</span> and slowly
onboarding new customers to ensure stable data pipelines. Request access below to join the
waitlist. Read replicas are available now.
</p>
<div className="flex items-center gap-x-2 mt-6">
<Button asChild type="secondary" iconRight={<ArrowUpRight size={16} strokeWidth={1.5} />}>
<Link href="https://forms.supabase.com/pg_replicate" target="_blank" rel="noreferrer">
Request alpha access
</Link>
</Button>
<Button asChild type="default" iconRight={<ArrowRight size={16} strokeWidth={1.5} />}>
<Link href={`/project/${projectRef}/settings/infrastructure?createReplica=true`}>
Create a read replica
</Link>
</Button>
</div>
</CardContent>
</Card>
)
}
@@ -1,4 +1,3 @@
import { useFlag } from 'common'
import { useCheckEntitlements } from 'hooks/misc/useCheckEntitlements'
import { ArrowUpRight } from 'lucide-react'
import Link from 'next/link'
@@ -34,7 +33,6 @@ interface DestinationPanelProps {
export const DestinationPanel = ({ onSuccessCreateReadReplica }: DestinationPanelProps) => {
const enablePgReplicate = useIsETLPrivateAlpha()
const unifiedReplication = useFlag('unifiedReplication')
const { hasAccess: hasETLReplicationAccess } = useCheckEntitlements('replication.etl')
const [urlDestinationType, setDestinationType] = useQueryState(
@@ -97,11 +95,7 @@ export const DestinationPanel = ({ onSuccessCreateReadReplica }: DestinationPane
return (
<>
<Sheet open={visible} onOpenChange={onClose}>
<SheetContent
size="default"
showClose={false}
className={cn(unifiedReplication ? 'md:!w-[850px]' : 'md:!w-[700px]')}
>
<SheetContent size="default" showClose={false} className="md:!w-[850px]">
<div className="flex flex-col h-full" tabIndex={-1}>
<SheetHeader>
<SheetTitle>{editMode ? 'Edit destination' : 'Create a new destination'}</SheetTitle>
@@ -118,7 +112,7 @@ export const DestinationPanel = ({ onSuccessCreateReadReplica }: DestinationPane
{destinationType === 'Read Replica' ? (
<ReadReplicaForm onClose={onClose} onSuccess={() => onSuccessCreateReadReplica?.()} />
) : unifiedReplication && !enablePgReplicate ? (
) : !enablePgReplicate ? (
<SheetSection>
<div className={cn('border rounded-md p-6 flex flex-col gap-y-4')}>
<div className="flex flex-col gap-y-1">
@@ -147,7 +141,7 @@ export const DestinationPanel = ({ onSuccessCreateReadReplica }: DestinationPane
</div>
</div>
</SheetSection>
) : unifiedReplication && replicationNotEnabled ? (
) : replicationNotEnabled ? (
<SheetSection>
<EnableReplicationCallout
className="!p-6"
@@ -1,4 +1,3 @@
import { useFlag } from 'common'
import { AnalyticsBucket, BigQuery, Database } from 'icons'
import { parseAsInteger, parseAsStringEnum, useQueryState } from 'nuqs'
import { Badge, cn, RadioGroupStacked, RadioGroupStackedItem } from 'ui'
@@ -7,13 +6,14 @@ import { useDestinationInformation } from '../useDestinationInformation'
import { useIsETLBigQueryPrivateAlpha, useIsETLIcebergPrivateAlpha } from '../useIsETLPrivateAlpha'
import { DestinationType } from './DestinationPanel.types'
import { InlineLink } from '@/components/ui/InlineLink'
import { useIsFeatureEnabled } from '@/hooks/misc/useIsFeatureEnabled'
export const DestinationTypeSelection = () => {
const unifiedReplication = useFlag('unifiedReplication')
const etlEnableBigQuery = useIsETLBigQueryPrivateAlpha()
const etlEnableIceberg = useIsETLIcebergPrivateAlpha()
const { infrastructureReadReplicas } = useIsFeatureEnabled(['infrastructure:read_replicas'])
const numberOfTypes = [unifiedReplication, etlEnableBigQuery, etlEnableIceberg].filter(
const numberOfTypes = [infrastructureReadReplicas, etlEnableBigQuery, etlEnableIceberg].filter(
Boolean
).length
@@ -57,7 +57,7 @@ export const DestinationTypeSelection = () => {
'[&>button:first-of-type]:!rounded-l-lg [&>button:last-of-type]:!rounded-r-lg'
)}
>
{((!editMode && unifiedReplication) ||
{((!editMode && infrastructureReadReplicas) ||
(editMode && destinationType === 'Read Replica')) && (
<RadioGroupStackedItem
label=""
@@ -85,7 +85,7 @@ export const DestinationTypeSelection = () => {
<div className="flex flex-col gap-y-0.5 text-sm text-left">
<div className="flex items-center gap-x-2">
<p>BigQuery</p>
{unifiedReplication && <Badge>Alpha</Badge>}
<Badge>Alpha</Badge>
</div>
<p className="text-foreground-lighter">
Send data to Google Cloud's data warehouse for analytics and business intelligence
@@ -108,7 +108,7 @@ export const DestinationTypeSelection = () => {
<div className="flex flex-col gap-y-0.5 text-sm text-left">
<div className="flex items-center gap-x-2">
<p>Analytics Bucket</p>
{unifiedReplication && <Badge>Alpha</Badge>}
<Badge>Alpha</Badge>
</div>
<p className="text-foreground-lighter">
Send data to Apache Iceberg tables in your Supabase Storage for flexible analytics
@@ -1,13 +1,11 @@
import { useQueryClient } from '@tanstack/react-query'
import { useFlag, useParams } from 'common'
import { useParams } from 'common'
import { AlertError } from 'components/ui/AlertError'
import { DocsButton } from 'components/ui/DocsButton'
import { useReplicationDestinationsQuery } from 'data/replication/destinations-query'
import { replicationKeys } from 'data/replication/keys'
import { fetchReplicationPipelineVersion } from 'data/replication/pipeline-version-query'
import { useReplicationPipelinesQuery } from 'data/replication/pipelines-query'
import { useReplicationSourcesQuery } from 'data/replication/sources-query'
import { useCheckEntitlements } from 'hooks/misc/useCheckEntitlements'
import { DOCS_URL } from 'lib/constants'
import { Plus, Search, X } from 'lucide-react'
import { parseAsStringEnum, useQueryState } from 'nuqs'
@@ -16,13 +14,13 @@ import {
Button,
Card,
CardContent,
cn,
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
cn,
} from 'ui'
import { GenericSkeletonLoader } from 'ui-patterns'
import { Input } from 'ui-patterns/DataInputs/Input'
@@ -31,18 +29,27 @@ import { REPLICA_STATUS } from '../../Settings/Infrastructure/InfrastructureConf
import { DestinationPanel } from './DestinationPanel/DestinationPanel'
import { DestinationType } from './DestinationPanel/DestinationPanel.types'
import { DestinationRow } from './DestinationRow'
import { EnableReplicationCallout } from './EnableReplicationCallout'
import { PIPELINE_ERROR_MESSAGES } from './Pipeline.utils'
import { ReadReplicaRow } from './ReadReplicas/ReadReplicaRow'
import { useIsETLBigQueryPrivateAlpha, useIsETLIcebergPrivateAlpha } from './useIsETLPrivateAlpha'
import { useReadReplicasQuery } from '@/data/read-replicas/replicas-query'
import { useIsFeatureEnabled } from '@/hooks/misc/useIsFeatureEnabled'
export const Destinations = () => {
const queryClient = useQueryClient()
const { ref: projectRef } = useParams()
const { hasAccess: hasETLReplicationAccess, isLoading: isLoadingEntitlement } =
useCheckEntitlements('replication.etl')
const unifiedReplication = useFlag('unifiedReplication')
const etlEnableBigQuery = useIsETLBigQueryPrivateAlpha()
const etlEnableIceberg = useIsETLIcebergPrivateAlpha()
const { infrastructureReadReplicas } = useIsFeatureEnabled(['infrastructure:read_replicas'])
const newDestinationDefaultType = infrastructureReadReplicas
? 'Read Replica'
: etlEnableBigQuery
? 'BigQuery'
: etlEnableIceberg
? 'Analytics Bucket'
: null
const prefetchedRef = useRef(false)
const [filterString, setFilterString] = useState<string>('')
@@ -74,18 +81,6 @@ export const Destinations = () => {
? readReplicas
: readReplicas.filter((replica) => replica.identifier.includes(filterString.toLowerCase()))
const {
data: sourcesData,
error: sourcesError,
isPending: isSourcesLoading,
isError: isSourcesError,
isSuccess: isSourcesSuccess,
} = useReplicationSourcesQuery({
projectRef,
})
const sourceId = sourcesData?.sources.find((s) => s.name === projectRef)?.id
const replicationNotEnabled = isSourcesSuccess && !sourceId
const {
data: destinationsData,
error: destinationsError,
@@ -104,19 +99,12 @@ export const Destinations = () => {
destination.name.toLowerCase().includes(filterString.toLowerCase())
)
const {
data: pipelinesData,
error: pipelinesError,
isPending: isPipelinesLoading,
isError: isPipelinesError,
isSuccess: isPipelinesSuccess,
} = useReplicationPipelinesQuery({
const { data: pipelinesData, isSuccess: isPipelinesSuccess } = useReplicationPipelinesQuery({
projectRef,
})
const isLoading =
isSourcesLoading || isDestinationsLoading || isDatabasesLoading || isLoadingEntitlement
const hasErrorsFetchingData = isSourcesError || isDestinationsError || isDatabasesError
const isLoading = isDestinationsLoading || isDatabasesLoading
const hasErrorsFetchingData = isDestinationsError || isDatabasesError
useEffect(() => {
if (
@@ -184,15 +172,14 @@ export const Destinations = () => {
/>
</div>
<div className="flex items-center gap-x-2">
{(unifiedReplication || !!sourceId) && (
<Button
type="default"
icon={<Plus />}
onClick={() => setDestinationType('Read Replica')}
>
Add destination
</Button>
)}
<Button
type="default"
icon={<Plus />}
disabled={!newDestinationDefaultType}
onClick={() => setDestinationType(newDestinationDefaultType)}
>
Add destination
</Button>
<DocsButton href={`${DOCS_URL}/guides/database/replication`} />
</div>
</div>
@@ -201,16 +188,14 @@ export const Destinations = () => {
<div className="w-full overflow-hidden overflow-x-auto flex flex-col gap-y-4">
{hasErrorsFetchingData && (
<AlertError
error={sourcesError || destinationsError || databasesError}
error={destinationsError || databasesError}
subject={PIPELINE_ERROR_MESSAGES.RETRIEVE_DESTINATIONS}
/>
)}
{isLoading ? (
<GenericSkeletonLoader />
) : !unifiedReplication && replicationNotEnabled ? (
<EnableReplicationCallout hasAccess={hasETLReplicationAccess} />
) : (unifiedReplication && hasReplicas) || hasDestinations ? (
) : hasReplicas || hasDestinations ? (
<Card>
<CardContent className="p-0">
<Table>
@@ -231,16 +216,15 @@ export const Destinations = () => {
</TableRow>
</TableHeader>
<TableBody>
{unifiedReplication &&
filteredReplicas.map((replica) => {
return (
<ReadReplicaRow
key={replica.identifier}
replica={replica}
onUpdateReplica={() => setStatusRefetchInterval(5000)}
/>
)
})}
{filteredReplicas.map((replica) => {
return (
<ReadReplicaRow
key={replica.identifier}
replica={replica}
onUpdateReplica={() => setStatusRefetchInterval(5000)}
/>
)
})}
{filteredDestinations.map((destination) => (
<DestinationRow key={destination.id} destinationId={destination.id} />
@@ -249,7 +233,7 @@ export const Destinations = () => {
{!isLoading &&
filteredDestinations.length === 0 &&
filteredReplicas.length === 0 &&
((unifiedReplication && hasReplicas) || hasDestinations) && (
(hasReplicas || hasDestinations) && (
<TableRow>
<TableCell colSpan={5}>
<p>No results found</p>
@@ -273,15 +257,10 @@ export const Destinations = () => {
'flex flex-col px-16 rounded-lg justify-center items-center py-8 mt-4'
)}
>
<h4>
{unifiedReplication
? 'Replication keeps your data in sync across systems'
: 'Create your first destination'}
</h4>
<h4>Replication keeps your data in sync across systems</h4>
<p className="text-foreground-light text-sm text-balance text-center mt-1">
{unifiedReplication
? 'Deploy read replicas for lower latency and better resource management, or capture database changes to external platforms for real-time data pipelines.'
: 'Destinations are external platforms where your database changes are automatically sent. Connect to various data warehouses and analytics platforms to enable real-time data pipelines.'}
Deploy read replicas for lower latency and better resource management, or capture
database changes to external platforms for real-time data pipelines.
</p>
<Button
icon={<Plus />}
@@ -14,6 +14,8 @@ import { ReplicationPipelineStatusResponse } from '@/data/replication/pipeline-s
import { useReplicationPipelinesQuery } from '@/data/replication/pipelines-query'
import { timeout } from '@/lib/helpers'
import 'reactflow/dist/style.css'
export const ReplicationDiagram = () => {
return (
<ReactFlowProvider>
@@ -11,7 +11,7 @@ import { useIsOrioleDb, useSelectedProjectQuery } from 'hooks/misc/useSelectedPr
import { DOCS_URL, PROJECT_STATUS } from 'lib/constants'
import Link from 'next/link'
import { ReactFlowProvider } from 'reactflow'
import { Badge, Tooltip, TooltipContent, TooltipTrigger, cn } from 'ui'
import { Badge, cn, Tooltip, TooltipContent, TooltipTrigger } from 'ui'
import { InstanceConfiguration } from '../Settings/Infrastructure/InfrastructureConfiguration/InstanceConfiguration'
@@ -1,573 +0,0 @@
import { ChevronDown } from 'lucide-react'
import Link from 'next/link'
import { useEffect, useMemo, useState } from 'react'
import { toast } from 'sonner'
import { SupportCategories } from '@supabase/shared-types/out/constants'
import { useParams } from 'common'
import {
calculateIOPSPrice,
calculateThroughputPrice,
} from 'components/interfaces/DiskManagement/DiskManagement.utils'
import {
DISK_PRICING,
DiskType,
} from 'components/interfaces/DiskManagement/ui/DiskManagement.constants'
import { SupportLink } from 'components/interfaces/Support/SupportLink'
import { DocsButton } from 'components/ui/DocsButton'
import { useDiskAttributesQuery } from 'data/config/disk-attributes-query'
import { useEnablePhysicalBackupsMutation } from 'data/database/enable-physical-backups-mutation'
import { useOverdueInvoicesQuery } from 'data/invoices/invoices-overdue-query'
import { useProjectDetailQuery } from 'data/projects/project-detail-query'
import { Region, useReadReplicaSetUpMutation } from 'data/read-replicas/replica-setup-mutation'
import {
MAX_REPLICAS_ABOVE_XL,
MAX_REPLICAS_BELOW_XL,
useReadReplicasQuery,
} from 'data/read-replicas/replicas-query'
import { useProjectAddonsQuery } from 'data/subscriptions/project-addons-query'
import { useCheckEntitlements } from 'hooks/misc/useCheckEntitlements'
import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization'
import { useIsAwsK8sCloudProvider, useSelectedProjectQuery } from 'hooks/misc/useSelectedProject'
import { AWS_REGIONS_DEFAULT, BASE_PATH, DOCS_URL } from 'lib/constants'
import { formatCurrency } from 'lib/helpers'
import type { AWS_REGIONS_KEYS } from 'shared-data'
import { AWS_REGIONS } from 'shared-data'
import {
AlertDescription_Shadcn_,
AlertTitle_Shadcn_,
Alert_Shadcn_,
Button,
CollapsibleContent_Shadcn_,
CollapsibleTrigger_Shadcn_,
Collapsible_Shadcn_,
Listbox,
SidePanel,
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
WarningIcon,
cn,
} from 'ui'
import { AVAILABLE_REPLICA_REGIONS } from './InstanceConfiguration.constants'
// [Joshen] FYI this is purely for AWS only, need to update to support Fly eventually
interface DeployNewReplicaPanelProps {
visible: boolean
selectedDefaultRegion?: AWS_REGIONS_KEYS
onSuccess: () => void
onClose: () => void
}
const DeployNewReplicaPanel = ({
visible,
selectedDefaultRegion,
onSuccess,
onClose,
}: DeployNewReplicaPanelProps) => {
const { ref: projectRef } = useParams()
const { data: project } = useSelectedProjectQuery()
const { data: org } = useSelectedOrganizationQuery()
const { hasAccess: hasReadReplicaAccess } = useCheckEntitlements('instances.read_replicas')
const { data } = useReadReplicasQuery({ projectRef })
const { data: addons, isSuccess } = useProjectAddonsQuery({ projectRef })
const { data: diskConfiguration } = useDiskAttributesQuery({ projectRef })
const isNotOnHigherPlan = useMemo(
() => !['team', 'enterprise', 'platform'].includes(org?.plan.id ?? ''),
[org]
)
const { data: allOverdueInvoices } = useOverdueInvoicesQuery({
enabled: isNotOnHigherPlan,
})
const overdueInvoices = (allOverdueInvoices ?? []).filter(
(x) => x.organization_id === project?.organization_id
)
const hasOverdueInvoices = overdueInvoices.length > 0 && isNotOnHigherPlan
const isAwsK8s = useIsAwsK8sCloudProvider()
// Opting for useState temporarily as Listbox doesn't seem to work with react-hook-form yet
const [defaultRegion] = Object.entries(AWS_REGIONS).find(
([_, name]) => name === AWS_REGIONS_DEFAULT
) ?? ['ap-southeast-1']
// Will be following the primary's compute size for the time being
const defaultCompute =
addons?.selected_addons.find((addon) => addon.type === 'compute_instance')?.variant
.identifier ?? 'ci_micro'
// @ts-ignore
const { size_gb, type, throughput_mbps, iops } = diskConfiguration?.attributes ?? {}
const showNewDiskManagementUI = project?.cloud_provider === 'AWS'
const readReplicaDiskSizes = (size_gb ?? 0) * 1.25
const additionalCostDiskSize =
readReplicaDiskSizes * (DISK_PRICING[type as DiskType]?.storage ?? 0)
const additionalCostIOPS = calculateIOPSPrice({
oldStorageType: type as DiskType,
newStorageType: type as DiskType,
oldProvisionedIOPS: 0,
newProvisionedIOPS: iops ?? 0,
numReplicas: 0,
}).newPrice
const additionalCostThroughput =
type === 'gp3'
? calculateThroughputPrice({
storageType: type as DiskType,
newThroughput: throughput_mbps ?? 0,
oldThroughput: 0,
numReplicas: 0,
}).newPrice
: 0
const [refetchInterval, setRefetchInterval] = useState<number | false>(false)
const [selectedRegion, setSelectedRegion] = useState<string>(defaultRegion)
const [selectedCompute, setSelectedCompute] = useState(defaultCompute)
const { data: projectDetail, isSuccess: isProjectDetailSuccess } = useProjectDetailQuery(
{ ref: projectRef },
{
refetchInterval,
refetchOnWindowFocus: false,
}
)
useEffect(() => {
if (!isProjectDetailSuccess) return
if (projectDetail.is_physical_backups_enabled) {
setRefetchInterval(false)
}
}, [projectDetail?.is_physical_backups_enabled, isProjectDetailSuccess])
const { mutate: enablePhysicalBackups, isPending: isEnabling } = useEnablePhysicalBackupsMutation(
{
onSuccess: () => {
toast.success(
'Physical backups are currently being enabled, please check back in a few minutes!'
)
setRefetchInterval(5000)
},
}
)
const { mutate: setUpReplica, isPending: isSettingUp } = useReadReplicaSetUpMutation({
onSuccess: () => {
const region = AVAILABLE_REPLICA_REGIONS.find((r) => r.key === selectedRegion)?.name
toast.success(`Spinning up new replica in ${region ?? ' Unknown'}...`)
onSuccess()
onClose()
},
})
const currentPgVersion = Number(
(project?.dbVersion ?? '').split('supabase-postgres-')[1]?.split('.')[0]
)
const maxNumberOfReplicas = ['ci_micro', 'ci_small', 'ci_medium', 'ci_large'].includes(
selectedCompute
)
? MAX_REPLICAS_BELOW_XL
: MAX_REPLICAS_ABOVE_XL
const reachedMaxReplicas =
(data ?? []).filter((db) => db.identifier !== projectRef).length >= maxNumberOfReplicas
const isAWSProvider = project?.cloud_provider === 'AWS'
const isWalgEnabled = project?.is_physical_backups_enabled
const currentComputeAddon = addons?.selected_addons.find(
(addon) => addon.type === 'compute_instance'
)
const isProWithSpendCapEnabled = org?.plan.id === 'pro' && !org.usage_billing_enabled
const isMinimallyOnSmallCompute =
currentComputeAddon?.variant.identifier !== undefined &&
currentComputeAddon?.variant.identifier !== 'ci_micro'
const canDeployReplica =
!reachedMaxReplicas &&
currentPgVersion >= 15 &&
isAWSProvider &&
hasReadReplicaAccess &&
isWalgEnabled &&
currentComputeAddon !== undefined &&
!hasOverdueInvoices &&
!isAwsK8s &&
!isProWithSpendCapEnabled
const computeAddons =
addons?.available_addons.find((addon) => addon.type === 'compute_instance')?.variants ?? []
const selectedComputeMeta = computeAddons.find((addon) => addon.identifier === selectedCompute)
const estComputeMonthlyCost = Math.floor((selectedComputeMeta?.price ?? 0) * 730) // 730 hours in a month
const availableRegions =
process.env.NEXT_PUBLIC_ENVIRONMENT === 'staging'
? AVAILABLE_REPLICA_REGIONS.filter((x) =>
['SOUTHEAST_ASIA', 'CENTRAL_EU', 'EAST_US'].includes(x.key)
)
: AVAILABLE_REPLICA_REGIONS
const onSubmit = async () => {
const regionKey = AWS_REGIONS[selectedRegion as AWS_REGIONS_KEYS].code
if (!projectRef) return console.error('Project is required')
if (!regionKey) return toast.error('Unable to deploy replica: Unsupported region selected')
const primary = data?.find((db) => db.identifier === projectRef)
setUpReplica({ projectRef, region: regionKey as Region, size: primary?.size ?? 't4g.small' })
}
useEffect(() => {
if (visible && isSuccess) {
if (selectedDefaultRegion !== undefined) {
setSelectedRegion(selectedDefaultRegion)
} else if (defaultRegion) {
setSelectedRegion(defaultRegion)
}
if (defaultCompute !== undefined) setSelectedCompute(defaultCompute)
}
}, [visible, isSuccess])
return (
<SidePanel
visible={visible}
onCancel={onClose}
loading={isSettingUp}
disabled={!canDeployReplica}
className={cn(showNewDiskManagementUI ? 'max-w-[500px]' : '')}
header="Deploy a new read replica"
onConfirm={() => onSubmit()}
confirmText="Deploy replica"
>
<SidePanel.Content className="flex flex-col py-4 gap-y-4">
{hasOverdueInvoices ? (
<Alert_Shadcn_>
<WarningIcon />
<AlertTitle_Shadcn_>Your organization has overdue invoices</AlertTitle_Shadcn_>
<AlertDescription_Shadcn_>
<span>
Please resolve all outstanding invoices first before deploying a new read replica
</span>
<div className="mt-3">
<Button asChild type="default">
<Link href={`/org/${org?.slug}/billing#invoices`}>View invoices</Link>
</Button>
</div>
</AlertDescription_Shadcn_>
</Alert_Shadcn_>
) : !isAWSProvider ? (
<Alert_Shadcn_>
<WarningIcon />
<AlertTitle_Shadcn_>
Read replicas are only supported for projects provisioned via AWS
</AlertTitle_Shadcn_>
<AlertDescription_Shadcn_>
<span>
Projects provisioned by other cloud providers currently will not be able to use read
replicas
</span>
<DocsButton
abbrev={false}
className="mt-3"
href={`${DOCS_URL}/guides/platform/read-replicas#prerequisites`}
/>
</AlertDescription_Shadcn_>
</Alert_Shadcn_>
) : isAwsK8s ? (
<Alert_Shadcn_>
<WarningIcon />
<AlertTitle_Shadcn_>
Read replicas are not supported for AWS (Revamped) projects
</AlertTitle_Shadcn_>
<AlertDescription_Shadcn_>
<span>
Projects provisioned by other cloud providers currently will not be able to use read
replicas
</span>
</AlertDescription_Shadcn_>
</Alert_Shadcn_>
) : currentPgVersion < 15 ? (
<Alert_Shadcn_>
<WarningIcon />
<AlertTitle_Shadcn_>
Read replicas can only be deployed with projects on Postgres version 15 and above
</AlertTitle_Shadcn_>
<AlertDescription_Shadcn_>
If you'd like to use read replicas, please contact us via support
</AlertDescription_Shadcn_>
<AlertDescription_Shadcn_ className="mt-2">
<Button type="default">
<SupportLink
queryParams={{
projectRef,
category: SupportCategories.SALES_ENQUIRY,
subject: 'Enquiry on read replicas',
message: `Project DB version: ${project?.dbVersion}`,
}}
>
Contact support
</SupportLink>
</Button>
</AlertDescription_Shadcn_>
</Alert_Shadcn_>
) : !isMinimallyOnSmallCompute ? (
<Alert_Shadcn_>
<WarningIcon />
<AlertTitle_Shadcn_>
Project required to at least be on a Small compute
</AlertTitle_Shadcn_>
<AlertDescription_Shadcn_>
<span>
This is to ensure that read replicas can keep up with the primary databases'
activities.
</span>
<div className="flex items-center gap-x-2 mt-3">
<Button asChild type="default">
<Link
href={
hasReadReplicaAccess
? `/project/${projectRef}/settings/compute-and-disk`
: `/org/${org?.slug}/billing?panel=subscriptionPlan&source=deployNewReplicaPanelSmallCompute`
}
>
{hasReadReplicaAccess ? 'Change compute size' : 'Upgrade to Pro'}
</Link>
</Button>
<DocsButton
abbrev={false}
href={`${DOCS_URL}/guides/platform/read-replicas#prerequisites`}
/>
</div>
</AlertDescription_Shadcn_>
</Alert_Shadcn_>
) : !isWalgEnabled ? (
<Alert_Shadcn_>
<WarningIcon />
<AlertTitle_Shadcn_>
{refetchInterval !== false
? 'Physical backups are currently being enabled'
: 'Physical backups are required to deploy replicas'}
</AlertTitle_Shadcn_>
{refetchInterval === false && (
<AlertDescription_Shadcn_ className="mb-2">
Physical backups are used under the hood to spin up read replicas for your project.
</AlertDescription_Shadcn_>
)}
<AlertDescription_Shadcn_>
{refetchInterval !== false
? 'This warning will go away once physical backups have been enabled - check back in a few minutes!'
: 'Enabling physical backups will take a few minutes, after which you will be able to deploy read replicas.'}
</AlertDescription_Shadcn_>
{refetchInterval !== false ? (
<AlertDescription_Shadcn_ className="mt-2">
You may start deploying read replicas thereafter once this is completed.
</AlertDescription_Shadcn_>
) : (
<AlertDescription_Shadcn_ className="flex items-center gap-x-2 mt-3">
<Button
type="default"
loading={isEnabling}
disabled={isEnabling}
onClick={() => {
if (projectRef) enablePhysicalBackups({ ref: projectRef })
}}
>
Enable physical backups
</Button>
<DocsButton
abbrev={false}
href={`${DOCS_URL}/guides/platform/read-replicas#how-are-read-replicas-made`}
/>
</AlertDescription_Shadcn_>
)}
</Alert_Shadcn_>
) : isProWithSpendCapEnabled ? (
<Alert_Shadcn_>
<WarningIcon />
<AlertTitle_Shadcn_>
Spend cap needs to be disabled to deploy replicas
</AlertTitle_Shadcn_>
<AlertDescription_Shadcn_>
<span>
Launching a replica incurs additional disk size that will exceed the plan's quota.
Disable the spend cap first to allow overages before launching a replica.
</span>
<div className="flex items-center gap-x-2 mt-3">
<Button asChild type="default">
<Link href={`/org/${org?.slug}/billing?panel=costControl`}>
Disable spend cap
</Link>
</Button>
</div>
</AlertDescription_Shadcn_>
</Alert_Shadcn_>
) : reachedMaxReplicas ? (
<Alert_Shadcn_>
<WarningIcon />
<AlertTitle_Shadcn_>
You can only deploy up to {maxNumberOfReplicas} read replicas at once
</AlertTitle_Shadcn_>
<AlertDescription_Shadcn_>
If you'd like to spin up another read replica, please drop an existing replica first.
</AlertDescription_Shadcn_>
{maxNumberOfReplicas === MAX_REPLICAS_BELOW_XL && (
<>
<AlertDescription_Shadcn_>
<span>
Alternatively, you may deploy up to{' '}
<span className="text-foreground">{MAX_REPLICAS_ABOVE_XL}</span> replicas if
your project is on an XL compute or higher.
</span>
<div className="flex items-center gap-x-2 mt-3">
<Button asChild type="default">
<Link
href={
hasReadReplicaAccess
? `/project/${projectRef}/settings/compute-and-disk`
: `/org/${org?.slug}/billing?panel=subscriptionPlan&source=deployNewReplicaPanelMaxReplicas`
}
>
Upgrade compute size
</Link>
</Button>
</div>
</AlertDescription_Shadcn_>
</>
)}
</Alert_Shadcn_>
) : null}
<div className="flex flex-col gap-y-6 mt-2">
<Listbox
size="small"
id="region"
name="region"
disabled={!canDeployReplica}
value={selectedRegion}
onChange={setSelectedRegion}
label="Select a region to deploy your read replica in"
>
{availableRegions.map((region) => (
<Listbox.Option
key={region.key}
label={region.name}
value={region.key}
addOnBefore={() => (
<img
alt="region icon"
className="w-5 rounded-sm"
src={`${BASE_PATH}/img/regions/${region.region}.svg`}
/>
)}
>
<p className="flex items-center gap-x-2">
<span>{region.name}</span>
<span className="text-xs text-foreground-lighter font-mono">{region.region}</span>
</p>
</Listbox.Option>
))}
</Listbox>
<div className="flex flex-col gap-y-2">
{showNewDiskManagementUI ? (
<>
<Collapsible_Shadcn_>
<CollapsibleTrigger_Shadcn_ className="w-full flex items-center justify-between [&[data-state=open]>svg]:!-rotate-180">
<p className="text-sm text-left">
New replica will cost an additional{' '}
<span translate="no">
{formatCurrency(
estComputeMonthlyCost +
additionalCostDiskSize +
Number(additionalCostIOPS) +
Number(additionalCostThroughput)
)}
/month
</span>
</p>
<ChevronDown size={14} className="transition" />
</CollapsibleTrigger_Shadcn_>
<CollapsibleContent_Shadcn_ className="flex flex-col gap-y-1 mt-1">
<p className="text-foreground-light text-sm">
Read replicas will match the compute size of your primary database and will
include 25% more disk size than the primary database to accommodate WAL files.
</p>
<p className="text-foreground-light text-sm">
The additional cost for the replica breaks down to:
</p>
<Table>
<TableHeader className="font-mono uppercase text-xs [&_th]:h-auto [&_th]:pb-2 [&_th]:pt-4">
<TableRow>
<TableHead className="w-[140px] pl-0">Item</TableHead>
<TableHead>Description</TableHead>
<TableHead className="text-right pr-0">Cost (/month)</TableHead>
</TableRow>
</TableHeader>
<TableBody className="[&_td]:py-0 [&_tr]:h-[50px] [&_tr]:border-dotted">
<TableRow>
<TableCell className="pl-0">Compute size</TableCell>
<TableCell>{selectedComputeMeta?.name}</TableCell>
<TableCell className="text-right font-mono pr-0" translate="no">
{formatCurrency(estComputeMonthlyCost)}
</TableCell>
</TableRow>
<TableRow>
<TableCell className="pl-0">Disk size</TableCell>
<TableCell>
{((size_gb ?? 0) * 1.25).toLocaleString()} GB ({type})
</TableCell>
<TableCell className="text-right font-mono pr-0" translate="no">
{formatCurrency(additionalCostDiskSize)}
</TableCell>
</TableRow>
<TableRow>
<TableCell className="pl-0">IOPS</TableCell>
<TableCell>{iops?.toLocaleString()} IOPS</TableCell>
<TableCell className="text-right font-mono pr-0" translate="no">
{formatCurrency(+additionalCostIOPS)}
</TableCell>
</TableRow>
{type === 'gp3' && (
<TableRow>
<TableCell className="pl-0">Throughput</TableCell>
<TableCell>{throughput_mbps?.toLocaleString()} MB/s</TableCell>
<TableCell className="text-right font-mono pr-0">
{formatCurrency(+additionalCostThroughput)}
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</CollapsibleContent_Shadcn_>
</Collapsible_Shadcn_>
</>
) : (
<p className="text-foreground-light text-sm">
Read replicas will be on the same compute size as your primary database. Deploying a
read replica on the{' '}
<span className="text-foreground">{selectedComputeMeta?.name}</span> size incurs
additional{' '}
<span className="text-foreground" translate="no">
{selectedComputeMeta?.price_description}
</span>
.
</p>
)}
<p className="text-foreground-light text-sm">
Read more about{' '}
<Link
href={`${DOCS_URL}/guides/platform/manage-your-usage/read-replicas`}
target="_blank"
rel="noreferrer"
className="underline hover:text-foreground transition"
>
billing
</Link>{' '}
for read replicas.
</p>
</div>
</div>
</SidePanel.Content>
</SidePanel>
)
}
export default DeployNewReplicaPanel
@@ -5,6 +5,7 @@ import { useTheme } from 'next-themes'
import Link from 'next/link'
import { useEffect, useMemo, useState } from 'react'
import ReactFlow, { Background, Edge, ReactFlowProvider, useReactFlow } from 'reactflow'
import 'reactflow/dist/style.css'
import { useParams } from 'common'
@@ -24,17 +25,17 @@ import {
useSelectedProjectQuery,
} from 'hooks/misc/useSelectedProject'
import { timeout } from 'lib/helpers'
import { type AWS_REGIONS_KEYS } from 'shared-data'
import { useRouter } from 'next/router'
import {
Button,
cn,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
cn,
} from 'ui'
import DeployNewReplicaPanel from './DeployNewReplicaPanel'
import DropAllReplicasConfirmationModal from './DropAllReplicasConfirmationModal'
import { DropReplicaConfirmationModal } from './DropReplicaConfirmationModal'
import { SmoothstepEdge } from './Edge'
@@ -43,13 +44,13 @@ import { addRegionNodes, generateNodes, getDagreGraphLayout } from './InstanceCo
import { LoadBalancerNode, PrimaryNode, RegionNode, ReplicaNode } from './InstanceNode'
import MapView from './MapView'
import { RestartReplicaConfirmationModal } from './RestartReplicaConfirmationModal'
import { useShowNewReplicaPanel } from './use-show-new-replica'
interface InstanceConfigurationUIProps {
diagramOnly?: boolean
}
const InstanceConfigurationUI = ({ diagramOnly = false }: InstanceConfigurationUIProps) => {
const router = useRouter()
const reactFlow = useReactFlow()
const isOrioleDb = useIsOrioleDb()
const { resolvedTheme } = useTheme()
@@ -58,12 +59,11 @@ const InstanceConfigurationUI = ({ diagramOnly = false }: InstanceConfigurationU
const isAws = useIsAwsCloudProvider()
const { infrastructureReadReplicas } = useIsFeatureEnabled(['infrastructure:read_replicas'])
const newReplicaURL = `/project/${projectRef}/database/replication?type=Read+Replica`
const [view, setView] = useState<'flow' | 'map'>('flow')
const [showDeleteAllModal, setShowDeleteAllModal] = useState(false)
const { showNewReplicaPanel, setShowNewReplicaPanel } = useShowNewReplicaPanel()
const [refetchInterval, setRefetchInterval] = useState<number | false>(10000)
const [newReplicaRegion, setNewReplicaRegion] = useState<AWS_REGIONS_KEYS>()
const [selectedReplicaToDrop, setSelectedReplicaToDrop] = useState<Database>()
const [selectedReplicaToRestart, setSelectedReplicaToRestart] = useState<Database>()
@@ -86,6 +86,7 @@ const InstanceConfigurationUI = ({ diagramOnly = false }: InstanceConfigurationU
() => partition(data ?? [], (db) => db.identifier === projectRef),
[data, projectRef]
)
const numReplicas = useMemo(() => data?.length ?? 0, [data])
const { data: replicasStatuses, isSuccess: isSuccessReplicasStatuses } =
useReadReplicasStatusesQuery(
@@ -96,7 +97,6 @@ const InstanceConfigurationUI = ({ diagramOnly = false }: InstanceConfigurationU
}
)
const numReplicas = useMemo(() => data?.length ?? 0, [data])
useEffect(() => {
if (!isSuccessReplicasStatuses) return
const refetch = async () => {
@@ -241,10 +241,10 @@ const InstanceConfigurationUI = ({ diagramOnly = false }: InstanceConfigurationU
<div className="z-10 absolute top-4 right-4 flex items-center justify-center gap-x-2">
<div className="flex items-center justify-center">
<ButtonTooltip
asChild
type="default"
disabled={!canManageReplicas || isOrioleDb}
className={cn(replicas.length > 0 ? 'rounded-r-none' : '')}
onClick={() => setShowNewReplicaPanel(true)}
tooltip={{
content: {
side: 'bottom',
@@ -256,7 +256,7 @@ const InstanceConfigurationUI = ({ diagramOnly = false }: InstanceConfigurationU
},
}}
>
Deploy a new replica
<Link href={newReplicaURL}>Deploy a new replica</Link>
</ButtonTooltip>
{replicas.length > 0 && (
<DropdownMenu>
@@ -325,10 +325,7 @@ const InstanceConfigurationUI = ({ diagramOnly = false }: InstanceConfigurationU
</ReactFlow>
) : (
<MapView
onSelectDeployNewReplica={(region) => {
setNewReplicaRegion(region)
setShowNewReplicaPanel(true)
}}
onSelectDeployNewReplica={() => router.push(newReplicaURL)}
onSelectRestartReplica={setSelectedReplicaToRestart}
onSelectDropReplica={setSelectedReplicaToDrop}
/>
@@ -339,16 +336,6 @@ const InstanceConfigurationUI = ({ diagramOnly = false }: InstanceConfigurationU
{!diagramOnly && (
<>
<DeployNewReplicaPanel
visible={showNewReplicaPanel}
selectedDefaultRegion={newReplicaRegion}
onSuccess={() => setRefetchInterval(5000)}
onClose={() => {
setNewReplicaRegion(undefined)
setShowNewReplicaPanel(false)
}}
/>
<DropReplicaConfirmationModal
selectedReplica={selectedReplicaToDrop}
onSuccess={() => setRefetchInterval(5000)}
@@ -1,6 +1,4 @@
import { PermissionAction } from '@supabase/shared-types/out/constants'
import { useParams } from 'common'
import { DropdownMenuItemTooltip } from 'components/ui/DropdownMenuItemTooltip'
import SparkBar from 'components/ui/SparkBar'
import {
DatabaseInitEstimations,
@@ -9,15 +7,12 @@ import {
} from 'data/read-replicas/replicas-status-query'
import { formatDatabaseID } from 'data/read-replicas/replicas.utils'
import dayjs from 'dayjs'
import { useCustomContent } from 'hooks/custom-content/useCustomContent'
import { useAsyncCheckPermissions } from 'hooks/misc/useCheckPermissions'
import { useIsFeatureEnabled } from 'hooks/misc/useIsFeatureEnabled'
import { BASE_PATH } from 'lib/constants'
import { Database, DatabaseBackup, HelpCircle, Loader2, MoreVertical } from 'lucide-react'
import Link from 'next/link'
import { parseAsBoolean, useQueryState } from 'nuqs'
import { parseAsBoolean, parseAsString, useQueryStates } from 'nuqs'
import { Handle, NodeProps, Position } from 'reactflow'
import { useDatabaseSelectorStateSnapshot } from 'state/database-selector'
import {
Badge,
Button,
@@ -41,6 +36,7 @@ import {
REPLICA_STATUS,
} from './InstanceConfiguration.constants'
import { formatSeconds } from './InstanceConfiguration.utils'
import { useDatabaseSelectorStateSnapshot } from '@/state/database-selector'
interface NodeData {
id: string
@@ -111,13 +107,11 @@ export const LoadBalancerNode = ({ data }: NodeProps<LoadBalancerData>) => {
export const PrimaryNode = ({ data }: NodeProps<PrimaryNodeData>) => {
// [Joshen] Just FYI Handles cannot be conditionally rendered
const { provider, region, computeSize, numReplicas, numRegions, hasLoadBalancer } = data
const { region, computeSize, numReplicas, numRegions, hasLoadBalancer } = data
const { projectHomepageShowInstanceSize } = useIsFeatureEnabled([
'project_homepage:show_instance_size',
])
const { infraAwsNimbusLabel } = useCustomContent(['infra:aws_nimbus_label'])
const providerLabel = provider === 'AWS_NIMBUS' ? infraAwsNimbusLabel : provider
return (
<>
@@ -183,23 +177,17 @@ export const PrimaryNode = ({ data }: NodeProps<PrimaryNodeData>) => {
}
export const ReplicaNode = ({ data }: NodeProps<ReplicaNodeData>) => {
const {
id,
region,
computeSize,
status,
inserted_at,
onSelectRestartReplica,
onSelectDropReplica,
} = data
const { ref } = useParams()
const dbSelectorState = useDatabaseSelectorStateSnapshot()
const { can: canManageReplicas } = useAsyncCheckPermissions(PermissionAction.CREATE, 'projects')
const { id, region, computeSize, status, inserted_at } = data
const { projectHomepageShowInstanceSize } = useIsFeatureEnabled([
'project_homepage:show_instance_size',
])
const [, setShowConnect] = useQueryState('showConnect', parseAsBoolean.withDefault(false))
const state = useDatabaseSelectorStateSnapshot()
const [, setConnect] = useQueryStates({
showConnect: parseAsBoolean.withDefault(false),
source: parseAsString,
})
const { data: databaseStatuses } = useReadReplicasStatusesQuery({ projectRef: ref })
const { replicaInitializationStatus } =
@@ -358,43 +346,20 @@ export const ReplicaNode = ({ data }: NodeProps<ReplicaNodeData>) => {
</DropdownMenuTrigger>
<DropdownMenuContent className="w-40" side="bottom" align="end">
<DropdownMenuItem
disabled={status !== REPLICA_STATUS.ACTIVE_HEALTHY}
className="gap-x-2"
onClick={() => {
setShowConnect(true)
dbSelectorState.setSelectedDatabaseId(id)
setConnect({ showConnect: true, source: id })
state.setSelectedDatabaseId(id)
}}
>
View connection string
</DropdownMenuItem>
<DropdownMenuItem
className="gap-x-2"
disabled={status !== REPLICA_STATUS.ACTIVE_HEALTHY}
>
<Link href={`/project/${ref}/observability/database?db=${id}&chart=replication-lag`}>
View replication lag
<DropdownMenuSeparator />
<DropdownMenuItem className="gap-x-2">
<Link href={`/project/${ref}/database/replication/replica/${id}`}>
Manage replica
</Link>
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem
className="gap-x-2"
onClick={() => onSelectRestartReplica()}
disabled={status !== REPLICA_STATUS.ACTIVE_HEALTHY}
>
Restart replica
</DropdownMenuItem>
<DropdownMenuItemTooltip
className="gap-x-2 !pointer-events-auto"
disabled={!canManageReplicas}
onClick={() => {
if (canManageReplicas) onSelectDropReplica()
}}
tooltip={{
content: { side: 'left', text: 'You need additional permissions to drop replicas' },
}}
>
Drop replica
</DropdownMenuItemTooltip>
</DropdownMenuContent>
</DropdownMenu>
</div>
@@ -1,13 +0,0 @@
import { parseAsBoolean, useQueryState } from 'nuqs'
/**
* @deprecated Can remove after read replicas is fully moved into database replication page
*/
export function useShowNewReplicaPanel() {
const [showNewReplicaPanel, setShowNewReplicaPanel] = useQueryState(
'createReplica',
parseAsBoolean.withDefault(false)
)
return { showNewReplicaPanel, setShowNewReplicaPanel }
}
@@ -1,4 +1,4 @@
import { useFlag, useParams } from 'common'
import { useParams } from 'common'
import { NoticeBar } from 'components/interfaces/DiskManagement/ui/NoticeBar'
import {
ScaffoldContainer,
@@ -19,15 +19,12 @@ import { Admonition } from 'ui-patterns/admonition'
import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader'
import { ProjectUpgradeAlert } from '../General/Infrastructure/ProjectUpgradeAlert'
import { InstanceConfiguration } from './InfrastructureConfiguration/InstanceConfiguration'
import { ReadReplicasWarning, ValidationErrorsWarning } from './UpgradeWarnings'
export const InfrastructureInfo = () => {
const { ref } = useParams()
const { data: project } = useSelectedProjectQuery()
const unifiedReplication = useFlag('unifiedReplication')
const { projectAuthAll: authEnabled, projectSettingsDatabaseUpgrades: showDatabaseUpgrades } =
useIsFeatureEnabled(['project_auth:all', 'project_settings:database_upgrades'])
@@ -73,31 +70,26 @@ export const InfrastructureInfo = () => {
return (
<>
<ScaffoldDivider />
{project?.cloud_provider !== 'FLY' &&
(unifiedReplication ? (
<ScaffoldContainer>
<ScaffoldSection isFullWidth>
<NoticeBar
visible={true}
type="default"
title="Management of read replicas has moved"
description="Read replicas is now managed under Replication in the Database section."
actions={
<Button type="default" asChild>
<Link href={`/project/${ref}/database/replication`} className="!no-underline">
Go to Replication
</Link>
</Button>
}
/>
</ScaffoldSection>
</ScaffoldContainer>
) : (
<>
<InstanceConfiguration />
<ScaffoldDivider />
</>
))}
{project?.cloud_provider !== 'FLY' && (
<ScaffoldContainer>
<ScaffoldSection isFullWidth>
<NoticeBar
visible={true}
type="default"
title="Management of read replicas has moved"
description="Read replicas is now managed under Replication in the Database section."
actions={
<Button type="default" asChild>
<Link href={`/project/${ref}/database/replication`} className="!no-underline">
Go to Replication
</Link>
</Button>
}
/>
</ScaffoldSection>
</ScaffoldContainer>
)}
<ScaffoldContainer>
<ScaffoldSection>
@@ -1,15 +1,14 @@
import Link from 'next/link'
import { ProjectUpgradeEligibilityValidationError } from '@/data/config/project-upgrade-eligibility-query'
import { useFlag, useParams } from 'common'
import { useParams } from 'common'
import { InlineLink } from 'components/ui/InlineLink'
import { DOCS_URL } from 'lib/constants'
import Link from 'next/link'
import { Button } from 'ui'
import { Admonition } from 'ui-patterns/admonition'
import { ProjectUpgradeEligibilityValidationError } from '@/data/config/project-upgrade-eligibility-query'
export const ReadReplicasWarning = ({ latestPgVersion }: { latestPgVersion: string }) => {
const { ref } = useParams()
const unifiedReplication = useFlag('unifiedReplication')
return (
<Admonition
@@ -18,11 +17,9 @@ export const ReadReplicasWarning = ({ latestPgVersion }: { latestPgVersion: stri
title="A newer version of Postgres is available"
description={`You will need to remove all read replicas prior to upgrading your Postgres version to the latest available (${latestPgVersion}).`}
actions={
unifiedReplication ? (
<Button asChild type="default">
<Link href={`/project/${ref}/database/replication`}>Manage read replicas</Link>
</Button>
) : undefined
<Button asChild type="default">
<Link href={`/project/${ref}/database/replication`}>Manage read replicas</Link>
</Button>
}
/>
)
+5 -17
View File
@@ -1,12 +1,10 @@
import { useFlag, useParams } from 'common'
import { useParams } from 'common'
import { Markdown } from 'components/interfaces/Markdown'
import { REPLICA_STATUS } from 'components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/InstanceConfiguration.constants'
import { useShowNewReplicaPanel } from 'components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/use-show-new-replica'
import { useReadReplicasQuery } from 'data/read-replicas/replicas-query'
import { formatDatabaseID, formatDatabaseRegion } from 'data/read-replicas/replicas.utils'
import { useIsFeatureEnabled } from 'hooks/misc/useIsFeatureEnabled'
import { IS_PLATFORM } from 'lib/constants'
import { timeout } from 'lib/helpers'
import { noop } from 'lodash'
import { Check, ChevronDown, Loader2, Plus } from 'lucide-react'
import Link from 'next/link'
@@ -17,18 +15,18 @@ import { useDatabaseSelectorStateSnapshot } from 'state/database-selector'
import {
Button,
ButtonProps,
cn,
Command_Shadcn_,
CommandGroup_Shadcn_,
CommandItem_Shadcn_,
CommandList_Shadcn_,
Command_Shadcn_,
Popover_Shadcn_,
PopoverContent_Shadcn_,
PopoverTrigger_Shadcn_,
Popover_Shadcn_,
ScrollArea,
Tooltip,
TooltipContent,
TooltipTrigger,
cn,
} from 'ui'
interface DatabaseSelectorProps {
@@ -54,9 +52,7 @@ export const DatabaseSelector = ({
const { ref: projectRef } = useParams()
const [open, setOpen] = useState(false)
const [, setShowConnect] = useQueryState('showConnect', parseAsBoolean.withDefault(false))
const { setShowNewReplicaPanel } = useShowNewReplicaPanel()
const unifiedReplication = useFlag('unifiedReplication')
const { infrastructureReadReplicas } = useIsFeatureEnabled(['infrastructure:read_replicas'])
const state = useDatabaseSelectorStateSnapshot()
@@ -74,9 +70,7 @@ export const DatabaseSelector = ({
const selectedAdditionalOption = additionalOptions.find((x) => x.id === selectedDatabaseId)
const newReplicaURL = unifiedReplication
? `/project/${projectRef}/database/replication?type=Read+Replica`
: `/project/${projectRef}/settings/infrastructure`
const newReplicaURL = `/project/${projectRef}/database/replication?type=Read+Replica`
useEffect(() => {
if (_selectedDatabaseId) state.setSelectedDatabaseId(_selectedDatabaseId)
@@ -229,12 +223,6 @@ export const DatabaseSelector = ({
setOpen(false)
// [Joshen] This is used in the Connect UI which is available across all pages
setShowConnect(false)
// [Joshen] Adding a short timeout to compensate for the shift in focus
// the replica panel from a "portal" based component (e.g dialog, sheet, dropdown, etc)
// Although I'd prefer if there's a better way to resolve this
await timeout(50)
if (!unifiedReplication) setShowNewReplicaPanel(true)
}}
className="w-full flex items-center gap-2"
>
@@ -1,11 +1,8 @@
import { useFlag, useParams } from 'common'
import { ReplicationComingSoon } from 'components/interfaces/Database/Replication/ComingSoon'
import { useParams } from 'common'
import { Destinations } from 'components/interfaces/Database/Replication/Destinations'
import { useIsETLPrivateAlpha } from 'components/interfaces/Database/Replication/useIsETLPrivateAlpha'
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 { UnknownInterface } from 'components/ui/UnknownInterface'
import { useIsFeatureEnabled } from 'hooks/misc/useIsFeatureEnabled'
import { PipelineRequestStatusProvider } from 'state/replication-pipeline-request-status'
@@ -15,56 +12,36 @@ import { ReplicationDiagram } from '@/components/interfaces/Database/Replication
const DatabaseReplicationPage: NextPageWithLayout = () => {
const { ref } = useParams()
const enablePgReplicate = useIsETLPrivateAlpha()
const showPgReplicate = useIsFeatureEnabled('database:replication')
const unifiedReplication = useFlag('unifiedReplication')
if (!showPgReplicate) {
return <UnknownInterface urlBack={`/project/${ref}/database/schemas`} />
}
return (
<>
{unifiedReplication || enablePgReplicate ? (
<PipelineRequestStatusProvider>
<ScaffoldContainer>
<ScaffoldSection isFullWidth>
<div className="w-full mb-6">
<div className="flex items-center gap-x-2 mb-1">
<h3 className="text-foreground text-xl prose">Replication</h3>
</div>
<p className="prose text-sm max-w-full">
{unifiedReplication
? 'Deploy read replicas across multiple regions, or replicate database changes to external data warehouses and analytics platforms'
: 'Automatically replicate your database changes to external data warehouses and analytics platforms in real-time'}
</p>
</div>
</ScaffoldSection>
</ScaffoldContainer>
<PipelineRequestStatusProvider>
<ScaffoldContainer>
<ScaffoldSection isFullWidth>
<div className="w-full mb-6">
<div className="flex items-center gap-x-2 mb-1">
<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 replicate database changes to
external data warehouses and analytics platforms
</p>
</div>
</ScaffoldSection>
</ScaffoldContainer>
<ReplicationDiagram />
<ReplicationDiagram />
<ScaffoldContainer>
<ScaffoldSection isFullWidth className="!pt-6">
<Destinations />
</ScaffoldSection>
</ScaffoldContainer>
</PipelineRequestStatusProvider>
) : (
<>
<ScaffoldContainer>
<ScaffoldSection isFullWidth>
<FormHeader
className="[&>div>p]:max-w-full"
title="Replication"
description=" Automatically replicate your database changes to external data warehouses and analytics platforms in real-time"
/>
</ScaffoldSection>
</ScaffoldContainer>
<ReplicationComingSoon />
</>
)}
</>
<ScaffoldContainer>
<ScaffoldSection isFullWidth className="!pt-6">
<Destinations />
</ScaffoldSection>
</ScaffoldContainer>
</PipelineRequestStatusProvider>
)
}