chore: migrate Integrations Modal to Dialog (#46380)

## Problem

We still use the deprecated `Modal` for:
- Deleting a wrapper
- Updating a vault secret
- Sending a queue message

## Solution

- use `Dialog` instead

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Refactor**
* Replaced several modal dialogs with updated dialog/alert patterns for
sending messages and confirming deletions, improving visual consistency
and content structure.
* **Bug Fixes**
* Prevent duplicate/accidental actions by disabling buttons and showing
loading states during pending operations; confirmation dialogs now
display relevant item details and close on success.

<!-- review_stack_entry_start -->

[![Review Change
Stack](https://storage.googleapis.com/coderabbit_public_assets/review-stack-in-coderabbit-ui.svg)](https://app.coderabbit.ai/change-stack/supabase/supabase/pull/46380?utm_source=github_walkthrough&utm_medium=github&utm_campaign=change_stack)

<!-- review_stack_entry_end -->
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
Gildas Garcia
2026-05-27 14:06:52 +02:00
committed by GitHub
parent e26303cf9c
commit 9155357d82
3 changed files with 147 additions and 115 deletions

View File

@@ -4,6 +4,14 @@ import { useEffect } from 'react'
import { SubmitHandler, useForm } from 'react-hook-form'
import { toast } from 'sonner'
import {
Button,
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogSection,
DialogSectionSeparator,
DialogTitle,
Form,
FormControl,
FormField,
@@ -11,7 +19,6 @@ import {
InputGroupAddon,
InputGroupInput,
InputGroupText,
Modal,
} from 'ui'
import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
import z from 'zod'
@@ -31,6 +38,7 @@ const FormSchema = z.object({
(val) => {
try {
JSON.parse(val)
return true
} catch {
return false
}
@@ -80,69 +88,77 @@ export const SendMessageModal = ({ visible, onClose }: SendMessageModalProps) =>
}, [visible])
return (
<Modal
size="medium"
alignFooter="right"
header="Add a message to the queue"
visible={visible}
loading={isPending}
onCancel={onClose}
confirmText="Add"
onConfirm={() => {
const values = form.getValues()
onSubmit(values)
}}
>
<Modal.Content className="flex flex-col gap-y-4">
<Form {...form}>
<form
id={FORM_ID}
className="grow overflow-auto gap-2 flex flex-col"
onSubmit={form.handleSubmit(onSubmit)}
<Dialog open={visible} onOpenChange={onClose}>
<DialogContent size="medium">
<DialogHeader>
<DialogTitle>Add a message to the queue</DialogTitle>
</DialogHeader>
<DialogSectionSeparator />
<DialogSection className="flex flex-col gap-y-4">
<Form {...form}>
<form
id={FORM_ID}
className="grow overflow-auto gap-2 flex flex-col"
onSubmit={form.handleSubmit(onSubmit)}
>
<FormField
control={form.control}
name="delay"
render={({ field: { ref, ...rest } }) => (
<FormItemLayout
label="Delay"
layout="vertical"
className="gap-1"
description="Time in seconds before the message becomes available for reading."
>
<FormControl>
<InputGroup>
<InputGroupInput {...rest} type="number" placeholder="1" />
<InputGroupAddon align="inline-end">
<InputGroupText>sec</InputGroupText>
</InputGroupAddon>
</InputGroup>
</FormControl>
</FormItemLayout>
)}
/>
<FormField
control={form.control}
name="payload"
render={({ field }) => (
<FormItemLayout label="Message payload" layout="vertical" className="gap-1">
<FormControl>
<CodeEditor
id="message-payload"
language="json"
autofocus={false}
className="mb-0! h-32 overflow-hidden rounded-sm border"
onInputChange={(e: string | undefined) => field.onChange(e)}
options={{ wordWrap: 'off', contextmenu: false }}
value={field.value}
/>
</FormControl>
</FormItemLayout>
)}
/>
</form>
</Form>
</DialogSection>
<DialogFooter>
<Button type="default" onClick={onClose} disabled={isPending}>
Cancel
</Button>
<Button
type="primary"
htmlType="submit"
form={FORM_ID}
disabled={isPending}
loading={isPending}
>
<FormField
control={form.control}
name="delay"
render={({ field: { ref, ...rest } }) => (
<FormItemLayout
label="Delay"
layout="vertical"
className="gap-1"
description="Time in seconds before the message becomes available for reading."
>
<FormControl>
<InputGroup>
<InputGroupInput {...rest} type="number" placeholder="1" />
<InputGroupAddon align="inline-end">
<InputGroupText>sec</InputGroupText>
</InputGroupAddon>
</InputGroup>
</FormControl>
</FormItemLayout>
)}
/>
<FormField
control={form.control}
name="payload"
render={({ field }) => (
<FormItemLayout label="Message payload" layout="vertical" className="gap-1">
<FormControl>
<CodeEditor
id="message-payload"
language="json"
autofocus={false}
className="mb-0! h-32 overflow-hidden rounded-sm border"
onInputChange={(e: string | undefined) => field.onChange(e)}
options={{ wordWrap: 'off', contextmenu: false }}
value={field.value}
/>
</FormControl>
</FormItemLayout>
)}
/>
</form>
</Form>
</Modal.Content>
</Modal>
Add
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
)
}

View File

@@ -1,7 +1,16 @@
import { parseAsString, useQueryState } from 'nuqs'
import { useEffect } from 'react'
import { toast } from 'sonner'
import { Modal } from 'ui'
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from 'ui'
import { useVaultSecretDeleteMutation } from '@/data/vault/vault-secret-delete-mutation'
import { useVaultSecretsQuery } from '@/data/vault/vault-secrets-query'
@@ -18,11 +27,7 @@ export const DeleteSecretModal = () => {
const [secretIdToDelete, setSelectedSecretToDelete] = useQueryState('delete', parseAsString)
const selectedSecret = secrets.find((secret) => secret.id === secretIdToDelete)
const {
mutate: deleteSecret,
isPending: isDeleting,
isSuccess: isSuccessDelete,
} = useVaultSecretDeleteMutation({
const { mutateAsync: deleteSecret, isSuccess: isSuccessDelete } = useVaultSecretDeleteMutation({
onSuccess: () => {
toast.success(`Successfully deleted secret ${selectedSecret?.name}`)
setSelectedSecretToDelete(null)
@@ -51,27 +56,30 @@ export const DeleteSecretModal = () => {
}, [isSuccess, isSuccessDelete, secretIdToDelete, selectedSecret, setSelectedSecretToDelete])
return (
<Modal
size="small"
variant="danger"
alignFooter="right"
header="Confirm to delete secret"
visible={!!selectedSecret}
loading={isDeleting}
onCancel={() => setSelectedSecretToDelete(null)}
onConfirm={onConfirmDeleteSecret}
>
<Modal.Content className="space-y-4">
<p className="text-sm">
The following secret will be permanently removed and cannot be recovered. Are you sure?
</p>
<div className="space-y-1">
<p className="text-sm">{selectedSecret?.description}</p>
<p className="text-sm text-foreground-light">
ID: <code className="text-code-inline">{selectedSecret?.id}</code>
</p>
</div>
</Modal.Content>
</Modal>
<AlertDialog open={!!selectedSecret} onOpenChange={() => setSelectedSecretToDelete(null)}>
<AlertDialogContent size="small">
<AlertDialogHeader>
<AlertDialogTitle>Confirm to delete secret</AlertDialogTitle>
<AlertDialogDescription className="space-y-4">
<p className="text-sm">
The following secret will be permanently removed and cannot be recovered. Are you
sure?
</p>
<div className="space-y-1">
<p className="text-sm">{selectedSecret?.description}</p>
<p className="text-sm text-foreground-light">
ID: <code className="text-code-inline">{selectedSecret?.id}</code>
</p>
</div>
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction variant="danger" onClick={onConfirmDeleteSecret}>
Confirm
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
)
}

View File

@@ -2,7 +2,16 @@ import { useParams } from 'common'
import { parseAsString, useQueryState } from 'nuqs'
import { useEffect, useMemo } from 'react'
import { toast } from 'sonner'
import { Modal } from 'ui'
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from 'ui'
import { INTEGRATIONS } from '../Landing/Integrations.constants'
import { getWrapperMetaForWrapper, wrapperMetaComparator } from './Wrappers.utils'
@@ -34,11 +43,7 @@ export const DeleteWrapperModal = () => {
)
const selectedWrapper = wrappers.find((x) => x.id.toString() === selectedWrapperIdToDelete)
const {
mutate: deleteFDW,
isPending: isDeleting,
isSuccess: isSuccessDelete,
} = useFDWDeleteMutation({
const { mutateAsync: deleteFDW, isSuccess: isSuccessDelete } = useFDWDeleteMutation({
onSuccess: () => {
toast.success(`Successfully disabled ${selectedWrapper?.name} foreign data wrapper`)
setSelectedWrapperToDelete(null)
@@ -51,7 +56,7 @@ export const DeleteWrapperModal = () => {
if (!selectedWrapper) return console.error('Wrapper is required')
if (!wrapperMeta) return console.error('Wrapper meta is required')
deleteFDW({
await deleteFDW({
projectRef: project?.ref,
connectionString: project?.connectionString,
wrapper: selectedWrapper,
@@ -73,22 +78,25 @@ export const DeleteWrapperModal = () => {
])
return (
<Modal
size="medium"
variant="danger"
alignFooter="right"
loading={isDeleting}
visible={selectedWrapper !== undefined}
onCancel={() => setSelectedWrapperToDelete(null)}
onConfirm={() => onConfirmDelete()}
header={`Confirm to disable ${selectedWrapper?.name}`}
<AlertDialog
open={selectedWrapper !== undefined}
onOpenChange={() => setSelectedWrapperToDelete(null)}
>
<Modal.Content>
<p className="text-sm">
Are you sure you want to disable {selectedWrapper?.name}? This will also remove all tables
created with this wrapper.
</p>
</Modal.Content>
</Modal>
<AlertDialogContent size="medium">
<AlertDialogHeader>
<AlertDialogTitle>{`Confirm to disable ${selectedWrapper?.name}`}</AlertDialogTitle>
<AlertDialogDescription>
Are you sure you want to disable {selectedWrapper?.name}? This will also remove all
tables created with this wrapper.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction variant="danger" onClick={onConfirmDelete}>
Confirm
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
)
}