Alerting: Add choice to external alertmanagers (#45157)

* implement alertmanagersChoice

* return empty array and remove non null assertion
This commit is contained in:
Peter Holmberg 2022-02-17 12:47:38 +01:00 committed by GitHub
parent 52d4e2ddcc
commit 0502a84922
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 128 additions and 66 deletions

View File

@ -13,6 +13,7 @@ import {
TestReceiversAlert, TestReceiversAlert,
TestReceiversPayload, TestReceiversPayload,
TestReceiversResult, TestReceiversResult,
ExternalAlertmanagerConfig,
} from 'app/plugins/datasource/alertmanager/types'; } from 'app/plugins/datasource/alertmanager/types';
import { lastValueFrom } from 'rxjs'; import { lastValueFrom } from 'rxjs';
import { getDatasourceAPIId, GRAFANA_RULES_SOURCE_NAME } from '../utils/datasource'; import { getDatasourceAPIId, GRAFANA_RULES_SOURCE_NAME } from '../utils/datasource';
@ -222,11 +223,11 @@ function getReceiverResultError(receiversResult: TestReceiversResult) {
.join('; '); .join('; ');
} }
export async function addAlertManagers(alertManagers: string[]): Promise<void> { export async function addAlertManagers(alertManagerConfig: ExternalAlertmanagerConfig): Promise<void> {
await lastValueFrom( await lastValueFrom(
getBackendSrv().fetch({ getBackendSrv().fetch({
method: 'POST', method: 'POST',
data: { alertmanagers: alertManagers }, data: alertManagerConfig,
url: '/api/v1/ngalert/admin_config', url: '/api/v1/ngalert/admin_config',
showErrorAlert: false, showErrorAlert: false,
showSuccessAlert: false, showSuccessAlert: false,
@ -247,9 +248,9 @@ export async function fetchExternalAlertmanagers(): Promise<ExternalAlertmanager
return result.data; return result.data;
} }
export async function fetchExternalAlertmanagerConfig(): Promise<{ alertmanagers: string[] }> { export async function fetchExternalAlertmanagerConfig(): Promise<ExternalAlertmanagerConfig> {
const result = await lastValueFrom( const result = await lastValueFrom(
getBackendSrv().fetch<{ alertmanagers: string[] }>({ getBackendSrv().fetch<ExternalAlertmanagerConfig>({
method: 'GET', method: 'GET',
url: '/api/v1/ngalert/admin_config', url: '/api/v1/ngalert/admin_config',
showErrorAlert: false, showErrorAlert: false,

View File

@ -1,19 +1,17 @@
import React, { FC, useMemo } from 'react'; import React, { FC, useMemo } from 'react';
import { css, cx } from '@emotion/css'; import { css, cx } from '@emotion/css';
import { useDispatch } from 'react-redux';
import { GrafanaTheme2 } from '@grafana/data'; import { GrafanaTheme2 } from '@grafana/data';
import { Button, Field, FieldArray, Form, Icon, Input, Modal, useStyles2 } from '@grafana/ui'; import { Button, Field, FieldArray, Form, Icon, Input, Modal, useStyles2 } from '@grafana/ui';
import { addExternalAlertmanagersAction } from '../../state/actions';
import { AlertmanagerUrl } from 'app/plugins/datasource/alertmanager/types'; import { AlertmanagerUrl } from 'app/plugins/datasource/alertmanager/types';
interface Props { interface Props {
onClose: () => void; onClose: () => void;
alertmanagers: AlertmanagerUrl[]; alertmanagers: AlertmanagerUrl[];
onChangeAlertmanagerConfig: (alertmanagers: string[]) => void;
} }
export const AddAlertManagerModal: FC<Props> = ({ alertmanagers, onClose }) => { export const AddAlertManagerModal: FC<Props> = ({ alertmanagers, onChangeAlertmanagerConfig, onClose }) => {
const styles = useStyles2(getStyles); const styles = useStyles2(getStyles);
const dispatch = useDispatch();
const defaultValues: Record<string, AlertmanagerUrl[]> = useMemo( const defaultValues: Record<string, AlertmanagerUrl[]> = useMemo(
() => ({ () => ({
alertmanagers: alertmanagers, alertmanagers: alertmanagers,
@ -29,7 +27,7 @@ export const AddAlertManagerModal: FC<Props> = ({ alertmanagers, onClose }) => {
); );
const onSubmit = (values: Record<string, AlertmanagerUrl[]>) => { const onSubmit = (values: Record<string, AlertmanagerUrl[]>) => {
dispatch(addExternalAlertmanagersAction(values.alertmanagers.map((am) => cleanAlertmanagerUrl(am.url)))); onChangeAlertmanagerConfig(values.alertmanagers.map((am) => cleanAlertmanagerUrl(am.url)));
onClose(); onClose();
}; };

View File

@ -1,8 +1,18 @@
import React, { useCallback, useEffect, useState } from 'react'; import React, { useCallback, useEffect, useState } from 'react';
import { useDispatch } from 'react-redux'; import { useDispatch, useSelector } from 'react-redux';
import { css } from '@emotion/css'; import { css, cx } from '@emotion/css';
import { GrafanaTheme2 } from '@grafana/data'; import { GrafanaTheme2 } from '@grafana/data';
import { Button, ConfirmModal, HorizontalGroup, Icon, Tooltip, useStyles2 } from '@grafana/ui'; import {
Button,
ConfirmModal,
Field,
HorizontalGroup,
Icon,
RadioButtonGroup,
Tooltip,
useStyles2,
useTheme2,
} from '@grafana/ui';
import EmptyListCTA from 'app/core/components/EmptyListCTA/EmptyListCTA'; import EmptyListCTA from 'app/core/components/EmptyListCTA/EmptyListCTA';
import { AddAlertManagerModal } from './AddAlertManagerModal'; import { AddAlertManagerModal } from './AddAlertManagerModal';
import { import {
@ -11,13 +21,25 @@ import {
fetchExternalAlertmanagersConfigAction, fetchExternalAlertmanagersConfigAction,
} from '../../state/actions'; } from '../../state/actions';
import { useExternalAmSelector } from '../../hooks/useExternalAmSelector'; import { useExternalAmSelector } from '../../hooks/useExternalAmSelector';
import { StoreState } from 'app/types/store';
const alertmanagerChoices = [
{ value: 'internal', label: 'Only Internal' },
{ value: 'external', label: 'Only External' },
{ value: 'all', label: 'Both internal and external' },
];
export const ExternalAlertmanagers = () => { export const ExternalAlertmanagers = () => {
const styles = useStyles2(getStyles); const styles = useStyles2(getStyles);
const dispatch = useDispatch(); const dispatch = useDispatch();
const [modalState, setModalState] = useState({ open: false, payload: [{ url: '' }] }); const [modalState, setModalState] = useState({ open: false, payload: [{ url: '' }] });
const [deleteModalState, setDeleteModalState] = useState({ open: false, index: 0 }); const [deleteModalState, setDeleteModalState] = useState({ open: false, index: 0 });
const externalAlertManagers = useExternalAmSelector(); const externalAlertManagers = useExternalAmSelector();
const alertmanagersChoice = useSelector(
(state: StoreState) => state.unifiedAlerting.externalAlertmanagers.alertmanagerConfig.result?.alertmanagersChoice
);
const theme = useTheme2();
useEffect(() => { useEffect(() => {
dispatch(fetchExternalAlertmanagersAction()); dispatch(fetchExternalAlertmanagersAction());
@ -37,10 +59,12 @@ export const ExternalAlertmanagers = () => {
.map((am) => { .map((am) => {
return am.url; return am.url;
}); });
dispatch(addExternalAlertmanagersAction(newList)); dispatch(
addExternalAlertmanagersAction({ alertmanagers: newList, alertmanagersChoice: alertmanagersChoice ?? 'all' })
);
setDeleteModalState({ open: false, index: 0 }); setDeleteModalState({ open: false, index: 0 });
}, },
[externalAlertManagers, dispatch] [externalAlertManagers, dispatch, alertmanagersChoice]
); );
const onEdit = useCallback(() => { const onEdit = useCallback(() => {
@ -70,16 +94,26 @@ export const ExternalAlertmanagers = () => {
})); }));
}, [setModalState]); }, [setModalState]);
const onChangeAlertmanagerChoice = (alertmanagersChoice: string) => {
dispatch(
addExternalAlertmanagersAction({ alertmanagers: externalAlertManagers.map((am) => am.url), alertmanagersChoice })
);
};
const onChangeAlertmanagers = (alertmanagers: string[]) => {
dispatch(addExternalAlertmanagersAction({ alertmanagers, alertmanagersChoice: alertmanagersChoice ?? 'all' }));
};
const getStatusColor = (status: string) => { const getStatusColor = (status: string) => {
switch (status) { switch (status) {
case 'active': case 'active':
return 'green'; return theme.colors.success.main;
case 'pending': case 'pending':
return 'yellow'; return theme.colors.warning.main;
default: default:
return 'red'; return theme.colors.error.main;
} }
}; };
@ -107,49 +141,63 @@ export const ExternalAlertmanagers = () => {
buttonIcon="bell-slash" buttonIcon="bell-slash"
/> />
) : ( ) : (
<table className="filter-table form-inline filter-table--hover"> <>
<thead> <table className={cx('filter-table form-inline filter-table--hover', styles.table)}>
<tr> <thead>
<th>Url</th> <tr>
<th>Status</th> <th>Url</th>
<th style={{ width: '2%' }}>Action</th> <th>Status</th>
</tr> <th style={{ width: '2%' }}>Action</th>
</thead> </tr>
<tbody> </thead>
{externalAlertManagers?.map((am, index) => { <tbody>
return ( {externalAlertManagers?.map((am, index) => {
<tr key={index}> return (
<td> <tr key={index}>
<span className={styles.url}>{am.url}</span> <td>
{am.actualUrl ? ( <span className={styles.url}>{am.url}</span>
<Tooltip content={`Discovered ${am.actualUrl} from ${am.url}`} theme="info"> {am.actualUrl ? (
<Icon name="info-circle" /> <Tooltip content={`Discovered ${am.actualUrl} from ${am.url}`} theme="info">
</Tooltip> <Icon name="info-circle" />
) : null} </Tooltip>
</td> ) : null}
<td> </td>
<Icon name="heart" style={{ color: getStatusColor(am.status) }} title={am.status} /> <td>
</td> <Icon name="heart" style={{ color: getStatusColor(am.status) }} title={am.status} />
<td> </td>
<HorizontalGroup> <td>
<Button variant="secondary" type="button" onClick={onEdit} aria-label="Edit alertmanager"> <HorizontalGroup>
<Icon name="pen" /> <Button variant="secondary" type="button" onClick={onEdit} aria-label="Edit alertmanager">
</Button> <Icon name="pen" />
<Button </Button>
variant="destructive" <Button
aria-label="Remove alertmanager" variant="destructive"
type="button" aria-label="Remove alertmanager"
onClick={() => setDeleteModalState({ open: true, index })} type="button"
> onClick={() => setDeleteModalState({ open: true, index })}
<Icon name="trash-alt" /> >
</Button> <Icon name="trash-alt" />
</HorizontalGroup> </Button>
</td> </HorizontalGroup>
</tr> </td>
); </tr>
})} );
</tbody> })}
</table> </tbody>
</table>
<div>
<Field
label="Send alerts to"
description="Sets which Alertmanager will handle your alerts. Internal (Grafana built in Alertmanager), External (All Alertmanagers configured above), or both."
>
<RadioButtonGroup
options={alertmanagerChoices}
value={alertmanagersChoice}
onChange={(value) => onChangeAlertmanagerChoice(value!)}
/>
</Field>
</div>
</>
)} )}
<ConfirmModal <ConfirmModal
isOpen={deleteModalState.open} isOpen={deleteModalState.open}
@ -159,7 +207,13 @@ export const ExternalAlertmanagers = () => {
onConfirm={() => onDelete(deleteModalState.index)} onConfirm={() => onDelete(deleteModalState.index)}
onDismiss={() => setDeleteModalState({ open: false, index: 0 })} onDismiss={() => setDeleteModalState({ open: false, index: 0 })}
/> />
{modalState.open && <AddAlertManagerModal onClose={onCloseModal} alertmanagers={modalState.payload} />} {modalState.open && (
<AddAlertManagerModal
onClose={onCloseModal}
alertmanagers={modalState.payload}
onChangeAlertmanagerConfig={onChangeAlertmanagers}
/>
)}
</div> </div>
); );
}; };
@ -176,5 +230,7 @@ const getStyles = (theme: GrafanaTheme2) => ({
display: flex; display: flex;
justify-content: flex-end; justify-content: flex-end;
`, `,
table: css``, table: css`
margin-bottom: ${theme.spacing(2)};
`,
}); });

View File

@ -4,7 +4,7 @@ import { StoreState } from '../../../../types';
const SUFFIX_REGEX = /\/api\/v[1|2]\/alerts/i; const SUFFIX_REGEX = /\/api\/v[1|2]\/alerts/i;
type AlertmanagerConfig = { url: string; status: string; actualUrl: string }; type AlertmanagerConfig = { url: string; status: string; actualUrl: string };
export function useExternalAmSelector(): AlertmanagerConfig[] | undefined { export function useExternalAmSelector(): AlertmanagerConfig[] | [] {
const discoveredAlertmanagers = useSelector( const discoveredAlertmanagers = useSelector(
(state: StoreState) => state.unifiedAlerting.externalAlertmanagers.discoveredAlertmanagers.result?.data (state: StoreState) => state.unifiedAlerting.externalAlertmanagers.discoveredAlertmanagers.result?.data
); );

View File

@ -2,6 +2,7 @@ import { getBackendSrv, locationService } from '@grafana/runtime';
import { createAsyncThunk } from '@reduxjs/toolkit'; import { createAsyncThunk } from '@reduxjs/toolkit';
import { import {
AlertmanagerAlert, AlertmanagerAlert,
ExternalAlertmanagerConfig,
AlertManagerCortexConfig, AlertManagerCortexConfig,
AlertmanagerGroup, AlertmanagerGroup,
ExternalAlertmanagersResponse, ExternalAlertmanagersResponse,
@ -122,7 +123,7 @@ export const fetchExternalAlertmanagersAction = createAsyncThunk(
export const fetchExternalAlertmanagersConfigAction = createAsyncThunk( export const fetchExternalAlertmanagersConfigAction = createAsyncThunk(
'unifiedAlerting/fetchExternAlertmanagersConfig', 'unifiedAlerting/fetchExternAlertmanagersConfig',
(): Promise<{ alertmanagers: string[] }> => { (): Promise<ExternalAlertmanagerConfig> => {
return withSerializedError(fetchExternalAlertmanagerConfig()); return withSerializedError(fetchExternalAlertmanagerConfig());
} }
); );
@ -808,11 +809,11 @@ export const updateLotexNamespaceAndGroupAction = createAsyncThunk(
export const addExternalAlertmanagersAction = createAsyncThunk( export const addExternalAlertmanagersAction = createAsyncThunk(
'unifiedAlerting/addExternalAlertmanagers', 'unifiedAlerting/addExternalAlertmanagers',
async (alertManagerUrls: string[], thunkAPI): Promise<void> => { async (alertmanagerConfig: ExternalAlertmanagerConfig, thunkAPI): Promise<void> => {
return withAppEvents( return withAppEvents(
withSerializedError( withSerializedError(
(async () => { (async () => {
await addAlertManagers(alertManagerUrls); await addAlertManagers(alertmanagerConfig);
thunkAPI.dispatch(fetchExternalAlertmanagersConfigAction()); thunkAPI.dispatch(fetchExternalAlertmanagersConfigAction());
})() })()
), ),

View File

@ -272,6 +272,12 @@ export interface ExternalAlertmanagersResponse {
data: ExternalAlertmanagers; data: ExternalAlertmanagers;
status: 'string'; status: 'string';
} }
export interface ExternalAlertmanagerConfig {
alertmanagers: string[];
alertmanagersChoice: string;
}
export enum AlertManagerImplementation { export enum AlertManagerImplementation {
cortex = 'cortex', cortex = 'cortex',
prometheus = 'prometheus', prometheus = 'prometheus',