Alerting: Refactor fetch for receivers for using RTKQuery (#56624)

* Refactor fetch for reveivers for using  RTKQuery

* Simplify receiversApi contactPointsState endpoint

* Fix tests and create useGetContactPointsState hook for using receiversApi.useContactPointsStateQuery
This commit is contained in:
Sonia Aguilar 2022-10-13 10:00:59 +02:00 committed by GitHub
parent 5af36e48ba
commit 277803a894
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 65 additions and 47 deletions

View File

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

View File

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

View File

@ -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<ContactPointsState, string>({
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;
};

View File

@ -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[] = [
{

View File

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

View File

@ -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<NotifierDTO[]> => withSerializedError(fetchNotifiers())
);
export const fetchContactPointsStateAction = createAsyncThunk(
'unifiedalerting/fetchContactPointsState',
(alertManagerSourceName: string): Promise<ContactPointsState> =>
withSerializedError(fetchContactPointsState(alertManagerSourceName))
);
export const fetchGrafanaAnnotationsAction = createAsyncThunk(
'unifiedalerting/fetchGrafanaAnnotations',
(alertId: string): Promise<StateHistoryItem[]> => withSerializedError(fetchAnnotations(alertId))

View File

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