QueuesSettings.tsx381 lines · main
1import { zodResolver } from '@hookform/resolvers/zod'
2import { QUEUES_SCHEMA } from '@supabase/pg-meta'
3import { PermissionAction } from '@supabase/shared-types/out/constants'
4import { useEffect, useState } from 'react'
5import { useForm } from 'react-hook-form'
6import { toast } from 'sonner'
7import { Button, Form, FormControl, FormField, FormItem, Switch } from 'ui'
8import { Admonition } from 'ui-patterns'
9import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal'
10import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
11import { z } from 'zod'
12
13import { ConstrainedIntegrationTabScaffold } from '@/components/interfaces/Integrations/ConstrainedIntegrationTabScaffold'
14import { DocsButton } from '@/components/ui/DocsButton'
15import { FormHeader } from '@/components/ui/Forms/FormHeader'
16import {
17 FormPanelContainer,
18 FormPanelContent,
19 FormPanelFooter,
20} from '@/components/ui/Forms/FormPanel'
21import { InlineLink } from '@/components/ui/InlineLink'
22import { useProjectPostgrestConfigQuery } from '@/data/config/project-postgrest-config-query'
23import { useProjectPostgrestConfigUpdateMutation } from '@/data/config/project-postgrest-config-update-mutation'
24import { useQueuesExposePostgrestStatusQuery } from '@/data/database-queues/database-queues-expose-postgrest-status-query'
25import { useQueuesQuery } from '@/data/database-queues/database-queues-query'
26import { useDatabaseQueueToggleExposeMutation } from '@/data/database-queues/database-queues-toggle-postgrest-mutation'
27import { useDatabaseQueuesVersionQuery } from '@/data/database-queues/database-queues-version-query'
28import { useTableUpdateMutation } from '@/data/tables/table-update-mutation'
29import { useTablesQuery } from '@/data/tables/tables-query'
30import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions'
31import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
32import { DOCS_URL, IS_PLATFORM } from '@/lib/constants'
33
34export const QueuesSettings = () => {
35 const { data: project } = useSelectedProjectQuery()
36 const { can: canUpdatePostgrestConfig } = useAsyncCheckPermissions(
37 PermissionAction.UPDATE,
38 'custom_config_postgrest'
39 )
40 const [isToggling, setIsToggling] = useState(false)
41 const [rlsConfirmModalOpen, setRlsConfirmModalOpen] = useState(false)
42 const [isUpdatingRls, setIsUpdatingRls] = useState(false)
43
44 const formSchema = z.object({ enable: z.boolean() })
45 const form = useForm<z.infer<typeof formSchema>>({
46 resolver: zodResolver(formSchema as any),
47 mode: 'onChange',
48 defaultValues: { enable: false },
49 })
50 const { formState } = form
51 const { enable } = form.watch()
52
53 const { data: queueTables } = useTablesQuery({
54 projectRef: project?.ref,
55 connectionString: project?.connectionString,
56 schema: 'pgmq',
57 })
58 const tablesWithoutRLS =
59 queueTables?.filter((x) => x.name.startsWith('q_') && !x.rls_enabled) ?? []
60
61 // pgmq lowercases queue names when building q_/a_ relations, but pgmq.meta keeps
62 // the original casing. Look up each relname in list_queues() so we render the
63 // user-provided name rather than the lowercased relname slice.
64 const { data: queues } = useQueuesQuery({
65 projectRef: project?.ref,
66 connectionString: project?.connectionString,
67 })
68 const queueDisplayName = (relname: string) => {
69 const stripped = relname.slice(2)
70 return queues?.find((q) => q.queue_name.toLowerCase() === stripped)?.queue_name ?? stripped
71 }
72
73 const { data: config, error: configError } = useProjectPostgrestConfigQuery({
74 projectRef: project?.ref,
75 })
76
77 const {
78 data: isExposed,
79 isSuccess,
80 isPending: isLoading,
81 } = useQueuesExposePostgrestStatusQuery({
82 projectRef: project?.ref,
83 connectionString: project?.connectionString,
84 })
85 const schemas = config?.db_schema.replace(/ /g, '').split(',') ?? []
86
87 const { data: pgmqVersion } = useDatabaseQueuesVersionQuery({
88 projectRef: project?.ref,
89 connectionString: project?.connectionString,
90 })
91
92 const { mutateAsync: updateTable } = useTableUpdateMutation()
93
94 const onPostgrestConfigUpdateSuccess = () => {
95 if (enable) {
96 toast.success('Queues can now be managed through client libraries or PostgREST endpoints!')
97 } else {
98 toast.success(
99 'Queues can no longer be managed through client libraries or PostgREST endpoints'
100 )
101 }
102 setIsToggling(false)
103 form.reset({ enable })
104 }
105
106 const { mutate: updatePostgrestConfig } = useProjectPostgrestConfigUpdateMutation({
107 onSuccess: onPostgrestConfigUpdateSuccess,
108 onError: (error) => {
109 setIsToggling(false)
110 toast.error(`Failed to toggle queue exposure via PostgREST: ${error.message}`)
111 },
112 })
113
114 const { mutate: toggleExposeQueuePostgrest } = useDatabaseQueueToggleExposeMutation({
115 onSuccess: (_, values) => {
116 if (!IS_PLATFORM) return onPostgrestConfigUpdateSuccess()
117 if (project && config) {
118 if (values.enable) {
119 const updatedSchemas = schemas.concat([QUEUES_SCHEMA])
120 updatePostgrestConfig({
121 projectRef: project?.ref,
122 dbSchema: updatedSchemas.join(', '),
123 maxRows: config.max_rows,
124 dbExtraSearchPath: config.db_extra_search_path,
125 dbPool: config.db_pool,
126 })
127 } else {
128 const updatedSchemas = schemas.filter((x) => x !== QUEUES_SCHEMA)
129 updatePostgrestConfig({
130 projectRef: project?.ref,
131 dbSchema: updatedSchemas.join(', '),
132 maxRows: config.max_rows,
133 dbExtraSearchPath: config.db_extra_search_path,
134 dbPool: config.db_pool,
135 })
136 }
137 }
138 },
139 onError: (error) => {
140 setIsToggling(false)
141 toast.error(`Failed to toggle queue exposure via PostgREST: ${error.message}`)
142 },
143 })
144
145 const onToggleRLS = async () => {
146 if (!project) return console.error('Project is required')
147 setIsUpdatingRls(true)
148 try {
149 await Promise.all(
150 tablesWithoutRLS.map((x) =>
151 updateTable({
152 projectRef: project?.ref,
153 connectionString: project?.connectionString,
154 id: x.id,
155 name: x.name,
156 schema: x.schema,
157 payload: { id: x.id, rls_enabled: true },
158 })
159 )
160 )
161 toast.success(
162 `Successfully enabled RLS on ${tablesWithoutRLS.length === 1 ? tablesWithoutRLS[0].name : `${tablesWithoutRLS.length} queue${tablesWithoutRLS.length > 1 ? 's' : ''}`} `
163 )
164 setRlsConfirmModalOpen(false)
165 } catch (error: any) {
166 setIsUpdatingRls(false)
167 toast.error(`Failed to enable RLS on queues: ${error.message}`)
168 }
169 }
170
171 const onSubmit = async (values: z.infer<typeof formSchema>) => {
172 if (!project) return console.error('Project is required')
173 if (configError) {
174 return toast.error(
175 `Failed to toggle queue exposure via PostgREST: Unable to retrieve PostgREST configuration (${configError.message})`
176 )
177 }
178 if (!pgmqVersion) {
179 return toast.error('Unable to retrieve PGMQ version. Please try again later.')
180 }
181
182 setIsToggling(true)
183 toggleExposeQueuePostgrest({
184 projectRef: project.ref,
185 connectionString: project.connectionString,
186 enable: values.enable,
187 pgmqVersion,
188 })
189 }
190
191 useEffect(() => {
192 if (isSuccess) form.reset({ enable: isExposed })
193 }, [isSuccess])
194
195 return (
196 <>
197 <ConstrainedIntegrationTabScaffold className="flex flex-col gap-y-4">
198 <FormHeader
199 className="mb-0"
200 title="Settings"
201 description="Manage your queues via any client library or Data APIs endpoints"
202 />
203 <Form {...form}>
204 <form id="pgmq-postgrest" onSubmit={form.handleSubmit(onSubmit)}>
205 <FormPanelContainer>
206 <FormPanelContent className="px-8 py-8">
207 <FormField
208 control={form.control}
209 name="enable"
210 render={({ field }) => (
211 <FormItem className="w-full">
212 <FormItemLayout
213 className="w-full"
214 layout="flex"
215 label="Expose Queues via PostgREST"
216 description={
217 <>
218 <p className="max-w-2xl">
219 When enabled, you will be able to use the following functions from the{' '}
220 <code className="text-code-inline">{QUEUES_SCHEMA}</code> schema to
221 manage your queues via any Briven client library or PostgREST
222 endpoints:
223 </p>
224 <p className="mt-2">
225 <code className="text-code-inline">send</code>,{' '}
226 <code className="text-code-inline">send_batch</code>,{' '}
227 <code className="text-code-inline">read</code>,{' '}
228 <code className="text-code-inline">pop</code>,
229 <code className="text-code-inline">archive</code>, and{' '}
230 <code className="text-code-inline">delete</code>
231 </p>
232 {!IS_PLATFORM ? (
233 <div className="mt-6 max-w-2xl">
234 When running Briven locally with the CLI or self-hosting using
235 Docker Compose, you also need to update your configuration to expose
236 the <code className="text-code-inline">{QUEUES_SCHEMA}</code>{' '}
237 schema.
238 <br />
239 <InlineLink
240 href={`${DOCS_URL}/guides/queues/expose-self-hosted-queues`}
241 >
242 Learn more
243 </InlineLink>
244 </div>
245 ) : null}
246 </>
247 }
248 >
249 <FormControl>
250 <Switch
251 name="enable"
252 size="large"
253 disabled={
254 isLoading || tablesWithoutRLS.length > 0 || !canUpdatePostgrestConfig
255 }
256 checked={field.value}
257 onCheckedChange={(value) => field.onChange(value)}
258 />
259 </FormControl>
260 </FormItemLayout>
261 {tablesWithoutRLS.length > 0 && (
262 <Admonition
263 type="default"
264 title="Existing Queues must have RLS enabled first before exposing via PostgREST"
265 className="mt-2"
266 >
267 <p className="m-0!">
268 Please ensure that the following {tablesWithoutRLS.length} queue
269 {tablesWithoutRLS.length > 1 ? 's' : ''} have RLS enabled in order to
270 prevent anonymous access.
271 </p>
272 <ul className="list-disc pl-6">
273 {tablesWithoutRLS.map((x) => {
274 return (
275 <li key={x.name}>
276 <code className="text-code-inline">
277 {queueDisplayName(x.name)}
278 </code>
279 </li>
280 )
281 })}
282 </ul>
283
284 <Button
285 type="default"
286 className="mt-3"
287 onClick={() => setRlsConfirmModalOpen(true)}
288 >
289 Enable RLS on{' '}
290 {tablesWithoutRLS.length === 1
291 ? queueDisplayName(tablesWithoutRLS[0].name)
292 : `${tablesWithoutRLS.length} queues`}
293 </Button>
294 </Admonition>
295 )}
296 {formState.dirtyFields.enable && field.value === true && (
297 <Admonition type="warning" className="mt-2">
298 <p>
299 Queues will be exposed and managed through the{' '}
300 <code className="text-code-inline">{QUEUES_SCHEMA}</code> schema
301 </p>
302 <p className="text-foreground-light">
303 Database functions will be created in the{' '}
304 <code className="text-code-inline">{QUEUES_SCHEMA}</code> schema upon
305 enabling. Call these functions via any Briven client library or
306 PostgREST endpoint to manage your queues. Permissions on individual
307 queues can also be further managed through privileges and row level
308 security (RLS).
309 </p>
310 </Admonition>
311 )}
312 {formState.dirtyFields.enable && field.value === false && (
313 <Admonition type="warning" className="mt-2">
314 <p>
315 The <code className="text-code-inline">{QUEUES_SCHEMA}</code> schema
316 will be removed once disabled
317 </p>
318 <p className="text-foreground-light">
319 Ensure that the database functions from the{' '}
320 <code className="text-code-inline">{QUEUES_SCHEMA}</code> schema are not
321 in use within your client applications before disabling.
322 </p>
323 </Admonition>
324 )}
325 </FormItem>
326 )}
327 />
328 </FormPanelContent>
329
330 <FormPanelFooter className="flex px-8 py-4 flex items-center justify-between">
331 <DocsButton
332 href={`${DOCS_URL}/guides/queues/quickstart#expose-queues-to-client-side-consumers`}
333 />
334 <div className="flex items-center gap-x-2">
335 <Button
336 type="default"
337 disabled={Object.keys(formState.dirtyFields).length === 0 || isToggling}
338 onClick={() => form.reset({ enable: false })}
339 >
340 Cancel
341 </Button>
342 <Button
343 type="primary"
344 htmlType="submit"
345 disabled={Object.keys(formState.dirtyFields).length === 0}
346 loading={isToggling}
347 >
348 Save changes
349 </Button>
350 </div>
351 </FormPanelFooter>
352 </FormPanelContainer>
353 </form>
354 </Form>
355 </ConstrainedIntegrationTabScaffold>
356
357 <ConfirmationModal
358 visible={rlsConfirmModalOpen}
359 title="Enable Row Level Security"
360 confirmLabel="Enable RLS"
361 confirmLabelLoading="Enabling RLS"
362 loading={isUpdatingRls}
363 onCancel={() => setRlsConfirmModalOpen(false)}
364 onConfirm={() => onToggleRLS()}
365 >
366 <p className="text-sm text-foreground-light">
367 Are you sure you want to enable Row Level Security for the following queues:
368 </p>
369 <ul className="list-disc pl-6">
370 {tablesWithoutRLS.map((x) => {
371 return (
372 <li key={x.id}>
373 <code className="text-code-inline">{queueDisplayName(x.name)}</code>
374 </li>
375 )
376 })}
377 </ul>
378 </ConfirmationModal>
379 </>
380 )
381}