feat(studio): query details metadata tidy up (#38867)

* feat: move query details to sheet

This moves the click through on Query Performance to a sheet as opposed to a resizable area. This gives us more space to play with and sets us up for the Query details revamp.

* fix: tabs font size

* style: expand size of sheet

* feat: hasOverlay prop for sheets

* feat: add optional overlay for sheets

* fix: closing only when clicking outside of rows

* style: width of panel on different  viewports

* fix: horizontal scroll for table

* fix: query queries label check in metrics

* feat: tidying up metadata values in query details

* feat: tidy up ms values

* fix: query pattern heading
This commit is contained in:
kemal.earth
2025-09-22 13:35:58 +01:00
committed by GitHub
parent a54727fb47
commit 4d91b0956c
2 changed files with 137 additions and 22 deletions

View File

@@ -1,6 +1,8 @@
import { Lightbulb } from 'lucide-react'
import { useEffect, useState } from 'react'
import dynamic from 'next/dynamic'
import dayjs from 'dayjs'
import duration from 'dayjs/plugin/duration'
import { formatSql } from 'lib/formatSql'
import { AlertDescription_Shadcn_, AlertTitle_Shadcn_, Alert_Shadcn_, Button, cn } from 'ui'
@@ -37,10 +39,26 @@ export const QueryDetail = ({ selectedRow, onClickViewSuggestion }: QueryDetailP
}
}, [selectedRow])
const formatDuration = (seconds: number) => {
const dur = dayjs.duration(seconds, 'seconds')
const minutes = Math.floor(dur.asMinutes())
const remainingSeconds = dur.seconds() + dur.milliseconds() / 1000
const parts = []
if (minutes > 0) parts.push(`${minutes}m`)
if (remainingSeconds > 0) {
const formattedSeconds = remainingSeconds.toFixed(2)
parts.push(`${formattedSeconds}s`)
}
return parts.join(' ')
}
return (
<QueryPanelContainer>
<QueryPanelSection>
<p className="text-sm">Query pattern</p>
<h4 className="mb-2">Query pattern</h4>
<SqlMonacoBlock value={query} height={310} lineNumbers="off" wrapperClassName="pl-3" />
{isLinterWarning && (
<Alert_Shadcn_
@@ -61,28 +79,125 @@ export const QueryDetail = ({ selectedRow, onClickViewSuggestion }: QueryDetailP
)}
</QueryPanelSection>
<div className="border-t" />
<QueryPanelSection className="gap-y-1">
{report
.filter((x) => x.id !== 'query')
.map((x) => {
const rawValue = selectedRow?.[x.id]
const isTime = x.name.includes('time')
<QueryPanelSection className="pb-3">
<h4 className="mb-2">Metadata</h4>
<ul className="flex flex-col gap-y-3 divide-y divide-dashed">
{report
.filter((x) => x.id !== 'query')
.map((x) => {
const rawValue = selectedRow?.[x.id]
const isTime = x.name.includes('time')
const formattedValue = isTime
? typeof rawValue === 'number' && !isNaN(rawValue) && isFinite(rawValue)
? `${rawValue.toFixed(2)}ms`
: 'N/A'
: rawValue != null
? String(rawValue)
: 'N/A'
const formattedValue = isTime
? typeof rawValue === 'number' && !isNaN(rawValue) && isFinite(rawValue)
? `${Math.round(rawValue).toLocaleString()}ms`
: 'n/a'
: rawValue != null
? String(rawValue)
: 'n/a'
return (
<div key={x.id} className="flex gap-x-2">
<p className="text-foreground-lighter text-sm w-32">{x.name}</p>
<p className="text-sm w-32">{formattedValue}</p>
</div>
)
})}
if (x.id === 'prop_total_time') {
return (
<li key={x.id} className="flex justify-between pt-3 text-sm">
<p className="text-foreground-light">{x.name}</p>
{rawValue ? (
<p
className={cn(
'tabular-nums',
rawValue.toFixed(1) === '0.0' && 'text-foreground-lighter'
)}
>
{rawValue.toFixed(1)}%
</p>
) : (
<p className="text-muted">&ndash;</p>
)}
</li>
)
}
if (x.id == 'total_time') {
return (
<li key={x.id} className="flex justify-between pt-3 text-sm">
<p className="text-foreground-light">
{x.name + ' '}
<span className="text-foreground-lighter">latency</span>
</p>
{isTime &&
typeof rawValue === 'number' &&
!isNaN(rawValue) &&
isFinite(rawValue) ? (
<p
className={cn(
'tabular-nums',
formatDuration(rawValue / 1000) === '0.0s' && 'text-foreground-lighter'
)}
>
{formatDuration(rawValue / 1000)}
</p>
) : (
<p className="text-muted">&ndash;</p>
)}
</li>
)
}
if (x.id == 'rows_read') {
return (
<li key={x.id} className="flex justify-between pt-3 text-sm">
<p className="text-foreground-light">{x.name}</p>
{typeof rawValue === 'number' && !isNaN(rawValue) && isFinite(rawValue) ? (
<p
className={cn('tabular-nums', rawValue === 0 && 'text-foreground-lighter')}
>
{rawValue.toLocaleString()}
</p>
) : (
<p className="text-muted">&ndash;</p>
)}
</li>
)
}
const cacheHitRateToNumber = (value: number | string) => {
if (typeof value === 'number') return value
return parseFloat(value.toString().replace('%', '')) || 0
}
if (x.id === 'cache_hit_rate') {
return (
<li key={x.id} className="flex justify-between pt-3 text-sm">
<p className="text-foreground-light">{x.name}</p>
{typeof rawValue === 'string' ? (
<p
className={cn(
cacheHitRateToNumber(rawValue).toFixed(2) === '0.00' &&
'text-foreground-lighter'
)}
>
{cacheHitRateToNumber(rawValue).toLocaleString(undefined, {
minimumFractionDigits: 2,
maximumFractionDigits: 2,
})}
%
</p>
) : (
<p className="text-muted">&ndash;</p>
)}
</li>
)
}
return (
<li key={x.id} className="flex justify-between pt-3 text-sm">
<p className="text-foreground-light">{x.name}</p>
<p className={cn('tabular-nums', x.id === 'rolname' && 'font-mono')}>
{formattedValue}
</p>
</li>
)
})}
</ul>
</QueryPanelSection>
</QueryPanelContainer>
)

View File

@@ -14,7 +14,7 @@ export const QueryPanelSection = ({
children,
className,
}: PropsWithChildren<{ className?: string }>) => (
<div className={cn('px-5 flex flex-col gap-y-2', className)}>{children}</div>
<div className={cn('px-6 flex flex-col gap-y-0', className)}>{children}</div>
)
export const QueryPanelScoreSection = ({