Alerting: Add Alertmanager choice warning (#55311)

pull/57391/head^2
Konrad Lalik 3 years ago committed by GitHub
parent e402a8f27d
commit 5c710a5590
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 19
      public/app/features/alerting/unified/AlertGroups.tsx
  2. 8
      public/app/features/alerting/unified/AmRoutes.tsx
  3. 45
      public/app/features/alerting/unified/Receivers.test.tsx
  4. 62
      public/app/features/alerting/unified/Receivers.tsx
  5. 9
      public/app/features/alerting/unified/Silences.tsx
  6. 1
      public/app/features/alerting/unified/api/alertingApi.ts
  7. 2
      public/app/features/alerting/unified/api/alertmanager.ts
  8. 37
      public/app/features/alerting/unified/api/alertmanagerApi.ts
  9. 47
      public/app/features/alerting/unified/components/GrafanaAlertmanagerDeliveryWarning.test.tsx
  10. 46
      public/app/features/alerting/unified/components/GrafanaAlertmanagerDeliveryWarning.tsx
  11. 60
      public/app/features/alerting/unified/components/admin/ExternalAlertmanagers.tsx
  12. 303
      public/app/features/alerting/unified/hooks/useExternalAMSelector.test.tsx
  13. 22
      public/app/features/alerting/unified/hooks/useExternalAmSelector.ts
  14. 20
      public/app/features/alerting/unified/mocks/alertmanagerApi.ts

@ -6,6 +6,9 @@ import { Alert, LoadingPlaceholder, useStyles2 } from '@grafana/ui';
import { useQueryParams } from 'app/core/hooks/useQueryParams';
import { useDispatch } from 'app/types';
import { AlertmanagerChoice } from '../../../plugins/datasource/alertmanager/types';
import { alertmanagerApi } from './api/alertmanagerApi';
import { AlertingPageWrapper } from './components/AlertingPageWrapper';
import { NoAlertManagerWarning } from './components/NoAlertManagerWarning';
import { AlertGroup } from './components/alert-groups/AlertGroup';
@ -17,10 +20,13 @@ import { useGroupedAlerts } from './hooks/useGroupedAlerts';
import { useUnifiedAlertingSelector } from './hooks/useUnifiedAlertingSelector';
import { fetchAlertGroupsAction } from './state/actions';
import { NOTIFICATIONS_POLL_INTERVAL_MS } from './utils/constants';
import { GRAFANA_RULES_SOURCE_NAME } from './utils/datasource';
import { getFiltersFromUrlParams } from './utils/misc';
import { initialAsyncRequestState } from './utils/redux';
const AlertGroups = () => {
const { useGetAlertmanagerChoiceQuery } = alertmanagerApi;
const alertManagers = useAlertManagersByPermission('instance');
const [alertManagerSourceName] = useAlertManagerSourceName(alertManagers);
const dispatch = useDispatch();
@ -28,6 +34,8 @@ const AlertGroups = () => {
const { groupBy = [] } = getFiltersFromUrlParams(queryParams);
const styles = useStyles2(getStyles);
const { currentData: alertmanagerChoice } = useGetAlertmanagerChoiceQuery();
const alertGroups = useUnifiedAlertingSelector((state) => state.amAlertGroups);
const {
loading,
@ -38,6 +46,9 @@ const AlertGroups = () => {
const groupedAlerts = useGroupedAlerts(results, groupBy);
const filteredAlertGroups = useFilteredAmGroups(groupedAlerts);
const grafanaAmDeliveryDisabled =
alertManagerSourceName === GRAFANA_RULES_SOURCE_NAME && alertmanagerChoice === AlertmanagerChoice.External;
useEffect(() => {
function fetchNotifications() {
if (alertManagerSourceName) {
@ -68,6 +79,14 @@ const AlertGroups = () => {
{error.message || 'Unknown error'}
</Alert>
)}
{grafanaAmDeliveryDisabled && (
<Alert title="Grafana alerts are not delivered to Grafana Alertmanager">
Grafana is configured to send alerts to external alertmanagers only. No alerts are expected to be available
here for the selected Alertmanager.
</Alert>
)}
{results &&
filteredAlertGroups.map((group, index) => {
return (

@ -8,8 +8,10 @@ import { useDispatch } from 'app/types';
import { useCleanup } from '../../../core/hooks/useCleanup';
import { alertmanagerApi } from './api/alertmanagerApi';
import { AlertManagerPicker } from './components/AlertManagerPicker';
import { AlertingPageWrapper } from './components/AlertingPageWrapper';
import { GrafanaAlertmanagerDeliveryWarning } from './components/GrafanaAlertmanagerDeliveryWarning';
import { NoAlertManagerWarning } from './components/NoAlertManagerWarning';
import { ProvisionedResource, ProvisioningAlert } from './components/Provisioning';
import { AmRootRoute } from './components/amroutes/AmRootRoute';
@ -26,10 +28,12 @@ import { initialAsyncRequestState } from './utils/redux';
const AmRoutes = () => {
const dispatch = useDispatch();
const { useGetAlertmanagerChoiceQuery } = alertmanagerApi;
const styles = useStyles2(getStyles);
const [isRootRouteEditMode, setIsRootRouteEditMode] = useState(false);
const alertManagers = useAlertManagersByPermission('notification');
const [alertManagerSourceName, setAlertManagerSourceName] = useAlertManagerSourceName(alertManagers);
const { currentData: alertmanagerChoice } = useGetAlertmanagerChoiceQuery();
const amConfigs = useUnifiedAlertingSelector((state) => state.amConfigs);
@ -127,6 +131,10 @@ const AmRoutes = () => {
{resultError.message || 'Unknown error.'}
</Alert>
)}
<GrafanaAlertmanagerDeliveryWarning
currentAlertmanager={alertManagerSourceName}
alertmanagerChoice={alertmanagerChoice}
/>
{isProvisioned && <ProvisioningAlert resource={ProvisionedResource.RootNotificationPolicy} />}
{resultLoading && <LoadingPlaceholder text="Loading Alertmanager config..." />}
{result && !resultLoading && !resultError && (

@ -1,21 +1,29 @@
import { render, waitFor, within, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { setupServer } from 'msw/node';
import React from 'react';
import { Provider } from 'react-redux';
import { Router } from 'react-router-dom';
import { selectOptionInTest } from 'test/helpers/selectOptionInTest';
import { byLabelText, byPlaceholderText, byRole, byTestId, byText } from 'testing-library-selector';
import { locationService, setDataSourceSrv } from '@grafana/runtime';
import { locationService, setBackendSrv, setDataSourceSrv } from '@grafana/runtime';
import { interceptLinkClicks } from 'app/core/navigation/patch/interceptLinkClicks';
import { backendSrv } from 'app/core/services/backend_srv';
import { contextSrv } from 'app/core/services/context_srv';
import store from 'app/core/store';
import { AlertManagerDataSourceJsonData, AlertManagerImplementation } from 'app/plugins/datasource/alertmanager/types';
import {
AlertmanagerChoice,
AlertManagerDataSourceJsonData,
AlertManagerImplementation,
} from 'app/plugins/datasource/alertmanager/types';
import { configureStore } from 'app/store/configureStore';
import { AccessControlAction, ContactPointsState } from 'app/types';
import 'whatwg-fetch';
import Receivers from './Receivers';
import { updateAlertManagerConfig, fetchAlertManagerConfig, fetchStatus, testReceivers } from './api/alertmanager';
import { fetchAlertManagerConfig, fetchStatus, testReceivers, updateAlertManagerConfig } from './api/alertmanager';
import { discoverAlertmanagerFeatures } from './api/buildInfo';
import { fetchNotifiers } from './api/grafana';
import * as receiversApi from './api/receiversApi';
@ -26,6 +34,7 @@ import {
someCloudAlertManagerStatus,
someGrafanaAlertManagerConfig,
} from './mocks';
import { mockAlertmanagerChoiceResponse } from './mocks/alertmanagerApi';
import { grafanaNotifiersMock } from './mocks/grafana-notifiers';
import { getAllDataSources } from './utils/config';
import { ALERTMANAGER_NAME_LOCAL_STORAGE_KEY, ALERTMANAGER_NAME_QUERY_KEY } from './utils/constants';
@ -135,7 +144,19 @@ document.addEventListener('click', interceptLinkClicks);
const emptyContactPointsState: ContactPointsState = { receivers: {}, errorCount: 0 };
describe('Receivers', () => {
const server = setupServer();
beforeAll(() => {
setBackendSrv(backendSrv);
server.listen({ onUnhandledRequest: 'error' });
});
afterAll(() => {
server.close();
});
beforeEach(() => {
server.resetHandlers();
jest.resetAllMocks();
mocks.getAllDataSources.mockReturnValue(Object.values(dataSources));
mocks.api.fetchNotifiers.mockResolvedValue(grafanaNotifiersMock);
@ -160,6 +181,7 @@ describe('Receivers', () => {
});
it('Template and receiver tables are rendered, alertmanager can be selected, no notification errors', async () => {
mockAlertmanagerChoiceResponse(server, { alertmanagersChoice: AlertmanagerChoice.All });
mocks.api.fetchConfig.mockImplementation((name) =>
Promise.resolve(name === GRAFANA_RULES_SOURCE_NAME ? someGrafanaAlertManagerConfig : someCloudAlertManagerConfig)
);
@ -204,6 +226,8 @@ describe('Receivers', () => {
});
it('Grafana receiver can be tested', async () => {
mockAlertmanagerChoiceResponse(server, { alertmanagersChoice: AlertmanagerChoice.All });
mocks.api.fetchConfig.mockResolvedValue(someGrafanaAlertManagerConfig);
await renderReceivers();
@ -260,6 +284,8 @@ describe('Receivers', () => {
});
it('Grafana receiver can be created', async () => {
mockAlertmanagerChoiceResponse(server, { alertmanagersChoice: AlertmanagerChoice.All });
mocks.api.fetchConfig.mockResolvedValue(someGrafanaAlertManagerConfig);
mocks.api.updateConfig.mockResolvedValue();
await renderReceivers();
@ -322,6 +348,8 @@ describe('Receivers', () => {
});
it('Hides create contact point button for users without permission', () => {
mockAlertmanagerChoiceResponse(server, { alertmanagersChoice: AlertmanagerChoice.All });
mocks.api.fetchConfig.mockResolvedValue(someGrafanaAlertManagerConfig);
mocks.api.updateConfig.mockResolvedValue();
mocks.contextSrv.hasAccess.mockImplementation((action) =>
@ -336,6 +364,8 @@ describe('Receivers', () => {
});
it('Cloud alertmanager receiver can be edited', async () => {
mockAlertmanagerChoiceResponse(server, { alertmanagersChoice: AlertmanagerChoice.All });
mocks.api.fetchConfig.mockResolvedValue(someCloudAlertManagerConfig);
mocks.api.updateConfig.mockResolvedValue();
await renderReceivers('CloudManager');
@ -430,6 +460,8 @@ describe('Receivers', () => {
});
it('Prometheus Alertmanager receiver cannot be edited', async () => {
mockAlertmanagerChoiceResponse(server, { alertmanagersChoice: AlertmanagerChoice.All });
mocks.api.fetchStatus.mockResolvedValue({
...someCloudAlertManagerStatus,
config: someCloudAlertManagerConfig.alertmanager_config,
@ -467,6 +499,7 @@ describe('Receivers', () => {
});
it('Loads config from status endpoint if there is no user config', async () => {
mockAlertmanagerChoiceResponse(server, { alertmanagersChoice: AlertmanagerChoice.All });
// loading an empty config with make it fetch config from status endpoint
mocks.api.fetchConfig.mockResolvedValue({
template_files: {},
@ -488,6 +521,8 @@ describe('Receivers', () => {
});
it('Shows an empty config when config returns an error and the AM supports lazy config initialization', async () => {
mockAlertmanagerChoiceResponse(server, { alertmanagersChoice: AlertmanagerChoice.All });
mocks.api.discoverAlertmanagerFeatures.mockResolvedValue({ lazyConfigInit: true });
mocks.api.fetchConfig.mockRejectedValue({ message: 'alertmanager storage object not found' });
@ -500,8 +535,10 @@ describe('Receivers', () => {
expect(receiversTable).toBeInTheDocument();
expect(ui.newContactPointButton.get()).toBeInTheDocument();
});
describe('Contact points state', () => {
it('Should render error notifications when there are some points state ', async () => {
mockAlertmanagerChoiceResponse(server, { alertmanagersChoice: AlertmanagerChoice.All });
mocks.api.fetchConfig.mockResolvedValue(someGrafanaAlertManagerConfig);
mocks.api.updateConfig.mockResolvedValue();
@ -573,6 +610,7 @@ describe('Receivers', () => {
expect(byText('OK').getAll(criticalDetailTable)).toHaveLength(2);
});
it('Should render no attempt message when there are some points state with null lastNotifyAttempt, and "-" in null values', async () => {
mockAlertmanagerChoiceResponse(server, { alertmanagersChoice: AlertmanagerChoice.All });
mocks.api.fetchConfig.mockResolvedValue(someGrafanaAlertManagerConfig);
mocks.api.updateConfig.mockResolvedValue();
@ -649,6 +687,7 @@ describe('Receivers', () => {
});
it('Should not render error notifications when fetching contact points state raises 404 error ', async () => {
mockAlertmanagerChoiceResponse(server, { alertmanagersChoice: AlertmanagerChoice.All });
mocks.api.fetchConfig.mockResolvedValue(someGrafanaAlertManagerConfig);
mocks.api.updateConfig.mockResolvedValue();

@ -7,11 +7,13 @@ 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 { ContactPointsState } from '../../../types';
import { alertmanagerApi } from './api/alertmanagerApi';
import { useGetContactPointsState } from './api/receiversApi';
import { AlertManagerPicker } from './components/AlertManagerPicker';
import { AlertingPageWrapper } from './components/AlertingPageWrapper';
import { GrafanaAlertmanagerDeliveryWarning } from './components/GrafanaAlertmanagerDeliveryWarning';
import { NoAlertManagerWarning } from './components/NoAlertManagerWarning';
import { EditReceiverView } from './components/receivers/EditReceiverView';
import { EditTemplateView } from './components/receivers/EditTemplateView';
@ -48,14 +50,16 @@ function NotificationError({ errorCount }: NotificationErrorProps) {
);
}
type PageType = 'receivers' | 'templates' | 'global-config';
const Receivers = () => {
const { useGetAlertmanagerChoiceQuery } = alertmanagerApi;
const alertManagers = useAlertManagersByPermission('notification');
const [alertManagerSourceName, setAlertManagerSourceName] = useAlertManagerSourceName(alertManagers);
const dispatch = useDispatch();
const styles = useStyles2(getStyles);
type PageType = 'receivers' | 'templates' | 'global-config';
const { id, type } = useParams<{ id?: string; type?: PageType }>();
const location = useLocation();
const isRoot = location.pathname.endsWith('/alerting/notifications');
@ -87,31 +91,15 @@ const Receivers = () => {
dispatch(fetchGrafanaNotifiersAction());
}
}, [alertManagerSourceName, dispatch, receiverTypes]);
const contactPointsState: ContactPointsState = useGetContactPointsState(alertManagerSourceName ?? '');
const integrationsErrorCount = contactPointsState?.errorCount ?? 0;
const { data: alertmanagerChoice } = useGetAlertmanagerChoiceQuery();
const disableAmSelect = !isRoot;
let pageNav: NavModelItem | undefined;
if (type === 'receivers' || type === 'templates') {
const objectText = type === 'receivers' ? 'contact point' : 'message template';
if (id) {
pageNav = {
text: id,
subTitle: `Edit the settings for a specific ${objectText}`,
};
} else {
pageNav = {
text: `New ${objectText}`,
subTitle: `Create a new ${objectText} for your notifications`,
};
}
} else if (type === 'global-config') {
pageNav = {
text: 'Global config',
subTitle: 'Manage your global configuration',
};
}
let pageNav = getPageNavigationModel(type, id);
if (!alertManagerSourceName) {
return isRoot ? (
@ -141,6 +129,10 @@ const Receivers = () => {
{error.message || 'Unknown error.'}
</Alert>
)}
<GrafanaAlertmanagerDeliveryWarning
alertmanagerChoice={alertmanagerChoice}
currentAlertmanager={alertManagerSourceName}
/>
{loading && !config && <LoadingPlaceholder text="loading configuration..." />}
{config && !error && (
<Switch>
@ -184,6 +176,30 @@ const Receivers = () => {
);
};
function getPageNavigationModel(type: PageType | undefined, id: string | undefined) {
let pageNav: NavModelItem | undefined;
if (type === 'receivers' || type === 'templates') {
const objectText = type === 'receivers' ? 'contact point' : 'message template';
if (id) {
pageNav = {
text: id,
subTitle: `Edit the settings for a specific ${objectText}`,
};
} else {
pageNav = {
text: `New ${objectText}`,
subTitle: `Create a new ${objectText} for your notifications`,
};
}
} else if (type === 'global-config') {
pageNav = {
text: 'Global config',
subTitle: 'Manage your global configuration',
};
}
return pageNav;
}
export default withErrorBoundary(Receivers, { style: 'page' });
const getStyles = (theme: GrafanaTheme2) => ({

@ -5,9 +5,11 @@ import { Alert, withErrorBoundary } from '@grafana/ui';
import { Silence } from 'app/plugins/datasource/alertmanager/types';
import { useDispatch } from 'app/types';
import { alertmanagerApi } from './api/alertmanagerApi';
import { featureDiscoveryApi } from './api/featureDiscoveryApi';
import { AlertManagerPicker } from './components/AlertManagerPicker';
import { AlertingPageWrapper } from './components/AlertingPageWrapper';
import { GrafanaAlertmanagerDeliveryWarning } from './components/GrafanaAlertmanagerDeliveryWarning';
import { NoAlertManagerWarning } from './components/NoAlertManagerWarning';
import SilencesEditor from './components/silences/SilencesEditor';
import SilencesTable from './components/silences/SilencesTable';
@ -24,6 +26,7 @@ const Silences = () => {
const [alertManagerSourceName, setAlertManagerSourceName] = useAlertManagerSourceName(alertManagers);
const dispatch = useDispatch();
const { useGetAlertmanagerChoiceQuery } = alertmanagerApi;
const silences = useUnifiedAlertingSelector((state) => state.silences);
const alertsRequests = useUnifiedAlertingSelector((state) => state.amAlerts);
const alertsRequest = alertManagerSourceName
@ -39,6 +42,8 @@ const Silences = () => {
{ skip: !alertManagerSourceName }
);
const { currentData: alertmanagerChoice } = useGetAlertmanagerChoiceQuery();
useEffect(() => {
function fetchAll() {
if (alertManagerSourceName) {
@ -79,6 +84,10 @@ const Silences = () => {
onChange={setAlertManagerSourceName}
dataSources={alertManagers}
/>
<GrafanaAlertmanagerDeliveryWarning
currentAlertmanager={alertManagerSourceName}
alertmanagerChoice={alertmanagerChoice}
/>
{mimirLazyInitError && (
<Alert title="The selected Alertmanager has no configuration" severity="warning">

@ -16,5 +16,6 @@ const backendSrvBaseQuery = (): BaseQueryFn<BackendSrvRequest> => async (request
export const alertingApi = createApi({
reducerPath: 'alertingApi',
baseQuery: backendSrvBaseQuery(),
tagTypes: ['AlertmanagerChoice'],
endpoints: () => ({}),
});

@ -7,6 +7,7 @@ import {
AlertManagerCortexConfig,
AlertmanagerGroup,
AlertmanagerStatus,
ExternalAlertmanagerConfig,
ExternalAlertmanagersResponse,
Matcher,
Receiver,
@ -15,7 +16,6 @@ import {
TestReceiversAlert,
TestReceiversPayload,
TestReceiversResult,
ExternalAlertmanagerConfig,
} from 'app/plugins/datasource/alertmanager/types';
import { getDatasourceAPIUid, GRAFANA_RULES_SOURCE_NAME } from '../utils/datasource';

@ -0,0 +1,37 @@
import {
AlertmanagerChoice,
ExternalAlertmanagerConfig,
ExternalAlertmanagers,
ExternalAlertmanagersResponse,
} from '../../../../plugins/datasource/alertmanager/types';
import { alertingApi } from './alertingApi';
export interface AlertmanagersChoiceResponse {
alertmanagersChoice: AlertmanagerChoice;
}
export const alertmanagerApi = alertingApi.injectEndpoints({
endpoints: (build) => ({
getAlertmanagerChoice: build.query<AlertmanagerChoice, void>({
query: () => ({ url: '/api/v1/ngalert' }),
providesTags: ['AlertmanagerChoice'],
transformResponse: (response: AlertmanagersChoiceResponse) => response.alertmanagersChoice,
}),
getExternalAlertmanagerConfig: build.query<ExternalAlertmanagerConfig, void>({
query: () => ({ url: '/api/v1/ngalert/admin_config' }),
providesTags: ['AlertmanagerChoice'],
}),
getExternalAlertmanagers: build.query<ExternalAlertmanagers, void>({
query: () => ({ url: '/api/v1/ngalert/alertmanagers' }),
transformResponse: (response: ExternalAlertmanagersResponse) => response.data,
}),
saveExternalAlertmanagersConfig: build.mutation<{ message: string }, ExternalAlertmanagerConfig>({
query: (config) => ({ url: '/api/v1/ngalert/admin_config', method: 'POST', data: config }),
invalidatesTags: ['AlertmanagerChoice'],
}),
}),
});

@ -0,0 +1,47 @@
import { render } from '@testing-library/react';
import React from 'react';
import { AlertmanagerChoice } from '../../../../plugins/datasource/alertmanager/types';
import { GRAFANA_RULES_SOURCE_NAME } from '../utils/datasource';
import { GrafanaAlertmanagerDeliveryWarning } from './GrafanaAlertmanagerDeliveryWarning';
describe('GrafanaAlertmanagerDeliveryWarning', () => {
describe('When AlertmanagerChoice set to External', () => {
it('Should not render when the datasource is not Grafana', () => {
const { container } = render(
<GrafanaAlertmanagerDeliveryWarning
currentAlertmanager="custom-alertmanager"
alertmanagerChoice={AlertmanagerChoice.External}
/>
);
expect(container).toBeEmptyDOMElement();
});
it('Should render warning when the datasource is Grafana', () => {
const { container } = render(
<GrafanaAlertmanagerDeliveryWarning
currentAlertmanager={GRAFANA_RULES_SOURCE_NAME}
alertmanagerChoice={AlertmanagerChoice.External}
/>
);
expect(container).toHaveTextContent('Grafana alerts are not delivered to Grafana Alertmanager');
});
});
it.each([AlertmanagerChoice.All, AlertmanagerChoice.Internal])(
'Should not render when datasource is Grafana and Alertmanager choice is %s',
(choice) => {
const { container } = render(
<GrafanaAlertmanagerDeliveryWarning
currentAlertmanager={GRAFANA_RULES_SOURCE_NAME}
alertmanagerChoice={choice}
/>
);
expect(container).toBeEmptyDOMElement();
}
);
});

@ -0,0 +1,46 @@
import { css } from '@emotion/css';
import React from 'react';
import { GrafanaTheme2 } from '@grafana/data/src';
import { Alert, useStyles2 } from '@grafana/ui/src';
import { AlertmanagerChoice } from '../../../../plugins/datasource/alertmanager/types';
import { GRAFANA_RULES_SOURCE_NAME } from '../utils/datasource';
interface GrafanaAlertmanagerDeliveryWarningProps {
alertmanagerChoice?: AlertmanagerChoice;
currentAlertmanager: string;
}
export function GrafanaAlertmanagerDeliveryWarning({
alertmanagerChoice,
currentAlertmanager,
}: GrafanaAlertmanagerDeliveryWarningProps) {
const styles = useStyles2(getStyles);
if (currentAlertmanager !== GRAFANA_RULES_SOURCE_NAME) {
return null;
}
if (alertmanagerChoice !== AlertmanagerChoice.External) {
return null;
}
return (
<Alert title="Grafana alerts are not delivered to Grafana Alertmanager">
Grafana is configured to send alerts to external Alertmanagers only. Changing Grafana Alertmanager configuration
will not affect delivery of your alerts!
<div className={styles.adminHint}>
You can change the configuration on the Alerting Admin page. If you do not have access, contact your
Administrator
</div>
</Alert>
);
}
const getStyles = (theme: GrafanaTheme2) => ({
adminHint: css`
font-size: ${theme.typography.bodySmall.fontSize};
font-weight: ${theme.typography.bodySmall.fontWeight};
`,
});

@ -17,15 +17,10 @@ import {
import EmptyListCTA from 'app/core/components/EmptyListCTA/EmptyListCTA';
import { loadDataSources } from 'app/features/datasources/state/actions';
import { AlertmanagerChoice } from 'app/plugins/datasource/alertmanager/types';
import { useDispatch, useSelector } from 'app/types';
import { StoreState } from 'app/types/store';
import { useDispatch } from 'app/types';
import { alertmanagerApi } from '../../api/alertmanagerApi';
import { useExternalAmSelector, useExternalDataSourceAlertmanagers } from '../../hooks/useExternalAmSelector';
import {
addExternalAlertmanagersAction,
fetchExternalAlertmanagersAction,
fetchExternalAlertmanagersConfigAction,
} from '../../state/actions';
import { AddAlertManagerModal } from './AddAlertManagerModal';
import { ExternalAlertmanagerDataSources } from './ExternalAlertmanagerDataSources';
@ -45,20 +40,23 @@ export const ExternalAlertmanagers = () => {
const externalAlertManagers = useExternalAmSelector();
const externalDsAlertManagers = useExternalDataSourceAlertmanagers();
const alertmanagersChoice = useSelector(
(state: StoreState) => state.unifiedAlerting.externalAlertmanagers.alertmanagerConfig.result?.alertmanagersChoice
);
const {
useSaveExternalAlertmanagersConfigMutation,
useGetExternalAlertmanagerConfigQuery,
useGetExternalAlertmanagersQuery,
} = alertmanagerApi;
const [saveExternalAlertManagers] = useSaveExternalAlertmanagersConfigMutation();
const { currentData: externalAlertmanagerConfig } = useGetExternalAlertmanagerConfigQuery();
// Just to refresh the status periodically
useGetExternalAlertmanagersQuery(undefined, { pollingInterval: 5000 });
const alertmanagersChoice = externalAlertmanagerConfig?.alertmanagersChoice;
const theme = useTheme2();
useEffect(() => {
dispatch(fetchExternalAlertmanagersAction());
dispatch(fetchExternalAlertmanagersConfigAction());
dispatch(loadDataSources());
const interval = setInterval(() => dispatch(fetchExternalAlertmanagersAction()), 5000);
return () => {
clearInterval(interval);
};
}, [dispatch]);
const onDelete = useCallback(
@ -69,15 +67,15 @@ export const ExternalAlertmanagers = () => {
.map((am) => {
return am.url;
});
dispatch(
addExternalAlertmanagersAction({
alertmanagers: newList,
alertmanagersChoice: alertmanagersChoice ?? AlertmanagerChoice.All,
})
);
saveExternalAlertManagers({
alertmanagers: newList,
alertmanagersChoice: alertmanagersChoice ?? AlertmanagerChoice.All,
});
setDeleteModalState({ open: false, index: 0 });
},
[externalAlertManagers, dispatch, alertmanagersChoice]
[externalAlertManagers, saveExternalAlertManagers, alertmanagersChoice]
);
const onEdit = useCallback(() => {
@ -108,18 +106,14 @@ export const ExternalAlertmanagers = () => {
}, [setModalState]);
const onChangeAlertmanagerChoice = (alertmanagersChoice: AlertmanagerChoice) => {
dispatch(
addExternalAlertmanagersAction({ alertmanagers: externalAlertManagers.map((am) => am.url), alertmanagersChoice })
);
saveExternalAlertManagers({ alertmanagers: externalAlertManagers.map((am) => am.url), alertmanagersChoice });
};
const onChangeAlertmanagers = (alertmanagers: string[]) => {
dispatch(
addExternalAlertmanagersAction({
alertmanagers,
alertmanagersChoice: alertmanagersChoice ?? AlertmanagerChoice.All,
})
);
saveExternalAlertManagers({
alertmanagers,
alertmanagersChoice: alertmanagersChoice ?? AlertmanagerChoice.All,
});
};
const getStatusColor = (status: string) => {

@ -1,21 +1,58 @@
import { renderHook } from '@testing-library/react-hooks';
import { setupServer } from 'msw/node';
import React from 'react';
import { Provider } from 'react-redux';
import 'whatwg-fetch';
import { DataSourceJsonData, DataSourceSettings } from '@grafana/data';
import { config } from '@grafana/runtime';
import { backendSrv } from 'app/core/services/backend_srv';
import { AlertmanagerChoice, AlertManagerDataSourceJsonData } from 'app/plugins/datasource/alertmanager/types';
import { mockDataSource, mockDataSourcesStore, mockStore } from '../mocks';
import { mockAlertmanagerConfigResponse, mockAlertmanagersResponse } from '../mocks/alertmanagerApi';
import { useExternalAmSelector, useExternalDataSourceAlertmanagers } from './useExternalAmSelector';
const server = setupServer();
jest.mock('@grafana/runtime', () => ({
...(jest.requireActual('@grafana/runtime') as unknown as object),
getBackendSrv: () => backendSrv,
}));
beforeAll(() => {
server.listen({ onUnhandledRequest: 'error' });
});
beforeEach(() => {
server.resetHandlers();
});
afterAll(() => {
server.close();
});
describe('useExternalAmSelector', () => {
it('should have one in pending', () => {
const store = createMockStoreState([], [], ['some/url/to/am']);
it('should have one in pending', async () => {
mockAlertmanagersResponse(server, {
data: {
activeAlertManagers: [],
droppedAlertManagers: [],
},
});
mockAlertmanagerConfigResponse(server, {
alertmanagers: ['some/url/to/am'],
alertmanagersChoice: AlertmanagerChoice.All,
});
const store = mockStore(() => null);
const wrapper = ({ children }: React.PropsWithChildren<{}>) => <Provider store={store}>{children}</Provider>;
const { result } = renderHook(() => useExternalAmSelector(), { wrapper });
const alertmanagers = result.current;
const { result, waitFor } = renderHook(() => useExternalAmSelector(), { wrapper });
await waitFor(() => result.current.length > 0);
const { current: alertmanagers } = result;
expect(alertmanagers).toEqual([
{
@ -26,15 +63,24 @@ describe('useExternalAmSelector', () => {
]);
});
it('should have one active, one pending', () => {
const store = createMockStoreState(
[{ url: 'some/url/to/am/api/v2/alerts' }],
[],
['some/url/to/am', 'some/url/to/am1']
);
it('should have one active, one pending', async () => {
mockAlertmanagersResponse(server, {
data: {
activeAlertManagers: [{ url: 'some/url/to/am/api/v2/alerts' }],
droppedAlertManagers: [],
},
});
mockAlertmanagerConfigResponse(server, {
alertmanagers: ['some/url/to/am', 'some/url/to/am1'],
alertmanagersChoice: AlertmanagerChoice.All,
});
const store = mockStore(() => null);
const wrapper = ({ children }: React.PropsWithChildren<{}>) => <Provider store={store}>{children}</Provider>;
const { result } = renderHook(() => useExternalAmSelector(), { wrapper });
const alertmanagers = result.current;
const { result, waitFor } = renderHook(() => useExternalAmSelector(), { wrapper });
await waitFor(() => result.current.length > 0);
const { current: alertmanagers } = result;
expect(alertmanagers).toEqual([
{
@ -50,15 +96,24 @@ describe('useExternalAmSelector', () => {
]);
});
it('should have two active', () => {
const store = createMockStoreState(
[{ url: 'some/url/to/am/api/v2/alerts' }, { url: 'some/url/to/am1/api/v2/alerts' }],
[],
['some/url/to/am', 'some/url/to/am1']
);
it('should have two active', async () => {
mockAlertmanagersResponse(server, {
data: {
activeAlertManagers: [{ url: 'some/url/to/am/api/v2/alerts' }, { url: 'some/url/to/am1/api/v2/alerts' }],
droppedAlertManagers: [],
},
});
mockAlertmanagerConfigResponse(server, {
alertmanagers: ['some/url/to/am', 'some/url/to/am1'],
alertmanagersChoice: AlertmanagerChoice.All,
});
const store = mockStore(() => null);
const wrapper = ({ children }: React.PropsWithChildren<{}>) => <Provider store={store}>{children}</Provider>;
const { result } = renderHook(() => useExternalAmSelector(), { wrapper });
const alertmanagers = result.current;
const { result, waitFor } = renderHook(() => useExternalAmSelector(), { wrapper });
await waitFor(() => result.current.length > 0);
const { current: alertmanagers } = result;
expect(alertmanagers).toEqual([
{
@ -74,16 +129,25 @@ describe('useExternalAmSelector', () => {
]);
});
it('should have one active, one dropped, one pending', () => {
const store = createMockStoreState(
[{ url: 'some/url/to/am/api/v2/alerts' }],
[{ url: 'some/dropped/url/api/v2/alerts' }],
['some/url/to/am', 'some/url/to/am1']
);
it('should have one active, one dropped, one pending', async () => {
mockAlertmanagersResponse(server, {
data: {
activeAlertManagers: [{ url: 'some/url/to/am/api/v2/alerts' }],
droppedAlertManagers: [{ url: 'some/dropped/url/api/v2/alerts' }],
},
});
mockAlertmanagerConfigResponse(server, {
alertmanagers: ['some/url/to/am', 'some/url/to/am1'],
alertmanagersChoice: AlertmanagerChoice.All,
});
const store = mockStore(() => null);
const wrapper = ({ children }: React.PropsWithChildren<{}>) => <Provider store={store}>{children}</Provider>;
const { result } = renderHook(() => useExternalAmSelector(), { wrapper });
const alertmanagers = result.current;
const { result, waitFor } = renderHook(() => useExternalAmSelector(), { wrapper });
await waitFor(() => result.current.length > 0);
const { current: alertmanagers } = result;
expect(alertmanagers).toEqual([
{
url: 'some/url/to/am',
@ -103,20 +167,29 @@ describe('useExternalAmSelector', () => {
]);
});
it('The number of alert managers should match config entries when there are multiple entries of the same url', () => {
const store = createMockStoreState(
[
{ url: 'same/url/to/am/api/v2/alerts' },
{ url: 'same/url/to/am/api/v2/alerts' },
{ url: 'same/url/to/am/api/v2/alerts' },
],
[],
['same/url/to/am', 'same/url/to/am', 'same/url/to/am']
);
it('The number of alert managers should match config entries when there are multiple entries of the same url', async () => {
mockAlertmanagersResponse(server, {
data: {
activeAlertManagers: [
{ url: 'same/url/to/am/api/v2/alerts' },
{ url: 'same/url/to/am/api/v2/alerts' },
{ url: 'same/url/to/am/api/v2/alerts' },
],
droppedAlertManagers: [],
},
});
mockAlertmanagerConfigResponse(server, {
alertmanagers: ['same/url/to/am', 'same/url/to/am', 'same/url/to/am'],
alertmanagersChoice: AlertmanagerChoice.All,
});
const store = mockStore(() => null);
const wrapper = ({ children }: React.PropsWithChildren<{}>) => <Provider store={store}>{children}</Provider>;
const { result } = renderHook(() => useExternalAmSelector(), { wrapper });
const alertmanagers = result.current;
const { result, waitFor } = renderHook(() => useExternalAmSelector(), { wrapper });
await waitFor(() => result.current.length > 0);
const { current: alertmanagers } = result;
expect(alertmanagers.length).toBe(3);
expect(alertmanagers).toEqual([
@ -140,7 +213,7 @@ describe('useExternalAmSelector', () => {
});
describe('useExternalDataSourceAlertmanagers', () => {
it('Should merge data sources information from config and api responses', () => {
it('Should merge data sources information from config and api responses', async () => {
// Arrange
const { dsSettings, dsInstanceSettings } = setupAlertmanagerDataSource({ url: 'http://grafana.com' });
@ -152,20 +225,23 @@ describe('useExternalDataSourceAlertmanagers', () => {
dataSources: [dsSettings],
});
mockAlertmanagersResponse(server, { data: { activeAlertManagers: [], droppedAlertManagers: [] } });
const wrapper: React.FC = ({ children }) => <Provider store={store}>{children}</Provider>;
// Act
const {
result: { current },
} = renderHook(() => useExternalDataSourceAlertmanagers(), { wrapper });
const { result, waitForNextUpdate } = renderHook(() => useExternalDataSourceAlertmanagers(), { wrapper });
await waitForNextUpdate();
// Assert
const { current } = result;
expect(current).toHaveLength(1);
expect(current[0].dataSource.uid).toBe('1');
expect(current[0].url).toBe('http://grafana.com');
});
it('Should have active state if available in the activeAlertManagers', () => {
it('Should have active state if available in the activeAlertManagers', async () => {
// Arrange
const { dsSettings, dsInstanceSettings } = setupAlertmanagerDataSource({ url: 'http://grafana.com' });
@ -175,28 +251,30 @@ describe('useExternalDataSourceAlertmanagers', () => {
const store = mockStore((state) => {
state.dataSources.dataSources = [dsSettings];
state.unifiedAlerting.externalAlertmanagers.discoveredAlertmanagers.result = {
data: {
activeAlertManagers: [{ url: 'http://grafana.com/api/v2/alerts' }],
droppedAlertManagers: [],
},
};
});
mockAlertmanagersResponse(server, {
data: {
activeAlertManagers: [{ url: 'http://grafana.com/api/v2/alerts' }],
droppedAlertManagers: [],
},
});
const wrapper: React.FC = ({ children }) => <Provider store={store}>{children}</Provider>;
// Act
const {
result: { current },
} = renderHook(() => useExternalDataSourceAlertmanagers(), { wrapper });
const { result, waitForNextUpdate } = renderHook(() => useExternalDataSourceAlertmanagers(), { wrapper });
await waitForNextUpdate();
// Assert
const { current } = result;
expect(current).toHaveLength(1);
expect(current[0].status).toBe('active');
expect(current[0].statusInconclusive).toBe(false);
});
it('Should have dropped state if available in the droppedAlertManagers', () => {
it('Should have dropped state if available in the droppedAlertManagers', async () => {
// Arrange
const { dsSettings, dsInstanceSettings } = setupAlertmanagerDataSource({ url: 'http://grafana.com' });
@ -206,28 +284,30 @@ describe('useExternalDataSourceAlertmanagers', () => {
const store = mockStore((state) => {
state.dataSources.dataSources = [dsSettings];
state.unifiedAlerting.externalAlertmanagers.discoveredAlertmanagers.result = {
data: {
activeAlertManagers: [],
droppedAlertManagers: [{ url: 'http://grafana.com/api/v2/alerts' }],
},
};
});
mockAlertmanagersResponse(server, {
data: {
activeAlertManagers: [],
droppedAlertManagers: [{ url: 'http://grafana.com/api/v2/alerts' }],
},
});
const wrapper: React.FC = ({ children }) => <Provider store={store}>{children}</Provider>;
// Act
const {
result: { current },
} = renderHook(() => useExternalDataSourceAlertmanagers(), { wrapper });
const { result, waitForNextUpdate } = renderHook(() => useExternalDataSourceAlertmanagers(), { wrapper });
await waitForNextUpdate();
// Assert
const { current } = result;
expect(current).toHaveLength(1);
expect(current[0].status).toBe('dropped');
expect(current[0].statusInconclusive).toBe(false);
});
it('Should have pending state if not available neither in dropped nor in active alertManagers', () => {
it('Should have pending state if not available neither in dropped nor in active alertManagers', async () => {
// Arrange
const { dsSettings, dsInstanceSettings } = setupAlertmanagerDataSource();
@ -237,28 +317,30 @@ describe('useExternalDataSourceAlertmanagers', () => {
const store = mockStore((state) => {
state.dataSources.dataSources = [dsSettings];
state.unifiedAlerting.externalAlertmanagers.discoveredAlertmanagers.result = {
data: {
activeAlertManagers: [],
droppedAlertManagers: [],
},
};
});
mockAlertmanagersResponse(server, {
data: {
activeAlertManagers: [],
droppedAlertManagers: [],
},
});
const wrapper: React.FC = ({ children }) => <Provider store={store}>{children}</Provider>;
// Act
const {
result: { current },
} = renderHook(() => useExternalDataSourceAlertmanagers(), { wrapper });
const { result, waitForNextUpdate } = renderHook(() => useExternalDataSourceAlertmanagers(), { wrapper });
await waitForNextUpdate();
// Assert
const { current } = result;
expect(current).toHaveLength(1);
expect(current[0].status).toBe('pending');
expect(current[0].statusInconclusive).toBe(false);
});
it('Should match Alertmanager url when datasource url does not have protocol specified', () => {
it('Should match Alertmanager url when datasource url does not have protocol specified', async () => {
// Arrange
const { dsSettings, dsInstanceSettings } = setupAlertmanagerDataSource({ url: 'localhost:9093' });
@ -268,29 +350,38 @@ describe('useExternalDataSourceAlertmanagers', () => {
const store = mockStore((state) => {
state.dataSources.dataSources = [dsSettings];
state.unifiedAlerting.externalAlertmanagers.discoveredAlertmanagers.result = {
data: {
activeAlertManagers: [{ url: 'http://localhost:9093/api/v2/alerts' }],
droppedAlertManagers: [],
},
};
});
mockAlertmanagersResponse(server, {
data: {
activeAlertManagers: [{ url: 'http://localhost:9093/api/v2/alerts' }],
droppedAlertManagers: [],
},
});
const wrapper: React.FC = ({ children }) => <Provider store={store}>{children}</Provider>;
// Act
const {
result: { current },
} = renderHook(() => useExternalDataSourceAlertmanagers(), { wrapper });
const { result, waitForNextUpdate } = renderHook(() => useExternalDataSourceAlertmanagers(), { wrapper });
await waitForNextUpdate();
// Assert
const { current } = result;
expect(current).toHaveLength(1);
expect(current[0].status).toBe('active');
expect(current[0].url).toBe('localhost:9093');
});
it('Should have inconclusive state when there are many Alertmanagers of the same URL', () => {
it('Should have inconclusive state when there are many Alertmanagers of the same URL', async () => {
// Arrange
mockAlertmanagersResponse(server, {
data: {
activeAlertManagers: [{ url: 'http://grafana.com/api/v2/alerts' }, { url: 'http://grafana.com/api/v2/alerts' }],
droppedAlertManagers: [],
},
});
const { dsSettings, dsInstanceSettings } = setupAlertmanagerDataSource({ url: 'http://grafana.com' });
config.datasources = {
@ -299,28 +390,21 @@ describe('useExternalDataSourceAlertmanagers', () => {
const store = mockStore((state) => {
state.dataSources.dataSources = [dsSettings];
state.unifiedAlerting.externalAlertmanagers.discoveredAlertmanagers.result = {
data: {
activeAlertManagers: [
{ url: 'http://grafana.com/api/v2/alerts' },
{ url: 'http://grafana.com/api/v2/alerts' },
],
droppedAlertManagers: [],
},
};
});
const wrapper: React.FC = ({ children }) => <Provider store={store}>{children}</Provider>;
// Act
const {
result: { current },
} = renderHook(() => useExternalDataSourceAlertmanagers(), { wrapper });
const { result, waitForNextUpdate } = renderHook(() => useExternalDataSourceAlertmanagers(), {
wrapper,
});
await waitForNextUpdate();
// Assert
expect(current).toHaveLength(1);
expect(current[0].status).toBe('active');
expect(current[0].statusInconclusive).toBe(true);
expect(result.current).toHaveLength(1);
expect(result.current[0].status).toBe('active');
expect(result.current[0].statusInconclusive).toBe(true);
});
});
@ -367,22 +451,3 @@ function mockApiDataSource(partial: Partial<DataSourceSettings<DataSourceJsonDat
return dsSettings;
}
const createMockStoreState = (
activeAlertmanagers: Array<{ url: string }>,
droppedAlertmanagers: Array<{ url: string }>,
alertmanagerConfig: string[]
) => {
return mockStore((state) => {
state.unifiedAlerting.externalAlertmanagers.alertmanagerConfig.result = {
alertmanagers: alertmanagerConfig,
alertmanagersChoice: AlertmanagerChoice.All,
};
state.unifiedAlerting.externalAlertmanagers.discoveredAlertmanagers.result = {
data: {
activeAlertManagers: activeAlertmanagers,
droppedAlertManagers: droppedAlertmanagers,
},
};
});
};

@ -4,20 +4,17 @@ import { DataSourceInstanceSettings, DataSourceJsonData, DataSourceSettings } fr
import { AlertManagerDataSourceJsonData } from 'app/plugins/datasource/alertmanager/types';
import { useSelector } from 'app/types';
import { alertmanagerApi } from '../api/alertmanagerApi';
import { getAlertManagerDataSources } from '../utils/datasource';
import { useUnifiedAlertingSelector } from './useUnifiedAlertingSelector';
const SUFFIX_REGEX = /\/api\/v[1|2]\/alerts/i;
type AlertmanagerConfig = { url: string; status: string; actualUrl: string };
export function useExternalAmSelector(): AlertmanagerConfig[] | [] {
const discoveredAlertmanagers = useSelector(
(state) => state.unifiedAlerting.externalAlertmanagers.discoveredAlertmanagers.result?.data
);
const alertmanagerConfig = useSelector(
(state) => state.unifiedAlerting.externalAlertmanagers.alertmanagerConfig.result?.alertmanagers
);
const { useGetExternalAlertmanagersQuery, useGetExternalAlertmanagerConfigQuery } = alertmanagerApi;
const { currentData: discoveredAlertmanagers } = useGetExternalAlertmanagersQuery();
const { currentData: alertmanagerConfig } = useGetExternalAlertmanagerConfigQuery();
if (!discoveredAlertmanagers || !alertmanagerConfig) {
return [];
@ -30,7 +27,7 @@ export function useExternalAmSelector(): AlertmanagerConfig[] | [] {
actualUrl: am.url,
}));
for (const url of alertmanagerConfig) {
for (const url of alertmanagerConfig.alertmanagers) {
if (discoveredAlertmanagers.activeAlertManagers.length === 0) {
enabledAlertmanagers.push({
url: url,
@ -66,6 +63,9 @@ export interface ExternalDataSourceAM {
}
export function useExternalDataSourceAlertmanagers(): ExternalDataSourceAM[] {
const { useGetExternalAlertmanagersQuery } = alertmanagerApi;
const { currentData: discoveredAlertmanagers } = useGetExternalAlertmanagersQuery();
const externalDsAlertManagers = getAlertManagerDataSources().filter((ds) => ds.jsonData.handleGrafanaManagedAlerts);
const alertmanagerDatasources = useSelector((state) =>
@ -75,10 +75,6 @@ export function useExternalDataSourceAlertmanagers(): ExternalDataSourceAM[] {
)
);
const discoveredAlertmanagers = useUnifiedAlertingSelector(
(state) => state.externalAlertmanagers.discoveredAlertmanagers.result?.data
);
const droppedAMUrls = countBy(discoveredAlertmanagers?.droppedAlertManagers, (x) => x.url);
const activeAMUrls = countBy(discoveredAlertmanagers?.activeAlertManagers, (x) => x.url);

@ -0,0 +1,20 @@
import { rest } from 'msw';
import { SetupServerApi } from 'msw/node';
import {
ExternalAlertmanagerConfig,
ExternalAlertmanagersResponse,
} from '../../../../plugins/datasource/alertmanager/types';
import { AlertmanagersChoiceResponse } from '../api/alertmanagerApi';
export function mockAlertmanagerChoiceResponse(server: SetupServerApi, respose: AlertmanagersChoiceResponse) {
server.use(rest.get('/api/v1/ngalert', (req, res, ctx) => res(ctx.status(200), ctx.json(respose))));
}
export function mockAlertmanagersResponse(server: SetupServerApi, response: ExternalAlertmanagersResponse) {
server.use(rest.get('/api/v1/ngalert/alertmanagers', (req, res, ctx) => res(ctx.status(200), ctx.json(response))));
}
export function mockAlertmanagerConfigResponse(server: SetupServerApi, response: ExternalAlertmanagerConfig) {
server.use(rest.get('/api/v1/ngalert/admin_config', (req, res, ctx) => res(ctx.status(200), ctx.json(response))));
}
Loading…
Cancel
Save