mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Variables: Multi-select DataSource variables are inconsistently displayed in the Data source picker (#76039)
Always show multi-select DataSource(DS) variables in the DS picker, and display a warning in the panel when a DataSource variable holds multiple values and is not being repeated. --------- Co-authored-by: Alexandra Vargas <alexa1866@gmail.com> Co-authored-by: Alexa V <239999+axelavargas@users.noreply.github.com> Co-authored-by: Torkel Ödegaard <torkel@grafana.com>
This commit is contained in:
parent
f5e83d07a7
commit
fce78aea2c
@ -4088,7 +4088,10 @@ exports[`better eslint`] = {
|
|||||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
|
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
|
||||||
],
|
],
|
||||||
"public/app/features/templating/template_srv.mock.ts:5381": [
|
"public/app/features/templating/template_srv.mock.ts:5381": [
|
||||||
[0, 0, 0, "Do not use any type assertions.", "0"]
|
[0, 0, 0, "Do not use any type assertions.", "0"],
|
||||||
|
[0, 0, 0, "Do not use any type assertions.", "1"],
|
||||||
|
[0, 0, 0, "Do not use any type assertions.", "2"],
|
||||||
|
[0, 0, 0, "Do not use any type assertions.", "3"]
|
||||||
],
|
],
|
||||||
"public/app/features/templating/template_srv.ts:5381": [
|
"public/app/features/templating/template_srv.ts:5381": [
|
||||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
|
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
|
||||||
|
@ -251,8 +251,10 @@ export class DatasourceSrv implements DataSourceService {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let dsValue = variable.current.value === 'default' ? this.defaultName : variable.current.value;
|
let dsValue = variable.current.value === 'default' ? this.defaultName : variable.current.value;
|
||||||
if (Array.isArray(dsValue) && dsValue.length === 1) {
|
// Support for multi-value DataSource (ds) variables
|
||||||
// Support for multi-value variables with only one selected datasource
|
if (Array.isArray(dsValue)) {
|
||||||
|
// If the ds variable have multiple selected datasources
|
||||||
|
// We will use the first one
|
||||||
dsValue = dsValue[0];
|
dsValue = dsValue[0];
|
||||||
}
|
}
|
||||||
const dsSettings =
|
const dsSettings =
|
||||||
|
@ -3,8 +3,9 @@ const applyFieldOverridesMock = jest.fn(); // needs to be first in this file
|
|||||||
import { Subject } from 'rxjs';
|
import { Subject } from 'rxjs';
|
||||||
|
|
||||||
// Importing this way to be able to spy on grafana/data
|
// Importing this way to be able to spy on grafana/data
|
||||||
|
|
||||||
import * as grafanaData from '@grafana/data';
|
import * as grafanaData from '@grafana/data';
|
||||||
import { DataSourceApi } from '@grafana/data';
|
import { DataSourceApi, TypedVariableModel } from '@grafana/data';
|
||||||
import { DataSourceSrv, setDataSourceSrv, setEchoSrv } from '@grafana/runtime';
|
import { DataSourceSrv, setDataSourceSrv, setEchoSrv } from '@grafana/runtime';
|
||||||
import { TemplateSrvMock } from 'app/features/templating/template_srv.mock';
|
import { TemplateSrvMock } from 'app/features/templating/template_srv.mock';
|
||||||
|
|
||||||
@ -46,7 +47,27 @@ jest.mock('app/features/dashboard/services/DashboardSrv', () => ({
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
jest.mock('app/features/templating/template_srv', () => ({
|
jest.mock('app/features/templating/template_srv', () => ({
|
||||||
getTemplateSrv: () => new TemplateSrvMock({}),
|
...jest.requireActual('app/features/templating/template_srv'),
|
||||||
|
getTemplateSrv: () =>
|
||||||
|
new TemplateSrvMock([
|
||||||
|
{
|
||||||
|
name: 'server',
|
||||||
|
type: 'datasource',
|
||||||
|
current: { text: 'Server1', value: 'server' },
|
||||||
|
options: [{ text: 'Server1', value: 'server1' }],
|
||||||
|
},
|
||||||
|
//multi value variable
|
||||||
|
{
|
||||||
|
name: 'multi',
|
||||||
|
type: 'datasource',
|
||||||
|
multi: true,
|
||||||
|
current: { text: 'Server1,Server2', value: ['server-1', 'server-2'] },
|
||||||
|
options: [
|
||||||
|
{ text: 'Server1', value: 'server1' },
|
||||||
|
{ text: 'Server2', value: 'server2' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
] as TypedVariableModel[]),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
interface ScenarioContext {
|
interface ScenarioContext {
|
||||||
@ -405,4 +426,53 @@ describe('PanelQueryRunner', () => {
|
|||||||
snapshotData,
|
snapshotData,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
describeQueryRunnerScenario(
|
||||||
|
'shouldAddErrorwhenDatasourceVariableIsMultiple',
|
||||||
|
(ctx) => {
|
||||||
|
it('should add error when datasource variable is multiple and not repeated', async () => {
|
||||||
|
// scopedVars is an object that represent the variables repeated in a panel
|
||||||
|
const scopedVars = {
|
||||||
|
server: { text: 'Server1', value: 'server-1' },
|
||||||
|
};
|
||||||
|
|
||||||
|
// We are spying on the replace method of the TemplateSrvMock to check if the custom format function is being called
|
||||||
|
const spyReplace = jest.spyOn(TemplateSrvMock.prototype, 'replace');
|
||||||
|
|
||||||
|
const response = {
|
||||||
|
data: [
|
||||||
|
{
|
||||||
|
target: 'hello',
|
||||||
|
datapoints: [
|
||||||
|
[1, 1000],
|
||||||
|
[2, 2000],
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const datasource = {
|
||||||
|
name: '${multi}',
|
||||||
|
uid: '${multi}',
|
||||||
|
interval: ctx.dsInterval,
|
||||||
|
query: (options: grafanaData.DataQueryRequest) => {
|
||||||
|
ctx.queryCalledWith = options;
|
||||||
|
return Promise.resolve(response);
|
||||||
|
},
|
||||||
|
getRef: () => ({ type: 'test', uid: 'TestDB-uid' }),
|
||||||
|
testDatasource: jest.fn(),
|
||||||
|
} as unknown as DataSourceApi;
|
||||||
|
|
||||||
|
ctx.runner.shouldAddErrorWhenDatasourceVariableIsMultiple(datasource, scopedVars);
|
||||||
|
|
||||||
|
// the test is checking implementation details :(, but it is the only way to check if the error will be added
|
||||||
|
// if the getTemplateSrv.replace is called with the custom format function,it means we will check
|
||||||
|
// if the error should be added
|
||||||
|
expect(spyReplace.mock.calls[0][2]).toBeInstanceOf(Function);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
{
|
||||||
|
...defaultPanelConfig,
|
||||||
|
}
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
@ -275,6 +275,9 @@ export class PanelQueryRunner {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//check if datasource is a variable datasource and if that variable has multiple values
|
||||||
|
const addErroDSVariable = this.shouldAddErrorWhenDatasourceVariableIsMultiple(datasource, scopedVars);
|
||||||
|
|
||||||
const request: DataQueryRequest = {
|
const request: DataQueryRequest = {
|
||||||
app: app ?? CoreApp.Dashboard,
|
app: app ?? CoreApp.Dashboard,
|
||||||
requestId: getNextRequestId(),
|
requestId: getNextRequestId(),
|
||||||
@ -327,7 +330,7 @@ export class PanelQueryRunner {
|
|||||||
|
|
||||||
this.lastRequest = request;
|
this.lastRequest = request;
|
||||||
|
|
||||||
this.pipeToSubject(runRequest(ds, request), panelId);
|
this.pipeToSubject(runRequest(ds, request), panelId, false, addErroDSVariable);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.pipeToSubject(
|
this.pipeToSubject(
|
||||||
of({
|
of({
|
||||||
@ -341,7 +344,12 @@ export class PanelQueryRunner {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private pipeToSubject(observable: Observable<PanelData>, panelId?: number, skipPreProcess = false) {
|
private pipeToSubject(
|
||||||
|
observable: Observable<PanelData>,
|
||||||
|
panelId?: number,
|
||||||
|
skipPreProcess = false,
|
||||||
|
addErroDSVariable = false
|
||||||
|
) {
|
||||||
if (this.subscription) {
|
if (this.subscription) {
|
||||||
this.subscription.unsubscribe();
|
this.subscription.unsubscribe();
|
||||||
}
|
}
|
||||||
@ -379,6 +387,17 @@ export class PanelQueryRunner {
|
|||||||
|
|
||||||
this.lastResult = next;
|
this.lastResult = next;
|
||||||
|
|
||||||
|
//add error message if datasource is a variable and has multiple values
|
||||||
|
if (addErroDSVariable) {
|
||||||
|
next.errors = [
|
||||||
|
{
|
||||||
|
message:
|
||||||
|
'Panel is using a variable datasource with multiple values without repeat option. Please configure the panel to be repeated by the same datasource variable.',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
next.state = LoadingState.Error;
|
||||||
|
}
|
||||||
|
|
||||||
// Store preprocessed query results for applying overrides later on in the pipeline
|
// Store preprocessed query results for applying overrides later on in the pipeline
|
||||||
this.subject.next(next);
|
this.subject.next(next);
|
||||||
},
|
},
|
||||||
@ -451,6 +470,28 @@ export class PanelQueryRunner {
|
|||||||
getLastRequest(): DataQueryRequest | undefined {
|
getLastRequest(): DataQueryRequest | undefined {
|
||||||
return this.lastRequest;
|
return this.lastRequest;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
shouldAddErrorWhenDatasourceVariableIsMultiple(
|
||||||
|
datasource: DataSourceRef | DataSourceApi | null,
|
||||||
|
scopedVars: ScopedVars | undefined
|
||||||
|
): boolean {
|
||||||
|
let addWarningMessageMultipleDatasourceVariable = false;
|
||||||
|
|
||||||
|
//If datasource is a variable
|
||||||
|
if (datasource?.uid?.startsWith('${')) {
|
||||||
|
// we can access the raw datasource variable values inside the replace function if we pass a custom format function
|
||||||
|
this.templateSrv.replace(datasource.uid, scopedVars, (value: string | string[]) => {
|
||||||
|
// if the variable has multiple values it means it's not being repeated
|
||||||
|
if (Array.isArray(value) && value.length > 1) {
|
||||||
|
addWarningMessageMultipleDatasourceVariable = true;
|
||||||
|
}
|
||||||
|
// return empty string to avoid replacing the variable
|
||||||
|
return '';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return addWarningMessageMultipleDatasourceVariable;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getDataSource(
|
async function getDataSource(
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { ScopedVars, TimeRange, TypedVariableModel } from '@grafana/data';
|
import { ScopedVars, TimeRange, TypedVariableModel, VariableOption } from '@grafana/data';
|
||||||
import { TemplateSrv } from '@grafana/runtime';
|
import { TemplateSrv } from '@grafana/runtime';
|
||||||
|
|
||||||
import { variableRegex } from '../variables/utils';
|
import { variableRegex } from '../variables/utils';
|
||||||
@ -13,18 +13,37 @@ import { variableRegex } from '../variables/utils';
|
|||||||
*/
|
*/
|
||||||
export class TemplateSrvMock implements TemplateSrv {
|
export class TemplateSrvMock implements TemplateSrv {
|
||||||
private regex = variableRegex;
|
private regex = variableRegex;
|
||||||
constructor(private variables: Record<string, string>) {}
|
constructor(private variables: TypedVariableModel[]) {}
|
||||||
|
|
||||||
getVariables(): TypedVariableModel[] {
|
getVariables(): TypedVariableModel[] {
|
||||||
return Object.keys(this.variables).map((key) => {
|
if (!this.variables) {
|
||||||
return {
|
return [];
|
||||||
type: 'custom',
|
}
|
||||||
name: key,
|
|
||||||
label: key,
|
return this.variables.reduce((acc: TypedVariableModel[], variable) => {
|
||||||
|
const commonProps = {
|
||||||
|
type: variable.type ?? 'custom',
|
||||||
|
name: variable.name ?? 'test',
|
||||||
|
label: variable.label ?? 'test',
|
||||||
};
|
};
|
||||||
// TODO: we remove this type assertion in a later PR
|
if (variable.type === 'datasource') {
|
||||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
acc.push({
|
||||||
}) as TypedVariableModel[];
|
...commonProps,
|
||||||
|
current: {
|
||||||
|
text: variable.current?.text,
|
||||||
|
value: variable.current?.value,
|
||||||
|
} as VariableOption,
|
||||||
|
options: variable.options ?? [],
|
||||||
|
multi: variable.multi ?? false,
|
||||||
|
includeAll: variable.includeAll ?? false,
|
||||||
|
} as TypedVariableModel);
|
||||||
|
} else {
|
||||||
|
acc.push({
|
||||||
|
...commonProps,
|
||||||
|
} as TypedVariableModel);
|
||||||
|
}
|
||||||
|
return acc as TypedVariableModel[];
|
||||||
|
}, []);
|
||||||
}
|
}
|
||||||
|
|
||||||
replace(target?: string, scopedVars?: ScopedVars, format?: string | Function): string {
|
replace(target?: string, scopedVars?: ScopedVars, format?: string | Function): string {
|
||||||
@ -35,8 +54,7 @@ export class TemplateSrvMock implements TemplateSrv {
|
|||||||
this.regex.lastIndex = 0;
|
this.regex.lastIndex = 0;
|
||||||
|
|
||||||
return target.replace(this.regex, (match, var1, var2, fmt2, var3, fieldPath, fmt3) => {
|
return target.replace(this.regex, (match, var1, var2, fmt2, var3, fieldPath, fmt3) => {
|
||||||
const variableName = var1 || var2 || var3;
|
return var1 || var2 || var3;
|
||||||
return this.variables[variableName];
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user