UtilityTabResults.tsx192 lines · main
1import { useParams } from 'common'
2import { ExternalLink, Loader2 } from 'lucide-react'
3import { parseAsBoolean, useQueryState } from 'nuqs'
4import { forwardRef } from 'react'
5import { Button, cn, Tooltip, TooltipContent, TooltipTrigger } from 'ui'
6
7import Results from './Results'
8import { getSqlErrorLines } from './UtilityTabResults.utils'
9import { subscriptionHasHipaaAddon } from '@/components/interfaces/Billing/Subscription/Subscription.utils'
10import { AiAssistantDropdown } from '@/components/ui/AiAssistantDropdown'
11import CopyButton from '@/components/ui/CopyButton'
12import { InlineLink, InlineLinkClassName } from '@/components/ui/InlineLink'
13import { useProjectSettingsV2Query } from '@/data/config/project-settings-v2-query'
14import { useOrgSubscriptionQuery } from '@/data/subscriptions/org-subscription-query'
15import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization'
16import { DOCS_URL } from '@/lib/constants'
17import { useDatabaseSelectorStateSnapshot } from '@/state/database-selector'
18import { useSqlEditorV2StateSnapshot } from '@/state/sql-editor-v2'
19
20export type UtilityTabResultsProps = {
21 id: string
22 isExecuting?: boolean
23 isDisabled?: boolean
24 onDebug: () => void
25 buildDebugPrompt: () => string
26 isDebugging?: boolean
27}
28
29export const UtilityTabResults = forwardRef<HTMLDivElement, UtilityTabResultsProps>(
30 ({ id, isExecuting, isDisabled, isDebugging, onDebug, buildDebugPrompt }) => {
31 const { ref } = useParams()
32 const state = useDatabaseSelectorStateSnapshot()
33 const { data: organization } = useSelectedOrganizationQuery()
34 const snapV2 = useSqlEditorV2StateSnapshot()
35 const [, setShowConnect] = useQueryState('showConnect', parseAsBoolean.withDefault(false))
36
37 const result = snapV2.results[id]?.[0]
38 const { data: subscription } = useOrgSubscriptionQuery({ orgSlug: organization?.slug })
39
40 // Customers on HIPAA plans should not have access to Briven AI
41 const { data: projectSettings } = useProjectSettingsV2Query({ projectRef: ref })
42 const hasHipaaAddon = subscriptionHasHipaaAddon(subscription) && projectSettings?.is_sensitive
43
44 const isTimeout =
45 result?.error?.message?.includes('canceling statement due to statement timeout') ||
46 result?.error?.message?.includes('upstream request timeout') ||
47 result?.error?.message?.includes('Query read timeout')
48
49 const isNetWorkError = result?.error?.message?.includes('EHOSTUNREACH')
50
51 if (isExecuting) {
52 return (
53 <div className="flex items-center gap-x-4 px-6 py-4 bg-table-header-light in-data-[theme*=dark]:bg-table-header-dark">
54 <Loader2 size={14} className="animate-spin" />
55 <p className="m-0 border-0 font-mono text-sm">Running...</p>
56 </div>
57 )
58 } else if (result?.error) {
59 const errorLines = getSqlErrorLines(result.error)
60 const readReplicaError =
61 state.selectedDatabaseId !== ref &&
62 result.error.message.includes('in a read-only transaction')
63 const payloadTooLargeError = result.error.message.includes(
64 'Query is too large to be run via the SQL Editor'
65 )
66
67 return (
68 <div className="bg-table-header-light in-data-[theme*=dark]:bg-table-header-dark overflow-y-auto">
69 <div className="flex flex-row justify-between items-start py-4 px-6 gap-x-4">
70 {isTimeout ? (
71 <div className="flex flex-col gap-y-1">
72 <p className="font-mono text-sm tracking-tight">
73 Error: SQL query ran into an upstream timeout
74 </p>
75 <p className="text-sm text-foreground-light">
76 You can either{' '}
77 <InlineLink
78 href={`${DOCS_URL}/guides/platform/performance#examining-query-performance`}
79 >
80 optimize your query
81 </InlineLink>
82 , or{' '}
83 <InlineLink href={`${DOCS_URL}/guides/database/timeouts`}>
84 increase the statement timeout
85 </InlineLink>
86 {' or '}
87 <span
88 className={cn(InlineLinkClassName, 'cursor-pointer')}
89 onClick={() => setShowConnect(true)}
90 >
91 connect to your database directly
92 </span>
93 .
94 </p>
95 </div>
96 ) : (
97 <div className="flex flex-col gap-y-1">
98 {errorLines.length > 0 ? (
99 errorLines.map((x: string, i: number) => (
100 <pre key={`error-${i}`} className="font-mono text-sm text-wrap">
101 {x}
102 </pre>
103 ))
104 ) : (
105 <p className="font-mono text-sm tracking-tight">Error: {result.error?.message}</p>
106 )}
107 {!isTimeout && !isNetWorkError && result.autoLimit && (
108 <p className="text-sm text-foreground-light">
109 Note: A limit of {result.autoLimit} was applied to your query. If this was the
110 cause of a syntax error, try selecting "No limit" instead and re-run the query.
111 </p>
112 )}
113 {readReplicaError && (
114 <p className="text-sm text-foreground-light">
115 Note: Read replicas are for read only queries. Run write queries on the primary
116 database instead.
117 </p>
118 )}
119 {payloadTooLargeError && (
120 <p className="text-sm text-foreground-light flex items-center gap-x-1">
121 Run this query by{' '}
122 <span
123 onClick={() => setShowConnect(true)}
124 className={cn(InlineLinkClassName, 'flex items-center gap-x-1')}
125 >
126 connecting to your database directly
127 <ExternalLink size={12} />
128 </span>
129 .
130 </p>
131 )}
132 </div>
133 )}
134
135 <div className="flex items-center gap-x-2">
136 {readReplicaError && (
137 <Button
138 className="py-2"
139 type="default"
140 onClick={() => {
141 state.setSelectedDatabaseId(ref)
142 snapV2.resetResults(id)
143 }}
144 >
145 Switch to primary database
146 </Button>
147 )}
148 {errorLines.length > 0 && (
149 <Tooltip>
150 <TooltipTrigger>
151 <CopyButton iconOnly type="default" text={errorLines.join('\n')} />
152 </TooltipTrigger>
153 <TooltipContent side="bottom" align="center">
154 <span>Copy error</span>
155 </TooltipContent>
156 </Tooltip>
157 )}
158 {!hasHipaaAddon && (
159 <AiAssistantDropdown
160 label="Debug with Assistant"
161 buildPrompt={buildDebugPrompt}
162 onOpenAssistant={onDebug}
163 telemetrySource="sql_debug"
164 disabled={!!isDisabled || isDebugging}
165 loading={isDebugging}
166 />
167 )}
168 </div>
169 </div>
170 </div>
171 )
172 } else if (!result) {
173 return (
174 <div className="bg-table-header-light in-data-[theme*=dark]:bg-table-header-dark overflow-y-auto">
175 <p className="m-0 border-0 px-4 py-4 text-sm text-foreground-light">
176 Click <code className="text-code-inline">Run</code> to execute your query
177 </p>
178 </div>
179 )
180 } else if (result.rows.length <= 0) {
181 return (
182 <div className="bg-table-header-light in-data-[theme*=dark]:bg-table-header-dark overflow-y-auto">
183 <p className="m-0 border-0 px-6 py-4 font-mono text-sm">Success. No rows returned</p>
184 </div>
185 )
186 }
187
188 return <Results rows={result.rows} />
189 }
190)
191
192UtilityTabResults.displayName = 'UtilityTabResults'