Azure Monitor: Add Resource Picker to Template Variable Query Editor (#40841)

Azure Monitor: Add Resource Picker to Template Variable Query Editor
- Should fix bug related broken template variables that relied on a deprecated default workspace.
This commit is contained in:
Sarah Zinger 2021-11-02 15:30:15 -04:00 committed by GitHub
parent 7b7c193551
commit 3da4e1cdcd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 1277 additions and 798 deletions

View File

@ -5,7 +5,7 @@ type DeepPartial<T> = {
[P in keyof T]?: DeepPartial<T[P]>;
};
export default function createMockDatasource() {
export default function createMockDatasource(overrides?: DeepPartial<Datasource>) {
// We make this a partial so we get _some_ kind of type safety when making this, rather than
// having it be any or casted immediately to Datasource
const _mockDatasource: DeepPartial<Datasource> = {
@ -16,6 +16,7 @@ export default function createMockDatasource() {
return true;
},
getSubscriptions: jest.fn().mockResolvedValueOnce([]),
defaultSubscriptionId: 'subscriptionId',
},
getAzureLogAnalyticsWorkspaces: jest.fn().mockResolvedValueOnce([]),
@ -34,12 +35,14 @@ export default function createMockDatasource() {
azureLogAnalyticsDatasource: {
getKustoSchema: () => Promise.resolve(),
getDeprecatedDefaultWorkSpace: () => 'defaultWorkspaceId',
},
resourcePickerData: {
getResourcePickerData: () => ({}),
getResourcesForResourceGroup: () => ({}),
getResourceURIFromWorkspace: () => '',
},
...overrides,
};
const mockDatasource = _mockDatasource as Datasource;

View File

@ -394,73 +394,6 @@ describe('AppInsightsDatasource', () => {
});
});
describe('When performing metricFindQuery', () => {
describe('with a metric names query', () => {
const response = {
metrics: {
'exceptions/server': {},
'requests/count': {},
},
};
beforeEach(() => {
ctx.ds.getResource = jest.fn().mockImplementation((path) => {
expect(path).toContain('/metrics/metadata');
return Promise.resolve(response);
});
});
it('should return a list of metric names', () => {
return ctx.ds.metricFindQueryInternal('appInsightsMetricNames()').then((results: any) => {
expect(results.length).toBe(2);
expect(results[0].text).toBe('exceptions/server');
expect(results[0].value).toBe('exceptions/server');
expect(results[1].text).toBe('requests/count');
expect(results[1].value).toBe('requests/count');
});
});
});
describe('with metadata group by query', () => {
const response = {
metrics: {
'exceptions/server': {
supportedAggregations: ['sum'],
supportedGroupBy: {
all: ['client/os', 'client/city', 'client/browser'],
},
defaultAggregation: 'sum',
},
'requests/count': {
supportedAggregations: ['avg', 'sum', 'total'],
supportedGroupBy: {
all: ['client/os', 'client/city', 'client/browser'],
},
defaultAggregation: 'avg',
},
},
};
beforeEach(() => {
ctx.ds.getResource = jest.fn().mockImplementation((path) => {
expect(path).toContain('/metrics/metadata');
return Promise.resolve(response);
});
});
it('should return a list of group bys', () => {
return ctx.ds.metricFindQueryInternal('appInsightsGroupBys(requests/count)').then((results: any) => {
expect(results[0].text).toContain('client/os');
expect(results[0].value).toContain('client/os');
expect(results[1].text).toContain('client/city');
expect(results[1].value).toContain('client/city');
expect(results[2].text).toContain('client/browser');
expect(results[2].value).toContain('client/browser');
});
});
});
});
describe('When getting Metric Names', () => {
const response = {
metrics: {

View File

@ -1,4 +1,4 @@
import { DataQueryRequest, DataSourceInstanceSettings, ScopedVars, MetricFindValue } from '@grafana/data';
import { DataQueryRequest, DataSourceInstanceSettings, ScopedVars } from '@grafana/data';
import { getTemplateSrv, DataSourceWithBackend } from '@grafana/runtime';
import { isString } from 'lodash';
@ -106,27 +106,6 @@ export default class AppInsightsDatasource extends DataSourceWithBackend<AzureMo
};
}
/**
* This is named differently than DataSourceApi.metricFindQuery
* because it's not exposed to Grafana like the main AzureMonitorDataSource.
* And some of the azure internal data sources return null in this function, which the
* external interface does not support
*/
metricFindQueryInternal(query: string): Promise<MetricFindValue[]> | null {
const appInsightsMetricNameQuery = query.match(/^AppInsightsMetricNames\(\)/i);
if (appInsightsMetricNameQuery) {
return this.getMetricNames();
}
const appInsightsGroupByQuery = query.match(/^AppInsightsGroupBys\(([^\)]+?)(,\s?([^,]+?))?\)/i);
if (appInsightsGroupByQuery) {
const metricName = appInsightsGroupByQuery[1];
return this.getGroupBys(getTemplateSrv().replace(metricName));
}
return null;
}
testDatasource(): Promise<DatasourceValidationResult> {
const path = `${this.resourcePath}/metrics/metadata`;
return this.getResource(path)

View File

@ -2,7 +2,7 @@ import AzureMonitorDatasource from '../datasource';
import AzureLogAnalyticsDatasource from './azure_log_analytics_datasource';
import FakeSchemaData from './__mocks__/schema';
import { TemplateSrv } from 'app/features/templating/template_srv';
import { AzureLogsVariable, AzureMonitorQuery, DatasourceValidationResult } from '../types';
import { AzureMonitorQuery, DatasourceValidationResult } from '../types';
import { toUtc } from '@grafana/data';
const templateSrv = new TemplateSrv();
@ -111,157 +111,6 @@ describe('AzureLogAnalyticsDatasource', () => {
});
});
describe('When performing metricFindQuery', () => {
let queryResults: AzureLogsVariable[];
const workspacesResponse = {
value: [
{
name: 'workspace1',
id: makeResourceURI('workspace-1'),
properties: {
customerId: 'eeee4fde-1aaa-4d60-9974-eeee562ffaa1',
},
},
{
name: 'workspace2',
id: makeResourceURI('workspace-2'),
properties: {
customerId: 'eeee4fde-1aaa-4d60-9974-eeee562ffaa2',
},
},
],
};
describe('and is the workspaces() macro', () => {
beforeEach(async () => {
ctx.ds.azureLogAnalyticsDatasource.getResource = jest.fn().mockImplementation((path: string) => {
expect(path).toContain('xxx');
return Promise.resolve(workspacesResponse);
});
queryResults = await ctx.ds.metricFindQuery('workspaces()');
});
it('should return a list of workspaces', () => {
expect(queryResults).toEqual([
{ text: 'workspace1', value: makeResourceURI('workspace-1') },
{ text: 'workspace2', value: makeResourceURI('workspace-2') },
]);
});
});
describe('and is the workspaces() macro with the subscription parameter', () => {
beforeEach(async () => {
ctx.ds.azureLogAnalyticsDatasource.getResource = jest.fn().mockImplementation((path: string) => {
expect(path).toContain('11112222-eeee-4949-9b2d-9106972f9123');
return Promise.resolve(workspacesResponse);
});
queryResults = await ctx.ds.metricFindQuery('workspaces(11112222-eeee-4949-9b2d-9106972f9123)');
});
it('should return a list of workspaces', () => {
expect(queryResults).toEqual([
{ text: 'workspace1', value: makeResourceURI('workspace-1') },
{ text: 'workspace2', value: makeResourceURI('workspace-2') },
]);
});
});
describe('and is the workspaces() macro with the subscription parameter quoted', () => {
beforeEach(async () => {
ctx.ds.azureLogAnalyticsDatasource.getResource = jest.fn().mockImplementation((path: string) => {
expect(path).toContain('11112222-eeee-4949-9b2d-9106972f9123');
return Promise.resolve(workspacesResponse);
});
queryResults = await ctx.ds.metricFindQuery('workspaces("11112222-eeee-4949-9b2d-9106972f9123")');
});
it('should return a list of workspaces', () => {
expect(queryResults).toEqual([
{ text: 'workspace1', value: makeResourceURI('workspace-1') },
{ text: 'workspace2', value: makeResourceURI('workspace-2') },
]);
});
});
describe('and is a custom query', () => {
const tableResponseWithOneColumn = {
tables: [
{
name: 'PrimaryResult',
columns: [
{
name: 'Category',
type: 'string',
},
],
rows: [['Administrative'], ['Policy']],
},
],
};
const workspaceResponse = {
value: [
{
name: 'aworkspace',
id: makeResourceURI('a-workspace'),
properties: {
source: 'Azure',
customerId: 'abc1b44e-3e57-4410-b027-6cc0ae6dee67',
},
},
],
};
beforeEach(async () => {
ctx.ds.azureLogAnalyticsDatasource.getResource = jest.fn().mockImplementation((path: string) => {
if (path.indexOf('OperationalInsights/workspaces?api-version=') > -1) {
return Promise.resolve(workspaceResponse);
} else {
return Promise.resolve(tableResponseWithOneColumn);
}
});
});
it('should return a list of categories in the correct format', async () => {
const results = await ctx.ds.metricFindQuery('workspace("aworkspace").AzureActivity | distinct Category');
expect(results.length).toBe(2);
expect(results[0].text).toBe('Administrative');
expect(results[0].value).toBe('Administrative');
expect(results[1].text).toBe('Policy');
expect(results[1].value).toBe('Policy');
});
});
describe('and contain options', () => {
const queryResponse = {
tables: [],
};
it('should substitute macros', async () => {
ctx.ds.azureLogAnalyticsDatasource.getResource = jest.fn().mockImplementation((path: string) => {
const params = new URLSearchParams(path.split('?')[1]);
const query = params.get('query');
expect(query).toEqual(
'Perf| where TimeGenerated >= datetime(2021-01-01T05:01:00.000Z) and TimeGenerated <= datetime(2021-01-01T05:02:00.000Z)'
);
return Promise.resolve(queryResponse);
});
ctx.ds.azureLogAnalyticsDatasource.firstWorkspace = 'foo';
await ctx.ds.metricFindQuery('Perf| where TimeGenerated >= $__timeFrom() and TimeGenerated <= $__timeTo()', {
range: {
from: new Date('2021-01-01 00:01:00'),
to: new Date('2021-01-01 00:02:00'),
},
});
});
});
});
describe('When performing annotationQuery', () => {
const tableResponse = {
tables: [

View File

@ -8,13 +8,7 @@ import {
AzureQueryType,
DatasourceValidationResult,
} from '../types';
import {
DataQueryRequest,
DataQueryResponse,
ScopedVars,
DataSourceInstanceSettings,
MetricFindValue,
} from '@grafana/data';
import { DataQueryRequest, DataQueryResponse, ScopedVars, DataSourceInstanceSettings } from '@grafana/data';
import { getTemplateSrv, DataSourceWithBackend } from '@grafana/runtime';
import { Observable, from } from 'rxjs';
import { mergeMap } from 'rxjs/operators';
@ -218,60 +212,12 @@ export default class AzureLogAnalyticsDatasource extends DataSourceWithBackend<
};
}
/**
* This is named differently than DataSourceApi.metricFindQuery
* because it's not exposed to Grafana like the main AzureMonitorDataSource.
* And some of the azure internal data sources return null in this function, which the
* external interface does not support
*/
metricFindQueryInternal(query: string, optionalOptions?: unknown): Promise<MetricFindValue[]> {
// workspaces() - Get workspaces in the default subscription
const workspacesQuery = query.match(/^workspaces\(\)/i);
if (workspacesQuery) {
if (this.defaultSubscriptionId) {
return this.getWorkspaces(this.defaultSubscriptionId);
} else {
throw new Error(
'No subscription ID. Specify a default subscription ID in the data source config to use workspaces() without a subscription ID'
);
}
}
// workspaces("abc-def-etc") - Get workspaces a specified subscription
const workspacesQueryWithSub = query.match(/^workspaces\(["']?([^\)]+?)["']?\)/i);
if (workspacesQueryWithSub) {
return this.getWorkspaces((workspacesQueryWithSub[1] || '').trim());
}
// Execute the query as KQL to the default or first workspace
return this.getFirstWorkspace().then((resourceURI) => {
if (!resourceURI) {
return [];
}
const queries = this.buildQuery(query, optionalOptions, resourceURI);
const promises = this.doQueries(queries);
return Promise.all(promises)
.then((results) => {
return new ResponseParser(results).parseToVariables();
})
.catch((err) => {
if (
err.error &&
err.error.data &&
err.error.data.error &&
err.error.data.error.innererror &&
err.error.data.error.innererror.innererror
) {
throw { message: err.error.data.error.innererror.innererror.message };
} else if (err.error && err.error.data && err.error.data.error) {
throw { message: err.error.data.error.message };
}
throw err;
});
}) as Promise<MetricFindValue[]>;
/*
In 7.5.x it used to be possible to set a default workspace id in the config on the auth page.
This has been deprecated, however is still used by a few legacy template queries.
*/
getDeprecatedDefaultWorkSpace() {
return this.instanceSettings.jsonData.logAnalyticsDefaultWorkspace;
}
private buildQuery(query: string, options: any, workspace: string): AdhocQuery[] {

View File

@ -73,358 +73,6 @@ describe('AzureMonitorDatasource', () => {
});
});
});
describe('When performing metricFindQuery', () => {
describe('with a subscriptions query', () => {
const response = {
value: [
{ displayName: 'Primary', subscriptionId: 'sub1' },
{ displayName: 'Secondary', subscriptionId: 'sub2' },
],
};
beforeEach(() => {
ctx.instanceSettings.jsonData.azureAuthType = 'msi';
ctx.ds.azureMonitorDatasource.getResource = jest.fn().mockResolvedValue(response);
});
it('should return a list of subscriptions', async () => {
const results = await ctx.ds.metricFindQuery('subscriptions()');
expect(results.length).toBe(2);
expect(results[0].text).toBe('Primary');
expect(results[0].value).toBe('sub1');
expect(results[1].text).toBe('Secondary');
expect(results[1].value).toBe('sub2');
});
});
describe('with a resource groups query', () => {
const response = {
value: [{ name: 'grp1' }, { name: 'grp2' }],
};
beforeEach(() => {
ctx.ds.azureMonitorDatasource.getResource = jest.fn().mockResolvedValue(response);
});
it('should return a list of resource groups', async () => {
const results = await ctx.ds.metricFindQuery('ResourceGroups()');
expect(results.length).toBe(2);
expect(results[0].text).toBe('grp1');
expect(results[0].value).toBe('grp1');
expect(results[1].text).toBe('grp2');
expect(results[1].value).toBe('grp2');
});
});
describe('with a resource groups query that specifies a subscription id', () => {
const response = {
value: [{ name: 'grp1' }, { name: 'grp2' }],
};
beforeEach(() => {
ctx.ds.azureMonitorDatasource.getResource = jest.fn().mockImplementation((path: string) => {
expect(path).toContain('11112222-eeee-4949-9b2d-9106972f9123');
return Promise.resolve(response);
});
});
it('should return a list of resource groups', async () => {
const results = await ctx.ds.metricFindQuery('ResourceGroups(11112222-eeee-4949-9b2d-9106972f9123)');
expect(results.length).toBe(2);
expect(results[0].text).toBe('grp1');
expect(results[0].value).toBe('grp1');
expect(results[1].text).toBe('grp2');
expect(results[1].value).toBe('grp2');
});
});
describe('with namespaces query', () => {
const response = {
value: [
{
name: 'test',
type: 'Microsoft.Network/networkInterfaces',
},
],
};
beforeEach(() => {
ctx.ds.azureMonitorDatasource.getResource = jest.fn().mockImplementation((path: string) => {
const basePath = 'azuremonitor/subscriptions/9935389e-9122-4ef9-95f9-1513dd24753f/resourceGroups';
expect(path).toBe(basePath + '/nodesapp/resources?api-version=2018-01-01');
return Promise.resolve(response);
});
});
it('should return a list of namespaces', async () => {
const results = await ctx.ds.metricFindQuery('Namespaces(nodesapp)');
expect(results.length).toEqual(1);
expect(results[0].text).toEqual('Network interface');
expect(results[0].value).toEqual('Microsoft.Network/networkInterfaces');
});
});
describe('with namespaces query that specifies a subscription id', () => {
const response = {
value: [
{
name: 'test',
type: 'Microsoft.Network/networkInterfaces',
},
],
};
beforeEach(() => {
ctx.ds.azureMonitorDatasource.getResource = jest.fn().mockImplementation((path: string) => {
const basePath = 'azuremonitor/subscriptions/11112222-eeee-4949-9b2d-9106972f9123/resourceGroups';
expect(path).toBe(basePath + '/nodesapp/resources?api-version=2018-01-01');
return Promise.resolve(response);
});
});
it('should return a list of namespaces', async () => {
const results = await ctx.ds.metricFindQuery('namespaces(11112222-eeee-4949-9b2d-9106972f9123, nodesapp)');
expect(results.length).toEqual(1);
expect(results[0].text).toEqual('Network interface');
expect(results[0].value).toEqual('Microsoft.Network/networkInterfaces');
});
});
describe('with resource names query', () => {
const response = {
value: [
{
name: 'Failure Anomalies - nodeapp',
type: 'microsoft.insights/alertrules',
},
{
name: 'nodeapp',
type: 'microsoft.insights/components',
},
],
};
beforeEach(() => {
ctx.ds.azureMonitorDatasource.getResource = jest.fn().mockImplementation((path: string) => {
const basePath = 'azuremonitor/subscriptions/9935389e-9122-4ef9-95f9-1513dd24753f/resourceGroups';
expect(path).toBe(basePath + '/nodeapp/resources?api-version=2018-01-01');
return Promise.resolve(response);
});
});
it('should return a list of resource names', async () => {
const results = await ctx.ds.metricFindQuery('resourceNames(nodeapp, microsoft.insights/components )');
expect(results.length).toEqual(1);
expect(results[0].text).toEqual('nodeapp');
expect(results[0].value).toEqual('nodeapp');
});
});
describe('with resource names query and that specifies a subscription id', () => {
const response = {
value: [
{
name: 'Failure Anomalies - nodeapp',
type: 'microsoft.insights/alertrules',
},
{
name: 'nodeapp',
type: 'microsoft.insights/components',
},
],
};
beforeEach(() => {
ctx.ds.azureMonitorDatasource.getResource = jest.fn().mockImplementation((path: string) => {
const basePath = 'azuremonitor/subscriptions/11112222-eeee-4949-9b2d-9106972f9123/resourceGroups';
expect(path).toBe(basePath + '/nodeapp/resources?api-version=2018-01-01');
return Promise.resolve(response);
});
});
it('should return a list of resource names', () => {
return ctx.ds
.metricFindQuery(
'resourceNames(11112222-eeee-4949-9b2d-9106972f9123, nodeapp, microsoft.insights/components )'
)
.then((results: any) => {
expect(results.length).toEqual(1);
expect(results[0].text).toEqual('nodeapp');
expect(results[0].value).toEqual('nodeapp');
});
});
});
describe('with metric names query', () => {
const response = {
value: [
{
name: {
value: 'Percentage CPU',
localizedValue: 'Percentage CPU',
},
},
{
name: {
value: 'UsedCapacity',
localizedValue: 'Used capacity',
},
},
],
};
beforeEach(() => {
ctx.ds.azureMonitorDatasource.getResource = jest.fn().mockImplementation((path: string) => {
const basePath = 'azuremonitor/subscriptions/9935389e-9122-4ef9-95f9-1513dd24753f/resourceGroups';
expect(path).toBe(
basePath +
'/nodeapp/providers/microsoft.insights/components/rn/providers/microsoft.insights/' +
'metricdefinitions?api-version=2018-01-01&metricnamespace=default'
);
return Promise.resolve(response);
});
});
it('should return a list of metric names', async () => {
const results = await ctx.ds.metricFindQuery(
'Metricnames(nodeapp, microsoft.insights/components, rn, default)'
);
expect(results.length).toEqual(2);
expect(results[0].text).toEqual('Percentage CPU');
expect(results[0].value).toEqual('Percentage CPU');
expect(results[1].text).toEqual('Used capacity');
expect(results[1].value).toEqual('UsedCapacity');
});
});
describe('with metric names query and specifies a subscription id', () => {
const response = {
value: [
{
name: {
value: 'Percentage CPU',
localizedValue: 'Percentage CPU',
},
},
{
name: {
value: 'UsedCapacity',
localizedValue: 'Used capacity',
},
},
],
};
beforeEach(() => {
ctx.ds.azureMonitorDatasource.getResource = jest.fn().mockImplementation((path: string) => {
const basePath = 'azuremonitor/subscriptions/11112222-eeee-4949-9b2d-9106972f9123/resourceGroups';
expect(path).toBe(
basePath +
'/nodeapp/providers/microsoft.insights/components/rn/providers/microsoft.insights/' +
'metricdefinitions?api-version=2018-01-01&metricnamespace=default'
);
return Promise.resolve(response);
});
});
it('should return a list of metric names', async () => {
const results = await ctx.ds.metricFindQuery(
'Metricnames(11112222-eeee-4949-9b2d-9106972f9123, nodeapp, microsoft.insights/components, rn, default)'
);
expect(results.length).toEqual(2);
expect(results[0].text).toEqual('Percentage CPU');
expect(results[0].value).toEqual('Percentage CPU');
expect(results[1].text).toEqual('Used capacity');
expect(results[1].value).toEqual('UsedCapacity');
});
});
describe('with metric namespace query', () => {
const response = {
value: [
{
name: 'Microsoft.Compute-virtualMachines',
properties: {
metricNamespaceName: 'Microsoft.Compute/virtualMachines',
},
},
{
name: 'Telegraf-mem',
properties: {
metricNamespaceName: 'Telegraf/mem',
},
},
],
};
beforeEach(() => {
ctx.ds.azureMonitorDatasource.getResource = jest.fn().mockImplementation((path: string) => {
const basePath = 'azuremonitor/subscriptions/9935389e-9122-4ef9-95f9-1513dd24753f/resourceGroups';
expect(path).toBe(
basePath +
'/nodeapp/providers/Microsoft.Compute/virtualMachines/rn/providers/microsoft.insights/metricNamespaces?api-version=2017-12-01-preview'
);
return Promise.resolve(response);
});
});
it('should return a list of metric names', async () => {
const results = await ctx.ds.metricFindQuery('Metricnamespace(nodeapp, Microsoft.Compute/virtualMachines, rn)');
expect(results.length).toEqual(2);
expect(results[0].text).toEqual('Microsoft.Compute-virtualMachines');
expect(results[0].value).toEqual('Microsoft.Compute/virtualMachines');
expect(results[1].text).toEqual('Telegraf-mem');
expect(results[1].value).toEqual('Telegraf/mem');
});
});
describe('with metric namespace query and specifies a subscription id', () => {
const response = {
value: [
{
name: 'Microsoft.Compute-virtualMachines',
properties: {
metricNamespaceName: 'Microsoft.Compute/virtualMachines',
},
},
{
name: 'Telegraf-mem',
properties: {
metricNamespaceName: 'Telegraf/mem',
},
},
],
};
beforeEach(() => {
ctx.ds.azureMonitorDatasource.getResource = jest.fn().mockImplementation((path: string) => {
const basePath = 'azuremonitor/subscriptions/11112222-eeee-4949-9b2d-9106972f9123/resourceGroups';
expect(path).toBe(
basePath +
'/nodeapp/providers/Microsoft.Compute/virtualMachines/rn/providers/microsoft.insights/metricNamespaces?api-version=2017-12-01-preview'
);
return Promise.resolve(response);
});
});
it('should return a list of metric namespaces', async () => {
const results = await ctx.ds.metricFindQuery(
'Metricnamespace(11112222-eeee-4949-9b2d-9106972f9123, nodeapp, Microsoft.Compute/virtualMachines, rn)'
);
expect(results.length).toEqual(2);
expect(results[0].text).toEqual('Microsoft.Compute-virtualMachines');
expect(results[0].value).toEqual('Microsoft.Compute/virtualMachines');
expect(results[1].text).toEqual('Telegraf-mem');
expect(results[1].value).toEqual('Telegraf/mem');
});
});
});
describe('When performing getSubscriptions', () => {
const response = {
value: [

View File

@ -11,7 +11,7 @@ import {
AzureQueryType,
DatasourceValidationResult,
} from '../types';
import { DataSourceInstanceSettings, ScopedVars, MetricFindValue } from '@grafana/data';
import { DataSourceInstanceSettings, ScopedVars } from '@grafana/data';
import { DataSourceWithBackend, getTemplateSrv } from '@grafana/runtime';
import { getTimeSrv, TimeSrv } from 'app/features/dashboard/services/TimeSrv';
@ -122,114 +122,6 @@ export default class AzureMonitorDatasource extends DataSourceWithBackend<AzureM
};
}
/**
* This is named differently than DataSourceApi.metricFindQuery
* because it's not exposed to Grafana like the main AzureMonitorDataSource.
* And some of the azure internal data sources return null in this function, which the
* external interface does not support
*/
metricFindQueryInternal(query: string): Promise<MetricFindValue[]> | null {
const subscriptionsQuery = query.match(/^Subscriptions\(\)/i);
if (subscriptionsQuery) {
return this.getSubscriptions();
}
const resourceGroupsQuery = query.match(/^ResourceGroups\(\)/i);
if (resourceGroupsQuery && this.defaultSubscriptionId) {
return this.getResourceGroups(this.defaultSubscriptionId);
}
const resourceGroupsQueryWithSub = query.match(/^ResourceGroups\(([^\)]+?)(,\s?([^,]+?))?\)/i);
if (resourceGroupsQueryWithSub) {
return this.getResourceGroups(this.toVariable(resourceGroupsQueryWithSub[1]));
}
const metricDefinitionsQuery = query.match(/^Namespaces\(([^\)]+?)(,\s?([^,]+?))?\)/i);
if (metricDefinitionsQuery && this.defaultSubscriptionId) {
if (!metricDefinitionsQuery[3]) {
return this.getMetricDefinitions(this.defaultSubscriptionId, this.toVariable(metricDefinitionsQuery[1]));
}
}
const metricDefinitionsQueryWithSub = query.match(/^Namespaces\(([^,]+?),\s?([^,]+?)\)/i);
if (metricDefinitionsQueryWithSub) {
return this.getMetricDefinitions(
this.toVariable(metricDefinitionsQueryWithSub[1]),
this.toVariable(metricDefinitionsQueryWithSub[2])
);
}
const resourceNamesQuery = query.match(/^ResourceNames\(([^,]+?),\s?([^,]+?)\)/i);
if (resourceNamesQuery && this.defaultSubscriptionId) {
const resourceGroup = this.toVariable(resourceNamesQuery[1]);
const metricDefinition = this.toVariable(resourceNamesQuery[2]);
return this.getResourceNames(this.defaultSubscriptionId, resourceGroup, metricDefinition);
}
const resourceNamesQueryWithSub = query.match(/^ResourceNames\(([^,]+?),\s?([^,]+?),\s?(.+?)\)/i);
if (resourceNamesQueryWithSub) {
const subscription = this.toVariable(resourceNamesQueryWithSub[1]);
const resourceGroup = this.toVariable(resourceNamesQueryWithSub[2]);
const metricDefinition = this.toVariable(resourceNamesQueryWithSub[3]);
return this.getResourceNames(subscription, resourceGroup, metricDefinition);
}
const metricNamespaceQuery = query.match(/^MetricNamespace\(([^,]+?),\s?([^,]+?),\s?([^,]+?)\)/i);
if (metricNamespaceQuery && this.defaultSubscriptionId) {
const resourceGroup = this.toVariable(metricNamespaceQuery[1]);
const metricDefinition = this.toVariable(metricNamespaceQuery[2]);
const resourceName = this.toVariable(metricNamespaceQuery[3]);
return this.getMetricNamespaces(this.defaultSubscriptionId, resourceGroup, metricDefinition, resourceName);
}
const metricNamespaceQueryWithSub = query.match(
/^metricnamespace\(([^,]+?),\s?([^,]+?),\s?([^,]+?),\s?([^,]+?)\)/i
);
if (metricNamespaceQueryWithSub) {
const subscription = this.toVariable(metricNamespaceQueryWithSub[1]);
const resourceGroup = this.toVariable(metricNamespaceQueryWithSub[2]);
const metricDefinition = this.toVariable(metricNamespaceQueryWithSub[3]);
const resourceName = this.toVariable(metricNamespaceQueryWithSub[4]);
return this.getMetricNamespaces(subscription, resourceGroup, metricDefinition, resourceName);
}
const metricNamesQuery = query.match(/^MetricNames\(([^,]+?),\s?([^,]+?),\s?([^,]+?),\s?([^,]+?)\)/i);
if (metricNamesQuery && this.defaultSubscriptionId) {
if (metricNamesQuery[3].indexOf(',') === -1) {
const resourceGroup = this.toVariable(metricNamesQuery[1]);
const metricDefinition = this.toVariable(metricNamesQuery[2]);
const resourceName = this.toVariable(metricNamesQuery[3]);
const metricNamespace = this.toVariable(metricNamesQuery[4]);
return this.getMetricNames(
this.defaultSubscriptionId,
resourceGroup,
metricDefinition,
resourceName,
metricNamespace
);
}
}
const metricNamesQueryWithSub = query.match(
/^MetricNames\(([^,]+?),\s?([^,]+?),\s?([^,]+?),\s?([^,]+?),\s?(.+?)\)/i
);
if (metricNamesQueryWithSub) {
const subscription = this.toVariable(metricNamesQueryWithSub[1]);
const resourceGroup = this.toVariable(metricNamesQueryWithSub[2]);
const metricDefinition = this.toVariable(metricNamesQueryWithSub[3]);
const resourceName = this.toVariable(metricNamesQueryWithSub[4]);
const metricNamespace = this.toVariable(metricNamesQueryWithSub[5]);
return this.getMetricNames(subscription, resourceGroup, metricDefinition, resourceName, metricNamespace);
}
return null;
}
toVariable(metric: string) {
return getTemplateSrv().replace((metric || '').trim());
}
async getSubscriptions(): Promise<Array<{ text: string; value: string }>> {
if (!this.isConfigured()) {
return [];

View File

@ -14,6 +14,7 @@ interface LogsQueryEditorProps {
onChange: (newQuery: AzureMonitorQuery) => void;
variableOptionGroup: { label: string; options: AzureMonitorOption[] };
setError: (source: string, error: AzureMonitorErrorish | undefined) => void;
hideFormatAs?: boolean;
}
const LogsQueryEditor: React.FC<LogsQueryEditorProps> = ({
@ -23,6 +24,7 @@ const LogsQueryEditor: React.FC<LogsQueryEditorProps> = ({
variableOptionGroup,
onChange,
setError,
hideFormatAs,
}) => {
const migrationError = useMigrations(datasource, query, onChange);
@ -48,14 +50,16 @@ const LogsQueryEditor: React.FC<LogsQueryEditorProps> = ({
setError={setError}
/>
<FormatAsField
query={query}
datasource={datasource}
subscriptionId={subscriptionId}
variableOptionGroup={variableOptionGroup}
onQueryChange={onChange}
setError={setError}
/>
{!hideFormatAs && (
<FormatAsField
query={query}
datasource={datasource}
subscriptionId={subscriptionId}
variableOptionGroup={variableOptionGroup}
onQueryChange={onChange}
setError={setError}
/>
)}
{migrationError && <Alert title={migrationError.title}>{migrationError.message}</Alert>}
</div>

View File

@ -27,7 +27,6 @@ function parseResourceDetails(resourceURI: string) {
const ResourceField: React.FC<AzureQueryEditorFieldProps> = ({ query, datasource, onQueryChange }) => {
const styles = useStyles2(getStyles);
const { resource } = query.azureLogAnalytics ?? {};
const [pickerIsOpen, setPickerIsOpen] = useState(false);
const handleOpenPicker = useCallback(() => {
@ -61,7 +60,7 @@ const ResourceField: React.FC<AzureQueryEditorFieldProps> = ({ query, datasource
</Modal>
<Field label="Resource">
<Button variant="secondary" onClick={handleOpenPicker}>
<Button variant="secondary" onClick={handleOpenPicker} type="button">
<ResourceLabel resource={resource} datasource={datasource} />
</Button>
</Field>

View File

@ -0,0 +1,163 @@
import React from 'react';
import { render, screen, waitFor } from '@testing-library/react';
import VariableEditor from './VariableEditor';
import userEvent from '@testing-library/user-event';
import createMockDatasource from '../../__mocks__/datasource';
import { AzureMonitorQuery, AzureQueryType } from '../../types';
import { select } from 'react-select-event';
import * as ui from '@grafana/ui';
// eslint-disable-next-line lodash/import-scope
import _ from 'lodash';
// Have to mock CodeEditor because it doesnt seem to work in tests???
jest.mock('@grafana/ui', () => ({
...jest.requireActual<typeof ui>('@grafana/ui'),
CodeEditor: function CodeEditor({ value, onSave }: { value: string; onSave: (newQuery: string) => void }) {
return <input data-testid="mockeditor" value={value} onChange={(event) => onSave(event.target.value)} />;
},
}));
jest.mock('lodash', () => ({
...jest.requireActual<typeof _>('lodash'),
debounce: jest.fn((fn: unknown) => fn),
}));
describe('VariableEditor:', () => {
it('can select a query type', async () => {
const onChange = jest.fn();
const props = {
query: {
refId: 'A',
queryType: AzureQueryType.LogAnalytics,
azureLogAnalytics: {
query: 'test query',
},
subscription: 'id',
},
onChange,
datasource: createMockDatasource(),
};
render(<VariableEditor {...props} />);
await waitFor(() => screen.getByLabelText('select query type'));
expect(screen.getByLabelText('select query type')).toBeInTheDocument();
screen.getByLabelText('select query type').click();
await select(screen.getByLabelText('select query type'), 'Grafana Query Function', {
container: document.body,
});
expect(screen.queryByText('Logs')).not.toBeInTheDocument();
expect(screen.queryByText('Grafana Query Function')).toBeInTheDocument();
});
describe('log queries:', () => {
it('should render', async () => {
const props = {
query: {
refId: 'A',
queryType: AzureQueryType.LogAnalytics,
azureLogAnalytics: {
query: 'test query',
},
subscription: 'id',
},
onChange: () => {},
datasource: createMockDatasource(),
};
render(<VariableEditor {...props} />);
await waitFor(() => screen.queryByTestId('mockeditor'));
expect(screen.queryByText('Resource')).toBeInTheDocument();
expect(screen.queryByTestId('mockeditor')).toBeInTheDocument();
});
it('should render with legacy query strings', async () => {
const props = {
query: 'test query',
onChange: () => {},
datasource: createMockDatasource(),
};
render(<VariableEditor {...props} />);
await waitFor(() => screen.queryByTestId('mockeditor'));
expect(screen.queryByText('Resource')).toBeInTheDocument();
expect(screen.queryByTestId('mockeditor')).toBeInTheDocument();
});
it('should call on change if the query changes', async () => {
const props = {
query: {
refId: 'A',
queryType: AzureQueryType.LogAnalytics,
azureLogAnalytics: {
query: 'test query',
},
subscription: 'id',
},
onChange: jest.fn(),
datasource: createMockDatasource(),
};
render(<VariableEditor {...props} />);
await waitFor(() => screen.queryByTestId('mockeditor'));
expect(screen.queryByTestId('mockeditor')).toBeInTheDocument();
await userEvent.type(screen.getByTestId('mockeditor'), '{backspace}');
expect(props.onChange).toHaveBeenCalledWith({
azureLogAnalytics: {
query: 'test quer',
},
queryType: 'Azure Log Analytics',
refId: 'A',
subscription: 'id',
});
});
});
describe('grafana template variable fn queries:', () => {
it('should render', async () => {
const props = {
query: {
refId: 'A',
queryType: AzureQueryType.GrafanaTemplateVariableFn,
grafanaTemplateVariableFn: {
rawQuery: 'test query',
kind: 'SubscriptionsQuery',
},
subscription: 'id',
} as AzureMonitorQuery,
onChange: () => {},
datasource: createMockDatasource(),
};
render(<VariableEditor {...props} />);
await waitFor(() => screen.queryByText('Grafana template variable function'));
expect(screen.queryByText('Grafana template variable function')).toBeInTheDocument();
expect(screen.queryByDisplayValue('test query')).toBeInTheDocument();
});
it('should call on change if the query changes', async () => {
const props = {
query: {
refId: 'A',
queryType: AzureQueryType.GrafanaTemplateVariableFn,
grafanaTemplateVariableFn: {
rawQuery: 'Su',
kind: 'UnknownQuery',
},
subscription: 'subscriptionId',
} as AzureMonitorQuery,
onChange: jest.fn(),
datasource: createMockDatasource(),
debounceTime: 1,
};
render(<VariableEditor {...props} />);
await waitFor(() => screen.queryByText('Grafana template variable function'));
userEvent.type(screen.getByDisplayValue('Su'), 'bscriptions()');
expect(screen.getByDisplayValue('Subscriptions()')).toBeInTheDocument();
screen.getByDisplayValue('Subscriptions()').blur();
await waitFor(() => screen.queryByText('None'));
expect(props.onChange).toHaveBeenCalledWith({
refId: 'A',
queryType: AzureQueryType.GrafanaTemplateVariableFn,
grafanaTemplateVariableFn: {
rawQuery: 'Subscriptions()',
kind: 'SubscriptionsQuery',
},
subscription: 'subscriptionId',
});
});
});
});

View File

@ -0,0 +1,150 @@
import { SelectableValue } from '@grafana/data';
import { Alert, InlineField, Input, Select } from '@grafana/ui';
import React, { ChangeEvent, useCallback, useEffect, useMemo, useState } from 'react';
import { AzureMonitorQuery, AzureQueryType } from '../../types';
import LogsQueryEditor from '../LogsQueryEditor';
import DataSource from '../../datasource';
import useLastError from '../../utils/useLastError';
import { Space } from '../Space';
import { migrateStringQueriesToObjectQueries } from '../../grafanaTemplateVariableFns';
import { debounce } from 'lodash';
const AZURE_QUERY_VARIABLE_TYPE_OPTIONS = [
{ label: 'Grafana Query Function', value: AzureQueryType.GrafanaTemplateVariableFn },
{ label: 'Logs', value: AzureQueryType.LogAnalytics },
];
const GrafanaTemplateVariableFnInput = ({
query,
updateQuery,
datasource,
}: {
query: AzureMonitorQuery;
updateQuery: (val: AzureMonitorQuery) => void;
datasource: DataSource;
}) => {
const [inputVal, setInputVal] = useState('');
useEffect(() => {
setInputVal(query.grafanaTemplateVariableFn?.rawQuery || '');
}, [query.grafanaTemplateVariableFn?.rawQuery]);
const onRunQuery = useCallback(
(newQuery: string) => {
migrateStringQueriesToObjectQueries(newQuery, { datasource }).then((updatedQuery) => {
if (updatedQuery.queryType === AzureQueryType.GrafanaTemplateVariableFn) {
updateQuery(updatedQuery);
} else {
updateQuery({
...query,
grafanaTemplateVariableFn: {
kind: 'UnknownQuery',
rawQuery: newQuery,
},
});
}
});
},
[datasource, query, updateQuery]
);
const debouncedRunQuery = useMemo(() => debounce(onRunQuery, 500), [onRunQuery]);
const onChange = (event: ChangeEvent<HTMLInputElement>) => {
setInputVal(event.target.value);
debouncedRunQuery(event.target.value);
};
return (
<InlineField label="Grafana template variable function">
<Input
placeholder={'type a grafana template variable function, ex: Subscriptions()'}
value={inputVal}
onChange={onChange}
/>
</InlineField>
);
};
type Props = {
query: AzureMonitorQuery | string;
onChange: (query: AzureMonitorQuery) => void;
datasource: DataSource;
};
const VariableEditor = (props: Props) => {
const defaultQuery: AzureMonitorQuery = {
refId: 'A',
queryType: AzureQueryType.GrafanaTemplateVariableFn,
};
const [query, setQuery] = useState(defaultQuery);
useEffect(() => {
migrateStringQueriesToObjectQueries(props.query, { datasource: props.datasource }).then((migratedQuery) => {
setQuery(migratedQuery);
});
}, [props.query, props.datasource]);
const onQueryTypeChange = (selectableValue: SelectableValue) => {
if (selectableValue.value) {
setQuery({
...query,
queryType: selectableValue.value,
});
}
};
const onLogsQueryChange = (queryChange: AzureMonitorQuery) => {
setQuery(queryChange);
// only hit backend if there's something to query (prevents error when selecting the resource before pinging a query)
if (queryChange.azureLogAnalytics?.query) {
props.onChange(queryChange);
}
};
const [errorMessage, setError] = useLastError();
const variableOptionGroup = {
label: 'Template Variables',
// TODO: figure out a way to filter out the current variable from the variables list
// options: props.datasource.getVariables().map((v) => ({ label: v, value: v })),
options: [],
};
return (
<>
<InlineField label="Select query type">
<Select
aria-label="select query type"
onChange={onQueryTypeChange}
options={AZURE_QUERY_VARIABLE_TYPE_OPTIONS}
width={25}
value={query.queryType}
/>
</InlineField>
{query.queryType === AzureQueryType.LogAnalytics && (
<>
<LogsQueryEditor
subscriptionId={query.subscription}
query={query}
datasource={props.datasource}
onChange={onLogsQueryChange}
variableOptionGroup={variableOptionGroup}
setError={setError}
hideFormatAs={true}
/>
{errorMessage && (
<>
<Space v={2} />
<Alert severity="error" title="An error occurred while requesting metadata from Azure Monitor">
{errorMessage}
</Alert>
</>
)}
</>
)}
{query.queryType === AzureQueryType.GrafanaTemplateVariableFn && (
<GrafanaTemplateVariableFnInput query={query} updateQuery={props.onChange} datasource={props.datasource} />
)}
</>
);
};
export default VariableEditor;

View File

@ -21,7 +21,7 @@ import { map } from 'rxjs/operators';
import AzureResourceGraphDatasource from './azure_resource_graph/azure_resource_graph_datasource';
import { getAzureCloud } from './credentials';
import migrateAnnotation from './utils/migrateAnnotation';
import { VariableSupport } from './variables';
export default class Datasource extends DataSourceApi<AzureMonitorQuery, AzureDataSourceJsonData> {
annotations = {
prepareAnnotation: migrateAnnotation,
@ -71,6 +71,8 @@ export default class Datasource extends DataSourceApi<AzureMonitorQuery, AzureDa
this.pseudoDatasource[AzureQueryType.ApplicationInsights] = this.appInsightsDatasource;
this.pseudoDatasource[AzureQueryType.InsightsAnalytics] = this.insightsAnalyticsDatasource;
}
this.variables = new VariableSupport(this);
}
query(options: DataQueryRequest<AzureMonitorQuery>): Observable<DataQueryResponse> {
@ -133,29 +135,6 @@ export default class Datasource extends DataSourceApi<AzureMonitorQuery, AzureDa
return this.azureLogAnalyticsDatasource.annotationQuery(options);
}
async metricFindQuery(query: string, optionalOptions?: unknown) {
if (!query) {
return Promise.resolve([]);
}
const aiResult = this.appInsightsDatasource?.metricFindQueryInternal(query);
if (aiResult) {
return aiResult;
}
const amResult = this.azureMonitorDatasource.metricFindQueryInternal(query);
if (amResult) {
return amResult;
}
const alaResult = this.azureLogAnalyticsDatasource.metricFindQueryInternal(query, optionalOptions);
if (alaResult) {
return alaResult;
}
return Promise.resolve([]);
}
async testDatasource(): Promise<DatasourceValidationResult> {
const promises: Array<Promise<DatasourceValidationResult>> = [];
@ -307,6 +286,9 @@ function hasQueryForType(query: AzureMonitorQuery): boolean {
case AzureQueryType.AzureResourceGraph:
return !!query.azureResourceGraph;
case AzureQueryType.GrafanaTemplateVariableFn:
return !!query.grafanaTemplateVariableFn;
case AzureQueryType.ApplicationInsights:
return !!query.appInsights;

View File

@ -0,0 +1,268 @@
import { AzureMonitorQuery, AzureQueryType } from './types';
import DataSource from './datasource';
import {
AppInsightsGroupByQuery,
AppInsightsMetricNameQuery,
GrafanaTemplateVariableQuery,
MetricDefinitionsQuery,
MetricNamespaceQuery,
MetricNamesQuery,
ResourceGroupsQuery,
ResourceNamesQuery,
SubscriptionsQuery,
WorkspacesQuery,
} from './types/templateVariables';
import { isGUIDish } from './components/ResourcePicker/utils';
/*
Grafana Template Variable Functions
ex: Subscriptions()
These are helper functions we have created and exposed to users to make the writing of template variables easier.
Due to legacy reasons, we still need to parse strings to determine if a query is a Grafana Template Variable Function
or if it's a KQL-type query
*/
export const grafanaTemplateVariableFnMatches = (query: string) => {
return {
subscriptions: query.match(/^Subscriptions\(\)/i),
resourceGroups: query.match(/^ResourceGroups\(\)/i),
resourceGroupsWithSub: query.match(/^ResourceGroups\(([^\)]+?)(,\s?([^,]+?))?\)/i),
metricDefinitions: query.match(/^Namespaces\(([^\)]+?)(,\s?([^,]+?))?\)/i),
metricDefinitionsWithSub: query.match(/^Namespaces\(([^,]+?),\s?([^,]+?)\)/i),
resourceNames: query.match(/^ResourceNames\(([^,]+?),\s?([^,]+?)\)/i),
resourceNamesWithSub: query.match(/^ResourceNames\(([^,]+?),\s?([^,]+?),\s?(.+?)\)/i),
metricNamespace: query.match(/^MetricNamespace\(([^,]+?),\s?([^,]+?),\s?([^,]+?)\)/i),
metricNamespaceWithSub: query.match(/^metricnamespace\(([^,]+?),\s?([^,]+?),\s?([^,]+?),\s?([^,]+?)\)/i),
metricNames: query.match(/^MetricNames\(([^,]+?),\s?([^,]+?),\s?([^,]+?),\s?([^,]+?)\)/i),
metricNamesWithSub: query.match(/^MetricNames\(([^,]+?),\s?([^,]+?),\s?([^,]+?),\s?([^,]+?),\s?(.+?)\)/i),
appInsightsMetricNameQuery: query.match(/^AppInsightsMetricNames\(\)/i),
appInsightsGroupByQuery: query.match(/^AppInsightsGroupBys\(([^\)]+?)(,\s?([^,]+?))?\)/i),
workspacesQuery: query.match(/^workspaces\(\)/i),
workspacesQueryWithSub: query.match(/^workspaces\(["']?([^\)]+?)["']?\)/i),
};
};
const isGrafanaTemplateVariableFnQuery = (query: string) => {
const matches: Record<string, RegExpMatchArray | null> = grafanaTemplateVariableFnMatches(query);
return Object.keys(matches).some((key) => !!matches[key]);
};
const createGrafanaTemplateVariableQuery = (rawQuery: string, datasource: DataSource): AzureMonitorQuery => {
const matchesForQuery = grafanaTemplateVariableFnMatches(rawQuery);
const defaultSubscriptionId = datasource.azureMonitorDatasource.defaultSubscriptionId;
const createGrafanaTemplateVariableDetails = (): GrafanaTemplateVariableQuery => {
// deprecated app insights template variables (will most likely remove in grafana 9)
if (matchesForQuery.appInsightsMetricNameQuery) {
const queryDetails: AppInsightsMetricNameQuery = { rawQuery, kind: 'AppInsightsMetricNameQuery' };
return queryDetails;
}
if (matchesForQuery.appInsightsGroupByQuery) {
const queryDetails: AppInsightsGroupByQuery = {
kind: 'AppInsightsGroupByQuery',
rawQuery,
metricName: matchesForQuery.appInsightsGroupByQuery[1],
};
return queryDetails;
}
if (matchesForQuery.subscriptions) {
const queryDetails: SubscriptionsQuery = {
kind: 'SubscriptionsQuery',
rawQuery,
};
return queryDetails;
}
if (matchesForQuery.resourceGroups && defaultSubscriptionId) {
const queryDetails: ResourceGroupsQuery = {
kind: 'ResourceGroupsQuery',
rawQuery,
subscription: defaultSubscriptionId,
};
return queryDetails;
}
if (matchesForQuery.resourceGroupsWithSub) {
const queryDetails: ResourceGroupsQuery = {
kind: 'ResourceGroupsQuery',
rawQuery,
subscription: matchesForQuery.resourceGroupsWithSub[1],
};
return queryDetails;
}
if (matchesForQuery.metricDefinitions && defaultSubscriptionId) {
const queryDetails: MetricDefinitionsQuery = {
kind: 'MetricDefinitionsQuery',
rawQuery,
subscription: defaultSubscriptionId,
resourceGroup: matchesForQuery.metricDefinitions[1],
};
return queryDetails;
}
if (matchesForQuery.metricDefinitionsWithSub) {
const queryDetails: MetricDefinitionsQuery = {
kind: 'MetricDefinitionsQuery',
rawQuery,
subscription: matchesForQuery.metricDefinitionsWithSub[1],
resourceGroup: matchesForQuery.metricDefinitionsWithSub[2],
};
return queryDetails;
}
if (matchesForQuery.resourceNames && defaultSubscriptionId) {
const queryDetails: ResourceNamesQuery = {
kind: 'ResourceNamesQuery',
rawQuery,
subscription: defaultSubscriptionId,
resourceGroup: matchesForQuery.resourceNames[1],
metricDefinition: matchesForQuery.resourceNames[2],
};
return queryDetails;
}
if (matchesForQuery.resourceNamesWithSub) {
const queryDetails: ResourceNamesQuery = {
kind: 'ResourceNamesQuery',
rawQuery,
subscription: matchesForQuery.resourceNamesWithSub[1],
resourceGroup: matchesForQuery.resourceNamesWithSub[2],
metricDefinition: matchesForQuery.resourceNamesWithSub[3],
};
return queryDetails;
}
if (matchesForQuery.metricNamespace && defaultSubscriptionId) {
const queryDetails: MetricNamespaceQuery = {
kind: 'MetricNamespaceQuery',
rawQuery,
subscription: defaultSubscriptionId,
resourceGroup: matchesForQuery.metricNamespace[1],
metricDefinition: matchesForQuery.metricNamespace[2],
resourceName: matchesForQuery.metricNamespace[3],
};
return queryDetails;
}
if (matchesForQuery.metricNamespaceWithSub) {
const queryDetails: MetricNamespaceQuery = {
kind: 'MetricNamespaceQuery',
rawQuery,
subscription: matchesForQuery.metricNamespaceWithSub[1],
resourceGroup: matchesForQuery.metricNamespaceWithSub[2],
metricDefinition: matchesForQuery.metricNamespaceWithSub[3],
resourceName: matchesForQuery.metricNamespaceWithSub[4],
};
return queryDetails;
}
if (matchesForQuery.metricNames && defaultSubscriptionId) {
if (matchesForQuery.metricNames[3].indexOf(',') === -1) {
const queryDetails: MetricNamesQuery = {
kind: 'MetricNamesQuery',
rawQuery,
subscription: defaultSubscriptionId,
resourceGroup: matchesForQuery.metricNames[1],
metricDefinition: matchesForQuery.metricNames[2],
resourceName: matchesForQuery.metricNames[3],
metricNamespace: matchesForQuery.metricNames[4],
};
return queryDetails;
}
}
if (matchesForQuery.metricNamesWithSub) {
const queryDetails: MetricNamesQuery = {
kind: 'MetricNamesQuery',
rawQuery,
subscription: matchesForQuery.metricNamesWithSub[1],
resourceGroup: matchesForQuery.metricNamesWithSub[2],
metricDefinition: matchesForQuery.metricNamesWithSub[3],
resourceName: matchesForQuery.metricNamesWithSub[4],
metricNamespace: matchesForQuery.metricNamesWithSub[5],
};
return queryDetails;
}
if (matchesForQuery.workspacesQuery && defaultSubscriptionId) {
const queryDetails: WorkspacesQuery = {
kind: 'WorkspacesQuery',
rawQuery,
subscription: defaultSubscriptionId,
};
return queryDetails;
}
if (matchesForQuery.workspacesQueryWithSub) {
const queryDetails: WorkspacesQuery = {
kind: 'WorkspacesQuery',
rawQuery,
subscription: (matchesForQuery.workspacesQueryWithSub[1] || '').trim(),
};
return queryDetails;
}
// fallback
const queryDetails: SubscriptionsQuery = { kind: 'SubscriptionsQuery', rawQuery };
return queryDetails;
};
const query: AzureMonitorQuery = {
refId: 'A',
queryType: AzureQueryType.GrafanaTemplateVariableFn,
grafanaTemplateVariableFn: createGrafanaTemplateVariableDetails(),
subscription: defaultSubscriptionId,
};
return query;
};
const createLogAnalyticsTemplateVariableQuery = async (
rawQuery: string,
datasource: DataSource
): Promise<AzureMonitorQuery> => {
const defaultSubscriptionId = datasource.azureMonitorDatasource.defaultSubscriptionId;
let resource = '';
// if there's an existing query, we try to get the resourcesuri from a deprecated default workspace
// a note this is very similar logic to what is used in useMigrations but moved out of the react-hook land
if (rawQuery) {
const defaultWorkspaceId = datasource.azureLogAnalyticsDatasource.getDeprecatedDefaultWorkSpace();
if (defaultWorkspaceId) {
const isWorkspaceGUID = isGUIDish(defaultWorkspaceId);
if (isWorkspaceGUID) {
resource = await datasource.resourcePickerData.getResourceURIFromWorkspace(defaultWorkspaceId);
} else {
resource = defaultWorkspaceId;
}
} else {
const maybeFirstWorkspace = await datasource.azureLogAnalyticsDatasource.getFirstWorkspace();
resource = maybeFirstWorkspace || '';
}
}
return {
refId: 'A',
queryType: AzureQueryType.LogAnalytics,
azureLogAnalytics: {
query: rawQuery,
resource,
},
subscription: defaultSubscriptionId,
};
};
export const migrateStringQueriesToObjectQueries = async (
rawQuery: string | AzureMonitorQuery,
options: { datasource: DataSource }
): Promise<AzureMonitorQuery> => {
// no need to migrate already migrated queries
if (typeof rawQuery !== 'string') {
return rawQuery;
}
return isGrafanaTemplateVariableFnQuery(rawQuery)
? createGrafanaTemplateVariableQuery(rawQuery, options.datasource)
: createLogAnalyticsTemplateVariableQuery(rawQuery, options.datasource);
};

View File

@ -1,4 +1,5 @@
import { DataQuery } from '@grafana/data';
import { GrafanaTemplateVariableQuery } from './templateVariables';
export enum AzureQueryType {
AzureMonitor = 'Azure Monitor',
@ -6,6 +7,7 @@ export enum AzureQueryType {
InsightsAnalytics = 'Insights Analytics',
LogAnalytics = 'Azure Log Analytics',
AzureResourceGraph = 'Azure Resource Graph',
GrafanaTemplateVariableFn = 'Grafana Template Variable Function',
}
/**
@ -23,6 +25,7 @@ export interface AzureMonitorQuery extends DataQuery {
azureMonitor?: AzureMetricQuery;
azureLogAnalytics?: AzureLogsQuery;
azureResourceGraph?: AzureResourceGraphQuery;
grafanaTemplateVariableFn?: GrafanaTemplateVariableQuery;
/** @deprecated App Insights/Insights Analytics deprecated in v8 */
appInsights?: ApplicationInsightsQuery;

View File

@ -0,0 +1,75 @@
export type GrafanaTemplateVariableQueryType =
| 'AppInsightsMetricNameQuery'
| 'AppInsightsGroupByQuery'
| 'SubscriptionsQuery'
| 'ResourceGroupsQuery'
| 'MetricDefinitionsQuery'
| 'ResourceNamesQuery'
| 'MetricNamespaceQuery'
| 'MetricNamesQuery'
| 'WorkspacesQuery'
| 'UnknownQuery';
interface BaseGrafanaTemplateVariableQuery {
rawQuery?: string;
}
export interface UnknownQuery extends BaseGrafanaTemplateVariableQuery {
kind: 'UnknownQuery';
}
export interface AppInsightsMetricNameQuery extends BaseGrafanaTemplateVariableQuery {
kind: 'AppInsightsMetricNameQuery';
}
export interface AppInsightsGroupByQuery extends BaseGrafanaTemplateVariableQuery {
kind: 'AppInsightsGroupByQuery';
metricName: string;
}
export interface SubscriptionsQuery extends BaseGrafanaTemplateVariableQuery {
kind: 'SubscriptionsQuery';
}
export interface ResourceGroupsQuery extends BaseGrafanaTemplateVariableQuery {
kind: 'ResourceGroupsQuery';
subscription: string;
}
export interface MetricDefinitionsQuery extends BaseGrafanaTemplateVariableQuery {
kind: 'MetricDefinitionsQuery';
subscription: string;
resourceGroup: string;
}
export interface ResourceNamesQuery extends BaseGrafanaTemplateVariableQuery {
kind: 'ResourceNamesQuery';
subscription: string;
resourceGroup: string;
metricDefinition: string;
}
export interface MetricNamespaceQuery extends BaseGrafanaTemplateVariableQuery {
kind: 'MetricNamespaceQuery';
subscription: string;
resourceGroup: string;
metricDefinition: string;
resourceName: string;
}
export interface MetricNamesQuery extends BaseGrafanaTemplateVariableQuery {
kind: 'MetricNamesQuery';
subscription: string;
resourceGroup: string;
metricDefinition: string;
resourceName: string;
metricNamespace: string;
}
export interface WorkspacesQuery extends BaseGrafanaTemplateVariableQuery {
kind: 'WorkspacesQuery';
subscription: string;
}
export type GrafanaTemplateVariableQuery =
| AppInsightsMetricNameQuery
| AppInsightsGroupByQuery
| SubscriptionsQuery
| ResourceGroupsQuery
| MetricDefinitionsQuery
| ResourceNamesQuery
| MetricNamespaceQuery
| MetricNamesQuery
| WorkspacesQuery
| UnknownQuery;

View File

@ -64,6 +64,8 @@ export interface AzureDataSourceJsonData extends DataSourceJsonData {
logAnalyticsClientId?: string;
/** @deprecated Azure Logs credentials */
logAnalyticsSubscriptionId?: string;
/** @deprecated Azure Logs credentials */
logAnalyticsDefaultWorkspace?: string;
// App Insights
appInsightsAppId?: string;

View File

@ -0,0 +1,478 @@
import { DataQueryRequest, DataQueryResponseData, toDataFrame } from '@grafana/data';
import { from } from 'rxjs';
import { AzureMonitorQuery, AzureQueryType } from './types';
import { VariableSupport } from './variables';
import createMockDatasource from './__mocks__/datasource';
jest.mock('@grafana/runtime', () => ({
...((jest.requireActual('@grafana/runtime') as unknown) as object),
getTemplateSrv: () => ({
replace: (val: string) => {
return val;
},
}),
}));
describe('VariableSupport', () => {
describe('querying for grafana template variable fns', () => {
it('can fetch deprecated log analytics metric names', (done) => {
const expectedResults = ['test'];
const variableSupport = new VariableSupport(
createMockDatasource({
insightsAnalyticsDatasource: {
getMetricNames: jest.fn().mockResolvedValueOnce(expectedResults),
},
})
);
const mockRequest = {
targets: [
{
refId: 'A',
queryType: AzureQueryType.GrafanaTemplateVariableFn,
grafanaTemplateVariableFn: {
kind: 'AppInsightsMetricNameQuery',
rawQuery: 'AppInsightsMetricNames()',
},
} as AzureMonitorQuery,
],
} as DataQueryRequest<AzureMonitorQuery>;
const observables = variableSupport.query(mockRequest);
observables.subscribe((result: DataQueryResponseData) => {
expect(result.data[0].source).toEqual(expectedResults);
done();
});
});
it('can fetch deprecated log analytics groupBys', (done) => {
const expectedResults = ['test'];
const variableSupport = new VariableSupport(
createMockDatasource({
insightsAnalyticsDatasource: {
getGroupBys: jest.fn().mockResolvedValueOnce(expectedResults),
},
})
);
const mockRequest = {
targets: [
{
refId: 'A',
queryType: AzureQueryType.GrafanaTemplateVariableFn,
grafanaTemplateVariableFn: {
kind: 'AppInsightsGroupByQuery',
rawQuery: 'AppInsightsGroupBys(metricname)',
},
} as AzureMonitorQuery,
],
} as DataQueryRequest<AzureMonitorQuery>;
const observables = variableSupport.query(mockRequest);
observables.subscribe((result: DataQueryResponseData) => {
expect(result.data[0].source).toEqual(expectedResults);
done();
});
});
it('can fetch subscriptions', (done) => {
const fakeSubscriptions = ['subscriptionId'];
const variableSupport = new VariableSupport(
createMockDatasource({
getSubscriptions: jest.fn().mockResolvedValueOnce(fakeSubscriptions),
})
);
const mockRequest = {
targets: [
{
refId: 'A',
queryType: AzureQueryType.GrafanaTemplateVariableFn,
grafanaTemplateVariableFn: {
kind: 'SubscriptionsQuery',
rawQuery: 'Subscriptions()',
},
} as AzureMonitorQuery,
],
} as DataQueryRequest<AzureMonitorQuery>;
const observables = variableSupport.query(mockRequest);
observables.subscribe((result: DataQueryResponseData) => {
expect(result.data[0].source).toEqual(fakeSubscriptions);
done();
});
});
it('can fetch resourceGroups with a default subscriptionId', (done) => {
const expectedResults = ['test'];
const variableSupport = new VariableSupport(
createMockDatasource({
azureLogAnalyticsDatasource: {
defaultSubscriptionId: 'defaultSubscriptionId',
},
getResourceGroups: jest.fn().mockResolvedValueOnce(expectedResults),
})
);
const mockRequest = {
targets: [
{
refId: 'A',
queryType: AzureQueryType.GrafanaTemplateVariableFn,
grafanaTemplateVariableFn: {
kind: 'ResourceGroupsQuery',
rawQuery: 'ResourceGroups()',
},
} as AzureMonitorQuery,
],
} as DataQueryRequest<AzureMonitorQuery>;
const observables = variableSupport.query(mockRequest);
observables.subscribe((result: DataQueryResponseData) => {
expect(result.data[0].source).toEqual(expectedResults);
done();
});
});
it('can fetch resourceGroups with a subscriptionId arg', (done) => {
const expectedResults = ['test'];
const variableSupport = new VariableSupport(
createMockDatasource({
getResourceGroups: jest.fn().mockResolvedValueOnce(expectedResults),
})
);
const mockRequest = {
targets: [
{
refId: 'A',
queryType: AzureQueryType.GrafanaTemplateVariableFn,
grafanaTemplateVariableFn: {
kind: 'ResourceGroupsQuery',
rawQuery: 'ResourceGroups(sub)',
},
} as AzureMonitorQuery,
],
} as DataQueryRequest<AzureMonitorQuery>;
const observables = variableSupport.query(mockRequest);
observables.subscribe((result: DataQueryResponseData) => {
expect(result.data[0].source).toEqual(expectedResults);
done();
});
});
it('can fetch metricDefinitions with a default subscriptionId', (done) => {
const expectedResults = ['test'];
const variableSupport = new VariableSupport(
createMockDatasource({
azureLogAnalyticsDatasource: {
defaultSubscriptionId: 'defaultSubscriptionId',
},
getMetricDefinitions: jest.fn().mockResolvedValueOnce(expectedResults),
})
);
const mockRequest = {
targets: [
{
refId: 'A',
queryType: AzureQueryType.GrafanaTemplateVariableFn,
grafanaTemplateVariableFn: {
kind: 'MetricDefinitionsQuery',
rawQuery: 'Namespaces(resourceGroup)',
},
} as AzureMonitorQuery,
],
} as DataQueryRequest<AzureMonitorQuery>;
const observables = variableSupport.query(mockRequest);
observables.subscribe((result: DataQueryResponseData) => {
expect(result.data[0].source).toEqual(expectedResults);
done();
});
});
it('can fetch metricDefinitions with a subscriptionId', (done) => {
const expectedResults = ['test'];
const variableSupport = new VariableSupport(
createMockDatasource({
getMetricDefinitions: jest.fn().mockResolvedValueOnce(expectedResults),
})
);
const mockRequest = {
targets: [
{
refId: 'A',
queryType: AzureQueryType.GrafanaTemplateVariableFn,
grafanaTemplateVariableFn: {
kind: 'MetricDefinitionsQuery',
rawQuery: 'Namespaces(resourceGroup, subscriptionId)',
},
} as AzureMonitorQuery,
],
} as DataQueryRequest<AzureMonitorQuery>;
const observables = variableSupport.query(mockRequest);
observables.subscribe((result: DataQueryResponseData) => {
expect(result.data[0].source).toEqual(expectedResults);
done();
});
});
it('can fetch resourceNames with a default subscriptionId', (done) => {
const expectedResults = ['test'];
const variableSupport = new VariableSupport(
createMockDatasource({
azureLogAnalyticsDatasource: {
defaultSubscriptionId: 'defaultSubscriptionId',
},
getResourceNames: jest.fn().mockResolvedValueOnce(expectedResults),
})
);
const mockRequest = {
targets: [
{
refId: 'A',
queryType: AzureQueryType.GrafanaTemplateVariableFn,
grafanaTemplateVariableFn: {
kind: 'ResourceNamesQuery',
rawQuery: 'ResourceNames(resourceGroup, metricDefinition)',
},
} as AzureMonitorQuery,
],
} as DataQueryRequest<AzureMonitorQuery>;
const observables = variableSupport.query(mockRequest);
observables.subscribe((result: DataQueryResponseData) => {
expect(result.data[0].source).toEqual(expectedResults);
done();
});
});
it('can fetch resourceNames with a subscriptionId', (done) => {
const expectedResults = ['test'];
const variableSupport = new VariableSupport(
createMockDatasource({
getResourceNames: jest.fn().mockResolvedValueOnce(expectedResults),
})
);
const mockRequest = {
targets: [
{
refId: 'A',
queryType: AzureQueryType.GrafanaTemplateVariableFn,
grafanaTemplateVariableFn: {
kind: 'ResourceNamesQuery',
rawQuery: 'ResourceNames(subscriptionId, resourceGroup, metricDefinition)',
},
} as AzureMonitorQuery,
],
} as DataQueryRequest<AzureMonitorQuery>;
const observables = variableSupport.query(mockRequest);
observables.subscribe((result: DataQueryResponseData) => {
expect(result.data[0].source).toEqual(expectedResults);
done();
});
});
it('can fetch a metricNamespace with a default subscriptionId', (done) => {
const expectedResults = ['test'];
const variableSupport = new VariableSupport(
createMockDatasource({
azureLogAnalyticsDatasource: {
defaultSubscriptionId: 'defaultSubscriptionId',
},
getMetricNamespaces: jest.fn().mockResolvedValueOnce(expectedResults),
})
);
const mockRequest = {
targets: [
{
refId: 'A',
queryType: AzureQueryType.GrafanaTemplateVariableFn,
grafanaTemplateVariableFn: {
kind: 'MetricNamespaceQuery',
rawQuery: 'metricNamespace(resourceGroup, metricDefinition, resourceName)',
},
} as AzureMonitorQuery,
],
} as DataQueryRequest<AzureMonitorQuery>;
const observables = variableSupport.query(mockRequest);
observables.subscribe((result: DataQueryResponseData) => {
expect(result.data[0].source).toEqual(expectedResults);
done();
});
});
it('can fetch a metricNamespace with a subscriptionId', (done) => {
const expectedResults = ['test'];
const variableSupport = new VariableSupport(
createMockDatasource({
getMetricNamespaces: jest.fn().mockResolvedValueOnce(expectedResults),
})
);
const mockRequest = {
targets: [
{
refId: 'A',
queryType: AzureQueryType.GrafanaTemplateVariableFn,
grafanaTemplateVariableFn: {
kind: 'MetricNamespaceQuery',
rawQuery: 'metricNamespace(subscriptionId, resourceGroup, metricDefinition, resourceName)',
},
} as AzureMonitorQuery,
],
} as DataQueryRequest<AzureMonitorQuery>;
const observables = variableSupport.query(mockRequest);
observables.subscribe((result: DataQueryResponseData) => {
expect(result.data[0].source).toEqual(expectedResults);
done();
});
});
it('can fetch metricNames with a default subscriptionId', (done) => {
const expectedResults = ['test'];
const variableSupport = new VariableSupport(
createMockDatasource({
azureLogAnalyticsDatasource: {
defaultSubscriptionId: 'defaultSubscriptionId',
},
getMetricNames: jest.fn().mockResolvedValueOnce(expectedResults),
})
);
const mockRequest = {
targets: [
{
refId: 'A',
queryType: AzureQueryType.GrafanaTemplateVariableFn,
grafanaTemplateVariableFn: {
kind: 'MetricNamesQuery',
rawQuery: 'metricNames(resourceGroup, metricDefinition, resourceName, metricNamespace)',
},
} as AzureMonitorQuery,
],
} as DataQueryRequest<AzureMonitorQuery>;
const observables = variableSupport.query(mockRequest);
observables.subscribe((result: DataQueryResponseData) => {
expect(result.data[0].source).toEqual(expectedResults);
done();
});
});
it('can fetch metricNames with a subscriptionId', (done) => {
const expectedResults = ['test'];
const variableSupport = new VariableSupport(
createMockDatasource({
getMetricNames: jest.fn().mockResolvedValueOnce(expectedResults),
})
);
const mockRequest = {
targets: [
{
refId: 'A',
queryType: AzureQueryType.GrafanaTemplateVariableFn,
grafanaTemplateVariableFn: {
kind: 'MetricNamesQuery',
rawQuery: 'metricNames(subscription, resourceGroup, metricDefinition, resourceName, metricNamespace)',
},
} as AzureMonitorQuery,
],
} as DataQueryRequest<AzureMonitorQuery>;
const observables = variableSupport.query(mockRequest);
observables.subscribe((result: DataQueryResponseData) => {
expect(result.data[0].source).toEqual(expectedResults);
done();
});
});
it('can fetch workspaces with a default subscriptionId', (done) => {
const expectedResults = ['test'];
const variableSupport = new VariableSupport(
createMockDatasource({
azureLogAnalyticsDatasource: {
defaultSubscriptionId: 'defaultSubscriptionId',
getWorkspaces: jest.fn().mockResolvedValueOnce(expectedResults),
},
})
);
const mockRequest = {
targets: [
{
refId: 'A',
queryType: AzureQueryType.GrafanaTemplateVariableFn,
grafanaTemplateVariableFn: {
kind: 'WorkspacesQuery',
rawQuery: 'workspaces()',
},
} as AzureMonitorQuery,
],
} as DataQueryRequest<AzureMonitorQuery>;
const observables = variableSupport.query(mockRequest);
observables.subscribe((result: DataQueryResponseData) => {
expect(result.data[0].source).toEqual(expectedResults);
done();
});
});
it('can fetch workspaces with a subscriptionId', (done) => {
const expectedResults = ['test'];
const variableSupport = new VariableSupport(
createMockDatasource({
azureLogAnalyticsDatasource: {
getWorkspaces: jest.fn().mockResolvedValueOnce(expectedResults),
},
})
);
const mockRequest = {
targets: [
{
refId: 'A',
queryType: AzureQueryType.GrafanaTemplateVariableFn,
grafanaTemplateVariableFn: {
kind: 'WorkspacesQuery',
rawQuery: 'workspaces(subscriptionId)',
},
} as AzureMonitorQuery,
],
} as DataQueryRequest<AzureMonitorQuery>;
const observables = variableSupport.query(mockRequest);
observables.subscribe((result: DataQueryResponseData) => {
expect(result.data[0].source).toEqual(expectedResults);
done();
});
});
it('returns an empty array for unknown queries', (done) => {
const variableSupport = new VariableSupport(createMockDatasource());
const mockRequest = {
targets: [
{
queryType: AzureQueryType.GrafanaTemplateVariableFn,
grafanaTemplateVariableFn: {
rawQuery: 'nonsense',
},
} as AzureMonitorQuery,
],
} as DataQueryRequest<AzureMonitorQuery>;
const observables = variableSupport.query(mockRequest);
observables.subscribe((result: DataQueryResponseData) => {
expect(result.data).toEqual([]);
done();
});
});
});
it('passes on the query to the main datasource for all non-grafana template variable fns', (done) => {
const expectedResults = ['test'];
const variableSupport = new VariableSupport(
createMockDatasource({
query: () =>
from(
Promise.resolve({
data: [toDataFrame(expectedResults)],
})
),
})
);
const mockRequest = {
targets: [
{
queryType: AzureQueryType.LogAnalytics,
azureLogAnalytics: {
query: 'some log thing',
},
} as AzureMonitorQuery,
],
} as DataQueryRequest<AzureMonitorQuery>;
const observables = variableSupport.query(mockRequest);
observables.subscribe((result: DataQueryResponseData) => {
expect(result.data[0].source).toEqual(expectedResults);
done();
});
});
});

View File

@ -0,0 +1,105 @@
import { from, lastValueFrom, Observable } from 'rxjs';
import {
CustomVariableSupport,
DataQueryRequest,
DataQueryResponse,
MetricFindValue,
toDataFrame,
} from '@grafana/data';
import VariableEditor from './components/VariableEditor/VariableEditor';
import DataSource from './datasource';
import { AzureQueryType, AzureMonitorQuery } from './types';
import { getTemplateSrv } from '@grafana/runtime';
import { migrateStringQueriesToObjectQueries } from './grafanaTemplateVariableFns';
import { GrafanaTemplateVariableQuery } from './types/templateVariables';
export class VariableSupport extends CustomVariableSupport<DataSource, AzureMonitorQuery> {
constructor(private readonly datasource: DataSource) {
super();
this.datasource = datasource;
this.query = this.query.bind(this);
}
editor = VariableEditor;
query(request: DataQueryRequest<AzureMonitorQuery>): Observable<DataQueryResponse> {
const promisedResults = async () => {
const queryObj = await migrateStringQueriesToObjectQueries(request.targets[0], { datasource: this.datasource });
if (queryObj.queryType === AzureQueryType.GrafanaTemplateVariableFn && queryObj.grafanaTemplateVariableFn) {
const templateVariablesResults = await this.callGrafanaTemplateVariableFn(queryObj.grafanaTemplateVariableFn);
return {
data: templateVariablesResults ? [toDataFrame(templateVariablesResults)] : [],
};
}
request.targets[0] = queryObj;
return lastValueFrom(this.datasource.query(request));
};
return from(promisedResults());
}
callGrafanaTemplateVariableFn(query: GrafanaTemplateVariableQuery): Promise<MetricFindValue[]> | null {
// deprecated app insights template variables (will most likely remove in grafana 9)
if (this.datasource.insightsAnalyticsDatasource) {
if (query.kind === 'AppInsightsMetricNameQuery') {
return this.datasource.insightsAnalyticsDatasource.getMetricNames();
}
if (query.kind === 'AppInsightsGroupByQuery') {
return this.datasource.insightsAnalyticsDatasource.getGroupBys(getTemplateSrv().replace(query.metricName));
}
}
if (query.kind === 'SubscriptionsQuery') {
return this.datasource.getSubscriptions();
}
if (query.kind === 'ResourceGroupsQuery') {
return this.datasource.getResourceGroups(this.replaceVariable(query.subscription));
}
if (query.kind === 'MetricDefinitionsQuery') {
return this.datasource.getMetricDefinitions(
this.replaceVariable(query.subscription),
this.replaceVariable(query.resourceGroup)
);
}
if (query.kind === 'ResourceNamesQuery') {
return this.datasource.getResourceNames(
this.replaceVariable(query.subscription),
this.replaceVariable(query.resourceGroup),
this.replaceVariable(query.metricDefinition)
);
}
if (query.kind === 'MetricNamespaceQuery') {
return this.datasource.getMetricNamespaces(
this.replaceVariable(query.subscription),
this.replaceVariable(query.resourceGroup),
this.replaceVariable(query.metricDefinition),
this.replaceVariable(query.resourceName)
);
}
if (query.kind === 'MetricNamesQuery') {
return this.datasource.getMetricNames(
this.replaceVariable(query.subscription),
this.replaceVariable(query.resourceGroup),
this.replaceVariable(query.metricDefinition),
this.replaceVariable(query.resourceName),
this.replaceVariable(query.metricNamespace)
);
}
if (query.kind === 'WorkspacesQuery') {
return this.datasource.azureLogAnalyticsDatasource.getWorkspaces(this.replaceVariable(query.subscription));
}
return null;
}
replaceVariable(metric: string) {
return getTemplateSrv().replace((metric || '').trim());
}
}