SQL: Add macro support in select case (#88514)

* Feat: timeGroup macro handling in VQB

* Add tests

* Add functions to SQL ds

* Fix lint errors

* Add feature toggle

* Add rendering based on object

* Fix lint

* Fix CI failures

* Fix tests

* Address review comments

* Add docs

* Fix JSX runtime warnings

* Remove docs part that mentions suggest more macros

* Update docs/sources/shared/datasources/sql-query-builder-macros.md

Co-authored-by: Jack Baldry <jack.baldry@grafana.com>

* Add smoke test for this feature

* lint

* Add supported macros to influx

* Add setupTests.ts to include in tsconfig.json

* Import jest-dom instead of setupTests.ts

---------

Co-authored-by: Jack Baldry <jack.baldry@grafana.com>
This commit is contained in:
Zoltán Bedi
2024-11-04 17:13:35 +01:00
committed by GitHub
co-authored by Jack Baldry
parent aacc83be5c
commit 85c696c4ad
39 changed files with 1103 additions and 139 deletions
@@ -1,30 +0,0 @@
import { SelectableValue, toOption } from '@grafana/data';
import { COMMON_AGGREGATE_FNS } from '../../constants';
import { QueryWithDefaults } from '../../defaults';
import { DB, SQLQuery } from '../../types';
import { useSqlChange } from '../../utils/useSqlChange';
import { SelectRow } from './SelectRow';
interface SQLSelectRowProps {
fields: SelectableValue[];
query: QueryWithDefaults;
onQueryChange: (query: SQLQuery) => void;
db: DB;
}
export function SQLSelectRow({ fields, query, onQueryChange, db }: SQLSelectRowProps) {
const { onSqlChange } = useSqlChange({ query, onQueryChange, db });
const functions = [...COMMON_AGGREGATE_FNS, ...(db.functions?.() || [])].map(toOption);
return (
<SelectRow
columns={fields}
sql={query.sql!}
format={query.format}
functions={functions}
onSqlChange={onSqlChange}
/>
);
}
@@ -0,0 +1,30 @@
import { useId } from 'react';
import { SelectableValue } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
import { EditorField } from '@grafana/experimental';
import { Select } from '@grafana/ui';
interface Props {
columns: Array<SelectableValue<string>>;
onParameterChange: (value?: string) => void;
value: SelectableValue<string> | null;
}
export function SelectColumn({ columns, onParameterChange, value }: Props) {
const selectInputId = useId();
return (
<EditorField label="Column" width={25}>
<Select
value={value}
data-testid={selectors.components.SQLQueryEditor.selectColumn}
inputId={selectInputId}
menuShouldPortal
options={[{ label: '*', value: '*' }, ...columns]}
allowCustomValue
onChange={(s) => onParameterChange(s.value)}
/>
</EditorField>
);
}
@@ -0,0 +1,137 @@
import { css } from '@emotion/css';
import { useCallback } from 'react';
import { SelectableValue } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
import { Button, InlineLabel, Input, Stack, useStyles2 } from '@grafana/ui';
import { QueryEditorExpressionType } from '../../expressions';
import { SQLExpression, SQLQuery } from '../../types';
import { getColumnValue } from '../../utils/sql.utils';
import { SelectColumn } from './SelectColumn';
interface Props {
columns: Array<SelectableValue<string>>;
query: SQLQuery;
onSqlChange: (sql: SQLExpression) => void;
onParameterChange: (index: number) => (value?: string) => void;
currentColumnIndex: number;
}
export function SelectCustomFunctionParameters({
columns,
query,
onSqlChange,
onParameterChange,
currentColumnIndex,
}: Props) {
const styles = useStyles2(getStyles);
const macroOrFunction = query.sql?.columns?.[currentColumnIndex];
const addParameter = useCallback(
(index: number) => {
const item = query.sql?.columns?.[index];
if (!item) {
return;
}
item.parameters = item.parameters
? [...item.parameters, { type: QueryEditorExpressionType.FunctionParameter, name: '' }]
: [];
const newSql: SQLExpression = {
...query.sql,
columns: query.sql?.columns?.map((c, i) => (i === index ? item : c)),
};
onSqlChange(newSql);
},
[onSqlChange, query.sql]
);
const removeParameter = useCallback(
(columnIndex: number, index: number) => {
const item = query.sql?.columns?.[columnIndex];
if (!item?.parameters) {
return;
}
item.parameters = item.parameters?.filter((_, i) => i !== index);
const newSql: SQLExpression = {
...query.sql,
columns: query.sql?.columns?.map((c, i) => (i === columnIndex ? item : c)),
};
onSqlChange(newSql);
},
[onSqlChange, query.sql]
);
function renderParameters(columnIndex: number) {
if (!macroOrFunction?.parameters || macroOrFunction.parameters.length <= 1) {
return null;
}
const paramComponents = macroOrFunction.parameters.map((param, index) => {
// Skip the first parameter as it is the column name
if (index === 0) {
return null;
}
return (
<Stack key={index} gap={2}>
<InlineLabel className={styles.label}>,</InlineLabel>
<Input
onChange={(e) => onParameterChange(index)(e.currentTarget.value)}
value={param.name}
aria-label={`Parameter ${index} for column ${columnIndex}`}
data-testid={selectors.components.SQLQueryEditor.selectInputParameter}
addonAfter={
<Button
title="Remove parameter"
type="button"
icon="times"
variant="secondary"
size="md"
onClick={() => removeParameter(columnIndex, index)}
/>
}
/>
</Stack>
);
});
return paramComponents;
}
return (
<>
<InlineLabel className={styles.label}>(</InlineLabel>
<SelectColumn
columns={columns}
onParameterChange={(s) => onParameterChange(0)(s)}
value={getColumnValue(macroOrFunction?.parameters?.[0])}
/>
{renderParameters(currentColumnIndex)}
<Button
type="button"
onClick={() => addParameter(currentColumnIndex)}
variant="secondary"
size="md"
icon="plus"
title="Add parameter"
/>
<InlineLabel className={styles.label}>)</InlineLabel>
</>
);
}
const getStyles = () => {
return {
label: css({
padding: 0,
margin: 0,
width: 'unset',
}),
};
};
@@ -0,0 +1,167 @@
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/experimental';
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<SelectableValue<string>>;
}
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<Array<Array<SelectableValue<string>>>>([]);
useEffect(() => {
const getFieldsFromFunction = async () => {
if (!func) {
return;
}
const options: Array<Array<SelectableValue<string>>> = [];
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 (
<Stack key={index} alignItems="flex-end" gap={2}>
<EditorField label={funcParam.name} width={25} optional={!funcParam.required}>
<>
{funcParam.options ? (
<Select
value={getColumnValue(macroOrFunction?.parameters![index])}
options={fieldsFromFunction?.[index]}
data-testid={selectors.components.SQLQueryEditor.selectFunctionParameter(funcParam.name)}
inputId={selectInputId}
menuShouldPortal
allowCustomValue
isClearable
onChange={(s) => onParameterChange(index, true)(s?.value)}
/>
) : (
<Input
onChange={(e) => onParameterChange(index, true)(e.currentTarget.value)}
value={macroOrFunction?.parameters![index]?.name}
data-testid={selectors.components.SQLQueryEditor.selectInputParameter}
/>
)}
</>
</EditorField>
{func.parameters!.length !== index + 1 && <InlineLabel className={styles.label}>,</InlineLabel>}
</Stack>
);
});
}
// This means that no function is selected, we render a column selector
if (macroOrFunction?.name === undefined) {
return (
<SelectColumn
columns={columns}
onParameterChange={(s) => 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 (
<SelectCustomFunctionParameters
query={query}
onSqlChange={onSqlChange}
currentColumnIndex={currentColumnIndex}
columns={columns}
onParameterChange={onParameterChange}
/>
);
}
// Else we render the function parameters based on the provided settings
return (
<>
<InlineLabel className={styles.label}>(</InlineLabel>
{renderParametersWithFunctions()}
<InlineLabel className={styles.label}>)</InlineLabel>
</>
);
}
const getStyles = () => {
return {
label: css({
padding: 0,
margin: 0,
width: 'unset',
}),
};
};
@@ -0,0 +1,307 @@
import '@testing-library/jest-dom';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { selectors } from '@grafana/e2e-selectors';
import { QueryEditorExpressionType } from '../../expressions';
import { SQLQuery } from '../../types';
import { buildMockDB } from '../SqlComponents.testHelpers';
import { SelectRow } from './SelectRow';
// Mock featureToggle sqlQuerybuilderFunctionParameters
jest.mock('@grafana/runtime', () => ({
config: {
featureToggles: {
sqlQuerybuilderFunctionParameters: true,
},
},
}));
describe('SelectRow', () => {
const query = Object.freeze<SQLQuery>({
refId: 'A',
rawSql: '',
sql: {
columns: [
{
name: '$__timeGroup',
parameters: [
{ name: 'createdAt', type: QueryEditorExpressionType.FunctionParameter },
{ name: '$__interval', type: QueryEditorExpressionType.FunctionParameter },
],
alias: 'time',
type: QueryEditorExpressionType.Function,
},
],
},
});
it('should show query passed as a prop', () => {
const onQueryChange = jest.fn();
render(<SelectRow onQueryChange={onQueryChange} query={query} columns={[]} db={buildMockDB()} />);
expect(screen.getByTestId(selectors.components.SQLQueryEditor.selectAggregation)).toHaveTextContent('$__timeGroup');
expect(screen.getByTestId(selectors.components.SQLQueryEditor.selectAlias)).toHaveTextContent('time');
expect(screen.getByTestId(selectors.components.SQLQueryEditor.selectColumn)).toHaveTextContent('createdAt');
expect(screen.getByTestId(selectors.components.SQLQueryEditor.selectInputParameter)).toHaveValue('$__interval');
});
describe('should handle multiple columns manipulations', () => {
it('adding column', () => {
const onQueryChange = jest.fn();
render(<SelectRow onQueryChange={onQueryChange} query={query} columns={[]} db={buildMockDB()} />);
screen.getByRole('button', { name: 'Add column' }).click();
expect(onQueryChange).toHaveBeenCalledWith({
...query,
sql: {
columns: [
...query.sql?.columns!,
{
name: undefined,
parameters: [],
type: QueryEditorExpressionType.Function,
},
],
},
});
});
it('show multiple columns when new column added', () => {
const onQueryChange = jest.fn();
render(
<SelectRow
columns={[]}
onQueryChange={onQueryChange}
db={buildMockDB()}
query={{
...query,
sql: {
...query.sql,
columns: [
...query.sql?.columns!,
{ name: undefined, parameters: [], type: QueryEditorExpressionType.Function },
],
},
}}
/>
);
// Check the first column values
expect(screen.getAllByTestId(selectors.components.SQLQueryEditor.selectAggregation)[0]).toHaveTextContent(
'$__timeGroup'
);
expect(screen.getAllByTestId(selectors.components.SQLQueryEditor.selectAlias)[0]).toHaveTextContent('time');
expect(screen.getAllByTestId(selectors.components.SQLQueryEditor.selectColumn)[0]).toHaveTextContent('createdAt');
expect(screen.getAllByTestId(selectors.components.SQLQueryEditor.selectInputParameter)[0]).toHaveValue(
'$__interval'
);
// Check the second column values
expect(
screen.getAllByTestId(selectors.components.SQLQueryEditor.selectAggregationInput)[1]
).toBeEmptyDOMElement();
expect(screen.getAllByTestId(selectors.components.SQLQueryEditor.selectAliasInput)[1]).toBeEmptyDOMElement();
expect(screen.getAllByTestId(selectors.components.SQLQueryEditor.selectColumnInput)[1]).toBeEmptyDOMElement();
expect(screen.queryAllByTestId(selectors.components.SQLQueryEditor.selectInputParameter)[1]).toBeFalsy();
});
it('removing column', () => {
const onQueryChange = jest.fn();
render(
<SelectRow
columns={[]}
db={buildMockDB()}
onQueryChange={onQueryChange}
query={{
...query,
sql: {
columns: [
...query.sql?.columns!,
{
name: undefined,
parameters: [],
type: QueryEditorExpressionType.Function,
},
],
},
}}
/>
);
screen.getAllByRole('button', { name: 'Remove column' })[1].click();
expect(onQueryChange).toHaveBeenCalledWith(query);
});
it('modifying second column aggregation', async () => {
const onQueryChange = jest.fn();
const db = buildMockDB();
db.functions = () => [{ name: 'AVG' }];
const multipleColumns = Object.freeze<SQLQuery>({
...query,
sql: {
columns: [
...query.sql?.columns!,
{
name: '',
parameters: [{ name: 'gaugeValue', type: QueryEditorExpressionType.FunctionParameter }],
type: QueryEditorExpressionType.Function,
},
],
},
});
render(<SelectRow columns={[]} db={db} onQueryChange={onQueryChange} query={multipleColumns} />);
await userEvent.click(screen.getAllByTestId(selectors.components.SQLQueryEditor.selectAggregation)[1]);
await userEvent.click(screen.getByText('AVG'));
expect(onQueryChange).toHaveBeenCalledWith({
...query,
sql: {
columns: [
...query.sql?.columns!,
{
name: 'AVG',
parameters: [{ name: 'gaugeValue', type: QueryEditorExpressionType.FunctionParameter }],
type: QueryEditorExpressionType.Function,
},
],
},
});
});
it('modifying second column name with custom value', async () => {
const onQueryChange = jest.fn();
const db = buildMockDB();
const multipleColumns = Object.freeze<SQLQuery>({
...query,
sql: {
columns: [
...query.sql?.columns!,
{
name: '',
parameters: [{ name: undefined, type: QueryEditorExpressionType.FunctionParameter }],
type: QueryEditorExpressionType.Function,
},
],
},
});
render(
<SelectRow
db={db}
columns={[{ label: 'newColumn', value: 'newColumn' }]}
onQueryChange={onQueryChange}
query={multipleColumns}
/>
);
await userEvent.click(screen.getAllByTestId(selectors.components.SQLQueryEditor.selectColumn)[1]);
await userEvent.type(
screen.getAllByTestId(selectors.components.SQLQueryEditor.selectColumnInput)[1],
'newColumn2{enter}'
);
expect(onQueryChange).toHaveBeenCalledWith({
...query,
sql: {
columns: [
...query.sql?.columns!,
{
name: '',
parameters: [{ name: 'newColumn2', type: QueryEditorExpressionType.FunctionParameter }],
type: QueryEditorExpressionType.Function,
},
],
},
});
});
it('handles second parameter', async () => {
const onQueryChange = jest.fn();
const db = buildMockDB();
const multipleColumns = Object.freeze<SQLQuery>({
...query,
sql: {
columns: [
...query.sql?.columns!,
{
name: '$__timeGroup',
parameters: [{ name: 'gaugeValue', type: QueryEditorExpressionType.FunctionParameter }],
type: QueryEditorExpressionType.Function,
},
],
},
});
render(
<SelectRow
db={db}
columns={[{ label: 'gaugeValue', value: 'gaugeValue' }]}
onQueryChange={onQueryChange}
query={multipleColumns}
/>
);
await userEvent.click(screen.getAllByRole('button', { name: 'Add parameter' })[1]);
expect(onQueryChange).toHaveBeenCalledWith({
...query,
sql: {
columns: [
...query.sql?.columns!,
{
name: '$__timeGroup',
parameters: [
{ name: 'gaugeValue', type: QueryEditorExpressionType.FunctionParameter },
{ name: '', type: QueryEditorExpressionType.FunctionParameter },
],
type: QueryEditorExpressionType.Function,
},
],
},
});
});
it('handles second parameter removal', () => {
const onQueryChange = jest.fn();
const db = buildMockDB();
render(
<SelectRow
onQueryChange={onQueryChange}
db={db}
columns={[]}
query={{
...query,
sql: {
columns: [
...query.sql?.columns!,
{
name: '$__timeGroup',
parameters: [
{ name: 'gaugeValue', type: QueryEditorExpressionType.FunctionParameter },
{ name: 'null', type: QueryEditorExpressionType.FunctionParameter },
],
type: QueryEditorExpressionType.Function,
},
],
},
}}
/>
);
screen.getAllByRole('button', { name: 'Remove parameter' })[1].click();
expect(onQueryChange).toHaveBeenCalledWith({
...query,
sql: {
columns: [
...query.sql?.columns!,
{
name: '$__timeGroup',
parameters: [{ name: 'gaugeValue', type: QueryEditorExpressionType.FunctionParameter }],
type: QueryEditorExpressionType.Function,
},
],
},
});
});
});
});
@@ -4,54 +4,56 @@ import { useCallback } from 'react';
import { SelectableValue, toOption } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
import { EditorField, Stack } from '@grafana/experimental';
import { Button, Select, useStyles2 } from '@grafana/ui';
import { EditorField } from '@grafana/experimental';
import { config } from '@grafana/runtime';
import { Button, Select, Stack, useStyles2 } from '@grafana/ui';
import { QueryEditorExpressionType, QueryEditorFunctionExpression } from '../../expressions';
import { SQLExpression, QueryFormat } from '../../types';
import { DB, QueryFormat, SQLExpression, SQLQuery } from '../../types';
import { createFunctionField } from '../../utils/sql.utils';
import { useSqlChange } from '../../utils/useSqlChange';
import { SelectColumn } from './SelectColumn';
import { SelectFunctionParameters } from './SelectFunctionParameters';
interface SelectRowProps {
sql: SQLExpression;
format: QueryFormat | undefined;
onSqlChange: (sql: SQLExpression) => void;
columns?: Array<SelectableValue<string>>;
functions?: Array<SelectableValue<string>>;
query: SQLQuery;
onQueryChange: (sql: SQLQuery) => void;
db: DB;
columns: Array<SelectableValue<string>>;
}
const asteriskValue = { label: '*', value: '*' };
export function SelectRow({ sql, format, columns, onSqlChange, functions }: SelectRowProps) {
export function SelectRow({ query, onQueryChange, db, columns }: SelectRowProps) {
const styles = useStyles2(getStyles);
const columnsWithAsterisk = [asteriskValue, ...(columns || [])];
const { onSqlChange } = useSqlChange({ query, onQueryChange, db });
const timeSeriesAliasOpts: Array<SelectableValue<string>> = [];
// Add necessary alias options for time series format
// when that format has been selected
if (format === QueryFormat.Timeseries) {
if (query.format === QueryFormat.Timeseries) {
timeSeriesAliasOpts.push({ label: 'time', value: 'time' });
timeSeriesAliasOpts.push({ label: 'value', value: 'value' });
}
const onColumnChange = useCallback(
(item: QueryEditorFunctionExpression, index: number) => (column: SelectableValue<string>) => {
(item: QueryEditorFunctionExpression, index: number) => (column?: string) => {
let modifiedItem = { ...item };
if (!item.parameters?.length) {
modifiedItem.parameters = [{ type: QueryEditorExpressionType.FunctionParameter, name: column.value } as const];
modifiedItem.parameters = [{ type: QueryEditorExpressionType.FunctionParameter, name: column } as const];
} else {
modifiedItem.parameters = item.parameters.map((p) =>
p.type === QueryEditorExpressionType.FunctionParameter ? { ...p, name: column.value } : p
p.type === QueryEditorExpressionType.FunctionParameter ? { ...p, name: column } : p
);
}
const newSql: SQLExpression = {
...sql,
columns: sql.columns?.map((c, i) => (i === index ? modifiedItem : c)),
...query.sql,
columns: query.sql?.columns?.map((c, i) => (i === index ? modifiedItem : c)),
};
onSqlChange(newSql);
},
[onSqlChange, sql]
[onSqlChange, query.sql]
);
const onAggregationChange = useCallback(
@@ -59,15 +61,18 @@ export function SelectRow({ sql, format, columns, onSqlChange, functions }: Sele
const newItem = {
...item,
name: aggregation?.value,
parameters: [
{ type: QueryEditorExpressionType.FunctionParameter as const, name: item.parameters?.[0]?.name || '' },
],
};
const newSql: SQLExpression = {
...sql,
columns: sql.columns?.map((c, i) => (i === index ? newItem : c)),
...query.sql,
columns: query.sql?.columns?.map((c, i) => (i === index ? newItem : c)),
};
onSqlChange(newSql);
},
[onSqlChange, sql]
[onSqlChange, query.sql]
);
const onAliasChange = useCallback(
@@ -81,51 +86,66 @@ export function SelectRow({ sql, format, columns, onSqlChange, functions }: Sele
}
const newSql: SQLExpression = {
...sql,
columns: sql.columns?.map((c, i) => (i === index ? newItem : c)),
...query.sql,
columns: query.sql?.columns?.map((c, i) => (i === index ? newItem : c)),
};
onSqlChange(newSql);
},
[onSqlChange, sql]
[onSqlChange, query.sql]
);
const removeColumn = useCallback(
(index: number) => () => {
const clone = [...sql.columns!];
const clone = [...(query.sql?.columns || [])];
clone.splice(index, 1);
const newSql: SQLExpression = {
...sql,
...query.sql,
columns: clone,
};
onSqlChange(newSql);
},
[onSqlChange, sql]
[onSqlChange, query.sql]
);
const addColumn = useCallback(() => {
const newSql: SQLExpression = { ...sql, columns: [...sql.columns!, createFunctionField()] };
const newSql: SQLExpression = { ...query.sql, columns: [...(query.sql?.columns || []), createFunctionField()] };
onSqlChange(newSql);
}, [onSqlChange, sql]);
}, [onSqlChange, query.sql]);
const aggregateOptions = () => {
const options: Array<SelectableValue<string>> = [
{ label: 'Aggregations', options: [] },
{ label: 'Macros', options: [] },
];
for (const func of db.functions()) {
// Create groups for macros
if (func.name.startsWith('$__')) {
options[1].options.push({ label: func.name, value: func.name });
} else {
options[0].options.push({ label: func.name, value: func.name });
}
}
return options;
};
return (
<Stack gap={2} wrap direction="column">
{sql.columns?.map((item, index) => (
<Stack gap={2} wrap="wrap" direction="column">
{query.sql?.columns?.map((item, index) => (
<div key={index}>
<Stack gap={2} alignItems="end">
<EditorField label="Column" width={25}>
<Select
{!config.featureToggles.sqlQuerybuilderFunctionParameters && (
<SelectColumn
columns={columns}
onParameterChange={(v) => onColumnChange(item, index)(v)}
value={getColumnValue(item)}
data-testid={selectors.components.SQLQueryEditor.selectColumn}
options={columnsWithAsterisk}
inputId={`select-column-${index}-${uniqueId()}`}
menuShouldPortal
allowCustomValue
onChange={onColumnChange(item, index)}
/>
</EditorField>
<EditorField label="Aggregation" optional width={25}>
)}
<EditorField
label={config.featureToggles.sqlQuerybuilderFunctionParameters ? 'Data operations' : 'Aggregation'}
optional
width={25}
>
<Select
value={item.name ? toOption(item.name) : null}
inputId={`select-aggregation-${index}-${uniqueId()}`}
@@ -133,10 +153,20 @@ export function SelectRow({ sql, format, columns, onSqlChange, functions }: Sele
isClearable
menuShouldPortal
allowCustomValue
options={functions}
options={aggregateOptions()}
onChange={onAggregationChange(item, index)}
/>
</EditorField>
{config.featureToggles.sqlQuerybuilderFunctionParameters && (
<SelectFunctionParameters
currentColumnIndex={index}
columns={columns}
onSqlChange={onSqlChange}
query={query}
db={db}
/>
)}
<EditorField label="Alias" optional width={15}>
<Select
value={item.alias ? toOption(item.alias) : null}
@@ -174,7 +204,14 @@ export function SelectRow({ sql, format, columns, onSqlChange, functions }: Sele
}
const getStyles = () => {
return { addButton: css({ alignSelf: 'flex-start' }) };
return {
addButton: css({ alignSelf: 'flex-start' }),
label: css({
padding: 0,
margin: 0,
width: 'unset',
}),
};
};
function getColumnValue({ parameters }: QueryEditorFunctionExpression): SelectableValue<string> | null {
@@ -8,8 +8,8 @@ import { QueryToolbox } from '../query-editor-raw/QueryToolbox';
import { Preview } from './Preview';
import { SQLGroupByRow } from './SQLGroupByRow';
import { SQLOrderByRow } from './SQLOrderByRow';
import { SQLSelectRow } from './SQLSelectRow';
import { SQLWhereRow } from './SQLWhereRow';
import { SelectRow } from './SelectRow';
interface VisualEditorProps extends QueryEditorProps {
db: DB;
@@ -27,7 +27,7 @@ export const VisualEditor = ({ query, db, queryRowFilter, onChange, onValidate,
<>
<EditorRows>
<EditorRow>
<SQLSelectRow fields={state.value || []} query={query} onQueryChange={onChange} db={db} />
<SelectRow columns={state.value || []} query={query} onQueryChange={onChange} db={db} />
</EditorRow>
{queryRowFilter.filter && (
<EditorRow>