* 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:
@@ -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 {
|
||||
|
||||
+70
@@ -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);
|
||||
});
|
||||
});
|
||||
+79
-66
@@ -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', () => {
|
||||
|
||||
+13
-1
@@ -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;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user