From ca7b7d19bb0ab0141443c096c5f98eefc0a8b2ff Mon Sep 17 00:00:00 2001
From: Joshen Lim
Date: Fri, 21 Mar 2025 15:30:50 +0800
Subject: [PATCH] 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
---
.../interfaces/Realtime/RealtimeSettings.tsx | 160 +++++++++++++-----
.../pages/project/[ref]/realtime/settings.tsx | 8 +-
2 files changed, 124 insertions(+), 44 deletions(-)
diff --git a/apps/studio/components/interfaces/Realtime/RealtimeSettings.tsx b/apps/studio/components/interfaces/Realtime/RealtimeSettings.tsx
index a56bf523fc2..c46e3bbb953 100644
--- a/apps/studio/components/interfaces/Realtime/RealtimeSettings.tsx
+++ b/apps/studio/components/interfaces/Realtime/RealtimeSettings.tsx
@@ -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>({
resolver: zodResolver(FormSchema),
- defaultValues: REALTIME_DEFAULT_CONFIG,
+ defaultValues: {
+ ...REALTIME_DEFAULT_CONFIG,
+ allow_public: !REALTIME_DEFAULT_CONFIG.private_only,
+ },
})
const onSubmit: SubmitHandler> = (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 = () => {
(
{
{
- Sets connection pool size for Realtime Authorization
+ Realtime Authorization uses this database pool to check client access
}
>
- Connection pool size
+ Database connection pool size
}
>
@@ -138,6 +167,14 @@ export const RealtimeSettings = () => {
/>
+ {!!maxConn && field.value > maxConn.maxConnections * 0.5 && (
+
+ )}
)}
@@ -154,11 +191,12 @@ export const RealtimeSettings = () => {
- Sets maximum number of concurrent users rate limit
+ Sets maximum number of concurrent clients that can connect to your
+ Realtime service
}
>
- Max concurrent users
+ Max concurrent clients
}
>
@@ -167,17 +205,43 @@ export const RealtimeSettings = () => {
+ {isSuccessSubscription && !isUsageBillingEnabled && (
+
+ You may adjust this setting in the{' '}
+
+ organization billing settings
+
+ >
+ }
+ />
+ )}
)}
/>
-
+
+ {/*
+ [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
+ */}
+
+ {/*
{
)}
/>
-
-
+ */}
+ {/*
{
)
}}
/>
-
-
+ */}
+ {/*
{
)}
/>
-
-
+ */}
+ {/*
{
)}
/>
-
-
- {form.formState.isDirty && (
- */}
+
+
+ {!canUpdateConfig && (
+
+ You need additional permissions to update realtime settings
+
+ )}
+
+
+ {form.formState.isDirty && (
+
+ )}
+
- )}
-
+
)}
diff --git a/apps/studio/pages/project/[ref]/realtime/settings.tsx b/apps/studio/pages/project/[ref]/realtime/settings.tsx
index fc6c50e2170..bddfd65f9c2 100644
--- a/apps/studio/pages/project/[ref]/realtime/settings.tsx
+++ b/apps/studio/pages/project/[ref]/realtime/settings.tsx
@@ -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) => (
-
+ }
+ >
{page}