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 && ( - + )} + - )} - + )} 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}