IndexSuggestionIcon.tsx 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146
  1. import { Loader2 } from 'lucide-react'
  2. import { MouseEvent, useState } from 'react'
  3. import {
  4. Button,
  5. cn,
  6. HoverCard,
  7. HoverCardContent,
  8. HoverCardTrigger,
  9. Separator,
  10. WarningIcon,
  11. } from 'ui'
  12. import { CodeBlock } from 'ui-patterns/CodeBlock'
  13. import { useIndexInvalidation } from '../hooks/useIndexInvalidation'
  14. import { QueryPanelScoreSection } from '../QueryPanel'
  15. import { createIndexes } from './index-advisor.utils'
  16. import { IndexImprovementText } from './IndexImprovementText'
  17. import { GetIndexAdvisorResultResponse } from '@/data/database/retrieve-index-advisor-result-query'
  18. import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
  19. interface IndexSuggestionIconProps {
  20. indexAdvisorResult: GetIndexAdvisorResultResponse
  21. onClickIcon?: () => void
  22. }
  23. export const IndexSuggestionIcon = ({
  24. indexAdvisorResult,
  25. onClickIcon,
  26. }: IndexSuggestionIconProps) => {
  27. const { data: project } = useSelectedProjectQuery()
  28. const [isCreatingIndex, setIsCreatingIndex] = useState(false)
  29. const [isHoverCardOpen, setIsHoverCardOpen] = useState(false)
  30. const invalidateQueries = useIndexInvalidation()
  31. const handleCreateIndex = async (e: MouseEvent) => {
  32. e.stopPropagation()
  33. setIsCreatingIndex(true)
  34. try {
  35. await createIndexes({
  36. projectRef: project?.ref,
  37. connectionString: project?.connectionString,
  38. indexStatements: indexAdvisorResult.index_statements,
  39. onSuccess: () => {
  40. // Handle UI-specific logic
  41. if (onClickIcon) {
  42. onClickIcon()
  43. setIsHoverCardOpen(false)
  44. }
  45. },
  46. })
  47. // Only invalidate queries if index creation was successful
  48. invalidateQueries()
  49. } catch (error) {
  50. // Error is already handled by createIndexes with a toast notification
  51. // But we could add component-specific error handling here if needed
  52. console.error('Failed to create index:', error)
  53. setIsCreatingIndex(false)
  54. } finally {
  55. // Reset the loading state after a short delay to show feedback
  56. setTimeout(() => setIsCreatingIndex(false), 1000)
  57. }
  58. }
  59. if (!indexAdvisorResult?.index_statements?.length) return null
  60. return (
  61. <HoverCard open={isHoverCardOpen} onOpenChange={setIsHoverCardOpen}>
  62. <HoverCardTrigger>
  63. <div
  64. onClick={(e) => {
  65. if (onClickIcon && !isCreatingIndex) {
  66. e.stopPropagation()
  67. onClickIcon()
  68. }
  69. }}
  70. className="cursor-pointer"
  71. >
  72. {isCreatingIndex ? (
  73. <Loader2 size={16} className="animate-spin text-foreground-light" />
  74. ) : (
  75. <WarningIcon />
  76. )}
  77. </div>
  78. </HoverCardTrigger>
  79. <HoverCardContent className="w-[520px] p-0 overflow-hidden" align="start" alignOffset={-32}>
  80. <div className="px-4 py-3 bg-surface-75">
  81. <IndexImprovementText
  82. indexStatements={indexAdvisorResult.index_statements}
  83. totalCostBefore={indexAdvisorResult.total_cost_before}
  84. totalCostAfter={indexAdvisorResult.total_cost_after}
  85. className="text-sm"
  86. />
  87. </div>
  88. <Separator />
  89. <div>
  90. <CodeBlock
  91. hideLineNumbers
  92. value={indexAdvisorResult.index_statements.join(';\n') + ';'}
  93. language="sql"
  94. className={cn(
  95. 'border-none rounded-none',
  96. 'max-w-full',
  97. 'py-0.5! px-3.5! prose dark:prose-dark transition',
  98. '[&>code]:m-0 [&>code>span]:flex [&>code>span]:flex-wrap'
  99. )}
  100. />
  101. </div>
  102. <Separator />
  103. <QueryPanelScoreSection
  104. name="Total cost of query"
  105. description="An estimate of how long it will take to return all the rows (Includes start up cost)"
  106. before={indexAdvisorResult.total_cost_before}
  107. after={indexAdvisorResult.total_cost_after}
  108. />
  109. <QueryPanelScoreSection
  110. hideArrowMarkers
  111. className="border-t"
  112. name="Start up cost"
  113. description="An estimate of how long it will take to fetch the first row"
  114. before={indexAdvisorResult.startup_cost_before}
  115. after={indexAdvisorResult.startup_cost_after}
  116. />
  117. <div className="p-3 flex gap-2 items-center border-t justify-end">
  118. <Button
  119. type="text"
  120. onClick={(e) => {
  121. e.stopPropagation()
  122. if (onClickIcon && !isCreatingIndex) onClickIcon()
  123. setIsHoverCardOpen(false)
  124. }}
  125. disabled={isCreatingIndex}
  126. >
  127. View details
  128. </Button>
  129. <Button onClick={handleCreateIndex} loading={isCreatingIndex} disabled={isCreatingIndex}>
  130. Create index
  131. </Button>
  132. </div>
  133. </HoverCardContent>
  134. </HoverCard>
  135. )
  136. }