mirror of
https://github.com/grafana/grafana.git
synced 2025-08-01 18:13:09 +08: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:
@ -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.
|
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)
|
### Filter data (WHERE)
|
||||||
|
|
||||||
To add a filter, toggle the **Filter** switch at the top of the editor.
|
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. |
|
| `$__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. |
|
| `$__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
|
### 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.
|
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.
|
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)
|
### Filter data (WHERE)
|
||||||
|
|
||||||
To add a filter, toggle the **Filter** switch at the top of the editor.
|
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). |
|
| `$__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). |
|
| `$__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
|
## 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.
|
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.
|
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)
|
### Filter data (WHERE)
|
||||||
|
|
||||||
To add a filter, toggle the **Filter** switch at the top of the editor.
|
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). |
|
| `$__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). |
|
| `$__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
|
## 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.
|
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 |
|
| `alertingListViewV2` | Enables the new alert list view design |
|
||||||
| `dashboardRestore` | Enables deleted dashboard restore feature |
|
| `dashboardRestore` | Enables deleted dashboard restore feature |
|
||||||
| `alertingCentralAlertHistory` | Enables the new central alert history. |
|
| `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 |
|
| `failWrongDSUID` | Throws an error if a datasource has an invalid UIDs |
|
||||||
| `alertingApiServer` | Register Alerting APIs with the K8s API server |
|
| `alertingApiServer` | Register Alerting APIs with the K8s API server |
|
||||||
| `dataplaneAggregator` | Enable grafana dataplane aggregator |
|
| `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: {
|
results: {
|
||||||
fields: {
|
[refId]: {
|
||||||
status: 200,
|
status: 200,
|
||||||
frames: [
|
frames: [
|
||||||
{
|
{
|
||||||
schema: {
|
schema: {
|
||||||
refId: 'fields',
|
refId,
|
||||||
meta: {
|
meta: {
|
||||||
executedQueryString:
|
executedQueryString:
|
||||||
"SELECT column_name, data_type FROM information_schema.columns WHERE table_schema = 'DataMaker' AND table_name = 'RandomIntsWithTimes' ORDER BY column_name",
|
"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 = {
|
export const datasetResponse = {
|
||||||
results: {
|
results: {
|
||||||
|
@ -1,29 +1,10 @@
|
|||||||
import { selectors } from '@grafana/e2e-selectors';
|
import { selectors } from '@grafana/e2e-selectors';
|
||||||
import { expect, test } from '@grafana/plugin-e2e';
|
import { expect, test } from '@grafana/plugin-e2e';
|
||||||
|
|
||||||
import {
|
import { normalTableName, tableNameWithSpecialCharacter } from './mocks/mysql.mocks';
|
||||||
tablesResponse,
|
import { mockDataSourceRequest } from './utils';
|
||||||
fieldsResponse,
|
|
||||||
datasetResponse,
|
|
||||||
normalTableName,
|
|
||||||
tableNameWithSpecialCharacter,
|
|
||||||
} from './mocks/mysql.mocks';
|
|
||||||
|
|
||||||
test.beforeEach(async ({ context, selectors, explorePage }) => {
|
test.beforeEach(mockDataSourceRequest);
|
||||||
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('code editor autocomplete should handle table name escaping/quoting', async ({ explorePage, selectors, page }) => {
|
test('code editor autocomplete should handle table name escaping/quoting', async ({ explorePage, selectors, page }) => {
|
||||||
await page.getByLabel('Code').check();
|
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;
|
preserveDashboardStateWhenNavigating?: boolean;
|
||||||
alertingCentralAlertHistory?: boolean;
|
alertingCentralAlertHistory?: boolean;
|
||||||
pluginProxyPreserveTrailingSlash?: boolean;
|
pluginProxyPreserveTrailingSlash?: boolean;
|
||||||
|
sqlQuerybuilderFunctionParameters?: boolean;
|
||||||
azureMonitorPrometheusExemplars?: boolean;
|
azureMonitorPrometheusExemplars?: boolean;
|
||||||
pinNavItems?: boolean;
|
pinNavItems?: boolean;
|
||||||
authZGRPCServer?: boolean;
|
authZGRPCServer?: boolean;
|
||||||
|
@ -1191,12 +1191,17 @@ export const versionedComponents = {
|
|||||||
selectColumn: {
|
selectColumn: {
|
||||||
'11.0.0': 'data-testid select-column',
|
'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: {
|
selectAggregation: {
|
||||||
'11.0.0': 'data-testid select-aggregation',
|
'11.0.0': 'data-testid select-aggregation',
|
||||||
},
|
},
|
||||||
|
selectAggregationInput: { '11.0.0': 'data-testid select-aggregation-input' },
|
||||||
selectAlias: {
|
selectAlias: {
|
||||||
'11.0.0': 'data-testid select-alias',
|
'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: {
|
filterConjunction: {
|
||||||
'11.0.0': 'data-testid filter-conjunction',
|
'11.0.0': 'data-testid filter-conjunction',
|
||||||
},
|
},
|
||||||
|
@ -5,7 +5,7 @@ import { DB, SQLQuery, SQLSelectableValue, ValidationResults } from '../types';
|
|||||||
import { DatasetSelectorProps } from './DatasetSelector';
|
import { DatasetSelectorProps } from './DatasetSelector';
|
||||||
import { TableSelectorProps } from './TableSelector';
|
import { TableSelectorProps } from './TableSelector';
|
||||||
|
|
||||||
const buildMockDB = (): DB => ({
|
export const buildMockDB = (): DB => ({
|
||||||
datasets: jest.fn(() => Promise.resolve(['dataset1', 'dataset2'])),
|
datasets: jest.fn(() => Promise.resolve(['dataset1', 'dataset2'])),
|
||||||
tables: jest.fn((_ds: string | undefined) => Promise.resolve(['table1', 'table2'])),
|
tables: jest.fn((_ds: string | undefined) => Promise.resolve(['table1', 'table2'])),
|
||||||
fields: jest.fn((_query: SQLQuery, _order?: boolean) => Promise.resolve<SQLSelectableValue[]>([])),
|
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 })
|
Promise.resolve<ValidationResults>({ query: { refId: '123' }, error: '', isError: false, isValid: true })
|
||||||
),
|
),
|
||||||
dsID: jest.fn(() => 1234),
|
dsID: jest.fn(() => 1234),
|
||||||
|
functions: jest.fn(() => []),
|
||||||
getEditorLanguageDefinition: jest.fn(() => ({ id: '4567' })),
|
getEditorLanguageDefinition: jest.fn(() => ({ id: '4567' })),
|
||||||
toRawSql: (_query: SQLQuery) => '',
|
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 { SelectableValue, toOption } from '@grafana/data';
|
||||||
import { selectors } from '@grafana/e2e-selectors';
|
import { selectors } from '@grafana/e2e-selectors';
|
||||||
import { EditorField, Stack } from '@grafana/experimental';
|
import { EditorField } from '@grafana/experimental';
|
||||||
import { Button, Select, useStyles2 } from '@grafana/ui';
|
import { config } from '@grafana/runtime';
|
||||||
|
import { Button, Select, Stack, useStyles2 } from '@grafana/ui';
|
||||||
|
|
||||||
import { QueryEditorExpressionType, QueryEditorFunctionExpression } from '../../expressions';
|
import { QueryEditorExpressionType, QueryEditorFunctionExpression } from '../../expressions';
|
||||||
import { SQLExpression, QueryFormat } from '../../types';
|
import { DB, QueryFormat, SQLExpression, SQLQuery } from '../../types';
|
||||||
import { createFunctionField } from '../../utils/sql.utils';
|
import { createFunctionField } from '../../utils/sql.utils';
|
||||||
|
import { useSqlChange } from '../../utils/useSqlChange';
|
||||||
|
|
||||||
|
import { SelectColumn } from './SelectColumn';
|
||||||
|
import { SelectFunctionParameters } from './SelectFunctionParameters';
|
||||||
|
|
||||||
interface SelectRowProps {
|
interface SelectRowProps {
|
||||||
sql: SQLExpression;
|
query: SQLQuery;
|
||||||
format: QueryFormat | undefined;
|
onQueryChange: (sql: SQLQuery) => void;
|
||||||
onSqlChange: (sql: SQLExpression) => void;
|
db: DB;
|
||||||
columns?: Array<SelectableValue<string>>;
|
columns: Array<SelectableValue<string>>;
|
||||||
functions?: Array<SelectableValue<string>>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const asteriskValue = { label: '*', value: '*' };
|
export function SelectRow({ query, onQueryChange, db, columns }: SelectRowProps) {
|
||||||
|
|
||||||
export function SelectRow({ sql, format, columns, onSqlChange, functions }: SelectRowProps) {
|
|
||||||
const styles = useStyles2(getStyles);
|
const styles = useStyles2(getStyles);
|
||||||
const columnsWithAsterisk = [asteriskValue, ...(columns || [])];
|
const { onSqlChange } = useSqlChange({ query, onQueryChange, db });
|
||||||
const timeSeriesAliasOpts: Array<SelectableValue<string>> = [];
|
const timeSeriesAliasOpts: Array<SelectableValue<string>> = [];
|
||||||
|
|
||||||
// Add necessary alias options for time series format
|
// Add necessary alias options for time series format
|
||||||
// when that format has been selected
|
// when that format has been selected
|
||||||
if (format === QueryFormat.Timeseries) {
|
if (query.format === QueryFormat.Timeseries) {
|
||||||
timeSeriesAliasOpts.push({ label: 'time', value: 'time' });
|
timeSeriesAliasOpts.push({ label: 'time', value: 'time' });
|
||||||
timeSeriesAliasOpts.push({ label: 'value', value: 'value' });
|
timeSeriesAliasOpts.push({ label: 'value', value: 'value' });
|
||||||
}
|
}
|
||||||
|
|
||||||
const onColumnChange = useCallback(
|
const onColumnChange = useCallback(
|
||||||
(item: QueryEditorFunctionExpression, index: number) => (column: SelectableValue<string>) => {
|
(item: QueryEditorFunctionExpression, index: number) => (column?: string) => {
|
||||||
let modifiedItem = { ...item };
|
let modifiedItem = { ...item };
|
||||||
if (!item.parameters?.length) {
|
if (!item.parameters?.length) {
|
||||||
modifiedItem.parameters = [{ type: QueryEditorExpressionType.FunctionParameter, name: column.value } as const];
|
modifiedItem.parameters = [{ type: QueryEditorExpressionType.FunctionParameter, name: column } as const];
|
||||||
} else {
|
} else {
|
||||||
modifiedItem.parameters = item.parameters.map((p) =>
|
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 = {
|
const newSql: SQLExpression = {
|
||||||
...sql,
|
...query.sql,
|
||||||
columns: sql.columns?.map((c, i) => (i === index ? modifiedItem : c)),
|
columns: query.sql?.columns?.map((c, i) => (i === index ? modifiedItem : c)),
|
||||||
};
|
};
|
||||||
|
|
||||||
onSqlChange(newSql);
|
onSqlChange(newSql);
|
||||||
},
|
},
|
||||||
[onSqlChange, sql]
|
[onSqlChange, query.sql]
|
||||||
);
|
);
|
||||||
|
|
||||||
const onAggregationChange = useCallback(
|
const onAggregationChange = useCallback(
|
||||||
@ -59,15 +61,18 @@ export function SelectRow({ sql, format, columns, onSqlChange, functions }: Sele
|
|||||||
const newItem = {
|
const newItem = {
|
||||||
...item,
|
...item,
|
||||||
name: aggregation?.value,
|
name: aggregation?.value,
|
||||||
|
parameters: [
|
||||||
|
{ type: QueryEditorExpressionType.FunctionParameter as const, name: item.parameters?.[0]?.name || '' },
|
||||||
|
],
|
||||||
};
|
};
|
||||||
const newSql: SQLExpression = {
|
const newSql: SQLExpression = {
|
||||||
...sql,
|
...query.sql,
|
||||||
columns: sql.columns?.map((c, i) => (i === index ? newItem : c)),
|
columns: query.sql?.columns?.map((c, i) => (i === index ? newItem : c)),
|
||||||
};
|
};
|
||||||
|
|
||||||
onSqlChange(newSql);
|
onSqlChange(newSql);
|
||||||
},
|
},
|
||||||
[onSqlChange, sql]
|
[onSqlChange, query.sql]
|
||||||
);
|
);
|
||||||
|
|
||||||
const onAliasChange = useCallback(
|
const onAliasChange = useCallback(
|
||||||
@ -81,51 +86,66 @@ export function SelectRow({ sql, format, columns, onSqlChange, functions }: Sele
|
|||||||
}
|
}
|
||||||
|
|
||||||
const newSql: SQLExpression = {
|
const newSql: SQLExpression = {
|
||||||
...sql,
|
...query.sql,
|
||||||
columns: sql.columns?.map((c, i) => (i === index ? newItem : c)),
|
columns: query.sql?.columns?.map((c, i) => (i === index ? newItem : c)),
|
||||||
};
|
};
|
||||||
|
|
||||||
onSqlChange(newSql);
|
onSqlChange(newSql);
|
||||||
},
|
},
|
||||||
[onSqlChange, sql]
|
[onSqlChange, query.sql]
|
||||||
);
|
);
|
||||||
|
|
||||||
const removeColumn = useCallback(
|
const removeColumn = useCallback(
|
||||||
(index: number) => () => {
|
(index: number) => () => {
|
||||||
const clone = [...sql.columns!];
|
const clone = [...(query.sql?.columns || [])];
|
||||||
clone.splice(index, 1);
|
clone.splice(index, 1);
|
||||||
const newSql: SQLExpression = {
|
const newSql: SQLExpression = {
|
||||||
...sql,
|
...query.sql,
|
||||||
columns: clone,
|
columns: clone,
|
||||||
};
|
};
|
||||||
onSqlChange(newSql);
|
onSqlChange(newSql);
|
||||||
},
|
},
|
||||||
[onSqlChange, sql]
|
[onSqlChange, query.sql]
|
||||||
);
|
);
|
||||||
|
|
||||||
const addColumn = useCallback(() => {
|
const addColumn = useCallback(() => {
|
||||||
const newSql: SQLExpression = { ...sql, columns: [...sql.columns!, createFunctionField()] };
|
const newSql: SQLExpression = { ...query.sql, columns: [...(query.sql?.columns || []), createFunctionField()] };
|
||||||
onSqlChange(newSql);
|
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 (
|
return (
|
||||||
<Stack gap={2} wrap direction="column">
|
<Stack gap={2} wrap="wrap" direction="column">
|
||||||
{sql.columns?.map((item, index) => (
|
{query.sql?.columns?.map((item, index) => (
|
||||||
<div key={index}>
|
<div key={index}>
|
||||||
<Stack gap={2} alignItems="end">
|
<Stack gap={2} alignItems="end">
|
||||||
<EditorField label="Column" width={25}>
|
{!config.featureToggles.sqlQuerybuilderFunctionParameters && (
|
||||||
<Select
|
<SelectColumn
|
||||||
|
columns={columns}
|
||||||
|
onParameterChange={(v) => onColumnChange(item, index)(v)}
|
||||||
value={getColumnValue(item)}
|
value={getColumnValue(item)}
|
||||||
data-testid={selectors.components.SQLQueryEditor.selectColumn}
|
|
||||||
options={columnsWithAsterisk}
|
|
||||||
inputId={`select-column-${index}-${uniqueId()}`}
|
|
||||||
menuShouldPortal
|
|
||||||
allowCustomValue
|
|
||||||
onChange={onColumnChange(item, index)}
|
|
||||||
/>
|
/>
|
||||||
</EditorField>
|
)}
|
||||||
|
<EditorField
|
||||||
<EditorField label="Aggregation" optional width={25}>
|
label={config.featureToggles.sqlQuerybuilderFunctionParameters ? 'Data operations' : 'Aggregation'}
|
||||||
|
optional
|
||||||
|
width={25}
|
||||||
|
>
|
||||||
<Select
|
<Select
|
||||||
value={item.name ? toOption(item.name) : null}
|
value={item.name ? toOption(item.name) : null}
|
||||||
inputId={`select-aggregation-${index}-${uniqueId()}`}
|
inputId={`select-aggregation-${index}-${uniqueId()}`}
|
||||||
@ -133,10 +153,20 @@ export function SelectRow({ sql, format, columns, onSqlChange, functions }: Sele
|
|||||||
isClearable
|
isClearable
|
||||||
menuShouldPortal
|
menuShouldPortal
|
||||||
allowCustomValue
|
allowCustomValue
|
||||||
options={functions}
|
options={aggregateOptions()}
|
||||||
onChange={onAggregationChange(item, index)}
|
onChange={onAggregationChange(item, index)}
|
||||||
/>
|
/>
|
||||||
</EditorField>
|
</EditorField>
|
||||||
|
{config.featureToggles.sqlQuerybuilderFunctionParameters && (
|
||||||
|
<SelectFunctionParameters
|
||||||
|
currentColumnIndex={index}
|
||||||
|
columns={columns}
|
||||||
|
onSqlChange={onSqlChange}
|
||||||
|
query={query}
|
||||||
|
db={db}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
<EditorField label="Alias" optional width={15}>
|
<EditorField label="Alias" optional width={15}>
|
||||||
<Select
|
<Select
|
||||||
value={item.alias ? toOption(item.alias) : null}
|
value={item.alias ? toOption(item.alias) : null}
|
||||||
@ -174,7 +204,14 @@ export function SelectRow({ sql, format, columns, onSqlChange, functions }: Sele
|
|||||||
}
|
}
|
||||||
|
|
||||||
const getStyles = () => {
|
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 {
|
function getColumnValue({ parameters }: QueryEditorFunctionExpression): SelectableValue<string> | null {
|
||||||
|
@ -8,8 +8,8 @@ import { QueryToolbox } from '../query-editor-raw/QueryToolbox';
|
|||||||
import { Preview } from './Preview';
|
import { Preview } from './Preview';
|
||||||
import { SQLGroupByRow } from './SQLGroupByRow';
|
import { SQLGroupByRow } from './SQLGroupByRow';
|
||||||
import { SQLOrderByRow } from './SQLOrderByRow';
|
import { SQLOrderByRow } from './SQLOrderByRow';
|
||||||
import { SQLSelectRow } from './SQLSelectRow';
|
|
||||||
import { SQLWhereRow } from './SQLWhereRow';
|
import { SQLWhereRow } from './SQLWhereRow';
|
||||||
|
import { SelectRow } from './SelectRow';
|
||||||
|
|
||||||
interface VisualEditorProps extends QueryEditorProps {
|
interface VisualEditorProps extends QueryEditorProps {
|
||||||
db: DB;
|
db: DB;
|
||||||
@ -27,7 +27,7 @@ export const VisualEditor = ({ query, db, queryRowFilter, onChange, onValidate,
|
|||||||
<>
|
<>
|
||||||
<EditorRows>
|
<EditorRows>
|
||||||
<EditorRow>
|
<EditorRow>
|
||||||
<SQLSelectRow fields={state.value || []} query={query} onQueryChange={onChange} db={db} />
|
<SelectRow columns={state.value || []} query={query} onQueryChange={onChange} db={db} />
|
||||||
</EditorRow>
|
</EditorRow>
|
||||||
{queryRowFilter.filter && (
|
{queryRowFilter.filter && (
|
||||||
<EditorRow>
|
<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 = [
|
export const MACRO_NAMES = [
|
||||||
'$__time',
|
'$__time',
|
||||||
|
@ -6,8 +6,11 @@ export type {
|
|||||||
SQLQuery,
|
SQLQuery,
|
||||||
SqlQueryModel,
|
SqlQueryModel,
|
||||||
SQLSelectableValue,
|
SQLSelectableValue,
|
||||||
|
Func,
|
||||||
|
FuncParameter,
|
||||||
} from './types';
|
} from './types';
|
||||||
export { QueryFormat } from './types'; // this is an enum, we cannot export-type it
|
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 { SqlDatasource } from './datasource/SqlDatasource';
|
||||||
export { formatSQL } from './utils/formatSQL';
|
export { formatSQL } from './utils/formatSQL';
|
||||||
export { ConnectionLimits } from './components/configuration/ConnectionLimits';
|
export { ConnectionLimits } from './components/configuration/ConnectionLimits';
|
||||||
|
@ -134,7 +134,18 @@ export interface DB {
|
|||||||
lookup?: (path?: string) => Promise<Array<{ name: string; completion: string }>>;
|
lookup?: (path?: string) => Promise<Array<{ name: string; completion: string }>>;
|
||||||
getEditorLanguageDefinition: () => LanguageDefinition;
|
getEditorLanguageDefinition: () => LanguageDefinition;
|
||||||
toRawSql: (query: SQLQuery) => string;
|
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 {
|
export interface QueryEditorProps {
|
||||||
|
@ -1,6 +1,9 @@
|
|||||||
|
import { SelectableValue, toOption } from '@grafana/data';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
QueryEditorExpressionType,
|
QueryEditorExpressionType,
|
||||||
QueryEditorFunctionExpression,
|
QueryEditorFunctionExpression,
|
||||||
|
QueryEditorFunctionParameterExpression,
|
||||||
QueryEditorGroupByExpression,
|
QueryEditorGroupByExpression,
|
||||||
QueryEditorPropertyExpression,
|
QueryEditorPropertyExpression,
|
||||||
QueryEditorPropertyType,
|
QueryEditorPropertyType,
|
||||||
@ -67,3 +70,18 @@ export function createFunctionField(functionName?: string): QueryEditorFunctionE
|
|||||||
parameters: [],
|
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) {
|
func (g *appBuilderGroup) setApp(app app.App) {
|
||||||
g.app = app
|
g.app = app
|
||||||
for i, _ := range g.builders {
|
for i := range g.builders {
|
||||||
g.builders[i].setApp(app)
|
g.builders[i].setApp(app)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -14,7 +14,7 @@ var typedResources = map[string]TypeInfo{
|
|||||||
NewNamespaceResourceIdent(
|
NewNamespaceResourceIdent(
|
||||||
folderalpha1.FolderResourceInfo.GroupResource().Group,
|
folderalpha1.FolderResourceInfo.GroupResource().Group,
|
||||||
folderalpha1.FolderResourceInfo.GroupResource().Resource,
|
folderalpha1.FolderResourceInfo.GroupResource().Resource,
|
||||||
): TypeInfo{Type: "folder2"},
|
): {Type: "folder2"},
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetTypeInfo(group, resource string) (TypeInfo, bool) {
|
func GetTypeInfo(group, resource string) (TypeInfo, bool) {
|
||||||
|
@ -676,9 +676,9 @@ func TestGetParentNames(t *testing.T) {
|
|||||||
{UID: "libraryElementUID-1"},
|
{UID: "libraryElementUID-1"},
|
||||||
},
|
},
|
||||||
expectedParentNames: map[cloudmigration.MigrateDataType][]string{
|
expectedParentNames: map[cloudmigration.MigrateDataType][]string{
|
||||||
cloudmigration.DashboardDataType: []string{"", "Folder A", "Folder B"},
|
cloudmigration.DashboardDataType: {"", "Folder A", "Folder B"},
|
||||||
cloudmigration.FolderDataType: []string{"Folder A"},
|
cloudmigration.FolderDataType: {"Folder A"},
|
||||||
cloudmigration.LibraryElementDataType: []string{"Folder A"},
|
cloudmigration.LibraryElementDataType: {"Folder A"},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -28,5 +28,6 @@ const (
|
|||||||
enterpriseDatasourcesSquad codeowner = "@grafana/enterprise-datasources"
|
enterpriseDatasourcesSquad codeowner = "@grafana/enterprise-datasources"
|
||||||
grafanaSharingSquad codeowner = "@grafana/sharing-squad"
|
grafanaSharingSquad codeowner = "@grafana/sharing-squad"
|
||||||
grafanaDatabasesFrontend codeowner = "@grafana/databases-frontend"
|
grafanaDatabasesFrontend codeowner = "@grafana/databases-frontend"
|
||||||
|
grafanaOSSBigTent codeowner = "@grafana/oss-big-tent"
|
||||||
growthAndOnboarding codeowner = "@grafana/growth-and-onboarding"
|
growthAndOnboarding codeowner = "@grafana/growth-and-onboarding"
|
||||||
)
|
)
|
||||||
|
@ -1305,6 +1305,13 @@ var (
|
|||||||
Owner: grafanaPluginsPlatformSquad,
|
Owner: grafanaPluginsPlatformSquad,
|
||||||
Expression: "false", // disabled by default
|
Expression: "false", // disabled by default
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
Name: "sqlQuerybuilderFunctionParameters",
|
||||||
|
Description: "Enables SQL query builder function parameters",
|
||||||
|
Stage: FeatureStageExperimental,
|
||||||
|
Owner: grafanaOSSBigTent,
|
||||||
|
FrontendOnly: true,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
Name: "azureMonitorPrometheusExemplars",
|
Name: "azureMonitorPrometheusExemplars",
|
||||||
Description: "Allows configuration of Azure Monitor as a data source that can provide Prometheus exemplars",
|
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
|
preserveDashboardStateWhenNavigating,experimental,@grafana/dashboards-squad,false,false,false
|
||||||
alertingCentralAlertHistory,experimental,@grafana/alerting-squad,false,false,true
|
alertingCentralAlertHistory,experimental,@grafana/alerting-squad,false,false,true
|
||||||
pluginProxyPreserveTrailingSlash,GA,@grafana/plugins-platform-backend,false,false,false
|
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
|
azureMonitorPrometheusExemplars,preview,@grafana/partner-datasources,false,false,false
|
||||||
pinNavItems,GA,@grafana/grafana-frontend-platform,false,false,false
|
pinNavItems,GA,@grafana/grafana-frontend-platform,false,false,false
|
||||||
authZGRPCServer,experimental,@grafana/identity-access-team,false,false,false
|
authZGRPCServer,experimental,@grafana/identity-access-team,false,false,false
|
||||||
|
|
@ -695,6 +695,10 @@ const (
|
|||||||
// Preserve plugin proxy trailing slash.
|
// Preserve plugin proxy trailing slash.
|
||||||
FlagPluginProxyPreserveTrailingSlash = "pluginProxyPreserveTrailingSlash"
|
FlagPluginProxyPreserveTrailingSlash = "pluginProxyPreserveTrailingSlash"
|
||||||
|
|
||||||
|
// FlagSqlQuerybuilderFunctionParameters
|
||||||
|
// Enables SQL query builder function parameters
|
||||||
|
FlagSqlQuerybuilderFunctionParameters = "sqlQuerybuilderFunctionParameters"
|
||||||
|
|
||||||
// FlagAzureMonitorPrometheusExemplars
|
// FlagAzureMonitorPrometheusExemplars
|
||||||
// Allows configuration of Azure Monitor as a data source that can provide Prometheus exemplars
|
// Allows configuration of Azure Monitor as a data source that can provide Prometheus exemplars
|
||||||
FlagAzureMonitorPrometheusExemplars = "azureMonitorPrometheusExemplars"
|
FlagAzureMonitorPrometheusExemplars = "azureMonitorPrometheusExemplars"
|
||||||
|
@ -2985,6 +2985,19 @@
|
|||||||
"codeowner": "@grafana/grafana-app-platform-squad"
|
"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": {
|
"metadata": {
|
||||||
"name": "sseGroupByDatasource",
|
"name": "sseGroupByDatasource",
|
||||||
|
@ -120,7 +120,7 @@ func (i *Index) AddToBatches(ctx context.Context, list *ListResponse) ([]string,
|
|||||||
}
|
}
|
||||||
|
|
||||||
tenants := make([]string, 0, len(tenantsWithChanges))
|
tenants := make([]string, 0, len(tenantsWithChanges))
|
||||||
for tenant, _ := range tenantsWithChanges {
|
for tenant := range tenantsWithChanges {
|
||||||
tenants = append(tenants, tenant)
|
tenants = append(tenants, tenant)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -36,6 +36,12 @@ const fakeDataSourceSrv: DataSourceSrv = {
|
|||||||
getInstanceSettings: () => ({ id: 8674 }),
|
getInstanceSettings: () => ({ id: 8674 }),
|
||||||
} as unknown as DataSourceSrv;
|
} 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 origBackendSrv: BackendSrv;
|
||||||
let origDataSourceSrv: DataSourceSrv;
|
let origDataSourceSrv: DataSourceSrv;
|
||||||
beforeAll(() => {
|
beforeAll(() => {
|
||||||
@ -404,8 +410,7 @@ describe('PostgreSQLDatasource', () => {
|
|||||||
it('should return a list of fields when fetchFields is called', async () => {
|
it('should return a list of fields when fetchFields is called', async () => {
|
||||||
const fetchFieldsResponse = {
|
const fetchFieldsResponse = {
|
||||||
results: {
|
results: {
|
||||||
columns: {
|
[`columns-${uid}`]: {
|
||||||
refId: 'columns',
|
|
||||||
frames: [
|
frames: [
|
||||||
dataFrameToJSON(
|
dataFrameToJSON(
|
||||||
createDataFrame({
|
createDataFrame({
|
||||||
|
@ -1,7 +1,18 @@
|
|||||||
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
|
|
||||||
import { DataSourceInstanceSettings, ScopedVars } from '@grafana/data';
|
import { DataSourceInstanceSettings, ScopedVars } from '@grafana/data';
|
||||||
import { LanguageDefinition } from '@grafana/experimental';
|
import { LanguageDefinition } from '@grafana/experimental';
|
||||||
import { TemplateSrv } from '@grafana/runtime';
|
import { TemplateSrv, config } from '@grafana/runtime';
|
||||||
import { SqlDatasource, DB, SQLQuery, SQLSelectableValue, formatSQL } from '@grafana/sql';
|
import {
|
||||||
|
COMMON_FNS,
|
||||||
|
DB,
|
||||||
|
FuncParameter,
|
||||||
|
MACRO_FUNCTIONS,
|
||||||
|
SQLQuery,
|
||||||
|
SQLSelectableValue,
|
||||||
|
SqlDatasource,
|
||||||
|
formatSQL,
|
||||||
|
} from '@grafana/sql';
|
||||||
|
|
||||||
import { PostgresQueryModel } from './PostgresQueryModel';
|
import { PostgresQueryModel } from './PostgresQueryModel';
|
||||||
import { getSchema, getTimescaleDBVersion, getVersion, showTables } from './postgresMetaQuery';
|
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
|
// if no table-name, we are not able to query for fields
|
||||||
return [];
|
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[] = [];
|
const result: SQLSelectableValue[] = [];
|
||||||
for (let i = 0; i < schema.length; i++) {
|
for (let i = 0; i < schema.length; i++) {
|
||||||
const column = schema.fields.column.values[i];
|
const column = schema.fields.column.values[i];
|
||||||
@ -80,6 +93,20 @@ export class PostgresDatasource extends SqlDatasource {
|
|||||||
return result;
|
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 {
|
getDB(): DB {
|
||||||
if (this.db !== undefined) {
|
if (this.db !== undefined) {
|
||||||
return this.db;
|
return this.db;
|
||||||
@ -100,6 +127,7 @@ export class PostgresDatasource extends SqlDatasource {
|
|||||||
Promise.resolve({ isError: false, isValid: true, query, error: '', rawSql: query.rawSql }),
|
Promise.resolve({ isError: false, isValid: true, query, error: '', rawSql: query.rawSql }),
|
||||||
dsID: () => this.id,
|
dsID: () => this.id,
|
||||||
toRawSql,
|
toRawSql,
|
||||||
|
functions: () => this.getFunctions(),
|
||||||
lookup: async () => {
|
lookup: async () => {
|
||||||
const tables = await this.fetchTables();
|
const tables = await this.fetchTables();
|
||||||
return tables.map((t) => ({ name: t, completion: t }));
|
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 { DataSourceInstanceSettings, TimeRange } from '@grafana/data';
|
||||||
import { CompletionItemKind, LanguageDefinition, TableIdentifier } from '@grafana/experimental';
|
import { CompletionItemKind, LanguageDefinition, TableIdentifier } from '@grafana/experimental';
|
||||||
import { getTemplateSrv, TemplateSrv } from '@grafana/runtime';
|
import { TemplateSrv, config, getTemplateSrv } from '@grafana/runtime';
|
||||||
import { DB, formatSQL, SqlDatasource, SQLQuery } from '@grafana/sql';
|
import { COMMON_FNS, DB, FuncParameter, SQLQuery, SqlDatasource, formatSQL } from '@grafana/sql';
|
||||||
|
|
||||||
import { mapFieldsToTypes } from './fields';
|
import { mapFieldsToTypes } from './fields';
|
||||||
import { buildColumnQuery, buildTableQuery } from './flightsqlMetaQuery';
|
import { buildColumnQuery, buildTableQuery } from './flightsqlMetaQuery';
|
||||||
@ -57,7 +59,7 @@ export class FlightSQLDatasource extends SqlDatasource {
|
|||||||
}
|
}
|
||||||
const interpolatedTable = this.templateSrv.replace(query.table);
|
const interpolatedTable = this.templateSrv.replace(query.table);
|
||||||
const queryString = buildColumnQuery(interpolatedTable, query.dataset);
|
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) => ({
|
const fields = frame.map((f) => ({
|
||||||
name: f[0],
|
name: f[0],
|
||||||
text: 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 {
|
getDB(): DB {
|
||||||
if (this.db !== undefined) {
|
if (this.db !== undefined) {
|
||||||
return this.db;
|
return this.db;
|
||||||
@ -114,7 +150,7 @@ export class FlightSQLDatasource extends SqlDatasource {
|
|||||||
Promise.resolve({ query, error: '', isError: false, isValid: true }),
|
Promise.resolve({ query, error: '', isError: false, isValid: true }),
|
||||||
dsID: () => this.id,
|
dsID: () => this.id,
|
||||||
toRawSql,
|
toRawSql,
|
||||||
functions: () => ['VARIANCE', 'STDDEV'],
|
functions: () => this.getFunctions(),
|
||||||
getEditorLanguageDefinition: () => this.getSqlLanguageDefinition(),
|
getEditorLanguageDefinition: () => this.getSqlLanguageDefinition(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -25,6 +25,11 @@ jest.mock('@grafana/runtime', () => ({
|
|||||||
getBackendSrv: () => backendSrv,
|
getBackendSrv: () => backendSrv,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
// mock uuidv4 to give back the same value every time
|
||||||
|
jest.mock('uuid', () => ({
|
||||||
|
v4: () => '0000',
|
||||||
|
}));
|
||||||
|
|
||||||
const instanceSettings = {
|
const instanceSettings = {
|
||||||
id: 1,
|
id: 1,
|
||||||
uid: 'mssql-datasource',
|
uid: 'mssql-datasource',
|
||||||
@ -173,8 +178,7 @@ describe('MSSQLDatasource', () => {
|
|||||||
it('should return a list of fields when fetchFields is called', async () => {
|
it('should return a list of fields when fetchFields is called', async () => {
|
||||||
const fetchFieldsResponse = {
|
const fetchFieldsResponse = {
|
||||||
results: {
|
results: {
|
||||||
columns: {
|
[`columns-0000`]: {
|
||||||
refId: 'columns',
|
|
||||||
frames: [
|
frames: [
|
||||||
dataFrameToJSON(
|
dataFrameToJSON(
|
||||||
createDataFrame({
|
createDataFrame({
|
||||||
|
@ -1,9 +1,20 @@
|
|||||||
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
|
|
||||||
import { DataSourceInstanceSettings, ScopedVars } from '@grafana/data';
|
import { DataSourceInstanceSettings, ScopedVars } from '@grafana/data';
|
||||||
import { LanguageDefinition } from '@grafana/experimental';
|
import { LanguageDefinition } from '@grafana/experimental';
|
||||||
import { TemplateSrv } from '@grafana/runtime';
|
import { TemplateSrv, config } from '@grafana/runtime';
|
||||||
import { DB, SQLQuery, SqlDatasource, SQLSelectableValue, formatSQL } from '@grafana/sql';
|
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 { MSSqlQueryModel } from './MSSqlQueryModel';
|
||||||
import { fetchColumns, fetchTables, getSqlCompletionProvider } from './sqlCompletionProvider';
|
import { fetchColumns, fetchTables, getSqlCompletionProvider } from './sqlCompletionProvider';
|
||||||
import { getIcon, getRAQBType, toRawSql } from './sqlUtil';
|
import { getIcon, getRAQBType, toRawSql } from './sqlUtil';
|
||||||
@ -36,7 +47,7 @@ export class MssqlDatasource extends SqlDatasource {
|
|||||||
}
|
}
|
||||||
const [_, table] = query.table.split('.');
|
const [_, table] = query.table.split('.');
|
||||||
const schema = await this.runSql<{ column: string; type: string }>(getSchema(query.dataset, table), {
|
const schema = await this.runSql<{ column: string; type: string }>(getSchema(query.dataset, table), {
|
||||||
refId: 'columns',
|
refId: `columns-${uuidv4()}`,
|
||||||
});
|
});
|
||||||
const result: SQLSelectableValue[] = [];
|
const result: SQLSelectableValue[] = [];
|
||||||
for (let i = 0; i < schema.length; i++) {
|
for (let i = 0; i < schema.length; i++) {
|
||||||
@ -63,6 +74,20 @@ export class MssqlDatasource extends SqlDatasource {
|
|||||||
return this.sqlLanguageDefinition;
|
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 {
|
getDB(): DB {
|
||||||
if (this.db !== undefined) {
|
if (this.db !== undefined) {
|
||||||
return this.db;
|
return this.db;
|
||||||
@ -83,6 +108,7 @@ export class MssqlDatasource extends SqlDatasource {
|
|||||||
dsID: () => this.id,
|
dsID: () => this.id,
|
||||||
dispose: (_dsID?: string) => {},
|
dispose: (_dsID?: string) => {},
|
||||||
toRawSql,
|
toRawSql,
|
||||||
|
functions: () => this.getFunctions(),
|
||||||
lookup: async (path?: string) => {
|
lookup: async (path?: string) => {
|
||||||
if (!path) {
|
if (!path) {
|
||||||
const datasets = await this.fetchDatasets();
|
const datasets = await this.fetchDatasets();
|
||||||
|
@ -1,6 +1,9 @@
|
|||||||
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
|
|
||||||
import { DataSourceInstanceSettings, TimeRange } from '@grafana/data';
|
import { DataSourceInstanceSettings, TimeRange } from '@grafana/data';
|
||||||
import { CompletionItemKind, LanguageDefinition, TableIdentifier } from '@grafana/experimental';
|
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 { mapFieldsToTypes } from './fields';
|
||||||
import { buildColumnQuery, buildTableQuery, showDatabases } from './mySqlMetaQuery';
|
import { buildColumnQuery, buildTableQuery, showDatabases } from './mySqlMetaQuery';
|
||||||
@ -52,7 +55,7 @@ export class MySqlDatasource extends SqlDatasource {
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
const queryString = buildColumnQuery(query.table, query.dataset);
|
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) => ({
|
const fields = frame.map((f) => ({
|
||||||
name: f[0],
|
name: f[0],
|
||||||
text: 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 {
|
getDB(): DB {
|
||||||
if (this.db !== undefined) {
|
if (this.db !== undefined) {
|
||||||
return this.db;
|
return this.db;
|
||||||
@ -97,7 +115,7 @@ export class MySqlDatasource extends SqlDatasource {
|
|||||||
Promise.resolve({ query, error: '', isError: false, isValid: true }),
|
Promise.resolve({ query, error: '', isError: false, isValid: true }),
|
||||||
dsID: () => this.id,
|
dsID: () => this.id,
|
||||||
toRawSql,
|
toRawSql,
|
||||||
functions: () => ['VARIANCE', 'STDDEV'],
|
functions: () => this.getFunctions(),
|
||||||
getEditorLanguageDefinition: () => this.getSqlLanguageDefinition(),
|
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', () => {
|
describe('MySQLDatasource', () => {
|
||||||
const defaultRange = getDefaultTimeRange(); // it does not matter what value this has
|
const defaultRange = getDefaultTimeRange(); // it does not matter what value this has
|
||||||
const setupTestContext = (response: unknown, templateSrv?: unknown) => {
|
const setupTestContext = (response: unknown, templateSrv?: unknown) => {
|
||||||
@ -134,7 +140,7 @@ describe('MySQLDatasource', () => {
|
|||||||
it('should return a list of fields when fetchFields is called', async () => {
|
it('should return a list of fields when fetchFields is called', async () => {
|
||||||
const fetchFieldsResponse = {
|
const fetchFieldsResponse = {
|
||||||
results: {
|
results: {
|
||||||
fields: {
|
[`fields-${uid}`]: {
|
||||||
refId: 'fields',
|
refId: 'fields',
|
||||||
frames: [
|
frames: [
|
||||||
dataFrameToJSON(
|
dataFrameToJSON(
|
||||||
|
Reference in New Issue
Block a user