Alerting: button to test contact point (#37475)

This commit is contained in:
Domas
2021-08-18 10:16:35 +03:00
committed by GitHub
parent a160930e24
commit cb9912ec0a
12 changed files with 176 additions and 10 deletions

View File

@@ -100,6 +100,7 @@ export const getAvailableIcons = () =>
'list-ui-alt',
'list-ul',
'lock',
'message',
'minus',
'minus-circle',
'mobile-android',

View File

@@ -63,12 +63,11 @@ func (am *Alertmanager) TestReceivers(ctx context.Context, c apimodels.TestRecei
testAlert := &types.Alert{
Alert: model.Alert{
Labels: model.LabelSet{
model.LabelName("alertname"): "TestAlertAlwaysFiring",
model.LabelName("alertname"): "TestAlert",
model.LabelName("instance"): "Grafana",
},
Annotations: model.LabelSet{
model.LabelName("summary"): "TestAlertAlwaysFiring",
model.LabelName("description"): "This is a test alert from Grafana",
model.LabelName("summary"): "Notification test",
},
StartsAt: now,
},

View File

@@ -4,10 +4,10 @@ import { Router } from 'react-router-dom';
import Receivers from './Receivers';
import React from 'react';
import { locationService, setDataSourceSrv } from '@grafana/runtime';
import { act, render } from '@testing-library/react';
import { act, render, waitFor } from '@testing-library/react';
import { getAllDataSources } from './utils/config';
import { typeAsJestMock } from 'test/helpers/typeAsJestMock';
import { updateAlertManagerConfig, fetchAlertManagerConfig, fetchStatus } from './api/alertmanager';
import { updateAlertManagerConfig, fetchAlertManagerConfig, fetchStatus, testReceivers } from './api/alertmanager';
import {
mockDataSource,
MockDataSourceSrv,
@@ -37,6 +37,7 @@ const mocks = {
fetchStatus: typeAsJestMock(fetchStatus),
updateConfig: typeAsJestMock(updateAlertManagerConfig),
fetchNotifiers: typeAsJestMock(fetchNotifiers),
testReceivers: typeAsJestMock(testReceivers),
},
};
@@ -68,6 +69,7 @@ const ui = {
newContactPointButton: byRole('link', { name: /new contact point/i }),
saveContactButton: byRole('button', { name: /save contact point/i }),
newContactPointTypeButton: byRole('button', { name: /new contact point type/i }),
testContactPointButton: byRole('button', { name: /Test/ }),
receiversTable: byTestId('receivers-table'),
templatesTable: byTestId('templates-table'),
@@ -78,7 +80,7 @@ const ui = {
inputs: {
name: byLabelText('Name'),
email: {
addresses: byLabelText('Addresses'),
addresses: byLabelText(/Addresses/),
},
hipchat: {
url: byLabelText('Hip Chat Url'),
@@ -149,6 +151,46 @@ describe('Receivers', () => {
expect(locationService.getSearchObject()[ALERTMANAGER_NAME_QUERY_KEY]).toEqual('CloudManager');
});
it('Grafana receiver can be tested', async () => {
mocks.api.fetchConfig.mockResolvedValue(someGrafanaAlertManagerConfig);
await renderReceivers();
// go to new contact point page
userEvent.click(await ui.newContactPointButton.find());
await byRole('heading', { name: /create contact point/i }).find();
expect(locationService.getLocation().pathname).toEqual('/alerting/notifications/receivers/new');
// type in a name for the new receiver
await userEvent.type(ui.inputs.name.get(), 'my new receiver');
// enter some email
const email = ui.inputs.email.addresses.get();
userEvent.clear(email);
await userEvent.type(email, 'tester@grafana.com');
// try to test the contact point
userEvent.click(ui.testContactPointButton.get());
await waitFor(() => expect(mocks.api.testReceivers).toHaveBeenCalled());
expect(mocks.api.testReceivers).toHaveBeenCalledWith('grafana', [
{
grafana_managed_receiver_configs: [
{
disableResolveMessage: false,
name: 'test',
secureSettings: {},
settings: { addresses: 'tester@grafana.com', singleEmail: false },
type: 'email',
},
],
name: 'test',
},
]);
});
it('Grafana receiver can be created', async () => {
mocks.api.fetchConfig.mockResolvedValue(someGrafanaAlertManagerConfig);
mocks.api.updateConfig.mockResolvedValue();
@@ -164,7 +206,7 @@ describe('Receivers', () => {
await userEvent.type(byLabelText('Name').get(), 'my new receiver');
// check that default email form is rendered
await ui.inputs.name.find();
await ui.inputs.email.addresses.find();
// select hipchat
await clickSelectOption(byTestId('items.0.type').get(), 'HipChat');

View File

@@ -8,6 +8,9 @@ import {
SilenceCreatePayload,
Matcher,
AlertmanagerStatus,
Receiver,
TestReceiversPayload,
TestReceiversResult,
} from 'app/plugins/datasource/alertmanager/types';
import { getDatasourceAPIId, GRAFANA_RULES_SOURCE_NAME } from '../utils/datasource';
@@ -155,6 +158,34 @@ export async function fetchStatus(alertManagerSourceName: string): Promise<Alert
return result.data;
}
export async function testReceivers(alertManagerSourceName: string, receivers: Receiver[]): Promise<void> {
const data: TestReceiversPayload = {
receivers,
};
const result = await getBackendSrv()
.fetch<TestReceiversResult>({
method: 'POST',
data,
url: `/api/alertmanager/${getDatasourceAPIId(alertManagerSourceName)}/config/api/v1/receivers/test`,
showErrorAlert: false,
showSuccessAlert: false,
})
.toPromise();
// api returns 207 if one or more receivers has failed test. Collect errors in this case
if (result.status === 207) {
throw new Error(
result.data.receivers
.flatMap((receiver) =>
receiver.grafana_managed_receiver_configs
.filter((receiver) => receiver.status === 'failed')
.map((receiver) => receiver.error ?? 'Unknown error.')
)
.join('; ')
);
}
}
function escapeQuotes(value: string): string {
return value.replace(/"/g, '\\"');
}

View File

@@ -7,12 +7,14 @@ import { useFormContext, FieldErrors } from 'react-hook-form';
import { ChannelValues, CommonSettingsComponentType } from '../../../types/receiver-form';
import { ChannelOptions } from './ChannelOptions';
import { CollapsibleSection } from './CollapsibleSection';
import { useUnifiedAlertingSelector } from '../../../hooks/useUnifiedAlertingSelector';
interface Props<R> {
defaultValues: R;
pathPrefix: string;
notifiers: NotifierDTO[];
onDuplicate: () => void;
onTest?: () => void;
commonSettingsComponent: CommonSettingsComponentType;
secureFields?: Record<string, boolean>;
@@ -25,6 +27,7 @@ export function ChannelSubForm<R extends ChannelValues>({
pathPrefix,
onDuplicate,
onDelete,
onTest,
notifiers,
errors,
secureFields,
@@ -34,6 +37,7 @@ export function ChannelSubForm<R extends ChannelValues>({
const name = (fieldName: string) => `${pathPrefix}${fieldName}`;
const { control, watch, register } = useFormContext();
const selectedType = watch(name('type')) ?? defaultValues.type; // nope, setting "default" does not work at all.
const { loading: testingReceiver } = useUnifiedAlertingSelector((state) => state.testReceivers);
useEffect(() => {
register(`${pathPrefix}.__id`);
@@ -89,6 +93,18 @@ export function ChannelSubForm<R extends ChannelValues>({
</Field>
</div>
<div className={styles.buttons}>
{onTest && (
<Button
disabled={testingReceiver}
size="xs"
variant="secondary"
type="button"
onClick={() => onTest()}
icon={testingReceiver ? 'fa fa-spinner' : 'message'}
>
Test
</Button>
)}
<Button size="xs" variant="secondary" type="button" onClick={() => onDuplicate()} icon="copy">
Duplicate
</Button>

View File

@@ -7,10 +7,15 @@ import {
import React, { FC, useEffect, useMemo } from 'react';
import { useDispatch } from 'react-redux';
import { useUnifiedAlertingSelector } from '../../../hooks/useUnifiedAlertingSelector';
import { fetchGrafanaNotifiersAction, updateAlertManagerConfigAction } from '../../../state/actions';
import {
fetchGrafanaNotifiersAction,
testReceiversAction,
updateAlertManagerConfigAction,
} from '../../../state/actions';
import { GrafanaChannelValues, ReceiverFormValues } from '../../../types/receiver-form';
import { GRAFANA_RULES_SOURCE_NAME } from '../../../utils/datasource';
import {
formChannelValuesToGrafanaChannelConfig,
formValuesToGrafanaReceiver,
grafanaReceiverToFormValues,
updateConfigWithReceiver,
@@ -68,6 +73,22 @@ export const GrafanaReceiverForm: FC<Props> = ({ existing, alertManagerSourceNam
);
};
const onTestChannel = (values: GrafanaChannelValues) => {
const existing: GrafanaManagedReceiverConfig | undefined = id2original[values.__id];
const chan = formChannelValuesToGrafanaChannelConfig(values, defaultChannelValues, 'test', existing);
dispatch(
testReceiversAction({
alertManagerSourceName,
receivers: [
{
name: 'test',
grafana_managed_receiver_configs: [chan],
},
],
})
);
};
const takenReceiverNames = useMemo(
() => config.alertmanager_config.receivers?.map(({ name }) => name).filter((name) => name !== existing?.name) ?? [],
[config, existing]
@@ -79,6 +100,7 @@ export const GrafanaReceiverForm: FC<Props> = ({ existing, alertManagerSourceNam
config={config}
onSubmit={onSubmit}
initialValues={existingValue}
onTestChannel={onTestChannel}
notifiers={grafanaNotifiers.result}
alertManagerSourceName={alertManagerSourceName}
defaultItem={defaultChannelValues}

View File

@@ -19,6 +19,7 @@ interface Props<R extends ChannelValues> {
notifiers: NotifierDTO[];
defaultItem: R;
alertManagerSourceName: string;
onTestChannel?: (channel: R) => void;
onSubmit: (values: ReceiverFormValues<R>) => void;
takenReceiverNames: string[]; // will validate that user entered receiver name is not one of these
commonSettingsComponent: CommonSettingsComponentType;
@@ -32,6 +33,7 @@ export function ReceiverForm<R extends ChannelValues>({
notifiers,
alertManagerSourceName,
onSubmit,
onTestChannel,
takenReceiverNames,
commonSettingsComponent,
}: Props<R>): JSX.Element {
@@ -117,6 +119,14 @@ export function ReceiverForm<R extends ChannelValues>({
const currentValues: R = getValues().items[index];
append({ ...currentValues, __id: String(Math.random()) });
}}
onTest={
onTestChannel
? () => {
const currentValues: R = getValues().items[index];
onTestChannel(currentValues);
}
: undefined
}
onDelete={() => remove(index)}
pathPrefix={pathPrefix}
notifiers={notifiers}

View File

@@ -75,7 +75,13 @@ export function RuleListErrors(): ReactElement {
<>
<div>{errors[0]}</div>
{errors.length >= 2 && (
<Button className={styles.moreButton} variant="link" size="sm" onClick={() => setExpanded(true)}>
<Button
className={styles.moreButton}
variant="link"
icon="angle-right"
size="sm"
onClick={() => setExpanded(true)}
>
{errors.length - 1} more {pluralize('error', errors.length - 1)}
</Button>
)}

View File

@@ -4,6 +4,7 @@ import {
AlertmanagerAlert,
AlertManagerCortexConfig,
AlertmanagerGroup,
Receiver,
Silence,
SilenceCreatePayload,
} from 'app/plugins/datasource/alertmanager/types';
@@ -26,6 +27,7 @@ import {
updateAlertManagerConfig,
fetchStatus,
deleteAlertManagerConfig,
testReceivers,
} from '../api/alertmanager';
import { fetchRules } from '../api/prometheus';
import {
@@ -604,3 +606,18 @@ export const deleteAlertManagerConfigAction = createAsyncThunk(
);
}
);
interface TestReceiversOptions {
alertManagerSourceName: string;
receivers: Receiver[];
}
export const testReceiversAction = createAsyncThunk(
'unifiedalerting/testReceivers',
({ alertManagerSourceName, receivers }: TestReceiversOptions): Promise<void> => {
return withAppEvents(withSerializedError(testReceivers(alertManagerSourceName, receivers)), {
errorMessage: 'Failed to send test alert.',
successMessage: 'Test alert sent.',
});
}
);

View File

@@ -15,6 +15,7 @@ import {
fetchAlertGroupsAction,
checkIfLotexSupportsEditingRulesAction,
deleteAlertManagerConfigAction,
testReceiversAction,
} from './actions';
export const reducer = combineReducers({
@@ -48,6 +49,7 @@ export const reducer = combineReducers({
checkIfLotexSupportsEditingRulesAction,
(source) => source
).reducer,
testReceivers: createAsyncSlice('testReceivers', testReceiversAction).reducer,
});
export type UnifiedAlertingState = ReturnType<typeof reducer>;

View File

@@ -207,7 +207,7 @@ function grafanaChannelConfigToFormChannelValues(
return values;
}
function formChannelValuesToGrafanaChannelConfig(
export function formChannelValuesToGrafanaChannelConfig(
values: GrafanaChannelValues,
defaults: GrafanaChannelValues,
name: string,

View File

@@ -226,3 +226,23 @@ export interface AlertmanagerStatus {
version: string;
};
}
export interface TestReceiversPayload {
receivers?: Receiver[];
}
interface TestReceiversResultGrafanaReceiverConfig {
name: string;
uid?: string;
error?: string;
status: 'failed';
}
interface TestReceiversResultReceiver {
name: string;
grafana_managed_receiver_configs: TestReceiversResultGrafanaReceiverConfig[];
}
export interface TestReceiversResult {
notified_at: string;
receivers: TestReceiversResultReceiver[];
}