import { PermissionAction } from '@supabase/shared-types/out/constants' import { Lightbulb, Lock, MousePointer2, PlusCircle, Unlock } from 'lucide-react' import Link from 'next/link' import { useState } from 'react' import { toast } from 'sonner' import { useParams } from 'common' import { RefreshButton } from 'components/grid/components/header/RefreshButton' import { useTableIndexAdvisor } from 'components/grid/context/TableIndexAdvisorContext' import { EnableIndexAdvisorButton } from 'components/interfaces/QueryPerformance/IndexAdvisor/EnableIndexAdvisorButton' import { getEntityLintDetails } from 'components/interfaces/TableGridEditor/TableEntity.utils' import { APIDocsButton } from 'components/ui/APIDocsButton' import { ButtonTooltip } from 'components/ui/ButtonTooltip' import { useDatabasePoliciesQuery } from 'data/database-policies/database-policies-query' import { useDatabasePublicationsQuery } from 'data/database-publications/database-publications-query' import { useDatabasePublicationUpdateMutation } from 'data/database-publications/database-publications-update-mutation' import { useDatabaseTriggersQuery } from 'data/database-triggers/database-triggers-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 { useAsyncCheckPermissions } from 'hooks/misc/useCheckPermissions' import { useIsFeatureEnabled } from 'hooks/misc/useIsFeatureEnabled' import { RealtimeButtonVariant, useRealtimeExperiment } from 'hooks/misc/useRealtimeExperiment' import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' import { useIsProtectedSchema } from 'hooks/useProtectedSchemas' import { DOCS_URL } from 'lib/constants' import { useTrack } from 'lib/telemetry/track' import { parseAsBoolean, useQueryState } from 'nuqs' import { useTableEditorTableStateSnapshot } from 'state/table-editor-table' import { Button, PopoverContent_Shadcn_, PopoverTrigger_Shadcn_, Popover_Shadcn_, Tooltip, TooltipContent, TooltipTrigger, cn, } from 'ui' import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal' import { RoleImpersonationPopover } from '../RoleImpersonationSelector/RoleImpersonationPopover' import ViewEntityAutofixSecurityModal from './ViewEntityAutofixSecurityModal' export interface GridHeaderActionsProps { table: Entity isRefetching: boolean } export const GridHeaderActions = ({ table, isRefetching }: GridHeaderActionsProps) => { const { ref } = useParams() const { data: project } = useSelectedProjectQuery() const track = useTrack() 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 { mutate: updateTable, isPending: isUpdatingTable } = useTableUpdateMutation({ onError: (error) => { toast.error(`Failed to toggle RLS: ${error.message}`) }, onSettled: () => { closeConfirmModal() }, }) const [showEnableRealtime, setShowEnableRealtime] = useState(false) const [rlsConfirmModalOpen, setRlsConfirmModalOpen] = useState(false) const [isAutofixViewSecurityModalOpen, setIsAutofixViewSecurityModalOpen] = useState(false) const snap = useTableEditorTableStateSnapshot() 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 { data: publications } = useDatabasePublicationsQuery({ projectRef: project?.ref, connectionString: project?.connectionString, }) const realtimePublication = (publications ?? []).find( (publication) => publication.name === 'supabase_realtime' ) const realtimeEnabledTables = realtimePublication?.tables ?? [] const isRealtimeEnabled = realtimeEnabledTables.some((t) => t.id === table?.id) const { activeVariant: activeRealtimeVariant } = useRealtimeExperiment({ isTable, isRealtimeEnabled, }) const { mutate: updatePublications, isPending: isTogglingRealtime } = useDatabasePublicationUpdateMutation({ onSuccess: () => { setShowEnableRealtime(false) track(isRealtimeEnabled ? 'table_realtime_disabled' : 'table_realtime_enabled', { method: 'ui', schema_name: table.schema, table_name: table.name, }) }, onError: (error) => { toast.error(`Failed to toggle realtime for ${table.name}: ${error.message}`) }, }) const { data: triggersData } = useDatabaseTriggersQuery( { projectRef: project?.ref, connectionString: project?.connectionString, }, { enabled: isTable, } ) const tableTriggers = (triggersData ?? []).filter( (trigger) => trigger.schema === table.schema && trigger.table === table.name ) const tableTriggersCount = tableTriggers.length 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 manageTriggersHref = `/project/${ref}/database/triggers?schema=${table.schema}` const toggleRealtime = async () => { if (!project || !realtimePublication) return const exists = realtimeEnabledTables.some((x) => x.id === table.id) const tables = !exists ? [`${table.schema}.${table.name}`].concat( realtimeEnabledTables.map((t) => `${t.schema}.${t.name}`) ) : realtimeEnabledTables.filter((x) => x.id !== table.id).map((x) => `${x.schema}.${x.name}`) track('realtime_toggle_table_clicked', { newState: exists ? 'disabled' : 'enabled', origin: 'tableGridHeader', }) updatePublications({ projectRef: project?.ref, connectionString: project?.connectionString, id: realtimePublication.id, tables, }) } const closeConfirmModal = () => { setRlsConfirmModalOpen(false) } const onToggleRLS = async () => { const payload = { id: table.id, rls_enabled: !(isTable && table.rls_enabled), } 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 (
{showHeaderActions && (
{isReadOnly && (
Viewing as read-only
You need additional permissions to manage your project's data
)} {isTable && !isSchemaLocked ? ( table.rls_enabled ? ( <> {policies.length < 1 && !isSchemaLocked ? ( } 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.', }, }} > Add RLS policy ) : ( )} ) : tableHasLints ? (

Row Level Security (RLS)

You can restrict and control who can read, write and update data in this table using Row Level Security.

With RLS enabled, anonymous users will not be able to read/write data in the table.

{!isSchemaLocked && ( )}
) : null ) : null} {isTable && isIndexAdvisorAvailable && !isIndexAdvisorEnabled && (

Index Advisor

Index Advisor recommends indexes to improve query performance on this table.

Enable Index Advisor to get recommendations based on your actual query patterns.

)} {isTable && activeRealtimeVariant === RealtimeButtonVariant.TRIGGERS ? (
} > {tableTriggersCount === 1 ? 'Trigger' : 'Triggers'} ) : ( activeRealtimeVariant !== RealtimeButtonVariant.HIDE_BUTTON && realtimeEnabled && ( } onClick={() => setShowEnableRealtime(true)} className={cn(isRealtimeEnabled && 'w-7 h-7 p-0 text-brand hover:text-brand-hover')} tooltip={{ content: { side: 'bottom', text: isRealtimeEnabled ? 'Click to disable realtime for this table' : 'Click to enable realtime for this table', }, }} > {!isRealtimeEnabled && 'Enable Realtime'} ) )} {isView && viewHasLints && (

Secure your View

This view is defined with the Security Definer property, giving it permissions of the view's creator (Postgres), rather than the permissions of the querying user.

Since this view is in the public schema, it is accessible via your project's APIs.

)} {isMaterializedView && materializedViewHasLints && (

Secure your View

This view is defined with the Security Definer property, giving it permissions of the view's creator (Postgres), rather than the permissions of the querying user.

Since this view is in the public schema, it is accessible via your project's APIs.

)} {isForeignTable && table.schema === 'public' && (

Secure Foreign table

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 disable PostgREST access entirely.

)} {doesHaveAutoGeneratedAPIDocs && ( )}
)} setShowEnableRealtime(false)} onConfirm={() => toggleRealtime()} >

Once realtime has been {isRealtimeEnabled ? 'disabled' : 'enabled'}, the table will{' '} {isRealtimeEnabled ? 'no longer ' : ''}broadcast any changes to authorized subscribers.

{!isRealtimeEnabled && (

You may also select which events to broadcast to subscribers on the{' '} database publications {' '} settings.

)}
{isTable && ( )} ) }