IndexSuggestionIcon.tsx146 lines · main
| 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 | |
| 14 | import { useIndexInvalidation } from '../hooks/useIndexInvalidation' |
| 15 | import { QueryPanelScoreSection } from '../QueryPanel' |
| 16 | import { createIndexes } from './index-advisor.utils' |
| 17 | import { IndexImprovementText } from './IndexImprovementText' |
| 18 | import { GetIndexAdvisorResultResponse } from '@/data/database/retrieve-index-advisor-result-query' |
| 19 | import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject' |
| 20 | |
| 21 | interface IndexSuggestionIconProps { |
| 22 | indexAdvisorResult: GetIndexAdvisorResultResponse |
| 23 | onClickIcon?: () => void |
| 24 | } |
| 25 | |
| 26 | export 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 | } |