Skip to content

Commit 48c0578

Browse files
SaxonFjoshenlim
andauthored
Feat/assistant triggers (supabase#30665)
* add assistant to triggers * adjust copy * remove console * Lint --------- Co-authored-by: Joshen Lim <[email protected]>
1 parent 2bcc17d commit 48c0578

File tree

5 files changed

+154
-52
lines changed

5 files changed

+154
-52
lines changed

apps/studio/components/interfaces/Database/Triggers/TriggersList/TriggerList.tsx

Lines changed: 39 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,11 @@ import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectConte
1717
import Table from 'components/to-be-cleaned/Table'
1818
import { useDatabaseTriggersQuery } from 'data/database-triggers/database-triggers-query'
1919
import { useCheckPermissions } from 'hooks/misc/useCheckPermissions'
20-
import { Check, X, MoreVertical, Edit3, Trash } from 'lucide-react'
20+
import { Check, X, MoreVertical, Edit3, Trash, Edit, Edit2 } from 'lucide-react'
21+
import { useAppStateSnapshot } from 'state/app-state'
22+
import { useIsAssistantV2Enabled } from 'components/interfaces/App/FeaturePreview/FeaturePreviewContext'
23+
import { cn } from 'ui'
24+
import { generateTriggerCreateSQL } from './TriggerList.utils'
2125

2226
interface TriggerListProps {
2327
schema: string
@@ -35,6 +39,8 @@ const TriggerList = ({
3539
deleteTrigger,
3640
}: TriggerListProps) => {
3741
const { project } = useProjectContext()
42+
const { setAiAssistantPanel } = useAppStateSnapshot()
43+
const isAssistantV2Enabled = useIsAssistantV2Enabled()
3844

3945
const { data: triggers } = useDatabaseTriggersQuery({
4046
projectRef: project?.ref,
@@ -135,13 +141,42 @@ const TriggerList = ({
135141
<DropdownMenuTrigger asChild>
136142
<Button type="default" className="px-1" icon={<MoreVertical />} />
137143
</DropdownMenuTrigger>
138-
<DropdownMenuContent side="bottom" align="end" className="w-36">
144+
<DropdownMenuContent
145+
side="bottom"
146+
align="end"
147+
className={cn(isAssistantV2Enabled ? 'w-52' : 'w-36')}
148+
>
139149
<DropdownMenuItem className="space-x-2" onClick={() => editTrigger(x)}>
140-
<Edit3 size="14" />
150+
<Edit2 size={14} />
141151
<p>Edit trigger</p>
142152
</DropdownMenuItem>
153+
{isAssistantV2Enabled && (
154+
<DropdownMenuItem
155+
className="space-x-2"
156+
onClick={() => {
157+
const sql = generateTriggerCreateSQL(x)
158+
setAiAssistantPanel({
159+
open: true,
160+
initialInput: `Update this trigger which exists on the ${x.schema}.${x.table} table to...`,
161+
suggestions: {
162+
title:
163+
'I can help you make a change to this trigger, here are a few example prompts to get you started:',
164+
prompts: [
165+
'Rename this trigger to ...',
166+
'Change the events this trigger responds to ...',
167+
'Modify this trigger to run after instead of before ...',
168+
],
169+
},
170+
sqlSnippets: [sql],
171+
})
172+
}}
173+
>
174+
<Edit size={14} />
175+
<p>Edit with Assistant</p>
176+
</DropdownMenuItem>
177+
)}
143178
<DropdownMenuItem className="space-x-2" onClick={() => deleteTrigger(x)}>
144-
<Trash stroke="red" size="14" />
179+
<Trash stroke="red" size={14} />
145180
<p>Delete trigger</p>
146181
</DropdownMenuItem>
147182
</DropdownMenuContent>
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
interface PostgresTrigger {
2+
activation: string
3+
condition: string | null
4+
enabled_mode: string
5+
events: string[]
6+
function_args: string[]
7+
function_name: string
8+
function_schema: string
9+
id: number
10+
name: string
11+
orientation: string
12+
schema: string
13+
table: string
14+
table_id: number
15+
}
16+
17+
export const generateTriggerCreateSQL = (trigger: PostgresTrigger) => {
18+
const events = trigger.events.join(' OR ')
19+
const args = trigger.function_args.length > 0 ? `(${trigger.function_args.join(', ')})` : '()'
20+
21+
let sql = `
22+
CREATE TRIGGER "${trigger.name}"
23+
${trigger.activation} ${events}
24+
ON "${trigger.schema}"."${trigger.table}"
25+
FOR EACH ${trigger.orientation}
26+
`
27+
28+
if (trigger.condition) {
29+
sql += `WHEN (${trigger.condition})\n`
30+
}
31+
32+
sql += `EXECUTE FUNCTION "${trigger.function_schema}"."${trigger.function_name}"${args};`
33+
34+
return sql.trim()
35+
}

apps/studio/components/interfaces/Database/Triggers/TriggersList/TriggersList.tsx

Lines changed: 72 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
1-
import * as Tooltip from '@radix-ui/react-tooltip'
21
import { PermissionAction } from '@supabase/shared-types/out/constants'
32
import { noop, partition } from 'lodash'
43
import { useState } from 'react'
5-
import { Button, Input } from 'ui'
6-
4+
import { Input, AiIconAnimation } from 'ui'
5+
import { ButtonTooltip } from 'components/ui/ButtonTooltip'
76
import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
87
import AlphaPreview from 'components/to-be-cleaned/AlphaPreview'
98
import ProductEmptyState from 'components/to-be-cleaned/ProductEmptyState'
@@ -19,6 +18,8 @@ import { EXCLUDED_SCHEMAS } from 'lib/constants/schemas'
1918
import { Search } from 'lucide-react'
2019
import ProtectedSchemaWarning from '../../ProtectedSchemaWarning'
2120
import TriggerList from './TriggerList'
21+
import { useAppStateSnapshot } from 'state/app-state'
22+
import { useIsAssistantV2Enabled } from 'components/interfaces/App/FeaturePreview/FeaturePreviewContext'
2223

2324
interface TriggersListProps {
2425
createTrigger: () => void
@@ -57,6 +58,9 @@ const TriggersList = ({
5758

5859
const canCreateTriggers = useCheckPermissions(PermissionAction.TENANT_SQL_ADMIN_WRITE, 'triggers')
5960

61+
const { setAiAssistantPanel } = useAppStateSnapshot()
62+
const isAssistantV2Enabled = useIsAssistantV2Enabled()
63+
6064
if (isLoading) {
6165
return <GenericSkeletonLoader />
6266
}
@@ -87,51 +91,74 @@ const TriggersList = ({
8791
</div>
8892
) : (
8993
<div className="space-y-4">
90-
<div className="flex items-center gap-2 flex-wrap">
91-
<SchemaSelector
92-
className="w-[180px]"
93-
size="tiny"
94-
showError={false}
95-
selectedSchemaName={selectedSchema}
96-
onSelectSchema={setSelectedSchema}
97-
/>
98-
<Input
99-
placeholder="Search for a trigger"
100-
size="tiny"
101-
icon={<Search size="14" />}
102-
value={filterString}
103-
className="w-52"
104-
onChange={(e) => setFilterString(e.target.value)}
105-
/>
94+
<div className="flex items-center justify-between gap-2 flex-wrap">
95+
<div className="flex items-center gap-x-2">
96+
<SchemaSelector
97+
className="w-[180px]"
98+
size="tiny"
99+
showError={false}
100+
selectedSchemaName={selectedSchema}
101+
onSelectSchema={setSelectedSchema}
102+
/>
103+
<Input
104+
placeholder="Search for a trigger"
105+
size="tiny"
106+
icon={<Search size="14" />}
107+
value={filterString}
108+
className="w-52"
109+
onChange={(e) => setFilterString(e.target.value)}
110+
/>
111+
</div>
106112
{!isLocked && (
107-
<Tooltip.Root delayDuration={0}>
108-
<Tooltip.Trigger asChild>
109-
<Button
110-
className="ml-auto"
113+
<div className="flex items-center gap-x-2">
114+
<ButtonTooltip
115+
disabled={!canCreateTriggers}
116+
onClick={() => createTrigger()}
117+
tooltip={{
118+
content: {
119+
side: 'bottom',
120+
text: !canCreateTriggers
121+
? 'You need additional permissions to create triggers'
122+
: undefined,
123+
},
124+
}}
125+
>
126+
Create a new trigger
127+
</ButtonTooltip>
128+
{isAssistantV2Enabled && (
129+
<ButtonTooltip
130+
type="default"
111131
disabled={!canCreateTriggers}
112-
onClick={() => createTrigger()}
113-
>
114-
Create a new trigger
115-
</Button>
116-
</Tooltip.Trigger>
117-
{!canCreateTriggers && (
118-
<Tooltip.Portal>
119-
<Tooltip.Content side="bottom">
120-
<Tooltip.Arrow className="radix-tooltip-arrow" />
121-
<div
122-
className={[
123-
'rounded bg-alternative py-1 px-2 leading-none shadow',
124-
'border border-background',
125-
].join(' ')}
126-
>
127-
<span className="text-xs text-foreground">
128-
You need additional permissions to create triggers
129-
</span>
130-
</div>
131-
</Tooltip.Content>
132-
</Tooltip.Portal>
132+
className="px-1 pointer-events-auto"
133+
icon={
134+
<AiIconAnimation className="scale-75 [&>div>div]:border-black dark:[&>div>div]:border-white" />
135+
}
136+
onClick={() =>
137+
setAiAssistantPanel({
138+
open: true,
139+
initialInput: `Create a new trigger for the schema ${selectedSchema} that does ...`,
140+
suggestions: {
141+
title:
142+
'I can help you create a new trigger, here are a few example prompts to get you started:',
143+
prompts: [
144+
'Create a trigger that logs changes to the users table',
145+
'Create a trigger that updates updated_at timestamp',
146+
'Create a trigger that validates email format before insert',
147+
],
148+
},
149+
})
150+
}
151+
tooltip={{
152+
content: {
153+
side: 'bottom',
154+
text: !canCreateTriggers
155+
? 'You need additional permissions to create triggers'
156+
: 'Create with Supabase Assistant',
157+
},
158+
}}
159+
/>
133160
)}
134-
</Tooltip.Root>
161+
</div>
135162
)}
136163
</div>
137164

apps/studio/components/layouts/ProjectLayout/ProjectLayout.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -198,7 +198,11 @@ const ProjectLayout = forwardRef<HTMLDivElement, PropsWithChildren<ProjectLayout
198198
<ResizableHandle />
199199
<ResizablePanel
200200
id="panel-assistant"
201-
className="min-w-[400px] max-w-[500px] bg 2xl:max-w-[600px] xl:relative xl:top-0 absolute right-0 top-[48px] bottom-0"
201+
className={cn(
202+
'bg absolute right-0 top-[48px] bottom-0 xl:relative xl:top-0',
203+
'min-w-[400px] max-w-[500px]',
204+
'2xl:min-w-[500px] 2xl:max-w-[600px]'
205+
)}
202206
>
203207
<AiAssistantPanel />
204208
</ResizablePanel>

apps/studio/components/ui/AIAssistantPanel/AIAssistant.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -394,8 +394,9 @@ export const AIAssistant = ({
394394
</h3>
395395
{suggestions.title && <p>{suggestions.title}</p>}
396396
<div className="-mx-3 mt-4 mb-12">
397-
{suggestions?.prompts?.map((prompt) => (
397+
{suggestions?.prompts?.map((prompt, idx) => (
398398
<Button
399+
key={`suggestion-${idx}`}
399400
size="small"
400401
icon={<FileText strokeWidth={1.5} size={16} />}
401402
type="text"
@@ -518,8 +519,8 @@ export const AIAssistant = ({
518519
<div className="mb-2">
519520
{sqlSnippets.map((snippet, index) => (
520521
<CollapsibleCodeBlock
521-
hideLineNumbers
522522
key={index}
523+
hideLineNumbers
523524
value={snippet}
524525
onRemove={() => {
525526
const newSnippets = [...sqlSnippets]

0 commit comments

Comments
 (0)