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 index 013c61bd56d..f1b85b0c570 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/__mocks__/datasource.ts +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/__mocks__/datasource.ts @@ -8,7 +8,7 @@ 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([]), + getVariables: jest.fn().mockReturnValue([]), azureMonitorDatasource: { isConfigured() { diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/__mocks__/errors.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/__mocks__/errors.ts new file mode 100644 index 00000000000..cda4bd5c15b --- /dev/null +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/__mocks__/errors.ts @@ -0,0 +1,22 @@ +export function invalidNamespaceError() { + return { + status: 404, + statusText: 'Not Found', + data: { + error: { + code: 'InvalidResourceNamespace', + message: "The resource namespace 'grafanadev' is invalid.", + }, + }, + config: { + url: + 'api/datasources/proxy/31/azuremonitor/subscriptions/44693801-6ee6-49de-9b2d-9106972f9572/resourceGroups/grafanadev/providers/grafanadev/select/providers/microsoft.insights/metricdefinitions?api-version=2018-01-01&metricnamespace=select', + method: 'GET', + retry: 0, + headers: { + 'X-Grafana-Org-Id': 1, + }, + hideFromInspector: false, + }, + }; +} 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 029d372162f..5639ce2b18c 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,7 +3,7 @@ import { Select } from '@grafana/ui'; import { SelectableValue } from '@grafana/data'; import { Field } from '../Field'; -import { findOption } from '../common'; +import { findOption } from '../../utils/common'; import { AzureQueryEditorFieldProps, AzureMonitorOption } from '../../types'; interface AggregationFieldProps extends AzureQueryEditorFieldProps { diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/MetricsQueryEditor/DimensionFields.tsx b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/MetricsQueryEditor/DimensionFields.tsx index 0ac7fb05a9f..19216c3ca15 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/MetricsQueryEditor/DimensionFields.tsx +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/MetricsQueryEditor/DimensionFields.tsx @@ -2,7 +2,7 @@ import React, { useCallback } from 'react'; import { Button, Select, Input, HorizontalGroup, VerticalGroup, InlineLabel } from '@grafana/ui'; import { Field } from '../Field'; -import { findOption } from '../common'; +import { findOption } from '../../utils/common'; import { AzureMetricDimension, AzureMonitorOption, AzureQueryEditorFieldProps } from '../../types'; interface DimensionFieldsProps extends AzureQueryEditorFieldProps { 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 42d175f6efe..32b74b38ec8 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 @@ -3,15 +3,17 @@ import { Select } from '@grafana/ui'; import { SelectableValue } from '@grafana/data'; import { Field } from '../Field'; -import { findOption, toOption } from '../common'; +import { findOption, toOption } from '../../utils/common'; import { AzureQueryEditorFieldProps, AzureMonitorOption } from '../../types'; +const ERROR_SOURCE = 'metrics-metricname'; const MetricName: React.FC = ({ query, datasource, subscriptionId, variableOptionGroup, onQueryChange, + setError, }) => { const [metricNames, setMetricNames] = useState([]); @@ -28,10 +30,7 @@ const MetricName: React.FC = ({ .then((results) => { setMetricNames(results.map(toOption)); }) - .catch((err) => { - // TODO: handle error - console.error(err); - }); + .catch((err) => setError(ERROR_SOURCE, err)); }, [ subscriptionId, query.azureMonitor.resourceGroup, diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/MetricsQueryEditor/MetricNamespaceField.tsx b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/MetricsQueryEditor/MetricNamespaceField.tsx index a754a27b6b2..7748a0fe322 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/MetricsQueryEditor/MetricNamespaceField.tsx +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/MetricsQueryEditor/MetricNamespaceField.tsx @@ -3,15 +3,17 @@ import { Select } from '@grafana/ui'; import { SelectableValue } from '@grafana/data'; import { Field } from '../Field'; -import { findOption, toOption } from '../common'; +import { findOption, toOption } from '../../utils/common'; import { AzureQueryEditorFieldProps, AzureMonitorOption } from '../../types'; +const ERROR_SOURCE = 'metrics-metricnamespace'; const MetricNamespaceField: React.FC = ({ query, datasource, subscriptionId, variableOptionGroup, onQueryChange, + setError, }) => { const [metricNamespaces, setMetricNamespaces] = useState([]); @@ -26,21 +28,18 @@ const MetricNamespaceField: React.FC = ({ datasource .getMetricNamespaces(subscriptionId, resourceGroup, metricDefinition, resourceName) .then((results) => { - // if (results.length === 1) { - // onQueryChange({ - // ...query, - // azureMonitor: { - // ...query.azureMonitor, - // metricNamespace: results[0].value, - // }, - // }); - // } + if (results.length === 1) { + onQueryChange({ + ...query, + azureMonitor: { + ...query.azureMonitor, + metricNamespace: results[0].value, + }, + }); + } setMetricNamespaces(results.map(toOption)); }) - .catch((err) => { - // TODO: handle error - console.error(err); - }); + .catch((err) => setError(ERROR_SOURCE, err)); }, [ subscriptionId, query.azureMonitor.resourceGroup, 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 index 3e0f3782ff6..867e74b6340 100644 --- 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 @@ -22,6 +22,7 @@ describe('Azure Monitor QueryEditor', () => { datasource={mockDatasource} variableOptionGroup={variableOptionGroup} onChange={() => {}} + setError={() => {}} /> ); await waitFor(() => expect(screen.getByTestId('azure-monitor-metrics-query-editor')).toBeInTheDocument()); @@ -50,6 +51,7 @@ describe('Azure Monitor QueryEditor', () => { datasource={mockDatasource} variableOptionGroup={variableOptionGroup} onChange={onChange} + setError={() => {}} /> ); @@ -94,6 +96,7 @@ describe('Azure Monitor QueryEditor', () => { datasource={mockDatasource} variableOptionGroup={variableOptionGroup} onChange={onChange} + setError={() => {}} /> ); await waitFor(() => expect(screen.getByTestId('azure-monitor-metrics-query-editor')).toBeInTheDocument()); 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 8b72cf0e70d..ac7814ae523 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 @@ -1,7 +1,7 @@ import React from 'react'; import Datasource from '../../datasource'; -import { AzureMonitorQuery, AzureMonitorOption } from '../../types'; +import { AzureMonitorQuery, AzureMonitorOption, AzureMonitorErrorish } from '../../types'; import { useMetricsMetadata } from '../metrics'; import SubscriptionField from '../SubscriptionField'; import MetricNamespaceField from './MetricNamespaceField'; @@ -22,6 +22,7 @@ interface MetricsQueryEditorProps { subscriptionId: string; onChange: (newQuery: AzureMonitorQuery) => void; variableOptionGroup: { label: string; options: AzureMonitorOption[] }; + setError: (source: string, error: AzureMonitorErrorish | undefined) => void; } const MetricsQueryEditor: React.FC = ({ @@ -30,6 +31,7 @@ const MetricsQueryEditor: React.FC = ({ subscriptionId, variableOptionGroup, onChange, + setError, }) => { const metricsMetadata = useMetricsMetadata(datasource, query, subscriptionId, onChange); @@ -42,6 +44,7 @@ const MetricsQueryEditor: React.FC = ({ subscriptionId={subscriptionId} variableOptionGroup={variableOptionGroup} onQueryChange={onChange} + setError={setError} /> = ({ subscriptionId={subscriptionId} variableOptionGroup={variableOptionGroup} onQueryChange={onChange} + setError={setError} /> @@ -60,6 +64,7 @@ const MetricsQueryEditor: React.FC = ({ subscriptionId={subscriptionId} variableOptionGroup={variableOptionGroup} onQueryChange={onChange} + setError={setError} /> = ({ subscriptionId={subscriptionId} variableOptionGroup={variableOptionGroup} onQueryChange={onChange} + setError={setError} /> @@ -77,6 +83,7 @@ const MetricsQueryEditor: React.FC = ({ subscriptionId={subscriptionId} variableOptionGroup={variableOptionGroup} onQueryChange={onChange} + setError={setError} /> = ({ subscriptionId={subscriptionId} variableOptionGroup={variableOptionGroup} onQueryChange={onChange} + setError={setError} /> @@ -93,6 +101,7 @@ const MetricsQueryEditor: React.FC = ({ subscriptionId={subscriptionId} variableOptionGroup={variableOptionGroup} onQueryChange={onChange} + setError={setError} aggregationOptions={metricsMetadata?.aggOptions ?? []} /> = ({ subscriptionId={subscriptionId} variableOptionGroup={variableOptionGroup} onQueryChange={onChange} + setError={setError} timeGrainOptions={metricsMetadata?.timeGrains ?? []} /> @@ -110,6 +120,7 @@ const MetricsQueryEditor: React.FC = ({ subscriptionId={subscriptionId} variableOptionGroup={variableOptionGroup} onQueryChange={onChange} + setError={setError} dimensionOptions={metricsMetadata?.dimensions ?? []} /> = ({ subscriptionId={subscriptionId} variableOptionGroup={variableOptionGroup} onQueryChange={onChange} + setError={setError} /> = ({ subscriptionId={subscriptionId} variableOptionGroup={variableOptionGroup} onQueryChange={onChange} + setError={setError} /> ); 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 index 070c4a42537..e79674c271c 100644 --- 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 @@ -3,15 +3,17 @@ import { Select } from '@grafana/ui'; import { SelectableValue } from '@grafana/data'; import { Field } from '../Field'; -import { findOption, toOption } from '../common'; +import { findOption, toOption } from '../../utils/common'; import { AzureQueryEditorFieldProps, AzureMonitorOption } from '../../types'; +const ERROR_SOURCE = 'metrics-namespace'; const NamespaceField: React.FC = ({ query, datasource, subscriptionId, variableOptionGroup, onQueryChange, + setError, }) => { const [namespaces, setNamespaces] = useState([]); @@ -26,10 +28,7 @@ const NamespaceField: React.FC = ({ datasource .getMetricDefinitions(subscriptionId, resourceGroup) .then((results) => setNamespaces(results.map(toOption))) - .catch((err) => { - // TODO: handle error - console.error(err); - }); + .catch((err) => setError(ERROR_SOURCE, err)); }, [subscriptionId, query.azureMonitor.resourceGroup]); const handleChange = useCallback( diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/MetricsQueryEditor/ResourceGroupsField.tsx b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/MetricsQueryEditor/ResourceGroupsField.tsx index c6a5025a8bd..51a0130970d 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/MetricsQueryEditor/ResourceGroupsField.tsx +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/MetricsQueryEditor/ResourceGroupsField.tsx @@ -3,15 +3,17 @@ import { Select } from '@grafana/ui'; import { SelectableValue } from '@grafana/data'; import { Field } from '../Field'; -import { findOption, toOption } from '../common'; +import { findOption, toOption } from '../../utils/common'; import { AzureQueryEditorFieldProps, AzureMonitorOption } from '../../types'; +const ERROR_SOURCE = 'metrics-resourcegroups'; const ResourceGroupsField: React.FC = ({ query, datasource, subscriptionId, variableOptionGroup, onQueryChange, + setError, }) => { const [resourceGroups, setResourceGroups] = useState([]); @@ -23,11 +25,11 @@ const ResourceGroupsField: React.FC = ({ datasource .getResourceGroups(subscriptionId) - .then((results) => setResourceGroups(results.map(toOption))) - .catch((err) => { - // TODO: handle error - console.error(err); - }); + .then((results) => { + setResourceGroups(results.map(toOption)); + setError(ERROR_SOURCE, undefined); + }) + .catch((err) => setError(ERROR_SOURCE, err)); }, [subscriptionId]); const handleChange = useCallback( 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 index 4faf36678c0..ac176194b90 100644 --- 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 @@ -3,15 +3,17 @@ import { Select } from '@grafana/ui'; import { SelectableValue } from '@grafana/data'; import { Field } from '../Field'; -import { findOption, toOption } from '../common'; +import { findOption, toOption } from '../../utils/common'; import { AzureQueryEditorFieldProps, AzureMonitorOption } from '../../types'; +const ERROR_SOURCE = 'metrics-resource'; const ResourceNameField: React.FC = ({ query, datasource, subscriptionId, variableOptionGroup, onQueryChange, + setError, }) => { const [resourceNames, setResourceNames] = useState([]); @@ -26,10 +28,7 @@ const ResourceNameField: React.FC = ({ datasource .getResourceNames(subscriptionId, resourceGroup, metricDefinition) .then((results) => setResourceNames(results.map(toOption))) - .catch((err) => { - // TODO: handle error - console.error(err); - }); + .catch((err) => setError(ERROR_SOURCE, err)); }, [subscriptionId, query.azureMonitor.resourceGroup, query.azureMonitor.metricDefinition]); const handleChange = useCallback( diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/MetricsQueryEditor/TimeGrainField.tsx b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/MetricsQueryEditor/TimeGrainField.tsx index be32f3246a5..894f9d0f9bd 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/MetricsQueryEditor/TimeGrainField.tsx +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/MetricsQueryEditor/TimeGrainField.tsx @@ -3,7 +3,7 @@ import { Select } from '@grafana/ui'; import { SelectableValue } from '@grafana/data'; import { Field } from '../Field'; -import { findOption } from '../common'; +import { findOption } from '../../utils/common'; import TimegrainConverter from '../../time_grain_converter'; import { AzureQueryEditorFieldProps, AzureMonitorOption } from '../../types'; 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 index 7874b8166a1..c90ba686931 100644 --- 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 @@ -7,6 +7,7 @@ import QueryEditor from './QueryEditor'; import createMockQuery from '../../__mocks__/query'; import createMockDatasource from '../../__mocks__/datasource'; import { AzureQueryType } from '../../types'; +import { invalidNamespaceError } from '../../__mocks__/errors'; const variableOptionGroup = { label: 'Template variables', @@ -67,4 +68,20 @@ describe('Azure Monitor QueryEditor', () => { queryType: AzureQueryType.LogAnalytics, }); }); + + it('displays error messages from frontend Azure calls', async () => { + const mockDatasource = createMockDatasource(); + mockDatasource.azureMonitorDatasource.getSubscriptions = jest.fn().mockRejectedValue(invalidNamespaceError()); + render( + {}} + /> + ); + await waitFor(() => expect(screen.getByTestId('azure-monitor-query-editor')).toBeInTheDocument()); + + expect(screen.getByText("The resource namespace 'grafanadev' is invalid.")).toBeInTheDocument(); + }); }); 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 index cdb3e9cf352..48a70ec4182 100644 --- 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 @@ -1,8 +1,10 @@ +import { Alert, VerticalGroup } from '@grafana/ui'; import React from 'react'; import Datasource from '../../datasource'; -import { AzureMonitorQuery, AzureQueryType, AzureMonitorOption } from '../../types'; +import { AzureMonitorQuery, AzureQueryType, AzureMonitorOption, AzureMonitorErrorish } from '../../types'; import MetricsQueryEditor from '../MetricsQueryEditor'; import QueryTypeField from './QueryTypeField'; +import useLastError from '../../utils/useLastError'; interface BaseQueryEditorProps { query: AzureMonitorQuery; @@ -12,6 +14,7 @@ interface BaseQueryEditorProps { } const QueryEditor: React.FC = ({ query, datasource, onChange }) => { + const [errorMessage, setError] = useLastError(); const subscriptionId = query.subscription || datasource.azureMonitorDatasource.subscriptionId; const variableOptionGroup = { label: 'Template Variables', @@ -21,19 +24,30 @@ const QueryEditor: React.FC = ({ query, datasource, onChan return (
- + + + + + {errorMessage && ( + + {errorMessage} + + )} +
); }; interface EditorForQueryTypeProps extends BaseQueryEditorProps { subscriptionId: string; + setError: (source: string, error: AzureMonitorErrorish | undefined) => void; } const EditorForQueryType: React.FC = ({ @@ -42,6 +56,7 @@ const EditorForQueryType: React.FC = ({ datasource, variableOptionGroup, onChange, + setError, }) => { switch (query.queryType) { case AzureQueryType.AzureMonitor: @@ -52,6 +67,7 @@ const EditorForQueryType: React.FC = ({ datasource={datasource} onChange={onChange} variableOptionGroup={variableOptionGroup} + setError={setError} /> ); } 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 1c1357b2d27..7c24270f343 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,7 @@ import { Select } from '@grafana/ui'; import { Field } from '../Field'; import { AzureMonitorQuery, AzureQueryType } from '../../types'; import { SelectableValue } from '@grafana/data'; -import { findOption } from '../common'; +import { findOption } from '../../utils/common'; const QUERY_TYPES = [ { value: AzureQueryType.AzureMonitor, label: 'Metrics' }, diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/SubscriptionField.tsx b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/SubscriptionField.tsx index 300333e6823..4433e77b852 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/SubscriptionField.tsx +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/SubscriptionField.tsx @@ -3,18 +3,20 @@ import { SelectableValue } from '@grafana/data'; import { Select } from '@grafana/ui'; import { AzureMonitorQuery, AzureQueryType, AzureQueryEditorFieldProps, AzureMonitorOption } from '../types'; -import { findOption } from './common'; +import { findOption } from '../utils/common'; import { Field } from './Field'; interface SubscriptionFieldProps extends AzureQueryEditorFieldProps { onQueryChange: (newQuery: AzureMonitorQuery) => void; } +const ERROR_SOURCE = 'metrics-subscription'; const SubscriptionField: React.FC = ({ datasource, query, variableOptionGroup, onQueryChange, + setError, }) => { const [subscriptions, setSubscriptions] = useState([]); @@ -23,31 +25,35 @@ const SubscriptionField: React.FC = ({ return; } - datasource.azureMonitorDatasource.getSubscriptions().then((results) => { - const newSubscriptions = results.map((v) => ({ label: v.text, value: v.value, description: v.value })); - setSubscriptions(newSubscriptions); + datasource.azureMonitorDatasource + .getSubscriptions() + .then((results) => { + const newSubscriptions = results.map((v) => ({ label: v.text, value: v.value, description: v.value })); + setSubscriptions(newSubscriptions); + setError(ERROR_SOURCE, undefined); - // Set a default subscription ID, if we can - let newSubscription = query.subscription; + // Set a default subscription ID, if we can + let newSubscription = query.subscription; - if (!newSubscription && query.queryType === AzureQueryType.AzureMonitor) { - newSubscription = datasource.azureMonitorDatasource.subscriptionId; - } else if (!query.subscription && query.queryType === AzureQueryType.LogAnalytics) { - newSubscription = - datasource.azureLogAnalyticsDatasource.logAnalyticsSubscriptionId || - datasource.azureLogAnalyticsDatasource.subscriptionId; - } + if (!newSubscription && query.queryType === AzureQueryType.AzureMonitor) { + newSubscription = datasource.azureMonitorDatasource.subscriptionId; + } else if (!query.subscription && query.queryType === AzureQueryType.LogAnalytics) { + newSubscription = + datasource.azureLogAnalyticsDatasource.logAnalyticsSubscriptionId || + datasource.azureLogAnalyticsDatasource.subscriptionId; + } - if (!newSubscription && newSubscriptions.length > 0) { - newSubscription = newSubscriptions[0].value; - } + if (!newSubscription && newSubscriptions.length > 0) { + newSubscription = newSubscriptions[0].value; + } - newSubscription !== query.subscription && - onQueryChange({ - ...query, - subscription: newSubscription, - }); - }); + newSubscription !== query.subscription && + onQueryChange({ + ...query, + subscription: newSubscription, + }); + }) + .catch((err) => setError(ERROR_SOURCE, err)); }, []); const handleChange = useCallback( @@ -62,6 +68,8 @@ const SubscriptionField: React.FC = ({ }; if (query.queryType === AzureQueryType.AzureMonitor) { + // TODO: set the fields to undefined so we don't + // get "resource group select could not be found" errors newQuery.azureMonitor = { ...newQuery.azureMonitor, resourceGroup: undefined, 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 index f2c07836529..4984648ca64 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/metrics.ts +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/metrics.ts @@ -2,7 +2,7 @@ import { useState, useEffect } from 'react'; import Datasource from '../datasource'; import { AzureMonitorQuery } from '../types'; -import { convertTimeGrainsToMs } from './common'; +import { convertTimeGrainsToMs } from '../utils/common'; export interface MetricMetadata { aggOptions: Array<{ label: string; value: string }>; 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 7ad2bf11b14..429e6fe3e86 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,7 @@ import { TemplateSrv } from '@grafana/runtime'; import { auto } from 'angular'; import { DataFrame, PanelEvents } from '@grafana/data'; import { AzureQueryType, AzureMetricQuery, AzureMonitorQuery } from './types'; -import { convertTimeGrainsToMs } from './components/common'; +import { convertTimeGrainsToMs } from './utils/common'; import Datasource from './datasource'; export interface ResultFormat { 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 7ba6d529207..00d557f86c1 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/types.ts +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/types.ts @@ -90,6 +90,10 @@ export interface InsightsAnalyticsQuery { resultFormat: string; } +// Represents an errors that come back from frontend requests. +// Not totally sure how accurate this type is. +export type AzureMonitorErrorish = Error; + // Azure Monitor API Types export interface AzureMonitorMetricsMetadataResponse { @@ -191,4 +195,5 @@ export interface AzureQueryEditorFieldProps { variableOptionGroup: { label: string; options: AzureMonitorOption[] }; onQueryChange: (newQuery: AzureMonitorQuery) => void; + setError: (source: string, error: AzureMonitorErrorish | undefined) => void; } diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/common.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/utils/common.ts similarity index 100% rename from public/app/plugins/datasource/grafana-azure-monitor-datasource/components/common.ts rename to public/app/plugins/datasource/grafana-azure-monitor-datasource/utils/common.ts diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/utils/messageFromError.test.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/utils/messageFromError.test.ts new file mode 100644 index 00000000000..9b178d93035 --- /dev/null +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/utils/messageFromError.test.ts @@ -0,0 +1,14 @@ +import { invalidNamespaceError } from '../__mocks__/errors'; +import messageFromError from './messageFromError'; + +describe('AzureMonitor: messageFromError', () => { + it('returns message from Error exception', () => { + const err = new Error('wowee an error'); + expect(messageFromError(err)).toBe('wowee an error'); + }); + + it('returns message from Azure API error', () => { + const err = invalidNamespaceError(); + expect(messageFromError(err)).toBe("The resource namespace 'grafanadev' is invalid."); + }); +}); diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/utils/messageFromError.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/utils/messageFromError.ts new file mode 100644 index 00000000000..aa0bb0a61ad --- /dev/null +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/utils/messageFromError.ts @@ -0,0 +1,33 @@ +export default function messageFromError(error: any): string | undefined { + if (!error || typeof error !== 'object') { + return undefined; + } + + if (typeof error.message === 'string') { + return error.message; + } + + if (typeof error.data?.error?.message === 'string') { + return error.data.error.message; + } + + // Copied from the old Angular code - this might be checking for errors in places + // that the new code just doesnt use. + // As new error objects are discovered they should be added to the above code, rather + // than below + const maybeAMessage = + error.error?.data?.error?.innererror?.innererror?.message || + error.error?.data?.error?.innererror?.message || + error.error?.data?.error?.message || + error.error?.data?.message || + error.data?.message || + error; + + if (typeof maybeAMessage === 'string') { + return maybeAMessage; + } else if (maybeAMessage && maybeAMessage.toString) { + return maybeAMessage.toString(); + } + + return undefined; +} diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/utils/useLastError.test.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/utils/useLastError.test.ts new file mode 100644 index 00000000000..7fbccbe3a50 --- /dev/null +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/utils/useLastError.test.ts @@ -0,0 +1,26 @@ +import { renderHook, act } from '@testing-library/react-hooks'; +import useLastError from './useLastError'; + +describe('AzureMonitor: useLastError', () => { + it('returns the set error', () => { + const { result } = renderHook(() => useLastError()); + + act(() => { + result.current[1]('component-a', new Error('an error')); + }); + + expect(result.current[0]).toBe('an error'); + }); + + it('returns the most recent error', () => { + const { result } = renderHook(() => useLastError()); + + act(() => { + result.current[1]('component-a', new Error('component a error')); + result.current[1]('component-b', new Error('component b error')); + result.current[1]('component-a', new Error('second component a error')); + }); + + expect(result.current[0]).toBe('second component a error'); + }); +}); diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/utils/useLastError.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/utils/useLastError.ts new file mode 100644 index 00000000000..851c3106b8c --- /dev/null +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/utils/useLastError.ts @@ -0,0 +1,38 @@ +import { useState, useCallback, useMemo } from 'react'; +import { AzureMonitorErrorish } from '../types'; +import messageFromError from './messageFromError'; + +type SourcedError = [string, AzureMonitorErrorish]; + +export default function useLastError() { + const [errors, setErrors] = useState([]); + + // Handles errors from any child components that request data to display their options + const addError = useCallback((errorSource: string, error: AzureMonitorErrorish | undefined) => { + setErrors((errors) => { + const errorsCopy = [...errors]; + const index = errors.findIndex(([vSource]) => vSource === errorSource); + + // If there's already an error, remove it. If we're setting a new error + // below, we'll move it to the front + if (index > -1) { + errorsCopy.splice(index, 1); + } + + // And then add the new error to the top of the array. If error is defined, it was already + // removed above. + if (error) { + errorsCopy.unshift([errorSource, error]); + } + + return errorsCopy; + }); + }, []); + + const errorMessage = useMemo(() => { + const recentError = errors[0]; + return recentError && messageFromError(recentError[1]); + }, [errors]); + + return [errorMessage, addError] as const; +}