feat: query performance improvements (#20907)
* very wip * add expandable rows * fix table layout, collapsible row, spacing issues * use new query with filters everywhere * rm unused queries * rm unused fn * improve loading state * fix text overflowing in role * rm padding so that table doesn't always need scroll * fix icon in search input * add latency to table row heading to clarify what col youre sorting with * rm unused imports * run prettier * align sql with row content * add syntax highlighting and sort icons * rm copy btn * move tailwind dep to correct package, rm unused syntax highlighting, rm unused component
This commit is contained in:
@@ -9,20 +9,6 @@ interface Props {
|
||||
const ReportHeader: React.FC<Props> = ({ title, onRefresh, isLoading }) => (
|
||||
<div className="flex flex-row justify-between gap-4 items-center">
|
||||
<h1 className="text-2xl text-foreground">{title}</h1>
|
||||
<Button
|
||||
type="default"
|
||||
size="tiny"
|
||||
onClick={onRefresh}
|
||||
disabled={isLoading ? true : false}
|
||||
icon={
|
||||
<IconRefreshCw
|
||||
size="tiny"
|
||||
className={`text-foreground-light ${isLoading ? 'animate-spin' : ''}`}
|
||||
/>
|
||||
}
|
||||
>
|
||||
{isLoading ? 'Refreshing' : 'Refresh'}
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
export default ReportHeader
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
import Table from 'components/to-be-cleaned/Table'
|
||||
import React from 'react'
|
||||
import { cn } from 'ui'
|
||||
import { Editor } from '@monaco-editor/react'
|
||||
|
||||
type Props = {
|
||||
sql: string
|
||||
colSpan: number
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
const ReportQueryPerformanceTableRow = ({ sql, colSpan, children }: Props) => {
|
||||
const [expanded, setExpanded] = React.useState(false)
|
||||
|
||||
return (
|
||||
<>
|
||||
<Table.tr onClick={() => setExpanded(!expanded)}>{children}</Table.tr>
|
||||
<tr
|
||||
className={cn(
|
||||
{
|
||||
'h-0 opacity-0': !expanded,
|
||||
'h-4 opacity-100': expanded,
|
||||
},
|
||||
'transition-all'
|
||||
)}
|
||||
>
|
||||
{expanded && (
|
||||
<td colSpan={colSpan} className="!p-0 max-w-xl relative">
|
||||
<div className="overflow-auto max-h-[400px] bg-background-alternative-200">
|
||||
<Editor
|
||||
className={cn('monaco-editor h-80')}
|
||||
theme={'supabase'}
|
||||
defaultLanguage="pgsql"
|
||||
value={sql}
|
||||
options={{
|
||||
readOnly: true,
|
||||
tabSize: 2,
|
||||
fontSize: 13,
|
||||
minimap: { enabled: false },
|
||||
wordWrap: 'on',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</td>
|
||||
)}
|
||||
</tr>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default ReportQueryPerformanceTableRow
|
||||
@@ -33,7 +33,7 @@ export const DEFAULT_QUERY_PARAMS = {
|
||||
iso_timestamp_end: REPORTS_DATEPICKER_HELPERS[0].calcTo(),
|
||||
}
|
||||
|
||||
const generateRegexpWhere = (filters: ReportFilterItem[], prepend = true) => {
|
||||
export const generateRegexpWhere = (filters: ReportFilterItem[], prepend = true) => {
|
||||
if (filters.length === 0) return ''
|
||||
const conditions = filters
|
||||
.map((filter) => {
|
||||
@@ -276,9 +276,8 @@ limit 12
|
||||
queries: {
|
||||
mostFrequentlyInvoked: {
|
||||
queryType: 'db',
|
||||
sql: (_params) => `
|
||||
sql: (_params, where, orderBy) => `
|
||||
-- Most frequently called queries
|
||||
-- A limit of 100 has been added below
|
||||
select
|
||||
auth.rolname,
|
||||
statements.query,
|
||||
@@ -296,13 +295,14 @@ select
|
||||
statements.rows / statements.calls as avg_rows
|
||||
from pg_stat_statements as statements
|
||||
inner join pg_authid as auth on statements.userid = auth.oid
|
||||
order by
|
||||
statements.calls desc
|
||||
${where || ''}
|
||||
${orderBy || 'order by statements.calls desc'}
|
||||
limit 10;`,
|
||||
},
|
||||
mostTimeConsuming: {
|
||||
queryType: 'db',
|
||||
sql: (_params) => `-- A limit of 100 has been added below
|
||||
sql: (_, where, orderBy) => `
|
||||
-- Most time consuming queries
|
||||
select
|
||||
auth.rolname,
|
||||
statements.query,
|
||||
@@ -311,14 +311,14 @@ select
|
||||
to_char(((statements.total_exec_time + statements.total_plan_time)/sum(statements.total_exec_time + statements.total_plan_time) OVER()) * 100, 'FM90D0') || '%' AS prop_total_time
|
||||
from pg_stat_statements as statements
|
||||
inner join pg_authid as auth on statements.userid = auth.oid
|
||||
order by
|
||||
total_time desc
|
||||
${where || ''}
|
||||
${orderBy || 'order by total_time desc'}
|
||||
limit 10;`,
|
||||
},
|
||||
slowestExecutionTime: {
|
||||
queryType: 'db',
|
||||
sql: (_params) => `-- Slowest queries by max execution time
|
||||
-- A limit of 100 has been added below
|
||||
sql: (_params, where, orderBy) => `
|
||||
-- Slowest queries by max execution time
|
||||
select
|
||||
auth.rolname,
|
||||
statements.query,
|
||||
@@ -336,8 +336,8 @@ select
|
||||
statements.rows / statements.calls as avg_rows
|
||||
from pg_stat_statements as statements
|
||||
inner join pg_authid as auth on statements.userid = auth.oid
|
||||
order by
|
||||
max_time desc
|
||||
${where || ''}
|
||||
${orderBy || 'order by max_time desc'}
|
||||
limit 10`,
|
||||
},
|
||||
queryHitRate: {
|
||||
|
||||
33
apps/studio/components/interfaces/Reports/Reports.queries.ts
Normal file
33
apps/studio/components/interfaces/Reports/Reports.queries.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { PRESET_CONFIG } from './Reports.constants'
|
||||
import { Presets } from './Reports.types'
|
||||
import useDbQuery from 'hooks/analytics/useDbQuery'
|
||||
|
||||
type QueryPerformanceQueryOpts = {
|
||||
searchQuery: string
|
||||
preset: 'mostFrequentlyInvoked' | 'mostTimeConsuming' | 'slowestExecutionTime' | 'queryHitRate'
|
||||
orderBy: string | 'lat_asc' | 'lat_desc'
|
||||
}
|
||||
export const useQueryPerformanceQuery = ({
|
||||
preset,
|
||||
orderBy,
|
||||
searchQuery,
|
||||
}: QueryPerformanceQueryOpts) => {
|
||||
const queryPerfQueries = PRESET_CONFIG[Presets.QUERY_PERFORMANCE]
|
||||
const baseSQL = queryPerfQueries.queries[preset]
|
||||
|
||||
if (orderBy !== 'lat_asc' && orderBy !== 'lat_desc') {
|
||||
// Default to lat_desc if not specified or invalid
|
||||
orderBy = 'lat_desc'
|
||||
}
|
||||
|
||||
const whereSql = searchQuery
|
||||
? `WHERE auth.rolname ~ '${searchQuery}' OR statements.query ~ '${searchQuery}'`
|
||||
: ''
|
||||
const orderBySql = orderBy === 'lat_asc' ? 'ORDER BY total_time asc' : 'ORDER BY total_time desc'
|
||||
|
||||
const sql = baseSQL.sql([], whereSql, orderBySql)
|
||||
|
||||
// console.log('DEBUG Using sql query: ', sql)
|
||||
|
||||
return useDbQuery(sql, undefined, whereSql, orderBySql)
|
||||
}
|
||||
@@ -20,7 +20,7 @@ export type BaseQueries<Keys extends string> = Record<Keys, ReportQuery>
|
||||
|
||||
export interface ReportQuery {
|
||||
queryType: ReportQueryType
|
||||
sql: (filters: ReportFilterItem[]) => string
|
||||
sql: (filters: ReportFilterItem[], where?: string, orderBy?: string) => string
|
||||
}
|
||||
|
||||
export type ReportQueryType = 'db' | 'logs'
|
||||
@@ -45,4 +45,5 @@ export interface ReportFilterItem {
|
||||
key: string
|
||||
value: string | number
|
||||
compare: 'matches' | 'is'
|
||||
query?: string
|
||||
}
|
||||
|
||||
@@ -5,8 +5,18 @@ import { useEffect, useState } from 'react'
|
||||
export interface CopyButtonProps extends ButtonProps {
|
||||
text: string
|
||||
iconOnly?: boolean
|
||||
copyLabel?: string
|
||||
copiedLabel?: string
|
||||
}
|
||||
const CopyButton = ({ text, iconOnly = false, children, onClick, ...props }: CopyButtonProps) => {
|
||||
const CopyButton = ({
|
||||
text,
|
||||
iconOnly = false,
|
||||
children,
|
||||
onClick,
|
||||
copyLabel = 'Copy',
|
||||
copiedLabel = 'Copied',
|
||||
...props
|
||||
}: CopyButtonProps) => {
|
||||
const [showCopied, setShowCopied] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
@@ -31,7 +41,7 @@ const CopyButton = ({ text, iconOnly = false, children, onClick, ...props }: Cop
|
||||
}
|
||||
{...props}
|
||||
>
|
||||
{!iconOnly && <>{children ?? (showCopied ? 'Copied' : 'Copy')}</>}
|
||||
{!iconOnly && <>{children ?? (showCopied ? copiedLabel : copyLabel)}</>}
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -21,8 +21,10 @@ export interface DbQueryHook<T = any> {
|
||||
}
|
||||
|
||||
const useDbQuery = (
|
||||
sql: ReportQuery['sql'],
|
||||
params: BaseReportParams = DEFAULT_QUERY_PARAMS
|
||||
sql: ReportQuery['sql'] | string,
|
||||
params: BaseReportParams = DEFAULT_QUERY_PARAMS,
|
||||
where?: string,
|
||||
orderBy?: string
|
||||
): DbQueryHook => {
|
||||
const { project } = useProjectContext()
|
||||
|
||||
@@ -35,7 +37,7 @@ const useDbQuery = (
|
||||
isRefetching,
|
||||
refetch,
|
||||
} = useQuery(
|
||||
['projects', project?.ref, 'db', { ...params, sql: resolvedSql }],
|
||||
['projects', project?.ref, 'db', { ...params, sql: resolvedSql }, where, orderBy],
|
||||
({ signal }) => {
|
||||
return executeSql(
|
||||
{
|
||||
|
||||
@@ -1,46 +1,75 @@
|
||||
import { useParams } from 'common'
|
||||
import ReportHeader from 'components/interfaces/Reports/ReportHeader'
|
||||
import ReportPadding from 'components/interfaces/Reports/ReportPadding'
|
||||
import ReportQueryPerformanceTableRow from 'components/interfaces/Reports/ReportQueryPerformanceTableRow'
|
||||
import { PRESET_CONFIG } from 'components/interfaces/Reports/Reports.constants'
|
||||
import { useQueryPerformanceQuery } from 'components/interfaces/Reports/Reports.queries'
|
||||
import { Presets } from 'components/interfaces/Reports/Reports.types'
|
||||
import { queriesFactory } from 'components/interfaces/Reports/Reports.utils'
|
||||
import { ReportsLayout } from 'components/layouts'
|
||||
import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
|
||||
import Table from 'components/to-be-cleaned/Table'
|
||||
import CopyButton from 'components/ui/CopyButton'
|
||||
import ConfirmModal from 'components/ui/Dialogs/ConfirmDialog'
|
||||
import ShimmeringLoader from 'components/ui/ShimmeringLoader'
|
||||
import { executeSql } from 'data/sql/execute-sql-query'
|
||||
import { useFlag } from 'hooks'
|
||||
import { sortBy } from 'lodash'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import Link from 'next/link'
|
||||
import { useRouter } from 'next/router'
|
||||
import { useState } from 'react'
|
||||
import ReactMarkdown from 'react-markdown'
|
||||
import { NextPageWithLayout } from 'types'
|
||||
import { Accordion, Button, IconAlertCircle, IconCheckCircle, Tabs } from 'ui'
|
||||
import {
|
||||
Accordion,
|
||||
Button,
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuRadioGroup,
|
||||
DropdownMenuRadioItem,
|
||||
DropdownMenuTrigger,
|
||||
IconAlertCircle,
|
||||
IconArrowDown,
|
||||
IconArrowUp,
|
||||
IconCheckCircle,
|
||||
IconList,
|
||||
IconRefreshCw,
|
||||
IconSearch,
|
||||
Input,
|
||||
Tabs,
|
||||
} from 'ui'
|
||||
|
||||
type QueryPerformancePreset = 'time' | 'frequent' | 'slowest'
|
||||
const QueryPerformanceReport: NextPageWithLayout = () => {
|
||||
const { project } = useProjectContext()
|
||||
const [showResetgPgStatStatements, setShowResetgPgStatStatements] = useState(false)
|
||||
const tableIndexEfficiencyEnabled = useFlag('tableIndexEfficiency')
|
||||
const config = PRESET_CONFIG[Presets.QUERY_PERFORMANCE]
|
||||
const { ref: projectRef } = useParams()
|
||||
const router = useRouter()
|
||||
const hooks = queriesFactory(config.queries, projectRef ?? 'default')
|
||||
const mostFrequentlyInvoked = hooks.mostFrequentlyInvoked()
|
||||
const mostTimeConsuming = hooks.mostTimeConsuming()
|
||||
const slowestExecutionTime = hooks.slowestExecutionTime()
|
||||
const queryHitRate = hooks.queryHitRate()
|
||||
|
||||
const isLoading = [
|
||||
mostFrequentlyInvoked.isLoading,
|
||||
mostTimeConsuming.isLoading,
|
||||
slowestExecutionTime.isLoading,
|
||||
queryHitRate.isLoading,
|
||||
].every((value) => value)
|
||||
const orderBy = (router.query.sort as 'lat_desc' | 'lat_asc') || 'lat_desc'
|
||||
const searchQuery = (router.query.search as string) || ''
|
||||
const presetMap = {
|
||||
time: 'mostTimeConsuming',
|
||||
frequent: 'mostFrequentlyInvoked',
|
||||
slowest: 'slowestExecutionTime',
|
||||
} as const
|
||||
const preset = presetMap[router.query.preset as QueryPerformancePreset] || 'mostTimeConsuming'
|
||||
|
||||
const queryPerformanceQuery = useQueryPerformanceQuery({
|
||||
searchQuery,
|
||||
orderBy,
|
||||
preset,
|
||||
})
|
||||
|
||||
const isLoading = [queryPerformanceQuery.isLoading, queryHitRate.isLoading].every(
|
||||
(value) => value
|
||||
)
|
||||
|
||||
const handleRefresh = async () => {
|
||||
mostFrequentlyInvoked.runQuery()
|
||||
mostTimeConsuming.runQuery()
|
||||
slowestExecutionTime.runQuery()
|
||||
queryPerformanceQuery.runQuery()
|
||||
queryHitRate.runQuery()
|
||||
}
|
||||
|
||||
@@ -98,7 +127,7 @@ const QueryPerformanceReport: NextPageWithLayout = () => {
|
||||
const helperTextClassNames = 'prose text-sm max-w-2xl text-foreground-light'
|
||||
|
||||
return (
|
||||
<ReportPadding>
|
||||
<div className="p-4 py-3">
|
||||
<ReportHeader title="Query Performance" isLoading={isLoading} onRefresh={handleRefresh} />
|
||||
{tableIndexEfficiencyEnabled && (
|
||||
<Accordion
|
||||
@@ -129,7 +158,7 @@ const QueryPerformanceReport: NextPageWithLayout = () => {
|
||||
: dangerAlert}
|
||||
<div className="flex items-baseline">
|
||||
<span className="text-3xl">
|
||||
{(queryHitRate?.data![0]?.ratio * 100).toFixed(2)}
|
||||
{queryHitRate?.data && (queryHitRate?.data[0]?.ratio * 100).toFixed(2)}
|
||||
</span>
|
||||
<span className="text-xl">%</span>
|
||||
</div>
|
||||
@@ -146,7 +175,7 @@ const QueryPerformanceReport: NextPageWithLayout = () => {
|
||||
: dangerAlert}
|
||||
<div className="flex items-baseline">
|
||||
<span className="text-3xl">
|
||||
{(queryHitRate?.data![1]?.ratio * 100).toFixed(2)}
|
||||
{queryHitRate?.data && (queryHitRate?.data[1]?.ratio * 100).toFixed(2)}
|
||||
</span>
|
||||
<span className="text-xl">%</span>
|
||||
</div>
|
||||
@@ -199,179 +228,180 @@ const QueryPerformanceReport: NextPageWithLayout = () => {
|
||||
/>
|
||||
|
||||
<div className="flex flex-col">
|
||||
<Tabs type="underlined" size="medium">
|
||||
<Tabs.Panel key={1} id="1" label="Most time consuming">
|
||||
<Tabs
|
||||
type="underlined"
|
||||
size="medium"
|
||||
onChange={(e: string) => {
|
||||
// To reset the search and sort query params when switching tabs
|
||||
const { sort, search, ...rest } = router.query
|
||||
router.push({
|
||||
...router,
|
||||
query: {
|
||||
...rest,
|
||||
preset: e,
|
||||
},
|
||||
})
|
||||
}}
|
||||
>
|
||||
<Tabs.Panel key={'time'} id="time" label="Most time consuming">
|
||||
<div className={panelClassNames}>
|
||||
<ReactMarkdown className={helperTextClassNames}>
|
||||
{TimeConsumingHelperText}
|
||||
</ReactMarkdown>
|
||||
<div className="thin-scrollbars max-w-full overflow-scroll">
|
||||
<div className="thin-scrollbars max-w-full overflow-auto min-h-[800px]">
|
||||
<QueryPerformanceFilterBar onRefreshClick={handleRefresh} isLoading={isLoading} />
|
||||
<Table
|
||||
className="table-fixed"
|
||||
head={
|
||||
<>
|
||||
<Table.th className="table-cell">Role</Table.th>
|
||||
<Table.th className="table-cell">Time Consumed</Table.th>
|
||||
<Table.th className="table-cell">Calls</Table.th>
|
||||
<Table.th className="table-cell">Total Time</Table.th>
|
||||
<Table.th className="table-cell">Query</Table.th>
|
||||
<Table.th className="text-ellipsis overflow-hidden">Role</Table.th>
|
||||
<Table.th className="w-[300px]">Query</Table.th>
|
||||
<Table.th className="text-right">Calls</Table.th>
|
||||
<Table.th className="text-right">Time Consumed</Table.th>
|
||||
<Table.th className="text-right">Total Time (Latency)</Table.th>
|
||||
</>
|
||||
}
|
||||
body={
|
||||
!isLoading && mostTimeConsuming && mostTimeConsuming?.data ? (
|
||||
mostTimeConsuming?.data?.map((item, i) => {
|
||||
!queryPerformanceQuery.isLoading ? (
|
||||
queryPerformanceQuery?.data?.map((item, i) => {
|
||||
return (
|
||||
<Table.tr key={i} hoverable className="relative">
|
||||
<Table.td className="table-cell whitespace-nowrap w-36">
|
||||
<ReportQueryPerformanceTableRow
|
||||
key={i + '-mosttimeconsumed'}
|
||||
sql={item.query}
|
||||
colSpan={5}
|
||||
>
|
||||
<Table.td className="truncate" title={item.rolname}>
|
||||
{item.rolname}
|
||||
</Table.td>
|
||||
<Table.td className="table-cell whitespace-nowrap">
|
||||
<Table.td className="max-w-xs">
|
||||
<p className="font-mono line-clamp-2 text-xs">{item.query}</p>
|
||||
</Table.td>
|
||||
<Table.td className="truncate text-right">{item.calls}</Table.td>
|
||||
<Table.td className="truncate text-right">
|
||||
{item.prop_total_time}
|
||||
</Table.td>
|
||||
<Table.td className="table-cell whitespace-nowrap">
|
||||
{item.calls}
|
||||
</Table.td>
|
||||
<Table.td className="table-cell whitespace-nowrap">
|
||||
<Table.td className="truncate text-right">
|
||||
{item.total_time.toFixed(2)}ms
|
||||
</Table.td>
|
||||
<Table.td className="relative table-cell whitespace-nowrap w-36">
|
||||
<p className="w-96 block truncate font-mono">{item.query}</p>
|
||||
<QueryActions
|
||||
sql={item.query}
|
||||
className="absolute inset-y-0 right-0"
|
||||
/>
|
||||
</Table.td>
|
||||
</Table.tr>
|
||||
</ReportQueryPerformanceTableRow>
|
||||
)
|
||||
})
|
||||
) : (
|
||||
<></>
|
||||
<QueryPerformanceLoadingRow colSpan={5} />
|
||||
)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Tabs.Panel>
|
||||
<Tabs.Panel key={2} id="2" label="Most frequent">
|
||||
<Tabs.Panel key={'frequent'} id="frequent" label="Most frequent">
|
||||
<div className={panelClassNames}>
|
||||
<ReactMarkdown className={helperTextClassNames}>
|
||||
{MostFrequentHelperText}
|
||||
</ReactMarkdown>
|
||||
<div className="thin-scrollbars max-w-full overflow-scroll">
|
||||
<div className="thin-scrollbars max-w-full overflow-auto min-h-[800px]">
|
||||
<QueryPerformanceFilterBar onRefreshClick={handleRefresh} isLoading={isLoading} />
|
||||
<Table
|
||||
head={
|
||||
<>
|
||||
{/* <Table.th className="table-cell">source</Table.th> */}
|
||||
<Table.th className="table-cell">Role</Table.th>
|
||||
<Table.th className="table-cell">Avg. Roles</Table.th>
|
||||
<Table.th className="table-cell">Calls</Table.th>
|
||||
<Table.th className="table-cell">Max Time</Table.th>
|
||||
<Table.th className="table-cell">Mean Time</Table.th>
|
||||
<Table.th className="table-cell">Min Time</Table.th>
|
||||
<Table.th className="table-cell">Total Time</Table.th>
|
||||
<Table.th className="table-cell">Query</Table.th>
|
||||
<Table.th className="">Role</Table.th>
|
||||
<Table.th className="w-[300px]">Query</Table.th>
|
||||
<Table.th className="text-right">Avg. Roles</Table.th>
|
||||
<Table.th className="text-right">Calls</Table.th>
|
||||
<Table.th className="text-right">Max Time</Table.th>
|
||||
<Table.th className="text-right">Mean Time</Table.th>
|
||||
<Table.th className="text-right">Min Time</Table.th>
|
||||
<Table.th className="text-right">Total Time (Latency)</Table.th>
|
||||
</>
|
||||
}
|
||||
body={
|
||||
!isLoading && mostFrequentlyInvoked && mostFrequentlyInvoked?.data ? (
|
||||
mostFrequentlyInvoked.data?.map((item, i) => {
|
||||
queryPerformanceQuery.isLoading ? (
|
||||
<QueryPerformanceLoadingRow colSpan={8} />
|
||||
) : (
|
||||
queryPerformanceQuery.data?.map((item, i) => {
|
||||
return (
|
||||
<Table.tr key={i} hoverable className="relative">
|
||||
<Table.td className="table-cell whitespace-nowrap w-28">
|
||||
<ReportQueryPerformanceTableRow
|
||||
key={i + '-mostfreq'}
|
||||
sql={item.query}
|
||||
colSpan={8}
|
||||
>
|
||||
<Table.td className="truncate" title={item.rolname}>
|
||||
{item.rolname}
|
||||
</Table.td>
|
||||
<Table.td className="table-cell whitespace-nowrap">
|
||||
{item.avg_rows}
|
||||
<Table.td className="min-w-xs">
|
||||
<p className="text-xs font-mono line-clamp-2">{item.query}</p>
|
||||
</Table.td>
|
||||
<Table.td className="table-cell whitespace-nowrap">
|
||||
{item.calls}
|
||||
<Table.td className="truncate text-right">{item.avg_rows}</Table.td>
|
||||
<Table.td className="truncate text-right">{item.calls}</Table.td>
|
||||
<Table.td className="truncate text-right">
|
||||
{item.max_time?.toFixed(2)}ms
|
||||
</Table.td>
|
||||
<Table.td className="table-cell whitespace-nowrap">
|
||||
{item.max_time.toFixed(2)}ms
|
||||
<Table.td className="text-right truncate">
|
||||
{item.mean_time?.toFixed(2)}ms
|
||||
</Table.td>
|
||||
<Table.td className="table-cell whitespace-nowrap truncate">
|
||||
{item.mean_time.toFixed(2)}ms
|
||||
<Table.td className="text-right truncate">
|
||||
{item.min_time?.toFixed(2)}ms
|
||||
</Table.td>
|
||||
<Table.td className="table-cell whitespace-nowrap truncate">
|
||||
{item.min_time.toFixed(2)}ms
|
||||
<Table.td className="text-right truncate">
|
||||
{item.total_time?.toFixed(2)}ms
|
||||
</Table.td>
|
||||
<Table.td className="table-cell whitespace-nowrap truncate">
|
||||
{item.total_time.toFixed(2)}ms
|
||||
</Table.td>
|
||||
<Table.td className="relative table-cell whitespace-nowrap w-24">
|
||||
<p className="w-48 block truncate font-mono ">{item.query}</p>
|
||||
<QueryActions
|
||||
sql={item.query}
|
||||
className="absolute inset-y-0 right-0"
|
||||
/>
|
||||
</Table.td>
|
||||
</Table.tr>
|
||||
</ReportQueryPerformanceTableRow>
|
||||
)
|
||||
})
|
||||
) : (
|
||||
<></>
|
||||
)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Tabs.Panel>
|
||||
<Tabs.Panel key={3} id="3" label="Slowest execution time">
|
||||
<Tabs.Panel key={'slowest'} id="slowest" label="Slowest execution time">
|
||||
<div className={panelClassNames}>
|
||||
<ReactMarkdown className={helperTextClassNames}>
|
||||
{SlowestExecutionHelperText}
|
||||
</ReactMarkdown>
|
||||
<div className="thin-scrollbars max-w-full overflow-scroll">
|
||||
<div className="thin-scrollbars max-w-full overflow-auto min-h-[800px]">
|
||||
<QueryPerformanceFilterBar onRefreshClick={handleRefresh} isLoading={isLoading} />
|
||||
<Table
|
||||
head={
|
||||
<>
|
||||
<Table.th className="table-cell">Role</Table.th>
|
||||
<Table.th className="table-cell">Query</Table.th>
|
||||
<Table.th className="table-cell">Avg Rows</Table.th>
|
||||
<Table.th className="table-cell">Calls</Table.th>
|
||||
<Table.th className="table-cell">Max Time</Table.th>
|
||||
<Table.th className="table-cell">Mean Time</Table.th>
|
||||
<Table.th className="table-cell">Min Time</Table.th>
|
||||
<Table.th className="table-cell">Total Time</Table.th>
|
||||
<Table.th className="table-cell">Query</Table.th>
|
||||
<Table.th className="table-cell">Total Time (Latency)</Table.th>
|
||||
</>
|
||||
}
|
||||
body={
|
||||
!isLoading && slowestExecutionTime && slowestExecutionTime?.data ? (
|
||||
slowestExecutionTime.data?.map((item, i) => {
|
||||
queryPerformanceQuery.isLoading ? (
|
||||
<QueryPerformanceLoadingRow colSpan={8} />
|
||||
) : (
|
||||
queryPerformanceQuery.data?.map((item, i) => {
|
||||
return (
|
||||
<Table.tr key={i} hoverable className="relative">
|
||||
<Table.td className="table-cell whitespace-nowrap w-24">
|
||||
<ReportQueryPerformanceTableRow
|
||||
key={i + '-slowestexec'}
|
||||
sql={item.query}
|
||||
colSpan={8}
|
||||
>
|
||||
<Table.td className="truncate" title={item.rolname}>
|
||||
{item.rolname}
|
||||
</Table.td>
|
||||
<Table.td className="table-cell whitespace-nowrap">
|
||||
{item.avg_rows}
|
||||
<Table.td className="max-w-xs">
|
||||
<p className="font-mono line-clamp-2 text-xs">{item.query}</p>
|
||||
</Table.td>
|
||||
<Table.td className="table-cell whitespace-nowrap">
|
||||
{item.calls}
|
||||
<Table.td className="truncate">{item.avg_rows}</Table.td>
|
||||
<Table.td className="truncate">{item.calls}</Table.td>
|
||||
<Table.td className="truncate">{item.max_time?.toFixed(2)}ms</Table.td>
|
||||
<Table.td className="truncate">{item.mean_time?.toFixed(2)}ms</Table.td>
|
||||
<Table.td className="truncate">{item.min_time?.toFixed(2)}ms</Table.td>
|
||||
<Table.td className="truncate">
|
||||
{item.total_time?.toFixed(2)}ms
|
||||
</Table.td>
|
||||
<Table.td className="table-cell whitespace-nowrap">
|
||||
{item.max_time.toFixed(2)}ms
|
||||
</Table.td>
|
||||
<Table.td className="table-cell whitespace-nowrap truncate">
|
||||
{item.mean_time.toFixed(2)}ms
|
||||
</Table.td>
|
||||
<Table.td className="table-cell whitespace-nowrap truncate">
|
||||
{item.min_time.toFixed(2)}ms
|
||||
</Table.td>
|
||||
<Table.td className="table-cell whitespace-nowrap truncate">
|
||||
{item.total_time.toFixed(2)}ms
|
||||
</Table.td>
|
||||
<Table.td className="relative table-cell whitespace-nowrap">
|
||||
<p className="w-48 block truncate font-mono">{item.query}</p>
|
||||
<QueryActions
|
||||
sql={item.query}
|
||||
className="absolute inset-y-0 right-0"
|
||||
/>
|
||||
</Table.td>
|
||||
</Table.tr>
|
||||
</ReportQueryPerformanceTableRow>
|
||||
)
|
||||
})
|
||||
) : (
|
||||
<></>
|
||||
)
|
||||
}
|
||||
/>
|
||||
@@ -380,16 +410,6 @@ const QueryPerformanceReport: NextPageWithLayout = () => {
|
||||
</Tabs.Panel>
|
||||
</Tabs>
|
||||
</div>
|
||||
</ReportPadding>
|
||||
)
|
||||
}
|
||||
|
||||
const QueryActions = ({ sql, className }: { sql: string; className: string }) => {
|
||||
if (sql.includes('insufficient privilege')) return null
|
||||
|
||||
return (
|
||||
<div className={[className, 'flex justify-center items-center mr-4'].join(' ')}>
|
||||
<CopyButton type="default" text={sql} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -399,3 +419,149 @@ QueryPerformanceReport.getLayout = (page) => (
|
||||
)
|
||||
|
||||
export default observer(QueryPerformanceReport)
|
||||
|
||||
function QueryPerformanceFilterBar({
|
||||
isLoading,
|
||||
onRefreshClick,
|
||||
}: {
|
||||
isLoading: boolean
|
||||
onRefreshClick: () => void
|
||||
}) {
|
||||
const router = useRouter()
|
||||
const defaultSearchQueryValue = router.query.search ? String(router.query.search) : ''
|
||||
const defaultSortByValue = router.query.sort ? String(router.query.sort) : 'lat_desc'
|
||||
const [searchInputVal, setSearchInputVal] = useState(defaultSearchQueryValue)
|
||||
const [sortByValue, setSortByValue] = useState(defaultSortByValue)
|
||||
|
||||
function getSortButtonLabel() {
|
||||
const sort = router.query.sort as 'lat_desc' | 'lat_asc'
|
||||
|
||||
if (sort === 'lat_desc') {
|
||||
return 'Sorted by latency - high to low'
|
||||
} else {
|
||||
return 'Sorted by latency - low to high'
|
||||
}
|
||||
}
|
||||
|
||||
function onSortChange(sort: string) {
|
||||
setSortByValue(sort)
|
||||
router.push({
|
||||
...router,
|
||||
query: {
|
||||
...router.query,
|
||||
sort,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const ButtonIcon = sortByValue === 'lat_desc' ? IconArrowDown : IconArrowUp
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex justify-between items-center">
|
||||
<form
|
||||
className="py-3 flex gap-4"
|
||||
id="log-panel-search"
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault()
|
||||
const formData = new FormData(e.target as HTMLFormElement)
|
||||
const searchQuery = formData.get('search')
|
||||
|
||||
if (!searchQuery || typeof searchQuery !== 'string') {
|
||||
// if user has deleted the search query, remove it from the url
|
||||
const { search, ...rest } = router.query
|
||||
router.push({
|
||||
...router,
|
||||
query: {
|
||||
...rest,
|
||||
},
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
router.push({
|
||||
...router,
|
||||
query: {
|
||||
...router.query,
|
||||
search: searchQuery,
|
||||
},
|
||||
})
|
||||
}}
|
||||
>
|
||||
<Input
|
||||
className="w-60 group"
|
||||
size="tiny"
|
||||
placeholder="Search roles or queries"
|
||||
name="search"
|
||||
value={searchInputVal}
|
||||
onChange={(e) => setSearchInputVal(e.target.value)}
|
||||
autoComplete="off"
|
||||
icon={
|
||||
<div className="text-foreground-lighter">
|
||||
<IconSearch size={14} />
|
||||
</div>
|
||||
}
|
||||
actions={
|
||||
searchInputVal !== '' && (
|
||||
<button className="mx-2 text-foreground-light hover:text-foreground">{'↲'}</button>
|
||||
)
|
||||
}
|
||||
/>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button icon={<ButtonIcon />}>{getSortButtonLabel()}</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent className="w-56">
|
||||
<DropdownMenuRadioGroup value={sortByValue} onValueChange={onSortChange}>
|
||||
<DropdownMenuRadioItem
|
||||
defaultChecked={router.query.sort === 'lat_desc'}
|
||||
value={'lat_desc'}
|
||||
>
|
||||
Sort by latency - high to low
|
||||
</DropdownMenuRadioItem>
|
||||
<DropdownMenuRadioItem
|
||||
value={'lat_asc'}
|
||||
defaultChecked={router.query.sort === 'lat_asc'}
|
||||
>
|
||||
Sort by latency - low to high
|
||||
</DropdownMenuRadioItem>
|
||||
</DropdownMenuRadioGroup>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</form>
|
||||
<div>
|
||||
<Button
|
||||
type="default"
|
||||
size="tiny"
|
||||
onClick={onRefreshClick}
|
||||
disabled={isLoading ? true : false}
|
||||
icon={
|
||||
<IconRefreshCw
|
||||
size="tiny"
|
||||
className={`text-foreground-light ${isLoading ? 'animate-spin' : ''}`}
|
||||
/>
|
||||
}
|
||||
>
|
||||
{isLoading ? 'Refreshing' : 'Refresh'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function QueryPerformanceLoadingRow({ colSpan }: { colSpan: number }) {
|
||||
return (
|
||||
<>
|
||||
{Array(4)
|
||||
.fill('')
|
||||
.map((_, i) => (
|
||||
<tr key={'loading-' + { i }}>
|
||||
<td colSpan={colSpan}>
|
||||
<ShimmeringLoader />
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
9
package-lock.json
generated
9
package-lock.json
generated
@@ -11589,6 +11589,14 @@
|
||||
"tailwindcss": ">=3.0.0 || >= 3.0.0-alpha.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/line-clamp": {
|
||||
"version": "0.4.4",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/line-clamp/-/line-clamp-0.4.4.tgz",
|
||||
"integrity": "sha512-5U6SY5z8N42VtrCrKlsTAA35gy2VSyYtHWCsg1H87NU1SXnEfekTVlrga9fzUDrrHcGi2Lb5KenUWb4lRQT5/g==",
|
||||
"peerDependencies": {
|
||||
"tailwindcss": ">=2.0.0 || >=3.0.0 || >=3.0.0-alpha.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/typography": {
|
||||
"version": "0.5.10",
|
||||
"license": "MIT",
|
||||
@@ -37890,6 +37898,7 @@
|
||||
"@mertasan/tailwindcss-variables": "^2.2.3",
|
||||
"@radix-ui/colors": "^0.1.8",
|
||||
"@tailwindcss/forms": "^0.5.0",
|
||||
"@tailwindcss/line-clamp": "^0.4.4",
|
||||
"@tailwindcss/typography": "^0.5.9",
|
||||
"deepmerge": "^4.2.2",
|
||||
"eslint-config-next": "^14.1.0",
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
"@mertasan/tailwindcss-variables": "^2.2.3",
|
||||
"@radix-ui/colors": "^0.1.8",
|
||||
"@tailwindcss/forms": "^0.5.0",
|
||||
"@tailwindcss/line-clamp": "^0.4.4",
|
||||
"@tailwindcss/typography": "^0.5.9",
|
||||
"deepmerge": "^4.2.2",
|
||||
"eslint-config-next": "^14.1.0",
|
||||
|
||||
@@ -392,7 +392,7 @@ const uiConfig = ui({
|
||||
// shadcn defaults END
|
||||
},
|
||||
},
|
||||
plugins: [require('@tailwindcss/typography'), require('tailwindcss-animate')],
|
||||
plugins: [require('@tailwindcss/typography'), require('tailwindcss-animate'), require('@tailwindcss/line-clamp')],
|
||||
})
|
||||
|
||||
function arrayMergeFn(destinationArray, sourceArray) {
|
||||
|
||||
Reference in New Issue
Block a user