From e530d66275138a075bc0e170c767716b2a73a340 Mon Sep 17 00:00:00 2001 From: Josh Hunt Date: Tue, 3 Aug 2021 10:04:04 +0100 Subject: [PATCH] AzureMonitor: Refactor query state and fetching data for Metrics (#37067) * Split query modifications into seperate file * seperate setQueryValue for Logs * wip for centralising api requests * more wip * moved data hooks out into seperate module, added post-request cleanup * metric metadata * , * more cleanup * update tests, fix hasOption to work with grouped variables * update ARG subscriptiopns field * last select stuff * remove todo comment * pr feedback * more pr feedback :) :) * updated comment --- .../ArgQueryEditor/ArgQueryEditor.tsx | 30 +- .../InsightsAnalyticsEditor/index.tsx | 3 +- .../LogsQueryEditor/FormatAsField.tsx | 13 +- .../components/LogsQueryEditor/QueryField.tsx | 25 +- .../LogsQueryEditor/ResourceField.tsx | 9 +- .../LogsQueryEditor/setQueryValue.ts | 31 ++ .../MetricsQueryEditor/AggregationField.tsx | 13 +- .../MetricsQueryEditor/DimensionFields.tsx | 41 +-- .../MetricsQueryEditor/LegendFormatField.tsx | 10 +- .../MetricsQueryEditor/MetricNameField.tsx | 53 +--- .../MetricNamespaceField.tsx | 62 +--- .../MetricsQueryEditor/MetricsQueryEditor.tsx | 24 +- .../ResourceGroupsField.tsx | 53 +--- .../MetricsQueryEditor/ResourceNameField.tsx | 54 +--- .../MetricsQueryEditor/ResourceTypeField.tsx | 50 +--- .../MetricsQueryEditor/TimeGrainField.tsx | 13 +- .../MetricsQueryEditor/TopField.tsx | 10 +- .../MetricsQueryEditor/dataHooks.test.ts | 66 +++++ .../MetricsQueryEditor/dataHooks.ts | 272 ++++++++++++++++++ .../MetricsQueryEditor/setQueryValue.ts | 202 +++++++++++++ .../components/QueryEditor/QueryTypeField.tsx | 3 +- .../components/SubscriptionField.tsx | 43 +-- .../components/metrics.ts | 95 ------ .../datasource.ts | 2 - .../types/query.ts | 8 +- .../types/types.ts | 1 + .../utils/common.test.ts | 42 +++ .../utils/common.ts | 5 +- 28 files changed, 773 insertions(+), 460 deletions(-) create mode 100644 public/app/plugins/datasource/grafana-azure-monitor-datasource/components/LogsQueryEditor/setQueryValue.ts create mode 100644 public/app/plugins/datasource/grafana-azure-monitor-datasource/components/MetricsQueryEditor/dataHooks.test.ts create mode 100644 public/app/plugins/datasource/grafana-azure-monitor-datasource/components/MetricsQueryEditor/dataHooks.ts create mode 100644 public/app/plugins/datasource/grafana-azure-monitor-datasource/components/MetricsQueryEditor/setQueryValue.ts delete mode 100644 public/app/plugins/datasource/grafana-azure-monitor-datasource/components/metrics.ts create mode 100644 public/app/plugins/datasource/grafana-azure-monitor-datasource/utils/common.test.ts diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/ArgQueryEditor/ArgQueryEditor.tsx b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/ArgQueryEditor/ArgQueryEditor.tsx index c5405cfc2da..490af50d0b9 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/ArgQueryEditor/ArgQueryEditor.tsx +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/ArgQueryEditor/ArgQueryEditor.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useEffect, useState, useRef } from 'react'; import { AzureMonitorErrorish, AzureMonitorOption, AzureMonitorQuery } from '../../types'; import Datasource from '../../datasource'; import { InlineFieldRow } from '@grafana/ui'; @@ -14,6 +14,7 @@ interface LogsQueryEditorProps { setError: (source: string, error: AzureMonitorErrorish | undefined) => void; } +const ERROR_SOURCE = 'arg-subscriptions'; const ArgQueryEditor: React.FC = ({ query, datasource, @@ -22,11 +23,38 @@ const ArgQueryEditor: React.FC = ({ onChange, setError, }) => { + const fetchedRef = useRef(false); + const [subscriptions, setSubscriptions] = useState([]); + + useEffect(() => { + if (fetchedRef.current) { + return; + } + + fetchedRef.current = true; + datasource.azureMonitorDatasource + .getSubscriptions() + .then((results) => { + const fetchedSubscriptions = results.map((v) => ({ label: v.text, value: v.value, description: v.value })); + setSubscriptions(fetchedSubscriptions); + setError(ERROR_SOURCE, undefined); + + if (!query.subscriptions?.length && fetchedSubscriptions?.length) { + onChange({ + ...query, + subscriptions: [query.subscription ?? fetchedSubscriptions[0].value], + }); + } + }) + .catch((err) => setError(ERROR_SOURCE, err)); + }, [datasource, onChange, query, setError]); + return (
= ({ query = ({ query, datasource, o Promise.all(promises).then(([schema, { monaco, editor }]) => { const languages = (monaco.languages as unknown) as MonacoLanguages; - languages.kusto.getKustoWorker().then((kusto) => { - const model = editor.getModel(); - if (!model) { - return; - } - kusto(model.uri).then((worker) => { - worker.setSchema(schema, 'https://help.kusto.windows.net', 'Samples'); + languages.kusto + .getKustoWorker() + .then((kusto) => { + const model = editor.getModel(); + return model && kusto(model.uri); + }) + .then((worker) => { + worker?.setSchema(schema, 'https://help.kusto.windows.net', 'Samples'); }); - }); }); }, [datasource.azureLogAnalyticsDatasource, query.azureLogAnalytics?.resource]); @@ -62,13 +63,7 @@ const QueryField: React.FC = ({ query, datasource, o const onChange = useCallback( (newQuery: string) => { - onQueryChange({ - ...query, - azureLogAnalytics: { - ...query.azureLogAnalytics, - query: newQuery, - }, - }); + onQueryChange(setKustoQuery(query, newQuery)); }, [onQueryChange, query] ); diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/LogsQueryEditor/ResourceField.tsx b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/LogsQueryEditor/ResourceField.tsx index f2e9350156f..a7f00a23b05 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/LogsQueryEditor/ResourceField.tsx +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/LogsQueryEditor/ResourceField.tsx @@ -8,6 +8,7 @@ import { Field } from '../Field'; import ResourcePicker from '../ResourcePicker'; import { parseResourceURI } from '../ResourcePicker/utils'; import { Space } from '../Space'; +import { setResource } from './setQueryValue'; function parseResourceDetails(resourceURI: string) { const parsed = parseResourceURI(resourceURI); @@ -39,13 +40,7 @@ const ResourceField: React.FC = ({ query, datasource const handleApply = useCallback( (resourceURI: string | undefined) => { - onQueryChange({ - ...query, - azureLogAnalytics: { - ...query.azureLogAnalytics, - resource: resourceURI, - }, - }); + onQueryChange(setResource(query, resourceURI)); closePicker(); }, [closePicker, onQueryChange, query] diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/LogsQueryEditor/setQueryValue.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/LogsQueryEditor/setQueryValue.ts new file mode 100644 index 00000000000..b8f870b3f5d --- /dev/null +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/LogsQueryEditor/setQueryValue.ts @@ -0,0 +1,31 @@ +import { AzureMonitorQuery } from '../../types'; + +export function setKustoQuery(query: AzureMonitorQuery, kustoQuery: string): AzureMonitorQuery { + return { + ...query, + azureLogAnalytics: { + ...query.azureLogAnalytics, + query: kustoQuery, + }, + }; +} + +export function setFormatAs(query: AzureMonitorQuery, formatAs: string): AzureMonitorQuery { + return { + ...query, + azureLogAnalytics: { + ...query.azureLogAnalytics, + resultFormat: formatAs, + }, + }; +} + +export function setResource(query: AzureMonitorQuery, resourceURI: string | undefined): AzureMonitorQuery { + return { + ...query, + azureLogAnalytics: { + ...query.azureLogAnalytics, + resource: resourceURI, + }, + }; +} 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 index 75b1fbfdbdf..740e7728608 100644 --- 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 @@ -3,8 +3,8 @@ import { Select } from '@grafana/ui'; import { SelectableValue } from '@grafana/data'; import { Field } from '../Field'; -import { findOption } from '../../utils/common'; import { AzureQueryEditorFieldProps, AzureMonitorOption } from '../../types'; +import { setAggregation } from './setQueryValue'; interface AggregationFieldProps extends AzureQueryEditorFieldProps { aggregationOptions: AzureMonitorOption[]; @@ -24,13 +24,8 @@ const AggregationField: React.FC = ({ return; } - onQueryChange({ - ...query, - azureMonitor: { - ...query.azureMonitor, - aggregation: change.value, - }, - }); + const newQuery = setAggregation(query, change.value); + onQueryChange(newQuery); }, [onQueryChange, query] ); @@ -44,7 +39,7 @@ const AggregationField: React.FC = ({ onFieldChange(index, 'dimension', v.value ?? '')} width={38} 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 index 5bb467562fd..4b8522819f6 100644 --- 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 @@ -3,6 +3,7 @@ import { Input } from '@grafana/ui'; import { Field } from '../Field'; import { AzureQueryEditorFieldProps } from '../../types'; +import { setLegendAlias } from './setQueryValue'; const LegendFormatField: React.FC = ({ onQueryChange, query }) => { const [value, setValue] = useState(query.azureMonitor?.alias ?? ''); @@ -16,13 +17,8 @@ const LegendFormatField: React.FC = ({ onQueryChange }, []); const handleBlur = useCallback(() => { - onQueryChange({ - ...query, - azureMonitor: { - ...query.azureMonitor, - alias: value, - }, - }); + const newQuery = setLegendAlias(query, value); + onQueryChange(newQuery); }, [onQueryChange, query, value]); return ( 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 index 7243ce8e025..45e1090a29f 100644 --- 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 @@ -1,56 +1,24 @@ -import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import React, { useCallback, useMemo } from 'react'; import { Select } from '@grafana/ui'; import { SelectableValue } from '@grafana/data'; import { Field } from '../Field'; -import { findOption, toOption } from '../../utils/common'; import { AzureQueryEditorFieldProps, AzureMonitorOption } from '../../types'; +import { setMetricName } from './setQueryValue'; -const ERROR_SOURCE = 'metrics-metricname'; -const MetricName: React.FC = ({ - query, - datasource, - subscriptionId, - variableOptionGroup, - onQueryChange, - setError, -}) => { - const [metricNames, setMetricNames] = useState([]); - const [isLoading, setIsLoading] = useState(false); - - useEffect(() => { - const { resourceGroup, metricDefinition, resourceName, metricNamespace } = query.azureMonitor ?? {}; - if (!(subscriptionId && resourceGroup && metricDefinition && resourceName && metricNamespace)) { - metricNames.length > 0 && setMetricNames([]); - return; - } - setIsLoading(true); - - datasource - .getMetricNames(subscriptionId, resourceGroup, metricDefinition, resourceName, metricNamespace) - .then((results) => { - setMetricNames(results.map(toOption)); - setIsLoading(false); - }) - .catch((err) => { - setError(ERROR_SOURCE, err); - setIsLoading(false); - }); - }, [datasource, metricNames.length, query.azureMonitor, setError, subscriptionId]); +interface MetricNameProps extends AzureQueryEditorFieldProps { + metricNames: AzureMonitorOption[]; +} +const MetricNameField: React.FC = ({ metricNames, query, variableOptionGroup, onQueryChange }) => { const handleChange = useCallback( (change: SelectableValue) => { if (!change.value) { return; } - onQueryChange({ - ...query, - azureMonitor: { - ...query.azureMonitor, - metricName: change.value, - }, - }); + const newQuery = setMetricName(query, change.value); + onQueryChange(newQuery); }, [onQueryChange, query] ); @@ -61,14 +29,13 @@ const MetricName: React.FC = ({ ); 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 index 423bd12e17d..bfbcd9091ff 100644 --- 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 @@ -2,7 +2,6 @@ import React from 'react'; import Datasource from '../../datasource'; import { AzureMonitorQuery, AzureMonitorOption, AzureMonitorErrorish } from '../../types'; -import { useMetricsMetadata } from '../metrics'; import SubscriptionField from '../SubscriptionField'; import MetricNamespaceField from './MetricNamespaceField'; import ResourceTypeField from './ResourceTypeField'; @@ -15,6 +14,15 @@ import DimensionFields from './DimensionFields'; import TopField from './TopField'; import LegendFormatField from './LegendFormatField'; import { InlineFieldRow } from '@grafana/ui'; +import { + useMetricNames, + useMetricNamespaces, + useResourceGroups, + useResourceNames, + useResourceTypes, + useSubscriptions, + useMetricMetadata, +} from './dataHooks'; interface MetricsQueryEditorProps { query: AzureMonitorQuery; @@ -33,12 +41,19 @@ const MetricsQueryEditor: React.FC = ({ onChange, setError, }) => { - const metricsMetadata = useMetricsMetadata(datasource, query, subscriptionId, onChange); + const metricsMetadata = useMetricMetadata(query, datasource, onChange); + const subscriptions = useSubscriptions(query, datasource, onChange, setError); + const resourceGroups = useResourceGroups(query, datasource, onChange, setError); + const resourceTypes = useResourceTypes(query, datasource, onChange, setError); + const resourceNames = useResourceNames(query, datasource, onChange, setError); + const metricNames = useMetricNames(query, datasource, onChange, setError); + const metricNamespaces = useMetricNamespaces(query, datasource, onChange, setError); return (
= ({ /> = ({ = ({ setError={setError} /> = ({ = ({ setError={setError} /> = ({ +interface ResourceGroupsFieldProps extends AzureQueryEditorFieldProps { + resourceGroups: AzureMonitorOption[]; +} + +const ResourceGroupsField: React.FC = ({ query, - datasource, - subscriptionId, + resourceGroups, variableOptionGroup, onQueryChange, setError, }) => { - const [resourceGroups, setResourceGroups] = useState([]); - - useEffect(() => { - if (!subscriptionId) { - resourceGroups.length > 0 && setResourceGroups([]); - return; - } - - datasource - .getResourceGroups(subscriptionId) - .then((results) => { - setResourceGroups(results.map(toOption)); - setError(ERROR_SOURCE, undefined); - }) - .catch((err) => setError(ERROR_SOURCE, err)); - }, [datasource, resourceGroups.length, setError, subscriptionId]); - const handleChange = useCallback( (change: SelectableValue) => { - if (!change.value) { - return; - } - - onQueryChange({ - ...query, - azureMonitor: { - ...query.azureMonitor, - resourceGroup: change.value, - metricDefinition: undefined, - resourceName: undefined, - metricNamespace: undefined, - metricName: undefined, - aggregation: undefined, - timeGrain: '', - dimensionFilters: [], - }, - }); + const newQuery = setResourceGroup(query, change.value); + onQueryChange(newQuery); }, [onQueryChange, query] ); @@ -62,7 +31,7 @@ const ResourceGroupsField: React.FC = ({ = ({ +interface NamespaceFieldProps extends AzureQueryEditorFieldProps { + resourceTypes: AzureMonitorOption[]; +} + +const NamespaceField: React.FC = ({ + resourceTypes, query, - datasource, - subscriptionId, variableOptionGroup, onQueryChange, - setError, }) => { - const [namespaces, setNamespaces] = useState([]); - - useEffect(() => { - const { resourceGroup } = query.azureMonitor ?? {}; - - if (!(subscriptionId && resourceGroup)) { - namespaces.length && setNamespaces([]); - return; - } - - datasource - .getMetricDefinitions(subscriptionId, resourceGroup) - .then((results) => setNamespaces(results.map(toOption))) - .catch((err) => setError(ERROR_SOURCE, err)); - }, [datasource, namespaces.length, query.azureMonitor, setError, subscriptionId]); - const handleChange = useCallback( (change: SelectableValue) => { if (!change.value) { return; } - onQueryChange({ - ...query, - azureMonitor: { - ...query.azureMonitor, - metricDefinition: change.value, - resourceName: undefined, - metricNamespace: undefined, - metricName: undefined, - aggregation: undefined, - timeGrain: '', - dimensionFilters: [], - }, - }); + const newQuery = setResourceType(query, change.value); + onQueryChange(newQuery); }, [onQueryChange, query] ); - const options = useMemo(() => [...namespaces, variableOptionGroup], [namespaces, variableOptionGroup]); + const options = useMemo(() => [...resourceTypes, variableOptionGroup], [resourceTypes, variableOptionGroup]); return ( {/* It's expected that the label reads Resource type but the property is metricDefinition */} = ({ onQueryChange, query }) => { const [value, setValue] = useState(query.azureMonitor?.top ?? ''); @@ -16,13 +17,8 @@ const TopField: React.FC = ({ onQueryChange, query } }, []); const handleBlur = useCallback(() => { - onQueryChange({ - ...query, - azureMonitor: { - ...query.azureMonitor, - top: value, - }, - }); + const newQuery = setTop(query, value); + onQueryChange(newQuery); }, [onQueryChange, query, value]); return ( 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 new file mode 100644 index 00000000000..1a08d8c406d --- /dev/null +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/MetricsQueryEditor/dataHooks.test.ts @@ -0,0 +1,66 @@ +import { renderHook } from '@testing-library/react-hooks'; + +import { useAsyncState } from './dataHooks'; + +interface WaitableMock extends jest.Mock { + waitToBeCalled(): Promise; +} + +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(); + + 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); + }); +}); 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 new file mode 100644 index 00000000000..122163c2636 --- /dev/null +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/MetricsQueryEditor/dataHooks.ts @@ -0,0 +1,272 @@ +import { useState, useEffect, useMemo } from 'react'; +import Datasource from '../../datasource'; +import { AzureMonitorErrorish, AzureMonitorOption, AzureMonitorQuery } from '../../types'; +import { hasOption, toOption } from '../../utils/common'; +import { + setMetricName, + setMetricNamespace, + setResourceGroup, + setResourceName, + setResourceType, + setSubscriptionID, +} from './setQueryValue'; + +export interface MetricMetadata { + aggOptions: AzureMonitorOption[]; + timeGrains: AzureMonitorOption[]; + dimensions: AzureMonitorOption[]; + isLoading: boolean; + + // These two properties are only used within the hook, and not elsewhere + supportedAggTypes: string[]; + primaryAggType: string | undefined; +} + +type SetErrorFn = (source: string, error: AzureMonitorErrorish | undefined) => void; +type OnChangeFn = (newQuery: AzureMonitorQuery) => void; + +type DataHook = ( + query: AzureMonitorQuery, + datasource: Datasource, + onChange: OnChangeFn, + setError: SetErrorFn +) => AzureMonitorOption[]; + +export function useAsyncState(asyncFn: () => Promise, setError: Function, dependencies: unknown[]) { + // Use the lazy initial state functionality of useState to assign a random ID to the API call + // to track where errors come from. See useLastError. + const [errorSource] = useState(() => Math.random()); + const [value, setValue] = useState(); + + const finalValue = useMemo(() => value ?? [], [value]); + + useEffect(() => { + asyncFn() + .then((results) => { + setValue(results); + setError(errorSource, undefined); + }) + .catch((err) => { + setError(errorSource, err); + }); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, dependencies); + + return finalValue; +} + +export const useSubscriptions: DataHook = (query, datasource, onChange, setError) => { + const { subscription } = query; + const defaultSubscription = datasource.azureMonitorDatasource.defaultSubscriptionId; + + return useAsyncState( + async () => { + const results = await datasource.azureMonitorDatasource.getSubscriptions(); + const options = results.map((v) => ({ label: v.text, value: v.value, description: v.value })); + + if (!subscription && defaultSubscription && hasOption(options, defaultSubscription)) { + onChange(setSubscriptionID(query, defaultSubscription)); + } else if ((!subscription && options.length) || options.length === 1) { + onChange(setSubscriptionID(query, options[0].value)); + } + + return options; + }, + setError, + [] + ); +}; + +export const useResourceGroups: DataHook = (query, datasource, onChange, setError) => { + const { subscription } = query; + const { resourceGroup } = query.azureMonitor ?? {}; + + return useAsyncState( + async () => { + if (!subscription) { + return; + } + + const results = await datasource.getResourceGroups(subscription); + const options = results.map(toOption); + + if (resourceGroup && !hasOption(options, resourceGroup)) { + onChange(setResourceGroup(query, undefined)); + } + + return options; + }, + setError, + [subscription] + ); +}; + +export const useResourceTypes: DataHook = (query, datasource, onChange, setError) => { + const { subscription } = query; + const { resourceGroup, metricDefinition } = query.azureMonitor ?? {}; + + return useAsyncState( + async () => { + if (!(subscription && resourceGroup)) { + return; + } + + const results = await datasource.getMetricDefinitions(subscription, resourceGroup); + const options = results.map(toOption); + + if (metricDefinition && !hasOption(options, metricDefinition)) { + onChange(setResourceType(query, undefined)); + } + + return options; + }, + setError, + [subscription, resourceGroup] + ); +}; + +export const useResourceNames: DataHook = (query, datasource, onChange, setError) => { + const { subscription } = query; + const { resourceGroup, metricDefinition, resourceName } = query.azureMonitor ?? {}; + + return useAsyncState( + async () => { + if (!(subscription && resourceGroup && metricDefinition)) { + return; + } + + const results = await datasource.getResourceNames(subscription, resourceGroup, metricDefinition); + const options = results.map(toOption); + + if (resourceName && !hasOption(options, resourceName)) { + onChange(setResourceName(query, undefined)); + } + + return options; + }, + setError, + [subscription, resourceGroup, metricDefinition] + ); +}; + +export const useMetricNamespaces: DataHook = (query, datasource, onChange, setError) => { + const { subscription } = query; + const { resourceGroup, metricDefinition, resourceName, metricNamespace } = query.azureMonitor ?? {}; + + const metricNamespaces = useAsyncState( + async () => { + if (!(subscription && resourceGroup && metricDefinition && resourceName)) { + return; + } + + const results = await datasource.getMetricNamespaces(subscription, resourceGroup, metricDefinition, resourceName); + const options = results.map(toOption); + + // Do some cleanup of the query state if need be + if ((!metricNamespace && options.length) || options.length === 1) { + onChange(setMetricNamespace(query, options[0].value)); + } else if (options[0] && metricNamespace && !hasOption(options, metricNamespace)) { + onChange(setMetricNamespace(query, options[0].value)); + } + + return options; + }, + setError, + [subscription, resourceGroup, metricDefinition, resourceName] + ); + + return metricNamespaces; +}; + +export const useMetricNames: DataHook = (query, datasource, onChange, setError) => { + const { subscription } = query; + const { resourceGroup, metricDefinition, resourceName, metricNamespace, metricName } = query.azureMonitor ?? {}; + + return useAsyncState( + async () => { + if (!(subscription && resourceGroup && metricDefinition && resourceName && metricNamespace)) { + return; + } + + const results = await datasource.getMetricNames( + subscription, + resourceGroup, + metricDefinition, + resourceName, + metricNamespace + ); + + const options = results.map(toOption); + + if (metricName && !hasOption(options, metricName)) { + onChange(setMetricName(query, undefined)); + } + + return options; + }, + setError, + [subscription, resourceGroup, metricDefinition, resourceName, metricNamespace] + ); +}; + +export const useMetricMetadata = (query: AzureMonitorQuery, datasource: Datasource, onChange: OnChangeFn) => { + const [metricMetadata, setMetricMetadata] = useState({ + aggOptions: [], + timeGrains: [], + dimensions: [], + isLoading: false, + supportedAggTypes: [], + primaryAggType: undefined, + }); + + const { subscription } = query; + const { resourceGroup, metricDefinition, resourceName, metricNamespace, metricName, aggregation, timeGrain } = + query.azureMonitor ?? {}; + + // Fetch new metric metadata when the fields change + useEffect(() => { + if (!(subscription && resourceGroup && metricDefinition && resourceName && metricNamespace && metricName)) { + return; + } + + datasource + .getMetricMetadata(subscription, resourceGroup, metricDefinition, resourceName, metricNamespace, metricName) + .then((metadata) => { + // 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, + isLoading: false, + supportedAggTypes: metadata.supportedAggTypes ?? [], + primaryAggType: metadata.primaryAggType, + }); + }); + }, [datasource, subscription, resourceGroup, metricDefinition, resourceName, metricNamespace, metricName]); + + // Update the query state in response to the meta data changing + useEffect(() => { + const aggregationIsValid = aggregation && metricMetadata.supportedAggTypes.includes(aggregation); + + const newAggregation = aggregationIsValid ? aggregation : metricMetadata.primaryAggType; + const newTimeGrain = timeGrain || 'auto'; + + if (newAggregation !== aggregation || newTimeGrain !== timeGrain) { + onChange({ + ...query, + azureMonitor: { + ...query.azureMonitor, + aggregation: newAggregation, + timeGrain: newTimeGrain, + }, + }); + } + }, [onChange, metricMetadata, aggregation, timeGrain, query]); + + return metricMetadata; +}; diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/MetricsQueryEditor/setQueryValue.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/MetricsQueryEditor/setQueryValue.ts new file mode 100644 index 00000000000..0c9506a8805 --- /dev/null +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/MetricsQueryEditor/setQueryValue.ts @@ -0,0 +1,202 @@ +import { AzureMetricDimension, AzureMonitorQuery } from '../../types'; + +export function setSubscriptionID(query: AzureMonitorQuery, subscriptionID: string): AzureMonitorQuery { + if (query.subscription === subscriptionID) { + return query; + } + + return { + ...query, + subscription: subscriptionID, + azureMonitor: { + ...query.azureMonitor, + resourceGroup: undefined, + }, + }; +} + +export function setResourceGroup(query: AzureMonitorQuery, resourceGroup: string | undefined): AzureMonitorQuery { + if (query.azureMonitor?.resourceGroup === resourceGroup) { + return query; + } + + return { + ...query, + azureMonitor: { + ...query.azureMonitor, + resourceGroup: resourceGroup, + resourceName: undefined, + }, + }; +} + +// In the query as "metricDefinition" for some reason +export function setResourceType(query: AzureMonitorQuery, resourceType: string | undefined): AzureMonitorQuery { + if (query.azureMonitor?.metricDefinition === resourceType) { + return query; + } + + const newQuery = { + ...query, + azureMonitor: { + ...query.azureMonitor, + metricDefinition: resourceType, + resourceName: undefined, + metricNamespace: undefined, + metricName: undefined, + }, + }; + + return newQuery; +} + +export function setResourceName(query: AzureMonitorQuery, resourceName: string | undefined): AzureMonitorQuery { + if (query.azureMonitor?.resourceName === resourceName) { + return query; + } + + return { + ...query, + azureMonitor: { + ...query.azureMonitor, + resourceName: resourceName, + }, + }; +} + +export function setMetricNamespace(query: AzureMonitorQuery, metricNamespace: string | undefined): AzureMonitorQuery { + if (query.azureMonitor?.metricNamespace === metricNamespace) { + return query; + } + + return { + ...query, + azureMonitor: { + ...query.azureMonitor, + metricNamespace: metricNamespace, + metricName: undefined, + }, + }; +} + +export function setMetricName(query: AzureMonitorQuery, metricName: string | undefined): AzureMonitorQuery { + if (query.azureMonitor?.metricName === metricName) { + return query; + } + + return { + ...query, + azureMonitor: { + ...query.azureMonitor, + metricName: metricName, + }, + }; +} + +export function setAggregation(query: AzureMonitorQuery, aggregation: string): AzureMonitorQuery { + if (query.azureMonitor?.aggregation === aggregation) { + return query; + } + + return { + ...query, + azureMonitor: { + ...query.azureMonitor, + aggregation: aggregation, + }, + }; +} + +export function setTimeGrain(query: AzureMonitorQuery, timeGrain: string): AzureMonitorQuery { + if (query.azureMonitor?.timeGrain === timeGrain) { + return query; + } + + return { + ...query, + azureMonitor: { + ...query.azureMonitor, + timeGrain: timeGrain, + }, + }; +} + +export function setDimensionFilters(query: AzureMonitorQuery, dimensions: AzureMetricDimension[]): AzureMonitorQuery { + if (query.azureMonitor?.dimensionFilters === dimensions) { + return query; + } + + return { + ...query, + azureMonitor: { + ...query.azureMonitor, + dimensionFilters: dimensions, + }, + }; +} + +export function appendDimensionFilter( + query: AzureMonitorQuery, + dimension = '', + operator = 'eq', + filter = '' +): AzureMonitorQuery { + const existingFilters = query.azureMonitor?.dimensionFilters ?? []; + + return setDimensionFilters(query, [ + ...existingFilters, + { + dimension, + operator, + filter, + }, + ]); +} + +export function removeDimensionFilter(query: AzureMonitorQuery, indexToRemove: number): AzureMonitorQuery { + const existingFilters = query.azureMonitor?.dimensionFilters ?? []; + const newFilters = [...existingFilters]; + newFilters.splice(indexToRemove, 1); + return setDimensionFilters(query, newFilters); +} + +export function setDimensionFilterValue( + query: AzureMonitorQuery, + index: number, + fieldName: Key, + value: AzureMetricDimension[Key] +): AzureMonitorQuery { + const existingFilters = query.azureMonitor?.dimensionFilters ?? []; + const newFilters = [...existingFilters]; + const newFilter = newFilters[index]; + newFilter[fieldName] = value; + return setDimensionFilters(query, newFilters); +} + +export function setTop(query: AzureMonitorQuery, top: string): AzureMonitorQuery { + if (query.azureMonitor?.top === top) { + return query; + } + + return { + ...query, + azureMonitor: { + ...query.azureMonitor, + top: top, + }, + }; +} + +export function setLegendAlias(query: AzureMonitorQuery, alias: string): AzureMonitorQuery { + if (query.azureMonitor?.alias === alias) { + return query; + } + + return { + ...query, + azureMonitor: { + ...query.azureMonitor, + alias: alias, + }, + }; +} 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 index a31aa9a8c3c..d365db0b7bf 100644 --- 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 @@ -3,7 +3,6 @@ import { Select } from '@grafana/ui'; import { Field } from '../Field'; import { AzureMonitorQuery, AzureQueryType } from '../../types'; import { SelectableValue } from '@grafana/data'; -import { findOption } from '../../utils/common'; interface QueryTypeFieldProps { query: AzureMonitorQuery; @@ -45,7 +44,7 @@ const QueryTypeField: React.FC = ({ query, onQueryChange }) ; - timeGrains: Array<{ label: string; value: string }>; - dimensions: Array<{ label: string; value: string }>; - isLoading: boolean; -} - -export function useMetricsMetadata( - datasource: Datasource, - query: AzureMonitorQuery, - subscriptionId: string | undefined, - onQueryChange: (newQuery: AzureMonitorQuery) => void -) { - const [metricMetadata, setMetricMetadata] = useState({ - aggOptions: [], - timeGrains: [], - dimensions: [], - isLoading: false, - }); - - useEffect(() => { - if ( - !( - subscriptionId && - query.azureMonitor && - query.azureMonitor.resourceGroup && - query.azureMonitor.metricDefinition && - query.azureMonitor.resourceName && - query.azureMonitor.metricNamespace && - query.azureMonitor.metricName - ) - ) { - return; - } - setMetricMetadata((prevState) => ({ ...prevState, isLoading: true })); - 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: - query.azureMonitor?.aggregation && metadata.supportedAggTypes.includes(query.azureMonitor.aggregation) - ? query.azureMonitor.aggregation - : metadata.primaryAggType, - timeGrain: query.azureMonitor?.timeGrain || 'auto', // TODO: move this default value somewhere better? - 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, - isLoading: false, - }); - }) - .catch((err) => { - // TODO: handle error - console.error(err); - }); - }, [ - subscriptionId, - query.azureMonitor?.resourceGroup, - query.azureMonitor?.metricDefinition, - query.azureMonitor?.resourceName, - query.azureMonitor?.metricNamespace, - query.azureMonitor?.metricName, - query, - datasource, - onQueryChange, - ]); - - 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 cb0f7ec531c..220c5d2a1e7 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/datasource.ts +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/datasource.ts @@ -75,8 +75,6 @@ export default class Datasource extends DataSourceApi): Observable { - console.log('query options.targets', options.targets); - const byType = new Map>(); for (const target of options.targets) { 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 f32ee296b53..cfea7f69266 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 @@ -41,7 +41,6 @@ export interface AzureMetricQuery { metricNamespace?: string; metricName?: string; timeGrain?: string; - allowedTimeGrainsMs?: number[]; aggregation?: string; dimensionFilters?: AzureMetricDimension[]; alias?: string; @@ -49,6 +48,9 @@ export interface AzureMetricQuery { /** @deprecated */ timeGrainUnit?: string; + + /** @deprecated Remove this once angular is removed */ + allowedTimeGrainsMs?: number[]; } /** @@ -100,6 +102,6 @@ export interface InsightsAnalyticsQuery { export interface AzureMetricDimension { dimension: string; - operator: 'eq'; // future proof - filter?: string; // * + operator: string; + filter?: string; } diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/types/types.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/types/types.ts index fb226f20e75..c4ab753d067 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/types/types.ts +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/types/types.ts @@ -140,6 +140,7 @@ export interface AzureLogsTableColumn { export interface AzureMonitorOption { label: string; value: T; + options?: AzureMonitorOption[]; } export interface AzureQueryEditorFieldProps { diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/utils/common.test.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/utils/common.test.ts new file mode 100644 index 00000000000..d0093a106cd --- /dev/null +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/utils/common.test.ts @@ -0,0 +1,42 @@ +import { hasOption } from './common'; + +describe('AzureMonitor: hasOption', () => { + it('can find an option in flat array', () => { + const options = [ + { value: 'a', label: 'a' }, + { value: 'b', label: 'b' }, + { value: 'c', label: 'c' }, + ]; + + expect(hasOption(options, 'b')).toBeTruthy(); + }); + + it('can not find an option in flat array', () => { + const options = [ + { value: 'a', label: 'a' }, + { value: 'b', label: 'b' }, + { value: 'c', label: 'c' }, + ]; + + expect(hasOption(options, 'not-there')).not.toBeTruthy(); + }); + + it('can find an option in a nested group', () => { + const options = [ + { value: 'a', label: 'a' }, + { value: 'b', label: 'b' }, + { + label: 'c', + value: 'c', + options: [ + { value: 'c-a', label: 'c-a' }, + { value: 'c-b', label: 'c-b' }, + { value: 'c-c', label: 'c-c' }, + ], + }, + { value: 'd', label: 'd' }, + ]; + + expect(hasOption(options, 'c-b')).toBeTruthy(); + }); +}); diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/utils/common.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/utils/common.ts index 07cb15a435c..1a131605813 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/utils/common.ts +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/utils/common.ts @@ -2,9 +2,8 @@ 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 | undefined) => - value ? options.find((v) => v.value === value) ?? { value, label: value } : null; +export const hasOption = (options: AzureMonitorOption[], value: string): boolean => + options.some((v) => (v.options ? hasOption(v.options, value) : v.value === value)); export const findOptions = (options: AzureMonitorOption[], values: string[] = []) => { if (values.length === 0) {