AzureMonitor: display errors from requests for the dropdowns (#31921)

* AzureMonitor: display errors from requests for the dropdowns

* switch to array of errors, using just the last one

* unify error object

* move files into utils

* TESTS

* fix tests
This commit is contained in:
Josh Hunt 2021-03-24 12:06:55 +00:00 committed by GitHub
parent 2179a2658e
commit 9a7c10cffe
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 266 additions and 73 deletions

View File

@ -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<Datasource> = {
getVariables: jest.fn().mockReturnValueOnce([]),
getVariables: jest.fn().mockReturnValue([]),
azureMonitorDatasource: {
isConfigured() {

View File

@ -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,
},
};
}

View File

@ -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 {

View File

@ -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 {

View File

@ -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<AzureQueryEditorFieldProps> = ({
query,
datasource,
subscriptionId,
variableOptionGroup,
onQueryChange,
setError,
}) => {
const [metricNames, setMetricNames] = useState<AzureMonitorOption[]>([]);
@ -28,10 +30,7 @@ const MetricName: React.FC<AzureQueryEditorFieldProps> = ({
.then((results) => {
setMetricNames(results.map(toOption));
})
.catch((err) => {
// TODO: handle error
console.error(err);
});
.catch((err) => setError(ERROR_SOURCE, err));
}, [
subscriptionId,
query.azureMonitor.resourceGroup,

View File

@ -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<AzureQueryEditorFieldProps> = ({
query,
datasource,
subscriptionId,
variableOptionGroup,
onQueryChange,
setError,
}) => {
const [metricNamespaces, setMetricNamespaces] = useState<AzureMonitorOption[]>([]);
@ -26,21 +28,18 @@ const MetricNamespaceField: React.FC<AzureQueryEditorFieldProps> = ({
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,

View File

@ -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());

View File

@ -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<MetricsQueryEditorProps> = ({
@ -30,6 +31,7 @@ const MetricsQueryEditor: React.FC<MetricsQueryEditorProps> = ({
subscriptionId,
variableOptionGroup,
onChange,
setError,
}) => {
const metricsMetadata = useMetricsMetadata(datasource, query, subscriptionId, onChange);
@ -42,6 +44,7 @@ const MetricsQueryEditor: React.FC<MetricsQueryEditorProps> = ({
subscriptionId={subscriptionId}
variableOptionGroup={variableOptionGroup}
onQueryChange={onChange}
setError={setError}
/>
<ResourceGroupsField
@ -50,6 +53,7 @@ const MetricsQueryEditor: React.FC<MetricsQueryEditorProps> = ({
subscriptionId={subscriptionId}
variableOptionGroup={variableOptionGroup}
onQueryChange={onChange}
setError={setError}
/>
</InlineFieldRow>
@ -60,6 +64,7 @@ const MetricsQueryEditor: React.FC<MetricsQueryEditorProps> = ({
subscriptionId={subscriptionId}
variableOptionGroup={variableOptionGroup}
onQueryChange={onChange}
setError={setError}
/>
<ResourceNameField
query={query}
@ -67,6 +72,7 @@ const MetricsQueryEditor: React.FC<MetricsQueryEditorProps> = ({
subscriptionId={subscriptionId}
variableOptionGroup={variableOptionGroup}
onQueryChange={onChange}
setError={setError}
/>
</InlineFieldRow>
@ -77,6 +83,7 @@ const MetricsQueryEditor: React.FC<MetricsQueryEditorProps> = ({
subscriptionId={subscriptionId}
variableOptionGroup={variableOptionGroup}
onQueryChange={onChange}
setError={setError}
/>
<MetricNameField
query={query}
@ -84,6 +91,7 @@ const MetricsQueryEditor: React.FC<MetricsQueryEditorProps> = ({
subscriptionId={subscriptionId}
variableOptionGroup={variableOptionGroup}
onQueryChange={onChange}
setError={setError}
/>
</InlineFieldRow>
<InlineFieldRow>
@ -93,6 +101,7 @@ const MetricsQueryEditor: React.FC<MetricsQueryEditorProps> = ({
subscriptionId={subscriptionId}
variableOptionGroup={variableOptionGroup}
onQueryChange={onChange}
setError={setError}
aggregationOptions={metricsMetadata?.aggOptions ?? []}
/>
<TimeGrainField
@ -101,6 +110,7 @@ const MetricsQueryEditor: React.FC<MetricsQueryEditorProps> = ({
subscriptionId={subscriptionId}
variableOptionGroup={variableOptionGroup}
onQueryChange={onChange}
setError={setError}
timeGrainOptions={metricsMetadata?.timeGrains ?? []}
/>
</InlineFieldRow>
@ -110,6 +120,7 @@ const MetricsQueryEditor: React.FC<MetricsQueryEditorProps> = ({
subscriptionId={subscriptionId}
variableOptionGroup={variableOptionGroup}
onQueryChange={onChange}
setError={setError}
dimensionOptions={metricsMetadata?.dimensions ?? []}
/>
<TopField
@ -118,6 +129,7 @@ const MetricsQueryEditor: React.FC<MetricsQueryEditorProps> = ({
subscriptionId={subscriptionId}
variableOptionGroup={variableOptionGroup}
onQueryChange={onChange}
setError={setError}
/>
<LegendFormatField
query={query}
@ -125,6 +137,7 @@ const MetricsQueryEditor: React.FC<MetricsQueryEditorProps> = ({
subscriptionId={subscriptionId}
variableOptionGroup={variableOptionGroup}
onQueryChange={onChange}
setError={setError}
/>
</div>
);

View File

@ -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<AzureQueryEditorFieldProps> = ({
query,
datasource,
subscriptionId,
variableOptionGroup,
onQueryChange,
setError,
}) => {
const [namespaces, setNamespaces] = useState<AzureMonitorOption[]>([]);
@ -26,10 +28,7 @@ const NamespaceField: React.FC<AzureQueryEditorFieldProps> = ({
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(

View File

@ -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<AzureQueryEditorFieldProps> = ({
query,
datasource,
subscriptionId,
variableOptionGroup,
onQueryChange,
setError,
}) => {
const [resourceGroups, setResourceGroups] = useState<AzureMonitorOption[]>([]);
@ -23,11 +25,11 @@ const ResourceGroupsField: React.FC<AzureQueryEditorFieldProps> = ({
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(

View File

@ -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<AzureQueryEditorFieldProps> = ({
query,
datasource,
subscriptionId,
variableOptionGroup,
onQueryChange,
setError,
}) => {
const [resourceNames, setResourceNames] = useState<AzureMonitorOption[]>([]);
@ -26,10 +28,7 @@ const ResourceNameField: React.FC<AzureQueryEditorFieldProps> = ({
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(

View File

@ -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';

View File

@ -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(
<QueryEditor
query={createMockQuery()}
datasource={mockDatasource}
variableOptionGroup={variableOptionGroup}
onChange={() => {}}
/>
);
await waitFor(() => expect(screen.getByTestId('azure-monitor-query-editor')).toBeInTheDocument());
expect(screen.getByText("The resource namespace 'grafanadev' is invalid.")).toBeInTheDocument();
});
});

View File

@ -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<BaseQueryEditorProps> = ({ 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<BaseQueryEditorProps> = ({ query, datasource, onChan
return (
<div data-testid="azure-monitor-query-editor">
<QueryTypeField query={query} onQueryChange={onChange} />
<EditorForQueryType
subscriptionId={subscriptionId}
query={query}
datasource={datasource}
onChange={onChange}
variableOptionGroup={variableOptionGroup}
/>
<VerticalGroup>
<EditorForQueryType
subscriptionId={subscriptionId}
query={query}
datasource={datasource}
onChange={onChange}
variableOptionGroup={variableOptionGroup}
setError={setError}
/>
{errorMessage && (
<Alert severity="error" title="An error occurred while requesting metadata from Azure Monitor">
{errorMessage}
</Alert>
)}
</VerticalGroup>
</div>
);
};
interface EditorForQueryTypeProps extends BaseQueryEditorProps {
subscriptionId: string;
setError: (source: string, error: AzureMonitorErrorish | undefined) => void;
}
const EditorForQueryType: React.FC<EditorForQueryTypeProps> = ({
@ -42,6 +56,7 @@ const EditorForQueryType: React.FC<EditorForQueryTypeProps> = ({
datasource,
variableOptionGroup,
onChange,
setError,
}) => {
switch (query.queryType) {
case AzureQueryType.AzureMonitor:
@ -52,6 +67,7 @@ const EditorForQueryType: React.FC<EditorForQueryTypeProps> = ({
datasource={datasource}
onChange={onChange}
variableOptionGroup={variableOptionGroup}
setError={setError}
/>
);
}

View File

@ -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' },

View File

@ -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<SubscriptionFieldProps> = ({
datasource,
query,
variableOptionGroup,
onQueryChange,
setError,
}) => {
const [subscriptions, setSubscriptions] = useState<AzureMonitorOption[]>([]);
@ -23,31 +25,35 @@ const SubscriptionField: React.FC<SubscriptionFieldProps> = ({
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<SubscriptionFieldProps> = ({
};
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,

View File

@ -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 }>;

View File

@ -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 {

View File

@ -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;
}

View File

@ -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.");
});
});

View File

@ -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;
}

View File

@ -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');
});
});

View File

@ -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<SourcedError[]>([]);
// 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;
}