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