[replicaId].tsx186 lines · main
1import { useParams } from 'common'
2import { Database } from 'icons'
3import { Loader2, Trash } from 'lucide-react'
4import Link from 'next/link'
5import { useRouter } from 'next/router'
6import { useEffect, useMemo, useState } from 'react'
7import { AWS_REGIONS } from 'shared-data'
8import { Badge, Button } from 'ui'
9import { ShimmeringLoader } from 'ui-patterns'
10
11import { ReadReplicaDetails } from '@/components/interfaces/Database/Replication/ReadReplicas/ReadReplicaDetails'
12import {
13 getIsInTransition,
14 getStatusLabel,
15} from '@/components/interfaces/Database/Replication/ReadReplicas/ReadReplicas.utils'
16import { DropReplicaConfirmationModal } from '@/components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/DropReplicaConfirmationModal'
17import { REPLICA_STATUS } from '@/components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/InstanceConfiguration.constants'
18import { RestartReplicaConfirmationModal } from '@/components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/RestartReplicaConfirmationModal'
19import DatabaseLayout from '@/components/layouts/DatabaseLayout/DatabaseLayout'
20import { DefaultLayout } from '@/components/layouts/DefaultLayout'
21import { PageLayout } from '@/components/layouts/PageLayout/PageLayout'
22import { ScaffoldDescription, ScaffoldTitle } from '@/components/layouts/Scaffold'
23import { ButtonTooltip } from '@/components/ui/ButtonTooltip'
24import CopyButton from '@/components/ui/CopyButton'
25import { useReadReplicasQuery } from '@/data/read-replicas/replicas-query'
26import {
27 ReplicaInitializationStatus,
28 useReadReplicasStatusesQuery,
29} from '@/data/read-replicas/replicas-status-query'
30import type { NextPageWithLayout } from '@/types'
31
32const DatabaseReadReplicaPage: NextPageWithLayout = () => {
33 const router = useRouter()
34 const { ref, replicaId } = useParams()
35
36 const [showConfirmRestart, setShowConfirmRestart] = useState(false)
37 const [showConfirmDrop, setShowConfirmDrop] = useState(false)
38 const [statusRefetchInterval, setStatusRefetchInterval] = useState<number | false>(5000)
39
40 const {
41 data: databases = [],
42 isPending: isLoadingDatabases,
43 isSuccess: isSuccessDatabases,
44 } = useReadReplicasQuery({ projectRef: ref })
45 const replica = databases.find((x) => x.identifier === replicaId)
46 const { identifier, region, status: baseStatus } = replica ?? {}
47
48 const { data: statuses = [], isSuccess: isSuccessReplicasStatuses } =
49 useReadReplicasStatusesQuery({ projectRef: ref }, { refetchInterval: statusRefetchInterval })
50 const replicaStatus = statuses.find((x) => x.identifier === identifier)
51 const status = replicaStatus?.status ?? baseStatus
52 const initStatus = replicaStatus?.replicaInitializationStatus?.status
53
54 const regionLabel = Object.values(AWS_REGIONS).find((x) => x.code === region)?.displayName
55 const statusLabel = useMemo(() => getStatusLabel({ initStatus, status }), [initStatus, status])
56 const isInTransition = useMemo(
57 () => getIsInTransition({ initStatus, status }),
58 [initStatus, status]
59 )
60
61 useEffect(() => {
62 if (!isSuccessReplicasStatuses) return
63
64 const pollReplicas = async () => {
65 const fixedStatuses = [
66 REPLICA_STATUS.ACTIVE_HEALTHY,
67 REPLICA_STATUS.ACTIVE_UNHEALTHY,
68 REPLICA_STATUS.INIT_READ_REPLICA_FAILED,
69 ]
70 const replicasInTransition = statuses.filter((db) => {
71 const { status } = db.replicaInitializationStatus || {}
72 return (
73 !fixedStatuses.includes(db.status) || status === ReplicaInitializationStatus.InProgress
74 )
75 })
76 const hasTransientStatus = replicasInTransition.length > 0
77
78 // If all replicas are active healthy, stop fetching statuses
79 if (!hasTransientStatus && statuses.length === databases.length) {
80 setStatusRefetchInterval(false)
81 }
82 }
83
84 pollReplicas()
85 }, [databases.length, isSuccessReplicasStatuses, statuses])
86
87 return (
88 <PageLayout
89 title={
90 <div className="flex items-center gap-x-3">
91 <ScaffoldTitle>Read Replica</ScaffoldTitle>
92 {isSuccessDatabases && (
93 <>
94 <Badge
95 variant={
96 statusLabel === 'Healthy'
97 ? 'success'
98 : statusLabel === 'Failed'
99 ? 'destructive'
100 : 'default'
101 }
102 >
103 {statusLabel}
104 </Badge>
105 {isInTransition && <Loader2 size={14} className="animate-spin" />}
106 </>
107 )}
108 </div>
109 }
110 subtitle={
111 isLoadingDatabases ? (
112 <ShimmeringLoader className="py-[11px]" />
113 ) : (
114 <div className="flex items-center gap-x-2 mt-0!">
115 <ScaffoldDescription>ID: {identifier}</ScaffoldDescription>
116 <CopyButton iconOnly type="default" text={identifier ?? ''} />
117 </div>
118 )
119 }
120 icon={
121 <div className="shrink-0 w-10 h-10 relative bg-surface-100 border rounded-md flex items-center justify-center">
122 <Database size={20} className="text-foreground-light" />
123 </div>
124 }
125 breadcrumbs={[
126 {
127 label: 'Replication',
128 href: `/project/${ref}/database/replication`,
129 },
130 {
131 label: `Read Replica - ${regionLabel}`,
132 },
133 ]}
134 secondaryActions={
135 <ButtonTooltip
136 type="default"
137 className="w-7"
138 icon={<Trash />}
139 tooltip={{
140 content: { side: 'bottom', text: 'Drop replica' },
141 }}
142 onClick={() => setShowConfirmDrop(true)}
143 />
144 }
145 primaryActions={[
146 <Button asChild key="logs" type="default">
147 <Link
148 href={`/project/${ref}/logs/postgres-logs${!!identifier ? `?db=${identifier}` : ''}`}
149 >
150 View logs
151 </Link>
152 </Button>,
153 <Button
154 key="drop"
155 type="default"
156 disabled={status !== 'ACTIVE_HEALTHY'}
157 onClick={() => setShowConfirmRestart(true)}
158 >
159 Restart replica
160 </Button>,
161 ]}
162 >
163 <ReadReplicaDetails />
164
165 <DropReplicaConfirmationModal
166 selectedReplica={showConfirmDrop ? replica : undefined}
167 onSuccess={() => router.push(`/project/${ref}/database/replication`)}
168 onCancel={() => setShowConfirmDrop(false)}
169 />
170
171 <RestartReplicaConfirmationModal
172 selectedReplica={showConfirmRestart ? replica : undefined}
173 onSuccess={() => setStatusRefetchInterval(5000)}
174 onCancel={() => setShowConfirmRestart(false)}
175 />
176 </PageLayout>
177 )
178}
179
180DatabaseReadReplicaPage.getLayout = (page) => (
181 <DefaultLayout>
182 <DatabaseLayout title="Replication">{page}</DatabaseLayout>
183 </DefaultLayout>
184)
185
186export default DatabaseReadReplicaPage