Update realtime settings based on feedback (#34268)

* Update realtime settings based on feedback

* Add pool size warning

* Update pool size warning threshold from 80 to 50%

* Smol update

* Check

* Check

* Fix

* Update label
This commit is contained in:
Joshen Lim authored and GitHub committed 2025-03-21 15:30:50 +08:00
1 parent ecaba079be
commit ca7b7d19bb
2 files changed
+124 -44

No files matched your search

@@ -6,16 +6,20 @@ import { toast } from 'sonner'
import * as z from 'zod'
import { useParams } from 'common'
import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
import { ScaffoldSection } from 'components/layouts/Scaffold'
import { convertFromBytes } from 'components/to-be-cleaned/Storage/StorageSettings/StorageSettings.utils'
import AlertError from 'components/ui/AlertError'
import { FormSection, FormSectionContent, FormSectionLabel } from 'components/ui/Forms/FormSection'
import { InlineLink } from 'components/ui/InlineLink'
import { useMaxConnectionsQuery } from 'data/database/max-connections-query'
import { useRealtimeConfigurationUpdateMutation } from 'data/realtime/realtime-config-mutation'
import {
REALTIME_DEFAULT_CONFIG,
useRealtimeConfigurationQuery,
} from 'data/realtime/realtime-config-query'
import { useOrgSubscriptionQuery } from 'data/subscriptions/org-subscription-query'
import { useCheckPermissions } from 'hooks/misc/useCheckPermissions'
import { useSelectedOrganization } from 'hooks/misc/useSelectedOrganization'
import {
Button,
Card,
@@ -28,27 +32,29 @@ import {
Input_Shadcn_,
Switch,
} from 'ui'
import { Admonition } from 'ui-patterns'
import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
const formId = 'realtime-configuration-form'
const FormSchema = z.object({
private_only: z.boolean(),
connection_pool: z.coerce.number().min(1).max(100),
max_concurrent_users: z.coerce.number().min(1).max(50000),
max_events_per_second: z.coerce.number().min(1).max(50000),
max_bytes_per_second: z.coerce.number().min(1).max(10000000),
max_channels_per_client: z.coerce.number().min(1).max(10000),
max_joins_per_second: z.coerce.number().min(1).max(5000),
})
export const RealtimeSettings = () => {
const { ref: projectRef } = useParams()
const { project } = useProjectContext()
const organization = useSelectedOrganization()
const canUpdateConfig = useCheckPermissions(PermissionAction.REALTIME_ADMIN_READ, '*')
const { data: maxConn } = useMaxConnectionsQuery({
projectRef: project?.ref,
connectionString: project?.connectionString,
})
const { data, error, isLoading, isSuccess, isError } = useRealtimeConfigurationQuery({
projectRef,
})
const { data: subscription, isSuccess: isSuccessSubscription } = useOrgSubscriptionQuery({
orgSlug: organization?.slug,
})
const isUsageBillingEnabled = subscription?.usage_billing_enabled
const { mutate: updateRealtimeConfig, isLoading: isUpdatingConfig } =
useRealtimeConfigurationUpdateMutation({
onSuccess: () => {
@@ -57,20 +63,43 @@ export const RealtimeSettings = () => {
},
})
const FormSchema = z.object({
allow_public: z.boolean(),
connection_pool: z.coerce
.number()
.min(1)
.max(maxConn?.maxConnections ?? 100),
max_concurrent_users: z.coerce.number().min(1).max(50000),
// [Joshen] These fields are temporarily hidden from the UI
// max_events_per_second: z.coerce.number().min(1).max(50000),
// max_bytes_per_second: z.coerce.number().min(1).max(10000000),
// max_channels_per_client: z.coerce.number().min(1).max(10000),
// max_joins_per_second: z.coerce.number().min(1).max(5000),
})
const form = useForm<z.infer<typeof FormSchema>>({
resolver: zodResolver(FormSchema),
defaultValues: REALTIME_DEFAULT_CONFIG,
defaultValues: {
...REALTIME_DEFAULT_CONFIG,
allow_public: !REALTIME_DEFAULT_CONFIG.private_only,
},
})
const onSubmit: SubmitHandler<z.infer<typeof FormSchema>> = (data) => {
if (!projectRef) return console.error('Project ref is required')
updateRealtimeConfig({ ref: projectRef, ...data })
updateRealtimeConfig({
ref: projectRef,
private_only: !data.allow_public,
connection_pool: data.connection_pool,
max_concurrent_users: data.max_concurrent_users,
})
}
useEffect(() => {
// [Joshen] Temp typed with any - API typing marks all the properties as nullable,
// but checked with Filipe that they're not supposed to
if (data) form.reset(data as any)
if (data) form.reset({ ...data, allow_public: !data.private_only } as any)
}, [isSuccess])
return (
@@ -84,7 +113,7 @@ export const RealtimeSettings = () => {
<CardContent>
<FormField_Shadcn_
control={form.control}
name="private_only"
name="allow_public"
render={({ field }) => (
<FormSection
className="!p-0 !pt-2"
@@ -93,8 +122,8 @@ export const RealtimeSettings = () => {
<FormSectionContent loading={isLoading} className="!gap-y-2">
<FormItemLayout
layout="flex"
label="Private channels only"
description="If this is enabled, only private channels will be allowed"
label="Allow public access"
description="If disabled, only private channels will be allowed"
>
<FormControl_Shadcn_>
<Switch
@@ -120,11 +149,11 @@ export const RealtimeSettings = () => {
<FormSectionLabel
description={
<p className="text-foreground-lighter text-sm !mt-1">
Sets connection pool size for Realtime Authorization
Realtime Authorization uses this database pool to check client access
</p>
}
>
Connection pool size
Database connection pool size
</FormSectionLabel>
}
>
@@ -138,6 +167,14 @@ export const RealtimeSettings = () => {
/>
</FormControl_Shadcn_>
<FormMessage_Shadcn_ />
{!!maxConn && field.value > maxConn.maxConnections * 0.5 && (
<Admonition
showIcon={false}
type="warning"
title={`Pool size is greater than 50% of the max connections (${maxConn.maxConnections}) on your database`}
description="This may result in instability and unreliability with your database connections."
/>
)}
</FormSectionContent>
</FormSection>
)}
@@ -154,11 +191,12 @@ export const RealtimeSettings = () => {
<FormSectionLabel
description={
<p className="text-foreground-lighter text-sm !mt-1">
Sets maximum number of concurrent users rate limit
Sets maximum number of concurrent clients that can connect to your
Realtime service
</p>
}
>
Max concurrent users
Max concurrent clients
</FormSectionLabel>
}
>
@@ -167,17 +205,43 @@ export const RealtimeSettings = () => {
<Input_Shadcn_
{...field}
type="number"
disabled={!canUpdateConfig}
disabled={!isUsageBillingEnabled || !canUpdateConfig}
value={field.value || ''}
/>
</FormControl_Shadcn_>
<FormMessage_Shadcn_ />
{isSuccessSubscription && !isUsageBillingEnabled && (
<Admonition
showIcon={false}
type="default"
title="Spend cap needs to be disabled to configure this value"
description={
<>
You may adjust this setting in the{' '}
<InlineLink
href={`/org/${organization?.slug}/billing?panel=costControl`}
>
organization billing settings
</InlineLink>
</>
}
/>
)}
</FormSectionContent>
</FormSection>
)}
/>
</CardContent>
<CardContent>
{/*
[Joshen] The following fields are hidden from the UI temporarily while we figure out what settings to expose to the users
- Max events per second
- Max bytes per second
- Max channels per client
- Max joins per second
*/}
{/* <CardContent>
<FormField_Shadcn_
control={form.control}
name="max_events_per_second"
@@ -210,8 +274,8 @@ export const RealtimeSettings = () => {
</FormSection>
)}
/>
</CardContent>
<CardContent>
</CardContent> */}
{/* <CardContent>
<FormField_Shadcn_
control={form.control}
name="max_bytes_per_second"
@@ -253,8 +317,8 @@ export const RealtimeSettings = () => {
)
}}
/>
</CardContent>
<CardContent>
</CardContent> */}
{/* <CardContent>
<FormField_Shadcn_
control={form.control}
name="max_channels_per_client"
@@ -288,8 +352,8 @@ export const RealtimeSettings = () => {
</FormSection>
)}
/>
</CardContent>
<CardContent>
</CardContent> */}
{/* <CardContent>
<FormField_Shadcn_
control={form.control}
name="max_joins_per_second"
@@ -323,21 +387,31 @@ export const RealtimeSettings = () => {
</FormSection>
)}
/>
</CardContent>
<CardFooter className="justify-end space-x-2">
{form.formState.isDirty && (
<Button type="default" onClick={() => form.reset(data as any)}>
Cancel
</CardContent> */}
<CardFooter className="justify-between">
<div>
{!canUpdateConfig && (
<p className="text-sm text-foreground-light">
You need additional permissions to update realtime settings
</p>
)}
</div>
<div className="flex items-center gap-x-2">
{form.formState.isDirty && (
<Button type="default" onClick={() => form.reset(data as any)}>
Cancel
</Button>
)}
<Button
type="primary"
htmlType="submit"
form={formId}
disabled={!canUpdateConfig || isUpdatingConfig || !form.formState.isDirty}
loading={isUpdatingConfig}
>
Save changes
</Button>
)}
<Button
type="primary"
htmlType="submit"
disabled={!canUpdateConfig || isUpdatingConfig || !form.formState.isDirty}
loading={isUpdatingConfig}
>
Save changes
</Button>
</div>
</CardFooter>
</Card>
)}
@@ -5,6 +5,7 @@ import DefaultLayout from 'components/layouts/DefaultLayout'
import { PageLayout } from 'components/layouts/PageLayout/PageLayout'
import RealtimeLayout from 'components/layouts/RealtimeLayout/RealtimeLayout'
import { ScaffoldContainer } from 'components/layouts/Scaffold'
import { DocsButton } from 'components/ui/DocsButton'
const RealtimePoliciesPage: NextPageWithLayout = () => {
return (
@@ -17,7 +18,12 @@ const RealtimePoliciesPage: NextPageWithLayout = () => {
RealtimePoliciesPage.getLayout = (page) => (
<DefaultLayout>
<RealtimeLayout title="Realtime Settings">
<PageLayout title="Realtime Settings" subtitle="Configure your project's Realtime settings">
<PageLayout
title="Realtime Settings"
subtitle="Configure your project's Realtime settings"
// [Joshen] Scaffolding for now - once docs for this is ready
primaryActions={<DocsButton href="https://supabase.com/docs" />}
>
{page}
</PageLayout>
</RealtimeLayout>