Skip to content

Commit cea8e45

Browse files
authored
Chore/read replicas fixes 040324 (supabase#21731)
* Remove size selection and add info on rr billing * Add prompt in SQL editor if trying to run mutation query in read replica * Fix * Fix dropping replica not optimistically updating, and support new replica statuses * Invalidate load balancers after read replicas are spun up * fix
1 parent fdc8201 commit cea8e45

File tree

10 files changed

+311
-89
lines changed

10 files changed

+311
-89
lines changed

apps/studio/components/interfaces/SQLEditor/UtilityPanel/UtilityActions.tsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import FavoriteButton from './FavoriteButton'
2828
import SavingIndicator from './SavingIndicator'
2929
import toast from 'react-hot-toast'
3030
import { FileCog, Keyboard, SlidersHorizontal } from 'lucide-react'
31+
import { useSqlEditorStateSnapshot } from 'state/sql-editor'
3132

3233
export type UtilityActionsProps = {
3334
id: string
@@ -48,6 +49,7 @@ const UtilityActions = ({
4849
}: UtilityActionsProps) => {
4950
const os = detectOS()
5051
const project = useSelectedProject()
52+
const snap = useSqlEditorStateSnapshot()
5153
const showReadReplicasUI = project?.is_read_replicas_enabled
5254

5355
const [intellisenseEnabled, setIntellisenseEnabled] = useLocalStorageQuery(
@@ -97,7 +99,12 @@ const UtilityActions = ({
9799

98100
<div className="flex items-center justify-between gap-x-2 mx-2">
99101
<div className="flex items-center">
100-
{showReadReplicasUI && <DatabaseSelector variant="connected-on-right" />}
102+
{showReadReplicasUI && (
103+
<DatabaseSelector
104+
variant="connected-on-right"
105+
onSelectId={() => snap.resetResult(id)}
106+
/>
107+
)}
101108
<RoleImpersonationPopover
102109
serviceRoleLabel="postgres"
103110
variant={showReadReplicasUI ? 'connected-on-both' : 'connected-on-right'}

apps/studio/components/interfaces/SQLEditor/UtilityPanel/UtilityTabResults.tsx

Lines changed: 71 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { useParams } from 'common'
2+
import toast from 'react-hot-toast'
13
import { format } from 'sql-formatter'
24
import { AiIconAnimation, Button } from 'ui'
35

@@ -6,8 +8,9 @@ import { useSqlDebugMutation } from 'data/ai/sql-debug-mutation'
68
import { useEntityDefinitionsQuery } from 'data/database/entity-definitions-query'
79
import { useOrgSubscriptionQuery } from 'data/subscriptions/org-subscription-query'
810
import { isError } from 'data/utils/error-check'
9-
import { useLocalStorageQuery, useSelectedOrganization, useSelectedProject, useStore } from 'hooks'
11+
import { useLocalStorageQuery, useSelectedOrganization, useSelectedProject } from 'hooks'
1012
import { IS_PLATFORM, LOCAL_STORAGE_KEYS, OPT_IN_TAGS } from 'lib/constants'
13+
import { useDatabaseSelectorStateSnapshot } from 'state/database-selector'
1114
import { useSqlEditorStateSnapshot } from 'state/sql-editor'
1215
import { useSqlEditor } from '../SQLEditor'
1316
import { sqlAiDisclaimerComment } from '../SQLEditor.constants'
@@ -20,16 +23,17 @@ export type UtilityTabResultsProps = {
2023
}
2124

2225
const UtilityTabResults = ({ id, isExecuting }: UtilityTabResultsProps) => {
23-
const { ui } = useStore()
24-
const organization = useSelectedOrganization()
26+
const { ref } = useParams()
2527
const snap = useSqlEditorStateSnapshot()
26-
const { mutateAsync: debugSql, isLoading: isDebugSqlLoading } = useSqlDebugMutation()
27-
const { setDebugSolution, setAiInput, setSqlDiff, sqlDiff, setSelectedDiffType } = useSqlEditor()
28-
const selectedOrganization = useSelectedOrganization()
28+
const state = useDatabaseSelectorStateSnapshot()
2929
const selectedProject = useSelectedProject()
30-
const isOptedInToAI = selectedOrganization?.opt_in_tags?.includes(OPT_IN_TAGS.AI_SQL) ?? false
31-
const [hasEnabledAISchema] = useLocalStorageQuery(LOCAL_STORAGE_KEYS.SQL_EDITOR_AI_SCHEMA, true)
30+
const organization = useSelectedOrganization()
31+
32+
const { sqlDiff, setDebugSolution, setAiInput, setSqlDiff, setSelectedDiffType } = useSqlEditor()
33+
const { mutateAsync: debugSql, isLoading: isDebugSqlLoading } = useSqlDebugMutation()
3234

35+
const isOptedInToAI = organization?.opt_in_tags?.includes(OPT_IN_TAGS.AI_SQL) ?? false
36+
const [hasEnabledAISchema] = useLocalStorageQuery(LOCAL_STORAGE_KEYS.SQL_EDITOR_AI_SCHEMA, true)
3337
const includeSchemaMetadata = (isOptedInToAI || !IS_PLATFORM) && hasEnabledAISchema
3438

3539
const { data } = useEntityDefinitionsQuery(
@@ -67,6 +71,9 @@ const UtilityTabResults = ({ id, isExecuting }: UtilityTabResultsProps) => {
6771
const formattedError = (result.error?.formattedError?.split('\n') ?? []).filter(
6872
(x: string) => x.length > 0
6973
)
74+
const readReplicaError =
75+
state.selectedDatabaseId !== ref &&
76+
result.error.message.includes('in a read-only transaction')
7077

7178
return (
7279
<div className="bg-table-header-light [[data-theme*=dark]_&]:bg-table-header-dark">
@@ -105,50 +112,67 @@ const UtilityTabResults = ({ id, isExecuting }: UtilityTabResultsProps) => {
105112
</pre>
106113
))
107114
) : (
108-
<p className="font-mono text-sm">{result.error?.message}</p>
115+
<>
116+
<p className="font-mono text-sm">Error: {result.error?.message}</p>
117+
{readReplicaError && (
118+
<p className="text-sm text-foreground-light">
119+
Note: Read replicas are for read only queries. Run write queries on the
120+
primary database instead.
121+
</p>
122+
)}
123+
</>
109124
)}
110125
</div>
111126
)}
112-
{!hasHipaaAddon && (
113-
<Button
114-
icon={
115-
<div className="scale-75">
116-
<AiIconAnimation className="w-3 h-3" loading={isDebugSqlLoading} />
117-
</div>
118-
}
119-
disabled={!!sqlDiff || isDebugSqlLoading}
120-
onClick={async () => {
121-
try {
122-
const { solution, sql } = await debugSql({
123-
sql: snippet.snippet.content.sql.replace(sqlAiDisclaimerComment, '').trim(),
124-
errorMessage: result.error.message,
125-
entityDefinitions,
126-
})
127-
128-
const formattedSql = format(sql, {
129-
language: 'postgresql',
130-
keywordCase: 'lower',
131-
})
132-
setAiInput('')
133-
setDebugSolution(solution)
134-
setSqlDiff({
135-
original: snippet.snippet.content.sql,
136-
modified: formattedSql,
137-
})
138-
setSelectedDiffType(DiffType.Modification)
139-
} catch (error: unknown) {
140-
if (isError(error)) {
141-
ui.setNotification({
142-
category: 'error',
143-
message: `Failed to debug: ${error.message}`,
127+
128+
<div className="flex items-center gap-x-2">
129+
{readReplicaError && (
130+
<Button
131+
className="py-2"
132+
type="default"
133+
onClick={() => {
134+
state.setSelectedDatabaseId(ref)
135+
snap.resetResult(id)
136+
}}
137+
>
138+
Switch to primary database
139+
</Button>
140+
)}
141+
{!hasHipaaAddon && (
142+
<Button
143+
icon={<AiIconAnimation className="scale-75 w-3 h-3" loading={isDebugSqlLoading} />}
144+
disabled={!!sqlDiff || isDebugSqlLoading}
145+
onClick={async () => {
146+
try {
147+
const { solution, sql } = await debugSql({
148+
sql: snippet.snippet.content.sql.replace(sqlAiDisclaimerComment, '').trim(),
149+
errorMessage: result.error.message,
150+
entityDefinitions,
144151
})
152+
153+
const formattedSql =
154+
sqlAiDisclaimerComment +
155+
'\n\n' +
156+
format(sql, {
157+
language: 'postgresql',
158+
keywordCase: 'lower',
159+
})
160+
setAiInput('')
161+
setDebugSolution(solution)
162+
setSqlDiff({
163+
original: snippet.snippet.content.sql,
164+
modified: formattedSql,
165+
})
166+
setSelectedDiffType(DiffType.Modification)
167+
} catch (error: unknown) {
168+
if (isError(error)) toast.error(`Failed to debug: ${error.message}`)
145169
}
146-
}
147-
}}
148-
>
149-
Debug with Supabase AI
150-
</Button>
151-
)}
170+
}}
171+
>
172+
Debug with Supabase AI
173+
</Button>
174+
)}
175+
</div>
152176
</div>
153177
</div>
154178
)

apps/studio/components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/DeployNewReplicaPanel.tsx

Lines changed: 30 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { useParams } from 'common'
2+
import Link from 'next/link'
23
import { useEffect, useState } from 'react'
34
import toast from 'react-hot-toast'
45
import {
@@ -10,16 +11,14 @@ import {
1011
SidePanel,
1112
} from 'ui'
1213

14+
import { WarningIcon } from 'components/ui/Icons'
1315
import { Region, useReadReplicaSetUpMutation } from 'data/read-replicas/replica-setup-mutation'
14-
import { useProjectAddonsQuery } from 'data/subscriptions/project-addons-query'
15-
import { AWS_REGIONS, AWS_REGIONS_DEFAULT, AWS_REGIONS_KEYS, BASE_PATH } from 'lib/constants'
16-
import { AVAILABLE_REPLICA_REGIONS, AWS_REGIONS_VALUES } from './InstanceConfiguration.constants'
1716
import { useReadReplicasQuery } from 'data/read-replicas/replicas-query'
18-
import Link from 'next/link'
1917
import { useOrgSubscriptionQuery } from 'data/subscriptions/org-subscription-query'
18+
import { useProjectAddonsQuery } from 'data/subscriptions/project-addons-query'
2019
import { useSelectedOrganization, useSelectedProject } from 'hooks'
21-
import { WarningIcon } from 'components/ui/Icons'
22-
import { getSemanticVersion } from 'lib/helpers'
20+
import { AWS_REGIONS, AWS_REGIONS_DEFAULT, AWS_REGIONS_KEYS, BASE_PATH } from 'lib/constants'
21+
import { AVAILABLE_REPLICA_REGIONS, AWS_REGIONS_VALUES } from './InstanceConfiguration.constants'
2322

2423
// [Joshen] FYI this is purely for AWS only, need to update to support Fly eventually
2524

@@ -81,7 +80,7 @@ const DeployNewReplicaPanel = ({
8180
// Will be following the primary's instance size for the time being
8281
const defaultCompute =
8382
addons?.selected_addons.find((addon) => addon.type === 'compute_instance')?.variant
84-
.identifier ?? 'ci_small'
83+
.identifier ?? 'ci_micro'
8584

8685
const [selectedRegion, setSelectedRegion] = useState<string>(defaultRegion)
8786
const [selectedCompute, setSelectedCompute] = useState(defaultCompute)
@@ -161,6 +160,7 @@ const DeployNewReplicaPanel = ({
161160
</AlertDescription_Shadcn_>
162161
</Alert_Shadcn_>
163162
)}
163+
164164
{reachedMaxReplicas && (
165165
<Alert_Shadcn_>
166166
<WarningIcon />
@@ -172,6 +172,7 @@ const DeployNewReplicaPanel = ({
172172
</AlertDescription_Shadcn_>
173173
</Alert_Shadcn_>
174174
)}
175+
175176
{/* [Joshen] Not particular about this warning as all users on prod are on AWS */}
176177
{project?.cloud_provider !== 'AWS' && (
177178
<Alert_Shadcn_>
@@ -185,6 +186,7 @@ const DeployNewReplicaPanel = ({
185186
</AlertDescription_Shadcn_>
186187
</Alert_Shadcn_>
187188
)}
189+
188190
{currentPgVersion < 15 && (
189191
<Alert_Shadcn_>
190192
<WarningIcon />
@@ -207,6 +209,7 @@ const DeployNewReplicaPanel = ({
207209
</AlertDescription_Shadcn_>
208210
</Alert_Shadcn_>
209211
)}
212+
210213
<Listbox
211214
size="small"
212215
id="region"
@@ -234,26 +237,27 @@ const DeployNewReplicaPanel = ({
234237
))}
235238
</Listbox>
236239

237-
<Listbox
238-
disabled
239-
size="small"
240-
id="compute"
241-
name="compute"
242-
value={selectedCompute}
243-
onChange={setSelectedCompute}
244-
label="Select the instance size for your read replica"
245-
descriptionText="Read replicas will be on the same instance size as your primary"
246-
>
247-
{computeAddons.map((option) => (
248-
<Listbox.Option key={option.identifier} label={option.name} value={option.identifier}>
249-
{option.name}
250-
</Listbox.Option>
251-
))}
252-
</Listbox>
240+
<div className="flex flex-col gap-y-2">
241+
<p className="text-foreground-light text-sm">
242+
Read replicas will be on the same compute size as your primary database. Deploying a
243+
read replica incurs additional{' '}
244+
<span className="text-foreground">{selectedComputeMeta?.name}</span> compute hours.
245+
Pricing is still in early access and is subject to change.
246+
</p>
253247

254-
{/* <p className="text-xs text-foreground-light">
255-
Show some preview info on cost for deploying this replica here
256-
</p> */}
248+
<p className="text-foreground-light text-sm">
249+
Read more about{' '}
250+
<Link
251+
href="https://supabase.com/docs/guides/platform/org-based-billing#usage-based-billing-for-compute"
252+
target="_blank"
253+
rel="noreferrer"
254+
className="underline"
255+
>
256+
Usage-based billing
257+
</Link>{' '}
258+
for compute.
259+
</p>
260+
</div>
257261
</SidePanel.Content>
258262
</SidePanel>
259263
)

apps/studio/components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/InstanceConfiguration.constants.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import { AWS_REGIONS, AWS_REGIONS_KEYS } from 'lib/constants'
1+
import { components } from 'data/api'
2+
import { AWS_REGIONS, AWS_REGIONS_KEYS, PROJECT_STATUS } from 'lib/constants'
23

34
export interface Region {
45
key: AWS_REGIONS_KEYS
@@ -12,6 +13,14 @@ export const NODE_WIDTH = 660
1213
export const NODE_ROW_HEIGHT = 50
1314
export const NODE_SEP = 20
1415

16+
export const REPLICA_STATUS: {
17+
[key: string]: components['schemas']['DatabaseStatusResponse']['status']
18+
} = {
19+
...PROJECT_STATUS,
20+
INIT_READ_REPLICA: 'INIT_READ_REPLICA',
21+
INIT_READ_REPLICA_FAILED: 'INIT_READ_REPLICA_FAILED',
22+
}
23+
1524
// [Joshen] Coordinates from https://github.com/jsonmaur/aws-regions/issues/11
1625
// In the format of [lon, lat]
1726
export const AWS_REGIONS_COORDINATES: { [key: string]: [number, number] } = {

apps/studio/components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/InstanceConfiguration.tsx

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -49,13 +49,17 @@ const InstanceConfigurationUI = () => {
4949
const [selectedReplicaToDrop, setSelectedReplicaToDrop] = useState<Database>()
5050
const [selectedReplicaToRestart, setSelectedReplicaToRestart] = useState<Database>()
5151

52-
const { data: loadBalancers, isSuccess: isSuccessLoadBalancers } = useLoadBalancersQuery({
52+
const {
53+
data: loadBalancers,
54+
refetch: refetchLoadBalancers,
55+
isSuccess: isSuccessLoadBalancers,
56+
} = useLoadBalancersQuery({
5357
projectRef,
5458
})
5559
const {
5660
data,
5761
error,
58-
refetch,
62+
refetch: refetchReplicas,
5963
isLoading,
6064
isError,
6165
isSuccess: isSuccessReplicas,
@@ -79,7 +83,8 @@ const InstanceConfigurationUI = () => {
7983
// If any replica's status has changed, refetch databases
8084
if (numComingUp.current !== comingUpReplicas.length) {
8185
numComingUp.current = comingUpReplicas.length
82-
await refetch()
86+
await refetchReplicas()
87+
setTimeout(() => refetchLoadBalancers(), 2000)
8388
}
8489

8590
// If all replicas are active healthy, stop fetching statuses

0 commit comments

Comments
 (0)