mirror of https://github.com/grafana/grafana
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>pull/95794/head
parent
aacc83be5c
commit
85c696c4ad
@ -0,0 +1,22 @@ |
||||
--- |
||||
headless: true |
||||
labels: |
||||
products: |
||||
- enterprise |
||||
- oss |
||||
--- |
||||
|
||||
#### Macros |
||||
|
||||
You can enable macros support in the select clause to create time-series queries. |
||||
|
||||
{{< docs/experimental product="Macros support in visual query builder" featureFlag="`sqlQuerybuilderFunctionParameters`" >}} |
||||
|
||||
Use the **Data operations** drop-down to select a macro like `$__timeGroup` or `$__timeGroupAlias`. |
||||
Select a time column from the **Column** drop-down and a time interval from the **Interval** drop-down to create a time-series query. |
||||
|
||||
{{< figure src="/media/docs/grafana/data-sources/screenshot-sql-builder-time-series-query.png" class="docs-image--no-shadow" caption="SQL query builder time-series query" >}} |
||||
|
||||
You can also add custom value to the **Data operations**. |
||||
For example, a function that's not in the drop-down list. |
||||
This allows you to add any number of parameters. |
||||
@ -0,0 +1,23 @@ |
||||
import { PlaywrightTestArgs } from '@playwright/test'; |
||||
|
||||
import { PluginFixture } from '@grafana/plugin-e2e'; |
||||
|
||||
import { datasetResponse, fieldsResponse, tablesResponse } from './mocks/mysql.mocks'; |
||||
|
||||
export async function mockDataSourceRequest({ context, explorePage, selectors }: PlaywrightTestArgs & PluginFixture) { |
||||
await explorePage.datasource.set('gdev-mysql'); |
||||
await context.route(selectors.apis.DataSource.queryPattern, async (route, request) => { |
||||
const refId = request.postDataJSON().queries[0].refId; |
||||
if (/fields-.*/g.test(refId)) { |
||||
return route.fulfill({ json: fieldsResponse(refId), status: 200 }); |
||||
} |
||||
switch (refId) { |
||||
case 'tables': |
||||
return route.fulfill({ json: tablesResponse, status: 200 }); |
||||
case 'datasets': |
||||
return route.fulfill({ json: datasetResponse, status: 200 }); |
||||
default: |
||||
return route.continue(); |
||||
} |
||||
}); |
||||
} |
||||
@ -0,0 +1,47 @@ |
||||
import { selectors } from '@grafana/e2e-selectors'; |
||||
import { test, expect } from '@grafana/plugin-e2e'; |
||||
|
||||
import { normalTableName } from './mocks/mysql.mocks'; |
||||
import { mockDataSourceRequest } from './utils'; |
||||
|
||||
test.beforeEach(mockDataSourceRequest); |
||||
|
||||
test.use({ featureToggles: { sqlQuerybuilderFunctionParameters: true } }); |
||||
|
||||
test('visual query builder should handle macros', async ({ explorePage, page }) => { |
||||
await explorePage.getByGrafanaSelector(selectors.components.SQLQueryEditor.headerTableSelector).click(); |
||||
await page.getByText(normalTableName, { exact: true }).click(); |
||||
|
||||
// Open Data operations
|
||||
await explorePage.getByGrafanaSelector(selectors.components.SQLQueryEditor.selectAggregation).click(); |
||||
const select = page.getByLabel('Select options menu'); |
||||
await select.locator(page.getByText('$__timeGroupAlias')).click(); |
||||
|
||||
// Open column selector
|
||||
await explorePage.getByGrafanaSelector(selectors.components.SQLQueryEditor.selectFunctionParameter('Column')).click(); |
||||
await select.locator(page.getByText('createdAt')).click(); |
||||
|
||||
// Open Interval selector
|
||||
await explorePage |
||||
.getByGrafanaSelector(selectors.components.SQLQueryEditor.selectFunctionParameter('Interval')) |
||||
.click(); |
||||
await select.locator(page.getByText('$__interval')).click(); |
||||
|
||||
await page.getByRole('button', { name: 'Add column' }).click(); |
||||
|
||||
await explorePage.getByGrafanaSelector(selectors.components.SQLQueryEditor.selectAggregation).nth(1).click(); |
||||
await select.locator(page.getByText('AVG')).click(); |
||||
|
||||
await explorePage |
||||
.getByGrafanaSelector(selectors.components.SQLQueryEditor.selectFunctionParameter('Column')) |
||||
.nth(1) |
||||
.click(); |
||||
await select.locator(page.getByText('bigint')).click(); |
||||
|
||||
// Validate the query
|
||||
await expect( |
||||
explorePage.getByGrafanaSelector(selectors.components.CodeEditor.container).getByRole('textbox') |
||||
).toHaveValue( |
||||
`SELECT\n $__timeGroupAlias(createdAt, $__interval),\n AVG(\`bigint\`)\nFROM\n DataMaker.normalTable\nLIMIT\n 50` |
||||
); |
||||
}); |
||||
@ -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, |
||||
}, |
||||
], |
||||
}, |
||||
}); |
||||
}); |
||||
}); |
||||
}); |
||||
|
Loading…
Reference in new issue