UtilityTabExplain.tsx 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124
  1. import { Loader2 } from 'lucide-react'
  2. import { useState } from 'react'
  3. import { Tooltip, TooltipContent, TooltipTrigger } from 'ui'
  4. import Results from './Results'
  5. import { ExplainVisualizer } from '@/components/interfaces/ExplainVisualizer/ExplainVisualizer'
  6. import { ExplainHeader } from '@/components/interfaces/ExplainVisualizer/ExplainVisualizer.Header'
  7. import {
  8. isExplainQuery,
  9. isTextFormatExplain,
  10. } from '@/components/interfaces/ExplainVisualizer/ExplainVisualizer.utils'
  11. import CopyButton from '@/components/ui/CopyButton'
  12. import { useSqlEditorV2StateSnapshot } from '@/state/sql-editor-v2'
  13. export type UtilityTabExplainProps = {
  14. id: string
  15. isExecuting?: boolean
  16. }
  17. export function UtilityTabExplain({ id, isExecuting }: UtilityTabExplainProps) {
  18. const snapV2 = useSqlEditorV2StateSnapshot()
  19. const explainResult = snapV2.explainResults[id]
  20. const [mode, setMode] = useState<'visual' | 'raw'>('visual')
  21. if (isExecuting) {
  22. return (
  23. <div className="flex items-center gap-x-4 px-6 py-4 bg-table-header-light in-data-[theme*=dark]:bg-table-header-dark">
  24. <Loader2 size={14} className="animate-spin" />
  25. <p className="m-0 border-0 font-mono text-sm">Running EXPLAIN ANALYZE...</p>
  26. </div>
  27. )
  28. }
  29. if (explainResult?.error) {
  30. const formattedError = (explainResult.error?.formattedError?.split('\n') ?? []).filter(
  31. (x: string) => x.length > 0
  32. )
  33. return (
  34. <div className="bg-table-header-light in-data-[theme*=dark]:bg-table-header-dark overflow-y-auto">
  35. <div className="flex flex-row justify-between items-start py-4 px-6 gap-x-4 pb-9">
  36. <div className="flex flex-col gap-y-1">
  37. {formattedError.length > 0 ? (
  38. formattedError.map((x: string, i: number) => (
  39. <pre key={`error-${i}`} className="font-mono text-sm text-wrap">
  40. {x}
  41. </pre>
  42. ))
  43. ) : (
  44. <p className="font-mono text-sm tracking-tight">
  45. Error: {explainResult.error?.message}
  46. </p>
  47. )}
  48. </div>
  49. <div className="flex items-center gap-x-2">
  50. {formattedError.length > 0 && (
  51. <Tooltip>
  52. <TooltipTrigger>
  53. <CopyButton iconOnly type="default" text={formattedError.join('\n')} />
  54. </TooltipTrigger>
  55. <TooltipContent side="bottom" align="center">
  56. <span>Copy error</span>
  57. </TooltipContent>
  58. </Tooltip>
  59. )}
  60. </div>
  61. </div>
  62. </div>
  63. )
  64. }
  65. if (!explainResult || explainResult.rows.length === 0) {
  66. return (
  67. <div className="bg-table-header-light in-data-[theme*=dark]:bg-table-header-dark overflow-y-auto">
  68. <p className="m-0 border-0 px-4 py-4 text-sm text-foreground-light">
  69. No execution plan available. The query will be analyzed when you switch to this tab.
  70. </p>
  71. </div>
  72. )
  73. }
  74. const isValidExplain = isExplainQuery(explainResult.rows)
  75. const isTextFormat = isTextFormatExplain(explainResult.rows)
  76. if (!isValidExplain) {
  77. return (
  78. <div className="bg-table-header-light in-data-[theme*=dark]:bg-table-header-dark overflow-y-auto">
  79. <p className="m-0 border-0 px-4 py-4 text-sm text-foreground-light">
  80. Unable to parse explain results. Please try running the query again.
  81. </p>
  82. </div>
  83. )
  84. }
  85. // Handle non-TEXT formats (JSON, YAML, XML) - show raw output only
  86. if (!isTextFormat) {
  87. return (
  88. <div className="h-full flex flex-col pb-9">
  89. <div className="px-4 py-3 bg-surface-100 border-b border-default flex items-center gap-2">
  90. <span className="text-sm text-foreground-light">
  91. Visual execution plan is only available for TEXT format. Showing raw output.
  92. </span>
  93. </div>
  94. <Results rows={explainResult.rows} />
  95. </div>
  96. )
  97. }
  98. const toggleMode = () => setMode(mode === 'visual' ? 'raw' : 'visual')
  99. return (
  100. <div className="h-full flex flex-col pb-9">
  101. {mode === 'visual' ? (
  102. <ExplainVisualizer rows={explainResult.rows} onShowRaw={toggleMode} id={id} />
  103. ) : (
  104. <>
  105. <ExplainHeader mode="raw" onToggleMode={toggleMode} id={id} rows={explainResult.rows} />
  106. <Results rows={explainResult.rows} />
  107. </>
  108. )}
  109. </div>
  110. )
  111. }