Alerting: Global config form for cloud alert manager (#34074)

This commit is contained in:
Domas
2021-05-17 11:50:29 +03:00
committed by GitHub
parent d721298e03
commit 7a2dff741b
14 changed files with 210 additions and 33 deletions

View File

@@ -193,10 +193,8 @@ describe('Receivers', () => {
disableResolveMessage: false,
name: 'my new receiver',
secureSettings: {},
sendReminder: true,
settings: {
apiKey: 'foobarbaz',
roomid: '',
url: 'http://hipchat',
},
type: 'hipchat',

View File

@@ -6,6 +6,7 @@ import { AlertingPageWrapper } from './components/AlertingPageWrapper';
import { AlertManagerPicker } from './components/AlertManagerPicker';
import { EditReceiverView } from './components/receivers/EditReceiverView';
import { EditTemplateView } from './components/receivers/EditTemplateView';
import { GlobalConfigForm } from './components/receivers/GlobalConfigForm';
import { NewReceiverView } from './components/receivers/NewReceiverView';
import { NewTemplateView } from './components/receivers/NewTemplateView';
import { ReceiversAndTemplatesView } from './components/receivers/ReceiversAndTemplatesView';
@@ -94,6 +95,9 @@ const Receivers: FC = () => {
)
}
</Route>
<Route exact={true} path="/alerting/notifications/global-config">
<GlobalConfigForm config={config} alertManagerSourceName={alertManagerSourceName} />
</Route>
</Switch>
)}
</AlertingPageWrapper>

View File

@@ -0,0 +1,112 @@
import { useCleanup } from 'app/core/hooks/useCleanup';
import { AlertManagerCortexConfig } from 'app/plugins/datasource/alertmanager/types';
import React, { FC } from 'react';
import { useUnifiedAlertingSelector } from '../../hooks/useUnifiedAlertingSelector';
import { useForm, FormProvider } from 'react-hook-form';
import { globalConfigOptions } from '../../utils/cloud-alertmanager-notifier-types';
import { OptionField } from './form/fields/OptionField';
import { Alert, Button, HorizontalGroup, LinkButton, useStyles2 } from '@grafana/ui';
import { makeAMLink } from '../../utils/misc';
import { useDispatch } from 'react-redux';
import { updateAlertManagerConfigAction } from '../../state/actions';
import { omitEmptyValues } from '../../utils/receiver-form';
import { css } from '@emotion/css';
import { GrafanaTheme2 } from '@grafana/data';
interface Props {
config: AlertManagerCortexConfig;
alertManagerSourceName: string;
}
type FormValues = Record<string, unknown>;
const defaultValues: FormValues = {
smtp_require_tls: true,
} as const;
export const GlobalConfigForm: FC<Props> = ({ config, alertManagerSourceName }) => {
const dispatch = useDispatch();
useCleanup((state) => state.unifiedAlerting.saveAMConfig);
const { loading, error } = useUnifiedAlertingSelector((state) => state.saveAMConfig);
const styles = useStyles2(getStyles);
const formAPI = useForm<FormValues>({
// making a copy here beacuse react-hook-form will mutate these, and break if the object is frozen. for real.
defaultValues: JSON.parse(
JSON.stringify({
...defaultValues,
...(config.alertmanager_config.global ?? {}),
})
),
});
const {
handleSubmit,
formState: { errors },
} = formAPI;
const onSubmitCallback = (values: FormValues) => {
dispatch(
updateAlertManagerConfigAction({
newConfig: {
...config,
alertmanager_config: {
...config.alertmanager_config,
global: omitEmptyValues(values),
},
},
oldConfig: config,
alertManagerSourceName,
successMessage: 'Global config updated.',
redirectPath: makeAMLink('/alerting/notifications', alertManagerSourceName),
})
);
};
return (
<FormProvider {...formAPI}>
<form onSubmit={handleSubmit(onSubmitCallback)}>
<h4 className={styles.heading}>Global config</h4>
{error && (
<Alert severity="error" title="Error saving receiver">
{error.message || String(error)}
</Alert>
)}
{globalConfigOptions.map((option) => (
<OptionField
defaultValue={defaultValues[option.propertyName]}
key={option.propertyName}
option={option}
error={errors[option.propertyName]}
pathPrefix={''}
/>
))}
<div>
<HorizontalGroup>
{loading && (
<Button disabled={true} icon="fa fa-spinner" variant="primary">
Saving...
</Button>
)}
{!loading && <Button type="submit">Save global config</Button>}
<LinkButton
disabled={loading}
fill="outline"
variant="secondary"
href={makeAMLink('alerting/notifications', alertManagerSourceName)}
>
Cancel
</LinkButton>
</HorizontalGroup>
</div>
</form>
</FormProvider>
);
};
const getStyles = (theme: GrafanaTheme2) => ({
heading: css`
margin: ${theme.spacing(4, 0)};
`,
});

View File

@@ -1,5 +1,10 @@
import { css } from '@emotion/css';
import { GrafanaTheme2 } from '@grafana/data';
import { Alert, LinkButton, useStyles2 } from '@grafana/ui';
import { AlertManagerCortexConfig } from 'app/plugins/datasource/alertmanager/types';
import React, { FC } from 'react';
import { GRAFANA_RULES_SOURCE_NAME } from '../../utils/datasource';
import { makeAMLink } from '../../utils/misc';
import { ReceiversTable } from './ReceiversTable';
import { TemplatesTable } from './TemplatesTable';
@@ -8,9 +13,30 @@ interface Props {
alertManagerName: string;
}
export const ReceiversAndTemplatesView: FC<Props> = ({ config, alertManagerName }) => (
<>
<TemplatesTable config={config} alertManagerName={alertManagerName} />
<ReceiversTable config={config} alertManagerName={alertManagerName} />
</>
);
export const ReceiversAndTemplatesView: FC<Props> = ({ config, alertManagerName }) => {
const isCloud = alertManagerName !== GRAFANA_RULES_SOURCE_NAME;
const styles = useStyles2(getStyles);
return (
<>
<TemplatesTable config={config} alertManagerName={alertManagerName} />
<ReceiversTable config={config} alertManagerName={alertManagerName} />
{isCloud && (
<Alert className={styles.section} severity="info" title="Global config for contact points">
<p>
For each external alert managers you can define global settings, like server addresses, usernames and
password, for all the supported contact points.
</p>
<LinkButton href={makeAMLink('alerting/notifications/global-config', alertManagerName)} variant="secondary">
Edit global config
</LinkButton>
</Alert>
)}
</>
);
};
const getStyles = (theme: GrafanaTheme2) => ({
section: css`
margin-top: ${theme.spacing(4)};
`,
});

View File

@@ -39,8 +39,6 @@ const mockGrafanaReceiver = (type: string): GrafanaManagedReceiverConfig => ({
disableResolveMessage: false,
secureFields: {},
settings: {},
sendReminder: false,
uid: '2',
name: type,
});

View File

@@ -14,13 +14,6 @@ export const GrafanaCommonChannelSettings: FC<CommonSettingsComponentProps> = ({
description="Disable the resolve message [OK] that is sent when alerting state returns to false"
/>
</Field>
<Field>
<Checkbox
{...register(`${pathPrefix}sendReminder`)}
label="Send reminders"
description="Send additional notifications for triggered alerts"
/>
</Field>
</div>
);
};

View File

@@ -26,7 +26,6 @@ interface Props {
const defaultChannelValues: GrafanaChannelValues = Object.freeze({
__id: '',
sendReminder: true,
secureSettings: {},
settings: {},
secureFields: {},

View File

@@ -150,7 +150,6 @@ export const mockGrafanaReceiver = (
name: type,
disableResolveMessage: false,
settings: {},
sendReminder: true,
...overrides,
});

View File

@@ -23,8 +23,6 @@ export interface CloudChannelValues extends ChannelValues {
export interface GrafanaChannelValues extends ChannelValues {
type: NotifierType;
uid?: string;
sendReminder: boolean;
disableResolveMessage: boolean;
}

View File

@@ -28,7 +28,7 @@ const basicAuthOption: NotificationChannelOption = option(
{
element: 'subform',
subformOptions: [
option('ussername', 'Username', ''),
option('username', 'Username', ''),
option('password', 'Password', ''),
option('password_file', 'Password file', ''),
],
@@ -330,3 +330,54 @@ export const cloudNotifierTypes: NotifierDTO[] = [
],
},
];
export const globalConfigOptions: NotificationChannelOption[] = [
// email
option('smtp_from', 'SMTP from', 'The default SMTP From header field.'),
option(
'smtp_smarthost',
'SMTP smarthost',
'The default SMTP smarthost used for sending emails, including port number. Port number usually is 25, or 587 for SMTP over TLS (sometimes referred to as STARTTLS). Example: smtp.example.org:587'
),
option('smtp_hello', 'SMTP hello', 'The default hostname to identify to the SMTP server.', {
placeholder: 'localhost',
}),
option(
'smtp_auth_username',
'SMTP auth username',
"SMTP Auth using CRAM-MD5, LOGIN and PLAIN. If empty, Alertmanager doesn't authenticate to the SMTP server."
),
option('smtp_auth_password', 'SMTP auth password', 'SMTP Auth using LOGIN and PLAIN.'),
option('smtp_auth_identity', 'SMTP auth identity', 'SMTP Auth using PLAIN.'),
option('smtp_auth_secret', 'SMTP auth secret', 'SMTP Auth using CRAM-MD5.'),
option(
'smtp_require_tls',
'SMTP require TLS',
'The default SMTP TLS requirement. Note that Go does not support unencrypted connections to remote SMTP endpoints.',
{
element: 'checkbox',
}
),
// slack
option('slack_api_url', 'Slack API URL', ''),
option('victorops_api_key', 'VictorOps API key', ''),
option('victorops_api_url', 'VictorOps API URL', '', {
placeholder: 'https://alert.victorops.com/integrations/generic/20131114/alert/',
}),
option('pagerduty_url', 'PagerDuty URL', 'https://events.pagerduty.com/v2/enqueue'),
option('opsgenie_api_key', 'OpsGenie API key', ''),
option('opsgenie_api_url', 'OpsGenie API URL', '', { placeholder: 'https://api.opsgenie.com/' }),
option('wechat_api_url', 'WeChat API URL', '', { placeholder: 'https://qyapi.weixin.qq.com/cgi-bin/' }),
option('wechat_api_secret', 'WeChat API secret', ''),
option('wechat_api_corp_id', 'WeChat API corp id', ''),
httpConfigOption,
option(
'resolve_timeout',
'Resolve timeout',
'ResolveTimeout is the default value used by alertmanager if the alert does not include EndsAt, after this time passes it can declare the alert as resolved if it has not been updated. This has no impact on alerts from Prometheus, as they always include EndsAt.',
{
placeholder: '5m',
}
),
];

View File

@@ -190,10 +190,8 @@ function grafanaChannelConfigToFormChannelValues(
const values: GrafanaChannelValues = {
__id: id,
type: channel.type as NotifierType,
uid: channel.uid,
secureSettings: {},
settings: { ...channel.settings },
sendReminder: channel.sendReminder,
secureFields: { ...channel.secureFields },
disableResolveMessage: channel.disableResolveMessage,
};
@@ -216,20 +214,16 @@ function formChannelValuesToGrafanaChannelConfig(
existing?: GrafanaManagedReceiverConfig
): GrafanaManagedReceiverConfig {
const channel: GrafanaManagedReceiverConfig = {
settings: {
settings: omitEmptyValues({
...(existing && existing.type === values.type ? existing.settings ?? {} : {}),
...(values.settings ?? {}),
},
}),
secureSettings: values.secureSettings ?? {},
type: values.type,
sendReminder: values.sendReminder ?? existing?.sendReminder ?? defaults.sendReminder,
name,
disableResolveMessage:
values.disableResolveMessage ?? existing?.disableResolveMessage ?? defaults.disableResolveMessage,
};
if (existing) {
channel.uid = existing.uid;
}
return channel;
}

View File

@@ -68,12 +68,10 @@ export type WebhookConfig = {
};
export type GrafanaManagedReceiverConfig = {
uid?: string;
disableResolveMessage: boolean;
secureFields?: Record<string, boolean>;
secureSettings?: Record<string, unknown>;
settings: Record<string, unknown>;
sendReminder: boolean;
type: string;
name: string;
updated?: string;

View File

@@ -410,6 +410,13 @@ export function getAppRoutes(): RouteDescriptor[] {
() => import(/* webpackChunkName: "NotificationsListPage" */ 'app/features/alerting/NotificationsIndex')
),
},
{
path: '/alerting/notifications/global-config',
roles: () => ['Admin', 'Editor'],
component: SafeDynamicImport(
() => import(/* webpackChunkName: "NotificationsListPage" */ 'app/features/alerting/NotificationsIndex')
),
},
{
path: '/alerting/notification/new',
component: SafeDynamicImport(