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