[release-11.5.4] Azure: Variable editor and resource picker improvements (#103657)

Azure: Variable editor and resource picker improvements (#101695)

* Update namespace endpoint to filter out only relevant namespaces

* Update tests

* Fix url builder tests

* Add todo comments

* Update func to use ARG to retrieve namespaces with metrics

* Refactor getMetricNamespaces for readability

* Lint

* Remove comments

* Remove type assertion

* Refactor ARG query

* Update tests and refactor class to use ARG

* Update resource group query

- Updates the resource groups query to support users/apps with restricted permissions

* Update resources request to be paginated

- Also order by name
- Add tests

* Start refactoring azure monitor util functions to resource graph

* Minor lint

* Add getMetricNamespaces resource graph function

* Modify getMetricNamespaces call

- Use resource graph function for variable queries

* Return names for getResourceNames values

* Use getMetricNamespaces variable specific req in editor

* Substantial refactor

- Update Azure Resource Graph data source with a method for making paged requests and a method for retrieving metric namespaces (and add tests)
- Extract helpers from azure_monitor_datasource to utils and generalise them (also revert previous changes to datasource and test file)
- Update mock with Azure Resource Graph data source
- Revert response parser changes
- Revert url builder changes
- Update get metric namespaces query to use the resource graph method for variable queries
- Update docs

* Lint

* Oops

* Fix type

* Lint and betterer

* Simplify imports

* Improve type

* Simplify resource picker class

* Start updating tests

* Fix naming and include missing error

* Update resource graph data source mock

* Update tests

* Remove unneeded parser

* Add get subscriptions to resource graph

* Generalise resource groups request

* Fix resource names request to ensure no clashing

* Update types

* Use resource graph queries for resource picker

* Correctly map resource group names

* Update mocks

* Update tests

* Fix mapping

* Refactor getResourceNames

- Remove most of the logic from resourcePickerData
- Add helper for parsing resource names as template vars
- Some renames for clarity
- Update types
- Update tests

* Ensure namespaces are lowercase

* Update docs/sources/datasources/azure-monitor/template-variables/index.md

Co-authored-by: Larissa Wandzura <126723338+lwandz13@users.noreply.github.com>

* Prettier write

* Ensure we return all namespaces if resourceGroup isn't specified

---------

Co-authored-by: alyssabull <alyssabull@gmail.com>
Co-authored-by: Larissa Wandzura <126723338+lwandz13@users.noreply.github.com>
(cherry picked from commit c7b0bbd262)
pull/103771/head
Andreas Christou 3 months ago committed by GitHub
parent c32b2f5655
commit 4df6c9085a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 18
      public/app/plugins/datasource/azuremonitor/__mocks__/argResourcePickerResponse.ts
  2. 5
      public/app/plugins/datasource/azuremonitor/__mocks__/datasource.ts
  3. 173
      public/app/plugins/datasource/azuremonitor/azure_monitor/azure_monitor_datasource.test.ts
  4. 66
      public/app/plugins/datasource/azuremonitor/azure_monitor/azure_monitor_datasource.ts
  5. 26
      public/app/plugins/datasource/azuremonitor/azure_monitor/response_parser.ts
  6. 96
      public/app/plugins/datasource/azuremonitor/azure_resource_graph/azure_resource_graph_datasource.ts
  7. 3
      public/app/plugins/datasource/azuremonitor/components/MetricsQueryEditor/MetricsQueryEditor.test.tsx
  8. 16
      public/app/plugins/datasource/azuremonitor/components/ResourcePicker/ResourcePicker.test.tsx
  9. 20
      public/app/plugins/datasource/azuremonitor/components/VariableEditor/VariableEditor.test.tsx
  10. 6
      public/app/plugins/datasource/azuremonitor/components/VariableEditor/VariableEditor.tsx
  11. 32
      public/app/plugins/datasource/azuremonitor/datasource.ts
  12. 47
      public/app/plugins/datasource/azuremonitor/resourcePicker/resourcePickerData.test.ts
  13. 171
      public/app/plugins/datasource/azuremonitor/resourcePicker/resourcePickerData.ts
  14. 6
      public/app/plugins/datasource/azuremonitor/types/types.ts
  15. 8
      public/app/plugins/datasource/azuremonitor/variables.test.ts
  16. 48
      public/app/plugins/datasource/azuremonitor/variables.ts

@ -10,26 +10,38 @@ export const createMockARGSubscriptionResponse = (): AzureGraphResponse<RawAzure
{
subscriptionId: '1',
subscriptionName: 'Primary Subscription',
subscriptionURI: '/subscriptions/1',
count: 1,
},
{
subscriptionId: '2',
subscriptionName: 'Dev Subscription',
subscriptionURI: '/subscriptions/2',
count: 1,
},
{
subscriptionId: '3',
subscriptionName: 'Dev Subscription',
subscriptionURI: '/subscriptions/3',
count: 1,
},
{
subscriptionId: '4',
subscriptionName: 'Primary Subscription',
subscriptionURI: '/subscriptions/4',
count: 1,
},
{
subscriptionId: '5',
subscriptionName: 'Primary Subscription',
subscriptionURI: '/subscriptions/5',
count: 1,
},
{
subscriptionId: '6',
subscriptionName: 'Dev Subscription',
subscriptionURI: '/subscriptions/6',
count: 1,
},
],
});
@ -39,31 +51,37 @@ export const createMockARGResourceGroupsResponse = (): AzureGraphResponse<RawAzu
{
resourceGroupURI: '/subscriptions/abc-123/resourceGroups/prod',
resourceGroupName: 'Production',
count: 1,
},
{
resourceGroupURI: '/subscriptions/def-456/resourceGroups/dev',
resourceGroupName: 'Development',
count: 1,
},
{
resourceGroupURI: '/subscriptions/def-456/resourceGroups/test',
resourceGroupName: 'Test',
count: 1,
},
{
resourceGroupURI: '/subscriptions/abc-123/resourceGroups/test',
resourceGroupName: 'Test',
count: 1,
},
{
resourceGroupURI: '/subscriptions/abc-123/resourceGroups/pre-prod',
resourceGroupName: 'Pre-production',
count: 1,
},
{
resourceGroupURI: '/subscriptions/def-456/resourceGroups/qa',
resourceGroupName: 'QA',
count: 1,
},
],
});

@ -75,7 +75,10 @@ export default function createMockDatasource(overrides?: DeepPartial<Datasource>
getResourceURIDisplayProperties: jest.fn().mockResolvedValue({}),
},
azureResourceGraphDatasource: {},
azureResourceGraphDatasource: {
pagedResourceGraphRequest: jest.fn().mockResolvedValue([]),
...overrides?.azureResourceGraphDatasource,
},
getVariablesRaw: jest.fn().mockReturnValue([]),
currentUserAuth: false,
...overrides,

@ -7,7 +7,13 @@ import createMockQuery from '../__mocks__/query';
import { createTemplateVariables } from '../__mocks__/utils';
import { multiVariable } from '../__mocks__/variables';
import AzureMonitorDatasource from '../datasource';
import { AzureAPIResponse, AzureMonitorDataSourceInstanceSettings, Location } from '../types';
import {
AzureAPIResponse,
AzureMonitorDataSourceInstanceSettings,
Location,
RawAzureResourceGroupItem,
RawAzureResourceItem,
} from '../types';
// We want replace to just return the value as is in general/
// We declare this as a function so that we can overwrite it in each test
@ -754,20 +760,20 @@ describe('AzureMonitorDatasource', () => {
describe('When performing getResourceGroups', () => {
const response = {
value: [{ name: 'grp1' }, { name: 'grp2' }],
data: [{ resourceGroupName: 'grp1' }, { resourceGroupName: 'grp2' }],
};
beforeEach(() => {
ctx.ds.azureMonitorDatasource.getResource = jest.fn().mockResolvedValue(response);
ctx.ds.azureResourceGraphDatasource.postResource = jest.fn().mockResolvedValue(response);
});
it('should return list of Resource Groups', () => {
return ctx.ds.getResourceGroups('subscriptionId').then((results: Array<{ text: string; value: string }>) => {
return ctx.ds.getResourceGroups('subscriptionId').then((results: RawAzureResourceGroupItem[]) => {
expect(results.length).toEqual(2);
expect(results[0].text).toEqual('grp1');
expect(results[0].value).toEqual('grp1');
expect(results[1].text).toEqual('grp2');
expect(results[1].value).toEqual('grp2');
expect(results[0].resourceGroupName).toEqual('grp1');
expect(results[0].resourceGroupName).toEqual('grp1');
expect(results[1].resourceGroupName).toEqual('grp2');
expect(results[1].resourceGroupName).toEqual('grp2');
});
});
});
@ -786,11 +792,7 @@ describe('AzureMonitorDatasource', () => {
describe('and there are no special cases', () => {
const response = {
value: [
{
name: 'Failure Anomalies - nodeapp',
type: 'microsoft.insights/alertrules',
},
data: [
{
name: resourceGroup,
type: metricNamespace,
@ -799,13 +801,7 @@ describe('AzureMonitorDatasource', () => {
};
beforeEach(() => {
ctx.ds.azureMonitorDatasource.getResource = jest.fn().mockImplementation((path: string) => {
const basePath = `azuremonitor/subscriptions/${subscription}/resourceGroups`;
expect(path).toBe(
`${basePath}/${resourceGroup}/resources?api-version=2021-04-01&$filter=resourceType eq '${metricNamespace}'${
region ? ` and location eq '${region}'` : ''
}`
);
ctx.ds.azureResourceGraphDatasource.postResource = jest.fn().mockImplementation((path: string) => {
return Promise.resolve(response);
});
});
@ -813,10 +809,10 @@ describe('AzureMonitorDatasource', () => {
it('should return list of Resource Names', () => {
return ctx.ds
.getResourceNames(subscription, resourceGroup, metricNamespace)
.then((results: Array<{ text: string; value: string }>) => {
.then((results: RawAzureResourceItem[]) => {
expect(results.length).toEqual(1);
expect(results[0].text).toEqual('nodeapp');
expect(results[0].value).toEqual('nodeapp');
expect(results[0].name).toEqual('nodeapp');
expect(results[0].name).toEqual('nodeapp');
});
});
@ -824,10 +820,10 @@ describe('AzureMonitorDatasource', () => {
metricNamespace = 'microsoft.insights/Components';
return ctx.ds
.getResourceNames(subscription, resourceGroup, metricNamespace)
.then((results: Array<{ text: string; value: string }>) => {
.then((results: RawAzureResourceItem[]) => {
expect(results.length).toEqual(1);
expect(results[0].text).toEqual('nodeapp');
expect(results[0].value).toEqual('nodeapp');
expect(results[0].name).toEqual('nodeapp');
expect(results[0].name).toEqual('nodeapp');
});
});
@ -835,10 +831,10 @@ describe('AzureMonitorDatasource', () => {
region = 'eastus';
return ctx.ds
.getResourceNames(subscription, resourceGroup, metricNamespace, region)
.then((results: Array<{ text: string; value: string }>) => {
.then((results: RawAzureResourceItem[]) => {
expect(results.length).toEqual(1);
expect(results[0].text).toEqual('nodeapp');
expect(results[0].value).toEqual('nodeapp');
expect(results[0].name).toEqual('nodeapp');
expect(results[0].name).toEqual('nodeapp');
});
});
@ -869,43 +865,34 @@ describe('AzureMonitorDatasource', () => {
return target === `$${multiVariable.id}` ? 'foo,bar' : (target ?? '');
};
const ds = new AzureMonitorDatasource(ctx.instanceSettings);
//ds.azureMonitorDatasource.templateSrv = tsrv;
ds.azureMonitorDatasource.getResource = jest
ds.azureResourceGraphDatasource.postResource = jest
.fn()
.mockImplementationOnce((path: string) => {
expect(path).toMatch('foo');
return Promise.resolve(response);
})
.mockImplementationOnce((path: string) => {
expect(path).toMatch('bar');
return Promise.resolve({
value: [
.mockImplementationOnce(() => Promise.resolve(response))
.mockImplementationOnce(() =>
Promise.resolve({
data: [
{
name: resourceGroup + '2',
type: metricNamespace,
},
],
});
});
})
);
return ds
.getResourceNames(subscription, `$${multiVariable.id}`, metricNamespace)
.then((results: Array<{ text: string; value: string }>) => {
.then((results: RawAzureResourceItem[]) => {
expect(results.length).toEqual(2);
expect(results[0].text).toEqual('nodeapp');
expect(results[0].value).toEqual('nodeapp');
expect(results[1].text).toEqual('nodeapp2');
expect(results[1].value).toEqual('nodeapp2');
expect(results[0].name).toEqual('nodeapp');
expect(results[0].name).toEqual('nodeapp');
expect(results[1].name).toEqual('nodeapp2');
expect(results[1].name).toEqual('nodeapp2');
});
});
});
describe('and the metric definition is blobServices', () => {
const response = {
value: [
{
name: 'Failure Anomalies - nodeapp',
type: 'microsoft.insights/alertrules',
},
data: [
{
name: 'storagetest',
type: 'microsoft.storage/storageaccounts',
@ -916,78 +903,32 @@ describe('AzureMonitorDatasource', () => {
it('should return list of Resource Names', () => {
metricNamespace = 'microsoft.storage/storageaccounts/blobservices';
const validMetricNamespace = 'microsoft.storage/storageaccounts';
ctx.ds.azureMonitorDatasource.getResource = jest.fn().mockImplementation((path: string) => {
const basePath = `azuremonitor/subscriptions/${subscription}/resourceGroups`;
expect(path).toBe(
basePath +
`/${resourceGroup}/resources?api-version=2021-04-01&$filter=resourceType eq '${validMetricNamespace}'`
);
return Promise.resolve(response);
});
ctx.ds.azureResourceGraphDatasource.postResource = jest
.fn()
.mockImplementation(() => Promise.resolve(response));
return ctx.ds
.getResourceNames(subscription, resourceGroup, metricNamespace)
.then((results: Array<{ text: string; value: string }>) => {
.then((results: RawAzureResourceItem[]) => {
expect(results.length).toEqual(1);
expect(results[0].text).toEqual('storagetest/default');
expect(results[0].value).toEqual('storagetest/default');
expect(ctx.ds.azureMonitorDatasource.getResource).toHaveBeenCalledWith(
`azuremonitor/subscriptions/${subscription}/resourceGroups/${resourceGroup}/resources?api-version=2021-04-01&$filter=resourceType eq '${validMetricNamespace}'`
expect(results[0].name).toEqual('storagetest');
expect(results[0].name).toEqual('storagetest');
expect(ctx.ds.azureResourceGraphDatasource.postResource).toHaveBeenCalledWith(
'resourcegraph/providers/Microsoft.ResourceGraph/resources?api-version=2021-03-01',
{
options: { resultFormat: 'objectArray' },
query: `resources
| where id hasprefix \"/subscriptions/${subscription}/resourceGroups/${resourceGroup}/\"
| where type == '${validMetricNamespace}'
| order by tolower(name) asc`,
}
);
});
});
});
describe('and there are several pages', () => {
const skipToken = 'token';
const response1 = {
value: [
{
name: `${resourceGroup}1`,
type: metricNamespace,
},
],
nextLink: `https://management.azure.com/resourceuri?$skiptoken=${skipToken}`,
};
const response2 = {
value: [
{
name: `${resourceGroup}2`,
type: metricNamespace,
},
],
};
beforeEach(() => {
const fn = jest.fn();
ctx.ds.azureMonitorDatasource.getResource = fn;
const basePath = `azuremonitor/subscriptions/${subscription}/resourceGroups`;
const expectedPath = `${basePath}/${resourceGroup}/resources?api-version=2021-04-01&$filter=resourceType eq '${metricNamespace}'`;
// first page
fn.mockImplementationOnce((path: string) => {
expect(path).toBe(expectedPath);
return Promise.resolve(response1);
});
// second page
fn.mockImplementationOnce((path: string) => {
expect(path).toBe(`${expectedPath}&$skiptoken=${skipToken}`);
return Promise.resolve(response2);
});
});
it('should return list of Resource Names', () => {
return ctx.ds
.getResourceNames(subscription, resourceGroup, metricNamespace)
.then((results: Array<{ text: string; value: string }>) => {
expect(results.length).toEqual(2);
expect(results[0].value).toEqual(`${resourceGroup}1`);
expect(results[1].value).toEqual(`${resourceGroup}2`);
});
});
});
describe('without a resource group or a metric definition', () => {
const response = {
value: [
data: [
{
name: 'Failure Anomalies - nodeapp',
type: 'microsoft.insights/alertrules',
@ -1000,15 +941,13 @@ describe('AzureMonitorDatasource', () => {
};
beforeEach(() => {
ctx.ds.azureMonitorDatasource.getResource = jest.fn().mockImplementation((path: string) => {
const basePath = `azuremonitor/subscriptions/${subscription}/resources?api-version=2021-04-01`;
expect(path).toBe(basePath);
ctx.ds.azureResourceGraphDatasource.postResource = jest.fn().mockImplementation((path: string) => {
return Promise.resolve(response);
});
});
it('should return list of Resource Names', () => {
return ctx.ds.getResourceNames(subscription).then((results: Array<{ text: string; value: string }>) => {
return ctx.ds.getResourceNames(subscription).then((results: RawAzureResourceItem[]) => {
expect(results.length).toEqual(2);
});
});

@ -1,4 +1,4 @@
import { find, startsWith } from 'lodash';
import { find } from 'lodash';
import { AzureCredentials } from '@grafana/azure-sdk';
import { ScopedVars } from '@grafana/data';
@ -20,10 +20,8 @@ import {
AzureMonitorLocations,
AzureMonitorProvidersResponse,
AzureAPIResponse,
AzureGetResourceNamesQuery,
Subscription,
Location,
ResourceGroup,
Metric,
MetricNamespace,
} from '../types';
@ -178,68 +176,6 @@ export default class AzureMonitorDatasource extends DataSourceWithBackend<
});
}
getResourceGroups(subscriptionId: string) {
return this.getResource(
`${this.resourcePath}/subscriptions/${subscriptionId}/resourceGroups?api-version=${this.listByResourceGroupApiVersion}`
).then((result: AzureAPIResponse<ResourceGroup>) => {
return ResponseParser.parseResponseValues<ResourceGroup>(result, 'name', 'name');
});
}
async getResourceNames(query: AzureGetResourceNamesQuery, skipToken?: string) {
const promises = replaceTemplateVariables(this.templateSrv, query).map(
({ metricNamespace, subscriptionId, resourceGroup, region }) => {
const validMetricNamespace = startsWith(metricNamespace?.toLowerCase(), 'microsoft.storage/storageaccounts/')
? 'microsoft.storage/storageaccounts'
: metricNamespace;
let url = `${this.resourcePath}/subscriptions/${subscriptionId}`;
if (resourceGroup) {
url += `/resourceGroups/${resourceGroup}`;
}
url += `/resources?api-version=${this.listByResourceGroupApiVersion}`;
const filters: string[] = [];
if (validMetricNamespace) {
filters.push(`resourceType eq '${validMetricNamespace}'`);
}
if (region) {
filters.push(`location eq '${region}'`);
}
if (filters.length > 0) {
url += `&$filter=${filters.join(' and ')}`;
}
if (skipToken) {
url += `&$skiptoken=${skipToken}`;
}
return this.getResource(url).then(async (result) => {
let list: Array<{ text: string; value: string }> = [];
if (startsWith(metricNamespace?.toLowerCase(), 'microsoft.storage/storageaccounts/')) {
list = ResponseParser.parseResourceNames(result, 'microsoft.storage/storageaccounts');
for (let i = 0; i < list.length; i++) {
list[i].text += '/default';
list[i].value += '/default';
}
} else {
list = ResponseParser.parseResourceNames(result, metricNamespace);
}
if (result.nextLink) {
// If there is a nextLink, we should request more pages
const nextURL = new URL(result.nextLink);
const nextToken = nextURL.searchParams.get('$skiptoken');
if (!nextToken) {
throw Error('unable to request the next page of resources');
}
const nextPage = await this.getResourceNames({ metricNamespace, subscriptionId, resourceGroup }, nextToken);
list = list.concat(nextPage);
}
return list;
});
}
);
return (await Promise.all(promises)).flat();
}
// Note globalRegion should be false when querying custom metric namespaces
getMetricNamespaces(query: GetMetricNamespacesQuery, globalRegion: boolean, region?: string, custom?: boolean) {
const url = UrlBuilder.buildAzureMonitorGetMetricNamespacesUrl(

@ -12,7 +12,6 @@ import {
AzureAPIResponse,
Location,
Subscription,
Resource,
} from '../types';
export default class ResponseParser {
static parseResponseValues<T>(
@ -40,31 +39,6 @@ export default class ResponseParser {
return list;
}
static parseResourceNames(
result: AzureAPIResponse<Resource>,
metricNamespace?: string
): Array<{ text: string; value: string }> {
const list: Array<{ text: string; value: string }> = [];
if (!result) {
return list;
}
for (let i = 0; i < result.value.length; i++) {
if (
typeof result.value[i].type === 'string' &&
(!metricNamespace || result.value[i].type.toLocaleLowerCase() === metricNamespace.toLocaleLowerCase())
) {
list.push({
text: result.value[i].name,
value: result.value[i].name,
});
}
}
return list;
}
static parseMetadata(result: AzureMonitorMetricsMetadataResponse, metricName: string) {
const defaultAggTypes = ['None', 'Average', 'Minimum', 'Maximum', 'Total', 'Count'];
const metricData = result?.value.find((v) => v.name.value === metricName);

@ -1,5 +1,4 @@
// eslint-disable-next-line lodash/import-scope
import _ from 'lodash';
import { startsWith, includes, find, filter } from 'lodash';
import { ScopedVars } from '@grafana/data';
import { getTemplateSrv, DataSourceWithBackend, TemplateSrv } from '@grafana/runtime';
@ -9,10 +8,13 @@ import {
AzureMonitorQuery,
AzureMonitorDataSourceJsonData,
AzureQueryType,
RawAzureResourceGroupItem,
AzureGetResourceNamesQuery,
AzureMonitorDataSourceInstanceSettings,
RawAzureResourceItem,
AzureGraphResponse,
AzureResourceGraphOptions,
RawAzureSubscriptionItem,
} from '../types';
import { interpolateVariable, replaceTemplateVariables, routeNames } from '../utils/common';
@ -41,14 +43,14 @@ export default class AzureResourceGraphDatasource extends DataSourceWithBackend<
return target;
}
const variableNames = ts.getVariables().map((v) => `$${v.name}`);
const subscriptionVar = _.find(target.subscriptions, (sub) => _.includes(variableNames, sub));
const subscriptionVar = find(target.subscriptions, (sub) => includes(variableNames, sub));
const interpolatedSubscriptions = ts
.replace(subscriptionVar, scopedVars, (v: string[] | string) => v)
.split(',')
.filter((v) => v.length > 0);
const subscriptions = [
...interpolatedSubscriptions,
..._.filter(target.subscriptions, (sub) => !_.includes(variableNames, sub)),
...filter(target.subscriptions, (sub) => !includes(variableNames, sub)),
];
const query = ts.replace(item.query, scopedVars, interpolateVariable);
@ -63,7 +65,7 @@ export default class AzureResourceGraphDatasource extends DataSourceWithBackend<
};
}
async pagedResourceGraphRequest<T = unknown>(query: string, maxRetries = 1): Promise<T[]> {
async pagedResourceGraphRequest<T>(query: string, maxRetries = 1): Promise<T[]> {
try {
let allFetched = false;
let $skipToken = undefined;
@ -101,6 +103,90 @@ export default class AzureResourceGraphDatasource extends DataSourceWithBackend<
}
}
async getSubscriptions() {
const query = `
resources
| join kind=inner (
ResourceContainers
| where type == 'microsoft.resources/subscriptions'
| project subscriptionName=name, subscriptionURI=id, subscriptionId
) on subscriptionId
| summarize count=count() by subscriptionName, subscriptionURI, subscriptionId
| order by subscriptionName desc
`;
const subscriptions = await this.pagedResourceGraphRequest<RawAzureSubscriptionItem>(query, 1);
return subscriptions;
}
async getResourceGroups(subscriptionId: string, metricNamespacesFilter?: string) {
// We can use subscription ID for the filtering here as they're unique
// The logic of this query is:
// Retrieve _all_ resources a user/app registration/identity has access to
// Filter by the namespaces that support metrics (if this request is from the resource picker)
// Filter to resources contained within the subscription
// Conduct a left-outer join on the resourcecontainers table to allow us to get the case-sensitive resource group name
// Return the count of resources in a group, the URI, and name of the group in ascending order
const query = `resources
${metricNamespacesFilter || ''}
| where subscriptionId == '${subscriptionId}'
| extend resourceGroupURI = strcat("/subscriptions/", subscriptionId, "/resourcegroups/", resourceGroup)
| join kind=leftouter (resourcecontainers
| where type =~ 'microsoft.resources/subscriptions/resourcegroups'
| project resourceGroupName=name, resourceGroupURI=tolower(id)) on resourceGroupURI
| project resourceGroupName=iff(resourceGroupName != "", resourceGroupName, resourceGroup), resourceGroupURI
| summarize count=count() by resourceGroupName, resourceGroupURI
| order by tolower(resourceGroupName) asc `;
const resourceGroups = await this.pagedResourceGraphRequest<RawAzureResourceGroupItem>(query);
return resourceGroups;
}
async getResourceNames(query: AzureGetResourceNamesQuery, metricNamespacesFilter?: string) {
const promises = replaceTemplateVariables(this.templateSrv, query).map(
async ({ metricNamespace, subscriptionId, resourceGroup, region, uri }) => {
const validMetricNamespace = startsWith(metricNamespace?.toLowerCase(), 'microsoft.storage/storageaccounts/')
? 'microsoft.storage/storageaccounts'
: metricNamespace;
// URI takes precedence over subscription ID and resource group
let prefix = uri;
if (!prefix) {
if (subscriptionId) {
prefix = `/subscriptions/${subscriptionId}`;
}
if (resourceGroup) {
prefix += `/resourceGroups/${resourceGroup}`;
}
}
const filters: string[] = [];
if (validMetricNamespace) {
// Ensure the namespace is always lowercase as that's how it's stored in Resource Graph
filters.push(`type == '${validMetricNamespace.toLowerCase()}'`);
}
if (region) {
filters.push(`location == '${region}'`);
}
// We use URIs for the filtering here because resource group names are not unique across subscriptions
// We also add a slash at the end of the URI to ensure we do not pull resources from a resource group
// that has a similar naming prefix e.g. resourceGroup1 and resourceGroup10
const query = `resources${metricNamespacesFilter ? '\n' + metricNamespacesFilter : ''}
| where id hasprefix "${prefix}/"
${filters.length > 0 ? `| where ${filters.join(' and ')}` : ''}
| order by tolower(name) asc`;
const resources = await this.pagedResourceGraphRequest<RawAzureResourceItem>(query);
return resources;
}
);
return (await Promise.all(promises)).flat();
}
// Retrieve metric namespaces relevant to a subscription/resource group/resource
async getMetricNamespaces(resourceUri: string) {
const promises = replaceTemplateVariables(this.templateSrv, { resourceUri }).map(async ({ resourceUri }) => {

@ -34,7 +34,8 @@ export function createMockResourcePickerData() {
const mockDatasource = createMockDatasource();
const mockResourcePicker = new ResourcePickerData(
createMockInstanceSetttings(),
mockDatasource.azureMonitorDatasource
mockDatasource.azureMonitorDatasource,
mockDatasource.azureResourceGraphDatasource
);
mockResourcePicker.getSubscriptions = jest.fn().mockResolvedValue(createMockSubscriptions());

@ -10,6 +10,8 @@ import {
mockResourcesByResourceGroup,
mockSearchResults,
} from '../../__mocks__/resourcePickerRows';
import { DeepPartial } from '../../__mocks__/utils';
import Datasource from '../../datasource';
import ResourcePickerData, { ResourcePickerQueryType } from '../../resourcePicker/resourcePickerData';
import { ResourceRowType } from './types';
@ -32,11 +34,15 @@ const singleResourceSelectionURI =
'/subscriptions/def-456/resourceGroups/dev-3/providers/Microsoft.Compute/virtualMachines/db-server';
const noop = () => {};
function createMockResourcePickerData(preserveImplementation?: string[]) {
const mockDatasource = createMockDatasource();
function createMockResourcePickerData(
preserveImplementation?: string[],
datasourceOverrides?: DeepPartial<Datasource>
) {
const mockDatasource = createMockDatasource(datasourceOverrides);
const mockResourcePicker = new ResourcePickerData(
createMockInstanceSetttings(),
mockDatasource.azureMonitorDatasource
mockDatasource.azureMonitorDatasource,
mockDatasource.azureResourceGraphDatasource
);
const mockFunctions = omit(
@ -332,7 +338,9 @@ describe('AzureMonitor ResourcePicker', () => {
});
it('should not throw an error if no namespaces are found - fallback used', async () => {
const resourcePickerData = createMockResourcePickerData(['getResourceGroupsBySubscriptionId']);
const resourcePickerData = createMockResourcePickerData(['getResourceGroupsBySubscriptionId'], {
azureResourceGraphDatasource: { getResourceGroups: jest.fn().mockResolvedValue([]) },
});
resourcePickerData.postResource = jest.fn().mockResolvedValueOnce({ data: [] });
render(
<ResourcePicker

@ -26,7 +26,17 @@ jest.mock('@grafana/runtime', () => ({
},
}),
}));
const getResourceGroups = jest.fn().mockResolvedValue([{ resourceGroupURI: 'rg', resourceGroupName: 'rg', count: 1 }]);
const getResourceNames = jest.fn().mockResolvedValue([
{
id: 'foobarID',
name: 'foobar',
subscriptionId: 'subID',
resourceGroup: 'resourceGroup',
type: 'foobarType',
location: 'london',
},
]);
const defaultProps = {
query: {
refId: 'A',
@ -39,7 +49,6 @@ const defaultProps = {
onChange: jest.fn(),
datasource: createMockDatasource({
getSubscriptions: jest.fn().mockResolvedValue([{ text: 'Primary Subscription', value: 'sub' }]),
getResourceGroups: jest.fn().mockResolvedValue([{ text: 'rg', value: 'rg' }]),
getMetricNamespaces: jest
.fn()
.mockImplementation(
@ -50,11 +59,16 @@ const defaultProps = {
return [{ text: 'foo/custom', value: 'foo/custom' }];
}
),
getResourceNames: jest.fn().mockResolvedValue([{ text: 'foobar', value: 'foobar' }]),
getVariablesRaw: jest.fn().mockReturnValue([
{ label: 'query0', name: 'sub0' },
{ label: 'query1', name: 'rg', query: { queryType: AzureQueryType.ResourceGroupsQuery } },
]),
azureResourceGraphDatasource: {
getResourceGroups,
getResourceNames,
},
getResourceGroups,
getResourceNames,
}),
};

@ -149,7 +149,7 @@ const VariableEditor = (props: Props) => {
useEffect(() => {
if (subscription) {
datasource.getResourceGroups(subscription).then((rgs) => {
setResourceGroups(rgs.map((s) => ({ label: s.text, value: s.value })));
setResourceGroups(rgs.map((s) => ({ label: s.resourceGroupName, value: s.resourceGroupName })));
});
}
}, [datasource, subscription]);
@ -179,8 +179,8 @@ const VariableEditor = (props: Props) => {
// When subscription, resource group, and namespace are all set, retrieve resource names
useEffect(() => {
if (subscription && resourceGroup && namespace) {
datasource.getResourceNames(subscription, resourceGroup, namespace).then((rgs) => {
setResources(rgs.map((s) => ({ label: s.text, value: s.value })));
datasource.getResourceNames(subscription, resourceGroup, namespace).then((resources) => {
setResources(resources.map((s) => ({ label: s.name, value: s.name })));
});
}
}, [datasource, subscription, resourceGroup, namespace]);

@ -49,7 +49,11 @@ export default class Datasource extends DataSourceWithBackend<AzureMonitorQuery,
this.azureMonitorDatasource = new AzureMonitorDatasource(instanceSettings);
this.azureResourceGraphDatasource = new AzureResourceGraphDatasource(instanceSettings);
this.azureLogAnalyticsDatasource = new AzureLogAnalyticsDatasource(instanceSettings);
this.resourcePickerData = new ResourcePickerData(instanceSettings, this.azureMonitorDatasource);
this.resourcePickerData = new ResourcePickerData(
instanceSettings,
this.azureMonitorDatasource,
this.azureResourceGraphDatasource
);
this.pseudoDatasource = {
[AzureQueryType.AzureMonitor]: this.azureMonitorDatasource,
@ -164,10 +168,6 @@ export default class Datasource extends DataSourceWithBackend<AzureMonitorQuery,
}
/* Azure Monitor REST API methods */
getResourceGroups(subscriptionId: string) {
return this.azureMonitorDatasource.getResourceGroups(this.templateSrv.replace(subscriptionId));
}
getMetricNamespaces(
subscriptionId: string,
resourceGroup?: string,
@ -200,10 +200,6 @@ export default class Datasource extends DataSourceWithBackend<AzureMonitorQuery,
);
}
getResourceNames(subscriptionId: string, resourceGroup?: string, metricNamespace?: string, region?: string) {
return this.azureMonitorDatasource.getResourceNames({ subscriptionId, resourceGroup, metricNamespace, region });
}
getMetricNames(
subscriptionId: string,
resourceGroup: string,
@ -220,13 +216,27 @@ export default class Datasource extends DataSourceWithBackend<AzureMonitorQuery,
});
}
getSubscriptions() {
return this.azureMonitorDatasource.getSubscriptions();
}
/*Azure Log Analytics */
getAzureLogAnalyticsWorkspaces(subscriptionId: string) {
return this.azureLogAnalyticsDatasource.getWorkspaces(subscriptionId);
}
getSubscriptions() {
return this.azureMonitorDatasource.getSubscriptions();
/*Azure Resource Graph */
getResourceGroups(subscriptionId: string) {
return this.azureResourceGraphDatasource.getResourceGroups(this.templateSrv.replace(subscriptionId));
}
getResourceNames(subscriptionId: string, resourceGroup?: string, metricNamespace?: string, region?: string) {
return this.azureResourceGraphDatasource.getResourceNames({
subscriptionId,
resourceGroup,
metricNamespace,
region,
});
}
interpolateVariablesInQueries(queries: AzureMonitorQuery[], scopedVars: ScopedVars): AzureMonitorQuery[] {

@ -6,6 +6,7 @@ import {
import createMockDatasource from '../__mocks__/datasource';
import { createMockInstanceSetttings } from '../__mocks__/instanceSettings';
import { resourceTypes } from '../azureMetadata';
import AzureResourceGraphDatasource from '../azure_resource_graph/azure_resource_graph_datasource';
import { ResourceRowType } from '../components/ResourcePicker/types';
import { AzureGraphResponse } from '../types';
@ -22,18 +23,24 @@ jest.mock('@grafana/runtime', () => ({
const createResourcePickerData = (responses: AzureGraphResponse[], noNamespaces?: boolean) => {
const instanceSettings = createMockInstanceSetttings();
const mockDatasource = createMockDatasource();
const azureResourceGraphDatasource = new AzureResourceGraphDatasource(instanceSettings);
const postResource = jest.fn();
responses.forEach((res) => {
postResource.mockResolvedValueOnce(res);
});
azureResourceGraphDatasource.postResource = postResource;
const mockDatasource = createMockDatasource({ azureResourceGraphDatasource: azureResourceGraphDatasource });
mockDatasource.azureMonitorDatasource.getMetricNamespaces = jest
.fn()
.mockResolvedValueOnce(
noNamespaces ? [] : [{ text: 'Microsoft.Storage/storageAccounts', value: 'Microsoft.Storage/storageAccounts' }]
);
const resourcePickerData = new ResourcePickerData(instanceSettings, mockDatasource.azureMonitorDatasource);
const postResource = jest.fn();
responses.forEach((res) => {
postResource.mockResolvedValueOnce(res);
});
resourcePickerData.postResource = postResource;
const resourcePickerData = new ResourcePickerData(
instanceSettings,
mockDatasource.azureMonitorDatasource,
mockDatasource.azureResourceGraphDatasource
);
return { resourcePickerData, postResource, mockDatasource };
};
@ -301,32 +308,6 @@ describe('AzureMonitor resourcePickerData', () => {
});
});
it('throws an error if it recieves data with a malformed uri', async () => {
const mockResponse = {
data: [
{
id: '/a-differently-formatted/uri/than/the/type/we/planned/to/parse',
name: 'web-server',
type: 'Microsoft.Compute/virtualMachines',
resourceGroup: 'dev',
subscriptionId: 'def-456',
location: 'northeurope',
},
],
};
const { resourcePickerData } = createResourcePickerData([mockResponse]);
try {
await resourcePickerData.getResourcesForResourceGroup('dev', 'logs');
throw Error('expected getResourcesForResourceGroup to fail but it succeeded');
} catch (err) {
if (err instanceof Error) {
expect(err.message).toEqual('unable to fetch resource details');
} else {
throw err;
}
}
});
it('should filter metrics resources', async () => {
const mockSubscriptionsResponse = createMockARGSubscriptionResponse();
const mockResourcesResponse = createARGResourcesResponse();

@ -2,6 +2,7 @@ import { DataSourceWithBackend, reportInteraction } from '@grafana/runtime';
import { logsResourceTypes, resourceTypeDisplayNames, resourceTypes } from '../azureMetadata';
import AzureMonitorDatasource from '../azure_monitor/azure_monitor_datasource';
import AzureResourceGraphDatasource from '../azure_resource_graph/azure_resource_graph_datasource';
import { ResourceRow, ResourceRowGroup, ResourceRowType } from '../components/ResourcePicker/types';
import {
addResources,
@ -14,18 +15,11 @@ import {
import {
AzureMonitorDataSourceInstanceSettings,
AzureMonitorDataSourceJsonData,
AzureGraphResponse,
AzureMonitorResource,
AzureMonitorQuery,
AzureResourceGraphOptions,
AzureResourceSummaryItem,
RawAzureResourceGroupItem,
RawAzureResourceItem,
RawAzureSubscriptionItem,
} from '../types';
import { routeNames } from '../utils/common';
const RESOURCE_GRAPH_URL = '/providers/Microsoft.ResourceGraph/resources?api-version=2021-03-01';
const logsSupportedResourceTypesKusto = logsResourceTypes.map((v) => `"${v}"`).join(',');
@ -35,18 +29,19 @@ export default class ResourcePickerData extends DataSourceWithBackend<
AzureMonitorQuery,
AzureMonitorDataSourceJsonData
> {
private resourcePath: string;
resultLimit = 200;
azureMonitorDatasource;
azureResourceGraphDatasource;
supportedMetricNamespaces = '';
constructor(
instanceSettings: AzureMonitorDataSourceInstanceSettings,
azureMonitorDatasource: AzureMonitorDatasource
azureMonitorDatasource: AzureMonitorDatasource,
azureResourceGraphDatasource: AzureResourceGraphDatasource
) {
super(instanceSettings);
this.resourcePath = `${routeNames.resourceGraph}`;
this.azureMonitorDatasource = azureMonitorDatasource;
this.azureResourceGraphDatasource = azureResourceGraphDatasource;
}
async fetchInitialRows(
@ -111,7 +106,8 @@ export default class ResourcePickerData extends DataSourceWithBackend<
| order by tolower(name) asc
| limit ${this.resultLimit}
`;
const { data: response } = await this.makeResourceGraphRequest<RawAzureResourceItem[]>(searchQuery);
const response =
await this.azureResourceGraphDatasource.pagedResourceGraphRequest<RawAzureResourceItem>(searchQuery);
return response.map((item) => {
const parsedUri = parseResourceURI(item.id);
if (!parsedUri || !(parsedUri.resourceName || parsedUri.resourceGroup || parsedUri.subscription)) {
@ -138,41 +134,14 @@ export default class ResourcePickerData extends DataSourceWithBackend<
});
};
// private
async getSubscriptions(): Promise<ResourceRowGroup> {
const query = `
resources
| join kind=inner (
ResourceContainers
| where type == 'microsoft.resources/subscriptions'
| project subscriptionName=name, subscriptionURI=id, subscriptionId
) on subscriptionId
| summarize count() by subscriptionName, subscriptionURI, subscriptionId
| order by subscriptionName desc
`;
let resources: RawAzureSubscriptionItem[] = [];
let allFetched = false;
let $skipToken = undefined;
while (!allFetched) {
// The response may include several pages
let options: Partial<AzureResourceGraphOptions> = {};
if ($skipToken) {
options = {
$skipToken,
};
}
const resourceResponse = await this.makeResourceGraphRequest<RawAzureSubscriptionItem[]>(query, 1, options);
if (!resourceResponse.data.length) {
throw new Error('No subscriptions were found');
}
resources = resources.concat(resourceResponse.data);
$skipToken = resourceResponse.$skipToken;
allFetched = !$skipToken;
const subscriptions = await this.azureResourceGraphDatasource.getSubscriptions();
if (!subscriptions.length) {
throw new Error('No subscriptions were found');
}
return resources.map((subscription) => ({
return subscriptions.map((subscription) => ({
name: subscription.subscriptionName,
id: subscription.subscriptionId,
uri: `/subscriptions/${subscription.subscriptionId}`,
@ -186,41 +155,9 @@ export default class ResourcePickerData extends DataSourceWithBackend<
subscriptionId: string,
type: ResourcePickerQueryType
): Promise<ResourceRowGroup> {
// We can use subscription ID for the filtering here as they're unique
// The logic of this query is:
// Retrieve _all_ resources a user/app registration/identity has access to
// Filter by the namespaces that support metrics
// Filter to resources contained within the subscription
// Conduct a left-outer join on the resourcecontainers table to allow us to get the case-sensitive resource group name
// Return the count of resources in a group, the URI, and name of the group in ascending order
const query = `
resources
${await this.filterByType(type)}
| where subscriptionId == '${subscriptionId}'
| extend resourceGroupURI = strcat("/subscriptions/", subscriptionId, "/resourcegroups/", resourceGroup)
| join kind=leftouter (resourcecontainers
| where type =~ 'microsoft.resources/subscriptions/resourcegroups'
| project resourceGroupName=name, resourceGroupURI=tolower(id)) on resourceGroupURI
| project resourceGroupName=iff(resourceGroupName != "", resourceGroupName, resourceGroup), resourceGroupURI
| summarize count() by resourceGroupName, resourceGroupURI
| order by tolower(resourceGroupName) asc `;
let resourceGroups: RawAzureResourceGroupItem[] = [];
let allFetched = false;
let $skipToken = undefined;
while (!allFetched) {
// The response may include several pages
let options: Partial<AzureResourceGraphOptions> = {};
if ($skipToken) {
options = {
$skipToken,
};
}
const resourceResponse = await this.makeResourceGraphRequest<RawAzureResourceGroupItem[]>(query, 1, options);
resourceGroups = resourceGroups.concat(resourceResponse.data);
$skipToken = resourceResponse.$skipToken;
allFetched = !$skipToken;
}
const filter = await this.filterByType(type);
const resourceGroups = await this.azureResourceGraphDatasource.getResourceGroups(subscriptionId, filter);
return resourceGroups.map((r) => {
const parsedUri = parseResourceURI(r.resourceGroupURI);
@ -238,50 +175,20 @@ export default class ResourcePickerData extends DataSourceWithBackend<
});
}
async getResourcesForResourceGroup(
resourceGroupUri: string,
type: ResourcePickerQueryType
): Promise<ResourceRowGroup> {
// We use resource group URI for the filtering here because resource group names are not unique across subscriptions
// We also add a slash at the end of the resource group URI to ensure we do not pull resources from a resource group
// that has a similar naming prefix e.g. resourceGroup1 and resourceGroup10
const query = `
resources
| where id hasprefix "${resourceGroupUri}/"
${await this.filterByType(type)}
| order by tolower(name) asc`;
let resources: RawAzureResourceItem[] = [];
let allFetched = false;
let $skipToken = undefined;
while (!allFetched) {
// The response may include several pages
let options: Partial<AzureResourceGraphOptions> = {};
if ($skipToken) {
options = {
$skipToken,
};
}
const resourceResponse = await this.makeResourceGraphRequest<RawAzureResourceItem[]>(query, 1, options);
resources = resources.concat(resourceResponse.data);
$skipToken = resourceResponse.$skipToken;
allFetched = !$skipToken;
}
// Refactor this one out at a later date
async getResourcesForResourceGroup(uri: string, type: ResourcePickerQueryType): Promise<ResourceRowGroup> {
const resources = await this.azureResourceGraphDatasource.getResourceNames({ uri }, await this.filterByType(type));
return resources.map((item) => {
const parsedUri = parseResourceURI(item.id);
if (!parsedUri || !parsedUri.resourceName) {
throw new Error('unable to fetch resource details');
}
return resources.map((resource) => {
return {
name: item.name,
id: parsedUri.resourceName,
uri: item.id,
resourceGroupName: item.resourceGroup,
name: resource.name,
id: resource.name,
uri: resource.id,
resourceGroupName: resource.resourceGroup,
type: ResourceRowType.Resource,
typeLabel: resourceTypeDisplayNames[item.type] || item.type,
locationDisplayName: item.location,
location: item.location,
typeLabel: resourceTypeDisplayNames[resource.type] || resource.type,
locationDisplayName: resource.location,
location: resource.location,
};
});
}
@ -321,7 +228,7 @@ export default class ResourcePickerData extends DataSourceWithBackend<
| project subscriptionName, resourceGroupName, resourceName
`;
const { data: response } = await this.makeResourceGraphRequest<AzureResourceSummaryItem[]>(query);
const response = await this.azureResourceGraphDatasource.pagedResourceGraphRequest<AzureResourceSummaryItem>(query);
if (!response.length) {
throw new Error('unable to fetch resource details');
@ -339,7 +246,7 @@ export default class ResourcePickerData extends DataSourceWithBackend<
}
async getResourceURIFromWorkspace(workspace: string) {
const { data: response } = await this.makeResourceGraphRequest<RawAzureResourceItem[]>(`
const response = await this.azureResourceGraphDatasource.pagedResourceGraphRequest<RawAzureResourceItem>(`
resources
| where properties['customerId'] == "${workspace}"
| project id
@ -352,28 +259,6 @@ export default class ResourcePickerData extends DataSourceWithBackend<
return response[0].id;
}
async makeResourceGraphRequest<T = unknown>(
query: string,
maxRetries = 1,
reqOptions?: Partial<AzureResourceGraphOptions>
): Promise<AzureGraphResponse<T>> {
try {
return await this.postResource(this.resourcePath + RESOURCE_GRAPH_URL, {
query: query,
options: {
resultFormat: 'objectArray',
...reqOptions,
},
});
} catch (error) {
if (maxRetries > 0) {
return this.makeResourceGraphRequest(query, maxRetries - 1);
}
throw error;
}
}
private filterByType = async (t: ResourcePickerQueryType) => {
if (this.supportedMetricNamespaces === '' && t !== 'logs') {
await this.fetchAllNamespaces();

@ -145,11 +145,14 @@ export interface AzureResourceSummaryItem {
export interface RawAzureSubscriptionItem {
subscriptionName: string;
subscriptionId: string;
subscriptionURI: string;
count: number;
}
export interface RawAzureResourceGroupItem {
resourceGroupURI: string;
resourceGroupName: string;
count: number;
}
export interface RawAzureResourceItem {
@ -226,10 +229,11 @@ export interface LegacyAzureGetMetricMetadataQuery {
}
export interface AzureGetResourceNamesQuery {
subscriptionId: string;
subscriptionId?: string;
resourceGroup?: string;
metricNamespace?: string;
region?: string;
uri?: string;
}
export interface AzureMonitorLocations {

@ -91,7 +91,7 @@ describe('VariableSupport', () => {
});
it('can fetch resourceNames with a subscriptionId', async () => {
const expectedResults = ['test'];
const expectedResults = [{ name: 'test' }];
const variableSupport = new VariableSupport(
createMockDatasource({
getResourceNames: jest.fn().mockResolvedValueOnce(expectedResults),
@ -113,7 +113,7 @@ describe('VariableSupport', () => {
],
} as DataQueryRequest<AzureMonitorQuery>;
const result = await lastValueFrom(variableSupport.query(mockRequest));
expect(result.data[0].fields[0].values).toEqual(expectedResults);
expect(result.data[0].fields[0].values).toEqual([expectedResults[0].name]);
});
it('can fetch a metricNamespace with a subscriptionId', async () => {
@ -448,7 +448,7 @@ describe('VariableSupport', () => {
});
it('can fetch resource names', async () => {
const expectedResults = ['test'];
const expectedResults = [{ name: 'test' }];
const variableSupport = new VariableSupport(
createMockDatasource({
getResourceNames: jest.fn().mockResolvedValueOnce(expectedResults),
@ -464,7 +464,7 @@ describe('VariableSupport', () => {
],
} as DataQueryRequest<AzureMonitorQuery>;
const result = await lastValueFrom(variableSupport.query(mockRequest));
expect(result.data[0].fields[0].values).toEqual(expectedResults);
expect(result.data[0].fields[0].values).toEqual([expectedResults[0].name]);
});
it('returns no data if calling resourceNames but the subscription is a template variable with no value', async () => {

@ -1,3 +1,4 @@
import { startsWith } from 'lodash';
import { from, lastValueFrom, Observable } from 'rxjs';
import {
@ -13,10 +14,26 @@ import UrlBuilder from './azure_monitor/url_builder';
import VariableEditor from './components/VariableEditor/VariableEditor';
import DataSource from './datasource';
import { migrateQuery } from './grafanaTemplateVariableFns';
import { AzureMonitorQuery, AzureQueryType } from './types';
import { AzureMonitorQuery, AzureQueryType, RawAzureResourceItem } from './types';
import { GrafanaTemplateVariableQuery } from './types/templateVariables';
import messageFromError from './utils/messageFromError';
export function parseResourceNamesAsTemplateVariable(resources: RawAzureResourceItem[], metricNamespace?: string) {
return resources.map((r) => {
if (startsWith(metricNamespace?.toLowerCase(), 'microsoft.storage/storageaccounts/')) {
return {
text: r.name + '/default',
value: r.name + '/default',
};
}
return {
text: r.name,
value: r.name,
};
});
}
export class VariableSupport extends CustomVariableSupport<DataSource, AzureMonitorQuery> {
constructor(
private readonly datasource: DataSource,
@ -67,14 +84,14 @@ export class VariableSupport extends CustomVariableSupport<DataSource, AzureMoni
return { data: [] };
case AzureQueryType.ResourceNamesQuery:
if (queryObj.subscription && this.hasValue(queryObj.subscription)) {
const rgs = await this.datasource.getResourceNames(
const resources = await this.datasource.getResourceNames(
queryObj.subscription,
queryObj.resourceGroup,
queryObj.namespace,
queryObj.region
);
return {
data: rgs?.length ? [toDataFrame(rgs)] : [],
data: resources?.length ? [toDataFrame(parseResourceNamesAsTemplateVariable(resources))] : [],
};
}
return { data: [] };
@ -201,15 +218,28 @@ export class VariableSupport extends CustomVariableSupport<DataSource, AzureMoni
}
if (query.kind === 'ResourceGroupsQuery') {
return this.datasource.getResourceGroups(this.replaceVariable(query.subscription));
return this.datasource.getResourceGroups(this.replaceVariable(query.subscription)).then((rgs) => {
if (rgs.length > 0) {
return rgs.map((rg) => ({ text: rg.resourceGroupName, value: rg.resourceGroupName }));
}
return [];
});
}
if (query.kind === 'ResourceNamesQuery') {
return this.datasource.getResourceNames(
this.replaceVariable(query.subscription),
this.replaceVariable(query.resourceGroup),
this.replaceVariable(query.metricNamespace)
);
return this.datasource
.getResourceNames(
this.replaceVariable(query.subscription),
this.replaceVariable(query.resourceGroup),
this.replaceVariable(query.metricNamespace)
)
.then((resources) => {
if (resources.length > 0) {
return parseResourceNamesAsTemplateVariable(resources, query.metricNamespace);
}
return [];
});
}
if (query.kind === 'MetricNamespaceQuery') {

Loading…
Cancel
Save