import { PermissionAction } from '@supabase/shared-types/out/constants' import { partition } from 'lodash' import { ChevronDown, Globe2, Loader2, Network } from 'lucide-react' import { useTheme } from 'next-themes' import Link from 'next/link' import { useEffect, useMemo, useState } from 'react' import ReactFlow, { Background, Edge, ReactFlowProvider, useReactFlow } from 'reactflow' import 'reactflow/dist/style.css' import { useParams } from 'common' import AlertError from 'components/ui/AlertError' import { ButtonTooltip } from 'components/ui/ButtonTooltip' import { useLoadBalancersQuery } from 'data/read-replicas/load-balancers-query' import { Database, useReadReplicasQuery } from 'data/read-replicas/replicas-query' import { ReplicaInitializationStatus, useReadReplicasStatusesQuery, } from 'data/read-replicas/replicas-status-query' import { useAsyncCheckPermissions } from 'hooks/misc/useCheckPermissions' import { useIsFeatureEnabled } from 'hooks/misc/useIsFeatureEnabled' import { useIsAwsCloudProvider, useIsOrioleDb, useSelectedProjectQuery, } from 'hooks/misc/useSelectedProject' import { timeout } from 'lib/helpers' import { useRouter } from 'next/router' import { Button, cn, DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger, } from 'ui' import DropAllReplicasConfirmationModal from './DropAllReplicasConfirmationModal' import { DropReplicaConfirmationModal } from './DropReplicaConfirmationModal' import { SmoothstepEdge } from './Edge' 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 { RestartReplicaConfirmationModal } from './RestartReplicaConfirmationModal' interface InstanceConfigurationUIProps { diagramOnly?: boolean } const InstanceConfigurationUI = ({ diagramOnly = false }: InstanceConfigurationUIProps) => { const router = useRouter() const reactFlow = useReactFlow() const isOrioleDb = useIsOrioleDb() const { resolvedTheme } = useTheme() const { ref: projectRef } = useParams() const { isPending: isLoadingProject } = useSelectedProjectQuery() const isAws = useIsAwsCloudProvider() const { infrastructureReadReplicas } = useIsFeatureEnabled(['infrastructure:read_replicas']) const newReplicaURL = `/project/${projectRef}/database/replication?type=Read+Replica` const [view, setView] = useState<'flow' | 'map'>('flow') const [showDeleteAllModal, setShowDeleteAllModal] = useState(false) const [refetchInterval, setRefetchInterval] = useState(10000) const [selectedReplicaToDrop, setSelectedReplicaToDrop] = useState() const [selectedReplicaToRestart, setSelectedReplicaToRestart] = useState() const { can: canManageReplicas } = useAsyncCheckPermissions(PermissionAction.CREATE, 'projects') 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 backgroundPatternColor = resolvedTheme === 'dark' ? 'rgba(255, 255, 255, 0.3)' : 'rgba(0, 0, 0, 0.4)' const nodes = useMemo( () => isSuccessReplicas && isSuccessLoadBalancers && primary !== undefined ? generateNodes({ primary, replicas, loadBalancers: loadBalancers ?? [], onSelectRestartReplica: setSelectedReplicaToRestart, onSelectDropReplica: setSelectedReplicaToDrop, }) : [], [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', animated: true, 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] ) const nodeTypes = useMemo( () => ({ PRIMARY: PrimaryNode, READ_REPLICA: ReplicaNode, REGION: RegionNode, LOAD_BALANCER: LoadBalancerNode, }), [] ) const edgeTypes = useMemo( () => ({ smoothstep: SmoothstepEdge, }), [] ) const setReactFlow = async () => { const graph = getDagreGraphLayout(nodes, edges) const { nodes: updatedNodes } = addRegionNodes(graph.nodes, graph.edges) reactFlow.setNodes(updatedNodes) reactFlow.setEdges(graph.edges) // [Joshen] Odd fix to ensure that react flow snaps back to center when adding nodes await timeout(1) reactFlow.fitView({ maxZoom: 0.9, minZoom: 0.9 }) } // [Joshen] Just FYI this block is oddly triggering whenever we refocus on the viewport // even if I change the dependency array to just data. Not blocker, just an area to optimize useEffect(() => { if (isSuccessReplicas && isSuccessLoadBalancers && nodes.length > 0 && view === 'flow') { setReactFlow() } }, [isSuccessReplicas, isSuccessLoadBalancers, nodes, edges, view]) return (
{/* Sometimes the read replicas are loaded before the project info and causes read replicas to be shown on Fly deploys. You can replicate this to going to this page and refresh. This isLoadingProject flag fixes that. */} {(isLoading || isLoadingProject) && ( )} {isError && } {isSuccessReplicas && !isLoadingProject && ( <> {!diagramOnly && infrastructureReadReplicas && (
0 ? 'rounded-r-none' : '')} tooltip={{ content: { side: 'bottom', text: !canManageReplicas ? 'You need additional permissions to deploy replicas' : isOrioleDb ? 'Read replicas are not supported with OrioleDB' : undefined, }, }} > Deploy a new replica {replicas.length > 0 && (
{isAws && (
)}
)} {view === 'flow' ? ( ) : ( router.push(newReplicaURL)} onSelectRestartReplica={setSelectedReplicaToRestart} onSelectDropReplica={setSelectedReplicaToDrop} /> )} )}
{!diagramOnly && ( <> setRefetchInterval(5000)} onCancel={() => setSelectedReplicaToDrop(undefined)} /> setRefetchInterval(5000)} onCancel={() => setShowDeleteAllModal(false)} /> setRefetchInterval(5000)} onCancel={() => setSelectedReplicaToRestart(undefined)} /> )}
) } interface InstanceConfigurationProps { diagramOnly?: boolean } export const InstanceConfiguration = ({ diagramOnly = false }: InstanceConfigurationProps) => { return ( ) }