Files
supabase/apps/studio/components/interfaces/Realtime/RealtimeSettings.tsx
Sean GeogheganandJoshen Lim f2ff4ec0e9 fix(realtime): display banner when realtime has been suspended by admin (#50497)
## 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?

If Realtime's admin_suspended_at is set we display a banner.

## What is the current behavior?

REAL-1095

## What is the new behavior?

<img width="1160" height="442" alt="Screenshot 2026-09-17 at 12 06
30 pm"
src="https://github.com/user-attachments/assets/f5abe900-a163-48c9-ab55-945fc62df0d1"
/>


## Additional context

Depends on https://github.com/supabase/platform/pull/38476


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

## Summary by CodeRabbit

- **New Features**
- Added a notice to Realtime settings when the service is suspended,
with instructions to contact support.
- **Bug Fixes**
- Improved invitation resending and role updates for roles without a
linked base role.
- **Tests**
- Added coverage to verify the suspension notice appears only when
applicable.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Joshen Lim <joshenlimek@gmail.com>
2026-09-28 15:17:24 +08:00

762 lines
32 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 { SuspensionNotice } from '../Settings/SuspensionNotice'
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
const isAdminSuspended = Boolean(data?.admin_suspended_at)
// 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 (
<>
{isAdminSuspended && <SuspensionNotice suspendedAt={data?.admin_suspended_at} />}
<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>
</>
)
}