mirror of
https://github.com/supabase/supabase.git
synced 2026-10-17 15:19:08 +03:00
## Context This is largely just a mechanical change here: Opting to use lucide icons with the `Icon` suffix across all icon imports within `studio` - so that it’s clearer which components are icons + reduces the possibility of name collision with our own components - e.g <User /> vs <UserIcon /> Also added a eslint rule for this: <img width="1069" height="167" alt="image" src="https://github.com/user-attachments/assets/7f36f443-5ca2-4bf6-8d22-86e774513bfc" /> And updated `AGENTS.md` as such: ``` Icons come from `lucide-react`, always imported with the `Icon` suffix (`import { CheckIcon, Trash2Icon } from 'lucide-react'`) — never the bare names (`Check`) or `Check as CheckIcon` aliases (lint-enforced). ```
150 lines
5.1 KiB
TypeScript
150 lines
5.1 KiB
TypeScript
import { useQuery } from '@tanstack/react-query'
|
|
import { useParams } from 'common'
|
|
import { Loader2Icon } 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>
|
|
<Loader2Icon
|
|
aria-hidden="true"
|
|
className="motion-safe:animate-spin text-foreground-light"
|
|
/>
|
|
</>
|
|
) : (
|
|
<EmptyStatePresentational
|
|
icon={
|
|
<Loader2Icon
|
|
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>
|
|
)
|
|
}
|