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:
Jordi Enric
2024-02-06 15:47:22 +01:00
committed by GitHub
parent 1b1180c8bb
commit 44b9ce3e5f
11 changed files with 418 additions and 159 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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: {

View 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)
}

View File

@@ -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
}

View File

@@ -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>
)
}

View File

@@ -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(
{

View File

@@ -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
View File

@@ -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",

View File

@@ -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",

View File

@@ -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) {