mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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
This commit is contained in:
parent
61a7f6e2f3
commit
6465b2f0a3
@ -23,7 +23,7 @@ export function Form<T>({
|
||||
validateOn = 'onSubmit',
|
||||
maxWidth = 400,
|
||||
}: FormProps<T>) {
|
||||
const { handleSubmit, register, errors, control, triggerValidation, getValues, formState } = useForm<T>({
|
||||
const { handleSubmit, register, errors, control, triggerValidation, getValues, formState, watch } = useForm<T>({
|
||||
mode: validateOn,
|
||||
defaultValues,
|
||||
});
|
||||
@ -42,7 +42,7 @@ export function Form<T>({
|
||||
`}
|
||||
onSubmit={handleSubmit(onSubmit)}
|
||||
>
|
||||
{children({ register, errors, control, getValues, formState })}
|
||||
{children({ register, errors, control, getValues, formState, watch })}
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
@ -1,4 +1,7 @@
|
||||
import { FormContextValues } from 'react-hook-form';
|
||||
export { OnSubmit as FormsOnSubmit, FieldErrors as FormFieldErrors } from 'react-hook-form';
|
||||
|
||||
export type FormAPI<T> = Pick<FormContextValues<T>, 'register' | 'errors' | 'control' | 'formState' | 'getValues'>;
|
||||
export type FormAPI<T> = Pick<
|
||||
FormContextValues<T>,
|
||||
'register' | 'errors' | 'control' | 'formState' | 'getValues' | 'watch'
|
||||
>;
|
||||
|
@ -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 {
|
||||
|
@ -18,6 +18,7 @@ func init() {
|
||||
Type: "prometheus-alertmanager",
|
||||
Name: "Prometheus Alertmanager",
|
||||
Description: "Sends alert to Prometheus Alertmanager",
|
||||
Heading: "Alertmanager settings",
|
||||
Factory: NewAlertmanagerNotifier,
|
||||
OptionsTemplate: `
|
||||
<h3 class="page-heading">Alertmanager settings</h3>
|
||||
@ -38,6 +39,29 @@ func init() {
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
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",
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -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",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
}
|
||||
|
@ -22,6 +22,7 @@ func init() {
|
||||
Name: "Discord",
|
||||
Description: "Sends notifications to Discord",
|
||||
Factory: newDiscordNotifier,
|
||||
Heading: "Discord settings",
|
||||
OptionsTemplate: `
|
||||
<h3 class="page-heading">Discord settings</h3>
|
||||
<div class="gf-form max-width-30">
|
||||
@ -40,6 +41,23 @@ func init() {
|
||||
<input type="text" required class="gf-form-input max-width-30" ng-model="ctrl.model.settings.url" placeholder="Discord webhook URL"></input>
|
||||
</div>
|
||||
`,
|
||||
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,
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -18,6 +18,7 @@ func init() {
|
||||
Name: "Email",
|
||||
Description: "Sends notifications using Grafana server configured SMTP settings",
|
||||
Factory: NewEmailNotifier,
|
||||
Heading: "Email settings",
|
||||
OptionsTemplate: `
|
||||
<h3 class="page-heading">Email settings</h3>
|
||||
<div class="gf-form">
|
||||
@ -39,6 +40,21 @@ func init() {
|
||||
<span>You can enter multiple email addresses using a ";" separator</span>
|
||||
</div>
|
||||
`,
|
||||
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,
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -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: `
|
||||
<h3 class="page-heading">Google Hangouts Chat settings</h3>
|
||||
<div class="gf-form max-width-30">
|
||||
@ -26,6 +26,16 @@ func init() {
|
||||
<input type="text" required class="gf-form-input max-width-30" ng-model="ctrl.model.settings.url" placeholder="Google Hangouts Chat incoming webhook url"></input>
|
||||
</div>
|
||||
`,
|
||||
Options: []alerting.NotifierOption{
|
||||
{
|
||||
Label: "Url",
|
||||
Element: alerting.ElementTypeInput,
|
||||
InputType: alerting.InputTypeText,
|
||||
Placeholder: "Google Hangouts Chat incoming webhook url",
|
||||
PropertyName: "url",
|
||||
Required: true,
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -18,6 +18,7 @@ func init() {
|
||||
Type: "hipchat",
|
||||
Name: "HipChat",
|
||||
Description: "Sends notifications uto a HipChat Room",
|
||||
Heading: "HipChat settings",
|
||||
Factory: NewHipChatNotifier,
|
||||
OptionsTemplate: `
|
||||
<h3 class="page-heading">HipChat settings</h3>
|
||||
@ -38,6 +39,30 @@ func init() {
|
||||
</input>
|
||||
</div>
|
||||
`,
|
||||
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",
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
}
|
||||
|
@ -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: `
|
||||
<h3 class="page-heading">Kafka settings</h3>
|
||||
@ -29,6 +30,24 @@ func init() {
|
||||
<input type="text" required class="gf-form-input max-width-22" ng-model="ctrl.model.settings.kafkaTopic" placeholder="topic1"></input>
|
||||
</div>
|
||||
`,
|
||||
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,
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -15,6 +15,7 @@ func init() {
|
||||
Type: "LINE",
|
||||
Name: "LINE",
|
||||
Description: "Send notifications to LINE notify",
|
||||
Heading: "LINE notify settings",
|
||||
Factory: NewLINENotifier,
|
||||
OptionsTemplate: `
|
||||
<div class="gf-form-group">
|
||||
@ -25,6 +26,15 @@ func init() {
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
Options: []alerting.NotifierOption{
|
||||
{
|
||||
Label: "Token",
|
||||
Element: alerting.ElementTypeInput,
|
||||
InputType: alerting.InputTypeText,
|
||||
Placeholder: "LINE notify token key",
|
||||
PropertyName: "token",
|
||||
Required: true,
|
||||
}},
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -16,6 +16,7 @@ func init() {
|
||||
Type: "opsgenie",
|
||||
Name: "OpsGenie",
|
||||
Description: "Sends notifications to OpsGenie",
|
||||
Heading: "OpsGenie settings",
|
||||
Factory: NewOpsGenieNotifier,
|
||||
OptionsTemplate: `
|
||||
<h3 class="page-heading">OpsGenie settings</h3>
|
||||
@ -46,6 +47,35 @@ func init() {
|
||||
</gf-form-switch>
|
||||
</div>
|
||||
`,
|
||||
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",
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -18,6 +18,7 @@ func init() {
|
||||
Type: "pagerduty",
|
||||
Name: "PagerDuty",
|
||||
Description: "Sends notifications to PagerDuty",
|
||||
Heading: "PagerDuty settings",
|
||||
Factory: NewPagerdutyNotifier,
|
||||
OptionsTemplate: `
|
||||
<h3 class="page-heading">PagerDuty settings</h3>
|
||||
@ -54,6 +55,45 @@ func init() {
|
||||
</gf-form-switch>
|
||||
</div>
|
||||
`,
|
||||
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",
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -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: `
|
||||
<h3 class="page-heading">Pushover settings</h3>
|
||||
@ -77,7 +152,7 @@ func init() {
|
||||
</div>
|
||||
<div class="gf-form" ng-show="ctrl.model.settings.priority == '2'">
|
||||
<span class="gf-form-label width-10">Expire</span>
|
||||
<input type="text" class="gf-form-input max-width-14" ng-required="ctrl.model.settings.priority == '2'" placeholder="maximum 86400 seconds" ng-model="ctrl.model.settings.expire" ng-init="ctrl.model.settings.expire=ctrl.model.settings.expire||'3600'"></input>
|
||||
<input type="text" class="gf-form-input max-width-14" ng-required="ctrl.model.settings.priority == '2'" placeholder="maximum 86400 seconds" ng-model="ctrl.model.settings.expire" ng-init="ctrl.model.settings.expire=ctrl.model.settings.expire||'3600'"></input>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-10">Alerting sound</span>
|
||||
@ -92,6 +167,92 @@ func init() {
|
||||
]" ng-init="ctrl.model.settings.okSound=ctrl.model.settings.okSound||'default'"></select>
|
||||
</div>
|
||||
`,
|
||||
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",
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -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: `
|
||||
<h3 class="page-heading">Sensu settings</h3>
|
||||
@ -40,6 +41,42 @@ func init() {
|
||||
<input type="text" class="gf-form-input max-width-14" ng-model="ctrl.model.settings.password"></input>
|
||||
</div>
|
||||
`,
|
||||
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 ",
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
}
|
||||
|
@ -25,6 +25,7 @@ func init() {
|
||||
Type: "slack",
|
||||
Name: "Slack",
|
||||
Description: "Sends notifications to Slack via Slack Webhooks",
|
||||
Heading: "Slack settings",
|
||||
Factory: NewSlackNotifier,
|
||||
OptionsTemplate: `
|
||||
<h3 class="page-heading">Slack settings</h3>
|
||||
@ -124,6 +125,85 @@ func init() {
|
||||
</info-popover>
|
||||
</div>
|
||||
`,
|
||||
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",
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -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: `
|
||||
<h3 class="page-heading">Teams settings</h3>
|
||||
<div class="gf-form max-width-30">
|
||||
<span class="gf-form-label width-6">Url</span>
|
||||
<input type="text" required class="gf-form-input max-width-30" ng-model="ctrl.model.settings.url" placeholder="Teams incoming webhook url"></input>
|
||||
<input type="text" InputType class="gf-form-input max-width-30" ng-model="ctrl.model.settings.url" placeholder="Teams incoming webhook url"></input>
|
||||
</div>
|
||||
`,
|
||||
Options: []alerting.NotifierOption{
|
||||
{
|
||||
Label: "URL",
|
||||
Element: alerting.ElementTypeInput,
|
||||
InputType: alerting.InputTypeText,
|
||||
Placeholder: "Teams incoming webhook url",
|
||||
PropertyName: "url",
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
}
|
||||
|
@ -26,6 +26,7 @@ func init() {
|
||||
Type: "telegram",
|
||||
Name: "Telegram",
|
||||
Description: "Sends notifications to Telegram",
|
||||
Heading: "Telegram API settings",
|
||||
Factory: NewTelegramNotifier,
|
||||
OptionsTemplate: `
|
||||
<h3 class="page-heading">Telegram API settings</h3>
|
||||
@ -48,6 +49,24 @@ func init() {
|
||||
</info-popover>
|
||||
</div>
|
||||
`,
|
||||
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,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
}
|
||||
|
@ -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: `
|
||||
<h3 class="page-heading">Threema Gateway settings</h3>
|
||||
<p>
|
||||
@ -64,6 +67,36 @@ func init() {
|
||||
</info-popover>
|
||||
</div>
|
||||
`,
|
||||
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,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
}
|
||||
|
@ -23,6 +23,7 @@ func init() {
|
||||
Type: "victorops",
|
||||
Name: "VictorOps",
|
||||
Description: "Sends notifications to VictorOps",
|
||||
Heading: "VictorOps settings",
|
||||
Factory: NewVictoropsNotifier,
|
||||
OptionsTemplate: `
|
||||
<h3 class="page-heading">VictorOps settings</h3>
|
||||
@ -47,6 +48,22 @@ func init() {
|
||||
</gf-form-switch>
|
||||
</div>
|
||||
`,
|
||||
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",
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -13,6 +13,7 @@ func init() {
|
||||
Type: "webhook",
|
||||
Name: "webhook",
|
||||
Description: "Sends HTTP POST request to a URL",
|
||||
Heading: "Webhook settings",
|
||||
Factory: NewWebHookNotifier,
|
||||
OptionsTemplate: `
|
||||
<h3 class="page-heading">Webhook settings</h3>
|
||||
@ -36,6 +37,42 @@ func init() {
|
||||
<input type="text" class="gf-form-input max-width-14" ng-model="ctrl.model.settings.password"></input>
|
||||
</div>
|
||||
`,
|
||||
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",
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
}
|
||||
|
132
public/app/features/alerting/NewAlertNotificationPage.tsx
Normal file
132
public/app/features/alerting/NewAlertNotificationPage.tsx
Normal file
@ -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<Props> {
|
||||
componentDidMount() {
|
||||
this.props.loadNotificationTypes();
|
||||
}
|
||||
|
||||
onSubmit = (data: NotificationChannelDTO) => {
|
||||
/*
|
||||
Some settings can be options in a select, in order to not save a SelectableValue<T>
|
||||
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<SelectableValue<string>> = notificationChannels.map(channel => ({
|
||||
value: channel.value,
|
||||
label: channel.label,
|
||||
description: channel.description,
|
||||
}));
|
||||
|
||||
return (
|
||||
<Page navModel={navModel}>
|
||||
<Page.Contents>
|
||||
<h2>New Notification Channel</h2>
|
||||
<Form onSubmit={this.onSubmit} validateOn="onChange" defaultValues={defaultValues}>
|
||||
{({ register, errors, control, getValues, watch }) => {
|
||||
const selectedChannel = notificationChannels.find(c => c.value === getValues().type.value);
|
||||
|
||||
return (
|
||||
<NewNotificationChannelForm
|
||||
selectableChannels={selectableChannels}
|
||||
selectedChannel={selectedChannel}
|
||||
onTestChannel={this.onTestChannel}
|
||||
register={register}
|
||||
errors={errors}
|
||||
getValues={getValues}
|
||||
control={control}
|
||||
watch={watch}
|
||||
imageRendererAvailable={config.rendererAvailable}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
</Form>
|
||||
</Page.Contents>
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const mapStateToProps: MapStateToProps<ConnectedProps, OwnProps, StoreState> = state => {
|
||||
return {
|
||||
navModel: getNavModel(state.navIndex, 'channels'),
|
||||
notificationChannels: state.alertRules.notificationChannels,
|
||||
};
|
||||
};
|
||||
|
||||
const mapDispatchToProps: MapDispatchToProps<DispatchProps, OwnProps> = {
|
||||
createNotificationChannel,
|
||||
loadNotificationTypes,
|
||||
testNotificationChannel,
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(NewAlertNotificationPage);
|
@ -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<FormAPI<NotificationChannelDTO>, 'formState'> {
|
||||
selectableChannels: Array<SelectableValue<string>>;
|
||||
selectedChannel?: NotificationChannel;
|
||||
imageRendererAvailable: boolean;
|
||||
|
||||
onTestChannel: (data: NotificationChannelDTO) => void;
|
||||
}
|
||||
|
||||
export const NewNotificationChannelForm: FC<Props> = ({
|
||||
control,
|
||||
errors,
|
||||
selectedChannel,
|
||||
selectableChannels,
|
||||
register,
|
||||
watch,
|
||||
getValues,
|
||||
imageRendererAvailable,
|
||||
onTestChannel,
|
||||
}) => {
|
||||
const styles = getStyles(useTheme());
|
||||
|
||||
useEffect(() => {
|
||||
watch(['type', 'settings.priority', 'sendReminder', 'uploadImage']);
|
||||
}, []);
|
||||
|
||||
const currentFormValues = getValues();
|
||||
return (
|
||||
<>
|
||||
<div className={styles.basicSettings}>
|
||||
<Field label="Name" invalid={!!errors.name} error={errors.name && errors.name.message}>
|
||||
<Input name="name" ref={register({ required: 'Name is required' })} />
|
||||
</Field>
|
||||
<Field label="Type">
|
||||
<InputControl
|
||||
name="type"
|
||||
as={Select}
|
||||
options={selectableChannels}
|
||||
control={control}
|
||||
rules={{ required: true }}
|
||||
/>
|
||||
</Field>
|
||||
<Field label="Default" description="Use this notification for all alerts">
|
||||
<Switch name="isDefault" ref={register} />
|
||||
</Field>
|
||||
<Field label="Include image" description="Captures an image and include it in the notification">
|
||||
<Switch name="settings.uploadImage" ref={register} />
|
||||
</Field>
|
||||
{currentFormValues.uploadImage && !imageRendererAvailable && (
|
||||
<InfoBox title="No image renderer available/installed">
|
||||
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.
|
||||
</InfoBox>
|
||||
)}
|
||||
<Field
|
||||
label="Disable Resolve Message"
|
||||
description="Disable the resolve message [OK] that is sent when alerting state returns to false"
|
||||
>
|
||||
<Switch name="disableResolveMessage" ref={register} />
|
||||
</Field>
|
||||
<Field label="Send reminders" description="Send additional notifications for triggered alerts">
|
||||
<Switch name="sendReminder" ref={register} />
|
||||
</Field>
|
||||
{currentFormValues.sendReminder && (
|
||||
<>
|
||||
<Field
|
||||
label="Send reminder every"
|
||||
description="Specify how often reminders should be sent, e.g. every 30s, 1m, 10m, 30m or 1h etc."
|
||||
>
|
||||
<Input name="frequency" ref={register} />
|
||||
</Field>
|
||||
<InfoBox>
|
||||
Alert reminders are sent after rules are evaluated. Therefore a reminder can never be sent more frequently
|
||||
than a configured alert rule evaluation interval.
|
||||
</InfoBox>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
{selectedChannel && (
|
||||
<NotificationChannelOptions
|
||||
selectedChannel={selectedChannel}
|
||||
currentFormValues={currentFormValues}
|
||||
register={register}
|
||||
errors={errors}
|
||||
control={control}
|
||||
/>
|
||||
)}
|
||||
<HorizontalGroup>
|
||||
<Button type="submit">Save</Button>
|
||||
<Button type="button" variant="secondary" onClick={() => onTestChannel(getValues({ nest: true }))}>
|
||||
Test
|
||||
</Button>
|
||||
<Button type="button" variant="secondary">
|
||||
Back
|
||||
</Button>
|
||||
</HorizontalGroup>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const getStyles = stylesFactory((theme: GrafanaTheme) => {
|
||||
return {
|
||||
basicSettings: css`
|
||||
margin-bottom: ${theme.spacing.xl};
|
||||
`,
|
||||
};
|
||||
});
|
@ -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<FormAPI<NotificationChannelDTO>, 'formState' | 'getValues' | 'watch'> {
|
||||
selectedChannel: NotificationChannel;
|
||||
currentFormValues: NotificationChannelDTO;
|
||||
}
|
||||
|
||||
export const NotificationChannelOptions: FC<Props> = ({
|
||||
control,
|
||||
currentFormValues,
|
||||
errors,
|
||||
selectedChannel,
|
||||
register,
|
||||
}) => {
|
||||
return (
|
||||
<>
|
||||
<h3>{selectedChannel.heading}</h3>
|
||||
{selectedChannel.info !== '' && <InfoBox>{selectedChannel.info}</InfoBox>}
|
||||
{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<string>).value;
|
||||
|
||||
if (option.showWhen.field && selectedOptionValue !== option.showWhen.is) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Field
|
||||
key={key}
|
||||
label={option.label}
|
||||
description={option.description}
|
||||
invalid={errors.settings && !!errors.settings[option.propertyName]}
|
||||
error={errors.settings && errors.settings[option.propertyName]?.message}
|
||||
>
|
||||
<OptionElement option={option} register={register} control={control} />
|
||||
</Field>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
};
|
57
public/app/features/alerting/components/OptionElement.tsx
Normal file
57
public/app/features/alerting/components/OptionElement.tsx
Normal file
@ -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<FormAPI<any>, 'register' | 'control'> {
|
||||
option: Option;
|
||||
}
|
||||
|
||||
export const OptionElement: FC<Props> = ({ control, option, register }) => {
|
||||
const modelValue = `settings.${option.propertyName}`;
|
||||
switch (option.element) {
|
||||
case 'input':
|
||||
return (
|
||||
<Input
|
||||
type={option.inputType}
|
||||
name={`${modelValue}`}
|
||||
ref={register({
|
||||
required: option.required ? 'Required' : false,
|
||||
validate: v => (option.validationRule !== '' ? validateOption(v, option.validationRule) : true),
|
||||
})}
|
||||
placeholder={option.placeholder}
|
||||
/>
|
||||
);
|
||||
|
||||
case 'select':
|
||||
return <InputControl as={Select} options={option.selectOptions} control={control} name={`${modelValue}`} />;
|
||||
|
||||
case 'textarea':
|
||||
return (
|
||||
<TextArea
|
||||
name={`${modelValue}`}
|
||||
ref={register({
|
||||
required: option.required ? 'Required' : false,
|
||||
validate: v => (option.validationRule !== '' ? validateOption(v, option.validationRule) : true),
|
||||
})}
|
||||
/>
|
||||
);
|
||||
|
||||
case 'switch':
|
||||
return (
|
||||
<Switch
|
||||
name={`${modelValue}`}
|
||||
ref={register({
|
||||
required: option.required ? 'Required' : false,
|
||||
})}
|
||||
/>
|
||||
);
|
||||
|
||||
default:
|
||||
console.error('Element not supported', option.element);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const validateOption = (value: string, validationRule: string) => {
|
||||
return RegExp(validationRule).test(value) ? true : 'Invalid format';
|
||||
};
|
@ -1,6 +1,9 @@
|
||||
import { AppEvents } from '@grafana/data';
|
||||
import { getBackendSrv } from '@grafana/runtime';
|
||||
import { AlertRuleDTO, ThunkResult } from 'app/types';
|
||||
import { loadAlertRules, loadedAlertRules } from './reducers';
|
||||
import { AlertRuleDTO, NotifierDTO, ThunkResult } from 'app/types';
|
||||
import { appEvents } from 'app/core/core';
|
||||
import { updateLocation } from 'app/core/actions';
|
||||
import { loadAlertRules, loadedAlertRules, setNotificationChannels } from './reducers';
|
||||
|
||||
export function getAlertRulesAsync(options: { state: string }): ThunkResult<void> {
|
||||
return async dispatch => {
|
||||
@ -17,3 +20,45 @@ export function togglePauseAlertRule(id: number, options: { paused: boolean }):
|
||||
dispatch(getAlertRulesAsync({ state: stateFilter.toString() }));
|
||||
};
|
||||
}
|
||||
|
||||
export function createNotificationChannel(data: any): ThunkResult<void> {
|
||||
return async dispatch => {
|
||||
try {
|
||||
await getBackendSrv().post(`/api/alert-notifications`, data);
|
||||
appEvents.emit(AppEvents.alertSuccess, ['Notification created']);
|
||||
dispatch(updateLocation({ path: 'alerting/notifications' }));
|
||||
} catch (error) {
|
||||
appEvents.emit(AppEvents.alertError, [error.data.error]);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function testNotificationChannel(data: any): ThunkResult<void> {
|
||||
return async () => {
|
||||
await getBackendSrv().post('/api/alert-notifications/test', data);
|
||||
};
|
||||
}
|
||||
|
||||
export function loadNotificationTypes(): ThunkResult<void> {
|
||||
return async dispatch => {
|
||||
const alertNotifiers: NotifierDTO[] = await getBackendSrv().get(`/api/alert-notifiers`);
|
||||
|
||||
const notificationTypes = alertNotifiers
|
||||
.map((option: NotifierDTO) => {
|
||||
return {
|
||||
value: option.type,
|
||||
label: option.name,
|
||||
...option,
|
||||
typeName: option.type,
|
||||
};
|
||||
})
|
||||
.sort((o1, o2) => {
|
||||
if (o1.name > o2.name) {
|
||||
return 1;
|
||||
}
|
||||
return -1;
|
||||
});
|
||||
|
||||
dispatch(setNotificationChannels(notificationTypes));
|
||||
};
|
||||
}
|
||||
|
@ -1,9 +1,9 @@
|
||||
import { AlertRule, AlertRuleDTO, AlertRulesState } from 'app/types';
|
||||
import { AlertRule, AlertRuleDTO, AlertRulesState, NotificationChannel } from 'app/types';
|
||||
import alertDef from './alertDef';
|
||||
import { dateTime } from '@grafana/data';
|
||||
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
|
||||
|
||||
export const initialState: AlertRulesState = { items: [], searchQuery: '', isLoading: false };
|
||||
export const initialState: AlertRulesState = { items: [], searchQuery: '', isLoading: false, notificationChannels: [] };
|
||||
|
||||
function convertToAlertRule(dto: AlertRuleDTO, state: string): AlertRule {
|
||||
const stateModel = alertDef.getStateDisplayModel(state);
|
||||
@ -47,10 +47,13 @@ const alertRulesSlice = createSlice({
|
||||
setSearchQuery: (state, action: PayloadAction<string>): AlertRulesState => {
|
||||
return { ...state, searchQuery: action.payload };
|
||||
},
|
||||
setNotificationChannels: (state, action: PayloadAction<NotificationChannel[]>): AlertRulesState => {
|
||||
return { ...state, notificationChannels: action.payload };
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const { loadAlertRules, loadedAlertRules, setSearchQuery } = alertRulesSlice.actions;
|
||||
export const { loadAlertRules, loadedAlertRules, setSearchQuery, setNotificationChannels } = alertRulesSlice.actions;
|
||||
|
||||
export const alertRulesReducer = alertRulesSlice.reducer;
|
||||
|
||||
|
@ -15,7 +15,7 @@ import DataSourcePicker from 'app/core/components/Select/DataSourcePicker';
|
||||
import { DashboardInput, DashboardInputs, DataSourceInput, ImportDashboardDTO } from '../state/reducers';
|
||||
import { validateTitle, validateUid } from '../utils/validation';
|
||||
|
||||
interface Props extends Omit<FormAPI<ImportDashboardDTO>, 'formState'> {
|
||||
interface Props extends Omit<FormAPI<ImportDashboardDTO>, 'formState' | 'watch'> {
|
||||
uidReset: boolean;
|
||||
inputs: DashboardInputs;
|
||||
initialFolderId: number;
|
||||
|
@ -522,6 +522,15 @@ export function setupAngularRoutes($routeProvider: route.IRouteProvider, $locati
|
||||
controllerAs: 'ctrl',
|
||||
reloadOnSearch: false,
|
||||
})
|
||||
.when('/alerting/notification/new2', {
|
||||
template: '<react-container />',
|
||||
resolve: {
|
||||
component: () =>
|
||||
SafeDynamicImport(
|
||||
import(/* webpackChunkName: "NewNotificationChannel" */ 'app/features/alerting/NewAlertNotificationPage')
|
||||
),
|
||||
},
|
||||
})
|
||||
.when('/alerting/notification/:id/edit', {
|
||||
templateUrl: 'public/app/features/alerting/partials/notification_edit.html',
|
||||
controller: 'AlertNotificationEditCtrl',
|
||||
|
@ -1,3 +1,5 @@
|
||||
import { SelectableValue } from '@grafana/data';
|
||||
|
||||
export interface AlertRuleDTO {
|
||||
id: number;
|
||||
dashboardId: number;
|
||||
@ -33,10 +35,83 @@ export interface AlertRule {
|
||||
evalData?: { noData?: boolean; evalMatches?: any };
|
||||
}
|
||||
|
||||
export type NotifierType =
|
||||
| 'discord'
|
||||
| 'hipchat'
|
||||
| 'email'
|
||||
| 'sensu'
|
||||
| 'googlechat'
|
||||
| 'threema'
|
||||
| 'teams'
|
||||
| 'slack'
|
||||
| 'pagerduty'
|
||||
| 'prometheus-alertmanager'
|
||||
| 'telegram'
|
||||
| 'opsgenie'
|
||||
| 'dingding'
|
||||
| 'webhook'
|
||||
| 'victorops'
|
||||
| 'pushover'
|
||||
| 'LINE'
|
||||
| 'kafka';
|
||||
|
||||
export interface NotifierDTO {
|
||||
name: string;
|
||||
description: string;
|
||||
optionsTemplate: string;
|
||||
type: NotifierType;
|
||||
heading: string;
|
||||
options: Option[];
|
||||
info?: string;
|
||||
}
|
||||
|
||||
export interface NotificationChannel {
|
||||
value: string;
|
||||
label: string;
|
||||
description: string;
|
||||
type: NotifierType;
|
||||
heading: string;
|
||||
options: Option[];
|
||||
info?: string;
|
||||
}
|
||||
|
||||
export interface NotificationChannelDTO {
|
||||
[key: string]: string | boolean | SelectableValue<string>;
|
||||
name: string;
|
||||
type: SelectableValue<string>;
|
||||
sendReminder: boolean;
|
||||
disableResolveMessage: boolean;
|
||||
frequency: string;
|
||||
settings: ChannelTypeSettings;
|
||||
isDefault: boolean;
|
||||
}
|
||||
|
||||
export interface ChannelTypeSettings {
|
||||
[key: string]: any;
|
||||
autoResolve: true;
|
||||
httpMethod: string;
|
||||
severity: string;
|
||||
uploadImage: boolean;
|
||||
}
|
||||
|
||||
export interface Option {
|
||||
element: 'input' | 'select' | 'switch' | 'textarea';
|
||||
inputType: string;
|
||||
label: string;
|
||||
description: string;
|
||||
placeholder: string;
|
||||
propertyName: string;
|
||||
selectOptions: Array<SelectableValue<string>>;
|
||||
showWhen: { field: string; is: string };
|
||||
required: boolean;
|
||||
validationRule: string;
|
||||
}
|
||||
|
||||
export interface AlertRulesState {
|
||||
items: AlertRule[];
|
||||
searchQuery: string;
|
||||
isLoading: boolean;
|
||||
notificationChannels: NotificationChannel[];
|
||||
}
|
||||
|
||||
export interface AlertNotification {
|
||||
|
Loading…
Reference in New Issue
Block a user