mirror of
https://github.com/supabase/supabase.git
synced 2026-10-06 01:45:10 +03:00
Chore/expose load balancer api endpoint in api settings (#21314)
* Expose load balancer API endpoint in API settings * Add load balancer as a node in infrastructure config * Add load balancer node to infra config * Fix
This commit is contained in:
1 parent
b025cc3810
commit
fc405ce035
7 files changed
+312
-82
No files matched your search
@@ -20,12 +20,13 @@ import JWTSettings from './JWTSettings'
|
||||
import PostgrestConfig from './PostgrestConfig'
|
||||
import { GenericSkeletonLoader } from 'components/ui/ShimmeringLoader'
|
||||
import { PROJECT_STATUS } from 'lib/constants'
|
||||
import { useLoadBalancersQuery } from 'data/read-replicas/load-balancers-query'
|
||||
|
||||
const ServiceList = () => {
|
||||
const { ui } = useStore()
|
||||
const client = useQueryClient()
|
||||
const { project, isLoading } = useProjectContext()
|
||||
const { ref: projectRef } = useParams()
|
||||
const { ref: projectRef, source } = useParams()
|
||||
const state = useDatabaseSelectorStateSnapshot()
|
||||
|
||||
const readReplicasEnabled = useFlag('readReplicas')
|
||||
@@ -36,6 +37,7 @@ const ServiceList = () => {
|
||||
})
|
||||
const { data: customDomainData } = useCustomDomainsQuery({ projectRef })
|
||||
const { data: databases } = useReadReplicasQuery({ projectRef })
|
||||
const { data: loadBalancers } = useLoadBalancersQuery({ projectRef })
|
||||
|
||||
const { data } = useJwtSecretUpdatingStatusQuery({ projectRef })
|
||||
const jwtSecretUpdateStatus = data?.jwtSecretUpdateStatus
|
||||
@@ -46,6 +48,26 @@ const ServiceList = () => {
|
||||
const jwtSecretUpdateErrorMessage =
|
||||
JWT_SECRET_UPDATE_ERROR_MESSAGES[jwtSecretUpdateError as JwtSecretUpdateError]
|
||||
|
||||
// Get the API service
|
||||
const isCustomDomainActive = customDomainData?.customDomain?.status === 'active'
|
||||
const apiService = settings?.autoApiService
|
||||
const apiUrl = `${apiService?.protocol ?? 'https'}://${apiService?.endpoint ?? '-'}`
|
||||
|
||||
const selectedDatabase = databases?.find((db) => db.identifier === state.selectedDatabaseId)
|
||||
const loadBalancerSelected = state.selectedDatabaseId === 'load-balancer'
|
||||
const replicaSelected = selectedDatabase?.identifier !== projectRef
|
||||
|
||||
const primaryEndpoint = isCustomDomainActive
|
||||
? `https://${customDomainData.customDomain.hostname}`
|
||||
: apiUrl
|
||||
const endpoint = !showReadReplicasUI
|
||||
? primaryEndpoint
|
||||
: isCustomDomainActive && state.selectedDatabaseId === projectRef
|
||||
? `https://${customDomainData.customDomain.hostname}`
|
||||
: loadBalancerSelected
|
||||
? loadBalancers?.[0].endpoint ?? ''
|
||||
: selectedDatabase?.restUrl
|
||||
|
||||
useEffect(() => {
|
||||
if (previousJwtSecretUpdateStatus.current === Updating) {
|
||||
switch (jwtSecretUpdateStatus) {
|
||||
@@ -68,21 +90,11 @@ const ServiceList = () => {
|
||||
previousJwtSecretUpdateStatus.current = jwtSecretUpdateStatus
|
||||
}, [jwtSecretUpdateStatus])
|
||||
|
||||
// Get the API service
|
||||
const isCustomDomainActive = customDomainData?.customDomain?.status === 'active'
|
||||
const apiService = settings?.autoApiService
|
||||
const apiUrl = `${apiService?.protocol ?? 'https'}://${apiService?.endpoint ?? '-'}`
|
||||
|
||||
const selectedDatabase = databases?.find((db) => db.identifier === state.selectedDatabaseId)
|
||||
|
||||
const primaryEndpoint = isCustomDomainActive
|
||||
? `https://${customDomainData.customDomain.hostname}`
|
||||
: apiUrl
|
||||
const endpoint = !showReadReplicasUI
|
||||
? primaryEndpoint
|
||||
: isCustomDomainActive && state.selectedDatabaseId === projectRef
|
||||
? `https://${customDomainData.customDomain.hostname}`
|
||||
: selectedDatabase?.restUrl
|
||||
useEffect(() => {
|
||||
if (readReplicasEnabled && source !== undefined) {
|
||||
state.setSelectedDatabaseId('load-balancer')
|
||||
}
|
||||
}, [source])
|
||||
|
||||
return (
|
||||
<div>
|
||||
@@ -106,7 +118,15 @@ const ServiceList = () => {
|
||||
title={
|
||||
<div className="w-full flex items-center justify-between">
|
||||
<h5 className="mb-0">Project URL</h5>
|
||||
{showReadReplicasUI && <DatabaseSelector />}
|
||||
{showReadReplicasUI && (
|
||||
<DatabaseSelector
|
||||
additionalOptions={
|
||||
(loadBalancers ?? []).length > 0
|
||||
? [{ id: 'load-balancer', name: 'API Load Balancer' }]
|
||||
: []
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
>
|
||||
@@ -133,7 +153,13 @@ const ServiceList = () => {
|
||||
disabled
|
||||
className="input-mono"
|
||||
value={endpoint}
|
||||
descriptionText="A RESTful endpoint for querying and managing your database."
|
||||
descriptionText={
|
||||
loadBalancerSelected
|
||||
? 'A RESTful endpoint for querying and managing your databases through your load balancer'
|
||||
: replicaSelected && showReadReplicasUI
|
||||
? 'A RESTful endpoint for querying your read replica'
|
||||
: 'A RESTful endpoint for querying and managing your database'
|
||||
}
|
||||
layout="horizontal"
|
||||
/>
|
||||
)}
|
||||
|
||||
+49
-20
@@ -14,8 +14,9 @@ import { AWS_REGIONS_KEYS } from 'lib/constants'
|
||||
import DeployNewReplicaPanel from './DeployNewReplicaPanel'
|
||||
import DropReplicaConfirmationModal from './DropReplicaConfirmationModal'
|
||||
import { addRegionNodes, generateNodes, getDagreGraphLayout } from './InstanceConfiguration.utils'
|
||||
import { PrimaryNode, RegionNode, ReplicaNode } from './InstanceNode'
|
||||
import { LoadBalancerNode, PrimaryNode, RegionNode, ReplicaNode } from './InstanceNode'
|
||||
import MapView from './MapView'
|
||||
import { useLoadBalancersQuery } from 'data/read-replicas/load-balancers-query'
|
||||
|
||||
// [Joshen] Just FYI, UI assumes single provider for primary + replicas
|
||||
// [Joshen] Idea to visualize grouping based on region: https://reactflow.dev/examples/layout/sub-flows
|
||||
@@ -35,7 +36,17 @@ const InstanceConfigurationUI = () => {
|
||||
const [selectedReplicaToDrop, setSelectedReplicaToDrop] = useState<Database>()
|
||||
const [selectedReplicaToRestart, setSelectedReplicaToRestart] = useState<Database>()
|
||||
|
||||
const { data, error, refetch, isLoading, isError, isSuccess } = useReadReplicasQuery({
|
||||
const { data: loadBalancers, isSuccess: isSuccessLoadBalancers } = useLoadBalancersQuery({
|
||||
projectRef,
|
||||
})
|
||||
const {
|
||||
data,
|
||||
error,
|
||||
refetch,
|
||||
isLoading,
|
||||
isError,
|
||||
isSuccess: isSuccessReplicas,
|
||||
} = useReadReplicasQuery({
|
||||
projectRef,
|
||||
})
|
||||
const [[primary], replicas] = useMemo(
|
||||
@@ -71,28 +82,46 @@ const InstanceConfigurationUI = () => {
|
||||
|
||||
const nodes = useMemo(
|
||||
() =>
|
||||
isSuccess
|
||||
? generateNodes(primary, replicas, {
|
||||
isSuccessReplicas && isSuccessLoadBalancers
|
||||
? generateNodes(primary, replicas, loadBalancers ?? [], {
|
||||
onSelectRestartReplica: setSelectedReplicaToRestart,
|
||||
onSelectResizeReplica: setSelectedReplicaToResize,
|
||||
onSelectDropReplica: setSelectedReplicaToDrop,
|
||||
})
|
||||
: [],
|
||||
[isSuccess, primary, replicas]
|
||||
[isSuccessReplicas, isSuccessLoadBalancers]
|
||||
)
|
||||
|
||||
const edges: Edge[] = replicas.map((database) => {
|
||||
return {
|
||||
id: `${primary.identifier}-${database.identifier}`,
|
||||
source: primary.identifier,
|
||||
target: database.identifier,
|
||||
type: 'smoothstep',
|
||||
animated: true,
|
||||
}
|
||||
})
|
||||
const edges: Edge[] = [
|
||||
...((loadBalancers ?? []).length > 0
|
||||
? [
|
||||
{
|
||||
id: `load-balancer-${primary.identifier}`,
|
||||
source: 'load-balancer',
|
||||
target: primary.identifier,
|
||||
type: 'smoothstep',
|
||||
animated: true,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...replicas.map((database) => {
|
||||
return {
|
||||
id: `${primary.identifier}-${database.identifier}`,
|
||||
source: primary.identifier,
|
||||
target: database.identifier,
|
||||
type: 'smoothstep',
|
||||
animated: true,
|
||||
}
|
||||
}),
|
||||
]
|
||||
|
||||
const nodeTypes = useMemo(
|
||||
() => ({ PRIMARY: PrimaryNode, READ_REPLICA: ReplicaNode, REGION: RegionNode }),
|
||||
() => ({
|
||||
PRIMARY: PrimaryNode,
|
||||
READ_REPLICA: ReplicaNode,
|
||||
REGION: RegionNode,
|
||||
LOAD_BALANCER: LoadBalancerNode,
|
||||
}),
|
||||
[]
|
||||
)
|
||||
|
||||
@@ -107,24 +136,24 @@ const InstanceConfigurationUI = () => {
|
||||
// [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 (replicas.length > 0) {
|
||||
if (isSuccessReplicas && isSuccessLoadBalancers && replicas.length > 0) {
|
||||
const graph = getDagreGraphLayout(nodes, edges)
|
||||
const { nodes: updatedNodes } = addRegionNodes(graph.nodes, graph.edges)
|
||||
reactFlow.setNodes(updatedNodes)
|
||||
reactFlow.setEdges(graph.edges)
|
||||
}
|
||||
}, [replicas])
|
||||
}, [isSuccessReplicas, isSuccessLoadBalancers])
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className={`h-[500px] w-full relative ${
|
||||
isSuccess ? '' : 'flex items-center justify-center px-28'
|
||||
isSuccessReplicas ? '' : 'flex items-center justify-center px-28'
|
||||
}`}
|
||||
>
|
||||
{isLoading && <Loader2 className="animate-spin text-foreground-light" />}
|
||||
{isError && <AlertError error={error} subject="Failed to retrieve replicas" />}
|
||||
{isSuccess && (
|
||||
{isSuccessReplicas && (
|
||||
<>
|
||||
<div className="z-10 absolute top-4 right-4 flex items-center justify-center gap-x-2">
|
||||
<Button type="default" onClick={() => setShowNewReplicaPanel(true)}>
|
||||
@@ -152,7 +181,7 @@ const InstanceConfigurationUI = () => {
|
||||
{view === 'flow' ? (
|
||||
<ReactFlow
|
||||
fitView
|
||||
fitViewOptions={{ minZoom: 0.9, maxZoom: 1 }}
|
||||
fitViewOptions={{ minZoom: 0.9, maxZoom: 0.9 }}
|
||||
className="instance-configuration"
|
||||
zoomOnPinch={false}
|
||||
zoomOnScroll={false}
|
||||
|
||||
+33
-5
@@ -9,10 +9,15 @@ import {
|
||||
} from './InstanceConfiguration.constants'
|
||||
import { groupBy } from 'lodash'
|
||||
import { Database } from 'data/read-replicas/replicas-query'
|
||||
import { LoadBalancer } from 'data/read-replicas/load-balancers-query'
|
||||
|
||||
// [Joshen] Just FYI the nodes generation assumes each project only has one load balancer
|
||||
// Will need to change if this eventually becomes otherwise
|
||||
|
||||
export const generateNodes = (
|
||||
primary: Database,
|
||||
replicas: Database[],
|
||||
loadBalancers: LoadBalancer[],
|
||||
{
|
||||
onSelectRestartReplica,
|
||||
onSelectResizeReplica,
|
||||
@@ -29,6 +34,21 @@ export const generateNodes = (
|
||||
return region?.key
|
||||
})
|
||||
|
||||
const loadBalancer = loadBalancers.find((x) =>
|
||||
x.databases.some((db) => db.identifier === primary.identifier)
|
||||
)
|
||||
const loadBalancerNode: Node | undefined =
|
||||
loadBalancer !== undefined
|
||||
? {
|
||||
position,
|
||||
id: 'load-balancer',
|
||||
type: 'LOAD_BALANCER',
|
||||
data: {
|
||||
numDatabases: loadBalancer.databases.length,
|
||||
},
|
||||
}
|
||||
: undefined
|
||||
|
||||
const primaryRegion = AVAILABLE_REPLICA_REGIONS.find((region) =>
|
||||
primary.region.includes(region.region)
|
||||
)
|
||||
@@ -45,6 +65,7 @@ export const generateNodes = (
|
||||
status: primary.status,
|
||||
numReplicas: replicas.length,
|
||||
numRegions: Object.keys(regions).length,
|
||||
hasLoadBalancer: loadBalancer !== undefined,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -73,17 +94,24 @@ export const generateNodes = (
|
||||
}
|
||||
})
|
||||
|
||||
return [primaryNode, ...replicaNodes]
|
||||
return [
|
||||
...(loadBalancerNode !== undefined ? [loadBalancerNode] : []),
|
||||
primaryNode,
|
||||
...replicaNodes,
|
||||
]
|
||||
}
|
||||
|
||||
export const getDagreGraphLayout = (nodes: Node[], edges: Edge[]) => {
|
||||
const dagreGraph = new dagre.graphlib.Graph()
|
||||
dagreGraph.setDefaultEdgeLabel(() => ({}))
|
||||
dagreGraph.setGraph({ rankdir: 'TB', ranksep: 200, nodesep: NODE_SEP })
|
||||
dagreGraph.setGraph({ rankdir: 'TB', ranksep: 160, nodesep: NODE_SEP })
|
||||
|
||||
nodes.forEach((node) =>
|
||||
dagreGraph.setNode(node.id, { width: NODE_WIDTH / 2, height: NODE_ROW_HEIGHT / 2 })
|
||||
)
|
||||
nodes.forEach((node) => {
|
||||
dagreGraph.setNode(node.id, {
|
||||
width: NODE_WIDTH / 2,
|
||||
height: node.id === 'load-balancer' ? -70 : NODE_ROW_HEIGHT / 2,
|
||||
})
|
||||
})
|
||||
|
||||
edges.forEach((edge) => dagreGraph.setEdge(edge.source, edge.target))
|
||||
|
||||
|
||||
+89
-32
@@ -9,13 +9,14 @@ import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
IconMoreVertical,
|
||||
} from 'ui'
|
||||
|
||||
import { formatDatabaseID } from 'data/read-replicas/replicas.utils'
|
||||
import { BASE_PATH, PROJECT_STATUS } from 'lib/constants'
|
||||
import { NODE_SEP, NODE_WIDTH, Region } from './InstanceConfiguration.constants'
|
||||
import { formatDatabaseID } from 'data/read-replicas/replicas.utils'
|
||||
|
||||
interface NodeData {
|
||||
id: string
|
||||
@@ -29,6 +30,11 @@ interface NodeData {
|
||||
interface PrimaryNodeData extends NodeData {
|
||||
numReplicas: number
|
||||
numRegions: number
|
||||
hasLoadBalancer: boolean
|
||||
}
|
||||
|
||||
interface LoadBalancerData extends NodeData {
|
||||
numDatabases: number
|
||||
}
|
||||
|
||||
interface ReplicaNodeData extends NodeData {
|
||||
@@ -37,11 +43,64 @@ interface ReplicaNodeData extends NodeData {
|
||||
onSelectDropReplica: () => void
|
||||
}
|
||||
|
||||
export const PrimaryNode = ({ data }: NodeProps<PrimaryNodeData>) => {
|
||||
const { provider, region, computeSize, numReplicas, numRegions } = data
|
||||
export const LoadBalancerNode = ({ data }: NodeProps<LoadBalancerData>) => {
|
||||
const { ref } = useParams()
|
||||
const { numDatabases } = data
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-col rounded bg-surface-100 border border-default">
|
||||
<div
|
||||
className="flex items-start justify-between p-3 gap-x-4"
|
||||
style={{ width: NODE_WIDTH / 2 - 10 }}
|
||||
>
|
||||
<div className="flex gap-x-3">
|
||||
<div className="min-w-8 h-8 bg-blue-600 border border-blue-800 rounded-md flex items-center justify-center">
|
||||
<Database size={16} />
|
||||
</div>
|
||||
<div className="flex flex-col gap-y-0.5">
|
||||
<p className="text-sm">API Load Balancer</p>
|
||||
<p className="text-sm text-foreground-light">
|
||||
Distributes incoming API requests across{' '}
|
||||
<span className="text-foreground">{numDatabases} databases</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<DropdownMenu modal={false}>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button type="text" icon={<IconMoreVertical />} className="px-1" />
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent className="w-40" side="bottom" align="end">
|
||||
<DropdownMenuItem asChild className="gap-x-2">
|
||||
<Link href={`/project/${ref}/settings/api?source=loadbalancer`}>View API URL</Link>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
</div>
|
||||
<Handle
|
||||
type="source"
|
||||
id="handle-b"
|
||||
position={Position.Bottom}
|
||||
style={{ background: 'transparent' }}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export const PrimaryNode = ({ data }: NodeProps<PrimaryNodeData>) => {
|
||||
const { provider, region, computeSize, numReplicas, numRegions, hasLoadBalancer } = data
|
||||
|
||||
return (
|
||||
<>
|
||||
{hasLoadBalancer && (
|
||||
<Handle
|
||||
type="target"
|
||||
id="handle-t"
|
||||
position={Position.Top}
|
||||
style={{ background: 'transparent' }}
|
||||
/>
|
||||
)}
|
||||
<div className="flex flex-col rounded bg-surface-100 border border-default">
|
||||
<div
|
||||
className="flex items-start justify-between p-3"
|
||||
@@ -54,12 +113,12 @@ export const PrimaryNode = ({ data }: NodeProps<PrimaryNodeData>) => {
|
||||
<div className="flex flex-col gap-y-0.5">
|
||||
<p className="text-sm">Primary Database</p>
|
||||
<p className="flex items-center gap-x-1">
|
||||
<span className="text-xs text-foreground-light">{region.name}</span>
|
||||
<span className="text-sm text-foreground-light">{region.name}</span>
|
||||
</p>
|
||||
<p className="flex items-center gap-x-1">
|
||||
<span className="text-xs text-foreground-light">{provider}</span>
|
||||
<span className="text-xs text-foreground-light">•</span>
|
||||
<span className="text-xs text-foreground-light">{computeSize}</span>
|
||||
<span className="text-sm text-foreground-light">{provider}</span>
|
||||
<span className="text-sm text-foreground-light">•</span>
|
||||
<span className="text-sm text-foreground-light">{computeSize}</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -119,7 +178,7 @@ export const ReplicaNode = ({ data }: NodeProps<ReplicaNodeData>) => {
|
||||
style={{ background: 'transparent' }}
|
||||
/>
|
||||
<div
|
||||
className="flex justify-between rounded bg-surface-100 border border-default p-3"
|
||||
className="flex justify-between items-start rounded bg-surface-100 border border-default p-3"
|
||||
style={{ width: NODE_WIDTH / 2 - 10 }}
|
||||
>
|
||||
<div className="flex gap-x-3">
|
||||
@@ -140,40 +199,38 @@ export const ReplicaNode = ({ data }: NodeProps<ReplicaNodeData>) => {
|
||||
)}
|
||||
</div>
|
||||
<div className="my-0.5">
|
||||
<p className="text-xs text-foreground-light">{region.name}</p>
|
||||
<p className="flex text-xs text-foreground-light items-center gap-x-1">
|
||||
<p className="text-sm text-foreground-light">{region.name}</p>
|
||||
<p className="flex text-sm text-foreground-light items-center gap-x-1">
|
||||
<span>{provider}</span>
|
||||
<span>•</span>
|
||||
<span>{computeSize}</span>
|
||||
</p>
|
||||
</div>
|
||||
<p className="text-xs text-foreground-light">Created: {created}</p>
|
||||
<p className="text-sm text-foreground-light">Created: {created}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<DropdownMenu modal={false}>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button type="text" icon={<IconMoreVertical />} className="px-1" />
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent className="p-0 w-40" side="bottom" align="end">
|
||||
<DropdownMenuItem className="gap-x-2">
|
||||
<Link href={`/project/${ref}/settings/database?connectionString=${id}`}>
|
||||
View connection string
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
{/* <DropdownMenuItem className="gap-x-2" onClick={() => onSelectRestartReplica()}>
|
||||
<DropdownMenu modal={false}>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button type="text" icon={<IconMoreVertical />} className="px-1" />
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent className="w-40" side="bottom" align="end">
|
||||
<DropdownMenuItem className="gap-x-2">
|
||||
<Link href={`/project/${ref}/settings/database?connectionString=${id}`}>
|
||||
View connection string
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
{/* <DropdownMenuItem className="gap-x-2" onClick={() => onSelectRestartReplica()}>
|
||||
Restart replica
|
||||
</DropdownMenuItem> */}
|
||||
{/* <DropdownMenuItem className="gap-x-2" onClick={() => onSelectResizeReplica()}>
|
||||
{/* <DropdownMenuItem className="gap-x-2" onClick={() => onSelectResizeReplica()}>
|
||||
Resize replica
|
||||
</DropdownMenuItem> */}
|
||||
<div className="border-t" />
|
||||
<DropdownMenuItem className="gap-x-2" onClick={() => onSelectDropReplica()}>
|
||||
Drop replica
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem className="gap-x-2" onClick={() => onSelectDropReplica()}>
|
||||
Drop replica
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
@@ -187,7 +244,7 @@ export const RegionNode = ({ data }: any) => {
|
||||
return (
|
||||
<div
|
||||
className="relative flex justify-between rounded bg-black/10 border border-default border-white/10 border-2 p-3"
|
||||
style={{ width: regionNodeWidth, height: 150 }}
|
||||
style={{ width: regionNodeWidth, height: 162 }}
|
||||
>
|
||||
<div className="absolute bottom-2 flex items-center justify-between gap-x-2">
|
||||
<img
|
||||
|
||||
@@ -24,12 +24,14 @@ import { useDatabaseSelectorStateSnapshot } from 'state/database-selector'
|
||||
|
||||
interface DatabaseSelectorProps {
|
||||
variant?: 'regular' | 'connected-on-right' | 'connected-on-left' | 'connected-on-both'
|
||||
additionalOptions?: { id: string; name: string }[]
|
||||
selectedId?: string // Optional to let parent component control its state (e.g Database Settings page)
|
||||
setSelectedId?: (value: string) => void
|
||||
}
|
||||
|
||||
const DatabaseSelector = ({
|
||||
variant = 'regular',
|
||||
additionalOptions = [],
|
||||
selectedId,
|
||||
setSelectedId,
|
||||
}: DatabaseSelectorProps) => {
|
||||
@@ -47,6 +49,8 @@ const DatabaseSelector = ({
|
||||
const selectedDatabaseRegion = formatDatabaseRegion(selectedDatabase?.region ?? '')
|
||||
const formattedDatabaseId = formatDatabaseID(selectedDatabaseId ?? '')
|
||||
|
||||
const selectedAdditionalOption = additionalOptions.find((x) => x.id === selectedDatabaseId)
|
||||
|
||||
return (
|
||||
<Popover_Shadcn_ open={open} onOpenChange={setOpen} modal={false}>
|
||||
<PopoverTrigger_Shadcn_ asChild>
|
||||
@@ -64,13 +68,21 @@ const DatabaseSelector = ({
|
||||
}
|
||||
>
|
||||
Source:{' '}
|
||||
<span className="capitalize">
|
||||
{selectedDatabase?.identifier === projectRef ? 'Primary database' : 'Read replica'}
|
||||
</span>{' '}
|
||||
{selectedDatabase?.identifier !== projectRef && (
|
||||
<span>
|
||||
({selectedDatabaseRegion} - {formattedDatabaseId})
|
||||
</span>
|
||||
{selectedAdditionalOption ? (
|
||||
<span>{selectedAdditionalOption.name}</span>
|
||||
) : (
|
||||
<>
|
||||
<span className="capitalize">
|
||||
{selectedDatabase?.identifier === projectRef
|
||||
? 'Primary database'
|
||||
: 'Read replica'}
|
||||
</span>{' '}
|
||||
{selectedDatabase?.identifier !== projectRef && (
|
||||
<span>
|
||||
({selectedDatabaseRegion} - {formattedDatabaseId})
|
||||
</span>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
@@ -78,6 +90,30 @@ const DatabaseSelector = ({
|
||||
<PopoverContent_Shadcn_ className="p-0 w-64" side="bottom" align="end">
|
||||
<Command_Shadcn_>
|
||||
<CommandList_Shadcn_>
|
||||
{additionalOptions.length > 0 && (
|
||||
<CommandGroup_Shadcn_ className="border-b">
|
||||
{additionalOptions.map((option) => (
|
||||
<CommandItem_Shadcn_
|
||||
key={option.id}
|
||||
value={option.id}
|
||||
className="cursor-pointer w-full"
|
||||
onSelect={() => {
|
||||
state.setSelectedDatabaseId(option.id)
|
||||
setOpen(false)
|
||||
}}
|
||||
onClick={() => {
|
||||
state.setSelectedDatabaseId(option.id)
|
||||
setOpen(false)
|
||||
}}
|
||||
>
|
||||
<div className="w-full flex items-center justify-between">
|
||||
<p>{option.name}</p>
|
||||
{option.id === selectedDatabaseId && <IconCheck />}
|
||||
</div>
|
||||
</CommandItem_Shadcn_>
|
||||
))}
|
||||
</CommandGroup_Shadcn_>
|
||||
)}
|
||||
<CommandGroup_Shadcn_>
|
||||
<ScrollArea className={(databases || []).length > 7 ? 'h-[210px]' : ''}>
|
||||
{databases?.map((database) => {
|
||||
|
||||
@@ -2,4 +2,6 @@ export const replicaKeys = {
|
||||
list: (projectRef: string | undefined) => ['project', projectRef, 'replicas'] as const,
|
||||
statuses: (projectRef: string | undefined) =>
|
||||
['project', projectRef, 'replicas-statuses'] as const,
|
||||
loadBalancers: (projectRef: string | undefined) =>
|
||||
['project', projectRef, 'load-balancers'] as const,
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import { useQuery, UseQueryOptions } from '@tanstack/react-query'
|
||||
import { get } from 'data/fetchers'
|
||||
import { ResponseError } from 'types'
|
||||
import { replicaKeys } from './keys'
|
||||
import { components } from 'data/api'
|
||||
import { useFlag } from 'hooks'
|
||||
import { useProjectDetailQuery } from 'data/projects/project-detail-query'
|
||||
|
||||
export type LoadBalancersVariables = {
|
||||
projectRef?: string
|
||||
}
|
||||
|
||||
export type LoadBalancer = components['schemas']['LoadBalancerDetailResponse']
|
||||
|
||||
export async function getLoadBalancers(
|
||||
{ projectRef }: LoadBalancersVariables,
|
||||
signal?: AbortSignal
|
||||
) {
|
||||
if (!projectRef) throw new Error('Project ref is required')
|
||||
|
||||
const { data, error } = await get(`/platform/projects/{ref}/load-balancers`, {
|
||||
params: { path: { ref: projectRef } },
|
||||
signal,
|
||||
})
|
||||
|
||||
if (error) throw new Error((error as ResponseError).message)
|
||||
return data
|
||||
}
|
||||
|
||||
export type LoadBalancersData = Awaited<ReturnType<typeof getLoadBalancers>>
|
||||
export type LoadBalancersError = ResponseError
|
||||
|
||||
export const useLoadBalancersQuery = <TData = LoadBalancersData>(
|
||||
{ projectRef }: LoadBalancersVariables,
|
||||
{ enabled = true, ...options }: UseQueryOptions<LoadBalancersData, LoadBalancersError, TData> = {}
|
||||
) => {
|
||||
const readReplicasEnabled = useFlag('readReplicas')
|
||||
const { data } = useProjectDetailQuery({ ref: projectRef })
|
||||
|
||||
return useQuery<LoadBalancersData, LoadBalancersError, TData>(
|
||||
replicaKeys.loadBalancers(projectRef),
|
||||
({ signal }) => getLoadBalancers({ projectRef }, signal),
|
||||
{
|
||||
enabled:
|
||||
enabled &&
|
||||
data?.is_read_replicas_enabled &&
|
||||
readReplicasEnabled &&
|
||||
typeof projectRef !== 'undefined',
|
||||
...options,
|
||||
}
|
||||
)
|
||||
}
|
||||
Reference in new issue
Block a user