TriggerList.tsx 9.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261
  1. import { PermissionAction } from '@supabase/shared-types/out/constants'
  2. import { useParams } from 'common'
  3. import { includes, sortBy } from 'lodash'
  4. import { Check, Copy, Edit, Edit2, MoreVertical, Trash, X } from 'lucide-react'
  5. import Link from 'next/link'
  6. import { parseAsJson, parseAsString, useQueryState } from 'nuqs'
  7. import {
  8. Badge,
  9. Button,
  10. DropdownMenu,
  11. DropdownMenuContent,
  12. DropdownMenuItem,
  13. DropdownMenuSeparator,
  14. DropdownMenuTrigger,
  15. TableCell,
  16. TableRow,
  17. } from 'ui'
  18. import {
  19. generateTriggerCreateSQL,
  20. getDatabaseFunctionsHref,
  21. type PostgresTrigger,
  22. } from './TriggerList.utils'
  23. import { selectFilterSchema } from '@/components/interfaces/Reports/v2/ReportsSelectFilter'
  24. import { SIDEBAR_KEYS } from '@/components/layouts/ProjectLayout/LayoutSidebar/LayoutSidebarProvider'
  25. import { ButtonTooltip } from '@/components/ui/ButtonTooltip'
  26. import { useDatabaseTriggersQuery } from '@/data/database-triggers/database-triggers-query'
  27. import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions'
  28. import { useQuerySchemaState } from '@/hooks/misc/useSchemaQueryState'
  29. import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
  30. import { useIsProtectedSchema } from '@/hooks/useProtectedSchemas'
  31. import { useAiAssistantStateSnapshot } from '@/state/ai-assistant-state'
  32. import { useSidebarManagerSnapshot } from '@/state/sidebar-manager-state'
  33. interface TriggerListProps {
  34. editTrigger: (trigger: PostgresTrigger) => void
  35. duplicateTrigger: (trigger: PostgresTrigger) => void
  36. deleteTrigger: (trigger: PostgresTrigger) => void
  37. }
  38. export const TriggerList = ({ editTrigger, duplicateTrigger, deleteTrigger }: TriggerListProps) => {
  39. const { ref: projectRef } = useParams()
  40. const { data: project } = useSelectedProjectQuery()
  41. const aiSnap = useAiAssistantStateSnapshot()
  42. const { openSidebar } = useSidebarManagerSnapshot()
  43. const { can: canUpdateTriggers } = useAsyncCheckPermissions(
  44. PermissionAction.TENANT_SQL_ADMIN_WRITE,
  45. 'triggers'
  46. )
  47. const { selectedSchema: schema } = useQuerySchemaState()
  48. const { isSchemaLocked: isLocked } = useIsProtectedSchema({ schema })
  49. const [filterString] = useQueryState('search', parseAsString.withDefault(''))
  50. const [tablesFilter] = useQueryState(
  51. 'tables',
  52. parseAsJson(selectFilterSchema.parse).withDefault([])
  53. )
  54. const { data: triggers } = useDatabaseTriggersQuery({
  55. projectRef: project?.ref,
  56. connectionString: project?.connectionString,
  57. })
  58. const filteredTriggers = (triggers ?? []).filter((x) => {
  59. const search = filterString?.toLowerCase()
  60. const matchesSearch =
  61. !search ||
  62. x.name.toLowerCase().includes(search) ||
  63. (!!x.function_name && includes(x.function_name.toLowerCase(), search))
  64. const matchesTables = !tablesFilter?.length || tablesFilter.includes(x.table)
  65. return matchesSearch && matchesTables
  66. })
  67. const _triggers = sortBy(
  68. filteredTriggers.filter((x) => x.schema == schema),
  69. (trigger) => trigger.name.toLocaleLowerCase()
  70. )
  71. if (_triggers.length === 0 && filterString.length === 0) {
  72. return (
  73. <TableRow key={schema}>
  74. <TableCell colSpan={7}>
  75. <p className="text-sm text-foreground">No triggers created yet</p>
  76. <p className="text-sm text-foreground-light">
  77. There are no triggers found in the schema "{schema}"
  78. </p>
  79. </TableCell>
  80. </TableRow>
  81. )
  82. }
  83. if (_triggers.length === 0 && filterString.length > 0) {
  84. return (
  85. <TableRow key={schema}>
  86. <TableCell colSpan={7}>
  87. <p className="text-sm text-foreground">No results found</p>
  88. <p className="text-sm text-foreground-light">
  89. Your search for "{filterString}" did not return any results
  90. </p>
  91. </TableCell>
  92. </TableRow>
  93. )
  94. }
  95. return (
  96. <>
  97. {_triggers.map((x) => (
  98. <TableRow key={x.id}>
  99. <TableCell className="space-x-2">
  100. <Button
  101. type="text"
  102. disabled={isLocked || !canUpdateTriggers}
  103. onClick={() => editTrigger(x)}
  104. title={x.name}
  105. className="text-link-table-cell text-left text-sm disabled:opacity-90 disabled:no-underline min-w-0 p-0 hover:bg-transparent font-medium max-w-48 title"
  106. >
  107. {x.name}
  108. </Button>
  109. </TableCell>
  110. <TableCell className="break-all">
  111. {x.table_id ? (
  112. <Link
  113. href={`/project/${projectRef}/editor/${x.table_id}`}
  114. className="text-link-table-cell block max-w-40 text-foreground-light"
  115. >
  116. {x.table}
  117. </Link>
  118. ) : (
  119. <p title={x.table} className="truncate">
  120. {x.table}
  121. </p>
  122. )}
  123. </TableCell>
  124. <TableCell className="space-x-2">
  125. {x.function_name ? (
  126. <Link
  127. href={getDatabaseFunctionsHref(projectRef, x.function_schema, x.function_name)}
  128. className="text-link-table-cell block max-w-40 text-foreground-light"
  129. >
  130. {x.function_name}
  131. </Link>
  132. ) : (
  133. <p className="truncate text-foreground-light">-</p>
  134. )}
  135. </TableCell>
  136. <TableCell>
  137. <div className="flex gap-2 flex-wrap">
  138. {x.events.map((event: string) => (
  139. <Badge key={event}>{`${x.activation} ${event}`}</Badge>
  140. ))}
  141. </div>
  142. </TableCell>
  143. <TableCell className="space-x-2">
  144. <p title={x.orientation} className="truncate">
  145. {x.orientation}
  146. </p>
  147. </TableCell>
  148. <TableCell>
  149. <div className="flex items-center justify-center">
  150. {x.enabled_mode !== 'DISABLED' ? (
  151. <Check strokeWidth={2} className="text-brand" />
  152. ) : (
  153. <X strokeWidth={2} />
  154. )}
  155. </div>
  156. </TableCell>
  157. <TableCell className="text-right">
  158. {!isLocked && (
  159. <div className="flex items-center justify-end">
  160. {canUpdateTriggers ? (
  161. <DropdownMenu>
  162. <DropdownMenuTrigger asChild>
  163. <Button
  164. aria-label="More options"
  165. type="default"
  166. className="px-1"
  167. icon={<MoreVertical />}
  168. />
  169. </DropdownMenuTrigger>
  170. <DropdownMenuContent side="bottom" align="end" className="w-52">
  171. <DropdownMenuItem
  172. className="space-x-2"
  173. onClick={() => {
  174. editTrigger(x)
  175. }}
  176. >
  177. <Edit2 size={14} />
  178. <p>Edit trigger</p>
  179. </DropdownMenuItem>
  180. <DropdownMenuItem
  181. className="space-x-2"
  182. onClick={() => {
  183. const sql = generateTriggerCreateSQL(x)
  184. openSidebar(SIDEBAR_KEYS.AI_ASSISTANT)
  185. aiSnap.newChat({
  186. name: `Update trigger ${x.name}`,
  187. initialInput: `Update this trigger which exists on the ${x.schema}.${x.table} table to...`,
  188. suggestions: {
  189. title:
  190. 'I can help you make a change to this trigger, here are a few example prompts to get you started:',
  191. prompts: [
  192. {
  193. label: 'Rename Trigger',
  194. description: 'Rename this trigger to ...',
  195. },
  196. {
  197. label: 'Change Events',
  198. description: 'Change the events this trigger responds to ...',
  199. },
  200. {
  201. label: 'Modify Timing',
  202. description:
  203. 'Modify this trigger to run after instead of before ...',
  204. },
  205. ],
  206. },
  207. sqlSnippets: [sql],
  208. })
  209. }}
  210. >
  211. <Edit size={14} />
  212. <p>Edit with Assistant</p>
  213. </DropdownMenuItem>
  214. <DropdownMenuItem className="space-x-2" onClick={() => duplicateTrigger(x)}>
  215. <Copy size={14} />
  216. <p>Duplicate trigger</p>
  217. </DropdownMenuItem>
  218. <DropdownMenuSeparator />
  219. <DropdownMenuItem className="space-x-2" onClick={() => deleteTrigger(x)}>
  220. <Trash stroke="red" size={14} />
  221. <p>Delete trigger</p>
  222. </DropdownMenuItem>
  223. </DropdownMenuContent>
  224. </DropdownMenu>
  225. ) : (
  226. <ButtonTooltip
  227. disabled
  228. type="default"
  229. className="px-1"
  230. icon={<MoreVertical />}
  231. tooltip={{
  232. content: {
  233. side: 'bottom',
  234. text: 'You need additional permissions to update triggers',
  235. },
  236. }}
  237. />
  238. )}
  239. </div>
  240. )}
  241. </TableCell>
  242. </TableRow>
  243. ))}
  244. </>
  245. )
  246. }