diff --git a/apps/studio/components/interfaces/QueryPerformance/QueryDetail.tsx b/apps/studio/components/interfaces/QueryPerformance/QueryDetail.tsx index d22830df4dd39..9f1531829c5eb 100644 --- a/apps/studio/components/interfaces/QueryPerformance/QueryDetail.tsx +++ b/apps/studio/components/interfaces/QueryPerformance/QueryDetail.tsx @@ -1,15 +1,9 @@ import { Lightbulb } from 'lucide-react' import { useEffect, useState } from 'react' +import dynamic from 'next/dynamic' import { formatSql } from 'lib/formatSql' -import { - AlertDescription_Shadcn_, - AlertTitle_Shadcn_, - Alert_Shadcn_, - Button, - CodeBlock, - cn, -} from 'ui' +import { AlertDescription_Shadcn_, AlertTitle_Shadcn_, Alert_Shadcn_, Button, cn } from 'ui' import { QueryPanelContainer, QueryPanelSection } from './QueryPanel' import { QUERY_PERFORMANCE_REPORTS, @@ -22,6 +16,14 @@ interface QueryDetailProps { onClickViewSuggestion: () => void } +// Load SqlMonacoBlock (monaco editor) client-side only (does not behave well server-side) +const SqlMonacoBlock = dynamic( + () => import('./SqlMonacoBlock').then(({ SqlMonacoBlock }) => SqlMonacoBlock), + { + ssr: false, + } +) + export const QueryDetail = ({ reportType, selectedRow, @@ -43,16 +45,7 @@ export const QueryDetail = ({

Query pattern

- code]:m-0 [&>code>span]:flex [&>code>span]:flex-wrap' - )} - /> + {isLinterWarning && ( } +// Load the monaco editor client-side only (does not behave well server-side) +const Editor = dynamic(() => import('@monaco-editor/react').then(({ Editor }) => Editor), { + ssr: false, +}) + export const QueryPerformanceGrid = ({ queryPerformanceQuery }: QueryPerformanceGridProps) => { const router = useRouter() const gridRef = useRef(null) @@ -74,7 +80,7 @@ export const QueryPerformanceGrid = ({ queryPerformanceQuery }: QueryPerformance const value = props.row?.[col.id] if (col.id === 'query') { return ( -
+
{hasIndexRecommendations(props.row.index_advisor_result, true) && ( )} -
{value}
+
) } @@ -170,6 +207,43 @@ export const QueryPerformanceGrid = ({ queryPerformanceQuery }: QueryPerformance setSelectedRow(undefined) }, [preset, search, roles, urlSort, order]) + const handleKeyDown = useCallback( + (event: KeyboardEvent) => { + if (!reportData.length || selectedRow === undefined) return + + if (event.key !== 'ArrowUp' && event.key !== 'ArrowDown') return + + // stop default RDG behavior (which moves focus to header when selectedRow is 0) + event.stopPropagation() + + let nextIndex = selectedRow + if (event.key === 'ArrowUp' && selectedRow > 0) { + nextIndex = selectedRow - 1 + } else if (event.key === 'ArrowDown' && selectedRow < reportData.length - 1) { + nextIndex = selectedRow + 1 + } + + if (nextIndex !== selectedRow) { + setSelectedRow(nextIndex) + gridRef.current?.scrollToCell({ idx: 0, rowIdx: nextIndex }) + + const rowQuery = reportData[nextIndex]?.query ?? '' + if (!rowQuery.trim().toLowerCase().startsWith('select')) { + setView('details') + } + } + }, + [reportData, selectedRow] + ) + + useEffect(() => { + // run before RDG to prevent header focus (the third param: true) + window.addEventListener('keydown', handleKeyDown, true) + return () => { + window.removeEventListener('keydown', handleKeyDown, true) + } + }, [handleKeyDown]) + return ( { + const [copied, setCopied] = useState(false) + + const content = useMemo(() => value ?? '', [value]) + + const handleCopy = () => { + setCopied(true) + setTimeout(() => setCopied(false), 1000) + } + + return ( +
+ + + {!hideCopy && ( +
+ + + +
+ )} +
+ ) +} diff --git a/apps/studio/components/interfaces/Reports/Reports.constants.ts b/apps/studio/components/interfaces/Reports/Reports.constants.ts index 844b50dcd6c3c..b6cf92f089428 100644 --- a/apps/studio/components/interfaces/Reports/Reports.constants.ts +++ b/apps/studio/components/interfaces/Reports/Reports.constants.ts @@ -406,6 +406,7 @@ select statements.query, statements.calls, statements.total_exec_time + statements.total_plan_time as total_time, + statements.mean_exec_time + statements.mean_plan_time as mean_time, 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${ runIndexAdvisor ? `, diff --git a/apps/studio/csp.js b/apps/studio/csp.js index 3175bd5096b13..5c94f4093e19f 100644 --- a/apps/studio/csp.js +++ b/apps/studio/csp.js @@ -33,6 +33,10 @@ const isDevOrStaging = const NIMBUS_STAGING_PROJECTS_URL = 'https://*.nmb-proj.com' const NIMBUS_STAGING_PROJECTS_URL_WS = 'wss://*.nmb-proj.com' + +const NIMBUS_PROD_PROJECTS_URL = process.env.NIMBUS_PROD_PROJECTS_URL || '' +const NIMBUS_PROD_PROJECTS_URL_WS = process.env.NIMBUS_PROD_PROJECTS_URL_WS || '' + const SUPABASE_STAGING_PROJECTS_URL = 'https://*.supabase.red' const SUPABASE_STAGING_PROJECTS_URL_WS = 'wss://*.supabase.red' const SUPABASE_COM_URL = 'https://supabase.com' @@ -98,6 +102,7 @@ module.exports.getCSP = function getCSP() { STAPE_URL, GOOGLE_MAPS_API_URL, POSTHOG_URL, + ...(!!NIMBUS_PROD_PROJECTS_URL ? [NIMBUS_PROD_PROJECTS_URL, NIMBUS_PROD_PROJECTS_URL_WS] : []), ] const SCRIPT_SRC_URLS = [ CLOUDFLARE_CDN_URL, @@ -117,6 +122,7 @@ module.exports.getCSP = function getCSP() { SUPABASE_ASSETS_URL, USERCENTRICS_APP_URL, STAPE_URL, + ...(!!NIMBUS_PROD_PROJECTS_URL ? [NIMBUS_PROD_PROJECTS_URL, NIMBUS_PROD_PROJECTS_URL_WS] : []), ] const STYLE_SRC_URLS = [CLOUDFLARE_CDN_URL, SUPABASE_ASSETS_URL] const FONT_SRC_URLS = [CLOUDFLARE_CDN_URL, SUPABASE_ASSETS_URL]