AzureMonitor: Fix auto-selection of time-grain for metrics. (#49278) (#49420)

* AzureMonitor: Fix auto-selection of time-grain for metrics. (#49278)

* Update query editor to fix auto time-grain selection

* Update new query editor to fix auto time-grain selection

* Remove log and fix lint issues

* Add test for useMetricMetadata

- Add necessary types

* More test updates

- Update old dataHooks test
- Ensure query changes

Co-authored-by: Kevin Yu <kevinwcyu@users.noreply.github.com>
Co-authored-by: Andres Martinez Gotor <andres.mgotor@gmail.com>
Co-authored-by: Erik Sundell <erik.sundell87@gmail.com>
(cherry picked from commit 2780651ea8)

# Conflicts:
#	public/app/plugins/datasource/grafana-azure-monitor-datasource/components/MetricsQueryEditor/dataHooks.test.ts
#	public/app/plugins/datasource/grafana-azure-monitor-datasource/components/MetricsQueryEditor/dataHooks.ts
#	public/app/plugins/datasource/grafana-azure-monitor-datasource/components/NewMetricsQueryEditor/dataHooks.test.ts
#	public/app/plugins/datasource/grafana-azure-monitor-datasource/components/NewMetricsQueryEditor/dataHooks.ts

* Update mocks appropriately

* Separate asyncState tests

* Fix lint error
This commit is contained in:
Andreas Christou
2022-05-24 11:16:11 +01:00
committed by GitHub
parent f34627b068
commit 21a9ccbb3e
5 changed files with 164 additions and 70 deletions
@@ -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 {
@@ -0,0 +1,70 @@
import { renderHook } from '@testing-library/react-hooks';
import { useAsyncState } from './dataHooks';
interface WaitableMock extends jest.Mock<any, any> {
waitToBeCalled(): Promise<unknown>;
}
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);
});
});
@@ -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<any, any> {
waitToBeCalled(): Promise<unknown>;
}
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<AzureMonitorQuery>;
expectedCustomPropertyResults?: Array<AzureMonitorOption<string>>;
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', () => {
@@ -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<T>(asyncFn: () => Promise<T>, 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))
),
},
});
}
@@ -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;