update parameter behaviour

This commit is contained in:
Saxon Fletcher committed 2024-12-20 14:34:09 +10:00
1 parent ce27cd3c0f
commit b4b6b4348f
8 files changed
+401 -350

No files matched your search

@@ -28,10 +28,12 @@ import { useCallback, useState, useEffect } from 'react'
import BarChart from 'components/ui/Charts/BarChart'
import dayjs from 'dayjs'
import { ChartConfig } from '../SQLEditor/UtilityPanel/ChartConfig'
import ReportGridItem from './ReportGridItem'
import ReportGridItem from 'components/interfaces/Reports/ReportGridItem'
const ReactGridLayout = WidthProvider(Responsive)
import { Parameter } from 'lib/sql-parameters'
interface GridResizeProps {
startDate: string
endDate: string
@@ -40,8 +42,8 @@ interface GridResizeProps {
disableUpdate: boolean
onRemoveChart: ({ metric }: { metric: { key: string } }) => void
setEditableReport: (payload: any) => void
parameters?: Record<string, string>
onSetParameter: (params: ParameterMetadata[]) => void
parameterValues?: Record<string, string>
onSetParameter: (params: Parameter[]) => void
}
// Add chart config type
@@ -66,13 +68,6 @@ interface LayoutItem {
// ... other existing fields ...
}
interface ParameterMetadata {
name: string
value: string
defaultValue?: string
occurrences: number
}
const GridResize = ({
startDate,
endDate,
@@ -81,10 +76,10 @@ const GridResize = ({
disableUpdate,
onRemoveChart,
setEditableReport,
parameters,
parameterValues,
onSetParameter,
}: GridResizeProps) => {
const [collectedParams, setCollectedParams] = useState<ParameterMetadata[]>([])
const [collectedParams, setCollectedParams] = useState<Parameter[]>([])
function onLayoutChange(layout: any) {
let updatedLayout = [...editableReport.layout]
@@ -101,8 +96,8 @@ const GridResize = ({
})
}
// Merge parameters from all charts and notify parent once all are collected
const handleChartParameters = (params: ParameterMetadata[]) => {
// Merge parameters from all blocks and notify parent once all are collected
const handleBlockParameters = (params: Parameter[]) => {
setCollectedParams((prev) => {
const merged = [...prev]
params.forEach((param) => {
@@ -113,7 +108,6 @@ const GridResize = ({
})
return merged
})
console.log('collected params:', collectedParams)
}
// Notify parent when all parameters are collected
@@ -161,8 +155,8 @@ const GridResize = ({
interval={interval}
editableReport={editableReport}
setEditableReport={setEditableReport}
parameters={parameters}
onSetParameter={handleChartParameters}
parameterValues={parameterValues}
onSetParameter={handleBlockParameters}
/>
</div>
))}
@@ -0,0 +1,167 @@
import { ArrowUpDown } from 'lucide-react'
import { Parameter } from 'lib/sql-parameters'
import { useEffect, useState } from 'react'
import {
Button,
Popover_Shadcn_,
PopoverContent_Shadcn_,
PopoverTrigger_Shadcn_,
Label_Shadcn_,
Input_Shadcn_,
Select_Shadcn_,
SelectContent_Shadcn_,
SelectItem_Shadcn_,
SelectTrigger_Shadcn_,
SelectValue_Shadcn_,
Calendar,
Calendar as CalendarIcon,
} from 'ui'
import { DatePicker } from 'components/ui/DatePicker'
import { format } from 'date-fns'
interface ParametersPopoverProps {
parameters: Parameter[]
parameterValues?: Record<string, string>
onSubmit?: (parameters: Record<string, string>) => void
onChange?: (parameters: Record<string, string>) => void
}
const ParametersPopover = ({
parameters,
parameterValues = {},
onSubmit,
onChange,
}: ParametersPopoverProps) => {
const [tempParameters, setTempParameters] = useState<Record<string, string>>(
parameters.reduce(
(acc, param) => ({
...acc,
[param.name]: parameterValues[param.name] ?? param.defaultValue ?? '',
}),
{}
)
)
// Update temp parameters when external parameters or values change
useEffect(() => {
setTempParameters(
parameters.reduce(
(acc, param) => ({
...acc,
[param.name]: parameterValues[param.name] ?? param.defaultValue ?? '',
}),
{}
)
)
}, [parameters, parameterValues])
const handleParameterChange = (name: string, value: string) => {
const newParameters = {
...tempParameters,
[name]: value,
}
setTempParameters(newParameters)
onChange?.(newParameters)
}
const renderInput = (param: Parameter) => {
const currentValue = tempParameters[param.name] ?? parameterValues[param.name] ?? ''
if (param.type === 'date') {
// Check if the current value is a valid date
const isValidDate = currentValue && !isNaN(new Date(currentValue).getTime())
if (isValidDate) {
return (
<DatePicker
selectsRange={false}
from={currentValue}
to={currentValue}
onChange={(date) => {
if (date && date.to) {
handleParameterChange(param.name, date.to)
} else {
handleParameterChange(param.name, '')
}
}}
>
<span>{currentValue ? format(new Date(currentValue), 'dd MMM') : 'Pick a date'}</span>
</DatePicker>
)
}
// Fallback to regular input if date is invalid
return (
<Input_Shadcn_
size="tiny"
value={currentValue}
onChange={(e) => handleParameterChange(param.name, e.target.value)}
/>
)
}
if (param.type === 'enum' && param.possibleValues) {
return (
<Select_Shadcn_
value={currentValue}
onValueChange={(value) => handleParameterChange(param.name, value)}
>
<SelectTrigger_Shadcn_ className="h-8">
<SelectValue_Shadcn_ placeholder="Select value" />
</SelectTrigger_Shadcn_>
<SelectContent_Shadcn_>
{param.possibleValues.map((value) => (
<SelectItem_Shadcn_ key={value} value={value}>
{value}
</SelectItem_Shadcn_>
))}
</SelectContent_Shadcn_>
</Select_Shadcn_>
)
}
return (
<Input_Shadcn_
size="tiny"
value={currentValue}
onChange={(e) => handleParameterChange(param.name, e.target.value)}
/>
)
}
return (
<Popover_Shadcn_ modal={false}>
<PopoverTrigger_Shadcn_ asChild>
<Button icon={<ArrowUpDown size={14} />} type="outline" size="tiny" className="w-7 h-7" />
</PopoverTrigger_Shadcn_>
<PopoverContent_Shadcn_ side="bottom" align="end" className="w-[300px] p-4">
<div className="space-y-4">
<div className="space-y-2">
{parameters.map((param) => (
<div key={param.name} className="grid gap-2">
<Label_Shadcn_ className="flex items-center gap-2">
{param.name}
{param.occurrences > 1 && (
<span className="text-xs text-foreground-light">
(used {param.occurrences} times)
</span>
)}
</Label_Shadcn_>
{renderInput(param)}
</div>
))}
</div>
{onSubmit && (
<div className="flex justify-end">
<Button type="primary" size="tiny" onClick={() => onSubmit(tempParameters)}>
Apply changes
</Button>
</div>
)}
</div>
</PopoverContent_Shadcn_>
</Popover_Shadcn_>
)
}
export default ParametersPopover
@@ -1,6 +1,6 @@
import { useContentIdQuery } from 'data/content/content-id-query'
import { useParams } from 'common'
import { useEffect, useState, useCallback } from 'react'
import { useEffect, useState, useCallback, useMemo } from 'react'
import 'react-grid-layout/css/styles.css'
import 'react-resizable/css/styles.css'
import { X, Settings2, ArrowUpDown, Play } from 'lucide-react'
@@ -40,13 +40,9 @@ import { CartesianGrid, Bar, BarChart, XAxis, YAxis } from 'recharts'
import dayjs from 'dayjs'
import { ChartConfig } from '../SQLEditor/UtilityPanel/ChartConfig'
import { BarChart2, Table } from 'lucide-react'
// Add helper function for date formatting
const getDateFormat = (value: any) => {
if (typeof value === 'number') return 'number'
if (dayjs(value).isValid()) return 'date'
return 'string'
}
import { toast } from 'sonner'
import { parseParameters, processParameterizedSql, Parameter } from 'lib/sql-parameters'
import ParametersPopover from './ParametersPopover'
// Add helper function for cumulative results
const getCumulativeResults = (results: { rows: any[] }, config: ChartConfig) => {
@@ -87,13 +83,6 @@ interface LayoutItem {
// ... other existing fields ...
}
interface Parameter {
name: string
value: string
defaultValue?: string
occurrences: number
}
interface ReportGridItemProps {
item: LayoutItem
disableUpdate: boolean
@@ -103,43 +92,10 @@ interface ReportGridItemProps {
interval?: string
editableReport: any
setEditableReport: (payload: any) => void
parameters?: Record<string, string>
parameterValues?: Record<string, string>
onSetParameter?: (params: Parameter[]) => void
}
const parseParameters = (sql: string | undefined) => {
if (!sql) return []
// Parse @set parameter defaults
const setParamRegex = /@set\s+(\w+)\s*=\s*([^;\n]+)/g
const paramDefaults: Record<string, string> = {}
let match
while ((match = setParamRegex.exec(sql)) !== null) {
const [_, paramName, paramValue] = match
paramDefaults[paramName] = paramValue.trim()
}
// Find all {{parameter}} occurrences and count them
const paramRegex = /\{\{(\w+)\}\}/g
const paramOccurrences: Record<string, number> = {}
const uniqueParams = new Set<string>()
while ((match = paramRegex.exec(sql)) !== null) {
const [_, paramName] = match
paramOccurrences[paramName] = (paramOccurrences[paramName] || 0) + 1
uniqueParams.add(paramName)
}
// Create parameter objects for unique parameters
return Array.from(uniqueParams).map((paramName) => ({
name: paramName,
value: paramDefaults[paramName] || '',
defaultValue: paramDefaults[paramName],
occurrences: paramOccurrences[paramName],
}))
}
const ReportGridItem = ({
item,
disableUpdate,
@@ -149,7 +105,7 @@ const ReportGridItem = ({
interval,
editableReport,
setEditableReport,
parameters: externalParameters,
parameterValues: externalParameterValues,
onSetParameter,
}: ReportGridItemProps) => {
const { ref } = useParams()
@@ -158,9 +114,9 @@ const ReportGridItem = ({
const [sql, setSql] = useState<string>()
const [queryResult, setQueryResult] = useState<any>()
const [localParameters, setLocalParameters] = useState<Parameter[]>([])
const [hasLocalChanges, setHasLocalChanges] = useState<Record<string, boolean>>({})
const [tempParameters, setTempParameters] = useState<Parameter[]>([])
const [parameterValues, setParameterValues] = useState<Record<string, string>>({})
const combinedParameterValues = { ...externalParameterValues, ...parameterValues }
const { data: snippetData } = useContentIdQuery(
{ projectRef: ref, id: item.id },
@@ -209,48 +165,21 @@ const ReportGridItem = ({
// Run once on mount to parse parameters and notify parent
useEffect(() => {
if (!sql) return
const params = parseParameters(sql)
console.log('params', params, sql)
if (onSetParameter) {
console.log('parsed params:', params)
onSetParameter(params)
}
}, [sql])
// Handle parameter updates (external or local)
useEffect(() => {
if (!sql) return
const parameters = useMemo(() => {
if (!sql) return []
return parseParameters(sql)
}, [sql])
const params = parseParameters(sql)
const updatedParams = params.map((param) => ({
...param,
value: hasLocalChanges[param.name]
? localParameters.find((p) => p.name === param.name)?.value || param.value
: externalParameters?.[param.name] || param.value,
}))
setLocalParameters(updatedParams)
setTempParameters(updatedParams)
}, [sql, externalParameters, hasLocalChanges])
useEffect(() => {
handleExecute()
}, [localParameters])
const handleParameterChange = (paramName: string, value: string) => {
setHasLocalChanges((prev) => ({ ...prev, [paramName]: true }))
setLocalParameters((prev) => prev.map((p) => (p.name === paramName ? { ...p, value } : p)))
}
// Helper to get parameter values
const getParameterValues = () => {
return localParameters.reduce(
(acc, param) => {
acc[param.name] = param.value
return acc
},
{} as Record<string, string>
)
// Update handleParametersSubmit to work with Record type
const handleParametersSubmit = (newParameters: Record<string, string>) => {
setParameterValues(newParameters)
}
// Update execute call to include parameters
@@ -258,32 +187,7 @@ const ReportGridItem = ({
if (!sql) return
try {
let processedSql = sql
// Parse @set parameter defaults from SQL
const setParamRegex = /@set\s+(\w+)\s*=\s*([^;\n]+)/g
const paramDefaults: Record<string, string> = {}
let match
while ((match = setParamRegex.exec(sql)) !== null) {
const [_, paramName, paramValue] = match
paramDefaults[paramName] = paramValue.trim()
}
// Remove @set lines from SQL
processedSql = processedSql.replace(/@set\s+\w+\s*=\s*[^;\n]+[\n;]*/g, '')
// Replace {{parameters}} with values
const paramRegex = /\{\{(\w+)\}\}/g
processedSql = processedSql.replace(paramRegex, (match, paramName) => {
const value =
localParameters.find((p) => p.name === paramName)?.value ?? paramDefaults[paramName]
if (value === undefined) {
throw new Error(`Missing value for parameter: ${paramName}`)
}
return value
})
const processedSql = processParameterizedSql(sql, combinedParameterValues)
console.log('processedSql', processedSql)
execute({
@@ -296,68 +200,26 @@ const ReportGridItem = ({
}
}
// Add submit handler
const handleParametersSubmit = () => {
setHasLocalChanges(tempParameters.reduce((acc, param) => ({ ...acc, [param.name]: true }), {}))
setLocalParameters(tempParameters)
}
useEffect(() => {
handleExecute()
}, [sql, parameterValues, externalParameterValues])
console.log('localParameters', item.label, parameterValues)
if (item.isSnippet) {
return (
<div className="h-full flex flex-col">
<div className="flex justify-between px-5 py-4 items-center z-10 border-b">
<div className="flex justify-between px-5 py-2 items-center z-10 border-b">
<h3 className="text-sm text-foreground-light">{item.label}</h3>
<div className="flex gap-2">
{queryResult && (
<>
{localParameters.length > 0 && (
<Popover_Shadcn_ modal={false}>
<PopoverTrigger_Shadcn_ asChild>
<Button
icon={<ArrowUpDown size={14} />}
type="outline"
size="tiny"
className="w-7 h-7"
/>
</PopoverTrigger_Shadcn_>
<PopoverContent_Shadcn_ side="bottom" align="end" className="w-[300px] p-4">
<div className="space-y-4">
<div className="space-y-2">
{localParameters.map((param) => (
<div key={param.name} className="grid gap-2">
<Label_Shadcn_ className="flex items-center gap-2">
{param.name}
{param.occurrences > 1 && (
<span className="text-xs text-foreground-light">
(used {param.occurrences} times)
</span>
)}
</Label_Shadcn_>
<Input_Shadcn_
size="tiny"
value={
tempParameters.find((p) => p.name === param.name)?.value ??
param.value
}
onChange={(e) =>
setTempParameters((prev) =>
prev.map((p) =>
p.name === param.name ? { ...p, value: e.target.value } : p
)
)
}
/>
</div>
))}
</div>
<div className="flex justify-end">
<Button type="primary" size="tiny" onClick={handleParametersSubmit}>
Apply changes
</Button>
</div>
</div>
</PopoverContent_Shadcn_>
</Popover_Shadcn_>
{parameters.length > 0 && (
<ParametersPopover
parameters={parameters}
parameterValues={combinedParameterValues}
onSubmit={handleParametersSubmit}
/>
)}
<Popover_Shadcn_ modal={false}>
<PopoverTrigger_Shadcn_ asChild>
@@ -491,26 +353,12 @@ const ReportGridItem = ({
}
>
<CartesianGrid vertical={false} />
{/* <XAxis
<XAxis
dataKey={item.chartConfig?.xKey}
tickLine={false}
tickMargin={10}
axisLine={false}
hide={!item.chartConfig?.showLabels}
tickFormatter={(value) => {
if (getDateFormat(value) === 'date') {
return dayjs(value).format('MMM D YYYY')
}
return value
}}
/>
<YAxis
tickLine={false}
tickMargin={10}
axisLine={false}
hide={!item.chartConfig?.showLabels}
tickFormatter={(value) => value.toLocaleString()}
/> */}
<ChartTooltip content={<ChartTooltipContent />} />
<Bar dataKey={item.chartConfig?.yKey} fill="var(--chart-1)" radius={4} />
</BarChart>
@@ -3,6 +3,7 @@ import dayjs from 'dayjs'
import { groupBy, isNull } from 'lodash'
import { useEffect, useState } from 'react'
import { toast } from 'sonner'
import { useRouter } from 'next/router'
import { useParams } from 'common'
import DateRangePicker from 'components/to-be-cleaned/DateRangePicker'
@@ -31,28 +32,23 @@ import {
import GridResize from './GridResize'
import { MetricOptions } from './MetricOptions'
import { LAYOUT_COLUMN_COUNT } from './Reports.constants'
import { Parameter } from 'lib/sql-parameters'
import ParametersPopover from './ParametersPopover'
const DEFAULT_CHART_COLUMN_COUNT = 12
const DEFAULT_CHART_ROW_COUNT = 4
interface ParameterMetadata {
name: string
value: string
defaultValue?: string
occurrences: number
}
const Reports = () => {
const { id, ref } = useParams()
const router = useRouter()
const { profile } = useProfile()
const [config, setConfig] = useState<any>(undefined)
const [startDate, setStartDate] = useState<any>(null)
const [endDate, setEndDate] = useState<any>(null)
const [hasEdits, setHasEdits] = useState<any>(false)
const [parameters, setParameters] = useState<Record<string, string>>({})
const [parameterMetadata, setParameterMetadata] = useState<ParameterMetadata[]>([])
const [tempParameters, setTempParameters] = useState<Record<string, string>>({})
const [parameterValues, setParameterValues] = useState<Record<string, string>>({})
const [parameterMetadata, setParameterMetadata] = useState<Parameter[]>([])
const { data: userContents, isLoading } = useContentQuery(ref)
const { mutate: saveReport, isLoading: isSaving } = useContentUpdateMutation({
@@ -230,10 +226,53 @@ const Reports = () => {
checkEditState()
}
const handleParametersSubmit = () => {
setParameters(tempParameters)
const handleParametersSubmit = (parameters: Record<string, string>) => {
setParameterValues(parameters)
// Create new query object with existing query params
const newQuery = { ...router.query }
// Remove old parameter values
Object.keys(newQuery).forEach((key) => {
if (key.startsWith('param_')) {
delete newQuery[key]
}
})
// Add new parameter values
Object.entries(parameters).forEach(([key, value]) => {
newQuery[`param_${key}`] = value
})
// Update URL with new query params
router.replace(
{
pathname: router.pathname,
query: newQuery,
},
undefined,
{ shallow: true }
)
}
useEffect(() => {
if (router.isReady) {
const paramValues: Record<string, string> = {}
// Extract parameter values from router.query
Object.entries(router.query).forEach(([key, value]) => {
if (key.startsWith('param_') && typeof value === 'string') {
paramValues[key.replace('param_', '')] = value
}
})
// Only set if there are parameters in query
if (Object.keys(paramValues).length > 0) {
setParameterValues(paramValues)
}
}
}, [router.isReady])
if (isLoading) {
return <Loading />
}
@@ -294,54 +333,11 @@ const Reports = () => {
</div>
)} */}
{parameterMetadata.length > 0 && (
<Popover_Shadcn_ modal={false}>
<PopoverTrigger_Shadcn_ asChild>
<Button type="default" icon={<ArrowUpDown size={14} />}>
<div className="flex items-center gap-1">
<span className="text-foreground-muted">Parameters</span>
<span>{parameterMetadata.length}</span>
</div>
</Button>
</PopoverTrigger_Shadcn_>
<PopoverContent_Shadcn_ side="bottom" align="start" className="w-[300px] p-4">
<div className="space-y-4">
<div className="space-y-2">
{parameterMetadata.map((param) => (
<div key={param.name} className="grid gap-2">
<Label_Shadcn_ className="flex items-center gap-2">
{param.name}
{param.occurrences > 1 && (
<span className="text-xs text-foreground-light">
(used {param.occurrences} times)
</span>
)}
</Label_Shadcn_>
<Input_Shadcn_
size="tiny"
value={
tempParameters[param.name] ||
parameters[param.name] ||
param.defaultValue ||
''
}
onChange={(e) =>
setTempParameters((prev) => ({
...prev,
[param.name]: e.target.value,
}))
}
/>
</div>
))}
</div>
<div className="flex justify-end">
<Button type="primary" size="tiny" onClick={handleParametersSubmit}>
Apply changes
</Button>
</div>
</div>
</PopoverContent_Shadcn_>
</Popover_Shadcn_>
<ParametersPopover
parameters={parameterMetadata}
parameterValues={parameterValues}
onSubmit={handleParametersSubmit}
/>
)}
</div>
@@ -415,7 +411,7 @@ const Reports = () => {
disableUpdate={!canUpdateReport}
onRemoveChart={popChart}
setEditableReport={handleSetConfig}
parameters={parameters}
parameterValues={parameterValues}
onSetParameter={setParameterMetadata}
/>
)}
@@ -69,6 +69,7 @@ import {
suffixWithLimit,
} from './SQLEditor.utils'
import UtilityPanel from './UtilityPanel/UtilityPanel'
import { parseParameters, processParameterizedSql, Parameter } from 'lib/sql-parameters'
// Load the monaco editor client-side only (does not behave well server-side)
const MonacoEditor = dynamic(() => import('./MonacoEditor'), { ssr: false })
@@ -254,7 +255,7 @@ export const SQLEditor = () => {
}, [formatQuery, id, isDiffOpen, project, snapV2])
const executeQuery = useCallback(
async (force: boolean = false, parameters: any) => {
async (force: boolean = false, parameterValues?: Record<string, string>) => {
if (isDiffOpen) return
// use the latest state
@@ -270,28 +271,10 @@ export const SQLEditor = () => {
? (selectedValue || editorRef.current?.getValue()) ?? snippet.snippet.content.sql
: selectedValue || editorRef.current?.getValue()
// Parse @set parameter defaults from SQL
const setParamRegex = /@set\s+(\w+)\s*=\s*([^;\n]+)/g
const paramDefaults: Record<string, string> = {}
let match
while ((match = setParamRegex.exec(sql)) !== null) {
const [_, paramName, paramValue] = match
paramDefaults[paramName] = paramValue.trim()
if (parameterValues) {
sql = processParameterizedSql(sql, parameterValues)
}
// Remove @set lines from SQL
sql = sql.replace(/@set\s+\w+\s*=\s*[^;\n]+[\n;]*/g, '')
// Replace {{parameters}} with values
const paramRegex = /\{\{(\w+)\}\}/g
sql = sql.replace(paramRegex, (match, paramName) => {
const value = parameters?.[paramName] ?? paramDefaults[paramName]
if (value === undefined) {
throw new Error(`Missing value for parameter: ${paramName}`)
}
return value
})
let queryHasIssues = false
const destructiveOperations = checkDestructiveQuery(sql)
@@ -40,6 +40,8 @@ import {
} from 'ui'
import SavingIndicator from './SavingIndicator'
import { Label } from '@ui/components/shadcn/ui/label'
import { parseParameters, processParameterizedSql, Parameter } from 'lib/sql-parameters'
import ParametersPopover from 'components/interfaces/Reports/ParametersPopover'
export type UtilityActionsProps = {
id: string
@@ -63,42 +65,20 @@ const UtilityActions = ({
const { project } = useProjectContext()
const snapV2 = useSqlEditorV2StateSnapshot()
const [parameterValues, setParameterValues] = useState<Record<string, string> | undefined>({})
const [isAiOpen] = useLocalStorageQuery(LOCAL_STORAGE_KEYS.SQL_EDITOR_AI_OPEN, true)
const [intellisenseEnabled, setIntellisenseEnabled] = useLocalStorageQuery(
LOCAL_STORAGE_KEYS.SQL_EDITOR_INTELLISENSE,
true
)
const [parameters, setParameters] = useState<Record<string, string>>({})
const snippet = snapV2.snippets[id]
const isFavorite = snippet !== undefined ? snippet.snippet.favorite : false
useEffect(() => {
if (!snippet) return
const sql = snippet.snippet.content.sql
// Parse @set parameter defaults
const setParamRegex = /@set\s+(\w+)\s*=\s*([^;\n]+)/g
const paramDefaults: Record<string, string> = {}
let match
while ((match = setParamRegex.exec(sql)) !== null) {
const [_, paramName, paramValue] = match
paramDefaults[paramName] = paramValue.trim()
}
// Find all {{parameter}} occurrences
const paramRegex = /\{\{(\w+)\}\}/g
const params: Record<string, string> = {}
while ((match = paramRegex.exec(sql)) !== null) {
const [_, paramName] = match
params[paramName] = paramDefaults[paramName] || ''
}
setParameters(params)
}, [snippet])
const parameters = snippet?.snippet?.content?.sql
? parseParameters(snippet.snippet.content.sql)
: undefined
const toggleIntellisense = () => {
setIntellisenseEnabled(!intellisenseEnabled)
@@ -155,7 +135,7 @@ const UtilityActions = ({
)
}
const hasParameters = Object.keys(parameters).length > 0
const hasParameters = parameters && Object.keys(parameters).length > 0
return (
<div className="inline-flex items-center justify-end gap-x-2">
@@ -259,53 +239,24 @@ const UtilityActions = ({
</div>
<div className="flex items-center justify-between gap-x-2">
{hasParameters && (
<ParametersPopover
parameters={parameters}
parameterValues={parameterValues}
onChange={setParameterValues}
/>
)}
<div className="flex items-center">
<DatabaseSelector
variant="connected-on-right"
onSelectId={() => snapV2.resetResult(id)}
/>
{hasParameters && (
<Popover_Shadcn_ modal={false}>
<PopoverTrigger_Shadcn_ asChild>
<Button
size="tiny"
type="default"
className="h-[26px] pr-3 gap-0 rounded-none border-x-0"
>
<div className="flex items-center gap-1">
<span className="text-foreground-muted">Params</span>
<span>{Object.keys(parameters).length}</span>
<ChevronDown className="text-muted" strokeWidth={1} size={12} />
</div>
</Button>
</PopoverTrigger_Shadcn_>
<PopoverContent_Shadcn_ className="w-[300px]" side="bottom" align="end">
<div className="space-y-2">
{Object.entries(parameters).map(([name, value]) => (
<div key={name}>
<Label className="text-xs mb-2 block">{name}</Label>
<Input_Shadcn_
size="tiny"
value={value}
onChange={(e) =>
setParameters((prev) => ({
...prev,
[name]: e.target.value,
}))
}
/>
</div>
))}
</div>
</PopoverContent_Shadcn_>
</Popover_Shadcn_>
)}
<RoleImpersonationPopover
serviceRoleLabel="postgres"
variant={hasParameters ? 'connected-on-both' : 'connected-on-both'}
/>
<Button
onClick={() => executeQuery(false, parameters)}
onClick={() => executeQuery(false, parameterValues)}
disabled={isDisabled || isExecuting}
type="primary"
size="tiny"
@@ -113,7 +113,7 @@ const ChartHandler = ({
'maximum' in chartData
const shouldHighlightTotalGroupedValue = chartData !== undefined && 'totalGrouped' in chartData
console.log('chart data:', chartData)
console.log('chart data:', label, chartData)
const _highlightedValue =
highlightedValue !== undefined
@@ -150,9 +150,9 @@ const ChartHandler = ({
<div>
<h3 className="text-sm text-foreground-light">{label}</h3>
<div>
{chartData?.format === '%'
{/* {chartData?.format === '%'
? `${_highlightedValue.toFixed(1)}%`
: _highlightedValue?.toLocaleString()}
: _highlightedValue?.toLocaleString()} */}
</div>
</div>
<div className="flex gap-2">{children}</div>
+112
View File
@@ -0,0 +1,112 @@
export interface Parameter {
name: string
value: string
defaultValue?: string
type?: string
possibleValues?: string[]
occurrences: number
}
export const parseParameters = (sql: string | undefined) => {
if (!sql) return []
// Parse @set parameter defaults with optional type information
const setParamRegex = /@set\s+(\w+)(?::([^=]+))?\s*=\s*([^;\n]+)/g
const paramDefaults: Record<string, { value: string; type?: string; possibleValues?: string[] }> =
{}
let match
while ((match = setParamRegex.exec(sql)) !== null) {
const [_, paramName, paramType, paramValue] = match
if (!paramName || !paramValue?.trim()) continue
const typeInfo = paramType?.trim()
let type: string | undefined
let possibleValues: string[] | undefined
if (typeInfo) {
// Handle union types (value1 | value2 | value3)
if (typeInfo.includes('|')) {
possibleValues = typeInfo.split('|').map((v) => v.trim())
type = 'enum'
} else {
type = typeInfo.trim()
}
}
paramDefaults[paramName] = {
value: paramValue.trim(),
type,
possibleValues,
}
}
// Find all :parameter occurrences and count them
const paramRegex = /:(\w+)/g
const paramOccurrences: Record<string, number> = {}
const uniqueParams = new Set<string>()
while ((match = paramRegex.exec(sql)) !== null) {
const [_, paramName] = match
paramOccurrences[paramName] = (paramOccurrences[paramName] || 0) + 1
uniqueParams.add(paramName)
}
// Create parameter objects for unique parameters
return Array.from(uniqueParams).map((paramName) => ({
name: paramName,
value: paramDefaults[paramName]?.value || '',
defaultValue: paramDefaults[paramName]?.value,
type: paramDefaults[paramName]?.type,
possibleValues: paramDefaults[paramName]?.possibleValues,
occurrences: paramOccurrences[paramName],
}))
}
export const processParameterizedSql = (sql: string, parameters: Record<string, string>) => {
// Parse @set parameter defaults with type information from SQL
const setParamRegex = /@set\s+(\w+)(?::([^=]+))?\s*=\s*([^;\n]+)/g
const paramDefaults: Record<string, { value: string; type?: string; possibleValues?: string[] }> =
{}
let match
while ((match = setParamRegex.exec(sql)) !== null) {
const [_, paramName, paramType, paramValue] = match
if (!paramName || !paramValue?.trim()) continue
const typeInfo = paramType?.trim()
let type: string | undefined
let possibleValues: string[] | undefined
if (typeInfo) {
if (typeInfo.includes('|')) {
possibleValues = typeInfo.split('|').map((v) => v.trim())
type = 'enum'
} else {
type = typeInfo.trim()
}
}
paramDefaults[paramName] = {
value: paramValue.trim(),
type,
possibleValues,
}
}
// Remove @set lines from SQL
let processedSql = sql.replace(/@set\s+\w+(?:\s*:\s*[^=]+)?\s*=\s*[^;\n]+[\n;]*/g, '')
// Replace :parameters with values
const paramRegex = /:(\w+)/g
processedSql = processedSql.replace(paramRegex, (match, paramName) => {
const value = parameters[paramName] ?? paramDefaults[paramName]?.value
if (value === undefined) {
throw new Error(`Missing value for parameter: ${paramName}`)
}
return value
})
return processedSql
}