QueueTab.tsx 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287
  1. import { useParams } from 'common'
  2. import { Lock, Paintbrush, PlusCircle, Trash2 } from 'lucide-react'
  3. import Link from 'next/link'
  4. import { parseAsBoolean, useQueryState } from 'nuqs'
  5. import { useMemo, useState } from 'react'
  6. import { toast } from 'sonner'
  7. import { Button, cn, LoadingLine, Popover, PopoverContent, PopoverTrigger, Separator } from 'ui'
  8. import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal'
  9. import { ShimmeringLoader } from 'ui-patterns/ShimmeringLoader'
  10. import { pgmqQueueTable } from './Queues.utils'
  11. import { DeleteQueue } from '@/components/interfaces/Integrations/Queues/SingleQueue/DeleteQueue'
  12. import { PurgeQueue } from '@/components/interfaces/Integrations/Queues/SingleQueue/PurgeQueue'
  13. import { QUEUE_MESSAGE_TYPE } from '@/components/interfaces/Integrations/Queues/SingleQueue/Queue.utils'
  14. import { QueueMessagesDataGrid } from '@/components/interfaces/Integrations/Queues/SingleQueue/QueueDataGrid'
  15. import { QueueFilters } from '@/components/interfaces/Integrations/Queues/SingleQueue/QueueFilters'
  16. import { QueueSettings } from '@/components/interfaces/Integrations/Queues/SingleQueue/QueueSettings'
  17. import { SendMessageModal } from '@/components/interfaces/Integrations/Queues/SingleQueue/SendMessageModal'
  18. import { Markdown } from '@/components/interfaces/Markdown'
  19. import { ButtonTooltip } from '@/components/ui/ButtonTooltip'
  20. import { useDatabasePoliciesQuery } from '@/data/database-policies/database-policies-query'
  21. import { useQueueMessagesInfiniteQuery } from '@/data/database-queues/database-queue-messages-infinite-query'
  22. import { useQueuesExposePostgrestStatusQuery } from '@/data/database-queues/database-queues-expose-postgrest-status-query'
  23. import { useTableUpdateMutation } from '@/data/tables/table-update-mutation'
  24. import { useTablesQuery } from '@/data/tables/tables-query'
  25. import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
  26. export const QueueTab = () => {
  27. const { childId: queueName, ref } = useParams()
  28. const { data: project } = useSelectedProjectQuery()
  29. const [openRlsPopover, setOpenRlsPopover] = useState(false)
  30. const [rlsConfirmModalOpen, setRlsConfirmModalOpen] = useState(false)
  31. const [sendMessageModalShown, setSendMessageModalShown] = useQueryState(
  32. 'new-message',
  33. parseAsBoolean.withDefault(false).withOptions({ history: 'push', clearOnDefault: true })
  34. )
  35. const [purgeQueueModalShown, setPurgeQueueModalShown] = useState(false)
  36. const [deleteQueueModalShown, setDeleteQueueModalShown] = useState(false)
  37. const [selectedTypes, setSelectedTypes] = useState<QUEUE_MESSAGE_TYPE[]>([])
  38. const { data: tables, isPending: isLoadingTables } = useTablesQuery({
  39. projectRef: project?.ref,
  40. connectionString: project?.connectionString,
  41. schema: 'pgmq',
  42. })
  43. const queueRelname = queueName ? pgmqQueueTable(queueName) : undefined
  44. const queueTable = tables?.find((x) => x.name === queueRelname)
  45. const isRlsEnabled = queueTable?.rls_enabled ?? false
  46. const { data: policies } = useDatabasePoliciesQuery({
  47. projectRef: project?.ref,
  48. connectionString: project?.connectionString,
  49. schema: 'pgmq',
  50. })
  51. const queuePolicies = (policies ?? []).filter((policy) => policy.table === queueRelname)
  52. const { data: isExposed } = useQueuesExposePostgrestStatusQuery({
  53. projectRef: project?.ref,
  54. connectionString: project?.connectionString,
  55. })
  56. const {
  57. data,
  58. error,
  59. isPending: isLoading,
  60. fetchNextPage,
  61. isFetching,
  62. } = useQueueMessagesInfiniteQuery(
  63. {
  64. projectRef: project?.ref,
  65. connectionString: project?.connectionString,
  66. queueName: queueName!,
  67. // when no types are selected, include all types of messages
  68. status: selectedTypes.length === 0 ? ['archived', 'available', 'scheduled'] : selectedTypes,
  69. },
  70. { staleTime: 30 }
  71. )
  72. const messages = useMemo(() => data?.pages.flatMap((p) => p), [data?.pages])
  73. const { mutate: updateTable, isPending: isUpdatingTable } = useTableUpdateMutation({
  74. onSettled: () => {
  75. toast.success(`Successfully enabled RLS for ${queueName}`)
  76. setRlsConfirmModalOpen(false)
  77. },
  78. })
  79. const onToggleRLS = async () => {
  80. if (!project) return console.error('Project is required')
  81. if (!queueTable) return toast.error('Unable to toggle RLS: Queue table not found')
  82. const payload = {
  83. id: queueTable.id,
  84. rls_enabled: true,
  85. }
  86. updateTable({
  87. projectRef: project?.ref,
  88. connectionString: project?.connectionString,
  89. id: queueTable.id,
  90. name: queueTable.name,
  91. schema: 'pgmq',
  92. payload: payload,
  93. })
  94. }
  95. return (
  96. <div className="h-full flex flex-col">
  97. <div className="flex items-center justify-between gap-x-4 py-1.5 px-10 mb-0 bg-surface-200">
  98. <QueueFilters selectedTypes={selectedTypes} setSelectedTypes={setSelectedTypes} />
  99. <div className="flex gap-x-2">
  100. <QueueSettings />
  101. <ButtonTooltip
  102. type="text"
  103. className="px-1.5"
  104. onClick={() => setPurgeQueueModalShown(true)}
  105. icon={<Paintbrush />}
  106. title="Purge messages"
  107. aria-label="Purge messages"
  108. tooltip={{ content: { side: 'bottom', text: 'Purge messages' } }}
  109. />
  110. <ButtonTooltip
  111. type="text"
  112. className="px-1.5"
  113. onClick={() => setDeleteQueueModalShown(true)}
  114. icon={<Trash2 />}
  115. title="Delete queue"
  116. aria-label="Delete queue"
  117. tooltip={{ content: { side: 'bottom', text: 'Delete queue' } }}
  118. />
  119. <Separator orientation="vertical" className="h-[26px]" />
  120. {isLoadingTables ? (
  121. <ShimmeringLoader className="w-[123px]" />
  122. ) : isRlsEnabled ? (
  123. <>
  124. {queuePolicies.length === 0 ? (
  125. <ButtonTooltip
  126. asChild
  127. type="default"
  128. className="group"
  129. icon={<PlusCircle strokeWidth={1.5} className="text-foreground-muted" />}
  130. tooltip={{
  131. content: {
  132. side: 'bottom',
  133. className: 'w-[280px]',
  134. text: 'RLS is enabled for this queue, but no policies are set. Queue will not be accessible.',
  135. },
  136. }}
  137. >
  138. <Link
  139. passHref
  140. href={`/project/${ref}/auth/policies?search=${queueTable?.id}&schema=pgmq`}
  141. >
  142. Add RLS policy
  143. </Link>
  144. </ButtonTooltip>
  145. ) : (
  146. <Button
  147. asChild
  148. type="default"
  149. className="group"
  150. icon={
  151. <div
  152. className={cn(
  153. 'flex items-center justify-center rounded-full bg-border-stronger h-[16px]',
  154. queuePolicies.length > 9 ? ' px-1' : 'w-[16px]'
  155. )}
  156. >
  157. <span className="text-[11px] text-foreground font-mono text-center">
  158. {queuePolicies.length}
  159. </span>
  160. </div>
  161. }
  162. >
  163. <Link
  164. passHref
  165. href={`/project/${ref}/auth/policies?search=${queueTable?.id}&schema=pgmq`}
  166. >
  167. Auth {queuePolicies.length > 1 ? 'policies' : 'policy'}
  168. </Link>
  169. </Button>
  170. )}
  171. </>
  172. ) : (
  173. <Popover
  174. modal={false}
  175. open={openRlsPopover}
  176. onOpenChange={() => setOpenRlsPopover(!openRlsPopover)}
  177. >
  178. <PopoverTrigger asChild>
  179. <Button type={isExposed ? 'warning' : 'default'} icon={<Lock strokeWidth={1.5} />}>
  180. RLS disabled
  181. </Button>
  182. </PopoverTrigger>
  183. <PopoverContent className="w-80 text-sm" align="end">
  184. <h3 className="text-xs flex items-center gap-x-2">
  185. <Lock size={14} /> Row Level Security (RLS)
  186. </h3>
  187. <div className="grid gap-2 mt-2 text-foreground-light text-xs">
  188. {isExposed ? (
  189. <>
  190. <p>
  191. You can restrict and control who can manage this queue using Row Level
  192. Security.
  193. </p>
  194. <p>With RLS enabled, anonymous users will not have access to this queue.</p>
  195. <Button
  196. type="default"
  197. className="w-min"
  198. onClick={() => setRlsConfirmModalOpen(!rlsConfirmModalOpen)}
  199. >
  200. Enable RLS for this queue
  201. </Button>
  202. </>
  203. ) : (
  204. <>
  205. <Markdown
  206. className="[&>p]:leading-normal! text-xs [&>p]:m-0! flex flex-col gap-y-2"
  207. content={`
  208. RLS for queues is only relevant if exposure through PostgREST has been enabled, in which you can restrict and control who can manage this queue using Row Level Security.
  209. You may opt to manage your queues via any Briven client libraries or PostgREST endpoints by enabling this in the [queues settings](/project/${project?.ref}/integrations/queues/settings).`}
  210. />
  211. <Button
  212. type="default"
  213. className="w-min"
  214. onClick={() => setRlsConfirmModalOpen(!rlsConfirmModalOpen)}
  215. >
  216. Enable RLS for this queue
  217. </Button>
  218. </>
  219. )}
  220. </div>
  221. </PopoverContent>
  222. </Popover>
  223. )}
  224. <Button type="primary" onClick={() => setSendMessageModalShown(true)}>
  225. Add message
  226. </Button>
  227. {/* <DocsButton href={docsUrl} />} */}
  228. </div>
  229. </div>
  230. <LoadingLine loading={isFetching} />
  231. <QueueMessagesDataGrid
  232. error={error}
  233. messages={messages || []}
  234. isLoading={isLoading}
  235. showMessageModal={() => setSendMessageModalShown(true)}
  236. fetchNextPage={fetchNextPage}
  237. />
  238. <SendMessageModal
  239. visible={sendMessageModalShown}
  240. onClose={() => setSendMessageModalShown(false)}
  241. />
  242. <DeleteQueue
  243. queueName={queueName!}
  244. visible={deleteQueueModalShown}
  245. onClose={() => setDeleteQueueModalShown(false)}
  246. />
  247. <PurgeQueue
  248. queueName={queueName!}
  249. visible={purgeQueueModalShown}
  250. onClose={() => setPurgeQueueModalShown(false)}
  251. />
  252. <ConfirmationModal
  253. visible={rlsConfirmModalOpen}
  254. title="Enable Row Level Security"
  255. confirmLabel="Enable RLS"
  256. confirmLabelLoading="Enabling RLS"
  257. loading={isUpdatingTable}
  258. onCancel={() => setRlsConfirmModalOpen(false)}
  259. onConfirm={() => onToggleRLS()}
  260. >
  261. <p className="text-sm text-foreground-light">
  262. Are you sure you want to enable Row Level Security for the queue "{queueName}"?
  263. </p>
  264. </ConfirmationModal>
  265. </div>
  266. )
  267. }