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