diff --git a/public/app/core/angular_wrappers.ts b/public/app/core/angular_wrappers.ts index 9aea8be0aab2e..222be19d297ca 100644 --- a/public/app/core/angular_wrappers.ts +++ b/public/app/core/angular_wrappers.ts @@ -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; @@ -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', + ]); } diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/__mocks__/datasource.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/__mocks__/datasource.ts new file mode 100644 index 0000000000000..013c61bd56d53 --- /dev/null +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/__mocks__/datasource.ts @@ -0,0 +1,36 @@ +import Datasource from '../datasource'; + +type DeepPartial = { + [P in keyof T]?: DeepPartial; +}; + +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 = { + 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; +} diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/__mocks__/query.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/__mocks__/query.ts new file mode 100644 index 0000000000000..16b9aa555b9ab --- /dev/null +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/__mocks__/query.ts @@ -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 //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 , 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; diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_log_analytics/azure_log_analytics_datasource.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_log_analytics/azure_log_analytics_datasource.ts index 50690fd4d1f73..5ae4a68391c4e 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_log_analytics/azure_log_analytics_datasource.ts +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_log_analytics/azure_log_analytics_datasource.ts @@ -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; constructor(private instanceSettings: DataSourceInstanceSettings) { diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/azure_monitor_datasource.test.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/azure_monitor_datasource.test.ts index f70544139caf1..fe98c6dab6847 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/azure_monitor_datasource.test.ts +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/azure_monitor_datasource.test.ts @@ -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'); }); }); @@ -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'); }); }); @@ -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 }); }); }); @@ -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", }, ] diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/azure_monitor_datasource.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/azure_monitor_datasource.ts index d8c386682c858..67504a5e05e6a 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/azure_monitor_datasource.ts +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/azure_monitor_datasource.ts @@ -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'; @@ -224,7 +225,7 @@ export default class AzureMonitorDatasource extends DataSourceWithBackend { 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()) { @@ -235,7 +236,7 @@ export default class AzureMonitorDatasource extends DataSourceWithBackend { + .then((result) => { let shouldHardcodeBlobStorage = false; for (let i = 0; i < result.length; i++) { if (result[i].value === 'Microsoft.Storage/storageAccounts') { @@ -340,8 +341,8 @@ export default class AzureMonitorDatasource extends DataSourceWithBackend { - return ResponseParser.parseMetadata(result, metricName); + return this.doRequest(url).then((result) => { + return ResponseParser.parseMetadata(result.data, metricName); }); } @@ -400,15 +401,15 @@ export default class AzureMonitorDatasource extends DataSourceWithBackend 0; } - doRequest(url: string, maxRetries = 1): Promise { + doRequest(url: string, maxRetries = 1): Promise> { return getBackendSrv() - .datasourceRequest({ + .datasourceRequest({ url: this.url + url, method: 'GET', }) .catch((error: any) => { if (maxRetries > 0) { - return this.doRequest(url, maxRetries - 1); + return this.doRequest(url, maxRetries - 1); } throw error; diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/response_parser.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/response_parser.ts index bcfeb9db68cf1..6d3eeb9556c9e 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/response_parser.ts +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/response_parser.ts @@ -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, @@ -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, @@ -57,20 +64,21 @@ 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; } @@ -78,30 +86,22 @@ export default class ResponseParser { 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 }> { @@ -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), }); } diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/Field.tsx b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/Field.tsx new file mode 100644 index 0000000000000..6105bbc02eb1c --- /dev/null +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/Field.tsx @@ -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 ; +}; diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/MetricsQueryEditor/AggregationField.tsx b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/MetricsQueryEditor/AggregationField.tsx new file mode 100644 index 0000000000000..029d372162f98 --- /dev/null +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/MetricsQueryEditor/AggregationField.tsx @@ -0,0 +1,54 @@ +import React, { useCallback, useMemo } from 'react'; +import { Select } from '@grafana/ui'; +import { SelectableValue } from '@grafana/data'; + +import { Field } from '../Field'; +import { findOption } from '../common'; +import { AzureQueryEditorFieldProps, AzureMonitorOption } from '../../types'; + +interface AggregationFieldProps extends AzureQueryEditorFieldProps { + aggregationOptions: AzureMonitorOption[]; +} + +const AggregationField: React.FC = ({ + query, + variableOptionGroup, + onQueryChange, + aggregationOptions, +}) => { + const handleChange = useCallback( + (change: SelectableValue) => { + if (!change.value) { + return; + } + + onQueryChange({ + ...query, + azureMonitor: { + ...query.azureMonitor, + aggregation: change.value, + }, + }); + }, + [query] + ); + + const options = useMemo(() => [...aggregationOptions, variableOptionGroup], [ + aggregationOptions, + variableOptionGroup, + ]); + + return ( + + onFieldChange(index, 'dimension', v.value ?? '')} + width={38} + /> + == + onFilterInputChange(index, ev)} /> + + + ))} + + + + + ); +}; + +export default DimensionFields; diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/MetricsQueryEditor/LegendFormatField.tsx b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/MetricsQueryEditor/LegendFormatField.tsx new file mode 100644 index 0000000000000..44e0bf641e786 --- /dev/null +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/MetricsQueryEditor/LegendFormatField.tsx @@ -0,0 +1,42 @@ +import React, { useCallback, useState } from 'react'; +import { Input } from '@grafana/ui'; + +import { Field } from '../Field'; +import { AzureQueryEditorFieldProps } from '../../types'; + +const LegendFormatField: React.FC = ({ onQueryChange, query }) => { + const [value, setValue] = useState(query.azureMonitor.alias ?? ''); + + // As calling onQueryChange initiates a the datasource refresh, we only want to call it once + // the field loses focus + const handleChange = useCallback((ev: React.FormEvent) => { + if (ev.target instanceof HTMLInputElement) { + setValue(ev.target.value); + } + }, []); + + const handleBlur = useCallback(() => { + onQueryChange({ + ...query, + azureMonitor: { + ...query.azureMonitor, + alias: value, + }, + }); + }, [query, value]); + + return ( + + + + ); +}; + +export default LegendFormatField; diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/MetricsQueryEditor/MetricNameField.tsx b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/MetricsQueryEditor/MetricNameField.tsx new file mode 100644 index 0000000000000..0cfa8e71ff6a1 --- /dev/null +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/MetricsQueryEditor/MetricNameField.tsx @@ -0,0 +1,85 @@ +import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import { Select } from '@grafana/ui'; +import { SelectableValue } from '@grafana/data'; + +import { Field } from '../Field'; +import { findOption, toOption } from '../common'; +import { AzureQueryEditorFieldProps, AzureMonitorOption } from '../../types'; + +const MetricName: React.FC = ({ + query, + datasource, + subscriptionId, + variableOptionGroup, + onQueryChange, +}) => { + const [metricNames, setMetricNames] = useState([]); + + useEffect(() => { + if ( + !( + subscriptionId && + query.azureMonitor.resourceGroup && + query.azureMonitor.metricDefinition && + query.azureMonitor.resourceName && + query.azureMonitor.metricNamespace + ) + ) { + metricNames.length > 0 && setMetricNames([]); + return; + } + + datasource + .getMetricNames( + subscriptionId, + query.azureMonitor.resourceGroup, + query.azureMonitor.metricDefinition, + query.azureMonitor.resourceName, + query.azureMonitor.metricNamespace + ) + .then((results) => setMetricNames(results.map(toOption))) + .catch((err) => { + // TODO: handle error + console.error(err); + }); + }, [ + subscriptionId, + query.azureMonitor.resourceGroup, + query.azureMonitor.metricDefinition, + query.azureMonitor.resourceName, + query.azureMonitor.metricNamespace, + ]); + + const handleChange = useCallback( + (change: SelectableValue) => { + if (!change.value) { + return; + } + + onQueryChange({ + ...query, + azureMonitor: { + ...query.azureMonitor, + metricName: change.value, + }, + }); + }, + [query] + ); + + const options = useMemo(() => [...metricNames, variableOptionGroup], [metricNames, variableOptionGroup]); + + return ( + + + + ); +}; + +export default MetricNamespaceField; diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/MetricsQueryEditor/MetricsQueryEditor.test.tsx b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/MetricsQueryEditor/MetricsQueryEditor.test.tsx new file mode 100644 index 0000000000000..f8e64fc6fc25e --- /dev/null +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/MetricsQueryEditor/MetricsQueryEditor.test.tsx @@ -0,0 +1,109 @@ +import React from 'react'; +import { render, screen, waitFor } from '@testing-library/react'; +import selectEvent from 'react-select-event'; + +import MetricsQueryEditor from './MetricsQueryEditor'; + +import mockQuery from '../../__mocks__/query'; +import createMockDatasource from '../../__mocks__/datasource'; + +const variableOptionGroup = { + label: 'Template variables', + options: [], +}; + +describe('Azure Monitor QueryEditor', () => { + it('should render', async () => { + const mockDatasource = createMockDatasource(); + render( + {}} + /> + ); + await waitFor(() => expect(screen.getByTestId('azure-monitor-metrics-query-editor')).toBeInTheDocument()); + }); + + it('should change the subscription ID when selected', async () => { + const mockDatasource = createMockDatasource(); + const onChange = jest.fn(); + mockDatasource.azureMonitorDatasource.getSubscriptions = jest.fn().mockResolvedValueOnce([ + { + value: 'abc-123', + text: 'Primary Subscription', + }, + { + value: 'abc-456', + text: 'Another Subscription', + }, + ]); + + render( + + ); + + const subscriptions = await screen.findByLabelText('Subscription'); + await selectEvent.select(subscriptions, 'Another Subscription'); + + expect(onChange).toHaveBeenCalledWith({ + ...mockQuery, + subscription: 'abc-456', + azureMonitor: { + ...mockQuery.azureMonitor, + resourceGroup: 'select', + metricDefinition: 'select', + resourceName: 'select', + metricName: 'select', + aggregation: '', + timeGrain: '', + dimensionFilters: [], + }, + }); + }); + + it('should change the metric name when selected', async () => { + const mockDatasource = createMockDatasource(); + const onChange = jest.fn(); + mockDatasource.getMetricNames = jest.fn().mockResolvedValueOnce([ + { + value: 'metric-a', + text: 'Metric A', + }, + { + value: 'metric-b', + text: 'Metric B', + }, + ]); + + render( + + ); + await waitFor(() => expect(screen.getByTestId('azure-monitor-metrics-query-editor')).toBeInTheDocument()); + + const metrics = await screen.findByLabelText('Metric'); + await selectEvent.select(metrics, 'Metric B'); + + expect(onChange).toHaveBeenCalledWith({ + ...mockQuery, + azureMonitor: { + ...mockQuery.azureMonitor, + metricName: 'metric-b', + }, + }); + }); +}); diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/MetricsQueryEditor/MetricsQueryEditor.tsx b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/MetricsQueryEditor/MetricsQueryEditor.tsx new file mode 100644 index 0000000000000..8b72cf0e70d7d --- /dev/null +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/MetricsQueryEditor/MetricsQueryEditor.tsx @@ -0,0 +1,133 @@ +import React from 'react'; + +import Datasource from '../../datasource'; +import { AzureMonitorQuery, AzureMonitorOption } from '../../types'; +import { useMetricsMetadata } from '../metrics'; +import SubscriptionField from '../SubscriptionField'; +import MetricNamespaceField from './MetricNamespaceField'; +import NamespaceField from './NamespaceField'; +import ResourceGroupsField from './ResourceGroupsField'; +import ResourceNameField from './ResourceNameField'; +import MetricNameField from './MetricNameField'; +import AggregationField from './AggregationField'; +import TimeGrainField from './TimeGrainField'; +import DimensionFields from './DimensionFields'; +import TopField from './TopField'; +import LegendFormatField from './LegendFormatField'; +import { InlineFieldRow } from '@grafana/ui'; + +interface MetricsQueryEditorProps { + query: AzureMonitorQuery; + datasource: Datasource; + subscriptionId: string; + onChange: (newQuery: AzureMonitorQuery) => void; + variableOptionGroup: { label: string; options: AzureMonitorOption[] }; +} + +const MetricsQueryEditor: React.FC = ({ + query, + datasource, + subscriptionId, + variableOptionGroup, + onChange, +}) => { + const metricsMetadata = useMetricsMetadata(datasource, query, subscriptionId, onChange); + + return ( +
+ + + + + + + + + + + + + + + + + + + + + + +
+ ); +}; + +export default MetricsQueryEditor; diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/MetricsQueryEditor/NamespaceField.tsx b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/MetricsQueryEditor/NamespaceField.tsx new file mode 100644 index 0000000000000..bc1e58a90b62d --- /dev/null +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/MetricsQueryEditor/NamespaceField.tsx @@ -0,0 +1,72 @@ +import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import { Select } from '@grafana/ui'; +import { SelectableValue } from '@grafana/data'; + +import { Field } from '../Field'; +import { findOption, toOption } from '../common'; +import { AzureQueryEditorFieldProps, AzureMonitorOption } from '../../types'; + +const NamespaceField: React.FC = ({ + query, + datasource, + subscriptionId, + variableOptionGroup, + onQueryChange, +}) => { + const [namespaces, setNamespaces] = useState([]); + + useEffect(() => { + if (!(subscriptionId && query.azureMonitor.resourceGroup)) { + namespaces.length && setNamespaces([]); + return; + } + + datasource + .getMetricDefinitions(subscriptionId, query.azureMonitor.resourceGroup) + .then((results) => setNamespaces(results.map(toOption))) + .catch((err) => { + // TODO: handle error + console.error(err); + }); + }, [subscriptionId, query.azureMonitor.resourceGroup]); + + const handleChange = useCallback( + (change: SelectableValue) => { + if (!change.value) { + return; + } + + onQueryChange({ + ...query, + azureMonitor: { + ...query.azureMonitor, + metricDefinition: change.value, + resourceName: 'select', + metricNamespace: 'select', + metricName: 'select', + aggregation: '', + timeGrain: '', + dimensionFilters: [], + }, + }); + }, + [query] + ); + + const options = useMemo(() => [...namespaces, variableOptionGroup], [namespaces, variableOptionGroup]); + + return ( + + {/* It's expected that the label reads Namespace but the property is metricDefinition */} + + + ); +}; + +export default ResourceGroupsField; diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/MetricsQueryEditor/ResourceNameField.tsx b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/MetricsQueryEditor/ResourceNameField.tsx new file mode 100644 index 0000000000000..020697f96f450 --- /dev/null +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/MetricsQueryEditor/ResourceNameField.tsx @@ -0,0 +1,71 @@ +import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import { Select } from '@grafana/ui'; +import { SelectableValue } from '@grafana/data'; + +import { Field } from '../Field'; +import { findOption, toOption } from '../common'; +import { AzureQueryEditorFieldProps, AzureMonitorOption } from '../../types'; + +const ResourceNameField: React.FC = ({ + query, + datasource, + subscriptionId, + variableOptionGroup, + onQueryChange, +}) => { + const [resourceNames, setResourceNames] = useState([]); + + useEffect(() => { + if (!(subscriptionId && query.azureMonitor.resourceGroup && query.azureMonitor.metricDefinition)) { + resourceNames.length > 0 && setResourceNames([]); + return; + } + + datasource + .getResourceNames(subscriptionId, query.azureMonitor.resourceGroup, query.azureMonitor.metricDefinition) + .then((results) => setResourceNames(results.map(toOption))) + .catch((err) => { + // TODO: handle error + console.error(err); + }); + }, [subscriptionId, query.azureMonitor.resourceGroup, query.azureMonitor.metricDefinition]); + + const handleChange = useCallback( + (change: SelectableValue) => { + if (!change.value) { + return; + } + + onQueryChange({ + ...query, + azureMonitor: { + ...query.azureMonitor, + resourceName: change.value, + + metricNamespace: 'select', + metricName: 'select', + aggregation: '', + timeGrain: '', + dimensionFilters: [], + }, + }); + }, + [query] + ); + + const options = useMemo(() => [...resourceNames, variableOptionGroup], [resourceNames, variableOptionGroup]); + + return ( + + + + ); +}; + +export default TimeGrainField; diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/MetricsQueryEditor/TopField.tsx b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/MetricsQueryEditor/TopField.tsx new file mode 100644 index 0000000000000..5b6c568711c54 --- /dev/null +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/MetricsQueryEditor/TopField.tsx @@ -0,0 +1,41 @@ +import React, { useCallback, useState } from 'react'; +import { Input } from '@grafana/ui'; + +import { Field } from '../Field'; +import { AzureQueryEditorFieldProps } from '../../types'; + +const TopField: React.FC = ({ onQueryChange, query }) => { + const [value, setValue] = useState(query.azureMonitor.top ?? ''); + + // As calling onQueryChange initiates a the datasource refresh, we only want to call it once + // the field loses focus + const handleChange = useCallback((ev: React.FormEvent) => { + if (ev.target instanceof HTMLInputElement) { + setValue(ev.target.value); + } + }, []); + + const handleBlur = useCallback(() => { + onQueryChange({ + ...query, + azureMonitor: { + ...query.azureMonitor, + top: value, + }, + }); + }, [query, value]); + + return ( + + + + ); +}; + +export default TopField; diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/MetricsQueryEditor/index.tsx b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/MetricsQueryEditor/index.tsx new file mode 100644 index 0000000000000..d51921c77502b --- /dev/null +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/MetricsQueryEditor/index.tsx @@ -0,0 +1 @@ +export { default } from './MetricsQueryEditor'; diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/QueryEditor/QueryEditor.test.tsx b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/QueryEditor/QueryEditor.test.tsx new file mode 100644 index 0000000000000..bb5ed5ccd309e --- /dev/null +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/QueryEditor/QueryEditor.test.tsx @@ -0,0 +1,68 @@ +import React from 'react'; +import { render, screen, waitFor } from '@testing-library/react'; +import selectEvent from 'react-select-event'; + +import QueryEditor from './QueryEditor'; + +import mockQuery from '../../__mocks__/query'; +import createMockDatasource from '../../__mocks__/datasource'; +import { AzureQueryType } from '../../types'; + +const variableOptionGroup = { + label: 'Template variables', + options: [], +}; + +describe('Azure Monitor QueryEditor', () => { + it('renders the Metrics query editor when the query type is Metrics', async () => { + const mockDatasource = createMockDatasource(); + render( + {}} + /> + ); + await waitFor(() => expect(screen.getByTestId('azure-monitor-metrics-query-editor')).toBeInTheDocument()); + }); + + it("does not render the Metrics query editor when the query type isn't Metrics", async () => { + const mockDatasource = createMockDatasource(); + const logsMockQuery = { + ...mockQuery, + queryType: AzureQueryType.LogAnalytics, + }; + render( + {}} + /> + ); + await waitFor(() => expect(screen.queryByTestId('azure-monitor-metrics-query-editor')).not.toBeInTheDocument()); + }); + + it('changes the query type when selected', async () => { + const mockDatasource = createMockDatasource(); + const onChange = jest.fn(); + render( + + ); + await waitFor(() => expect(screen.getByTestId('azure-monitor-query-editor')).toBeInTheDocument()); + + const metrics = await screen.findByLabelText('Service'); + await selectEvent.select(metrics, 'Logs'); + + expect(onChange).toHaveBeenCalledWith({ + ...mockQuery, + queryType: AzureQueryType.LogAnalytics, + }); + }); +}); diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/QueryEditor/QueryEditor.tsx b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/QueryEditor/QueryEditor.tsx new file mode 100644 index 0000000000000..cdb3e9cf352fe --- /dev/null +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/QueryEditor/QueryEditor.tsx @@ -0,0 +1,62 @@ +import React from 'react'; +import Datasource from '../../datasource'; +import { AzureMonitorQuery, AzureQueryType, AzureMonitorOption } from '../../types'; +import MetricsQueryEditor from '../MetricsQueryEditor'; +import QueryTypeField from './QueryTypeField'; + +interface BaseQueryEditorProps { + query: AzureMonitorQuery; + datasource: Datasource; + onChange: (newQuery: AzureMonitorQuery) => void; + variableOptionGroup: { label: string; options: AzureMonitorOption[] }; +} + +const QueryEditor: React.FC = ({ query, datasource, onChange }) => { + const subscriptionId = query.subscription || datasource.azureMonitorDatasource.subscriptionId; + const variableOptionGroup = { + label: 'Template Variables', + options: datasource.getVariables().map((v) => ({ label: v, value: v })), + }; + + return ( +
+ + +
+ ); +}; + +interface EditorForQueryTypeProps extends BaseQueryEditorProps { + subscriptionId: string; +} + +const EditorForQueryType: React.FC = ({ + subscriptionId, + query, + datasource, + variableOptionGroup, + onChange, +}) => { + switch (query.queryType) { + case AzureQueryType.AzureMonitor: + return ( + + ); + } + + return null; +}; + +export default QueryEditor; diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/QueryEditor/QueryTypeField.tsx b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/QueryEditor/QueryTypeField.tsx new file mode 100644 index 0000000000000..1c1357b2d2772 --- /dev/null +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/QueryEditor/QueryTypeField.tsx @@ -0,0 +1,45 @@ +import React, { useCallback } from 'react'; +import { Select } from '@grafana/ui'; +import { Field } from '../Field'; +import { AzureMonitorQuery, AzureQueryType } from '../../types'; +import { SelectableValue } from '@grafana/data'; +import { findOption } from '../common'; + +const QUERY_TYPES = [ + { value: AzureQueryType.AzureMonitor, label: 'Metrics' }, + { value: AzureQueryType.LogAnalytics, label: 'Logs' }, + { value: AzureQueryType.ApplicationInsights, label: 'Application Insights' }, + { value: AzureQueryType.InsightsAnalytics, label: 'Insights Analytics' }, +]; + +interface QueryTypeFieldProps { + query: AzureMonitorQuery; + onQueryChange: (newQuery: AzureMonitorQuery) => void; +} + +const QueryTypeField: React.FC = ({ query, onQueryChange }) => { + const handleChange = useCallback( + (change: SelectableValue) => { + change.value && + onQueryChange({ + ...query, + queryType: change.value, + }); + }, + [query] + ); + + return ( + + + + ); +}; + +export default SubscriptionField; diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/common.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/common.ts new file mode 100644 index 0000000000000..9c8c91a054132 --- /dev/null +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/common.ts @@ -0,0 +1,19 @@ +import { rangeUtil } from '@grafana/data'; +import TimegrainConverter from '../time_grain_converter'; +import { AzureMonitorOption } from '../types'; + +// Defaults to returning a fallback option so the UI still shows the value while the API is loading +export const findOption = (options: AzureMonitorOption[], value: string) => + options.find((v) => v.value === value) ?? { value, label: value }; + +export const toOption = (v: { text: string; value: string }) => ({ value: v.value, label: v.text }); + +export function convertTimeGrainsToMs(timeGrains: T[]) { + const allowedTimeGrainsMs: number[] = []; + timeGrains.forEach((tg: any) => { + if (tg.value !== 'auto') { + allowedTimeGrainsMs.push(rangeUtil.intervalToMs(TimegrainConverter.createKbnUnitFromISO8601Duration(tg.value))); + } + }); + return allowedTimeGrainsMs; +} diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/metrics.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/metrics.ts new file mode 100644 index 0000000000000..f2c0783652909 --- /dev/null +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/metrics.ts @@ -0,0 +1,85 @@ +import { useState, useEffect } from 'react'; + +import Datasource from '../datasource'; +import { AzureMonitorQuery } from '../types'; +import { convertTimeGrainsToMs } from './common'; + +export interface MetricMetadata { + aggOptions: Array<{ label: string; value: string }>; + timeGrains: Array<{ label: string; value: string }>; + dimensions: Array<{ label: string; value: string }>; +} + +export function useMetricsMetadata( + datasource: Datasource, + query: AzureMonitorQuery, + subscriptionId: string, + onQueryChange: (newQuery: AzureMonitorQuery) => void +) { + const [metricMetadata, setMetricMetadata] = useState({ + aggOptions: [], + timeGrains: [], + dimensions: [], + }); + + useEffect(() => { + if ( + !( + subscriptionId && + query.azureMonitor.resourceGroup && + query.azureMonitor.metricDefinition && + query.azureMonitor.resourceName && + query.azureMonitor.metricNamespace && + query.azureMonitor.metricName + ) + ) { + return; + } + + datasource + .getMetricMetadata( + subscriptionId, + query.azureMonitor.resourceGroup, + query.azureMonitor.metricDefinition, + query.azureMonitor.resourceName, + query.azureMonitor.metricNamespace, + query.azureMonitor.metricName + ) + .then((metadata) => { + onQueryChange({ + ...query, + azureMonitor: { + ...query.azureMonitor, + aggregation: metadata.primaryAggType, + timeGrain: 'auto', + allowedTimeGrainsMs: convertTimeGrainsToMs(metadata.supportedTimeGrains), + }, + }); + + // TODO: Move the aggregationTypes and timeGrain defaults into `getMetricMetadata` + const aggregations = (metadata.supportedAggTypes || [metadata.primaryAggType]).map((v) => ({ + label: v, + value: v, + })); + + setMetricMetadata({ + aggOptions: aggregations, + timeGrains: metadata.supportedTimeGrains, + dimensions: metadata.dimensions, + }); + }) + .catch((err) => { + // TODO: handle error + console.error(err); + }); + }, [ + subscriptionId, + query.azureMonitor.resourceGroup, + query.azureMonitor.metricDefinition, + query.azureMonitor.resourceName, + query.azureMonitor.metricNamespace, + query.azureMonitor.metricName, + ]); + + return metricMetadata; +} diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/datasource.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/datasource.ts index f6a2cc5c1c24a..2dbed4527c224 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/datasource.ts +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/datasource.ts @@ -13,7 +13,7 @@ import { ScopedVars, } from '@grafana/data'; import { forkJoin, Observable, of } from 'rxjs'; -import { DataSourceWithBackend } from '@grafana/runtime'; +import { DataSourceWithBackend, getTemplateSrv, TemplateSrv } from '@grafana/runtime'; import InsightsAnalyticsDatasource from './insights_analytics/insights_analytics_datasource'; import { migrateMetricsDimensionFilters } from './query_ctrl'; import { map } from 'rxjs/operators'; @@ -27,7 +27,10 @@ export default class Datasource extends DataSourceApi; optionsKey: Record; - constructor(instanceSettings: DataSourceInstanceSettings) { + constructor( + instanceSettings: DataSourceInstanceSettings, + private readonly templateSrv: TemplateSrv = getTemplateSrv() + ) { super(instanceSettings); this.azureMonitorDatasource = new AzureMonitorDatasource(instanceSettings); this.appInsightsDatasource = new AppInsightsDatasource(instanceSettings); @@ -190,15 +193,22 @@ export default class Datasource extends DataSourceApi this.pseudoDatasource[query.queryType].applyTemplateVariables(query, scopedVars) as AzureMonitorQuery ); } + + replaceTemplateVariable(variable: string) { + return this.templateSrv.replace(variable); + } + + getVariables() { + return this.templateSrv.getVariables().map((v) => `$${v.name}`); + } } diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/partials/query.editor.html b/public/app/plugins/datasource/grafana-azure-monitor-datasource/partials/query.editor.html index ec142ab4c49f2..a61b37ebf1a26 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/partials/query.editor.html +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/partials/query.editor.html @@ -1,4 +1,8 @@ - +
@@ -519,3 +523,9 @@

+ + +
+ + +
diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/query_ctrl.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/query_ctrl.ts index 5c0fc6aabd114..619354b585231 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/query_ctrl.ts +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/query_ctrl.ts @@ -7,7 +7,9 @@ import './editor/editor_component'; import { TemplateSrv } from '@grafana/runtime'; import { auto, IPromise } from 'angular'; import { DataFrame, PanelEvents, rangeUtil } from '@grafana/data'; -import { AzureQueryType, AzureMetricQuery } from './types'; +import { AzureQueryType, AzureMetricQuery, AzureMonitorQuery } from './types'; +import { convertTimeGrainsToMs } from './components/common'; +import Datasource from './datasource'; export interface ResultFormat { text: string; @@ -28,6 +30,11 @@ export class AzureMonitorQueryCtrl extends QueryCtrl { { id: AzureQueryType.InsightsAnalytics, label: 'Insights Analytics' }, ]; + // Query types that have been migrated to React + reactQueryEditors = [AzureQueryType.AzureMonitor]; + + // target: AzureMonitorQuery; + target: { // should be: AzureMonitorQuery refId: string; @@ -217,7 +224,7 @@ export class AzureMonitorQueryCtrl extends QueryCtrl { oldAzureTimeGrains.length > 0 && (!this.target.azureMonitor.allowedTimeGrainsMs || this.target.azureMonitor.allowedTimeGrainsMs.length === 0) ) { - this.target.azureMonitor.allowedTimeGrainsMs = this.convertTimeGrainsToMs(oldAzureTimeGrains); + this.target.azureMonitor.allowedTimeGrainsMs = convertTimeGrainsToMs(oldAzureTimeGrains); } if ( @@ -225,7 +232,7 @@ export class AzureMonitorQueryCtrl extends QueryCtrl { this.target.appInsights.timeGrains.length > 0 && (!this.target.appInsights.allowedTimeGrainsMs || this.target.appInsights.allowedTimeGrainsMs.length === 0) ) { - this.target.appInsights.allowedTimeGrainsMs = this.convertTimeGrainsToMs(this.target.appInsights.timeGrains); + this.target.appInsights.allowedTimeGrainsMs = convertTimeGrainsToMs(this.target.appInsights.timeGrains); } } @@ -279,9 +286,9 @@ export class AzureMonitorQueryCtrl extends QueryCtrl { } } - replace(variable: string) { + replace = (variable: string) => { return this.templateSrv.replace(variable, this.panelCtrl.panel.scopedVars); - } + }; onQueryTypeChange() { if (this.target.queryType === 'Azure Log Analytics') { @@ -294,7 +301,18 @@ export class AzureMonitorQueryCtrl extends QueryCtrl { return; } - return this.datasource.azureMonitorDatasource.getSubscriptions().then((subs: any) => { + // assert the type + if (!(this.datasource instanceof Datasource)) { + return; + } + + return this.datasource.azureMonitorDatasource.getSubscriptions().then((subscriptions) => { + // We changed the format in the datasource for the new react stuff, so here we change it back + const subs = subscriptions.map((v) => ({ + text: `${v.text} - ${v.value}`, + value: v.value, + })); + this.subscriptions = subs; if (!this.target.subscription && this.target.queryType === 'Azure Monitor') { this.target.subscription = this.datasource.azureMonitorDatasource.subscriptionId; @@ -475,7 +493,7 @@ export class AzureMonitorQueryCtrl extends QueryCtrl { .then((metadata: any) => { this.target.azureMonitor.aggregation = metadata.primaryAggType; this.target.azureMonitor.timeGrain = 'auto'; - this.target.azureMonitor.allowedTimeGrainsMs = this.convertTimeGrainsToMs(metadata.supportedTimeGrains || []); + this.target.azureMonitor.allowedTimeGrainsMs = convertTimeGrainsToMs(metadata.supportedTimeGrains || []); // HACK: this saves the last metadata values in the panel json ¯\_(ツ)_/¯ const hackState = this.target.azureMonitor as any; @@ -492,6 +510,7 @@ export class AzureMonitorQueryCtrl extends QueryCtrl { .catch(this.handleQueryCtrlError.bind(this)); } + // This is reimplement convertTimeGrainsToMs(timeGrains: Array<{ text: string; value: string }>) { const allowedTimeGrainsMs: number[] = []; timeGrains.forEach((tg: any) => { @@ -683,6 +702,14 @@ export class AzureMonitorQueryCtrl extends QueryCtrl { } this.refresh(); } + + /** + * Receives a full new query object from React and updates it into the Angular controller + */ + handleNewQuery = (newQuery: AzureMonitorQuery) => { + Object.assign(this.target, newQuery); + this.refresh(); + }; } // Modifies the actual query object diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/types.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/types.ts index 86510d6387653..5beed14593dac 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/types.ts +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/types.ts @@ -1,4 +1,5 @@ import { DataQuery, DataSourceJsonData, DataSourceSettings, TableData } from '@grafana/data'; +import Datasource from './datasource'; export type AzureDataSourceSettings = DataSourceSettings; @@ -91,6 +92,30 @@ export interface InsightsAnalyticsQuery { // Azure Monitor API Types +export interface AzureMonitorMetricsMetadataResponse { + value: AzureMonitorMetricMetadataItem[]; +} + +export interface AzureMonitorMetricMetadataItem { + id: string; + resourceId: string; + primaryAggregationType: string; + supportedAggregationTypes: string[]; + name: AzureMonitorLocalizedValue; + dimensions?: AzureMonitorLocalizedValue[]; + metricAvailabilities?: AzureMonitorMetricAvailabilityMetadata[]; +} + +export interface AzureMonitorMetricAvailabilityMetadata { + timeGrain: string; + retention: string; +} + +export interface AzureMonitorLocalizedValue { + value: string; + localizedValue: string; +} + export interface AzureMonitorMetricDefinitionsResponse { data: { value: Array<{ name: string; type: string; location?: string }>; @@ -153,3 +178,17 @@ export interface AzureLogsTableColumn { text: string; type: string; } + +export interface AzureMonitorOption { + label: string; + value: T; +} + +export interface AzureQueryEditorFieldProps { + query: AzureMonitorQuery; + datasource: Datasource; + subscriptionId: string; + variableOptionGroup: { label: string; options: AzureMonitorOption[] }; + + onQueryChange: (newQuery: AzureMonitorQuery) => void; +}