mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Alerting: receivers table in the receivers page (#33119)
This commit is contained in:
parent
e6a9654d0e
commit
67f6611d85
@ -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];
|
||||
}
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
6
public/app/features/alerting/unified/api/grafana.ts
Normal file
6
public/app/features/alerting/unified/api/grafana.ts
Normal 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`);
|
||||
}
|
@ -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;
|
||||
`,
|
||||
|
@ -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('');
|
||||
});
|
||||
});
|
@ -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>
|
||||
);
|
||||
};
|
@ -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())
|
||||
);
|
||||
|
@ -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>;
|
||||
|
31
public/app/features/alerting/unified/utils/receivers.ts
Normal file
31
public/app/features/alerting/unified/utils/receivers.ts
Normal 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
|
||||
}
|
@ -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>,
|
||||
|
9
public/app/plugins/datasource/alertmanager/consts.ts
Normal file
9
public/app/plugins/datasource/alertmanager/consts.ts
Normal 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',
|
||||
};
|
@ -92,6 +92,7 @@ export type Receiver = {
|
||||
victorops_configs?: unknown[];
|
||||
wechat_configs?: unknown[];
|
||||
grafana_managed_receiver_configs?: GrafanaManagedReceiverConfig[];
|
||||
[key: string]: unknown;
|
||||
};
|
||||
|
||||
export type Route = {
|
||||
|
Loading…
Reference in New Issue
Block a user