UtilityTabResults.tsx192 lines · main
| 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 | |
| 7 | import Results from './Results' |
| 8 | import { getSqlErrorLines } from './UtilityTabResults.utils' |
| 9 | import { subscriptionHasHipaaAddon } from '@/components/interfaces/Billing/Subscription/Subscription.utils' |
| 10 | import { AiAssistantDropdown } from '@/components/ui/AiAssistantDropdown' |
| 11 | import CopyButton from '@/components/ui/CopyButton' |
| 12 | import { InlineLink, InlineLinkClassName } from '@/components/ui/InlineLink' |
| 13 | import { useProjectSettingsV2Query } from '@/data/config/project-settings-v2-query' |
| 14 | import { useOrgSubscriptionQuery } from '@/data/subscriptions/org-subscription-query' |
| 15 | import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization' |
| 16 | import { DOCS_URL } from '@/lib/constants' |
| 17 | import { useDatabaseSelectorStateSnapshot } from '@/state/database-selector' |
| 18 | import { useSqlEditorV2StateSnapshot } from '@/state/sql-editor-v2' |
| 19 | |
| 20 | export type UtilityTabResultsProps = { |
| 21 | id: string |
| 22 | isExecuting?: boolean |
| 23 | isDisabled?: boolean |
| 24 | onDebug: () => void |
| 25 | buildDebugPrompt: () => string |
| 26 | isDebugging?: boolean |
| 27 | } |
| 28 | |
| 29 | export 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 | |
| 192 | UtilityTabResults.displayName = 'UtilityTabResults' |