feat(studio): move query details to sheet (#38815)

* 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
This commit is contained in:
kemal.earth
2025-09-19 15:00:59 +01:00
committed by GitHub
parent 770b47a416
commit 19cfa66d9e
4 changed files with 67 additions and 57 deletions

View File

@@ -22,7 +22,7 @@ export const QUERY_PERFORMANCE_COLUMNS = [
{ id: 'min_time', name: 'Min time', description: undefined, minWidth: 100 }, { id: 'min_time', name: 'Min time', description: undefined, minWidth: 100 },
{ id: 'rows_read', name: 'Rows processed', description: undefined, minWidth: 130 }, { id: 'rows_read', name: 'Rows processed', description: undefined, minWidth: 130 },
{ id: 'cache_hit_rate', name: 'Cache hit rate', description: undefined, minWidth: 130 }, { id: 'cache_hit_rate', name: 'Cache hit rate', description: undefined, minWidth: 130 },
{ id: 'rolname', name: 'Role', description: undefined, minWidth: 160 }, { id: 'rolname', name: 'Role', description: undefined, minWidth: 200 },
] as const ] as const
export const QUERY_PERFORMANCE_ROLE_DESCRIPTION = [ export const QUERY_PERFORMANCE_ROLE_DESCRIPTION = [

View File

@@ -1,4 +1,4 @@
import { ArrowDown, ArrowUp, ChevronDown, TextSearch, X } from 'lucide-react' import { ArrowDown, ArrowUp, ChevronDown, TextSearch } from 'lucide-react'
import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import DataGrid, { Column, DataGridHandle, Row } from 'react-data-grid' import DataGrid, { Column, DataGridHandle, Row } from 'react-data-grid'
@@ -10,15 +10,15 @@ import {
DropdownMenuContent, DropdownMenuContent,
DropdownMenuItem, DropdownMenuItem,
DropdownMenuTrigger, DropdownMenuTrigger,
ResizableHandle, Sheet,
ResizablePanel, SheetContent,
ResizablePanelGroup,
TabsContent_Shadcn_, TabsContent_Shadcn_,
TabsList_Shadcn_, TabsList_Shadcn_,
TabsTrigger_Shadcn_, TabsTrigger_Shadcn_,
Tabs_Shadcn_, Tabs_Shadcn_,
cn, cn,
CodeBlock, CodeBlock,
SheetTitle,
} from 'ui' } from 'ui'
import { InfoTooltip } from 'ui-patterns/info-tooltip' import { InfoTooltip } from 'ui-patterns/info-tooltip'
import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader' import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader'
@@ -42,6 +42,7 @@ export const QueryPerformanceGrid = ({ queryPerformanceQuery }: QueryPerformance
const gridRef = useRef<DataGridHandle>(null) const gridRef = useRef<DataGridHandle>(null)
const { sort: urlSort, order, roles, search } = useParams() const { sort: urlSort, order, roles, search } = useParams()
const { isLoading, data } = queryPerformanceQuery const { isLoading, data } = queryPerformanceQuery
const dataGridContainerRef = useRef<HTMLDivElement>(null)
const [view, setView] = useState<'details' | 'suggestion'>('details') const [view, setView] = useState<'details' | 'suggestion'>('details')
const [selectedRow, setSelectedRow] = useState<number>() const [selectedRow, setSelectedRow] = useState<number>()
@@ -313,6 +314,7 @@ export const QueryPerformanceGrid = ({ queryPerformanceQuery }: QueryPerformance
return rawData return rawData
}, [data, sort]) }, [data, sort])
const selectedQuery = selectedRow !== undefined ? reportData[selectedRow]?.query : undefined const selectedQuery = selectedRow !== undefined ? reportData[selectedRow]?.query : undefined
const query = (selectedQuery ?? '').trim().toLowerCase() const query = (selectedQuery ?? '').trim().toLowerCase()
const showIndexSuggestions = const showIndexSuggestions =
@@ -364,12 +366,8 @@ export const QueryPerformanceGrid = ({ queryPerformanceQuery }: QueryPerformance
}, [handleKeyDown]) }, [handleKeyDown])
return ( return (
<ResizablePanelGroup <div className="relative flex flex-grow bg-alternative min-h-0">
direction="horizontal" <div ref={dataGridContainerRef} className="flex-1 min-w-0 overflow-x-auto">
className="relative flex flex-grow bg-alternative min-h-0"
autoSaveId="query-performance-layout-v1"
>
<ResizablePanel defaultSize={1}>
<DataGrid <DataGrid
ref={gridRef} ref={gridRef}
style={{ height: '100%' }} style={{ height: '100%' }}
@@ -393,7 +391,9 @@ export const QueryPerformanceGrid = ({ queryPerformanceQuery }: QueryPerformance
<Row <Row
{...props} {...props}
key={`qp-row-${props.rowIdx}`} key={`qp-row-${props.rowIdx}`}
onClick={() => { onClick={(event) => {
event.stopPropagation()
if (typeof idx === 'number' && idx >= 0) { if (typeof idx === 'number' && idx >= 0) {
setSelectedRow(idx) setSelectedRow(idx)
gridRef.current?.scrollToCell({ idx: 0, rowIdx: idx }) gridRef.current?.scrollToCell({ idx: 0, rowIdx: idx })
@@ -424,58 +424,70 @@ export const QueryPerformanceGrid = ({ queryPerformanceQuery }: QueryPerformance
), ),
}} }}
/> />
</ResizablePanel> </div>
{selectedRow !== undefined && (
<> <Sheet
<ResizableHandle withHandle /> open={selectedRow !== undefined}
<ResizablePanel defaultSize={30} maxSize={45} minSize={30} className="bg-studio border-t"> onOpenChange={(open) => {
<Button if (!open) {
type="text" setSelectedRow(undefined)
className="absolute top-3 right-3 px-1" }
icon={<X />} }}
onClick={() => setSelectedRow(undefined)} modal={false}
/> >
<Tabs_Shadcn_ <SheetTitle className="sr-only">Query details</SheetTitle>
value={view} <SheetContent
className="flex flex-col h-full" side="right"
onValueChange={(value: any) => setView(value)} className="flex flex-col h-full bg-studio border-l lg:!w-[calc(100vw-802px)] max-w-[700px] w-full"
> hasOverlay={false}
<TabsList_Shadcn_ className="px-5 flex gap-x-4 min-h-[46px]"> 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_ <TabsTrigger_Shadcn_
value="details" value="details"
className="px-0 pb-0 h-full text-xs data-[state=active]:bg-transparent !shadow-none" className="px-0 pb-0 data-[state=active]:bg-transparent !shadow-none"
> >
Query details Query details
</TabsTrigger_Shadcn_> </TabsTrigger_Shadcn_>
{showIndexSuggestions && ( {showIndexSuggestions && (
<TabsTrigger_Shadcn_ <TabsTrigger_Shadcn_
value="suggestion" value="suggestion"
className="px-0 pb-0 h-full text-xs data-[state=active]:bg-transparent !shadow-none" className="px-0 pb-0 data-[state=active]:bg-transparent !shadow-none"
> >
Indexes Indexes
</TabsTrigger_Shadcn_> </TabsTrigger_Shadcn_>
)} )}
</TabsList_Shadcn_> </TabsList_Shadcn_>
<TabsContent_Shadcn_ </div>
value="details"
className="mt-0 flex-grow min-h-0 overflow-y-auto" <TabsContent_Shadcn_ value="details" className="mt-0 flex-grow min-h-0 overflow-y-auto">
> {selectedRow !== undefined && (
<QueryDetail <QueryDetail
reportType={reportType} reportType={reportType}
selectedRow={reportData[selectedRow]} selectedRow={reportData[selectedRow]}
onClickViewSuggestion={() => setView('suggestion')} onClickViewSuggestion={() => setView('suggestion')}
/> />
</TabsContent_Shadcn_> )}
<TabsContent_Shadcn_ </TabsContent_Shadcn_>
value="suggestion" <TabsContent_Shadcn_
className="mt-0 flex-grow min-h-0 overflow-y-auto" value="suggestion"
> className="mt-0 flex-grow min-h-0 overflow-y-auto"
<QueryIndexes selectedRow={reportData[selectedRow]} /> >
</TabsContent_Shadcn_> {selectedRow !== undefined && <QueryIndexes selectedRow={reportData[selectedRow]} />}
</Tabs_Shadcn_> </TabsContent_Shadcn_>
</ResizablePanel> </Tabs_Shadcn_>
</> </SheetContent>
)} </Sheet>
</ResizablePanelGroup> </div>
) )
} }

View File

@@ -1,4 +1,4 @@
import { useMemo } from 'react' import React, { useMemo } from 'react'
import { Skeleton } from 'ui' import { Skeleton } from 'ui'
import { useQueryPerformanceQuery } from '../Reports/Reports.queries' import { useQueryPerformanceQuery } from '../Reports/Reports.queries'
@@ -11,7 +11,7 @@ export const QueryPerformanceMetrics = () => {
const stats = useMemo(() => { const stats = useMemo(() => {
return [ return [
{ {
title: queryMetrics?.[0]?.slow_queries === '1' ? 'Slow Query' : 'Slow Queries', title: queryMetrics?.[0]?.slow_queries === 1 ? 'Slow Query' : 'Slow Queries',
value: queryMetrics?.[0]?.slow_queries || '0', value: queryMetrics?.[0]?.slow_queries || '0',
}, },
{ {
@@ -28,11 +28,8 @@ export const QueryPerformanceMetrics = () => {
return ( return (
<section className="px-6 pt-2 pb-4 flex flex-wrap gap-x-6 gap-y-2 w-full"> <section className="px-6 pt-2 pb-4 flex flex-wrap gap-x-6 gap-y-2 w-full">
{stats.map((card, i) => ( {stats.map((card, i) => (
<> <React.Fragment key={i}>
<div <div className="flex items-baseline gap-2 heading-subSection text-foreground-light">
key={i}
className="flex items-baseline gap-2 heading-subSection text-foreground-light"
>
{isLoading ? ( {isLoading ? (
<Skeleton className="h-5 w-24" /> <Skeleton className="h-5 w-24" />
) : ( ) : (
@@ -43,7 +40,7 @@ export const QueryPerformanceMetrics = () => {
)} )}
</div> </div>
{i < stats.length - 1 && <span className="text-foreground-muted">/</span>} {i < stats.length - 1 && <span className="text-foreground-muted">/</span>}
</> </React.Fragment>
))} ))}
</section> </section>
) )

View File

@@ -151,14 +151,15 @@ export interface DialogContentProps
extends React.ComponentPropsWithoutRef<typeof SheetPrimitive.Content>, extends React.ComponentPropsWithoutRef<typeof SheetPrimitive.Content>,
VariantProps<typeof sheetVariants> { VariantProps<typeof sheetVariants> {
showClose?: boolean showClose?: boolean
hasOverlay?: boolean
} }
const SheetContent = React.forwardRef< const SheetContent = React.forwardRef<
React.ElementRef<typeof SheetPrimitive.Content>, React.ElementRef<typeof SheetPrimitive.Content>,
DialogContentProps DialogContentProps
>(({ side, size, className, children, showClose = true, ...props }, ref) => ( >(({ side, size, className, children, showClose = true, hasOverlay = true, ...props }, ref) => (
<SheetPortal side={side}> <SheetPortal side={side}>
<SheetOverlay /> {hasOverlay && <SheetOverlay />}
<SheetPrimitive.Content <SheetPrimitive.Content
ref={ref} ref={ref}
className={cn(sheetVariants({ side, size }), className)} className={cn(sheetVariants({ side, size }), className)}