Alerting: Refactor simplified routing contact points hook (#90762)

This commit is contained in:
Tom Ratcliffe 2024-07-29 17:22:31 +01:00 committed by GitHub
parent 24c64fdffa
commit 9d639278f4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 421 additions and 479 deletions

View File

@ -17,7 +17,7 @@ import {
} from '@grafana/runtime';
import appEvents from 'app/core/app_events';
import * as ruleActionButtons from 'app/features/alerting/unified/components/rules/RuleActionsButtons';
import { mockSearchApi, mockUserApi, setupMswServer } from 'app/features/alerting/unified/mockApi';
import { mockUserApi, setupMswServer } from 'app/features/alerting/unified/mockApi';
import { setAlertmanagerChoices } from 'app/features/alerting/unified/mocks/server/configure';
import * as actions from 'app/features/alerting/unified/state/actions';
import { getMockUser } from 'app/features/users/__mocks__/userMocks';
@ -156,7 +156,6 @@ const ui = {
const server = setupMswServer();
const configureMockServer = (server: SetupServer) => {
mockSearchApi(server).search([]);
mockUserApi(server).user(getMockUser());
setAlertmanagerChoices(AlertmanagerChoice.All, 1);
};

View File

@ -1,14 +1,10 @@
import { lastValueFrom } from 'rxjs';
import { getBackendSrv } from '@grafana/runtime';
import { ContactPointsState, NotifierDTO, ReceiversStateDTO, ReceiverState } from 'app/types';
import { ContactPointsState, ReceiversStateDTO, ReceiverState } from 'app/types';
import { getDatasourceAPIUid } from '../utils/datasource';
export function fetchNotifiers(): Promise<NotifierDTO[]> {
return getBackendSrv().get(`/api/alert-notifiers`);
}
interface IntegrationNameObject {
type: string;
index?: string;

View File

@ -1,6 +1,190 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`useContactPoints should return contact points with status 1`] = `
{
"contactPoints": [
{
"grafana_managed_receiver_configs": [
{
"disableResolveMessage": false,
"name": "grafana-default-email",
"secureFields": {},
"settings": {
"addresses": "gilles.demey@grafana.com",
"singleEmail": false,
},
"type": "email",
"uid": "xeKQrBrnk",
Symbol(receiver_status): {
"lastNotifyAttempt": "2023-07-02T21:35:34.841+02:00",
"lastNotifyAttemptDuration": "1ms",
"lastNotifyAttemptError": "failed to send notification to email addresses: gilles.demey@grafana.com: dial tcp 192.168.1.21:1025: connect: connection refused",
"name": "email",
"sendResolved": true,
},
Symbol(receiver_metadata): {
"description": "Sends notifications using Grafana server configured SMTP settings",
"name": "Email",
},
Symbol(receiver_plugin_metadata): undefined,
},
],
"name": "grafana-default-email",
"policies": [
{
"receiver": "grafana-default-email",
"route": {
"type": "normal",
},
},
],
},
{
"grafana_managed_receiver_configs": [
{
"disableResolveMessage": false,
"name": "lotsa-emails",
"secureFields": {},
"settings": {
"addresses": "gilles.demey+1@grafana.com, gilles.demey+2@grafana.com, gilles.demey+3@grafana.com, gilles.demey+4@grafana.com",
"singleEmail": false,
},
"type": "email",
"uid": "af306c96-35a2-4d6e-908a-4993e245dbb2",
Symbol(receiver_status): {
"lastNotifyAttempt": "",
"lastNotifyAttemptDuration": "",
"name": "email",
"sendResolved": true,
},
Symbol(receiver_metadata): {
"description": "Sends notifications using Grafana server configured SMTP settings",
"name": "Email",
},
Symbol(receiver_plugin_metadata): undefined,
},
],
"name": "lotsa-emails",
"policies": [],
},
{
"grafana_managed_receiver_configs": [
{
"disableResolveMessage": false,
"name": "Oncall-integration",
"settings": {
"url": "https://oncall-endpoint.example.com",
},
"type": "oncall",
Symbol(receiver_status): undefined,
Symbol(receiver_metadata): {
"description": "Sends notifications to Grafana OnCall",
"name": "Grafana OnCall",
},
Symbol(receiver_plugin_metadata): {
"icon": "public/img/alerting/oncall_logo.svg",
"title": "Grafana OnCall",
},
},
],
"name": "OnCall Conctact point",
"policies": [],
},
{
"grafana_managed_receiver_configs": [
{
"disableResolveMessage": false,
"name": "provisioned-contact-point",
"provenance": "api",
"secureFields": {},
"settings": {
"addresses": "gilles.demey@grafana.com",
"singleEmail": false,
},
"type": "email",
"uid": "s8SdCVjnk",
Symbol(receiver_status): {
"lastNotifyAttempt": "",
"lastNotifyAttemptDuration": "",
"name": "email",
"sendResolved": true,
},
Symbol(receiver_metadata): {
"description": "Sends notifications using Grafana server configured SMTP settings",
"name": "Email",
},
Symbol(receiver_plugin_metadata): undefined,
},
],
"name": "provisioned-contact-point",
"policies": [
{
"receiver": "provisioned-contact-point",
"route": {
"type": "normal",
},
},
],
},
{
"grafana_managed_receiver_configs": [
{
"disableResolveMessage": false,
"name": "Slack with multiple channels",
"secureFields": {
"token": true,
},
"settings": {
"recipient": "test-alerts",
},
"type": "slack",
"uid": "c02ad56a-31da-46b9-becb-4348ec0890fd",
Symbol(receiver_status): {
"lastNotifyAttempt": "",
"lastNotifyAttemptDuration": "",
"name": "slack",
"sendResolved": true,
},
Symbol(receiver_metadata): {
"description": "Sends notifications to Slack",
"name": "Slack",
},
Symbol(receiver_plugin_metadata): undefined,
},
{
"disableResolveMessage": false,
"name": "Slack with multiple channels",
"secureFields": {
"token": true,
},
"settings": {
"recipient": "test-alerts2",
},
"type": "slack",
"uid": "b286a3be-f690-49e2-8605-b075cbace2df",
Symbol(receiver_status): {
"lastNotifyAttempt": "",
"lastNotifyAttemptDuration": "",
"name": "slack",
"sendResolved": true,
},
Symbol(receiver_metadata): {
"description": "Sends notifications to Slack",
"name": "Slack",
},
Symbol(receiver_plugin_metadata): undefined,
},
],
"name": "Slack with multiple channels",
"policies": [],
},
],
"error": undefined,
"isLoading": false,
}
`;
exports[`useContactPoints when having oncall plugin installed and no alert manager config data should return contact points with oncall metadata 1`] = `
{
"contactPoints": [
{
@ -184,177 +368,5 @@ exports[`useContactPoints should return contact points with status 1`] = `
],
"error": undefined,
"isLoading": false,
"refetchReceivers": [Function],
}
`;
exports[`useContactPoints when having oncall plugin installed and no alert manager config data should return contact points with oncall metadata 1`] = `
{
"contactPoints": [
{
"grafana_managed_receiver_configs": [
{
"disableResolveMessage": false,
"name": "grafana-default-email",
"secureFields": {},
"settings": {
"addresses": "gilles.demey@grafana.com",
"singleEmail": false,
},
"type": "email",
"uid": "xeKQrBrnk",
Symbol(receiver_status): {
"lastNotifyAttempt": "2023-07-02T21:35:34.841+02:00",
"lastNotifyAttemptDuration": "1ms",
"lastNotifyAttemptError": "failed to send notification to email addresses: gilles.demey@grafana.com: dial tcp 192.168.1.21:1025: connect: connection refused",
"name": "email",
"sendResolved": true,
},
Symbol(receiver_metadata): {
"description": "Sends notifications using Grafana server configured SMTP settings",
"name": "Email",
},
Symbol(receiver_plugin_metadata): undefined,
},
],
"name": "grafana-default-email",
"policies": undefined,
},
{
"grafana_managed_receiver_configs": [
{
"disableResolveMessage": false,
"name": "lotsa-emails",
"secureFields": {},
"settings": {
"addresses": "gilles.demey+1@grafana.com, gilles.demey+2@grafana.com, gilles.demey+3@grafana.com, gilles.demey+4@grafana.com",
"singleEmail": false,
},
"type": "email",
"uid": "af306c96-35a2-4d6e-908a-4993e245dbb2",
Symbol(receiver_status): {
"lastNotifyAttempt": "",
"lastNotifyAttemptDuration": "",
"name": "email",
"sendResolved": true,
},
Symbol(receiver_metadata): {
"description": "Sends notifications using Grafana server configured SMTP settings",
"name": "Email",
},
Symbol(receiver_plugin_metadata): undefined,
},
],
"name": "lotsa-emails",
"policies": undefined,
},
{
"grafana_managed_receiver_configs": [
{
"disableResolveMessage": false,
"name": "Oncall-integration",
"settings": {
"url": "https://oncall-endpoint.example.com",
},
"type": "oncall",
Symbol(receiver_status): undefined,
Symbol(receiver_metadata): {
"description": "Sends notifications to Grafana OnCall",
"name": "Grafana OnCall",
},
Symbol(receiver_plugin_metadata): {
"icon": "public/img/alerting/oncall_logo.svg",
"title": "Grafana OnCall",
},
},
],
"name": "OnCall Conctact point",
"policies": undefined,
},
{
"grafana_managed_receiver_configs": [
{
"disableResolveMessage": false,
"name": "provisioned-contact-point",
"provenance": "api",
"secureFields": {},
"settings": {
"addresses": "gilles.demey@grafana.com",
"singleEmail": false,
},
"type": "email",
"uid": "s8SdCVjnk",
Symbol(receiver_status): {
"lastNotifyAttempt": "",
"lastNotifyAttemptDuration": "",
"name": "email",
"sendResolved": true,
},
Symbol(receiver_metadata): {
"description": "Sends notifications using Grafana server configured SMTP settings",
"name": "Email",
},
Symbol(receiver_plugin_metadata): undefined,
},
],
"name": "provisioned-contact-point",
"policies": undefined,
},
{
"grafana_managed_receiver_configs": [
{
"disableResolveMessage": false,
"name": "Slack with multiple channels",
"secureFields": {
"token": true,
},
"settings": {
"recipient": "test-alerts",
},
"type": "slack",
"uid": "c02ad56a-31da-46b9-becb-4348ec0890fd",
Symbol(receiver_status): {
"lastNotifyAttempt": "",
"lastNotifyAttemptDuration": "",
"name": "slack",
"sendResolved": true,
},
Symbol(receiver_metadata): {
"description": "Sends notifications to Slack",
"name": "Slack",
},
Symbol(receiver_plugin_metadata): undefined,
},
{
"disableResolveMessage": false,
"name": "Slack with multiple channels",
"secureFields": {
"token": true,
},
"settings": {
"recipient": "test-alerts2",
},
"type": "slack",
"uid": "b286a3be-f690-49e2-8605-b075cbace2df",
Symbol(receiver_status): {
"lastNotifyAttempt": "",
"lastNotifyAttemptDuration": "",
"name": "slack",
"sendResolved": true,
},
Symbol(receiver_metadata): {
"description": "Sends notifications to Slack",
"name": "Slack",
},
Symbol(receiver_plugin_metadata): undefined,
},
],
"name": "Slack with multiple channels",
"policies": undefined,
},
],
"error": undefined,
"isLoading": false,
"refetchReceivers": [Function],
}
`;

View File

@ -1,17 +1,30 @@
import { renderHook, waitFor } from '@testing-library/react';
import { TestProvider } from 'test/helpers/TestProvider';
import { ReactNode } from 'react';
import { getWrapper } from 'test/test-utils';
import alertmanagerMock from 'app/features/alerting/unified/components/contact-points/__mocks__/alertmanager.config.mock.json';
import { disablePlugin } from 'app/features/alerting/unified/mocks/server/configure';
import { setOnCallIntegrations } from 'app/features/alerting/unified/mocks/server/handlers/plugins/configure-plugins';
import { SupportedPlugin } from 'app/features/alerting/unified/types/pluginBridges';
import { AccessControlAction } from 'app/types';
import { mockApi, setupMswServer } from '../../mockApi';
import { setupMswServer } from '../../mockApi';
import { grantUserPermissions } from '../../mocks';
import { AlertmanagerProvider } from '../../state/AlertmanagerContext';
import { useContactPointsWithStatus } from './useContactPoints';
const server = setupMswServer();
const wrapper = ({ children }: { children: ReactNode }) => {
const ProviderWrapper = getWrapper({ renderWithRouter: true });
return (
<ProviderWrapper>
<AlertmanagerProvider accessType="notification" alertmanagerSourceName="grafana">
{children}
</AlertmanagerProvider>
</ProviderWrapper>
);
};
setupMswServer();
describe('useContactPoints', () => {
beforeAll(() => {
@ -19,23 +32,10 @@ describe('useContactPoints', () => {
});
it('should return contact points with status', async () => {
setOnCallIntegrations([
{
display_name: 'grafana-integration',
value: 'ABC123',
integration_url: 'https://oncall-endpoint.example.com',
},
]);
mockApi(server).getContactPointsList(receivers);
disablePlugin(SupportedPlugin.OnCall);
const { result } = renderHook(() => useContactPointsWithStatus(), {
wrapper: ({ children }) => (
<TestProvider>
<AlertmanagerProvider accessType={'notification'} alertmanagerSourceName={'grafana'}>
{children}
</AlertmanagerProvider>
</TestProvider>
),
wrapper,
});
await waitFor(() => {
@ -53,20 +53,10 @@ describe('useContactPoints', () => {
integration_url: 'https://oncall-endpoint.example.com',
},
]);
mockApi(server).getContactPointsList(receivers);
const { result } = renderHook(
() => useContactPointsWithStatus({ includePoliciesCount: false, receiverStatusPollingInterval: 0 }),
{
wrapper: ({ children }) => (
<TestProvider>
<AlertmanagerProvider accessType={'notification'} alertmanagerSourceName={'grafana'}>
{children}
</AlertmanagerProvider>
</TestProvider>
),
}
);
const { result } = renderHook(() => useContactPointsWithStatus(), {
wrapper,
});
await waitFor(() => {
expect(result.current.isLoading).toBe(false);
@ -75,5 +65,3 @@ describe('useContactPoints', () => {
});
});
});
const receivers = JSON.parse(JSON.stringify(alertmanagerMock)).alertmanager_config.receivers;

View File

@ -5,9 +5,10 @@
import { produce } from 'immer';
import { remove } from 'lodash';
import { useMemo } from 'react';
import { alertmanagerApi } from '../../api/alertmanagerApi';
import { onCallApi, OnCallIntegrationDTO } from '../../api/onCallApi';
import { onCallApi } from '../../api/onCallApi';
import { usePluginBridge } from '../../hooks/usePluginBridge';
import { useAlertmanager } from '../../state/AlertmanagerContext';
import { SupportedPlugin } from '../../types/pluginBridges';
@ -27,82 +28,116 @@ const RECEIVER_STATUS_POLLING_INTERVAL = 10 * 1000; // 10 seconds
* 3. (if available) additional metadata about Grafana Managed contact points
* 4. (if available) the OnCall plugin metadata
*/
interface UseContactPointsWithStatusOptions {
includePoliciesCount: boolean;
receiverStatusPollingInterval?: number;
}
const defaultHookOptions = {
includePoliciesCount: true,
receiverStatusPollingInterval: RECEIVER_STATUS_POLLING_INTERVAL,
const {
useGetAlertmanagerConfigurationQuery,
useGetContactPointsListQuery,
useGetContactPointsStatusQuery,
useGrafanaNotifiersQuery,
useLazyGetAlertmanagerConfigurationQuery,
useUpdateAlertmanagerConfigurationMutation,
} = alertmanagerApi;
const { useGrafanaOnCallIntegrationsQuery } = onCallApi;
/**
* Check if OnCall is installed, and fetch the list of integrations if so.
*
* Otherwise, returns no data
*/
const useOnCallIntegrations = ({ skip }: { skip?: boolean } = {}) => {
const { installed, loading } = usePluginBridge(SupportedPlugin.OnCall);
const oncallIntegrationsResponse = useGrafanaOnCallIntegrationsQuery(undefined, { skip: skip || !installed });
return useMemo(() => {
if (installed) {
return oncallIntegrationsResponse;
}
return {
isLoading: loading,
data: undefined,
};
}, [installed, loading, oncallIntegrationsResponse]);
};
export function useContactPointsWithStatus({
includePoliciesCount,
receiverStatusPollingInterval,
}: UseContactPointsWithStatusOptions = defaultHookOptions) {
const { selectedAlertmanager, isGrafanaAlertmanager } = useAlertmanager();
const { installed: onCallPluginInstalled, loading: onCallPluginStatusLoading } = usePluginBridge(
SupportedPlugin.OnCall
);
/**
* Fetch contact points from separate endpoint (i.e. not the Alertmanager config) and combine with
* OnCall integrations and any additional metadata from list of notifiers
* (e.g. hydrate with additional names/descriptions)
*/
export const useGetContactPoints = () => {
const onCallResponse = useOnCallIntegrations();
const alertNotifiers = useGrafanaNotifiersQuery();
const contactPointsListResponse = useGetContactPointsListQuery();
// fetch receiver status if we're dealing with a Grafana Managed Alertmanager
const fetchContactPointsStatus = alertmanagerApi.endpoints.getContactPointsStatus.useQuery(undefined, {
return useMemo(() => {
const isLoading = onCallResponse.isLoading || alertNotifiers.isLoading || contactPointsListResponse.isLoading;
if (isLoading || !contactPointsListResponse.data) {
return {
...contactPointsListResponse,
// If we're inside this block, it means that at least one of the endpoints we care about is still loading,
// but the contactPointsListResponse may have in fact finished.
// If we were to use _that_ loading state, it might be inaccurate elsewhere when consuming this hook,
// so we explicitly say "yes, this is definitely still loading"
isLoading: true,
contactPoints: [],
};
}
const enhanced = enhanceContactPointsWithMetadata(
[],
alertNotifiers.data,
onCallResponse?.data,
contactPointsListResponse.data,
undefined
);
return {
...contactPointsListResponse,
contactPoints: enhanced,
};
}, [
alertNotifiers.data,
alertNotifiers.isLoading,
contactPointsListResponse,
onCallResponse?.data,
onCallResponse.isLoading,
]);
};
export function useContactPointsWithStatus() {
const { selectedAlertmanager, isGrafanaAlertmanager } = useAlertmanager();
const defaultOptions = {
refetchOnFocus: true,
refetchOnReconnect: true,
// re-fetch status every so often for up-to-date information, allow disabling by passing "receiverStatusPollingInterval: 0"
pollingInterval: receiverStatusPollingInterval,
};
// fetch receiver status if we're dealing with a Grafana Managed Alertmanager
const fetchContactPointsStatus = useGetContactPointsStatusQuery(undefined, {
...defaultOptions,
// re-fetch status every so often for up-to-date information
pollingInterval: RECEIVER_STATUS_POLLING_INTERVAL,
// skip fetching receiver statuses if not Grafana AM
skip: !isGrafanaAlertmanager,
});
// fetch notifier metadata from the Grafana API if we're using a Grafana AM this will be used to add additional
// metadata and canonical names to the receiver
const fetchReceiverMetadata = alertmanagerApi.endpoints.grafanaNotifiers.useQuery(undefined, {
const fetchReceiverMetadata = useGrafanaNotifiersQuery(undefined, {
skip: !isGrafanaAlertmanager,
});
// if the OnCall plugin is installed, fetch its list of integrations so we can match those to the Grafana Managed contact points
const { data: onCallIntegrations, isLoading: onCallPluginIntegrationsLoading } =
onCallApi.endpoints.grafanaOnCallIntegrations.useQuery(undefined, {
skip: !onCallPluginInstalled || !isGrafanaAlertmanager,
});
// null = no installed, undefined = loading, [n] is installed with integrations
let onCallMetadata: null | undefined | OnCallIntegrationDTO[] = undefined;
if (onCallPluginInstalled) {
onCallMetadata = onCallIntegrations ?? [];
} else if (onCallPluginInstalled === false) {
onCallMetadata = null;
}
const { data: onCallMetadata, isLoading: onCallPluginIntegrationsLoading } = useOnCallIntegrations({
skip: !isGrafanaAlertmanager,
});
// fetch the latest config from the Alertmanager
// we use this endpoint only when we need to get the number of policies
const fetchAlertmanagerConfiguration = alertmanagerApi.endpoints.getAlertmanagerConfiguration.useQuery(
selectedAlertmanager!,
{
refetchOnFocus: true,
refetchOnReconnect: true,
selectFromResult: (result) => ({
...result,
contactPoints: result.data
? enhanceContactPointsWithMetadata(
fetchContactPointsStatus.data,
fetchReceiverMetadata.data,
onCallMetadata,
result.data.alertmanager_config.receivers ?? [],
result.data
)
: [],
}),
skip: !includePoliciesCount,
}
);
// for Grafana Managed Alertmanager, we use the new read-only endpoint for getting the list of contact points
const fetchGrafanaContactPoints = alertmanagerApi.endpoints.getContactPointsList.useQuery(undefined, {
refetchOnFocus: true,
refetchOnReconnect: true,
const fetchAlertmanagerConfiguration = useGetAlertmanagerConfigurationQuery(selectedAlertmanager!, {
...defaultOptions,
selectFromResult: (result) => ({
...result,
contactPoints: result.data
@ -110,40 +145,28 @@ export function useContactPointsWithStatus({
fetchContactPointsStatus.data,
fetchReceiverMetadata.data,
onCallMetadata,
result.data, // contact points from the new readonly endpoint
undefined //no config data
result.data.alertmanager_config.receivers ?? [],
result.data
)
: [],
}),
skip: includePoliciesCount || !isGrafanaAlertmanager,
});
// we will fail silently for fetching OnCall plugin status and integrations
const error =
fetchAlertmanagerConfiguration.error || fetchGrafanaContactPoints.error || fetchContactPointsStatus.error;
const error = fetchAlertmanagerConfiguration.error || fetchContactPointsStatus.error;
const isLoading =
fetchAlertmanagerConfiguration.isLoading ||
fetchGrafanaContactPoints.isLoading ||
fetchContactPointsStatus.isLoading ||
onCallPluginStatusLoading ||
onCallPluginIntegrationsLoading;
fetchAlertmanagerConfiguration.isLoading || fetchContactPointsStatus.isLoading || onCallPluginIntegrationsLoading;
const unsortedContactPoints = includePoliciesCount
? fetchAlertmanagerConfiguration.contactPoints
: fetchGrafanaContactPoints.contactPoints;
const contactPoints = unsortedContactPoints.sort((a, b) => a.name.localeCompare(b.name));
return {
error,
isLoading,
contactPoints,
refetchReceivers: fetchGrafanaContactPoints.refetch,
contactPoints: fetchAlertmanagerConfiguration.contactPoints,
};
}
export function useDeleteContactPoint(selectedAlertmanager: string) {
const [fetchAlertmanagerConfig] = alertmanagerApi.endpoints.getAlertmanagerConfiguration.useLazyQuery();
const [updateAlertManager, updateAlertmanagerState] =
alertmanagerApi.endpoints.updateAlertmanagerConfiguration.useMutation();
const [fetchAlertmanagerConfig] = useLazyGetAlertmanagerConfigurationQuery();
const [updateAlertManager, updateAlertmanagerState] = useUpdateAlertmanagerConfigurationMutation();
const deleteTrigger = (contactPointName: string) => {
return fetchAlertmanagerConfig(selectedAlertmanager).then(({ data }) => {

View File

@ -127,7 +127,7 @@ export function enhanceContactPointsWithMetadata(
? (alertmanagerConfiguration?.alertmanager_config.receivers ?? [])
: (contactPoints ?? []);
return contactPointsList.map((contactPoint) => {
const enhanced = contactPointsList.map((contactPoint) => {
const receivers = extractReceivers(contactPoint);
const statusForReceiver = status.find((status) => status.name === contactPoint.name);
@ -152,6 +152,8 @@ export function enhanceContactPointsWithMetadata(
}),
};
});
return enhanced.sort((a, b) => a.name.localeCompare(b.name));
}
export function isAutoGeneratedPolicy(route: Route) {

View File

@ -4,9 +4,8 @@ import { Props } from 'react-virtualized-auto-sizer';
import { render, waitFor, waitForElementToBeRemoved, userEvent } from 'test/test-utils';
import { byRole, byTestId, byText } from 'testing-library-selector';
import { DashboardSearchItemType } from '../../../../search/types';
import { mockExportApi, mockSearchApi, setupMswServer } from '../../mockApi';
import { mockDashboardSearchItem, mockDataSource } from '../../mocks';
import { mockExportApi, setupMswServer } from '../../mockApi';
import { mockDataSource } from '../../mocks';
import { grafanaRulerRule } from '../../mocks/grafanaRulerApi';
import { setupDataSources } from '../../testSetup/datasources';
@ -67,15 +66,6 @@ describe('GrafanaModifyExport', () => {
setupDataSources(dataSources.default);
it('Should render edit form for the specified rule', async () => {
mockSearchApi(server).search([
mockDashboardSearchItem({
title: grafanaRulerRule.grafana_alert.title,
uid: grafanaRulerRule.grafana_alert.namespace_uid,
url: '',
tags: [],
type: DashboardSearchItemType.DashFolder,
}),
]);
mockExportApi(server).modifiedExport(grafanaRulerRule.grafana_alert.namespace_uid, {
yaml: 'Yaml Export Content',
json: 'Json Export Content',

View File

@ -8,7 +8,7 @@ import { RuleFormValues } from 'app/features/alerting/unified/types/rule-form';
import { AlertManagerDataSource } from 'app/features/alerting/unified/utils/datasource';
import { ContactPointReceiverSummary } from '../../../contact-points/ContactPoint';
import { useContactPointsWithStatus } from '../../../contact-points/useContactPoints';
import { useGetContactPoints } from '../../../contact-points/useContactPoints';
import { ContactPointWithMetadata } from '../../../contact-points/utils';
import { ContactPointDetails } from './contactPoint/ContactPointDetails';
@ -28,8 +28,9 @@ export function AlertManagerManualRouting({ alertManager }: AlertManagerManualRo
isLoading,
error: errorInContactPointStatus,
contactPoints,
refetchReceivers,
} = useContactPointsWithStatus({ includePoliciesCount: false, receiverStatusPollingInterval: 0 });
refetch: refetchReceivers,
} = useGetContactPoints();
const [selectedContactPointWithMetadata, setSelectedContactPointWithMetadata] = useState<
ContactPointWithMetadata | undefined
>();

View File

@ -1,72 +1,39 @@
import { render, screen, waitFor, waitForElementToBeRemoved } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import * as React from 'react';
import { ReactNode } from 'react';
import { Route } from 'react-router-dom';
import { TestProvider } from 'test/helpers/TestProvider';
import { ui } from 'test/helpers/alertingRuleEditor';
import { clickSelectOption } from 'test/helpers/selectOptionInTest';
import { render, screen, waitFor, waitForElementToBeRemoved, userEvent } from 'test/test-utils';
import { byRole } from 'testing-library-selector';
import { config, locationService } from '@grafana/runtime';
import { config } from '@grafana/runtime';
import { contextSrv } from 'app/core/services/context_srv';
import RuleEditor from 'app/features/alerting/unified/RuleEditor';
import * as ruler from 'app/features/alerting/unified/api/ruler';
import * as useContactPoints from 'app/features/alerting/unified/components/contact-points/useContactPoints';
import { setupMswServer } from 'app/features/alerting/unified/mockApi';
import { grantUserPermissions, mockDataSource } from 'app/features/alerting/unified/mocks';
import { setAlertmanagerChoices } from 'app/features/alerting/unified/mocks/server/configure';
import { FOLDER_TITLE_HAPPY_PATH } from 'app/features/alerting/unified/mocks/server/handlers/search';
import { AlertmanagerProvider } from 'app/features/alerting/unified/state/AlertmanagerContext';
import * as utils_config from 'app/features/alerting/unified/utils/config';
import {
DataSourceType,
GRAFANA_DATASOURCE_NAME,
GRAFANA_RULES_SOURCE_NAME,
useGetAlertManagerDataSourcesByPermissionAndConfig,
} from 'app/features/alerting/unified/utils/datasource';
import { getDefaultQueries } from 'app/features/alerting/unified/utils/rule-form';
import { searchFolders } from 'app/features/manage-dashboards/state/actions';
import { DashboardSearchHit, DashboardSearchItemType } from 'app/features/search/types';
import { AlertmanagerChoice } from 'app/plugins/datasource/alertmanager/types';
import { AccessControlAction } from 'app/types';
import { GrafanaAlertStateDecision } from 'app/types/unified-alerting-dto';
import { grafanaRulerEmptyGroup, grafanaRulerNamespace2, grafanaRulerRule } from '../../../../mocks/grafanaRulerApi';
import { grafanaRulerEmptyGroup, grafanaRulerNamespace2 } from '../../../../mocks/grafanaRulerApi';
import { setupDataSources } from '../../../../testSetup/datasources';
import { RECEIVER_META_KEY } from '../../../contact-points/useContactPoints';
import { ContactPointWithMetadata } from '../../../contact-points/utils';
import { ExpressionEditorProps } from '../../ExpressionEditor';
jest.mock('app/features/alerting/unified/components/rule-editor/ExpressionEditor', () => ({
ExpressionEditor: ({ value, onChange }: ExpressionEditorProps) => (
<input value={value} data-testid="expr" onChange={(e) => onChange(e.target.value)} />
),
}));
jest.mock('app/features/manage-dashboards/state/actions');
jest.mock('app/core/components/AppChrome/AppChromeUpdate', () => ({
AppChromeUpdate: ({ actions }: { actions: React.ReactNode }) => <div>{actions}</div>,
AppChromeUpdate: ({ actions }: { actions: ReactNode }) => <div>{actions}</div>,
}));
// there's no angular scope in test and things go terribly wrong when trying to render the query editor row.
// lets just skip it
jest.mock('app/features/query/components/QueryEditorRow', () => ({
QueryEditorRow: () => <p>hi</p>,
}));
const user = userEvent.setup();
// jest.spyOn(utils_config, 'getAllDataSources');
// jest.spyOn(dsByPermission, 'useAlertManagersByPermission');
jest.spyOn(useContactPoints, 'useContactPointsWithStatus');
jest.setTimeout(60 * 1000);
const mocks = {
getAllDataSources: jest.mocked(utils_config.getAllDataSources),
searchFolders: jest.mocked(searchFolders),
useContactPointsWithStatus: jest.mocked(useContactPoints.useContactPointsWithStatus),
useGetAlertManagerDataSourcesByPermissionAndConfig: jest.mocked(useGetAlertManagerDataSourcesByPermissionAndConfig),
api: {
setRulerRuleGroup: jest.spyOn(ruler, 'setRulerRuleGroup'),
},
@ -74,11 +41,37 @@ const mocks = {
setupMswServer();
const dataSources = {
default: mockDataSource(
{
type: 'prometheus',
name: 'Prom',
isDefault: true,
},
{ alerting: false }
),
am: mockDataSource({
name: 'Alertmanager',
type: DataSourceType.Alertmanager,
}),
};
setupDataSources(dataSources.default, dataSources.am);
const selectFolderAndGroup = async () => {
const user = userEvent.setup();
const folderInput = await ui.inputs.folder.find();
await clickSelectOption(folderInput, FOLDER_TITLE_HAPPY_PATH);
const groupInput = await ui.inputs.group.find();
await user.click(await byRole('combobox').find(groupInput));
await clickSelectOption(groupInput, grafanaRulerEmptyGroup.name);
};
describe('Can create a new grafana managed alert unsing simplified routing', () => {
beforeEach(() => {
jest.clearAllMocks();
contextSrv.isEditor = true;
contextSrv.hasEditPermissionInFolders = true;
config.featureToggles.alertingSimplifiedRouting = true;
grantUserPermissions([
AccessControlAction.AlertingRuleRead,
AccessControlAction.AlertingRuleUpdate,
@ -96,74 +89,26 @@ describe('Can create a new grafana managed alert unsing simplified routing', ()
]);
});
const dataSources = {
default: mockDataSource(
{
type: 'prometheus',
name: 'Prom',
isDefault: true,
},
{ alerting: false }
),
am: mockDataSource({
name: 'Alertmanager',
type: DataSourceType.Alertmanager,
}),
};
setupDataSources(dataSources.default, dataSources.am);
it('cannot create new grafana managed alert when using simplified routing and not selecting a contact point', async () => {
// no contact points found
mocks.useContactPointsWithStatus.mockReturnValue({
contactPoints: [],
isLoading: false,
error: undefined,
refetchReceivers: jest.fn(),
});
const user = userEvent.setup();
mocks.api.setRulerRuleGroup.mockResolvedValue();
mocks.searchFolders.mockResolvedValue([
{
title: 'Folder A',
uid: grafanaRulerRule.grafana_alert.namespace_uid,
id: 1,
type: DashboardSearchItemType.DashDB,
},
{
title: 'Folder B',
id: 2,
},
{
title: 'Folder / with slash',
id: 2,
uid: 'b',
type: DashboardSearchItemType.DashDB,
},
] as DashboardSearchHit[]);
config.featureToggles.alertingSimplifiedRouting = true;
renderSimplifiedRuleEditor();
await waitForElementToBeRemoved(screen.getAllByTestId('Spinner'));
await user.type(await ui.inputs.name.find(), 'my great new rule');
const folderInput = await ui.inputs.folder.find();
await clickSelectOption(folderInput, 'Folder A');
const groupInput = await ui.inputs.group.find();
await user.click(byRole('combobox').get(groupInput));
await clickSelectOption(groupInput, grafanaRulerRule.grafana_alert.rule_group);
await selectFolderAndGroup();
//select contact point routing
await user.click(ui.inputs.simplifiedRouting.contactPointRouting.get());
// do not select a contact point
// save and check that call to backend was not made
await user.click(ui.buttons.saveAndExit.get());
await waitFor(() => {
expect(screen.getByText('Contact point is required.')).toBeInTheDocument();
expect(mocks.api.setRulerRuleGroup).not.toHaveBeenCalled();
});
expect(await screen.findByText('Contact point is required.')).toBeInTheDocument();
expect(mocks.api.setRulerRuleGroup).not.toHaveBeenCalled();
});
it('simplified routing is not available when Grafana AM is not enabled', async () => {
config.featureToggles.alertingSimplifiedRouting = true;
setAlertmanagerChoices(AlertmanagerChoice.External, 1);
renderSimplifiedRuleEditor();
await waitForElementToBeRemoved(screen.getAllByTestId('Spinner'));
@ -172,69 +117,20 @@ describe('Can create a new grafana managed alert unsing simplified routing', ()
});
it('can create new grafana managed alert when using simplified routing and selecting a contact point', async () => {
const contactPointsAvailable: ContactPointWithMetadata[] = [
{
name: 'contact_point1',
grafana_managed_receiver_configs: [
{
name: 'contact_point1',
type: 'email',
disableResolveMessage: false,
[RECEIVER_META_KEY]: {
name: 'contact_point1',
description: 'contact_point1 description',
},
settings: {},
},
],
policies: [],
},
];
mocks.useContactPointsWithStatus.mockReturnValue({
contactPoints: contactPointsAvailable,
isLoading: false,
error: undefined,
refetchReceivers: jest.fn(),
});
const user = userEvent.setup();
const contactPointName = 'lotsa-emails';
mocks.api.setRulerRuleGroup.mockResolvedValue();
mocks.searchFolders.mockResolvedValue([
{
title: 'Folder A',
uid: grafanaRulerNamespace2.uid,
id: 1,
type: DashboardSearchItemType.DashDB,
},
{
title: 'Folder B',
id: 2,
uid: 'b',
type: DashboardSearchItemType.DashDB,
},
{
title: 'Folder / with slash',
uid: 'c',
id: 2,
type: DashboardSearchItemType.DashDB,
},
] as DashboardSearchHit[]);
config.featureToggles.alertingSimplifiedRouting = true;
renderSimplifiedRuleEditor();
await waitForElementToBeRemoved(screen.getAllByTestId('Spinner'));
await user.type(await ui.inputs.name.find(), 'my great new rule');
const folderInput = await ui.inputs.folder.find();
await clickSelectOption(folderInput, 'Folder A');
const groupInput = await ui.inputs.group.find();
await user.click(byRole('combobox').get(groupInput));
await clickSelectOption(groupInput, grafanaRulerEmptyGroup.name);
await selectFolderAndGroup();
//select contact point routing
await user.click(ui.inputs.simplifiedRouting.contactPointRouting.get());
const contactPointInput = await ui.inputs.simplifiedRouting.contactPoint.find();
await user.click(byRole('combobox').get(contactPointInput));
await clickSelectOption(contactPointInput, 'contact_point1');
await clickSelectOption(contactPointInput, contactPointName);
// save and check what was sent to backend
await user.click(ui.buttons.saveAndExit.get());
@ -262,7 +158,7 @@ describe('Can create a new grafana managed alert unsing simplified routing', ()
group_interval: undefined,
group_wait: undefined,
mute_timings: undefined,
receiver: 'contact_point1',
receiver: contactPointName,
repeat_interval: undefined,
},
},
@ -274,13 +170,10 @@ describe('Can create a new grafana managed alert unsing simplified routing', ()
});
function renderSimplifiedRuleEditor() {
locationService.push(`/alerting/new/alerting`);
return render(
<TestProvider>
<AlertmanagerProvider alertmanagerSourceName={GRAFANA_DATASOURCE_NAME} accessType="notification">
<Route path={['/alerting/new/:type', '/alerting/:id/edit']} component={RuleEditor} />
</AlertmanagerProvider>
</TestProvider>
<AlertmanagerProvider alertmanagerSourceName={GRAFANA_DATASOURCE_NAME} accessType="notification">
<Route path={['/alerting/new/:type', '/alerting/:id/edit']} component={RuleEditor} />
</AlertmanagerProvider>,
{ historyOptions: { initialEntries: ['/alerting/new/alerting'] } }
);
}

View File

@ -4,7 +4,7 @@ import { TestProvider } from 'test/helpers/TestProvider';
import { byLabelText, byRole } from 'testing-library-selector';
import { locationService, setDataSourceSrv } from '@grafana/runtime';
import { mockSearchApi, setupMswServer } from 'app/features/alerting/unified/mockApi';
import { setupMswServer } from 'app/features/alerting/unified/mockApi';
import * as analytics from '../../Analytics';
import { MockDataSourceSrv } from '../../mocks';
@ -12,7 +12,7 @@ import { setupPluginsExtensionsHook } from '../../testSetup/plugins';
import RulesFilter from './RulesFilter';
const server = setupMswServer();
setupMswServer();
jest.spyOn(analytics, 'logInfo');
jest.mock('./MultipleDataSourcePicker', () => {
@ -43,7 +43,6 @@ const ui = {
beforeEach(() => {
locationService.replace({ search: '' });
mockSearchApi(server).search([]);
});
describe('RulesFilter', () => {

View File

@ -20,7 +20,6 @@ import {
AlertManagerCortexConfig,
AlertmanagerReceiver,
EmailConfig,
GrafanaManagedContactPoint,
GrafanaManagedReceiverConfig,
MatcherOperator,
Route,
@ -170,10 +169,6 @@ export function mockApi(server: SetupServer) {
)
);
},
getContactPointsList: (response: GrafanaManagedContactPoint[]) => {
server.use(http.get(`/api/v1/notifications/receivers`, () => HttpResponse.json(response)));
},
};
}

View File

@ -9,9 +9,12 @@ import evalHandlers from 'app/features/alerting/unified/mocks/server/handlers/ev
import folderHandlers from 'app/features/alerting/unified/mocks/server/handlers/folders';
import grafanaRulerHandlers from 'app/features/alerting/unified/mocks/server/handlers/grafanaRuler';
import mimirRulerHandlers from 'app/features/alerting/unified/mocks/server/handlers/mimirRuler';
import notificationsHandlers from 'app/features/alerting/unified/mocks/server/handlers/notifications';
import pluginsHandlers from 'app/features/alerting/unified/mocks/server/handlers/plugins';
import allPluginHandlers from 'app/features/alerting/unified/mocks/server/handlers/plugins/all-plugin-handlers';
import searchHandlers from 'app/features/alerting/unified/mocks/server/handlers/search';
import silenceHandlers from 'app/features/alerting/unified/mocks/server/handlers/silences';
/**
* Array of all mock handlers that are required across Alerting tests
*/
@ -25,6 +28,8 @@ const allHandlers = [
...folderHandlers,
...pluginsHandlers,
...silenceHandlers,
...searchHandlers,
...notificationsHandlers,
...allPluginHandlers,
];

View File

@ -0,0 +1,13 @@
import { HttpResponse, http } from 'msw';
import alertmanagerConfig from 'app/features/alerting/unified/components/contact-points/__mocks__/alertmanager.config.mock.json';
import { GrafanaManagedContactPoint } from 'app/plugins/datasource/alertmanager/types';
const defaultReceiversResponse: GrafanaManagedContactPoint[] = JSON.parse(JSON.stringify(alertmanagerConfig))
.alertmanager_config.receivers;
const getNotificationReceiversHandler = (response = defaultReceiversResponse) =>
http.get('/api/v1/notifications/receivers', () => HttpResponse.json(response));
const handlers = [getNotificationReceiversHandler()];
export default handlers;

View File

@ -0,0 +1,34 @@
import { HttpResponse, http } from 'msw';
import { grafanaRulerNamespace2 } from 'app/features/alerting/unified/mocks/grafanaRulerApi';
import { DashboardSearchItemType } from 'app/features/search/types';
export const FOLDER_TITLE_HAPPY_PATH = 'Folder A';
// TODO: Generalise/scaffold out default response for search
// to be more multi purpose
const defaultSearchResponse = [
{
title: FOLDER_TITLE_HAPPY_PATH,
uid: grafanaRulerNamespace2.uid,
id: 1,
type: DashboardSearchItemType.DashFolder,
},
{
title: 'Folder B',
id: 2,
},
{
title: 'Folder / with slash',
id: 2,
uid: 'b',
type: DashboardSearchItemType.DashFolder,
},
];
export const searchHandler = (response = defaultSearchResponse) =>
http.get(`/api/search`, () => HttpResponse.json(response));
const handlers = [searchHandler()];
export default handlers;

View File

@ -10,7 +10,7 @@ import {
Receiver,
TestReceiversAlert,
} from 'app/plugins/datasource/alertmanager/types';
import { FolderDTO, NotifierDTO, StoreState, ThunkResult } from 'app/types';
import { FolderDTO, StoreState, ThunkResult } from 'app/types';
import {
CombinedRuleGroup,
CombinedRuleNamespace,
@ -49,7 +49,6 @@ import {
import { alertmanagerApi } from '../api/alertmanagerApi';
import { fetchAnnotations } from '../api/annotations';
import { discoverFeatures } from '../api/buildInfo';
import { fetchNotifiers } from '../api/grafana';
import { FetchPromRulesFilter, fetchRules } from '../api/prometheus';
import { FetchRulerRulesFilter, deleteRulerRulesGroup, fetchRulerRules, setRulerRuleGroup } from '../api/ruler';
import { RuleFormValues } from '../types/rule-form';
@ -437,11 +436,6 @@ function reportSwitchingRoutingType(values: RuleFormValues, existingRule: RuleWi
}
}
export const fetchGrafanaNotifiersAction = createAsyncThunk(
'unifiedalerting/fetchGrafanaNotifiers',
(): Promise<NotifierDTO[]> => withSerializedError(fetchNotifiers())
);
export const fetchGrafanaAnnotationsAction = createAsyncThunk(
'unifiedalerting/fetchGrafanaAnnotations',
(alertId: string): Promise<StateHistoryItem[]> => withSerializedError(fetchAnnotations(alertId))

View File

@ -7,7 +7,6 @@ import {
fetchAlertGroupsAction,
fetchFolderAction,
fetchGrafanaAnnotationsAction,
fetchGrafanaNotifiersAction,
fetchPromRulesAction,
fetchRulerRulesAction,
fetchRulesSourceBuildInfoAction,
@ -28,7 +27,6 @@ export const reducer = combineReducers({
ruleForm: combineReducers({
saveRule: createAsyncSlice('saveRule', saveRuleFormAction).reducer,
}),
grafanaNotifiers: createAsyncSlice('grafanaNotifiers', fetchGrafanaNotifiersAction).reducer,
saveAMConfig: createAsyncSlice('saveAMConfig', updateAlertManagerConfigAction).reducer,
deleteAMConfig: createAsyncSlice('deleteAMConfig', deleteAlertManagerConfigAction).reducer,
folders: createAsyncMapSlice('folders', fetchFolderAction, (uid) => uid).reducer,