mirror of
https://github.com/supabase/supabase.git
synced 2026-10-17 23:29: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). ```
262 lines
9.3 KiB
TypeScript
262 lines
9.3 KiB
TypeScript
import { Edge, ReactFlowProvider } from '@xyflow/react'
|
|
import { useParams } from 'common'
|
|
import { partition } from 'lodash'
|
|
import { Globe2Icon, Loader2Icon, NetworkIcon } from 'lucide-react'
|
|
import { useEffect, useMemo, useState } from 'react'
|
|
import { Button, FloatingPlate, Tooltip, TooltipContent, TooltipTrigger } from 'ui'
|
|
|
|
import { DiagramFlow } from './DiagramFlow'
|
|
import { SmoothstepEdge } from './Edge'
|
|
import { HaInstanceConfiguration } from './HaInstanceConfiguration'
|
|
import { addRegionNodes, generateNodes } from './InstanceConfiguration.utils'
|
|
import { LoadBalancerNode, PrimaryNode, RegionNode, ReplicaNode } from './InstanceNode'
|
|
import MapView from './MapView'
|
|
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'
|
|
import { useReadReplicasQuery } from '@/data/read-replicas/replicas-query'
|
|
import {
|
|
ReplicaInitializationStatus,
|
|
useReadReplicasStatusesQuery,
|
|
} from '@/data/read-replicas/replicas-status-query'
|
|
import { useHighAvailability } from '@/hooks/misc/useHighAvailability'
|
|
import { useIsFeatureEnabled } from '@/hooks/misc/useIsFeatureEnabled'
|
|
import { useIsAwsCloudProvider, useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
|
|
|
|
const nodeTypes = {
|
|
PRIMARY: PrimaryNode,
|
|
READ_REPLICA: ReplicaNode,
|
|
REGION: RegionNode,
|
|
LOAD_BALANCER: LoadBalancerNode,
|
|
}
|
|
|
|
const edgeTypes = {
|
|
smoothstep: SmoothstepEdge,
|
|
}
|
|
|
|
const InstanceConfigurationUI = () => {
|
|
const { ref: projectRef } = useParams()
|
|
const { isPending: isLoadingProject } = useSelectedProjectQuery()
|
|
|
|
const isAws = useIsAwsCloudProvider()
|
|
const { infrastructureReadReplicas } = useIsFeatureEnabled(['infrastructure:read_replicas'])
|
|
|
|
const [view, setView] = useState<'flow' | 'map'>('flow')
|
|
const [refetchInterval, setRefetchInterval] = useState<number | false>(10000)
|
|
|
|
const {
|
|
data: loadBalancers,
|
|
refetch: refetchLoadBalancers,
|
|
isSuccess: isSuccessLoadBalancers,
|
|
} = useLoadBalancersQuery({ projectRef })
|
|
const {
|
|
data,
|
|
error,
|
|
refetch: refetchReplicas,
|
|
isPending: isLoading,
|
|
isError,
|
|
isSuccess: isSuccessReplicas,
|
|
} = useReadReplicasQuery({ projectRef })
|
|
const [[primary], replicas] = useMemo(
|
|
() => partition(data ?? [], (db) => db.identifier === projectRef),
|
|
[data, projectRef]
|
|
)
|
|
const numReplicas = useMemo(() => data?.length ?? 0, [data])
|
|
|
|
const { data: replicasStatuses, isSuccess: isSuccessReplicasStatuses } =
|
|
useReadReplicasStatusesQuery(
|
|
{ projectRef },
|
|
{
|
|
refetchInterval: refetchInterval,
|
|
refetchOnWindowFocus: false,
|
|
}
|
|
)
|
|
|
|
useEffect(() => {
|
|
if (!isSuccessReplicasStatuses) return
|
|
const refetch = async () => {
|
|
const fixedStatues = [
|
|
REPLICA_STATUS.ACTIVE_HEALTHY,
|
|
REPLICA_STATUS.ACTIVE_UNHEALTHY,
|
|
REPLICA_STATUS.INIT_READ_REPLICA_FAILED,
|
|
]
|
|
const replicasInTransition = replicasStatuses.filter((db) => {
|
|
const { status } = db.replicaInitializationStatus || {}
|
|
return (
|
|
!fixedStatues.includes(db.status) || status === ReplicaInitializationStatus.InProgress
|
|
)
|
|
})
|
|
const hasTransientStatus = replicasInTransition.length > 0
|
|
|
|
// If any replica's status has changed, refetch databases
|
|
if (replicasStatuses.length !== numReplicas) {
|
|
await refetchReplicas()
|
|
setTimeout(() => refetchLoadBalancers(), 2000)
|
|
}
|
|
|
|
// If all replicas are active healthy, stop fetching statuses
|
|
if (!hasTransientStatus) {
|
|
setRefetchInterval(false)
|
|
}
|
|
}
|
|
refetch()
|
|
}, [
|
|
numReplicas,
|
|
isSuccessReplicasStatuses,
|
|
refetchLoadBalancers,
|
|
refetchReplicas,
|
|
replicasStatuses,
|
|
])
|
|
|
|
const nodes = useMemo(
|
|
() =>
|
|
isSuccessReplicas && isSuccessLoadBalancers && primary !== undefined
|
|
? generateNodes({
|
|
primary,
|
|
replicas,
|
|
loadBalancers: loadBalancers ?? [],
|
|
})
|
|
: [],
|
|
[isSuccessReplicas, isSuccessLoadBalancers, primary, replicas, loadBalancers]
|
|
)
|
|
|
|
const edges: Edge[] = useMemo(
|
|
() =>
|
|
isSuccessReplicas && isSuccessLoadBalancers
|
|
? [
|
|
...((loadBalancers ?? []).length > 0
|
|
? [
|
|
{
|
|
id: `load-balancer-${primary.identifier}`,
|
|
source: 'load-balancer',
|
|
target: primary.identifier,
|
|
type: 'smoothstep',
|
|
// Static: no data flows between the load balancer and the
|
|
// database — the line only indicates a relation.
|
|
className: 'cursor-default!',
|
|
},
|
|
]
|
|
: []),
|
|
...replicas.map((database) => {
|
|
return {
|
|
id: `${primary.identifier}-${database.identifier}`,
|
|
source: primary.identifier,
|
|
target: database.identifier,
|
|
type: 'smoothstep',
|
|
animated: true,
|
|
className: 'cursor-default!',
|
|
data: {
|
|
status: database.status,
|
|
identifier: database.identifier,
|
|
connectionString: database.connectionString,
|
|
},
|
|
}
|
|
}),
|
|
]
|
|
: [],
|
|
[isSuccessLoadBalancers, isSuccessReplicas, loadBalancers, primary?.identifier, replicas]
|
|
)
|
|
|
|
return (
|
|
<div className="nowheel h-full">
|
|
<div
|
|
className={`h-full w-full relative ${
|
|
isSuccessReplicas && !isLoadingProject ? '' : 'flex items-center justify-center px-28'
|
|
}`}
|
|
>
|
|
{(isLoading || isLoadingProject) && (
|
|
<div role="status">
|
|
<span className="sr-only">Loading infrastructure...</span>
|
|
<Loader2Icon
|
|
aria-hidden="true"
|
|
className="motion-safe:animate-spin text-foreground-light"
|
|
/>
|
|
</div>
|
|
)}
|
|
{isError && <AlertError error={error} subject="Failed to retrieve replicas" />}
|
|
{isSuccessReplicas && !isLoadingProject && (
|
|
<>
|
|
{infrastructureReadReplicas && (
|
|
<FloatingPlate className="z-10 absolute top-4 right-4 items-center justify-center gap-x-2">
|
|
{isAws && (
|
|
<div className="flex items-center justify-center">
|
|
<Tooltip>
|
|
<TooltipTrigger asChild>
|
|
<Button
|
|
icon={<NetworkIcon size={15} />}
|
|
className={`rounded-r-none transition motion-reduce:transition-none ${
|
|
view === 'flow' ? 'opacity-100' : 'opacity-50'
|
|
}`}
|
|
onClick={() => setView('flow')}
|
|
aria-label="Show infrastructure as a diagram"
|
|
aria-pressed={view === 'flow'}
|
|
// Tooltip repeats the label; screen readers would read it twice
|
|
aria-describedby={undefined}
|
|
/>
|
|
</TooltipTrigger>
|
|
<TooltipContent side="bottom">
|
|
Show infrastructure as a diagram
|
|
</TooltipContent>
|
|
</Tooltip>
|
|
<Tooltip>
|
|
<TooltipTrigger asChild>
|
|
<Button
|
|
icon={<Globe2Icon size={15} />}
|
|
className={`rounded-l-none transition motion-reduce:transition-none ${
|
|
view === 'map' ? 'opacity-100' : 'opacity-50'
|
|
}`}
|
|
onClick={() => setView('map')}
|
|
aria-label="Show infrastructure on the map"
|
|
aria-pressed={view === 'map'}
|
|
// Tooltip repeats the label; screen readers would read it twice
|
|
aria-describedby={undefined}
|
|
/>
|
|
</TooltipTrigger>
|
|
<TooltipContent side="bottom">Show infrastructure on the map</TooltipContent>
|
|
</Tooltip>
|
|
</div>
|
|
)}
|
|
</FloatingPlate>
|
|
)}
|
|
{view === 'flow' ? (
|
|
<DiagramFlow
|
|
nodes={nodes}
|
|
edges={edges}
|
|
nodeTypes={nodeTypes}
|
|
edgeTypes={edgeTypes}
|
|
addGroupNodes={addRegionNodes}
|
|
/>
|
|
) : (
|
|
<MapView />
|
|
)}
|
|
</>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
export const InstanceConfiguration = () => {
|
|
const { isHighAvailability, isPending } = useHighAvailability()
|
|
|
|
// Wait for the project record so an HA project never briefly mounts the
|
|
// standard diagram (and fires its queries) before swapping.
|
|
if (isPending) {
|
|
return (
|
|
<div role="status" className="h-full w-full flex items-center justify-center">
|
|
<span className="sr-only">Loading infrastructure...</span>
|
|
<Loader2Icon
|
|
aria-hidden="true"
|
|
className="motion-safe:animate-spin text-foreground-light"
|
|
/>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
return (
|
|
<ReactFlowProvider>
|
|
{isHighAvailability ? <HaInstanceConfiguration /> : <InstanceConfigurationUI />}
|
|
</ReactFlowProvider>
|
|
)
|
|
}
|