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