RunQueryWarningModal.tsx188 lines · main
| 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 | |
| 13 | import { type PotentialIssues } from './SQLEditor.types' |
| 14 | |
| 15 | interface RunQueryWarningModalProps { |
| 16 | visible: boolean |
| 17 | potentialIssues: PotentialIssues | undefined |
| 18 | onCancel: () => void |
| 19 | onConfirm: () => void |
| 20 | onConfirmWithRLS?: () => void |
| 21 | } |
| 22 | |
| 23 | type WarningMessage = { |
| 24 | id: string |
| 25 | summary: ReactNode |
| 26 | description: ReactNode |
| 27 | } |
| 28 | |
| 29 | type MissingRLSTable = NonNullable<PotentialIssues['createTablesMissingRLS']>[number] |
| 30 | |
| 31 | const getMissingRLSTableName = (table: MissingRLSTable) => |
| 32 | table.schema ? `${table.schema}.${table.tableName}` : table.tableName |
| 33 | |
| 34 | export 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 | } |