Преглед на файлове

Azure Monitor: Add support for cross resource queries (#19115)

* Add new query mode picker with different states for each query. Also really simple migration script

* Populate cross resource dropdowns

* Cleanup. Handle change events

* Add multi select picker for subscriptions

* Fix markup issue

* Prepare for new query mode

* More cleanup

* Handle multiple queries both in ds and backend

* Refactoring

* Improve migration

* Add support for multiselect display name

* Use multiselect also for locations and resources

* Add more typings

* Fix migrations

* Custom multiselect built for array of options instead of variables

* Add url builder test

* fix datasource tests

* UI fixes

* Improve query editor init

* Fix brokens tests

* Cleanup

* Fix tslint issue

* Change query mode display name

* Make sure alerting works for single queries

* Friendly error for multi resources

* Add temporary typings
Erik Sundell преди 6 години
родител
ревизия
88051258e9
променени са 16 файла, в които са добавени 1039 реда и са изтрити 311 реда
  1. 0 0
      pkg/services/provisioning/datasources/testdata/zero-datasources/placeholder-for-git
  2. 31 13
      pkg/tsdb/azuremonitor/azuremonitor-datasource.go
  3. 14 9
      pkg/tsdb/azuremonitor/azuremonitor-datasource_test.go
  4. 6 6
      public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/azure_monitor_datasource.test.ts
  5. 192 58
      public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/azure_monitor_datasource.ts
  6. 3 2
      public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/response_parser.ts
  7. 30 12
      public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/url_builder.test.ts
  8. 6 8
      public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/url_builder.ts
  9. 13 6
      public/app/plugins/datasource/grafana-azure-monitor-datasource/datasource.ts
  10. 15 0
      public/app/plugins/datasource/grafana-azure-monitor-datasource/migrations.ts
  11. 169 0
      public/app/plugins/datasource/grafana-azure-monitor-datasource/multi-select.directive.ts
  12. 24 0
      public/app/plugins/datasource/grafana-azure-monitor-datasource/partials/multi-select.directive.html
  13. 104 36
      public/app/plugins/datasource/grafana-azure-monitor-datasource/partials/query.editor.html
  14. 29 29
      public/app/plugins/datasource/grafana-azure-monitor-datasource/query_ctrl.test.ts
  15. 376 130
      public/app/plugins/datasource/grafana-azure-monitor-datasource/query_ctrl.ts
  16. 27 2
      public/app/plugins/datasource/grafana-azure-monitor-datasource/types.ts

+ 0 - 0
pkg/services/provisioning/datasources/testdata/zero-datasources/placeholder-for-git


+ 31 - 13
pkg/tsdb/azuremonitor/azuremonitor-datasource.go

@@ -60,7 +60,11 @@ func (e *AzureMonitorDatasource) executeTimeSeriesQuery(ctx context.Context, ori
 		if err != nil {
 			queryRes.Error = err
 		}
-		result.Results[query.RefID] = queryRes
+		if val, ok := result.Results[query.RefID]; ok {
+			val.Series = append(result.Results[query.RefID].Series, queryRes.Series...)
+		} else {
+			result.Results[query.RefID] = queryRes
+		}
 	}
 
 	return result, nil
@@ -84,11 +88,22 @@ func (e *AzureMonitorDatasource) buildQueries(queries []*tsdb.Query, timeRange *
 		azureMonitorTarget := query.Model.Get("azureMonitor").MustMap()
 		azlog.Debug("AzureMonitor", "target", azureMonitorTarget)
 
+		queryMode := fmt.Sprintf("%v", azureMonitorTarget["queryMode"])
+		if queryMode == "crossResource" {
+			return nil, fmt.Errorf("Alerting not supported for multiple resource queries")
+		}
+
+		var azureMonitorData map[string]interface{}
+		if queryMode == "singleResource" {
+			azureMonitorData = azureMonitorTarget["data"].(map[string]interface{})[queryMode].(map[string]interface{})
+		} else {
+			azureMonitorData = azureMonitorTarget
+		}
 		urlComponents := map[string]string{}
 		urlComponents["subscription"] = fmt.Sprintf("%v", query.Model.Get("subscription").MustString())
-		urlComponents["resourceGroup"] = fmt.Sprintf("%v", azureMonitorTarget["resourceGroup"])
-		urlComponents["metricDefinition"] = fmt.Sprintf("%v", azureMonitorTarget["metricDefinition"])
-		urlComponents["resourceName"] = fmt.Sprintf("%v", azureMonitorTarget["resourceName"])
+		urlComponents["resourceGroup"] = fmt.Sprintf("%v", azureMonitorData["resourceGroup"])
+		urlComponents["metricDefinition"] = fmt.Sprintf("%v", azureMonitorData["metricDefinition"])
+		urlComponents["resourceName"] = fmt.Sprintf("%v", azureMonitorData["resourceName"])
 
 		ub := urlBuilder{
 			DefaultSubscription: query.DataSource.JsonData.Get("subscriptionId").MustString(),
@@ -100,12 +115,12 @@ func (e *AzureMonitorDatasource) buildQueries(queries []*tsdb.Query, timeRange *
 		azureURL := ub.Build()
 
 		alias := ""
-		if val, ok := azureMonitorTarget["alias"]; ok {
+		if val, ok := azureMonitorData["alias"]; ok {
 			alias = fmt.Sprintf("%v", val)
 		}
 
-		timeGrain := fmt.Sprintf("%v", azureMonitorTarget["timeGrain"])
-		timeGrains := azureMonitorTarget["allowedTimeGrainsMs"]
+		timeGrain := fmt.Sprintf("%v", azureMonitorData["timeGrain"])
+		timeGrains := azureMonitorData["allowedTimeGrainsMs"]
 		if timeGrain == "auto" {
 			timeGrain, err = e.setAutoTimeGrain(query.IntervalMs, timeGrains)
 			if err != nil {
@@ -117,13 +132,16 @@ func (e *AzureMonitorDatasource) buildQueries(queries []*tsdb.Query, timeRange *
 		params.Add("api-version", "2018-01-01")
 		params.Add("timespan", fmt.Sprintf("%v/%v", startTime.UTC().Format(time.RFC3339), endTime.UTC().Format(time.RFC3339)))
 		params.Add("interval", timeGrain)
-		params.Add("aggregation", fmt.Sprintf("%v", azureMonitorTarget["aggregation"]))
-		params.Add("metricnames", fmt.Sprintf("%v", azureMonitorTarget["metricName"]))
-		params.Add("metricnamespace", fmt.Sprintf("%v", azureMonitorTarget["metricNamespace"]))
+		params.Add("aggregation", fmt.Sprintf("%v", azureMonitorData["aggregation"]))
+		params.Add("metricnames", fmt.Sprintf("%v", azureMonitorData["metricName"]))
+
+		if val, ok := azureMonitorData["metricNamespace"]; ok {
+			params.Add("metricnamespace", fmt.Sprintf("%v", val))
+		}
 
-		dimension := strings.TrimSpace(fmt.Sprintf("%v", azureMonitorTarget["dimension"]))
-		dimensionFilter := strings.TrimSpace(fmt.Sprintf("%v", azureMonitorTarget["dimensionFilter"]))
-		if azureMonitorTarget["dimension"] != nil && azureMonitorTarget["dimensionFilter"] != nil && len(dimension) > 0 && len(dimensionFilter) > 0 && dimension != "None" {
+		dimension := strings.TrimSpace(fmt.Sprintf("%v", azureMonitorData["dimension"]))
+		dimensionFilter := strings.TrimSpace(fmt.Sprintf("%v", azureMonitorData["dimensionFilter"]))
+		if azureMonitorData["dimension"] != nil && azureMonitorData["dimensionFilter"] != nil && len(dimension) > 0 && len(dimensionFilter) > 0 && dimension != "None" {
 			params.Add("$filter", fmt.Sprintf("%s eq '%s'", dimension, dimensionFilter))
 		}
 

+ 14 - 9
pkg/tsdb/azuremonitor/azuremonitor-datasource_test.go

@@ -36,15 +36,20 @@ func TestAzureMonitorDatasource(t *testing.T) {
 						Model: simplejson.NewFromAny(map[string]interface{}{
 							"subscription": "12345678-aaaa-bbbb-cccc-123456789abc",
 							"azureMonitor": map[string]interface{}{
-								"timeGrain":        "PT1M",
-								"aggregation":      "Average",
-								"resourceGroup":    "grafanastaging",
-								"resourceName":     "grafana",
-								"metricDefinition": "Microsoft.Compute/virtualMachines",
-								"metricNamespace":  "Microsoft.Compute-virtualMachines",
-								"metricName":       "Percentage CPU",
-								"alias":            "testalias",
-								"queryType":        "Azure Monitor",
+								"queryMode": "singleResource",
+								"data": map[string]interface{}{
+									"singleResource": map[string]interface{}{
+										"timeGrain":        "PT1M",
+										"aggregation":      "Average",
+										"resourceGroup":    "grafanastaging",
+										"resourceName":     "grafana",
+										"metricDefinition": "Microsoft.Compute/virtualMachines",
+										"metricNamespace":  "Microsoft.Compute-virtualMachines",
+										"metricName":       "Percentage CPU",
+										"alias":            "testalias",
+										"queryType":        "Azure Monitor",
+									},
+								},
 							},
 						}),
 						RefId: "A",

+ 6 - 6
public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/azure_monitor_datasource.test.ts

@@ -918,8 +918,8 @@ describe('AzureMonitorDatasource', () => {
           'nodeapp',
           'microsoft.insights/components',
           'resource1',
-          'default',
-          'UsedCapacity'
+          'UsedCapacity',
+          'default'
         )
         .then((results: any) => {
           expect(results.primaryAggType).toEqual('Total');
@@ -992,8 +992,8 @@ describe('AzureMonitorDatasource', () => {
           'nodeapp',
           'microsoft.insights/components',
           'resource1',
-          'default',
-          'Transactions'
+          'Transactions',
+          'default'
         )
         .then((results: any) => {
           expect(results.dimensions.length).toEqual(4);
@@ -1011,8 +1011,8 @@ describe('AzureMonitorDatasource', () => {
           'nodeapp',
           'microsoft.insights/components',
           'resource1',
-          'default',
-          'FreeCapacity'
+          'FreeCapacity',
+          'default'
         )
         .then((results: any) => {
           expect(results.dimensions.length).toEqual(0);

+ 192 - 58
public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/azure_monitor_datasource.ts

@@ -5,9 +5,12 @@ import SupportedNamespaces from './supported_namespaces';
 import TimegrainConverter from '../time_grain_converter';
 import {
   AzureMonitorQuery,
+  AzureMonitorQueryData,
   AzureDataSourceJsonData,
   AzureMonitorMetricDefinitionsResponse,
   AzureMonitorResourceGroupsResponse,
+  AzureMonitorResourceResponse,
+  Resource,
 } from '../types';
 import { DataQueryRequest, DataQueryResponseData, DataSourceInstanceSettings } from '@grafana/ui';
 
@@ -47,60 +50,172 @@ export default class AzureMonitorDatasource {
     return !!this.subscriptionId && this.subscriptionId.length > 0;
   }
 
-  async query(options: DataQueryRequest<AzureMonitorQuery>): Promise<DataQueryResponseData[]> {
-    const queries = _.filter(options.targets, item => {
-      return (
-        item.hide !== true &&
-        item.azureMonitor.resourceGroup &&
-        item.azureMonitor.resourceGroup !== this.defaultDropdownValue &&
-        item.azureMonitor.resourceName &&
-        item.azureMonitor.resourceName !== this.defaultDropdownValue &&
-        item.azureMonitor.metricDefinition &&
-        item.azureMonitor.metricDefinition !== this.defaultDropdownValue &&
-        item.azureMonitor.metricName &&
-        item.azureMonitor.metricName !== this.defaultDropdownValue
-      );
-    }).map(target => {
-      const item = target.azureMonitor;
+  buildQuery(
+    options: DataQueryRequest<AzureMonitorQuery>,
+    target: any,
+    {
+      resourceGroup,
+      resourceName,
+      metricDefinition,
+      timeGrainUnit,
+      timeGrain,
+      metricName,
+      metricNamespace,
+      allowedTimeGrainsMs,
+      aggregation,
+      dimension,
+      dimensionFilter,
+      alias,
+    }: AzureMonitorQueryData,
+    subscriptionId?: string
+  ) {
+    if (timeGrainUnit && timeGrain !== 'auto') {
+      timeGrain = TimegrainConverter.createISO8601Duration(timeGrain, timeGrainUnit);
+    }
 
-      // fix for timeGrainUnit which is a deprecated/removed field name
-      if (item.timeGrainUnit && item.timeGrain !== 'auto') {
-        item.timeGrain = TimegrainConverter.createISO8601Duration(item.timeGrain, item.timeGrainUnit);
-      }
+    const metricNamespaceParsed = this.templateSrv.replace(metricNamespace, options.scopedVars);
+
+    return {
+      refId: target.refId,
+      intervalMs: options.intervalMs,
+      datasourceId: this.id,
+      subscription: this.templateSrv.replace(
+        subscriptionId || target.subscription || this.subscriptionId,
+        options.scopedVars
+      ),
+      queryType: 'Azure Monitor',
+      type: 'timeSeriesQuery',
+      raw: false,
+      azureMonitor: {
+        resourceGroup: this.templateSrv.replace(resourceGroup, options.scopedVars),
+        resourceName: this.templateSrv.replace(resourceName, options.scopedVars),
+        metricDefinition: this.templateSrv.replace(metricDefinition, options.scopedVars),
+        timeGrain: this.templateSrv.replace((timeGrain || '').toString(), options.scopedVars),
+        allowedTimeGrainsMs: allowedTimeGrainsMs,
+        metricName: this.templateSrv.replace(metricName, options.scopedVars),
+        metricNamespace:
+          metricNamespaceParsed && metricNamespaceParsed !== this.defaultDropdownValue
+            ? metricNamespaceParsed
+            : metricDefinition,
+        aggregation: this.templateSrv.replace(aggregation, options.scopedVars),
+        dimension: this.templateSrv.replace(dimension, options.scopedVars),
+        dimensionFilter: this.templateSrv.replace(dimensionFilter, options.scopedVars),
+        alias,
+        format: target.format,
+      },
+    };
+  }
 
-      const subscriptionId = this.templateSrv.replace(target.subscription || this.subscriptionId, options.scopedVars);
-      const resourceGroup = this.templateSrv.replace(item.resourceGroup, options.scopedVars);
-      const resourceName = this.templateSrv.replace(item.resourceName, options.scopedVars);
-      const metricNamespace = this.templateSrv.replace(item.metricNamespace, options.scopedVars);
-      const metricDefinition = this.templateSrv.replace(item.metricDefinition, options.scopedVars);
-      const timeGrain = this.templateSrv.replace((item.timeGrain || '').toString(), options.scopedVars);
-      const aggregation = this.templateSrv.replace(item.aggregation, options.scopedVars);
+  buildSingleQuery(
+    options: DataQueryRequest<AzureMonitorQuery>,
+    target: any,
+    {
+      resourceGroup,
+      resourceName,
+      metricDefinition,
+      timeGrainUnit,
+      timeGrain,
+      metricName,
+      metricNamespace,
+      allowedTimeGrainsMs,
+      aggregation,
+      dimension,
+      dimensionFilter,
+      alias,
+    }: AzureMonitorQueryData,
+    queryMode: string
+  ) {
+    if (timeGrainUnit && timeGrain !== 'auto') {
+      timeGrain = TimegrainConverter.createISO8601Duration(timeGrain, timeGrainUnit);
+    }
 
-      return {
-        refId: target.refId,
-        intervalMs: options.intervalMs,
-        datasourceId: this.id,
-        subscription: subscriptionId,
-        queryType: 'Azure Monitor',
-        type: 'timeSeriesQuery',
-        raw: false,
-        azureMonitor: {
-          resourceGroup: resourceGroup,
-          resourceName: resourceName,
-          metricDefinition: metricDefinition,
-          timeGrain: timeGrain,
-          allowedTimeGrainsMs: item.allowedTimeGrainsMs,
-          metricName: this.templateSrv.replace(item.metricName, options.scopedVars),
-          metricNamespace:
-            metricNamespace && metricNamespace !== this.defaultDropdownValue ? metricNamespace : metricDefinition,
-          aggregation: aggregation,
-          dimension: this.templateSrv.replace(item.dimension, options.scopedVars),
-          dimensionFilter: this.templateSrv.replace(item.dimensionFilter, options.scopedVars),
-          alias: item.alias,
-          format: target.format,
+    const metricNamespaceParsed = this.templateSrv.replace(metricNamespace, options.scopedVars);
+
+    return {
+      refId: target.refId,
+      intervalMs: options.intervalMs,
+      datasourceId: this.id,
+      subscription: this.templateSrv.replace(target.subscription || this.subscriptionId, options.scopedVars),
+      queryType: 'Azure Monitor',
+      type: 'timeSeriesQuery',
+      raw: false,
+      azureMonitor: {
+        queryMode,
+        data: {
+          [queryMode]: {
+            resourceGroup: this.templateSrv.replace(resourceGroup, options.scopedVars),
+            resourceName: this.templateSrv.replace(resourceName, options.scopedVars),
+            metricDefinition: this.templateSrv.replace(metricDefinition, options.scopedVars),
+            timeGrain: this.templateSrv.replace((timeGrain || '').toString(), options.scopedVars),
+            allowedTimeGrainsMs: allowedTimeGrainsMs,
+            metricName: this.templateSrv.replace(metricName, options.scopedVars),
+            metricNamespace:
+              metricNamespaceParsed && metricNamespaceParsed !== this.defaultDropdownValue
+                ? metricNamespaceParsed
+                : metricDefinition,
+            aggregation: this.templateSrv.replace(aggregation, options.scopedVars),
+            dimension: this.templateSrv.replace(dimension, options.scopedVars),
+            dimensionFilter: this.templateSrv.replace(dimensionFilter, options.scopedVars),
+            alias,
+            format: target.format,
+          },
         },
-      };
-    });
+      },
+    };
+  }
+
+  async query(options: DataQueryRequest<any>): Promise<DataQueryResponseData[]> {
+    const groupedQueries: any[] = await Promise.all(
+      options.targets
+        .filter(item => {
+          const { data, queryMode } = item.azureMonitor;
+          const { resourceGroup, resourceGroups, metricDefinition, metricName } = data[queryMode];
+
+          return (
+            item.hide !== true &&
+            ((resourceGroup && resourceGroup !== this.defaultDropdownValue) || resourceGroups.length) &&
+            metricDefinition &&
+            metricDefinition !== this.defaultDropdownValue &&
+            metricName &&
+            metricName !== this.defaultDropdownValue
+          );
+        })
+        .map(async target => {
+          const { data, queryMode } = target.azureMonitor;
+
+          if (queryMode === 'crossResource') {
+            const { resourceGroups, metricDefinition, locations } = data[queryMode];
+            const resources = await this.getResources(target.subscriptions).then(resources =>
+              resources.filter(
+                ({ type, group, subscriptionId, location }) =>
+                  target.subscriptions.includes(subscriptionId) &&
+                  resourceGroups.includes(group) &&
+                  locations.includes(location) &&
+                  metricDefinition === type
+              )
+            );
+            delete data.crossResource.metricNamespace;
+            return resources.map(
+              ({ type: metricDefinition, group: resourceGroup, subscriptionId, name: resourceName }) =>
+                this.buildQuery(
+                  options,
+                  target,
+                  {
+                    ...data[queryMode],
+                    metricDefinition,
+                    resourceGroup,
+                    resourceName,
+                  },
+                  subscriptionId
+                )
+            );
+          } else {
+            return Promise.resolve(this.buildSingleQuery(options, target, data[queryMode], queryMode));
+          }
+        })
+    );
+
+    const queries = _.flatten(groupedQueries);
 
     if (!queries || queries.length === 0) {
       return Promise.resolve([]);
@@ -118,7 +233,7 @@ export default class AzureMonitorDatasource {
 
     const result: DataQueryResponseData[] = [];
     if (data.results) {
-      Object['values'](data.results).forEach((queryRes: any) => {
+      Object.values(data.results).forEach((queryRes: any) => {
         if (!queryRes.series) {
           return;
         }
@@ -337,12 +452,31 @@ export default class AzureMonitorDatasource {
     });
   }
 
+  async getResources(subscriptionIds: string[]): Promise<Resource[]> {
+    const responses: Resource[][] = await Promise.all(
+      subscriptionIds.map(subscriptionId =>
+        this.doRequest(`${this.baseUrl}/${subscriptionId}/resources?api-version=2018-02-01`).then(
+          (res: AzureMonitorResourceResponse) =>
+            res.data.value
+              .map(r => ({
+                ...r,
+                group: /.*\/resourceGroups\/(.*?)\//.exec(r.id)[1],
+                subscriptionId,
+              }))
+              .filter(({ type }) => this.supportedMetricNamespaces.includes(type))
+        )
+      )
+    );
+
+    return responses.reduce((result, resources) => [...result, ...resources], []);
+  }
+
   getMetricNames(
     subscriptionId: string,
     resourceGroup: string,
     metricDefinition: string,
     resourceName: string,
-    metricNamespace: string
+    metricNamespace?: string
   ) {
     const url = UrlBuilder.buildAzureMonitorGetMetricNamesUrl(
       this.baseUrl,
@@ -350,8 +484,8 @@ export default class AzureMonitorDatasource {
       resourceGroup,
       metricDefinition,
       resourceName,
-      metricNamespace,
-      this.apiVersion
+      this.apiVersion,
+      metricNamespace
     );
 
     return this.doRequest(url).then((result: any) => {
@@ -364,8 +498,8 @@ export default class AzureMonitorDatasource {
     resourceGroup: string,
     metricDefinition: string,
     resourceName: string,
-    metricNamespace: string,
-    metricName: string
+    metricName: string,
+    metricNamespace?: string
   ) {
     const url = UrlBuilder.buildAzureMonitorGetMetricNamesUrl(
       this.baseUrl,
@@ -373,8 +507,8 @@ export default class AzureMonitorDatasource {
       resourceGroup,
       metricDefinition,
       resourceName,
-      metricNamespace,
-      this.apiVersion
+      this.apiVersion,
+      metricNamespace
     );
 
     return this.doRequest(url).then((result: any) => {

+ 3 - 2
public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/response_parser.ts

@@ -108,8 +108,8 @@ export default class ResponseParser {
     return dimensions;
   }
 
-  static parseSubscriptions(result: any): Array<{ text: string; value: string }> {
-    const list: Array<{ text: string; value: string }> = [];
+  static parseSubscriptions(result: any): Array<{ text: string; value: string; displayName: string }> {
+    const list: Array<{ text: string; value: string; displayName: string }> = [];
 
     if (!result) {
       return list;
@@ -122,6 +122,7 @@ export default class ResponseParser {
         list.push({
           text: `${_.get(result.data.value[i], textFieldName)} - ${_.get(result.data.value[i], valueFieldName)}`,
           value: _.get(result.data.value[i], valueFieldName),
+          displayName: _.get(result.data.value[i], textFieldName),
         });
       }
     }

+ 30 - 12
public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/url_builder.test.ts

@@ -9,8 +9,8 @@ describe('AzureMonitorUrlBuilder', () => {
         'rg',
         'Microsoft.Sql/servers/databases',
         'rn1/rn2',
-        'default',
-        '2017-05-01-preview'
+        '2017-05-01-preview',
+        'default'
       );
       expect(url).toBe(
         '/sub1/resourceGroups/rg/providers/Microsoft.Sql/servers/rn1/databases/rn2/' +
@@ -27,8 +27,8 @@ describe('AzureMonitorUrlBuilder', () => {
         'rg',
         'Microsoft.Sql/servers',
         'rn',
-        'default',
-        '2017-05-01-preview'
+        '2017-05-01-preview',
+        'default'
       );
       expect(url).toBe(
         '/sub1/resourceGroups/rg/providers/Microsoft.Sql/servers/rn/' +
@@ -45,8 +45,8 @@ describe('AzureMonitorUrlBuilder', () => {
         'rg',
         'Microsoft.Storage/storageAccounts/blobServices',
         'rn1/default',
-        'default',
-        '2017-05-01-preview'
+        '2017-05-01-preview',
+        'default'
       );
       expect(url).toBe(
         '/sub1/resourceGroups/rg/providers/Microsoft.Storage/storageAccounts/rn1/blobServices/default/' +
@@ -63,8 +63,8 @@ describe('AzureMonitorUrlBuilder', () => {
         'rg',
         'Microsoft.Storage/storageAccounts/fileServices',
         'rn1/default',
-        'default',
-        '2017-05-01-preview'
+        '2017-05-01-preview',
+        'default'
       );
       expect(url).toBe(
         '/sub1/resourceGroups/rg/providers/Microsoft.Storage/storageAccounts/rn1/fileServices/default/' +
@@ -81,8 +81,8 @@ describe('AzureMonitorUrlBuilder', () => {
         'rg',
         'Microsoft.Storage/storageAccounts/tableServices',
         'rn1/default',
-        'default',
-        '2017-05-01-preview'
+        '2017-05-01-preview',
+        'default'
       );
       expect(url).toBe(
         '/sub1/resourceGroups/rg/providers/Microsoft.Storage/storageAccounts/rn1/tableServices/default/' +
@@ -99,8 +99,8 @@ describe('AzureMonitorUrlBuilder', () => {
         'rg',
         'Microsoft.Storage/storageAccounts/queueServices',
         'rn1/default',
-        'default',
-        '2017-05-01-preview'
+        '2017-05-01-preview',
+        'default'
       );
       expect(url).toBe(
         '/sub1/resourceGroups/rg/providers/Microsoft.Storage/storageAccounts/rn1/queueServices/default/' +
@@ -108,4 +108,22 @@ describe('AzureMonitorUrlBuilder', () => {
       );
     });
   });
+
+  describe('when metric namespace is missing', () => {
+    it('should be excluded from the query', () => {
+      const url = UrlBuilder.buildAzureMonitorGetMetricNamesUrl(
+        '',
+        'sub1',
+        'rg',
+        'Microsoft.Storage/storageAccounts/queueServices',
+        'rn1/default',
+        '2017-05-01-preview'
+      );
+
+      expect(url).toBe(
+        '/sub1/resourceGroups/rg/providers/Microsoft.Storage/storageAccounts/rn1/queueServices/default/' +
+          'providers/microsoft.insights/metricdefinitions?api-version=2017-05-01-preview'
+      );
+    });
+  });
 });

+ 6 - 8
public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/url_builder.ts

@@ -29,26 +29,24 @@ export default class UrlBuilder {
     resourceGroup: string,
     metricDefinition: string,
     resourceName: string,
-    metricNamespace: string,
-    apiVersion: string
+    apiVersion: string,
+    metricNamespace?: string
   ) {
+    const metricNameSpaceParam = metricNamespace ? `&metricnamespace=${encodeURIComponent(metricNamespace)}` : '';
     if ((metricDefinition.match(/\//g) || []).length > 1) {
       const rn = resourceName.split('/');
       const service = metricDefinition.substring(metricDefinition.lastIndexOf('/') + 1);
       const md = metricDefinition.substring(0, metricDefinition.lastIndexOf('/'));
+
       return (
         `${baseUrl}/${subscriptionId}/resourceGroups/${resourceGroup}/providers/${md}/${rn[0]}/${service}/${rn[1]}` +
-        `/providers/microsoft.insights/metricdefinitions?api-version=${apiVersion}&metricnamespace=${encodeURIComponent(
-          metricNamespace
-        )}`
+        `/providers/microsoft.insights/metricdefinitions?api-version=${apiVersion}${metricNameSpaceParam}`
       );
     }
 
     return (
       `${baseUrl}/${subscriptionId}/resourceGroups/${resourceGroup}/providers/${metricDefinition}/${resourceName}` +
-      `/providers/microsoft.insights/metricdefinitions?api-version=${apiVersion}&metricnamespace=${encodeURIComponent(
-        metricNamespace
-      )}`
+      `/providers/microsoft.insights/metricdefinitions?api-version=${apiVersion}${metricNameSpaceParam}`
     );
   }
 }

+ 13 - 6
public/app/plugins/datasource/grafana-azure-monitor-datasource/datasource.ts

@@ -1,4 +1,5 @@
 import _ from 'lodash';
+import { migrateTargetSchema } from './migrations';
 import AzureMonitorDatasource from './azure_monitor/azure_monitor_datasource';
 import AppInsightsDatasource from './app_insights/app_insights_datasource';
 import AzureLogAnalyticsDatasource from './azure_log_analytics/azure_log_analytics_datasource';
@@ -42,7 +43,9 @@ export default class Datasource extends DataSourceApi<AzureMonitorQuery, AzureDa
     const appInsightsOptions = _.cloneDeep(options);
     const azureLogAnalyticsOptions = _.cloneDeep(options);
 
-    azureMonitorOptions.targets = _.filter(azureMonitorOptions.targets, ['queryType', 'Azure Monitor']);
+    azureMonitorOptions.targets = azureMonitorOptions.targets
+      .filter((t: any) => t.queryType === 'Azure Monitor')
+      .map((t: any) => migrateTargetSchema(t));
     appInsightsOptions.targets = _.filter(appInsightsOptions.targets, ['queryType', 'Application Insights']);
     azureLogAnalyticsOptions.targets = _.filter(azureLogAnalyticsOptions.targets, ['queryType', 'Azure Log Analytics']);
 
@@ -163,7 +166,7 @@ export default class Datasource extends DataSourceApi<AzureMonitorQuery, AzureDa
     resourceGroup: string,
     metricDefinition: string,
     resourceName: string,
-    metricNamespace: string
+    metricNamespace?: string
   ) {
     return this.azureMonitorDatasource.getMetricNames(
       subscriptionId,
@@ -188,19 +191,23 @@ export default class Datasource extends DataSourceApi<AzureMonitorQuery, AzureDa
     resourceGroup: string,
     metricDefinition: string,
     resourceName: string,
-    metricNamespace: string,
-    metricName: string
+    metricName: string,
+    metricNamespace?: string
   ) {
     return this.azureMonitorDatasource.getMetricMetadata(
       subscriptionId,
       resourceGroup,
       metricDefinition,
       resourceName,
-      metricNamespace,
-      metricName
+      metricName,
+      metricNamespace
     );
   }
 
+  getResources(subscriptions: string[]) {
+    return this.azureMonitorDatasource.getResources(subscriptions);
+  }
+
   /* Application Insights API method */
   getAppInsightsMetricNames() {
     return this.appInsightsDatasource.getMetricNames();

+ 15 - 0
public/app/plugins/datasource/grafana-azure-monitor-datasource/migrations.ts

@@ -0,0 +1,15 @@
+import { AzureMonitorQueryCtrl } from './query_ctrl';
+
+export function migrateTargetSchema(target: any) {
+  if (target.azureMonitor && !target.azureMonitor.data) {
+    const temp = { ...target.azureMonitor };
+    target.azureMonitor = {
+      queryMode: AzureMonitorQueryCtrl.defaultQueryMode,
+      data: {
+        [AzureMonitorQueryCtrl.defaultQueryMode]: temp,
+      },
+    };
+  }
+
+  return target;
+}

+ 169 - 0
public/app/plugins/datasource/grafana-azure-monitor-datasource/multi-select.directive.ts

@@ -0,0 +1,169 @@
+import angular from 'angular';
+import _ from 'lodash';
+
+export class MultiSelectDropdownCtrl {
+  dropdownVisible: boolean;
+  highlightIndex: number;
+  linkText: string;
+  options: Array<{ selected: boolean; text: string; value: string }>;
+  selectedValues: Array<{ text: string; value: string }>;
+  initialValues: string[];
+  onUpdated: any;
+
+  show() {
+    this.highlightIndex = -1;
+    this.options = this.options;
+    this.selectedValues = this.options.filter(({ selected }) => selected);
+
+    this.dropdownVisible = true;
+  }
+
+  hide() {
+    this.dropdownVisible = false;
+  }
+
+  updateLinkText() {
+    this.linkText =
+      this.selectedValues.length === 1 ? this.selectedValues[0].text : `(${this.selectedValues.length}) selected`;
+  }
+
+  clearSelections() {
+    this.selectedValues = _.filter(this.options, { selected: true });
+
+    if (this.selectedValues.length > 1) {
+      _.each(this.options, option => {
+        option.selected = false;
+      });
+    } else {
+      _.each(this.options, option => {
+        option.selected = true;
+      });
+    }
+    this.selectionsChanged();
+  }
+
+  selectValue(option: any) {
+    if (!option) {
+      return;
+    }
+
+    option.selected = !option.selected;
+    this.selectionsChanged();
+  }
+
+  selectionsChanged() {
+    this.selectedValues = _.filter(this.options, { selected: true });
+    if (!this.selectedValues.length && this.options.length) {
+      this.selectedValues = this.options.slice(0, 1);
+    }
+    this.updateLinkText();
+    this.onUpdated({ values: this.selectedValues.map(({ value }) => value) });
+  }
+
+  onClickOutside() {
+    this.selectedValues = _.filter(this.options, { selected: true });
+    if (this.selectedValues.length === 0) {
+      this.options[0].selected = true;
+      this.selectionsChanged();
+    }
+    this.dropdownVisible = false;
+  }
+
+  init() {
+    if (!this.options) {
+      return;
+    }
+
+    this.options = this.options.map(o => ({
+      ...o,
+      selected: this.initialValues.includes(o.value),
+    }));
+    this.selectedValues = _.filter(this.options, { selected: true });
+    if (!this.selectedValues.length) {
+      this.options = this.options.map(o => ({
+        ...o,
+        selected: true,
+      }));
+    }
+    this.updateLinkText();
+  }
+
+  updateSelection() {
+    this.selectedValues = _.filter(this.options, { selected: true });
+    if (!this.selectedValues.length && this.options.length) {
+      this.options = this.options.map(o => ({
+        ...o,
+        selected: true,
+      }));
+      this.selectedValues = _.filter(this.options, { selected: true });
+      this.selectionsChanged();
+    }
+    this.updateLinkText();
+  }
+}
+
+/** @ngInject */
+export function multiSelectDropdown($window: any, $timeout: any) {
+  return {
+    scope: { onUpdated: '&', options: '=', initialValues: '=' },
+    templateUrl: 'public/app/plugins/datasource/grafana-azure-monitor-datasource/partials/multi-select.directive.html',
+    controller: MultiSelectDropdownCtrl,
+    controllerAs: 'vm',
+    bindToController: true,
+    link: (scope: any, elem: any) => {
+      const bodyEl = angular.element($window.document.body);
+      const linkEl = elem.find('.variable-value-link');
+      const inputEl = elem.find('input');
+
+      function openDropdown() {
+        inputEl.css('width', Math.max(linkEl.width(), 80) + 'px');
+
+        inputEl.show();
+        linkEl.hide();
+
+        inputEl.focus();
+        $timeout(
+          () => {
+            bodyEl.on('click', () => {
+              bodyEl.on('click', bodyOnClick);
+            });
+          },
+          0,
+          false
+        );
+      }
+
+      function switchToLink() {
+        inputEl.hide();
+        linkEl.show();
+        bodyEl.off('click', bodyOnClick);
+      }
+
+      function bodyOnClick(e: any) {
+        if (elem.has(e.target).length === 0) {
+          scope.$apply(() => {
+            scope.vm.onClickOutside();
+          });
+        }
+      }
+
+      scope.$watch('vm.options', (newValue: any) => {
+        if (newValue) {
+          scope.vm.updateSelection(newValue);
+        }
+      });
+
+      scope.$watch('vm.dropdownVisible', (newValue: any) => {
+        if (newValue) {
+          openDropdown();
+        } else {
+          switchToLink();
+        }
+      });
+
+      scope.vm.init();
+    },
+  };
+}
+
+angular.module('grafana.directives').directive('multiSelect', multiSelectDropdown);

+ 24 - 0
public/app/plugins/datasource/grafana-azure-monitor-datasource/partials/multi-select.directive.html

@@ -0,0 +1,24 @@
+<div class="variable-link-wrapper">
+        <a ng-click="vm.show()" class="variable-value-link">
+            {{vm.linkText}}
+            <i class="fa fa-caret-down" style="font-size:12px"></i>
+        </a>
+    
+        <input type="text" class="gf-form-input width-11" ng-model="vm.linkText" ng-change="vm.queryChanged()" ></input>
+    
+        <div class="variable-value-dropdown multi" ng-if="vm.dropdownVisible">
+            <div class="variable-options-wrapper">
+                <div class="variable-options-column">
+                    <a ng-if="vm.options.length > 1" class="variable-options-column-header" ng-class="{'many-selected': vm.selectedValues.length > 1}" bs-tooltip="'Clear selections'" data-placement="top" ng-click="vm.clearSelections()">
+                        <span class="variable-option-icon"></span>
+                        Selected ({{vm.selectedValues.length}})
+                    </a>
+                    <a class="variable-option pointer" ng-repeat="option in vm.options" ng-class="{'selected': option.selected, 'highlighted': $index === vm.highlightIndex}" ng-click="vm.selectValue(option, $event)">
+                        <span class="variable-option-icon"></span>
+                        <span>{{option.text}}</span>
+                    </a>
+                </div>
+            </div>
+        </div>
+    </div>
+    

+ 104 - 36
public/app/plugins/datasource/grafana-azure-monitor-datasource/partials/query.editor.html

@@ -7,70 +7,138 @@
           ng-change="ctrl.onQueryTypeChange()"></select>
       </div>
     </div>
-    <div class="gf-form" ng-if="ctrl.target.queryType === 'Azure Monitor' || ctrl.target.queryType === 'Azure Log Analytics'">
+    <div class="gf-form" ng-if="ctrl.target.queryType === 'Azure Monitor'">
+      <label class="gf-form-label query-keyword width-9">Query Mode</label>
+      <div class="gf-form-select-wrapper gf-form-select-wrapper--caret-indent">
+        <select class="gf-form-input service-dropdown" ng-model="ctrl.target.azureMonitor.queryMode" 
+        ng-options="f.value as f.text for f in [{value: 'singleResource', text: 'Single Resource'}, {value: 'crossResource', text: 'Multiple Resources'}]"
+          ng-change="ctrl.refresh()"></select>
+      </div>
+    </div>
+    <div class="gf-form gf-form--grow">
+      <div class="gf-form-label gf-form-label--grow"></div>
+    </div>
+  </div>    
+  <div class="gf-form-inline" ng-if="(ctrl.target.queryType === 'Azure Monitor' && ctrl.target.azureMonitor.queryMode === 'singleResource') || ctrl.target.queryType === 'Azure Log Analytics'">
+    <div class="gf-form" >
       <label class="gf-form-label query-keyword width-9">Subscription</label>
       <gf-form-dropdown model="ctrl.target.subscription" allow-custom="true" lookup-text="true"
-        get-options="ctrl.getSubscriptions()" on-change="ctrl.onSubscriptionChange()" css-class="min-width-12">
+        get-options="ctrl.getSubscriptions()" on-change="ctrl.onSubscriptionChange()" css-class="min-width-6">
       </gf-form-dropdown>
     </div>
     <div class="gf-form gf-form--grow">
       <div class="gf-form-label gf-form-label--grow"></div>
     </div>
   </div>
-  <div ng-if="ctrl.target.queryType === 'Azure Monitor'">
-    <div class="gf-form-inline">
-      <div class="gf-form">
-        <label class="gf-form-label query-keyword width-9">Resource Group</label>
-        <gf-form-dropdown model="ctrl.target.azureMonitor.resourceGroup" allow-custom="true" lookup-text="true"
-          get-options="ctrl.getResourceGroups($query)" on-change="ctrl.onResourceGroupChange()" css-class="min-width-12">
-        </gf-form-dropdown>
-      </div>
-      <div class="gf-form">
-        <label class="gf-form-label query-keyword width-9">Namespace</label>
-        <gf-form-dropdown model="ctrl.target.azureMonitor.metricDefinition" allow-custom="true" lookup-text="true"
-          get-options="ctrl.getMetricDefinitions($query)" on-change="ctrl.onMetricDefinitionChange()" css-class="min-width-20">
-        </gf-form-dropdown>
-      </div>
-      <div class="gf-form">
-        <label class="gf-form-label query-keyword width-9">Resource Name</label>
-        <gf-form-dropdown model="ctrl.target.azureMonitor.resourceName" allow-custom="true" lookup-text="true"
-          get-options="ctrl.getResourceNames($query)" on-change="ctrl.onResourceNameChange()" css-class="min-width-12">
-        </gf-form-dropdown>
-      </div>
-      <div class="gf-form gf-form--grow">
-        <div class="gf-form-label gf-form-label--grow"></div>
-      </div>
+  <div class="gf-form-inline" ng-if="(ctrl.target.queryType === 'Azure Monitor' && ctrl.target.azureMonitor.queryMode === 'crossResource')">
+    <div class="gf-form">
+      <label class="gf-form-label query-keyword width-9">Subscriptions</label>
+      <multi-select ng-if="ctrl.subscriptionValues.length"
+        initial-values="ctrl.target.subscriptions"
+        options="ctrl.subscriptionValues"
+        on-updated="ctrl.onSubscriptionsChange(values)">
+      </multi-select> 
     </div>
+    <div class="gf-form gf-form--grow">
+      <div class="gf-form-label gf-form-label--grow"></div>
+    </div>
+  </div>
+  <div class="gf-form-inline" ng-if="ctrl.target.queryType === 'Azure Monitor' && ctrl.target.azureMonitor.queryMode === 'singleResource'">
+    <div class="gf-form">
+      <label class="gf-form-label query-keyword width-9">Resource Group</label>
+      <gf-form-dropdown model="ctrl.target.azureMonitor.data[ctrl.target.azureMonitor.queryMode].resourceGroup" allow-custom="true" lookup-text="true"
+        get-options="ctrl.getResourceGroups($query)" on-change="ctrl.onResourceGroupChange()" css-class="min-width-12">
+      </gf-form-dropdown>
+    </div>
+    <div class="gf-form">
+      <label class="gf-form-label query-keyword width-9">Namespace</label>
+      <gf-form-dropdown model="ctrl.target.azureMonitor.data[ctrl.target.azureMonitor.queryMode].metricDefinition" allow-custom="true" lookup-text="true"
+        get-options="ctrl.getMetricDefinitions($query)" on-change="ctrl.onMetricDefinitionChange()" css-class="min-width-20">
+      </gf-form-dropdown>
+    </div>
+    <div class="gf-form">
+      <label class="gf-form-label query-keyword width-9">Resource Name</label>
+      <gf-form-dropdown model="ctrl.target.azureMonitor.data[ctrl.target.azureMonitor.queryMode].resourceName" allow-custom="true" lookup-text="true"
+        get-options="ctrl.getResourceNames($query)" on-change="ctrl.onResourceNameChange()" css-class="min-width-12">
+      </gf-form-dropdown>
+    </div>
+    <div class="gf-form gf-form--grow">
+      <div class="gf-form-label gf-form-label--grow"></div>
+    </div>
+  </div>
+  <div class="gf-form-inline" ng-if="ctrl.target.azureMonitor.queryMode === 'crossResource'">
+    <div class="gf-form">
+      <label class="gf-form-label query-keyword width-9">Locations</label>
+      <multi-select ng-if="ctrl.locations.length"
+        initial-values="ctrl.target.azureMonitor.data.crossResource.locations"
+        options="ctrl.locations"
+        on-updated="ctrl.onLocationsChange(values)">
+      </multi-select> 
+    </div>
+    <div class="gf-form gf-form--grow">
+      <div class="gf-form-label gf-form-label--grow"></div>
+    </div>
+  </div>
+  <div class="gf-form-inline" ng-if="ctrl.target.azureMonitor.queryMode === 'crossResource'">
+    <div class="gf-form">
+      <label class="gf-form-label query-keyword width-9">Resource Groups</label>
+      <multi-select class="az-multi-picker"
+        initial-values="ctrl.target.azureMonitor.data.crossResource.resourceGroups"
+        options="ctrl.resourceGroups"
+        on-updated="ctrl.onCrossResourceGroupChange(values)">
+      </multi-select>
+    </div>
+    <div class="gf-form gf-form--grow">
+      <div class="gf-form-label gf-form-label--grow"></div>
+    </div>
+  </div>
+  <div class="gf-form-inline" ng-if="ctrl.target.azureMonitor.queryMode === 'crossResource'">
+    <div class="gf-form">
+      <label class="gf-form-label query-keyword width-9">Resource Type</label>
+      <gf-form-dropdown model="ctrl.target.azureMonitor.data.crossResource.metricDefinition" allow-custom="true" lookup-text="true"
+        get-options="ctrl.getCrossResourceMetricDefinitions($query)" on-change="ctrl.onCrossResourceMetricDefinitionChange()" css-class="min-width-12">
+      </gf-form-dropdown>
+    </div>
+    <div class="gf-form gf-form--grow">
+      <div class="gf-form-label gf-form-label--grow"></div>
+    </div>
+  </div>
     <div class="gf-form-inline">
-      <div class="gf-form">
+      <div class="gf-form" ng-if="ctrl.target.azureMonitor.queryMode === 'singleResource'">
         <label class="gf-form-label query-keyword width-9">Metric Namespace</label>
-        <gf-form-dropdown model="ctrl.target.azureMonitor.metricNamespace" allow-custom="true" lookup-text="true"
+        <gf-form-dropdown model="ctrl.target.azureMonitor.data[ctrl.target.azureMonitor.queryMode].metricNamespace" allow-custom="true" lookup-text="true"
           get-options="ctrl.getMetricNamespaces($query)" on-change="ctrl.onMetricNamespacesChange()" css-class="min-width-12">
         </gf-form-dropdown>
       </div>
       <div class="gf-form">        
         <label class="gf-form-label query-keyword width-9">Metric</label>
-        <gf-form-dropdown model="ctrl.target.azureMonitor.metricName" allow-custom="true" lookup-text="true"
+        <gf-form-dropdown ng-if="ctrl.target.azureMonitor.queryMode === 'singleResource'" model="ctrl.target.azureMonitor.data.singleResource.metricName" allow-custom="true" lookup-text="true"
           get-options="ctrl.getMetricNames($query)" on-change="ctrl.onMetricNameChange()" css-class="min-width-12">
         </gf-form-dropdown>
+        <gf-form-dropdown ng-if="ctrl.target.azureMonitor.queryMode === 'crossResource'" model="ctrl.target.azureMonitor.data.crossResource.metricName" allow-custom="true" lookup-text="true"
+          get-options="ctrl.getCrossResourceMetricNames($query)" on-change="ctrl.onCrossResourceMetricNameChange()" css-class="min-width-12">
+        </gf-form-dropdown>
       </div>
-      <div class="gf-form gf-form--grow aggregation-dropdown-wrapper">
+      <div class="gf-form aggregation-dropdown-wrapper">
         <label class="gf-form-label query-keyword width-9">Aggregation</label>
         <div class="gf-form-select-wrapper gf-form-select-wrapper--caret-indent">
-          <select class="gf-form-input width-11" ng-model="ctrl.target.azureMonitor.aggregation" ng-options="f as f for f in ctrl.target.azureMonitor.aggOptions"
+          <select class="gf-form-input width-11" ng-model="ctrl.target.azureMonitor.data[ctrl.target.azureMonitor.queryMode].aggregation" ng-options="f as f for f in ctrl.target.azureMonitor.data[ctrl.target.azureMonitor.queryMode].aggOptions"
             ng-change="ctrl.refresh()"></select>
         </div>
       </div>
+      <div class="gf-form gf-form--grow">
+        <div class="gf-form-label gf-form-label--grow"></div>
+      </div>
     </div>
     <div class="gf-form-inline">      
       <div class="gf-form">
         <label class="gf-form-label query-keyword width-9">Time Grain</label>
         <div class="gf-form-select-wrapper gf-form-select-wrapper--caret-indent timegrainunit-dropdown-wrapper">
-          <select class="gf-form-input" ng-model="ctrl.target.azureMonitor.timeGrain" ng-options="f.value as f.text for f in ctrl.target.azureMonitor.timeGrains"
+          <select class="gf-form-input" ng-model="ctrl.target.azureMonitor.data[ctrl.target.azureMonitor.queryMode].timeGrain" ng-options="f.value as f.text for f in ctrl.target.azureMonitor.data[ctrl.target.azureMonitor.queryMode].timeGrains"
             ng-change="ctrl.refresh()"></select>
         </div>
       </div>
-      <div class="gf-form" ng-show="ctrl.target.azureMonitor.timeGrain.trim() === 'auto'">
+      <div class="gf-form" ng-show="ctrl.target.azureMonitor.data[ctrl.target.azureMonitor.queryMode].timeGrain.trim() === 'auto'">
         <label class="gf-form-label">Auto Interval</label>
         <label class="gf-form-label">{{ctrl.getAutoInterval()}}</label>
       </div>
@@ -78,17 +146,17 @@
         <div class="gf-form-label gf-form-label--grow"></div>
       </div>
     </div>
-    <div class="gf-form-inline" ng-show="ctrl.target.azureMonitor.dimensions.length > 0">
+    <div class="gf-form-inline" ng-show="ctrl.target.azureMonitor.data[ctrl.target.azureMonitor.queryMode].dimensions.length > 0">
       <div class="gf-form">
         <label class="gf-form-label query-keyword width-9">Dimension</label>
         <div class="gf-form-select-wrapper gf-form-select-wrapper--caret-indent">
-          <select class="gf-form-input min-width-12" ng-model="ctrl.target.azureMonitor.dimension" ng-options="f.value as f.text for f in ctrl.target.azureMonitor.dimensions"
+          <select class="gf-form-input min-width-12" ng-model="ctrl.target.azureMonitor.data[ctrl.target.azureMonitor.queryMode].dimension" ng-options="f.value as f.text for f in ctrl.target.azureMonitor.data[ctrl.target.azureMonitor.queryMode].dimensions"
             ng-change="ctrl.refresh()"></select>
         </div>
       </div>
       <div class="gf-form">
         <label class="gf-form-label query-keyword width-3">eq</label>
-        <input type="text" class="gf-form-input width-17" ng-model="ctrl.target.azureMonitor.dimensionFilter"
+        <input type="text" class="gf-form-input width-17" ng-model="ctrl.target.azureMonitor.data[ctrl.target.azureMonitor.queryMode].dimensionFilter"
           spellcheck="false" placeholder="auto" ng-blur="ctrl.refresh()">
       </div>
       <div class="gf-form gf-form--grow">
@@ -98,7 +166,7 @@
     <div class="gf-form-inline">
       <div class="gf-form">
         <label class="gf-form-label query-keyword width-9">Legend Format</label>
-        <input type="text" class="gf-form-input width-30" ng-model="ctrl.target.azureMonitor.alias" spellcheck="false"
+        <input type="text" class="gf-form-input width-30" ng-model="ctrl.target.azureMonitor.data[ctrl.target.azureMonitor.queryMode].alias" spellcheck="false"
           placeholder="alias patterns (see help for more info)" ng-blur="ctrl.refresh()">
       </div>
 

+ 29 - 29
public/app/plugins/datasource/grafana-azure-monitor-datasource/query_ctrl.test.ts

@@ -36,11 +36,11 @@ describe('AzureMonitorQueryCtrl', () => {
     });
 
     it('should set query parts to select', () => {
-      expect(queryCtrl.target.azureMonitor.resourceGroup).toBe('select');
-      expect(queryCtrl.target.azureMonitor.metricDefinition).toBe('select');
-      expect(queryCtrl.target.azureMonitor.resourceName).toBe('select');
-      expect(queryCtrl.target.azureMonitor.metricNamespace).toBe('select');
-      expect(queryCtrl.target.azureMonitor.metricName).toBe('select');
+      expect(queryCtrl.target.azureMonitor.data.singleResource.resourceGroup).toBe('select');
+      expect(queryCtrl.target.azureMonitor.data.singleResource.metricDefinition).toBe('select');
+      expect(queryCtrl.target.azureMonitor.data.singleResource.resourceName).toBe('select');
+      expect(queryCtrl.target.azureMonitor.data.singleResource.metricNamespace).toBe('select');
+      expect(queryCtrl.target.azureMonitor.data.singleResource.metricName).toBe('select');
       expect(queryCtrl.target.appInsights.groupBy).toBe('none');
     });
   });
@@ -76,7 +76,7 @@ describe('AzureMonitorQueryCtrl', () => {
 
         beforeEach(() => {
           queryCtrl.target.subscription = 'sub1';
-          queryCtrl.target.azureMonitor.resourceGroup = 'test';
+          queryCtrl.target.azureMonitor.data.singleResource.resourceGroup = 'test';
           queryCtrl.datasource.getMetricDefinitions = function(subscriptionId: any, query: any) {
             expect(subscriptionId).toBe('sub1');
             expect(query).toBe('test');
@@ -94,7 +94,7 @@ describe('AzureMonitorQueryCtrl', () => {
 
       describe('and resource group has no value', () => {
         beforeEach(() => {
-          queryCtrl.target.azureMonitor.resourceGroup = 'select';
+          queryCtrl.target.azureMonitor.data.singleResource.resourceGroup = 'select';
         });
 
         it('should return without making a call to datasource', () => {
@@ -109,8 +109,8 @@ describe('AzureMonitorQueryCtrl', () => {
 
         beforeEach(() => {
           queryCtrl.target.subscription = 'sub1';
-          queryCtrl.target.azureMonitor.resourceGroup = 'test';
-          queryCtrl.target.azureMonitor.metricDefinition = 'Microsoft.Compute/virtualMachines';
+          queryCtrl.target.azureMonitor.data.singleResource.resourceGroup = 'test';
+          queryCtrl.target.azureMonitor.data.singleResource.metricDefinition = 'Microsoft.Compute/virtualMachines';
           queryCtrl.datasource.getResourceNames = function(
             subscriptionId: any,
             resourceGroup: any,
@@ -133,8 +133,8 @@ describe('AzureMonitorQueryCtrl', () => {
 
       describe('and resourceGroup and metricDefinition do not have values', () => {
         beforeEach(() => {
-          queryCtrl.target.azureMonitor.resourceGroup = 'select';
-          queryCtrl.target.azureMonitor.metricDefinition = 'select';
+          queryCtrl.target.azureMonitor.data.singleResource.resourceGroup = 'select';
+          queryCtrl.target.azureMonitor.data.singleResource.metricDefinition = 'select';
         });
 
         it('should return without making a call to datasource', () => {
@@ -149,10 +149,10 @@ describe('AzureMonitorQueryCtrl', () => {
 
         beforeEach(() => {
           queryCtrl.target.subscription = 'sub1';
-          queryCtrl.target.azureMonitor.resourceGroup = 'test';
-          queryCtrl.target.azureMonitor.metricDefinition = 'Microsoft.Compute/virtualMachines';
-          queryCtrl.target.azureMonitor.resourceName = 'test';
-          queryCtrl.target.azureMonitor.metricNamespace = 'test';
+          queryCtrl.target.azureMonitor.data.singleResource.resourceGroup = 'test';
+          queryCtrl.target.azureMonitor.data.singleResource.metricDefinition = 'Microsoft.Compute/virtualMachines';
+          queryCtrl.target.azureMonitor.data.singleResource.resourceName = 'test';
+          queryCtrl.target.azureMonitor.data.singleResource.metricNamespace = 'test';
           queryCtrl.datasource.getMetricNames = function(
             subscriptionId: any,
             resourceGroup: any,
@@ -179,10 +179,10 @@ describe('AzureMonitorQueryCtrl', () => {
 
       describe('and resourceGroup, metricDefinition, resourceName and metricNamespace do not have values', () => {
         beforeEach(() => {
-          queryCtrl.target.azureMonitor.resourceGroup = 'select';
-          queryCtrl.target.azureMonitor.metricDefinition = 'select';
-          queryCtrl.target.azureMonitor.resourceName = 'select';
-          queryCtrl.target.azureMonitor.metricNamespace = 'select';
+          queryCtrl.target.azureMonitor.data.singleResource.resourceGroup = 'select';
+          queryCtrl.target.azureMonitor.data.singleResource.metricDefinition = 'select';
+          queryCtrl.target.azureMonitor.data.singleResource.resourceName = 'select';
+          queryCtrl.target.azureMonitor.data.singleResource.metricNamespace = 'select';
         });
 
         it('should return without making a call to datasource', () => {
@@ -201,18 +201,18 @@ describe('AzureMonitorQueryCtrl', () => {
 
       beforeEach(() => {
         queryCtrl.target.subscription = 'sub1';
-        queryCtrl.target.azureMonitor.resourceGroup = 'test';
-        queryCtrl.target.azureMonitor.metricDefinition = 'Microsoft.Compute/virtualMachines';
-        queryCtrl.target.azureMonitor.resourceName = 'test';
-        queryCtrl.target.azureMonitor.metricNamespace = 'test';
-        queryCtrl.target.azureMonitor.metricName = 'Percentage CPU';
+        queryCtrl.target.azureMonitor.data.singleResource.resourceGroup = 'test';
+        queryCtrl.target.azureMonitor.data.singleResource.metricDefinition = 'Microsoft.Compute/virtualMachines';
+        queryCtrl.target.azureMonitor.data.singleResource.resourceName = 'test';
+        queryCtrl.target.azureMonitor.data.singleResource.metricNamespace = 'test';
+        queryCtrl.target.azureMonitor.data.singleResource.metricName = 'Percentage CPU';
         queryCtrl.datasource.getMetricMetadata = function(
           subscription: any,
           resourceGroup: any,
           metricDefinition: any,
           resourceName: any,
-          metricNamespace: any,
-          metricName: any
+          metricName: any,
+          metricNamespace: any
         ) {
           expect(subscription).toBe('sub1');
           expect(resourceGroup).toBe('test');
@@ -226,9 +226,9 @@ describe('AzureMonitorQueryCtrl', () => {
 
       it('should set the options and default selected value for the Aggregations dropdown', () => {
         queryCtrl.onMetricNameChange().then(() => {
-          expect(queryCtrl.target.azureMonitor.aggregation).toBe('Average');
-          expect(queryCtrl.target.azureMonitor.aggOptions).toBe(['Average', 'Total']);
-          expect(queryCtrl.target.azureMonitor.timeGrains).toBe(['PT1M', 'P1D']);
+          expect(queryCtrl.target.azureMonitor.data.singleResource.aggregation).toBe('Average');
+          expect(queryCtrl.target.azureMonitor.data.singleResource.aggOptions).toBe(['Average', 'Total']);
+          expect(queryCtrl.target.azureMonitor.data.singleResource.timeGrains).toBe(['PT1M', 'P1D']);
         });
       });
     });

+ 376 - 130
public/app/plugins/datasource/grafana-azure-monitor-datasource/query_ctrl.ts

@@ -1,21 +1,51 @@
 import _ from 'lodash';
 import { QueryCtrl } from 'app/plugins/sdk';
-// import './css/query_editor.css';
-import TimegrainConverter from './time_grain_converter';
-import './editor/editor_component';
 import kbn from 'app/core/utils/kbn';
 
 import { TemplateSrv } from 'app/features/templating/template_srv';
 import { auto } from 'angular';
 import { DataFrame } from '@grafana/data';
 
+import { Resource } from './types';
+import { migrateTargetSchema } from './migrations';
+import TimegrainConverter from './time_grain_converter';
+import './editor/editor_component';
+import './multi-select.directive';
+
 export interface ResultFormat {
   text: string;
   value: string;
 }
 
+interface AzureMonitor {
+  resourceGroup: string;
+  resourceGroups: string[];
+  resourceName: string;
+  metricDefinition: string;
+  metricNamespace: string;
+  metricName: string;
+  dimensionFilter: string;
+  timeGrain: string;
+  timeGrainUnit: string;
+  timeGrains: Option[];
+  allowedTimeGrainsMs: number[];
+  dimensions: any[];
+  dimension: any;
+  aggregation: string;
+  aggOptions: string[];
+  locations: string[];
+  queryMode: string;
+}
+
+interface Option {
+  value: string;
+  text: string;
+  displayName?: string;
+}
+
 export class AzureMonitorQueryCtrl extends QueryCtrl {
   static templateUrl = 'partials/query.editor.html';
+  static defaultQueryMode = 'singleResource';
 
   defaultDropdownValue = 'select';
 
@@ -23,21 +53,10 @@ export class AzureMonitorQueryCtrl extends QueryCtrl {
     refId: string;
     queryType: string;
     subscription: string;
+    subscriptions: string[];
     azureMonitor: {
-      resourceGroup: string;
-      resourceName: string;
-      metricDefinition: string;
-      metricNamespace: string;
-      metricName: string;
-      dimensionFilter: string;
-      timeGrain: string;
-      timeGrainUnit: string;
-      timeGrains: Array<{ text: string; value: string }>;
-      allowedTimeGrainsMs: number[];
-      dimensions: any[];
-      dimension: any;
-      aggregation: string;
-      aggOptions: string[];
+      queryMode: string;
+      data: { [queryMode: string]: AzureMonitor };
     };
     azureLogAnalytics: {
       query: string;
@@ -63,14 +82,30 @@ export class AzureMonitorQueryCtrl extends QueryCtrl {
 
   defaults = {
     queryType: 'Azure Monitor',
+    subscriptions: new Array<string>(),
     azureMonitor: {
-      resourceGroup: this.defaultDropdownValue,
-      metricDefinition: this.defaultDropdownValue,
-      resourceName: this.defaultDropdownValue,
-      metricNamespace: this.defaultDropdownValue,
-      metricName: this.defaultDropdownValue,
-      dimensionFilter: '*',
-      timeGrain: 'auto',
+      queryMode: 'singleResource',
+      data: {
+        singleResource: {
+          resourceGroups: new Array<string>(),
+          resourceGroup: this.defaultDropdownValue,
+          metricDefinition: this.defaultDropdownValue,
+          metricNamespace: this.defaultDropdownValue,
+          metricName: this.defaultDropdownValue,
+          resourceName: this.defaultDropdownValue,
+          dimensionFilter: '*',
+          timeGrain: 'auto',
+        },
+        crossResource: {
+          resourceGroups: new Array<string>(),
+          locations: new Array<string>(),
+          metricDefinition: this.defaultDropdownValue,
+          resourceName: this.defaultDropdownValue,
+          metricName: this.defaultDropdownValue,
+          dimensionFilter: '*',
+          timeGrain: 'auto',
+        },
+      },
     },
     azureLogAnalytics: {
       query: [
@@ -108,12 +143,17 @@ export class AzureMonitorQueryCtrl extends QueryCtrl {
   showLastQuery: boolean;
   lastQuery: string;
   lastQueryError?: string;
-  subscriptions: Array<{ text: string; value: string }>;
+  subscriptions: Option[];
+  subscriptionValues: string[];
+  resources: Resource[];
+  locations: Option[];
+  resourceGroups: Option[];
 
   /** @ngInject */
   constructor($scope: any, $injector: auto.IInjectorService, private templateSrv: TemplateSrv) {
     super($scope, $injector);
 
+    this.target = migrateTargetSchema(this.target);
     _.defaultsDeep(this.target, this.defaults);
 
     this.migrateTimeGrains();
@@ -125,12 +165,35 @@ export class AzureMonitorQueryCtrl extends QueryCtrl {
     this.panelCtrl.events.on('data-received', this.onDataReceived.bind(this), $scope);
     this.panelCtrl.events.on('data-error', this.onDataError.bind(this), $scope);
     this.resultFormats = [{ text: 'Time series', value: 'time_series' }, { text: 'Table', value: 'table' }];
-    this.getSubscriptions();
+    this.resources = new Array<Resource>();
+    this.subscriptionValues = [];
+
+    this.init();
     if (this.target.queryType === 'Azure Log Analytics') {
       this.getWorkspaces();
     }
   }
 
+  async init() {
+    const subscriptions = await this.getSubscriptions();
+    this.datasource.getResources(subscriptions.map((s: Option) => s.value)).then(async (resources: Resource[]) => {
+      if (!this.target.subscriptions.length) {
+        this.target.subscriptions = this.subscriptions.map(s => s.value);
+      }
+      this.resources = resources;
+      this.updateLocations();
+      this.updateCrossResourceGroups();
+    });
+  }
+
+  updateLocations() {
+    this.locations = this.getLocations().map(l => ({ text: l, value: l }));
+  }
+
+  updateCrossResourceGroups() {
+    this.resourceGroups = this.getCrossResourceGroups().map(rg => ({ text: rg, value: rg }));
+  }
+
   onDataReceived(dataList: DataFrame[]) {
     this.lastQueryError = undefined;
     this.lastQuery = '';
@@ -170,24 +233,28 @@ export class AzureMonitorQueryCtrl extends QueryCtrl {
   }
 
   migrateTimeGrains() {
-    if (this.target.azureMonitor.timeGrainUnit) {
-      if (this.target.azureMonitor.timeGrain !== 'auto') {
-        this.target.azureMonitor.timeGrain = TimegrainConverter.createISO8601Duration(
-          this.target.azureMonitor.timeGrain,
-          this.target.azureMonitor.timeGrainUnit
+    const { queryMode } = this.target.azureMonitor;
+    if (this.target.azureMonitor.data[queryMode].timeGrainUnit) {
+      if (this.target.azureMonitor.data[queryMode].timeGrain !== 'auto') {
+        this.target.azureMonitor.data[queryMode].timeGrain = TimegrainConverter.createISO8601Duration(
+          this.target.azureMonitor.data[queryMode].timeGrain,
+          this.target.azureMonitor.data[queryMode].timeGrainUnit
         );
       }
 
-      delete this.target.azureMonitor.timeGrainUnit;
+      delete this.target.azureMonitor.data[queryMode].timeGrainUnit;
       this.onMetricNameChange();
     }
 
     if (
-      this.target.azureMonitor.timeGrains &&
-      this.target.azureMonitor.timeGrains.length > 0 &&
-      (!this.target.azureMonitor.allowedTimeGrainsMs || this.target.azureMonitor.allowedTimeGrainsMs.length === 0)
+      this.target.azureMonitor.data[queryMode].timeGrains &&
+      this.target.azureMonitor.data[queryMode].timeGrains.length > 0 &&
+      (!this.target.azureMonitor.data[queryMode].allowedTimeGrainsMs ||
+        this.target.azureMonitor.data[queryMode].allowedTimeGrainsMs.length === 0)
     ) {
-      this.target.azureMonitor.allowedTimeGrainsMs = this.convertTimeGrainsToMs(this.target.azureMonitor.timeGrains);
+      this.target.azureMonitor.data[queryMode].allowedTimeGrainsMs = this.convertTimeGrainsToMs(
+        this.target.azureMonitor.data[queryMode].timeGrains
+      );
     }
   }
 
@@ -197,15 +264,18 @@ export class AzureMonitorQueryCtrl extends QueryCtrl {
   }
 
   async migrateToDefaultNamespace() {
+    const { queryMode } = this.target.azureMonitor;
     if (
-      this.target.azureMonitor.metricNamespace &&
-      this.target.azureMonitor.metricNamespace !== this.defaultDropdownValue &&
-      this.target.azureMonitor.metricDefinition
+      this.target.azureMonitor.data[queryMode].metricNamespace &&
+      this.target.azureMonitor.data[queryMode].metricNamespace !== this.defaultDropdownValue &&
+      this.target.azureMonitor.data[queryMode].metricDefinition
     ) {
       return;
     }
 
-    this.target.azureMonitor.metricNamespace = this.target.azureMonitor.metricDefinition;
+    this.target.azureMonitor.data[queryMode].metricNamespace = this.target.azureMonitor.data[
+      queryMode
+    ].metricDefinition;
   }
 
   replace(variable: string) {
@@ -218,13 +288,19 @@ export class AzureMonitorQueryCtrl extends QueryCtrl {
     }
   }
 
-  getSubscriptions() {
+  async getSubscriptions() {
     if (!this.datasource.azureMonitorDatasource.isConfigured()) {
       return;
     }
 
     return this.datasource.azureMonitorDatasource.getSubscriptions().then((subs: any) => {
       this.subscriptions = subs;
+      this.subscriptionValues = subs.map((s: Option) => ({ value: s.value, text: s.displayName }));
+
+      if (!this.target.subscriptions.length) {
+        this.target.subscriptions = subs.map((s: Option) => s.value);
+      }
+
       if (!this.target.subscription && this.target.queryType === 'Azure Monitor') {
         this.target.subscription = this.datasource.azureMonitorDatasource.subscriptionId;
       } else if (!this.target.subscription && this.target.queryType === 'Azure Log Analytics') {
@@ -244,16 +320,36 @@ export class AzureMonitorQueryCtrl extends QueryCtrl {
       return this.getWorkspaces();
     }
 
+    const { queryMode } = this.target.azureMonitor;
     if (this.target.queryType === 'Azure Monitor') {
-      this.target.azureMonitor.resourceGroup = this.defaultDropdownValue;
-      this.target.azureMonitor.metricDefinition = this.defaultDropdownValue;
-      this.target.azureMonitor.resourceName = this.defaultDropdownValue;
-      this.target.azureMonitor.metricName = this.defaultDropdownValue;
-      this.target.azureMonitor.aggregation = '';
-      this.target.azureMonitor.timeGrains = [];
-      this.target.azureMonitor.timeGrain = '';
-      this.target.azureMonitor.dimensions = [];
-      this.target.azureMonitor.dimension = '';
+      this.target.azureMonitor.data[queryMode].resourceGroup = this.defaultDropdownValue;
+      this.target.azureMonitor.data[queryMode].metricDefinition = this.defaultDropdownValue;
+      this.target.azureMonitor.data[queryMode].resourceName = this.defaultDropdownValue;
+      this.target.azureMonitor.data[queryMode].metricName = this.defaultDropdownValue;
+      this.target.azureMonitor.data[queryMode].aggregation = '';
+      this.target.azureMonitor.data[queryMode].timeGrains = [];
+      this.target.azureMonitor.data[queryMode].timeGrain = '';
+      this.target.azureMonitor.data[queryMode].dimensions = [];
+      this.target.azureMonitor.data[queryMode].dimension = '';
+    }
+  }
+
+  async onSubscriptionsChange(values: any) {
+    if (!_.isEqual(this.target.subscriptions.sort(), values.sort())) {
+      this.target.subscriptions = values;
+      this.resources = await this.datasource.getResources(this.target.subscriptions);
+      const { queryMode } = this.target.azureMonitor;
+      this.target.azureMonitor.data[queryMode].resourceGroup = this.defaultDropdownValue;
+      this.target.azureMonitor.data[queryMode].metricDefinition = this.defaultDropdownValue;
+      this.target.azureMonitor.data[queryMode].resourceName = this.defaultDropdownValue;
+      this.target.azureMonitor.data[queryMode].metricName = this.defaultDropdownValue;
+      this.target.azureMonitor.data[queryMode].aggregation = '';
+      this.target.azureMonitor.data[queryMode].timeGrains = [];
+      this.target.azureMonitor.data[queryMode].timeGrain = '';
+      this.target.azureMonitor.data[queryMode].dimensions = [];
+      this.target.azureMonitor.data[queryMode].dimension = '';
+      this.updateLocations();
+      this.updateCrossResourceGroups();
     }
   }
 
@@ -270,29 +366,70 @@ export class AzureMonitorQueryCtrl extends QueryCtrl {
       .catch(this.handleQueryCtrlError.bind(this));
   }
 
+  getCrossResourceGroups() {
+    if (this.target.queryType !== 'Azure Monitor' || !this.datasource.azureMonitorDatasource.isConfigured()) {
+      return [];
+    }
+
+    return this.resources
+      .filter(({ location, subscriptionId }) => {
+        if (this.target.azureMonitor.data.crossResource.locations.length) {
+          return (
+            this.target.azureMonitor.data.crossResource.locations.includes(location) &&
+            this.target.subscriptions.includes(subscriptionId)
+          );
+        }
+        return this.target.subscriptions.includes(subscriptionId);
+      })
+      .reduce((options, { group }: Resource) => (options.some(o => o === group) ? options : [...options, group]), []);
+  }
+
+  async getCrossResourceMetricDefinitions(query: any) {
+    const { locations, resourceGroups } = this.target.azureMonitor.data.crossResource;
+    return this.resources
+      .filter(({ location, group }) => locations.includes(location) && resourceGroups.includes(group))
+      .reduce(
+        (options: Option[], { type }: Resource) =>
+          options.some(o => o.value === type) ? options : [...options, { text: type, value: type }],
+        []
+      );
+  }
+
+  getLocations() {
+    return this.resources
+      .filter(({ subscriptionId }) => this.target.subscriptions.includes(subscriptionId))
+      .reduce(
+        (options: string[], { location }: Resource) =>
+          options.some(o => o === location) ? options : [...options, location],
+        []
+      );
+  }
+
   getMetricDefinitions(query: any) {
+    const { queryMode } = this.target.azureMonitor;
     if (
       this.target.queryType !== 'Azure Monitor' ||
-      !this.target.azureMonitor.resourceGroup ||
-      this.target.azureMonitor.resourceGroup === this.defaultDropdownValue
+      !this.target.azureMonitor.data[queryMode].resourceGroup ||
+      this.target.azureMonitor.data[queryMode].resourceGroup === this.defaultDropdownValue
     ) {
       return;
     }
     return this.datasource
       .getMetricDefinitions(
         this.replace(this.target.subscription || this.datasource.azureMonitorDatasource.subscriptionId),
-        this.replace(this.target.azureMonitor.resourceGroup)
+        this.replace(this.target.azureMonitor.data[queryMode].resourceGroup)
       )
       .catch(this.handleQueryCtrlError.bind(this));
   }
 
   getResourceNames(query: any) {
+    const { queryMode } = this.target.azureMonitor;
     if (
       this.target.queryType !== 'Azure Monitor' ||
-      !this.target.azureMonitor.resourceGroup ||
-      this.target.azureMonitor.resourceGroup === this.defaultDropdownValue ||
-      !this.target.azureMonitor.metricDefinition ||
-      this.target.azureMonitor.metricDefinition === this.defaultDropdownValue
+      !this.target.azureMonitor.data[queryMode].resourceGroup ||
+      this.target.azureMonitor.data[queryMode].resourceGroup === this.defaultDropdownValue ||
+      !this.target.azureMonitor.data[queryMode].metricDefinition ||
+      this.target.azureMonitor.data[queryMode].metricDefinition === this.defaultDropdownValue
     ) {
       return;
     }
@@ -300,21 +437,22 @@ export class AzureMonitorQueryCtrl extends QueryCtrl {
     return this.datasource
       .getResourceNames(
         this.replace(this.target.subscription || this.datasource.azureMonitorDatasource.subscriptionId),
-        this.replace(this.target.azureMonitor.resourceGroup),
-        this.replace(this.target.azureMonitor.metricDefinition)
+        this.replace(this.target.azureMonitor.data[queryMode].resourceGroup),
+        this.replace(this.target.azureMonitor.data[queryMode].metricDefinition)
       )
       .catch(this.handleQueryCtrlError.bind(this));
   }
 
   getMetricNamespaces() {
+    const { queryMode } = this.target.azureMonitor;
     if (
       this.target.queryType !== 'Azure Monitor' ||
-      !this.target.azureMonitor.resourceGroup ||
-      this.target.azureMonitor.resourceGroup === this.defaultDropdownValue ||
-      !this.target.azureMonitor.metricDefinition ||
-      this.target.azureMonitor.metricDefinition === this.defaultDropdownValue ||
-      !this.target.azureMonitor.resourceName ||
-      this.target.azureMonitor.resourceName === this.defaultDropdownValue
+      !this.target.azureMonitor.data[queryMode].resourceGroup ||
+      this.target.azureMonitor.data[queryMode].resourceGroup === this.defaultDropdownValue ||
+      !this.target.azureMonitor.data[queryMode].metricDefinition ||
+      this.target.azureMonitor.data[queryMode].metricDefinition === this.defaultDropdownValue ||
+      !this.target.azureMonitor.data[queryMode].resourceName ||
+      this.target.azureMonitor.data[queryMode].resourceName === this.defaultDropdownValue
     ) {
       return;
     }
@@ -322,24 +460,50 @@ export class AzureMonitorQueryCtrl extends QueryCtrl {
     return this.datasource
       .getMetricNamespaces(
         this.replace(this.target.subscription || this.datasource.azureMonitorDatasource.subscriptionId),
-        this.replace(this.target.azureMonitor.resourceGroup),
-        this.replace(this.target.azureMonitor.metricDefinition),
-        this.replace(this.target.azureMonitor.resourceName)
+        this.replace(this.target.azureMonitor.data[queryMode].resourceGroup),
+        this.replace(this.target.azureMonitor.data[queryMode].metricDefinition),
+        this.replace(this.target.azureMonitor.data[queryMode].resourceName)
       )
       .catch(this.handleQueryCtrlError.bind(this));
   }
 
+  async getCrossResourceMetricNames() {
+    const { locations, resourceGroups, metricDefinition } = this.target.azureMonitor.data.crossResource;
+
+    const resources = this.resources.filter(
+      ({ type, location, name, group }) =>
+        resourceGroups.includes(group) && type === metricDefinition && locations.includes(location)
+    );
+
+    const uniqueResources = _.uniqBy(resources, ({ subscriptionId, name, type, group }: Resource) =>
+      [subscriptionId, name, locations, group].join()
+    );
+
+    const responses = await Promise.all(
+      uniqueResources.map(({ subscriptionId, group, type, name }) =>
+        this.datasource
+          .getMetricNames(subscriptionId, group, type, name)
+          .then((metrics: any) => metrics.map((m: any) => ({ ...m, subscriptionIds: [subscriptionId] })), [
+            { text: this.defaultDropdownValue, value: this.defaultDropdownValue },
+          ])
+      )
+    );
+
+    return _.uniqBy(responses.reduce((result, resources) => [...result, ...resources], []), ({ value }) => value);
+  }
+
   getMetricNames() {
+    const { queryMode } = this.target.azureMonitor;
     if (
       this.target.queryType !== 'Azure Monitor' ||
-      !this.target.azureMonitor.resourceGroup ||
-      this.target.azureMonitor.resourceGroup === this.defaultDropdownValue ||
-      !this.target.azureMonitor.metricDefinition ||
-      this.target.azureMonitor.metricDefinition === this.defaultDropdownValue ||
-      !this.target.azureMonitor.resourceName ||
-      this.target.azureMonitor.resourceName === this.defaultDropdownValue ||
-      !this.target.azureMonitor.metricNamespace ||
-      this.target.azureMonitor.metricNamespace === this.defaultDropdownValue
+      !this.target.azureMonitor.data[queryMode].resourceGroup ||
+      this.target.azureMonitor.data[queryMode].resourceGroup === this.defaultDropdownValue ||
+      !this.target.azureMonitor.data[queryMode].metricDefinition ||
+      this.target.azureMonitor.data[queryMode].metricDefinition === this.defaultDropdownValue ||
+      !this.target.azureMonitor.data[queryMode].resourceName ||
+      this.target.azureMonitor.data[queryMode].resourceName === this.defaultDropdownValue ||
+      !this.target.azureMonitor.data[queryMode].metricNamespace ||
+      this.target.azureMonitor.data[queryMode].metricNamespace === this.defaultDropdownValue
     ) {
       return;
     }
@@ -347,87 +511,168 @@ export class AzureMonitorQueryCtrl extends QueryCtrl {
     return this.datasource
       .getMetricNames(
         this.replace(this.target.subscription || this.datasource.azureMonitorDatasource.subscriptionId),
-        this.replace(this.target.azureMonitor.resourceGroup),
-        this.replace(this.target.azureMonitor.metricDefinition),
-        this.replace(this.target.azureMonitor.resourceName),
-        this.replace(this.target.azureMonitor.metricNamespace)
+        this.replace(this.target.azureMonitor.data[queryMode].resourceGroup),
+        this.replace(this.target.azureMonitor.data[queryMode].metricDefinition),
+        this.replace(this.target.azureMonitor.data[queryMode].resourceName),
+        this.replace(this.target.azureMonitor.data[queryMode].metricNamespace)
       )
       .catch(this.handleQueryCtrlError.bind(this));
   }
 
   onResourceGroupChange() {
-    this.target.azureMonitor.metricDefinition = this.defaultDropdownValue;
-    this.target.azureMonitor.resourceName = this.defaultDropdownValue;
-    this.target.azureMonitor.metricNamespace = this.defaultDropdownValue;
-    this.target.azureMonitor.metricName = this.defaultDropdownValue;
-    this.target.azureMonitor.aggregation = '';
-    this.target.azureMonitor.timeGrains = [];
-    this.target.azureMonitor.timeGrain = '';
-    this.target.azureMonitor.dimensions = [];
-    this.target.azureMonitor.dimension = '';
+    const { queryMode } = this.target.azureMonitor;
+    this.target.azureMonitor.data[queryMode].metricDefinition = this.defaultDropdownValue;
+    this.target.azureMonitor.data[queryMode].resourceName = this.defaultDropdownValue;
+    this.target.azureMonitor.data[queryMode].metricNamespace = this.defaultDropdownValue;
+    this.target.azureMonitor.data[queryMode].metricName = this.defaultDropdownValue;
+    this.target.azureMonitor.data[queryMode].aggregation = '';
+    this.target.azureMonitor.data[queryMode].timeGrains = [];
+    this.target.azureMonitor.data[queryMode].timeGrain = '';
+    this.target.azureMonitor.data[queryMode].dimensions = [];
+    this.target.azureMonitor.data[queryMode].dimension = '';
     this.refresh();
   }
 
+  onCrossResourceGroupChange(values: string[]) {
+    if (!_.isEqual(this.target.azureMonitor.data.crossResource.resourceGroups.sort(), values.sort())) {
+      this.target.azureMonitor.data.crossResource.resourceGroups = values;
+      const { queryMode } = this.target.azureMonitor;
+      this.target.azureMonitor.data[queryMode].metricDefinition = '';
+      this.target.azureMonitor.data[queryMode].metricName = '';
+      this.refresh();
+    }
+  }
+
+  onCrossResourceMetricDefinitionChange() {
+    const { queryMode } = this.target.azureMonitor;
+    this.target.azureMonitor.data[queryMode].metricName = this.defaultDropdownValue;
+    this.target.azureMonitor.data[queryMode].aggregation = '';
+    this.target.azureMonitor.data[queryMode].timeGrains = [];
+    this.target.azureMonitor.data[queryMode].timeGrain = '';
+    this.target.azureMonitor.data[queryMode].dimensions = [];
+    this.target.azureMonitor.data[queryMode].dimension = '';
+    this.refresh();
+  }
+
+  async onLocationsChange(values: string[]) {
+    if (!_.isEqual(this.target.azureMonitor.data.crossResource.locations.sort(), values.sort())) {
+      this.target.azureMonitor.data.crossResource.locations = values;
+      const { queryMode } = this.target.azureMonitor;
+      this.target.azureMonitor.data[queryMode].metricDefinition = '';
+      this.target.azureMonitor.data[queryMode].resourceGroup = '';
+      this.target.azureMonitor.data[queryMode].metricName = this.defaultDropdownValue;
+      this.target.azureMonitor.data[queryMode].aggregation = '';
+      this.target.azureMonitor.data[queryMode].timeGrains = [];
+      this.target.azureMonitor.data[queryMode].timeGrain = '';
+      this.target.azureMonitor.data[queryMode].dimensions = [];
+      this.target.azureMonitor.data[queryMode].dimension = '';
+      this.updateCrossResourceGroups();
+      this.refresh();
+    }
+  }
+
   onMetricDefinitionChange() {
-    this.target.azureMonitor.resourceName = this.defaultDropdownValue;
-    this.target.azureMonitor.metricNamespace = this.defaultDropdownValue;
-    this.target.azureMonitor.metricName = this.defaultDropdownValue;
-    this.target.azureMonitor.aggregation = '';
-    this.target.azureMonitor.timeGrains = [];
-    this.target.azureMonitor.timeGrain = '';
-    this.target.azureMonitor.dimensions = [];
-    this.target.azureMonitor.dimension = '';
+    const { queryMode } = this.target.azureMonitor;
+    this.target.azureMonitor.data[queryMode].resourceName = this.defaultDropdownValue;
+    this.target.azureMonitor.data[queryMode].metricNamespace = this.defaultDropdownValue;
+    this.target.azureMonitor.data[queryMode].metricName = this.defaultDropdownValue;
+    this.target.azureMonitor.data[queryMode].aggregation = '';
+    this.target.azureMonitor.data[queryMode].timeGrains = [];
+    this.target.azureMonitor.data[queryMode].timeGrain = '';
+    this.target.azureMonitor.data[queryMode].dimensions = [];
+    this.target.azureMonitor.data[queryMode].dimension = '';
   }
 
   onResourceNameChange() {
-    this.target.azureMonitor.metricNamespace = this.defaultDropdownValue;
-    this.target.azureMonitor.metricName = this.defaultDropdownValue;
-    this.target.azureMonitor.aggregation = '';
-    this.target.azureMonitor.timeGrains = [];
-    this.target.azureMonitor.timeGrain = '';
-    this.target.azureMonitor.dimensions = [];
-    this.target.azureMonitor.dimension = '';
+    const { queryMode } = this.target.azureMonitor;
+    this.target.azureMonitor.data[queryMode].metricNamespace = this.defaultDropdownValue;
+    this.target.azureMonitor.data[queryMode].metricName = this.defaultDropdownValue;
+    this.target.azureMonitor.data[queryMode].aggregation = '';
+    this.target.azureMonitor.data[queryMode].timeGrains = [];
+    this.target.azureMonitor.data[queryMode].timeGrain = '';
+    this.target.azureMonitor.data[queryMode].dimensions = [];
+    this.target.azureMonitor.data[queryMode].dimension = '';
     this.refresh();
   }
 
   onMetricNamespacesChange() {
-    this.target.azureMonitor.metricName = this.defaultDropdownValue;
-    this.target.azureMonitor.dimensions = [];
-    this.target.azureMonitor.dimension = '';
+    const { queryMode } = this.target.azureMonitor;
+    this.target.azureMonitor.data[queryMode].metricName = this.defaultDropdownValue;
+    this.target.azureMonitor.data[queryMode].dimensions = [];
+    this.target.azureMonitor.data[queryMode].dimension = '';
+  }
+
+  setMetricMetadata(metadata: any) {
+    const { queryMode } = this.target.azureMonitor;
+    this.target.azureMonitor.data[queryMode].aggOptions = metadata.supportedAggTypes || [metadata.primaryAggType];
+    this.target.azureMonitor.data[queryMode].aggregation = metadata.primaryAggType;
+    this.target.azureMonitor.data[queryMode].timeGrains = [{ text: 'auto', value: 'auto' }].concat(
+      metadata.supportedTimeGrains
+    );
+    this.target.azureMonitor.data[queryMode].timeGrain = 'auto';
+
+    this.target.azureMonitor.data[queryMode].allowedTimeGrainsMs = this.convertTimeGrainsToMs(
+      metadata.supportedTimeGrains || []
+    );
+
+    this.target.azureMonitor.data[queryMode].dimensions = metadata.dimensions;
+    if (metadata.dimensions.length > 0) {
+      this.target.azureMonitor.data[queryMode].dimension = metadata.dimensions[0].value;
+    }
+    return this.refresh();
   }
 
-  onMetricNameChange() {
-    if (!this.target.azureMonitor.metricName || this.target.azureMonitor.metricName === this.defaultDropdownValue) {
+  onCrossResourceMetricNameChange() {
+    const { queryMode } = this.target.azureMonitor;
+    if (
+      !this.target.azureMonitor.data[queryMode].metricName ||
+      this.target.azureMonitor.data[queryMode].metricName === this.defaultDropdownValue
+    ) {
       return;
     }
 
+    const { resourceGroups, metricDefinition, metricName } = this.target.azureMonitor.data[queryMode];
+
+    const resource = this.resources.find(
+      ({ type, group }) => type === metricDefinition && resourceGroups.includes(group)
+    );
+
     return this.datasource
       .getMetricMetadata(
-        this.replace(this.target.subscription),
-        this.replace(this.target.azureMonitor.resourceGroup),
-        this.replace(this.target.azureMonitor.metricDefinition),
-        this.replace(this.target.azureMonitor.resourceName),
-        this.replace(this.target.azureMonitor.metricNamespace),
-        this.replace(this.target.azureMonitor.metricName)
+        this.replace(this.target.subscriptions[0]),
+        resource.group,
+        metricDefinition,
+        resource.name,
+        metricName
       )
-      .then((metadata: any) => {
-        this.target.azureMonitor.aggOptions = metadata.supportedAggTypes || [metadata.primaryAggType];
-        this.target.azureMonitor.aggregation = metadata.primaryAggType;
-        this.target.azureMonitor.timeGrains = [{ text: 'auto', value: 'auto' }].concat(metadata.supportedTimeGrains);
-        this.target.azureMonitor.timeGrain = 'auto';
+      .then(this.setMetricMetadata.bind(this))
+      .then(() => this.refresh())
+      .catch(this.handleQueryCtrlError.bind(this));
+  }
 
-        this.target.azureMonitor.allowedTimeGrainsMs = this.convertTimeGrainsToMs(metadata.supportedTimeGrains || []);
+  onMetricNameChange() {
+    const { queryMode } = this.target.azureMonitor;
+    if (
+      !this.target.azureMonitor.data[queryMode].metricName ||
+      this.target.azureMonitor.data[queryMode].metricName === this.defaultDropdownValue
+    ) {
+      return;
+    }
 
-        this.target.azureMonitor.dimensions = metadata.dimensions;
-        if (metadata.dimensions.length > 0) {
-          this.target.azureMonitor.dimension = metadata.dimensions[0].value;
-        }
-        return this.refresh();
-      })
+    return this.datasource
+      .getMetricMetadata(
+        this.replace(this.target.subscription),
+        this.replace(this.target.azureMonitor.data[queryMode].resourceGroup),
+        this.replace(this.target.azureMonitor.data[queryMode].metricDefinition),
+        this.replace(this.target.azureMonitor.data[queryMode].resourceName),
+        this.replace(this.target.azureMonitor.data[queryMode].metricName),
+        this.replace(this.target.azureMonitor.data[queryMode].metricNamespace)
+      )
+      .then(this.setMetricMetadata.bind(this))
       .catch(this.handleQueryCtrlError.bind(this));
   }
 
-  convertTimeGrainsToMs(timeGrains: Array<{ text: string; value: string }>) {
+  convertTimeGrainsToMs(timeGrains: Option[]) {
     const allowedTimeGrainsMs: number[] = [];
     timeGrains.forEach((tg: any) => {
       if (tg.value !== 'auto') {
@@ -438,10 +683,11 @@ export class AzureMonitorQueryCtrl extends QueryCtrl {
   }
 
   getAutoInterval() {
-    if (this.target.azureMonitor.timeGrain === 'auto') {
+    const { queryMode } = this.target.azureMonitor;
+    if (this.target.azureMonitor.data[queryMode].timeGrain === 'auto') {
       return TimegrainConverter.findClosestTimeGrain(
         this.templateSrv.getBuiltInIntervalValue(),
-        _.map(this.target.azureMonitor.timeGrains, o =>
+        _.map(this.target.azureMonitor.data[queryMode].timeGrains, o =>
           TimegrainConverter.createKbnUnitFromISO8601Duration(o.value)
         ) || ['1m', '5m', '15m', '30m', '1h', '6h', '12h', '1d']
       );

+ 27 - 2
public/app/plugins/datasource/grafana-azure-monitor-datasource/types.ts

@@ -3,6 +3,7 @@ import { DataQuery, DataSourceJsonData } from '@grafana/ui';
 export interface AzureMonitorQuery extends DataQuery {
   format: string;
   subscription: string;
+  subscriptions: string[];
   azureMonitor: AzureMetricQuery;
   azureLogAnalytics: AzureLogsQuery;
   //   appInsights: any;
@@ -26,9 +27,9 @@ export interface AzureDataSourceJsonData extends DataSourceJsonData {
   // App Insights
   appInsightsAppId?: string;
 }
-
-export interface AzureMetricQuery {
+export interface AzureMonitorQueryData {
   resourceGroup: string;
+  resourceGroups: string[];
   resourceName: string;
   metricDefinition: string;
   metricNamespace: string;
@@ -41,6 +42,12 @@ export interface AzureMetricQuery {
   dimension: string;
   dimensionFilter: string;
   alias: string;
+  locations: string[];
+}
+
+export interface AzureMetricQuery extends AzureMonitorQueryData {
+  queryMode: string;
+  data: { [queryMode: string]: AzureMonitorQueryData };
 }
 
 export interface AzureLogsQuery {
@@ -67,6 +74,24 @@ export interface AzureMonitorResourceGroupsResponse {
   statusText: string;
 }
 
+export interface Resource {
+  id: string;
+  name: string;
+  type: string;
+  location: string;
+  kind: string;
+  subscriptionId: string;
+  group: string;
+}
+
+export interface AzureMonitorResourceResponse {
+  data: {
+    value: Resource[];
+    status: number;
+    statusText: string;
+  };
+}
+
 // Azure Log Analytics types
 export interface KustoSchema {
   Databases: { [key: string]: KustoDatabase };