Skip to content

Commit

Permalink
AzureMonitor: Migrate Metrics query editor to React (grafana#30783)
Browse files Browse the repository at this point in the history
* AzureMonitor: Remove anys from datasource to get the inferred type

* AzureMonitor: Cast some datasource types

TODO: we want proper types for these

* AzureMonitor: Initial react Metrics editor components

* start dimension fields

* replace replaceTemplateVariable with datasource.replace, and rename onQueryChange to onChange

* actually just do template variable replacement in the datasource

* don't use azureMonitorIsConfigured

* Refactors, mainly around the metric metadata

 - Convert all the metric metadata options for the Select before its set into state
 - Stop using SelectableValue because it's basically any when all the properties are optional
 - the onChange function passed to the fields now just accepts the direct value, rather than wrapped in a SelectableValue

* added proper fields, and adding and removing for DimensionFields

* Update query with Dimension changes

* Width

* subscription and query type fields

* Should be feature complete now, more or less

* fix missing import

* fix lint issues

* set default subscription ID

* Starting to write some tests

* tests for query editor

* Remove subscription ID from the label in Metrics

But we keep it there for the angular stuff

* MetricsQueryEditor tests

* Update index.test.tsx

* fix tests

* add template variables to dropdowns

* clean up

* update tests

* Reorganise react components

* Group query fields into rows

* Rename Option type, add Azure response type

* Refactor Metrics metric metadata

 - Types the Azure API
 - Moves default metadata values into datasource

* nit

* update test
  • Loading branch information
joshhunt authored Mar 11, 2021
1 parent 05d6d32 commit 13a47ae
Show file tree
Hide file tree
Showing 32 changed files with 1,576 additions and 73 deletions.
20 changes: 20 additions & 0 deletions public/app/core/angular_wrappers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ import { HelpModal } from './components/help/HelpModal';
import { Footer } from './components/Footer/Footer';
import { FolderPicker } from 'app/core/components/Select/FolderPicker';
import { SearchField, SearchResults, SearchResultsFilter } from '../features/search';
import { TimePickerSettings } from 'app/features/dashboard/components/DashboardSettings/TimePickerSettings';
import QueryEditor from 'app/plugins/datasource/grafana-azure-monitor-datasource/components/QueryEditor/QueryEditor';

const { SecretFormField } = LegacyForms;

Expand Down Expand Up @@ -181,4 +183,22 @@ export function registerAngularDirectives() {
['onLoad', { watchDepth: 'reference', wrapApply: true }],
['onChange', { watchDepth: 'reference', wrapApply: true }],
]);

react2AngularDirective('timePickerSettings', TimePickerSettings, [
'renderCount',
'refreshIntervals',
'timePickerHidden',
'nowDelay',
'timezone',
['onTimeZoneChange', { watchDepth: 'reference', wrapApply: true }],
['onRefreshIntervalChange', { watchDepth: 'reference', wrapApply: true }],
['onNowDelayChange', { watchDepth: 'reference', wrapApply: true }],
['onHideTimePickerChange', { watchDepth: 'reference', wrapApply: true }],
]);

react2AngularDirective('azureMonitorQueryEditor', QueryEditor, [
'query',
['datasource', { watchDepth: 'reference' }],
'onChange',
]);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import Datasource from '../datasource';

type DeepPartial<T> = {
[P in keyof T]?: DeepPartial<T[P]>;
};

export default function createMockDatasource() {
// We make this a partial so we get _some_ kind of type safety when making this, rather than
// having it be any or casted immediately to Datasource
const _mockDatasource: DeepPartial<Datasource> = {
getVariables: jest.fn().mockReturnValueOnce([]),

azureMonitorDatasource: {
isConfigured() {
return true;
},
getSubscriptions: jest.fn().mockResolvedValueOnce([]),
},

getResourceGroups: jest.fn().mockResolvedValueOnce([]),
getMetricDefinitions: jest.fn().mockResolvedValueOnce([]),
getResourceNames: jest.fn().mockResolvedValueOnce([]),
getMetricNamespaces: jest.fn().mockResolvedValueOnce([]),
getMetricNames: jest.fn().mockResolvedValueOnce([]),
getMetricMetadata: jest.fn().mockResolvedValueOnce({
primaryAggType: 'average',
supportedAggTypes: [],
supportedTimeGrains: [],
dimensions: [],
}),
};

const mockDatasource = _mockDatasource as Datasource;

return mockDatasource;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { AzureMonitorQuery, AzureQueryType } from '../types';

const azureMonitorQuery: AzureMonitorQuery = {
appInsights: undefined, // The actualy shape of this at runtime disagrees with the ts interface

azureLogAnalytics: {
query:
'//change this example to create your own time series query\n<table name> //the table to query (e.g. Usage, Heartbeat, Perf)\n| where $__timeFilter(TimeGenerated) //this is a macro used to show the full chart’s time range, choose the datetime column here\n| summarize count() by <group by column>, bin(TimeGenerated, $__interval) //change “group by column” to a column in your table, such as “Computer”. The $__interval macro is used to auto-select the time grain. Can also use 1h, 5m etc.\n| order by TimeGenerated asc',
resultFormat: 'time_series',
workspace: 'e3fe4fde-ad5e-4d60-9974-e2f3562ffdf2',
},

azureMonitor: {
// aggOptions: [],
aggregation: 'Average',
allowedTimeGrainsMs: [60000, 300000, 900000, 1800000, 3600000, 21600000, 43200000, 86400000],
// dimensionFilter: '*',
dimensionFilters: [],
metricDefinition: 'Microsoft.Compute/virtualMachines',
metricName: 'Metric A',
metricNamespace: 'Microsoft.Compute/virtualMachines',
resourceGroup: 'grafanastaging',
resourceName: 'grafana',
timeGrain: 'auto',
alias: '',
// timeGrains: [],
top: '10',
},

insightsAnalytics: {
query: '',
resultFormat: 'time_series',
},

queryType: AzureQueryType.AzureMonitor,
refId: 'A',
subscription: 'abc-123',

format: 'dunno lol', // unsure what this value should be. It's not there at runtime, but it's in the ts interface
};

export default azureMonitorQuery;
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,16 @@ export default class AzureLogAnalyticsDatasource extends DataSourceWithBackend<
url: string;
baseUrl: string;
applicationId: string;

/**
* @deprecated
* TODO: Which one of these values should be used? Was there a migration?
* */
logAnalyticsSubscriptionId: string;
subscriptionId: string;

azureMonitorUrl: string;
defaultOrFirstWorkspace: string;
subscriptionId: string;
cache: Map<string, any>;

constructor(private instanceSettings: DataSourceInstanceSettings<AzureDataSourceJsonData>) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -105,9 +105,9 @@ describe('AzureMonitorDatasource', () => {
it('should return a list of subscriptions', () => {
return ctx.ds.metricFindQuery('subscriptions()').then((results: Array<{ text: string; value: string }>) => {
expect(results.length).toBe(2);
expect(results[0].text).toBe('Primary - sub1');
expect(results[0].text).toBe('Primary');
expect(results[0].value).toBe('sub1');
expect(results[1].text).toBe('Secondary - sub2');
expect(results[1].text).toBe('Secondary');
expect(results[1].value).toBe('sub2');
});
});
Expand Down Expand Up @@ -545,7 +545,7 @@ describe('AzureMonitorDatasource', () => {
it('should return list of Resource Groups', () => {
return ctx.ds.getSubscriptions().then((results: Array<{ text: string; value: string }>) => {
expect(results.length).toEqual(1);
expect(results[0].text).toEqual('Primary Subscription - 99999999-cccc-bbbb-aaaa-9106972f9572');
expect(results[0].text).toEqual('Primary Subscription');
expect(results[0].value).toEqual('99999999-cccc-bbbb-aaaa-9106972f9572');
});
});
Expand Down Expand Up @@ -856,10 +856,10 @@ describe('AzureMonitorDatasource', () => {
'default',
'UsedCapacity'
)
.then((results: any) => {
.then((results) => {
expect(results.primaryAggType).toEqual('Total');
expect(results.supportedAggTypes.length).toEqual(6);
expect(results.supportedTimeGrains.length).toEqual(4);
expect(results.supportedTimeGrains.length).toEqual(5); // 4 time grains from the API + auto
});
});
});
Expand Down Expand Up @@ -934,15 +934,15 @@ describe('AzureMonitorDatasource', () => {
expect(results.dimensions).toMatchInlineSnapshot(`
Array [
Object {
"text": "Response type",
"label": "Response type",
"value": "ResponseType",
},
Object {
"text": "Geo type",
"label": "Geo type",
"value": "GeoType",
},
Object {
"text": "API name",
"label": "API name",
"value": "ApiName",
},
]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,10 @@ import {
AzureMonitorMetricDefinitionsResponse,
AzureMonitorResourceGroupsResponse,
AzureQueryType,
AzureMonitorMetricsMetadataResponse,
} from '../types';
import { DataSourceInstanceSettings, ScopedVars, MetricFindValue } from '@grafana/data';
import { getBackendSrv, DataSourceWithBackend, getTemplateSrv } from '@grafana/runtime';
import { getBackendSrv, DataSourceWithBackend, getTemplateSrv, FetchResponse } from '@grafana/runtime';

const defaultDropdownValue = 'select';

Expand Down Expand Up @@ -224,7 +225,7 @@ export default class AzureMonitorDatasource extends DataSourceWithBackend<AzureM
.then((result: AzureMonitorMetricDefinitionsResponse) => {
return ResponseParser.parseResponseValues(result, 'type', 'type');
})
.then((result: any) => {
.then((result) => {
return filter(result, (t) => {
for (let i = 0; i < this.supportedMetricNamespaces.length; i++) {
if (t.value.toLowerCase() === this.supportedMetricNamespaces[i].toLowerCase()) {
Expand All @@ -235,7 +236,7 @@ export default class AzureMonitorDatasource extends DataSourceWithBackend<AzureM
return false;
});
})
.then((result: any) => {
.then((result) => {
let shouldHardcodeBlobStorage = false;
for (let i = 0; i < result.length; i++) {
if (result[i].value === 'Microsoft.Storage/storageAccounts') {
Expand Down Expand Up @@ -340,8 +341,8 @@ export default class AzureMonitorDatasource extends DataSourceWithBackend<AzureM
this.apiVersion
);

return this.doRequest(url).then((result: any) => {
return ResponseParser.parseMetadata(result, metricName);
return this.doRequest<AzureMonitorMetricsMetadataResponse>(url).then((result) => {
return ResponseParser.parseMetadata(result.data, metricName);
});
}

Expand Down Expand Up @@ -400,15 +401,15 @@ export default class AzureMonitorDatasource extends DataSourceWithBackend<AzureM
return field && field.length > 0;
}

doRequest(url: string, maxRetries = 1): Promise<any> {
doRequest<T = any>(url: string, maxRetries = 1): Promise<FetchResponse<T>> {
return getBackendSrv()
.datasourceRequest({
.datasourceRequest<T>({
url: this.url + url,
method: 'GET',
})
.catch((error: any) => {
if (maxRetries > 0) {
return this.doRequest(url, maxRetries - 1);
return this.doRequest<T>(url, maxRetries - 1);
}

throw error;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
import _ from 'lodash';
import TimeGrainConverter from '../time_grain_converter';
import {
AzureMonitorLocalizedValue,
AzureMonitorMetricAvailabilityMetadata,
AzureMonitorMetricsMetadataResponse,
AzureMonitorOption,
} from '../types';
export default class ResponseParser {
static parseResponseValues(
result: any,
Expand Down Expand Up @@ -45,10 +51,11 @@ export default class ResponseParser {
return list;
}

static parseMetadata(result: any, metricName: string) {
static parseMetadata(result: AzureMonitorMetricsMetadataResponse, metricName: string) {
const defaultAggTypes = ['None', 'Average', 'Minimum', 'Maximum', 'Total', 'Count'];
const metricData = result?.value.find((v) => v.name.value === metricName);

if (!result) {
if (!metricData) {
return {
primaryAggType: '',
supportedAggTypes: defaultAggTypes,
Expand All @@ -57,51 +64,44 @@ export default class ResponseParser {
};
}

const metricData: any = _.find(result.data.value, (o) => {
return _.get(o, 'name.value') === metricName;
});

return {
primaryAggType: metricData.primaryAggregationType,
supportedAggTypes: metricData.supportedAggregationTypes || defaultAggTypes,
supportedTimeGrains: ResponseParser.parseTimeGrains(metricData.metricAvailabilities || []),
dimensions: ResponseParser.parseDimensions(metricData),

supportedTimeGrains: [
{ label: 'Auto', value: 'auto' },
...ResponseParser.parseTimeGrains(metricData.metricAvailabilities ?? []),
],
dimensions: ResponseParser.parseDimensions(metricData.dimensions ?? []),
};
}

static parseTimeGrains(metricAvailabilities: any[]): Array<{ text: string; value: string }> {
const timeGrains: any[] = [];
static parseTimeGrains(metricAvailabilities: AzureMonitorMetricAvailabilityMetadata[]): AzureMonitorOption[] {
const timeGrains: AzureMonitorOption[] = [];

if (!metricAvailabilities) {
return timeGrains;
}

metricAvailabilities.forEach((avail) => {
if (avail.timeGrain) {
timeGrains.push({
text: TimeGrainConverter.createTimeGrainFromISO8601Duration(avail.timeGrain),
label: TimeGrainConverter.createTimeGrainFromISO8601Duration(avail.timeGrain),
value: avail.timeGrain,
});
}
});

return timeGrains;
}

static parseDimensions(metricData: any): Array<{ text: string; value: string }> {
const dimensions: Array<{ text: string; value: string }> = [];
if (!metricData.dimensions || metricData.dimensions.length === 0) {
return dimensions;
}

for (let i = 0; i < metricData.dimensions.length; i++) {
const text = metricData.dimensions[i].localizedValue;
const value = metricData.dimensions[i].value;

dimensions.push({
text: !text ? value : text,
value: value,
});
}
return dimensions;
static parseDimensions(metadataDimensions: AzureMonitorLocalizedValue[]) {
return metadataDimensions.map((dimension) => {
return {
label: dimension.localizedValue || dimension.value,
value: dimension.value,
};
});
}

static parseSubscriptions(result: any): Array<{ text: string; value: string }> {
Expand All @@ -116,7 +116,7 @@ export default class ResponseParser {
for (let i = 0; i < result.data.value.length; i++) {
if (!_.find(list, ['value', _.get(result.data.value[i], valueFieldName)])) {
list.push({
text: `${_.get(result.data.value[i], textFieldName)} - ${_.get(result.data.value[i], valueFieldName)}`,
text: `${_.get(result.data.value[i], textFieldName)}`,
value: _.get(result.data.value[i], valueFieldName),
});
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { InlineField } from '@grafana/ui';
import React from 'react';
import { Props as InlineFieldProps } from '@grafana/ui/src/components/Forms/InlineField';

const DEFAULT_LABEL_WIDTH = 18;

export const Field = (props: InlineFieldProps) => {
return <InlineField labelWidth={DEFAULT_LABEL_WIDTH} {...props} />;
};
Loading

0 comments on commit 13a47ae

Please sign in to comment.