mirror of
https://github.com/grafana/grafana.git
synced 2024-12-25 08:21:46 -06:00
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:
parent
aacc83be5c
commit
85c696c4ad
@ -129,6 +129,8 @@ You can select an optional aggregation function for the column in the **Aggregat
|
||||
|
||||
To add more value columns, click the plus (`+`) button to the right of the column's row.
|
||||
|
||||
{{< docs/shared source="grafana" lookup="datasources/sql-query-builder-macros.md" version="<GRAFANA_VERSION>" >}}
|
||||
|
||||
### Filter data (WHERE)
|
||||
|
||||
To add a filter, toggle the **Filter** switch at the top of the editor.
|
||||
@ -180,8 +182,6 @@ To simplify syntax and to allow for dynamic components, such as date range filte
|
||||
| `$__unixEpochGroup(dateColumn,'5m', [fillmode])` | Same as `$__timeGroup` but for times stored as Unix timestamp. |
|
||||
| `$__unixEpochGroupAlias(dateColumn,'5m', [fillmode])` | Same as above but also adds a column alias. |
|
||||
|
||||
To suggest more macros, please [open an issue](https://github.com/grafana/grafana) in our GitHub repo.
|
||||
|
||||
### View the interpolated query
|
||||
|
||||
The query editor also includes a link named **Generated SQL** that appears after running a query while in panel edit mode.
|
||||
|
@ -247,6 +247,8 @@ Using the dropdown, select a column to include in the data. You can also specify
|
||||
|
||||
Add further value columns by clicking the plus button and another column dropdown appears.
|
||||
|
||||
{{< docs/shared source="grafana" lookup="datasources/sql-query-builder-macros.md" version="<GRAFANA_VERSION>" >}}
|
||||
|
||||
### Filter data (WHERE)
|
||||
|
||||
To add a filter, toggle the **Filter** switch at the top of the editor.
|
||||
@ -304,10 +306,6 @@ To simplify syntax and to allow for dynamic parts, like date range filters, the
|
||||
| `$__unixEpochGroup(dateColumn,'5m', [fillmode])` | Same as $\_\_timeGroup but for times stored as Unix timestamp (`fillMode` only works with time series queries). |
|
||||
| `$__unixEpochGroupAlias(dateColumn,'5m', [fillmode])` | Same as above but also adds a column alias (`fillMode` only works with time series queries). |
|
||||
|
||||
We plan to add many more macros. If you have suggestions for what macros you would like to see, please [open an issue](https://github.com/grafana/grafana) in our GitHub repo.
|
||||
|
||||
The query editor has a link named `Generated SQL` that shows up after a query has been executed, while in panel edit mode. Click on it and it will expand and show the raw interpolated SQL string that was executed.
|
||||
|
||||
## Table queries
|
||||
|
||||
If the `Format as` query option is set to `Table` then you can basically do any type of SQL query. The table panel will automatically show the results of whatever columns and rows your query returns.
|
||||
|
@ -155,6 +155,8 @@ Using the dropdown, select a column to include in the data. You can also specify
|
||||
|
||||
Add further value columns by clicking the plus button and another column dropdown appears.
|
||||
|
||||
{{< docs/shared source="grafana" lookup="datasources/sql-query-builder-macros.md" version="<GRAFANA_VERSION>" >}}
|
||||
|
||||
### Filter data (WHERE)
|
||||
|
||||
To add a filter, toggle the **Filter** switch at the top of the editor.
|
||||
@ -250,8 +252,6 @@ Macros can be used within a query to simplify syntax and allow for dynamic parts
|
||||
| `$__unixEpochGroup(dateColumn,'5m', [fillmode])` | Same as $\_\_timeGroup but for times stored as Unix timestamp (`fillMode` only works with time series queries). |
|
||||
| `$__unixEpochGroupAlias(dateColumn,'5m', [fillmode])` | Same as above but also adds a column alias (`fillMode` only works with time series queries). |
|
||||
|
||||
We plan to add many more macros. If you have suggestions for what macros you would like to see, please [open an issue](https://github.com/grafana/grafana) in our GitHub repo.
|
||||
|
||||
## Table queries
|
||||
|
||||
If the `Format as` query option is set to `Table` then you can basically do any type of SQL query. The table panel will automatically show the results of whatever columns and rows your query returns.
|
||||
|
@ -196,6 +196,7 @@ Experimental features might be changed or removed without prior notice.
|
||||
| `alertingListViewV2` | Enables the new alert list view design |
|
||||
| `dashboardRestore` | Enables deleted dashboard restore feature |
|
||||
| `alertingCentralAlertHistory` | Enables the new central alert history. |
|
||||
| `sqlQuerybuilderFunctionParameters` | Enables SQL query builder function parameters |
|
||||
| `failWrongDSUID` | Throws an error if a datasource has an invalid UIDs |
|
||||
| `alertingApiServer` | Register Alerting APIs with the K8s API server |
|
||||
| `dataplaneAggregator` | Enable grafana dataplane aggregator |
|
||||
|
22
docs/sources/shared/datasources/sql-query-builder-macros.md
Normal file
22
docs/sources/shared/datasources/sql-query-builder-macros.md
Normal file
@ -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.
|
@ -21,14 +21,14 @@ export const tablesResponse = {
|
||||
},
|
||||
};
|
||||
|
||||
export const fieldsResponse = {
|
||||
export const fieldsResponse = (refId: string) => ({
|
||||
results: {
|
||||
fields: {
|
||||
[refId]: {
|
||||
status: 200,
|
||||
frames: [
|
||||
{
|
||||
schema: {
|
||||
refId: 'fields',
|
||||
refId,
|
||||
meta: {
|
||||
executedQueryString:
|
||||
"SELECT column_name, data_type FROM information_schema.columns WHERE table_schema = 'DataMaker' AND table_name = 'RandomIntsWithTimes' ORDER BY column_name",
|
||||
@ -48,7 +48,7 @@ export const fieldsResponse = {
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
export const datasetResponse = {
|
||||
results: {
|
||||
|
@ -1,29 +1,10 @@
|
||||
import { selectors } from '@grafana/e2e-selectors';
|
||||
import { expect, test } from '@grafana/plugin-e2e';
|
||||
|
||||
import {
|
||||
tablesResponse,
|
||||
fieldsResponse,
|
||||
datasetResponse,
|
||||
normalTableName,
|
||||
tableNameWithSpecialCharacter,
|
||||
} from './mocks/mysql.mocks';
|
||||
import { normalTableName, tableNameWithSpecialCharacter } from './mocks/mysql.mocks';
|
||||
import { mockDataSourceRequest } from './utils';
|
||||
|
||||
test.beforeEach(async ({ context, selectors, explorePage }) => {
|
||||
await explorePage.datasource.set('gdev-mysql');
|
||||
await context.route(selectors.apis.DataSource.queryPattern, async (route, request) => {
|
||||
switch (request.postDataJSON().queries[0].refId) {
|
||||
case 'tables':
|
||||
return route.fulfill({ json: tablesResponse, status: 200 });
|
||||
case 'fields':
|
||||
return route.fulfill({ json: fieldsResponse, status: 200 });
|
||||
case 'datasets':
|
||||
return route.fulfill({ json: datasetResponse, status: 200 });
|
||||
default:
|
||||
return route.continue();
|
||||
}
|
||||
});
|
||||
});
|
||||
test.beforeEach(mockDataSourceRequest);
|
||||
|
||||
test('code editor autocomplete should handle table name escaping/quoting', async ({ explorePage, selectors, page }) => {
|
||||
await page.getByLabel('Code').check();
|
||||
|
23
e2e/plugin-e2e/mysql/utils.ts
Normal file
23
e2e/plugin-e2e/mysql/utils.ts
Normal file
@ -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();
|
||||
}
|
||||
});
|
||||
}
|
47
e2e/plugin-e2e/mysql/visual-query-builder.spec.ts
Normal file
47
e2e/plugin-e2e/mysql/visual-query-builder.spec.ts
Normal file
@ -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`
|
||||
);
|
||||
});
|
@ -190,6 +190,7 @@ export interface FeatureToggles {
|
||||
preserveDashboardStateWhenNavigating?: boolean;
|
||||
alertingCentralAlertHistory?: boolean;
|
||||
pluginProxyPreserveTrailingSlash?: boolean;
|
||||
sqlQuerybuilderFunctionParameters?: boolean;
|
||||
azureMonitorPrometheusExemplars?: boolean;
|
||||
pinNavItems?: boolean;
|
||||
authZGRPCServer?: boolean;
|
||||
|
@ -1191,12 +1191,17 @@ export const versionedComponents = {
|
||||
selectColumn: {
|
||||
'11.0.0': 'data-testid select-column',
|
||||
},
|
||||
selectColumnInput: { '11.0.0': 'data-testid select-column-input' },
|
||||
selectFunctionParameter: { '11.0.0': (name: string) => `data-testid select-function-parameter-${name}` },
|
||||
selectAggregation: {
|
||||
'11.0.0': 'data-testid select-aggregation',
|
||||
},
|
||||
selectAggregationInput: { '11.0.0': 'data-testid select-aggregation-input' },
|
||||
selectAlias: {
|
||||
'11.0.0': 'data-testid select-alias',
|
||||
},
|
||||
selectAliasInput: { '11.0.0': 'data-testid select-alias-input' },
|
||||
selectInputParameter: { '11.0.0': 'data-testid select-input-parameter' },
|
||||
filterConjunction: {
|
||||
'11.0.0': 'data-testid filter-conjunction',
|
||||
},
|
||||
|
@ -5,7 +5,7 @@ import { DB, SQLQuery, SQLSelectableValue, ValidationResults } from '../types';
|
||||
import { DatasetSelectorProps } from './DatasetSelector';
|
||||
import { TableSelectorProps } from './TableSelector';
|
||||
|
||||
const buildMockDB = (): DB => ({
|
||||
export const buildMockDB = (): DB => ({
|
||||
datasets: jest.fn(() => Promise.resolve(['dataset1', 'dataset2'])),
|
||||
tables: jest.fn((_ds: string | undefined) => Promise.resolve(['table1', 'table2'])),
|
||||
fields: jest.fn((_query: SQLQuery, _order?: boolean) => Promise.resolve<SQLSelectableValue[]>([])),
|
||||
@ -13,6 +13,7 @@ const buildMockDB = (): DB => ({
|
||||
Promise.resolve<ValidationResults>({ query: { refId: '123' }, error: '', isError: false, isValid: true })
|
||||
),
|
||||
dsID: jest.fn(() => 1234),
|
||||
functions: jest.fn(() => []),
|
||||
getEditorLanguageDefinition: jest.fn(() => ({ id: '4567' })),
|
||||
toRawSql: (_query: SQLQuery) => '',
|
||||
});
|
||||
|
@ -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>
|
||||
|
@ -1,4 +1,60 @@
|
||||
export const COMMON_AGGREGATE_FNS = ['AVG', 'COUNT', 'MAX', 'MIN', 'SUM'];
|
||||
import { Func, FuncParameter } from './types';
|
||||
|
||||
export const COMMON_FNS: Func[] = [
|
||||
{ name: 'AVG' },
|
||||
{ name: 'COUNT' },
|
||||
{ name: 'MAX' },
|
||||
{ name: 'MIN' },
|
||||
{ name: 'SUM' },
|
||||
];
|
||||
|
||||
const intervalParam: FuncParameter = {
|
||||
name: 'Interval',
|
||||
required: true,
|
||||
options: () => {
|
||||
return Promise.resolve([{ label: '$__interval', value: '$__interval' }]);
|
||||
},
|
||||
};
|
||||
const fillParam: FuncParameter = {
|
||||
name: 'Fill',
|
||||
required: false,
|
||||
options: () =>
|
||||
Promise.resolve([
|
||||
{ label: '0', value: '0' },
|
||||
{ label: 'NULL', value: 'NULL' },
|
||||
{ label: 'previous', value: 'previous' },
|
||||
]),
|
||||
};
|
||||
|
||||
export const MACRO_FUNCTIONS = (columnParam: FuncParameter) => [
|
||||
{
|
||||
name: '$__timeGroup',
|
||||
description: 'Time grouping function',
|
||||
parameters: [columnParam, intervalParam, fillParam],
|
||||
},
|
||||
{
|
||||
name: '$__timeGroupAlias',
|
||||
description: 'Time grouping function with time as alias',
|
||||
parameters: [columnParam, intervalParam, fillParam],
|
||||
},
|
||||
{
|
||||
name: '$__time',
|
||||
description: 'An expression to rename the column to time',
|
||||
parameters: [columnParam],
|
||||
},
|
||||
{
|
||||
name: '$__timeEpoch',
|
||||
parameters: [columnParam],
|
||||
},
|
||||
{
|
||||
name: '$__unixEpochGroup',
|
||||
parameters: [columnParam, intervalParam, fillParam],
|
||||
},
|
||||
{
|
||||
name: '$__unixEpochGroupAlias',
|
||||
parameters: [columnParam, intervalParam, fillParam],
|
||||
},
|
||||
];
|
||||
|
||||
export const MACRO_NAMES = [
|
||||
'$__time',
|
||||
|
@ -6,8 +6,11 @@ export type {
|
||||
SQLQuery,
|
||||
SqlQueryModel,
|
||||
SQLSelectableValue,
|
||||
Func,
|
||||
FuncParameter,
|
||||
} from './types';
|
||||
export { QueryFormat } from './types'; // this is an enum, we cannot export-type it
|
||||
export { COMMON_FNS, MACRO_FUNCTIONS } from './constants';
|
||||
export { SqlDatasource } from './datasource/SqlDatasource';
|
||||
export { formatSQL } from './utils/formatSQL';
|
||||
export { ConnectionLimits } from './components/configuration/ConnectionLimits';
|
||||
|
@ -134,7 +134,18 @@ export interface DB {
|
||||
lookup?: (path?: string) => Promise<Array<{ name: string; completion: string }>>;
|
||||
getEditorLanguageDefinition: () => LanguageDefinition;
|
||||
toRawSql: (query: SQLQuery) => string;
|
||||
functions?: () => string[];
|
||||
functions: () => Func[];
|
||||
}
|
||||
|
||||
export interface FuncParameter {
|
||||
name: string;
|
||||
required?: boolean;
|
||||
options?: (query: SQLQuery) => Promise<SelectableValue[]>;
|
||||
}
|
||||
export interface Func {
|
||||
name: string;
|
||||
parameters?: FuncParameter[];
|
||||
description?: string;
|
||||
}
|
||||
|
||||
export interface QueryEditorProps {
|
||||
|
@ -1,6 +1,9 @@
|
||||
import { SelectableValue, toOption } from '@grafana/data';
|
||||
|
||||
import {
|
||||
QueryEditorExpressionType,
|
||||
QueryEditorFunctionExpression,
|
||||
QueryEditorFunctionParameterExpression,
|
||||
QueryEditorGroupByExpression,
|
||||
QueryEditorPropertyExpression,
|
||||
QueryEditorPropertyType,
|
||||
@ -67,3 +70,18 @@ export function createFunctionField(functionName?: string): QueryEditorFunctionE
|
||||
parameters: [],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the column value from a QueryEditorFunctionParameterExpression object.
|
||||
*
|
||||
* @param column - The QueryEditorFunctionParameterExpression object representing the column.
|
||||
* @returns The column value as a SelectableValue<string> or null if the column is undefined or null.
|
||||
*/
|
||||
export function getColumnValue(
|
||||
column?: QueryEditorFunctionParameterExpression | QueryEditorFunctionExpression
|
||||
): SelectableValue<string> | null {
|
||||
if (column?.name) {
|
||||
return toOption(column.name);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
@ -117,7 +117,7 @@ func newAppBuilderGroup(cfg RunnerConfig, provider app.Provider) (appBuilderGrou
|
||||
|
||||
func (g *appBuilderGroup) setApp(app app.App) {
|
||||
g.app = app
|
||||
for i, _ := range g.builders {
|
||||
for i := range g.builders {
|
||||
g.builders[i].setApp(app)
|
||||
}
|
||||
}
|
||||
|
@ -14,7 +14,7 @@ var typedResources = map[string]TypeInfo{
|
||||
NewNamespaceResourceIdent(
|
||||
folderalpha1.FolderResourceInfo.GroupResource().Group,
|
||||
folderalpha1.FolderResourceInfo.GroupResource().Resource,
|
||||
): TypeInfo{Type: "folder2"},
|
||||
): {Type: "folder2"},
|
||||
}
|
||||
|
||||
func GetTypeInfo(group, resource string) (TypeInfo, bool) {
|
||||
|
@ -676,9 +676,9 @@ func TestGetParentNames(t *testing.T) {
|
||||
{UID: "libraryElementUID-1"},
|
||||
},
|
||||
expectedParentNames: map[cloudmigration.MigrateDataType][]string{
|
||||
cloudmigration.DashboardDataType: []string{"", "Folder A", "Folder B"},
|
||||
cloudmigration.FolderDataType: []string{"Folder A"},
|
||||
cloudmigration.LibraryElementDataType: []string{"Folder A"},
|
||||
cloudmigration.DashboardDataType: {"", "Folder A", "Folder B"},
|
||||
cloudmigration.FolderDataType: {"Folder A"},
|
||||
cloudmigration.LibraryElementDataType: {"Folder A"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
@ -28,5 +28,6 @@ const (
|
||||
enterpriseDatasourcesSquad codeowner = "@grafana/enterprise-datasources"
|
||||
grafanaSharingSquad codeowner = "@grafana/sharing-squad"
|
||||
grafanaDatabasesFrontend codeowner = "@grafana/databases-frontend"
|
||||
grafanaOSSBigTent codeowner = "@grafana/oss-big-tent"
|
||||
growthAndOnboarding codeowner = "@grafana/growth-and-onboarding"
|
||||
)
|
||||
|
@ -1305,6 +1305,13 @@ var (
|
||||
Owner: grafanaPluginsPlatformSquad,
|
||||
Expression: "false", // disabled by default
|
||||
},
|
||||
{
|
||||
Name: "sqlQuerybuilderFunctionParameters",
|
||||
Description: "Enables SQL query builder function parameters",
|
||||
Stage: FeatureStageExperimental,
|
||||
Owner: grafanaOSSBigTent,
|
||||
FrontendOnly: true,
|
||||
},
|
||||
{
|
||||
Name: "azureMonitorPrometheusExemplars",
|
||||
Description: "Allows configuration of Azure Monitor as a data source that can provide Prometheus exemplars",
|
||||
|
@ -171,6 +171,7 @@ alertingDisableSendAlertsExternal,experimental,@grafana/alerting-squad,false,fal
|
||||
preserveDashboardStateWhenNavigating,experimental,@grafana/dashboards-squad,false,false,false
|
||||
alertingCentralAlertHistory,experimental,@grafana/alerting-squad,false,false,true
|
||||
pluginProxyPreserveTrailingSlash,GA,@grafana/plugins-platform-backend,false,false,false
|
||||
sqlQuerybuilderFunctionParameters,experimental,@grafana/oss-big-tent,false,false,true
|
||||
azureMonitorPrometheusExemplars,preview,@grafana/partner-datasources,false,false,false
|
||||
pinNavItems,GA,@grafana/grafana-frontend-platform,false,false,false
|
||||
authZGRPCServer,experimental,@grafana/identity-access-team,false,false,false
|
||||
|
|
@ -695,6 +695,10 @@ const (
|
||||
// Preserve plugin proxy trailing slash.
|
||||
FlagPluginProxyPreserveTrailingSlash = "pluginProxyPreserveTrailingSlash"
|
||||
|
||||
// FlagSqlQuerybuilderFunctionParameters
|
||||
// Enables SQL query builder function parameters
|
||||
FlagSqlQuerybuilderFunctionParameters = "sqlQuerybuilderFunctionParameters"
|
||||
|
||||
// FlagAzureMonitorPrometheusExemplars
|
||||
// Allows configuration of Azure Monitor as a data source that can provide Prometheus exemplars
|
||||
FlagAzureMonitorPrometheusExemplars = "azureMonitorPrometheusExemplars"
|
||||
|
@ -2985,6 +2985,19 @@
|
||||
"codeowner": "@grafana/grafana-app-platform-squad"
|
||||
}
|
||||
},
|
||||
{
|
||||
"metadata": {
|
||||
"name": "sqlQuerybuilderFunctionParameters",
|
||||
"resourceVersion": "1718487716739",
|
||||
"creationTimestamp": "2024-06-15T21:41:56Z"
|
||||
},
|
||||
"spec": {
|
||||
"description": "Enables SQL query builder function parameters",
|
||||
"stage": "experimental",
|
||||
"codeowner": "@grafana/oss-big-tent",
|
||||
"frontend": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"metadata": {
|
||||
"name": "sseGroupByDatasource",
|
||||
|
@ -120,7 +120,7 @@ func (i *Index) AddToBatches(ctx context.Context, list *ListResponse) ([]string,
|
||||
}
|
||||
|
||||
tenants := make([]string, 0, len(tenantsWithChanges))
|
||||
for tenant, _ := range tenantsWithChanges {
|
||||
for tenant := range tenantsWithChanges {
|
||||
tenants = append(tenants, tenant)
|
||||
}
|
||||
|
||||
|
@ -36,6 +36,12 @@ const fakeDataSourceSrv: DataSourceSrv = {
|
||||
getInstanceSettings: () => ({ id: 8674 }),
|
||||
} as unknown as DataSourceSrv;
|
||||
|
||||
const uid = '0000';
|
||||
// mock uuidv4 to give back the same value every time
|
||||
jest.mock('uuid', () => ({
|
||||
v4: () => uid,
|
||||
}));
|
||||
|
||||
let origBackendSrv: BackendSrv;
|
||||
let origDataSourceSrv: DataSourceSrv;
|
||||
beforeAll(() => {
|
||||
@ -404,8 +410,7 @@ describe('PostgreSQLDatasource', () => {
|
||||
it('should return a list of fields when fetchFields is called', async () => {
|
||||
const fetchFieldsResponse = {
|
||||
results: {
|
||||
columns: {
|
||||
refId: 'columns',
|
||||
[`columns-${uid}`]: {
|
||||
frames: [
|
||||
dataFrameToJSON(
|
||||
createDataFrame({
|
||||
|
@ -1,7 +1,18 @@
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import { DataSourceInstanceSettings, ScopedVars } from '@grafana/data';
|
||||
import { LanguageDefinition } from '@grafana/experimental';
|
||||
import { TemplateSrv } from '@grafana/runtime';
|
||||
import { SqlDatasource, DB, SQLQuery, SQLSelectableValue, formatSQL } from '@grafana/sql';
|
||||
import { TemplateSrv, config } from '@grafana/runtime';
|
||||
import {
|
||||
COMMON_FNS,
|
||||
DB,
|
||||
FuncParameter,
|
||||
MACRO_FUNCTIONS,
|
||||
SQLQuery,
|
||||
SQLSelectableValue,
|
||||
SqlDatasource,
|
||||
formatSQL,
|
||||
} from '@grafana/sql';
|
||||
|
||||
import { PostgresQueryModel } from './PostgresQueryModel';
|
||||
import { getSchema, getTimescaleDBVersion, getVersion, showTables } from './postgresMetaQuery';
|
||||
@ -70,7 +81,9 @@ export class PostgresDatasource extends SqlDatasource {
|
||||
// if no table-name, we are not able to query for fields
|
||||
return [];
|
||||
}
|
||||
const schema = await this.runSql<{ column: string; type: string }>(getSchema(table), { refId: 'columns' });
|
||||
const schema = await this.runSql<{ column: string; type: string }>(getSchema(table), {
|
||||
refId: `columns-${uuidv4()}`,
|
||||
});
|
||||
const result: SQLSelectableValue[] = [];
|
||||
for (let i = 0; i < schema.length; i++) {
|
||||
const column = schema.fields.column.values[i];
|
||||
@ -80,6 +93,20 @@ export class PostgresDatasource extends SqlDatasource {
|
||||
return result;
|
||||
}
|
||||
|
||||
getFunctions = (): ReturnType<DB['functions']> => {
|
||||
if (config.featureToggles.sqlQuerybuilderFunctionParameters) {
|
||||
const columnParam: FuncParameter = {
|
||||
name: 'Column',
|
||||
required: true,
|
||||
options: (query) => this.fetchFields(query),
|
||||
};
|
||||
|
||||
return [...MACRO_FUNCTIONS(columnParam), ...COMMON_FNS.map((fn) => ({ ...fn, parameters: [columnParam] }))];
|
||||
} else {
|
||||
return COMMON_FNS;
|
||||
}
|
||||
};
|
||||
|
||||
getDB(): DB {
|
||||
if (this.db !== undefined) {
|
||||
return this.db;
|
||||
@ -100,6 +127,7 @@ export class PostgresDatasource extends SqlDatasource {
|
||||
Promise.resolve({ isError: false, isValid: true, query, error: '', rawSql: query.rawSql }),
|
||||
dsID: () => this.id,
|
||||
toRawSql,
|
||||
functions: () => this.getFunctions(),
|
||||
lookup: async () => {
|
||||
const tables = await this.fetchTables();
|
||||
return tables.map((t) => ({ name: t, completion: t }));
|
||||
|
@ -1,7 +1,9 @@
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import { DataSourceInstanceSettings, TimeRange } from '@grafana/data';
|
||||
import { CompletionItemKind, LanguageDefinition, TableIdentifier } from '@grafana/experimental';
|
||||
import { getTemplateSrv, TemplateSrv } from '@grafana/runtime';
|
||||
import { DB, formatSQL, SqlDatasource, SQLQuery } from '@grafana/sql';
|
||||
import { TemplateSrv, config, getTemplateSrv } from '@grafana/runtime';
|
||||
import { COMMON_FNS, DB, FuncParameter, SQLQuery, SqlDatasource, formatSQL } from '@grafana/sql';
|
||||
|
||||
import { mapFieldsToTypes } from './fields';
|
||||
import { buildColumnQuery, buildTableQuery } from './flightsqlMetaQuery';
|
||||
@ -57,7 +59,7 @@ export class FlightSQLDatasource extends SqlDatasource {
|
||||
}
|
||||
const interpolatedTable = this.templateSrv.replace(query.table);
|
||||
const queryString = buildColumnQuery(interpolatedTable, query.dataset);
|
||||
const frame = await this.runSql<string[]>(queryString, { refId: 'fields' });
|
||||
const frame = await this.runSql<string[]>(queryString, { refId: `fields-${uuidv4}` });
|
||||
const fields = frame.map((f) => ({
|
||||
name: f[0],
|
||||
text: f[0],
|
||||
@ -102,6 +104,40 @@ export class FlightSQLDatasource extends SqlDatasource {
|
||||
}
|
||||
}
|
||||
|
||||
getFunctions = (): ReturnType<DB['functions']> => {
|
||||
const fns = [...COMMON_FNS, { name: 'VARIANCE' }, { name: 'STDDEV' }];
|
||||
if (config.featureToggles.sqlQuerybuilderFunctionParameters) {
|
||||
const columnParam: FuncParameter = {
|
||||
name: 'Column',
|
||||
required: true,
|
||||
options: (query) => this.fetchFields(query),
|
||||
};
|
||||
const intervalParam: FuncParameter = {
|
||||
name: 'Interval',
|
||||
required: true,
|
||||
options: () => {
|
||||
return Promise.resolve([{ label: '$__interval', value: '$__interval' }]);
|
||||
},
|
||||
};
|
||||
|
||||
return [
|
||||
...fns.map((fn) => ({ ...fn, parameters: [columnParam] })),
|
||||
{
|
||||
name: '$__timeGroup',
|
||||
description: 'Time grouping function',
|
||||
parameters: [columnParam, intervalParam],
|
||||
},
|
||||
{
|
||||
name: '$__timeGroupAlias',
|
||||
description: 'Time grouping function with time as alias',
|
||||
parameters: [columnParam, intervalParam],
|
||||
},
|
||||
];
|
||||
} else {
|
||||
return fns;
|
||||
}
|
||||
};
|
||||
|
||||
getDB(): DB {
|
||||
if (this.db !== undefined) {
|
||||
return this.db;
|
||||
@ -114,7 +150,7 @@ export class FlightSQLDatasource extends SqlDatasource {
|
||||
Promise.resolve({ query, error: '', isError: false, isValid: true }),
|
||||
dsID: () => this.id,
|
||||
toRawSql,
|
||||
functions: () => ['VARIANCE', 'STDDEV'],
|
||||
functions: () => this.getFunctions(),
|
||||
getEditorLanguageDefinition: () => this.getSqlLanguageDefinition(),
|
||||
};
|
||||
}
|
||||
|
@ -25,6 +25,11 @@ jest.mock('@grafana/runtime', () => ({
|
||||
getBackendSrv: () => backendSrv,
|
||||
}));
|
||||
|
||||
// mock uuidv4 to give back the same value every time
|
||||
jest.mock('uuid', () => ({
|
||||
v4: () => '0000',
|
||||
}));
|
||||
|
||||
const instanceSettings = {
|
||||
id: 1,
|
||||
uid: 'mssql-datasource',
|
||||
@ -173,8 +178,7 @@ describe('MSSQLDatasource', () => {
|
||||
it('should return a list of fields when fetchFields is called', async () => {
|
||||
const fetchFieldsResponse = {
|
||||
results: {
|
||||
columns: {
|
||||
refId: 'columns',
|
||||
[`columns-0000`]: {
|
||||
frames: [
|
||||
dataFrameToJSON(
|
||||
createDataFrame({
|
||||
|
@ -1,9 +1,20 @@
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import { DataSourceInstanceSettings, ScopedVars } from '@grafana/data';
|
||||
import { LanguageDefinition } from '@grafana/experimental';
|
||||
import { TemplateSrv } from '@grafana/runtime';
|
||||
import { DB, SQLQuery, SqlDatasource, SQLSelectableValue, formatSQL } from '@grafana/sql';
|
||||
import { TemplateSrv, config } from '@grafana/runtime';
|
||||
import {
|
||||
COMMON_FNS,
|
||||
DB,
|
||||
FuncParameter,
|
||||
MACRO_FUNCTIONS,
|
||||
SQLQuery,
|
||||
SQLSelectableValue,
|
||||
SqlDatasource,
|
||||
formatSQL,
|
||||
} from '@grafana/sql';
|
||||
|
||||
import { getSchema, showDatabases, getSchemaAndName } from './MSSqlMetaQuery';
|
||||
import { getSchema, getSchemaAndName, showDatabases } from './MSSqlMetaQuery';
|
||||
import { MSSqlQueryModel } from './MSSqlQueryModel';
|
||||
import { fetchColumns, fetchTables, getSqlCompletionProvider } from './sqlCompletionProvider';
|
||||
import { getIcon, getRAQBType, toRawSql } from './sqlUtil';
|
||||
@ -36,7 +47,7 @@ export class MssqlDatasource extends SqlDatasource {
|
||||
}
|
||||
const [_, table] = query.table.split('.');
|
||||
const schema = await this.runSql<{ column: string; type: string }>(getSchema(query.dataset, table), {
|
||||
refId: 'columns',
|
||||
refId: `columns-${uuidv4()}`,
|
||||
});
|
||||
const result: SQLSelectableValue[] = [];
|
||||
for (let i = 0; i < schema.length; i++) {
|
||||
@ -63,6 +74,20 @@ export class MssqlDatasource extends SqlDatasource {
|
||||
return this.sqlLanguageDefinition;
|
||||
}
|
||||
|
||||
getFunctions = (): ReturnType<DB['functions']> => {
|
||||
if (config.featureToggles.sqlQuerybuilderFunctionParameters) {
|
||||
const columnParam: FuncParameter = {
|
||||
name: 'Column',
|
||||
required: true,
|
||||
options: (query) => this.fetchFields(query),
|
||||
};
|
||||
|
||||
return [...MACRO_FUNCTIONS(columnParam), ...COMMON_FNS.map((fn) => ({ ...fn, parameters: [columnParam] }))];
|
||||
} else {
|
||||
return COMMON_FNS;
|
||||
}
|
||||
};
|
||||
|
||||
getDB(): DB {
|
||||
if (this.db !== undefined) {
|
||||
return this.db;
|
||||
@ -83,6 +108,7 @@ export class MssqlDatasource extends SqlDatasource {
|
||||
dsID: () => this.id,
|
||||
dispose: (_dsID?: string) => {},
|
||||
toRawSql,
|
||||
functions: () => this.getFunctions(),
|
||||
lookup: async (path?: string) => {
|
||||
if (!path) {
|
||||
const datasets = await this.fetchDatasets();
|
||||
|
@ -1,6 +1,9 @@
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import { DataSourceInstanceSettings, TimeRange } from '@grafana/data';
|
||||
import { CompletionItemKind, LanguageDefinition, TableIdentifier } from '@grafana/experimental';
|
||||
import { SqlDatasource, DB, SQLQuery, formatSQL } from '@grafana/sql';
|
||||
import { config } from '@grafana/runtime';
|
||||
import { COMMON_FNS, DB, FuncParameter, MACRO_FUNCTIONS, SQLQuery, SqlDatasource, formatSQL } from '@grafana/sql';
|
||||
|
||||
import { mapFieldsToTypes } from './fields';
|
||||
import { buildColumnQuery, buildTableQuery, showDatabases } from './mySqlMetaQuery';
|
||||
@ -52,7 +55,7 @@ export class MySqlDatasource extends SqlDatasource {
|
||||
return [];
|
||||
}
|
||||
const queryString = buildColumnQuery(query.table, query.dataset);
|
||||
const frame = await this.runSql<string[]>(queryString, { refId: 'fields' });
|
||||
const frame = await this.runSql<string[]>(queryString, { refId: `fields-${uuidv4()}` });
|
||||
const fields = frame.map((f) => ({
|
||||
name: f[0],
|
||||
text: f[0],
|
||||
@ -84,6 +87,21 @@ export class MySqlDatasource extends SqlDatasource {
|
||||
}
|
||||
}
|
||||
|
||||
getFunctions = (): ReturnType<DB['functions']> => {
|
||||
const fns = [...COMMON_FNS, { name: 'VARIANCE' }, { name: 'STDDEV' }];
|
||||
if (config.featureToggles.sqlQuerybuilderFunctionParameters) {
|
||||
const columnParam: FuncParameter = {
|
||||
name: 'Column',
|
||||
required: true,
|
||||
options: (query) => this.fetchFields(query),
|
||||
};
|
||||
|
||||
return [...MACRO_FUNCTIONS(columnParam), ...fns.map((fn) => ({ ...fn, parameters: [columnParam] }))];
|
||||
} else {
|
||||
return fns;
|
||||
}
|
||||
};
|
||||
|
||||
getDB(): DB {
|
||||
if (this.db !== undefined) {
|
||||
return this.db;
|
||||
@ -97,7 +115,7 @@ export class MySqlDatasource extends SqlDatasource {
|
||||
Promise.resolve({ query, error: '', isError: false, isValid: true }),
|
||||
dsID: () => this.id,
|
||||
toRawSql,
|
||||
functions: () => ['VARIANCE', 'STDDEV'],
|
||||
functions: () => this.getFunctions(),
|
||||
getEditorLanguageDefinition: () => this.getSqlLanguageDefinition(),
|
||||
};
|
||||
}
|
||||
|
@ -21,6 +21,12 @@ jest.mock('@grafana/runtime', () => ({
|
||||
}),
|
||||
}));
|
||||
|
||||
const uid = '0000';
|
||||
// mock uuidv4 to give back the same value every time
|
||||
jest.mock('uuid', () => ({
|
||||
v4: () => uid,
|
||||
}));
|
||||
|
||||
describe('MySQLDatasource', () => {
|
||||
const defaultRange = getDefaultTimeRange(); // it does not matter what value this has
|
||||
const setupTestContext = (response: unknown, templateSrv?: unknown) => {
|
||||
@ -134,7 +140,7 @@ describe('MySQLDatasource', () => {
|
||||
it('should return a list of fields when fetchFields is called', async () => {
|
||||
const fetchFieldsResponse = {
|
||||
results: {
|
||||
fields: {
|
||||
[`fields-${uid}`]: {
|
||||
refId: 'fields',
|
||||
frames: [
|
||||
dataFrameToJSON(
|
||||
|
Loading…
Reference in New Issue
Block a user