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:
Saxon FletcherandJoshen Lim authored and GitHub committed 2024-11-27 09:57:21 +10:00
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]