mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 09:25:06 +03:00
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:
1 parent
efdcc8e631
commit
ac8cdcc58c
5 files changed
+72
-52
No files matched your search
+12
-20
@@ -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()
|
||||
|
||||
+5
-1
@@ -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 (
|
||||
|
||||
+21
-13
@@ -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)}
|
||||
/>
|
||||
|
||||
|
||||
+27
-5
@@ -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)
|
||||
|
||||
Reference in new issue
Block a user