diff --git a/apps/studio/components/interfaces/Integrations/Queues/CreateQueueSheet.tsx b/apps/studio/components/interfaces/Integrations/Queues/CreateQueueSheet.tsx deleted file mode 100644 index e877dc3243f..00000000000 --- a/apps/studio/components/interfaces/Integrations/Queues/CreateQueueSheet.tsx +++ /dev/null @@ -1,365 +0,0 @@ -import { zodResolver } from '@hookform/resolvers/zod' -import { EnableExtensionModal } from 'components/interfaces/Database/Extensions/EnableExtensionModal' -import { Markdown } from 'components/interfaces/Markdown' -import { DiscardChangesConfirmationDialog } from 'components/ui-patterns/Dialogs/DiscardChangesConfirmationDialog' -import { useDatabaseQueueCreateMutation } from 'data/database-queues/database-queues-create-mutation' -import { useQueuesExposePostgrestStatusQuery } from 'data/database-queues/database-queues-expose-postgrest-status-query' -import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' -import { useConfirmOnClose } from 'hooks/ui/useConfirmOnClose' -import { useRouter } from 'next/router' -import { useEffect, useState } from 'react' -import { SubmitHandler, useForm } from 'react-hook-form' -import { toast } from 'sonner' -import { - Badge, - Button, - Checkbox_Shadcn_, - Form_Shadcn_, - FormControl_Shadcn_, - FormField_Shadcn_, - Input, - Input_Shadcn_, - RadioGroupStacked, - RadioGroupStackedItem, - Separator, - Sheet, - SheetContent, - SheetFooter, - SheetHeader, - SheetSection, - SheetTitle, -} from 'ui' -import { Admonition } from 'ui-patterns' -import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' -import z from 'zod' - -import { QUEUE_TYPES } from './Queues.constants' -import { QueryNameSchema } from './Queues.utils' -import { usePgPartmanStatus } from './usePgPartmanStatus' - -export interface CreateQueueSheetProps { - visible: boolean - onClose: () => void -} - -const normalQueueSchema = z.object({ - type: z.literal('basic'), -}) - -const partitionedQueueSchema = z.object({ - type: z.literal('partitioned'), - partitionInterval: z.coerce.number(), - retentionInterval: z.coerce.number(), -}) - -const unloggedQueueSchema = z.object({ - type: z.literal('unlogged'), -}) - -const FormSchema = z.object({ - name: QueryNameSchema, - enableRls: z.boolean(), - values: z.discriminatedUnion('type', [ - normalQueueSchema, - partitionedQueueSchema, - unloggedQueueSchema, - ]), -}) - -export type CreateQueueForm = z.infer -export type QueueType = CreateQueueForm['values'] - -const FORM_ID = 'create-queue-sidepanel' - -export const CreateQueueSheet = ({ visible, onClose }: CreateQueueSheetProps) => { - const router = useRouter() - const { data: project } = useSelectedProjectQuery() - - const { data: isExposed } = useQueuesExposePostgrestStatusQuery({ - projectRef: project?.ref, - connectionString: project?.connectionString, - }) - - const { mutate: createQueue, isPending } = useDatabaseQueueCreateMutation() - - const { - pgPartmanExtension, - isAvailable: pgPartmanAvailable, - isInstalled: pgPartmanInstalled, - } = usePgPartmanStatus() - - const defaultValues: CreateQueueForm = pgPartmanInstalled - ? { - name: '', - enableRls: true, - values: { type: 'partitioned', partitionInterval: 0, retentionInterval: 0 }, - } - : { name: '', enableRls: true, values: { type: 'basic' } } - - const form = useForm({ - resolver: zodResolver(FormSchema), - defaultValues, - }) - - useEffect(() => { - if (visible) { - form.reset(defaultValues) - } - }, [ - form, - pgPartmanInstalled, - // end of stable references - visible, - ]) - - const checkIsDirty = () => form.formState.isDirty - - const { confirmOnClose, handleOpenChange, modalProps } = useConfirmOnClose({ - checkIsDirty, - onClose, - }) - - const onSubmit: SubmitHandler = async ({ name, enableRls, values }) => { - createQueue( - { - projectRef: project!.ref, - connectionString: project?.connectionString, - name, - enableRls, - type: values.type, - configuration: - values.type === 'partitioned' - ? { - partitionInterval: values.partitionInterval, - retentionInterval: values.retentionInterval, - } - : undefined, - }, - { - onSuccess: () => { - toast.success(`Successfully created queue ${name}`) - router.push(`/project/${project?.ref}/integrations/queues/queues/${name}`) - onClose() - }, - } - ) - } - - const [showEnableExtensionModal, setShowEnableExtensionModal] = useState(false) - - const queueType = form.watch('values.type') - - return ( - - -
- - Create a new queue - - -
- -
- - ( - - - - - - Must be all lowercase letters - - - )} - /> - - - - ( - - - - {QUEUE_TYPES.filter( - (definition) => - definition.value !== 'partitioned' || pgPartmanInstalled - ).map((definition) => { - const isPartitioned = definition.value === 'partitioned' - - return ( - -
-
{definition.icon}
-
-
-

- {definition.label} -

- {isPartitioned && ( - Recommended - )} -
-

- {isPartitioned - ? 'Automatically manages data retention and improves performance for high-volume queues via pg_partman.' - : definition.description} -

-
-
-
- ) - })} -
-
-
- )} - /> - {pgPartmanAvailable && !pgPartmanInstalled && ( - - - - )} -
- - {queueType === 'partitioned' && ( - <> - - ( - - ms

} - /> -
- )} - /> - ( - - ms

} - /> -
- )} - /> -
- - - )} - - ( - -

Enable Row Level Security (RLS)

- Recommended -
- } - description="Restrict access to your queue by enabling RLS and writing Postgres policies to control access for each role." - > - - - - - )} - /> - {!isExposed ? ( - - - - ) : ( - - )} - - - -
- - - - - - - {pgPartmanExtension && !pgPartmanInstalled && ( - setShowEnableExtensionModal(false)} - /> - )} -
-
- ) -} diff --git a/apps/studio/components/interfaces/Integrations/Queues/CreateQueueSheet/CreateQueueSheet.schema.ts b/apps/studio/components/interfaces/Integrations/Queues/CreateQueueSheet/CreateQueueSheet.schema.ts new file mode 100644 index 00000000000..393c244061b --- /dev/null +++ b/apps/studio/components/interfaces/Integrations/Queues/CreateQueueSheet/CreateQueueSheet.schema.ts @@ -0,0 +1,30 @@ +import z from 'zod' + +import { QueryNameSchema } from '../Queues.utils' + +const normalQueueSchema = z.object({ + type: z.literal('basic'), +}) + +const partitionedQueueSchema = z.object({ + type: z.literal('partitioned'), + partitionInterval: z.coerce.number(), + retentionInterval: z.coerce.number(), +}) + +const unloggedQueueSchema = z.object({ + type: z.literal('unlogged'), +}) + +export const FormSchema = z.object({ + name: QueryNameSchema, + enableRls: z.boolean(), + values: z.discriminatedUnion('type', [ + normalQueueSchema, + partitionedQueueSchema, + unloggedQueueSchema, + ]), +}) + +export type CreateQueueForm = z.infer +export type QueueType = CreateQueueForm['values'] diff --git a/apps/studio/components/interfaces/Integrations/Queues/CreateQueueSheet/CreateQueueSheet.tsx b/apps/studio/components/interfaces/Integrations/Queues/CreateQueueSheet/CreateQueueSheet.tsx new file mode 100644 index 00000000000..ca82965422c --- /dev/null +++ b/apps/studio/components/interfaces/Integrations/Queues/CreateQueueSheet/CreateQueueSheet.tsx @@ -0,0 +1,149 @@ +import { zodResolver } from '@hookform/resolvers/zod' +import { DiscardChangesConfirmationDialog } from 'components/ui-patterns/Dialogs/DiscardChangesConfirmationDialog' +import { useDatabaseQueueCreateMutation } from 'data/database-queues/database-queues-create-mutation' +import { useQueuesExposePostgrestStatusQuery } from 'data/database-queues/database-queues-expose-postgrest-status-query' +import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' +import { useConfirmOnClose } from 'hooks/ui/useConfirmOnClose' +import { useRouter } from 'next/router' +import { useEffect, useMemo } from 'react' +import { SubmitHandler, useForm } from 'react-hook-form' +import { toast } from 'sonner' +import { + Button, + Form_Shadcn_, + Separator, + Sheet, + SheetContent, + SheetFooter, + SheetHeader, + SheetTitle, +} from 'ui' + +import { usePgPartmanStatus } from '../usePgPartmanStatus' +import { CreateQueueForm, FormSchema } from './CreateQueueSheet.schema' +import { PartitionConfigFields } from './PartitionConfigFields' +import { PgPartmanCallout } from './PgPartmanCallout' +import { QueueNameField } from './QueueNameField' +import { QueueTypeSelector } from './QueueTypeSelector' +import { RlsSection } from './RlsSection' + +export interface CreateQueueSheetProps { + visible: boolean + onClose: () => void +} + +const FORM_ID = 'create-queue-sidepanel' + +export const CreateQueueSheet = ({ visible, onClose }: CreateQueueSheetProps) => { + const router = useRouter() + const { data: project } = useSelectedProjectQuery() + + const { data: isExposed } = useQueuesExposePostgrestStatusQuery({ + projectRef: project?.ref, + connectionString: project?.connectionString, + }) + + const { mutate: createQueue, isPending } = useDatabaseQueueCreateMutation() + const { isInstalled: pgPartmanInstalled } = usePgPartmanStatus() + + const defaultValues: CreateQueueForm = useMemo( + () => + pgPartmanInstalled + ? { + name: '', + enableRls: true, + values: { type: 'partitioned', partitionInterval: 0, retentionInterval: 0 }, + } + : { name: '', enableRls: true, values: { type: 'basic' } }, + [pgPartmanInstalled] + ) + + const form = useForm({ + resolver: zodResolver(FormSchema), + defaultValues, + }) + + useEffect(() => { + if (visible) { + form.reset(defaultValues) + } + }, [form, defaultValues, visible]) + + const checkIsDirty = () => form.formState.isDirty + + const { confirmOnClose, handleOpenChange, modalProps } = useConfirmOnClose({ + checkIsDirty, + onClose, + }) + + const onSubmit: SubmitHandler = async ({ name, enableRls, values }) => { + createQueue( + { + projectRef: project!.ref, + connectionString: project?.connectionString, + name, + enableRls, + type: values.type, + configuration: + values.type === 'partitioned' + ? { + partitionInterval: values.partitionInterval, + retentionInterval: values.retentionInterval, + } + : undefined, + }, + { + onSuccess: () => { + toast.success(`Successfully created queue ${name}`) + router.push(`/project/${project?.ref}/integrations/queues/queues/${name}`) + onClose() + }, + } + ) + } + + return ( + + +
+ + Create a new queue + + +
+ +
+ + + + + + + + +
+
+ + + + +
+ +
+
+ ) +} diff --git a/apps/studio/components/interfaces/Integrations/Queues/CreateQueueSheet/PartitionConfigFields.tsx b/apps/studio/components/interfaces/Integrations/Queues/CreateQueueSheet/PartitionConfigFields.tsx new file mode 100644 index 00000000000..21b2314ecea --- /dev/null +++ b/apps/studio/components/interfaces/Integrations/Queues/CreateQueueSheet/PartitionConfigFields.tsx @@ -0,0 +1,47 @@ +import { UseFormReturn } from 'react-hook-form' +import { FormField_Shadcn_, Input, Separator, SheetSection } from 'ui' +import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' + +import { CreateQueueForm } from './CreateQueueSheet.schema' + +export function PartitionConfigFields({ form }: { form: UseFormReturn }) { + const queueType = form.watch('values.type') + + if (queueType !== 'partitioned') return null + + return ( + <> + + ( + + ms

} + /> +
+ )} + /> + ( + + ms

} + /> +
+ )} + /> +
+ + + ) +} diff --git a/apps/studio/components/interfaces/Integrations/Queues/CreateQueueSheet/PgPartmanCallout.tsx b/apps/studio/components/interfaces/Integrations/Queues/CreateQueueSheet/PgPartmanCallout.tsx new file mode 100644 index 00000000000..4e7e54f35d8 --- /dev/null +++ b/apps/studio/components/interfaces/Integrations/Queues/CreateQueueSheet/PgPartmanCallout.tsx @@ -0,0 +1,39 @@ +import { EnableExtensionModal } from 'components/interfaces/Database/Extensions/EnableExtensionModal' +import { useState } from 'react' +import { Button } from 'ui' +import { Admonition } from 'ui-patterns' + +import { usePgPartmanStatus } from '../usePgPartmanStatus' + +export function PgPartmanCallout() { + const { pgPartmanExtension, isAvailable, isInstalled } = usePgPartmanStatus() + const [showEnableModal, setShowEnableModal] = useState(false) + + if (!isAvailable || isInstalled) return null + + return ( +
+ + + + {pgPartmanExtension && ( + setShowEnableModal(false)} + /> + )} +
+ ) +} diff --git a/apps/studio/components/interfaces/Integrations/Queues/CreateQueueSheet/QueueNameField.tsx b/apps/studio/components/interfaces/Integrations/Queues/CreateQueueSheet/QueueNameField.tsx new file mode 100644 index 00000000000..8743be1e433 --- /dev/null +++ b/apps/studio/components/interfaces/Integrations/Queues/CreateQueueSheet/QueueNameField.tsx @@ -0,0 +1,26 @@ +import { UseFormReturn } from 'react-hook-form' +import { FormControl_Shadcn_, FormField_Shadcn_, Input_Shadcn_, SheetSection } from 'ui' +import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' + +import { CreateQueueForm } from './CreateQueueSheet.schema' + +export function QueueNameField({ form }: { form: UseFormReturn }) { + return ( + + ( + + + + + + Must be all lowercase letters + + + )} + /> + + ) +} diff --git a/apps/studio/components/interfaces/Integrations/Queues/CreateQueueSheet/QueueTypeSelector.tsx b/apps/studio/components/interfaces/Integrations/Queues/CreateQueueSheet/QueueTypeSelector.tsx new file mode 100644 index 00000000000..2af655d10f4 --- /dev/null +++ b/apps/studio/components/interfaces/Integrations/Queues/CreateQueueSheet/QueueTypeSelector.tsx @@ -0,0 +1,71 @@ +import { UseFormReturn } from 'react-hook-form' +import { + Badge, + FormControl_Shadcn_, + FormField_Shadcn_, + RadioGroupStacked, + RadioGroupStackedItem, + SheetSection, +} from 'ui' +import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' + +import { QUEUE_TYPES } from '../Queues.constants' +import { usePgPartmanStatus } from '../usePgPartmanStatus' +import { CreateQueueForm } from './CreateQueueSheet.schema' + +export function QueueTypeSelector({ form }: { form: UseFormReturn }) { + const { isInstalled } = usePgPartmanStatus() + + return ( + + ( + + + + {QUEUE_TYPES.filter( + (definition) => definition.value !== 'partitioned' || isInstalled + ).map((definition) => { + const isPartitioned = definition.value === 'partitioned' + + return ( + +
+
{definition.icon}
+
+
+

{definition.label}

+ {isPartitioned && Recommended} +
+

+ {isPartitioned + ? 'Automatically manages data retention and improves performance for high-volume queues via pg_partman.' + : definition.description} +

+
+
+
+ ) + })} +
+
+
+ )} + /> +
+ ) +} diff --git a/apps/studio/components/interfaces/Integrations/Queues/CreateQueueSheet/RlsSection.tsx b/apps/studio/components/interfaces/Integrations/Queues/CreateQueueSheet/RlsSection.tsx new file mode 100644 index 00000000000..5039b804726 --- /dev/null +++ b/apps/studio/components/interfaces/Integrations/Queues/CreateQueueSheet/RlsSection.tsx @@ -0,0 +1,64 @@ +import { Markdown } from 'components/interfaces/Markdown' +import { UseFormReturn } from 'react-hook-form' +import { Badge, Checkbox_Shadcn_, FormControl_Shadcn_, FormField_Shadcn_, SheetSection } from 'ui' +import { Admonition } from 'ui-patterns' +import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' + +import { CreateQueueForm } from './CreateQueueSheet.schema' + +export function RlsSection({ + form, + isExposed, + projectRef, +}: { + form: UseFormReturn + isExposed: boolean | undefined + projectRef: string | undefined +}) { + return ( + + ( + +

Enable Row Level Security (RLS)

+ Recommended + + } + description="Restrict access to your queue by enabling RLS and writing Postgres policies to control access for each role." + > + + + +
+ )} + /> + {!isExposed ? ( + + + + ) : ( + + )} +
+ ) +} diff --git a/apps/studio/components/interfaces/Integrations/Queues/CreateQueueSheet/index.ts b/apps/studio/components/interfaces/Integrations/Queues/CreateQueueSheet/index.ts new file mode 100644 index 00000000000..7ce513a3e00 --- /dev/null +++ b/apps/studio/components/interfaces/Integrations/Queues/CreateQueueSheet/index.ts @@ -0,0 +1,3 @@ +export { CreateQueueSheet } from './CreateQueueSheet' +export type { CreateQueueSheetProps } from './CreateQueueSheet' +export type { CreateQueueForm, QueueType } from './CreateQueueSheet.schema'