feat(studio): surface index advisor indicators (#40788)

* feat: change the check to show index advisor tab at all times

* fix: hide add to log drains on export menu in query perf

* fix: small fallback for pathname check

* fix: query perf header block responsiveness

* feat: admonition for index advisor

* fix: add aria-describedby to query perf sheet

* feat: proper way to do sheet description

* chore: better title spacing in panel

* fix: indexes in use empty state

* fix: key in observability menu

* feat: better highlighting of index advisor issues

* feat: add docs button to empty indexes tab

* feat: remove unused code

* feat: use button tooltips for reset and refresh to gain space

* feat: add dismiss to index advisor banner

* feat: add warnings filter to query perf

* feat: filter all queries for warnings

* fix: selected state for warning rows

* fix: fallback for isLogs check

* fix: other instance of download button

---------

Co-authored-by: Ali Waseem <waseema393@gmail.com>
This commit is contained in:
kemal.earth
2025-11-27 15:20:07 +00:00
committed by GitHub
parent 4bfe7d0480
commit 511b6faada
16 changed files with 290 additions and 169 deletions

View File

@@ -1,7 +1,6 @@
import { useState } from 'react'
import { toast } from 'sonner'
import { useIndexAdvisorStatus } from 'components/interfaces/QueryPerformance/hooks/useIsIndexAdvisorStatus'
import { useDatabaseExtensionEnableMutation } from 'data/database-extensions/database-extension-enable-mutation'
import { useDatabaseExtensionsQuery } from 'data/database-extensions/database-extensions-query'
import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject'
@@ -17,18 +16,12 @@ import {
AlertDialogTrigger,
Badge,
Button,
InfoIcon,
Tooltip,
TooltipContent,
TooltipTrigger,
} from 'ui'
import { getIndexAdvisorExtensions } from './index-advisor.utils'
export const EnableIndexAdvisorButton = () => {
const { data: project } = useSelectedProjectQuery()
const { isIndexAdvisorAvailable, isIndexAdvisorEnabled } = useIndexAdvisorStatus()
const [isDialogOpen, setIsDialogOpen] = useState(false)
const { data: extensions } = useDatabaseExtensionsQuery({
@@ -72,21 +65,11 @@ export const EnableIndexAdvisorButton = () => {
}
}
// if index_advisor is already enabled or not available to install, show nothing
if (!isIndexAdvisorAvailable || isIndexAdvisorEnabled) return null
return (
<AlertDialog open={isDialogOpen} onOpenChange={() => setIsDialogOpen(!isDialogOpen)}>
<Tooltip>
<TooltipTrigger asChild>
<AlertDialogTrigger asChild>
<Button type="outline" className={`rounded-full`} icon={<InfoIcon />}>
Enable Index Advisor
</Button>
</AlertDialogTrigger>
</TooltipTrigger>
<TooltipContent side="top">Recommends indexes to improve query performance</TooltipContent>
</Tooltip>
<AlertDialogTrigger asChild>
<Button type="primary">Enable</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>

View File

@@ -0,0 +1,60 @@
import { useIndexAdvisorStatus } from 'components/interfaces/QueryPerformance/hooks/useIsIndexAdvisorStatus'
import { BASE_PATH } from 'lib/constants'
import { Admonition } from 'ui-patterns'
import { EnableIndexAdvisorButton } from './EnableIndexAdvisorButton'
import { useLocalStorageQuery } from 'hooks/misc/useLocalStorage'
import { LOCAL_STORAGE_KEYS } from 'common'
import { useParams } from 'common/hooks'
import { Button } from 'ui'
export const IndexAdvisorNotice = () => {
const { ref } = useParams()
const { isIndexAdvisorAvailable, isIndexAdvisorEnabled } = useIndexAdvisorStatus()
const [isDismissed, setIsDismissed] = useLocalStorageQuery(
LOCAL_STORAGE_KEYS.INDEX_ADVISOR_NOTICE_DISMISSED(ref ?? ''),
false
)
if (!isIndexAdvisorAvailable || isIndexAdvisorEnabled || isDismissed) return null
return (
<div className="px-6">
<Admonition showIcon={false} type="tip" className="relative overflow-hidden mb-4">
<div className="absolute -inset-16 z-0 opacity-50">
<img
src={`${BASE_PATH}/img/reports/bg-grafana-dark.svg`}
alt="Index Advisor"
className="w-full h-full object-cover object-right hidden dark:block"
/>
<img
src={`${BASE_PATH}/img/reports/bg-grafana-light.svg`}
alt="Index Advisor"
className="w-full h-full object-cover object-right dark:hidden"
/>
<div className="absolute inset-0 bg-gradient-to-r from-background-alternative to-transparent" />
</div>
<div className="relative z-10 flex flex-col md:flex-row md:items-center gap-y-2 md:gap-x-8 justify-between px-2 py-1">
<div className="flex flex-col gap-y-0.5">
<div className="flex flex-col gap-y-2 items-start">
<p className="text-sm font-medium">Enable Index Advisor</p>
</div>
<p className="text-sm text-foreground-lighter text-balance">
Recommends indexes to improve query performance.
</p>
</div>
<div className="flex items-center gap-x-2">
<Button
type="default"
size="tiny"
onClick={() => setIsDismissed(true)}
aria-label="Dismiss notification"
>
Dismiss
</Button>
<EnableIndexAdvisorButton />
</div>
</div>
</Admonition>
</div>
)
}

View File

@@ -32,6 +32,9 @@ import {
createIndexes,
hasIndexRecommendations,
} from './IndexAdvisor/index-advisor.utils'
import { EnableIndexAdvisorButton } from './IndexAdvisor/EnableIndexAdvisorButton'
import { DocsButton } from 'components/ui/DocsButton'
import { DOCS_URL } from 'lib/constants'
interface QueryIndexesProps {
selectedRow: any
@@ -122,11 +125,30 @@ export const QueryIndexes = ({ selectedRow }: QueryIndexesProps) => {
}
}
if (!isLoadingExtensions && !isIndexAdvisorEnabled) {
return (
<QueryPanelContainer className="h-full">
<QueryPanelSection className="pt-2">
<div className="border rounded border-dashed flex flex-col items-center justify-center py-4 px-12 gap-y-1 text-center">
<p className="text-sm text-foreground-light">Enable Index Advisor</p>
<p className="text-center text-xs text-foreground-lighter mb-2">
Recommends indexes to improve query performance.
</p>
<div className="flex items-center gap-x-2">
<DocsButton href={`${DOCS_URL}/guides/database/extensions/index_advisor`} />
<EnableIndexAdvisorButton />
</div>
</div>
</QueryPanelSection>
</QueryPanelContainer>
)
}
return (
<QueryPanelContainer className="h-full">
<QueryPanelSection className="pt-2 mb-6">
<div className="mb-4 flex flex-col gap-y-1">
<h4>Indexes in use</h4>
<h4 className="mb-2">Indexes in use</h4>
<p className="text-sm text-foreground-light">
This query is using the following index{(usedIndexes ?? []).length > 1 ? 's' : ''}:
</p>
@@ -142,7 +164,7 @@ export const QueryIndexes = ({ selectedRow }: QueryIndexesProps) => {
{isSuccess && (
<div>
{usedIndexes.length === 0 && (
<div className="border rounded border-dashed flex flex-col items-center justify-center py-4 px-20 gap-y-1">
<div className="border rounded border-dashed flex flex-col items-center justify-center py-4 px-12 gap-y-1 text-center">
<p className="text-sm text-foreground-light">
No indexes are involved in this query
</p>
@@ -173,7 +195,7 @@ export const QueryIndexes = ({ selectedRow }: QueryIndexesProps) => {
</QueryPanelSection>
<QueryPanelSection className="flex flex-col gap-y-6 py-6 border-t">
<div className="flex flex-col gap-y-1">
<h4>New index recommendations</h4>
<h4 className="mb-2">New index recommendations</h4>
{isLoadingExtensions ? (
<GenericSkeletonLoader />
) : !isIndexAdvisorEnabled ? (
@@ -252,8 +274,8 @@ export const QueryIndexes = ({ selectedRow }: QueryIndexesProps) => {
<>
<QueryPanelSection className="py-6 border-t">
<div className="flex flex-col gap-y-1">
<h4>Query costs</h4>
<div className="border rounded-md flex flex-col bg-surface-100 mt-3">
<h4 className="mb-2">Query costs</h4>
<div className="border rounded-md flex flex-col bg-surface-100">
<QueryPanelScoreSection
name="Total cost of query"
description="An estimate of how long it will take to return all the rows (Includes start up cost)"
@@ -280,7 +302,7 @@ export const QueryIndexes = ({ selectedRow }: QueryIndexesProps) => {
</QueryPanelSection>
<QueryPanelSection className="py-6 border-t">
<div className="flex flex-col gap-y-2">
<h4>FAQ</h4>
<h4 className="mb-2">FAQ</h4>
<Accordion_Shadcn_ collapsible type="single" className="border rounded-md">
<AccordionItem_Shadcn_ value="1">
<AccordionTrigger className="px-4 py-3 text-sm font-normal text-foreground-light hover:text-foreground transition [&[data-state=open]]:text-foreground">

View File

@@ -335,7 +335,7 @@ export const QueryPerformanceChart = ({
</TabsList_Shadcn_>
<TabsContent_Shadcn_ value={selectedMetric} className="bg-surface-200 mt-0 h-inherit">
<div className="w-full flex items-center justify-center min-h-[320px]">
<div className="w-full flex items-center justify-center min-h-[282px]">
{isLoading ? (
<Loader2 size={20} className="animate-spin text-foreground-lighter" />
) : error ? (
@@ -375,7 +375,7 @@ export const QueryPerformanceChart = ({
tickFormatter: getYAxisFormatter,
}}
xAxisIsDate={true}
className="mt-6"
className="mt-2"
/>
</div>
)}

View File

@@ -13,6 +13,7 @@ import {
ReportsNumericFilter,
NumericFilter,
} from 'components/interfaces/Reports/v2/ReportsNumericFilter'
import { useIndexAdvisorStatus } from './hooks/useIsIndexAdvisorStatus'
export const QueryPerformanceFilterBar = ({
actions,
@@ -23,16 +24,20 @@ export const QueryPerformanceFilterBar = ({
}) => {
const { data: project } = useSelectedProjectQuery()
const { sort, clearSort } = useQueryPerformanceSort()
const { isIndexAdvisorEnabled } = useIndexAdvisorStatus()
const [{ search: searchQuery, roles: defaultFilterRoles, callsFilter }, setSearchParams] =
useQueryStates({
search: parseAsString.withDefault(''),
roles: parseAsArrayOf(parseAsString).withDefault([]),
callsFilter: parseAsJson((value) => value as NumericFilter | null).withDefault({
operator: '>=',
value: 0,
} as NumericFilter),
})
const [
{ search: searchQuery, roles: defaultFilterRoles, callsFilter, indexAdvisor },
setSearchParams,
] = useQueryStates({
search: parseAsString.withDefault(''),
roles: parseAsArrayOf(parseAsString).withDefault([]),
callsFilter: parseAsJson((value) => value as NumericFilter | null).withDefault({
operator: '>=',
value: 0,
} as NumericFilter),
indexAdvisor: parseAsString.withDefault('false'),
})
const { data, isLoading: isLoadingRoles } = useDatabaseRolesQuery({
projectRef: project?.ref,
connectionString: project?.connectionString,
@@ -56,11 +61,17 @@ export const QueryPerformanceFilterBar = ({
setSearchParams({ roles })
}
const onIndexAdvisorChange = (options: string[]) => {
setSearchParams({ indexAdvisor: options.includes('true') ? 'true' : 'false' })
}
useEffect(() => {
onSearchQueryChange(searchValue)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [searchValue])
const indexAdvisorOptions = [{ value: 'true', label: 'Index Advisor' }]
return (
<div className="px-4 py-1.5 bg-surface-200 border-t -mt-px flex justify-between items-center overflow-x-auto overflow-y-hidden w-full flex-shrink-0">
<div className="flex items-center gap-x-4">
@@ -111,6 +122,18 @@ export const QueryPerformanceFilterBar = ({
/>
)}
{isIndexAdvisorEnabled && (
<FilterPopover
name="Warnings"
options={indexAdvisorOptions}
labelKey="label"
valueKey="value"
activeOptions={indexAdvisor === 'true' ? ['true'] : []}
onSaveFilters={onIndexAdvisorChange}
className="w-56"
/>
)}
{sort && (
<div className="text-xs border rounded-md px-1.5 md:px-2.5 py-1 h-[26px] flex items-center gap-x-2">
<p className="md:inline-flex gap-x-1 hidden truncate">

View File

@@ -13,6 +13,7 @@ import {
DropdownMenuTrigger,
Sheet,
SheetContent,
SheetDescription,
SheetTitle,
TabsContent_Shadcn_,
TabsList_Shadcn_,
@@ -40,8 +41,8 @@ import { NumericFilter } from 'components/interfaces/Reports/v2/ReportsNumericFi
interface QueryPerformanceGridProps {
aggregatedData: QueryPerformanceRow[]
isLoading: boolean
currentSelectedQuery?: string | null // Make optional
onCurrentSelectQuery?: (query: string) => void // Make optional
currentSelectedQuery?: string | null
onCurrentSelectQuery?: (query: string) => void
}
const calculateTimeConsumedWidth = (data: QueryPerformanceRow[]) => {
@@ -397,14 +398,6 @@ export const QueryPerformanceGrid = ({
return data
}, [aggregatedData, sort, search, roles, callsFilter])
const selectedQuery = selectedRow !== undefined ? reportData[selectedRow]?.query : undefined
const query = (selectedQuery ?? '').trim().toLowerCase()
const showIndexSuggestions =
(query.startsWith('select') ||
query.startsWith('with pgrst_source') ||
query.startsWith('with pgrst_payload')) &&
hasIndexRecommendations(reportData[selectedRow!]?.index_advisor_result, true)
useEffect(() => {
setSelectedRow(undefined)
}, [search, roles, urlSort, order, callsFilter])
@@ -460,10 +453,14 @@ export const QueryPerformanceGrid = ({
const isSelected = idx === selectedRow
const query = reportData[idx]?.query
const isCharted = currentSelectedQuery ? currentSelectedQuery === query : false
const hasRecommendations = hasIndexRecommendations(
reportData[idx]?.index_advisor_result,
true
)
return [
`${isSelected ? 'bg-surface-300 dark:bg-surface-300' : 'bg-200'} cursor-pointer`,
`${isSelected ? '[&>div:first-child]:border-l-4 border-l-secondary [&>div]:!border-l-foreground' : ''}`,
`${isSelected ? (hasRecommendations ? 'bg-warning/10 hover:bg-warning/20' : 'bg-surface-300 dark:bg-surface-300') : hasRecommendations ? 'bg-warning/10 hover:bg-warning/20' : 'bg-200 hover:bg-surface-200'} cursor-pointer`,
`${isSelected ? (hasRecommendations ? '[&>div:first-child]:border-l-4 border-l-warning [&>div]:border-l-warning' : '[&>div:first-child]:border-l-4 border-l-secondary [&>div]:!border-l-foreground') : ''}`,
`${isCharted ? 'bg-surface-200 dark:bg-surface-200' : ''}`,
`${isCharted ? '[&>div:first-child]:border-l-4 border-l-secondary [&>div]:border-l-brand' : ''}`,
'[&>.rdg-cell]:box-border [&>.rdg-cell]:outline-none [&>.rdg-cell]:shadow-none',
@@ -489,7 +486,11 @@ export const QueryPerformanceGrid = ({
} else {
// Otherwise, open the detail panel
setSelectedRow(idx)
setView('details')
const hasRecommendations = hasIndexRecommendations(
reportData[idx]?.index_advisor_result,
true
)
setView(hasRecommendations ? 'suggestion' : 'details')
gridRef.current?.scrollToCell({ idx: 0, rowIdx: idx })
}
}
@@ -526,6 +527,9 @@ export const QueryPerformanceGrid = ({
modal={false}
>
<SheetTitle className="sr-only">Query details</SheetTitle>
<SheetDescription className="sr-only">
Query Performance Details &amp; Indexes
</SheetDescription>
<SheetContent
side="right"
className="flex flex-col h-full bg-studio border-l lg:!w-[calc(100vw-802px)] max-w-[700px] w-full"
@@ -549,14 +553,12 @@ export const QueryPerformanceGrid = ({
>
Query details
</TabsTrigger_Shadcn_>
{showIndexSuggestions && (
<TabsTrigger_Shadcn_
value="suggestion"
className="px-0 pb-0 data-[state=active]:bg-transparent !shadow-none"
>
Indexes
</TabsTrigger_Shadcn_>
)}
<TabsTrigger_Shadcn_
value="suggestion"
className="px-0 pb-0 data-[state=active]:bg-transparent !shadow-none"
>
Indexes
</TabsTrigger_Shadcn_>
</TabsList_Shadcn_>
</div>

View File

@@ -14,6 +14,7 @@ import {
} from './WithMonitor.utils'
import { useParams } from 'common'
import { DownloadResultsButton } from 'components/ui/DownloadResultsButton'
import { IndexAdvisorNotice } from '../IndexAdvisor/IndexAdvisorNotice'
dayjs.extend(utc)
@@ -81,6 +82,7 @@ export const WithMonitor = ({ dateRange, onDateRangeChange }: WithMonitorProps)
return (
<>
<IndexAdvisorNotice />
<QueryPerformanceChart
dateRange={dateRange}
onDateRangeChange={onDateRangeChange}

View File

@@ -21,6 +21,8 @@ import { QueryPerformanceFilterBar } from '../QueryPerformanceFilterBar'
import { QueryPerformanceGrid } from '../QueryPerformanceGrid'
import { transformStatementDataToRows } from './WithStatements.utils'
import { DownloadResultsButton } from 'components/ui/DownloadResultsButton'
import { IndexAdvisorNotice } from '../IndexAdvisor/IndexAdvisorNotice'
import { ButtonTooltip } from 'components/ui/ButtonTooltip'
interface WithStatementsProps {
queryHitRate: PresetHookResult
@@ -66,28 +68,29 @@ export const WithStatements = ({
return (
<>
<IndexAdvisorNotice />
<QueryPerformanceMetrics />
<QueryPerformanceFilterBar
showRolesFilter={true}
actions={
<>
<Button
<ButtonTooltip
type="default"
size="tiny"
icon={<RefreshCw />}
onClick={handleRefresh}
loading={isRefetching}
>
Refresh
</Button>
<Button
tooltip={{ content: { side: 'top', text: 'Refresh' } }}
className="w-[26px]"
/>
<ButtonTooltip
type="default"
size="tiny"
icon={<RotateCcw />}
onClick={() => setShowResetgPgStatStatements(true)}
>
Reset report
</Button>
tooltip={{ content: { side: 'top', text: 'Reset report' } }}
className="w-[26px]"
/>
<DownloadResultsButton
results={processedData}
fileName={`Supabase Query Performance Statements (${ref})`}

View File

@@ -360,7 +360,7 @@ limit 12
queries: {
mostFrequentlyInvoked: {
queryType: 'db',
sql: (_params, where, orderBy, runIndexAdvisor = false) => `
sql: (_params, where, orderBy, runIndexAdvisor = false, filterIndexAdvisor = false) => `
-- reports-query-performance-most-frequently-invoked
set search_path to public, extensions;
@@ -416,7 +416,7 @@ select
},
mostTimeConsuming: {
queryType: 'db',
sql: (_, where, orderBy, runIndexAdvisor = false) => `
sql: (_, where, orderBy, runIndexAdvisor = false, filterIndexAdvisor = false) => `
-- reports-query-performance-most-time-consuming
set search_path to public, extensions;
@@ -454,7 +454,7 @@ select
},
slowestExecutionTime: {
queryType: 'db',
sql: (_params, where, orderBy, runIndexAdvisor = false) => `
sql: (_params, where, orderBy, runIndexAdvisor = false, filterIndexAdvisor = false) => `
-- reports-query-performance-slowest-execution-time
set search_path to public, extensions;
@@ -513,59 +513,68 @@ select
},
unified: {
queryType: 'db',
sql: (_params, where, orderBy, runIndexAdvisor = false) => `
sql: (_params, where, orderBy, runIndexAdvisor = false, filterIndexAdvisor = false) => {
const baseQuery = `
-- reports-query-performance-unified
set search_path to public, extensions;
select
auth.rolname,
statements.query,
statements.calls,
-- -- Postgres 13, 14, 15
statements.total_exec_time + statements.total_plan_time as total_time,
statements.min_exec_time + statements.min_plan_time as min_time,
statements.max_exec_time + statements.max_plan_time as max_time,
statements.mean_exec_time + statements.mean_plan_time as mean_time,
-- -- Postgres <= 12
-- total_time,
-- min_time,
-- max_time,
-- mean_time,
statements.rows / statements.calls as avg_rows,
statements.rows as rows_read,
statements.shared_blks_hit as debug_hit,
statements.shared_blks_read as debug_read,
case
when (statements.shared_blks_hit + statements.shared_blks_read) > 0
then (statements.shared_blks_hit::numeric * 100.0) /
(statements.shared_blks_hit + statements.shared_blks_read)
else 0
end as cache_hit_rate,
((statements.total_exec_time + statements.total_plan_time)/sum(statements.total_exec_time + statements.total_plan_time) OVER()) * 100 as prop_total_time${
runIndexAdvisor
? `,
case
when (lower(statements.query) like 'select%' or lower(statements.query) like 'with pgrst%')
then (
select json_build_object(
'has_suggestion', array_length(index_statements, 1) > 0,
'startup_cost_before', startup_cost_before,
'startup_cost_after', startup_cost_after,
'total_cost_before', total_cost_before,
'total_cost_after', total_cost_after,
'index_statements', index_statements
with query_results as (
select
auth.rolname,
statements.query,
statements.calls,
-- -- Postgres 13, 14, 15
statements.total_exec_time + statements.total_plan_time as total_time,
statements.min_exec_time + statements.min_plan_time as min_time,
statements.max_exec_time + statements.max_plan_time as max_time,
statements.mean_exec_time + statements.mean_plan_time as mean_time,
-- -- Postgres <= 12
-- total_time,
-- min_time,
-- max_time,
-- mean_time,
statements.rows / statements.calls as avg_rows,
statements.rows as rows_read,
statements.shared_blks_hit as debug_hit,
statements.shared_blks_read as debug_read,
case
when (statements.shared_blks_hit + statements.shared_blks_read) > 0
then (statements.shared_blks_hit::numeric * 100.0) /
(statements.shared_blks_hit + statements.shared_blks_read)
else 0
end as cache_hit_rate,
((statements.total_exec_time + statements.total_plan_time)/sum(statements.total_exec_time + statements.total_plan_time) OVER()) * 100 as prop_total_time${
runIndexAdvisor
? `,
case
when (lower(statements.query) like 'select%' or lower(statements.query) like 'with pgrst%')
then (
select json_build_object(
'has_suggestion', array_length(index_statements, 1) > 0,
'startup_cost_before', startup_cost_before,
'startup_cost_after', startup_cost_after,
'total_cost_before', total_cost_before,
'total_cost_after', total_cost_after,
'index_statements', index_statements
)
from index_advisor(statements.query)
)
from index_advisor(statements.query)
)
else null
end as index_advisor_result`
: ''
}
from pg_stat_statements as statements
inner join pg_authid as auth on statements.userid = auth.oid
${where || ''}
${orderBy || 'order by statements.total_exec_time + statements.total_plan_time desc'}
limit 20`,
else null
end as index_advisor_result`
: ''
}
from pg_stat_statements as statements
inner join pg_authid as auth on statements.userid = auth.oid
${where || ''}
)
select *
from query_results
${filterIndexAdvisor && runIndexAdvisor ? `where (index_advisor_result->>'has_suggestion')::boolean = true` : ''}
${orderBy || 'order by total_time desc'}
limit 20`
return baseQuery
},
},
slowQueriesCount: {
queryType: 'db',
@@ -580,7 +589,7 @@ select
},
queryMetrics: {
queryType: 'db',
sql: (_params, where, orderBy, runIndexAdvisor = false) => `
sql: (_params, where, orderBy, runIndexAdvisor = false, filterIndexAdvisor = false) => `
-- reports-query-performance-metrics
set search_path to public, extensions;

View File

@@ -30,6 +30,7 @@ export type QueryPerformanceQueryOpts = {
roles?: string[]
runIndexAdvisor?: boolean
minCalls?: number
filterIndexAdvisor?: boolean
}
export const useQueryPerformanceQuery = ({
@@ -39,6 +40,7 @@ export const useQueryPerformanceQuery = ({
roles,
runIndexAdvisor = false,
minCalls,
filterIndexAdvisor = false,
}: QueryPerformanceQueryOpts) => {
const queryPerfQueries = PRESET_CONFIG[Presets.QUERY_PERFORMANCE]
const baseSQL = queryPerfQueries.queries[preset]
@@ -58,7 +60,8 @@ export const useQueryPerformanceQuery = ({
[],
whereSql.length > 0 ? `WHERE ${whereSql}` : undefined,
orderBySql,
runIndexAdvisor
runIndexAdvisor,
filterIndexAdvisor
)
return useDbQuery({
sql,

View File

@@ -25,7 +25,8 @@ export interface ReportQuery {
filters: ReportFilterItem[],
where?: string,
orderBy?: string,
runIndexAdvisor?: boolean
runIndexAdvisor?: boolean,
filterIndexAdvisor?: boolean
) => string
}

View File

@@ -1,11 +1,12 @@
import saveAs from 'file-saver'
import { Download } from 'lucide-react'
import { Download, Settings } from 'lucide-react'
import Link from 'next/link'
import Papa from 'papaparse'
import { useEffect, useState } from 'react'
import { toast } from 'sonner'
import { useParams } from 'common'
import { usePathname } from 'next/navigation'
import { IS_PLATFORM, useParams } from 'common'
import { ButtonTooltip } from 'components/ui/ButtonTooltip'
import { useGetUnifiedLogsMutation } from 'data/logs/get-unified-logs'
import {
@@ -38,6 +39,8 @@ interface DownloadLogsButtonProps {
export const DownloadLogsButton = ({ searchParameters }: DownloadLogsButtonProps) => {
const { ref } = useParams()
const pathname = usePathname()
const isLogs = pathname?.includes?.('/logs') ?? false
const [numRows, setNumRows] = useState(DEFAULT_NUM_ROWS)
const [numHours, setNumHours] = useState(DEFAULT_NUM_ROWS)
const [selectedFormat, setSelectedFormat] = useState<'csv' | 'json'>()
@@ -98,16 +101,21 @@ export const DownloadLogsButton = ({ searchParameters }: DownloadLogsButtonProps
tooltip={{ content: { side: 'bottom', text: 'Download logs' } }}
/>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-36">
<DropdownMenuItem asChild className="gap-x-2">
<Link href={`/project/${ref}/settings/log-drains`}>
<p>Add a Log Drain</p>
</Link>
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setSelectedFormat('csv')}>
<DropdownMenuContent align="end" className="w-44">
{isLogs && IS_PLATFORM && (
<DropdownMenuItem asChild className="gap-x-2">
<Link href={`/project/${ref}/settings/log-drains`}>
<Settings size={14} />
<p>Add a Log Drain</p>
</Link>
</DropdownMenuItem>
)}
<DropdownMenuItem onClick={() => setSelectedFormat('csv')} className="gap-x-2">
<Download size={14} />
<p>Download as CSV</p>
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setSelectedFormat('json')}>
<DropdownMenuItem onClick={() => setSelectedFormat('json')} className="gap-x-2">
<Download size={14} />
<p>Download as JSON</p>
</DropdownMenuItem>
</DropdownMenuContent>

View File

@@ -2,7 +2,7 @@ import { PermissionAction } from '@supabase/shared-types/out/constants'
import { Plus } from 'lucide-react'
import Link from 'next/link'
import { useRouter } from 'next/router'
import { useMemo, useState } from 'react'
import { Fragment, useMemo, useState } from 'react'
import { toast } from 'sonner'
import { useFlag, useParams } from 'common'
@@ -275,9 +275,9 @@ const ObservabilityMenu = () => {
</div>
{menuItems.map((item, idx) => (
<>
<Fragment key={idx}>
<div className="h-px w-full bg-border-overlay first:hidden" />
<div key={item.key + '-menu-group'}>
<div>
{item.items ? (
<div className="px-2">
<Menu.Group title={<span className="uppercase font-mono">{item.title}</span>} />
@@ -305,7 +305,7 @@ const ObservabilityMenu = () => {
</div>
) : null}
</div>
</>
</Fragment>
))}
<UpdateCustomReportModal

View File

@@ -6,6 +6,8 @@ import { useMemo } from 'react'
import { toast } from 'sonner'
import Link from 'next/link'
import { useParams } from 'common'
import { usePathname } from 'next/navigation'
import { IS_PLATFORM } from 'common'
import {
Button,
@@ -40,6 +42,8 @@ export const DownloadResultsButton = ({
onCopyAsJSON,
}: DownloadResultsButtonProps) => {
const { ref } = useParams()
const pathname = usePathname()
const isLogs = pathname?.includes?.('/logs') ?? false
// [Joshen] Ensure JSON values are stringified for CSV and Markdown
const formattedResults = results.map((row) => {
const r = { ...row }
@@ -109,12 +113,14 @@ export const DownloadResultsButton = ({
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align={align} className="w-44">
<DropdownMenuItem asChild className="gap-x-2">
<Link href={`/project/${ref}/settings/log-drains`}>
<Settings size={14} />
<p>Add a Log Drain</p>
</Link>
</DropdownMenuItem>
{isLogs && IS_PLATFORM && (
<DropdownMenuItem asChild className="gap-x-2">
<Link href={`/project/${ref}/settings/log-drains`}>
<Settings size={14} />
<p>Add a Log Drain</p>
</Link>
</DropdownMenuItem>
)}
<DropdownMenuItem onClick={copyAsMarkdown} className="gap-x-2">
<Copy size={14} />
<p>Copy as markdown</p>

View File

@@ -3,7 +3,6 @@ import { parseAsArrayOf, parseAsInteger, parseAsString, useQueryStates } from 'n
import { useParams } from 'common'
import { useIndexAdvisorStatus } from 'components/interfaces/QueryPerformance/hooks/useIsIndexAdvisorStatus'
import { useQueryPerformanceSort } from 'components/interfaces/QueryPerformance/hooks/useQueryPerformanceSort'
import { EnableIndexAdvisorButton } from 'components/interfaces/QueryPerformance/IndexAdvisor/EnableIndexAdvisorButton'
import { QueryPerformance } from 'components/interfaces/QueryPerformance/QueryPerformance'
import {
PRESET_CONFIG,
@@ -17,7 +16,6 @@ import DefaultLayout from 'components/layouts/DefaultLayout'
import ObservabilityLayout from 'components/layouts/ObservabilityLayout/ObservabilityLayout'
import DatabaseSelector from 'components/ui/DatabaseSelector'
import { DocsButton } from 'components/ui/DocsButton'
import { FormHeader } from 'components/ui/Forms/FormHeader'
import { useReportDateRange } from 'hooks/misc/useReportDateRange'
import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject'
import { DOCS_URL } from 'lib/constants'
@@ -37,12 +35,13 @@ const QueryPerformanceReport: NextPageWithLayout = () => {
handleDatePickerChange,
} = useReportDateRange(REPORT_DATERANGE_HELPER_LABELS.LAST_60_MINUTES)
const [{ search: searchQuery, roles, minCalls }] = useQueryStates({
const [{ search: searchQuery, roles, minCalls, indexAdvisor }] = useQueryStates({
sort: parseAsString,
order: parseAsString,
search: parseAsString.withDefault(''),
roles: parseAsArrayOf(parseAsString).withDefault([]),
minCalls: parseAsInteger,
indexAdvisor: parseAsString.withDefault('false'),
})
const config = PRESET_CONFIG[Presets.QUERY_PERFORMANCE]
@@ -57,37 +56,34 @@ const QueryPerformanceReport: NextPageWithLayout = () => {
roles,
runIndexAdvisor: isIndexAdvisorEnabled,
minCalls: minCalls ?? undefined,
filterIndexAdvisor: indexAdvisor === 'true',
})
const isPgStatMonitorEnabled = project?.dbVersion === '17.4.1.076-psml-1'
return (
<div className="h-full flex flex-col">
<FormHeader
className="py-4 px-6 !mb-0 md:flex-row flex-col"
title="Query Performance"
actions={
<div className="flex items-center gap-2 flex-wrap">
<EnableIndexAdvisorButton />
<DocsButton
href={`${DOCS_URL}/guides/platform/performance#examining-query-performance`}
<div className="w-full mb-0 flex lg:items-center justify-between gap-4 py-4 px-6 lg:flex-row flex-col">
<h3 className="text-foreground text-xl prose">Query Performance</h3>
<div className="flex items-center gap-2 flex-wrap">
<DocsButton
href={`${DOCS_URL}/guides/platform/performance#examining-query-performance`}
/>
<DatabaseSelector />
{isPgStatMonitorEnabled && (
<LogsDatePicker
value={datePickerValue}
helpers={datePickerHelpers.filter(
(h) =>
h.text === REPORT_DATERANGE_HELPER_LABELS.LAST_60_MINUTES ||
h.text === REPORT_DATERANGE_HELPER_LABELS.LAST_3_HOURS ||
h.text === REPORT_DATERANGE_HELPER_LABELS.LAST_24_HOURS
)}
onSubmit={handleDatePickerChange}
/>
<DatabaseSelector />
{isPgStatMonitorEnabled && (
<LogsDatePicker
value={datePickerValue}
helpers={datePickerHelpers.filter(
(h) =>
h.text === REPORT_DATERANGE_HELPER_LABELS.LAST_60_MINUTES ||
h.text === REPORT_DATERANGE_HELPER_LABELS.LAST_3_HOURS ||
h.text === REPORT_DATERANGE_HELPER_LABELS.LAST_24_HOURS
)}
onSubmit={handleDatePickerChange}
/>
)}
</div>
}
/>
)}
</div>
</div>
<QueryPerformance
queryHitRate={queryHitRate}
queryPerformanceQuery={queryPerformanceQuery}

View File

@@ -84,6 +84,9 @@ export const LOCAL_STORAGE_KEYS = {
// Project sidebar hotkeys
HOTKEY_SIDEBAR: (sidebarId: string) => `supabase-dashboard-hotkey-sidebar-${sidebarId}`,
// Index Advisor notice dismissed
INDEX_ADVISOR_NOTICE_DISMISSED: (ref: string) => `index-advisor-notice-dismissed-${ref}`,
/**
* COMMON
*/