| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321 |
- import { safeSql } from '@supabase/pg-meta/src/pg-format'
- import { LOCAL_STORAGE_KEYS, useParams } from 'common'
- import { RefreshCw, RotateCcw, X } from 'lucide-react'
- import { parseAsString, useQueryStates } from 'nuqs'
- import { useEffect, useMemo, useState } from 'react'
- import { toast } from 'sonner'
- import { Button, cn, LoadingLine } from 'ui'
- import { Admonition } from 'ui-patterns'
- import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal'
- import { Markdown } from '../../Markdown'
- import { captureQueryPerformanceError } from '../QueryPerformance.utils'
- import { QueryPerformanceFilterBar } from '../QueryPerformanceFilterBar'
- import { QueryPerformanceGrid } from '../QueryPerformanceGrid'
- import { QueryPerformanceMetrics } from '../QueryPerformanceMetrics'
- import { QueryPerformanceInfiniteHook } from '../useQueryPerformanceQuery'
- import { transformStatementDataToRows } from './WithStatements.utils'
- import { PresetHookResult } from '@/components/interfaces/Reports/Reports.utils'
- import { ButtonTooltip } from '@/components/ui/ButtonTooltip'
- import { DownloadResultsButton } from '@/components/ui/DownloadResultsButton'
- import { useReadReplicasQuery } from '@/data/read-replicas/replicas-query'
- import { formatDatabaseID } from '@/data/read-replicas/replicas.utils'
- import { executeSql } from '@/data/sql/execute-sql-query'
- import { useInfiniteScroll } from '@/hooks/misc/useInfiniteScroll'
- import { useLocalStorageQuery } from '@/hooks/misc/useLocalStorage'
- import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
- import { DOCS_URL, IS_PLATFORM } from '@/lib/constants'
- import { getErrorMessage } from '@/lib/get-error-message'
- import { useDatabaseSelectorStateSnapshot } from '@/state/database-selector'
- interface WithStatementsProps {
- queryHitRate: PresetHookResult
- queryPerformanceQuery: QueryPerformanceInfiniteHook
- queryMetrics: PresetHookResult
- }
- export const WithStatements = ({
- queryHitRate,
- queryPerformanceQuery,
- queryMetrics,
- }: WithStatementsProps) => {
- const { ref } = useParams()
- const { data: project } = useSelectedProjectQuery()
- const state = useDatabaseSelectorStateSnapshot()
- const {
- data,
- isLoading,
- isRefetching,
- isFetchingNextPage,
- hasNextPage,
- error: queryError,
- fetchNextPage,
- refetch: runQuery,
- } = queryPerformanceQuery
- const isPrimaryDatabase = state.selectedDatabaseId === ref
- const formattedDatabaseId = formatDatabaseID(state.selectedDatabaseId ?? '')
- const hitRateError = 'error' in queryHitRate ? queryHitRate.error : null
- const metricsError = 'error' in queryMetrics ? queryMetrics.error : null
- const mainQueryError = queryError || null
- const [showResetgPgStatStatements, setShowResetgPgStatStatements] = useState(false)
- const [showBottomSection, setShowBottomSection] = useLocalStorageQuery(
- LOCAL_STORAGE_KEYS.QUERY_PERF_SHOW_BOTTOM_SECTION,
- true
- )
- const [{ indexAdvisor }] = useQueryStates({
- indexAdvisor: parseAsString.withDefault('false'),
- })
- const handleRefresh = () => {
- runQuery()
- queryHitRate.runQuery()
- queryMetrics.runQuery()
- }
- const processedData = useMemo(() => {
- return transformStatementDataToRows(data || [], indexAdvisor === 'true')
- }, [data, indexAdvisor])
- const { data: databases } = useReadReplicasQuery({ projectRef: ref })
- const handleScroll = useInfiniteScroll({
- isLoading,
- isFetchingNextPage,
- hasNextPage,
- fetchNextPage,
- })
- useEffect(() => {
- state.setSelectedDatabaseId(ref)
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [ref])
- useEffect(() => {
- if (mainQueryError) {
- const errorMessage = getErrorMessage(mainQueryError)
- const isNotInstalled =
- typeof errorMessage === 'string' &&
- errorMessage.includes('pg_stat_statements') &&
- errorMessage.includes('does not exist')
- if (!isNotInstalled) {
- captureQueryPerformanceError(mainQueryError, {
- projectRef: ref,
- databaseIdentifier: state.selectedDatabaseId,
- queryPreset: 'unified',
- queryType: 'mainQuery',
- postgresVersion: project?.dbVersion,
- databaseType: isPrimaryDatabase ? 'primary' : 'read-replica',
- sql: queryPerformanceQuery.resolvedSql,
- errorMessage: errorMessage || undefined,
- })
- }
- }
- }, [
- mainQueryError,
- ref,
- state.selectedDatabaseId,
- project?.dbVersion,
- isPrimaryDatabase,
- queryPerformanceQuery.resolvedSql,
- ])
- useEffect(() => {
- if (hitRateError) {
- const errorMessage = getErrorMessage(hitRateError)
- captureQueryPerformanceError(hitRateError, {
- projectRef: ref,
- databaseIdentifier: state.selectedDatabaseId,
- queryPreset: 'queryHitRate',
- queryType: 'hitRate',
- postgresVersion: project?.dbVersion,
- databaseType: isPrimaryDatabase ? 'primary' : 'read-replica',
- errorMessage: errorMessage || undefined,
- })
- }
- }, [hitRateError, ref, state.selectedDatabaseId, project?.dbVersion, isPrimaryDatabase])
- useEffect(() => {
- if (metricsError) {
- const errorMessage = getErrorMessage(metricsError)
- captureQueryPerformanceError(metricsError, {
- projectRef: ref,
- databaseIdentifier: state.selectedDatabaseId,
- queryPreset: 'queryMetrics',
- queryType: 'metrics',
- postgresVersion: project?.dbVersion,
- databaseType: isPrimaryDatabase ? 'primary' : 'read-replica',
- errorMessage: errorMessage || undefined,
- })
- }
- }, [metricsError, ref, state.selectedDatabaseId, project?.dbVersion, isPrimaryDatabase])
- const hasError = mainQueryError || hitRateError || metricsError
- const errorMessage = mainQueryError
- ? getErrorMessage(mainQueryError) || 'Failed to load query performance data'
- : hitRateError
- ? getErrorMessage(hitRateError) || 'Failed to load cache hit rate data'
- : metricsError
- ? getErrorMessage(metricsError) || 'Failed to load query metrics'
- : null
- const isPgStatStatementsNotInstalled =
- typeof errorMessage === 'string' &&
- errorMessage.includes('pg_stat_statements') &&
- errorMessage.includes('does not exist')
- return (
- <>
- {hasError && (
- <div className="px-6 pt-4">
- {isPgStatStatementsNotInstalled ? (
- <Admonition
- type="warning"
- title="pg_stat_statements extension is not enabled"
- description="Query Performance requires the pg_stat_statements extension. Enable it in Database → Extensions."
- />
- ) : (
- <Admonition
- type="destructive"
- title="Error loading query performance data"
- description={
- errorMessage ||
- 'An error occurred while loading query performance data. Please try refreshing the page.'
- }
- />
- )}
- </div>
- )}
- <QueryPerformanceMetrics />
- <QueryPerformanceFilterBar
- showRolesFilter
- showSourceFilter
- actions={
- <>
- <ButtonTooltip
- type="default"
- size="tiny"
- icon={<RefreshCw />}
- onClick={handleRefresh}
- tooltip={{ content: { side: 'top', text: 'Refresh' } }}
- className="w-[26px]"
- />
- <ButtonTooltip
- type="default"
- size="tiny"
- icon={<RotateCcw />}
- onClick={() => setShowResetgPgStatStatements(true)}
- tooltip={{ content: { side: 'top', text: 'Reset report' } }}
- className="w-[26px]"
- />
- <DownloadResultsButton
- results={processedData}
- fileName={`Briven Query Performance Statements (${ref})`}
- align="end"
- />
- </>
- }
- />
- <LoadingLine loading={isLoading || isRefetching || isFetchingNextPage} />
- <QueryPerformanceGrid
- aggregatedData={processedData}
- isLoading={isLoading}
- error={
- mainQueryError
- ? getErrorMessage(mainQueryError) || 'Failed to load query performance data'
- : null
- }
- onRetry={handleRefresh}
- onScroll={handleScroll}
- />
- <div
- className={cn('px-6 py-6 flex gap-x-4 border-t relative', {
- hidden: showBottomSection === false,
- })}
- >
- <Button
- className="absolute top-1.5 right-3 px-1.5"
- type="text"
- size="tiny"
- onClick={() => setShowBottomSection(false)}
- >
- <X size="14" />
- </Button>
- <div className="w-[33%] flex flex-col gap-y-1 text-sm">
- <p>Reset report</p>
- <p className="text-xs text-foreground-light">
- Consider resetting the analysis after optimizing any queries
- </p>
- <Button
- type="default"
- className="mt-3! w-min"
- onClick={() => setShowResetgPgStatStatements(true)}
- >
- Reset report
- </Button>
- </div>
- <div className="w-[33%] flex flex-col gap-y-1 text-sm">
- <p>How is this report generated?</p>
- <Markdown
- className="text-xs"
- 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).`}
- />
- </div>
- <div className="w-[33%] flex flex-col gap-y-1 text-sm">
- <p>Inspect your database for potential issues</p>
- <Markdown
- className="text-xs"
- content={`The Briven CLI comes with a range of tools to help inspect your Postgres instances for
- potential issues. [Learn more here](${DOCS_URL}/guides/database/inspect).`}
- />
- </div>
- </div>
- <ConfirmationModal
- visible={showResetgPgStatStatements}
- size="medium"
- variant="destructive"
- title="Reset query performance analysis"
- confirmLabel="Reset report"
- confirmLabelLoading="Resetting report"
- onCancel={() => setShowResetgPgStatStatements(false)}
- onConfirm={async () => {
- const connectionString = databases?.find(
- (db) => db.identifier === state.selectedDatabaseId
- )?.connectionString
- if (IS_PLATFORM && !connectionString) {
- return toast.error('Unable to run query: Connection string is missing')
- }
- try {
- await executeSql({
- projectRef: project?.ref,
- connectionString,
- sql: safeSql`SELECT pg_stat_statements_reset();`,
- })
- handleRefresh()
- setShowResetgPgStatStatements(false)
- } catch (error: any) {
- toast.error(`Failed to reset analysis: ${error.message}`)
- }
- }}
- >
- <p className="text-foreground-light text-sm">
- This will reset the pg_stat_statements table in the extensions schema on your{' '}
- <span className="text-foreground">
- {isPrimaryDatabase ? 'primary database' : `read replica (ID: ${formattedDatabaseId})`}
- </span>
- , which is used to calculate query performance. This data will repopulate immediately
- after.
- </p>
- </ConfirmationModal>
- </>
- )
- }
|