DeleteConfirmationDialogs.tsx354 lines · main
1import { ExternalLink } from 'lucide-react'
2import Link from 'next/link'
3import { toast } from 'sonner'
4import { Alert, AlertDescription, AlertTitle, Button, Checkbox } from 'ui'
5import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal'
6
7import { useTableFilter } from '@/components/grid/hooks/useTableFilter'
8import type { SupaRow } from '@/components/grid/types'
9import { useDatabaseColumnDeleteMutation } from '@/data/database-columns/database-column-delete-mutation'
10import { TableLike } from '@/data/table-editor/table-editor-types'
11import { useTableRowDeleteAllMutation } from '@/data/table-rows/table-row-delete-all-mutation'
12import { useTableRowDeleteMutation } from '@/data/table-rows/table-row-delete-mutation'
13import { useTableRowTruncateMutation } from '@/data/table-rows/table-row-truncate-mutation'
14import { useTableDeleteMutation } from '@/data/tables/table-delete-mutation'
15import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
16import { useGetImpersonatedRoleState } from '@/state/role-impersonation-state'
17import { useTableEditorStateSnapshot } from '@/state/table-editor'
18
19export type DeleteConfirmationDialogsProps = {
20 selectedTable?: TableLike
21 onTableDeleted?: () => void
22}
23
24const DeleteConfirmationDialogs = ({
25 selectedTable,
26 onTableDeleted,
27}: DeleteConfirmationDialogsProps) => {
28 const { data: project } = useSelectedProjectQuery()
29 const snap = useTableEditorStateSnapshot()
30 const { filters, setFilters } = useTableFilter()
31
32 const removeDeletedColumnFromFiltersAndSorts = ({
33 columnName,
34 }: {
35 ref?: string
36 tableName?: string
37 schema?: string
38 columnName: string
39 }) => {
40 setFilters(filters.filter((filter) => filter.column !== columnName))
41 }
42
43 const { mutate: deleteColumn } = useDatabaseColumnDeleteMutation({
44 onSuccess: () => {
45 if (!(snap.confirmationDialog?.type === 'column')) return
46 const selectedColumnToDelete = snap.confirmationDialog.column
47 removeDeletedColumnFromFiltersAndSorts({ columnName: selectedColumnToDelete.name })
48 toast.success(`Successfully deleted column "${selectedColumnToDelete.name}"`)
49 },
50 onError: (error) => {
51 if (!(snap.confirmationDialog?.type === 'column')) return
52 const selectedColumnToDelete = snap.confirmationDialog.column
53 toast.error(`Failed to delete ${selectedColumnToDelete!.name}: ${error.message}`)
54 },
55 onSettled: () => {
56 snap.closeConfirmationDialog()
57 },
58 })
59 const { mutate: deleteTable } = useTableDeleteMutation({
60 onSuccess: async () => {
61 toast.success(`Successfully deleted table "${selectedTable?.name}"`)
62 onTableDeleted?.()
63 },
64 onError: (error) => {
65 toast.error(`Failed to delete ${selectedTable?.name}: ${error.message}`)
66 },
67 onSettled: () => {
68 snap.closeConfirmationDialog()
69 },
70 })
71
72 const { mutate: deleteRows, isPending: isDeletingRows } = useTableRowDeleteMutation({
73 onSuccess: () => {
74 if (snap.confirmationDialog?.type === 'row') {
75 snap.confirmationDialog.callback?.()
76 }
77 toast.success(`Successfully deleted selected row(s)`)
78 },
79 onSettled: () => {
80 snap.closeConfirmationDialog()
81 },
82 })
83
84 const { mutate: deleteAllRows, isPending: isDeletingAllRows } = useTableRowDeleteAllMutation({
85 onSuccess: () => {
86 if (snap.confirmationDialog?.type === 'row') {
87 snap.confirmationDialog.callback?.()
88 }
89 toast.success(`Successfully deleted selected rows`)
90 },
91 onError: (error) => {
92 toast.error(`Failed to delete rows: ${error.message}`)
93 },
94 onSettled: () => {
95 snap.closeConfirmationDialog()
96 },
97 })
98
99 const { mutate: truncateRows, isPending: isTruncatingRows } = useTableRowTruncateMutation({
100 onSuccess: () => {
101 if (snap.confirmationDialog?.type === 'row') {
102 snap.confirmationDialog.callback?.()
103 }
104 toast.success(`Successfully deleted all rows from table`)
105 },
106 onError: (error) => {
107 toast.error(`Failed to delete rows: ${error.message}`)
108 },
109 onSettled: () => {
110 snap.closeConfirmationDialog()
111 },
112 })
113
114 const isAllRowsSelected =
115 snap.confirmationDialog?.type === 'row' ? snap.confirmationDialog.allRowsSelected : false
116 const numRows =
117 snap.confirmationDialog?.type === 'row'
118 ? snap.confirmationDialog.allRowsSelected
119 ? (snap.confirmationDialog.numRows ?? 0)
120 : snap.confirmationDialog.rows.length
121 : 0
122
123 const isDeleteWithCascade =
124 snap.confirmationDialog?.type === 'column' || snap.confirmationDialog?.type === 'table'
125 ? snap.confirmationDialog.isDeleteWithCascade
126 : false
127
128 const onConfirmDeleteColumn = async () => {
129 if (!(snap.confirmationDialog?.type === 'column')) return
130 if (project === undefined) return
131
132 const selectedColumnToDelete = snap.confirmationDialog.column
133 if (selectedColumnToDelete === undefined) return
134
135 deleteColumn({
136 column: selectedColumnToDelete,
137 cascade: isDeleteWithCascade,
138 projectRef: project.ref,
139 connectionString: project?.connectionString,
140 })
141 }
142
143 const onConfirmDeleteTable = async () => {
144 if (!(snap.confirmationDialog?.type === 'table')) return
145 const selectedTableToDelete = selectedTable
146
147 if (selectedTableToDelete === undefined) return
148
149 deleteTable({
150 projectRef: project?.ref!,
151 connectionString: project?.connectionString,
152 id: selectedTableToDelete.id,
153 name: selectedTableToDelete.name,
154 schema: selectedTableToDelete.schema,
155 cascade: isDeleteWithCascade,
156 })
157 }
158
159 const getImpersonatedRoleState = useGetImpersonatedRoleState()
160
161 const onConfirmDeleteRow = async () => {
162 if (!project) return console.error('Project ref is required')
163 if (!selectedTable) return console.error('Selected table required')
164 if (snap.confirmationDialog?.type !== 'row') return
165 const selectedRowsToDelete = snap.confirmationDialog.rows
166
167 if (snap.confirmationDialog.allRowsSelected) {
168 if (filters.length === 0) {
169 if (getImpersonatedRoleState().role !== undefined) {
170 snap.closeConfirmationDialog()
171 return toast.error('Table truncation is not supported when impersonating a role')
172 }
173
174 truncateRows({
175 projectRef: project.ref,
176 connectionString: project.connectionString,
177 table: selectedTable,
178 })
179 } else {
180 deleteAllRows({
181 projectRef: project.ref,
182 connectionString: project.connectionString,
183 table: selectedTable,
184 filters,
185 roleImpersonationState: getImpersonatedRoleState(),
186 })
187 }
188 } else {
189 deleteRows({
190 projectRef: project.ref,
191 connectionString: project.connectionString,
192 table: selectedTable,
193 rows: selectedRowsToDelete as SupaRow[],
194 roleImpersonationState: getImpersonatedRoleState(),
195 })
196 }
197 }
198
199 return (
200 <>
201 <ConfirmationModal
202 variant="destructive"
203 size="small"
204 visible={snap.confirmationDialog?.type === 'column'}
205 title={`Confirm deletion of column "${
206 snap.confirmationDialog?.type === 'column' && snap.confirmationDialog.column.name
207 }"`}
208 confirmLabel="Delete"
209 confirmLabelLoading="Deleting"
210 onCancel={() => {
211 snap.closeConfirmationDialog()
212 }}
213 onConfirm={onConfirmDeleteColumn}
214 >
215 <div className="space-y-4">
216 <p className="text-sm text-foreground-light">
217 Are you sure you want to delete the selected column? This action cannot be undone.
218 </p>
219 <div className="items-top flex space-x-2">
220 <Checkbox
221 id="checkbox-cascade"
222 checked={isDeleteWithCascade}
223 onCheckedChange={() => snap.toggleConfirmationIsWithCascade()}
224 />
225 <div className="grid gap-1.5 leading-none">
226 <label
227 htmlFor="checkbox-cascade"
228 className="text-sm text-foreground-light leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
229 >
230 Drop column with cascade?
231 </label>
232 <p className="text-sm text-foreground-muted">
233 Deletes the column and its dependent objects
234 </p>
235 </div>
236 </div>
237 {isDeleteWithCascade && (
238 <Alert
239 variant="warning"
240 title="Warning: Dropping with cascade may result in unintended consequences"
241 >
242 <AlertTitle>
243 All dependent objects will be removed, as will any objects that depend on them,
244 recursively.
245 </AlertTitle>
246 <AlertDescription>
247 <Button asChild size="tiny" type="default" icon={<ExternalLink />}>
248 <Link
249 href="https://www.postgresql.org/docs/current/ddl-depend.html"
250 target="_blank"
251 rel="noreferrer"
252 >
253 About dependency tracking
254 </Link>
255 </Button>
256 </AlertDescription>
257 </Alert>
258 )}
259 </div>
260 </ConfirmationModal>
261
262 <ConfirmationModal
263 variant={'destructive'}
264 size="small"
265 visible={snap.confirmationDialog?.type === 'table'}
266 title={
267 <span className="wrap-break-word">{`Confirm deletion of table "${selectedTable?.name}"`}</span>
268 }
269 confirmLabel="Delete"
270 confirmLabelLoading="Deleting"
271 onCancel={() => {
272 snap.closeConfirmationDialog()
273 }}
274 onConfirm={onConfirmDeleteTable}
275 >
276 <div data-testid="confirm-delete-table-modal" className="space-y-4">
277 <p className="text-sm text-foreground-light">
278 Are you sure you want to delete the selected table? This action cannot be undone.
279 </p>
280 <div className="items-top flex space-x-2">
281 <Checkbox
282 id="checkbox-cascade"
283 checked={isDeleteWithCascade}
284 onCheckedChange={() => snap.toggleConfirmationIsWithCascade(!isDeleteWithCascade)}
285 />
286 <div className="grid gap-1.5 leading-none">
287 <label
288 htmlFor="checkbox-cascade"
289 className="text-sm text-foreground-light leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
290 >
291 Drop table with cascade?
292 </label>
293 <p className="text-sm text-foreground-muted">
294 Deletes the table and its dependent objects
295 </p>
296 </div>
297 </div>
298 {isDeleteWithCascade && (
299 <Alert variant="warning">
300 <AlertTitle>
301 Warning: Dropping with cascade may result in unintended consequences
302 </AlertTitle>
303 <AlertDescription>
304 All dependent objects will be removed, as will any objects that depend on them,
305 recursively.
306 </AlertDescription>
307 <AlertDescription className="mt-4">
308 <Button asChild size="tiny" type="default" icon={<ExternalLink />}>
309 <Link
310 href="https://www.postgresql.org/docs/current/ddl-depend.html"
311 target="_blank"
312 rel="noreferrer"
313 >
314 About dependency tracking
315 </Link>
316 </Button>
317 </AlertDescription>
318 </Alert>
319 )}
320 </div>
321 </ConfirmationModal>
322
323 <ConfirmationModal
324 variant={'destructive'}
325 size="small"
326 visible={snap.confirmationDialog?.type === 'row'}
327 title={
328 <p className="wrap-break-word">
329 <span>Confirm to delete the selected row</span>
330 <span>{numRows > 1 && 's'}</span>
331 </p>
332 }
333 confirmLabel="Delete"
334 confirmLabelLoading="Deleting"
335 onCancel={() => snap.closeConfirmationDialog()}
336 onConfirm={() => onConfirmDeleteRow()}
337 loading={isTruncatingRows || isDeletingRows || isDeletingAllRows}
338 >
339 <div className="space-y-4">
340 <p className="text-sm text-foreground-light">
341 <span>Are you sure you want to delete </span>
342 <span>{isAllRowsSelected ? 'all' : 'the selected'} </span>
343 <span>{numRows > 1 && `${numRows} `}</span>
344 <span>row</span>
345 <span>{numRows > 1 && 's'}</span>
346 <span>? This action cannot be undone.</span>
347 </p>
348 </div>
349 </ConfirmationModal>
350 </>
351 )
352}
353
354export default DeleteConfirmationDialogs