mirror of
https://github.com/supabase/supabase.git
synced 2026-10-13 05:09:12 +03:00
## 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 #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 --> [](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 -->
415 lines
15 KiB
TypeScript
415 lines
15 KiB
TypeScript
import { PermissionAction } from '@supabase/shared-types/out/constants'
|
|
import { useParams } from 'common'
|
|
import { Realtime } from 'icons'
|
|
import { BookOpenText, Lightbulb, Lock, MoreVertical, PlusCircle, Unlock } from 'lucide-react'
|
|
import Link from 'next/link'
|
|
import { parseAsBoolean, useQueryState } from 'nuqs'
|
|
import { useState } from 'react'
|
|
import { toast } from 'sonner'
|
|
import {
|
|
Button,
|
|
cn,
|
|
DropdownMenu,
|
|
DropdownMenuContent,
|
|
DropdownMenuItem,
|
|
DropdownMenuSeparator,
|
|
DropdownMenuTrigger,
|
|
Popover,
|
|
PopoverContent,
|
|
PopoverTrigger,
|
|
Tooltip,
|
|
TooltipContent,
|
|
TooltipTrigger,
|
|
} from 'ui'
|
|
|
|
import { EnableIndexAdvisorDialog } from '../QueryPerformance/IndexAdvisor/EnableIndexAdvisorButton'
|
|
import { RoleImpersonationPopover } from '../RoleImpersonationSelector/RoleImpersonationPopover'
|
|
import { InsertButton } from './InsertButton'
|
|
import { RealtimeToggleDialog } from './RealtimeToggleDialog'
|
|
import { SecurityDefinerViewPopover } from './SecurityDefinerViewPopover'
|
|
import { ViewEntityAutofixSecurityModal } from './ViewEntityAutofixSecurityModal'
|
|
import { RefreshButton } from '@/components/grid/components/header/RefreshButton'
|
|
import { useTableIndexAdvisor } from '@/components/grid/context/TableIndexAdvisorContext'
|
|
import { RLSToggleDialog } from '@/components/interfaces/Database/RLSToggleDialog'
|
|
import {
|
|
getEntityLintDetails,
|
|
getTablePoliciesUrl,
|
|
} from '@/components/interfaces/TableGridEditor/TableEntity.utils'
|
|
import { ButtonTooltip } from '@/components/ui/ButtonTooltip'
|
|
import { useDatabasePoliciesQuery } from '@/data/database-policies/database-policies-query'
|
|
import { useIsTableRealtimeEnabled } from '@/data/database-publications/database-publications-query'
|
|
import { useProjectLintsQuery } from '@/data/lint/lint-query'
|
|
import {
|
|
Entity,
|
|
isTableLike,
|
|
isForeignTable as isTableLikeForeignTable,
|
|
isMaterializedView as isTableLikeMaterializedView,
|
|
isView as isTableLikeView,
|
|
} from '@/data/table-editor/table-editor-types'
|
|
import { useTableUpdateMutation } from '@/data/tables/table-update-mutation'
|
|
import { useSendEventMutation } from '@/data/telemetry/send-event-mutation'
|
|
import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions'
|
|
import { useIsFeatureEnabled } from '@/hooks/misc/useIsFeatureEnabled'
|
|
import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization'
|
|
import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
|
|
import { useIsProtectedSchema } from '@/hooks/useProtectedSchemas'
|
|
import { DOCS_URL } from '@/lib/constants'
|
|
import { useTrack } from '@/lib/telemetry/track'
|
|
import { useAppStateSnapshot } from '@/state/app-state'
|
|
import { useTableEditorTableStateSnapshot } from '@/state/table-editor-table'
|
|
|
|
export interface GridHeaderActionsProps {
|
|
table: Entity
|
|
isRefetching: boolean
|
|
}
|
|
export const GridHeaderActions = ({ table, isRefetching }: GridHeaderActionsProps) => {
|
|
const track = useTrack()
|
|
const { ref } = useParams()
|
|
const appSnap = useAppStateSnapshot()
|
|
const snap = useTableEditorTableStateSnapshot()
|
|
const { data: project } = useSelectedProjectQuery()
|
|
const { data: org } = useSelectedOrganizationQuery()
|
|
const { mutate: sendEvent } = useSendEventMutation()
|
|
|
|
const [rlsConfirmModalOpen, setRlsConfirmModalOpen] = useState(false)
|
|
const [realtimeDialogOpen, setRealtimeDialogOpen] = useState(false)
|
|
const [indexAdvisorDialogOpen, setIndexAdvisorDialogOpen] = useState(false)
|
|
const [isAutofixViewSecurityModalOpen, setIsAutofixViewSecurityModalOpen] = useState(false)
|
|
|
|
const [showWarning, setShowWarning] = useQueryState(
|
|
'showWarning',
|
|
parseAsBoolean.withDefault(false)
|
|
)
|
|
|
|
// need project lints to get security status for views
|
|
const { data: lints = [] } = useProjectLintsQuery({ projectRef: project?.ref })
|
|
|
|
// Use table-specific index advisor context
|
|
const { isAvailable: isIndexAdvisorAvailable, isEnabled: isIndexAdvisorEnabled } =
|
|
useTableIndexAdvisor()
|
|
|
|
const isTable = isTableLike(table)
|
|
const isForeignTable = isTableLikeForeignTable(table)
|
|
const isView = isTableLikeView(table)
|
|
const isMaterializedView = isTableLikeMaterializedView(table)
|
|
|
|
const { realtimeAll: realtimeEnabled } = useIsFeatureEnabled(['realtime:all'])
|
|
const { isSchemaLocked } = useIsProtectedSchema({ schema: table.schema })
|
|
|
|
const isRealtimeEnabled = useIsTableRealtimeEnabled({ id: table.id })
|
|
|
|
const { mutateAsync: updateTable, isPending: isUpdatingTable } = useTableUpdateMutation({
|
|
onError: (error) => {
|
|
toast.error(`Failed to toggle RLS: ${error.message}`)
|
|
},
|
|
})
|
|
|
|
const showHeaderActions = snap.selectedRows.size === 0
|
|
|
|
const projectRef = project?.ref
|
|
const { data } = useDatabasePoliciesQuery({
|
|
projectRef: project?.ref,
|
|
connectionString: project?.connectionString,
|
|
})
|
|
const policies = (data ?? []).filter(
|
|
(policy) => policy.schema === table.schema && policy.table === table.name
|
|
)
|
|
|
|
const { can: canSqlWriteTables, isLoading: isLoadingPermissions } = useAsyncCheckPermissions(
|
|
PermissionAction.TENANT_SQL_ADMIN_WRITE,
|
|
'tables'
|
|
)
|
|
const { can: canSqlWriteColumns } = useAsyncCheckPermissions(
|
|
PermissionAction.TENANT_SQL_ADMIN_WRITE,
|
|
'columns'
|
|
)
|
|
const isReadOnly = !isLoadingPermissions && !canSqlWriteTables && !canSqlWriteColumns
|
|
// This will change when we allow autogenerated API docs for schemas other than `public`
|
|
const doesHaveAutoGeneratedAPIDocs = table.schema === 'public'
|
|
|
|
const { hasLint: tableHasLints } = getEntityLintDetails(
|
|
table.name,
|
|
'rls_disabled_in_public',
|
|
['ERROR'],
|
|
lints,
|
|
table.schema
|
|
)
|
|
|
|
const { hasLint: viewHasLints, matchingLint: matchingViewLint } = getEntityLintDetails(
|
|
table.name,
|
|
'security_definer_view',
|
|
['ERROR', 'WARN'],
|
|
lints,
|
|
table.schema
|
|
)
|
|
|
|
const { hasLint: materializedViewHasLints, matchingLint: matchingMaterializedViewLint } =
|
|
getEntityLintDetails(
|
|
table.name,
|
|
'materialized_view_in_api',
|
|
['ERROR', 'WARN'],
|
|
lints,
|
|
table.schema
|
|
)
|
|
|
|
const onViewAPIDocs = () => {
|
|
appSnap.setActiveDocsSection(['entities', table.name])
|
|
appSnap.setShowProjectApiDocs(true)
|
|
|
|
sendEvent({
|
|
action: 'api_docs_opened',
|
|
properties: {
|
|
source: 'table_editor',
|
|
},
|
|
groups: {
|
|
project: ref ?? 'Unknown',
|
|
organization: org?.slug ?? 'Unknown',
|
|
},
|
|
})
|
|
}
|
|
|
|
const onToggleRLS = async () => {
|
|
const payload = {
|
|
id: table.id,
|
|
rls_enabled: !(isTable && table.rls_enabled),
|
|
}
|
|
|
|
const updateTablePromise = updateTable({
|
|
projectRef: project?.ref!,
|
|
connectionString: project?.connectionString,
|
|
id: table.id,
|
|
name: table.name,
|
|
schema: table.schema,
|
|
payload: payload,
|
|
})
|
|
|
|
track('table_rls_enabled', {
|
|
method: 'table_editor',
|
|
schema_name: table.schema,
|
|
table_name: table.name,
|
|
})
|
|
|
|
return updateTablePromise
|
|
}
|
|
|
|
return (
|
|
<div className="sb-grid-header__inner">
|
|
{showHeaderActions && (
|
|
<div className="flex items-center gap-x-2">
|
|
{isReadOnly && (
|
|
<Tooltip>
|
|
<TooltipTrigger asChild>
|
|
<div className="border border-strong rounded-sm bg-overlay-hover px-3 py-1 text-xs">
|
|
Viewing as read-only
|
|
</div>
|
|
</TooltipTrigger>
|
|
<TooltipContent side="bottom">
|
|
You need additional permissions to manage your project's data
|
|
</TooltipContent>
|
|
</Tooltip>
|
|
)}
|
|
|
|
{isTable && !isSchemaLocked ? (
|
|
table.rls_enabled ? (
|
|
<>
|
|
{policies.length < 1 && !isSchemaLocked ? (
|
|
<ButtonTooltip
|
|
asChild
|
|
type="default"
|
|
className="group"
|
|
icon={<PlusCircle strokeWidth={1.5} className="text-foreground-muted" />}
|
|
tooltip={{
|
|
content: {
|
|
side: 'bottom',
|
|
className: 'w-[280px]',
|
|
text: 'RLS is enabled for this table, but no policies are set. Select queries may return 0 results.',
|
|
},
|
|
}}
|
|
>
|
|
<Link passHref href={getTablePoliciesUrl(projectRef, table.schema, table.name)}>
|
|
Add RLS policy
|
|
</Link>
|
|
</ButtonTooltip>
|
|
) : (
|
|
<Button
|
|
asChild
|
|
type={policies.length < 1 && !isSchemaLocked ? 'warning' : 'default'}
|
|
className="group"
|
|
icon={
|
|
isSchemaLocked || policies.length > 0 ? (
|
|
<div
|
|
className={cn(
|
|
'flex items-center justify-center rounded-full bg-border-stronger h-[16px]',
|
|
policies.length > 9 ? ' px-1' : 'w-[16px]',
|
|
''
|
|
)}
|
|
>
|
|
<span className="text-[11px] text-foreground font-mono text-center">
|
|
{policies.length}
|
|
</span>
|
|
</div>
|
|
) : (
|
|
<PlusCircle strokeWidth={1.5} />
|
|
)
|
|
}
|
|
>
|
|
<Link passHref href={getTablePoliciesUrl(projectRef, table.schema, table.name)}>
|
|
RLS {policies.length > 1 ? 'policies' : 'policy'}
|
|
</Link>
|
|
</Button>
|
|
)}
|
|
</>
|
|
) : tableHasLints ? (
|
|
<Popover modal={false} open={showWarning} onOpenChange={setShowWarning}>
|
|
<PopoverTrigger asChild>
|
|
<Button type="danger" icon={<Lock strokeWidth={1.5} />}>
|
|
RLS disabled
|
|
</Button>
|
|
</PopoverTrigger>
|
|
<PopoverContent className="w-80 text-sm" align="end">
|
|
<h4 className="flex items-center gap-2">
|
|
<Lock size={16} /> Row Level Security (RLS)
|
|
</h4>
|
|
<div className="grid gap-2 mt-4 text-foreground-light text-xs">
|
|
<p>
|
|
You can restrict and control who can read, write and update data in this table
|
|
using Row Level Security.
|
|
</p>
|
|
<p>
|
|
With RLS enabled, anonymous users will not be able to read/write data in the
|
|
table.
|
|
</p>
|
|
{!isSchemaLocked && (
|
|
<Button
|
|
type="default"
|
|
className="mt-2 w-min"
|
|
onClick={() => setRlsConfirmModalOpen(!rlsConfirmModalOpen)}
|
|
>
|
|
Enable RLS for this table
|
|
</Button>
|
|
)}
|
|
</div>
|
|
</PopoverContent>
|
|
</Popover>
|
|
) : null
|
|
) : null}
|
|
|
|
{isView && viewHasLints && (
|
|
<SecurityDefinerViewPopover
|
|
lint={matchingViewLint}
|
|
onAutofix={() => {
|
|
setIsAutofixViewSecurityModalOpen(true)
|
|
}}
|
|
/>
|
|
)}
|
|
|
|
{isMaterializedView && materializedViewHasLints && (
|
|
<SecurityDefinerViewPopover lint={matchingMaterializedViewLint} />
|
|
)}
|
|
|
|
{isForeignTable && table.schema === 'public' && (
|
|
<Popover modal={false} open={showWarning} onOpenChange={setShowWarning}>
|
|
<PopoverTrigger asChild>
|
|
<Button type="warning" icon={<Unlock strokeWidth={1.5} />}>
|
|
Unprotected Data API access
|
|
</Button>
|
|
</PopoverTrigger>
|
|
<PopoverContent className="min-w-[395px] text-sm" align="end">
|
|
<h3 className="flex items-center gap-2">
|
|
<Unlock size={16} /> Secure Foreign table
|
|
</h3>
|
|
<div className="grid gap-2 mt-4 text-foreground-light text-sm">
|
|
<p>
|
|
Foreign tables do not enforce RLS, which may allow unrestricted access. To
|
|
secure them, either move foreign tables to a private schema not exposed by
|
|
PostgREST, or <a href="">disable PostgREST access</a> entirely.
|
|
</p>
|
|
|
|
<div className="mt-2">
|
|
<Button type="default" asChild>
|
|
<Link
|
|
target="_blank"
|
|
href={`${DOCS_URL}/guides/database/extensions/wrappers/overview#security`}
|
|
>
|
|
Learn more
|
|
</Link>
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</PopoverContent>
|
|
</Popover>
|
|
)}
|
|
|
|
<RoleImpersonationPopover header="View data as a role" align="center" />
|
|
|
|
<DropdownMenu>
|
|
<DropdownMenuTrigger asChild>
|
|
<Button
|
|
type="default"
|
|
icon={<MoreVertical />}
|
|
className="h-7 w-7"
|
|
aria-label="More actions"
|
|
/>
|
|
</DropdownMenuTrigger>
|
|
<DropdownMenuContent className="w-48">
|
|
{isTable && realtimeEnabled && (
|
|
<DropdownMenuItem className="gap-x-2" onClick={() => setRealtimeDialogOpen(true)}>
|
|
<Realtime size={14} className={isRealtimeEnabled ? 'text-brand' : ''} />
|
|
<span>{isRealtimeEnabled ? 'Disable' : 'Enable'} Realtime</span>
|
|
</DropdownMenuItem>
|
|
)}
|
|
{doesHaveAutoGeneratedAPIDocs && (
|
|
<DropdownMenuItem className="gap-x-2" onClick={() => onViewAPIDocs()}>
|
|
<BookOpenText size={14} />
|
|
<span>View API docs</span>
|
|
</DropdownMenuItem>
|
|
)}
|
|
{isTable && isIndexAdvisorAvailable && !isIndexAdvisorEnabled && (
|
|
<>
|
|
<DropdownMenuSeparator />
|
|
<DropdownMenuItem
|
|
className="gap-x-2"
|
|
onClick={() => setIndexAdvisorDialogOpen(true)}
|
|
>
|
|
<Lightbulb size={14} />
|
|
<span>Enable Index Advisor</span>
|
|
</DropdownMenuItem>
|
|
</>
|
|
)}
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
|
|
<RefreshButton tableId={table.id} isRefetching={isRefetching} />
|
|
|
|
{showHeaderActions && <InsertButton />}
|
|
</div>
|
|
)}
|
|
|
|
<ViewEntityAutofixSecurityModal
|
|
table={table}
|
|
isAutofixViewSecurityModalOpen={isAutofixViewSecurityModalOpen}
|
|
setIsAutofixViewSecurityModalOpen={setIsAutofixViewSecurityModalOpen}
|
|
/>
|
|
|
|
{isTable && (
|
|
<RLSToggleDialog
|
|
open={rlsConfirmModalOpen}
|
|
tableName={table.name}
|
|
isEnabled={table.rls_enabled}
|
|
isSubmitting={isUpdatingTable}
|
|
onOpenChange={setRlsConfirmModalOpen}
|
|
onConfirm={onToggleRLS}
|
|
/>
|
|
)}
|
|
|
|
<RealtimeToggleDialog
|
|
table={table}
|
|
open={realtimeDialogOpen}
|
|
setOpen={setRealtimeDialogOpen}
|
|
/>
|
|
|
|
<EnableIndexAdvisorDialog open={indexAdvisorDialogOpen} setOpen={setIndexAdvisorDialogOpen} />
|
|
</div>
|
|
)
|
|
}
|