diff --git a/apps/studio/components/interfaces/Settings/API/ServiceList.tsx b/apps/studio/components/interfaces/Settings/API/ServiceList.tsx
index 602bb1ef820..37ba646a136 100644
--- a/apps/studio/components/interfaces/Settings/API/ServiceList.tsx
+++ b/apps/studio/components/interfaces/Settings/API/ServiceList.tsx
@@ -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 (
@@ -106,7 +118,15 @@ const ServiceList = () => {
title={
Project URL
- {showReadReplicasUI && }
+ {showReadReplicasUI && (
+ 0
+ ? [{ id: 'load-balancer', name: 'API Load Balancer' }]
+ : []
+ }
+ />
+ )}
}
>
@@ -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"
/>
)}
diff --git a/apps/studio/components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/InstanceConfiguration.tsx b/apps/studio/components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/InstanceConfiguration.tsx
index 572d0a6cb98..6325f596b89 100644
--- a/apps/studio/components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/InstanceConfiguration.tsx
+++ b/apps/studio/components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/InstanceConfiguration.tsx
@@ -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
()
const [selectedReplicaToRestart, setSelectedReplicaToRestart] = useState()
- 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 (
<>
{isLoading &&
}
{isError &&
}
- {isSuccess && (
+ {isSuccessReplicas && (
<>
>
)
@@ -187,7 +244,7 @@ export const RegionNode = ({ data }: any) => {
return (
![]()
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 (
@@ -64,13 +68,21 @@ const DatabaseSelector = ({
}
>
Source:{' '}
-
- {selectedDatabase?.identifier === projectRef ? 'Primary database' : 'Read replica'}
- {' '}
- {selectedDatabase?.identifier !== projectRef && (
-
- ({selectedDatabaseRegion} - {formattedDatabaseId})
-
+ {selectedAdditionalOption ? (
+ {selectedAdditionalOption.name}
+ ) : (
+ <>
+
+ {selectedDatabase?.identifier === projectRef
+ ? 'Primary database'
+ : 'Read replica'}
+ {' '}
+ {selectedDatabase?.identifier !== projectRef && (
+
+ ({selectedDatabaseRegion} - {formattedDatabaseId})
+
+ )}
+ >
)}
@@ -78,6 +90,30 @@ const DatabaseSelector = ({
+ {additionalOptions.length > 0 && (
+
+ {additionalOptions.map((option) => (
+ {
+ state.setSelectedDatabaseId(option.id)
+ setOpen(false)
+ }}
+ onClick={() => {
+ state.setSelectedDatabaseId(option.id)
+ setOpen(false)
+ }}
+ >
+
+
{option.name}
+ {option.id === selectedDatabaseId &&
}
+
+
+ ))}
+
+ )}
7 ? 'h-[210px]' : ''}>
{databases?.map((database) => {
diff --git a/apps/studio/data/read-replicas/keys.ts b/apps/studio/data/read-replicas/keys.ts
index 1af4e2a04f2..26b8cbd816d 100644
--- a/apps/studio/data/read-replicas/keys.ts
+++ b/apps/studio/data/read-replicas/keys.ts
@@ -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,
}
diff --git a/apps/studio/data/read-replicas/load-balancers-query.ts b/apps/studio/data/read-replicas/load-balancers-query.ts
new file mode 100644
index 00000000000..5d610e7d58e
--- /dev/null
+++ b/apps/studio/data/read-replicas/load-balancers-query.ts
@@ -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>
+export type LoadBalancersError = ResponseError
+
+export const useLoadBalancersQuery = (
+ { projectRef }: LoadBalancersVariables,
+ { enabled = true, ...options }: UseQueryOptions = {}
+) => {
+ const readReplicasEnabled = useFlag('readReplicas')
+ const { data } = useProjectDetailQuery({ ref: projectRef })
+
+ return useQuery(
+ replicaKeys.loadBalancers(projectRef),
+ ({ signal }) => getLoadBalancers({ projectRef }, signal),
+ {
+ enabled:
+ enabled &&
+ data?.is_read_replicas_enabled &&
+ readReplicasEnabled &&
+ typeof projectRef !== 'undefined',
+ ...options,
+ }
+ )
+}