Skip to content

Commit 1470c51

Browse files
authored
fix(studio): align rls confirmation dialogs (supabase#45961)
## What kind of change does this PR introduce? Studio UI cleanup and docs update. Resolves FE-3217. ## What is the current behavior? The table RLS confirmation dialogs differ across the Policies page and Table Editor surfaces. The table editor disable flow also uses a verbose confirmation modal with admonitions and repeated warning copy. | Policies | Table Editor | | --- | --- | | <img width="852" height="448" alt="25125" src="https://github.com/user-attachments/assets/baa9fafa-752c-4701-bf54-40da4e32030b" /> | <img width="1108" height="1016" alt="57354" src="https://github.com/user-attachments/assets/b1b88355-be39-49b9-aed2-a60f20c25104" /> | | <img width="866" height="434" alt="CleanShot 2026-05-15 at 18 01 15@2x" src="https://github.com/user-attachments/assets/d67f69ee-0a49-4dbd-939e-ca968361fc33" /> | <img width="840" height="426" alt="CleanShot 2026-05-15 at 18 01 52@2x" src="https://github.com/user-attachments/assets/a0b5f390-abeb-453f-8636-ad097f22308b" /> | ## What is the new behavior? Table RLS enable and disable confirmations now use one shared concise Alert Dialog treatment across the Policies page, Table Editor header, and Table Editor side panel. The server-backed toggles use the async Alert Dialog action behaviour from the stacked base branch, and the design-system docs now clarify when to start with Alert Dialog versus Confirmation Modal. This PR is stacked on supabase#45960. | Before | After | | --- | --- | | <img width="852" height="448" alt="25125" src="https://github.com/user-attachments/assets/baa9fafa-752c-4701-bf54-40da4e32030b" /> | <img width="864" height="518" alt="CleanShot 2026-05-15 at 18 07 21@2x" src="https://github.com/user-attachments/assets/bda53246-164b-4d5b-81a1-25d3bc661eb3" /> | | <img width="1108" height="1016" alt="57354" src="https://github.com/user-attachments/assets/b1b88355-be39-49b9-aed2-a60f20c25104" /> | <img width="864" height="518" alt="CleanShot 2026-05-15 at 18 07 21@2x" src="https://github.com/user-attachments/assets/bda53246-164b-4d5b-81a1-25d3bc661eb3" /> | | <img width="866" height="434" alt="CleanShot 2026-05-15 at 18 01 15@2x" src="https://github.com/user-attachments/assets/d67f69ee-0a49-4dbd-939e-ca968361fc33" /> | <img width="860" height="488" alt="CleanShot 2026-05-15 at 18 08 41@2x" src="https://github.com/user-attachments/assets/4c67f797-e2cd-4d56-a49d-e6c6fc0edff3" /> | | <img width="840" height="426" alt="CleanShot 2026-05-15 at 18 01 52@2x" src="https://github.com/user-attachments/assets/a0b5f390-abeb-453f-8636-ad097f22308b" /> | <img width="860" height="488" alt="CleanShot 2026-05-15 at 18 08 41@2x" src="https://github.com/user-attachments/assets/4c67f797-e2cd-4d56-a49d-e6c6fc0edff3" /> | <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Documentation** * Refined guidance and wording for dialog usage, clarifying when to use Alert Dialog, Confirmation Modal, and modal modality. * **New Features** * Reworked RLS confirmation flow with a streamlined toggle dialog, clearer messaging, and a “Learn more” link to docs. * **Tests** * Updated end-to-end tests to cover the updated RLS dialog flows and explanatory content. <!-- review_stack_entry_start --> [![Review Change Stack](https://storage.googleapis.com/coderabbit_public_assets/review-stack-in-coderabbit-ui.svg)](https://app.coderabbit.ai/change-stack/supabase/supabase/pull/45961?utm_source=github_walkthrough&utm_medium=github&utm_campaign=change_stack) <!-- review_stack_entry_end --> <!-- end of auto-generated comment: release notes by coderabbit.ai -->
1 parent 1e35420 commit 1470c51

10 files changed

Lines changed: 132 additions & 112 deletions

File tree

apps/design-system/content/docs/components/alert-dialog.mdx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,10 @@ source:
1212

1313
Alert Dialog interrupts the user’s workflow to communicate critical information or confirm an action that cannot be taken lightly. It presents a short, focused message and requires the user to explicitly confirm or cancel before proceeding.
1414

15-
Use Alert Dialog for actions such as deleting data, performing irreversible changes, or acknowledging important warnings where dismissal without a decision would be unsafe.
15+
Use Alert Dialog for actions such as deleting data, performing irreversible changes, or
16+
acknowledging important warnings where dismissal without a decision would be unsafe. It is the
17+
preferred starting point for critical confirmations when the decision can be explained in a short,
18+
focused message.
1619

1720
<ComponentPreview name="alert-dialog-demo" peekCode wide />
1821

@@ -108,7 +111,7 @@ inline error feedback.
108111
- **Use for dirty-form discard confirmation:** A short discard-confirmation step after a dirty form dismissal attempt (backdrop, Escape, or `Cancel`) is a valid Alert Dialog pattern. In Studio, prefer `DiscardChangesConfirmationDialog` for this flow.
109112
- **Always provide a cancel action:** Include AlertDialogCancel so users can safely back out, in addition to supporting the Escape key.
110113
- **Use AlertDialogBody for inline feedback:** If async actions can fail, render inline feedback such as an Admonition inside AlertDialogBody so spacing stays consistent.
111-
- **Avoid rich content:** If the dialog requires detailed explanations, callouts, or form inputs, use [Confirmation Modal](../fragments/confirmation-modal) or [Dialog](../components/dialog) instead.
114+
- **Avoid rich content:** If the decision requires detailed explanations, callouts, multiple paragraphs, or form inputs, move to [Confirmation Modal](../fragments/confirmation-modal) or [Dialog](../components/dialog) instead.
112115

113116
See [Modality](../ui-patterns/modality) for guidance on choosing the appropriate dialog pattern.
114117

apps/design-system/content/docs/fragments/confirmation-modal.mdx

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,16 @@ description: A modal dialog for confirmations that require additional context or
44
component: true
55
---
66

7-
Confirmation Modal is a convenience wrapper for confirmation flows that are more complex than a single paragraph but do not warrant a full custom dialog. It is built on top of [Dialog](../components/dialog) and provides a prop-based API for consistent confirmation patterns.
7+
Confirmation Modal is a convenience wrapper for confirmation flows that need more body structure
8+
than a concise Alert Dialog but do not warrant a full custom dialog. It is built on top of
9+
[Dialog](../components/dialog) and provides a prop-based API for consistent confirmation patterns.
810

911
Use Confirmation Modal when the user needs extra context to make a decision, such as explanatory copy, callouts, or small form elements, and the action is not so destructive that it requires typed confirmation.
1012

11-
If the confirmation can be expressed as a single short paragraph, use [Alert Dialog](../components/alert-dialog). If the action is highly destructive and requires explicit typed intent, use [Text Confirm Dialog](../fragments/text-confirm-dialog). See [Modality](../ui-patterns/modality) for broader guidance on choosing the appropriate pattern.
13+
If a critical confirmation can be expressed as a single short paragraph, start with
14+
[Alert Dialog](../components/alert-dialog). If the action is highly destructive and requires
15+
explicit typed intent, use [Text Confirm Dialog](../fragments/text-confirm-dialog). See
16+
[Modality](../ui-patterns/modality) for broader guidance on choosing the appropriate pattern.
1217

1318
For dirty-form dismissal in dialogs/sheets, use the dedicated discard-confirmation pattern (`DiscardChangesConfirmationDialog` + `useConfirmOnClose`) instead of `ConfirmationModal`. Avoid creating custom wrapper components for this flow; wire `modalProps` from `useConfirmOnClose` directly into `DiscardChangesConfirmationDialog`.
1419

@@ -55,7 +60,8 @@ export default function ConfirmationModalDemo() {
5560

5661
## Guidelines
5762

58-
- **Use for moderate complexity:** Suitable when the confirmation requires more than a single sentence but does not need typed intent.
63+
- **Use for moderate complexity:** Suitable when the confirmation needs extra body content, such as multiple paragraphs, callouts, or simple supporting controls, but does not need typed intent.
64+
- **Do not use as the default critical confirmation:** Prefer [Alert Dialog](../components/alert-dialog) for short, critical confirmations with a clear confirm/cancel decision.
5965
- **Do not use for standard dirty-form dismissal:** Prefer `DiscardChangesConfirmationDialog` for unsaved-changes prompts so copy, behavior, and wiring stay consistent across dialogs/sheets.
6066
- **Avoid critical destruction:** Do not use for irreversible or high-risk actions that could benefit from stronger safeguards.
6167
- **Keep content focused:** Include only the context needed to make the decision. If the dialog becomes a full flow, use a custom [Dialog](../components/dialog) instead.

apps/design-system/content/docs/ui-patterns/modality.mdx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,9 @@ Dialogs are centered overlays used for short, focused tasks. All dialogs should
3030

3131
There are quite a few dialog components, each suited to a different task or context:
3232

33-
- [Alert Dialog](../components/alert-dialog) contains a single, short paragraph and an explicit action.
33+
- [Alert Dialog](../components/alert-dialog) is the preferred starting point for critical confirmations that can be explained in a single, short paragraph.
3434
- [Text Confirm Dialog](../fragments/text-confirm-dialog) requires a textual response before the action is enabled.
35-
- [Confirmation Modal](../fragments/confirmation-modal) provides more flexible dialog body contents.
35+
- [Confirmation Modal](../fragments/confirmation-modal) provides more flexible dialog body contents when a confirmation needs extra context, callouts, or simple supporting controls.
3636
- [Dialog](../components/dialog) is a generalized component for bespoke purposes.
3737

3838
#### Alert Dialog
@@ -49,7 +49,7 @@ There are quite a few dialog components, each suited to a different task or cont
4949

5050
#### Confirmation Modal
5151

52-
[Confirmation Modal](../fragments/confirmation-modal) is a convenience wrapper for less-critical confirmations that require more than a single paragraph, such as additional context, callouts, or simple form elements.
52+
[Confirmation Modal](../fragments/confirmation-modal) is a convenience wrapper for confirmations that require more than a single paragraph, such as additional context, callouts, or simple form elements.
5353

5454
<ComponentPreview name="confirmation-modal-demo" />
5555

apps/studio/components/interfaces/Auth/Policies/Policies.tsx

Lines changed: 11 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
} from '@/components/interfaces/Auth/Policies/PolicyTableRow'
1313
import type { Policy } from '@/components/interfaces/Auth/Policies/PolicyTableRow/PolicyTableRow.utils'
1414
import { ProtectedSchemaWarning } from '@/components/interfaces/Database/ProtectedSchemaWarning'
15+
import { RLSToggleDialog } from '@/components/interfaces/Database/RLSToggleDialog'
1516
import { NoSearchResults } from '@/components/ui/NoSearchResults'
1617
import { useDatabasePolicyDeleteMutation } from '@/data/database-policies/database-policy-delete-mutation'
1718
import { useTableUpdateMutation } from '@/data/tables/table-update-mutation'
@@ -51,13 +52,10 @@ export const Policies = ({
5152
}>()
5253
const [selectedPolicyToDelete, setSelectedPolicyToDelete] = useState<any>({})
5354

54-
const { mutate: updateTable, isPending: isUpdatingTable } = useTableUpdateMutation({
55+
const { mutateAsync: updateTable, isPending: isUpdatingTable } = useTableUpdateMutation({
5556
onError: (error) => {
5657
toast.error(`Failed to toggle RLS: ${error.message}`)
5758
},
58-
onSettled: () => {
59-
closeConfirmModal()
60-
},
6159
})
6260

6361
const { mutate: deleteDatabasePolicy, isPending: isDeletingPolicy } =
@@ -102,7 +100,7 @@ export const Policies = ({
102100
rls_enabled: !selectedTableToToggleRLS.rls_enabled,
103101
}
104102

105-
updateTable({
103+
return updateTable({
106104
projectRef: project?.ref!,
107105
connectionString: project?.connectionString,
108106
id: selectedTableToToggleRLS.id,
@@ -193,17 +191,14 @@ export const Policies = ({
193191
onConfirm={onDeletePolicy}
194192
/>
195193

196-
<ConfirmationModal
197-
visible={selectedTableToToggleRLS !== undefined}
198-
variant={selectedTableToToggleRLS?.rls_enabled ? 'destructive' : 'default'}
199-
title={`${selectedTableToToggleRLS?.rls_enabled ? 'Disable' : 'Enable'} Row Level Security`}
200-
description={`Are you sure you want to ${
201-
selectedTableToToggleRLS?.rls_enabled ? 'disable' : 'enable'
202-
} Row Level Security (RLS) for the table “${selectedTableToToggleRLS?.name}”?`}
203-
confirmLabel={`${selectedTableToToggleRLS?.rls_enabled ? 'Disable' : 'Enable'} RLS`}
204-
confirmLabelLoading={`${selectedTableToToggleRLS?.rls_enabled ? 'Disabling' : 'Enabling'} RLS`}
205-
loading={isUpdatingTable}
206-
onCancel={closeConfirmModal}
194+
<RLSToggleDialog
195+
open={selectedTableToToggleRLS !== undefined}
196+
tableName={selectedTableToToggleRLS?.name}
197+
isEnabled={selectedTableToToggleRLS?.rls_enabled ?? false}
198+
isSubmitting={isUpdatingTable}
199+
onOpenChange={(open) => {
200+
if (!open) closeConfirmModal()
201+
}}
207202
onConfirm={onToggleRLS}
208203
/>
209204
</>
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import {
2+
AlertDialog,
3+
AlertDialogAction,
4+
AlertDialogCancel,
5+
AlertDialogContent,
6+
AlertDialogDescription,
7+
AlertDialogFooter,
8+
AlertDialogHeader,
9+
AlertDialogTitle,
10+
} from 'ui'
11+
12+
import { InlineLink } from '@/components/ui/InlineLink'
13+
import { DOCS_URL } from '@/lib/constants'
14+
15+
interface RLSToggleDialogProps {
16+
open: boolean
17+
tableName?: string
18+
isEnabled: boolean
19+
isSubmitting?: boolean
20+
onOpenChange: (open: boolean) => void
21+
onConfirm: () => void | Promise<void>
22+
}
23+
24+
export function RLSToggleDialog({
25+
open,
26+
isEnabled,
27+
isSubmitting = false,
28+
onOpenChange,
29+
onConfirm,
30+
}: RLSToggleDialogProps) {
31+
const title = isEnabled ? 'Disable Row Level Security' : 'Enable Row Level Security'
32+
const description = isEnabled
33+
? 'This table will become publicly readable and writable. Anyone can view, add, update, or delete data in this table, and existing RLS policies will no longer apply.'
34+
: 'RLS restricts table access until policies allow a request. Existing queries may stop returning rows until policies are added.'
35+
const confirmLabel = isEnabled ? 'Disable RLS' : 'Enable RLS'
36+
const confirmVariant = isEnabled ? 'danger' : 'primary'
37+
38+
return (
39+
<AlertDialog open={open} onOpenChange={onOpenChange}>
40+
<AlertDialogContent>
41+
<AlertDialogHeader>
42+
<AlertDialogTitle>{title}</AlertDialogTitle>
43+
<AlertDialogDescription>
44+
{description}{' '}
45+
<InlineLink href={`${DOCS_URL}/guides/database/postgres/row-level-security`}>
46+
Learn more
47+
</InlineLink>
48+
.
49+
</AlertDialogDescription>
50+
</AlertDialogHeader>
51+
<AlertDialogFooter>
52+
<AlertDialogCancel>Cancel</AlertDialogCancel>
53+
<AlertDialogAction
54+
variant={confirmVariant}
55+
loading={isSubmitting}
56+
onClick={() => onConfirm()}
57+
>
58+
{confirmLabel}
59+
</AlertDialogAction>
60+
</AlertDialogFooter>
61+
</AlertDialogContent>
62+
</AlertDialog>
63+
)
64+
}

apps/studio/components/interfaces/TableGridEditor/GridHeaderActions.tsx

Lines changed: 11 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@ import {
2121
TooltipContent,
2222
TooltipTrigger,
2323
} from 'ui'
24-
import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal'
2524

2625
import { EnableIndexAdvisorDialog } from '../QueryPerformance/IndexAdvisor/EnableIndexAdvisorButton'
2726
import { RoleImpersonationPopover } from '../RoleImpersonationSelector/RoleImpersonationPopover'
@@ -31,6 +30,7 @@ import { SecurityDefinerViewPopover } from './SecurityDefinerViewPopover'
3130
import { ViewEntityAutofixSecurityModal } from './ViewEntityAutofixSecurityModal'
3231
import { RefreshButton } from '@/components/grid/components/header/RefreshButton'
3332
import { useTableIndexAdvisor } from '@/components/grid/context/TableIndexAdvisorContext'
33+
import { RLSToggleDialog } from '@/components/interfaces/Database/RLSToggleDialog'
3434
import {
3535
getEntityLintDetails,
3636
getTablePoliciesUrl,
@@ -98,13 +98,10 @@ export const GridHeaderActions = ({ table, isRefetching }: GridHeaderActionsProp
9898

9999
const isRealtimeEnabled = useIsTableRealtimeEnabled({ id: table.id })
100100

101-
const { mutate: updateTable, isPending: isUpdatingTable } = useTableUpdateMutation({
101+
const { mutateAsync: updateTable, isPending: isUpdatingTable } = useTableUpdateMutation({
102102
onError: (error) => {
103103
toast.error(`Failed to toggle RLS: ${error.message}`)
104104
},
105-
onSettled: () => {
106-
closeConfirmModal()
107-
},
108105
})
109106

110107
const showHeaderActions = snap.selectedRows.size === 0
@@ -155,10 +152,6 @@ export const GridHeaderActions = ({ table, isRefetching }: GridHeaderActionsProp
155152
table.schema
156153
)
157154

158-
const closeConfirmModal = () => {
159-
setRlsConfirmModalOpen(false)
160-
}
161-
162155
const onViewAPIDocs = () => {
163156
appSnap.setActiveDocsSection(['entities', table.name])
164157
appSnap.setShowProjectApiDocs(true)
@@ -181,7 +174,7 @@ export const GridHeaderActions = ({ table, isRefetching }: GridHeaderActionsProp
181174
rls_enabled: !(isTable && table.rls_enabled),
182175
}
183176

184-
updateTable({
177+
const updateTablePromise = updateTable({
185178
projectRef: project?.ref!,
186179
connectionString: project?.connectionString,
187180
id: table.id,
@@ -195,6 +188,8 @@ export const GridHeaderActions = ({ table, isRefetching }: GridHeaderActionsProp
195188
schema_name: table.schema,
196189
table_name: table.name,
197190
})
191+
192+
return updateTablePromise
198193
}
199194

200195
return (
@@ -397,17 +392,12 @@ export const GridHeaderActions = ({ table, isRefetching }: GridHeaderActionsProp
397392
/>
398393

399394
{isTable && (
400-
<ConfirmationModal
401-
visible={rlsConfirmModalOpen}
402-
variant={table.rls_enabled ? 'destructive' : 'default'}
403-
title={`${table.rls_enabled ? 'Disable' : 'Enable'} Row Level Security`}
404-
description={`Are you sure you want to ${
405-
table.rls_enabled ? 'disable' : 'enable'
406-
} Row Level Security for this table?`}
407-
confirmLabel={`${table.rls_enabled ? 'Disable' : 'Enable'} RLS`}
408-
confirmLabelLoading={`${table.rls_enabled ? 'Disabling' : 'Enabling'} RLS`}
409-
loading={isUpdatingTable}
410-
onCancel={closeConfirmModal}
395+
<RLSToggleDialog
396+
open={rlsConfirmModalOpen}
397+
tableName={table.name}
398+
isEnabled={table.rls_enabled}
399+
isSubmitting={isUpdatingTable}
400+
onOpenChange={setRlsConfirmModalOpen}
411401
onConfirm={onToggleRLS}
412402
/>
413403
)}

apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/RLSDisableModal.tsx

Lines changed: 0 additions & 54 deletions
This file was deleted.

apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/TableEditor.tsx

Lines changed: 8 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ import { useContext, useEffect, useMemo, useRef, useState } from 'react'
33
import { toast } from 'sonner'
44
import { Badge, Checkbox, Input, SidePanel } from 'ui'
55
import { Admonition } from 'ui-patterns'
6-
import { ConfirmationModal } from 'ui-patterns/Dialogs/ConfirmationModal'
76
import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
87

98
import { ActionBar } from '../ActionBar'
@@ -16,7 +15,6 @@ import { ApiAccessToggle, type TableApiAccessHandlerWithHistoryReturn } from './
1615
import { ColumnManagement } from './ColumnManagement'
1716
import { ForeignKeysManagement } from './ForeignKeysManagement/ForeignKeysManagement'
1817
import { HeaderTitle } from './HeaderTitle'
19-
import { RLSDisableModalContent } from './RLSDisableModal'
2018
import { DEFAULT_COLUMNS } from './TableEditor.constants'
2119
import type { ImportContent, TableField } from './TableEditor.types'
2220
import {
@@ -26,6 +24,7 @@ import {
2624
mergeForeignKeyMeta,
2725
validateFields,
2826
} from './TableEditor.utils'
27+
import { RLSToggleDialog } from '@/components/interfaces/Database/RLSToggleDialog'
2928
import { DocsButton } from '@/components/ui/DocsButton'
3029
import { useDatabasePublicationsQuery } from '@/data/database-publications/database-publications-query'
3130
import { CONSTRAINT_TYPE, useTableConstraintsQuery } from '@/data/database/constraints-query'
@@ -586,19 +585,16 @@ export const TableEditor = ({
586585
closePanel={() => setIsImportingSpreadsheet(false)}
587586
/>
588587

589-
<ConfirmationModal
590-
visible={rlsConfirmVisible}
591-
title="Turn off Row Level Security"
592-
confirmLabel="Confirm"
593-
size="medium"
594-
onCancel={() => setRlsConfirmVisible(false)}
588+
<RLSToggleDialog
589+
open={rlsConfirmVisible}
590+
tableName={tableFields.name}
591+
isEnabled={tableFields.isRLSEnabled}
592+
onOpenChange={setRlsConfirmVisible}
595593
onConfirm={() => {
596-
onUpdateField({ isRLSEnabled: !tableFields.isRLSEnabled })
594+
onUpdateField({ isRLSEnabled: false })
597595
setRlsConfirmVisible(false)
598596
}}
599-
>
600-
<RLSDisableModalContent />
601-
</ConfirmationModal>
597+
/>
602598
</SidePanel.Content>
603599

604600
{!isDuplicating && (

0 commit comments

Comments
 (0)