DeleteConfirmationDialogs.tsx 13 KB

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