mirror of
https://github.com/supabase/supabase.git
synced 2026-10-16 06:39:09 +03:00
Feat/assistant triggers (#30665)
* add assistant to triggers * adjust copy * remove console * Lint --------- Co-authored-by: Joshen Lim <joshenlimek@gmail.com>
This commit is contained in:
1 parent
2bcc17d514
commit
48c0578ff7
5 files changed
+154
-52
No files matched your search
@@ -17,7 +17,11 @@ import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectConte
|
||||
import Table from 'components/to-be-cleaned/Table'
|
||||
import { useDatabaseTriggersQuery } from 'data/database-triggers/database-triggers-query'
|
||||
import { useCheckPermissions } from 'hooks/misc/useCheckPermissions'
|
||||
import { Check, X, MoreVertical, Edit3, Trash } from 'lucide-react'
|
||||
import { Check, X, MoreVertical, Edit3, Trash, Edit, Edit2 } from 'lucide-react'
|
||||
import { useAppStateSnapshot } from 'state/app-state'
|
||||
import { useIsAssistantV2Enabled } from 'components/interfaces/App/FeaturePreview/FeaturePreviewContext'
|
||||
import { cn } from 'ui'
|
||||
import { generateTriggerCreateSQL } from './TriggerList.utils'
|
||||
|
||||
interface TriggerListProps {
|
||||
schema: string
|
||||
@@ -35,6 +39,8 @@ const TriggerList = ({
|
||||
deleteTrigger,
|
||||
}: TriggerListProps) => {
|
||||
const { project } = useProjectContext()
|
||||
const { setAiAssistantPanel } = useAppStateSnapshot()
|
||||
const isAssistantV2Enabled = useIsAssistantV2Enabled()
|
||||
|
||||
const { data: triggers } = useDatabaseTriggersQuery({
|
||||
projectRef: project?.ref,
|
||||
@@ -135,13 +141,42 @@ const TriggerList = ({
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button type="default" className="px-1" icon={<MoreVertical />} />
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent side="bottom" align="end" className="w-36">
|
||||
<DropdownMenuContent
|
||||
side="bottom"
|
||||
align="end"
|
||||
className={cn(isAssistantV2Enabled ? 'w-52' : 'w-36')}
|
||||
>
|
||||
<DropdownMenuItem className="space-x-2" onClick={() => editTrigger(x)}>
|
||||
<Edit3 size="14" />
|
||||
<Edit2 size={14} />
|
||||
<p>Edit trigger</p>
|
||||
</DropdownMenuItem>
|
||||
{isAssistantV2Enabled && (
|
||||
<DropdownMenuItem
|
||||
className="space-x-2"
|
||||
onClick={() => {
|
||||
const sql = generateTriggerCreateSQL(x)
|
||||
setAiAssistantPanel({
|
||||
open: true,
|
||||
initialInput: `Update this trigger which exists on the ${x.schema}.${x.table} table to...`,
|
||||
suggestions: {
|
||||
title:
|
||||
'I can help you make a change to this trigger, here are a few example prompts to get you started:',
|
||||
prompts: [
|
||||
'Rename this trigger to ...',
|
||||
'Change the events this trigger responds to ...',
|
||||
'Modify this trigger to run after instead of before ...',
|
||||
],
|
||||
},
|
||||
sqlSnippets: [sql],
|
||||
})
|
||||
}}
|
||||
>
|
||||
<Edit size={14} />
|
||||
<p>Edit with Assistant</p>
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
<DropdownMenuItem className="space-x-2" onClick={() => deleteTrigger(x)}>
|
||||
<Trash stroke="red" size="14" />
|
||||
<Trash stroke="red" size={14} />
|
||||
<p>Delete trigger</p>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
interface PostgresTrigger {
|
||||
activation: string
|
||||
condition: string | null
|
||||
enabled_mode: string
|
||||
events: string[]
|
||||
function_args: string[]
|
||||
function_name: string
|
||||
function_schema: string
|
||||
id: number
|
||||
name: string
|
||||
orientation: string
|
||||
schema: string
|
||||
table: string
|
||||
table_id: number
|
||||
}
|
||||
|
||||
export const generateTriggerCreateSQL = (trigger: PostgresTrigger) => {
|
||||
const events = trigger.events.join(' OR ')
|
||||
const args = trigger.function_args.length > 0 ? `(${trigger.function_args.join(', ')})` : '()'
|
||||
|
||||
let sql = `
|
||||
CREATE TRIGGER "${trigger.name}"
|
||||
${trigger.activation} ${events}
|
||||
ON "${trigger.schema}"."${trigger.table}"
|
||||
FOR EACH ${trigger.orientation}
|
||||
`
|
||||
|
||||
if (trigger.condition) {
|
||||
sql += `WHEN (${trigger.condition})\n`
|
||||
}
|
||||
|
||||
sql += `EXECUTE FUNCTION "${trigger.function_schema}"."${trigger.function_name}"${args};`
|
||||
|
||||
return sql.trim()
|
||||
}
|
||||
@@ -1,9 +1,8 @@
|
||||
import * as Tooltip from '@radix-ui/react-tooltip'
|
||||
import { PermissionAction } from '@supabase/shared-types/out/constants'
|
||||
import { noop, partition } from 'lodash'
|
||||
import { useState } from 'react'
|
||||
import { Button, Input } from 'ui'
|
||||
|
||||
import { Input, AiIconAnimation } from 'ui'
|
||||
import { ButtonTooltip } from 'components/ui/ButtonTooltip'
|
||||
import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
|
||||
import AlphaPreview from 'components/to-be-cleaned/AlphaPreview'
|
||||
import ProductEmptyState from 'components/to-be-cleaned/ProductEmptyState'
|
||||
@@ -19,6 +18,8 @@ import { EXCLUDED_SCHEMAS } from 'lib/constants/schemas'
|
||||
import { Search } from 'lucide-react'
|
||||
import ProtectedSchemaWarning from '../../ProtectedSchemaWarning'
|
||||
import TriggerList from './TriggerList'
|
||||
import { useAppStateSnapshot } from 'state/app-state'
|
||||
import { useIsAssistantV2Enabled } from 'components/interfaces/App/FeaturePreview/FeaturePreviewContext'
|
||||
|
||||
interface TriggersListProps {
|
||||
createTrigger: () => void
|
||||
@@ -57,6 +58,9 @@ const TriggersList = ({
|
||||
|
||||
const canCreateTriggers = useCheckPermissions(PermissionAction.TENANT_SQL_ADMIN_WRITE, 'triggers')
|
||||
|
||||
const { setAiAssistantPanel } = useAppStateSnapshot()
|
||||
const isAssistantV2Enabled = useIsAssistantV2Enabled()
|
||||
|
||||
if (isLoading) {
|
||||
return <GenericSkeletonLoader />
|
||||
}
|
||||
@@ -87,51 +91,74 @@ const TriggersList = ({
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<SchemaSelector
|
||||
className="w-[180px]"
|
||||
size="tiny"
|
||||
showError={false}
|
||||
selectedSchemaName={selectedSchema}
|
||||
onSelectSchema={setSelectedSchema}
|
||||
/>
|
||||
<Input
|
||||
placeholder="Search for a trigger"
|
||||
size="tiny"
|
||||
icon={<Search size="14" />}
|
||||
value={filterString}
|
||||
className="w-52"
|
||||
onChange={(e) => setFilterString(e.target.value)}
|
||||
/>
|
||||
<div className="flex items-center justify-between gap-2 flex-wrap">
|
||||
<div className="flex items-center gap-x-2">
|
||||
<SchemaSelector
|
||||
className="w-[180px]"
|
||||
size="tiny"
|
||||
showError={false}
|
||||
selectedSchemaName={selectedSchema}
|
||||
onSelectSchema={setSelectedSchema}
|
||||
/>
|
||||
<Input
|
||||
placeholder="Search for a trigger"
|
||||
size="tiny"
|
||||
icon={<Search size="14" />}
|
||||
value={filterString}
|
||||
className="w-52"
|
||||
onChange={(e) => setFilterString(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
{!isLocked && (
|
||||
<Tooltip.Root delayDuration={0}>
|
||||
<Tooltip.Trigger asChild>
|
||||
<Button
|
||||
className="ml-auto"
|
||||
<div className="flex items-center gap-x-2">
|
||||
<ButtonTooltip
|
||||
disabled={!canCreateTriggers}
|
||||
onClick={() => createTrigger()}
|
||||
tooltip={{
|
||||
content: {
|
||||
side: 'bottom',
|
||||
text: !canCreateTriggers
|
||||
? 'You need additional permissions to create triggers'
|
||||
: undefined,
|
||||
},
|
||||
}}
|
||||
>
|
||||
Create a new trigger
|
||||
</ButtonTooltip>
|
||||
{isAssistantV2Enabled && (
|
||||
<ButtonTooltip
|
||||
type="default"
|
||||
disabled={!canCreateTriggers}
|
||||
onClick={() => createTrigger()}
|
||||
>
|
||||
Create a new trigger
|
||||
</Button>
|
||||
</Tooltip.Trigger>
|
||||
{!canCreateTriggers && (
|
||||
<Tooltip.Portal>
|
||||
<Tooltip.Content side="bottom">
|
||||
<Tooltip.Arrow className="radix-tooltip-arrow" />
|
||||
<div
|
||||
className={[
|
||||
'rounded bg-alternative py-1 px-2 leading-none shadow',
|
||||
'border border-background',
|
||||
].join(' ')}
|
||||
>
|
||||
<span className="text-xs text-foreground">
|
||||
You need additional permissions to create triggers
|
||||
</span>
|
||||
</div>
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Portal>
|
||||
className="px-1 pointer-events-auto"
|
||||
icon={
|
||||
<AiIconAnimation className="scale-75 [&>div>div]:border-black dark:[&>div>div]:border-white" />
|
||||
}
|
||||
onClick={() =>
|
||||
setAiAssistantPanel({
|
||||
open: true,
|
||||
initialInput: `Create a new trigger for the schema ${selectedSchema} that does ...`,
|
||||
suggestions: {
|
||||
title:
|
||||
'I can help you create a new trigger, here are a few example prompts to get you started:',
|
||||
prompts: [
|
||||
'Create a trigger that logs changes to the users table',
|
||||
'Create a trigger that updates updated_at timestamp',
|
||||
'Create a trigger that validates email format before insert',
|
||||
],
|
||||
},
|
||||
})
|
||||
}
|
||||
tooltip={{
|
||||
content: {
|
||||
side: 'bottom',
|
||||
text: !canCreateTriggers
|
||||
? 'You need additional permissions to create triggers'
|
||||
: 'Create with Supabase Assistant',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Tooltip.Root>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
||||
@@ -198,7 +198,11 @@ const ProjectLayout = forwardRef<HTMLDivElement, PropsWithChildren<ProjectLayout
|
||||
<ResizableHandle />
|
||||
<ResizablePanel
|
||||
id="panel-assistant"
|
||||
className="min-w-[400px] max-w-[500px] bg 2xl:max-w-[600px] xl:relative xl:top-0 absolute right-0 top-[48px] bottom-0"
|
||||
className={cn(
|
||||
'bg absolute right-0 top-[48px] bottom-0 xl:relative xl:top-0',
|
||||
'min-w-[400px] max-w-[500px]',
|
||||
'2xl:min-w-[500px] 2xl:max-w-[600px]'
|
||||
)}
|
||||
>
|
||||
<AiAssistantPanel />
|
||||
</ResizablePanel>
|
||||
|
||||
@@ -394,8 +394,9 @@ export const AIAssistant = ({
|
||||
</h3>
|
||||
{suggestions.title && <p>{suggestions.title}</p>}
|
||||
<div className="-mx-3 mt-4 mb-12">
|
||||
{suggestions?.prompts?.map((prompt) => (
|
||||
{suggestions?.prompts?.map((prompt, idx) => (
|
||||
<Button
|
||||
key={`suggestion-${idx}`}
|
||||
size="small"
|
||||
icon={<FileText strokeWidth={1.5} size={16} />}
|
||||
type="text"
|
||||
@@ -518,8 +519,8 @@ export const AIAssistant = ({
|
||||
<div className="mb-2">
|
||||
{sqlSnippets.map((snippet, index) => (
|
||||
<CollapsibleCodeBlock
|
||||
hideLineNumbers
|
||||
key={index}
|
||||
hideLineNumbers
|
||||
value={snippet}
|
||||
onRemove={() => {
|
||||
const newSnippets = [...sqlSnippets]
|
||||
|
||||
Reference in new issue
Block a user