-
-
- {status === 'coming_up' ? (
+
+ {status === 'coming_up' || isPromoting ? (
) : (
)}
-
-
-
-
Read Replica
- {/* Stable live region so polled status changes are announced */}
-
- {status !== undefined && }
-
-
-
-
+
+
+
+ {promotion !== undefined && }
+ {/* Stable live region so polled status changes are announced */}
+
+ {status !== undefined && }
+
+
+
+
>
)
}
diff --git a/apps/studio/components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/HaTopology.utils.ts b/apps/studio/components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/HaTopology.utils.ts
index 8c1867d6105..418def840f0 100644
--- a/apps/studio/components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/HaTopology.utils.ts
+++ b/apps/studio/components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/HaTopology.utils.ts
@@ -154,11 +154,14 @@ export const buildHaTopology = ({
best === undefined || compareRoutingRules(candidate, best) > 0 ? candidate : best,
undefined
)
+
+ const orderedReplicas = [...primaries.filter((pooler) => pooler !== primary), ...replicas]
+
return {
id,
name: `Shard ${index + 1}`,
primary,
- replicas: [...primaries.filter((pooler) => pooler !== primary), ...replicas],
+ replicas: orderedReplicas,
}
})
diff --git a/apps/studio/components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/InstanceConfiguration.tsx b/apps/studio/components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/InstanceConfiguration.tsx
index 11c57cebee4..9f0418808a2 100644
--- a/apps/studio/components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/InstanceConfiguration.tsx
+++ b/apps/studio/components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/InstanceConfiguration.tsx
@@ -11,6 +11,7 @@ import { HaInstanceConfiguration } from './HaInstanceConfiguration'
import { addRegionNodes, generateNodes } from './InstanceConfiguration.utils'
import { LoadBalancerNode, PrimaryNode, RegionNode, ReplicaNode } from './InstanceNode'
import MapView from './MapView'
+import type { FailoverSimulationPhase } from '@/components/interfaces/ProjectHome/simulateFailover.utils'
import { REPLICA_STATUS } from '@/components/interfaces/Settings/Infrastructure/ReadReplicas/ReadReplicas.constants'
import { AlertError } from '@/components/ui/AlertError'
import { useLoadBalancersQuery } from '@/data/read-replicas/load-balancers-query'
@@ -216,8 +217,15 @@ const InstanceConfigurationUI = () => {
)
}
-export const InstanceConfiguration = () => {
- const { isHighAvailability, isPending } = useHighAvailability()
+export const InstanceConfiguration = ({
+ simulateHighAvailability = false,
+ failoverPhase = 'off',
+}: {
+ simulateHighAvailability?: boolean
+ failoverPhase?: FailoverSimulationPhase
+}) => {
+ const { isHighAvailability: isProjectHighAvailability, isPending } = useHighAvailability()
+ const isHighAvailability = isProjectHighAvailability || simulateHighAvailability
// Wait for the project record so an HA project never briefly mounts the
// standard diagram (and fires its queries) before swapping.
@@ -232,7 +240,14 @@ export const InstanceConfiguration = () => {
return (
- {isHighAvailability ? : }
+ {isHighAvailability ? (
+
+ ) : (
+
+ )}
)
}
diff --git a/apps/studio/components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/useHaPooler.ts b/apps/studio/components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/useHaPooler.ts
index af64b7301b8..fe7e3eddbc2 100644
--- a/apps/studio/components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/useHaPooler.ts
+++ b/apps/studio/components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/useHaPooler.ts
@@ -1,7 +1,7 @@
import { useQuery } from '@tanstack/react-query'
import { useParams } from 'common'
-import { formatCellAsAvailabilityZone, getPoolerStatus } from './HaTopology.utils'
+import { formatCellAsAvailabilityZone, getPoolerStatus, HaPoolerStatus } from './HaTopology.utils'
import { AVAILABLE_REPLICA_REGIONS } from './InstanceConfiguration.constants'
import { haClusterPoolersQueryOptions } from '@/data/ha-admin/ha-cluster-poolers-query'
import { usePrimaryDatabase } from '@/data/read-replicas/replicas-query'
@@ -11,11 +11,21 @@ import { useIsFeatureEnabled } from '@/hooks/misc/useIsFeatureEnabled'
* Subscribes to a single pooler's live state from the shared poolers query, so
* each node/edge updates on its own without re-laying-out the whole diagram.
*/
-export const useHaPooler = ({ cell, name }: { cell?: string; name?: string }) => {
+export const useHaPooler = ({
+ cell,
+ name,
+ enabled = true,
+}: {
+ cell?: string
+ name?: string
+ enabled?: boolean
+}) => {
const { ref: projectRef } = useParams()
+ const queryOptions = haClusterPoolersQueryOptions({ projectRef })
return useQuery({
- ...haClusterPoolersQueryOptions({ projectRef }),
+ ...queryOptions,
+ enabled: enabled && queryOptions.enabled,
select: (data) =>
(data.poolers ?? []).find((pooler) => pooler.id?.cell === cell && pooler.id?.name === name),
})
@@ -27,17 +37,30 @@ export const useHaPooler = ({ cell, name }: { cell?: string; name?: string }) =>
* one project-level size, so the primary's size is correct for every node in
* the cluster.
*/
-export const useHaPoolerCard = ({ cell, name }: { cell?: string; name?: string }) => {
+export const useHaPoolerCard = ({
+ cell,
+ name,
+ statusOverride,
+}: {
+ cell?: string
+ name?: string
+ statusOverride?: HaPoolerStatus
+}) => {
const { ref: projectRef } = useParams()
const { projectHomepageShowInstanceSize } = useIsFeatureEnabled([
'project_homepage:show_instance_size',
])
const { database: primary } = usePrimaryDatabase({ projectRef })
- const { data: pooler } = useHaPooler({ cell, name })
+ const { data: pooler } = useHaPooler({ cell, name, enabled: statusOverride === undefined })
return {
- status: pooler !== undefined ? getPoolerStatus(pooler) : undefined,
+ status:
+ statusOverride !== undefined
+ ? statusOverride
+ : pooler !== undefined
+ ? getPoolerStatus(pooler)
+ : undefined,
availabilityZone: formatCellAsAvailabilityZone(cell),
computeSize: projectHomepageShowInstanceSize ? primary?.size : undefined,
primaryRegion: AVAILABLE_REPLICA_REGIONS.find((r) => primary?.region.includes(r.region)),
diff --git a/apps/studio/styles/globals.css b/apps/studio/styles/globals.css
index 7c2183b1fc0..60512c809d3 100644
--- a/apps/studio/styles/globals.css
+++ b/apps/studio/styles/globals.css
@@ -61,6 +61,7 @@
--animate-chevron-up: chevron-up 2s ease-in-out infinite;
--animate-sway: sway 3s cubic-bezier(0.4, 0, 0.6, 1) infinite;
--animate-marketplace-featured-progress: marketplace-featured-progress linear forwards;
+ --animate-ha-primary-fail-flash: ha-primary-fail-flash 0.7s ease-in-out both;
/* font sizing and weights optimized for Inter */
--text-sm: 0.8125rem;
@@ -118,6 +119,26 @@
}
}
+@keyframes ha-primary-fail-flash {
+ 0%,
+ 28% {
+ background-color: var(--background-surface-100);
+ border-color: var(--border-color-default);
+ box-shadow: 0 0 0 0 transparent;
+ }
+ 14%,
+ 42% {
+ background-color: var(--color-destructive-200);
+ border-color: var(--color-destructive-400);
+ box-shadow: 0 0 0 3px hsl(var(--destructive-600) / 0.4);
+ }
+ 100% {
+ background-color: var(--color-destructive-200);
+ border-color: var(--color-destructive-400);
+ box-shadow: 0 0 0 0 transparent;
+ }
+}
+
@keyframes chevron-up {
0%,
100% {