From b4b6b4348ff3fc06d4338abe91a0ccb56daf7fd3 Mon Sep 17 00:00:00 2001 From: Saxon Fletcher Date: Fri, 20 Dec 2024 14:34:09 +1000 Subject: [PATCH] update parameter behaviour --- .../interfaces/Reports/GridResize.tsx | 28 +-- .../interfaces/Reports/ParametersPopover.tsx | 167 ++++++++++++++ .../interfaces/Reports/ReportGridItem.tsx | 214 +++--------------- .../components/interfaces/Reports/Reports.tsx | 118 +++++----- .../interfaces/SQLEditor/SQLEditor.tsx | 25 +- .../SQLEditor/UtilityPanel/UtilityActions.tsx | 81 ++----- .../to-be-cleaned/Charts/ChartHandler.tsx | 6 +- apps/studio/lib/sql-parameters.ts | 112 +++++++++ 8 files changed, 401 insertions(+), 350 deletions(-) create mode 100644 apps/studio/components/interfaces/Reports/ParametersPopover.tsx create mode 100644 apps/studio/lib/sql-parameters.ts diff --git a/apps/studio/components/interfaces/Reports/GridResize.tsx b/apps/studio/components/interfaces/Reports/GridResize.tsx index 6c183ac652f..663dc9f0143 100644 --- a/apps/studio/components/interfaces/Reports/GridResize.tsx +++ b/apps/studio/components/interfaces/Reports/GridResize.tsx @@ -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 - onSetParameter: (params: ParameterMetadata[]) => void + parameterValues?: Record + 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([]) + const [collectedParams, setCollectedParams] = useState([]) 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} /> ))} diff --git a/apps/studio/components/interfaces/Reports/ParametersPopover.tsx b/apps/studio/components/interfaces/Reports/ParametersPopover.tsx new file mode 100644 index 00000000000..6ee157395e1 --- /dev/null +++ b/apps/studio/components/interfaces/Reports/ParametersPopover.tsx @@ -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 + onSubmit?: (parameters: Record) => void + onChange?: (parameters: Record) => void +} + +const ParametersPopover = ({ + parameters, + parameterValues = {}, + onSubmit, + onChange, +}: ParametersPopoverProps) => { + const [tempParameters, setTempParameters] = useState>( + 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 ( + { + if (date && date.to) { + handleParameterChange(param.name, date.to) + } else { + handleParameterChange(param.name, '') + } + }} + > + {currentValue ? format(new Date(currentValue), 'dd MMM') : 'Pick a date'} + + ) + } + + // Fallback to regular input if date is invalid + return ( + handleParameterChange(param.name, e.target.value)} + /> + ) + } + + if (param.type === 'enum' && param.possibleValues) { + return ( + handleParameterChange(param.name, value)} + > + + + + + {param.possibleValues.map((value) => ( + + {value} + + ))} + + + ) + } + + return ( + handleParameterChange(param.name, e.target.value)} + /> + ) + } + + return ( + + + + + )} + + + + ) +} + +export default ParametersPopover diff --git a/apps/studio/components/interfaces/Reports/ReportGridItem.tsx b/apps/studio/components/interfaces/Reports/ReportGridItem.tsx index 7813fb0856b..2b27a51619f 100644 --- a/apps/studio/components/interfaces/Reports/ReportGridItem.tsx +++ b/apps/studio/components/interfaces/Reports/ReportGridItem.tsx @@ -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 + parameterValues?: Record 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 = {} - 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 = {} - const uniqueParams = new Set() - - 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() const [queryResult, setQueryResult] = useState() - const [localParameters, setLocalParameters] = useState([]) - const [hasLocalChanges, setHasLocalChanges] = useState>({}) - const [tempParameters, setTempParameters] = useState([]) + const [parameterValues, setParameterValues] = useState>({}) + + 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 - ) + // Update handleParametersSubmit to work with Record type + const handleParametersSubmit = (newParameters: Record) => { + 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 = {} - 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 (
-
+

{item.label}

{queryResult && ( <> - {localParameters.length > 0 && ( - - - -
-
- - + {parameters.length > 0 && ( + )} @@ -491,26 +353,12 @@ const ReportGridItem = ({ } > - {/* { - if (getDateFormat(value) === 'date') { - return dayjs(value).format('MMM D YYYY') - } - return value - }} /> - value.toLocaleString()} - /> */} } /> diff --git a/apps/studio/components/interfaces/Reports/Reports.tsx b/apps/studio/components/interfaces/Reports/Reports.tsx index b28fd902e97..55da42ff443 100644 --- a/apps/studio/components/interfaces/Reports/Reports.tsx +++ b/apps/studio/components/interfaces/Reports/Reports.tsx @@ -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(undefined) const [startDate, setStartDate] = useState(null) const [endDate, setEndDate] = useState(null) const [hasEdits, setHasEdits] = useState(false) - const [parameters, setParameters] = useState>({}) - const [parameterMetadata, setParameterMetadata] = useState([]) - const [tempParameters, setTempParameters] = useState>({}) + const [parameterValues, setParameterValues] = useState>({}) + const [parameterMetadata, setParameterMetadata] = useState([]) 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) => { + 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 = {} + + // 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 } @@ -294,54 +333,11 @@ const Reports = () => {
)} */} {parameterMetadata.length > 0 && ( - - - - - -
-
- {parameterMetadata.map((param) => ( -
- - {param.name} - {param.occurrences > 1 && ( - - (used {param.occurrences} times) - - )} - - - setTempParameters((prev) => ({ - ...prev, - [param.name]: e.target.value, - })) - } - /> -
- ))} -
-
- -
-
-
-
+ )}
@@ -415,7 +411,7 @@ const Reports = () => { disableUpdate={!canUpdateReport} onRemoveChart={popChart} setEditableReport={handleSetConfig} - parameters={parameters} + parameterValues={parameterValues} onSetParameter={setParameterMetadata} /> )} diff --git a/apps/studio/components/interfaces/SQLEditor/SQLEditor.tsx b/apps/studio/components/interfaces/SQLEditor/SQLEditor.tsx index 157bf0b9e56..3faa4310b25 100644 --- a/apps/studio/components/interfaces/SQLEditor/SQLEditor.tsx +++ b/apps/studio/components/interfaces/SQLEditor/SQLEditor.tsx @@ -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) => { 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 = {} - 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) diff --git a/apps/studio/components/interfaces/SQLEditor/UtilityPanel/UtilityActions.tsx b/apps/studio/components/interfaces/SQLEditor/UtilityPanel/UtilityActions.tsx index 7b8b91283d7..8d5dfbfce17 100644 --- a/apps/studio/components/interfaces/SQLEditor/UtilityPanel/UtilityActions.tsx +++ b/apps/studio/components/interfaces/SQLEditor/UtilityPanel/UtilityActions.tsx @@ -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 | 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>({}) - 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 = {} - 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 = {} - - 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 (
@@ -259,53 +239,24 @@ const UtilityActions = ({
+ {hasParameters && ( + + )}
snapV2.resetResult(id)} /> - {hasParameters && ( - - - - - -
- {Object.entries(parameters).map(([name, value]) => ( -
- - - setParameters((prev) => ({ - ...prev, - [name]: e.target.value, - })) - } - /> -
- ))} -
-
-
- )}