Read replicas fix not removing node from UI when replica is dropped (#23267)

* read replicas fix not removing node from UI when replica is dropped

* Update refresh durationm
This commit is contained in:
Joshen Lim authored and GitHub committed 2024-04-29 16:54:33 +08:00
1 parent efdcc8e631
commit ac8cdcc58c
5 files changed
+72 -52

No files matched your search

@@ -1,18 +1,11 @@
import { useParams } from 'common'
import toast from 'react-hot-toast'
import {
AlertDescription_Shadcn_,
AlertTitle_Shadcn_,
Alert_Shadcn_,
IconAlertTriangle,
Modal,
} from 'ui'
import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal'
import { useReadReplicaRemoveMutation } from 'data/read-replicas/replica-remove-mutation'
import { Database, useReadReplicasQuery } from 'data/read-replicas/replicas-query'
import { useQueryClient } from '@tanstack/react-query'
import { replicaKeys } from 'data/read-replicas/keys'
import { useReadReplicaRemoveMutation } from 'data/read-replicas/replica-remove-mutation'
import { useReadReplicasQuery } from 'data/read-replicas/replicas-query'
import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal'
interface DropAllReplicasConfirmationModalProps {
visible: boolean
@@ -38,20 +31,19 @@ const DropAllReplicasConfirmationModal = ({
const replicas = databases.filter((db) => db.identifier !== projectRef)
await Promise.all(
replicas.map((db) =>
removeReadReplica({ projectRef, identifier: db.identifier, skipInvalidateOnSuccess: true })
removeReadReplica({
projectRef,
identifier: db.identifier,
invalidateReplicaQueries: false,
})
)
)
toast.success(`Tearing down all read replicas`)
queryClient.setQueriesData<any>(replicaKeys.list(projectRef), (old: Database[]) => {
return old.filter((db: Database) => db.identifier === projectRef)
})
queryClient.setQueriesData<any>(replicaKeys.loadBalancers(projectRef), (old: Database[]) => [])
setTimeout(async () => {
await queryClient.invalidateQueries(replicaKeys.list(projectRef))
await queryClient.invalidateQueries(replicaKeys.loadBalancers(projectRef))
}, 5000)
await Promise.all([
queryClient.invalidateQueries(replicaKeys.list(projectRef)),
queryClient.invalidateQueries(replicaKeys.loadBalancers(projectRef)),
])
onSuccess()
onCancel()
@@ -38,7 +38,11 @@ const DropReplicaConfirmationModal = ({
if (!projectRef) return console.error('Project is required')
if (selectedReplica === undefined) return toast.error('No replica selected')
await removeReadReplica({ projectRef, identifier: selectedReplica.identifier })
await removeReadReplica({
projectRef,
identifier: selectedReplica.identifier,
invalidateReplicaQueries: true,
})
}
return (
@@ -1,6 +1,6 @@
import { useParams } from 'common'
import { partition } from 'lodash'
import { Globe2, Loader2, Network } from 'lucide-react'
import { ChevronDown, Globe2, Loader2, Network } from 'lucide-react'
import { useTheme } from 'next-themes'
import { useEffect, useMemo, useRef, useState } from 'react'
import ReactFlow, { Background, Edge, ReactFlowProvider, useReactFlow } from 'reactflow'
@@ -12,7 +12,6 @@ import {
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
IconChevronDown,
} from 'ui'
import AlertError from 'components/ui/AlertError'
@@ -24,11 +23,12 @@ import { timeout } from 'lib/helpers'
import { useSubscriptionPageStateSnapshot } from 'state/subscription-page'
import ComputeInstanceSidePanel from '../../Addons/ComputeInstanceSidePanel'
import DeployNewReplicaPanel from './DeployNewReplicaPanel'
import DropAllReplicasConfirmationModal from './DropAllReplicasConfirmationModal'
import DropReplicaConfirmationModal from './DropReplicaConfirmationModal'
import { REPLICA_STATUS } from './InstanceConfiguration.constants'
import { addRegionNodes, generateNodes, getDagreGraphLayout } from './InstanceConfiguration.utils'
import { LoadBalancerNode, PrimaryNode, RegionNode, ReplicaNode } from './InstanceNode'
import MapView from './MapView'
import DropAllReplicasConfirmationModal from './DropAllReplicasConfirmationModal'
// [Joshen] Just FYI, UI assumes single provider for primary + replicas
// [Joshen] Idea to visualize grouping based on region: https://reactflow.dev/examples/layout/sub-flows
@@ -38,7 +38,7 @@ const InstanceConfigurationUI = () => {
const reactFlow = useReactFlow()
const { resolvedTheme } = useTheme()
const { ref: projectRef } = useParams()
const numComingUp = useRef<number>()
const numTransition = useRef<number>()
const snap = useSubscriptionPageStateSnapshot()
const [view, setView] = useState<'flow' | 'map'>('flow')
@@ -76,13 +76,21 @@ const InstanceConfigurationUI = () => {
{
refetchInterval: refetchInterval as any,
refetchOnWindowFocus: false,
onSuccess: async (data) => {
const comingUpReplicas = data.filter((db) => db.status === 'COMING_UP')
const hasTransientStatus = comingUpReplicas.length > 0
onSuccess: async (res) => {
const fixedStatues = [
REPLICA_STATUS.ACTIVE_HEALTHY,
REPLICA_STATUS.ACTIVE_UNHEALTHY,
REPLICA_STATUS.INIT_READ_REPLICA_FAILED,
]
const replicasInTransition = res.filter((db) => !fixedStatues.includes(db.status))
const hasTransientStatus = replicasInTransition.length > 0
// If any replica's status has changed, refetch databases
if (numComingUp.current !== comingUpReplicas.length) {
numComingUp.current = comingUpReplicas.length
if (
numTransition.current !== replicasInTransition.length ||
res.length !== (data ?? []).length
) {
numTransition.current = replicasInTransition.length
await refetchReplicas()
setTimeout(() => refetchLoadBalancers(), 2000)
}
@@ -192,7 +200,7 @@ const InstanceConfigurationUI = () => {
<DropdownMenuTrigger asChild>
<Button
type="default"
icon={<IconChevronDown size={16} />}
icon={<ChevronDown size={16} />}
className="px-1 rounded-l-none border-l-0"
/>
</DropdownMenuTrigger>
@@ -261,7 +269,7 @@ const InstanceConfigurationUI = () => {
<DeployNewReplicaPanel
visible={showNewReplicaPanel}
selectedDefaultRegion={newReplicaRegion}
onSuccess={() => setRefetchInterval(10000)}
onSuccess={() => setRefetchInterval(5000)}
onClose={() => {
setNewReplicaRegion(undefined)
setShowNewReplicaPanel(false)
@@ -270,13 +278,13 @@ const InstanceConfigurationUI = () => {
<DropReplicaConfirmationModal
selectedReplica={selectedReplicaToDrop}
onSuccess={() => setRefetchInterval(10000)}
onSuccess={() => setRefetchInterval(5000)}
onCancel={() => setSelectedReplicaToDrop(undefined)}
/>
<DropAllReplicasConfirmationModal
visible={showDeleteAllModal}
onSuccess={() => setRefetchInterval(10000)}
onSuccess={() => setRefetchInterval(5000)}
onCancel={() => setShowDeleteAllModal(false)}
/>
@@ -1,6 +1,6 @@
import { useParams } from 'common'
import dayjs from 'dayjs'
import { Database, DatabaseBackup } from 'lucide-react'
import { Database, DatabaseBackup, HelpCircle, Loader2 } from 'lucide-react'
import Link from 'next/link'
import { Handle, NodeProps, Position } from 'reactflow'
import {
@@ -11,11 +11,11 @@ import {
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
IconHelpCircle,
IconMoreVertical,
TooltipContent_Shadcn_,
TooltipTrigger_Shadcn_,
Tooltip_Shadcn_,
cn,
} from 'ui'
import { formatDatabaseID } from 'data/read-replicas/replicas.utils'
@@ -173,6 +173,15 @@ export const ReplicaNode = ({ data }: NodeProps<ReplicaNodeData>) => {
const { ref } = useParams()
const created = dayjs(inserted_at).format('DD MMM YYYY')
const isInTransition = (
[
REPLICA_STATUS.COMING_UP,
REPLICA_STATUS.GOING_DOWN,
REPLICA_STATUS.RESTORING,
REPLICA_STATUS.INIT_READ_REPLICA,
] as string[]
).includes(status)
return (
<>
<Handle
@@ -186,8 +195,19 @@ export const ReplicaNode = ({ data }: NodeProps<ReplicaNodeData>) => {
style={{ width: NODE_WIDTH / 2 - 10 }}
>
<div className="flex gap-x-3">
<div className="w-8 h-8 bg-brand-400 border border-brand-500 rounded-md flex items-center justify-center">
<DatabaseBackup size={16} />
<div
className={cn(
'w-8 h-8 border rounded-md flex items-center justify-center',
status === REPLICA_STATUS.ACTIVE_HEALTHY
? 'bg-brand-400 border-brand-500'
: 'bg-surface-100 border-foreground/20'
)}
>
{isInTransition ? (
<Loader2 className="animate-spin" size={16} />
) : (
<DatabaseBackup size={16} />
)}
</div>
<div className="flex flex-col gap-y-0.5">
<div className="flex items-center gap-x-2">
@@ -203,7 +223,7 @@ export const ReplicaNode = ({ data }: NodeProps<ReplicaNodeData>) => {
<Badge variant="destructive">Init failed</Badge>
<Tooltip_Shadcn_>
<TooltipTrigger_Shadcn_>
<IconHelpCircle />
<HelpCircle size={16} />
</TooltipTrigger_Shadcn_>
<TooltipContent_Shadcn_ side="bottom" className="w-60 text-center">
Replica failed to initialize. Please drop this replica, and spin up a new one.
@@ -212,6 +232,8 @@ export const ReplicaNode = ({ data }: NodeProps<ReplicaNodeData>) => {
</>
) : status === REPLICA_STATUS.COMING_UP ? (
<Badge>Coming up</Badge>
) : status === REPLICA_STATUS.GOING_DOWN ? (
<Badge>Going down</Badge>
) : status === REPLICA_STATUS.RESTORING ? (
<Badge>Restoring</Badge>
) : (
@@ -4,12 +4,11 @@ import { toast } from 'react-hot-toast'
import { post } from 'data/fetchers'
import type { ResponseError } from 'types'
import { replicaKeys } from './keys'
import type { Database } from './replicas-query'
export type ReadReplicaRemoveVariables = {
projectRef: string
identifier: string
skipInvalidateOnSuccess?: boolean
invalidateReplicaQueries: boolean
}
export async function removeReadReplica({ projectRef, identifier }: ReadReplicaRemoveVariables) {
@@ -40,18 +39,13 @@ export const useReadReplicaRemoveMutation = ({
(vars) => removeReadReplica(vars),
{
async onSuccess(data, variables, context) {
const { projectRef, identifier, skipInvalidateOnSuccess } = variables
const { projectRef, invalidateReplicaQueries } = variables
if (skipInvalidateOnSuccess === false) {
// [Joshen] Just FYI, will remove this once API changes to remove the need for optimistic rendering
queryClient.setQueriesData<any>(replicaKeys.list(projectRef), (old: any) => {
return old.filter((db: Database) => db.identifier !== identifier)
})
setTimeout(async () => {
await queryClient.invalidateQueries(replicaKeys.list(projectRef))
await queryClient.invalidateQueries(replicaKeys.loadBalancers(projectRef))
}, 5000)
if (invalidateReplicaQueries) {
await Promise.all([
queryClient.invalidateQueries(replicaKeys.list(projectRef)),
queryClient.invalidateQueries(replicaKeys.loadBalancers(projectRef)),
])
}
await onSuccess?.(data, variables, context)