Files
supabase/apps/studio/components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/HaInstanceConfiguration.tsx
Alaister Young a14fc04937 [MUL-1475] fix(studio): show setup state on HA topology diagram during boot (#49786)
While an HA (Multigres) project is provisioning, the `/ha-admin`
topology endpoints fail or return an empty topology as a matter of
course — the cluster topology diagram rendered that as a hard "Failed to
retrieve cluster topology" error (or the "Cluster topology unavailable"
contact-support state). The diagram now checks the project status and,
while the project is building (`COMING_UP`/`UNKNOWN`, same pair
`ProjectLayout` treats as booting), shows a "Setting up project" empty
state instead. Both the project-detail query (self-polls while booting)
and the ha-admin queries (30s interval) keep refetching, so the diagram
appears on its own once boot completes.

Once the project is past provisioning, genuine errors and the
empty-topology state surface exactly as before.

<img width="1491" height="769" alt="mul1475-setting-up-state-wide"
src="https://github.com/user-attachments/assets/3f095634-9939-41cd-88c3-0849cbad7374"
/>

**Added:**
- Component tests for the four states: booting + error, booting + empty
(→ setup state), running + error (→ error alert), running + empty (→
unavailable state)

Addresses
[MUL-1475](https://linear.app/supabase/issue/MUL-1475/polish-infra-diagram).

## To test

- On an HA project mid-provisioning (or simulate: dev toolbar
project-status override → `COMING_UP`, with `/ha-admin` requests
failing), open Settings → Infrastructure — the topology panel shows
"Setting up project" with a spinner, not the error alert
- On a healthy HA project, the topology diagram renders as before; if
`/ha-admin` genuinely fails there, the error alert still shows
- `pnpm --filter studio exec vitest run
components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/HaInstanceConfiguration.test.tsx`

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Added a “Setting up project” state while projects are provisioning or
their status is unavailable.
* Prevents premature topology errors or unavailable messages during
project setup.
* Added accessible status announcements for loading and setup-state
transitions.

* **Bug Fixes**
* Active projects now correctly display topology errors when cluster
health data cannot be retrieved.
* Healthy responses with no topology data display an appropriate
unavailable state.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Alaister Young <10985857+alaister@users.noreply.github.com>
2026-09-01 00:53:01 +08:00

150 lines
5.1 KiB
TypeScript

import { useQuery } from '@tanstack/react-query'
import { useParams } from 'common'
import { Loader2 } from 'lucide-react'
import { useMemo } from 'react'
import { EmptyStatePresentational } from 'ui-patterns/EmptyStatePresentational'
import { DiagramFlow } from './DiagramFlow'
import { addShardNodes, generateHaNodesAndEdges } from './HaInstanceConfiguration.utils'
import { HaPrimaryNode, HaReplicaNode, HaShardNode, MultigatewayNode } from './HaInstanceNode'
import { buildHaTopology, selectTopologyPoolers } from './HaTopology.utils'
import { HA_RANKSEP } from './InstanceConfiguration.constants'
import { AlertError } from '@/components/ui/AlertError'
import { HighAvailabilityDisabledEmptyState } from '@/components/ui/HighAvailability/HighAvailabilityDisabledEmptyState'
import { haClusterGatewaysQueryOptions } from '@/data/ha-admin/ha-cluster-gateways-query'
import { haClusterPoolersQueryOptions } from '@/data/ha-admin/ha-cluster-poolers-query'
import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
import { PROJECT_STATUS } from '@/lib/constants'
const nodeTypes = {
HA_GATEWAY: MultigatewayNode,
HA_PRIMARY: HaPrimaryNode,
HA_REPLICA: HaReplicaNode,
HA_SHARD: HaShardNode,
}
// The gateway and replication edges both use React Flow's built-in smoothstep.
const edgeTypes = {}
const POOLER_STATUS_REFRESH_MS = 30_000
/**
* Cluster topology diagram for High Availability (Multigres) projects:
* gateway tier → shard group → primary + read replicas, driven by the
* read-only multiadmin `/ha-admin` passthrough.
*/
export const HaInstanceConfiguration = () => {
const { ref: projectRef } = useParams()
const { data: project } = useSelectedProjectQuery()
const isProjectBuilding =
project?.status === PROJECT_STATUS.COMING_UP || project?.status === PROJECT_STATUS.UNKNOWN
// Gateways poll on the same interval as poolers so the gateway count and
// gateway→primary edge track cluster changes while the page stays open.
const {
data: gatewaysData,
error: gatewaysError,
isPending: isPendingGateways,
isError: isErrorGateways,
} = useQuery({
...haClusterGatewaysQueryOptions({ projectRef }),
refetchInterval: POOLER_STATUS_REFRESH_MS,
})
const {
data: poolers,
error: poolersError,
isPending: isPendingPoolers,
isError: isErrorPoolers,
} = useQuery({
...haClusterPoolersQueryOptions({ projectRef }),
refetchInterval: POOLER_STATUS_REFRESH_MS,
select: selectTopologyPoolers,
})
const gateways = gatewaysData?.gateways
const { nodes, edges } = useMemo(
() =>
generateHaNodesAndEdges(
buildHaTopology({ gateways: gateways ?? [], poolers: poolers ?? [] })
),
[gateways, poolers]
)
const isPending = isPendingGateways || isPendingPoolers
const isError = isErrorGateways || isErrorPoolers
const error = gatewaysError ?? poolersError
// While the project is provisioning, the /ha-admin endpoints fail or return an
// empty topology as a matter of course — that's a project still booting, not an
// unhealthy cluster.
const isProvisioning = isProjectBuilding && (isError || (poolers ?? []).length === 0)
// The initial load and the provisioning placeholder share one persistent
// role="status" live region so screen readers announce the transition between
// them — a live region only announces updates to content it already contains.
if (isPending || isProvisioning) {
return (
<div role="status" className="h-full w-full flex items-center justify-center p-6">
{isPending ? (
<>
<span className="sr-only">Loading cluster topology...</span>
<Loader2
aria-hidden="true"
className="motion-safe:animate-spin text-foreground-light"
/>
</>
) : (
<EmptyStatePresentational
icon={
<Loader2
size={24}
strokeWidth={1.5}
aria-hidden="true"
className="motion-safe:animate-spin text-foreground-muted"
/>
}
title="Setting up project"
description="Cluster topology will be available once your project is ready. This may take a few minutes."
className="max-w-md"
/>
)}
</div>
)
}
if (isError) {
return (
<div className="h-full w-full flex items-center justify-center px-28">
<AlertError error={error} subject="Failed to retrieve cluster topology" />
</div>
)
}
if ((poolers ?? []).length === 0) {
return (
<div className="h-full w-full flex items-center justify-center p-6">
<HighAvailabilityDisabledEmptyState
title="Cluster topology unavailable"
description="Topology data for this project is not available yet. Contact support if this persists."
/>
</div>
)
}
return (
<div className="nowheel h-full w-full relative">
<DiagramFlow
nodes={nodes}
edges={edges}
nodeTypes={nodeTypes}
edgeTypes={edgeTypes}
addGroupNodes={addShardNodes}
ranksep={HA_RANKSEP}
/>
</div>
)
}