WithStatements.tsx321 lines · main
| 1 | import { safeSql } from '@supabase/pg-meta/src/pg-format' |
| 2 | import { LOCAL_STORAGE_KEYS, useParams } from 'common' |
| 3 | import { RefreshCw, RotateCcw, X } from 'lucide-react' |
| 4 | import { parseAsString, useQueryStates } from 'nuqs' |
| 5 | import { useEffect, useMemo, useState } from 'react' |
| 6 | import { toast } from 'sonner' |
| 7 | import { Button, cn, LoadingLine } from 'ui' |
| 8 | import { Admonition } from 'ui-patterns' |
| 9 | import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal' |
| 10 | |
| 11 | import { Markdown } from '../../Markdown' |
| 12 | import { captureQueryPerformanceError } from '../QueryPerformance.utils' |
| 13 | import { QueryPerformanceFilterBar } from '../QueryPerformanceFilterBar' |
| 14 | import { QueryPerformanceGrid } from '../QueryPerformanceGrid' |
| 15 | import { QueryPerformanceMetrics } from '../QueryPerformanceMetrics' |
| 16 | import { QueryPerformanceInfiniteHook } from '../useQueryPerformanceQuery' |
| 17 | import { transformStatementDataToRows } from './WithStatements.utils' |
| 18 | import { PresetHookResult } from '@/components/interfaces/Reports/Reports.utils' |
| 19 | import { ButtonTooltip } from '@/components/ui/ButtonTooltip' |
| 20 | import { DownloadResultsButton } from '@/components/ui/DownloadResultsButton' |
| 21 | import { useReadReplicasQuery } from '@/data/read-replicas/replicas-query' |
| 22 | import { formatDatabaseID } from '@/data/read-replicas/replicas.utils' |
| 23 | import { executeSql } from '@/data/sql/execute-sql-query' |
| 24 | import { useInfiniteScroll } from '@/hooks/misc/useInfiniteScroll' |
| 25 | import { useLocalStorageQuery } from '@/hooks/misc/useLocalStorage' |
| 26 | import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject' |
| 27 | import { DOCS_URL, IS_PLATFORM } from '@/lib/constants' |
| 28 | import { getErrorMessage } from '@/lib/get-error-message' |
| 29 | import { useDatabaseSelectorStateSnapshot } from '@/state/database-selector' |
| 30 | |
| 31 | interface WithStatementsProps { |
| 32 | queryHitRate: PresetHookResult |
| 33 | queryPerformanceQuery: QueryPerformanceInfiniteHook |
| 34 | queryMetrics: PresetHookResult |
| 35 | } |
| 36 | |
| 37 | export const WithStatements = ({ |
| 38 | queryHitRate, |
| 39 | queryPerformanceQuery, |
| 40 | queryMetrics, |
| 41 | }: WithStatementsProps) => { |
| 42 | const { ref } = useParams() |
| 43 | const { data: project } = useSelectedProjectQuery() |
| 44 | const state = useDatabaseSelectorStateSnapshot() |
| 45 | const { |
| 46 | data, |
| 47 | isLoading, |
| 48 | isRefetching, |
| 49 | isFetchingNextPage, |
| 50 | hasNextPage, |
| 51 | error: queryError, |
| 52 | fetchNextPage, |
| 53 | refetch: runQuery, |
| 54 | } = queryPerformanceQuery |
| 55 | const isPrimaryDatabase = state.selectedDatabaseId === ref |
| 56 | const formattedDatabaseId = formatDatabaseID(state.selectedDatabaseId ?? '') |
| 57 | |
| 58 | const hitRateError = 'error' in queryHitRate ? queryHitRate.error : null |
| 59 | const metricsError = 'error' in queryMetrics ? queryMetrics.error : null |
| 60 | const mainQueryError = queryError || null |
| 61 | |
| 62 | const [showResetgPgStatStatements, setShowResetgPgStatStatements] = useState(false) |
| 63 | |
| 64 | const [showBottomSection, setShowBottomSection] = useLocalStorageQuery( |
| 65 | LOCAL_STORAGE_KEYS.QUERY_PERF_SHOW_BOTTOM_SECTION, |
| 66 | true |
| 67 | ) |
| 68 | |
| 69 | const [{ indexAdvisor }] = useQueryStates({ |
| 70 | indexAdvisor: parseAsString.withDefault('false'), |
| 71 | }) |
| 72 | |
| 73 | const handleRefresh = () => { |
| 74 | runQuery() |
| 75 | queryHitRate.runQuery() |
| 76 | queryMetrics.runQuery() |
| 77 | } |
| 78 | |
| 79 | const processedData = useMemo(() => { |
| 80 | return transformStatementDataToRows(data || [], indexAdvisor === 'true') |
| 81 | }, [data, indexAdvisor]) |
| 82 | |
| 83 | const { data: databases } = useReadReplicasQuery({ projectRef: ref }) |
| 84 | |
| 85 | const handleScroll = useInfiniteScroll({ |
| 86 | isLoading, |
| 87 | isFetchingNextPage, |
| 88 | hasNextPage, |
| 89 | fetchNextPage, |
| 90 | }) |
| 91 | |
| 92 | useEffect(() => { |
| 93 | state.setSelectedDatabaseId(ref) |
| 94 | // eslint-disable-next-line react-hooks/exhaustive-deps |
| 95 | }, [ref]) |
| 96 | |
| 97 | useEffect(() => { |
| 98 | if (mainQueryError) { |
| 99 | const errorMessage = getErrorMessage(mainQueryError) |
| 100 | const isNotInstalled = |
| 101 | typeof errorMessage === 'string' && |
| 102 | errorMessage.includes('pg_stat_statements') && |
| 103 | errorMessage.includes('does not exist') |
| 104 | if (!isNotInstalled) { |
| 105 | captureQueryPerformanceError(mainQueryError, { |
| 106 | projectRef: ref, |
| 107 | databaseIdentifier: state.selectedDatabaseId, |
| 108 | queryPreset: 'unified', |
| 109 | queryType: 'mainQuery', |
| 110 | postgresVersion: project?.dbVersion, |
| 111 | databaseType: isPrimaryDatabase ? 'primary' : 'read-replica', |
| 112 | sql: queryPerformanceQuery.resolvedSql, |
| 113 | errorMessage: errorMessage || undefined, |
| 114 | }) |
| 115 | } |
| 116 | } |
| 117 | }, [ |
| 118 | mainQueryError, |
| 119 | ref, |
| 120 | state.selectedDatabaseId, |
| 121 | project?.dbVersion, |
| 122 | isPrimaryDatabase, |
| 123 | queryPerformanceQuery.resolvedSql, |
| 124 | ]) |
| 125 | |
| 126 | useEffect(() => { |
| 127 | if (hitRateError) { |
| 128 | const errorMessage = getErrorMessage(hitRateError) |
| 129 | captureQueryPerformanceError(hitRateError, { |
| 130 | projectRef: ref, |
| 131 | databaseIdentifier: state.selectedDatabaseId, |
| 132 | queryPreset: 'queryHitRate', |
| 133 | queryType: 'hitRate', |
| 134 | postgresVersion: project?.dbVersion, |
| 135 | databaseType: isPrimaryDatabase ? 'primary' : 'read-replica', |
| 136 | errorMessage: errorMessage || undefined, |
| 137 | }) |
| 138 | } |
| 139 | }, [hitRateError, ref, state.selectedDatabaseId, project?.dbVersion, isPrimaryDatabase]) |
| 140 | |
| 141 | useEffect(() => { |
| 142 | if (metricsError) { |
| 143 | const errorMessage = getErrorMessage(metricsError) |
| 144 | captureQueryPerformanceError(metricsError, { |
| 145 | projectRef: ref, |
| 146 | databaseIdentifier: state.selectedDatabaseId, |
| 147 | queryPreset: 'queryMetrics', |
| 148 | queryType: 'metrics', |
| 149 | postgresVersion: project?.dbVersion, |
| 150 | databaseType: isPrimaryDatabase ? 'primary' : 'read-replica', |
| 151 | errorMessage: errorMessage || undefined, |
| 152 | }) |
| 153 | } |
| 154 | }, [metricsError, ref, state.selectedDatabaseId, project?.dbVersion, isPrimaryDatabase]) |
| 155 | |
| 156 | const hasError = mainQueryError || hitRateError || metricsError |
| 157 | const errorMessage = mainQueryError |
| 158 | ? getErrorMessage(mainQueryError) || 'Failed to load query performance data' |
| 159 | : hitRateError |
| 160 | ? getErrorMessage(hitRateError) || 'Failed to load cache hit rate data' |
| 161 | : metricsError |
| 162 | ? getErrorMessage(metricsError) || 'Failed to load query metrics' |
| 163 | : null |
| 164 | |
| 165 | const isPgStatStatementsNotInstalled = |
| 166 | typeof errorMessage === 'string' && |
| 167 | errorMessage.includes('pg_stat_statements') && |
| 168 | errorMessage.includes('does not exist') |
| 169 | |
| 170 | return ( |
| 171 | <> |
| 172 | {hasError && ( |
| 173 | <div className="px-6 pt-4"> |
| 174 | {isPgStatStatementsNotInstalled ? ( |
| 175 | <Admonition |
| 176 | type="warning" |
| 177 | title="pg_stat_statements extension is not enabled" |
| 178 | description="Query Performance requires the pg_stat_statements extension. Enable it in Database → Extensions." |
| 179 | /> |
| 180 | ) : ( |
| 181 | <Admonition |
| 182 | type="destructive" |
| 183 | title="Error loading query performance data" |
| 184 | description={ |
| 185 | errorMessage || |
| 186 | 'An error occurred while loading query performance data. Please try refreshing the page.' |
| 187 | } |
| 188 | /> |
| 189 | )} |
| 190 | </div> |
| 191 | )} |
| 192 | <QueryPerformanceMetrics /> |
| 193 | <QueryPerformanceFilterBar |
| 194 | showRolesFilter |
| 195 | showSourceFilter |
| 196 | actions={ |
| 197 | <> |
| 198 | <ButtonTooltip |
| 199 | type="default" |
| 200 | size="tiny" |
| 201 | icon={<RefreshCw />} |
| 202 | onClick={handleRefresh} |
| 203 | tooltip={{ content: { side: 'top', text: 'Refresh' } }} |
| 204 | className="w-[26px]" |
| 205 | /> |
| 206 | <ButtonTooltip |
| 207 | type="default" |
| 208 | size="tiny" |
| 209 | icon={<RotateCcw />} |
| 210 | onClick={() => setShowResetgPgStatStatements(true)} |
| 211 | tooltip={{ content: { side: 'top', text: 'Reset report' } }} |
| 212 | className="w-[26px]" |
| 213 | /> |
| 214 | |
| 215 | <DownloadResultsButton |
| 216 | results={processedData} |
| 217 | fileName={`Briven Query Performance Statements (${ref})`} |
| 218 | align="end" |
| 219 | /> |
| 220 | </> |
| 221 | } |
| 222 | /> |
| 223 | <LoadingLine loading={isLoading || isRefetching || isFetchingNextPage} /> |
| 224 | <QueryPerformanceGrid |
| 225 | aggregatedData={processedData} |
| 226 | isLoading={isLoading} |
| 227 | error={ |
| 228 | mainQueryError |
| 229 | ? getErrorMessage(mainQueryError) || 'Failed to load query performance data' |
| 230 | : null |
| 231 | } |
| 232 | onRetry={handleRefresh} |
| 233 | onScroll={handleScroll} |
| 234 | /> |
| 235 | <div |
| 236 | className={cn('px-6 py-6 flex gap-x-4 border-t relative', { |
| 237 | hidden: showBottomSection === false, |
| 238 | })} |
| 239 | > |
| 240 | <Button |
| 241 | className="absolute top-1.5 right-3 px-1.5" |
| 242 | type="text" |
| 243 | size="tiny" |
| 244 | onClick={() => setShowBottomSection(false)} |
| 245 | > |
| 246 | <X size="14" /> |
| 247 | </Button> |
| 248 | <div className="w-[33%] flex flex-col gap-y-1 text-sm"> |
| 249 | <p>Reset report</p> |
| 250 | <p className="text-xs text-foreground-light"> |
| 251 | Consider resetting the analysis after optimizing any queries |
| 252 | </p> |
| 253 | <Button |
| 254 | type="default" |
| 255 | className="mt-3! w-min" |
| 256 | onClick={() => setShowResetgPgStatStatements(true)} |
| 257 | > |
| 258 | Reset report |
| 259 | </Button> |
| 260 | </div> |
| 261 | |
| 262 | <div className="w-[33%] flex flex-col gap-y-1 text-sm"> |
| 263 | <p>How is this report generated?</p> |
| 264 | <Markdown |
| 265 | className="text-xs" |
| 266 | content={`This report uses the pg_stat_statements table, and pg_stat_statements extension. [Learn more here](${DOCS_URL}/guides/platform/performance#examining-query-performance).`} |
| 267 | /> |
| 268 | </div> |
| 269 | |
| 270 | <div className="w-[33%] flex flex-col gap-y-1 text-sm"> |
| 271 | <p>Inspect your database for potential issues</p> |
| 272 | <Markdown |
| 273 | className="text-xs" |
| 274 | content={`The Briven CLI comes with a range of tools to help inspect your Postgres instances for |
| 275 | potential issues. [Learn more here](${DOCS_URL}/guides/database/inspect).`} |
| 276 | /> |
| 277 | </div> |
| 278 | </div> |
| 279 | |
| 280 | <ConfirmationModal |
| 281 | visible={showResetgPgStatStatements} |
| 282 | size="medium" |
| 283 | variant="destructive" |
| 284 | title="Reset query performance analysis" |
| 285 | confirmLabel="Reset report" |
| 286 | confirmLabelLoading="Resetting report" |
| 287 | onCancel={() => setShowResetgPgStatStatements(false)} |
| 288 | onConfirm={async () => { |
| 289 | const connectionString = databases?.find( |
| 290 | (db) => db.identifier === state.selectedDatabaseId |
| 291 | )?.connectionString |
| 292 | |
| 293 | if (IS_PLATFORM && !connectionString) { |
| 294 | return toast.error('Unable to run query: Connection string is missing') |
| 295 | } |
| 296 | |
| 297 | try { |
| 298 | await executeSql({ |
| 299 | projectRef: project?.ref, |
| 300 | connectionString, |
| 301 | sql: safeSql`SELECT pg_stat_statements_reset();`, |
| 302 | }) |
| 303 | handleRefresh() |
| 304 | setShowResetgPgStatStatements(false) |
| 305 | } catch (error: any) { |
| 306 | toast.error(`Failed to reset analysis: ${error.message}`) |
| 307 | } |
| 308 | }} |
| 309 | > |
| 310 | <p className="text-foreground-light text-sm"> |
| 311 | This will reset the pg_stat_statements table in the extensions schema on your{' '} |
| 312 | <span className="text-foreground"> |
| 313 | {isPrimaryDatabase ? 'primary database' : `read replica (ID: ${formattedDatabaseId})`} |
| 314 | </span> |
| 315 | , which is used to calculate query performance. This data will repopulate immediately |
| 316 | after. |
| 317 | </p> |
| 318 | </ConfirmationModal> |
| 319 | </> |
| 320 | ) |
| 321 | } |