mirror of
https://github.com/supabase/supabase.git
synced 2026-10-17 07:09:07 +03:00
update parameter behaviour
This commit is contained in:
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>
|
||||
|
||||
@@ -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
|
||||
}
|
||||
Reference in new issue
Block a user