From 6465b2f0a39ff5548a6106ca448dbfea85c560e8 Mon Sep 17 00:00:00 2001 From: Peter Holmberg Date: Mon, 29 Jun 2020 13:39:12 +0200 Subject: [PATCH] Migration: Migrate New notification channel page (#25265) * creating page * add types select * adding switches * start with converting angular templates to json * converting more alert channels to new format * convert remaining channels * typing the form * add validation, update models * fix default value in type select * fix type * fix issue with validation rule * add missing settings * fix type errors * test notification * add comments to structs * fix selectable value and minor things on each channel * More typings * fix strictnull * rename ModelValue -> PropertyName * rename show -> showWhen * add enums and adding comments * fix comment * break out channel options to component * use try catch * adding default case to OptionElement if element not supported --- .../grafana-ui/src/components/Forms/Form.tsx | 4 +- packages/grafana-ui/src/types/forms.ts | 5 +- pkg/services/alerting/notifier.go | 63 ++++++- .../alerting/notifiers/alertmanager.go | 24 +++ pkg/services/alerting/notifiers/dingding.go | 25 +++ pkg/services/alerting/notifiers/discord.go | 18 ++ pkg/services/alerting/notifiers/email.go | 16 ++ pkg/services/alerting/notifiers/googlechat.go | 20 ++- pkg/services/alerting/notifiers/hipchat.go | 25 +++ pkg/services/alerting/notifiers/kafka.go | 19 ++ pkg/services/alerting/notifiers/line.go | 10 ++ pkg/services/alerting/notifiers/opsgenie.go | 30 ++++ pkg/services/alerting/notifiers/pagerduty.go | 40 +++++ pkg/services/alerting/notifiers/pushover.go | 165 +++++++++++++++++- pkg/services/alerting/notifiers/sensu.go | 37 ++++ pkg/services/alerting/notifiers/slack.go | 80 +++++++++ pkg/services/alerting/notifiers/teams.go | 12 +- pkg/services/alerting/notifiers/telegram.go | 19 ++ pkg/services/alerting/notifiers/threema.go | 35 +++- pkg/services/alerting/notifiers/victorops.go | 17 ++ pkg/services/alerting/notifiers/webhook.go | 37 ++++ .../alerting/NewAlertNotificationPage.tsx | 132 ++++++++++++++ .../components/NewNotificationChannelForm.tsx | 125 +++++++++++++ .../components/NotificationChannelOptions.tsx | 50 ++++++ .../alerting/components/OptionElement.tsx | 57 ++++++ public/app/features/alerting/state/actions.ts | 49 +++++- .../app/features/alerting/state/reducers.ts | 9 +- .../components/ImportDashboardForm.tsx | 2 +- public/app/routes/routes.ts | 9 + public/app/types/alerting.ts | 75 ++++++++ 30 files changed, 1186 insertions(+), 23 deletions(-) create mode 100644 public/app/features/alerting/NewAlertNotificationPage.tsx create mode 100644 public/app/features/alerting/components/NewNotificationChannelForm.tsx create mode 100644 public/app/features/alerting/components/NotificationChannelOptions.tsx create mode 100644 public/app/features/alerting/components/OptionElement.tsx diff --git a/packages/grafana-ui/src/components/Forms/Form.tsx b/packages/grafana-ui/src/components/Forms/Form.tsx index 0e31eec5e08..2425029358e 100644 --- a/packages/grafana-ui/src/components/Forms/Form.tsx +++ b/packages/grafana-ui/src/components/Forms/Form.tsx @@ -23,7 +23,7 @@ export function Form({ validateOn = 'onSubmit', maxWidth = 400, }: FormProps) { - const { handleSubmit, register, errors, control, triggerValidation, getValues, formState } = useForm({ + const { handleSubmit, register, errors, control, triggerValidation, getValues, formState, watch } = useForm({ mode: validateOn, defaultValues, }); @@ -42,7 +42,7 @@ export function Form({ `} onSubmit={handleSubmit(onSubmit)} > - {children({ register, errors, control, getValues, formState })} + {children({ register, errors, control, getValues, formState, watch })} ); } diff --git a/packages/grafana-ui/src/types/forms.ts b/packages/grafana-ui/src/types/forms.ts index edf931127a8..9442095440f 100644 --- a/packages/grafana-ui/src/types/forms.ts +++ b/packages/grafana-ui/src/types/forms.ts @@ -1,4 +1,7 @@ import { FormContextValues } from 'react-hook-form'; export { OnSubmit as FormsOnSubmit, FieldErrors as FormFieldErrors } from 'react-hook-form'; -export type FormAPI = Pick, 'register' | 'errors' | 'control' | 'formState' | 'getValues'>; +export type FormAPI = Pick< + FormContextValues, + 'register' | 'errors' | 'control' | 'formState' | 'getValues' | 'watch' +>; diff --git a/pkg/services/alerting/notifier.go b/pkg/services/alerting/notifier.go index 951b3d2aa3d..5f8ea9998d0 100644 --- a/pkg/services/alerting/notifier.go +++ b/pkg/services/alerting/notifier.go @@ -22,11 +22,64 @@ var newImageUploaderProvider = func() (imguploader.ImageUploader, error) { // NotifierPlugin holds meta information about a notifier. type NotifierPlugin struct { - Type string `json:"type"` - Name string `json:"name"` - Description string `json:"description"` - OptionsTemplate string `json:"optionsTemplate"` - Factory NotifierFactory `json:"-"` + 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"` +} + +// NotifierOption holds information about options specific for the NotifierPlugin. +type NotifierOption struct { + Element ElementType `json:"element"` + InputType InputType `json:"inputType"` + Label string `json:"label"` + Description string `json:"description"` + Placeholder string `json:"placeholder"` + PropertyName string `json:"propertyName"` + SelectOptions []SelectOption `json:"selectOptions"` + ShowWhen ShowWhen `json:"showWhen"` + Required bool `json:"required"` + ValidationRule string `json:"validationRule"` +} + +// InputType is the type of input that can be rendered in the frontend. +type InputType string + +const ( + // InputTypeText will render a text field in the frontend + InputTypeText = "text" + // InputTypePassword will render a text field in the frontend + InputTypePassword = "password" +) + +// ElementType is the type of element that can be rendered in the frontend. +type ElementType string + +const ( + // ElementTypeInput will render an input + ElementTypeInput = "input" + // ElementTypeSelect will render a select + ElementTypeSelect = "select" + // ElementTypeSwitch will render a switch + ElementTypeSwitch = "switch" + // ElementTypeTextArea will render a textarea + ElementTypeTextArea = "textarea" +) + +// SelectOption is a simple type for Options that have dropdown options. Should be used when Element is ElementTypeSelect. +type SelectOption struct { + Value string `json:"value"` + Label string `json:"label"` +} + +// ShowWhen holds information about when options are dependant on other options. +type ShowWhen struct { + Field string `json:"field"` + Is string `json:"is"` } func newNotificationService(renderService rendering.Service) *notificationService { diff --git a/pkg/services/alerting/notifiers/alertmanager.go b/pkg/services/alerting/notifiers/alertmanager.go index 0b7ea2efca1..8bedce02deb 100644 --- a/pkg/services/alerting/notifiers/alertmanager.go +++ b/pkg/services/alerting/notifiers/alertmanager.go @@ -18,6 +18,7 @@ func init() { Type: "prometheus-alertmanager", Name: "Prometheus Alertmanager", Description: "Sends alert to Prometheus Alertmanager", + Heading: "Alertmanager settings", Factory: NewAlertmanagerNotifier, OptionsTemplate: `

Alertmanager settings

@@ -38,6 +39,29 @@ func init() { `, + Options: []alerting.NotifierOption{ + { + Label: "Url", + Element: alerting.ElementTypeInput, + InputType: alerting.InputTypeText, + Description: "As specified in Alertmanager documentation, do not specify a load balancer here. Enter all your Alertmanager URLs comma-separated.", + Placeholder: "http://localhost:9093", + PropertyName: "url", + Required: true, + }, + { + Label: "Basic Auth User", + Element: alerting.ElementTypeInput, + InputType: alerting.InputTypeText, + PropertyName: "basicAuthUser", + }, + { + Label: "Basic Auth Password", + Element: alerting.ElementTypeInput, + InputType: alerting.InputTypePassword, + PropertyName: "basicAuthPassword", + }, + }, }) } diff --git a/pkg/services/alerting/notifiers/dingding.go b/pkg/services/alerting/notifiers/dingding.go index 3dc1286f7e7..740399168a7 100644 --- a/pkg/services/alerting/notifiers/dingding.go +++ b/pkg/services/alerting/notifiers/dingding.go @@ -29,8 +29,33 @@ func init() { Type: "dingding", Name: "DingDing", Description: "Sends HTTP POST request to DingDing", + Heading: "DingDing settings", Factory: newDingDingNotifier, OptionsTemplate: dingdingOptionsTemplate, + Options: []alerting.NotifierOption{ + { + Label: "Url", + Element: alerting.ElementTypeInput, + InputType: alerting.InputTypeText, + Placeholder: "https://oapi.dingtalk.com/robot/send?access_token=xxxxxxxxx", + PropertyName: "url", + Required: true, + }, + { + Label: "Message Type", + Element: alerting.ElementTypeSelect, + PropertyName: "msgType", + SelectOptions: []alerting.SelectOption{ + { + Value: "link", + Label: "Link"}, + { + Value: "actionCard", + Label: "ActionCard", + }, + }, + }, + }, }) } diff --git a/pkg/services/alerting/notifiers/discord.go b/pkg/services/alerting/notifiers/discord.go index 6506da906a6..54432940db1 100644 --- a/pkg/services/alerting/notifiers/discord.go +++ b/pkg/services/alerting/notifiers/discord.go @@ -22,6 +22,7 @@ func init() { Name: "Discord", Description: "Sends notifications to Discord", Factory: newDiscordNotifier, + Heading: "Discord settings", OptionsTemplate: `

Discord settings

@@ -40,6 +41,23 @@ func init() {
`, + Options: []alerting.NotifierOption{ + { + Label: "Message Content", + Description: "Mention a group using @ or a user using <@ID> when notifying in a channel", + Element: alerting.ElementTypeInput, + InputType: alerting.InputTypeText, + PropertyName: "content", + }, + { + Label: "Webhook URL", + Element: alerting.ElementTypeInput, + InputType: alerting.InputTypeText, + Placeholder: "Discord webhook URL", + PropertyName: "url", + Required: true, + }, + }, }) } diff --git a/pkg/services/alerting/notifiers/email.go b/pkg/services/alerting/notifiers/email.go index 4d1c093dcb5..81b29770dd1 100644 --- a/pkg/services/alerting/notifiers/email.go +++ b/pkg/services/alerting/notifiers/email.go @@ -18,6 +18,7 @@ func init() { Name: "Email", Description: "Sends notifications using Grafana server configured SMTP settings", Factory: NewEmailNotifier, + Heading: "Email settings", OptionsTemplate: `

Email settings

@@ -39,6 +40,21 @@ func init() { 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, + PropertyName: "singleEmail", + }, + { + Label: "Addresses", + Description: "You can enter multiple email addresses using a \";\" separator", + Element: alerting.ElementTypeTextArea, + PropertyName: "addresses", + Required: true, + }, + }, }) } diff --git a/pkg/services/alerting/notifiers/googlechat.go b/pkg/services/alerting/notifiers/googlechat.go index c69a72fc5c5..ddb469bd253 100644 --- a/pkg/services/alerting/notifiers/googlechat.go +++ b/pkg/services/alerting/notifiers/googlechat.go @@ -14,11 +14,11 @@ import ( func init() { alerting.RegisterNotifier(&alerting.NotifierPlugin{ - Type: "googlechat", - Name: "Google Hangouts Chat", - Description: "Sends notifications to Google Hangouts Chat via webhooks based on the official JSON message " + - "format (https://developers.google.com/hangouts/chat/reference/message-formats/).", - Factory: newGoogleChatNotifier, + Type: "googlechat", + Name: "Google Hangouts Chat", + 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

@@ -26,6 +26,16 @@ func init() {
`, + Options: []alerting.NotifierOption{ + { + Label: "Url", + Element: alerting.ElementTypeInput, + InputType: alerting.InputTypeText, + Placeholder: "Google Hangouts Chat incoming webhook url", + PropertyName: "url", + Required: true, + }, + }, }) } diff --git a/pkg/services/alerting/notifiers/hipchat.go b/pkg/services/alerting/notifiers/hipchat.go index 14a06f546af..ad4160f59db 100644 --- a/pkg/services/alerting/notifiers/hipchat.go +++ b/pkg/services/alerting/notifiers/hipchat.go @@ -18,6 +18,7 @@ func init() { Type: "hipchat", Name: "HipChat", Description: "Sends notifications uto a HipChat Room", + Heading: "HipChat settings", Factory: NewHipChatNotifier, OptionsTemplate: `

HipChat settings

@@ -38,6 +39,30 @@ func init() { `, + Options: []alerting.NotifierOption{ + { + Label: "Hip Chat Url", + Element: alerting.ElementTypeInput, + InputType: alerting.InputTypeText, + Placeholder: "HipChat URL (ex https://grafana.hipchat.com)", + PropertyName: "url", + Required: true, + }, + { + Label: "API Key", + Element: alerting.ElementTypeInput, + InputType: alerting.InputTypeText, + Placeholder: "HipChat API Key", + PropertyName: "apiKey", + Required: true, + }, + { + Label: "Room ID", + Element: alerting.ElementTypeInput, + InputType: alerting.InputTypeText, + PropertyName: "roomid", + }, + }, }) } diff --git a/pkg/services/alerting/notifiers/kafka.go b/pkg/services/alerting/notifiers/kafka.go index 3baf737b2f9..7ddc8d40fcd 100644 --- a/pkg/services/alerting/notifiers/kafka.go +++ b/pkg/services/alerting/notifiers/kafka.go @@ -17,6 +17,7 @@ func init() { Type: "kafka", Name: "Kafka REST Proxy", Description: "Sends notifications to Kafka Rest Proxy", + Heading: "Kafka settings", Factory: NewKafkaNotifier, OptionsTemplate: `

Kafka settings

@@ -29,6 +30,24 @@ func init() { `, + Options: []alerting.NotifierOption{ + { + Label: "Kafka REST Proxy", + Element: alerting.ElementTypeInput, + InputType: alerting.InputTypeText, + Placeholder: "http://localhost:8082", + PropertyName: "kafkaRestProxy", + Required: true, + }, + { + Label: "Topic", + Element: alerting.ElementTypeInput, + InputType: alerting.InputTypeText, + Placeholder: "topic1", + PropertyName: "kafkaTopic", + Required: true, + }, + }, }) } diff --git a/pkg/services/alerting/notifiers/line.go b/pkg/services/alerting/notifiers/line.go index 63e7fa29019..79cc4e0a6c2 100644 --- a/pkg/services/alerting/notifiers/line.go +++ b/pkg/services/alerting/notifiers/line.go @@ -15,6 +15,7 @@ func init() { Type: "LINE", Name: "LINE", Description: "Send notifications to LINE notify", + Heading: "LINE notify settings", Factory: NewLINENotifier, OptionsTemplate: `
@@ -25,6 +26,15 @@ func init() {
`, + Options: []alerting.NotifierOption{ + { + Label: "Token", + Element: alerting.ElementTypeInput, + InputType: alerting.InputTypeText, + Placeholder: "LINE notify token key", + PropertyName: "token", + Required: true, + }}, }) } diff --git a/pkg/services/alerting/notifiers/opsgenie.go b/pkg/services/alerting/notifiers/opsgenie.go index 0a830f66dbb..a5f52a20b5f 100644 --- a/pkg/services/alerting/notifiers/opsgenie.go +++ b/pkg/services/alerting/notifiers/opsgenie.go @@ -16,6 +16,7 @@ func init() { Type: "opsgenie", Name: "OpsGenie", Description: "Sends notifications to OpsGenie", + Heading: "OpsGenie settings", Factory: NewOpsGenieNotifier, OptionsTemplate: `

OpsGenie settings

@@ -46,6 +47,35 @@ func init() { `, + Options: []alerting.NotifierOption{ + { + Label: "API Key", + Element: alerting.ElementTypeInput, + InputType: alerting.InputTypeText, + Placeholder: "OpsGenie API Key", + PropertyName: "apiKey", + Required: true, + }, + { + Label: "Alert API Url", + Element: alerting.ElementTypeInput, + InputType: alerting.InputTypeText, + Placeholder: "https://api.opsgenie.com/v2/alerts", + PropertyName: "apiUrl", + Required: true, + }, + { + Label: "Auto close incidents", + Element: alerting.ElementTypeSwitch, + Description: "Automatically close alerts in OpsGenie once the alert goes back to ok.", + PropertyName: "autoClose", + }, { + Label: "Override priority", + Element: alerting.ElementTypeSwitch, + 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 fc919ef5b47..ee9dd6cfd8c 100644 --- a/pkg/services/alerting/notifiers/pagerduty.go +++ b/pkg/services/alerting/notifiers/pagerduty.go @@ -18,6 +18,7 @@ func init() { Type: "pagerduty", Name: "PagerDuty", Description: "Sends notifications to PagerDuty", + Heading: "PagerDuty settings", Factory: NewPagerdutyNotifier, OptionsTemplate: `

PagerDuty settings

@@ -54,6 +55,45 @@ func init() { `, + Options: []alerting.NotifierOption{ + { + Label: "Integration Key", + Element: alerting.ElementTypeInput, + InputType: alerting.InputTypeText, + Placeholder: "Pagerduty Integration Key", + PropertyName: "integrationKey", + Required: true, + }, + { + Label: "Severity", + Element: alerting.ElementTypeSelect, + SelectOptions: []alerting.SelectOption{ + { + Value: "critical", + Label: "Critical", + }, + { + Value: "error", + Label: "Error", + }, + { + Value: "warning", + Label: "Warning", + }, + { + Value: "info", + Label: "Info", + }, + }, + PropertyName: "severity", + }, + { + Label: "Auto resolve incidents", + Element: alerting.ElementTypeSwitch, + Description: "Resolve incidents in pagerduty once the alert goes back to ok.", + PropertyName: "autoResolve", + }, + }, }) } diff --git a/pkg/services/alerting/notifiers/pushover.go b/pkg/services/alerting/notifiers/pushover.go index 5da1a457e67..400c8989834 100644 --- a/pkg/services/alerting/notifiers/pushover.go +++ b/pkg/services/alerting/notifiers/pushover.go @@ -17,7 +17,7 @@ import ( const pushoverEndpoint = "https://api.pushover.net/1/messages.json" func init() { - sounds := ` + sounds := ` 'default', 'pushover', 'bike', @@ -42,10 +42,85 @@ func init() { 'updown', 'none'` + soundOptions := []alerting.SelectOption{ + { + Value: "default", + Label: "Default", + }, + { + Value: "pushover", + Label: "Pushover", + }, { + Value: "bike", + Label: "Bike", + }, { + Value: "bugle", + Label: "Bugle", + }, { + Value: "cashregister", + Label: "Cashregister", + }, { + Value: "classical", + Label: "Classical", + }, { + Value: "cosmic", + Label: "Cosmic", + }, { + Value: "falling", + Label: "Falling", + }, { + Value: "gamelan", + Label: "Gamelan", + }, { + Value: "incoming", + Label: "Incoming", + }, { + Value: "intermission", + Label: "Intermission", + }, { + Value: "magic", + Label: "Magic", + }, { + Value: "mechanical", + Label: "Mechanical", + }, { + Value: "pianobar", + Label: "Pianobar", + }, { + Value: "siren", + Label: "Siren", + }, { + Value: "spacealarm", + Label: "Spacealarm", + }, { + Value: "tugboat", + Label: "Tugboat", + }, { + Value: "alien", + Label: "Alien", + }, { + Value: "climb", + Label: "Climb", + }, { + Value: "persistent", + Label: "Persistent", + }, { + Value: "echo", + Label: "Echo", + }, { + Value: "updown", + Label: "Updown", + }, { + Value: "none", + Label: "None", + }, + } + alerting.RegisterNotifier(&alerting.NotifierPlugin{ Type: "pushover", Name: "Pushover", Description: "Sends HTTP POST request to the Pushover API", + Heading: "Pushover settings", Factory: NewPushoverNotifier, OptionsTemplate: `

Pushover settings

@@ -77,7 +152,7 @@ func init() {
Expire - +
Alerting sound @@ -92,6 +167,92 @@ func init() { ]" ng-init="ctrl.model.settings.okSound=ctrl.model.settings.okSound||'default'">
`, + Options: []alerting.NotifierOption{ + { + Label: "API Token", + Element: alerting.ElementTypeInput, + InputType: alerting.InputTypeText, + Placeholder: "Application token", + PropertyName: "apiToken", + Required: true, + }, + { + Label: "User key(s)", + Element: alerting.ElementTypeInput, + InputType: alerting.InputTypeText, + Placeholder: "comma-separated list", + PropertyName: "userKey", + Required: true, + }, + { + Label: "Device(s) (optional)", + Element: alerting.ElementTypeInput, + InputType: alerting.InputTypeText, + Placeholder: "comma-separated list; leave empty to send to all devices", + PropertyName: "device", + }, + { + Label: "Priority", + Element: alerting.ElementTypeSelect, + SelectOptions: []alerting.SelectOption{ + { + Value: "2", + Label: "Emergency", + }, + { + Value: "1", + Label: "High", + }, + { + Value: "0", + Label: "Normal", + }, + { + Value: "-1", + Label: "Low", + }, + { + Value: "-2", + Label: "Lowest", + }, + }, + PropertyName: "priority", + }, + { + Label: "Retry", + Element: alerting.ElementTypeInput, + InputType: alerting.InputTypeText, + Placeholder: "minimum 30 seconds", + PropertyName: "retry", + ShowWhen: alerting.ShowWhen{ + Field: "priority", + Is: "2", + }, + }, + { + Label: "Expire", + Element: alerting.ElementTypeInput, + InputType: alerting.InputTypeText, + Placeholder: "maximum 86400 seconds", + PropertyName: "expire", + ShowWhen: alerting.ShowWhen{ + Field: "priority", + Is: "2", + }, + }, + { + Label: "Alerting sound", + Element: alerting.ElementTypeSelect, + SelectOptions: soundOptions, + PropertyName: "sound", + }, + { + Label: "OK sound", + Element: alerting.ElementTypeSelect, + SelectOptions: soundOptions, + PropertyName: "okSound", + }, + }, }) } diff --git a/pkg/services/alerting/notifiers/sensu.go b/pkg/services/alerting/notifiers/sensu.go index 231981fb695..170ed508b05 100644 --- a/pkg/services/alerting/notifiers/sensu.go +++ b/pkg/services/alerting/notifiers/sensu.go @@ -16,6 +16,7 @@ func init() { Type: "sensu", Name: "Sensu", Description: "Sends HTTP POST request to a Sensu API", + Heading: "Sensu settings", Factory: NewSensuNotifier, OptionsTemplate: `

Sensu settings

@@ -40,6 +41,42 @@ func init() { `, + Options: []alerting.NotifierOption{ + { + Label: "Url", + Element: alerting.ElementTypeInput, + InputType: alerting.InputTypeText, + Placeholder: "http://sensu-api.local:4567/results", + PropertyName: "url", + Required: true, + }, + { + Label: "Source", + Element: alerting.ElementTypeInput, + InputType: alerting.InputTypeText, + Description: "If empty rule id will be used", + PropertyName: "source", + }, + { + Label: "Handler", + Element: alerting.ElementTypeInput, + InputType: alerting.InputTypeText, + Placeholder: "default", + PropertyName: "handler", + }, + { + Label: "Username", + Element: alerting.ElementTypeInput, + InputType: alerting.InputTypeText, + PropertyName: "username", + }, + { + Label: "Password", + Element: alerting.ElementTypeInput, + InputType: alerting.InputTypePassword, + PropertyName: "passsword ", + }, + }, }) } diff --git a/pkg/services/alerting/notifiers/slack.go b/pkg/services/alerting/notifiers/slack.go index 53a88093ef8..9df745be704 100644 --- a/pkg/services/alerting/notifiers/slack.go +++ b/pkg/services/alerting/notifiers/slack.go @@ -25,6 +25,7 @@ func init() { Type: "slack", Name: "Slack", Description: "Sends notifications to Slack via Slack Webhooks", + Heading: "Slack settings", Factory: NewSlackNotifier, OptionsTemplate: `

Slack settings

@@ -124,6 +125,85 @@ func init() { `, + Options: []alerting.NotifierOption{ + { + Label: "Url", + Element: alerting.ElementTypeInput, + InputType: alerting.InputTypeText, + Placeholder: "Slack incoming webhook url", + PropertyName: "url", + Required: true, + }, + { + Label: "Recipient", + Element: alerting.ElementTypeInput, + InputType: alerting.InputTypeText, + Description: "Override default channel or user, use #channel-name, @username (has to be all lowercase, no whitespace), or user/channel Slack ID", + PropertyName: "recipient", + }, + { + Label: "Username", + Element: alerting.ElementTypeInput, + InputType: alerting.InputTypeText, + Description: "Set the username for the bot's message", + PropertyName: "username", + }, + { + Label: "Icon emoji", + 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", + }, + { + 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", + }, + { + Label: "Mention Users", + Element: alerting.ElementTypeInput, + InputType: alerting.InputTypeText, + Description: "Mention one or more users (comma separated) when notifying in a channel, by ID (you can copy this from the user's Slack profile)", + PropertyName: "mentionUsers", + }, + { + Label: "Mention Groups", + Element: alerting.ElementTypeInput, + InputType: alerting.InputTypeText, + Description: "Mention one or more groups (comma separated) when notifying in a channel (you can copy this from the group's Slack profile URL)", + PropertyName: "mentionGroups", + }, + { + Label: "Mention Channel", + Element: alerting.ElementTypeSelect, + SelectOptions: []alerting.SelectOption{ + { + Value: "", + Label: "Disabled", + }, + { + Value: "here", + Label: "Every active channel member", + }, + { + Value: "channel", + Label: "Every channel member", + }, + }, + Description: "Mention whole channel or just active members when notifying", + PropertyName: "mentionChannel", + }, + { + Label: "Token", + Element: alerting.ElementTypeInput, + 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", + }, + }, }) } diff --git a/pkg/services/alerting/notifiers/teams.go b/pkg/services/alerting/notifiers/teams.go index f51d5de6ae3..3742c855141 100644 --- a/pkg/services/alerting/notifiers/teams.go +++ b/pkg/services/alerting/notifiers/teams.go @@ -14,14 +14,24 @@ func init() { Type: "teams", Name: "Microsoft Teams", Description: "Sends notifications using Incoming Webhook connector to Microsoft Teams", + Heading: "Teams settings", Factory: NewTeamsNotifier, OptionsTemplate: `

Teams settings

Url - +
`, + Options: []alerting.NotifierOption{ + { + Label: "URL", + Element: alerting.ElementTypeInput, + InputType: alerting.InputTypeText, + Placeholder: "Teams incoming webhook url", + PropertyName: "url", + }, + }, }) } diff --git a/pkg/services/alerting/notifiers/telegram.go b/pkg/services/alerting/notifiers/telegram.go index 207b88594fd..fe665889364 100644 --- a/pkg/services/alerting/notifiers/telegram.go +++ b/pkg/services/alerting/notifiers/telegram.go @@ -26,6 +26,7 @@ func init() { Type: "telegram", Name: "Telegram", Description: "Sends notifications to Telegram", + Heading: "Telegram API settings", Factory: NewTelegramNotifier, OptionsTemplate: `

Telegram API settings

@@ -48,6 +49,24 @@ func init() { `, + Options: []alerting.NotifierOption{ + { + Label: "BOT API Token", + Element: alerting.ElementTypeInput, + InputType: alerting.InputTypeText, + Placeholder: "Telegram BOT API Token", + PropertyName: "bottoken", + Required: true, + }, + { + Label: "Chat ID", + Element: alerting.ElementTypeInput, + InputType: alerting.InputTypeText, + Description: "Integer Telegram Chat Identifier", + PropertyName: "chatid", + Required: true, + }, + }, }) } diff --git a/pkg/services/alerting/notifiers/threema.go b/pkg/services/alerting/notifiers/threema.go index 12e722d68d8..7cc20f7e722 100644 --- a/pkg/services/alerting/notifiers/threema.go +++ b/pkg/services/alerting/notifiers/threema.go @@ -20,7 +20,10 @@ func init() { Type: "threema", Name: "Threema Gateway", Description: "Sends notifications to Threema using the Threema Gateway", - Factory: NewThreemaNotifier, + Heading: "Threema Gateway settings", + 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

@@ -64,6 +67,36 @@ func init() { `, + Options: []alerting.NotifierOption{ + { + Label: "Gateway ID", + Element: alerting.ElementTypeInput, + InputType: alerting.InputTypeText, + Placeholder: "*3MAGWID", + Description: "Your 8 character Threema Gateway ID (starting with a *).", + PropertyName: "gateway_id", + Required: true, + ValidationRule: "\\*[0-9A-Z]{7}", + }, + { + Label: "Recipient ID", + Element: alerting.ElementTypeInput, + InputType: alerting.InputTypeText, + Placeholder: "YOUR3MID", + Description: "The 8 character Threema ID that should receive the alerts.", + PropertyName: "recipient_id", + Required: true, + ValidationRule: "[0-9A-Z]{8}", + }, + { + Label: "API Secret", + Element: alerting.ElementTypeInput, + InputType: alerting.InputTypeText, + Description: "Your Threema Gateway API secret.", + PropertyName: "api_secret", + Required: true, + }, + }, }) } diff --git a/pkg/services/alerting/notifiers/victorops.go b/pkg/services/alerting/notifiers/victorops.go index 07621176c82..e0eb0092291 100644 --- a/pkg/services/alerting/notifiers/victorops.go +++ b/pkg/services/alerting/notifiers/victorops.go @@ -23,6 +23,7 @@ func init() { Type: "victorops", Name: "VictorOps", Description: "Sends notifications to VictorOps", + Heading: "VictorOps settings", Factory: NewVictoropsNotifier, OptionsTemplate: `

VictorOps settings

@@ -47,6 +48,22 @@ func init() { `, + Options: []alerting.NotifierOption{ + { + Label: "Url", + Element: alerting.ElementTypeInput, + InputType: alerting.InputTypeText, + Placeholder: "VictorOps url", + PropertyName: "url", + Required: true, + }, + { + Label: "Auto resolve incidents", + Description: "Resolve incidents in VictorOps once the alert goes back to ok.", + Element: alerting.ElementTypeSwitch, + PropertyName: "autoResolve", + }, + }, }) } diff --git a/pkg/services/alerting/notifiers/webhook.go b/pkg/services/alerting/notifiers/webhook.go index 8c9b4566aa1..916272a380c 100644 --- a/pkg/services/alerting/notifiers/webhook.go +++ b/pkg/services/alerting/notifiers/webhook.go @@ -13,6 +13,7 @@ func init() { Type: "webhook", Name: "webhook", Description: "Sends HTTP POST request to a URL", + Heading: "Webhook settings", Factory: NewWebHookNotifier, OptionsTemplate: `

Webhook settings

@@ -36,6 +37,42 @@ func init() { `, + Options: []alerting.NotifierOption{ + { + Label: "Url", + Element: alerting.ElementTypeInput, + InputType: alerting.InputTypeText, + PropertyName: "url", + Required: true, + }, + { + Label: "Http Method", + Element: alerting.ElementTypeSelect, + SelectOptions: []alerting.SelectOption{ + { + Value: "POST", + Label: "POST", + }, + { + Value: "PUT", + Label: "PUT", + }, + }, + PropertyName: "httpMethod", + }, + { + Label: "Username", + Element: alerting.ElementTypeInput, + InputType: alerting.InputTypeText, + PropertyName: "username", + }, + { + Label: "Password", + Element: alerting.ElementTypeInput, + InputType: alerting.InputTypePassword, + PropertyName: "password", + }, + }, }) } diff --git a/public/app/features/alerting/NewAlertNotificationPage.tsx b/public/app/features/alerting/NewAlertNotificationPage.tsx new file mode 100644 index 00000000000..527533cdf44 --- /dev/null +++ b/public/app/features/alerting/NewAlertNotificationPage.tsx @@ -0,0 +1,132 @@ +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

+
+ {({ register, errors, control, getValues, watch }) => { + const selectedChannel = notificationChannels.find(c => c.value === getValues().type.value); + + return ( + + ); + }} + +
+
+ ); + } +} + +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/components/NewNotificationChannelForm.tsx b/public/app/features/alerting/components/NewNotificationChannelForm.tsx new file mode 100644 index 00000000000..e0a7670c9e6 --- /dev/null +++ b/public/app/features/alerting/components/NewNotificationChannelForm.tsx @@ -0,0 +1,125 @@ +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 && ( + + )} + + + + + + + ); +}; + +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 new file mode 100644 index 00000000000..9fa7968a017 --- /dev/null +++ b/public/app/features/alerting/components/NotificationChannelOptions.tsx @@ -0,0 +1,50 @@ +import React, { FC } from 'react'; +import { SelectableValue } from '@grafana/data'; +import { Field, FormAPI, InfoBox } from '@grafana/ui'; +import { OptionElement } from './OptionElement'; +import { NotificationChannel, NotificationChannelDTO, Option } from '../../../types'; + +interface Props extends Omit, 'formState' | 'getValues' | 'watch'> { + selectedChannel: NotificationChannel; + currentFormValues: NotificationChannelDTO; +} + +export const NotificationChannelOptions: FC = ({ + control, + currentFormValues, + errors, + selectedChannel, + register, +}) => { + return ( + <> +

{selectedChannel.heading}

+ {selectedChannel.info !== '' && {selectedChannel.info}} + {selectedChannel.options.map((option: Option, 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 = + currentFormValues[`settings.${option.showWhen.field}`] && + (currentFormValues[`settings.${option.showWhen.field}`] as SelectableValue).value; + + if (option.showWhen.field && selectedOptionValue !== option.showWhen.is) { + return null; + } + + return ( + + + + ); + })} + + ); +}; diff --git a/public/app/features/alerting/components/OptionElement.tsx b/public/app/features/alerting/components/OptionElement.tsx new file mode 100644 index 00000000000..4082770e35e --- /dev/null +++ b/public/app/features/alerting/components/OptionElement.tsx @@ -0,0 +1,57 @@ +import React, { FC } from 'react'; +import { FormAPI, Input, InputControl, Select, Switch, TextArea } from '@grafana/ui'; +import { Option } from '../../../types'; + +interface Props extends Pick, 'register' | 'control'> { + option: Option; +} + +export const OptionElement: FC = ({ control, option, register }) => { + const modelValue = `settings.${option.propertyName}`; + switch (option.element) { + case 'input': + return ( + (option.validationRule !== '' ? validateOption(v, option.validationRule) : true), + })} + placeholder={option.placeholder} + /> + ); + + case 'select': + return ; + + case 'textarea': + return ( +