RunQueryWarningModal.tsx 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188
  1. import { useCallback, useEffect, useRef, type ReactNode } from 'react'
  2. import {
  3. AlertDialog,
  4. AlertDialogAction,
  5. AlertDialogCancel,
  6. AlertDialogContent,
  7. AlertDialogDescription,
  8. AlertDialogFooter,
  9. AlertDialogHeader,
  10. AlertDialogTitle,
  11. } from 'ui'
  12. import { type PotentialIssues } from './SQLEditor.types'
  13. interface RunQueryWarningModalProps {
  14. visible: boolean
  15. potentialIssues: PotentialIssues | undefined
  16. onCancel: () => void
  17. onConfirm: () => void
  18. onConfirmWithRLS?: () => void
  19. }
  20. type WarningMessage = {
  21. id: string
  22. summary: ReactNode
  23. description: ReactNode
  24. }
  25. type MissingRLSTable = NonNullable<PotentialIssues['createTablesMissingRLS']>[number]
  26. const getMissingRLSTableName = (table: MissingRLSTable) =>
  27. table.schema ? `${table.schema}.${table.tableName}` : table.tableName
  28. export const RunQueryWarningModal = ({
  29. visible,
  30. potentialIssues,
  31. onCancel,
  32. onConfirm,
  33. onConfirmWithRLS,
  34. }: RunQueryWarningModalProps) => {
  35. const {
  36. hasDestructiveOperations,
  37. hasUpdateWithoutWhere,
  38. hasAlterDatabasePreventConnection,
  39. createTablesMissingRLS,
  40. } = potentialIssues || {}
  41. const missingRLSTables = createTablesMissingRLS ?? []
  42. const hasMissingRLS = missingRLSTables.length > 0
  43. const isConfirmingRef = useRef(false)
  44. useEffect(() => {
  45. if (visible) {
  46. isConfirmingRef.current = false
  47. }
  48. }, [visible])
  49. const handleOpenChange = useCallback(
  50. (open: boolean) => {
  51. if (open) return
  52. if (isConfirmingRef.current) {
  53. isConfirmingRef.current = false
  54. return
  55. }
  56. onCancel()
  57. },
  58. [onCancel]
  59. )
  60. const handleConfirm = useCallback(() => {
  61. isConfirmingRef.current = true
  62. onConfirm()
  63. }, [onConfirm])
  64. const handleConfirmWithRLS = useCallback(() => {
  65. if (!onConfirmWithRLS) return
  66. isConfirmingRef.current = true
  67. onConfirmWithRLS()
  68. }, [onConfirmWithRLS])
  69. const warnings: WarningMessage[] = []
  70. if (hasDestructiveOperations) {
  71. warnings.push({
  72. id: 'destructive-operations',
  73. summary: 'This query includes destructive operations',
  74. description: 'It may permanently change or remove data, tables, schemas, or other objects.',
  75. })
  76. }
  77. if (hasUpdateWithoutWhere) {
  78. warnings.push({
  79. id: 'update-without-where',
  80. summary: (
  81. <>
  82. This query runs an <code className="text-code-inline">UPDATE</code> without a{' '}
  83. <code className="text-code-inline">WHERE</code> clause
  84. </>
  85. ),
  86. description: 'It may update every row in the target table.',
  87. })
  88. }
  89. if (hasAlterDatabasePreventConnection) {
  90. warnings.push({
  91. id: 'prevent-database-connections',
  92. summary: 'This query may prevent new database connections',
  93. description:
  94. 'The dashboard may lose access until the setting is restored from a direct database connection.',
  95. })
  96. }
  97. if (hasMissingRLS) {
  98. const tableName =
  99. missingRLSTables.length === 1 ? getMissingRLSTableName(missingRLSTables[0]) : undefined
  100. warnings.push({
  101. id: 'missing-rls',
  102. summary:
  103. missingRLSTables.length === 1
  104. ? 'This query creates a table without enabling Row Level Security'
  105. : 'This query creates tables without enabling Row Level Security',
  106. description: (
  107. <>
  108. Clients using anon or authenticated keys may be able to access{' '}
  109. {tableName ? <code className="text-code-inline">{tableName}</code> : 'these tables'}.
  110. </>
  111. ),
  112. })
  113. }
  114. const canEnableRLS = hasMissingRLS && onConfirmWithRLS !== undefined
  115. const confirmationCopy = canEnableRLS
  116. ? warnings.length > 1
  117. ? 'Review each issue, then choose whether to enable Row Level Security before running this query.'
  118. : 'Choose whether to enable Row Level Security before running this query.'
  119. : 'Run this query only if you intend these changes and understand the risks.'
  120. const title = warnings.length > 1 ? 'Potential issues detected' : 'Potential issue detected'
  121. return (
  122. <AlertDialog open={visible} onOpenChange={handleOpenChange}>
  123. <AlertDialogContent size="small">
  124. <AlertDialogHeader>
  125. <AlertDialogTitle>{title}</AlertDialogTitle>
  126. <AlertDialogDescription asChild>
  127. {warnings.length === 0 ? (
  128. <div>
  129. <p>Are you sure you want to run this query?</p>
  130. </div>
  131. ) : warnings.length === 1 ? (
  132. <div>
  133. <p>
  134. {warnings[0].summary}. {warnings[0].description}
  135. </p>
  136. <p className="mt-3">{confirmationCopy}</p>
  137. </div>
  138. ) : (
  139. <div>
  140. <p>This query has multiple potential issues:</p>
  141. <ul>
  142. {warnings.map((warning) => (
  143. <li key={warning.id} className="mt-3">
  144. <span className="font-medium text-foreground">{warning.summary}.</span>{' '}
  145. <span>{warning.description}</span>
  146. </li>
  147. ))}
  148. </ul>
  149. <p className="mt-3">{confirmationCopy}</p>
  150. </div>
  151. )}
  152. </AlertDialogDescription>
  153. </AlertDialogHeader>
  154. <AlertDialogFooter>
  155. <AlertDialogCancel>Cancel</AlertDialogCancel>
  156. <AlertDialogAction variant="warning" onClick={handleConfirm}>
  157. {canEnableRLS ? 'Run without RLS' : 'Run query'}
  158. </AlertDialogAction>
  159. {canEnableRLS && (
  160. <AlertDialogAction onClick={handleConfirmWithRLS}>Run and enable RLS</AlertDialogAction>
  161. )}
  162. </AlertDialogFooter>
  163. </AlertDialogContent>
  164. </AlertDialog>
  165. )
  166. }