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