mirror of
https://github.com/supabase/supabase.git
synced 2026-06-22 01:02:52 +08:00
## I have read the [CONTRIBUTING.md](https://github.com/supabase/supabase/blob/master/CONTRIBUTING.md) file. YES ## What kind of change does this PR introduce? Previously our Query Performance Query column was scrollable inline. This means if you have thicc scrollbars turned on via your OS, it would look a bit clunky. This fix truncates query, full query still viewable in the click through sheet. | Before | After | |--------|--------| | <img width="1106" height="1164" alt="cleanshot_2026-05-13_at_18 56 16_2x" src="https://github.com/user-attachments/assets/4718e8d7-d3c5-499b-a125-6192ac547bfe" /> | <img width="456" height="286" alt="Screenshot 2026-05-13 at 13 34 30" src="https://github.com/user-attachments/assets/7446afb5-c0d7-4272-905a-42c144334472" /> | <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit ## Release Notes * **Style** * Adjusted query column width in the query performance monitoring table for optimized layout. * **Bug Fixes** * Enhanced query display rendering with improved data type handling to prevent potential display issues. <!-- review_stack_entry_start --> [](https://app.coderabbit.ai/change-stack/supabase/supabase/pull/45878) <!-- review_stack_entry_end --> <!-- end of auto-generated comment: release notes by coderabbit.ai -->
651 lines
23 KiB
TypeScript
651 lines
23 KiB
TypeScript
import { useParams } from 'common'
|
|
import { ArrowDown, ArrowRight, ArrowUp, ChevronDown, TextSearch } from 'lucide-react'
|
|
import { parseAsArrayOf, parseAsJson, parseAsString, useQueryStates } from 'nuqs'
|
|
import { UIEvent, useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
|
import DataGrid, { Column, DataGridHandle, Row } from 'react-data-grid'
|
|
import {
|
|
Button,
|
|
cn,
|
|
DropdownMenu,
|
|
DropdownMenuContent,
|
|
DropdownMenuItem,
|
|
DropdownMenuTrigger,
|
|
Sheet,
|
|
SheetContent,
|
|
SheetDescription,
|
|
SheetTitle,
|
|
Tabs_Shadcn_,
|
|
TabsContent_Shadcn_,
|
|
TabsList_Shadcn_,
|
|
TabsTrigger_Shadcn_,
|
|
} from 'ui'
|
|
import { Admonition } from 'ui-patterns'
|
|
import { CodeBlock } from 'ui-patterns/CodeBlock'
|
|
import { InfoTooltip } from 'ui-patterns/info-tooltip'
|
|
import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader'
|
|
|
|
import { useQueryPerformanceSort } from './hooks/useQueryPerformanceSort'
|
|
import {
|
|
hasIndexRecommendations,
|
|
queryInvolvesProtectedSchemas,
|
|
} from './IndexAdvisor/index-advisor.utils'
|
|
import { IndexSuggestionIcon } from './IndexAdvisor/IndexSuggestionIcon'
|
|
import { QueryDetail } from './QueryDetail'
|
|
import { QueryIndexes } from './QueryIndexes'
|
|
import {
|
|
QUERY_PERFORMANCE_COLUMNS,
|
|
QUERY_PERFORMANCE_ROLE_DESCRIPTION,
|
|
} from './QueryPerformance.constants'
|
|
import { QueryPerformanceRow } from './QueryPerformance.types'
|
|
import { formatDuration } from './QueryPerformance.utils'
|
|
import { NumericFilter } from '@/components/interfaces/Reports/v2/ReportsNumericFilter'
|
|
import { ButtonTooltip } from '@/components/ui/ButtonTooltip'
|
|
|
|
interface QueryPerformanceGridProps {
|
|
aggregatedData: QueryPerformanceRow[]
|
|
isLoading: boolean
|
|
error?: string | null
|
|
currentSelectedQuery?: string | null
|
|
onCurrentSelectQuery?: (query: string) => void
|
|
onRetry?: () => void
|
|
onScroll?: (event: UIEvent<HTMLDivElement>) => void
|
|
}
|
|
|
|
const calculateTimeConsumedWidth = (data: QueryPerformanceRow[]) => {
|
|
if (!data || data.length === 0) return 150
|
|
|
|
let maxWidth = 150
|
|
|
|
data.forEach((row) => {
|
|
const percentage = row.prop_total_time || 0
|
|
const totalTime = row.total_time || 0
|
|
|
|
if (percentage && totalTime) {
|
|
const percentageText = `${percentage.toFixed(1)}%`
|
|
const durationText = formatDuration(totalTime)
|
|
const fullText = `${percentageText} / ${durationText}`
|
|
const estimatedWidth = fullText.length * 8 + 40
|
|
|
|
maxWidth = Math.max(maxWidth, estimatedWidth)
|
|
}
|
|
})
|
|
|
|
return Math.min(maxWidth, 300)
|
|
}
|
|
|
|
export const QueryPerformanceGrid = ({
|
|
aggregatedData,
|
|
isLoading,
|
|
error,
|
|
currentSelectedQuery,
|
|
onCurrentSelectQuery,
|
|
onRetry,
|
|
onScroll,
|
|
}: QueryPerformanceGridProps) => {
|
|
const { sort, setSortConfig } = useQueryPerformanceSort()
|
|
const gridRef = useRef<DataGridHandle>(null)
|
|
const { sort: urlSort, order } = useParams()
|
|
const [{ search, roles, callsFilter }] = useQueryStates({
|
|
search: parseAsString.withDefault(''),
|
|
roles: parseAsArrayOf(parseAsString).withDefault([]),
|
|
callsFilter: parseAsJson<NumericFilter | null>(
|
|
(value) => value as NumericFilter | null
|
|
).withDefault({
|
|
operator: '>=',
|
|
value: 0,
|
|
} as NumericFilter),
|
|
})
|
|
const dataGridContainerRef = useRef<HTMLDivElement>(null)
|
|
|
|
const [view, setView] = useState<'details' | 'suggestion'>('details')
|
|
const [selectedRow, setSelectedRow] = useState<number>()
|
|
|
|
const columns = QUERY_PERFORMANCE_COLUMNS.map((col) => {
|
|
const nonSortableColumns = ['query']
|
|
|
|
const result: Column<any> = {
|
|
key: col.id,
|
|
name: col.name,
|
|
cellClass: `column-${col.id}`,
|
|
resizable: true,
|
|
minWidth:
|
|
col.id === 'prop_total_time'
|
|
? calculateTimeConsumedWidth((aggregatedData as any) ?? [])
|
|
: (col.minWidth ?? 120),
|
|
sortable: !nonSortableColumns.includes(col.id),
|
|
headerCellClass: 'first:pl-6 cursor-pointer',
|
|
renderHeaderCell: () => {
|
|
const isSortable = !nonSortableColumns.includes(col.id)
|
|
|
|
return (
|
|
<div className="flex items-center justify-between text-xs w-full">
|
|
<div className="flex items-center gap-x-2">
|
|
<p className="text-foreground! font-medium">{col.name}</p>
|
|
{col.description && (
|
|
<p className="text-foreground-lighter font-normal">{col.description}</p>
|
|
)}
|
|
</div>
|
|
|
|
{isSortable && (
|
|
<DropdownMenu>
|
|
<DropdownMenuTrigger asChild>
|
|
<Button
|
|
type="text"
|
|
size="tiny"
|
|
className="p-1 h-5 w-5 shrink-0"
|
|
icon={<ChevronDown size={14} className="text-foreground-muted" />}
|
|
onClick={(e) => e.stopPropagation()}
|
|
/>
|
|
</DropdownMenuTrigger>
|
|
<DropdownMenuContent align="end" className="w-48">
|
|
<DropdownMenuItem
|
|
onClick={() => {
|
|
setSortConfig(col.id, 'asc')
|
|
}}
|
|
className={cn(
|
|
'flex gap-2',
|
|
sort?.column === col.id && sort?.order === 'asc' && 'text-foreground'
|
|
)}
|
|
>
|
|
<ArrowUp size={14} />
|
|
Sort Ascending
|
|
</DropdownMenuItem>
|
|
<DropdownMenuItem
|
|
onClick={() => {
|
|
setSortConfig(col.id, 'desc')
|
|
}}
|
|
className={cn(
|
|
'flex gap-2',
|
|
sort?.column === col.id && sort?.order === 'desc' && 'text-foreground'
|
|
)}
|
|
>
|
|
<ArrowDown size={14} />
|
|
Sort Descending
|
|
</DropdownMenuItem>
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
)}
|
|
</div>
|
|
)
|
|
},
|
|
renderCell: (props) => {
|
|
const value = props.row?.[col.id]
|
|
if (col.id === 'query') {
|
|
return (
|
|
<div className="w-full flex items-center gap-x-3 group">
|
|
<div className="shrink-0 w-4">
|
|
{hasIndexRecommendations(props.row.index_advisor_result, true) && (
|
|
<IndexSuggestionIcon
|
|
indexAdvisorResult={props.row.index_advisor_result}
|
|
onClickIcon={() => {
|
|
setSelectedRow(props.rowIdx)
|
|
setView('suggestion')
|
|
gridRef.current?.scrollToCell({ idx: 0, rowIdx: props.rowIdx })
|
|
}}
|
|
/>
|
|
)}
|
|
</div>
|
|
<CodeBlock
|
|
language="pgsql"
|
|
className="bg-transparent! p-0! m-0! border-none! truncate! whitespace-nowrap! w-full! pr-20! pointer-events-none"
|
|
wrapperClassName="flex-1 min-w-0 max-w-full overflow-hidden!"
|
|
hideLineNumbers
|
|
hideCopy
|
|
value={typeof value === 'string' ? value.replace(/\s+/g, ' ').trim() : ''}
|
|
wrapLines={false}
|
|
/>
|
|
{onCurrentSelectQuery && (
|
|
<ButtonTooltip
|
|
tooltip={{ content: { text: 'Query details' } }}
|
|
icon={<ArrowRight size={14} />}
|
|
size="tiny"
|
|
type="default"
|
|
onClick={(e) => {
|
|
e.stopPropagation()
|
|
setSelectedRow(props.rowIdx)
|
|
setView('details')
|
|
gridRef.current?.scrollToCell({ idx: 0, rowIdx: props.rowIdx })
|
|
}}
|
|
className="p-1 shrink-0 -translate-x-2 group-hover:flex hidden"
|
|
/>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
const isTime = col.name.includes('time')
|
|
const formattedValue =
|
|
!!value && typeof value === 'number' && !isNaN(value) && isFinite(value)
|
|
? isTime
|
|
? `${value.toFixed(0).toLocaleString()}ms`
|
|
: value.toLocaleString()
|
|
: ''
|
|
|
|
if (col.id === 'prop_total_time') {
|
|
const percentage = props.row.prop_total_time || 0
|
|
const totalTime = props.row.total_time || 0
|
|
const fillWidth = Math.min(percentage, 100)
|
|
|
|
return (
|
|
<div className="w-full flex flex-col justify-center text-xs text-right tabular-nums font-mono">
|
|
<div
|
|
className="absolute inset-0 bg-foreground transition-all duration-200 z-0"
|
|
style={{
|
|
width: `${fillWidth}%`,
|
|
opacity: 0.04,
|
|
}}
|
|
/>
|
|
{percentage && totalTime ? (
|
|
<span className="flex items-center justify-end gap-x-1.5">
|
|
<span
|
|
className={cn(percentage.toFixed(1) === '0.0' && 'text-foreground-lighter')}
|
|
>
|
|
{percentage.toFixed(1)}%
|
|
</span>{' '}
|
|
<span className="text-muted">/</span>
|
|
<span
|
|
className={cn(
|
|
formatDuration(totalTime) === '0.00s' && 'text-foreground-lighter'
|
|
)}
|
|
>
|
|
{formatDuration(totalTime)}
|
|
</span>
|
|
</span>
|
|
) : (
|
|
<p className="text-muted">–</p>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
if (col.id === 'calls') {
|
|
return (
|
|
<div className="w-full flex flex-col justify-center text-xs text-right tabular-nums font-mono">
|
|
{typeof value === 'number' && !isNaN(value) && isFinite(value) ? (
|
|
<p className={cn(value === 0 && 'text-foreground-lighter')}>
|
|
{value.toLocaleString()}
|
|
</p>
|
|
) : (
|
|
<p className="text-muted">–</p>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
if (col.id === 'max_time' || col.id === 'mean_time' || col.id === 'min_time') {
|
|
return (
|
|
<div className="w-full flex flex-col justify-center text-xs text-right tabular-nums font-mono">
|
|
{typeof value === 'number' && !isNaN(value) && isFinite(value) ? (
|
|
<p className={cn(value.toFixed(0) === '0' && 'text-foreground-lighter')}>
|
|
{Math.round(value).toLocaleString()}ms
|
|
</p>
|
|
) : (
|
|
<p className="text-muted">–</p>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
if (col.id === 'rows_read') {
|
|
return (
|
|
<div className="w-full flex flex-col justify-center text-xs text-right tabular-nums font-mono">
|
|
{typeof value === 'number' && !isNaN(value) && isFinite(value) ? (
|
|
<p className={cn(value === 0 && 'text-foreground-lighter')}>
|
|
{value.toLocaleString()}
|
|
</p>
|
|
) : (
|
|
<p className="text-muted">–</p>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
if (col.id === 'cache_hit_rate') {
|
|
const numericValue = typeof value === 'number' ? value : parseFloat(value)
|
|
return (
|
|
<div className="w-full flex flex-col justify-center text-xs text-right tabular-nums font-mono">
|
|
{typeof numericValue === 'number' &&
|
|
!isNaN(numericValue) &&
|
|
isFinite(numericValue) ? (
|
|
<p className={cn(numericValue.toFixed(2) === '0.00' && 'text-foreground-lighter')}>
|
|
{numericValue.toLocaleString(undefined, {
|
|
minimumFractionDigits: 2,
|
|
maximumFractionDigits: 2,
|
|
})}
|
|
%
|
|
</p>
|
|
) : (
|
|
<p className="text-muted">–</p>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
if (col.id === 'rolname') {
|
|
return (
|
|
<div className="w-full flex flex-col justify-center">
|
|
{value ? (
|
|
<span className="flex items-center gap-x-1">
|
|
<p className="font-mono text-xs">{value}</p>
|
|
<InfoTooltip align="end" alignOffset={-12} className="w-56">
|
|
{
|
|
QUERY_PERFORMANCE_ROLE_DESCRIPTION.find((role) => role.name === value)
|
|
?.description
|
|
}
|
|
</InfoTooltip>
|
|
</span>
|
|
) : (
|
|
<p className="text-muted">–</p>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
if (col.id === 'application_name') {
|
|
return (
|
|
<div className="w-full flex flex-col justify-center">
|
|
{value ? (
|
|
<p className="font-mono text-xs">{value}</p>
|
|
) : (
|
|
<p className="text-muted">–</p>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
return (
|
|
<div className="w-full flex flex-col gap-y-0.5 justify-center text-xs">
|
|
<p>{formattedValue}</p>
|
|
</div>
|
|
)
|
|
},
|
|
}
|
|
return result
|
|
})
|
|
|
|
const reportData = useMemo(() => {
|
|
let data = [...aggregatedData]
|
|
|
|
if (search && typeof search === 'string' && search.length > 0) {
|
|
data = data.filter((row) => row.query.toLowerCase().includes(search.toLowerCase()))
|
|
}
|
|
|
|
if (roles && Array.isArray(roles) && roles.length > 0) {
|
|
data = data.filter((row) => row.rolname && roles.includes(row.rolname))
|
|
}
|
|
|
|
if (callsFilter) {
|
|
const { operator, value } = callsFilter
|
|
data = data.filter((row) => {
|
|
const calls = row.calls || 0
|
|
switch (operator) {
|
|
case '=':
|
|
return calls === value
|
|
case '>=':
|
|
return calls >= value
|
|
case '<=':
|
|
return calls <= value
|
|
case '>':
|
|
return calls > value
|
|
case '<':
|
|
return calls < value
|
|
case '!=':
|
|
return calls !== value
|
|
default:
|
|
return true
|
|
}
|
|
})
|
|
}
|
|
|
|
if (sort?.column === 'prop_total_time') {
|
|
data.sort((a, b) => {
|
|
const aValue = a.prop_total_time || 0
|
|
const bValue = b.prop_total_time || 0
|
|
return sort.order === 'asc' ? aValue - bValue : bValue - aValue
|
|
})
|
|
} else if (sort?.column && sort.column !== 'query') {
|
|
data.sort((a, b) => {
|
|
const aValue = a[sort.column as keyof QueryPerformanceRow] || 0
|
|
const bValue = b[sort.column as keyof QueryPerformanceRow] || 0
|
|
|
|
if (typeof aValue === 'number' && typeof bValue === 'number') {
|
|
return sort.order === 'asc' ? aValue - bValue : bValue - aValue
|
|
}
|
|
return 0
|
|
})
|
|
}
|
|
|
|
return data
|
|
}, [aggregatedData, sort, search, roles, callsFilter])
|
|
|
|
useEffect(() => {
|
|
setSelectedRow(undefined)
|
|
}, [search, roles, urlSort, order, callsFilter])
|
|
|
|
const handleKeyDown = useCallback(
|
|
(event: KeyboardEvent) => {
|
|
if (!reportData.length || selectedRow === undefined) return
|
|
|
|
if (event.key !== 'ArrowUp' && event.key !== 'ArrowDown') return
|
|
|
|
event.stopPropagation()
|
|
|
|
let nextIndex = selectedRow
|
|
if (event.key === 'ArrowUp' && selectedRow > 0) {
|
|
nextIndex = selectedRow - 1
|
|
} else if (event.key === 'ArrowDown' && selectedRow < reportData.length - 1) {
|
|
nextIndex = selectedRow + 1
|
|
}
|
|
|
|
if (nextIndex !== selectedRow) {
|
|
setSelectedRow(nextIndex)
|
|
gridRef.current?.scrollToCell({ idx: 0, rowIdx: nextIndex })
|
|
|
|
const rowQuery = reportData[nextIndex]?.query ?? ''
|
|
if (!rowQuery.trim().toLowerCase().startsWith('select')) {
|
|
setView('details')
|
|
}
|
|
}
|
|
},
|
|
[reportData, selectedRow]
|
|
)
|
|
|
|
useEffect(() => {
|
|
// run before RDG to prevent header focus (the third param: true)
|
|
window.addEventListener('keydown', handleKeyDown, true)
|
|
return () => {
|
|
window.removeEventListener('keydown', handleKeyDown, true)
|
|
}
|
|
}, [handleKeyDown])
|
|
|
|
const isSelectQuery = (query: string | undefined): boolean => {
|
|
if (!query) return false
|
|
const formattedQuery = query.trim().toLowerCase()
|
|
return (
|
|
formattedQuery.startsWith('select') ||
|
|
formattedQuery.startsWith('with pgrst_source') ||
|
|
formattedQuery.startsWith('with pgrst_payload')
|
|
)
|
|
}
|
|
|
|
useEffect(() => {
|
|
if (selectedRow !== undefined && view === 'suggestion') {
|
|
const query = reportData[selectedRow]?.query
|
|
if (!isSelectQuery(query)) {
|
|
setView('details')
|
|
}
|
|
}
|
|
}, [selectedRow, view, reportData])
|
|
|
|
if (error) {
|
|
return (
|
|
<div className="relative flex grow bg-alternative min-h-0">
|
|
<div className="flex-1 min-w-0 p-6">
|
|
<Admonition
|
|
type="destructive"
|
|
title="Failed to load query performance data"
|
|
description={error}
|
|
>
|
|
{onRetry && (
|
|
<div className="mt-4">
|
|
<Button type="default" onClick={onRetry}>
|
|
Try again
|
|
</Button>
|
|
</div>
|
|
)}
|
|
</Admonition>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
const selectedQuery = selectedRow !== undefined ? reportData[selectedRow]?.query : undefined
|
|
const isProtectedSchemaQuery = queryInvolvesProtectedSchemas(selectedQuery)
|
|
const canShowIndexesTab = isSelectQuery(selectedQuery) && !isProtectedSchemaQuery
|
|
|
|
return (
|
|
<div className="relative flex grow bg-alternative min-h-0">
|
|
<div ref={dataGridContainerRef} className="flex-1 min-w-0 overflow-x-auto">
|
|
<DataGrid
|
|
ref={gridRef}
|
|
style={{ height: '100%' }}
|
|
className={cn('flex-1 grow h-full')}
|
|
rowHeight={44}
|
|
headerRowHeight={36}
|
|
columns={columns}
|
|
rows={reportData}
|
|
onScroll={onScroll}
|
|
rowClass={(_, idx) => {
|
|
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 ? (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-hidden [&>.rdg-cell]:shadow-none',
|
|
'[&>.rdg-cell.column-prop_total_time]:relative',
|
|
].join(' ')
|
|
}}
|
|
renderers={{
|
|
renderRow(idx, props) {
|
|
return (
|
|
<Row
|
|
{...props}
|
|
key={`qp-row-${props.rowIdx}`}
|
|
onClick={(event) => {
|
|
event.stopPropagation()
|
|
|
|
if (typeof idx === 'number' && idx >= 0) {
|
|
if (onCurrentSelectQuery) {
|
|
const query = reportData[idx]?.query
|
|
if (query) {
|
|
onCurrentSelectQuery(query)
|
|
}
|
|
} else {
|
|
setSelectedRow(idx)
|
|
const hasRecommendations = hasIndexRecommendations(
|
|
reportData[idx]?.index_advisor_result,
|
|
true
|
|
)
|
|
setView(hasRecommendations ? 'suggestion' : 'details')
|
|
gridRef.current?.scrollToCell({ idx: 0, rowIdx: idx })
|
|
}
|
|
}
|
|
}}
|
|
/>
|
|
)
|
|
},
|
|
noRowsFallback: isLoading ? (
|
|
<div className="absolute top-14 px-6 w-full">
|
|
<GenericSkeletonLoader />
|
|
</div>
|
|
) : (
|
|
<div className="absolute top-20 px-6 flex flex-col items-center justify-center w-full gap-y-2">
|
|
<TextSearch className="text-foreground-muted" strokeWidth={1} />
|
|
<div className="text-center">
|
|
<p className="text-foreground">No queries detected</p>
|
|
<p className="text-foreground-light">
|
|
There are no actively running queries that match the criteria
|
|
</p>
|
|
</div>
|
|
</div>
|
|
),
|
|
}}
|
|
/>
|
|
</div>
|
|
|
|
<Sheet
|
|
open={selectedRow !== undefined}
|
|
onOpenChange={(open) => {
|
|
if (!open) {
|
|
setSelectedRow(undefined)
|
|
}
|
|
}}
|
|
modal={false}
|
|
>
|
|
<SheetTitle className="sr-only">Query details</SheetTitle>
|
|
<SheetDescription className="sr-only">
|
|
Query Performance Details & 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"
|
|
hasOverlay={false}
|
|
onInteractOutside={(event) => {
|
|
if (dataGridContainerRef.current?.contains(event.target as Node)) {
|
|
event.preventDefault()
|
|
}
|
|
}}
|
|
>
|
|
<Tabs_Shadcn_
|
|
value={view}
|
|
className="flex flex-col h-full"
|
|
onValueChange={(value: any) => setView(value)}
|
|
>
|
|
<div className="px-5 border-b">
|
|
<TabsList_Shadcn_ className="px-0 flex gap-x-4 min-h-[46px] border-b-0 [&>button]:h-[47px]">
|
|
<TabsTrigger_Shadcn_
|
|
value="details"
|
|
className="px-0 pb-0 data-[state=active]:bg-transparent shadow-none!"
|
|
>
|
|
Query details
|
|
</TabsTrigger_Shadcn_>
|
|
{selectedRow !== undefined && canShowIndexesTab && (
|
|
<TabsTrigger_Shadcn_
|
|
value="suggestion"
|
|
className="px-0 pb-0 data-[state=active]:bg-transparent shadow-none!"
|
|
>
|
|
Indexes
|
|
</TabsTrigger_Shadcn_>
|
|
)}
|
|
</TabsList_Shadcn_>
|
|
</div>
|
|
|
|
<TabsContent_Shadcn_ value="details" className="mt-0 grow min-h-0 overflow-y-auto">
|
|
{selectedRow !== undefined && (
|
|
<QueryDetail
|
|
selectedRow={reportData[selectedRow]}
|
|
onClickViewSuggestion={() => setView('suggestion')}
|
|
onClose={() => setSelectedRow(undefined)}
|
|
/>
|
|
)}
|
|
</TabsContent_Shadcn_>
|
|
{selectedRow !== undefined && canShowIndexesTab && (
|
|
<TabsContent_Shadcn_ value="suggestion" className="mt-0 grow min-h-0 overflow-y-auto">
|
|
<QueryIndexes selectedRow={reportData[selectedRow]} />
|
|
</TabsContent_Shadcn_>
|
|
)}
|
|
</Tabs_Shadcn_>
|
|
</SheetContent>
|
|
</Sheet>
|
|
</div>
|
|
)
|
|
}
|