mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 17:35:10 +03:00
## What kind of change does this PR introduce? UI polish / design system: refreshed button styles, related token tweaks, and a shared floating-button plate. Resolves DEPR-652. ## What is the current behavior? Default, primary, and secondary buttons use older fills, borders, and hover treatments. Primary still leans on brand scale utilities. Default fills don’t always read as raised chrome across surfaces, and floating copy / expand / scroll controls can let busy content show through translucent fills. Call sites hand-roll `rounded-* bg-background` wrappers for that. ## What is the new behavior? Refreshes primary, default, and secondary buttons with medium-weight labels, subtle shadows and inset edges, and smoother transitions. Light-mode default buttons use a raised fill with an accent hover state, primary text is brighter, and inline keyboard shortcuts inherit the button’s colour. Adds `FloatingPlate`: an opaque `bg-popover` shell for floating default buttons (and small clusters). Migrates Studio, Docs-related patterns, www, and `ui-patterns` floaters onto it so busy content no longer shows through translucent fills. Positioning, z-index, and hover/focus reveal stay on the plate’s `className`. Use `rounded="full"` for pills. Also: - Moves primary onto semantic `--primary` / `--primary-hover` (with a light-theme override) instead of brand utility fills - Tokenises button shadows as `--button-shadow-drop` / `--button-shadow-raised` / `--button-shadow-default` on the Button base - Aligns hover direction: darken on light mode, lighten on dark mode for both default and primary - Default fill stays opaque `bg-card` in light (occlusion) and translucent `bg-muted` in dark (adapts to the local surface) - Documents fills and `FloatingPlate` on the design-system Button page (with a live example) - Scales shared radius tokens in Studio and www; medium+ Button sizes use a proportionally softer radius - Fixes www nav CTA centering (`lg:inline-flex` instead of `lg:block`) - Query detail Expand/Collapse wires `aria-expanded` / `aria-controls` | Before | After | | --- | --- | | <img width="1074" height="438" alt="CleanShot 2026-09-18 at 15 52 51@2x" src="https://github.com/user-attachments/assets/ef43da21-b053-4b7e-9ac4-ab8b428228ab" /> | <img width="1090" height="464" alt="CleanShot 2026-09-18 at 15 50 59@2x" src="https://github.com/user-attachments/assets/2ddc55fc-4c8d-499c-a280-f3db3d99023c" /> | | <img width="1082" height="446" alt="CleanShot 2026-09-18 at 15 52 35@2x" src="https://github.com/user-attachments/assets/3dd5452d-325a-4e4a-a79d-26c6c6950a31" /> | <img width="1078" height="446" alt="CleanShot 2026-09-18 at 15 51 13@2x" src="https://github.com/user-attachments/assets/93666385-3e6e-42e0-9891-9cd6bb935b67" /> | ## To test ### Design system - [Button page](https://design-system-git-chore-button-styles-supabase.vercel.app/design-system/docs/components/button): default / primary in light and dark; hover should darken on light, lighten on dark - Same page: [Floating over content](https://design-system-git-chore-button-styles-supabase.vercel.app/design-system/docs/components/button#floating-over-content) / [Floating plate](https://design-system-git-chore-button-styles-supabase.vercel.app/design-system/docs/components/button#floating-plate) example; Copy over SQL should stay opaque - Spot-check hover on a code preview Copy control ### Docs [Docs deploy preview](https://docs-git-chore-button-styles-supabase.vercel.app/docs): - [Docs homepage](https://docs-git-chore-button-styles-supabase.vercel.app/docs): top-right **Sign up** / **Dashboard** primary; menu icon beside it (default icon button) - Shrink below `lg` and open the hamburger drawer: bottom **Sign in** (default) + **Start your project** (primary) medium block buttons - Tab once for **Skip to content** (FloatingPlate) - [MCP guide](https://docs-git-chore-button-styles-supabase.vercel.app/docs/guides/ai-tools/mcp): project picker - [Apple login](https://docs-git-chore-button-styles-supabase.vercel.app/docs/guides/auth/social-login/auth-apple): **Generate Secret Key** button in the Apple Secret Generator - Optional opacity check: any guide code block Copy control (e.g. at the bottom of [Import data into Supabase](https://docs-git-chore-button-styles-supabase.vercel.app/docs/guides/database/import-data)) ### Studio [Studio deploy preview](https://studio-staging-git-chore-button-styles-supabase.vercel.app/): - **Observability → Query Performance**: open a query detail → Expand/Collapse pill + SQL Copy chip (dark: no bleed-through) - **Observability → Query Insights**: select a query → Clear query pill - **Table Editor → any table → Definition** → floating **Open in SQL Editor** - **Connect → Framework → Add files**: Copy on the code tabs (FloatingPlate; light hover follow-up is DEPR-694) - Tab once for **Skip to content** ### WWW - [www deploy preview](https://zone-www-dot-com-git-chore-button-styles-supabase.vercel.app/): nav Sign in / Start your project vertical centering; hero medium CTAs radius --------- Co-authored-by: Danny White <3104761+dnywh@users.noreply.github.com>
577 lines
22 KiB
TypeScript
577 lines
22 KiB
TypeScript
import { safeSql, type SafeSqlFragment } from '@supabase/pg-meta'
|
|
import { wrapWithRollback } from '@supabase/pg-meta/src/query'
|
|
import { useParams } from 'common'
|
|
import { Search, TextSearch, X } from 'lucide-react'
|
|
import { useRouter } from 'next/router'
|
|
import { parseAsArrayOf, parseAsString, useQueryStates } from 'nuqs'
|
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
|
import DataGrid, { DataGridHandle, Row } from 'react-data-grid'
|
|
import { Button, cn, FloatingPlate, Tabs, TabsList, TabsTrigger } from 'ui'
|
|
import { Input } from 'ui-patterns/DataInputs/Input'
|
|
import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader'
|
|
|
|
import { buildQueryInsightFixPrompt } from '../../QueryPerformance/QueryPerformance.ai'
|
|
import type { QueryPerformanceRow } from '../../QueryPerformance/QueryPerformance.types'
|
|
import { useQueryInsightsIssues } from '../hooks/useQueryInsightsIssues'
|
|
import { useQueryInsightsTableColumns } from '../hooks/useQueryInsightsTableColumns'
|
|
import type { ClassifiedQuery } from '../QueryInsightsHealth/QueryInsightsHealth.types'
|
|
import { QueryInsightsDetailSheet } from './QueryInsightsDetailSheet'
|
|
import type { IssueFilter, Mode } from './QueryInsightsTable.types'
|
|
import {
|
|
formatDuration,
|
|
getColumnName,
|
|
getQueryType,
|
|
getTableName,
|
|
} from './QueryInsightsTable.utils'
|
|
import type { QueryPlanRow } from '@/components/interfaces/ExplainVisualizer/ExplainVisualizer.types'
|
|
import { FilterPill } from '@/components/interfaces/QueryPerformance/components/FilterPill'
|
|
import { SIDEBAR_KEYS } from '@/components/layouts/ProjectLayout/LayoutSidebar/LayoutSidebarProvider'
|
|
import { FilterPopover } from '@/components/ui/FilterPopover'
|
|
import { TwoOptionToggle } from '@/components/ui/TwoOptionToggle'
|
|
import { useExecuteSqlMutation } from '@/data/sql/execute-sql-mutation'
|
|
import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
|
|
import { useAiAssistantStateSnapshot } from '@/state/ai-assistant-state'
|
|
import { useSidebarManagerSnapshot } from '@/state/sidebar-manager-state'
|
|
|
|
interface QueryInsightsTableProps {
|
|
data: QueryPerformanceRow[]
|
|
isLoading: boolean
|
|
currentSelectedQuery?: string | null
|
|
onCurrentSelectQuery?: (query: string | null) => void
|
|
}
|
|
|
|
export const QueryInsightsTable = ({
|
|
data,
|
|
isLoading,
|
|
currentSelectedQuery,
|
|
onCurrentSelectQuery,
|
|
}: QueryInsightsTableProps) => {
|
|
const [mode, setMode] = useState<Mode>('triage')
|
|
const [filter, setFilter] = useState<IssueFilter>('all')
|
|
const [
|
|
{ search: urlSearch, sort: urlSortCol, order: urlSortOrder, source: urlSource },
|
|
setQueryStates,
|
|
] = useQueryStates({
|
|
search: parseAsString.withDefault(''),
|
|
sort: parseAsString,
|
|
order: parseAsString,
|
|
source: parseAsArrayOf(parseAsString).withDefault([]),
|
|
})
|
|
const [searchQuery, setSearchQuery] = useState(urlSearch || '')
|
|
const appNameFilter = urlSource
|
|
const setAppNameFilter = (names: string[]) =>
|
|
setQueryStates({ source: names.length ? names : null })
|
|
|
|
const appNameOptions = useMemo(() => {
|
|
const names = Array.from(
|
|
new Set(data.map((r) => r.application_name).filter(Boolean))
|
|
) as string[]
|
|
return names.map((name) => ({ value: name, label: name }))
|
|
}, [data])
|
|
|
|
const filteredData = useMemo(() => {
|
|
if (appNameFilter.length === 0) return data
|
|
return data.filter((r) => appNameFilter.includes(r.application_name ?? ''))
|
|
}, [data, appNameFilter])
|
|
|
|
const { classified, errors, indexIssues, slowQueries } = useQueryInsightsIssues(filteredData)
|
|
const [selectedRow, setSelectedRow] = useState<number>()
|
|
const [selectedTriageRow, setSelectedTriageRow] = useState<number | undefined>()
|
|
const [sheetView, setSheetView] = useState<'details' | 'indexes' | 'explain'>('details')
|
|
const gridRef = useRef<DataGridHandle>(null)
|
|
const triageGridRef = useRef<DataGridHandle>(null)
|
|
const dataGridContainerRef = useRef<HTMLDivElement>(null)
|
|
const triageContainerRef = useRef<HTMLDivElement>(null)
|
|
const scrollContainerRef = useRef<HTMLDivElement>(null)
|
|
const [triageContainerWidth, setTriageContainerWidth] = useState(0)
|
|
const sort = useMemo<{ column: string; order: 'asc' | 'desc' }>(
|
|
() =>
|
|
urlSortCol && urlSortOrder && ['asc', 'desc'].includes(urlSortOrder)
|
|
? { column: urlSortCol, order: urlSortOrder as 'asc' | 'desc' }
|
|
: { column: 'prop_total_time', order: 'desc' },
|
|
[urlSortCol, urlSortOrder]
|
|
)
|
|
const setSort = useCallback(
|
|
(config: { column: string; order: 'asc' | 'desc' } | null) =>
|
|
setQueryStates(
|
|
config ? { sort: config.column, order: config.order } : { sort: null, order: null }
|
|
),
|
|
[setQueryStates]
|
|
)
|
|
|
|
const [explainResults, setExplainResults] = useState<Record<string, QueryPlanRow[]>>({})
|
|
const [explainLoadingQuery, setExplainLoadingQuery] = useState<string | null>(null)
|
|
|
|
const { ref } = useParams()
|
|
const router = useRouter()
|
|
const { openSidebar } = useSidebarManagerSnapshot()
|
|
const aiSnap = useAiAssistantStateSnapshot()
|
|
|
|
const { data: project } = useSelectedProjectQuery()
|
|
|
|
const { mutate: executeExplain } = useExecuteSqlMutation()
|
|
|
|
const triageItems = useMemo(() => classified.filter((q) => q.issueType !== null), [classified])
|
|
|
|
const filteredTriageItems = useMemo(() => {
|
|
const filtered =
|
|
filter === 'all' ? triageItems : triageItems.filter((q) => q.issueType === filter)
|
|
return filtered.map((item) => ({
|
|
...item,
|
|
queryType: getQueryType(item.query),
|
|
}))
|
|
}, [triageItems, filter])
|
|
|
|
const explorerItems = useMemo(() => {
|
|
let items = [...classified]
|
|
|
|
if (searchQuery.trim()) {
|
|
const searchLower = searchQuery.toLowerCase()
|
|
items = items.filter((item) => {
|
|
const queryType = getQueryType(item.query) ?? ''
|
|
const tableName = getTableName(item.query) ?? ''
|
|
const columnName = getColumnName(item.query) ?? ''
|
|
const appName = item.application_name ?? ''
|
|
const query = item.query ?? ''
|
|
|
|
return (
|
|
queryType.toLowerCase().includes(searchLower) ||
|
|
tableName.toLowerCase().includes(searchLower) ||
|
|
columnName.toLowerCase().includes(searchLower) ||
|
|
appName.toLowerCase().includes(searchLower) ||
|
|
query.toLowerCase().includes(searchLower)
|
|
)
|
|
})
|
|
}
|
|
|
|
if (sort) {
|
|
items.sort((a, b) => {
|
|
if (sort.column === 'query') {
|
|
const aDate = a.first_seen ? new Date(a.first_seen).getTime() : 0
|
|
const bDate = b.first_seen ? new Date(b.first_seen).getTime() : 0
|
|
return sort.order === 'asc' ? aDate - bDate : bDate - aDate
|
|
}
|
|
|
|
const aValue: unknown = a[sort.column as keyof typeof a]
|
|
const bValue: unknown = b[sort.column as keyof typeof b]
|
|
|
|
if (typeof aValue === 'number' && typeof bValue === 'number') {
|
|
return sort.order === 'asc' ? aValue - bValue : bValue - aValue
|
|
}
|
|
|
|
return 0
|
|
})
|
|
}
|
|
|
|
return items
|
|
}, [classified, searchQuery, sort])
|
|
|
|
const activeSheetRow: ClassifiedQuery | undefined = useMemo(() => {
|
|
if (mode === 'triage') {
|
|
return selectedTriageRow !== undefined ? filteredTriageItems[selectedTriageRow] : undefined
|
|
}
|
|
return selectedRow !== undefined ? (explorerItems[selectedRow] as ClassifiedQuery) : undefined
|
|
}, [mode, selectedTriageRow, selectedRow, filteredTriageItems, explorerItems])
|
|
|
|
const runExplain = useCallback(
|
|
(query: SafeSqlFragment) => {
|
|
if (explainResults[query]) return
|
|
if (explainLoadingQuery) return
|
|
const requestQuery = query
|
|
setExplainLoadingQuery(requestQuery)
|
|
executeExplain(
|
|
{
|
|
projectRef: project?.ref,
|
|
connectionString: project?.connectionString,
|
|
sql: wrapWithRollback(safeSql`EXPLAIN ANALYZE ${requestQuery}`),
|
|
},
|
|
{
|
|
onSuccess(data) {
|
|
setExplainResults((prev) => ({ ...prev, [requestQuery]: data.result }))
|
|
setExplainLoadingQuery(null)
|
|
},
|
|
onError() {
|
|
setExplainLoadingQuery(null)
|
|
},
|
|
}
|
|
)
|
|
},
|
|
[explainResults, explainLoadingQuery, executeExplain, project]
|
|
)
|
|
|
|
const handleGoToLogs = useCallback(() => {
|
|
router.push(`/project/${ref}/logs/postgres-logs`)
|
|
}, [router, ref])
|
|
|
|
const handleAiSuggestedFix = useCallback(
|
|
(item: ClassifiedQuery) => {
|
|
const { query, prompt } = buildQueryInsightFixPrompt(item)
|
|
openSidebar(SIDEBAR_KEYS.AI_ASSISTANT)
|
|
aiSnap.newChat({
|
|
sqlSnippets: [{ label: 'Query', content: query }],
|
|
initialMessage: prompt,
|
|
})
|
|
},
|
|
[openSidebar, aiSnap]
|
|
)
|
|
|
|
useEffect(() => {
|
|
if (sheetView === 'explain' && activeSheetRow?.query) {
|
|
runExplain(activeSheetRow.query)
|
|
}
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, [sheetView, activeSheetRow?.query])
|
|
|
|
useEffect(() => {
|
|
const currentPath = router.asPath.split('?')[0]
|
|
const handleRouteChange = (url: string) => {
|
|
if (url.split('?')[0] !== currentPath) {
|
|
setQueryStates({ search: null })
|
|
onCurrentSelectQuery?.(null)
|
|
}
|
|
}
|
|
router.events.on('routeChangeStart', handleRouteChange)
|
|
return () => router.events.off('routeChangeStart', handleRouteChange)
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, [])
|
|
|
|
const timeConsumedWidth = useMemo(() => {
|
|
if (!explorerItems.length) return 150
|
|
let maxWidth = 150
|
|
explorerItems.forEach((row) => {
|
|
const pct = row.prop_total_time || 0
|
|
const total = row.total_time || 0
|
|
if (pct && total) {
|
|
const text = `${pct.toFixed(1)}% / ${formatDuration(total)}`
|
|
maxWidth = Math.max(maxWidth, text.length * 8 + 40)
|
|
}
|
|
})
|
|
return Math.min(maxWidth, 300)
|
|
}, [explorerItems])
|
|
|
|
const triageQueryColWidth = useMemo(() => {
|
|
if (!triageContainerWidth) return 380
|
|
const fixed = timeConsumedWidth + 100 + 300 + 4
|
|
return Math.max(380, triageContainerWidth - fixed - 120)
|
|
}, [triageContainerWidth, timeConsumedWidth])
|
|
|
|
const { columns, triageColumns } = useQueryInsightsTableColumns({
|
|
sort,
|
|
setSort,
|
|
timeConsumedWidth,
|
|
triageQueryColWidth,
|
|
gridRef,
|
|
setSelectedRow,
|
|
setSelectedTriageRow,
|
|
setSheetView,
|
|
handleGoToLogs,
|
|
handleAiSuggestedFix,
|
|
})
|
|
|
|
const handleKeyDown = useCallback(
|
|
(event: KeyboardEvent) => {
|
|
if (!explorerItems.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 < explorerItems.length - 1) {
|
|
nextIndex = selectedRow + 1
|
|
}
|
|
|
|
if (nextIndex !== selectedRow) {
|
|
setSelectedRow(nextIndex)
|
|
gridRef.current?.scrollToCell({ idx: 0, rowIdx: nextIndex })
|
|
}
|
|
},
|
|
[explorerItems, selectedRow]
|
|
)
|
|
|
|
useEffect(() => {
|
|
window.addEventListener('keydown', handleKeyDown, true)
|
|
return () => {
|
|
window.removeEventListener('keydown', handleKeyDown, true)
|
|
}
|
|
}, [handleKeyDown])
|
|
|
|
useEffect(() => {
|
|
setSelectedRow(undefined)
|
|
}, [searchQuery, sort])
|
|
|
|
useEffect(() => {
|
|
if (mode === 'triage') {
|
|
triageGridRef.current?.scrollToCell({ idx: 0, rowIdx: 0 })
|
|
} else {
|
|
gridRef.current?.scrollToCell({ idx: 0, rowIdx: 0 })
|
|
}
|
|
}, [mode])
|
|
|
|
useEffect(() => {
|
|
const el = triageContainerRef.current
|
|
if (!el) return
|
|
const observer = new ResizeObserver(([entry]) => {
|
|
setTriageContainerWidth(entry.contentRect.width)
|
|
})
|
|
observer.observe(el)
|
|
return () => observer.disconnect()
|
|
}, [])
|
|
|
|
useEffect(() => {
|
|
if (urlSearch !== searchQuery) {
|
|
setQueryStates({ search: searchQuery || null })
|
|
}
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, [searchQuery])
|
|
|
|
const errorCount = errors.length
|
|
const indexCount = indexIssues.length
|
|
const slowCount = slowQueries.length
|
|
|
|
return (
|
|
<div className="flex flex-col flex-1 min-h-0">
|
|
<div className="overflow-x-auto shrink-0 bg-surface-100 border-b">
|
|
<div className="flex items-center justify-between px-6 h-10 min-w-max">
|
|
<div className="flex items-center gap-x-1.5">
|
|
<TwoOptionToggle
|
|
width={75}
|
|
options={['explorer', 'triage']}
|
|
activeOption={mode}
|
|
borderOverride="border"
|
|
onClickOption={(mode) => setMode(mode as Mode)}
|
|
/>
|
|
{appNameFilter.length > 0 ? (
|
|
<FilterPill
|
|
label="Source"
|
|
value={appNameFilter.join(', ')}
|
|
onClear={() => setAppNameFilter([])}
|
|
/>
|
|
) : (
|
|
<FilterPopover
|
|
name="Source"
|
|
options={appNameOptions}
|
|
activeOptions={appNameFilter}
|
|
valueKey="value"
|
|
labelKey="label"
|
|
onSaveFilters={setAppNameFilter}
|
|
showOnlyButton={false}
|
|
/>
|
|
)}
|
|
</div>
|
|
|
|
<div className="flex items-center">
|
|
{mode === 'triage' ? (
|
|
<Tabs value={filter} onValueChange={(v) => setFilter(v as IssueFilter)}>
|
|
<TabsList className="flex gap-x-4 rounded-none mt-0! pt-0 border-none!">
|
|
<TabsTrigger value="all" className="text-xs py-3 border-b font-mono uppercase">
|
|
All{triageItems.length > 0 && ` (${triageItems.length})`}
|
|
</TabsTrigger>
|
|
<TabsTrigger value="error" className="text-xs py-3 border-b font-mono uppercase">
|
|
Errors{errorCount > 0 && ` (${errorCount})`}
|
|
</TabsTrigger>
|
|
<TabsTrigger value="index" className="text-xs py-3 border-b font-mono uppercase">
|
|
Index{indexCount > 0 && ` (${indexCount})`}
|
|
</TabsTrigger>
|
|
<TabsTrigger value="slow" className="text-xs py-3 border-b font-mono uppercase">
|
|
Slow{slowCount > 0 && ` (${slowCount})`}
|
|
</TabsTrigger>
|
|
</TabsList>
|
|
</Tabs>
|
|
) : (
|
|
<Input
|
|
size="tiny"
|
|
autoComplete="off"
|
|
icon={<Search className="h-4 w-4" />}
|
|
value={searchQuery}
|
|
onChange={(e) => setSearchQuery(e.target.value)}
|
|
name="search"
|
|
id="search"
|
|
placeholder="Search queries..."
|
|
className="w-64"
|
|
actions={[
|
|
searchQuery && (
|
|
<Button
|
|
key="clear"
|
|
size="tiny"
|
|
variant="text"
|
|
icon={<X className="h-4 w-4" />}
|
|
onClick={() => setSearchQuery('')}
|
|
className="p-0 h-5 w-5"
|
|
/>
|
|
),
|
|
]}
|
|
/>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div
|
|
ref={scrollContainerRef}
|
|
className="relative flex-1 min-h-0 flex flex-col overflow-hidden"
|
|
>
|
|
<div
|
|
className={[
|
|
'absolute bottom-8 left-0 right-0 flex justify-center z-10 pointer-events-none',
|
|
'transition-all duration-200',
|
|
currentSelectedQuery
|
|
? 'opacity-100 translate-y-0 pointer-events-auto'
|
|
: 'opacity-0 translate-y-4 pointer-events-none',
|
|
].join(' ')}
|
|
>
|
|
<FloatingPlate rounded="full">
|
|
<Button
|
|
size="tiny"
|
|
className="rounded-full shadow-md"
|
|
onClick={() => onCurrentSelectQuery?.(null)}
|
|
>
|
|
Clear query
|
|
</Button>
|
|
</FloatingPlate>
|
|
</div>
|
|
{isLoading ? (
|
|
<div className="px-6 py-4">
|
|
<GenericSkeletonLoader />
|
|
</div>
|
|
) : mode === 'triage' ? (
|
|
<div ref={triageContainerRef} className="flex-1 min-h-0 min-w-0 overflow-x-auto">
|
|
<DataGrid
|
|
ref={triageGridRef}
|
|
style={{ height: '100%' }}
|
|
className="flex-1 grow h-full border-t-0! border-b-0!"
|
|
rowHeight={60}
|
|
headerRowHeight={36}
|
|
columns={triageColumns}
|
|
rows={filteredTriageItems}
|
|
rowClass={(_, idx) => {
|
|
const isSelected = idx === selectedTriageRow
|
|
const query = filteredTriageItems[idx]?.query
|
|
const isCharted = currentSelectedQuery ? currentSelectedQuery === query : false
|
|
return [
|
|
`${isSelected ? 'bg-surface-300 dark:bg-surface-300' : isCharted ? 'bg-surface-200 dark:bg-surface-200' : 'bg-200 hover:bg-surface-200'} cursor-pointer`,
|
|
'[&>div:first-child]:border-l-4 [&>div:first-child]:pl-5 [&>div:last-child]:pr-6',
|
|
`${isSelected || isCharted ? '[&>div:first-child]:border-l-foreground' : '[&>div:first-child]:border-l-transparent'}`,
|
|
'[&>.rdg-cell]:box-border [&>.rdg-cell]:outline-hidden [&>.rdg-cell]:shadow-none [&>.rdg-cell]:py-3',
|
|
'[&>.rdg-cell.column-prop_total_time]:relative',
|
|
].join(' ')
|
|
}}
|
|
renderers={{
|
|
renderRow(idx, props) {
|
|
return (
|
|
<Row
|
|
{...props}
|
|
key={`triage-row-${props.rowIdx}`}
|
|
onClick={(event) => {
|
|
event.stopPropagation()
|
|
if (typeof idx === 'number' && idx >= 0) {
|
|
const query = filteredTriageItems[idx]?.query
|
|
if (query && onCurrentSelectQuery) {
|
|
onCurrentSelectQuery(currentSelectedQuery === query ? null : query)
|
|
}
|
|
}
|
|
}}
|
|
/>
|
|
)
|
|
},
|
|
noRowsFallback: (
|
|
<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 issues found</p>
|
|
<p className="text-foreground-light">
|
|
{data.length === 0
|
|
? 'No query data available yet'
|
|
: 'No issues detected for the selected filter'}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
),
|
|
}}
|
|
/>
|
|
</div>
|
|
) : (
|
|
<div ref={dataGridContainerRef} className="flex-1 min-h-0 min-w-0 overflow-x-auto">
|
|
<DataGrid
|
|
ref={gridRef}
|
|
style={{ height: '100%' }}
|
|
className={cn('flex-1 grow h-full border-t-0! border-b-0!')}
|
|
rowHeight={44}
|
|
headerRowHeight={36}
|
|
columns={columns}
|
|
rows={explorerItems}
|
|
rowClass={(_, idx) => {
|
|
const isSelected = idx === selectedRow
|
|
const query = explorerItems[idx]?.query
|
|
const isCharted = currentSelectedQuery ? currentSelectedQuery === query : false
|
|
return [
|
|
`${isSelected ? 'bg-surface-300 dark:bg-surface-300' : isCharted ? 'bg-surface-200 dark:bg-surface-200' : 'bg-200 hover:bg-surface-200'} cursor-pointer`,
|
|
'[&>div:first-child]:border-l-4 [&>div:first-child]:pl-5 [&>div:last-child]:pr-6',
|
|
`${isSelected || isCharted ? '[&>div:first-child]:border-l-foreground' : '[&>div:first-child]:border-l-transparent'}`,
|
|
'[&>.rdg-cell]:box-border [&>.rdg-cell]:outline-hidden [&>.rdg-cell]:shadow-none [&>.rdg-cell]:py-3',
|
|
'[&>.rdg-cell.column-prop_total_time]:relative',
|
|
].join(' ')
|
|
}}
|
|
renderers={{
|
|
renderRow(idx, props) {
|
|
return (
|
|
<Row
|
|
{...props}
|
|
key={`explorer-row-${props.rowIdx}`}
|
|
onClick={(event) => {
|
|
event.stopPropagation()
|
|
if (typeof idx === 'number' && idx >= 0) {
|
|
const query = explorerItems[idx]?.query
|
|
if (query && onCurrentSelectQuery) {
|
|
onCurrentSelectQuery(currentSelectedQuery === query ? null : query)
|
|
}
|
|
}
|
|
}}
|
|
/>
|
|
)
|
|
},
|
|
noRowsFallback: (
|
|
<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 found</p>
|
|
<p className="text-foreground-light">
|
|
{searchQuery.trim()
|
|
? 'No queries match your search criteria'
|
|
: 'No query data available yet'}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
),
|
|
}}
|
|
/>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
<QueryInsightsDetailSheet
|
|
open={activeSheetRow !== undefined}
|
|
onOpenChange={(open) => {
|
|
if (!open) {
|
|
setSelectedTriageRow(undefined)
|
|
setSelectedRow(undefined)
|
|
}
|
|
}}
|
|
activeSheetRow={activeSheetRow}
|
|
sheetView={sheetView}
|
|
onSheetViewChange={setSheetView}
|
|
onClose={() => {
|
|
setSelectedTriageRow(undefined)
|
|
setSelectedRow(undefined)
|
|
}}
|
|
dataGridContainerRef={dataGridContainerRef}
|
|
triageContainerRef={triageContainerRef}
|
|
explainLoadingQuery={explainLoadingQuery}
|
|
explainResults={explainResults}
|
|
/>
|
|
</div>
|
|
)
|
|
}
|