import { Code, Play } from 'lucide-react' import { DragEvent, ReactNode, useEffect, useMemo, useState } from 'react' import { Bar, BarChart, CartesianGrid, Cell, Tooltip, XAxis, YAxis } from 'recharts' import { toast } from 'sonner' 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 { usePrimaryDatabase } from 'data/read-replicas/replicas-query' import { QueryResponseError, useExecuteSqlMutation } from 'data/sql/execute-sql-mutation' import dayjs from 'dayjs' import { Parameter, parseParameters } from 'lib/sql-parameters' import { Dashboards } from 'types' import { ChartContainer, ChartTooltipContent, cn, CodeBlock, SQL_ICON } from 'ui' import ShimmeringLoader from 'ui-patterns/ShimmeringLoader' import { ButtonTooltip } from '../ButtonTooltip' import { CHART_COLORS } from '../Charts/Charts.constants' import SqlWarningAdmonition from '../SqlWarningAdmonition' 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 /** 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 /** Optional: Any initial results to render as part of the query*/ results?: any[] /** Opt to show run button if query is not read only */ showRunButtonIfNotReadOnly?: boolean /** 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 /** Optional: callback when the results are returned from running the SQL query*/ onResults?: (results: any[]) => 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 /** UI to render if there's a read-only error while running the query */ readOnlyErrorPlaceholder?: ReactNode /** 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, lockColumns = false, draggable = false, isRefreshing = false, noResultPlaceholder = null, readOnlyErrorPlaceholder = null, showRunButtonIfNotReadOnly = false, tooltip, results, onRunQuery, onSetParameter, onUpdateChartConfig, onDragStart, onResults, }: QueryBlockProps) => { const { ref } = useParams() const [chartSettings, setChartSettings] = useState(chartConfig) const { xKey, yKey, view = 'table' } = chartSettings const [showSql, setShowSql] = useState(_showSql) const [readOnlyError, setReadOnlyError] = useState(false) const [queryError, setQueryError] = useState() const [queryResult, setQueryResult] = useState(results) const [focusDataIndex, setFocusDataIndex] = useState() const formattedQueryResult = useMemo(() => { // Make sure Y axis values are numbers return queryResult?.map((row) => { return Object.fromEntries( Object.entries(row).map(([key, value]) => { if (key === yKey) return [key, Number(value)] else return [key, value] }) ) }) }, [queryResult, yKey]) 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 { database: primaryDatabase } = usePrimaryDatabase({ projectRef: ref }) const postgresConnectionString = primaryDatabase?.connectionString const readOnlyConnectionString = primaryDatabase?.connection_string_read_only const chartData = chartSettings.cumulative ? getCumulativeResults({ rows: formattedQueryResult ?? [] }, chartSettings) : formattedQueryResult const { mutate: execute, isLoading: isExecuting } = useExecuteSqlMutation({ onSuccess: (data) => { onResults?.(data.result) setQueryResult(data.result) setReadOnlyError(false) setQueryError(undefined) }, onError: (error) => { const permissionDenied = error.message.includes('permission denied') const notOwner = error.message.includes('must be owner') if (permissionDenied || notOwner) { setReadOnlyError(true) if (showRunButtonIfNotReadOnly) setShowWarning('hasWriteOperation') } else { setQueryError(error) } }, }) const getDateFormat = (key: any) => { const value = chartData?.[0]?.[key] || '' if (typeof value === 'number') return 'number' if (dayjs(value).isValid()) return 'date' return 'string' } const xKeyDateFormat = getDateFormat(xKey) const handleExecute = () => { if (!sql || isLoading) return if (readOnlyError) { return setShowWarning('hasWriteOperation') } try { // [Joshen] This is for when we introduced the concept of parameters into our reports // const processedSql = processParameterizedSql(sql, combinedParameterValues) execute({ projectRef: ref, connectionString: readOnlyConnectionString, sql, }) } catch (error: any) { toast.error(`Failed to execute query: ${error.message}`) } } useEffect(() => { setChartSettings(chartConfig) }, [chartConfig]) // Run once on mount to parse parameters and notify parent useEffect(() => { if (!!sql && onSetParameter) { const params = parseParameters(sql) onSetParameter(params) } // eslint-disable-next-line react-hooks/exhaustive-deps }, [sql]) useEffect(() => { if (!!sql && !isLoading && runQuery && !!readOnlyConnectionString && !readOnlyError) { handleExecute() } }, [sql, isLoading, runQuery, readOnlyConnectionString]) 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) }} /> )} )} {(showRunButtonIfNotReadOnly || !readOnlyError) && ( } loading={isExecuting || isLoading} disabled={isLoading} onClick={() => { handleExecute() if (!!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 && ( setShowWarning(undefined)} onConfirm={() => { // [Joshen] This is for when we introduced the concept of parameters into our reports // const processedSql = processParameterizedSql(sql!, combinedParameterValues) if (sql) { setShowWarning(undefined) execute({ projectRef: ref, connectionString: postgresConnectionString, sql, }) onRunQuery?.('mutation') } }} disabled={!sql} /> )} {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

) : (
{ if (e.activeTooltipIndex !== focusDataIndex) { setFocusDataIndex(e.activeTooltipIndex) } }} onMouseLeave={() => setFocusDataIndex(undefined)} > xKeyDateFormat === 'date' ? dayjs(value).format('MMM D YYYY HH:mm') : value } /> } /> {chartData?.map((_: any, index: number) => ( ))}
)} ) : ( <> {!isExecuting && !!queryError ? (
ERROR: {queryError.message}
) : queryResult ? (
) : !isExecuting ? ( readOnlyError ? ( readOnlyErrorPlaceholder ) : ( noResultPlaceholder ) ) : null} )}
) }