Alerting: receivers table in the receivers page (#33119)

This commit is contained in:
Domas 2021-04-23 15:54:31 +03:00 committed by GitHub
parent e6a9654d0e
commit 67f6611d85
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 271 additions and 5 deletions

View File

@ -6,6 +6,9 @@ import { useLocation } from 'react-use';
export function useQueryParams(): [UrlQueryMap, (values: UrlQueryMap, replace?: boolean) => void] {
const { search } = useLocation();
const queryParams = useMemo(() => locationSearchToObject(search || ''), [search]);
const update = useCallback((values: UrlQueryMap, replace?: boolean) => locationService.partial(values, replace), []);
const update = useCallback(
(values: UrlQueryMap, replace?: boolean) => setImmediate(() => locationService.partial(values, replace)),
[]
);
return [queryParams, update];
}

View File

@ -3,10 +3,12 @@ import React, { FC, useEffect } from 'react';
import { useDispatch } from 'react-redux';
import { AlertingPageWrapper } from './components/AlertingPageWrapper';
import { AlertManagerPicker } from './components/AlertManagerPicker';
import { ReceiversTable } from './components/receivers/ReceiversTable';
import { TemplatesTable } from './components/receivers/TemplatesTable';
import { useAlertManagerSourceName } from './hooks/useAlertManagerSourceName';
import { useUnifiedAlertingSelector } from './hooks/useUnifiedAlertingSelector';
import { fetchAlertManagerConfigAction } from './state/actions';
import { fetchAlertManagerConfigAction, fetchGrafanaNotifiersAction } from './state/actions';
import { GRAFANA_RULES_SOURCE_NAME } from './utils/datasource';
import { initialAsyncRequestState } from './utils/redux';
const Receivers: FC = () => {
@ -14,11 +16,18 @@ const Receivers: FC = () => {
const dispatch = useDispatch();
const config = useUnifiedAlertingSelector((state) => state.amConfigs);
const receiverTypes = useUnifiedAlertingSelector((state) => state.grafanaNotifiers);
useEffect(() => {
dispatch(fetchAlertManagerConfigAction(alertManagerSourceName));
}, [alertManagerSourceName, dispatch]);
useEffect(() => {
if (alertManagerSourceName === GRAFANA_RULES_SOURCE_NAME && !(receiverTypes.result || receiverTypes.loading)) {
dispatch(fetchGrafanaNotifiersAction());
}
}, [alertManagerSourceName, dispatch, receiverTypes]);
const { result, loading, error } = config[alertManagerSourceName] || initialAsyncRequestState;
return (
@ -32,7 +41,12 @@ const Receivers: FC = () => {
</InfoBox>
)}
{loading && <LoadingPlaceholder text="loading receivers..." />}
{result && !loading && !error && <TemplatesTable config={result} />}
{result && !loading && !error && (
<>
<TemplatesTable config={result} />
<ReceiversTable config={result} />
</>
)}
</AlertingPageWrapper>
);
};

View File

@ -0,0 +1,6 @@
import { getBackendSrv } from '@grafana/runtime';
import { NotifierDTO } from 'app/types';
export function fetchNotifiers(): Promise<NotifierDTO[]> {
return getBackendSrv().get(`/api/alert-notifiers`);
}

View File

@ -27,6 +27,7 @@ export const ReceiversSection: FC<Props> = ({ title, description, addButtonLabel
const getStyles = (theme: GrafanaTheme) => ({
heading: css`
margin-top: ${theme.spacing.xl};
display: flex;
justify-content: space-between;
`,

View File

@ -0,0 +1,125 @@
import React from 'react';
import { render } from '@testing-library/react';
import {
AlertManagerCortexConfig,
GrafanaManagedReceiverConfig,
Receiver,
} from 'app/plugins/datasource/alertmanager/types';
import { configureStore } from 'app/store/configureStore';
import { Provider } from 'react-redux';
import { ReceiversTable } from './ReceiversTable';
import { fetchGrafanaNotifiersAction } from '../../state/actions';
import { NotifierDTO, NotifierType } from 'app/types';
import { byRole } from 'testing-library-selector';
const renderReceieversTable = async (receivers: Receiver[], notifiers: NotifierDTO[]) => {
const config: AlertManagerCortexConfig = {
template_files: {},
alertmanager_config: {
receivers,
},
};
const store = configureStore();
await store.dispatch(fetchGrafanaNotifiersAction.fulfilled(notifiers, 'initial'));
return render(
<Provider store={store}>
<ReceiversTable config={config} />
</Provider>
);
};
const mockGrafanaReceiver = (type: string): GrafanaManagedReceiverConfig => ({
type,
id: 2,
frequency: 1,
disableResolveMessage: false,
secureFields: {},
settings: {},
sendReminder: false,
uid: '2',
});
const mockNotifier = (type: NotifierType, name: string): NotifierDTO => ({
type,
name,
description: 'its a mock',
heading: 'foo',
options: [],
});
const ui = {
table: byRole<HTMLTableElement>('table'),
};
describe('ReceiversTable', () => {
it('render receivers with grafana notifiers', async () => {
const receivers: Receiver[] = [
{
name: 'with receivers',
grafana_managed_receiver_configs: [mockGrafanaReceiver('googlechat'), mockGrafanaReceiver('sensugo')],
},
{
name: 'without receivers',
grafana_managed_receiver_configs: [],
},
];
const notifiers: NotifierDTO[] = [mockNotifier('googlechat', 'Google Chat'), mockNotifier('sensugo', 'Sensu Go')];
await renderReceieversTable(receivers, notifiers);
const table = await ui.table.find();
const rows = table.querySelector('tbody')?.querySelectorAll('tr')!;
expect(rows).toHaveLength(2);
expect(rows[0].querySelectorAll('td')[0]).toHaveTextContent('with receivers');
expect(rows[0].querySelectorAll('td')[1]).toHaveTextContent('Google Chat, Sensu Go');
expect(rows[1].querySelectorAll('td')[0]).toHaveTextContent('without receivers');
expect(rows[1].querySelectorAll('td')[1].textContent).toEqual('');
});
it('render receivers with alert manager notifers', async () => {
const receivers: Receiver[] = [
{
name: 'with receivers',
email_configs: [
{
to: 'domas.lapinskas@grafana.com',
},
],
slack_configs: [],
webhook_configs: [
{
url: 'http://example.com',
},
],
opsgenie_configs: [
{
foo: 'bar',
},
],
foo_configs: [
{
url: 'bar',
},
],
},
{
name: 'without receivers',
},
];
await renderReceieversTable(receivers, []);
const table = await ui.table.find();
const rows = table.querySelector('tbody')?.querySelectorAll('tr')!;
expect(rows).toHaveLength(2);
expect(rows[0].querySelectorAll('td')[0]).toHaveTextContent('with receivers');
expect(rows[0].querySelectorAll('td')[1]).toHaveTextContent('Email, Webhook, OpsGenie, Foo');
expect(rows[1].querySelectorAll('td')[0]).toHaveTextContent('without receivers');
expect(rows[1].querySelectorAll('td')[1].textContent).toEqual('');
});
});

View File

@ -0,0 +1,68 @@
import { useStyles } from '@grafana/ui';
import { AlertManagerCortexConfig } from 'app/plugins/datasource/alertmanager/types';
import React, { FC, useMemo } from 'react';
import { useUnifiedAlertingSelector } from '../../hooks/useUnifiedAlertingSelector';
import { getAlertTableStyles } from '../../styles/table';
import { extractReadableNotifierTypes } from '../../utils/receivers';
import { ActionButton } from '../rules/ActionButton';
import { ActionIcon } from '../rules/ActionIcon';
import { ReceiversSection } from './ReceiversSection';
interface Props {
config: AlertManagerCortexConfig;
}
export const ReceiversTable: FC<Props> = ({ config }) => {
const tableStyles = useStyles(getAlertTableStyles);
const grafanaNotifiers = useUnifiedAlertingSelector((state) => state.grafanaNotifiers);
const rows = useMemo(
() =>
config.alertmanager_config.receivers?.map((receiver) => ({
name: receiver.name,
types: extractReadableNotifierTypes(receiver, grafanaNotifiers.result ?? []),
})) ?? [],
[config, grafanaNotifiers.result]
);
return (
<ReceiversSection
title="Contact points"
description="Define where the notifications will be sent to, for example email or Slack."
addButtonLabel="New contact point"
>
<table className={tableStyles.table}>
<colgroup>
<col />
<col />
<col />
</colgroup>
<thead>
<tr>
<th>Contact point name</th>
<th>Type</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{!rows.length && (
<tr className={tableStyles.evenRow}>
<td colSpan={3}>No receivers defined.</td>
</tr>
)}
{rows.map((receiver, idx) => (
<tr key={receiver.name} className={idx % 2 === 0 ? tableStyles.evenRow : undefined}>
<td>{receiver.name}</td>
<td>{receiver.types.join(', ')}</td>
<td className={tableStyles.actionsCell}>
<ActionButton icon="pen">Edit</ActionButton>
<ActionIcon tooltip="delete receiver" icon="trash-alt" />
</td>
</tr>
))}
</tbody>
</table>
</ReceiversSection>
);
};

View File

@ -3,7 +3,7 @@ import { locationService, config } from '@grafana/runtime';
import { createAsyncThunk } from '@reduxjs/toolkit';
import { appEvents } from 'app/core/core';
import { AlertManagerCortexConfig, Silence } from 'app/plugins/datasource/alertmanager/types';
import { ThunkResult } from 'app/types';
import { NotifierDTO, ThunkResult } from 'app/types';
import { RuleIdentifier, RuleNamespace, RuleWithLocation } from 'app/types/unified-alerting';
import {
PostableRulerRuleGroupDTO,
@ -12,6 +12,7 @@ import {
RulerRulesConfigDTO,
} from 'app/types/unified-alerting-dto';
import { fetchAlertManagerConfig, fetchSilences } from '../api/alertmanager';
import { fetchNotifiers } from '../api/grafana';
import { fetchRules } from '../api/prometheus';
import {
deleteRulerRulesGroup,
@ -312,3 +313,8 @@ export const saveRuleFormAction = createAsyncThunk(
})()
)
);
export const fetchGrafanaNotifiersAction = createAsyncThunk(
'unifiedalerting/fetchGrafanaNotifiers',
(): Promise<NotifierDTO[]> => withSerializedError(fetchNotifiers())
);

View File

@ -3,6 +3,7 @@ import { createAsyncMapSlice, createAsyncSlice } from '../utils/redux';
import {
fetchAlertManagerConfigAction,
fetchExistingRuleAction,
fetchGrafanaNotifiersAction,
fetchPromRulesAction,
fetchRulerRulesAction,
fetchSilencesAction,
@ -23,6 +24,7 @@ export const reducer = combineReducers({
saveRule: createAsyncSlice('saveRule', saveRuleFormAction).reducer,
existingRule: createAsyncSlice('existingRule', fetchExistingRuleAction).reducer,
}),
grafanaNotifiers: createAsyncSlice('grafanaNotifiers', fetchGrafanaNotifiersAction).reducer,
});
export type UnifiedAlertingState = ReturnType<typeof reducer>;

View File

@ -0,0 +1,31 @@
import { receiverTypeNames } from 'app/plugins/datasource/alertmanager/consts';
import { GrafanaManagedReceiverConfig, Receiver } from 'app/plugins/datasource/alertmanager/types';
import { NotifierDTO } from 'app/types';
import { capitalize } from 'lodash';
// extract readable notifier types that are in use for a receiver, eg ['Slack', 'Email', 'PagerDuty']
export function extractReadableNotifierTypes(receiver: Receiver, grafanaNotifiers: NotifierDTO[]): string[] {
return [
// grafana specific receivers
...getReadabaleGrafanaNotifierTypes(receiver.grafana_managed_receiver_configs ?? [], grafanaNotifiers),
// cortex alert manager receivers
...getReadableCortexAlertManagerNotifierTypes(receiver),
];
}
function getReadableCortexAlertManagerNotifierTypes(receiver: Receiver): string[] {
return Object.entries(receiver)
.filter(([key]) => key !== 'grafana_managed_receiver_configs' && key.endsWith('_configs')) // filter out only properties that are alert manager notifier
.filter(([_, value]) => Array.isArray(value) && !!value.length) // check that there are actually notifiers of this type configured
.map(([key]) => key.replace('_configs', '')) // remove the `_config` part from the key, making it intto a notifier name
.map((type) => receiverTypeNames[type] ?? capitalize(type)); // either map to readable name or, failing that, capitalize
}
function getReadabaleGrafanaNotifierTypes(
configs: GrafanaManagedReceiverConfig[],
grafanaNotifiers: NotifierDTO[]
): string[] {
return configs
.map((recv) => recv.type) // extract types from config
.map((type) => grafanaNotifiers.find((r) => r.type === type)?.name ?? capitalize(type)); // get readable name from notifier cofnig, or if not available, just capitalize
}

View File

@ -29,7 +29,7 @@ function requestStateReducer<T, ThunkArg = void, ThunkApiConfig = {}>(
requestId: action.meta.requestId,
};
} else if (asyncThunk.fulfilled.match(action)) {
if (state.requestId === action.meta.requestId) {
if (state.requestId === undefined || state.requestId === action.meta.requestId) {
return {
...state,
result: action.payload as Draft<T>,

View File

@ -0,0 +1,9 @@
export const receiverTypeNames: Record<string, string> = {
pagerduty: 'PagerDuty',
pushover: 'Pushover',
slack: 'Slack',
opsgenie: 'OpsGenie',
webhook: 'Webhook',
victorops: 'VictorOps',
wechat: 'WeChat',
};

View File

@ -92,6 +92,7 @@ export type Receiver = {
victorops_configs?: unknown[];
wechat_configs?: unknown[];
grafana_managed_receiver_configs?: GrafanaManagedReceiverConfig[];
[key: string]: unknown;
};
export type Route = {