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

pull/34185/head
Domas 4 years ago committed by GitHub
parent d721298e03
commit 7a2dff741b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      pkg/api/index.go
  2. 2
      public/app/features/alerting/unified/Receivers.test.tsx
  3. 4
      public/app/features/alerting/unified/Receivers.tsx
  4. 112
      public/app/features/alerting/unified/components/receivers/GlobalConfigForm.tsx
  5. 38
      public/app/features/alerting/unified/components/receivers/ReceiversAndTemplatesView.tsx
  6. 2
      public/app/features/alerting/unified/components/receivers/ReceiversTable.test.tsx
  7. 7
      public/app/features/alerting/unified/components/receivers/form/GrafanaCommonChannelSettings.tsx
  8. 1
      public/app/features/alerting/unified/components/receivers/form/GrafanaReceiverForm.tsx
  9. 1
      public/app/features/alerting/unified/mocks.ts
  10. 2
      public/app/features/alerting/unified/types/receiver-form.ts
  11. 53
      public/app/features/alerting/unified/utils/cloud-alertmanager-notifier-types.ts
  12. 10
      public/app/features/alerting/unified/utils/receiver-form.ts
  13. 2
      public/app/plugins/datasource/alertmanager/types.ts
  14. 7
      public/app/routes/routes.tsx

@ -214,7 +214,7 @@ func (hs *HTTPServer) getNavTree(c *models.ReqContext, hasEditPerm bool) ([]*dto
Text: "Contact points", Id: "receivers", Url: hs.Cfg.AppSubURL + "/alerting/notifications",
Icon: "comment-alt-share",
})
alertChildNavs = append(alertChildNavs, &dtos.NavLink{Text: "Routes", Id: "am-routes", Url: hs.Cfg.AppSubURL + "/alerting/routes", Icon: "sitemap"})
alertChildNavs = append(alertChildNavs, &dtos.NavLink{Text: "Notification policies", Id: "am-routes", Url: hs.Cfg.AppSubURL + "/alerting/routes", Icon: "sitemap"})
} else {
alertChildNavs = append(alertChildNavs, &dtos.NavLink{
Text: "Notification channels", Id: "channels", Url: hs.Cfg.AppSubURL + "/alerting/notifications",

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

@ -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>

@ -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)};
`,
});

@ -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)};
`,
});

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

@ -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>
);
};

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

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

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

@ -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',
}
),
];

@ -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;
}

@ -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;

@ -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(

Loading…
Cancel
Save