UtilityTabResults.tsx 8.0 KB

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