import { Code, ExternalLink, Play } from 'lucide-react' import { DragEvent, ReactNode, useEffect, useMemo, useState } from 'react' import { Bar, BarChart, CartesianGrid, XAxis } from 'recharts' import Link from 'next/link' import { useParams } from 'common' import { ReportBlockContainer } from 'components/interfaces/Reports/ReportBlock/ReportBlockContainer' import { ChartConfig } from 'components/interfaces/SQLEditor/UtilityPanel/ChartConfig' import Results from 'components/interfaces/SQLEditor/UtilityPanel/Results' import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext' import { useExecuteSqlMutation } from 'data/sql/execute-sql-mutation' import useLogsQuery from 'hooks/analytics/useLogsQuery' import { Parameter, parseParameters } from 'lib/sql-parameters' import { Dashboards } from 'types' import { Button, ChartContainer, ChartTooltip, ChartTooltipContent, cn, CodeBlock, SQL_ICON, } from 'ui' import { Admonition } from 'ui-patterns' import ShimmeringLoader from 'ui-patterns/ShimmeringLoader' import { containsUnknownFunction, isReadOnlySelect } from '../AIAssistantPanel/AIAssistant.utils' import { ButtonTooltip } from '../ButtonTooltip' import { BlockViewConfiguration } from './BlockViewConfiguration' import { EditQueryButton } from './EditQueryButton' import { ParametersPopover } from './ParametersPopover' import { getCumulativeResults } from './QueryBlock.utils' export const DEFAULT_CHART_CONFIG: ChartConfig = { type: 'bar', cumulative: false, xKey: '', yKey: '', showLabels: false, showGrid: false, view: 'table', } interface QueryBlockProps { /** Applicable if SQL is a snippet that's already saved (Used in Reports) */ id?: string /** Title of the QueryBlock */ label: string /** SQL query to render/run in the QueryBlock */ sql?: string /** Configuration of the output chart based on the query result */ chartConfig?: ChartConfig /** Not implemented yet: Will be the next part of ReportsV2 */ parameterValues?: Record /** Any other actions specific to the parent to be rendered in the header */ actions?: ReactNode /** Toggle visiblity of SQL query on render */ showSql?: boolean /** Indicate if SQL query can be rendered as a chart */ isChart?: boolean /** For Assistant as QueryBlock is rendered while streaming response */ isLoading?: boolean /** Override to prevent running the SQL query provided */ runQuery?: boolean /** Whether this is a logs query */ logs?: boolean /** Prevent updating of columns for X and Y axes in the chart view */ lockColumns?: boolean /** Max height set to render results / charts (Defaults to 250) */ maxHeight?: number /** Whether query block is draggable */ draggable?: boolean /** Tooltip when hovering over the header of the block (Used in Assistant Panel) */ tooltip?: ReactNode /** Not implemented yet: Will be the next part of ReportsV2 */ onSetParameter?: (params: Parameter[]) => void /** Optional callback the SQL query is run */ onRunQuery?: (queryType: 'select' | 'mutation') => void /** Optional callback on drag start */ onDragStart?: (e: DragEvent) => void // [Joshen] Params below are currently only used by ReportsV2 (Might revisit to see how to improve these) /** Optional height set to render the SQL query (Used in Reports) */ queryHeight?: number /** Override hiding Run Query button if SQL query is NOT readonly (Used in Reports) */ disableRunIfMutation?: boolean /** UI to render if there's no query results (Used in Reports) */ noResultPlaceholder?: ReactNode /** To trigger a refresh of the query */ isRefreshing?: boolean /** Optional callback whenever a chart configuration is updated (Used in Reports) */ onUpdateChartConfig?: ({ chart, chartConfig, }: { chart?: Partial chartConfig: Partial }) => void } // [Joshen ReportsV2] JFYI we may adjust this in subsequent PRs when we implement this into Reports V2 // First iteration here is just to make this work with the AI Assistant first export const QueryBlock = ({ id, label, sql, chartConfig = DEFAULT_CHART_CONFIG, maxHeight = 250, queryHeight, parameterValues: extParameterValues, actions, showSql: _showSql = false, isChart = false, isLoading = false, runQuery = false, logs = false, lockColumns = false, draggable = false, isRefreshing = false, disableRunIfMutation = false, noResultPlaceholder = null, tooltip, onRunQuery, onSetParameter, onUpdateChartConfig, onDragStart, }: QueryBlockProps) => { const { ref } = useParams() const { project } = useProjectContext() const [chartSettings, setChartSettings] = useState(chartConfig) const { xKey, yKey, view = 'table' } = chartSettings const [showSql, setShowSql] = useState(_showSql) const [queryResult, setQueryResult] = useState() const [parameterValues, setParameterValues] = useState>({}) const [showWarning, setShowWarning] = useState<'hasWriteOperation' | 'hasUnknownFunctions'>() const parameters = useMemo(() => { if (!sql) return [] return parseParameters(sql) }, [sql]) // [Joshen] This is for when we introduced the concept of parameters into our reports // const combinedParameterValues = { ...extParameterValues, ...parameterValues } const isReadOnlySelectSQL = isReadOnlySelect(sql ?? '') const { mutate: execute, isLoading: isExecuting, error: sqlError, } = useExecuteSqlMutation({ onSuccess: (data) => setQueryResult(data.result), }) const { params, logData, error: logsError, isLoading: isLoadingLogs, runQuery: runLogsQuery, } = useLogsQuery( ref || '', { sql: sql || '', }, logs && runQuery && Boolean(ref) ) const error = logs ? logsError : sqlError const errorMessage = error ? typeof error === 'string' ? error : Array.isArray((error as any)?.error) ? (error as any).error[0]?.message : (error as any)?.message ?? 'An error occurred' : undefined const handleExecute = () => { if (!sql || isLoading) return if (!isReadOnlySelectSQL) { const hasUnknownFunctions = containsUnknownFunction(sql) return setShowWarning(hasUnknownFunctions ? 'hasUnknownFunctions' : 'hasWriteOperation') } try { if (logs) { runLogsQuery() } else { execute({ projectRef: ref, connectionString: project?.connectionString, sql, }) } } catch (error: any) { console.error(error.message) } } useEffect(() => { setChartSettings(chartConfig) }, [chartConfig]) // Monitor the logData and set the queryResult useEffect(() => { if (logs && logData && !isLoading) { setQueryResult(logData) } }, [logData, isLoading]) // Run once on mount to parse parameters and notify parent useEffect(() => { if (!!sql && onSetParameter) { const params = parseParameters(sql) onSetParameter(params) } }, [sql, onSetParameter]) useEffect(() => { const shouldExecute = !!sql && !isLoading && !isLoadingLogs && runQuery && !!project const isValidQuery = isReadOnlySelect(sql ?? '') if (shouldExecute && isValidQuery) { handleExecute() } }, [sql, isLoading, isLoadingLogs, runQuery, project?.connectionString]) useEffect(() => { if (isRefreshing) handleExecute() }, [isRefreshing]) return ( ) => onDragStart?.(e)} icon={ } label={label} actions={ <> } onClick={() => setShowSql(!showSql)} tooltip={{ content: { side: 'bottom', text: showSql ? 'Hide query' : 'Show query' }, }} /> {queryResult && ( <> {/* [Joshen ReportsV2] Won't see this just yet as this is intended for Reports V2 */} {parameters.length > 0 && ( )} {isChart && ( { if (onUpdateChartConfig) onUpdateChartConfig({ chartConfig: { view } }) setChartSettings({ ...chartSettings, view }) }} updateChartConfig={(config) => { if (onUpdateChartConfig) onUpdateChartConfig({ chartConfig: config }) setChartSettings(config) }} /> )} )} {logs && sql ? ( } asChild tooltip={{ content: { side: 'bottom', text: 'Open in Logs Explorer' }, }} > ) : ( )} {(isReadOnlySelectSQL || (!isReadOnlySelectSQL && !disableRunIfMutation)) && ( } loading={isExecuting || isLoadingLogs || isLoading} disabled={isLoading} onClick={() => { handleExecute() if (!!sql && isReadOnlySelect(sql)) onRunQuery?.('select') }} tooltip={{ content: { side: 'bottom', className: 'max-w-56 text-center', text: isExecuting ? (

{`Query is running. You may cancel ongoing queries via the [SQL Editor](/project/${ref}/sql?viewOngoingQueries=true).`}

) : ( 'Run query' ), }, }} /> )} {actions} } > {!!showWarning && (

{showWarning === 'hasWriteOperation' ? 'This query contains write operations.' : 'This query involves running a function.'}{' '} Are you sure you want to execute it?

Make sure you are not accidentally removing something important.

)} {isExecuting && queryResult === undefined && (
)} {showSql && (
code]:m-0 [&>code>span]:text-foreground' )} />
)} {view === 'chart' && queryResult !== undefined ? ( <> {(queryResult ?? []).length === 0 ? (

No results returned from query

) : !xKey || !yKey ? (

Select columns for the X and Y axes

) : (
} />
)} ) : ( <> {error ? (

Failed to execute {logs ? 'logs' : 'SQL'} query

{errorMessage}

) : queryResult ? (
) : !isExecuting ? ( noResultPlaceholder ) : null} )}
) }