Commit 88051258 by Erik Sundell Committed by GitHub

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
parent b5f0a5d5
...@@ -60,7 +60,11 @@ func (e *AzureMonitorDatasource) executeTimeSeriesQuery(ctx context.Context, ori ...@@ -60,7 +60,11 @@ func (e *AzureMonitorDatasource) executeTimeSeriesQuery(ctx context.Context, ori
if err != nil { if err != nil {
queryRes.Error = err 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 return result, nil
...@@ -84,11 +88,22 @@ func (e *AzureMonitorDatasource) buildQueries(queries []*tsdb.Query, timeRange * ...@@ -84,11 +88,22 @@ func (e *AzureMonitorDatasource) buildQueries(queries []*tsdb.Query, timeRange *
azureMonitorTarget := query.Model.Get("azureMonitor").MustMap() azureMonitorTarget := query.Model.Get("azureMonitor").MustMap()
azlog.Debug("AzureMonitor", "target", azureMonitorTarget) 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 := map[string]string{}
urlComponents["subscription"] = fmt.Sprintf("%v", query.Model.Get("subscription").MustString()) urlComponents["subscription"] = fmt.Sprintf("%v", query.Model.Get("subscription").MustString())
urlComponents["resourceGroup"] = fmt.Sprintf("%v", azureMonitorTarget["resourceGroup"]) urlComponents["resourceGroup"] = fmt.Sprintf("%v", azureMonitorData["resourceGroup"])
urlComponents["metricDefinition"] = fmt.Sprintf("%v", azureMonitorTarget["metricDefinition"]) urlComponents["metricDefinition"] = fmt.Sprintf("%v", azureMonitorData["metricDefinition"])
urlComponents["resourceName"] = fmt.Sprintf("%v", azureMonitorTarget["resourceName"]) urlComponents["resourceName"] = fmt.Sprintf("%v", azureMonitorData["resourceName"])
ub := urlBuilder{ ub := urlBuilder{
DefaultSubscription: query.DataSource.JsonData.Get("subscriptionId").MustString(), DefaultSubscription: query.DataSource.JsonData.Get("subscriptionId").MustString(),
...@@ -100,12 +115,12 @@ func (e *AzureMonitorDatasource) buildQueries(queries []*tsdb.Query, timeRange * ...@@ -100,12 +115,12 @@ func (e *AzureMonitorDatasource) buildQueries(queries []*tsdb.Query, timeRange *
azureURL := ub.Build() azureURL := ub.Build()
alias := "" alias := ""
if val, ok := azureMonitorTarget["alias"]; ok { if val, ok := azureMonitorData["alias"]; ok {
alias = fmt.Sprintf("%v", val) alias = fmt.Sprintf("%v", val)
} }
timeGrain := fmt.Sprintf("%v", azureMonitorTarget["timeGrain"]) timeGrain := fmt.Sprintf("%v", azureMonitorData["timeGrain"])
timeGrains := azureMonitorTarget["allowedTimeGrainsMs"] timeGrains := azureMonitorData["allowedTimeGrainsMs"]
if timeGrain == "auto" { if timeGrain == "auto" {
timeGrain, err = e.setAutoTimeGrain(query.IntervalMs, timeGrains) timeGrain, err = e.setAutoTimeGrain(query.IntervalMs, timeGrains)
if err != nil { if err != nil {
...@@ -117,13 +132,16 @@ func (e *AzureMonitorDatasource) buildQueries(queries []*tsdb.Query, timeRange * ...@@ -117,13 +132,16 @@ func (e *AzureMonitorDatasource) buildQueries(queries []*tsdb.Query, timeRange *
params.Add("api-version", "2018-01-01") 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("timespan", fmt.Sprintf("%v/%v", startTime.UTC().Format(time.RFC3339), endTime.UTC().Format(time.RFC3339)))
params.Add("interval", timeGrain) params.Add("interval", timeGrain)
params.Add("aggregation", fmt.Sprintf("%v", azureMonitorTarget["aggregation"])) params.Add("aggregation", fmt.Sprintf("%v", azureMonitorData["aggregation"]))
params.Add("metricnames", fmt.Sprintf("%v", azureMonitorTarget["metricName"])) params.Add("metricnames", fmt.Sprintf("%v", azureMonitorData["metricName"]))
params.Add("metricnamespace", fmt.Sprintf("%v", azureMonitorTarget["metricNamespace"]))
if val, ok := azureMonitorData["metricNamespace"]; ok {
params.Add("metricnamespace", fmt.Sprintf("%v", val))
}
dimension := strings.TrimSpace(fmt.Sprintf("%v", azureMonitorTarget["dimension"])) dimension := strings.TrimSpace(fmt.Sprintf("%v", azureMonitorData["dimension"]))
dimensionFilter := strings.TrimSpace(fmt.Sprintf("%v", azureMonitorTarget["dimensionFilter"])) dimensionFilter := strings.TrimSpace(fmt.Sprintf("%v", azureMonitorData["dimensionFilter"]))
if azureMonitorTarget["dimension"] != nil && azureMonitorTarget["dimensionFilter"] != nil && len(dimension) > 0 && len(dimensionFilter) > 0 && dimension != "None" { 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)) params.Add("$filter", fmt.Sprintf("%s eq '%s'", dimension, dimensionFilter))
} }
......
...@@ -36,15 +36,20 @@ func TestAzureMonitorDatasource(t *testing.T) { ...@@ -36,15 +36,20 @@ func TestAzureMonitorDatasource(t *testing.T) {
Model: simplejson.NewFromAny(map[string]interface{}{ Model: simplejson.NewFromAny(map[string]interface{}{
"subscription": "12345678-aaaa-bbbb-cccc-123456789abc", "subscription": "12345678-aaaa-bbbb-cccc-123456789abc",
"azureMonitor": map[string]interface{}{ "azureMonitor": map[string]interface{}{
"timeGrain": "PT1M", "queryMode": "singleResource",
"aggregation": "Average", "data": map[string]interface{}{
"resourceGroup": "grafanastaging", "singleResource": map[string]interface{}{
"resourceName": "grafana", "timeGrain": "PT1M",
"metricDefinition": "Microsoft.Compute/virtualMachines", "aggregation": "Average",
"metricNamespace": "Microsoft.Compute-virtualMachines", "resourceGroup": "grafanastaging",
"metricName": "Percentage CPU", "resourceName": "grafana",
"alias": "testalias", "metricDefinition": "Microsoft.Compute/virtualMachines",
"queryType": "Azure Monitor", "metricNamespace": "Microsoft.Compute-virtualMachines",
"metricName": "Percentage CPU",
"alias": "testalias",
"queryType": "Azure Monitor",
},
},
}, },
}), }),
RefId: "A", RefId: "A",
......
...@@ -918,8 +918,8 @@ describe('AzureMonitorDatasource', () => { ...@@ -918,8 +918,8 @@ describe('AzureMonitorDatasource', () => {
'nodeapp', 'nodeapp',
'microsoft.insights/components', 'microsoft.insights/components',
'resource1', 'resource1',
'default', 'UsedCapacity',
'UsedCapacity' 'default'
) )
.then((results: any) => { .then((results: any) => {
expect(results.primaryAggType).toEqual('Total'); expect(results.primaryAggType).toEqual('Total');
...@@ -992,8 +992,8 @@ describe('AzureMonitorDatasource', () => { ...@@ -992,8 +992,8 @@ describe('AzureMonitorDatasource', () => {
'nodeapp', 'nodeapp',
'microsoft.insights/components', 'microsoft.insights/components',
'resource1', 'resource1',
'default', 'Transactions',
'Transactions' 'default'
) )
.then((results: any) => { .then((results: any) => {
expect(results.dimensions.length).toEqual(4); expect(results.dimensions.length).toEqual(4);
...@@ -1011,8 +1011,8 @@ describe('AzureMonitorDatasource', () => { ...@@ -1011,8 +1011,8 @@ describe('AzureMonitorDatasource', () => {
'nodeapp', 'nodeapp',
'microsoft.insights/components', 'microsoft.insights/components',
'resource1', 'resource1',
'default', 'FreeCapacity',
'FreeCapacity' 'default'
) )
.then((results: any) => { .then((results: any) => {
expect(results.dimensions.length).toEqual(0); expect(results.dimensions.length).toEqual(0);
......
...@@ -108,8 +108,8 @@ export default class ResponseParser { ...@@ -108,8 +108,8 @@ export default class ResponseParser {
return dimensions; return dimensions;
} }
static parseSubscriptions(result: any): Array<{ text: string; value: string }> { static parseSubscriptions(result: any): Array<{ text: string; value: string; displayName: string }> {
const list: Array<{ text: string; value: string }> = []; const list: Array<{ text: string; value: string; displayName: string }> = [];
if (!result) { if (!result) {
return list; return list;
...@@ -122,6 +122,7 @@ export default class ResponseParser { ...@@ -122,6 +122,7 @@ export default class ResponseParser {
list.push({ list.push({
text: `${_.get(result.data.value[i], textFieldName)} - ${_.get(result.data.value[i], valueFieldName)}`, text: `${_.get(result.data.value[i], textFieldName)} - ${_.get(result.data.value[i], valueFieldName)}`,
value: _.get(result.data.value[i], valueFieldName), value: _.get(result.data.value[i], valueFieldName),
displayName: _.get(result.data.value[i], textFieldName),
}); });
} }
} }
......
...@@ -9,8 +9,8 @@ describe('AzureMonitorUrlBuilder', () => { ...@@ -9,8 +9,8 @@ describe('AzureMonitorUrlBuilder', () => {
'rg', 'rg',
'Microsoft.Sql/servers/databases', 'Microsoft.Sql/servers/databases',
'rn1/rn2', 'rn1/rn2',
'default', '2017-05-01-preview',
'2017-05-01-preview' 'default'
); );
expect(url).toBe( expect(url).toBe(
'/sub1/resourceGroups/rg/providers/Microsoft.Sql/servers/rn1/databases/rn2/' + '/sub1/resourceGroups/rg/providers/Microsoft.Sql/servers/rn1/databases/rn2/' +
...@@ -27,8 +27,8 @@ describe('AzureMonitorUrlBuilder', () => { ...@@ -27,8 +27,8 @@ describe('AzureMonitorUrlBuilder', () => {
'rg', 'rg',
'Microsoft.Sql/servers', 'Microsoft.Sql/servers',
'rn', 'rn',
'default', '2017-05-01-preview',
'2017-05-01-preview' 'default'
); );
expect(url).toBe( expect(url).toBe(
'/sub1/resourceGroups/rg/providers/Microsoft.Sql/servers/rn/' + '/sub1/resourceGroups/rg/providers/Microsoft.Sql/servers/rn/' +
...@@ -45,8 +45,8 @@ describe('AzureMonitorUrlBuilder', () => { ...@@ -45,8 +45,8 @@ describe('AzureMonitorUrlBuilder', () => {
'rg', 'rg',
'Microsoft.Storage/storageAccounts/blobServices', 'Microsoft.Storage/storageAccounts/blobServices',
'rn1/default', 'rn1/default',
'default', '2017-05-01-preview',
'2017-05-01-preview' 'default'
); );
expect(url).toBe( expect(url).toBe(
'/sub1/resourceGroups/rg/providers/Microsoft.Storage/storageAccounts/rn1/blobServices/default/' + '/sub1/resourceGroups/rg/providers/Microsoft.Storage/storageAccounts/rn1/blobServices/default/' +
...@@ -63,8 +63,8 @@ describe('AzureMonitorUrlBuilder', () => { ...@@ -63,8 +63,8 @@ describe('AzureMonitorUrlBuilder', () => {
'rg', 'rg',
'Microsoft.Storage/storageAccounts/fileServices', 'Microsoft.Storage/storageAccounts/fileServices',
'rn1/default', 'rn1/default',
'default', '2017-05-01-preview',
'2017-05-01-preview' 'default'
); );
expect(url).toBe( expect(url).toBe(
'/sub1/resourceGroups/rg/providers/Microsoft.Storage/storageAccounts/rn1/fileServices/default/' + '/sub1/resourceGroups/rg/providers/Microsoft.Storage/storageAccounts/rn1/fileServices/default/' +
...@@ -81,8 +81,8 @@ describe('AzureMonitorUrlBuilder', () => { ...@@ -81,8 +81,8 @@ describe('AzureMonitorUrlBuilder', () => {
'rg', 'rg',
'Microsoft.Storage/storageAccounts/tableServices', 'Microsoft.Storage/storageAccounts/tableServices',
'rn1/default', 'rn1/default',
'default', '2017-05-01-preview',
'2017-05-01-preview' 'default'
); );
expect(url).toBe( expect(url).toBe(
'/sub1/resourceGroups/rg/providers/Microsoft.Storage/storageAccounts/rn1/tableServices/default/' + '/sub1/resourceGroups/rg/providers/Microsoft.Storage/storageAccounts/rn1/tableServices/default/' +
...@@ -99,8 +99,8 @@ describe('AzureMonitorUrlBuilder', () => { ...@@ -99,8 +99,8 @@ describe('AzureMonitorUrlBuilder', () => {
'rg', 'rg',
'Microsoft.Storage/storageAccounts/queueServices', 'Microsoft.Storage/storageAccounts/queueServices',
'rn1/default', 'rn1/default',
'default', '2017-05-01-preview',
'2017-05-01-preview' 'default'
); );
expect(url).toBe( expect(url).toBe(
'/sub1/resourceGroups/rg/providers/Microsoft.Storage/storageAccounts/rn1/queueServices/default/' + '/sub1/resourceGroups/rg/providers/Microsoft.Storage/storageAccounts/rn1/queueServices/default/' +
...@@ -108,4 +108,22 @@ describe('AzureMonitorUrlBuilder', () => { ...@@ -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'
);
});
});
}); });
...@@ -29,26 +29,24 @@ export default class UrlBuilder { ...@@ -29,26 +29,24 @@ export default class UrlBuilder {
resourceGroup: string, resourceGroup: string,
metricDefinition: string, metricDefinition: string,
resourceName: string, resourceName: string,
metricNamespace: string, apiVersion: string,
apiVersion: string metricNamespace?: string
) { ) {
const metricNameSpaceParam = metricNamespace ? `&metricnamespace=${encodeURIComponent(metricNamespace)}` : '';
if ((metricDefinition.match(/\//g) || []).length > 1) { if ((metricDefinition.match(/\//g) || []).length > 1) {
const rn = resourceName.split('/'); const rn = resourceName.split('/');
const service = metricDefinition.substring(metricDefinition.lastIndexOf('/') + 1); const service = metricDefinition.substring(metricDefinition.lastIndexOf('/') + 1);
const md = metricDefinition.substring(0, metricDefinition.lastIndexOf('/')); const md = metricDefinition.substring(0, metricDefinition.lastIndexOf('/'));
return ( return (
`${baseUrl}/${subscriptionId}/resourceGroups/${resourceGroup}/providers/${md}/${rn[0]}/${service}/${rn[1]}` + `${baseUrl}/${subscriptionId}/resourceGroups/${resourceGroup}/providers/${md}/${rn[0]}/${service}/${rn[1]}` +
`/providers/microsoft.insights/metricdefinitions?api-version=${apiVersion}&metricnamespace=${encodeURIComponent( `/providers/microsoft.insights/metricdefinitions?api-version=${apiVersion}${metricNameSpaceParam}`
metricNamespace
)}`
); );
} }
return ( return (
`${baseUrl}/${subscriptionId}/resourceGroups/${resourceGroup}/providers/${metricDefinition}/${resourceName}` + `${baseUrl}/${subscriptionId}/resourceGroups/${resourceGroup}/providers/${metricDefinition}/${resourceName}` +
`/providers/microsoft.insights/metricdefinitions?api-version=${apiVersion}&metricnamespace=${encodeURIComponent( `/providers/microsoft.insights/metricdefinitions?api-version=${apiVersion}${metricNameSpaceParam}`
metricNamespace
)}`
); );
} }
} }
import _ from 'lodash'; import _ from 'lodash';
import { migrateTargetSchema } from './migrations';
import AzureMonitorDatasource from './azure_monitor/azure_monitor_datasource'; import AzureMonitorDatasource from './azure_monitor/azure_monitor_datasource';
import AppInsightsDatasource from './app_insights/app_insights_datasource'; import AppInsightsDatasource from './app_insights/app_insights_datasource';
import AzureLogAnalyticsDatasource from './azure_log_analytics/azure_log_analytics_datasource'; import AzureLogAnalyticsDatasource from './azure_log_analytics/azure_log_analytics_datasource';
...@@ -42,7 +43,9 @@ export default class Datasource extends DataSourceApi<AzureMonitorQuery, AzureDa ...@@ -42,7 +43,9 @@ export default class Datasource extends DataSourceApi<AzureMonitorQuery, AzureDa
const appInsightsOptions = _.cloneDeep(options); const appInsightsOptions = _.cloneDeep(options);
const azureLogAnalyticsOptions = _.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']); appInsightsOptions.targets = _.filter(appInsightsOptions.targets, ['queryType', 'Application Insights']);
azureLogAnalyticsOptions.targets = _.filter(azureLogAnalyticsOptions.targets, ['queryType', 'Azure Log Analytics']); azureLogAnalyticsOptions.targets = _.filter(azureLogAnalyticsOptions.targets, ['queryType', 'Azure Log Analytics']);
...@@ -163,7 +166,7 @@ export default class Datasource extends DataSourceApi<AzureMonitorQuery, AzureDa ...@@ -163,7 +166,7 @@ export default class Datasource extends DataSourceApi<AzureMonitorQuery, AzureDa
resourceGroup: string, resourceGroup: string,
metricDefinition: string, metricDefinition: string,
resourceName: string, resourceName: string,
metricNamespace: string metricNamespace?: string
) { ) {
return this.azureMonitorDatasource.getMetricNames( return this.azureMonitorDatasource.getMetricNames(
subscriptionId, subscriptionId,
...@@ -188,19 +191,23 @@ export default class Datasource extends DataSourceApi<AzureMonitorQuery, AzureDa ...@@ -188,19 +191,23 @@ export default class Datasource extends DataSourceApi<AzureMonitorQuery, AzureDa
resourceGroup: string, resourceGroup: string,
metricDefinition: string, metricDefinition: string,
resourceName: string, resourceName: string,
metricNamespace: string, metricName: string,
metricName: string metricNamespace?: string
) { ) {
return this.azureMonitorDatasource.getMetricMetadata( return this.azureMonitorDatasource.getMetricMetadata(
subscriptionId, subscriptionId,
resourceGroup, resourceGroup,
metricDefinition, metricDefinition,
resourceName, resourceName,
metricNamespace, metricName,
metricName metricNamespace
); );
} }
getResources(subscriptions: string[]) {
return this.azureMonitorDatasource.getResources(subscriptions);
}
/* Application Insights API method */ /* Application Insights API method */
getAppInsightsMetricNames() { getAppInsightsMetricNames() {
return this.appInsightsDatasource.getMetricNames(); return this.appInsightsDatasource.getMetricNames();
......
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;
}
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);
<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>
\ No newline at end of file
...@@ -36,11 +36,11 @@ describe('AzureMonitorQueryCtrl', () => { ...@@ -36,11 +36,11 @@ describe('AzureMonitorQueryCtrl', () => {
}); });
it('should set query parts to select', () => { it('should set query parts to select', () => {
expect(queryCtrl.target.azureMonitor.resourceGroup).toBe('select'); expect(queryCtrl.target.azureMonitor.data.singleResource.resourceGroup).toBe('select');
expect(queryCtrl.target.azureMonitor.metricDefinition).toBe('select'); expect(queryCtrl.target.azureMonitor.data.singleResource.metricDefinition).toBe('select');
expect(queryCtrl.target.azureMonitor.resourceName).toBe('select'); expect(queryCtrl.target.azureMonitor.data.singleResource.resourceName).toBe('select');
expect(queryCtrl.target.azureMonitor.metricNamespace).toBe('select'); expect(queryCtrl.target.azureMonitor.data.singleResource.metricNamespace).toBe('select');
expect(queryCtrl.target.azureMonitor.metricName).toBe('select'); expect(queryCtrl.target.azureMonitor.data.singleResource.metricName).toBe('select');
expect(queryCtrl.target.appInsights.groupBy).toBe('none'); expect(queryCtrl.target.appInsights.groupBy).toBe('none');
}); });
}); });
...@@ -76,7 +76,7 @@ describe('AzureMonitorQueryCtrl', () => { ...@@ -76,7 +76,7 @@ describe('AzureMonitorQueryCtrl', () => {
beforeEach(() => { beforeEach(() => {
queryCtrl.target.subscription = 'sub1'; queryCtrl.target.subscription = 'sub1';
queryCtrl.target.azureMonitor.resourceGroup = 'test'; queryCtrl.target.azureMonitor.data.singleResource.resourceGroup = 'test';
queryCtrl.datasource.getMetricDefinitions = function(subscriptionId: any, query: any) { queryCtrl.datasource.getMetricDefinitions = function(subscriptionId: any, query: any) {
expect(subscriptionId).toBe('sub1'); expect(subscriptionId).toBe('sub1');
expect(query).toBe('test'); expect(query).toBe('test');
...@@ -94,7 +94,7 @@ describe('AzureMonitorQueryCtrl', () => { ...@@ -94,7 +94,7 @@ describe('AzureMonitorQueryCtrl', () => {
describe('and resource group has no value', () => { describe('and resource group has no value', () => {
beforeEach(() => { beforeEach(() => {
queryCtrl.target.azureMonitor.resourceGroup = 'select'; queryCtrl.target.azureMonitor.data.singleResource.resourceGroup = 'select';
}); });
it('should return without making a call to datasource', () => { it('should return without making a call to datasource', () => {
...@@ -109,8 +109,8 @@ describe('AzureMonitorQueryCtrl', () => { ...@@ -109,8 +109,8 @@ describe('AzureMonitorQueryCtrl', () => {
beforeEach(() => { beforeEach(() => {
queryCtrl.target.subscription = 'sub1'; queryCtrl.target.subscription = 'sub1';
queryCtrl.target.azureMonitor.resourceGroup = 'test'; queryCtrl.target.azureMonitor.data.singleResource.resourceGroup = 'test';
queryCtrl.target.azureMonitor.metricDefinition = 'Microsoft.Compute/virtualMachines'; queryCtrl.target.azureMonitor.data.singleResource.metricDefinition = 'Microsoft.Compute/virtualMachines';
queryCtrl.datasource.getResourceNames = function( queryCtrl.datasource.getResourceNames = function(
subscriptionId: any, subscriptionId: any,
resourceGroup: any, resourceGroup: any,
...@@ -133,8 +133,8 @@ describe('AzureMonitorQueryCtrl', () => { ...@@ -133,8 +133,8 @@ describe('AzureMonitorQueryCtrl', () => {
describe('and resourceGroup and metricDefinition do not have values', () => { describe('and resourceGroup and metricDefinition do not have values', () => {
beforeEach(() => { beforeEach(() => {
queryCtrl.target.azureMonitor.resourceGroup = 'select'; queryCtrl.target.azureMonitor.data.singleResource.resourceGroup = 'select';
queryCtrl.target.azureMonitor.metricDefinition = 'select'; queryCtrl.target.azureMonitor.data.singleResource.metricDefinition = 'select';
}); });
it('should return without making a call to datasource', () => { it('should return without making a call to datasource', () => {
...@@ -149,10 +149,10 @@ describe('AzureMonitorQueryCtrl', () => { ...@@ -149,10 +149,10 @@ describe('AzureMonitorQueryCtrl', () => {
beforeEach(() => { beforeEach(() => {
queryCtrl.target.subscription = 'sub1'; queryCtrl.target.subscription = 'sub1';
queryCtrl.target.azureMonitor.resourceGroup = 'test'; queryCtrl.target.azureMonitor.data.singleResource.resourceGroup = 'test';
queryCtrl.target.azureMonitor.metricDefinition = 'Microsoft.Compute/virtualMachines'; queryCtrl.target.azureMonitor.data.singleResource.metricDefinition = 'Microsoft.Compute/virtualMachines';
queryCtrl.target.azureMonitor.resourceName = 'test'; queryCtrl.target.azureMonitor.data.singleResource.resourceName = 'test';
queryCtrl.target.azureMonitor.metricNamespace = 'test'; queryCtrl.target.azureMonitor.data.singleResource.metricNamespace = 'test';
queryCtrl.datasource.getMetricNames = function( queryCtrl.datasource.getMetricNames = function(
subscriptionId: any, subscriptionId: any,
resourceGroup: any, resourceGroup: any,
...@@ -179,10 +179,10 @@ describe('AzureMonitorQueryCtrl', () => { ...@@ -179,10 +179,10 @@ describe('AzureMonitorQueryCtrl', () => {
describe('and resourceGroup, metricDefinition, resourceName and metricNamespace do not have values', () => { describe('and resourceGroup, metricDefinition, resourceName and metricNamespace do not have values', () => {
beforeEach(() => { beforeEach(() => {
queryCtrl.target.azureMonitor.resourceGroup = 'select'; queryCtrl.target.azureMonitor.data.singleResource.resourceGroup = 'select';
queryCtrl.target.azureMonitor.metricDefinition = 'select'; queryCtrl.target.azureMonitor.data.singleResource.metricDefinition = 'select';
queryCtrl.target.azureMonitor.resourceName = 'select'; queryCtrl.target.azureMonitor.data.singleResource.resourceName = 'select';
queryCtrl.target.azureMonitor.metricNamespace = 'select'; queryCtrl.target.azureMonitor.data.singleResource.metricNamespace = 'select';
}); });
it('should return without making a call to datasource', () => { it('should return without making a call to datasource', () => {
...@@ -201,18 +201,18 @@ describe('AzureMonitorQueryCtrl', () => { ...@@ -201,18 +201,18 @@ describe('AzureMonitorQueryCtrl', () => {
beforeEach(() => { beforeEach(() => {
queryCtrl.target.subscription = 'sub1'; queryCtrl.target.subscription = 'sub1';
queryCtrl.target.azureMonitor.resourceGroup = 'test'; queryCtrl.target.azureMonitor.data.singleResource.resourceGroup = 'test';
queryCtrl.target.azureMonitor.metricDefinition = 'Microsoft.Compute/virtualMachines'; queryCtrl.target.azureMonitor.data.singleResource.metricDefinition = 'Microsoft.Compute/virtualMachines';
queryCtrl.target.azureMonitor.resourceName = 'test'; queryCtrl.target.azureMonitor.data.singleResource.resourceName = 'test';
queryCtrl.target.azureMonitor.metricNamespace = 'test'; queryCtrl.target.azureMonitor.data.singleResource.metricNamespace = 'test';
queryCtrl.target.azureMonitor.metricName = 'Percentage CPU'; queryCtrl.target.azureMonitor.data.singleResource.metricName = 'Percentage CPU';
queryCtrl.datasource.getMetricMetadata = function( queryCtrl.datasource.getMetricMetadata = function(
subscription: any, subscription: any,
resourceGroup: any, resourceGroup: any,
metricDefinition: any, metricDefinition: any,
resourceName: any, resourceName: any,
metricNamespace: any, metricName: any,
metricName: any metricNamespace: any
) { ) {
expect(subscription).toBe('sub1'); expect(subscription).toBe('sub1');
expect(resourceGroup).toBe('test'); expect(resourceGroup).toBe('test');
...@@ -226,9 +226,9 @@ describe('AzureMonitorQueryCtrl', () => { ...@@ -226,9 +226,9 @@ describe('AzureMonitorQueryCtrl', () => {
it('should set the options and default selected value for the Aggregations dropdown', () => { it('should set the options and default selected value for the Aggregations dropdown', () => {
queryCtrl.onMetricNameChange().then(() => { queryCtrl.onMetricNameChange().then(() => {
expect(queryCtrl.target.azureMonitor.aggregation).toBe('Average'); expect(queryCtrl.target.azureMonitor.data.singleResource.aggregation).toBe('Average');
expect(queryCtrl.target.azureMonitor.aggOptions).toBe(['Average', 'Total']); expect(queryCtrl.target.azureMonitor.data.singleResource.aggOptions).toBe(['Average', 'Total']);
expect(queryCtrl.target.azureMonitor.timeGrains).toBe(['PT1M', 'P1D']); expect(queryCtrl.target.azureMonitor.data.singleResource.timeGrains).toBe(['PT1M', 'P1D']);
}); });
}); });
}); });
......
...@@ -3,6 +3,7 @@ import { DataQuery, DataSourceJsonData } from '@grafana/ui'; ...@@ -3,6 +3,7 @@ import { DataQuery, DataSourceJsonData } from '@grafana/ui';
export interface AzureMonitorQuery extends DataQuery { export interface AzureMonitorQuery extends DataQuery {
format: string; format: string;
subscription: string; subscription: string;
subscriptions: string[];
azureMonitor: AzureMetricQuery; azureMonitor: AzureMetricQuery;
azureLogAnalytics: AzureLogsQuery; azureLogAnalytics: AzureLogsQuery;
// appInsights: any; // appInsights: any;
...@@ -26,9 +27,9 @@ export interface AzureDataSourceJsonData extends DataSourceJsonData { ...@@ -26,9 +27,9 @@ export interface AzureDataSourceJsonData extends DataSourceJsonData {
// App Insights // App Insights
appInsightsAppId?: string; appInsightsAppId?: string;
} }
export interface AzureMonitorQueryData {
export interface AzureMetricQuery {
resourceGroup: string; resourceGroup: string;
resourceGroups: string[];
resourceName: string; resourceName: string;
metricDefinition: string; metricDefinition: string;
metricNamespace: string; metricNamespace: string;
...@@ -41,6 +42,12 @@ export interface AzureMetricQuery { ...@@ -41,6 +42,12 @@ export interface AzureMetricQuery {
dimension: string; dimension: string;
dimensionFilter: string; dimensionFilter: string;
alias: string; alias: string;
locations: string[];
}
export interface AzureMetricQuery extends AzureMonitorQueryData {
queryMode: string;
data: { [queryMode: string]: AzureMonitorQueryData };
} }
export interface AzureLogsQuery { export interface AzureLogsQuery {
...@@ -67,6 +74,24 @@ export interface AzureMonitorResourceGroupsResponse { ...@@ -67,6 +74,24 @@ export interface AzureMonitorResourceGroupsResponse {
statusText: string; 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 // Azure Log Analytics types
export interface KustoSchema { export interface KustoSchema {
Databases: { [key: string]: KustoDatabase }; Databases: { [key: string]: KustoDatabase };
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment