import { css } from '@emotion/css'; import { useCallback, useEffect, useId, useState } from 'react'; import { SelectableValue } from '@grafana/data'; import { selectors } from '@grafana/e2e-selectors'; import { EditorField } from '@grafana/plugin-ui'; import { InlineLabel, Input, Select, Stack, useStyles2 } from '@grafana/ui'; import { QueryEditorExpressionType } from '../../expressions'; import { DB, SQLExpression, SQLQuery } from '../../types'; import { getColumnValue } from '../../utils/sql.utils'; import { SelectColumn } from './SelectColumn'; import { SelectCustomFunctionParameters } from './SelectCustomFunctionParameters'; interface Props { query: SQLQuery; onSqlChange: (sql: SQLExpression) => void; currentColumnIndex: number; db: DB; columns: Array>; } export function SelectFunctionParameters({ query, onSqlChange, currentColumnIndex, db, columns }: Props) { const selectInputId = useId(); const macroOrFunction = query.sql?.columns?.[currentColumnIndex]; const styles = useStyles2(getStyles); const func = db.functions().find((f) => f.name === macroOrFunction?.name); const [fieldsFromFunction, setFieldsFromFunction] = useState>>>([]); useEffect(() => { const getFieldsFromFunction = async () => { if (!func) { return; } const options: Array>> = []; for (const param of func.parameters ?? []) { if (param.options) { options.push(await param.options(query)); } else { options.push([]); } } setFieldsFromFunction(options); }; getFieldsFromFunction(); // It is fine to ignore the warning here and omit the query object // only table property is used in the query object and whenever table changes the component is re-rendered // eslint-disable-next-line react-hooks/exhaustive-deps }, [macroOrFunction?.name]); const onParameterChange = useCallback( (index: number, keepIndex?: boolean) => (s: string | undefined) => { const item = query.sql?.columns?.[currentColumnIndex]; if (!item) { return; } if (!item.parameters) { item.parameters = []; } if (item.parameters[index] === undefined) { item.parameters[index] = { type: QueryEditorExpressionType.FunctionParameter, name: s }; } else if (s == null && keepIndex) { // Remove value from index item.parameters = item.parameters.map((p, i) => (i === index ? { ...p, name: '' } : p)); // Remove the last empty parameter if (item.parameters[item.parameters.length - 1]?.name === '') { item.parameters = item.parameters.filter((p) => p.name !== ''); } } else if (s == null) { item.parameters = item.parameters.filter((_, i) => i !== index); } else { item.parameters = item.parameters.map((p, i) => (i === index ? { ...p, name: s } : p)); } const newSql: SQLExpression = { ...query.sql, columns: query.sql?.columns?.map((c, i) => (i === currentColumnIndex ? item : c)), }; onSqlChange(newSql); }, [currentColumnIndex, onSqlChange, query.sql] ); function renderParametersWithFunctions() { if (!func?.parameters) { return null; } return func?.parameters.map((funcParam, index) => { return ( <> {funcParam.options ? ( onParameterChange(index, true)(e.currentTarget.value)} value={macroOrFunction?.parameters![index]?.name} data-testid={selectors.components.SQLQueryEditor.selectInputParameter} /> )} {func.parameters!.length !== index + 1 && ,} ); }); } // This means that no function is selected, we render a column selector if (macroOrFunction?.name === undefined) { return ( onParameterChange(0)(s)} value={getColumnValue(macroOrFunction?.parameters?.[0])} /> ); } // If the function is not found, that means that it might be a custom value // we let the user add any number of parameters if (!func) { return ( ); } // Else we render the function parameters based on the provided settings return ( <> ( {renderParametersWithFunctions()} ) ); } const getStyles = () => { return { label: css({ padding: 0, margin: 0, width: 'unset', }), }; };