Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 11 additions & 18 deletions apps/studio/components/interfaces/QueryPerformance/QueryDetail.tsx
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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,
Expand All @@ -43,16 +45,7 @@ export const QueryDetail = ({
<QueryPanelContainer>
<QueryPanelSection>
<p className="text-sm">Query pattern</p>
<CodeBlock
hideLineNumbers
value={query}
language="sql"
className={cn(
'max-w-full max-h-[310px]',
'!py-3 !px-3.5 prose dark:prose-dark transition',
'[&>code]:m-0 [&>code>span]:flex [&>code>span]:flex-wrap'
)}
/>
<SqlMonacoBlock value={query} height={310} lineNumbers="off" wrapperClassName="pl-3" />
{isLinterWarning && (
<Alert_Shadcn_
variant="default"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,10 @@ 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 },
{ id: 'calls', name: 'Calls', description: undefined, minWidth: 60 },
{ id: 'total_time', name: 'Total time', description: 'latency', minWidth: 150 },
{ id: 'prop_total_time', name: 'Time consumed', description: undefined, minWidth: 110 },
{ id: 'mean_time', name: 'Mean time', description: undefined, minWidth: 80 },
],
[QUERY_PERFORMANCE_REPORT_TYPES.MOST_FREQUENT]: [
{ id: 'query', name: 'Query', description: undefined, minWidth: 600 },
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { ArrowDown, ArrowUp, TextSearch, X } from 'lucide-react'
import { useRouter } from 'next/router'
import { useEffect, useRef, useState } from 'react'
import { useCallback, useEffect, useRef, useState } from 'react'
import DataGrid, { Column, DataGridHandle, Row } from 'react-data-grid'
import dynamic from 'next/dynamic'

import { useParams } from 'common'
import { DbQueryHook } from 'hooks/analytics/useDbQuery'
Expand Down Expand Up @@ -31,6 +32,11 @@ interface QueryPerformanceGridProps {
queryPerformanceQuery: DbQueryHook<any>
}

// 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<DataGridHandle>(null)
Expand Down Expand Up @@ -74,7 +80,7 @@ export const QueryPerformanceGrid = ({ queryPerformanceQuery }: QueryPerformance
const value = props.row?.[col.id]
if (col.id === 'query') {
return (
<div className="w-full flex items-center gap-x-2">
<div className="w-full flex items-center gap-x-2 pointer-events-none">
{hasIndexRecommendations(props.row.index_advisor_result, true) && (
<IndexSuggestionIcon
indexAdvisorResult={props.row.index_advisor_result}
Expand All @@ -85,7 +91,38 @@ export const QueryPerformanceGrid = ({ queryPerformanceQuery }: QueryPerformance
}}
/>
)}
<div className="font-mono text-xs">{value}</div>
<Editor
height={20}
theme="supabase"
language="pgsql"
value={value.replace(/\s+/g, ' ').trim()}
wrapperProps={{
className:
'[&_.monaco-editor]:!bg-transparent [&_.monaco-editor-background]:!bg-transparent [&_.monaco-editor]:!outline-transparent',
}}
options={{
readOnly: true,
domReadOnly: true,
cursorBlinking: 'solid',
tabIndex: -1,
fontSize: 12,
minimap: { enabled: false },
lineNumbers: 'off',
renderLineHighlight: 'none',
scrollbar: { vertical: 'hidden', horizontal: 'hidden' },
overviewRulerLanes: 0,
overviewRulerBorder: false,
glyphMargin: false,
folding: false,
lineDecorationsWidth: 0,
lineNumbersMinChars: 0,
wordWrap: 'off',
scrollBeyondLastLine: false,
contextmenu: false,
selectionHighlight: false,
occurrencesHighlight: 'off',
}}
/>
</div>
)
}
Expand Down Expand Up @@ -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 (
<ResizablePanelGroup
direction="horizontal"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import Editor from '@monaco-editor/react'
import { Check, Copy } from 'lucide-react'
import { useMemo, useState } from 'react'
import { CopyToClipboard } from 'react-copy-to-clipboard'

import { Button, cn } from 'ui'

type SqlMonacoBlockProps = {
value?: string
className?: string
wrapperClassName?: string
hideCopy?: boolean
// Fixed height in px. Defaults to 310 to match previous CodeBlock max height
height?: number
// Show line numbers. Defaults to false to match previous CodeBlock
lineNumbers?: 'on' | 'off'
}

export const SqlMonacoBlock = ({
value,
className,
wrapperClassName,
height = 310,
lineNumbers = 'off',
hideCopy = false,
}: SqlMonacoBlockProps) => {
const [copied, setCopied] = useState(false)

const content = useMemo(() => value ?? '', [value])

const handleCopy = () => {
setCopied(true)
setTimeout(() => setCopied(false), 1000)
}

return (
<div
className={cn('group relative border rounded-md overflow-hidden w-full', wrapperClassName)}
>
<Editor
theme="supabase"
language="pgsql"
value={content}
height={height}
className={className}
wrapperProps={{
className:
'[&_.monaco-editor]:!bg-transparent [&_.monaco-editor-background]:!bg-transparent [&_.monaco-editor]:!outline-transparent [&_.cursor]:!hidden',
}}
options={{
readOnly: true,
domReadOnly: true,
fontSize: 13,
minimap: { enabled: false },
lineNumbers,
renderLineHighlight: 'none',
scrollbar: { vertical: 'auto', horizontal: 'auto' },
overviewRulerLanes: 0,
overviewRulerBorder: false,
glyphMargin: false,
folding: false,
lineDecorationsWidth: 0,
lineNumbersMinChars: lineNumbers === 'off' ? 0 : 3,
wordWrap: 'on',
scrollBeyondLastLine: false,
selectionHighlight: false,
occurrencesHighlight: 'off',
fixedOverflowWidgets: true,
padding: { top: 12, bottom: 12 },
tabIndex: -1,
}}
/>

{!hideCopy && (
<div className="absolute right-2 top-2 opacity-0 group-hover:opacity-100 transition-opacity">
<CopyToClipboard text={content}>
<Button
type="default"
className="px-1.5"
icon={copied ? <Check /> : <Copy />}
onClick={handleCopy}
>
{copied ? 'Copied' : ''}
</Button>
</CopyToClipboard>
</div>
)}
</div>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
? `,
Expand Down
6 changes: 6 additions & 0 deletions apps/studio/csp.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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,
Expand All @@ -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]
Expand Down
Loading