diff --git a/public/app/features/alerting/unified/AlertGroups.tsx b/public/app/features/alerting/unified/AlertGroups.tsx index dab1d1b4eb8..9322973030d 100644 --- a/public/app/features/alerting/unified/AlertGroups.tsx +++ b/public/app/features/alerting/unified/AlertGroups.tsx @@ -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'} )} + + {grafanaAmDeliveryDisabled && ( + + Grafana is configured to send alerts to external alertmanagers only. No alerts are expected to be available + here for the selected Alertmanager. + + )} + {results && filteredAlertGroups.map((group, index) => { return ( diff --git a/public/app/features/alerting/unified/AmRoutes.tsx b/public/app/features/alerting/unified/AmRoutes.tsx index 86f44c7d58e..b69a685e23a 100644 --- a/public/app/features/alerting/unified/AmRoutes.tsx +++ b/public/app/features/alerting/unified/AmRoutes.tsx @@ -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.'} )} + {isProvisioned && } {resultLoading && } {result && !resultLoading && !resultError && ( diff --git a/public/app/features/alerting/unified/Receivers.test.tsx b/public/app/features/alerting/unified/Receivers.test.tsx index f30833fc02b..92661114386 100644 --- a/public/app/features/alerting/unified/Receivers.test.tsx +++ b/public/app/features/alerting/unified/Receivers.test.tsx @@ -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(); diff --git a/public/app/features/alerting/unified/Receivers.tsx b/public/app/features/alerting/unified/Receivers.tsx index 97d344de52b..49ce166c924 100644 --- a/public/app/features/alerting/unified/Receivers.tsx +++ b/public/app/features/alerting/unified/Receivers.tsx @@ -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.'} )} + {loading && !config && } {config && !error && ( @@ -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) => ({ diff --git a/public/app/features/alerting/unified/Silences.tsx b/public/app/features/alerting/unified/Silences.tsx index 051658c92dd..3800c9d7f83 100644 --- a/public/app/features/alerting/unified/Silences.tsx +++ b/public/app/features/alerting/unified/Silences.tsx @@ -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} /> + {mimirLazyInitError && ( diff --git a/public/app/features/alerting/unified/api/alertingApi.ts b/public/app/features/alerting/unified/api/alertingApi.ts index 473900da265..ef20816199e 100644 --- a/public/app/features/alerting/unified/api/alertingApi.ts +++ b/public/app/features/alerting/unified/api/alertingApi.ts @@ -16,5 +16,6 @@ const backendSrvBaseQuery = (): BaseQueryFn => async (request export const alertingApi = createApi({ reducerPath: 'alertingApi', baseQuery: backendSrvBaseQuery(), + tagTypes: ['AlertmanagerChoice'], endpoints: () => ({}), }); diff --git a/public/app/features/alerting/unified/api/alertmanager.ts b/public/app/features/alerting/unified/api/alertmanager.ts index 6921c8ea527..2f75264468f 100644 --- a/public/app/features/alerting/unified/api/alertmanager.ts +++ b/public/app/features/alerting/unified/api/alertmanager.ts @@ -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'; diff --git a/public/app/features/alerting/unified/api/alertmanagerApi.ts b/public/app/features/alerting/unified/api/alertmanagerApi.ts new file mode 100644 index 00000000000..53199140e22 --- /dev/null +++ b/public/app/features/alerting/unified/api/alertmanagerApi.ts @@ -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({ + query: () => ({ url: '/api/v1/ngalert' }), + providesTags: ['AlertmanagerChoice'], + transformResponse: (response: AlertmanagersChoiceResponse) => response.alertmanagersChoice, + }), + + getExternalAlertmanagerConfig: build.query({ + query: () => ({ url: '/api/v1/ngalert/admin_config' }), + providesTags: ['AlertmanagerChoice'], + }), + + getExternalAlertmanagers: build.query({ + 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'], + }), + }), +}); diff --git a/public/app/features/alerting/unified/components/GrafanaAlertmanagerDeliveryWarning.test.tsx b/public/app/features/alerting/unified/components/GrafanaAlertmanagerDeliveryWarning.test.tsx new file mode 100644 index 00000000000..405dc211d09 --- /dev/null +++ b/public/app/features/alerting/unified/components/GrafanaAlertmanagerDeliveryWarning.test.tsx @@ -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( + + ); + + expect(container).toBeEmptyDOMElement(); + }); + + it('Should render warning when the datasource is Grafana', () => { + const { container } = render( + + ); + + 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( + + ); + + expect(container).toBeEmptyDOMElement(); + } + ); +}); diff --git a/public/app/features/alerting/unified/components/GrafanaAlertmanagerDeliveryWarning.tsx b/public/app/features/alerting/unified/components/GrafanaAlertmanagerDeliveryWarning.tsx new file mode 100644 index 00000000000..89209f7204f --- /dev/null +++ b/public/app/features/alerting/unified/components/GrafanaAlertmanagerDeliveryWarning.tsx @@ -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 ( + + Grafana is configured to send alerts to external Alertmanagers only. Changing Grafana Alertmanager configuration + will not affect delivery of your alerts! +
+ You can change the configuration on the Alerting Admin page. If you do not have access, contact your + Administrator +
+
+ ); +} + +const getStyles = (theme: GrafanaTheme2) => ({ + adminHint: css` + font-size: ${theme.typography.bodySmall.fontSize}; + font-weight: ${theme.typography.bodySmall.fontWeight}; + `, +}); diff --git a/public/app/features/alerting/unified/components/admin/ExternalAlertmanagers.tsx b/public/app/features/alerting/unified/components/admin/ExternalAlertmanagers.tsx index 9c5002eadc6..d076aed7375 100644 --- a/public/app/features/alerting/unified/components/admin/ExternalAlertmanagers.tsx +++ b/public/app/features/alerting/unified/components/admin/ExternalAlertmanagers.tsx @@ -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) => { diff --git a/public/app/features/alerting/unified/hooks/useExternalAMSelector.test.tsx b/public/app/features/alerting/unified/hooks/useExternalAMSelector.test.tsx index f829676380a..3bdbdd5ac05 100644 --- a/public/app/features/alerting/unified/hooks/useExternalAMSelector.test.tsx +++ b/public/app/features/alerting/unified/hooks/useExternalAMSelector.test.tsx @@ -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<{}>) => {children}; - 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<{}>) => {children}; - 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<{}>) => {children}; - 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<{}>) => {children}; - 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<{}>) => {children}; - 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 }) => {children}; // 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 }) => {children}; // 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 }) => {children}; // 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 }) => {children}; // 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 }) => {children}; // 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 }) => {children}; // 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, - 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, - }, - }; - }); -}; diff --git a/public/app/features/alerting/unified/hooks/useExternalAmSelector.ts b/public/app/features/alerting/unified/hooks/useExternalAmSelector.ts index 38147df45bf..aeb81d6aa9e 100644 --- a/public/app/features/alerting/unified/hooks/useExternalAmSelector.ts +++ b/public/app/features/alerting/unified/hooks/useExternalAmSelector.ts @@ -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); diff --git a/public/app/features/alerting/unified/mocks/alertmanagerApi.ts b/public/app/features/alerting/unified/mocks/alertmanagerApi.ts new file mode 100644 index 00000000000..36e040966ab --- /dev/null +++ b/public/app/features/alerting/unified/mocks/alertmanagerApi.ts @@ -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)))); +}