QueueTab.tsx287 lines · main
| 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 | |
| 11 | import { pgmqQueueTable } from './Queues.utils' |
| 12 | import { DeleteQueue } from '@/components/interfaces/Integrations/Queues/SingleQueue/DeleteQueue' |
| 13 | import { PurgeQueue } from '@/components/interfaces/Integrations/Queues/SingleQueue/PurgeQueue' |
| 14 | import { QUEUE_MESSAGE_TYPE } from '@/components/interfaces/Integrations/Queues/SingleQueue/Queue.utils' |
| 15 | import { QueueMessagesDataGrid } from '@/components/interfaces/Integrations/Queues/SingleQueue/QueueDataGrid' |
| 16 | import { QueueFilters } from '@/components/interfaces/Integrations/Queues/SingleQueue/QueueFilters' |
| 17 | import { QueueSettings } from '@/components/interfaces/Integrations/Queues/SingleQueue/QueueSettings' |
| 18 | import { SendMessageModal } from '@/components/interfaces/Integrations/Queues/SingleQueue/SendMessageModal' |
| 19 | import { Markdown } from '@/components/interfaces/Markdown' |
| 20 | import { ButtonTooltip } from '@/components/ui/ButtonTooltip' |
| 21 | import { useDatabasePoliciesQuery } from '@/data/database-policies/database-policies-query' |
| 22 | import { useQueueMessagesInfiniteQuery } from '@/data/database-queues/database-queue-messages-infinite-query' |
| 23 | import { useQueuesExposePostgrestStatusQuery } from '@/data/database-queues/database-queues-expose-postgrest-status-query' |
| 24 | import { useTableUpdateMutation } from '@/data/tables/table-update-mutation' |
| 25 | import { useTablesQuery } from '@/data/tables/tables-query' |
| 26 | import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject' |
| 27 | |
| 28 | export 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={` |
| 222 | 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. |
| 223 | |
| 224 | 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).`} |
| 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 | } |