diff --git a/apps/studio/components/interfaces/QueryPerformance/QueryPerformance.tsx b/apps/studio/components/interfaces/QueryPerformance/QueryPerformance.tsx index 3f9b663f2a6..5014a36812d 100644 --- a/apps/studio/components/interfaces/QueryPerformance/QueryPerformance.tsx +++ b/apps/studio/components/interfaces/QueryPerformance/QueryPerformance.tsx @@ -6,9 +6,9 @@ import { Tabs } from 'ui' import { Markdown } from '../Markdown' import ReportQueryPerformanceTableRow from '../Reports/ReportQueryPerformanceTableRow' import { PresetHookResult } from '../Reports/Reports.utils' -import { QueryPerformanceFilterBar } from './QueryPerformanceFilterBar' -import { QueryPerformanceLoadingRow } from './QueryPerformanceLoadingRow' +import { QueryPerformanceFilterBar } from '../QueryPerformanceV2/QueryPerformanceFilterBar' import { ResetAnalysisNotice } from './ResetAnalysisNotice' +import ShimmeringLoader from 'ui-patterns/ShimmeringLoader' type QueryPerformancePreset = 'time' | 'frequent' | 'slowest' @@ -34,14 +34,27 @@ interface QueryPerformanceProps { queryPerformanceQuery: DbQueryHook } +const QueryPerformanceLoadingRow = ({ colSpan }: { colSpan: number }) => { + return ( + <> + {Array(4) + .fill('') + .map((_, i) => ( + + + + + + ))} + + ) +} + export const QueryPerformance = ({ queryHitRate, queryPerformanceQuery, }: QueryPerformanceProps) => { const router = useRouter() - const isLoading = [queryPerformanceQuery.isLoading, queryHitRate.isLoading].every( - (value) => value - ) const handleRefresh = async () => { queryPerformanceQuery.runQuery() @@ -71,8 +84,8 @@ export const QueryPerformance = ({ className="max-w-full [&>p]:mt-0 [&>p]:m-0 space-y-2" /> -
- +
+ -
- +
+
@@ -184,8 +197,8 @@ export const QueryPerformance = ({ className="max-w-full [&>p]:mt-0 [&>p]:m-0 space-y-2" /> -
- +
+
diff --git a/apps/studio/components/interfaces/QueryPerformance/QueryPerformanceLoadingRow.tsx b/apps/studio/components/interfaces/QueryPerformance/QueryPerformanceLoadingRow.tsx deleted file mode 100644 index 6bd24ccba31..00000000000 --- a/apps/studio/components/interfaces/QueryPerformance/QueryPerformanceLoadingRow.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import ShimmeringLoader from 'ui-patterns/ShimmeringLoader' - -export const QueryPerformanceLoadingRow = ({ colSpan }: { colSpan: number }) => { - return ( - <> - {Array(4) - .fill('') - .map((_, i) => ( - - - - ))} - - ) -} diff --git a/apps/studio/components/interfaces/QueryPerformance/IndexEfficiencyNotice.tsx b/apps/studio/components/interfaces/QueryPerformanceV2/IndexEfficiencyNotice.tsx similarity index 97% rename from apps/studio/components/interfaces/QueryPerformance/IndexEfficiencyNotice.tsx rename to apps/studio/components/interfaces/QueryPerformanceV2/IndexEfficiencyNotice.tsx index dee58df8a37..e512a2019c7 100644 --- a/apps/studio/components/interfaces/QueryPerformance/IndexEfficiencyNotice.tsx +++ b/apps/studio/components/interfaces/QueryPerformanceV2/IndexEfficiencyNotice.tsx @@ -11,6 +11,7 @@ interface IndexEfficiencyNoticeProps { isLoading: boolean } +// [Joshen] Currently not used, might be deprecated - just double checking first export const IndexEfficiencyNotice = ({ isLoading }: IndexEfficiencyNoticeProps) => { const { ref: projectRef } = useParams() const config = PRESET_CONFIG[Presets.QUERY_PERFORMANCE] diff --git a/apps/studio/components/interfaces/QueryPerformanceV2/IndexSuggestion.tsx b/apps/studio/components/interfaces/QueryPerformanceV2/IndexSuggestion.tsx new file mode 100644 index 00000000000..bb3267ee000 --- /dev/null +++ b/apps/studio/components/interfaces/QueryPerformanceV2/IndexSuggestion.tsx @@ -0,0 +1,10 @@ +export const IndexSuggestion = () => { + return ( +
+
+

Add new index

+

Hello

+
+
+ ) +} diff --git a/apps/studio/components/interfaces/QueryPerformanceV2/QueryDetail.tsx b/apps/studio/components/interfaces/QueryPerformanceV2/QueryDetail.tsx new file mode 100644 index 00000000000..c6e7c06be5d --- /dev/null +++ b/apps/studio/components/interfaces/QueryPerformanceV2/QueryDetail.tsx @@ -0,0 +1,65 @@ +import { CodeBlock, cn } from 'ui' +import { + QUERY_PERFORMANCE_REPORTS, + QUERY_PERFORMANCE_REPORT_TYPES, +} from './QueryPerformance.constants' +import { format } from 'sql-formatter' +import { useEffect, useState } from 'react' + +interface QueryDetailProps { + reportType: QUERY_PERFORMANCE_REPORT_TYPES + selectedRow: any +} + +export const QueryDetail = ({ reportType, selectedRow }: QueryDetailProps) => { + const report = QUERY_PERFORMANCE_REPORTS[reportType] + const [query, setQuery] = useState(selectedRow?.['query']) + + useEffect(() => { + if (selectedRow !== undefined) { + try { + const formattedQuery = format(selectedRow['query'], { + language: 'postgresql', + keywordCase: 'lower', + }) + setQuery(formattedQuery) + } catch (err) { + setQuery(selectedRow['query']) + } + } + }, [selectedRow]) + + return ( +
+
+

Query pattern

+ code]:m-0 [&>code>span]:flex [&>code>span]:flex-wrap' + )} + /> +
+
+ {report + .filter((x) => x.id !== 'query') + .map((x) => { + const isTime = x.name.includes('time') + const formattedValue = isTime + ? `${selectedRow[x.id].toFixed(2)}ms` + : String(selectedRow[x.id]) + return ( +
+

{x.name}

+

{formattedValue}

+
+ ) + })} +
+
+ ) +} diff --git a/apps/studio/components/interfaces/QueryPerformanceV2/QueryPerformance.constants.ts b/apps/studio/components/interfaces/QueryPerformanceV2/QueryPerformance.constants.ts new file mode 100644 index 00000000000..e787e221407 --- /dev/null +++ b/apps/studio/components/interfaces/QueryPerformanceV2/QueryPerformance.constants.ts @@ -0,0 +1,35 @@ +export enum QUERY_PERFORMANCE_REPORT_TYPES { + MOST_TIME_CONSUMING = 'most_time_consuming', + MOST_FREQUENT = 'most_frequent', + SLOWEST_EXECUTION = 'slowest_execution', +} + +export const QUERY_PERFORMANCE_REPORTS = { + [QUERY_PERFORMANCE_REPORT_TYPES.MOST_TIME_CONSUMING]: [ + { id: 'query', name: 'Query', description: undefined, minWidth: 600 }, + { id: 'rolname', name: 'Role', description: undefined, minWidth: undefined }, + { id: 'calls', name: 'Calls', description: undefined, minWidth: undefined }, + { id: 'total_time', name: 'Total time', description: 'latency', minWidth: 180 }, + { id: 'prop_total_time', name: 'Time consumed', description: undefined, minWidth: 150 }, + ], + [QUERY_PERFORMANCE_REPORT_TYPES.MOST_FREQUENT]: [ + { id: 'query', name: 'Query', description: undefined, minWidth: 600 }, + { id: 'rolname', name: 'Role', description: undefined, minWidth: undefined }, + { id: 'avg_rows', name: 'Avg. Rows', description: undefined, minWidth: undefined }, + { id: 'calls', name: 'Calls', description: undefined, minWidth: undefined }, + { id: 'max_time', name: 'Max time', description: undefined, minWidth: undefined }, + { id: 'mean_time', name: 'Mean time', description: undefined, minWidth: undefined }, + { id: 'min_time', name: 'Min time', description: undefined, minWidth: undefined }, + { id: 'total_time', name: 'Total time', description: 'latency', minWidth: 180 }, + ], + [QUERY_PERFORMANCE_REPORT_TYPES.SLOWEST_EXECUTION]: [ + { id: 'query', name: 'Query', description: undefined, minWidth: 600 }, + { id: 'rolname', name: 'Role', description: undefined, minWidth: undefined }, + { id: 'avg_rows', name: 'Avg. Rows', description: undefined, minWidth: undefined }, + { id: 'calls', name: 'Calls', description: undefined, minWidth: undefined }, + { id: 'max_time', name: 'Max time', description: undefined, minWidth: undefined }, + { id: 'mean_time', name: 'Mean time', description: undefined, minWidth: undefined }, + { id: 'min_time', name: 'Min time', description: undefined, minWidth: undefined }, + { id: 'total_time', name: 'Total time', description: 'latency', minWidth: 180 }, + ], +} as const diff --git a/apps/studio/components/interfaces/QueryPerformanceV2/QueryPerformance.tsx b/apps/studio/components/interfaces/QueryPerformanceV2/QueryPerformance.tsx new file mode 100644 index 00000000000..201e9eae327 --- /dev/null +++ b/apps/studio/components/interfaces/QueryPerformanceV2/QueryPerformance.tsx @@ -0,0 +1,219 @@ +import { InformationCircleIcon } from '@heroicons/react/16/solid' +import { useRouter } from 'next/router' +import { useMemo, useState } from 'react' +import toast from 'react-hot-toast' + +import { useParams } from 'common' +import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext' +import { executeSql } from 'data/sql/execute-sql-query' +import { DbQueryHook } from 'hooks/analytics/useDbQuery' +import { + Button, + TabsList_Shadcn_, + TabsTrigger_Shadcn_, + Tabs_Shadcn_, + TooltipContent_Shadcn_, + TooltipTrigger_Shadcn_, + Tooltip_Shadcn_, + cn, +} from 'ui' +import ConfirmModal from 'ui-patterns/Dialogs/ConfirmDialog' +import ShimmeringLoader from 'ui-patterns/ShimmeringLoader' +import { Markdown } from '../Markdown' +import { useQueryPerformanceQuery } from '../Reports/Reports.queries' +import { PresetHookResult } from '../Reports/Reports.utils' +import { QUERY_PERFORMANCE_REPORT_TYPES } from './QueryPerformance.constants' +import { QueryPerformanceFilterBar } from './QueryPerformanceFilterBar' +import { QueryPerformanceGrid } from './QueryPerformanceGrid' + +interface QueryPerformanceProps { + queryHitRate: PresetHookResult + queryPerformanceQuery: DbQueryHook +} + +export const QueryPerformance = ({ + queryHitRate, + queryPerformanceQuery, +}: QueryPerformanceProps) => { + const router = useRouter() + const { preset } = useParams() + const { project } = useProjectContext() + const [page, setPage] = useState( + (preset as QUERY_PERFORMANCE_REPORT_TYPES) ?? QUERY_PERFORMANCE_REPORT_TYPES.MOST_TIME_CONSUMING + ) + const [showResetgPgStatStatements, setShowResetgPgStatStatements] = useState(false) + + const handleRefresh = () => { + queryPerformanceQuery.runQuery() + queryHitRate.runQuery() + } + + const { data: mostTimeConsumingQueries, isLoading: isLoadingMTC } = useQueryPerformanceQuery({ + preset: 'mostTimeConsuming', + }) + const { data: mostFrequentlyInvoked, isLoading: isLoadingMFI } = useQueryPerformanceQuery({ + preset: 'mostFrequentlyInvoked', + }) + const { data: slowestExecutionTime, isLoading: isLoadingMMF } = useQueryPerformanceQuery({ + preset: 'slowestExecutionTime', + }) + + const QUERY_PERFORMANCE_TABS = useMemo(() => { + return [ + { + id: QUERY_PERFORMANCE_REPORT_TYPES.MOST_TIME_CONSUMING, + label: 'Most time consuming', + description: 'Lists queries ordered by their cumulative total execution time.', + isLoading: isLoadingMTC, + max: + (mostTimeConsumingQueries ?? []).length > 0 + ? Math.max(...(mostTimeConsumingQueries ?? []).map((x: any) => x.total_time)).toFixed(2) + : undefined, + }, + { + id: QUERY_PERFORMANCE_REPORT_TYPES.MOST_FREQUENT, + label: 'Most frequent', + description: 'Lists queries in order of their execution count', + isLoading: isLoadingMFI, + max: + (mostFrequentlyInvoked ?? []).length > 0 + ? Math.max(...(mostFrequentlyInvoked ?? []).map((x: any) => x.calls)).toFixed(2) + : undefined, + }, + { + id: QUERY_PERFORMANCE_REPORT_TYPES.SLOWEST_EXECUTION, + label: 'Slowest execution', + description: 'Lists queries ordered by their maximum execution time', + isLoading: isLoadingMMF, + max: + (slowestExecutionTime ?? []).length > 0 + ? Math.max(...(slowestExecutionTime ?? []).map((x: any) => x.max_time)).toFixed(2) + : undefined, + }, + ] + }, [ + isLoadingMFI, + isLoadingMMF, + isLoadingMTC, + mostFrequentlyInvoked, + mostTimeConsumingQueries, + slowestExecutionTime, + ]) + + return ( + <> + { + setPage(value as QUERY_PERFORMANCE_REPORT_TYPES) + const { sort, search, ...rest } = router.query + router.push({ ...router, query: { ...rest, preset: value } }) + }} + > + + {QUERY_PERFORMANCE_TABS.map((tab) => ( + + {tab.id === page && ( +
+ )} + +
+ {tab.label} + + + + + {tab.description} + +
+ {tab.isLoading ? ( + + ) : tab.max === undefined ? ( + + No data yet + + ) : ( + + {Number(tab.max).toLocaleString()} + {tab.id !== QUERY_PERFORMANCE_REPORT_TYPES.MOST_FREQUENT ? 'ms' : ' calls'} + + )} + + {tab.id === page && ( +
+ )} +
+ ))} +
+
+ +
+ +
+ + + +
+
+

Reset report

+

+ Consider resetting the analysis after optimizing any queries +

+ +
+
+

How is this report generated?

+ +
+
+ + setShowResetgPgStatStatements(false)} + onSelectConfirm={async () => { + try { + await executeSql({ + projectRef: project?.ref, + connectionString: project?.connectionString, + sql: `SELECT pg_stat_statements_reset();`, + }) + handleRefresh() + setShowResetgPgStatStatements(false) + } catch (error: any) { + toast.error(`Failed to reset analysis: ${error.message}`) + } + }} + /> + + ) +} diff --git a/apps/studio/components/interfaces/QueryPerformance/QueryPerformanceFilterBar.tsx b/apps/studio/components/interfaces/QueryPerformanceV2/QueryPerformanceFilterBar.tsx similarity index 52% rename from apps/studio/components/interfaces/QueryPerformance/QueryPerformanceFilterBar.tsx rename to apps/studio/components/interfaces/QueryPerformanceV2/QueryPerformanceFilterBar.tsx index ad69fbb90c0..c845f6b0639 100644 --- a/apps/studio/components/interfaces/QueryPerformance/QueryPerformanceFilterBar.tsx +++ b/apps/studio/components/interfaces/QueryPerformanceV2/QueryPerformanceFilterBar.tsx @@ -5,6 +5,8 @@ import { useState } from 'react' import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext' import { FilterPopover } from 'components/ui/FilterPopover' import { useDatabaseRolesQuery } from 'data/database-roles/database-roles-query' +import { useFlag } from 'hooks' +import { DbQueryHook } from 'hooks/analytics/useDbQuery' import { Button, DropdownMenu, @@ -14,43 +16,39 @@ import { DropdownMenuTrigger, } from 'ui' import { TextSearchPopover } from './TextSearchPopover' +import { QueryPerformanceSort } from '../Reports/Reports.queries' export const QueryPerformanceFilterBar = ({ - isLoading, - onRefreshClick, + queryPerformanceQuery, }: { - isLoading: boolean - onRefreshClick: () => void + queryPerformanceQuery: DbQueryHook }) => { const router = useRouter() const { project } = useProjectContext() + const enableQueryPerformanceV2 = useFlag('queryPerformanceV2') const defaultSearchQueryValue = router.query.search ? String(router.query.search) : '' - const defaultSortByValue = router.query.sort ? String(router.query.sort) : 'lat_desc' const defaultFilterRoles = router.query.roles ? (router.query.roles as string[]) : [] + const defaultSortByValue = router.query.sort + ? ({ column: router.query.sort, order: router.query.order } as QueryPerformanceSort) + : undefined - const [sortByValue, setSortByValue] = useState(defaultSortByValue) const [searchInputVal, setSearchInputVal] = useState(defaultSearchQueryValue) const [filters, setFilters] = useState<{ roles: string[]; query: string }>({ roles: typeof defaultFilterRoles === 'string' ? [defaultFilterRoles] : defaultFilterRoles, query: '', }) + // [Joshen] This is for the old UI, can deprecated after + const [sortByValue, setSortByValue] = useState( + defaultSortByValue ?? { column: 'prop_total_time', order: 'desc' } + ) + const { isLoading, isRefetching } = queryPerformanceQuery const { data, isLoading: isLoadingRoles } = useDatabaseRolesQuery({ projectRef: project?.ref, connectionString: project?.connectionString, }) const roles = (data ?? []).sort((a, b) => a.name.localeCompare(b.name)) - 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' - } - } - const onSearchQueryChange = (value: string) => { setSearchInputVal(value) @@ -68,15 +66,21 @@ export const QueryPerformanceFilterBar = ({ router.push({ ...router, query: { ...router.query, roles } }) } - const onSortChange = (sort: string) => { - setSortByValue(sort) - router.push({ ...router, query: { ...router.query, sort } }) + function getSortButtonLabel() { + if (defaultSortByValue?.order === 'desc') { + return 'Sorted by latency - high to low' + } else { + return 'Sorted by latency - low to high' + } } - const ButtonIcon = sortByValue === 'lat_desc' ? ArrowDown : ArrowUp + const onSortChange = (order: 'asc' | 'desc') => { + setSortByValue({ column: 'prop_total_time', order }) + router.push({ ...router, query: { ...router.query, sort: 'prop_total_time', order } }) + } return ( -
+

Filter by

@@ -89,44 +93,54 @@ export const QueryPerformanceFilterBar = ({ onSaveFilters={onFilterRolesChange} /> + + {!enableQueryPerformanceV2 && ( + <> +
+ + + + + + onSortChange(value)} + > + + Sort by latency - high to low + + + Sort by latency - low to high + + + + + + )}
-
- - - - - - - - Sort by latency - high to low - - - Sort by latency - low to high - - - -
) diff --git a/apps/studio/components/interfaces/QueryPerformanceV2/QueryPerformanceGrid.tsx b/apps/studio/components/interfaces/QueryPerformanceV2/QueryPerformanceGrid.tsx new file mode 100644 index 00000000000..f1bb109d4df --- /dev/null +++ b/apps/studio/components/interfaces/QueryPerformanceV2/QueryPerformanceGrid.tsx @@ -0,0 +1,210 @@ +import { ArrowDown, ArrowUp, TextSearch, X } from 'lucide-react' +import { useRouter } from 'next/router' +import { useEffect, useState } from 'react' +import DataGrid, { Column } from 'react-data-grid' + +import { useParams } from 'common' +import { DbQueryHook } from 'hooks/analytics/useDbQuery' +import { + Button, + ResizableHandle, + ResizablePanel, + ResizablePanelGroup, + TabsContent_Shadcn_, + TabsList_Shadcn_, + TabsTrigger_Shadcn_, + Tabs_Shadcn_, + cn, +} from 'ui' +import { GenericSkeletonLoader } from 'ui-patterns' +import { QueryPerformanceSort } from '../Reports/Reports.queries' +import { IndexSuggestion } from './IndexSuggestion' +import { QueryDetail } from './QueryDetail' +import { + QUERY_PERFORMANCE_REPORTS, + QUERY_PERFORMANCE_REPORT_TYPES, +} from './QueryPerformance.constants' + +interface QueryPerformanceGridProps { + queryPerformanceQuery: DbQueryHook +} + +export const QueryPerformanceGrid = ({ queryPerformanceQuery }: QueryPerformanceGridProps) => { + // [Joshen] This will come in another PR to integrate index advisor + const showIndexSuggestions = false + + const router = useRouter() + const { preset } = useParams() + const { isLoading } = queryPerformanceQuery + + const defaultSortValue = router.query.sort + ? ({ column: router.query.sort, order: router.query.order } as QueryPerformanceSort) + : undefined + + const [view, setView] = useState<'details' | 'suggestion'>('details') + const [sort, setSort] = useState(defaultSortValue) + const [selectedRow, setSelectedRow] = useState() + const reportType = + (preset as QUERY_PERFORMANCE_REPORT_TYPES) ?? QUERY_PERFORMANCE_REPORT_TYPES.MOST_TIME_CONSUMING + + const columns = QUERY_PERFORMANCE_REPORTS[reportType].map((col) => { + const result: Column = { + key: col.id, + name: col.name, + resizable: true, + minWidth: col.minWidth ?? 120, + headerCellClass: 'first:pl-6 cursor-pointer', + renderHeaderCell: () => { + return ( +
onSortChange(col.id)} + > +
+

{col.name}

+ {col.description &&

{col.description}

} +
+ {sort?.column === col.id && ( + <>{sort.order === 'desc' ? : } + )} +
+ ) + }, + renderCell: (props) => { + const value = props.row?.[col.id] + const isTime = col.name.includes('time') + const formattedValue = isTime ? `${value.toFixed(2)}ms` : String(value) + return ( +
+

{formattedValue}

+ {isTime &&

{(value / 1000).toFixed(2)}s

} +
+ ) + }, + } + return result + }) + + const onSortChange = (column: string) => { + let updatedSort = undefined + + if (sort?.column === column) { + if (sort.order === 'desc') { + updatedSort = { column, order: 'asc' } + } else { + updatedSort = undefined + } + } else { + updatedSort = { column, order: 'desc' } + } + + setSort(updatedSort as QueryPerformanceSort) + + if (updatedSort === undefined) { + const { sort, order, ...otherParams } = router.query + router.push({ ...router, query: otherParams }) + } else { + router.push({ + ...router, + query: { ...router.query, sort: updatedSort.column, order: updatedSort.order }, + }) + } + } + + useEffect(() => { + setSelectedRow(undefined) + }, [preset]) + + return ( + + + { + const { rowIdx } = props + if (rowIdx >= 0) setSelectedRow(rowIdx) + }} + columns={columns} + rows={queryPerformanceQuery?.data ?? []} + rowClass={(_, idx) => { + const isSelected = idx === selectedRow + return [ + `${isSelected ? 'bg-surface-300 dark:bg-surface-300' : 'bg-200'} cursor-pointer`, + `${isSelected ? '[&>div:first-child]:border-l-4 border-l-secondary [&>div]:border-l-foreground' : ''}`, + '[&>.rdg-cell]:border-box [&>.rdg-cell]:outline-none [&>.rdg-cell]:shadow-none', + '[&>.rdg-cell:first-child>div]:ml-4', + ].join(' ') + }} + renderers={{ + noRowsFallback: isLoading ? ( +
+ +
+ ) : ( +
+ +
+

No queries detected yet

+

+ There are no queries actively running that meet the criteria +

+
+
+ ), + }} + /> +
+ {selectedRow !== undefined && ( + <> + + +
- -