mirror of
https://github.com/supabase/supabase.git
synced 2026-10-06 18:05:11 +03:00
## I have read the [CONTRIBUTING.md](https://github.com/supabase/supabase/blob/master/CONTRIBUTING.md) file. YES ## What kind of change does this PR introduce? Bump realtime max_concurrent_users soft limit to 300k <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Increased the maximum supported concurrent clients from 50,000 to 300,000 when plan entitlements allow it. * **Bug Fixes** * Improved validation for concurrent-client limits, including clearer handling of entitlement-based and unlimited limits. <!-- end of auto-generated comment: release notes by coderabbit.ai --> Co-authored-by: Joshen Lim <joshenlimek@gmail.com>
760 lines
32 KiB
TypeScript
760 lines
32 KiB
TypeScript
import { zodResolver } from '@hookform/resolvers/zod'
|
||
import { PermissionAction } from '@supabase/shared-types/out/constants'
|
||
import { useParams } from 'common'
|
||
import Link from 'next/link'
|
||
import { useState } from 'react'
|
||
import { SubmitHandler, useForm, useWatch } from 'react-hook-form'
|
||
import { toast } from 'sonner'
|
||
import {
|
||
Button,
|
||
Card,
|
||
CardContent,
|
||
CardFooter,
|
||
Form,
|
||
FormControl,
|
||
FormField,
|
||
FormInputGroupInput,
|
||
FormMessage,
|
||
InputGroup,
|
||
InputGroupAddon,
|
||
InputGroupText,
|
||
Switch,
|
||
} from 'ui'
|
||
import { Admonition } from 'ui-patterns/Admonition'
|
||
import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal'
|
||
import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
|
||
import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader'
|
||
import * as z from 'zod'
|
||
|
||
import { AlertError } from '@/components/ui/AlertError'
|
||
import { ToggleSpendCapButton } from '@/components/ui/ToggleSpendCapButton'
|
||
import { UpgradePlanButton } from '@/components/ui/UpgradePlanButton'
|
||
import { useDatabasePoliciesQuery } from '@/data/database-policies/database-policies-query'
|
||
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 { useCheckEntitlements } from '@/hooks/misc/useCheckEntitlements'
|
||
import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions'
|
||
import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization'
|
||
import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
|
||
|
||
const formId = 'realtime-configuration-form'
|
||
|
||
const REALTIME_SOFT_LIMITS = {
|
||
max_concurrent_users: 300_000,
|
||
max_events_per_second: 50_000,
|
||
max_presence_events_per_second: 5_000,
|
||
max_payload_size_in_kb: 3_000,
|
||
}
|
||
|
||
const MAX_POSTGRES_CHANGES_POOL = 20
|
||
|
||
export const RealtimeSettings = () => {
|
||
const { ref: projectRef } = useParams()
|
||
const { data: project } = useSelectedProjectQuery()
|
||
const { data: organization, isSuccess: isSuccessOrganization } = useSelectedOrganizationQuery()
|
||
const { can: canUpdateConfig, isSuccess: isPermissionsLoaded } = useAsyncCheckPermissions(
|
||
PermissionAction.REALTIME_ADMIN_READ,
|
||
'*'
|
||
)
|
||
|
||
const [isConfirmNextModalOpen, setIsConfirmNextModalOpen] = useState(false)
|
||
|
||
const { data: maxConn } = useMaxConnectionsQuery({
|
||
projectRef: project?.ref,
|
||
connectionString: project?.connectionString,
|
||
})
|
||
const { data, error, isError, isPending } = useRealtimeConfigurationQuery({
|
||
projectRef,
|
||
})
|
||
|
||
const { data: policies, isSuccess: isSuccessPolicies } = useDatabasePoliciesQuery({
|
||
projectRef,
|
||
connectionString: project?.connectionString,
|
||
schemas: ['realtime'],
|
||
})
|
||
|
||
// Per-plan realtime ceilings come from the org's entitlements (plan + any overrides).
|
||
// The effective limit is the lower of the entitlement and the soft cap, so an unlimited
|
||
// plan is still bounded and self-hosted / loading falls back to the soft cap.
|
||
const { getEntitlementMax: getMaxConcurrentUsers, isSuccess: isSuccessMaxConcurrentUsers } =
|
||
useCheckEntitlements('realtime.max_concurrent_users')
|
||
const { getEntitlementMax: getMaxEventsPerSecond, isSuccess: isSuccessMaxEventsPerSecond } =
|
||
useCheckEntitlements('realtime.max_events_per_second')
|
||
const {
|
||
getEntitlementMax: getMaxPresenceEventsPerSecond,
|
||
isSuccess: isSuccessMaxPresenceEventsPerSecond,
|
||
} = useCheckEntitlements('realtime.max_presence_events_per_second')
|
||
const { getEntitlementMax: getMaxPayloadSizeInKb, isSuccess: isSuccessMaxPayloadSizeInKb } =
|
||
useCheckEntitlements('realtime.max_payload_size_in_kb')
|
||
|
||
const isRealtimeEntitlementsLoaded =
|
||
isSuccessMaxConcurrentUsers &&
|
||
isSuccessMaxEventsPerSecond &&
|
||
isSuccessMaxPresenceEventsPerSecond &&
|
||
isSuccessMaxPayloadSizeInKb
|
||
|
||
const isLoading = isPending || !isRealtimeEntitlementsLoaded
|
||
|
||
const maxConcurrentUsersLimit = Math.min(
|
||
getMaxConcurrentUsers() ?? Infinity,
|
||
REALTIME_SOFT_LIMITS.max_concurrent_users
|
||
)
|
||
const maxEventsPerSecondLimit = Math.min(
|
||
getMaxEventsPerSecond() ?? Infinity,
|
||
REALTIME_SOFT_LIMITS.max_events_per_second
|
||
)
|
||
const maxPresenceEventsPerSecondLimit = Math.min(
|
||
getMaxPresenceEventsPerSecond() ?? Infinity,
|
||
REALTIME_SOFT_LIMITS.max_presence_events_per_second
|
||
)
|
||
const maxPayloadSizeInKbLimit = Math.min(
|
||
getMaxPayloadSizeInKb() ?? Infinity,
|
||
REALTIME_SOFT_LIMITS.max_payload_size_in_kb
|
||
)
|
||
|
||
const isFreePlan = organization?.plan.id === 'free'
|
||
const isUsageBillingEnabled = organization?.usage_billing_enabled
|
||
const isRealtimeDisabled = data?.suspend ?? REALTIME_DEFAULT_CONFIG.suspend
|
||
// Check if RLS policies exist for realtime.messages table
|
||
const realtimeMessagesPolicies = policies?.filter(
|
||
(policy) => policy.schema === 'realtime' && policy.table === 'messages'
|
||
)
|
||
const hasRealtimeMessagesPolicies =
|
||
realtimeMessagesPolicies && realtimeMessagesPolicies.length > 0
|
||
|
||
const { mutate: updateRealtimeConfig, isPending: isUpdatingConfig } =
|
||
useRealtimeConfigurationUpdateMutation({
|
||
onSuccess: () => {
|
||
form.reset(form.getValues())
|
||
toast.success('Successfully updated realtime settings')
|
||
setIsConfirmNextModalOpen(false)
|
||
},
|
||
})
|
||
|
||
const FormSchema = z.discriminatedUnion('suspend', [
|
||
z.object({
|
||
suspend: z.literal(true),
|
||
connection_pool: z.coerce
|
||
.number()
|
||
.min(1)
|
||
.max(maxConn?.maxConnections ?? 100)
|
||
.optional(),
|
||
postgres_changes_pool: z.coerce.number().min(1).max(MAX_POSTGRES_CHANGES_POOL).optional(),
|
||
max_concurrent_users: z.coerce
|
||
.number()
|
||
.min(1)
|
||
.max(
|
||
maxConcurrentUsersLimit,
|
||
`Cannot exceed ${maxConcurrentUsersLimit.toLocaleString()} concurrent clients`
|
||
)
|
||
.optional(),
|
||
max_events_per_second: z.coerce
|
||
.number()
|
||
.min(1)
|
||
.max(
|
||
maxEventsPerSecondLimit,
|
||
`Cannot exceed ${maxEventsPerSecondLimit.toLocaleString()} events per second`
|
||
)
|
||
.optional(),
|
||
max_presence_events_per_second: z.coerce
|
||
.number()
|
||
.min(1)
|
||
.max(
|
||
maxPresenceEventsPerSecondLimit,
|
||
`Cannot exceed ${maxPresenceEventsPerSecondLimit.toLocaleString()} presence events per second`
|
||
)
|
||
.optional(),
|
||
max_payload_size_in_kb: z.coerce
|
||
.number()
|
||
.min(1)
|
||
.max(
|
||
maxPayloadSizeInKbLimit,
|
||
`Cannot exceed ${maxPayloadSizeInKbLimit.toLocaleString()} KB`
|
||
)
|
||
.optional(),
|
||
// [Joshen] These fields are temporarily hidden from the UI
|
||
// max_bytes_per_second: z.coerce.number().min(1).max(10000000).optional(),
|
||
// max_channels_per_client: z.coerce.number().min(1).max(10000).optional(),
|
||
// max_joins_per_second: z.coerce.number().min(1).max(5000).optional(),
|
||
|
||
allow_public: z.boolean().optional(),
|
||
}),
|
||
z.object({
|
||
suspend: z.literal(false),
|
||
connection_pool: z.coerce
|
||
.number()
|
||
.min(1)
|
||
.max(maxConn?.maxConnections ?? 100),
|
||
postgres_changes_pool: z.coerce.number().min(1).max(MAX_POSTGRES_CHANGES_POOL),
|
||
max_concurrent_users: z.coerce
|
||
.number()
|
||
.min(1)
|
||
.max(
|
||
maxConcurrentUsersLimit,
|
||
`Cannot exceed ${maxConcurrentUsersLimit.toLocaleString()} concurrent clients`
|
||
),
|
||
max_events_per_second: z.coerce
|
||
.number()
|
||
.min(1)
|
||
.max(
|
||
maxEventsPerSecondLimit,
|
||
`Cannot exceed ${maxEventsPerSecondLimit.toLocaleString()} events per second`
|
||
),
|
||
max_presence_events_per_second: z.coerce
|
||
.number()
|
||
.min(1)
|
||
.max(
|
||
maxPresenceEventsPerSecondLimit,
|
||
`Cannot exceed ${maxPresenceEventsPerSecondLimit.toLocaleString()} presence events per second`
|
||
),
|
||
max_payload_size_in_kb: z.coerce
|
||
.number()
|
||
.min(1)
|
||
.max(
|
||
maxPayloadSizeInKbLimit,
|
||
`Cannot exceed ${maxPayloadSizeInKbLimit.toLocaleString()} KB`
|
||
),
|
||
// [Joshen] These fields are temporarily hidden from the UI
|
||
// 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),
|
||
|
||
allow_public: z.boolean(),
|
||
}),
|
||
])
|
||
|
||
const configValues = data ?? REALTIME_DEFAULT_CONFIG
|
||
const sharedFormValues = {
|
||
connection_pool: configValues.connection_pool ?? REALTIME_DEFAULT_CONFIG.connection_pool,
|
||
postgres_changes_pool:
|
||
configValues.postgres_changes_pool ?? REALTIME_DEFAULT_CONFIG.postgres_changes_pool,
|
||
max_concurrent_users:
|
||
configValues.max_concurrent_users ?? REALTIME_DEFAULT_CONFIG.max_concurrent_users,
|
||
max_events_per_second:
|
||
configValues.max_events_per_second ?? REALTIME_DEFAULT_CONFIG.max_events_per_second,
|
||
max_presence_events_per_second:
|
||
configValues.max_presence_events_per_second ??
|
||
REALTIME_DEFAULT_CONFIG.max_presence_events_per_second,
|
||
max_payload_size_in_kb:
|
||
configValues.max_payload_size_in_kb ?? REALTIME_DEFAULT_CONFIG.max_payload_size_in_kb,
|
||
allow_public: !(configValues.private_only ?? REALTIME_DEFAULT_CONFIG.private_only),
|
||
}
|
||
const formValues: z.infer<typeof FormSchema> = {
|
||
suspend: configValues.suspend ?? REALTIME_DEFAULT_CONFIG.suspend,
|
||
...sharedFormValues,
|
||
}
|
||
|
||
const form = useForm<z.infer<typeof FormSchema>>({
|
||
resolver: zodResolver(FormSchema),
|
||
defaultValues: {
|
||
...REALTIME_DEFAULT_CONFIG,
|
||
allow_public: !REALTIME_DEFAULT_CONFIG.private_only,
|
||
},
|
||
values: formValues,
|
||
})
|
||
|
||
const [allow_public, suspend] = useWatch({
|
||
control: form.control,
|
||
name: ['allow_public', 'suspend'],
|
||
})
|
||
const isSettingToPrivate = !data?.private_only && !allow_public
|
||
const isDisablingRealtime = !isRealtimeDisabled && suspend
|
||
const isEnablingRealtime = isRealtimeDisabled && !suspend
|
||
|
||
const onSubmit: SubmitHandler<z.infer<typeof FormSchema>> = (_data) => {
|
||
if (!projectRef) return console.error('Project ref is required')
|
||
if (!isRealtimeEntitlementsLoaded) return
|
||
setIsConfirmNextModalOpen(true)
|
||
}
|
||
|
||
const onConfirmSave = () => {
|
||
if (!projectRef) return console.error('Project ref is required')
|
||
if (!isRealtimeEntitlementsLoaded) return
|
||
const values = form.getValues()
|
||
|
||
// [Joshen] Casting to `Number` here as the values are being set as string when edited in the form
|
||
// and returned in form.getValues() - I might be missing some easy util function from RHF though
|
||
updateRealtimeConfig({
|
||
ref: projectRef,
|
||
private_only: !values.allow_public,
|
||
connection_pool: Number(
|
||
values.connection_pool ?? data?.connection_pool ?? REALTIME_DEFAULT_CONFIG.connection_pool
|
||
),
|
||
postgres_changes_pool: Number(
|
||
values.postgres_changes_pool ??
|
||
data?.postgres_changes_pool ??
|
||
REALTIME_DEFAULT_CONFIG.postgres_changes_pool
|
||
),
|
||
max_concurrent_users: Number(
|
||
values.max_concurrent_users ??
|
||
data?.max_concurrent_users ??
|
||
REALTIME_DEFAULT_CONFIG.max_concurrent_users
|
||
),
|
||
max_events_per_second: Number(
|
||
values.max_events_per_second ??
|
||
data?.max_events_per_second ??
|
||
REALTIME_DEFAULT_CONFIG.max_events_per_second
|
||
),
|
||
max_presence_events_per_second: Number(
|
||
values.max_presence_events_per_second ??
|
||
data?.max_presence_events_per_second ??
|
||
REALTIME_DEFAULT_CONFIG.max_presence_events_per_second
|
||
),
|
||
max_payload_size_in_kb: Number(
|
||
values.max_payload_size_in_kb ??
|
||
data?.max_payload_size_in_kb ??
|
||
REALTIME_DEFAULT_CONFIG.max_payload_size_in_kb
|
||
),
|
||
suspend: values.suspend,
|
||
})
|
||
}
|
||
|
||
if (isLoading) {
|
||
return (
|
||
<Card>
|
||
<CardContent>
|
||
<GenericSkeletonLoader />
|
||
</CardContent>
|
||
</Card>
|
||
)
|
||
}
|
||
|
||
return (
|
||
<>
|
||
<Form {...form}>
|
||
<form id={formId} onSubmit={form.handleSubmit(onSubmit)}>
|
||
{isError ? (
|
||
<AlertError error={error} subject="Failed to retrieve realtime settings" />
|
||
) : (
|
||
<Card>
|
||
<CardContent className="space-y-4">
|
||
<FormField
|
||
control={form.control}
|
||
name="suspend"
|
||
render={({ field }) => (
|
||
<>
|
||
<FormItemLayout
|
||
id="suspend"
|
||
layout="flex-row-reverse"
|
||
label="Enable Realtime service"
|
||
description="If disabled, no clients will be able to connect and new connections will be rejected"
|
||
>
|
||
<FormControl>
|
||
<Switch
|
||
id="suspend"
|
||
aria-label="Toggle enabling of realtime service"
|
||
checked={!field.value}
|
||
onCheckedChange={(checked) => field.onChange(!checked)}
|
||
disabled={!canUpdateConfig}
|
||
/>
|
||
</FormControl>
|
||
</FormItemLayout>
|
||
<FormMessage />
|
||
</>
|
||
)}
|
||
/>
|
||
{(isRealtimeDisabled || isDisablingRealtime || isEnablingRealtime) && (
|
||
<Admonition
|
||
showIcon={false}
|
||
type={isDisablingRealtime || isEnablingRealtime ? 'warning' : 'default'}
|
||
title={
|
||
isDisablingRealtime
|
||
? 'Realtime service will be disabled'
|
||
: isEnablingRealtime
|
||
? 'Realtime service will be re-enabled'
|
||
: isRealtimeDisabled
|
||
? 'Realtime service is disabled'
|
||
: ''
|
||
}
|
||
description={
|
||
isDisablingRealtime
|
||
? 'Clients will no longer be able to connect to your project’s realtime service once saved'
|
||
: isEnablingRealtime
|
||
? "Clients will be able to connect to your project's realtime service again once saved"
|
||
: isRealtimeDisabled
|
||
? 'You will need to enable it to continue using Realtime'
|
||
: null
|
||
}
|
||
/>
|
||
)}
|
||
</CardContent>
|
||
|
||
{!suspend && (
|
||
<>
|
||
<CardContent>
|
||
<FormField
|
||
control={form.control}
|
||
name="allow_public"
|
||
render={({ field }) => (
|
||
<>
|
||
<FormItemLayout
|
||
id="allow_public"
|
||
layout="flex-row-reverse"
|
||
label="Allow public access to channels"
|
||
description="If disabled, only private channels will be allowed"
|
||
>
|
||
<FormControl>
|
||
<Switch
|
||
id="allow_public"
|
||
aria-label="Toggle allow public access"
|
||
checked={field.value}
|
||
onCheckedChange={field.onChange}
|
||
disabled={!canUpdateConfig}
|
||
/>
|
||
</FormControl>
|
||
</FormItemLayout>
|
||
|
||
{isSuccessPolicies &&
|
||
!hasRealtimeMessagesPolicies &&
|
||
!allow_public &&
|
||
!isRealtimeDisabled && (
|
||
<Admonition
|
||
showIcon={false}
|
||
className="mt-2"
|
||
type="warning"
|
||
title="No Realtime RLS policies found"
|
||
description={
|
||
<p className="prose max-w-full text-sm">
|
||
Private mode is {isSettingToPrivate ? 'being ' : ''}
|
||
enabled, but no RLS policies exist on the{' '}
|
||
<code className="text-code-inline">realtime.messages</code>{' '}
|
||
table. No messages will be received by users.
|
||
</p>
|
||
}
|
||
actions={
|
||
<Button asChild>
|
||
<Link href={`/project/${projectRef}/realtime/policies`}>
|
||
Create policy
|
||
</Link>
|
||
</Button>
|
||
}
|
||
/>
|
||
)}
|
||
</>
|
||
)}
|
||
/>
|
||
</CardContent>
|
||
<CardContent>
|
||
<FormField
|
||
control={form.control}
|
||
name="connection_pool"
|
||
render={({ field }) => (
|
||
<>
|
||
<FormItemLayout
|
||
id="connection_pool"
|
||
layout="flex-row-reverse"
|
||
label="Database connection pool size"
|
||
description="Realtime Authorization uses this database pool to check client access"
|
||
>
|
||
<FormControl>
|
||
<InputGroup>
|
||
<FormInputGroupInput
|
||
{...field}
|
||
id="connection_pool"
|
||
type="number"
|
||
disabled={!canUpdateConfig}
|
||
value={field.value || ''}
|
||
/>
|
||
<InputGroupAddon align="inline-end">
|
||
<InputGroupText>connections</InputGroupText>
|
||
</InputGroupAddon>
|
||
</InputGroup>
|
||
</FormControl>
|
||
</FormItemLayout>
|
||
{!!maxConn &&
|
||
field.value &&
|
||
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."
|
||
/>
|
||
)}
|
||
</>
|
||
)}
|
||
/>
|
||
</CardContent>
|
||
<CardContent>
|
||
<FormField
|
||
control={form.control}
|
||
name="postgres_changes_pool"
|
||
render={({ field }) => (
|
||
<FormItemLayout
|
||
id="postgres_changes_pool"
|
||
layout="flex-row-reverse"
|
||
label="Postgres Changes connection pool size"
|
||
description="Postgres Changes uses this database pool to create subscriptions when clients subscribe"
|
||
>
|
||
<FormControl>
|
||
<InputGroup>
|
||
<FormInputGroupInput
|
||
{...field}
|
||
id="postgres_changes_pool"
|
||
type="number"
|
||
disabled={!canUpdateConfig}
|
||
value={field.value || ''}
|
||
/>
|
||
<InputGroupAddon align="inline-end">
|
||
<InputGroupText>connections</InputGroupText>
|
||
</InputGroupAddon>
|
||
</InputGroup>
|
||
</FormControl>
|
||
</FormItemLayout>
|
||
)}
|
||
/>
|
||
</CardContent>
|
||
<CardContent>
|
||
<FormField
|
||
control={form.control}
|
||
name="max_concurrent_users"
|
||
render={({ field }) => (
|
||
<FormItemLayout
|
||
id="max_concurrent_users"
|
||
layout="flex-row-reverse"
|
||
label="Max concurrent clients"
|
||
description="Sets maximum number of concurrent clients that can connect to your Realtime service"
|
||
>
|
||
<FormControl>
|
||
<InputGroup>
|
||
<FormInputGroupInput
|
||
{...field}
|
||
id="max_concurrent_users"
|
||
type="number"
|
||
disabled={!canUpdateConfig}
|
||
value={field.value || ''}
|
||
/>
|
||
<InputGroupAddon align="inline-end">
|
||
<InputGroupText>clients</InputGroupText>
|
||
</InputGroupAddon>
|
||
</InputGroup>
|
||
</FormControl>
|
||
</FormItemLayout>
|
||
)}
|
||
/>
|
||
</CardContent>
|
||
<CardContent className="space-y-2">
|
||
<FormField
|
||
control={form.control}
|
||
name="max_events_per_second"
|
||
render={({ field }) => (
|
||
<FormItemLayout
|
||
id="max_events_per_second"
|
||
layout="flex-row-reverse"
|
||
label="Max events per second"
|
||
description="Sets maximum number of events per second that can be sent to your Realtime service"
|
||
>
|
||
<FormControl>
|
||
<InputGroup>
|
||
<FormInputGroupInput
|
||
{...field}
|
||
id="max_events_per_second"
|
||
type="number"
|
||
disabled={!isUsageBillingEnabled || !canUpdateConfig}
|
||
value={field.value || ''}
|
||
/>
|
||
<InputGroupAddon align="inline-end">
|
||
<InputGroupText>events/s</InputGroupText>
|
||
</InputGroupAddon>
|
||
</InputGroup>
|
||
</FormControl>
|
||
</FormItemLayout>
|
||
)}
|
||
/>
|
||
{isSuccessOrganization && !isUsageBillingEnabled && (
|
||
<Admonition showIcon={false} type="default">
|
||
<div className="flex items-center gap-x-2">
|
||
<div>
|
||
<h5 className="text-foreground mb-1">
|
||
Spend cap needs to be disabled to configure this value
|
||
</h5>
|
||
<p className="text-foreground-light">
|
||
{isFreePlan
|
||
? 'Upgrade to the Pro plan first to disable spend cap'
|
||
: 'You may adjust this setting in the organization billing settings'}
|
||
</p>
|
||
</div>
|
||
<div className="grow flex items-center justify-end">
|
||
{isFreePlan ? (
|
||
<UpgradePlanButton
|
||
source="realtimeSettings"
|
||
addon="spendCap"
|
||
featureProposition="configure the max events per second parameter of realtime settings"
|
||
/>
|
||
) : (
|
||
<ToggleSpendCapButton />
|
||
)}
|
||
</div>
|
||
</div>
|
||
</Admonition>
|
||
)}
|
||
</CardContent>
|
||
<CardContent className="space-y-2">
|
||
<FormField
|
||
control={form.control}
|
||
name="max_presence_events_per_second"
|
||
render={({ field }) => (
|
||
<FormItemLayout
|
||
id="max_presence_events_per_second"
|
||
layout="flex-row-reverse"
|
||
label="Max presence events per second"
|
||
description="Sets maximum number of presence events per second that can be sent to your Realtime service"
|
||
>
|
||
<FormControl>
|
||
<InputGroup>
|
||
<FormInputGroupInput
|
||
{...field}
|
||
id="max_presence_events_per_second"
|
||
type="number"
|
||
disabled={!isUsageBillingEnabled || !canUpdateConfig}
|
||
value={field.value || ''}
|
||
/>
|
||
<InputGroupAddon align="inline-end">
|
||
<InputGroupText>events/s</InputGroupText>
|
||
</InputGroupAddon>
|
||
</InputGroup>
|
||
</FormControl>
|
||
</FormItemLayout>
|
||
)}
|
||
/>
|
||
{isSuccessOrganization && !isUsageBillingEnabled && (
|
||
<Admonition showIcon={false} type="default">
|
||
<div className="flex items-center gap-x-2">
|
||
<div>
|
||
<h5 className="text-foreground mb-1">
|
||
Spend cap needs to be disabled to configure this value
|
||
</h5>
|
||
<p className="text-foreground-light">
|
||
{isFreePlan
|
||
? 'Upgrade to the Pro plan first to disable spend cap'
|
||
: 'You may adjust this setting in the organization billing settings'}
|
||
</p>
|
||
</div>
|
||
<div className="grow flex items-center justify-end">
|
||
{isFreePlan ? (
|
||
<UpgradePlanButton
|
||
source="realtimeSettings"
|
||
addon="spendCap"
|
||
featureProposition="configure the max presence events per second parameter of realtime settings"
|
||
/>
|
||
) : (
|
||
<ToggleSpendCapButton />
|
||
)}
|
||
</div>
|
||
</div>
|
||
</Admonition>
|
||
)}
|
||
</CardContent>
|
||
<CardContent className="space-y-2">
|
||
<FormField
|
||
control={form.control}
|
||
name="max_payload_size_in_kb"
|
||
render={({ field }) => (
|
||
<FormItemLayout
|
||
id="max_payload_size_in_kb"
|
||
layout="flex-row-reverse"
|
||
label="Max payload size in KB"
|
||
description="Sets maximum number of payload size in KB that can be sent to your Realtime service"
|
||
>
|
||
<FormControl>
|
||
<InputGroup>
|
||
<FormInputGroupInput
|
||
{...field}
|
||
id="max_payload_size_in_kb"
|
||
type="number"
|
||
disabled={!isUsageBillingEnabled || !canUpdateConfig}
|
||
value={field.value || ''}
|
||
/>
|
||
<InputGroupAddon align="inline-end">
|
||
<InputGroupText>KB</InputGroupText>
|
||
</InputGroupAddon>
|
||
</InputGroup>
|
||
</FormControl>
|
||
</FormItemLayout>
|
||
)}
|
||
/>
|
||
{isSuccessOrganization && !isUsageBillingEnabled && (
|
||
<Admonition showIcon={false} type="default">
|
||
<div className="flex items-center gap-x-2">
|
||
<div>
|
||
<h5 className="text-foreground mb-1">
|
||
Spend cap needs to be disabled to configure this value
|
||
</h5>
|
||
<p className="text-foreground-light">
|
||
{isFreePlan
|
||
? 'Upgrade to the Pro plan first to disable spend cap'
|
||
: 'You may adjust this setting in the organization billing settings'}
|
||
</p>
|
||
</div>
|
||
<div className="grow flex items-center justify-end">
|
||
{isFreePlan ? (
|
||
<UpgradePlanButton
|
||
addon="spendCap"
|
||
source="realtimeSettings"
|
||
featureProposition="configure the max payload size parameter of realtime settings"
|
||
/>
|
||
) : (
|
||
<ToggleSpendCapButton />
|
||
)}
|
||
</div>
|
||
</div>
|
||
</Admonition>
|
||
)}
|
||
</CardContent>
|
||
</>
|
||
)}
|
||
|
||
<CardFooter className="justify-between">
|
||
<div>
|
||
{isPermissionsLoaded && !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 onClick={() => form.reset(formValues)}>Cancel</Button>
|
||
)}
|
||
<Button
|
||
variant="primary"
|
||
type="submit"
|
||
form={formId}
|
||
disabled={
|
||
!canUpdateConfig ||
|
||
isUpdatingConfig ||
|
||
!form.formState.isDirty ||
|
||
!isRealtimeEntitlementsLoaded
|
||
}
|
||
loading={isUpdatingConfig}
|
||
>
|
||
Save changes
|
||
</Button>
|
||
</div>
|
||
</CardFooter>
|
||
</Card>
|
||
)}
|
||
</form>
|
||
</Form>
|
||
|
||
<ConfirmationModal
|
||
visible={isConfirmNextModalOpen}
|
||
title="Confirm saving changes"
|
||
confirmLabel="Save changes"
|
||
loading={isUpdatingConfig}
|
||
onCancel={() => setIsConfirmNextModalOpen(false)}
|
||
onConfirm={() => onConfirmSave()}
|
||
>
|
||
<p className="text-sm text-foreground-light">
|
||
Saving the changes will disconnect all the clients connected to your project. Are you sure
|
||
you want to continue?
|
||
</p>
|
||
</ConfirmationModal>
|
||
</>
|
||
)
|
||
}
|