import { safeSql } from '@supabase/pg-meta' import { PermissionAction } from '@supabase/shared-types/out/constants' import { DatabaseZap, Search } from 'lucide-react' import { parseAsBoolean, parseAsJson, parseAsString, useQueryState } from 'nuqs' import { useEffect, useRef, useState } from 'react' import { toast } from 'sonner' import { Card, InputGroup, InputGroupAddon, InputGroupInput, Table, TableBody, TableHead, TableHeader, TableRow, } from 'ui' import { EmptyStatePresentational } from 'ui-patterns' import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader' import { CreateTriggerButtons } from './CreateTriggerButtons' import { TriggerList } from './TriggerList' import { generateTriggerCreateSQL, type PostgresTrigger } from './TriggerList.utils' import { useIsInlineEditorEnabled } from '@/components/interfaces/Account/Preferences/useDashboardSettings' import { ProtectedSchemaWarning } from '@/components/interfaces/Database/ProtectedSchemaWarning' import { TriggerSheet } from '@/components/interfaces/Database/Triggers/TriggerSheet' import { ReportsSelectFilter, selectFilterSchema, } from '@/components/interfaces/Reports/v2/ReportsSelectFilter' import { SIDEBAR_KEYS } from '@/components/layouts/ProjectLayout/LayoutSidebar/LayoutSidebarProvider' import AlertError from '@/components/ui/AlertError' import { DocsButton } from '@/components/ui/DocsButton' import SchemaSelector from '@/components/ui/SchemaSelector' import { Shortcut } from '@/components/ui/Shortcut' import { TextConfirmModal } from '@/components/ui/TextConfirmModalWrapper' import { useDatabaseTriggerDeleteMutation } from '@/data/database-triggers/database-trigger-delete-mutation' import { useDatabaseTriggersQuery } from '@/data/database-triggers/database-triggers-query' import { useTablesQuery } from '@/data/tables/tables-query' import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions' import { useQuerySchemaState } from '@/hooks/misc/useSchemaQueryState' import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject' import { useIsProtectedSchema, useProtectedSchemas } from '@/hooks/useProtectedSchemas' import { DOCS_URL } from '@/lib/constants' import { onSearchInputEscape } from '@/lib/keyboard' import { useEditorPanelStateSnapshot } from '@/state/editor-panel-state' import { SHORTCUT_IDS } from '@/state/shortcuts/registry' import { useShortcut } from '@/state/shortcuts/useShortcut' import { useSidebarManagerSnapshot } from '@/state/sidebar-manager-state' export const TriggersList = () => { const { data: project } = useSelectedProjectQuery() const { openSidebar } = useSidebarManagerSnapshot() const { selectedSchema, setSelectedSchema } = useQuerySchemaState() const [filterString, setFilterString] = useQueryState('search', parseAsString.withDefault('')) const [tablesFilter, setTablesFilter] = useQueryState( 'tables', parseAsJson(selectFilterSchema.parse).withDefault([]) ) const [schemaSelectorOpen, setSchemaSelectorOpen] = useState(false) const searchInputRef = useRef(null) const isInlineEditorEnabled = useIsInlineEditorEnabled() const { templates: editorPanelTemplates, setValue: setEditorPanelValue, setTemplates: setEditorPanelTemplates, setInitialPrompt: setEditorPanelInitialPrompt, } = useEditorPanelStateSnapshot() const { data: protectedSchemas } = useProtectedSchemas() const { isSchemaLocked } = useIsProtectedSchema({ schema: selectedSchema }) const { data = [] } = useTablesQuery({ projectRef: project?.ref, connectionString: project?.connectionString, }) const hasTables = data.filter((a) => !protectedSchemas.find((s) => s.name === a.schema)).length > 0 const { data: triggers = [], error, isPending, isSuccess, isError, } = useDatabaseTriggersQuery({ projectRef: project?.ref, connectionString: project?.connectionString, }) const { can: canCreateTriggers } = useAsyncCheckPermissions( PermissionAction.TENANT_SQL_ADMIN_WRITE, 'triggers' ) const [showCreateTriggerForm, setShowCreateTriggerForm] = useQueryState( 'new', parseAsBoolean.withDefault(false).withOptions({ history: 'push', clearOnDefault: true }) ) const [triggerIdToEdit, setTriggerToEdit] = useQueryState('edit', parseAsString) const triggerToEdit = triggers?.find((fn) => fn.id.toString() === triggerIdToEdit) const [triggerIdToDuplicate, setTriggerToDuplicate] = useQueryState('duplicate', parseAsString) const triggerToDuplicate = triggers?.find((fn) => fn.id.toString() === triggerIdToDuplicate) const [triggerIdToDelete, setTriggerToDelete] = useQueryState('delete', parseAsString) const triggerToDelete = triggers?.find((fn) => fn.id.toString() === triggerIdToDelete) const { mutate: deleteDatabaseTrigger, isPending: isDeletingTrigger, isSuccess: isSuccessDelete, } = useDatabaseTriggerDeleteMutation({ onSuccess: (_, variables) => { toast.success(`Successfully removed ${variables.trigger.name}`) setTriggerToDelete(null) }, }) const createTrigger = () => { setTriggerToDuplicate(null) if (isInlineEditorEnabled) { setEditorPanelInitialPrompt('Create a new database trigger that...') setEditorPanelValue( safeSql`create trigger trigger_name after insert or update or delete on table_name for each row execute function function_name();` ) if (editorPanelTemplates.length > 0) { setEditorPanelTemplates([]) } openSidebar(SIDEBAR_KEYS.EDITOR_PANEL) } else { setShowCreateTriggerForm(true) } } const editTrigger = (trigger: PostgresTrigger) => { setTriggerToDuplicate(null) if (isInlineEditorEnabled) { setEditorPanelValue(generateTriggerCreateSQL(trigger)) setEditorPanelTemplates([]) openSidebar(SIDEBAR_KEYS.EDITOR_PANEL) } else { setTriggerToEdit(trigger.id.toString()) } } const duplicateTrigger = (trigger: PostgresTrigger) => { if (isInlineEditorEnabled) { const dupTrigger = { ...trigger, name: `${trigger.name}_duplicate`, } setEditorPanelValue(generateTriggerCreateSQL(dupTrigger)) setEditorPanelTemplates([]) openSidebar(SIDEBAR_KEYS.EDITOR_PANEL) } else { setTriggerToDuplicate(trigger.id.toString()) } } const onDeleteTrigger = () => { if (!project) return console.error('Project is required') if (!triggerToDelete) return console.error('Trigger ID is required') deleteDatabaseTrigger({ projectRef: project.ref, connectionString: project.connectionString, trigger: triggerToDelete, }) } const schemaTriggers = triggers.filter((x) => x.schema === selectedSchema) const tables = Array.from(new Set(schemaTriggers.map((x) => x.table))).sort() const canAddTriggers = canCreateTriggers && !isSchemaLocked && hasTables useShortcut( SHORTCUT_IDS.LIST_PAGE_FOCUS_SEARCH, () => { searchInputRef.current?.focus() searchInputRef.current?.select() }, { label: 'Search triggers' } ) useShortcut( SHORTCUT_IDS.LIST_PAGE_NEW_ITEM, () => { createTrigger() }, { enabled: canAddTriggers, label: 'Create new trigger' } ) useShortcut(SHORTCUT_IDS.LIST_PAGE_RESET_FILTERS, () => { setFilterString('') setTablesFilter([]) }) useEffect(() => { if (isSuccess && !!triggerIdToEdit && !triggerToEdit) { toast('Trigger not found') setTriggerToEdit(null) } }, [isSuccess, setTriggerToEdit, triggerIdToEdit, triggerToEdit]) useEffect(() => { if (isSuccess && !!triggerIdToDuplicate && !triggerToDuplicate) { toast('Trigger not found') setTriggerToDuplicate(null) } }, [isSuccess, triggerIdToDuplicate, triggerToDuplicate, setTriggerToDuplicate]) useEffect(() => { if (isSuccess && !!triggerIdToDelete && !triggerToDelete && !isSuccessDelete) { toast('Trigger not found') setTriggerToDelete(null) } }, [isSuccess, triggerIdToDelete, triggerToDelete, isSuccessDelete, setTriggerToDelete]) if (isPending) { return } if (isError) { return } return ( <>
setSchemaSelectorOpen(true)} side="bottom" tooltipOpen={schemaSelectorOpen ? false : undefined} > setFilterString(e.target.value)} onKeyDown={onSearchInputEscape(filterString, setFilterString)} /> ({ label: type, value: type }))} value={tablesFilter ?? []} onChange={setTablesFilter} showSearch />
{!isSchemaLocked && ( )}
{isSchemaLocked && } {!isSchemaLocked && (schemaTriggers ?? []).length === 0 ? ( ) : (
Name Table Function Events Orientation Enabled setTriggerToDelete(trigger.id.toString())} />
)}
{ setShowCreateTriggerForm(false) setTriggerToEdit(null) setTriggerToDuplicate(null) }} isDuplicatingTrigger={!!triggerToDuplicate} /> setTriggerToDelete(null)} onConfirm={() => onDeleteTrigger()} title="Delete this trigger" loading={isDeletingTrigger} confirmLabel={`Delete trigger ${triggerToDelete?.name}`} confirmPlaceholder="Type in name of trigger" confirmString={triggerToDelete?.name ?? ''} text={ <> This will delete your trigger called{' '} {triggerToDelete?.name} of schema{' '} {triggerToDelete?.schema} } alert={{ title: 'You cannot recover this trigger once deleted.', }} /> ) }