diff --git a/.betterer.results b/.betterer.results
index 7922ba7b01a..eb7876d964e 100644
--- a/.betterer.results
+++ b/.betterer.results
@@ -1925,6 +1925,9 @@ exports[`better eslint`] = {
"public/app/features/alerting/unified/components/silences/SilencesFilter.tsx:5381": [
[0, 0, 0, "Do not use any type assertions.", "0"]
],
+ "public/app/features/alerting/unified/hooks/useAlertmanagerConfig.ts:5381": [
+ [0, 0, 0, "Do not use any type assertions.", "0"]
+ ],
"public/app/features/alerting/unified/hooks/useControlledFieldArray.ts:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
],
diff --git a/public/app/features/alerting/unified/MuteTimings.tsx b/public/app/features/alerting/unified/MuteTimings.tsx
index f5333afcf25..3900a8ccf5e 100644
--- a/public/app/features/alerting/unified/MuteTimings.tsx
+++ b/public/app/features/alerting/unified/MuteTimings.tsx
@@ -5,43 +5,27 @@ import { NavModelItem } from '@grafana/data';
import { Alert } from '@grafana/ui';
import { useQueryParams } from 'app/core/hooks/useQueryParams';
import { MuteTimeInterval } from 'app/plugins/datasource/alertmanager/types';
-import { useDispatch } from 'app/types';
import { AlertmanagerPageWrapper } from './components/AlertingPageWrapper';
import MuteTimingForm from './components/mute-timings/MuteTimingForm';
-import { useUnifiedAlertingSelector } from './hooks/useUnifiedAlertingSelector';
+import { useAlertmanagerConfig } from './hooks/useAlertmanagerConfig';
import { useAlertmanager } from './state/AlertmanagerContext';
-import { fetchAlertManagerConfigAction } from './state/actions';
-import { initialAsyncRequestState } from './utils/redux';
const MuteTimings = () => {
const [queryParams] = useQueryParams();
- const dispatch = useDispatch();
const { selectedAlertmanager } = useAlertmanager();
-
- const amConfigs = useUnifiedAlertingSelector((state) => state.amConfigs);
-
- const fetchConfig = useCallback(() => {
- if (selectedAlertmanager) {
- dispatch(fetchAlertManagerConfigAction(selectedAlertmanager));
- }
- }, [selectedAlertmanager, dispatch]);
-
- useEffect(() => {
- fetchConfig();
- }, [fetchConfig]);
-
- const { result, error, loading } =
- (selectedAlertmanager && amConfigs[selectedAlertmanager]) || initialAsyncRequestState;
-
- const config = result?.alertmanager_config;
+ const { currentData, isLoading, error } = useAlertmanagerConfig(selectedAlertmanager, {
+ refetchOnFocus: true,
+ refetchOnReconnect: true,
+ });
+ const config = currentData?.alertmanager_config;
const getMuteTimingByName = useCallback(
(id: string): MuteTimeInterval | undefined => {
const timing = config?.mute_time_intervals?.find(({ name }: MuteTimeInterval) => name === id);
if (timing) {
- const provenance = (config?.muteTimeProvenances ?? {})[timing.name];
+ const provenance = config?.muteTimeProvenances?.[timing.name];
return {
...timing,
@@ -56,15 +40,15 @@ const MuteTimings = () => {
return (
<>
- {error && !loading && !result && (
+ {error && !isLoading && !currentData && (
{error.message || 'Unknown error.'}
)}
- {result && !error && (
+ {currentData && !error && (
-
+
{() => {
@@ -74,9 +58,9 @@ const MuteTimings = () => {
return (
);
diff --git a/public/app/features/alerting/unified/NotificationPolicies.tsx b/public/app/features/alerting/unified/NotificationPolicies.tsx
index e31e212a253..ad9ffa9f457 100644
--- a/public/app/features/alerting/unified/NotificationPolicies.tsx
+++ b/public/app/features/alerting/unified/NotificationPolicies.tsx
@@ -64,7 +64,16 @@ const AmRoutes = () => {
const contactPointsState = useGetContactPointsState(selectedAlertmanager ?? '');
- const { result, config, loading: resultLoading, error: resultError } = useAlertmanagerConfig(selectedAlertmanager);
+ const {
+ currentData: result,
+ isLoading: resultLoading,
+ error: resultError,
+ } = useAlertmanagerConfig(selectedAlertmanager, {
+ refetchOnFocus: true,
+ refetchOnReconnect: true,
+ });
+
+ const config = result?.alertmanager_config;
const { currentData: alertGroups, refetch: refetchAlertGroups } = useGetAlertmanagerAlertGroupsQuery(
{ amSourceName: selectedAlertmanager ?? '' },
@@ -187,7 +196,7 @@ const AmRoutes = () => {
const numberOfMuteTimings = result?.alertmanager_config.mute_time_intervals?.length ?? 0;
const haveData = result && !resultError && !resultLoading;
- const isLoading = !result && resultLoading;
+ const isFetching = !result && resultLoading;
const haveError = resultError && !resultLoading;
const muteTimingsTabActive = activeTab === ActiveTab.MuteTimings;
@@ -215,7 +224,7 @@ const AmRoutes = () => {
/>
- {isLoading && }
+ {isFetching && }
{haveError && (
{resultError.message || 'Unknown error.'}
diff --git a/public/app/features/alerting/unified/api/alertingApi.ts b/public/app/features/alerting/unified/api/alertingApi.ts
index 3c3cc75fbbc..fd7625e3c09 100644
--- a/public/app/features/alerting/unified/api/alertingApi.ts
+++ b/public/app/features/alerting/unified/api/alertingApi.ts
@@ -27,6 +27,6 @@ export const backendSrvBaseQuery = (): BaseQueryFn => async (
export const alertingApi = createApi({
reducerPath: 'alertingApi',
baseQuery: backendSrvBaseQuery(),
- tagTypes: ['AlertmanagerChoice'],
+ tagTypes: ['AlertmanagerChoice', 'AlertmanagerConfiguration'],
endpoints: () => ({}),
});
diff --git a/public/app/features/alerting/unified/api/alertmanagerApi.ts b/public/app/features/alerting/unified/api/alertmanagerApi.ts
index 793844309a3..67040babaf8 100644
--- a/public/app/features/alerting/unified/api/alertmanagerApi.ts
+++ b/public/app/features/alerting/unified/api/alertmanagerApi.ts
@@ -1,3 +1,7 @@
+import { isEmpty } from 'lodash';
+
+import { dispatch } from 'app/store/store';
+
import {
AlertmanagerAlert,
AlertmanagerChoice,
@@ -8,13 +12,22 @@ import {
ExternalAlertmanagersResponse,
Matcher,
} from '../../../../plugins/datasource/alertmanager/types';
+import { withPerformanceLogging } from '../Analytics';
import { matcherToOperator } from '../utils/alertmanager';
-import { getDatasourceAPIUid, GRAFANA_RULES_SOURCE_NAME } from '../utils/datasource';
-import { wrapWithQuotes } from '../utils/misc';
+import {
+ getDatasourceAPIUid,
+ GRAFANA_RULES_SOURCE_NAME,
+ isVanillaPrometheusAlertManagerDataSource,
+} from '../utils/datasource';
+import { retryWhile, wrapWithQuotes } from '../utils/misc';
+import { messageFromError, withSerializedError } from '../utils/redux';
import { alertingApi } from './alertingApi';
+import { fetchAlertManagerConfig, fetchStatus } from './alertmanager';
+import { featureDiscoveryApi } from './featureDiscoveryApi';
const LIMIT_TO_SUCCESSFULLY_APPLIED_AMS = 10;
+const FETCH_CONFIG_RETRY_TIMEOUT = 30 * 1000;
export interface AlertmanagersChoiceResponse {
alertmanagersChoice: AlertmanagerChoice;
@@ -107,5 +120,111 @@ export const alertmanagerApi = alertingApi.injectEndpoints({
method: 'POST',
}),
}),
+
+ // TODO we've sort of inherited the errors format here from the previous Redux actions, errors throw are of type "SerializedError"
+ getAlertmanagerConfiguration: build.query({
+ queryFn: async (alertmanagerSourceName: string) => {
+ const isGrafanaManagedAlertmanager = alertmanagerSourceName === GRAFANA_RULES_SOURCE_NAME;
+ const isVanillaPrometheusAlertmanager = isVanillaPrometheusAlertManagerDataSource(alertmanagerSourceName);
+
+ // for vanilla prometheus, there is no config endpoint. Only fetch config from status
+ if (isVanillaPrometheusAlertmanager) {
+ return withSerializedError(
+ fetchStatus(alertmanagerSourceName).then((status) => ({
+ data: {
+ alertmanager_config: status.config,
+ template_files: {},
+ },
+ }))
+ );
+ }
+
+ // discover features, we want to know if Mimir has "lazyConfigInit" configured
+ const { data: alertmanagerFeatures } = await dispatch(
+ featureDiscoveryApi.endpoints.discoverAmFeatures.initiate({
+ amSourceName: alertmanagerSourceName,
+ })
+ );
+
+ const defaultConfig = {
+ alertmanager_config: {},
+ template_files: {},
+ template_file_provenances: {},
+ };
+
+ const lazyConfigInitSupported = alertmanagerFeatures?.lazyConfigInit ?? false;
+
+ // wrap our fetchConfig function with some performance logging functions
+ const fetchAMconfigWithLogging = withPerformanceLogging(
+ fetchAlertManagerConfig,
+ `[${alertmanagerSourceName}] Alertmanager config loaded`,
+ {
+ dataSourceName: alertmanagerSourceName,
+ thunk: 'unifiedalerting/fetchAmConfig',
+ }
+ );
+
+ const tryFetchingConfiguration = retryWhile(
+ () => fetchAMconfigWithLogging(alertmanagerSourceName),
+ // if config has been recently deleted, it takes a while for cortex start returning the default one.
+ // retry for a short while instead of failing
+ (error) =>
+ !!messageFromError(error)?.includes('alertmanager storage object not found') && !lazyConfigInitSupported,
+ FETCH_CONFIG_RETRY_TIMEOUT
+ )
+ .then((result) => {
+ if (isGrafanaManagedAlertmanager) {
+ return result;
+ }
+
+ // if user config is empty for Mimir alertmanager, try to get config from status endpoint
+ const emptyConfiguration = isEmpty(result.alertmanager_config) && isEmpty(result.template_files);
+
+ if (emptyConfiguration) {
+ return fetchStatus(alertmanagerSourceName).then((status) => ({
+ alertmanager_config: status.config,
+ template_files: {},
+ template_file_provenances: result.template_file_provenances,
+ last_applied: result.last_applied,
+ id: result.id,
+ }));
+ }
+
+ return result;
+ })
+ .then((result) => result ?? defaultConfig)
+ .then((result) => ({ data: result }))
+ .catch((error) => {
+ // When mimir doesn't have fallback AM url configured the default response will be as above
+ // However it's fine, and it's possible to create AM configuration
+ if (lazyConfigInitSupported && messageFromError(error)?.includes('alertmanager storage object not found')) {
+ return {
+ data: defaultConfig,
+ };
+ }
+
+ throw error;
+ });
+
+ return withSerializedError(tryFetchingConfiguration).catch((err) => ({
+ error: err,
+ data: undefined,
+ }));
+ },
+ providesTags: ['AlertmanagerConfiguration'],
+ }),
+
+ updateAlertmanagerConfiguration: build.mutation<
+ void,
+ { selectedAlertmanager: string; config: AlertManagerCortexConfig }
+ >({
+ query: ({ selectedAlertmanager, config, ...rest }) => ({
+ url: `/api/alertmanager/${getDatasourceAPIUid(selectedAlertmanager)}/config/api/v1/alerts`,
+ method: 'POST',
+ data: config,
+ ...rest,
+ }),
+ invalidatesTags: ['AlertmanagerConfiguration'],
+ }),
}),
});
diff --git a/public/app/features/alerting/unified/components/admin/AlertmanagerConfig.test.tsx b/public/app/features/alerting/unified/components/admin/AlertmanagerConfig.test.tsx
index 43ea010454f..e8e02d2d341 100644
--- a/public/app/features/alerting/unified/components/admin/AlertmanagerConfig.test.tsx
+++ b/public/app/features/alerting/unified/components/admin/AlertmanagerConfig.test.tsx
@@ -132,7 +132,7 @@ describe('Admin config', () => {
await waitFor(() => expect(mocks.api.updateAlertManagerConfig).toHaveBeenCalled());
expect(mocks.api.updateAlertManagerConfig.mock.lastCall).toMatchSnapshot();
- await waitFor(() => expect(mocks.api.fetchConfig).toHaveBeenCalledTimes(3));
+ await waitFor(() => expect(mocks.api.fetchConfig).toHaveBeenCalledTimes(2));
});
it('Read-only when using Prometheus Alertmanager', async () => {
diff --git a/public/app/features/alerting/unified/components/admin/AlertmanagerConfig.tsx b/public/app/features/alerting/unified/components/admin/AlertmanagerConfig.tsx
index 294ba9dd0c4..914949b39c2 100644
--- a/public/app/features/alerting/unified/components/admin/AlertmanagerConfig.tsx
+++ b/public/app/features/alerting/unified/components/admin/AlertmanagerConfig.tsx
@@ -1,19 +1,15 @@
import { css } from '@emotion/css';
-import React, { useEffect, useState, useMemo } from 'react';
+import React, { useState, useMemo } from 'react';
import { GrafanaTheme2 } from '@grafana/data';
import { Alert, useStyles2 } from '@grafana/ui';
import { useDispatch } from 'app/types';
+import { useAlertmanagerConfig } from '../../hooks/useAlertmanagerConfig';
import { useUnifiedAlertingSelector } from '../../hooks/useUnifiedAlertingSelector';
import { useAlertmanager } from '../../state/AlertmanagerContext';
-import {
- deleteAlertManagerConfigAction,
- fetchAlertManagerConfigAction,
- updateAlertManagerConfigAction,
-} from '../../state/actions';
+import { deleteAlertManagerConfigAction, updateAlertManagerConfigAction } from '../../state/actions';
import { GRAFANA_RULES_SOURCE_NAME, isVanillaPrometheusAlertManagerDataSource } from '../../utils/datasource';
-import { initialAsyncRequestState } from '../../utils/redux';
import AlertmanagerConfigSelector, { ValidAmConfigOption } from './AlertmanagerConfigSelector';
import { ConfigEditor } from './ConfigEditor';
@@ -33,21 +29,13 @@ export default function AlertmanagerConfig(): JSX.Element {
const readOnly = selectedAlertmanager ? isVanillaPrometheusAlertManagerDataSource(selectedAlertmanager) : false;
const styles = useStyles2(getStyles);
- const configRequests = useUnifiedAlertingSelector((state) => state.amConfigs);
-
const [selectedAmConfig, setSelectedAmConfig] = useState();
const {
- result: config,
- loading: isLoadingConfig,
+ currentData: config,
error: loadingError,
- } = (selectedAlertmanager && configRequests[selectedAlertmanager]) || initialAsyncRequestState;
-
- useEffect(() => {
- if (selectedAlertmanager) {
- dispatch(fetchAlertManagerConfigAction(selectedAlertmanager));
- }
- }, [selectedAlertmanager, dispatch]);
+ isLoading: isLoadingConfig,
+ } = useAlertmanagerConfig(selectedAlertmanager);
const resetConfig = () => {
if (selectedAlertmanager) {
diff --git a/public/app/features/alerting/unified/components/contact-points/ContactPoints.v1.test.tsx b/public/app/features/alerting/unified/components/contact-points/ContactPoints.v1.test.tsx
index 3480ec531a4..5d2e6fca2b3 100644
--- a/public/app/features/alerting/unified/components/contact-points/ContactPoints.v1.test.tsx
+++ b/public/app/features/alerting/unified/components/contact-points/ContactPoints.v1.test.tsx
@@ -306,7 +306,7 @@ describe('Receivers', () => {
// see that we're back to main page and proper api calls have been made
await ui.receiversTable.find();
expect(mocks.api.updateConfig).toHaveBeenCalledTimes(1);
- expect(mocks.api.fetchConfig).toHaveBeenCalledTimes(3);
+ expect(mocks.api.fetchConfig).toHaveBeenCalledTimes(1);
expect(locationService.getLocation().pathname).toEqual('/alerting/notifications');
expect(mocks.api.updateConfig).toHaveBeenLastCalledWith(GRAFANA_RULES_SOURCE_NAME, {
...someGrafanaAlertManagerConfig,
@@ -400,7 +400,8 @@ describe('Receivers', () => {
// see that we're back to main page and proper api calls have been made
await ui.receiversTable.find();
expect(mocks.api.updateConfig).toHaveBeenCalledTimes(1);
- expect(mocks.api.fetchConfig).toHaveBeenCalledTimes(3);
+ expect(mocks.api.fetchConfig).toHaveBeenCalledTimes(1);
+
expect(locationService.getLocation().pathname).toEqual('/alerting/notifications');
expect(mocks.api.updateConfig).toHaveBeenLastCalledWith('CloudManager', {
...someCloudAlertManagerConfig,
diff --git a/public/app/features/alerting/unified/components/contact-points/ContactPoints.v1.tsx b/public/app/features/alerting/unified/components/contact-points/ContactPoints.v1.tsx
index 7f1efc7b76f..72b8b78fef8 100644
--- a/public/app/features/alerting/unified/components/contact-points/ContactPoints.v1.tsx
+++ b/public/app/features/alerting/unified/components/contact-points/ContactPoints.v1.tsx
@@ -1,14 +1,14 @@
import React, { useEffect } from 'react';
-import { Route, RouteChildrenProps, Switch, useLocation } from 'react-router-dom';
+import { Route, RouteChildrenProps, Switch } from 'react-router-dom';
import { Alert, LoadingPlaceholder } from '@grafana/ui';
import { useDispatch } from 'app/types';
+import { useAlertmanagerConfig } from '../../hooks/useAlertmanagerConfig';
import { useUnifiedAlertingSelector } from '../../hooks/useUnifiedAlertingSelector';
import { useAlertmanager } from '../../state/AlertmanagerContext';
-import { fetchAlertManagerConfigAction, fetchGrafanaNotifiersAction } from '../../state/actions';
+import { fetchGrafanaNotifiersAction } from '../../state/actions';
import { GRAFANA_RULES_SOURCE_NAME } from '../../utils/datasource';
-import { initialAsyncRequestState } from '../../utils/redux';
import { GrafanaAlertmanagerDeliveryWarning } from '../GrafanaAlertmanagerDeliveryWarning';
import { DuplicateTemplateView } from '../receivers/DuplicateTemplateView';
import { EditReceiverView } from '../receivers/EditReceiverView';
@@ -25,28 +25,10 @@ export interface NotificationErrorProps {
const Receivers = () => {
const { selectedAlertmanager: alertManagerSourceName } = useAlertmanager();
const dispatch = useDispatch();
-
- const location = useLocation();
- const isRoot = location.pathname.endsWith('/alerting/notifications');
- const configRequests = useUnifiedAlertingSelector((state) => state.amConfigs);
-
- const {
- result: config,
- loading,
- error,
- } = (alertManagerSourceName && configRequests[alertManagerSourceName]) || initialAsyncRequestState;
+ const { currentData: config, isLoading: loading, error } = useAlertmanagerConfig(alertManagerSourceName);
const receiverTypes = useUnifiedAlertingSelector((state) => state.grafanaNotifiers);
- const shouldLoadConfig = isRoot || !config;
- // const shouldRenderNotificationStatus = isRoot;
-
- useEffect(() => {
- if (alertManagerSourceName && shouldLoadConfig) {
- dispatch(fetchAlertManagerConfigAction(alertManagerSourceName));
- }
- }, [alertManagerSourceName, dispatch, shouldLoadConfig]);
-
useEffect(() => {
if (
alertManagerSourceName === GRAFANA_RULES_SOURCE_NAME &&
diff --git a/public/app/features/alerting/unified/components/mute-timings/MuteTimingForm.tsx b/public/app/features/alerting/unified/components/mute-timings/MuteTimingForm.tsx
index 9718fe60190..781c25ede1b 100644
--- a/public/app/features/alerting/unified/components/mute-timings/MuteTimingForm.tsx
+++ b/public/app/features/alerting/unified/components/mute-timings/MuteTimingForm.tsx
@@ -4,21 +4,16 @@ import { FormProvider, useForm } from 'react-hook-form';
import { GrafanaTheme2 } from '@grafana/data';
import { Alert, Button, Field, FieldSet, Input, LinkButton, LoadingPlaceholder, useStyles2 } from '@grafana/ui';
-import {
- AlertmanagerConfig,
- AlertManagerCortexConfig,
- MuteTimeInterval,
-} from 'app/plugins/datasource/alertmanager/types';
+import { AlertManagerCortexConfig, MuteTimeInterval } from 'app/plugins/datasource/alertmanager/types';
import { useDispatch } from 'app/types';
-import { useUnifiedAlertingSelector } from '../../hooks/useUnifiedAlertingSelector';
+import { useAlertmanagerConfig } from '../../hooks/useAlertmanagerConfig';
import { useAlertmanager } from '../../state/AlertmanagerContext';
import { updateAlertManagerConfigAction } from '../../state/actions';
import { MuteTimingFields } from '../../types/mute-timing-form';
import { renameMuteTimings } from '../../utils/alertmanager';
import { makeAMLink } from '../../utils/misc';
import { createMuteTiming, defaultTimeInterval } from '../../utils/mute-timings';
-import { initialAsyncRequestState } from '../../utils/redux';
import { ProvisionedResource, ProvisioningAlert } from '../Provisioning';
import { MuteTimingTimeInterval } from './MuteTimingTimeInterval';
@@ -62,21 +57,22 @@ const MuteTimingForm = ({ muteTiming, showError, loading, provenance }: Props) =
const [updating, setUpdating] = useState(false);
- const defaultAmCortexConfig = { alertmanager_config: {}, template_files: {} };
- const amConfigs = useUnifiedAlertingSelector((state) => state.amConfigs);
- const { result = defaultAmCortexConfig } =
- (selectedAlertmanager && amConfigs[selectedAlertmanager]) || initialAsyncRequestState;
+ const { currentData: result } = useAlertmanagerConfig(selectedAlertmanager);
+ const config = result?.alertmanager_config;
- const config: AlertmanagerConfig = result?.alertmanager_config ?? {};
const defaultValues = useDefaultValues(muteTiming);
const formApi = useForm({ defaultValues });
const onSubmit = (values: MuteTimingFields) => {
+ if (!result) {
+ return;
+ }
+
const newMuteTiming = createMuteTiming(values);
const muteTimings = muteTiming
? config?.mute_time_intervals?.filter(({ name }) => name !== muteTiming.name)
- : config.mute_time_intervals;
+ : config?.mute_time_intervals;
const newConfig: AlertManagerCortexConfig = {
...result,
@@ -84,8 +80,8 @@ const MuteTimingForm = ({ muteTiming, showError, loading, provenance }: Props) =
...config,
route:
muteTiming && newMuteTiming.name !== muteTiming.name
- ? renameMuteTimings(newMuteTiming.name, muteTiming.name, config.route ?? {})
- : config.route,
+ ? renameMuteTimings(newMuteTiming.name, muteTiming.name, config?.route ?? {})
+ : config?.route,
mute_time_intervals: [...(muteTimings || []), newMuteTiming],
},
};
diff --git a/public/app/features/alerting/unified/components/mute-timings/MuteTimingsTable.tsx b/public/app/features/alerting/unified/components/mute-timings/MuteTimingsTable.tsx
index e558abf2d5c..86bc55fcbd5 100644
--- a/public/app/features/alerting/unified/components/mute-timings/MuteTimingsTable.tsx
+++ b/public/app/features/alerting/unified/components/mute-timings/MuteTimingsTable.tsx
@@ -5,15 +5,14 @@ import { GrafanaTheme2 } from '@grafana/data';
import { Stack } from '@grafana/experimental';
import { IconButton, LinkButton, Link, useStyles2, ConfirmModal } from '@grafana/ui';
import { contextSrv } from 'app/core/services/context_srv';
-import { AlertManagerCortexConfig, MuteTimeInterval } from 'app/plugins/datasource/alertmanager/types';
-import { useDispatch } from 'app/types';
+import { MuteTimeInterval } from 'app/plugins/datasource/alertmanager/types';
+import { useDispatch } from 'app/types/store';
import { Authorize } from '../../components/Authorize';
-import { useUnifiedAlertingSelector } from '../../hooks/useUnifiedAlertingSelector';
+import { useAlertmanagerConfig } from '../../hooks/useAlertmanagerConfig';
import { deleteMuteTimingAction } from '../../state/actions';
import { getNotificationsPermissions } from '../../utils/access-control';
import { makeAMLink } from '../../utils/misc';
-import { AsyncRequestState, initialAsyncRequestState } from '../../utils/redux';
import { DynamicTable, DynamicTableItemProps, DynamicTableColumnProps } from '../DynamicTable';
import { EmptyAreaWithCTA } from '../EmptyAreaWithCTA';
import { ProvisioningBadge } from '../Provisioning';
@@ -31,14 +30,18 @@ export const MuteTimingsTable = ({ alertManagerSourceName, muteTimingNames, hide
const styles = useStyles2(getStyles);
const dispatch = useDispatch();
const permissions = getNotificationsPermissions(alertManagerSourceName);
- const amConfigs = useUnifiedAlertingSelector((state) => state.amConfigs);
+
+ const { currentData } = useAlertmanagerConfig(alertManagerSourceName, {
+ refetchOnFocus: true,
+ refetchOnReconnect: true,
+ });
+ const config = currentData?.alertmanager_config;
+
const [muteTimingName, setMuteTimingName] = useState('');
- const { result }: AsyncRequestState =
- (alertManagerSourceName && amConfigs[alertManagerSourceName]) || initialAsyncRequestState;
const items = useMemo((): Array> => {
- const muteTimings = result?.alertmanager_config?.mute_time_intervals ?? [];
- const muteTimingsProvenances = result?.alertmanager_config?.muteTimeProvenances ?? {};
+ const muteTimings = config?.mute_time_intervals ?? [];
+ const muteTimingsProvenances = config?.muteTimeProvenances ?? {};
return muteTimings
.filter(({ name }) => (muteTimingNames ? muteTimingNames.includes(name) : true))
@@ -51,11 +54,7 @@ export const MuteTimingsTable = ({ alertManagerSourceName, muteTimingNames, hide
},
};
});
- }, [
- result?.alertmanager_config?.mute_time_intervals,
- result?.alertmanager_config?.muteTimeProvenances,
- muteTimingNames,
- ]);
+ }, [config?.mute_time_intervals, config?.muteTimeProvenances, muteTimingNames]);
const columns = useColumns(alertManagerSourceName, hideActions, setMuteTimingName);
@@ -100,7 +99,10 @@ export const MuteTimingsTable = ({ alertManagerSourceName, muteTimingNames, hide
title="Delete mute timing"
body={`Are you sure you would like to delete "${muteTimingName}"`}
confirmText="Delete"
- onConfirm={() => dispatch(deleteMuteTimingAction(alertManagerSourceName, muteTimingName))}
+ onConfirm={() => {
+ dispatch(deleteMuteTimingAction(alertManagerSourceName, muteTimingName));
+ setMuteTimingName('');
+ }}
onDismiss={() => setMuteTimingName('')}
/>
)}
diff --git a/public/app/features/alerting/unified/components/rule-editor/notificaton-preview/useAlertmanagerNotificationRoutingPreview.ts b/public/app/features/alerting/unified/components/rule-editor/notificaton-preview/useAlertmanagerNotificationRoutingPreview.ts
index 77f99002f68..f0ead67585c 100644
--- a/public/app/features/alerting/unified/components/rule-editor/notificaton-preview/useAlertmanagerNotificationRoutingPreview.ts
+++ b/public/app/features/alerting/unified/components/rule-editor/notificaton-preview/useAlertmanagerNotificationRoutingPreview.ts
@@ -15,17 +15,14 @@ export const useAlertmanagerNotificationRoutingPreview = (
alertManagerSourceName: string,
potentialInstances: Labels[]
) => {
- const {
- config: AMConfig,
- loading: configLoading,
- error: configError,
- } = useAlertmanagerConfig(alertManagerSourceName);
+ const { currentData, isLoading: configLoading, error: configError } = useAlertmanagerConfig(alertManagerSourceName);
+ const config = currentData?.alertmanager_config;
const { matchInstancesToRoute } = useRouteGroupsMatcher();
// to create the list of matching contact points we need to first get the rootRoute
const { rootRoute, receivers } = useMemo(() => {
- if (!AMConfig) {
+ if (!config) {
return {
receivers: [],
rootRoute: undefined,
@@ -33,10 +30,10 @@ export const useAlertmanagerNotificationRoutingPreview = (
}
return {
- rootRoute: AMConfig.route ? normalizeRoute(addUniqueIdentifierToRoute(AMConfig.route)) : undefined,
- receivers: AMConfig.receivers ?? [],
+ rootRoute: config.route ? normalizeRoute(addUniqueIdentifierToRoute(config.route)) : undefined,
+ receivers: config.receivers ?? [],
};
- }, [AMConfig]);
+ }, [config]);
// create maps for routes to be get by id, this map also contains the path to the route
// ⚠️ don't forget to compute the inherited tree before using this map
diff --git a/public/app/features/alerting/unified/hooks/useAlertmanagerConfig.ts b/public/app/features/alerting/unified/hooks/useAlertmanagerConfig.ts
index be9e98a7c38..05067599dde 100644
--- a/public/app/features/alerting/unified/hooks/useAlertmanagerConfig.ts
+++ b/public/app/features/alerting/unified/hooks/useAlertmanagerConfig.ts
@@ -1,26 +1,23 @@
-import { useEffect } from 'react';
+import { SerializedError } from '@reduxjs/toolkit';
-import { useDispatch } from 'app/types';
+import { alertmanagerApi } from '../api/alertmanagerApi';
-import { fetchAlertManagerConfigAction } from '../state/actions';
-import { initialAsyncRequestState } from '../utils/redux';
+type Options = {
+ refetchOnFocus: boolean;
+ refetchOnReconnect: boolean;
+};
-import { useUnifiedAlertingSelector } from './useUnifiedAlertingSelector';
+// TODO refactor this so we can just call "alertmanagerApi.endpoints.getAlertmanagerConfiguration" everywhere
+// and remove this hook since it adds little value
+export function useAlertmanagerConfig(amSourceName?: string, options?: Options) {
+ const fetchConfig = alertmanagerApi.endpoints.getAlertmanagerConfiguration.useQuery(amSourceName ?? '', {
+ ...options,
+ skip: !amSourceName,
+ });
-export function useAlertmanagerConfig(amSourceName?: string) {
- const dispatch = useDispatch();
-
- useEffect(() => {
- if (amSourceName) {
- dispatch(fetchAlertManagerConfigAction(amSourceName));
- }
- }, [amSourceName, dispatch]);
-
- const amConfigs = useUnifiedAlertingSelector((state) => state.amConfigs);
-
- const { result, loading, error } = (amSourceName && amConfigs[amSourceName]) || initialAsyncRequestState;
-
- const config = result?.alertmanager_config;
-
- return { result, config, loading, error };
+ return {
+ ...fetchConfig,
+ // TODO refactor to get rid of this type assertion
+ error: fetchConfig.error as SerializedError,
+ };
}
diff --git a/public/app/features/alerting/unified/hooks/useMuteTimingOptions.ts b/public/app/features/alerting/unified/hooks/useMuteTimingOptions.ts
index 8047a30f00b..675c9219758 100644
--- a/public/app/features/alerting/unified/hooks/useMuteTimingOptions.ts
+++ b/public/app/features/alerting/unified/hooks/useMuteTimingOptions.ts
@@ -1,22 +1,18 @@
import { useMemo } from 'react';
import { SelectableValue } from '@grafana/data';
-import { AlertmanagerConfig } from 'app/plugins/datasource/alertmanager/types';
import { useAlertmanager } from '../state/AlertmanagerContext';
import { timeIntervalToString } from '../utils/alertmanager';
-import { initialAsyncRequestState } from '../utils/redux';
-import { useUnifiedAlertingSelector } from './useUnifiedAlertingSelector';
+import { useAlertmanagerConfig } from './useAlertmanagerConfig';
export function useMuteTimingOptions(): Array> {
const { selectedAlertmanager } = useAlertmanager();
- const amConfigs = useUnifiedAlertingSelector((state) => state.amConfigs);
+ const { currentData } = useAlertmanagerConfig(selectedAlertmanager);
+ const config = currentData?.alertmanager_config;
return useMemo(() => {
- const { result } = (selectedAlertmanager && amConfigs[selectedAlertmanager]) || initialAsyncRequestState;
- const config: AlertmanagerConfig = result?.alertmanager_config ?? {};
-
const muteTimingsOptions: Array> =
config?.mute_time_intervals?.map((value) => ({
value: value.name,
@@ -25,5 +21,5 @@ export function useMuteTimingOptions(): Array> {
})) ?? [];
return muteTimingsOptions;
- }, [selectedAlertmanager, amConfigs]);
+ }, [config]);
}
diff --git a/public/app/features/alerting/unified/state/actions.ts b/public/app/features/alerting/unified/state/actions.ts
index 6b3aab4e1c7..f300639c451 100644
--- a/public/app/features/alerting/unified/state/actions.ts
+++ b/public/app/features/alerting/unified/state/actions.ts
@@ -40,18 +40,16 @@ import {
deleteAlertManagerConfig,
expireSilence,
fetchAlertGroups,
- fetchAlertManagerConfig,
fetchAlerts,
fetchExternalAlertmanagerConfig,
fetchExternalAlertmanagers,
fetchSilences,
- fetchStatus,
testReceivers,
updateAlertManagerConfig,
} from '../api/alertmanager';
+import { alertmanagerApi } from '../api/alertmanagerApi';
import { fetchAnnotations } from '../api/annotations';
import { discoverFeatures } from '../api/buildInfo';
-import { featureDiscoveryApi } from '../api/featureDiscoveryApi';
import { fetchNotifiers } from '../api/grafana';
import { FetchPromRulesFilter, fetchRules } from '../api/prometheus';
import {
@@ -68,17 +66,14 @@ import {
getRulesDataSource,
getRulesSourceName,
GRAFANA_RULES_SOURCE_NAME,
- isVanillaPrometheusAlertManagerDataSource,
} from '../utils/datasource';
-import { makeAMLink, retryWhile } from '../utils/misc';
-import { AsyncRequestMapSlice, messageFromError, withAppEvents, withSerializedError } from '../utils/redux';
+import { makeAMLink } from '../utils/misc';
+import { AsyncRequestMapSlice, withAppEvents, withSerializedError } from '../utils/redux';
import * as ruleId from '../utils/rule-id';
import { getRulerClient } from '../utils/rulerClient';
import { getAlertInfo, isRulerNotSupportedResponse } from '../utils/rules';
import { safeParseDurationstr } from '../utils/time';
-const FETCH_CONFIG_RETRY_TIMEOUT = 30 * 1000;
-
function getDataSourceConfig(getState: () => unknown, rulesSourceName: string) {
const dataSources = (getState() as StoreState).unifiedAlerting.dataSources;
const dsConfig = dataSources[rulesSourceName]?.result;
@@ -131,76 +126,6 @@ export const fetchPromRulesAction = createAsyncThunk(
}
);
-export const fetchAlertManagerConfigAction = createAsyncThunk(
- 'unifiedalerting/fetchAmConfig',
- (alertManagerSourceName: string, thunkAPI): Promise =>
- withSerializedError(
- (async () => {
- // for vanilla prometheus, there is no config endpoint. Only fetch config from status
- if (isVanillaPrometheusAlertManagerDataSource(alertManagerSourceName)) {
- return fetchStatus(alertManagerSourceName).then((status) => ({
- alertmanager_config: status.config,
- template_files: {},
- }));
- }
-
- const { data: amFeatures } = await thunkAPI.dispatch(
- featureDiscoveryApi.endpoints.discoverAmFeatures.initiate({
- amSourceName: alertManagerSourceName,
- })
- );
-
- const lazyConfigInitSupported = amFeatures?.lazyConfigInit ?? false;
- const fetchAMconfigWithLogging = withPerformanceLogging(
- fetchAlertManagerConfig,
- `[${alertManagerSourceName}] Alertmanager config loaded`,
- {
- dataSourceName: alertManagerSourceName,
- thunk: 'unifiedalerting/fetchAmConfig',
- }
- );
-
- return retryWhile(
- () => fetchAMconfigWithLogging(alertManagerSourceName),
- // if config has been recently deleted, it takes a while for cortex start returning the default one.
- // retry for a short while instead of failing
- (e) => !!messageFromError(e)?.includes('alertmanager storage object not found') && !lazyConfigInitSupported,
- FETCH_CONFIG_RETRY_TIMEOUT
- )
- .then((result) => {
- // if user config is empty for cortex alertmanager, try to get config from status endpoint
- if (
- isEmpty(result.alertmanager_config) &&
- isEmpty(result.template_files) &&
- alertManagerSourceName !== GRAFANA_RULES_SOURCE_NAME
- ) {
- return fetchStatus(alertManagerSourceName).then((status) => ({
- alertmanager_config: status.config,
- template_files: {},
- template_file_provenances: result.template_file_provenances,
- last_applied: result.last_applied,
- id: result.id,
- }));
- }
- return result;
- })
- .catch((e) => {
- // When mimir doesn't have fallback AM url configured the default response will be as above
- // However it's fine, and it's possible to create AM configuration
- if (lazyConfigInitSupported && messageFromError(e)?.includes('alertmanager storage object not found')) {
- return Promise.resolve({
- alertmanager_config: {},
- template_files: {},
- template_file_provenances: {},
- });
- }
-
- throw e;
- });
- })()
- )
-);
-
export const fetchExternalAlertmanagersAction = createAsyncThunk(
'unifiedAlerting/fetchExternalAlertmanagers',
(): Promise => {
@@ -585,8 +510,9 @@ export const updateAlertManagerConfigAction = createAsyncThunk {
- // TODO there must be a better way here than to dispatch another fetch as this causes re-rendering :(
- const latestConfig = await thunkAPI.dispatch(fetchAlertManagerConfigAction(alertManagerSourceName)).unwrap();
+ const latestConfig = await thunkAPI
+ .dispatch(alertmanagerApi.endpoints.getAlertmanagerConfiguration.initiate(alertManagerSourceName))
+ .unwrap();
const isLatestConfigEmpty = isEmpty(latestConfig.alertmanager_config) && isEmpty(latestConfig.template_files);
const oldLastConfigsDiffer = JSON.stringify(latestConfig) !== JSON.stringify(oldConfig);
@@ -598,7 +524,7 @@ export const updateAlertManagerConfigAction = createAsyncThunk => {
- return (dispatch, getState) => {
- const config = getState().unifiedAlerting.amConfigs?.[alertManagerSourceName]?.result;
+ return async (dispatch) => {
+ const config = await dispatch(
+ alertmanagerApi.endpoints.getAlertmanagerConfiguration.initiate(alertManagerSourceName)
+ ).unwrap();
+
if (!config) {
throw new Error(`Config for ${alertManagerSourceName} not found`);
}
@@ -682,8 +611,11 @@ export const deleteReceiverAction = (receiverName: string, alertManagerSourceNam
};
export const deleteTemplateAction = (templateName: string, alertManagerSourceName: string): ThunkResult => {
- return (dispatch, getState) => {
- const config = getState().unifiedAlerting.amConfigs?.[alertManagerSourceName]?.result;
+ return async (dispatch) => {
+ const config = await dispatch(
+ alertmanagerApi.endpoints.getAlertmanagerConfiguration.initiate(alertManagerSourceName)
+ ).unwrap();
+
if (!config) {
throw new Error(`Config for ${alertManagerSourceName} not found`);
}
@@ -739,7 +671,7 @@ export const deleteAlertManagerConfigAction = createAsyncThunk(
withSerializedError(
(async () => {
await deleteAlertManagerConfig(alertManagerSourceName);
- await thunkAPI.dispatch(fetchAlertManagerConfigAction(alertManagerSourceName));
+ await thunkAPI.dispatch(alertmanagerApi.util.invalidateTags(['AlertmanagerConfiguration']));
})()
),
{
@@ -751,8 +683,10 @@ export const deleteAlertManagerConfigAction = createAsyncThunk(
);
export const deleteMuteTimingAction = (alertManagerSourceName: string, muteTimingName: string): ThunkResult => {
- return async (dispatch, getState) => {
- const config = getState().unifiedAlerting.amConfigs[alertManagerSourceName].result;
+ return async (dispatch) => {
+ const config = await dispatch(
+ alertmanagerApi.endpoints.getAlertmanagerConfiguration.initiate(alertManagerSourceName)
+ ).unwrap();
const muteIntervals =
config?.alertmanager_config?.mute_time_intervals?.filter(({ name }) => name !== muteTimingName) ?? [];
diff --git a/public/app/features/alerting/unified/state/reducers.ts b/public/app/features/alerting/unified/state/reducers.ts
index 41d81124d22..c6e2c1c531b 100644
--- a/public/app/features/alerting/unified/state/reducers.ts
+++ b/public/app/features/alerting/unified/state/reducers.ts
@@ -6,7 +6,6 @@ import {
createOrUpdateSilenceAction,
deleteAlertManagerConfigAction,
fetchAlertGroupsAction,
- fetchAlertManagerConfigAction,
fetchAmAlertsAction,
fetchEditableRuleAction,
fetchExternalAlertmanagersAction,
@@ -33,11 +32,6 @@ export const reducer = combineReducers({
promRules: createAsyncMapSlice('promRules', fetchPromRulesAction, ({ rulesSourceName }) => rulesSourceName).reducer,
rulerRules: createAsyncMapSlice('rulerRules', fetchRulerRulesAction, ({ rulesSourceName }) => rulesSourceName)
.reducer,
- amConfigs: createAsyncMapSlice(
- 'amConfigs',
- fetchAlertManagerConfigAction,
- (alertManagerSourceName) => alertManagerSourceName
- ).reducer,
silences: createAsyncMapSlice('silences', fetchSilencesAction, (alertManagerSourceName) => alertManagerSourceName)
.reducer,
ruleForm: combineReducers({