This commit is contained in:
Jonathan Summers-Muir authored and kemal committed 2025-08-08 11:21:23 +01:00
1 parent d60aceb562
commit cba8ca919d
10 files changed
+925

No files matched your search

@@ -0,0 +1,183 @@
import { useEffect, useState } from 'react'
import { useRouter } from 'next/router'
import { useParams } from 'common'
import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
import { DateRangePicker } from 'components/ui/DateRangePicker'
import { TabsList_Shadcn_, TabsTrigger_Shadcn_, Tabs_Shadcn_, cn } from 'ui'
import dayjs from 'dayjs'
import { InformationCircleIcon } from '@heroicons/react/16/solid'
import { Tooltip, TooltipContent, TooltipTrigger } from 'ui'
import { MetricsChart } from './components/MetricsChart'
import { QueryList } from './components/QueryList'
import {
useQueryInsightsMetrics,
useQueryInsightsQueries,
} from 'data/query-insights/query-insights-query'
import { useDatabaseSelectorStateSnapshot } from 'state/database-selector'
import { useReadReplicasQuery } from 'data/read-replicas/replicas-query'
export type MetricType = 'rows_read' | 'rows_written' | 'query_latency' | 'queries_per_second'
const METRICS: { id: MetricType; label: string; description: string }[] = [
{
id: 'query_latency',
label: 'Query Latency',
description: 'Average query execution time in milliseconds',
},
{
id: 'rows_read',
label: 'Rows Read',
description: 'Number of rows read by queries',
},
{
id: 'rows_written',
label: 'Rows Written',
description: 'Number of rows written by queries',
},
{
id: 'queries_per_second',
label: 'Queries per Second',
description: 'Number of queries executed per second',
},
]
export const QueryInsights = () => {
const router = useRouter()
const { ref } = useParams()
const { project } = useProjectContext()
const state = useDatabaseSelectorStateSnapshot()
const { data: databases } = useReadReplicasQuery({ projectRef: ref })
const [selectedMetric, setSelectedMetric] = useState<MetricType>('query_latency')
const [selectedTimeRange, setSelectedTimeRange] = useState('24h')
const [timeRange, setTimeRange] = useState({
period_start: {
date: dayjs().subtract(24, 'hour').format('YYYY-MM-DD HH:mm:ssZ'),
time_period: '24h',
},
period_end: {
date: dayjs().format('YYYY-MM-DD HH:mm:ssZ'),
time_period: 'now',
},
})
const { data: metricsData, isLoading: isLoadingMetrics } = useQueryInsightsMetrics(
ref,
selectedMetric,
timeRange.period_start.date,
timeRange.period_end.date
)
const { data: queriesData, isLoading: isLoadingQueries } = useQueryInsightsQueries(
ref,
timeRange.period_start.date,
timeRange.period_end.date
)
const selectedDatabase = databases?.find((db) => db.identifier === state.selectedDatabaseId)
useEffect(() => {
// Update the time range when selectedTimeRange changes
switch (selectedTimeRange) {
case '24h':
setTimeRange({
period_start: {
date: dayjs().subtract(24, 'hour').format('YYYY-MM-DD HH:mm:ssZ'),
time_period: '24h',
},
period_end: {
date: dayjs().format('YYYY-MM-DD HH:mm:ssZ'),
time_period: 'now',
},
})
break
case '7d':
setTimeRange({
period_start: {
date: dayjs().subtract(7, 'day').format('YYYY-MM-DD HH:mm:ssZ'),
time_period: '7d',
},
period_end: {
date: dayjs().format('YYYY-MM-DD HH:mm:ssZ'),
time_period: 'now',
},
})
break
case '30d':
setTimeRange({
period_start: {
date: dayjs().subtract(30, 'day').format('YYYY-MM-DD HH:mm:ssZ'),
time_period: '30d',
},
period_end: {
date: dayjs().format('YYYY-MM-DD HH:mm:ssZ'),
time_period: 'now',
},
})
break
}
}, [selectedTimeRange])
return (
<div className="flex flex-col gap-6">
<div className="flex items-center justify-between">
<div>
<h3 className="text-xl text-foreground">Query Metrics</h3>
<p className="text-sm text-foreground-light">
Monitor and analyze query performance over time
</p>
</div>
<DateRangePicker
value={selectedTimeRange}
loading={isLoadingMetrics}
onChange={({ period_start, period_end, interval }) => {
setSelectedTimeRange(period_start.time_period)
setTimeRange({ period_start, period_end })
}}
options={[
{
key: '24h',
label: 'Last 24 hours',
interval: '1h',
},
{
key: '7d',
label: 'Last 7 days',
interval: '1d',
},
{
key: '30d',
label: 'Last 30 days',
interval: '1d',
},
]}
/>
</div>
<Tabs_Shadcn_
value={selectedMetric}
onValueChange={(value) => setSelectedMetric(value as MetricType)}
className="space-y-4"
>
<TabsList_Shadcn_>
{METRICS.map((metric) => (
<TabsTrigger_Shadcn_ key={metric.id} value={metric.id}>
{metric.label}
</TabsTrigger_Shadcn_>
))}
</TabsList_Shadcn_>
<div className="h-80">
<MetricsChart data={metricsData} metric={selectedMetric} isLoading={isLoadingMetrics} />
</div>
</Tabs_Shadcn_>
<div>
<h3 className="text-xl text-foreground">Recent Queries</h3>
<p className="text-sm text-foreground-light">Detailed view of recent query executions</p>
</div>
<QueryList queries={queriesData || []} isLoading={isLoadingQueries} />
</div>
)
}
@@ -0,0 +1,48 @@
import { useEffect, useState } from 'react'
import { useRouter } from 'next/router'
import { useParams } from 'common'
import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
import { FormHeader } from 'components/ui/Forms/FormHeader'
import DatabaseSelector from 'components/ui/DatabaseSelector'
import { DateRangePicker } from 'components/ui/DateRangePicker'
import { MetricsOverview } from './components/MetricsOverview'
import { QueryList } from './components/QueryList'
import dayjs from 'dayjs'
export type MetricType = 'rows_read' | 'rows_written' | 'query_latency' | 'queries_per_second'
const QueryInsightsPage = () => {
const router = useRouter()
const { ref } = useParams()
const { project } = useProjectContext()
const [selectedMetric, setSelectedMetric] = useState<MetricType>('rows_read')
const [timeRange, setTimeRange] = useState({
startDate: dayjs().subtract(24, 'hour'),
endDate: dayjs(),
})
return (
<div className="h-full flex flex-col">
<FormHeader
className="py-4 px-6 !mb-0"
title="Query Insights"
description="Monitor and analyze query performance across your database"
docsUrl="https://supabase.com/docs/guides/platform/performance"
actions={<DatabaseSelector />}
/>
<div className="flex flex-col flex-grow">
<MetricsOverview
selectedMetric={selectedMetric}
onMetricChange={setSelectedMetric}
timeRange={timeRange}
onTimeRangeChange={setTimeRange}
/>
<QueryList timeRange={timeRange} />
</div>
</div>
)
}
export default QueryInsightsPage
@@ -0,0 +1,214 @@
import { LogsBarChart } from 'ui-patterns/LogsBarChart'
import { QueryInsightsMetric } from 'data/query-insights/query-insights-query'
import dayjs from 'dayjs'
import { Line, LineChart, XAxis, YAxis, CartesianGrid, Area, AreaChart } from 'recharts'
import {
ChartConfig,
ChartContainer,
ChartTooltip,
ChartTooltipContent,
ChartLegend,
ChartLegendContent,
cn,
} from 'ui'
import { useState } from 'react'
interface MetricsChartProps {
data: QueryInsightsMetric[] | undefined
metric: string
isLoading: boolean
}
export const MetricsChart = ({ data, metric, isLoading }: MetricsChartProps) => {
const [visibleMetrics, setVisibleMetrics] = useState({
p50: true,
p95: true,
p99: true,
p99_9: true,
})
if (isLoading) {
return (
<div className="flex items-center justify-center h-full">
<div className="text-sm text-foreground-light">Loading metrics...</div>
</div>
)
}
if (!Array.isArray(data) || data.length === 0) {
return (
<div className="flex items-center justify-center h-full">
<div className="text-sm text-foreground-light">No data available</div>
</div>
)
}
if (metric === 'query_latency') {
const chartData = data.map((point) => ({
timestamp: dayjs(point.timestamp).format('HH:mm'),
p50: point.p50,
p95: point.p95,
p99: point.p99,
p99_9: point.p99_9,
}))
const config = {
p50: {
label: 'p50',
color: 'hsl(var(--brand-default))',
},
p95: {
label: 'p95',
color: 'hsl(var(--foreground-default))',
},
p99: {
label: 'p99',
color: 'hsl(var(--warning-default))',
},
p99_9: {
label: 'p99.9',
color: 'hsl(var(--destructive-default))',
},
} satisfies ChartConfig
return (
<div className="flex flex-col gap-2">
<div className="flex gap-2">
{Object.entries(config).map(([key, value]) => {
const metricValue = Number(
chartData[chartData.length - 1]?.[key as keyof (typeof chartData)[0]]
)
const formattedValue = !isNaN(metricValue) ? `${Math.round(metricValue)} ms` : '0 ms'
return (
<button
key={key}
className={cn(
'text-xs px-2 py-1 rounded-full transition-colors inline-flex items-center gap-1.5',
visibleMetrics[key as keyof typeof visibleMetrics]
? 'bg-surface-300'
: 'hover:bg-surface-100'
)}
onClick={() =>
setVisibleMetrics((prev) => ({
...prev,
[key]: !prev[key as keyof typeof visibleMetrics],
}))
}
>
<div className="w-2 h-2 rounded-full" style={{ backgroundColor: value.color }} />
<span>
{value.label}: {formattedValue}
</span>
</button>
)
})}
</div>
<ChartContainer className="h-[300px] w-full" config={config}>
<AreaChart
data={chartData}
width={100}
height={100}
style={{ width: '100%', height: '100%' }}
margin={{
left: 12,
right: 12,
}}
>
<defs>
{Object.entries(config).map(([key, value]) => (
<linearGradient key={key} id={`gradient-${key}`} x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor={value.color} stopOpacity={0.3} />
<stop offset="95%" stopColor={value.color} stopOpacity={0.05} />
</linearGradient>
))}
</defs>
<CartesianGrid vertical={false} />
<XAxis
dataKey="timestamp"
interval="preserveStartEnd"
minTickGap={50}
tickLine={false}
axisLine={false}
tickMargin={8}
/>
<YAxis />
<ChartTooltip
cursor={false}
content={<ChartTooltipContent indicator="dot" hideLabel />}
/>
<ChartLegend content={<ChartLegendContent />} />
{visibleMetrics.p50 && (
<Area
type="monotone"
dataKey="p50"
fill={config.p50.color}
fillOpacity={0.4}
stroke={config.p50.color}
strokeWidth={2}
dot={false}
animationDuration={100}
/>
)}
{visibleMetrics.p95 && (
<Area
type="monotone"
dataKey="p95"
fill={config.p95.color}
fillOpacity={0.4}
stroke={config.p95.color}
strokeWidth={2}
dot={false}
animationDuration={100}
/>
)}
{visibleMetrics.p99 && (
<Area
type="monotone"
dataKey="p99"
fill={config.p99.color}
fillOpacity={0.4}
stroke={config.p99.color}
strokeWidth={2}
dot={false}
animationDuration={100}
/>
)}
{visibleMetrics.p99_9 && (
<Area
type="monotone"
dataKey="p99_9"
fill={config.p99_9.color}
fillOpacity={0.4}
stroke={config.p99_9.color}
strokeWidth={2}
dot={false}
animationDuration={100}
/>
)}
</AreaChart>
</ChartContainer>
</div>
)
}
// For other metrics, use the bar chart
const chartData = data.map((point) => ({
timestamp: dayjs(point.timestamp).toISOString(),
ok_count: point.value ?? 0,
warning_count: 0,
error_count: 0,
}))
return (
<LogsBarChart
data={chartData}
DateTimeFormat="HH:mm"
EmptyState={
<div className="flex items-center justify-center h-full">
<div className="text-sm text-foreground-light">No data available</div>
</div>
}
/>
)
}
@@ -0,0 +1,128 @@
import { useState } from 'react'
import { Line, XAxis, YAxis, Tooltip, Area, ComposedChart } from 'recharts'
import { ChartContainer, ChartConfig } from 'components/ui/Charts'
import { CHART_COLORS } from 'components/ui/Charts/Charts.constants'
import dayjs from 'dayjs'
import { cn } from 'ui'
export interface QueryInsightsChartDatum {
timestamp: string
p50: number
p95: number
p99: number
p999: number
}
export const QueryInsightsLineChart = ({
data,
onPointClick,
EmptyState,
DateTimeFormat = 'MMM D, HH:mm',
}: {
data: QueryInsightsChartDatum[]
onPointClick?: (datum: QueryInsightsChartDatum, tooltipData?: Record<string, any>) => void
EmptyState?: React.ReactNode
DateTimeFormat?: string
}) => {
const [focusDataIndex, setFocusDataIndex] = useState<number | null>(null)
if (data.length === 0) {
if (EmptyState) return EmptyState
return null
}
const startDate = dayjs(data[0]['timestamp']).format(DateTimeFormat)
const endDate = dayjs(data[data?.length - 1]?.['timestamp']).format(DateTimeFormat)
return (
<div className={cn('flex flex-col gap-y-3')}>
<ChartContainer
config={
{
p50: {
label: 'p50',
color: CHART_COLORS.GREEN_1,
},
p95: {
label: 'p95',
color: CHART_COLORS.BLUE_1,
},
p99: {
label: 'p99',
color: '#925AFF',
},
p999: {
label: 'p99.9',
color: '#FF8B3E',
},
} satisfies ChartConfig
}
className="h-[400px]"
>
<ComposedChart
data={data}
onMouseMove={(e: { activeTooltipIndex: number | null }) => {
if (e.activeTooltipIndex !== focusDataIndex) {
setFocusDataIndex(e.activeTooltipIndex)
}
}}
onMouseLeave={() => setFocusDataIndex(null)}
onClick={(tooltipData: Record<string, any>) => {
const datum = tooltipData?.activePayload?.[0]?.payload
if (onPointClick) onPointClick(datum, tooltipData)
}}
>
<defs>
<linearGradient id="p95Gradient" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stopColor={CHART_COLORS.BLUE_1} stopOpacity={0.2} />
<stop offset="100%" stopColor={CHART_COLORS.BLUE_1} stopOpacity={0} />
</linearGradient>
</defs>
<XAxis
dataKey="timestamp"
tickFormatter={(v) => dayjs(v).format(DateTimeFormat)}
stroke={CHART_COLORS.AXIS}
fontSize={10}
tickLine={false}
/>
<YAxis
tickFormatter={(v) => `${v}ms`}
stroke={CHART_COLORS.AXIS}
fontSize={10}
tickLine={false}
/>
<Tooltip
labelFormatter={(v) => dayjs(v).format(DateTimeFormat)}
formatter={(value: number) => [`${value}ms`]}
/>
<Area type="monotone" dataKey="p95" stroke="none" fill="url(#p95Gradient)" />
<Line
type="monotone"
dataKey="p50"
stroke={CHART_COLORS.GREEN_1}
strokeWidth={1.5}
dot={false}
/>
<Line
type="monotone"
dataKey="p95"
stroke={CHART_COLORS.BLUE_1}
strokeWidth={1.5}
dot={false}
/>
<Line type="monotone" dataKey="p99" stroke="#925AFF" strokeWidth={1.5} dot={false} />
<Line type="monotone" dataKey="p999" stroke="#FF8B3E" strokeWidth={1.5} dot={false} />
</ComposedChart>
</ChartContainer>
{data && (
<div className="text-foreground-lighter -mt-10 flex items-center justify-between text-[10px] font-mono">
<span>{startDate}</span>
<span>{endDate}</span>
</div>
)}
</div>
)
}
@@ -0,0 +1,128 @@
import { useEffect, useState } from 'react'
import { Clock, Database } from 'lucide-react'
import dayjs from 'dayjs'
import relativeTime from 'dayjs/plugin/relativeTime'
import { Button } from 'ui'
dayjs.extend(relativeTime)
interface Query {
query_id: string
query: string
total_time: number
calls: number
rows: number
shared_blks_read: number
shared_blks_hit: number
mean_exec_time: number
database: string
timestamp: string
}
interface QueryListProps {
queries?: Query[]
isLoading?: boolean
}
export const QueryList = ({ queries = [], isLoading = false }: QueryListProps) => {
const [expandedQueries, setExpandedQueries] = useState<Set<string>>(new Set())
const toggleQueryExpansion = (queryId: string) => {
const newExpanded = new Set(expandedQueries)
if (newExpanded.has(queryId)) {
newExpanded.delete(queryId)
} else {
newExpanded.add(queryId)
}
setExpandedQueries(newExpanded)
}
if (isLoading) {
return (
<div className="flex items-center justify-center py-8">
<div className="animate-pulse">Loading queries...</div>
</div>
)
}
if (queries.length === 0) {
return (
<div className="flex items-center justify-center py-8 text-foreground-light">
No queries found for the selected time range
</div>
)
}
return (
<div className="space-y-2">
{queries.map((query) => (
<div
key={query.query_id}
className="rounded-md border border-default bg-surface-100 overflow-hidden"
>
<div className="p-4">
<div className="flex items-start justify-between gap-4">
<div className="space-y-1 flex-grow">
<div className="font-mono text-xs text-foreground-light break-all">
{expandedQueries.has(query.query_id) ? (
query.query
) : (
<>
{query.query.slice(0, 100)}
{query.query.length > 100 && '...'}
</>
)}
</div>
<div className="flex items-center gap-x-4 text-xs text-foreground-light">
<div className="flex items-center gap-x-1">
<Database strokeWidth={1.5} size={14} />
<span>{query.database}</span>
</div>
<div className="flex items-center gap-x-1">
<Clock strokeWidth={1.5} size={14} />
<span>{dayjs(query.timestamp).fromNow()}</span>
</div>
</div>
</div>
{query.query.length > 100 && (
<Button
type="default"
size="tiny"
onClick={() => toggleQueryExpansion(query.query_id)}
>
{expandedQueries.has(query.query_id) ? 'Show less' : 'Show more'}
</Button>
)}
</div>
<div className="mt-4 grid grid-cols-4 gap-4">
<div>
<div className="text-xs text-foreground-light mb-1">Total time</div>
<div className="text-sm">{query.total_time.toFixed(2)}ms</div>
</div>
<div>
<div className="text-xs text-foreground-light mb-1">Calls</div>
<div className="text-sm">{query.calls}</div>
</div>
<div>
<div className="text-xs text-foreground-light mb-1">Rows</div>
<div className="text-sm">
{query.rows >= 1000 ? `${(query.rows / 1000).toFixed(1)}k` : query.rows}
</div>
</div>
<div>
<div className="text-xs text-foreground-light mb-1">Cache hit ratio</div>
<div className="text-sm">
{(
(query.shared_blks_hit / (query.shared_blks_hit + query.shared_blks_read)) *
100
).toFixed(1)}
%
</div>
</div>
</div>
</div>
</div>
))}
</div>
)
}
@@ -30,6 +30,12 @@ export const generateAdvisorsMenu = (
url: `/project/${ref}/advisors/query-performance`,
items: [],
},
{
name: 'Query Insights',
key: 'query-insights',
url: `/project/${ref}/advisors/query`,
items: [],
},
],
},
...(IS_PLATFORM && features?.advisorRules
+8
View File
@@ -0,0 +1,8 @@
import { useParams } from 'common'
export const queryInsightsKeys = {
metrics: (projectRef: string | undefined, metric: string, startTime: string, endTime: string) =>
['query-insights', 'metrics', projectRef, metric, startTime, endTime] as const,
queries: (projectRef: string | undefined, startTime: string, endTime: string) =>
['query-insights', 'queries', projectRef, startTime, endTime] as const,
}
@@ -0,0 +1,180 @@
import { useQuery, UseQueryOptions } from '@tanstack/react-query'
import { executeSql } from '../sql/execute-sql-query'
import { queryInsightsKeys } from './keys'
export type QueryInsightsMetric = {
timestamp: string
value?: number
database: string
rows?: number
shared_blks_read?: number
shared_blks_hit?: number
mean_exec_time?: number
total_exec_time?: number
p50?: number
p95?: number
p99?: number
p99_9?: number
}
export type QueryInsightsQuery = {
query_id: string
query: string
total_time: number
calls: number
rows: number
shared_blks_read: number
shared_blks_hit: number
mean_exec_time: number
database: string
timestamp: string
}
export type QueryInsightsMetrics = {
queries: {
total: number
total_time: number
avg_time: number
total_rows: number
}
cache: {
hit_ratio: number
hits: number
misses: number
}
errors: {
total: number
}
}
const getMetricsSql = (metric: string, startTime: string, endTime: string) => {
switch (metric) {
case 'rows_read':
return /* SQL */ `
SELECT
bucket_start_time as timestamp,
SUM(rows_retrieved) as value,
datname as database
FROM pg_stat_monitor
WHERE bucket_start_time >= '${startTime}'
AND bucket_start_time <= '${endTime}'
GROUP BY bucket_start_time, datname
ORDER BY bucket_start_time ASC
`
case 'rows_written':
return /* SQL */ `
SELECT
bucket_start_time as timestamp,
SUM(rows_retrieved) as value,
datname as database
FROM pg_stat_monitor
WHERE bucket_start_time >= '${startTime}'
AND bucket_start_time <= '${endTime}'
GROUP BY bucket_start_time, datname
ORDER BY bucket_start_time ASC
`
case 'query_latency':
return /* SQL */ `
SELECT
bucket_start_time as timestamp,
percentile_cont(0.50) WITHIN GROUP (ORDER BY mean_exec_time) as p50,
percentile_cont(0.95) WITHIN GROUP (ORDER BY mean_exec_time) as p95,
percentile_cont(0.99) WITHIN GROUP (ORDER BY mean_exec_time) as p99,
percentile_cont(0.999) WITHIN GROUP (ORDER BY mean_exec_time) as p99_9,
datname as database
FROM pg_stat_monitor
WHERE bucket_start_time >= '${startTime}'
AND bucket_start_time <= '${endTime}'
GROUP BY bucket_start_time, datname
ORDER BY bucket_start_time ASC
`
case 'queries_per_second':
return /* SQL */ `
SELECT
bucket_start_time as timestamp,
COUNT(*) as value,
datname as database
FROM pg_stat_monitor
WHERE bucket_start_time >= '${startTime}'
AND bucket_start_time <= '${endTime}'
GROUP BY bucket_start_time, datname
ORDER BY bucket_start_time ASC
`
default:
return /* SQL */ `
SELECT
bucket_start_time as timestamp,
SUM(rows_retrieved) as value,
datname as database
FROM pg_stat_monitor
WHERE bucket_start_time >= '${startTime}'
AND bucket_start_time <= '${endTime}'
GROUP BY bucket_start_time, datname
ORDER BY bucket_start_time ASC
`
}
}
const getQueriesSql = (startTime: string, endTime: string) => /* SQL */ `
SELECT
queryid as query_id,
query,
total_exec_time as total_time,
calls,
rows_retrieved as rows,
shared_blks_read,
shared_blks_hit,
mean_exec_time,
datname as database,
bucket_start_time as timestamp
FROM pg_stat_monitor
WHERE bucket_start_time >= '${startTime}'
AND bucket_start_time <= '${endTime}'
ORDER BY calls DESC
LIMIT 100
`
export function useQueryInsightsMetrics(
projectRef: string | undefined,
metric: string,
startTime: string,
endTime: string,
options?: UseQueryOptions<QueryInsightsMetric[]>
) {
return useQuery({
queryKey: queryInsightsKeys.metrics(projectRef, metric, startTime, endTime),
queryFn: async () => {
if (!projectRef) throw new Error('Project ref is required')
const { result } = await executeSql({
projectRef,
sql: getMetricsSql(metric, startTime, endTime),
})
return result as QueryInsightsMetric[]
},
...options,
})
}
export function useQueryInsightsQueries(
projectRef: string | undefined,
startTime: string,
endTime: string,
options?: UseQueryOptions<QueryInsightsQuery[]>
) {
return useQuery({
queryKey: queryInsightsKeys.queries(projectRef, startTime, endTime),
queryFn: async () => {
if (!projectRef) throw new Error('Project ref is required')
const { result } = await executeSql({
projectRef,
sql: getQueriesSql(startTime, endTime),
})
return result as QueryInsightsQuery[]
},
...options,
})
}
@@ -0,0 +1,29 @@
import { QueryInsights } from 'components/interfaces/QueryInsights/QueryInsights'
import AdvisorsLayout from 'components/layouts/AdvisorsLayout/AdvisorsLayout'
import DefaultLayout from 'components/layouts/DefaultLayout'
import DatabaseSelector from 'components/ui/DatabaseSelector'
import { FormHeader } from 'components/ui/Forms/FormHeader'
import type { NextPageWithLayout } from 'types'
const QueryInsightsPage: NextPageWithLayout = () => {
return (
<div className="h-full flex flex-col">
<FormHeader
className="py-4 px-6 !mb-0"
title="Query Insights"
description="Monitor and analyze query performance with enhanced metrics"
docsUrl="https://supabase.com/docs/guides/platform/performance"
actions={<DatabaseSelector />}
/>
<QueryInsights />
</div>
)
}
QueryInsightsPage.getLayout = (page) => (
<DefaultLayout>
<AdvisorsLayout title="Query insights">{page}</AdvisorsLayout>
</DefaultLayout>
)
export default QueryInsightsPage