mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Explore: Expand template variables when redirecting from dashboard panel (#27354)
* Explore: Add interpolateVariablesInQueries to opentsdb datasource * Explore: Add interpolateVariablesInQueries to cloudwatch datasource * Explore: Add interpolateVariablesInQueries to CloudMonitoring datasource * Explore: Add interpolateVariablesInQueries to Azure datasource * Explore: Add dimensions to interpolateMetricsQueryVariables in cloudwatch datasource
This commit is contained in:
parent
acf9938f61
commit
1e2f3ca599
@ -137,7 +137,8 @@ export default class CloudMonitoringDatasource extends DataSourceApi<CloudMonito
|
|||||||
const queries = options.targets
|
const queries = options.targets
|
||||||
.map(this.migrateQuery)
|
.map(this.migrateQuery)
|
||||||
.filter(this.shouldRunQuery)
|
.filter(this.shouldRunQuery)
|
||||||
.map(q => this.prepareTimeSeriesQuery(q, options));
|
.map(q => this.prepareTimeSeriesQuery(q, options.scopedVars))
|
||||||
|
.map(q => ({ ...q, intervalMs: options.intervalMs, type: 'timeSeriesQuery' }));
|
||||||
|
|
||||||
if (queries.length > 0) {
|
if (queries.length > 0) {
|
||||||
const { data } = await this.api.post({
|
const { data } = await this.api.post({
|
||||||
@ -309,13 +310,13 @@ export default class CloudMonitoringDatasource extends DataSourceApi<CloudMonito
|
|||||||
return query;
|
return query;
|
||||||
}
|
}
|
||||||
|
|
||||||
interpolateProps(object: { [key: string]: any } = {}, scopedVars: ScopedVars = {}): { [key: string]: any } {
|
interpolateProps<T extends Record<string, any>>(object: T, scopedVars: ScopedVars = {}): T {
|
||||||
return Object.entries(object).reduce((acc, [key, value]) => {
|
return Object.entries(object).reduce((acc, [key, value]) => {
|
||||||
return {
|
return {
|
||||||
...acc,
|
...acc,
|
||||||
[key]: value && _.isString(value) ? this.templateSrv.replace(value, scopedVars) : value,
|
[key]: value && _.isString(value) ? this.templateSrv.replace(value, scopedVars) : value,
|
||||||
};
|
};
|
||||||
}, {});
|
}, {} as T);
|
||||||
}
|
}
|
||||||
|
|
||||||
shouldRunQuery(query: CloudMonitoringQuery): boolean {
|
shouldRunQuery(query: CloudMonitoringQuery): boolean {
|
||||||
@ -335,14 +336,12 @@ export default class CloudMonitoringDatasource extends DataSourceApi<CloudMonito
|
|||||||
|
|
||||||
prepareTimeSeriesQuery(
|
prepareTimeSeriesQuery(
|
||||||
{ metricQuery, refId, queryType, sloQuery }: CloudMonitoringQuery,
|
{ metricQuery, refId, queryType, sloQuery }: CloudMonitoringQuery,
|
||||||
{ scopedVars, intervalMs }: DataQueryRequest<CloudMonitoringQuery>
|
scopedVars: ScopedVars
|
||||||
) {
|
): CloudMonitoringQuery {
|
||||||
return {
|
return {
|
||||||
datasourceId: this.id,
|
datasourceId: this.id,
|
||||||
refId,
|
refId,
|
||||||
queryType,
|
queryType,
|
||||||
intervalMs: intervalMs,
|
|
||||||
type: 'timeSeriesQuery',
|
|
||||||
metricQuery: {
|
metricQuery: {
|
||||||
...this.interpolateProps(metricQuery, scopedVars),
|
...this.interpolateProps(metricQuery, scopedVars),
|
||||||
projectName: this.templateSrv.replace(
|
projectName: this.templateSrv.replace(
|
||||||
@ -352,10 +351,14 @@ export default class CloudMonitoringDatasource extends DataSourceApi<CloudMonito
|
|||||||
groupBys: this.interpolateGroupBys(metricQuery.groupBys || [], scopedVars),
|
groupBys: this.interpolateGroupBys(metricQuery.groupBys || [], scopedVars),
|
||||||
view: metricQuery.view || 'FULL',
|
view: metricQuery.view || 'FULL',
|
||||||
},
|
},
|
||||||
sloQuery: this.interpolateProps(sloQuery, scopedVars),
|
sloQuery: sloQuery && this.interpolateProps(sloQuery, scopedVars),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interpolateVariablesInQueries(queries: CloudMonitoringQuery[], scopedVars: ScopedVars): CloudMonitoringQuery[] {
|
||||||
|
return queries.map(query => this.prepareTimeSeriesQuery(query, scopedVars));
|
||||||
|
}
|
||||||
|
|
||||||
interpolateFilters(filters: string[], scopedVars: ScopedVars) {
|
interpolateFilters(filters: string[], scopedVars: ScopedVars) {
|
||||||
const completeFilter = _.chunk(filters, 4)
|
const completeFilter = _.chunk(filters, 4)
|
||||||
.map(([key, operator, value, condition]) => ({
|
.map(([key, operator, value, condition]) => ({
|
||||||
|
@ -39,6 +39,7 @@ import {
|
|||||||
MetricQuery,
|
MetricQuery,
|
||||||
MetricRequest,
|
MetricRequest,
|
||||||
TSDBResponse,
|
TSDBResponse,
|
||||||
|
isCloudWatchLogsQuery,
|
||||||
} from './types';
|
} from './types';
|
||||||
import { from, Observable, of, merge, zip } from 'rxjs';
|
import { from, Observable, of, merge, zip } from 'rxjs';
|
||||||
import { catchError, finalize, map, mergeMap, tap, concatMap, scan, share, repeat, takeWhile } from 'rxjs/operators';
|
import { catchError, finalize, map, mergeMap, tap, concatMap, scan, share, repeat, takeWhile } from 'rxjs/operators';
|
||||||
@ -927,12 +928,12 @@ export class CloudWatchDatasource extends DataSourceApi<CloudWatchQuery, CloudWa
|
|||||||
}
|
}
|
||||||
|
|
||||||
replace(
|
replace(
|
||||||
target: string,
|
target?: string,
|
||||||
scopedVars: ScopedVars | undefined,
|
scopedVars?: ScopedVars,
|
||||||
displayErrorIfIsMultiTemplateVariable?: boolean,
|
displayErrorIfIsMultiTemplateVariable?: boolean,
|
||||||
fieldName?: string
|
fieldName?: string
|
||||||
) {
|
) {
|
||||||
if (displayErrorIfIsMultiTemplateVariable) {
|
if (displayErrorIfIsMultiTemplateVariable && !!target) {
|
||||||
const variable = this.templateSrv
|
const variable = this.templateSrv
|
||||||
.getVariables()
|
.getVariables()
|
||||||
.find(({ name }) => name === this.templateSrv.getVariableName(target));
|
.find(({ name }) => name === this.templateSrv.getVariableName(target));
|
||||||
@ -973,6 +974,39 @@ export class CloudWatchDatasource extends DataSourceApi<CloudWatchQuery, CloudWa
|
|||||||
metricsQueries,
|
metricsQueries,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
interpolateVariablesInQueries(queries: CloudWatchQuery[], scopedVars: ScopedVars): CloudWatchQuery[] {
|
||||||
|
if (!queries.length) {
|
||||||
|
return queries;
|
||||||
|
}
|
||||||
|
|
||||||
|
return queries.map(query => ({
|
||||||
|
...query,
|
||||||
|
region: this.getActualRegion(this.replace(query.region, scopedVars)),
|
||||||
|
expression: this.replace(query.expression, scopedVars),
|
||||||
|
|
||||||
|
...(!isCloudWatchLogsQuery(query) && this.interpolateMetricsQueryVariables(query, scopedVars)),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
interpolateMetricsQueryVariables(
|
||||||
|
query: CloudWatchMetricsQuery,
|
||||||
|
scopedVars: ScopedVars
|
||||||
|
): Pick<CloudWatchMetricsQuery, 'alias' | 'metricName' | 'namespace' | 'period' | 'dimensions'> {
|
||||||
|
return {
|
||||||
|
alias: this.replace(query.alias, scopedVars),
|
||||||
|
metricName: this.replace(query.metricName, scopedVars),
|
||||||
|
namespace: this.replace(query.namespace, scopedVars),
|
||||||
|
period: this.replace(query.period, scopedVars),
|
||||||
|
dimensions: Object.entries(query.dimensions).reduce((prev, [key, value]) => {
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
return { ...prev, [key]: value };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { ...prev, [this.replace(key, scopedVars)]: this.replace(value, scopedVars) };
|
||||||
|
}, {}),
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function withTeardown<T = any>(observable: Observable<T>, onUnsubscribe: () => void): Observable<T> {
|
function withTeardown<T = any>(observable: Observable<T>, onUnsubscribe: () => void): Observable<T> {
|
||||||
|
@ -10,13 +10,20 @@ import {
|
|||||||
DataQueryErrorType,
|
DataQueryErrorType,
|
||||||
} from '@grafana/data';
|
} from '@grafana/data';
|
||||||
import { TemplateSrv } from 'app/features/templating/template_srv';
|
import { TemplateSrv } from 'app/features/templating/template_srv';
|
||||||
import { CloudWatchLogsQueryStatus, CloudWatchMetricsQuery, CloudWatchQuery, LogAction } from '../types';
|
import {
|
||||||
|
CloudWatchLogsQueryStatus,
|
||||||
|
CloudWatchMetricsQuery,
|
||||||
|
CloudWatchQuery,
|
||||||
|
LogAction,
|
||||||
|
CloudWatchLogsQuery,
|
||||||
|
} from '../types';
|
||||||
import { backendSrv } from 'app/core/services/backend_srv'; // will use the version in __mocks__
|
import { backendSrv } from 'app/core/services/backend_srv'; // will use the version in __mocks__
|
||||||
import { TimeSrv } from 'app/features/dashboard/services/TimeSrv';
|
import { TimeSrv } from 'app/features/dashboard/services/TimeSrv';
|
||||||
import { convertToStoreState } from '../../../../../test/helpers/convertToStoreState';
|
import { convertToStoreState } from '../../../../../test/helpers/convertToStoreState';
|
||||||
import { getTemplateSrvDependencies } from 'test/helpers/getTemplateSrvDependencies';
|
import { getTemplateSrvDependencies } from 'test/helpers/getTemplateSrvDependencies';
|
||||||
import { of, interval } from 'rxjs';
|
import { of, interval } from 'rxjs';
|
||||||
import { CustomVariableModel, VariableHide } from '../../../../features/variables/types';
|
import { CustomVariableModel, VariableHide } from '../../../../features/variables/types';
|
||||||
|
import { TimeSrvStub } from '../../../../../test/specs/helpers';
|
||||||
|
|
||||||
import * as rxjsUtils from '../utils/rxjs/increasingInterval';
|
import * as rxjsUtils from '../utils/rxjs/increasingInterval';
|
||||||
|
|
||||||
@ -677,6 +684,69 @@ describe('CloudWatchDatasource', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('When interpolating variables', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
|
||||||
|
ctx.mockedTemplateSrv = {
|
||||||
|
replace: jest.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
|
ctx.ds = new CloudWatchDatasource(
|
||||||
|
instanceSettings,
|
||||||
|
ctx.mockedTemplateSrv,
|
||||||
|
(new TimeSrvStub() as unknown) as TimeSrv
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return an empty array if no queries are provided', () => {
|
||||||
|
expect(ctx.ds.interpolateVariablesInQueries([], {})).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should replace correct variables in CloudWatchLogsQuery', () => {
|
||||||
|
const variableName = 'someVar';
|
||||||
|
const logQuery: CloudWatchLogsQuery = {
|
||||||
|
id: 'someId',
|
||||||
|
refId: 'someRefId',
|
||||||
|
queryMode: 'Logs',
|
||||||
|
expression: `$${variableName}`,
|
||||||
|
region: `$${variableName}`,
|
||||||
|
};
|
||||||
|
|
||||||
|
ctx.ds.interpolateVariablesInQueries([logQuery], {});
|
||||||
|
|
||||||
|
// We interpolate `expression` and `region` in CloudWatchLogsQuery
|
||||||
|
expect(ctx.mockedTemplateSrv.replace).toHaveBeenCalledWith(`$${variableName}`, {});
|
||||||
|
expect(ctx.mockedTemplateSrv.replace).toHaveBeenCalledTimes(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should replace correct variables in CloudWatchMetricsQuery', () => {
|
||||||
|
const variableName = 'someVar';
|
||||||
|
const logQuery: CloudWatchMetricsQuery = {
|
||||||
|
id: 'someId',
|
||||||
|
refId: 'someRefId',
|
||||||
|
queryMode: 'Metrics',
|
||||||
|
expression: `$${variableName}`,
|
||||||
|
region: `$${variableName}`,
|
||||||
|
period: `$${variableName}`,
|
||||||
|
alias: `$${variableName}`,
|
||||||
|
metricName: `$${variableName}`,
|
||||||
|
namespace: `$${variableName}`,
|
||||||
|
dimensions: {
|
||||||
|
[`$${variableName}`]: `$${variableName}`,
|
||||||
|
},
|
||||||
|
matchExact: false,
|
||||||
|
statistics: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
ctx.ds.interpolateVariablesInQueries([logQuery], {});
|
||||||
|
|
||||||
|
// We interpolate `expression`, `region`, `period`, `alias`, `metricName`, `nameSpace` and `dimensions` in CloudWatchMetricsQuery
|
||||||
|
expect(ctx.mockedTemplateSrv.replace).toHaveBeenCalledWith(`$${variableName}`, {});
|
||||||
|
expect(ctx.mockedTemplateSrv.replace).toHaveBeenCalledTimes(8);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('When performing CloudWatch query for extended statistics', () => {
|
describe('When performing CloudWatch query for extended statistics', () => {
|
||||||
const query = {
|
const query = {
|
||||||
range: defaultTimeRange,
|
range: defaultTimeRange,
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { DataQuery, SelectableValue, DataSourceJsonData } from '@grafana/data';
|
import { DataQuery, SelectableValue, DataSourceJsonData } from '@grafana/data';
|
||||||
|
|
||||||
export interface CloudWatchMetricsQuery extends DataQuery {
|
export interface CloudWatchMetricsQuery extends DataQuery {
|
||||||
queryMode: 'Metrics';
|
queryMode?: 'Metrics';
|
||||||
|
|
||||||
id: string;
|
id: string;
|
||||||
region: string;
|
region: string;
|
||||||
@ -44,6 +44,9 @@ export interface CloudWatchLogsQuery extends DataQuery {
|
|||||||
|
|
||||||
export type CloudWatchQuery = CloudWatchMetricsQuery | CloudWatchLogsQuery;
|
export type CloudWatchQuery = CloudWatchMetricsQuery | CloudWatchLogsQuery;
|
||||||
|
|
||||||
|
export const isCloudWatchLogsQuery = (cloudwatchQuery: CloudWatchQuery): cloudwatchQuery is CloudWatchLogsQuery =>
|
||||||
|
(cloudwatchQuery as CloudWatchLogsQuery).queryMode === 'Logs';
|
||||||
|
|
||||||
export interface AnnotationQuery extends CloudWatchMetricsQuery {
|
export interface AnnotationQuery extends CloudWatchMetricsQuery {
|
||||||
prefixMatching: boolean;
|
prefixMatching: boolean;
|
||||||
actionPrefix: string;
|
actionPrefix: string;
|
||||||
|
@ -9,6 +9,7 @@ import {
|
|||||||
DataSourceInstanceSettings,
|
DataSourceInstanceSettings,
|
||||||
DataQueryResponseData,
|
DataQueryResponseData,
|
||||||
LoadingState,
|
LoadingState,
|
||||||
|
ScopedVars,
|
||||||
} from '@grafana/data';
|
} from '@grafana/data';
|
||||||
import { Observable, of, from } from 'rxjs';
|
import { Observable, of, from } from 'rxjs';
|
||||||
import { DataSourceWithBackend } from '@grafana/runtime';
|
import { DataSourceWithBackend } from '@grafana/runtime';
|
||||||
@ -255,4 +256,10 @@ export default class Datasource extends DataSourceApi<AzureMonitorQuery, AzureDa
|
|||||||
getSubscriptions() {
|
getSubscriptions() {
|
||||||
return this.azureMonitorDatasource.getSubscriptions();
|
return this.azureMonitorDatasource.getSubscriptions();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interpolateVariablesInQueries(queries: AzureMonitorQuery[], scopedVars: ScopedVars): AzureMonitorQuery[] {
|
||||||
|
return queries.map(
|
||||||
|
query => this.pseudoDatasource[query.queryType].applyTemplateVariables(query, scopedVars) as AzureMonitorQuery
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import angular from 'angular';
|
import angular from 'angular';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import { dateMath, DataQueryRequest, DataSourceApi } from '@grafana/data';
|
import { dateMath, DataQueryRequest, DataSourceApi, ScopedVars } from '@grafana/data';
|
||||||
import { getBackendSrv } from '@grafana/runtime';
|
import { getBackendSrv } from '@grafana/runtime';
|
||||||
import { TemplateSrv } from 'app/features/templating/template_srv';
|
import { TemplateSrv } from 'app/features/templating/template_srv';
|
||||||
import { OpenTsdbOptions, OpenTsdbQuery } from './types';
|
import { OpenTsdbOptions, OpenTsdbQuery } from './types';
|
||||||
@ -493,6 +493,17 @@ export default class OpenTsDatasource extends DataSourceApi<OpenTsdbQuery, OpenT
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interpolateVariablesInQueries(queries: OpenTsdbQuery[], scopedVars: ScopedVars): OpenTsdbQuery[] {
|
||||||
|
if (!queries.length) {
|
||||||
|
return queries;
|
||||||
|
}
|
||||||
|
|
||||||
|
return queries.map(query => ({
|
||||||
|
...query,
|
||||||
|
metric: this.templateSrv.replace(query.metric, scopedVars),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
convertToTSDBTime(date: any, roundUp: any, timezone: any) {
|
convertToTSDBTime(date: any, roundUp: any, timezone: any) {
|
||||||
if (date === 'now') {
|
if (date === 'now') {
|
||||||
return null;
|
return null;
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import OpenTsDatasource from '../datasource';
|
import OpenTsDatasource from '../datasource';
|
||||||
import { backendSrv } from 'app/core/services/backend_srv'; // will use the version in __mocks__
|
import { backendSrv } from 'app/core/services/backend_srv'; // will use the version in __mocks__
|
||||||
|
import { OpenTsdbQuery } from '../types';
|
||||||
|
|
||||||
jest.mock('@grafana/runtime', () => ({
|
jest.mock('@grafana/runtime', () => ({
|
||||||
...((jest.requireActual('@grafana/runtime') as unknown) as object),
|
...((jest.requireActual('@grafana/runtime') as unknown) as object),
|
||||||
@ -108,4 +109,33 @@ describe('opentsdb', () => {
|
|||||||
expect(requestOptions.params.q).toBe('bar');
|
expect(requestOptions.params.q).toBe('bar');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('When interpolating variables', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
|
||||||
|
ctx.mockedTemplateSrv = {
|
||||||
|
replace: jest.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
|
ctx.ds = new OpenTsDatasource(instanceSettings, ctx.mockedTemplateSrv);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return an empty array if no queries are provided', () => {
|
||||||
|
expect(ctx.ds.interpolateVariablesInQueries([], {})).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should replace correct variables', () => {
|
||||||
|
const variableName = 'someVar';
|
||||||
|
const logQuery: OpenTsdbQuery = {
|
||||||
|
refId: 'someRefId',
|
||||||
|
metric: `$${variableName}`,
|
||||||
|
};
|
||||||
|
|
||||||
|
ctx.ds.interpolateVariablesInQueries([logQuery], {});
|
||||||
|
|
||||||
|
expect(ctx.mockedTemplateSrv.replace).toHaveBeenCalledWith(`$${variableName}`, {});
|
||||||
|
expect(ctx.mockedTemplateSrv.replace).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
Loading…
Reference in New Issue
Block a user