diff --git a/public/app/features/alerting/unified/Receivers.test.tsx b/public/app/features/alerting/unified/Receivers.test.tsx index d69d5685742..5c7773f0c7e 100644 --- a/public/app/features/alerting/unified/Receivers.test.tsx +++ b/public/app/features/alerting/unified/Receivers.test.tsx @@ -17,7 +17,8 @@ import { AccessControlAction, ContactPointsState } from 'app/types'; import Receivers from './Receivers'; import { updateAlertManagerConfig, fetchAlertManagerConfig, fetchStatus, testReceivers } from './api/alertmanager'; import { discoverAlertmanagerFeatures } from './api/buildInfo'; -import { fetchNotifiers, fetchContactPointsState } from './api/grafana'; +import { fetchNotifiers } from './api/grafana'; +import * as receiversApi from './api/receiversApi'; import { mockDataSource, MockDataSourceSrv, @@ -29,7 +30,6 @@ import { grafanaNotifiersMock } from './mocks/grafana-notifiers'; import { getAllDataSources } from './utils/config'; import { ALERTMANAGER_NAME_LOCAL_STORAGE_KEY, ALERTMANAGER_NAME_QUERY_KEY } from './utils/constants'; import { DataSourceType, GRAFANA_RULES_SOURCE_NAME } from './utils/datasource'; - jest.mock('./api/alertmanager'); jest.mock('./api/grafana'); jest.mock('./utils/config'); @@ -46,7 +46,9 @@ const mocks = { fetchNotifiers: jest.mocked(fetchNotifiers), testReceivers: jest.mocked(testReceivers), discoverAlertmanagerFeatures: jest.mocked(discoverAlertmanagerFeatures), - fetchReceivers: jest.mocked(fetchContactPointsState), + }, + hooks: { + useGetContactPointsState: jest.spyOn(receiversApi, 'useGetContactPointsState'), }, contextSrv: jest.mocked(contextSrv), }; @@ -130,6 +132,7 @@ const clickSelectOption = async (selectElement: HTMLElement, optionText: string) }; document.addEventListener('click', interceptLinkClicks); +const emptyContactPointsState: ContactPointsState = { receivers: {}, errorCount: 0 }; describe('Receivers', () => { beforeEach(() => { @@ -137,8 +140,7 @@ describe('Receivers', () => { mocks.getAllDataSources.mockReturnValue(Object.values(dataSources)); mocks.api.fetchNotifiers.mockResolvedValue(grafanaNotifiersMock); mocks.api.discoverAlertmanagerFeatures.mockResolvedValue({ lazyConfigInit: false }); - const emptyContactPointsState: ContactPointsState = { receivers: {}, errorCount: 0 }; - mocks.api.fetchReceivers.mockResolvedValue(emptyContactPointsState); + mocks.hooks.useGetContactPointsState.mockReturnValue(emptyContactPointsState); setDataSourceSrv(new MockDataSourceSrv(dataSources)); mocks.contextSrv.isEditor = true; store.delete(ALERTMANAGER_NAME_LOCAL_STORAGE_KEY); @@ -327,6 +329,7 @@ describe('Receivers', () => { (a) => a === action ) ); + mocks.hooks.useGetContactPointsState.mockReturnValue(emptyContactPointsState); renderReceivers(); expect(ui.newContactPointButton.query()).not.toBeInTheDocument(); @@ -543,7 +546,7 @@ describe('Receivers', () => { errorCount: 1, }; - mocks.api.fetchReceivers.mockResolvedValue(receiversMock); + mocks.hooks.useGetContactPointsState.mockReturnValue(receiversMock); await renderReceivers(); // @@ -614,7 +617,7 @@ describe('Receivers', () => { errorCount: 1, }; - mocks.api.fetchReceivers.mockResolvedValue(receiversMock); + mocks.hooks.useGetContactPointsState.mockReturnValue(receiversMock); await renderReceivers(); // @@ -645,11 +648,11 @@ describe('Receivers', () => { expect(criticalDetailTableRows[1]).toHaveTextContent('117.2455ms'); }); - it('Should not render error notifications when fetchContactPointsState raises 404 error ', async () => { + it('Should not render error notifications when fetching contact points state raises 404 error ', async () => { mocks.api.fetchConfig.mockResolvedValue(someGrafanaAlertManagerConfig); mocks.api.updateConfig.mockResolvedValue(); - mocks.api.fetchReceivers.mockRejectedValue({ status: 404 }); + mocks.hooks.useGetContactPointsState.mockReturnValue(emptyContactPointsState); await renderReceivers(); await ui.receiversTable.find(); diff --git a/public/app/features/alerting/unified/Receivers.tsx b/public/app/features/alerting/unified/Receivers.tsx index b58204efec8..f73986b82ac 100644 --- a/public/app/features/alerting/unified/Receivers.tsx +++ b/public/app/features/alerting/unified/Receivers.tsx @@ -7,6 +7,9 @@ import { NavModelItem, GrafanaTheme2 } from '@grafana/data'; import { Alert, LoadingPlaceholder, withErrorBoundary, useStyles2, Icon, Stack } from '@grafana/ui'; import { useDispatch } from 'app/types'; +import { ContactPointsState } from '../../../types/alerting'; + +import { useGetContactPointsState } from './api/receiversApi'; import { AlertManagerPicker } from './components/AlertManagerPicker'; import { AlertingPageWrapper } from './components/AlertingPageWrapper'; import { NoAlertManagerWarning } from './components/NoAlertManagerWarning'; @@ -19,12 +22,7 @@ import { ReceiversAndTemplatesView } from './components/receivers/ReceiversAndTe import { useAlertManagerSourceName } from './hooks/useAlertManagerSourceName'; import { useAlertManagersByPermission } from './hooks/useAlertManagerSources'; import { useUnifiedAlertingSelector } from './hooks/useUnifiedAlertingSelector'; -import { - fetchAlertManagerConfigAction, - fetchContactPointsStateAction, - fetchGrafanaNotifiersAction, -} from './state/actions'; -import { CONTACT_POINTS_STATE_INTERVAL_MS } from './utils/constants'; +import { fetchAlertManagerConfigAction, fetchGrafanaNotifiersAction } from './state/actions'; import { GRAFANA_RULES_SOURCE_NAME } from './utils/datasource'; import { initialAsyncRequestState } from './utils/redux'; @@ -63,7 +61,6 @@ const Receivers: FC = () => { const isRoot = location.pathname.endsWith('/alerting/notifications'); const configRequests = useUnifiedAlertingSelector((state) => state.amConfigs); - const contactPointsStateRequest = useUnifiedAlertingSelector((state) => state.contactPointsState); const { result: config, @@ -71,9 +68,6 @@ const Receivers: FC = () => { error, } = (alertManagerSourceName && configRequests[alertManagerSourceName]) || initialAsyncRequestState; - const { result: contactPointsState } = - (alertManagerSourceName && contactPointsStateRequest) || initialAsyncRequestState; - const receiverTypes = useUnifiedAlertingSelector((state) => state.grafanaNotifiers); const shouldLoadConfig = isRoot || !config; @@ -93,20 +87,7 @@ const Receivers: FC = () => { dispatch(fetchGrafanaNotifiersAction()); } }, [alertManagerSourceName, dispatch, receiverTypes]); - - useEffect(() => { - function fetchContactPointStates() { - if (shouldRenderNotificationStatus && alertManagerSourceName) { - dispatch(fetchContactPointsStateAction(alertManagerSourceName)); - } - } - fetchContactPointStates(); - const interval = setInterval(fetchContactPointStates, CONTACT_POINTS_STATE_INTERVAL_MS); - return () => { - clearInterval(interval); - }; - }, [shouldRenderNotificationStatus, alertManagerSourceName, dispatch]); - + const contactPointsState: ContactPointsState = useGetContactPointsState(alertManagerSourceName ?? ''); const integrationsErrorCount = contactPointsState?.errorCount ?? 0; const disableAmSelect = !isRoot; diff --git a/public/app/features/alerting/unified/api/receiversApi.ts b/public/app/features/alerting/unified/api/receiversApi.ts new file mode 100644 index 00000000000..3dc45ba8215 --- /dev/null +++ b/public/app/features/alerting/unified/api/receiversApi.ts @@ -0,0 +1,29 @@ +import { ContactPointsState, ReceiversStateDTO } from 'app/types'; + +import { CONTACT_POINTS_STATE_INTERVAL_MS } from '../utils/constants'; +import { getDatasourceAPIUid } from '../utils/datasource'; + +import { alertingApi } from './alertingApi'; +import { contactPointsStateDtoToModel } from './grafana'; + +export const receiversApi = alertingApi.injectEndpoints({ + endpoints: (build) => ({ + contactPointsState: build.query({ + query: (amSourceName) => ({ + url: `/api/alertmanager/${getDatasourceAPIUid(amSourceName)}/config/api/v1/receivers`, + }), + transformResponse: (receivers: ReceiversStateDTO[]) => { + return contactPointsStateDtoToModel(receivers); + }, + }), + }), +}); + +export const useGetContactPointsState = (alertManagerSourceName: string) => { + const contactPointsStateEmpty: ContactPointsState = { receivers: {}, errorCount: 0 }; + const { currentData: contactPointsState } = receiversApi.useContactPointsStateQuery(alertManagerSourceName ?? '', { + skip: !alertManagerSourceName, + pollingInterval: CONTACT_POINTS_STATE_INTERVAL_MS, + }); + return contactPointsState ?? contactPointsStateEmpty; +}; diff --git a/public/app/features/alerting/unified/components/receivers/ReceiversTable.test.tsx b/public/app/features/alerting/unified/components/receivers/ReceiversTable.test.tsx index dc80a0c4b4d..6b10fa3373c 100644 --- a/public/app/features/alerting/unified/components/receivers/ReceiversTable.test.tsx +++ b/public/app/features/alerting/unified/components/receivers/ReceiversTable.test.tsx @@ -10,8 +10,9 @@ import { Receiver, } from 'app/plugins/datasource/alertmanager/types'; import { configureStore } from 'app/store/configureStore'; -import { NotifierDTO, NotifierType } from 'app/types'; +import { ContactPointsState, NotifierDTO, NotifierType } from 'app/types'; +import { useGetContactPointsState } from '../../api/receiversApi'; import { fetchGrafanaNotifiersAction } from '../../state/actions'; import { ReceiversTable } from './ReceiversTable'; @@ -52,7 +53,20 @@ const mockNotifier = (type: NotifierType, name: string): NotifierDTO => ({ options: [], }); +jest.mock('../../api/receiversApi', () => ({ + ...jest.requireActual('../../api/receiversApi'), + useGetContactPointsState: jest.fn(), +})); + +const useGetContactPointsStateMock = jest.mocked(useGetContactPointsState); + describe('ReceiversTable', () => { + beforeEach(() => { + jest.resetAllMocks(); + const emptyContactPointsState: ContactPointsState = { receivers: {}, errorCount: 0 }; + useGetContactPointsStateMock.mockReturnValue(emptyContactPointsState); + }); + it('render receivers with grafana notifiers', async () => { const receivers: Receiver[] = [ { diff --git a/public/app/features/alerting/unified/components/receivers/ReceiversTable.tsx b/public/app/features/alerting/unified/components/receivers/ReceiversTable.tsx index 216e6fd7570..339561b2519 100644 --- a/public/app/features/alerting/unified/components/receivers/ReceiversTable.tsx +++ b/public/app/features/alerting/unified/components/receivers/ReceiversTable.tsx @@ -8,6 +8,7 @@ import { contextSrv } from 'app/core/services/context_srv'; import { AlertManagerCortexConfig, Receiver } from 'app/plugins/datasource/alertmanager/types'; import { useDispatch, AccessControlAction, ContactPointsState, NotifiersState, ReceiversState } from 'app/types'; +import { useGetContactPointsState } from '../../api/receiversApi'; import { Authorize } from '../../components/Authorize'; import { useUnifiedAlertingSelector } from '../../hooks/useUnifiedAlertingSelector'; import { deleteReceiverAction } from '../../state/actions'; @@ -17,7 +18,6 @@ import { isReceiverUsed } from '../../utils/alertmanager'; import { isVanillaPrometheusAlertManagerDataSource } from '../../utils/datasource'; import { makeAMLink } from '../../utils/misc'; import { extractNotifierTypeCounts } from '../../utils/receivers'; -import { initialAsyncRequestState } from '../../utils/redux'; import { DynamicTable, DynamicTableColumnProps, DynamicTableItemProps } from '../DynamicTable'; import { ProvisioningBadge } from '../Provisioning'; import { ActionIcon } from '../rules/ActionIcon'; @@ -122,8 +122,7 @@ function ReceiverHealth({ errorsByReceiver, someWithNoAttempt }: ReceiverHealthP ); } const useContactPointsState = (alertManagerName: string) => { - const contactPointsStateRequest = useUnifiedAlertingSelector((state) => state.contactPointsState); - const { result: contactPointsState } = (alertManagerName && contactPointsStateRequest) || initialAsyncRequestState; + const contactPointsState = useGetContactPointsState(alertManagerName ?? ''); const receivers: ReceiversState = contactPointsState?.receivers ?? {}; const errorStateAvailable = Object.keys(receivers).length > 0; // this logic can change depending on how we implement this in the BE return { contactPointsState, errorStateAvailable }; diff --git a/public/app/features/alerting/unified/state/actions.ts b/public/app/features/alerting/unified/state/actions.ts index 749e3e6f03d..8fcfe09d197 100644 --- a/public/app/features/alerting/unified/state/actions.ts +++ b/public/app/features/alerting/unified/state/actions.ts @@ -13,7 +13,7 @@ import { SilenceCreatePayload, TestReceiversAlert, } from 'app/plugins/datasource/alertmanager/types'; -import { ContactPointsState, FolderDTO, NotifierDTO, StoreState, ThunkResult } from 'app/types'; +import { FolderDTO, NotifierDTO, StoreState, ThunkResult } from 'app/types'; import { CombinedRuleGroup, CombinedRuleNamespace, @@ -51,7 +51,7 @@ import { import { fetchAnnotations } from '../api/annotations'; import { discoverFeatures } from '../api/buildInfo'; import { featureDiscoveryApi } from '../api/featureDiscoveryApi'; -import { fetchContactPointsState, fetchNotifiers } from '../api/grafana'; +import { fetchNotifiers } from '../api/grafana'; import { FetchPromRulesFilter, fetchRules } from '../api/prometheus'; import { deleteNamespace, @@ -459,12 +459,6 @@ export const fetchGrafanaNotifiersAction = createAsyncThunk( (): Promise => withSerializedError(fetchNotifiers()) ); -export const fetchContactPointsStateAction = createAsyncThunk( - 'unifiedalerting/fetchContactPointsState', - (alertManagerSourceName: string): Promise => - withSerializedError(fetchContactPointsState(alertManagerSourceName)) -); - export const fetchGrafanaAnnotationsAction = createAsyncThunk( 'unifiedalerting/fetchGrafanaAnnotations', (alertId: string): Promise => withSerializedError(fetchAnnotations(alertId)) diff --git a/public/app/features/alerting/unified/state/reducers.ts b/public/app/features/alerting/unified/state/reducers.ts index 2e927acb8c9..41d81124d22 100644 --- a/public/app/features/alerting/unified/state/reducers.ts +++ b/public/app/features/alerting/unified/state/reducers.ts @@ -8,7 +8,6 @@ import { fetchAlertGroupsAction, fetchAlertManagerConfigAction, fetchAmAlertsAction, - fetchContactPointsStateAction, fetchEditableRuleAction, fetchExternalAlertmanagersAction, fetchExternalAlertmanagersConfigAction, @@ -46,7 +45,6 @@ export const reducer = combineReducers({ existingRule: createAsyncSlice('existingRule', fetchEditableRuleAction).reducer, }), grafanaNotifiers: createAsyncSlice('grafanaNotifiers', fetchGrafanaNotifiersAction).reducer, - contactPointsState: createAsyncSlice('contactPointsState', fetchContactPointsStateAction).reducer, saveAMConfig: createAsyncSlice('saveAMConfig', updateAlertManagerConfigAction).reducer, deleteAMConfig: createAsyncSlice('deleteAMConfig', deleteAlertManagerConfigAction).reducer, updateSilence: createAsyncSlice('updateSilence', createOrUpdateSilenceAction).reducer,