(isOpen);
+ const styles = useStyles(collapsableSectionStyles);
+
+ return (
+
+
toggleOpen(!open)} className={styles.header}>
+
+ {label}
+
+
{open && children}
+
+ );
+};
+
+const collapsableSectionStyles = (theme: GrafanaTheme) => {
+ return {
+ header: css`
+ font-size: ${theme.typography.size.lg};
+ cursor: pointer;
+ `,
+ content: css`
+ padding: ${theme.spacing.md} 0 ${theme.spacing.md} ${theme.spacing.md};
+ `,
+ };
+};
diff --git a/packages/grafana-ui/src/components/Forms/Checkbox.tsx b/packages/grafana-ui/src/components/Forms/Checkbox.tsx
index aa75364c1d3..92d42833428 100644
--- a/packages/grafana-ui/src/components/Forms/Checkbox.tsx
+++ b/packages/grafana-ui/src/components/Forms/Checkbox.tsx
@@ -80,7 +80,7 @@ export const getCheckboxStyles = stylesFactory((theme: GrafanaTheme) => {
background: ${theme.colors.formInputBg};
border: 1px solid ${theme.colors.formInputBorder};
position: absolute;
- top: 1px;
+ top: 2px;
left: 0;
&:hover {
diff --git a/packages/grafana-ui/src/components/index.ts b/packages/grafana-ui/src/components/index.ts
index 39553882e46..a75963c3715 100644
--- a/packages/grafana-ui/src/components/index.ts
+++ b/packages/grafana-ui/src/components/index.ts
@@ -93,6 +93,7 @@ export {
export { Alert, AlertVariant } from './Alert/Alert';
export { GraphSeriesToggler, GraphSeriesTogglerAPI } from './Graph/GraphSeriesToggler';
export { Collapse, ControlledCollapse } from './Collapse/Collapse';
+export { CollapsableSection } from './Collapse/CollapsableSection';
export { LogLabels } from './Logs/LogLabels';
export { LogRows } from './Logs/LogRows';
export { getLogRowStyles } from './Logs/getLogRowStyles';
diff --git a/pkg/services/alerting/notifier.go b/pkg/services/alerting/notifier.go
index 533d7866777..8603aa232c4 100644
--- a/pkg/services/alerting/notifier.go
+++ b/pkg/services/alerting/notifier.go
@@ -23,14 +23,13 @@ var newImageUploaderProvider = func() (imguploader.ImageUploader, error) {
// NotifierPlugin holds meta information about a notifier.
type NotifierPlugin struct {
- Type string `json:"type"`
- Name string `json:"name"`
- Heading string `json:"heading"`
- Description string `json:"description"`
- Info string `json:"info"`
- OptionsTemplate string `json:"optionsTemplate"`
- Factory NotifierFactory `json:"-"`
- Options []NotifierOption `json:"options"`
+ Type string `json:"type"`
+ Name string `json:"name"`
+ Heading string `json:"heading"`
+ Description string `json:"description"`
+ Info string `json:"info"`
+ Factory NotifierFactory `json:"-"`
+ Options []NotifierOption `json:"options"`
}
// NotifierOption holds information about options specific for the NotifierPlugin.
@@ -45,6 +44,7 @@ type NotifierOption struct {
ShowWhen ShowWhen `json:"showWhen"`
Required bool `json:"required"`
ValidationRule string `json:"validationRule"`
+ Secure bool `json:"secure"`
}
// InputType is the type of input that can be rendered in the frontend.
@@ -65,8 +65,8 @@ const (
ElementTypeInput = "input"
// ElementTypeSelect will render a select
ElementTypeSelect = "select"
- // ElementTypeSwitch will render a switch
- ElementTypeSwitch = "switch"
+ // ElementTypeCheckbox will render a checkbox
+ ElementTypeCheckbox = "checkbox"
// ElementTypeTextArea will render a textarea
ElementTypeTextArea = "textarea"
)
diff --git a/pkg/services/alerting/notifiers/alertmanager.go b/pkg/services/alerting/notifiers/alertmanager.go
index fea254e3159..bc0b9ea6f25 100644
--- a/pkg/services/alerting/notifiers/alertmanager.go
+++ b/pkg/services/alerting/notifiers/alertmanager.go
@@ -20,36 +20,6 @@ func init() {
Description: "Sends alert to Prometheus Alertmanager",
Heading: "Alertmanager settings",
Factory: NewAlertmanagerNotifier,
- OptionsTemplate: `
- Alertmanager settings
-
- Url(s)
-
-
- As specified in Alertmanager documentation, do not specify a load balancer here. Enter all your Alertmanager URLs comma-separated.
-
-
-
- Basic Auth User
-
-
-
-
- `,
Options: []alerting.NotifierOption{
{
Label: "Url",
@@ -71,6 +41,7 @@ func init() {
Element: alerting.ElementTypeInput,
InputType: alerting.InputTypePassword,
PropertyName: "basicAuthPassword",
+ Secure: true,
},
},
})
diff --git a/pkg/services/alerting/notifiers/dingding.go b/pkg/services/alerting/notifiers/dingding.go
index 9149f3a6665..0070cfdacd7 100644
--- a/pkg/services/alerting/notifiers/dingding.go
+++ b/pkg/services/alerting/notifiers/dingding.go
@@ -12,26 +12,14 @@ import (
)
const defaultDingdingMsgType = "link"
-const dingdingOptionsTemplate = `
- DingDing settings
-
- Url
-
-
-
- MessageType
-
-
-`
func init() {
alerting.RegisterNotifier(&alerting.NotifierPlugin{
- Type: "dingding",
- Name: "DingDing",
- Description: "Sends HTTP POST request to DingDing",
- Heading: "DingDing settings",
- Factory: newDingDingNotifier,
- OptionsTemplate: dingdingOptionsTemplate,
+ Type: "dingding",
+ Name: "DingDing",
+ Description: "Sends HTTP POST request to DingDing",
+ Heading: "DingDing settings",
+ Factory: newDingDingNotifier,
Options: []alerting.NotifierOption{
{
Label: "Url",
diff --git a/pkg/services/alerting/notifiers/discord.go b/pkg/services/alerting/notifiers/discord.go
index dbcb22609ea..b3a40612b12 100644
--- a/pkg/services/alerting/notifiers/discord.go
+++ b/pkg/services/alerting/notifiers/discord.go
@@ -23,24 +23,6 @@ func init() {
Description: "Sends notifications to Discord",
Factory: newDiscordNotifier,
Heading: "Discord settings",
- OptionsTemplate: `
- Discord settings
-
- Message Content
-
-
-
- Mention a group using @ or a user using <@ID> when notifying in a channel
-
-
-
- Webhook URL
-
-
- `,
Options: []alerting.NotifierOption{
{
Label: "Message Content",
diff --git a/pkg/services/alerting/notifiers/email.go b/pkg/services/alerting/notifiers/email.go
index 81b29770dd1..fadeaf0e3b0 100644
--- a/pkg/services/alerting/notifiers/email.go
+++ b/pkg/services/alerting/notifiers/email.go
@@ -19,32 +19,11 @@ func init() {
Description: "Sends notifications using Grafana server configured SMTP settings",
Factory: NewEmailNotifier,
Heading: "Email settings",
- OptionsTemplate: `
- Email settings
-
-
-
-
-
-
- Addresses
-
-
-
-
- You can enter multiple email addresses using a ";" separator
-
- `,
Options: []alerting.NotifierOption{
{
Label: "Single email",
Description: "Send a single email to all recipients",
- Element: alerting.ElementTypeSwitch,
+ Element: alerting.ElementTypeCheckbox,
PropertyName: "singleEmail",
},
{
diff --git a/pkg/services/alerting/notifiers/googlechat.go b/pkg/services/alerting/notifiers/googlechat.go
index ddb469bd253..123df9a7c5e 100644
--- a/pkg/services/alerting/notifiers/googlechat.go
+++ b/pkg/services/alerting/notifiers/googlechat.go
@@ -19,13 +19,6 @@ func init() {
Description: "Sends notifications to Google Hangouts Chat via webhooks based on the official JSON message format",
Factory: newGoogleChatNotifier,
Heading: "Google Hangouts Chat settings",
- OptionsTemplate: `
- Google Hangouts Chat settings
-
- Url
-
-
- `,
Options: []alerting.NotifierOption{
{
Label: "Url",
diff --git a/pkg/services/alerting/notifiers/hipchat.go b/pkg/services/alerting/notifiers/hipchat.go
index 8c76e7045b0..ae29fe4e64e 100644
--- a/pkg/services/alerting/notifiers/hipchat.go
+++ b/pkg/services/alerting/notifiers/hipchat.go
@@ -20,25 +20,6 @@ func init() {
Description: "Sends notifications uto a HipChat Room",
Heading: "HipChat settings",
Factory: NewHipChatNotifier,
- OptionsTemplate: `
- HipChat settings
-
- Hip Chat Url
-
-
-
- API Key
-
-
-
- Room ID
-
-
-
- `,
Options: []alerting.NotifierOption{
{
Label: "Hip Chat Url",
diff --git a/pkg/services/alerting/notifiers/kafka.go b/pkg/services/alerting/notifiers/kafka.go
index 8783620f6ed..bba43017bd3 100644
--- a/pkg/services/alerting/notifiers/kafka.go
+++ b/pkg/services/alerting/notifiers/kafka.go
@@ -19,17 +19,6 @@ func init() {
Description: "Sends notifications to Kafka Rest Proxy",
Heading: "Kafka settings",
Factory: NewKafkaNotifier,
- OptionsTemplate: `
- Kafka settings
-
- Kafka REST Proxy
-
-
-
- Topic
-
-
- `,
Options: []alerting.NotifierOption{
{
Label: "Kafka REST Proxy",
diff --git a/pkg/services/alerting/notifiers/line.go b/pkg/services/alerting/notifiers/line.go
index f61c9d605ea..85eca74bc54 100644
--- a/pkg/services/alerting/notifiers/line.go
+++ b/pkg/services/alerting/notifiers/line.go
@@ -17,25 +17,6 @@ func init() {
Description: "Send notifications to LINE notify",
Heading: "LINE notify settings",
Factory: NewLINENotifier,
- OptionsTemplate: `
- LINE notify settings
-
-`,
Options: []alerting.NotifierOption{
{
Label: "Token",
@@ -44,6 +25,7 @@ func init() {
Placeholder: "LINE notify token key",
PropertyName: "token",
Required: true,
+ Secure: true,
}},
})
}
diff --git a/pkg/services/alerting/notifiers/opsgenie.go b/pkg/services/alerting/notifiers/opsgenie.go
index e7a3158fee9..8a9d058dc40 100644
--- a/pkg/services/alerting/notifiers/opsgenie.go
+++ b/pkg/services/alerting/notifiers/opsgenie.go
@@ -18,48 +18,6 @@ func init() {
Description: "Sends notifications to OpsGenie",
Heading: "OpsGenie settings",
Factory: NewOpsGenieNotifier,
- OptionsTemplate: `
- OpsGenie settings
-
-
- Alert API Url
-
-
-
-
-
-
-
-
-
-
-`,
Options: []alerting.NotifierOption{
{
Label: "API Key",
@@ -68,6 +26,7 @@ func init() {
Placeholder: "OpsGenie API Key",
PropertyName: "apiKey",
Required: true,
+ Secure: true,
},
{
Label: "Alert API Url",
@@ -79,12 +38,12 @@ func init() {
},
{
Label: "Auto close incidents",
- Element: alerting.ElementTypeSwitch,
+ Element: alerting.ElementTypeCheckbox,
Description: "Automatically close alerts in OpsGenie once the alert goes back to ok.",
PropertyName: "autoClose",
}, {
Label: "Override priority",
- Element: alerting.ElementTypeSwitch,
+ Element: alerting.ElementTypeCheckbox,
Description: "Allow the alert priority to be set using the og_priority tag",
PropertyName: "overridePriority",
},
diff --git a/pkg/services/alerting/notifiers/pagerduty.go b/pkg/services/alerting/notifiers/pagerduty.go
index 7712bcf544a..b9b2813b447 100644
--- a/pkg/services/alerting/notifiers/pagerduty.go
+++ b/pkg/services/alerting/notifiers/pagerduty.go
@@ -20,53 +20,6 @@ func init() {
Description: "Sends notifications to PagerDuty",
Heading: "PagerDuty settings",
Factory: NewPagerdutyNotifier,
- OptionsTemplate: `
- PagerDuty settings
-
-
-
-
-
-
-
-
-
-
- `,
Options: []alerting.NotifierOption{
{
Label: "Integration Key",
@@ -101,10 +54,16 @@ func init() {
},
{
Label: "Auto resolve incidents",
- Element: alerting.ElementTypeSwitch,
+ Element: alerting.ElementTypeCheckbox,
Description: "Resolve incidents in pagerduty once the alert goes back to ok.",
PropertyName: "autoResolve",
},
+ {
+ Label: "Include message in details",
+ Element: alerting.ElementTypeCheckbox,
+ Description: "Move the alert message from the PD summary into the custom details. This changes the custom details object and may break event rules you have configured",
+ PropertyName: "messageInDetails",
+ },
},
})
}
diff --git a/pkg/services/alerting/notifiers/pushover.go b/pkg/services/alerting/notifiers/pushover.go
index 8b7fa0b19a4..79cc97520cc 100644
--- a/pkg/services/alerting/notifiers/pushover.go
+++ b/pkg/services/alerting/notifiers/pushover.go
@@ -17,31 +17,6 @@ import (
const pushoverEndpoint = "https://api.pushover.net/1/messages.json"
func init() {
- sounds := `
- 'default',
- 'pushover',
- 'bike',
- 'bugle',
- 'cashregister',
- 'classical',
- 'cosmic',
- 'falling',
- 'gamelan',
- 'incoming',
- 'intermission',
- 'magic',
- 'mechanical',
- 'pianobar',
- 'siren',
- 'spacealarm',
- 'tugboat',
- 'alien',
- 'climb',
- 'persistent',
- 'echo',
- 'updown',
- 'none'`
-
soundOptions := []alerting.SelectOption{
{
Value: "default",
@@ -122,76 +97,6 @@ func init() {
Description: "Sends HTTP POST request to the Pushover API",
Heading: "Pushover settings",
Factory: NewPushoverNotifier,
- OptionsTemplate: `
- Pushover settings
-
-
-
- Device(s) (optional)
-
-
-
- Priority
-
-
-
- Retry
-
- Expire
-
-
-
- Alerting sound
-
-
-
- OK sound
-
-
- `,
Options: []alerting.NotifierOption{
{
Label: "API Token",
@@ -200,6 +105,7 @@ func init() {
Placeholder: "Application token",
PropertyName: "apiToken",
Required: true,
+ Secure: true,
},
{
Label: "User key(s)",
@@ -208,6 +114,7 @@ func init() {
Placeholder: "comma-separated list",
PropertyName: "userKey",
Required: true,
+ Secure: true,
},
{
Label: "Device(s) (optional)",
diff --git a/pkg/services/alerting/notifiers/sensu.go b/pkg/services/alerting/notifiers/sensu.go
index 6397d15581e..69e392a7aa8 100644
--- a/pkg/services/alerting/notifiers/sensu.go
+++ b/pkg/services/alerting/notifiers/sensu.go
@@ -18,41 +18,6 @@ func init() {
Description: "Sends HTTP POST request to a Sensu API",
Heading: "Sensu settings",
Factory: NewSensuNotifier,
- OptionsTemplate: `
- Sensu settings
-
- Url
-
-
-
- Source
-
-
-
- Handler
-
-
-
- Username
-
-
-
- `,
Options: []alerting.NotifierOption{
{
Label: "Url",
@@ -87,6 +52,7 @@ func init() {
Element: alerting.ElementTypeInput,
InputType: alerting.InputTypePassword,
PropertyName: "passsword ",
+ Secure: true,
},
},
})
diff --git a/pkg/services/alerting/notifiers/slack.go b/pkg/services/alerting/notifiers/slack.go
index bd13e5a96f1..88674de9fb2 100644
--- a/pkg/services/alerting/notifiers/slack.go
+++ b/pkg/services/alerting/notifiers/slack.go
@@ -27,123 +27,6 @@ func init() {
Description: "Sends notifications to Slack via Slack Webhooks",
Heading: "Slack settings",
Factory: NewSlackNotifier,
- OptionsTemplate: `
- Slack settings
-
-
- Recipient
-
-
-
- Override default channel or user, use #channel-name, @username (has to be all lowercase, no whitespace), or user/channel Slack ID
-
-
-
- Username
-
-
-
- Set the username for the bot's message
-
-
-
- Icon emoji
-
-
-
- Provide an emoji to use as the icon for the bot's message. Overrides the icon URL
-
-
-
- Icon URL
-
-
-
- Provide a URL to an image to use as the icon for the bot's message
-
-
-
- Mention Users
-
-
-
- Mention one or more users (comma separated) when notifying in a channel, by ID (you can copy this from the user's Slack profile)
-
-
-
- Mention Groups
-
-
-
- Mention one or more groups (comma separated) when notifying in a channel (you can copy this from the group's Slack profile URL)
-
-
-
- Mention Channel
-
- Disabled
- Every active channel member
- Every channel member
-
-
- Mention whole channel or just active members when notifying
-
-
-
- `,
Options: []alerting.NotifierOption{
{
Label: "Url",
@@ -152,6 +35,7 @@ func init() {
Placeholder: "Slack incoming webhook url",
PropertyName: "url",
Required: true,
+ Secure: true,
},
{
Label: "Recipient",
@@ -172,14 +56,14 @@ func init() {
Element: alerting.ElementTypeInput,
InputType: alerting.InputTypeText,
Description: "Provide an emoji to use as the icon for the bot's message. Overrides the icon URL.",
- PropertyName: "icon_emoji",
+ PropertyName: "iconEmoji",
},
{
Label: "Icon URL",
Element: alerting.ElementTypeInput,
InputType: alerting.InputTypeText,
Description: "Provide a URL to an image to use as the icon for the bot's message",
- PropertyName: "icon_url",
+ PropertyName: "iconUrl",
},
{
Label: "Mention Users",
@@ -221,6 +105,7 @@ func init() {
InputType: alerting.InputTypeText,
Description: "Provide a bot token to use the Slack file.upload API (starts with \"xoxb\"). Specify Recipient for this to work",
PropertyName: "token",
+ Secure: true,
},
},
})
diff --git a/pkg/services/alerting/notifiers/teams.go b/pkg/services/alerting/notifiers/teams.go
index e30a10bcbfc..e31a095e192 100644
--- a/pkg/services/alerting/notifiers/teams.go
+++ b/pkg/services/alerting/notifiers/teams.go
@@ -16,13 +16,6 @@ func init() {
Description: "Sends notifications using Incoming Webhook connector to Microsoft Teams",
Heading: "Teams settings",
Factory: NewTeamsNotifier,
- OptionsTemplate: `
- Teams settings
-
- Url
-
-
- `,
Options: []alerting.NotifierOption{
{
Label: "URL",
@@ -30,6 +23,7 @@ func init() {
InputType: alerting.InputTypeText,
Placeholder: "Teams incoming webhook url",
PropertyName: "url",
+ Required: true,
},
},
})
diff --git a/pkg/services/alerting/notifiers/telegram.go b/pkg/services/alerting/notifiers/telegram.go
index 86fdbaeb4d8..ede1ccb1487 100644
--- a/pkg/services/alerting/notifiers/telegram.go
+++ b/pkg/services/alerting/notifiers/telegram.go
@@ -28,37 +28,6 @@ func init() {
Description: "Sends notifications to Telegram",
Heading: "Telegram API settings",
Factory: NewTelegramNotifier,
- OptionsTemplate: `
- Telegram API settings
-
-
- Chat ID
-
-
- Integer Telegram Chat Identifier
-
- `,
Options: []alerting.NotifierOption{
{
Label: "BOT API Token",
@@ -67,6 +36,7 @@ func init() {
Placeholder: "Telegram BOT API Token",
PropertyName: "bottoken",
Required: true,
+ Secure: true,
},
{
Label: "Chat ID",
diff --git a/pkg/services/alerting/notifiers/threema.go b/pkg/services/alerting/notifiers/threema.go
index 9a08fd67102..0ad1342e1a5 100644
--- a/pkg/services/alerting/notifiers/threema.go
+++ b/pkg/services/alerting/notifiers/threema.go
@@ -24,55 +24,6 @@ func init() {
Info: "Notifications can be configured for any Threema Gateway ID of type \"Basic\". End-to-End IDs are not currently supported." +
"The Threema Gateway ID can be set up at https://gateway.threema.ch/.",
Factory: NewThreemaNotifier,
- OptionsTemplate: `
- Threema Gateway settings
-
- Notifications can be configured for any Threema Gateway ID of type
- "Basic". End-to-End IDs are not currently supported.
-
-
- The Threema Gateway ID can be set up at
- https://gateway.threema.ch/ .
-
-
- Gateway ID
-
-
-
- Your 8 character Threema Gateway ID (starting with a *)
-
-
-
- Recipient ID
-
-
-
- The 8 character Threema ID that should receive the alerts
-
-
-
- `,
Options: []alerting.NotifierOption{
{
Label: "Gateway ID",
@@ -101,6 +52,7 @@ func init() {
Description: "Your Threema Gateway API secret.",
PropertyName: "api_secret",
Required: true,
+ Secure: true,
},
},
})
diff --git a/pkg/services/alerting/notifiers/victorops.go b/pkg/services/alerting/notifiers/victorops.go
index e0eb0092291..ae87150406c 100644
--- a/pkg/services/alerting/notifiers/victorops.go
+++ b/pkg/services/alerting/notifiers/victorops.go
@@ -25,29 +25,6 @@ func init() {
Description: "Sends notifications to VictorOps",
Heading: "VictorOps settings",
Factory: NewVictoropsNotifier,
- OptionsTemplate: `
- VictorOps settings
-
- Url
-
-
-
-
-
-
-
- `,
Options: []alerting.NotifierOption{
{
Label: "Url",
@@ -60,7 +37,7 @@ func init() {
{
Label: "Auto resolve incidents",
Description: "Resolve incidents in VictorOps once the alert goes back to ok.",
- Element: alerting.ElementTypeSwitch,
+ Element: alerting.ElementTypeCheckbox,
PropertyName: "autoResolve",
},
},
diff --git a/pkg/services/alerting/notifiers/webhook.go b/pkg/services/alerting/notifiers/webhook.go
index e9894c7b076..b0b842efdba 100644
--- a/pkg/services/alerting/notifiers/webhook.go
+++ b/pkg/services/alerting/notifiers/webhook.go
@@ -15,39 +15,6 @@ func init() {
Description: "Sends HTTP POST request to a URL",
Heading: "Webhook settings",
Factory: NewWebHookNotifier,
- OptionsTemplate: `
- Webhook settings
-
- Url
-
-
-
-
- Username
-
-
-
- `,
Options: []alerting.NotifierOption{
{
Label: "Url",
@@ -82,6 +49,7 @@ func init() {
Element: alerting.ElementTypeInput,
InputType: alerting.InputTypePassword,
PropertyName: "password",
+ Secure: true,
},
},
})
diff --git a/public/app/features/alerting/EditNotificationChannelPage.tsx b/public/app/features/alerting/EditNotificationChannelPage.tsx
new file mode 100644
index 00000000000..8d1bfa15710
--- /dev/null
+++ b/public/app/features/alerting/EditNotificationChannelPage.tsx
@@ -0,0 +1,150 @@
+import React, { PureComponent } from 'react';
+import { MapDispatchToProps, MapStateToProps } from 'react-redux';
+import { NavModel } from '@grafana/data';
+import { config } from '@grafana/runtime';
+import { Form, Spinner } from '@grafana/ui';
+import Page from 'app/core/components/Page/Page';
+import { connectWithCleanUp } from 'app/core/components/connectWithCleanUp';
+import { NotificationChannelForm } from './components/NotificationChannelForm';
+import {
+ loadNotificationChannel,
+ loadNotificationTypes,
+ testNotificationChannel,
+ updateNotificationChannel,
+} from './state/actions';
+import { getNavModel } from 'app/core/selectors/navModel';
+import { getRouteParamsId } from 'app/core/selectors/location';
+import { mapChannelsToSelectableValue, transformSubmitData, transformTestData } from './utils/notificationChannels';
+import { NotificationChannelType, NotificationChannelDTO, StoreState } from 'app/types';
+import { resetSecureField } from './state/reducers';
+
+interface OwnProps {}
+
+interface ConnectedProps {
+ navModel: NavModel;
+ channelId: number;
+ notificationChannel: any;
+ notificationChannelTypes: NotificationChannelType[];
+}
+
+interface DispatchProps {
+ loadNotificationTypes: typeof loadNotificationTypes;
+ loadNotificationChannel: typeof loadNotificationChannel;
+ testNotificationChannel: typeof testNotificationChannel;
+ updateNotificationChannel: typeof updateNotificationChannel;
+ resetSecureField: typeof resetSecureField;
+}
+
+type Props = OwnProps & ConnectedProps & DispatchProps;
+
+export class EditNotificationChannelPage extends PureComponent {
+ componentDidMount() {
+ const { channelId } = this.props;
+
+ this.props.loadNotificationTypes();
+ this.props.loadNotificationChannel(channelId);
+ }
+
+ onSubmit = (formData: NotificationChannelDTO) => {
+ const { notificationChannel } = this.props;
+
+ this.props.updateNotificationChannel({
+ /*
+ Some settings which lives in a collapsed section will not be registered since
+ the section will not be rendered if a user doesn't expand it. Therefore we need to
+ merge the initialData with any changes from the form.
+ */
+ ...transformSubmitData({
+ ...notificationChannel,
+ ...formData,
+ settings: { ...notificationChannel.settings, ...formData.settings },
+ }),
+ id: notificationChannel.id,
+ });
+ };
+
+ onTestChannel = (formData: NotificationChannelDTO) => {
+ const { notificationChannel } = this.props;
+ /*
+ Same as submit
+ */
+ this.props.testNotificationChannel(
+ transformTestData({
+ ...notificationChannel,
+ ...formData,
+ settings: { ...notificationChannel.settings, ...formData.settings },
+ })
+ );
+ };
+
+ render() {
+ const { navModel, notificationChannel, notificationChannelTypes } = this.props;
+
+ return (
+
+
+ Edit notification channel
+ {notificationChannel && notificationChannel.id > 0 ? (
+
+ ) : (
+
+ Loading notification channel
+
+
+ )}
+
+
+ );
+ }
+}
+
+const mapStateToProps: MapStateToProps = state => {
+ const channelId = getRouteParamsId(state.location) as number;
+ return {
+ navModel: getNavModel(state.navIndex, 'channels'),
+ channelId,
+ notificationChannel: state.notificationChannel.notificationChannel,
+ notificationChannelTypes: state.notificationChannel.notificationChannelTypes,
+ };
+};
+
+const mapDispatchToProps: MapDispatchToProps = {
+ loadNotificationTypes,
+ loadNotificationChannel,
+ testNotificationChannel,
+ updateNotificationChannel,
+ resetSecureField,
+};
+
+export default connectWithCleanUp(
+ mapStateToProps,
+ mapDispatchToProps,
+ state => state.notificationChannel
+)(EditNotificationChannelPage);
diff --git a/public/app/features/alerting/NewAlertNotificationPage.tsx b/public/app/features/alerting/NewAlertNotificationPage.tsx
deleted file mode 100644
index 527533cdf44..00000000000
--- a/public/app/features/alerting/NewAlertNotificationPage.tsx
+++ /dev/null
@@ -1,132 +0,0 @@
-import React, { PureComponent } from 'react';
-import { connect, MapDispatchToProps, MapStateToProps } from 'react-redux';
-import { NavModel, SelectableValue } from '@grafana/data';
-import { config } from '@grafana/runtime';
-import { Form } from '@grafana/ui';
-import Page from 'app/core/components/Page/Page';
-import { NewNotificationChannelForm } from './components/NewNotificationChannelForm';
-import { getNavModel } from 'app/core/selectors/navModel';
-import { createNotificationChannel, loadNotificationTypes, testNotificationChannel } from './state/actions';
-import { NotificationChannel, NotificationChannelDTO, StoreState } from '../../types';
-
-interface OwnProps {}
-
-interface ConnectedProps {
- navModel: NavModel;
- notificationChannels: NotificationChannel[];
-}
-
-interface DispatchProps {
- createNotificationChannel: typeof createNotificationChannel;
- loadNotificationTypes: typeof loadNotificationTypes;
- testNotificationChannel: typeof testNotificationChannel;
-}
-
-type Props = OwnProps & ConnectedProps & DispatchProps;
-
-const defaultValues: NotificationChannelDTO = {
- name: '',
- type: { value: 'email', label: 'Email' },
- sendReminder: false,
- disableResolveMessage: false,
- frequency: '15m',
- settings: {
- uploadImage: config.rendererAvailable,
- autoResolve: true,
- httpMethod: 'POST',
- severity: 'critical',
- },
- isDefault: false,
-};
-
-class NewAlertNotificationPage extends PureComponent {
- componentDidMount() {
- this.props.loadNotificationTypes();
- }
-
- onSubmit = (data: NotificationChannelDTO) => {
- /*
- Some settings can be options in a select, in order to not save a SelectableValue
- we need to use check if it is a SelectableValue and use its value.
- */
- const settings = Object.fromEntries(
- Object.entries(data.settings).map(([key, value]) => {
- return [key, value.hasOwnProperty('value') ? value.value : value];
- })
- );
-
- this.props.createNotificationChannel({
- ...defaultValues,
- ...data,
- type: data.type.value,
- settings: { ...defaultValues.settings, ...settings },
- });
- };
-
- onTestChannel = (data: NotificationChannelDTO) => {
- this.props.testNotificationChannel({
- name: data.name,
- type: data.type.value,
- frequency: data.frequency ?? defaultValues.frequency,
- settings: { ...Object.assign(defaultValues.settings, data.settings) },
- });
- };
-
- render() {
- const { navModel, notificationChannels } = this.props;
-
- /*
- Need to transform these as we have options on notificationChannels,
- this will render a dropdown within the select.
-
- TODO: Memoize?
- */
- const selectableChannels: Array> = notificationChannels.map(channel => ({
- value: channel.value,
- label: channel.label,
- description: channel.description,
- }));
-
- return (
-
-
- New Notification Channel
-
-
-
- );
- }
-}
-
-const mapStateToProps: MapStateToProps = state => {
- return {
- navModel: getNavModel(state.navIndex, 'channels'),
- notificationChannels: state.alertRules.notificationChannels,
- };
-};
-
-const mapDispatchToProps: MapDispatchToProps = {
- createNotificationChannel,
- loadNotificationTypes,
- testNotificationChannel,
-};
-
-export default connect(mapStateToProps, mapDispatchToProps)(NewAlertNotificationPage);
diff --git a/public/app/features/alerting/NewNotificationChannelPage.tsx b/public/app/features/alerting/NewNotificationChannelPage.tsx
new file mode 100644
index 00000000000..7d934010631
--- /dev/null
+++ b/public/app/features/alerting/NewNotificationChannelPage.tsx
@@ -0,0 +1,96 @@
+import React, { PureComponent } from 'react';
+import { connect, MapDispatchToProps, MapStateToProps } from 'react-redux';
+import { NavModel } from '@grafana/data';
+import { config } from '@grafana/runtime';
+import { Form } from '@grafana/ui';
+import Page from 'app/core/components/Page/Page';
+import { NotificationChannelForm } from './components/NotificationChannelForm';
+import {
+ defaultValues,
+ mapChannelsToSelectableValue,
+ transformSubmitData,
+ transformTestData,
+} from './utils/notificationChannels';
+import { getNavModel } from 'app/core/selectors/navModel';
+import { createNotificationChannel, loadNotificationTypes, testNotificationChannel } from './state/actions';
+import { NotificationChannelType, NotificationChannelDTO, StoreState } from '../../types';
+import { resetSecureField } from './state/reducers';
+
+interface OwnProps {}
+
+interface ConnectedProps {
+ navModel: NavModel;
+ notificationChannelTypes: NotificationChannelType[];
+}
+
+interface DispatchProps {
+ createNotificationChannel: typeof createNotificationChannel;
+ loadNotificationTypes: typeof loadNotificationTypes;
+ testNotificationChannel: typeof testNotificationChannel;
+ resetSecureField: typeof resetSecureField;
+}
+
+type Props = OwnProps & ConnectedProps & DispatchProps;
+
+class NewNotificationChannelPage extends PureComponent {
+ componentDidMount() {
+ this.props.loadNotificationTypes();
+ }
+
+ onSubmit = (data: NotificationChannelDTO) => {
+ this.props.createNotificationChannel(transformSubmitData({ ...defaultValues, ...data }));
+ };
+
+ onTestChannel = (data: NotificationChannelDTO) => {
+ this.props.testNotificationChannel(transformTestData({ ...defaultValues, ...data }));
+ };
+
+ render() {
+ const { navModel, notificationChannelTypes } = this.props;
+
+ return (
+
+
+ New notification channel
+
+
+
+ );
+ }
+}
+
+const mapStateToProps: MapStateToProps = state => {
+ return {
+ navModel: getNavModel(state.navIndex, 'channels'),
+ notificationChannelTypes: state.notificationChannel.notificationChannelTypes,
+ };
+};
+
+const mapDispatchToProps: MapDispatchToProps = {
+ createNotificationChannel,
+ loadNotificationTypes,
+ testNotificationChannel,
+ resetSecureField,
+};
+
+export default connect(mapStateToProps, mapDispatchToProps)(NewNotificationChannelPage);
diff --git a/public/app/features/alerting/components/BasicSettings.tsx b/public/app/features/alerting/components/BasicSettings.tsx
new file mode 100644
index 00000000000..9f686f541f4
--- /dev/null
+++ b/public/app/features/alerting/components/BasicSettings.tsx
@@ -0,0 +1,44 @@
+import React, { FC } from 'react';
+import { SelectableValue } from '@grafana/data';
+import { CollapsableSection, Field, Input, InputControl, Select } from '@grafana/ui';
+import { NotificationChannelOptions } from './NotificationChannelOptions';
+import { NotificationSettingsProps } from './NotificationChannelForm';
+import { NotificationChannelSecureFields, NotificationChannelType } from '../../../types';
+
+interface Props extends NotificationSettingsProps {
+ selectedChannel: NotificationChannelType;
+ channels: Array>;
+ secureFields: NotificationChannelSecureFields;
+ resetSecureField: (key: string) => void;
+}
+
+export const BasicSettings: FC = ({
+ control,
+ currentFormValues,
+ errors,
+ secureFields,
+ selectedChannel,
+ channels,
+ register,
+ resetSecureField,
+}) => {
+ return (
+
+
+
+
+
+
+
+ o.required)}
+ currentFormValues={currentFormValues}
+ secureFields={secureFields}
+ onResetSecureField={resetSecureField}
+ register={register}
+ errors={errors}
+ control={control}
+ />
+
+ );
+};
diff --git a/public/app/features/alerting/components/ChannelSettings.tsx b/public/app/features/alerting/components/ChannelSettings.tsx
new file mode 100644
index 00000000000..79cd233ea72
--- /dev/null
+++ b/public/app/features/alerting/components/ChannelSettings.tsx
@@ -0,0 +1,36 @@
+import React, { FC } from 'react';
+import { CollapsableSection, InfoBox } from '@grafana/ui';
+import { NotificationChannelOptions } from './NotificationChannelOptions';
+import { NotificationSettingsProps } from './NotificationChannelForm';
+import { NotificationChannelSecureFields, NotificationChannelType } from '../../../types';
+
+interface Props extends NotificationSettingsProps {
+ selectedChannel: NotificationChannelType;
+ secureFields: NotificationChannelSecureFields;
+ resetSecureField: (key: string) => void;
+}
+
+export const ChannelSettings: FC = ({
+ control,
+ currentFormValues,
+ errors,
+ selectedChannel,
+ secureFields,
+ register,
+ resetSecureField,
+}) => {
+ return (
+
+ {selectedChannel.info !== '' && {selectedChannel.info} }
+ !o.required)}
+ currentFormValues={currentFormValues}
+ register={register}
+ errors={errors}
+ control={control}
+ onResetSecureField={resetSecureField}
+ secureFields={secureFields}
+ />
+
+ );
+};
diff --git a/public/app/features/alerting/components/NewNotificationChannelForm.tsx b/public/app/features/alerting/components/NewNotificationChannelForm.tsx
deleted file mode 100644
index e0a7670c9e6..00000000000
--- a/public/app/features/alerting/components/NewNotificationChannelForm.tsx
+++ /dev/null
@@ -1,125 +0,0 @@
-import React, { FC, useEffect } from 'react';
-import { css } from 'emotion';
-import { GrafanaTheme, SelectableValue } from '@grafana/data';
-import {
- Button,
- Field,
- FormAPI,
- HorizontalGroup,
- InfoBox,
- Input,
- InputControl,
- Select,
- stylesFactory,
- Switch,
- useTheme,
-} from '@grafana/ui';
-import { NotificationChannel, NotificationChannelDTO } from '../../../types';
-import { NotificationChannelOptions } from './NotificationChannelOptions';
-
-interface Props extends Omit, 'formState'> {
- selectableChannels: Array>;
- selectedChannel?: NotificationChannel;
- imageRendererAvailable: boolean;
-
- onTestChannel: (data: NotificationChannelDTO) => void;
-}
-
-export const NewNotificationChannelForm: FC = ({
- control,
- errors,
- selectedChannel,
- selectableChannels,
- register,
- watch,
- getValues,
- imageRendererAvailable,
- onTestChannel,
-}) => {
- const styles = getStyles(useTheme());
-
- useEffect(() => {
- watch(['type', 'settings.priority', 'sendReminder', 'uploadImage']);
- }, []);
-
- const currentFormValues = getValues();
- return (
- <>
-
-
-
-
-
-
-
-
-
-
-
-
-
- {currentFormValues.uploadImage && !imageRendererAvailable && (
-
- Grafana cannot find an image renderer to capture an image for the notification. Please make sure the Grafana
- Image Renderer plugin is installed. Please contact your Grafana administrator to install the plugin.
-
- )}
-
-
-
-
-
-
- {currentFormValues.sendReminder && (
- <>
-
-
-
-
- Alert reminders are sent after rules are evaluated. Therefore a reminder can never be sent more frequently
- than a configured alert rule evaluation interval.
-
- >
- )}
-
- {selectedChannel && (
-
- )}
-
- Save
- onTestChannel(getValues({ nest: true }))}>
- Test
-
-
- Back
-
-
- >
- );
-};
-
-const getStyles = stylesFactory((theme: GrafanaTheme) => {
- return {
- basicSettings: css`
- margin-bottom: ${theme.spacing.xl};
- `,
- };
-});
diff --git a/public/app/features/alerting/components/NotificationChannelForm.tsx b/public/app/features/alerting/components/NotificationChannelForm.tsx
new file mode 100644
index 00000000000..29fd9afc0ac
--- /dev/null
+++ b/public/app/features/alerting/components/NotificationChannelForm.tsx
@@ -0,0 +1,100 @@
+import React, { FC, useEffect } from 'react';
+import { css } from 'emotion';
+import { GrafanaTheme, SelectableValue } from '@grafana/data';
+import { Button, FormAPI, HorizontalGroup, stylesFactory, useTheme, Spinner } from '@grafana/ui';
+import { NotificationChannelType, NotificationChannelDTO, NotificationChannelSecureFields } from '../../../types';
+import { NotificationSettings } from './NotificationSettings';
+import { BasicSettings } from './BasicSettings';
+import { ChannelSettings } from './ChannelSettings';
+
+interface Props extends Omit, 'formState'> {
+ selectableChannels: Array>;
+ selectedChannel?: NotificationChannelType;
+ imageRendererAvailable: boolean;
+ secureFields: NotificationChannelSecureFields;
+ resetSecureField: (key: string) => void;
+ onTestChannel: (data: NotificationChannelDTO) => void;
+}
+
+export interface NotificationSettingsProps
+ extends Omit, 'formState' | 'watch' | 'getValues'> {
+ currentFormValues: NotificationChannelDTO;
+}
+
+export const NotificationChannelForm: FC = ({
+ control,
+ errors,
+ selectedChannel,
+ selectableChannels,
+ register,
+ watch,
+ getValues,
+ imageRendererAvailable,
+ onTestChannel,
+ resetSecureField,
+ secureFields,
+}) => {
+ const styles = getStyles(useTheme());
+
+ useEffect(() => {
+ watch(['type', 'settings.priority', 'sendReminder', 'uploadImage']);
+ }, []);
+
+ const currentFormValues = getValues();
+ return selectedChannel ? (
+ <>
+
+
+ {/* If there are no non-required fields, don't render this section*/}
+ {selectedChannel.options.filter(o => !o.required).length > 0 && (
+
+ )}
+
+
+
+ Save
+ onTestChannel(getValues({ nest: true }))}>
+ Test
+
+
+
+ Back
+
+
+
+ >
+ ) : (
+
+ );
+};
+
+const getStyles = stylesFactory((theme: GrafanaTheme) => {
+ return {
+ basicSettings: css`
+ margin-bottom: ${theme.spacing.xl};
+ `,
+ };
+});
diff --git a/public/app/features/alerting/components/NotificationChannelOptions.tsx b/public/app/features/alerting/components/NotificationChannelOptions.tsx
index 9fa7968a017..52fb6eaa9f9 100644
--- a/public/app/features/alerting/components/NotificationChannelOptions.tsx
+++ b/public/app/features/alerting/components/NotificationChannelOptions.tsx
@@ -1,28 +1,30 @@
import React, { FC } from 'react';
import { SelectableValue } from '@grafana/data';
-import { Field, FormAPI, InfoBox } from '@grafana/ui';
+import { Button, Checkbox, Field, FormAPI, Input } from '@grafana/ui';
import { OptionElement } from './OptionElement';
-import { NotificationChannel, NotificationChannelDTO, Option } from '../../../types';
+import { NotificationChannelDTO, NotificationChannelOption, NotificationChannelSecureFields } from '../../../types';
interface Props extends Omit, 'formState' | 'getValues' | 'watch'> {
- selectedChannel: NotificationChannel;
+ selectedChannelOptions: NotificationChannelOption[];
currentFormValues: NotificationChannelDTO;
+ secureFields: NotificationChannelSecureFields;
+
+ onResetSecureField: (key: string) => void;
}
export const NotificationChannelOptions: FC = ({
control,
currentFormValues,
errors,
- selectedChannel,
+ selectedChannelOptions,
register,
+ onResetSecureField,
+ secureFields,
}) => {
return (
<>
- {selectedChannel.heading}
- {selectedChannel.info !== '' && {selectedChannel.info} }
- {selectedChannel.options.map((option: Option, index: number) => {
+ {selectedChannelOptions.map((option: NotificationChannelOption, index: number) => {
const key = `${option.label}-${index}`;
-
// Some options can be dependent on other options, this determines what is selected in the dependency options
// I think this needs more thought.
const selectedOptionValue =
@@ -33,6 +35,18 @@ export const NotificationChannelOptions: FC = ({
return null;
}
+ if (option.element === 'checkbox') {
+ return (
+
+
+
+ );
+ }
return (
= ({
invalid={errors.settings && !!errors.settings[option.propertyName]}
error={errors.settings && errors.settings[option.propertyName]?.message}
>
-
+ {secureFields && secureFields[option.propertyName] ? (
+ onResetSecureField(option.propertyName)} variant="secondary" type="button">
+ Reset
+
+ }
+ />
+ ) : (
+
+ )}
);
})}
diff --git a/public/app/features/alerting/components/NotificationSettings.tsx b/public/app/features/alerting/components/NotificationSettings.tsx
new file mode 100644
index 00000000000..56529bc90b3
--- /dev/null
+++ b/public/app/features/alerting/components/NotificationSettings.tsx
@@ -0,0 +1,59 @@
+import React, { FC } from 'react';
+import { Checkbox, CollapsableSection, Field, InfoBox, Input } from '@grafana/ui';
+import { NotificationSettingsProps } from './NotificationChannelForm';
+
+interface Props extends NotificationSettingsProps {
+ imageRendererAvailable: boolean;
+}
+
+export const NotificationSettings: FC = ({ currentFormValues, imageRendererAvailable, register }) => {
+ return (
+
+
+
+
+
+
+
+ {currentFormValues.uploadImage && !imageRendererAvailable && (
+
+ Grafana cannot find an image renderer to capture an image for the notification. Please make sure the Grafana
+ Image Renderer plugin is installed. Please contact your Grafana administrator to install the plugin.
+
+ )}
+
+
+
+
+
+
+ {currentFormValues.sendReminder && (
+ <>
+
+
+
+ >
+ )}
+
+ );
+};
diff --git a/public/app/features/alerting/components/OptionElement.tsx b/public/app/features/alerting/components/OptionElement.tsx
index 4082770e35e..1f3f4ab3e73 100644
--- a/public/app/features/alerting/components/OptionElement.tsx
+++ b/public/app/features/alerting/components/OptionElement.tsx
@@ -1,17 +1,19 @@
import React, { FC } from 'react';
-import { FormAPI, Input, InputControl, Select, Switch, TextArea } from '@grafana/ui';
-import { Option } from '../../../types';
+import { FormAPI, Input, InputControl, Select, TextArea } from '@grafana/ui';
+import { NotificationChannelOption } from '../../../types';
interface Props extends Pick, 'register' | 'control'> {
- option: Option;
+ option: NotificationChannelOption;
+ invalid?: boolean;
}
-export const OptionElement: FC = ({ control, option, register }) => {
- const modelValue = `settings.${option.propertyName}`;
+export const OptionElement: FC = ({ control, option, register, invalid }) => {
+ const modelValue = option.secure ? `secureSettings.${option.propertyName}` : `settings.${option.propertyName}`;
switch (option.element) {
case 'input':
return (
= ({ control, option, register }) => {
);
case 'select':
- return ;
-
- case 'textarea':
return (
-