diff --git a/pkg/tsdb/azuremonitor/metrics/azuremonitor-datasource.go b/pkg/tsdb/azuremonitor/metrics/azuremonitor-datasource.go index 92c3734e420..7833719a18f 100644 --- a/pkg/tsdb/azuremonitor/metrics/azuremonitor-datasource.go +++ b/pkg/tsdb/azuremonitor/metrics/azuremonitor-datasource.go @@ -95,6 +95,7 @@ func (e *AzureMonitorDatasource) buildQueries(queries []backend.DataQuery, dsInf timeGrain := azJSONModel.TimeGrain timeGrains := azJSONModel.AllowedTimeGrainsMs + if timeGrain == "auto" { timeGrain, err = azTime.SetAutoTimeGrain(query.Interval.Milliseconds(), timeGrains) if err != nil { diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/MetricsQueryEditor/asyncDataHooks.test.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/MetricsQueryEditor/asyncDataHooks.test.ts new file mode 100644 index 00000000000..8c665df5437 --- /dev/null +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/MetricsQueryEditor/asyncDataHooks.test.ts @@ -0,0 +1,70 @@ +import { renderHook } from '@testing-library/react-hooks'; + +import { useAsyncState } from './dataHooks'; + +interface WaitableMock extends jest.Mock { + waitToBeCalled(): Promise; +} + +const WAIT_OPTIONS = { + timeout: 1000, +}; + +function createWaitableMock() { + let resolve: Function; + + const mock = jest.fn() as WaitableMock; + mock.mockImplementation(() => { + resolve && resolve(); + }); + + mock.waitToBeCalled = () => { + return new Promise((_resolve) => (resolve = _resolve)); + }; + + return mock; +} + +describe('AzureMonitor: useAsyncState', () => { + const MOCKED_RANDOM_VALUE = 0.42069; + + beforeEach(() => { + jest.spyOn(global.Math, 'random').mockReturnValue(MOCKED_RANDOM_VALUE); + }); + + afterEach(() => { + jest.spyOn(global.Math, 'random').mockRestore(); + }); + + it('should return data from an async function', async () => { + const apiCall = () => Promise.resolve(['a', 'b', 'c']); + const setError = jest.fn(); + + const { result, waitForNextUpdate } = renderHook(() => useAsyncState(apiCall, setError, [])); + await waitForNextUpdate(WAIT_OPTIONS); + + expect(result.current).toEqual(['a', 'b', 'c']); + }); + + it('should report errors through setError', async () => { + const error = new Error(); + const apiCall = () => Promise.reject(error); + const setError = createWaitableMock(); + + const { result, waitForNextUpdate } = renderHook(() => useAsyncState(apiCall, setError, [])); + await Promise.race([waitForNextUpdate(WAIT_OPTIONS), setError.waitToBeCalled()]); + + expect(result.current).toEqual([]); + expect(setError).toHaveBeenCalledWith(MOCKED_RANDOM_VALUE, error); + }); + + it('should clear the error once the request is successful', async () => { + const apiCall = () => Promise.resolve(['a', 'b', 'c']); + const setError = createWaitableMock(); + + const { waitForNextUpdate } = renderHook(() => useAsyncState(apiCall, setError, [])); + await Promise.race([waitForNextUpdate(), setError.waitToBeCalled()]); + + expect(setError).toHaveBeenCalledWith(MOCKED_RANDOM_VALUE, undefined); + }); +}); diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/MetricsQueryEditor/dataHooks.test.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/MetricsQueryEditor/dataHooks.test.ts index e26863e4a63..54971a43492 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/MetricsQueryEditor/dataHooks.test.ts +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/MetricsQueryEditor/dataHooks.test.ts @@ -6,8 +6,10 @@ import { AzureMetricQuery, AzureMonitorOption, AzureMonitorQuery, AzureQueryType import { DataHook, + MetricMetadata, + MetricsMetadataHook, updateSubscriptions, - useAsyncState, + useMetricMetadata, useMetricNames, useMetricNamespaces, useResourceGroups, @@ -16,78 +18,15 @@ import { useSubscriptions, } from './dataHooks'; -interface WaitableMock extends jest.Mock { - waitToBeCalled(): Promise; -} - const WAIT_OPTIONS = { timeout: 1000, }; -function createWaitableMock() { - let resolve: Function; - - const mock = jest.fn() as WaitableMock; - mock.mockImplementation(() => { - resolve && resolve(); - }); - - mock.waitToBeCalled = () => { - return new Promise((_resolve) => (resolve = _resolve)); - }; - - return mock; -} - const opt = (text: string, value: string) => ({ text, value }); -describe('AzureMonitor: useAsyncState', () => { - const MOCKED_RANDOM_VALUE = 0.42069; - - beforeEach(() => { - jest.spyOn(global.Math, 'random').mockReturnValue(MOCKED_RANDOM_VALUE); - }); - - afterEach(() => { - jest.spyOn(global.Math, 'random').mockRestore(); - }); - - it('should return data from an async function', async () => { - const apiCall = () => Promise.resolve(['a', 'b', 'c']); - const setError = jest.fn(); - - const { result, waitForNextUpdate } = renderHook(() => useAsyncState(apiCall, setError, [])); - await waitForNextUpdate(); - - expect(result.current).toEqual(['a', 'b', 'c']); - }); - - it('should report errors through setError', async () => { - const error = new Error(); - const apiCall = () => Promise.reject(error); - const setError = createWaitableMock(); - - const { result, waitForNextUpdate } = renderHook(() => useAsyncState(apiCall, setError, [])); - await Promise.race([waitForNextUpdate(), setError.waitToBeCalled()]); - - expect(result.current).toEqual([]); - expect(setError).toHaveBeenCalledWith(MOCKED_RANDOM_VALUE, error); - }); - - it('should clear the error once the request is successful', async () => { - const apiCall = () => Promise.resolve(['a', 'b', 'c']); - const setError = createWaitableMock(); - - const { waitForNextUpdate } = renderHook(() => useAsyncState(apiCall, setError, [])); - await Promise.race([waitForNextUpdate(), setError.waitToBeCalled()]); - - expect(setError).toHaveBeenCalledWith(MOCKED_RANDOM_VALUE, undefined); - }); -}); - interface TestScenario { name: string; - hook: DataHook; + hook: DataHook | MetricsMetadataHook; // For convenience, only need to define the azureMonitor part of the query for some tests emptyQueryPartial: AzureMetricQuery; @@ -95,7 +34,7 @@ interface TestScenario { topLevelCustomProperties?: Partial; expectedCustomPropertyResults?: Array>; - expectedOptions: AzureMonitorOption[]; + expectedOptions: AzureMonitorOption[] | MetricMetadata; } describe('AzureMonitor: metrics dataHooks', () => { @@ -319,7 +258,27 @@ describe('AzureMonitor: metrics dataHooks', () => { datasource.getMetricNamespaces = jest .fn() .mockResolvedValue([opt('Compute Virtual Machine', 'azure/vmc'), opt('Database NS', 'azure/dbns')]); + + const getMetricMetadata = jest.fn().mockResolvedValue({ + primaryAggType: 'Average', + supportedAggTypes: ['Average'], + supportedTimeGrains: [ + { label: 'Auto', value: 'auto' }, + { label: '1 minute', value: 'PT1M' }, + { label: '5 minutes', value: 'PT5M' }, + { label: '15 minutes', value: 'PT15M' }, + { label: '30 minutes', value: 'PT30M' }, + { label: '1 hour', value: 'PT1H' }, + { label: '6 hours', value: 'PT6H' }, + { label: '12 hours', value: 'PT12H' }, + { label: '1 day', value: 'P1D' }, + ], + dimensions: [], + }); + + datasource.getMetricMetadata = jest.fn().mockImplementation(getMetricMetadata); }); + describe.each(testTable)('scenario %#: $name', (scenario) => { it('returns values', async () => { const query = { @@ -344,6 +303,60 @@ describe('AzureMonitor: metrics dataHooks', () => { expect(result.current).toEqual(scenario.expectedCustomPropertyResults); }); }); + + describe('useMetricsMetadataHook', () => { + const metricsMetadataConfig = { + name: 'useMetricMetadata', + hook: useMetricMetadata, + emptyQueryPartial: { + resourceGroup: 'web-app-development', + metricDefinition: 'azure/vm', + resourceName: 'web-server', + metricNamespace: 'azure/vm', + subscription: 'test-sub', + metricName: 'Average CPU', + }, + customProperties: {}, + expectedOptions: { + aggOptions: [{ label: 'Average', value: 'Average' }], + timeGrains: [ + { label: 'Auto', value: 'auto' }, + { label: '1 minute', value: 'PT1M' }, + { label: '5 minutes', value: 'PT5M' }, + { label: '15 minutes', value: 'PT15M' }, + { label: '30 minutes', value: 'PT30M' }, + { label: '1 hour', value: 'PT1H' }, + { label: '6 hours', value: 'PT6H' }, + { label: '12 hours', value: 'PT12H' }, + { label: '1 day', value: 'P1D' }, + ], + dimensions: [], + isLoading: false, + supportedAggTypes: ['Average'], + primaryAggType: 'Average', + }, + }; + + it('returns values', async () => { + const query = { + ...bareQuery, + azureMonitor: metricsMetadataConfig.emptyQueryPartial, + }; + const { result, waitForNextUpdate } = renderHook(() => metricsMetadataConfig.hook(query, datasource, onChange)); + await waitForNextUpdate(WAIT_OPTIONS); + + expect(result.current).toEqual(metricsMetadataConfig.expectedOptions); + expect(onChange).toHaveBeenCalledWith({ + ...query, + azureMonitor: { + ...query.azureMonitor, + aggregation: result.current.primaryAggType, + timeGrain: 'auto', + allowedTimeGrainsMs: [60_000, 300_000, 900_000, 1_800_000, 3_600_000, 21_600_000, 43_200_000, 86_400_000], + }, + }); + }); + }); }); describe('AzureMonitor: updateSubscriptions', () => { diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/MetricsQueryEditor/dataHooks.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/MetricsQueryEditor/dataHooks.ts index edb45017e10..93001c98120 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/MetricsQueryEditor/dataHooks.ts +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/MetricsQueryEditor/dataHooks.ts @@ -1,6 +1,9 @@ import { useEffect, useMemo, useState } from 'react'; +import { rangeUtil } from '@grafana/data'; + import Datasource from '../../datasource'; +import TimegrainConverter from '../../time_grain_converter'; import { AzureMonitorErrorish, AzureMonitorOption, AzureMonitorQuery } from '../../types'; import { hasOption, toOption } from '../../utils/common'; @@ -49,6 +52,11 @@ export function useAsyncState(asyncFn: () => Promise, setError: Function, return finalValue; } +export type MetricsMetadataHook = ( + query: AzureMonitorQuery, + datasource: Datasource, + onChange: OnChangeFn +) => MetricMetadata; export const updateSubscriptions = ( query: AzureMonitorQuery, @@ -248,7 +256,6 @@ export const useMetricMetadata = (query: AzureMonitorQuery, datasource: Datasour label: v, value: v, })); - setMetricMetadata({ aggOptions: aggregations, timeGrains: metadata.supportedTimeGrains, @@ -272,6 +279,11 @@ export const useMetricMetadata = (query: AzureMonitorQuery, datasource: Datasour ...query.azureMonitor, aggregation: newAggregation, timeGrain: newTimeGrain, + allowedTimeGrainsMs: metricMetadata.timeGrains + .filter((timeGrain) => timeGrain.value !== 'auto') + .map((timeGrain) => + rangeUtil.intervalToMs(TimegrainConverter.createKbnUnitFromISO8601Duration(timeGrain.value)) + ), }, }); } diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/types/query.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/types/query.ts index 3011ab9db45..280a88700b2 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/types/query.ts +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/types/query.ts @@ -50,13 +50,11 @@ export interface AzureMetricQuery { dimensionFilters?: AzureMetricDimension[]; alias?: string; top?: string; + allowedTimeGrainsMs?: number[]; /** @deprecated */ timeGrainUnit?: string; - /** @deprecated Remove this once angular is removed */ - allowedTimeGrainsMs?: number[]; - /** @deprecated This property was migrated to dimensionFilters and should only be accessed in the migration */ dimension?: string;