Files
supabase/apps/studio/components/ui/QueryBlock/QueryBlock.tsx
Saxon Fletcher 626eb30e77
Some checks failed
Generate Embeddings for Search / deploy (push) Has been cancelled
[Docs] Lint v2 (scheduled) / lint-all (push) Has been cancelled
[Docs] Update last-changed dates / deploy (push) Has been cancelled
Automatically label stale issues / build (push) Has been cancelled
Docs Production Smoke Tests / build (push) Has been cancelled
Publish to Image Registry / settings (push) Has been cancelled
Publish to Image Registry / release_x86 (push) Has been cancelled
Publish to Image Registry / release_arm (push) Has been cancelled
Publish to Image Registry / merge_manifest (push) Has been cancelled
Publish to Image Registry / publish (push) Has been cancelled
Update Mgmt Api Docs / update-docs (push) Has been cancelled
AI Unit Tests & Type Check / test (push) Has been cancelled
Assistant action orientated approach (#38806)
* update onboarding

* update model and fix part issue

* action orientated assistant

* fix tool

* lock

* remove unused filter

* fix tests

* fix again

* update package

* update container

* fix tests

* refactor(ai assistant): break out message markdown and profile picture

* wip

* refactor(ai assistant): break up message component

* refactor: break ai assistant message down into multiple files

* refactor: simplify ReportBlock state

* fix: styling of draggable report block header

When the drag handle is showing, it overlaps with the block header.
Decrease the opacity of the header so the handle can be seen and the two
can be distinguished.

* fix: minor tweaks to tool ui

* refactor: simplify DisplayBlockRenderer state

* fix: remove double deploy button in edge function block

When the confirm footer is shown, the deploy button on the top right should be
hidden (not just disabled) to avoid confusion.

* refactor, test: message sanitization by opt-in level

Refactor the message sanitization to have more type safety and be more testable.
Add tests to ensure:

- Message sanitization always runs on generate-v4
- Message sanitization correctly works by opt-in level

* Fix conflicts in pnpm lock

* Couple of nits and refactors

* Revert casing for report block snippet

* adjust sanitised prompt

* Fix tests

---------

Co-authored-by: Charis Lam <26616127+charislam@users.noreply.github.com>
Co-authored-by: Joshen Lim <joshenlimek@gmail.com>
2025-09-29 03:57:36 +00:00

330 lines
11 KiB
TypeScript

import dayjs from 'dayjs'
import { Code, Play } from 'lucide-react'
import { DragEvent, ReactNode, useEffect, useMemo, useRef, useState } from 'react'
import { Bar, BarChart, CartesianGrid, Cell, Tooltip, XAxis, YAxis } from 'recharts'
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 { Badge, Button, ChartContainer, ChartTooltipContent, cn, CodeBlock } 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 { getCumulativeResults } from './QueryBlock.utils'
export const DEFAULT_CHART_CONFIG: ChartConfig = {
type: 'bar',
cumulative: false,
xKey: '',
yKey: '',
showLabels: false,
showGrid: false,
view: 'table',
}
export interface QueryBlockProps {
id?: string
label: string
sql?: string
isWriteQuery?: boolean
chartConfig?: ChartConfig
actions?: ReactNode
results?: any[]
errorText?: string
isExecuting?: boolean
initialHideSql?: boolean
draggable?: boolean
disabled?: boolean
blockWriteQueries?: boolean
onExecute?: (queryType: 'select' | 'mutation') => void
onRemoveChart?: () => void
onUpdateChartConfig?: ({ chartConfig }: { chartConfig: Partial<ChartConfig> }) => void
onDragStart?: (e: DragEvent<Element>) => 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,
actions,
results,
errorText,
isWriteQuery = false,
isExecuting = false,
initialHideSql = false,
draggable = false,
disabled = false,
blockWriteQueries = false,
onExecute,
onRemoveChart,
onUpdateChartConfig,
onDragStart,
}: QueryBlockProps) => {
const [chartSettings, setChartSettings] = useState<ChartConfig>(chartConfig)
const { xKey, yKey, view = 'table' } = chartSettings
const [showSql, setShowSql] = useState(!results && !initialHideSql)
const [focusDataIndex, setFocusDataIndex] = useState<number>()
const [showWarning, setShowWarning] = useState<'hasWriteOperation' | 'hasUnknownFunctions'>()
const prevIsWriteQuery = useRef(isWriteQuery)
useEffect(() => {
if (!prevIsWriteQuery.current && isWriteQuery) {
setShowWarning('hasWriteOperation')
}
if (!isWriteQuery && showWarning === 'hasWriteOperation') {
setShowWarning(undefined)
}
prevIsWriteQuery.current = isWriteQuery
}, [isWriteQuery, showWarning])
useEffect(() => {
setChartSettings(chartConfig)
}, [chartConfig])
const formattedQueryResult = useMemo(() => {
return results?.map((row) => {
return Object.fromEntries(
Object.entries(row).map(([key, value]) => {
if (key === yKey) return [key, Number(value)]
return [key, value]
})
)
})
}, [results, yKey])
const chartData = chartSettings.cumulative
? getCumulativeResults({ rows: formattedQueryResult ?? [] }, chartSettings)
: formattedQueryResult
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 hasResults = Array.isArray(results) && results.length > 0
const runSelect = () => {
if (!sql || disabled || isExecuting) return
if (isWriteQuery) {
setShowWarning('hasWriteOperation')
return
}
onExecute?.('select')
}
const runMutation = () => {
if (!sql || disabled || isExecuting) return
setShowWarning(undefined)
onExecute?.('mutation')
}
return (
<ReportBlockContainer
draggable={draggable}
showDragHandle={draggable}
onDragStart={(e: DragEvent<Element>) => onDragStart?.(e)}
loading={isExecuting}
label={label}
badge={isWriteQuery && <Badge variant="warning">Write</Badge>}
actions={
disabled ? null : (
<>
<ButtonTooltip
type="text"
size="tiny"
className="w-7 h-7"
icon={<Code size={14} strokeWidth={1.5} />}
onClick={() => setShowSql(!showSql)}
tooltip={{
content: { side: 'bottom', text: showSql ? 'Hide query' : 'Show query' },
}}
/>
{hasResults && (
<BlockViewConfiguration
view={view}
isChart={view === 'chart'}
lockColumns={false}
chartConfig={chartSettings}
columns={Object.keys(results?.[0] ?? {})}
changeView={(nextView) => {
if (onUpdateChartConfig) onUpdateChartConfig({ chartConfig: { view: nextView } })
setChartSettings({ ...chartSettings, view: nextView })
}}
updateChartConfig={(config) => {
if (onUpdateChartConfig) onUpdateChartConfig({ chartConfig: config })
setChartSettings(config)
}}
/>
)}
<EditQueryButton id={id} title={label} sql={sql} />
<ButtonTooltip
type="text"
size="tiny"
className="w-7 h-7"
icon={<Play size={14} strokeWidth={1.5} />}
loading={isExecuting}
disabled={isExecuting || disabled || !sql}
onClick={runSelect}
tooltip={{
content: {
side: 'bottom',
className: 'max-w-56 text-center',
text: isExecuting
? 'Query is running. Check the SQL Editor to manage running queries.'
: 'Run query',
},
}}
/>
{actions}
</>
)
}
>
{!!showWarning && !blockWriteQueries && (
<SqlWarningAdmonition
warningType={showWarning}
className="border-b"
onCancel={() => setShowWarning(undefined)}
onConfirm={runMutation}
disabled={!sql}
{...(showWarning !== 'hasWriteOperation'
? {
message: 'Run this query now and send the results to the Assistant? ',
subMessage:
'We will execute the query and provide the result rows back to the Assistant to continue the conversation.',
cancelLabel: 'Skip',
confirmLabel: 'Run & send',
}
: {})}
/>
)}
{showSql && (
<div
className={cn('shrink-0 grow-1 w-full h-full overflow-y-auto max-h-[min(300px, 100%)]', {
'border-b': results !== undefined,
})}
>
<CodeBlock
hideLineNumbers
wrapLines={false}
value={sql}
language="sql"
className={cn(
'max-w-none block !bg-transparent !py-3 !px-3.5 prose dark:prose-dark border-0 text-foreground !rounded-none w-full',
'[&>code]:m-0 [&>code>span]:text-foreground'
)}
/>
</div>
)}
{isExecuting && !results && (
<div className="p-3 w-full border-t">
<ShimmeringLoader />
</div>
)}
{view === 'chart' && results !== undefined ? (
<>
{(results ?? []).length === 0 ? (
<div className="flex w-full h-full items-center justify-center py-3">
<p className="text-foreground-light text-xs">No results returned from query</p>
</div>
) : !xKey || !yKey ? (
<div className="flex w-full h-full items-center justify-center">
<p className="text-foreground-light text-xs">Select columns for the X and Y axes</p>
</div>
) : (
<div className="flex-1 w-full">
<ChartContainer
className="aspect-auto px-3 py-2"
style={{ height: '230px', minHeight: '230px' }}
>
<BarChart
accessibilityLayer
margin={{ left: -20, right: 0, top: 10 }}
data={chartData}
onMouseMove={(e: any) => {
if (e.activeTooltipIndex !== focusDataIndex) {
setFocusDataIndex(e.activeTooltipIndex)
}
}}
onMouseLeave={() => setFocusDataIndex(undefined)}
>
<CartesianGrid vertical={false} stroke={CHART_COLORS.AXIS} />
<XAxis
dataKey={xKey}
tickLine={{ stroke: CHART_COLORS.AXIS }}
axisLine={{ stroke: CHART_COLORS.AXIS }}
interval="preserveStartEnd"
tickMargin={4}
minTickGap={32}
tickFormatter={(value) =>
xKeyDateFormat === 'date' ? dayjs(value).format('MMM D YYYY HH:mm') : value
}
/>
<YAxis tickLine={false} axisLine={false} tickMargin={4} />
<Tooltip content={<ChartTooltipContent className="w-[150px]" />} />
<Bar radius={1} dataKey={yKey}>
{chartData?.map((_: any, index: number) => (
<Cell
key={`cell-${index}`}
className="transition-all duration-100"
fill="hsl(var(--chart-1))"
opacity={focusDataIndex === undefined || focusDataIndex === index ? 1 : 0.4}
enableBackground={12}
/>
))}
</Bar>
</BarChart>
</ChartContainer>
</div>
)}
</>
) : (
<>
{isWriteQuery && blockWriteQueries ? (
<div className="flex flex-col h-full justify-center items-center text-center">
<p className="text-xs text-foreground-light">
SQL query is not read-only and cannot be rendered
</p>
<p className="text-xs text-foreground-lighter text-center">
Queries that involve any mutation will not be run in reports
</p>
{!!onRemoveChart && (
<Button type="default" className="mt-2" onClick={() => onRemoveChart()}>
Remove chart
</Button>
)}
</div>
) : !isExecuting && !!errorText ? (
<div className={cn('flex-1 w-full overflow-auto relative border-t px-3.5 py-2')}>
<span className="font-mono text-xs">ERROR: {errorText}</span>
</div>
) : (
results && (
<div className={cn('flex-1 w-full overflow-auto relative max-h-64')}>
<Results rows={results} />
</div>
)
)}
</>
)}
</ReportBlockContainer>
)
}