import { acceptUntrustedSql, untrustedSql } from '@supabase/pg-meta' import { useFlag } from 'common' import { CodeSquare, Eye, EyeOff, Play } from 'lucide-react' import { useState, type ReactNode } from 'react' import { cn } from 'ui' import { resolveLogTimeRange } from '../QuerySources/LogTimeRange.utils' import { ExplorerQuery, ExplorerQueryEditor, ExplorerQueryFooter, ExplorerQueryResults, ExplorerQueryViewport, } from './ExplorerQuery' import { ExplorerQuerySourceMenu } from './ExplorerQuerySourceMenu' import { ExplorerToolbar, ExplorerToolbarAction, ExplorerToolbarActions, ExplorerToolbarIcon, ExplorerToolbarTitle, } from './ExplorerToolbar' import { DisplaySettingsButton } from './QueryCell/DisplaySettingsButton' import { QueryResultChart } from './QueryCell/QueryResultChart' import { QueryResultTable } from './QueryResultTable' import { type QueryDisplay, type QueryResult } from './types' import { applyAutoLimit } from '@/components/interfaces/SQLEditor/SQLEditor.utils' import { CodeEditor } from '@/components/ui/CodeEditor/CodeEditor' import { isValidConnString } from '@/data/fetchers' import { useExecuteLogsSqlMutation } from '@/data/logs/execute-logs-sql-mutation' import { acceptUntrustedLogsSql, untrustedLogSql } from '@/data/logs/safe-analytics-sql' import { createDefaultCellSource, QUERY_SOURCE_REGISTRY, type CellSource, } from '@/data/query-sources/query-source-registry' import { useReadReplicasQuery } from '@/data/read-replicas/replicas-query' import { useExecuteSqlMutation } from '@/data/sql/execute-sql-mutation' import { useLatest } from '@/hooks/misc/useLatest' import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject' export type QueryEditorProps = { id: string variant: 'embedded' | 'viewport' title: string sql: string source?: CellSource result?: QueryResult rowLimit?: number display?: QueryDisplay toolbarActions?: ReactNode onTitleChange: (title: string) => void onSqlChange: (sql: string) => void onSqlCommit?: (sql: string) => void onSourceChange?: (source: CellSource) => void onResultChange: (result: QueryResult) => void onDisplayChange?: (display: QueryDisplay) => void } /** * Shared query editor used by query tabs, notebook cells, and other Explorer surfaces. * The consuming surface owns persistence and surrounding chrome; this component owns * query-level UI and execution behavior. */ export const QueryEditor = ({ id, variant, title, sql, source, result, rowLimit, display, toolbarActions, onTitleChange, onSqlChange, onSqlCommit, onSourceChange, onResultChange, onDisplayChange, }: QueryEditorProps) => { const sqlRef = useLatest(sql) const onSqlCommitRef = useLatest(onSqlCommit) const isOtelLogsEnabled = useFlag('otelLegacyLogs') const { data: project, isPending: isLoadingProject } = useSelectedProjectQuery() const view = display?.view ?? 'table' const columns = Object.keys(result?.rows?.[0] ?? {}) const sourceBinding = source ?? createDefaultCellSource('database') const [showQuery, setShowQuery] = useState(true) const databaseIdentifier = sourceBinding.type === 'database' ? sourceBinding.parameters.identifier : undefined const { data: databases, isPending: isLoadingDatabases } = useReadReplicasQuery( { projectRef: project?.ref }, { enabled: databaseIdentifier !== undefined && project?.ref !== undefined && databaseIdentifier !== project.ref, } ) const { mutate: executeSql, isPending: isExecutingSql } = useExecuteSqlMutation({ onSuccess: (data) => onResultChange({ rows: data.result }), onError: (error) => onResultChange({ error }), }) const { mutate: executeLogsSql, isPending: isExecutingLogs } = useExecuteLogsSqlMutation({ onSuccess: (data) => onResultChange({ rows: data.rows as readonly Record[] }), onError: (error) => onResultChange({ error }), }) const isResolvingDatabase = databaseIdentifier !== undefined && databaseIdentifier !== project?.ref && isLoadingDatabases const isExecuting = isExecutingSql || isExecutingLogs const isBusy = isLoadingProject || isResolvingDatabase || isExecuting const handleRunQuery = (sqlToRun: string = sql) => { if (!project || isBusy || sqlToRun.trim().length === 0) return onSqlCommit?.(sql) if (sourceBinding.type === 'logs') { if (!isOtelLogsEnabled) { onResultChange({ error: { message: "Querying logs isn't available for this project yet." }, }) return } executeLogsSql({ projectRef: project.ref, sql: acceptUntrustedLogsSql(untrustedLogSql(sqlToRun)), range: resolveLogTimeRange(sourceBinding.parameters.time_range), endpoint: QUERY_SOURCE_REGISTRY.logs.endpoint, }) return } const safeSql = acceptUntrustedSql(untrustedSql(sqlToRun)) const limitedSql = applyAutoLimit(safeSql, rowLimit) const connectionString = databaseIdentifier === undefined || databaseIdentifier === project.ref ? project.connectionString : databases?.find((database) => database.identifier === databaseIdentifier) ?.connectionString if (!isValidConnString(connectionString)) { onResultChange({ error: { message: 'Unable to run query: Connection string is missing' } }) return } executeSql({ projectRef: project.ref, connectionString, sql: limitedSql.sql, autoLimit: limitedSql.appendAutoLimit ? rowLimit : undefined, contextualInvalidation: true, isStatementTimeoutDisabled: true, }) } const Shell = variant === 'viewport' ? ExplorerQueryViewport : ExplorerQuery return ( {title} {toolbarActions} {source && onSourceChange && ( )} {display && onDisplayChange && ( )} : } tooltip={showQuery ? 'Hide query' : 'Show query'} onClick={() => setShowQuery((value) => !value)} /> } tooltip="Run query" disabled={isLoadingProject || isExecuting || sql.trim().length === 0} onClick={() => handleRunQuery()} > Run {showQuery && ( onSqlChange(value ?? '')} onMount={(editor) => { editor.onDidBlurEditorWidget(() => onSqlCommitRef.current?.(sqlRef.current)) }} /> )} {view === 'table' && } {view === 'chart' && }

{(result?.rows ?? []).length.toLocaleString()} rows

{rowLimit && ( <>

ยท

Limit {rowLimit} rows

)}
) }