mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 09:25:06 +03:00
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:
1 parent
ddd94a9c24
commit
a2b40468d0
15 files changed
+133
-1073
No files matched your search
@@ -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>
|
||||
)
|
||||
}
|
||||
+3
-9
@@ -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"
|
||||
|
||||
+6
-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'
|
||||
|
||||
|
||||
-573
@@ -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
|
||||
+10
-23
@@ -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)}
|
||||
|
||||
+15
-50
@@ -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>
|
||||
|
||||
-13
@@ -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>
|
||||
}
|
||||
/>
|
||||
)
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user