mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Alerting: button to test contact point (#37475)
This commit is contained in:
@@ -100,6 +100,7 @@ export const getAvailableIcons = () =>
|
||||
'list-ui-alt',
|
||||
'list-ul',
|
||||
'lock',
|
||||
'message',
|
||||
'minus',
|
||||
'minus-circle',
|
||||
'mobile-android',
|
||||
|
@@ -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,
|
||||
},
|
||||
|
@@ -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');
|
||||
|
@@ -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, '\\"');
|
||||
}
|
||||
|
@@ -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>
|
||||
|
@@ -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}
|
||||
|
@@ -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}
|
||||
|
@@ -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>
|
||||
)}
|
||||
|
@@ -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.',
|
||||
});
|
||||
}
|
||||
);
|
||||
|
@@ -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>;
|
||||
|
@@ -207,7 +207,7 @@ function grafanaChannelConfigToFormChannelValues(
|
||||
return values;
|
||||
}
|
||||
|
||||
function formChannelValuesToGrafanaChannelConfig(
|
||||
export function formChannelValuesToGrafanaChannelConfig(
|
||||
values: GrafanaChannelValues,
|
||||
defaults: GrafanaChannelValues,
|
||||
name: string,
|
||||
|
@@ -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[];
|
||||
}
|
||||
|
Reference in New Issue
Block a user