mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Migration: Edit notification channel (#25980)
* implement edit page * connectWithCleanup * remove angular related code * use loadingindicator * use the correct loading component * handle secureFields * fixed implementation of secure fields * Keep secureFields after rerendering the form * CollapsableSection and Page refactor * use checkbox instead of switch * fix comment * add cursor to section * Fixed issues after PR review * Fix issue with some settings being undefined * new reducer and start with test * algorithm to migrate secure fields * UX: Minor UI Tweaks * Added field around checkboxes, and missing required field * fixed test * tests for util * minor tweaks and changes * define as records * fix typ error * forward invalid to textarea and inputcontrol * merge formdata and redux data in test * fix issue with creating channel * do not figure out securefields in migration Co-authored-by: Torkel Ödegaard <torkel@grafana.com>
This commit is contained in:
@@ -0,0 +1,10 @@
|
||||
import { Meta, Props } from '@storybook/addon-docs/blocks';
|
||||
import { CollapsableSection } from './CollapsableSection';
|
||||
|
||||
<Meta title="MDX|CollapsableSection" component={CollapsableSection} />
|
||||
|
||||
# Collapsable Section
|
||||
A simple container for enabling collapsing/expanding of content.
|
||||
|
||||
|
||||
<Props of={CollapsableSection} />
|
||||
@@ -0,0 +1,21 @@
|
||||
import React from 'react';
|
||||
import { CollapsableSection } from './CollapsableSection';
|
||||
import mdx from './CollapsableSection.mdx';
|
||||
|
||||
export default {
|
||||
title: 'Layout/CollapsableSection',
|
||||
component: CollapsableSection,
|
||||
parameters: {
|
||||
docs: {
|
||||
page: mdx,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const simple = () => {
|
||||
return (
|
||||
<CollapsableSection label="Collapsable section" isOpen>
|
||||
<div>Here's some content</div>
|
||||
</CollapsableSection>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,38 @@
|
||||
import React, { FC, ReactNode, useState } from 'react';
|
||||
import { css } from 'emotion';
|
||||
import { GrafanaTheme } from '@grafana/data';
|
||||
import { useStyles } from '../../themes';
|
||||
import { Icon } from '..';
|
||||
|
||||
export interface Props {
|
||||
label: string;
|
||||
isOpen: boolean;
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export const CollapsableSection: FC<Props> = ({ label, isOpen, children }) => {
|
||||
const [open, toggleOpen] = useState<boolean>(isOpen);
|
||||
const styles = useStyles(collapsableSectionStyles);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div onClick={() => toggleOpen(!open)} className={styles.header}>
|
||||
<Icon name={open ? 'angle-down' : 'angle-right'} size="xl" />
|
||||
{label}
|
||||
</div>
|
||||
<div className={styles.content}>{open && children}</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
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};
|
||||
`,
|
||||
};
|
||||
};
|
||||
@@ -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 {
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
@@ -20,36 +20,6 @@ func init() {
|
||||
Description: "Sends alert to Prometheus Alertmanager",
|
||||
Heading: "Alertmanager settings",
|
||||
Factory: NewAlertmanagerNotifier,
|
||||
OptionsTemplate: `
|
||||
<h3 class="page-heading">Alertmanager settings</h3>
|
||||
<div class="gf-form max-width-30">
|
||||
<span class="gf-form-label width-10">Url(s)</span>
|
||||
<input type="text" required class="gf-form-input max-width-30" ng-model="ctrl.model.settings.url" placeholder="http://localhost:9093"></input>
|
||||
<info-popover mode="right-absolute">
|
||||
As specified in Alertmanager documentation, do not specify a load balancer here. Enter all your Alertmanager URLs comma-separated.
|
||||
</info-popover>
|
||||
</div>
|
||||
<div class="gf-form max-width-30">
|
||||
<span class="gf-form-label width-10">Basic Auth User</span>
|
||||
<input type="text" class="gf-form-input max-width-30" ng-model="ctrl.model.settings.basicAuthUser" placeholder=""></input>
|
||||
</div>
|
||||
<div class="gf-form max-width-30">
|
||||
<span class="gf-form-label width-10">Basic Auth Password</span>
|
||||
<div class="gf-form gf-form--grow" ng-if="!ctrl.model.secureFields.basicAuthPassword">
|
||||
<input type="text"
|
||||
class="gf-form-input max-width-30"
|
||||
ng-init="ctrl.model.secureSettings.basicAuthPassword = ctrl.model.settings.basicAuthPassword || null; ctrl.model.settings.basicAuthPassword = null;"
|
||||
ng-model="ctrl.model.secureSettings.basicAuthPassword"
|
||||
data-placement="right">
|
||||
</input>
|
||||
</div>
|
||||
<div class="gf-form" ng-if="ctrl.model.secureFields.basicAuthPassword">
|
||||
<input type="text" class="gf-form-input max-width-18" disabled="disabled" value="configured" />
|
||||
<a class="btn btn-secondary gf-form-btn" href="#" ng-click="ctrl.model.secureFields.basicAuthPassword = false">reset</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
Options: []alerting.NotifierOption{
|
||||
{
|
||||
Label: "Url",
|
||||
@@ -71,6 +41,7 @@ func init() {
|
||||
Element: alerting.ElementTypeInput,
|
||||
InputType: alerting.InputTypePassword,
|
||||
PropertyName: "basicAuthPassword",
|
||||
Secure: true,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
@@ -12,26 +12,14 @@ import (
|
||||
)
|
||||
|
||||
const defaultDingdingMsgType = "link"
|
||||
const dingdingOptionsTemplate = `
|
||||
<h3 class="page-heading">DingDing settings</h3>
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-10">Url</span>
|
||||
<input type="text" required class="gf-form-input max-width-70" ng-model="ctrl.model.settings.url" placeholder="https://oapi.dingtalk.com/robot/send?access_token=xxxxxxxxx"></input>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-10">MessageType</span>
|
||||
<select class="gf-form-input max-width-14" ng-model="ctrl.model.settings.msgType" ng-options="s for s in ['link','actionCard']" ng-init="ctrl.model.settings.msgType=ctrl.model.settings.msgType || '` + defaultDingdingMsgType + `'"></select>
|
||||
</div>
|
||||
`
|
||||
|
||||
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",
|
||||
|
||||
@@ -23,24 +23,6 @@ func init() {
|
||||
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">
|
||||
<span class="gf-form-label width-10">Message Content</span>
|
||||
<input type="text"
|
||||
class="gf-form-input max-width-30"
|
||||
ng-model="ctrl.model.settings.content"
|
||||
data-placement="right">
|
||||
</input>
|
||||
<info-popover mode="right-absolute">
|
||||
Mention a group using @ or a user using <@ID> when notifying in a channel
|
||||
</info-popover>
|
||||
</div>
|
||||
<div class="gf-form max-width-30">
|
||||
<span class="gf-form-label width-10">Webhook URL</span>
|
||||
<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",
|
||||
|
||||
@@ -19,32 +19,11 @@ func init() {
|
||||
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">
|
||||
<gf-form-switch
|
||||
class="gf-form"
|
||||
label="Single email"
|
||||
label-class="width-8"
|
||||
checked="ctrl.model.settings.singleEmail"
|
||||
tooltip="Send a single email to all recipients">
|
||||
</gf-form-switch>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label width-8">
|
||||
Addresses
|
||||
</label>
|
||||
<textarea rows="7" class="gf-form-input width-27" required ng-model="ctrl.model.settings.addresses"></textarea>
|
||||
</div>
|
||||
<div class="gf-form offset-width-8">
|
||||
<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,
|
||||
Element: alerting.ElementTypeCheckbox,
|
||||
PropertyName: "singleEmail",
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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: `
|
||||
<h3 class="page-heading">Google Hangouts Chat 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="Google Hangouts Chat incoming webhook url"></input>
|
||||
</div>
|
||||
`,
|
||||
Options: []alerting.NotifierOption{
|
||||
{
|
||||
Label: "Url",
|
||||
|
||||
@@ -20,25 +20,6 @@ func init() {
|
||||
Description: "Sends notifications uto a HipChat Room",
|
||||
Heading: "HipChat settings",
|
||||
Factory: NewHipChatNotifier,
|
||||
OptionsTemplate: `
|
||||
<h3 class="page-heading">HipChat settings</h3>
|
||||
<div class="gf-form max-width-30">
|
||||
<span class="gf-form-label width-8">Hip Chat Url</span>
|
||||
<input type="text" required class="gf-form-input max-width-30" ng-model="ctrl.model.settings.url" placeholder="HipChat URL (ex https://grafana.hipchat.com)"></input>
|
||||
</div>
|
||||
<div class="gf-form max-width-30">
|
||||
<span class="gf-form-label width-8">API Key</span>
|
||||
<input type="text" required class="gf-form-input max-width-30" ng-model="ctrl.model.settings.apikey" placeholder="HipChat API Key"></input>
|
||||
</div>
|
||||
<div class="gf-form max-width-30">
|
||||
<span class="gf-form-label width-8">Room ID</span>
|
||||
<input type="text"
|
||||
class="gf-form-input max-width-30"
|
||||
ng-model="ctrl.model.settings.roomid"
|
||||
data-placement="right">
|
||||
</input>
|
||||
</div>
|
||||
`,
|
||||
Options: []alerting.NotifierOption{
|
||||
{
|
||||
Label: "Hip Chat Url",
|
||||
|
||||
@@ -19,17 +19,6 @@ func init() {
|
||||
Description: "Sends notifications to Kafka Rest Proxy",
|
||||
Heading: "Kafka settings",
|
||||
Factory: NewKafkaNotifier,
|
||||
OptionsTemplate: `
|
||||
<h3 class="page-heading">Kafka settings</h3>
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-14">Kafka REST Proxy</span>
|
||||
<input type="text" required class="gf-form-input max-width-22" ng-model="ctrl.model.settings.kafkaRestProxy" placeholder="http://localhost:8082"></input>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-14">Topic</span>
|
||||
<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",
|
||||
|
||||
@@ -17,25 +17,6 @@ func init() {
|
||||
Description: "Send notifications to LINE notify",
|
||||
Heading: "LINE notify settings",
|
||||
Factory: NewLINENotifier,
|
||||
OptionsTemplate: `
|
||||
<h3 class="page-heading">LINE notify settings</h3>
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label max-width-14">Token</label>
|
||||
<div class="gf-form gf-form--grow" ng-if="!ctrl.model.secureFields.token">
|
||||
<input type="text"
|
||||
required
|
||||
class="gf-form-input max-width-22"
|
||||
ng-init="ctrl.model.secureSettings.token = ctrl.model.settings.token || null; ctrl.model.settings.token = null;"
|
||||
ng-model="ctrl.model.secureSettings.token"
|
||||
data-placement="right">
|
||||
</input>
|
||||
</div>
|
||||
<div class="gf-form" ng-if="ctrl.model.secureFields.token">
|
||||
<input type="text" class="gf-form-input max-width-18" disabled="disabled" value="configured" />
|
||||
<a class="btn btn-secondary gf-form-btn" href="#" ng-click="ctrl.model.secureFields.token = false">reset</a>
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
Options: []alerting.NotifierOption{
|
||||
{
|
||||
Label: "Token",
|
||||
@@ -44,6 +25,7 @@ func init() {
|
||||
Placeholder: "LINE notify token key",
|
||||
PropertyName: "token",
|
||||
Required: true,
|
||||
Secure: true,
|
||||
}},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -18,48 +18,6 @@ func init() {
|
||||
Description: "Sends notifications to OpsGenie",
|
||||
Heading: "OpsGenie settings",
|
||||
Factory: NewOpsGenieNotifier,
|
||||
OptionsTemplate: `
|
||||
<h3 class="page-heading">OpsGenie settings</h3>
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label max-width-14">API Key</label>
|
||||
<div class="gf-form gf-form--grow" ng-if="!ctrl.model.secureFields.apiKey">
|
||||
<input type="text"
|
||||
required
|
||||
class="gf-form-input max-width-22"
|
||||
ng-init="ctrl.model.secureSettings.apiKey = ctrl.model.settings.apiKey || null; ctrl.model.settings.apiKey = null;"
|
||||
ng-model="ctrl.model.secureSettings.apiKey"
|
||||
placeholder="OpsGenie API Key"
|
||||
data-placement="right">
|
||||
</input>
|
||||
</div>
|
||||
<div class="gf-form" ng-if="ctrl.model.secureFields.apiKey">
|
||||
<input type="text" class="gf-form-input max-width-18" disabled="disabled" value="configured" />
|
||||
<a class="btn btn-secondary gf-form-btn" href="#" ng-click="ctrl.model.secureFields.apiKey = false">reset</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-14">Alert API Url</span>
|
||||
<input type="text" required class="gf-form-input max-width-22" ng-model="ctrl.model.settings.apiUrl" placeholder="https://api.opsgenie.com/v2/alerts"></input>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<gf-form-switch
|
||||
class="gf-form"
|
||||
label="Auto close incidents"
|
||||
label-class="width-14"
|
||||
checked="ctrl.model.settings.autoClose"
|
||||
tooltip="Automatically close alerts in OpsGenie once the alert goes back to ok.">
|
||||
</gf-form-switch>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<gf-form-switch
|
||||
class="gf-form"
|
||||
label="Override priority"
|
||||
label-class="width-14"
|
||||
checked="ctrl.model.settings.overridePriority"
|
||||
tooltip="Allow the alert priority to be set using the og_priority tag">
|
||||
</gf-form-switch>
|
||||
</div>
|
||||
`,
|
||||
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",
|
||||
},
|
||||
|
||||
@@ -20,53 +20,6 @@ func init() {
|
||||
Description: "Sends notifications to PagerDuty",
|
||||
Heading: "PagerDuty settings",
|
||||
Factory: NewPagerdutyNotifier,
|
||||
OptionsTemplate: `
|
||||
<h3 class="page-heading">PagerDuty settings</h3>
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-14">Integration Key</span>
|
||||
<div class="gf-form gf-form--grow" ng-if="!ctrl.model.secureFields.integrationKey">
|
||||
<input type="text"
|
||||
class="gf-form-input max-width-22"
|
||||
ng-init="ctrl.model.secureSettings.integrationKey = ctrl.model.settings.integrationKey || null; ctrl.model.settings.integrationKey = null;"
|
||||
ng-model="ctrl.model.secureSettings.integrationKey"
|
||||
placeholder="Pagerduty Integration Key"
|
||||
data-placement="right">
|
||||
</input>
|
||||
</div>
|
||||
<div class="gf-form" ng-if="ctrl.model.secureFields.integrationKey">
|
||||
<input type="text" class="gf-form-input max-width-18" disabled="disabled" value="configured" />
|
||||
<a class="btn btn-secondary gf-form-btn" href="#" ng-click="ctrl.model.secureFields.integrationKey = false">reset</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-14">Severity</span>
|
||||
<div class="gf-form-select-wrapper width-14">
|
||||
<select
|
||||
class="gf-form-input"
|
||||
ng-model="ctrl.model.settings.severity"
|
||||
ng-options="s for s in ['critical', 'error', 'warning', 'info']">
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<gf-form-switch
|
||||
class="gf-form"
|
||||
label="Auto resolve incidents"
|
||||
label-class="width-14"
|
||||
checked="ctrl.model.settings.autoResolve"
|
||||
tooltip="Resolve incidents in pagerduty once the alert goes back to ok.">
|
||||
</gf-form-switch>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<gf-form-switch
|
||||
class="gf-form"
|
||||
label="Include message in details"
|
||||
label-class="width-14"
|
||||
checked="ctrl.model.settings.messageInDetails"
|
||||
tooltip="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">
|
||||
</gf-form-switch>
|
||||
</div>
|
||||
`,
|
||||
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",
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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: `
|
||||
<h3 class="page-heading">Pushover settings</h3>
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label width-10">API Token</label>
|
||||
<div class="gf-form gf-form--grow" ng-if="!ctrl.model.secureFields.apiToken">
|
||||
<input type="text"
|
||||
required
|
||||
class="gf-form-input"
|
||||
ng-init="ctrl.model.secureSettings.apiToken = ctrl.model.settings.apiToken || null; ctrl.model.settings.apiToken = null;"
|
||||
ng-model="ctrl.model.secureSettings.apiToken"
|
||||
data-placement="right">
|
||||
</input>
|
||||
</div>
|
||||
<div class="gf-form" ng-if="ctrl.model.secureFields.apiToken">
|
||||
<input type="text" class="gf-form-input max-width-18" disabled="disabled" value="configured" />
|
||||
<a class="btn btn-secondary gf-form-btn" href="#" ng-click="ctrl.model.secureFields.apiToken = false">reset</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label max-width-10">User Key(s)</label>
|
||||
<div class="gf-form gf-form--grow" ng-if="!ctrl.model.secureFields.userKey">
|
||||
<input type="text"
|
||||
required
|
||||
class="gf-form-input"
|
||||
ng-init="ctrl.model.secureSettings.userKey = ctrl.model.settings.userKey || null; ctrl.model.settings.userKey = null;"
|
||||
ng-model="ctrl.model.secureSettings.userKey"
|
||||
placeholder="comma-separated list"
|
||||
data-placement="right">
|
||||
</input>
|
||||
</div>
|
||||
<div class="gf-form" ng-if="ctrl.model.secureFields.userKey">
|
||||
<input type="text" class="gf-form-input max-width-18" disabled="disabled" value="configured" />
|
||||
<a class="btn btn-secondary gf-form-btn" href="#" ng-click="ctrl.model.secureFields.userKey = false">reset</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-10">Device(s) (optional)</span>
|
||||
<input type="text" class="gf-form-input" placeholder="comma-separated list; leave empty to send to all devices" ng-model="ctrl.model.settings.device"></input>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-10">Priority</span>
|
||||
<select class="gf-form-input max-width-14" ng-model="ctrl.model.settings.priority" ng-options="v as k for (k, v) in {
|
||||
Emergency: '2',
|
||||
High: '1',
|
||||
Normal: '0',
|
||||
Low: '-1',
|
||||
Lowest: '-2'
|
||||
}" ng-init="ctrl.model.settings.priority=ctrl.model.settings.priority||'0'"></select>
|
||||
</div>
|
||||
<div class="gf-form" ng-show="ctrl.model.settings.priority == '2'">
|
||||
<span class="gf-form-label width-10">Retry</span>
|
||||
<input type="text" class="gf-form-input max-width-14" ng-required="ctrl.model.settings.priority == '2'" placeholder="minimum 30 seconds" ng-model="ctrl.model.settings.retry" ng-init="ctrl.model.settings.retry=ctrl.model.settings.retry||'60'></input>
|
||||
</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>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-10">Alerting sound</span>
|
||||
<select class="gf-form-input max-width-14" ng-model="ctrl.model.settings.sound" ng-options="s for s in [
|
||||
` + sounds + `
|
||||
]" ng-init="ctrl.model.settings.sound=ctrl.model.settings.sound||'default'"></select>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-10">OK sound</span>
|
||||
<select class="gf-form-input max-width-14" ng-model="ctrl.model.settings.okSound" ng-options="s for s in [
|
||||
` + sounds + `
|
||||
]" ng-init="ctrl.model.settings.okSound=ctrl.model.settings.okSound||'default'"></select>
|
||||
</div>
|
||||
`,
|
||||
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)",
|
||||
|
||||
@@ -18,41 +18,6 @@ func init() {
|
||||
Description: "Sends HTTP POST request to a Sensu API",
|
||||
Heading: "Sensu settings",
|
||||
Factory: NewSensuNotifier,
|
||||
OptionsTemplate: `
|
||||
<h3 class="page-heading">Sensu settings</h3>
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-10">Url</span>
|
||||
<input type="text" required class="gf-form-input max-width-26" ng-model="ctrl.model.settings.url" placeholder="http://sensu-api.local:4567/results"></input>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-10">Source</span>
|
||||
<input type="text" class="gf-form-input max-width-14" ng-model="ctrl.model.settings.source" bs-tooltip="'If empty rule id will be used'" data-placement="right"></input>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-10">Handler</span>
|
||||
<input type="text" class="gf-form-input max-width-14" ng-model="ctrl.model.settings.handler" placeholder="default"></input>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-10">Username</span>
|
||||
<input type="text" class="gf-form-input max-width-14" ng-model="ctrl.model.settings.username"></input>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label width-10">Password</label>
|
||||
<div class="gf-form gf-form--grow" ng-if="!ctrl.model.secureFields.password">
|
||||
<input type="text"
|
||||
required
|
||||
class="gf-form-input max-width-14"
|
||||
ng-init="ctrl.model.secureSettings.password = ctrl.model.settings.password || null; ctrl.model.settings.password = null;"
|
||||
ng-model="ctrl.model.secureSettings.password"
|
||||
data-placement="right">
|
||||
</input>
|
||||
</div>
|
||||
<div class="gf-form" ng-if="ctrl.model.secureFields.password">
|
||||
<input type="text" class="gf-form-input max-width-14" disabled="disabled" value="configured" />
|
||||
<a class="btn btn-secondary gf-form-btn" href="#" ng-click="ctrl.model.secureFields.password = false">reset</a>
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
Options: []alerting.NotifierOption{
|
||||
{
|
||||
Label: "Url",
|
||||
@@ -87,6 +52,7 @@ func init() {
|
||||
Element: alerting.ElementTypeInput,
|
||||
InputType: alerting.InputTypePassword,
|
||||
PropertyName: "passsword ",
|
||||
Secure: true,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
@@ -27,123 +27,6 @@ func init() {
|
||||
Description: "Sends notifications to Slack via Slack Webhooks",
|
||||
Heading: "Slack settings",
|
||||
Factory: NewSlackNotifier,
|
||||
OptionsTemplate: `
|
||||
<h3 class="page-heading">Slack settings</h3>
|
||||
<div class="gf-form max-width-30">
|
||||
<span class="gf-form-label width-8">Url</span>
|
||||
<div class="gf-form gf-form--grow" ng-if="!ctrl.model.secureFields.url">
|
||||
<input type="text"
|
||||
required
|
||||
class="gf-form-input max-width-30"
|
||||
ng-init="ctrl.model.secureSettings.url = ctrl.model.settings.url || null; ctrl.model.settings.url = null;"
|
||||
ng-model="ctrl.model.secureSettings.url"
|
||||
placeholder="Slack incoming webhook url">
|
||||
</input>
|
||||
</div>
|
||||
<div class="gf-form" ng-if="ctrl.model.secureFields.url">
|
||||
<input type="text" class="gf-form-input max-width-18" disabled="disabled" value="configured" />
|
||||
<a class="btn btn-secondary gf-form-btn" href="#" ng-click="ctrl.model.secureFields.url = false">reset</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gf-form max-width-30">
|
||||
<span class="gf-form-label width-8">Recipient</span>
|
||||
<input type="text"
|
||||
class="gf-form-input max-width-30"
|
||||
ng-model="ctrl.model.settings.recipient"
|
||||
data-placement="right">
|
||||
</input>
|
||||
<info-popover mode="right-absolute">
|
||||
Override default channel or user, use #channel-name, @username (has to be all lowercase, no whitespace), or user/channel Slack ID
|
||||
</info-popover>
|
||||
</div>
|
||||
<div class="gf-form max-width-30">
|
||||
<span class="gf-form-label width-8">Username</span>
|
||||
<input type="text"
|
||||
class="gf-form-input max-width-30"
|
||||
ng-model="ctrl.model.settings.username"
|
||||
data-placement="right">
|
||||
</input>
|
||||
<info-popover mode="right-absolute">
|
||||
Set the username for the bot's message
|
||||
</info-popover>
|
||||
</div>
|
||||
<div class="gf-form max-width-30">
|
||||
<span class="gf-form-label width-8">Icon emoji</span>
|
||||
<input type="text"
|
||||
class="gf-form-input max-width-30"
|
||||
ng-model="ctrl.model.settings.icon_emoji"
|
||||
data-placement="right">
|
||||
</input>
|
||||
<info-popover mode="right-absolute">
|
||||
Provide an emoji to use as the icon for the bot's message. Overrides the icon URL
|
||||
</info-popover>
|
||||
</div>
|
||||
<div class="gf-form max-width-30">
|
||||
<span class="gf-form-label width-8">Icon URL</span>
|
||||
<input type="text"
|
||||
class="gf-form-input max-width-30"
|
||||
ng-model="ctrl.model.settings.icon_url"
|
||||
data-placement="right">
|
||||
</input>
|
||||
<info-popover mode="right-absolute">
|
||||
Provide a URL to an image to use as the icon for the bot's message
|
||||
</info-popover>
|
||||
</div>
|
||||
<div class="gf-form max-width-30">
|
||||
<span class="gf-form-label width-8">Mention Users</span>
|
||||
<input type="text"
|
||||
class="gf-form-input max-width-30"
|
||||
ng-model="ctrl.model.settings.mentionUsers"
|
||||
data-placement="right">
|
||||
</input>
|
||||
<info-popover mode="right-absolute">
|
||||
Mention one or more users (comma separated) when notifying in a channel, by ID (you can copy this from the user's Slack profile)
|
||||
</info-popover>
|
||||
</div>
|
||||
<div class="gf-form max-width-30">
|
||||
<span class="gf-form-label width-8">Mention Groups</span>
|
||||
<input type="text"
|
||||
class="gf-form-input max-width-30"
|
||||
ng-model="ctrl.model.settings.mentionGroups"
|
||||
data-placement="right">
|
||||
</input>
|
||||
<info-popover mode="right-absolute">
|
||||
Mention one or more groups (comma separated) when notifying in a channel (you can copy this from the group's Slack profile URL)
|
||||
</info-popover>
|
||||
</div>
|
||||
<div class="gf-form max-width-30">
|
||||
<span class="gf-form-label width-8">Mention Channel</span>
|
||||
<select
|
||||
class="gf-form-input max-width-30"
|
||||
ng-model="ctrl.model.settings.mentionChannel"
|
||||
data-placement="right">
|
||||
<option value="">Disabled</option>
|
||||
<option value="here">Every active channel member</option>
|
||||
<option value="channel">Every channel member</option>
|
||||
</select>
|
||||
<info-popover mode="right-absolute">
|
||||
Mention whole channel or just active members when notifying
|
||||
</info-popover>
|
||||
</div>
|
||||
<div class="gf-form max-width-30">
|
||||
<div class="gf-form gf-form--v-stretch"><label class="gf-form-label width-8">Token</label></div>
|
||||
<div class="gf-form gf-form--grow" ng-if="!ctrl.model.secureFields.token">
|
||||
<input type="text"
|
||||
class="gf-form-input max-width-30"
|
||||
ng-init="ctrl.model.secureSettings.token = ctrl.model.settings.token || null; ctrl.model.settings.token = null;"
|
||||
ng-model="ctrl.model.secureSettings.token"
|
||||
data-placement="right">
|
||||
</input>
|
||||
<info-popover mode="right-absolute">
|
||||
Provide a bot token to use the Slack file.upload API (starts with "xoxb"). Specify Recipient for this to work
|
||||
</info-popover>
|
||||
</div>
|
||||
<div class="gf-form" ng-if="ctrl.model.secureFields.token">
|
||||
<input type="text" class="gf-form-input max-width-18" disabled="disabled" value="configured" />
|
||||
<a class="btn btn-secondary gf-form-btn" href="#" ng-click="ctrl.model.secureFields.token = false">reset</a>
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
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,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
@@ -16,13 +16,6 @@ func init() {
|
||||
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" 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",
|
||||
@@ -30,6 +23,7 @@ func init() {
|
||||
InputType: alerting.InputTypeText,
|
||||
Placeholder: "Teams incoming webhook url",
|
||||
PropertyName: "url",
|
||||
Required: true,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
@@ -28,37 +28,6 @@ func init() {
|
||||
Description: "Sends notifications to Telegram",
|
||||
Heading: "Telegram API settings",
|
||||
Factory: NewTelegramNotifier,
|
||||
OptionsTemplate: `
|
||||
<h3 class="page-heading">Telegram API settings</h3>
|
||||
<div class="gf-form max-width-30">
|
||||
<label class="gf-form-label width-10">BOT API Token</label>
|
||||
<div class="gf-form gf-form--grow" ng-if="!ctrl.model.secureFields.bottoken">
|
||||
<input type="text"
|
||||
required
|
||||
class="gf-form-input max-width-30"
|
||||
ng-init="ctrl.model.secureSettings.bottoken = ctrl.model.settings.bottoken || null; ctrl.model.settings.bottoken = null;"
|
||||
ng-model="ctrl.model.secureSettings.bottoken"
|
||||
placeholder="Telegram BOT API Token"
|
||||
data-placement="right">
|
||||
</input>
|
||||
</div>
|
||||
<div class="gf-form" ng-if="ctrl.model.secureFields.bottoken">
|
||||
<input type="text" class="gf-form-input max-width-18" disabled="disabled" value="configured" />
|
||||
<a class="btn btn-secondary gf-form-btn" href="#" ng-click="ctrl.model.secureFields.bottoken = false">reset</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gf-form max-width-30">
|
||||
<label class="gf-form-label width-10">Chat ID</label>
|
||||
<input type="text"
|
||||
required
|
||||
class="gf-form-input max-width-30"
|
||||
ng-model="ctrl.model.settings.chatid"
|
||||
placeholder="Telegram Chat ID"
|
||||
data-placement="right">
|
||||
</input>
|
||||
<info-popover mode="right-absolute">Integer Telegram Chat Identifier</info-popover>
|
||||
</div>
|
||||
`,
|
||||
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",
|
||||
|
||||
@@ -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: `
|
||||
<h3 class="page-heading">Threema Gateway settings</h3>
|
||||
<p>
|
||||
Notifications can be configured for any Threema Gateway ID of type
|
||||
"Basic". End-to-End IDs are not currently supported.
|
||||
</p>
|
||||
<p>
|
||||
The Threema Gateway ID can be set up at
|
||||
<a href="https://gateway.threema.ch/" target="_blank" rel="noopener noreferrer">https://gateway.threema.ch/</a>.
|
||||
</p>
|
||||
<div class="gf-form max-width-30">
|
||||
<span class="gf-form-label width-8">Gateway ID</span>
|
||||
<input type="text" required maxlength="8" pattern="\*[0-9A-Z]{7}"
|
||||
class="gf-form-input max-width-30"
|
||||
ng-model="ctrl.model.settings.gateway_id"
|
||||
placeholder="*3MAGWID">
|
||||
</input>
|
||||
<info-popover mode="right-absolute">
|
||||
Your 8 character Threema Gateway ID (starting with a *)
|
||||
</info-popover>
|
||||
</div>
|
||||
<div class="gf-form max-width-30">
|
||||
<span class="gf-form-label width-8">Recipient ID</span>
|
||||
<input type="text" required maxlength="8" pattern="[0-9A-Z]{8}"
|
||||
class="gf-form-input max-width-30"
|
||||
ng-model="ctrl.model.settings.recipient_id"
|
||||
placeholder="YOUR3MID">
|
||||
</input>
|
||||
<info-popover mode="right-absolute">
|
||||
The 8 character Threema ID that should receive the alerts
|
||||
</info-popover>
|
||||
</div>
|
||||
<div class="gf-form max-width-30">
|
||||
<label class="gf-form-label width-8">API Secret</label>
|
||||
<div class="gf-form gf-form--grow" ng-if="!ctrl.model.secureFields.api_secret">
|
||||
<input type="text"
|
||||
required
|
||||
class="gf-form-input max-width-30"
|
||||
ng-init="ctrl.model.secureSettings.api_secret = ctrl.model.settings.api_secret || null; ctrl.model.settings.api_secret = null;"
|
||||
ng-model="ctrl.model.secureSettings.api_secret"
|
||||
data-placement="right">
|
||||
</input>
|
||||
</div>
|
||||
<div class="gf-form" ng-if="ctrl.model.secureFields.api_secret">
|
||||
<input type="text" class="gf-form-input max-width-18" disabled="disabled" value="configured" />
|
||||
<a class="btn btn-secondary gf-form-btn" href="#" ng-click="ctrl.model.secureFields.api_secret = false">reset</a>
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
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,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
@@ -25,29 +25,6 @@ func init() {
|
||||
Description: "Sends notifications to VictorOps",
|
||||
Heading: "VictorOps settings",
|
||||
Factory: NewVictoropsNotifier,
|
||||
OptionsTemplate: `
|
||||
<h3 class="page-heading">VictorOps settings</h3>
|
||||
<div class="gf-form">
|
||||
<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="VictorOps url"></input>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-10">No Data Alert Type</span>
|
||||
<div class="gf-form-select-wrapper width-14">
|
||||
<select class="gf-form-input" ng-model="ctrl.model.settings.noDataAlertType" ng-options="t for t in ['CRITICAL', 'WARNING']" ng-init="ctrl.model.settings.noDataAlertType=ctrl.model.settings.noDataAlertType || '` + AlertStateWarning + `'">
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<gf-form-switch
|
||||
class="gf-form"
|
||||
label="Auto resolve incidents"
|
||||
label-class="width-14"
|
||||
checked="ctrl.model.settings.autoResolve"
|
||||
tooltip="Resolve incidents in VictorOps once the alert goes back to ok.">
|
||||
</gf-form-switch>
|
||||
</div>
|
||||
`,
|
||||
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",
|
||||
},
|
||||
},
|
||||
|
||||
@@ -15,39 +15,6 @@ func init() {
|
||||
Description: "Sends HTTP POST request to a URL",
|
||||
Heading: "Webhook settings",
|
||||
Factory: NewWebHookNotifier,
|
||||
OptionsTemplate: `
|
||||
<h3 class="page-heading">Webhook settings</h3>
|
||||
<div class="gf-form max-width-30">
|
||||
<span class="gf-form-label width-8">Url</span>
|
||||
<input type="text" required class="gf-form-input max-width-26" ng-model="ctrl.model.settings.url"></input>
|
||||
</div>
|
||||
<div class="gf-form max-width-30">
|
||||
<span class="gf-form-label width-8">Http Method</span>
|
||||
<div class="gf-form-select-wrapper max-width-30">
|
||||
<select class="gf-form-input" ng-model="ctrl.model.settings.httpMethod" ng-options="t for t in ['POST', 'PUT']">
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gf-form max-width-30">
|
||||
<span class="gf-form-label width-8">Username</span>
|
||||
<input type="text" class="gf-form-input max-width-30" ng-model="ctrl.model.settings.username"></input>
|
||||
</div>
|
||||
<div class="gf-form max-width-30">
|
||||
<div class="gf-form gf-form--v-stretch"><label class="gf-form-label width-8">Password</label></div>
|
||||
<div class="gf-form gf-form--grow" ng-if="!ctrl.model.secureFields.password">
|
||||
<input type="text"
|
||||
class="gf-form-input max-width-30"
|
||||
ng-init="ctrl.model.secureSettings.password = ctrl.model.settings.password || null; ctrl.model.settings.password = null;"
|
||||
ng-model="ctrl.model.secureSettings.password"
|
||||
data-placement="right">
|
||||
</input>
|
||||
</div>
|
||||
<div class="gf-form" ng-if="ctrl.model.secureFields.password">
|
||||
<input type="text" class="gf-form-input max-width-18" disabled="disabled" value="configured" />
|
||||
<a class="btn btn-secondary gf-form-btn" href="#" ng-click="ctrl.model.secureFields.password = false">reset</a>
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
Options: []alerting.NotifierOption{
|
||||
{
|
||||
Label: "Url",
|
||||
@@ -82,6 +49,7 @@ func init() {
|
||||
Element: alerting.ElementTypeInput,
|
||||
InputType: alerting.InputTypePassword,
|
||||
PropertyName: "password",
|
||||
Secure: true,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
150
public/app/features/alerting/EditNotificationChannelPage.tsx
Normal file
150
public/app/features/alerting/EditNotificationChannelPage.tsx
Normal file
@@ -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<Props> {
|
||||
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 (
|
||||
<Page navModel={navModel}>
|
||||
<Page.Contents>
|
||||
<h2 className="page-sub-heading">Edit notification channel</h2>
|
||||
{notificationChannel && notificationChannel.id > 0 ? (
|
||||
<Form
|
||||
width={600}
|
||||
onSubmit={this.onSubmit}
|
||||
defaultValues={{
|
||||
...notificationChannel,
|
||||
type: notificationChannelTypes.find(n => n.value === notificationChannel.type),
|
||||
}}
|
||||
>
|
||||
{({ control, errors, getValues, register, watch }) => {
|
||||
const selectedChannel = notificationChannelTypes.find(c => c.value === getValues().type.value);
|
||||
|
||||
return (
|
||||
<NotificationChannelForm
|
||||
selectableChannels={mapChannelsToSelectableValue(notificationChannelTypes)}
|
||||
selectedChannel={selectedChannel}
|
||||
imageRendererAvailable={config.rendererAvailable}
|
||||
onTestChannel={this.onTestChannel}
|
||||
register={register}
|
||||
watch={watch}
|
||||
errors={errors}
|
||||
getValues={getValues}
|
||||
control={control}
|
||||
resetSecureField={this.props.resetSecureField}
|
||||
secureFields={notificationChannel.secureFields}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
</Form>
|
||||
) : (
|
||||
<div>
|
||||
Loading notification channel
|
||||
<Spinner />
|
||||
</div>
|
||||
)}
|
||||
</Page.Contents>
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const mapStateToProps: MapStateToProps<ConnectedProps, OwnProps, StoreState> = 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<DispatchProps, OwnProps> = {
|
||||
loadNotificationTypes,
|
||||
loadNotificationChannel,
|
||||
testNotificationChannel,
|
||||
updateNotificationChannel,
|
||||
resetSecureField,
|
||||
};
|
||||
|
||||
export default connectWithCleanUp(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps,
|
||||
state => state.notificationChannel
|
||||
)(EditNotificationChannelPage);
|
||||
@@ -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<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);
|
||||
96
public/app/features/alerting/NewNotificationChannelPage.tsx
Normal file
96
public/app/features/alerting/NewNotificationChannelPage.tsx
Normal file
@@ -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<Props> {
|
||||
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 (
|
||||
<Page navModel={navModel}>
|
||||
<Page.Contents>
|
||||
<h2 className="page-sub-heading">New notification channel</h2>
|
||||
<Form onSubmit={this.onSubmit} validateOn="onChange" defaultValues={defaultValues}>
|
||||
{({ register, errors, control, getValues, watch }) => {
|
||||
const selectedChannel = notificationChannelTypes.find(c => c.value === getValues().type.value);
|
||||
|
||||
return (
|
||||
<NotificationChannelForm
|
||||
selectableChannels={mapChannelsToSelectableValue(notificationChannelTypes)}
|
||||
selectedChannel={selectedChannel}
|
||||
onTestChannel={this.onTestChannel}
|
||||
register={register}
|
||||
errors={errors}
|
||||
getValues={getValues}
|
||||
control={control}
|
||||
watch={watch}
|
||||
imageRendererAvailable={config.rendererAvailable}
|
||||
resetSecureField={this.props.resetSecureField}
|
||||
secureFields={{}}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
</Form>
|
||||
</Page.Contents>
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const mapStateToProps: MapStateToProps<ConnectedProps, OwnProps, StoreState> = state => {
|
||||
return {
|
||||
navModel: getNavModel(state.navIndex, 'channels'),
|
||||
notificationChannelTypes: state.notificationChannel.notificationChannelTypes,
|
||||
};
|
||||
};
|
||||
|
||||
const mapDispatchToProps: MapDispatchToProps<DispatchProps, OwnProps> = {
|
||||
createNotificationChannel,
|
||||
loadNotificationTypes,
|
||||
testNotificationChannel,
|
||||
resetSecureField,
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(NewNotificationChannelPage);
|
||||
44
public/app/features/alerting/components/BasicSettings.tsx
Normal file
44
public/app/features/alerting/components/BasicSettings.tsx
Normal file
@@ -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<SelectableValue<string>>;
|
||||
secureFields: NotificationChannelSecureFields;
|
||||
resetSecureField: (key: string) => void;
|
||||
}
|
||||
|
||||
export const BasicSettings: FC<Props> = ({
|
||||
control,
|
||||
currentFormValues,
|
||||
errors,
|
||||
secureFields,
|
||||
selectedChannel,
|
||||
channels,
|
||||
register,
|
||||
resetSecureField,
|
||||
}) => {
|
||||
return (
|
||||
<CollapsableSection label="Channel" isOpen>
|
||||
<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={channels} control={control} rules={{ required: true }} />
|
||||
</Field>
|
||||
<NotificationChannelOptions
|
||||
selectedChannelOptions={selectedChannel.options.filter(o => o.required)}
|
||||
currentFormValues={currentFormValues}
|
||||
secureFields={secureFields}
|
||||
onResetSecureField={resetSecureField}
|
||||
register={register}
|
||||
errors={errors}
|
||||
control={control}
|
||||
/>
|
||||
</CollapsableSection>
|
||||
);
|
||||
};
|
||||
36
public/app/features/alerting/components/ChannelSettings.tsx
Normal file
36
public/app/features/alerting/components/ChannelSettings.tsx
Normal file
@@ -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<Props> = ({
|
||||
control,
|
||||
currentFormValues,
|
||||
errors,
|
||||
selectedChannel,
|
||||
secureFields,
|
||||
register,
|
||||
resetSecureField,
|
||||
}) => {
|
||||
return (
|
||||
<CollapsableSection label={`Optional ${selectedChannel.heading}`} isOpen={false}>
|
||||
{selectedChannel.info !== '' && <InfoBox>{selectedChannel.info}</InfoBox>}
|
||||
<NotificationChannelOptions
|
||||
selectedChannelOptions={selectedChannel.options.filter(o => !o.required)}
|
||||
currentFormValues={currentFormValues}
|
||||
register={register}
|
||||
errors={errors}
|
||||
control={control}
|
||||
onResetSecureField={resetSecureField}
|
||||
secureFields={secureFields}
|
||||
/>
|
||||
</CollapsableSection>
|
||||
);
|
||||
};
|
||||
@@ -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<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,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<FormAPI<NotificationChannelDTO>, 'formState'> {
|
||||
selectableChannels: Array<SelectableValue<string>>;
|
||||
selectedChannel?: NotificationChannelType;
|
||||
imageRendererAvailable: boolean;
|
||||
secureFields: NotificationChannelSecureFields;
|
||||
resetSecureField: (key: string) => void;
|
||||
onTestChannel: (data: NotificationChannelDTO) => void;
|
||||
}
|
||||
|
||||
export interface NotificationSettingsProps
|
||||
extends Omit<FormAPI<NotificationChannelDTO>, 'formState' | 'watch' | 'getValues'> {
|
||||
currentFormValues: NotificationChannelDTO;
|
||||
}
|
||||
|
||||
export const NotificationChannelForm: FC<Props> = ({
|
||||
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 ? (
|
||||
<>
|
||||
<div className={styles.basicSettings}>
|
||||
<BasicSettings
|
||||
selectedChannel={selectedChannel}
|
||||
channels={selectableChannels}
|
||||
secureFields={secureFields}
|
||||
resetSecureField={resetSecureField}
|
||||
currentFormValues={currentFormValues}
|
||||
register={register}
|
||||
errors={errors}
|
||||
control={control}
|
||||
/>
|
||||
{/* If there are no non-required fields, don't render this section*/}
|
||||
{selectedChannel.options.filter(o => !o.required).length > 0 && (
|
||||
<ChannelSettings
|
||||
selectedChannel={selectedChannel}
|
||||
secureFields={secureFields}
|
||||
resetSecureField={resetSecureField}
|
||||
currentFormValues={currentFormValues}
|
||||
register={register}
|
||||
errors={errors}
|
||||
control={control}
|
||||
/>
|
||||
)}
|
||||
<NotificationSettings
|
||||
imageRendererAvailable={imageRendererAvailable}
|
||||
currentFormValues={currentFormValues}
|
||||
register={register}
|
||||
errors={errors}
|
||||
control={control}
|
||||
/>
|
||||
</div>
|
||||
<HorizontalGroup>
|
||||
<Button type="submit">Save</Button>
|
||||
<Button type="button" variant="secondary" onClick={() => onTestChannel(getValues({ nest: true }))}>
|
||||
Test
|
||||
</Button>
|
||||
<a href="/alerting/notifications">
|
||||
<Button type="button" variant="secondary">
|
||||
Back
|
||||
</Button>
|
||||
</a>
|
||||
</HorizontalGroup>
|
||||
</>
|
||||
) : (
|
||||
<Spinner />
|
||||
);
|
||||
};
|
||||
|
||||
const getStyles = stylesFactory((theme: GrafanaTheme) => {
|
||||
return {
|
||||
basicSettings: css`
|
||||
margin-bottom: ${theme.spacing.xl};
|
||||
`,
|
||||
};
|
||||
});
|
||||
@@ -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<FormAPI<NotificationChannelDTO>, 'formState' | 'getValues' | 'watch'> {
|
||||
selectedChannel: NotificationChannel;
|
||||
selectedChannelOptions: NotificationChannelOption[];
|
||||
currentFormValues: NotificationChannelDTO;
|
||||
secureFields: NotificationChannelSecureFields;
|
||||
|
||||
onResetSecureField: (key: string) => void;
|
||||
}
|
||||
|
||||
export const NotificationChannelOptions: FC<Props> = ({
|
||||
control,
|
||||
currentFormValues,
|
||||
errors,
|
||||
selectedChannel,
|
||||
selectedChannelOptions,
|
||||
register,
|
||||
onResetSecureField,
|
||||
secureFields,
|
||||
}) => {
|
||||
return (
|
||||
<>
|
||||
<h3>{selectedChannel.heading}</h3>
|
||||
{selectedChannel.info !== '' && <InfoBox>{selectedChannel.info}</InfoBox>}
|
||||
{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<Props> = ({
|
||||
return null;
|
||||
}
|
||||
|
||||
if (option.element === 'checkbox') {
|
||||
return (
|
||||
<Field key={key}>
|
||||
<Checkbox
|
||||
name={option.secure ? `secureSettings.${option.propertyName}` : `settings.${option.propertyName}`}
|
||||
ref={register}
|
||||
label={option.label}
|
||||
description={option.description}
|
||||
/>
|
||||
</Field>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Field
|
||||
key={key}
|
||||
@@ -41,7 +55,19 @@ export const NotificationChannelOptions: FC<Props> = ({
|
||||
invalid={errors.settings && !!errors.settings[option.propertyName]}
|
||||
error={errors.settings && errors.settings[option.propertyName]?.message}
|
||||
>
|
||||
<OptionElement option={option} register={register} control={control} />
|
||||
{secureFields && secureFields[option.propertyName] ? (
|
||||
<Input
|
||||
readOnly={true}
|
||||
value="Configured"
|
||||
addonAfter={
|
||||
<Button onClick={() => onResetSecureField(option.propertyName)} variant="secondary" type="button">
|
||||
Reset
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<OptionElement option={option} register={register} control={control} />
|
||||
)}
|
||||
</Field>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -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<Props> = ({ currentFormValues, imageRendererAvailable, register }) => {
|
||||
return (
|
||||
<CollapsableSection label="Notification settings" isOpen={false}>
|
||||
<Field>
|
||||
<Checkbox name="isDefault" ref={register} label="Default" description="Use this notification for all alerts" />
|
||||
</Field>
|
||||
<Field>
|
||||
<Checkbox
|
||||
name="settings.uploadImage"
|
||||
ref={register}
|
||||
label="Include image"
|
||||
description="Captures an image and include it in the notification"
|
||||
/>
|
||||
</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>
|
||||
<Checkbox
|
||||
name="disableResolveMessage"
|
||||
ref={register}
|
||||
label="Disable Resolve Message"
|
||||
description="Disable the resolve message [OK] that is sent when alerting state returns to false"
|
||||
/>
|
||||
</Field>
|
||||
<Field>
|
||||
<Checkbox
|
||||
name="sendReminder"
|
||||
ref={register}
|
||||
label="Send reminders"
|
||||
description="Send additional notifications for triggered alerts"
|
||||
/>
|
||||
</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.
|
||||
Alert reminders are sent after rules are evaluated. Therefore a reminder can never be sent more frequently
|
||||
than a configured alert rule evaluation interval."
|
||||
>
|
||||
<Input name="frequency" ref={register} width={8} />
|
||||
</Field>
|
||||
</>
|
||||
)}
|
||||
</CollapsableSection>
|
||||
);
|
||||
};
|
||||
@@ -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<FormAPI<any>, 'register' | 'control'> {
|
||||
option: Option;
|
||||
option: NotificationChannelOption;
|
||||
invalid?: boolean;
|
||||
}
|
||||
|
||||
export const OptionElement: FC<Props> = ({ control, option, register }) => {
|
||||
const modelValue = `settings.${option.propertyName}`;
|
||||
export const OptionElement: FC<Props> = ({ control, option, register, invalid }) => {
|
||||
const modelValue = option.secure ? `secureSettings.${option.propertyName}` : `settings.${option.propertyName}`;
|
||||
switch (option.element) {
|
||||
case 'input':
|
||||
return (
|
||||
<Input
|
||||
invalid={invalid}
|
||||
type={option.inputType}
|
||||
name={`${modelValue}`}
|
||||
ref={register({
|
||||
@@ -23,11 +25,20 @@ export const OptionElement: FC<Props> = ({ control, option, register }) => {
|
||||
);
|
||||
|
||||
case 'select':
|
||||
return <InputControl as={Select} options={option.selectOptions} control={control} name={`${modelValue}`} />;
|
||||
return (
|
||||
<InputControl
|
||||
as={Select}
|
||||
options={option.selectOptions}
|
||||
control={control}
|
||||
name={`${modelValue}`}
|
||||
invalid={invalid}
|
||||
/>
|
||||
);
|
||||
|
||||
case 'textarea':
|
||||
return (
|
||||
<TextArea
|
||||
invalid={invalid}
|
||||
name={`${modelValue}`}
|
||||
ref={register({
|
||||
required: option.required ? 'Required' : false,
|
||||
@@ -36,16 +47,6 @@ export const OptionElement: FC<Props> = ({ control, option, register }) => {
|
||||
/>
|
||||
);
|
||||
|
||||
case 'switch':
|
||||
return (
|
||||
<Switch
|
||||
name={`${modelValue}`}
|
||||
ref={register({
|
||||
required: option.required ? 'Required' : false,
|
||||
})}
|
||||
/>
|
||||
);
|
||||
|
||||
default:
|
||||
console.error('Element not supported', option.element);
|
||||
return null;
|
||||
|
||||
@@ -1,89 +0,0 @@
|
||||
<page-header model="ctrl.navModel"></page-header>
|
||||
|
||||
<div class="page-container page-body">
|
||||
|
||||
<h3 class="page-sub-heading" ng-hide="ctrl.isNew">Edit Notification Channel</h3>
|
||||
<h3 class="page-sub-heading" ng-show="ctrl.isNew">New Notification Channel</h3>
|
||||
|
||||
<form name="ctrl.theForm" ng-if="ctrl.notifiers">
|
||||
<div class="gf-form-group">
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-12">Name</span>
|
||||
<input type="text" required class="gf-form-input max-width-15" ng-model="ctrl.model.name" required></input>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-12">Type</span>
|
||||
<div class="gf-form-select-wrapper width-15">
|
||||
<select class="gf-form-input" ng-model="ctrl.model.type" ng-options="t.type as t.name for t in ctrl.notifiers" ng-change="ctrl.typeChanged(notification, $index)">
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<gf-form-switch
|
||||
class="gf-form"
|
||||
label="Default (send on all alerts)"
|
||||
label-class="width-14"
|
||||
checked="ctrl.model.isDefault"
|
||||
tooltip="Use this notification for all alerts">
|
||||
</gf-form-switch>
|
||||
<gf-form-switch
|
||||
class="gf-form"
|
||||
label="Include image"
|
||||
label-class="width-14"
|
||||
checked="ctrl.model.settings.uploadImage"
|
||||
tooltip="Captures an image and include it in the notification">
|
||||
</gf-form-switch>
|
||||
<div class="grafana-info-box m-t-2" ng-show="ctrl.model.settings.uploadImage && !ctrl.rendererAvailable">
|
||||
<h5>No image renderer available/installed</h5>
|
||||
<p>
|
||||
Grafana cannot find an image renderer to capture an image for the notification.
|
||||
Please make sure the <a href="https://grafana.com/grafana/plugins/grafana-image-renderer" target="_blank" rel="noreferrer">Grafana Image Renderer plugin</a> is installed.
|
||||
</p>
|
||||
<p>
|
||||
Please contact your Grafana administrator to install the plugin.
|
||||
</p>
|
||||
</div>
|
||||
<gf-form-switch
|
||||
class="gf-form"
|
||||
label="Disable Resolve Message"
|
||||
label-class="width-14"
|
||||
checked="ctrl.model.disableResolveMessage"
|
||||
tooltip="Disable the resolve message [OK] that is sent when alerting state returns to false">
|
||||
</gf-form-switch>
|
||||
<gf-form-switch
|
||||
class="gf-form"
|
||||
label="Send reminders"
|
||||
label-class="width-14"
|
||||
checked="ctrl.model.sendReminder"
|
||||
tooltip="Send additional notifications for triggered alerts">
|
||||
</gf-form-switch>
|
||||
<div class="gf-form-inline">
|
||||
<div class="gf-form" ng-if="ctrl.model.sendReminder">
|
||||
<span class="gf-form-label width-12">Send reminder every
|
||||
<info-popover mode="right-normal" position="top center">
|
||||
Specify how often reminders should be sent, e.g. every 30s, 1m, 10m, 30m or 1h etc.
|
||||
</info-popover>
|
||||
</span>
|
||||
<input type="text" placeholder="Select or specify custom" class="gf-form-input width-15" ng-model="ctrl.model.frequency"
|
||||
bs-typeahead="ctrl.getFrequencySuggestion" data-min-length=0 ng-required="ctrl.model.sendReminder">
|
||||
</div>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<span class="alert alert-info width-30" ng-if="ctrl.model.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.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="gf-form-group" ng-include src="ctrl.notifierTemplateId">
|
||||
</div>
|
||||
|
||||
<div class="gf-form-group gf-form-button-row">
|
||||
<button type="submit" ng-click="ctrl.save()" class="btn btn-primary width-7">Save</button>
|
||||
<button type="submit" ng-click="ctrl.testNotification()" class="btn btn-secondary">Send Test</button>
|
||||
<button type="delete" ng-if="!ctrl.isNew" ng-click="ctrl.deleteNotification()" class="btn btn-danger width-7">Delete</button>
|
||||
<a href="alerting/notifications" class="btn btn-inverse">Back</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<footer />
|
||||
@@ -3,7 +3,7 @@ import { getBackendSrv } from '@grafana/runtime';
|
||||
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';
|
||||
import { notificationChannelLoaded, loadAlertRules, loadedAlertRules, setNotificationChannels } from './reducers';
|
||||
|
||||
export function getAlertRulesAsync(options: { state: string }): ThunkResult<void> {
|
||||
return async dispatch => {
|
||||
@@ -33,9 +33,22 @@ export function createNotificationChannel(data: any): ThunkResult<void> {
|
||||
};
|
||||
}
|
||||
|
||||
export function updateNotificationChannel(data: any): ThunkResult<void> {
|
||||
return async dispatch => {
|
||||
try {
|
||||
await getBackendSrv().put(`/api/alert-notifications/${data.id}`, data);
|
||||
appEvents.emit(AppEvents.alertSuccess, ['Notification updated']);
|
||||
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);
|
||||
return async (dispatch, getState) => {
|
||||
const channel = getState().notificationChannel.notificationChannel;
|
||||
await getBackendSrv().post('/api/alert-notifications/test', { id: channel.id, ...data });
|
||||
};
|
||||
}
|
||||
|
||||
@@ -43,22 +56,20 @@ 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;
|
||||
});
|
||||
const notificationTypes = alertNotifiers.sort((o1, o2) => {
|
||||
if (o1.name > o2.name) {
|
||||
return 1;
|
||||
}
|
||||
return -1;
|
||||
});
|
||||
|
||||
dispatch(setNotificationChannels(notificationTypes));
|
||||
};
|
||||
}
|
||||
|
||||
export function loadNotificationChannel(id: number): ThunkResult<void> {
|
||||
return async dispatch => {
|
||||
const notificationChannel = await getBackendSrv().get(`/api/alert-notifications/${id}`);
|
||||
dispatch(notificationChannelLoaded(notificationChannel));
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,6 +1,15 @@
|
||||
import { dateTime } from '@grafana/data';
|
||||
import { alertRulesReducer, initialState, loadAlertRules, loadedAlertRules, setSearchQuery } from './reducers';
|
||||
import { AlertRuleDTO, AlertRulesState } from 'app/types';
|
||||
import {
|
||||
alertRulesReducer,
|
||||
initialChannelState,
|
||||
initialState,
|
||||
loadAlertRules,
|
||||
loadedAlertRules,
|
||||
notificationChannelReducer,
|
||||
setSearchQuery,
|
||||
notificationChannelLoaded,
|
||||
} from './reducers';
|
||||
import { AlertRuleDTO, AlertRulesState, NotificationChannelState, NotifierDTO } from 'app/types';
|
||||
import { reducerTester } from '../../../../test/core/redux/reducerTester';
|
||||
|
||||
describe('Alert rules', () => {
|
||||
@@ -225,3 +234,190 @@ describe('Alert rules', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Notification channel', () => {
|
||||
const notifiers: NotifierDTO[] = [
|
||||
{
|
||||
type: 'webhook',
|
||||
name: 'webhook',
|
||||
heading: 'Webhook settings',
|
||||
description: 'Sends HTTP POST request to a URL',
|
||||
info: '',
|
||||
options: [
|
||||
{
|
||||
element: 'input',
|
||||
inputType: 'text',
|
||||
label: 'Url',
|
||||
description: '',
|
||||
placeholder: '',
|
||||
propertyName: 'url',
|
||||
showWhen: { field: '', is: '' },
|
||||
required: true,
|
||||
validationRule: '',
|
||||
secure: false,
|
||||
},
|
||||
{
|
||||
element: 'select',
|
||||
inputType: '',
|
||||
label: 'Http Method',
|
||||
description: '',
|
||||
placeholder: '',
|
||||
propertyName: 'httpMethod',
|
||||
selectOptions: [
|
||||
{ value: 'POST', label: 'POST' },
|
||||
{ value: 'PUT', label: 'PUT' },
|
||||
],
|
||||
showWhen: { field: '', is: '' },
|
||||
required: false,
|
||||
validationRule: '',
|
||||
secure: false,
|
||||
},
|
||||
{
|
||||
element: 'input',
|
||||
inputType: 'text',
|
||||
label: 'Username',
|
||||
description: '',
|
||||
placeholder: '',
|
||||
propertyName: 'username',
|
||||
showWhen: { field: '', is: '' },
|
||||
required: false,
|
||||
validationRule: '',
|
||||
secure: false,
|
||||
},
|
||||
{
|
||||
element: 'input',
|
||||
inputType: 'password',
|
||||
label: 'Password',
|
||||
description: '',
|
||||
placeholder: '',
|
||||
propertyName: 'password',
|
||||
showWhen: { field: '', is: '' },
|
||||
required: false,
|
||||
validationRule: '',
|
||||
secure: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
describe('Load notification channel', () => {
|
||||
it('should migrate non secure settings to secure fields', () => {
|
||||
const payload = {
|
||||
id: 2,
|
||||
uid: '9L3FrrHGk',
|
||||
name: 'Webhook test',
|
||||
type: 'webhook',
|
||||
isDefault: false,
|
||||
sendReminder: false,
|
||||
disableResolveMessage: false,
|
||||
frequency: '',
|
||||
created: '2020-08-28T08:49:24Z',
|
||||
updated: '2020-08-28T08:49:24Z',
|
||||
settings: {
|
||||
autoResolve: true,
|
||||
httpMethod: 'POST',
|
||||
password: 'asdf',
|
||||
severity: 'critical',
|
||||
uploadImage: true,
|
||||
url: 'http://localhost.webhook',
|
||||
username: 'asdf',
|
||||
},
|
||||
};
|
||||
|
||||
const expected = {
|
||||
id: 2,
|
||||
uid: '9L3FrrHGk',
|
||||
name: 'Webhook test',
|
||||
type: 'webhook',
|
||||
isDefault: false,
|
||||
sendReminder: false,
|
||||
disableResolveMessage: false,
|
||||
frequency: '',
|
||||
created: '2020-08-28T08:49:24Z',
|
||||
updated: '2020-08-28T08:49:24Z',
|
||||
secureSettings: {
|
||||
password: 'asdf',
|
||||
},
|
||||
settings: {
|
||||
autoResolve: true,
|
||||
httpMethod: 'POST',
|
||||
password: '',
|
||||
severity: 'critical',
|
||||
uploadImage: true,
|
||||
url: 'http://localhost.webhook',
|
||||
username: 'asdf',
|
||||
},
|
||||
};
|
||||
|
||||
reducerTester<NotificationChannelState>()
|
||||
.givenReducer(notificationChannelReducer, { ...initialChannelState, notifiers: notifiers })
|
||||
.whenActionIsDispatched(notificationChannelLoaded(payload))
|
||||
.thenStateShouldEqual({
|
||||
...initialChannelState,
|
||||
notifiers: notifiers,
|
||||
notificationChannel: expected,
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle already secure field', () => {
|
||||
const payload = {
|
||||
id: 2,
|
||||
uid: '9L3FrrHGk',
|
||||
name: 'Webhook test',
|
||||
type: 'webhook',
|
||||
isDefault: false,
|
||||
sendReminder: false,
|
||||
disableResolveMessage: false,
|
||||
frequency: '',
|
||||
created: '2020-08-28T08:49:24Z',
|
||||
updated: '2020-08-28T08:49:24Z',
|
||||
secureFields: {
|
||||
password: true,
|
||||
},
|
||||
settings: {
|
||||
autoResolve: true,
|
||||
httpMethod: 'POST',
|
||||
password: '',
|
||||
severity: 'critical',
|
||||
uploadImage: true,
|
||||
url: 'http://localhost.webhook',
|
||||
username: 'asdf',
|
||||
},
|
||||
};
|
||||
|
||||
const expected = {
|
||||
id: 2,
|
||||
uid: '9L3FrrHGk',
|
||||
name: 'Webhook test',
|
||||
type: 'webhook',
|
||||
isDefault: false,
|
||||
sendReminder: false,
|
||||
disableResolveMessage: false,
|
||||
frequency: '',
|
||||
created: '2020-08-28T08:49:24Z',
|
||||
updated: '2020-08-28T08:49:24Z',
|
||||
secureFields: {
|
||||
password: true,
|
||||
},
|
||||
settings: {
|
||||
autoResolve: true,
|
||||
httpMethod: 'POST',
|
||||
password: '',
|
||||
severity: 'critical',
|
||||
uploadImage: true,
|
||||
url: 'http://localhost.webhook',
|
||||
username: 'asdf',
|
||||
},
|
||||
};
|
||||
|
||||
reducerTester<NotificationChannelState>()
|
||||
.givenReducer(notificationChannelReducer, { ...initialChannelState, notifiers: notifiers })
|
||||
.whenActionIsDispatched(notificationChannelLoaded(payload))
|
||||
.thenStateShouldEqual({
|
||||
...initialChannelState,
|
||||
notifiers: notifiers,
|
||||
notificationChannel: expected,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,9 +1,26 @@
|
||||
import { AlertRule, AlertRuleDTO, AlertRulesState, NotificationChannel } from 'app/types';
|
||||
import {
|
||||
AlertRule,
|
||||
AlertRuleDTO,
|
||||
AlertRulesState,
|
||||
NotificationChannelOption,
|
||||
NotificationChannelState,
|
||||
NotifierDTO,
|
||||
} 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, notificationChannels: [] };
|
||||
export const initialState: AlertRulesState = {
|
||||
items: [],
|
||||
searchQuery: '',
|
||||
isLoading: false,
|
||||
};
|
||||
|
||||
export const initialChannelState: NotificationChannelState = {
|
||||
notificationChannelTypes: [],
|
||||
notificationChannel: {},
|
||||
notifiers: [],
|
||||
};
|
||||
|
||||
function convertToAlertRule(dto: AlertRuleDTO, state: string): AlertRule {
|
||||
const stateModel = alertDef.getStateDisplayModel(state);
|
||||
@@ -47,16 +64,103 @@ 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 };
|
||||
},
|
||||
});
|
||||
|
||||
const notificationChannelSlice = createSlice({
|
||||
name: 'notificationChannel',
|
||||
initialState: initialChannelState,
|
||||
reducers: {
|
||||
setNotificationChannels: (state, action: PayloadAction<NotifierDTO[]>): NotificationChannelState => {
|
||||
return {
|
||||
...state,
|
||||
notificationChannelTypes: transformNotifiers(action.payload),
|
||||
notifiers: action.payload,
|
||||
};
|
||||
},
|
||||
notificationChannelLoaded: (state, action: PayloadAction<any>): NotificationChannelState => {
|
||||
const notificationChannel = action.payload;
|
||||
const selectedType: NotifierDTO = state.notifiers.find(t => t.type === notificationChannel.type)!;
|
||||
const secureChannelOptions = selectedType.options.filter((o: NotificationChannelOption) => o.secure);
|
||||
/*
|
||||
If any secure field is in plain text we need to migrate it to use secure field instead.
|
||||
*/
|
||||
if (
|
||||
secureChannelOptions.length > 0 &&
|
||||
secureChannelOptions.some((o: NotificationChannelOption) => {
|
||||
return notificationChannel.settings[o.propertyName] !== '';
|
||||
})
|
||||
) {
|
||||
return migrateSecureFields(state, action.payload, secureChannelOptions);
|
||||
}
|
||||
|
||||
return { ...state, notificationChannel: notificationChannel };
|
||||
},
|
||||
resetSecureField: (state, action: PayloadAction<string>): NotificationChannelState => {
|
||||
return {
|
||||
...state,
|
||||
notificationChannel: {
|
||||
...state.notificationChannel,
|
||||
secureFields: { ...state.notificationChannel.secureFields, [action.payload]: false },
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const { loadAlertRules, loadedAlertRules, setSearchQuery, setNotificationChannels } = alertRulesSlice.actions;
|
||||
export const { loadAlertRules, loadedAlertRules, setSearchQuery } = alertRulesSlice.actions;
|
||||
|
||||
export const {
|
||||
setNotificationChannels,
|
||||
notificationChannelLoaded,
|
||||
resetSecureField,
|
||||
} = notificationChannelSlice.actions;
|
||||
|
||||
export const alertRulesReducer = alertRulesSlice.reducer;
|
||||
export const notificationChannelReducer = notificationChannelSlice.reducer;
|
||||
|
||||
export default {
|
||||
alertRules: alertRulesReducer,
|
||||
notificationChannel: notificationChannelReducer,
|
||||
};
|
||||
|
||||
function migrateSecureFields(
|
||||
state: NotificationChannelState,
|
||||
notificationChannel: any,
|
||||
secureChannelOptions: NotificationChannelOption[]
|
||||
) {
|
||||
const cleanedSettings: { [key: string]: string } = {};
|
||||
const secureSettings: { [key: string]: string } = {};
|
||||
|
||||
secureChannelOptions.forEach(option => {
|
||||
secureSettings[option.propertyName] = notificationChannel.settings[option.propertyName];
|
||||
cleanedSettings[option.propertyName] = '';
|
||||
});
|
||||
|
||||
return {
|
||||
...state,
|
||||
notificationChannel: {
|
||||
...notificationChannel,
|
||||
settings: { ...notificationChannel.settings, ...cleanedSettings },
|
||||
secureSettings: { ...secureSettings },
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function transformNotifiers(notifiers: NotifierDTO[]) {
|
||||
return notifiers
|
||||
.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;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { AlertRulesState } from 'app/types';
|
||||
import { AlertRulesState, NotificationChannelState } from 'app/types';
|
||||
|
||||
export const getSearchQuery = (state: AlertRulesState) => state.searchQuery;
|
||||
|
||||
@@ -9,3 +9,11 @@ export const getAlertRuleItems = (state: AlertRulesState) => {
|
||||
return regex.test(item.name) || regex.test(item.stateText) || regex.test(item.info!);
|
||||
});
|
||||
};
|
||||
|
||||
export const getNotificationChannel = (state: NotificationChannelState, channelId: number) => {
|
||||
if (state.notificationChannel.id === channelId) {
|
||||
return state.notificationChannel;
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
209
public/app/features/alerting/utils/notificationChannel.test.ts
Normal file
209
public/app/features/alerting/utils/notificationChannel.test.ts
Normal file
@@ -0,0 +1,209 @@
|
||||
import { transformSubmitData } from './notificationChannels';
|
||||
import { NotificationChannelDTO } from '../../../types';
|
||||
|
||||
const basicFormData: NotificationChannelDTO = {
|
||||
id: 1,
|
||||
uid: 'pX7fbbHGk',
|
||||
name: 'Pete discord',
|
||||
type: {
|
||||
value: 'discord',
|
||||
label: 'Discord',
|
||||
type: 'discord',
|
||||
name: 'Discord',
|
||||
heading: 'Discord settings',
|
||||
description: 'Sends notifications to Discord',
|
||||
info: '',
|
||||
options: [
|
||||
{
|
||||
element: 'input',
|
||||
inputType: 'text',
|
||||
label: 'Message Content',
|
||||
description: 'Mention a group using @ or a user using <@ID> when notifying in a channel',
|
||||
placeholder: '',
|
||||
propertyName: 'content',
|
||||
selectOptions: null,
|
||||
showWhen: { field: '', is: '' },
|
||||
required: false,
|
||||
validationRule: '',
|
||||
secure: false,
|
||||
},
|
||||
{
|
||||
element: 'input',
|
||||
inputType: 'text',
|
||||
label: 'Webhook URL',
|
||||
description: '',
|
||||
placeholder: 'Discord webhook URL',
|
||||
propertyName: 'url',
|
||||
selectOptions: null,
|
||||
showWhen: { field: '', is: '' },
|
||||
required: true,
|
||||
validationRule: '',
|
||||
secure: false,
|
||||
},
|
||||
],
|
||||
typeName: 'discord',
|
||||
},
|
||||
isDefault: false,
|
||||
sendReminder: false,
|
||||
disableResolveMessage: false,
|
||||
frequency: '',
|
||||
created: '2020-08-24T10:46:43+02:00',
|
||||
updated: '2020-09-02T14:08:27+02:00',
|
||||
settings: {
|
||||
url: 'https://discordapp.com/api/webhooks/',
|
||||
uploadImage: true,
|
||||
content: '',
|
||||
autoResolve: true,
|
||||
httpMethod: 'POST',
|
||||
severity: 'critical',
|
||||
},
|
||||
secureFields: {},
|
||||
secureSettings: {},
|
||||
};
|
||||
|
||||
const selectFormData: NotificationChannelDTO = {
|
||||
id: 23,
|
||||
uid: 'BxEN9rNGk',
|
||||
name: 'Webhook',
|
||||
type: {
|
||||
value: 'webhook',
|
||||
label: 'webhook',
|
||||
type: 'webhook',
|
||||
name: 'webhook',
|
||||
heading: 'Webhook settings',
|
||||
description: 'Sends HTTP POST request to a URL',
|
||||
info: '',
|
||||
options: [
|
||||
{
|
||||
element: 'input',
|
||||
inputType: 'text',
|
||||
label: 'Url',
|
||||
description: '',
|
||||
placeholder: '',
|
||||
propertyName: 'url',
|
||||
selectOptions: null,
|
||||
showWhen: { field: '', is: '' },
|
||||
required: true,
|
||||
validationRule: '',
|
||||
secure: false,
|
||||
},
|
||||
{
|
||||
element: 'select',
|
||||
inputType: '',
|
||||
label: 'Http Method',
|
||||
description: '',
|
||||
placeholder: '',
|
||||
propertyName: 'httpMethod',
|
||||
selectOptions: [
|
||||
{ value: 'POST', label: 'POST' },
|
||||
{ value: 'PUT', label: 'PUT' },
|
||||
],
|
||||
showWhen: { field: '', is: '' },
|
||||
required: false,
|
||||
validationRule: '',
|
||||
secure: false,
|
||||
},
|
||||
{
|
||||
element: 'input',
|
||||
inputType: 'text',
|
||||
label: 'Username',
|
||||
description: '',
|
||||
placeholder: '',
|
||||
propertyName: 'username',
|
||||
selectOptions: null,
|
||||
showWhen: { field: '', is: '' },
|
||||
required: false,
|
||||
validationRule: '',
|
||||
secure: false,
|
||||
},
|
||||
{
|
||||
element: 'input',
|
||||
inputType: 'password',
|
||||
label: 'Password',
|
||||
description: '',
|
||||
placeholder: '',
|
||||
propertyName: 'password',
|
||||
selectOptions: null,
|
||||
showWhen: { field: '', is: '' },
|
||||
required: false,
|
||||
validationRule: '',
|
||||
secure: true,
|
||||
},
|
||||
],
|
||||
typeName: 'webhook',
|
||||
},
|
||||
isDefault: false,
|
||||
sendReminder: false,
|
||||
disableResolveMessage: false,
|
||||
frequency: '',
|
||||
created: '2020-08-28T10:47:37+02:00',
|
||||
updated: '2020-09-03T09:37:21+02:00',
|
||||
settings: {
|
||||
autoResolve: true,
|
||||
httpMethod: 'POST',
|
||||
password: '',
|
||||
severity: 'critical',
|
||||
uploadImage: true,
|
||||
url: 'http://asdf',
|
||||
username: 'asdf',
|
||||
},
|
||||
secureFields: { password: true },
|
||||
secureSettings: {},
|
||||
};
|
||||
|
||||
describe('Transform submit data', () => {
|
||||
it('basic transform', () => {
|
||||
const expected = {
|
||||
id: 1,
|
||||
name: 'Pete discord',
|
||||
type: 'discord',
|
||||
sendReminder: false,
|
||||
disableResolveMessage: false,
|
||||
frequency: '15m',
|
||||
settings: {
|
||||
uploadImage: true,
|
||||
autoResolve: true,
|
||||
httpMethod: 'POST',
|
||||
severity: 'critical',
|
||||
url: 'https://discordapp.com/api/webhooks/',
|
||||
content: '',
|
||||
},
|
||||
secureSettings: {},
|
||||
secureFields: {},
|
||||
isDefault: false,
|
||||
uid: 'pX7fbbHGk',
|
||||
created: '2020-08-24T10:46:43+02:00',
|
||||
updated: '2020-09-02T14:08:27+02:00',
|
||||
};
|
||||
|
||||
expect(transformSubmitData(basicFormData)).toEqual(expected);
|
||||
});
|
||||
|
||||
it('should transform form data with selects', () => {
|
||||
const expected = {
|
||||
created: '2020-08-28T10:47:37+02:00',
|
||||
disableResolveMessage: false,
|
||||
frequency: '15m',
|
||||
id: 23,
|
||||
isDefault: false,
|
||||
name: 'Webhook',
|
||||
secureFields: { password: true },
|
||||
secureSettings: {},
|
||||
sendReminder: false,
|
||||
settings: {
|
||||
autoResolve: true,
|
||||
httpMethod: 'POST',
|
||||
password: '',
|
||||
severity: 'critical',
|
||||
uploadImage: true,
|
||||
url: 'http://asdf',
|
||||
username: 'asdf',
|
||||
},
|
||||
type: 'webhook',
|
||||
uid: 'BxEN9rNGk',
|
||||
updated: '2020-09-03T09:37:21+02:00',
|
||||
};
|
||||
|
||||
expect(transformSubmitData(selectFormData)).toEqual(expected);
|
||||
});
|
||||
});
|
||||
63
public/app/features/alerting/utils/notificationChannels.ts
Normal file
63
public/app/features/alerting/utils/notificationChannels.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
import memoizeOne from 'memoize-one';
|
||||
import { SelectableValue } from '@grafana/data';
|
||||
import { config } from '@grafana/runtime';
|
||||
import { NotificationChannelDTO, NotificationChannelType } from 'app/types';
|
||||
|
||||
export const defaultValues: NotificationChannelDTO = {
|
||||
id: -1,
|
||||
name: '',
|
||||
type: { value: 'email', label: 'Email' },
|
||||
sendReminder: false,
|
||||
disableResolveMessage: false,
|
||||
frequency: '15m',
|
||||
settings: {
|
||||
uploadImage: config.rendererAvailable,
|
||||
autoResolve: true,
|
||||
httpMethod: 'POST',
|
||||
severity: 'critical',
|
||||
},
|
||||
secureSettings: {},
|
||||
secureFields: {},
|
||||
isDefault: false,
|
||||
};
|
||||
|
||||
export const mapChannelsToSelectableValue = memoizeOne(
|
||||
(notificationChannels: NotificationChannelType[]): Array<SelectableValue<string>> => {
|
||||
return notificationChannels.map(channel => ({
|
||||
value: channel.value,
|
||||
label: channel.label,
|
||||
description: channel.description,
|
||||
}));
|
||||
}
|
||||
);
|
||||
|
||||
export const transformSubmitData = (formData: 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(formData.settings).map(([key, value]) => {
|
||||
return [key, value && value.hasOwnProperty('value') ? value.value : value];
|
||||
})
|
||||
);
|
||||
|
||||
return {
|
||||
...defaultValues,
|
||||
...formData,
|
||||
frequency: formData.frequency === '' ? defaultValues.frequency : formData.frequency,
|
||||
type: formData.type.value,
|
||||
settings: { ...defaultValues.settings, ...settings },
|
||||
secureSettings: { ...formData.secureSettings },
|
||||
};
|
||||
};
|
||||
|
||||
export const transformTestData = (formData: NotificationChannelDTO) => {
|
||||
return {
|
||||
name: formData.name,
|
||||
type: formData.type.value,
|
||||
frequency: formData.frequency ?? defaultValues.frequency,
|
||||
settings: { ...Object.assign(defaultValues.settings, formData.settings) },
|
||||
secureSettings: { ...formData.secureSettings },
|
||||
};
|
||||
};
|
||||
@@ -5,7 +5,6 @@ import './dashboard';
|
||||
import './playlist/all';
|
||||
import './panel/all';
|
||||
import './admin';
|
||||
import './alerting/NotificationsEditCtrl';
|
||||
import './manage-dashboards';
|
||||
import './profile/all';
|
||||
import './datasources/settings/HttpSettingsCtrl';
|
||||
|
||||
@@ -535,25 +535,22 @@ export function setupAngularRoutes($routeProvider: route.IRouteProvider, $locati
|
||||
},
|
||||
})
|
||||
.when('/alerting/notification/new', {
|
||||
templateUrl: 'public/app/features/alerting/partials/notification_edit.html',
|
||||
controller: 'AlertNotificationEditCtrl',
|
||||
controllerAs: 'ctrl',
|
||||
reloadOnSearch: false,
|
||||
})
|
||||
.when('/alerting/notification/new2', {
|
||||
template: '<react-container />',
|
||||
resolve: {
|
||||
component: () =>
|
||||
SafeDynamicImport(
|
||||
import(/* webpackChunkName: "NewNotificationChannel" */ 'app/features/alerting/NewAlertNotificationPage')
|
||||
import(/* webpackChunkName: "NewNotificationChannel" */ 'app/features/alerting/NewNotificationChannelPage')
|
||||
),
|
||||
},
|
||||
})
|
||||
.when('/alerting/notification/:id/edit', {
|
||||
templateUrl: 'public/app/features/alerting/partials/notification_edit.html',
|
||||
controller: 'AlertNotificationEditCtrl',
|
||||
controllerAs: 'ctrl',
|
||||
reloadOnSearch: false,
|
||||
template: '<react-container />',
|
||||
resolve: {
|
||||
component: () =>
|
||||
SafeDynamicImport(
|
||||
import(/* webpackChunkName: "EditNotificationChannel"*/ 'app/features/alerting/EditNotificationChannelPage')
|
||||
),
|
||||
},
|
||||
})
|
||||
.otherwise({
|
||||
template: '<react-container />',
|
||||
|
||||
@@ -58,34 +58,40 @@ export type NotifierType =
|
||||
export interface NotifierDTO {
|
||||
name: string;
|
||||
description: string;
|
||||
optionsTemplate: string;
|
||||
type: NotifierType;
|
||||
heading: string;
|
||||
options: Option[];
|
||||
options: NotificationChannelOption[];
|
||||
info?: string;
|
||||
secure?: boolean;
|
||||
}
|
||||
|
||||
export interface NotificationChannel {
|
||||
export interface NotificationChannelType {
|
||||
value: string;
|
||||
label: string;
|
||||
description: string;
|
||||
type: NotifierType;
|
||||
heading: string;
|
||||
options: Option[];
|
||||
options: NotificationChannelOption[];
|
||||
info?: string;
|
||||
}
|
||||
|
||||
export interface NotificationChannelDTO {
|
||||
[key: string]: string | boolean | SelectableValue<string>;
|
||||
[key: string]: string | boolean | number | SelectableValue<string>;
|
||||
id: number;
|
||||
name: string;
|
||||
type: SelectableValue<string>;
|
||||
sendReminder: boolean;
|
||||
disableResolveMessage: boolean;
|
||||
frequency: string;
|
||||
settings: ChannelTypeSettings;
|
||||
secureSettings: NotificationChannelSecureSettings;
|
||||
secureFields: NotificationChannelSecureFields;
|
||||
isDefault: boolean;
|
||||
}
|
||||
|
||||
export type NotificationChannelSecureSettings = Record<string, string | number>;
|
||||
export type NotificationChannelSecureFields = Record<string, boolean>;
|
||||
|
||||
export interface ChannelTypeSettings {
|
||||
[key: string]: any;
|
||||
autoResolve: true;
|
||||
@@ -94,24 +100,30 @@ export interface ChannelTypeSettings {
|
||||
uploadImage: boolean;
|
||||
}
|
||||
|
||||
export interface Option {
|
||||
element: 'input' | 'select' | 'switch' | 'textarea';
|
||||
export interface NotificationChannelOption {
|
||||
element: 'input' | 'select' | 'checkbox' | 'textarea';
|
||||
inputType: string;
|
||||
label: string;
|
||||
description: string;
|
||||
placeholder: string;
|
||||
propertyName: string;
|
||||
selectOptions: Array<SelectableValue<string>>;
|
||||
showWhen: { field: string; is: string };
|
||||
required: boolean;
|
||||
secure: boolean;
|
||||
selectOptions?: Array<SelectableValue<string>>;
|
||||
showWhen: { field: string; is: string };
|
||||
validationRule: string;
|
||||
}
|
||||
|
||||
export interface NotificationChannelState {
|
||||
notificationChannelTypes: NotificationChannelType[];
|
||||
notifiers: NotifierDTO[];
|
||||
notificationChannel: any;
|
||||
}
|
||||
|
||||
export interface AlertRulesState {
|
||||
items: AlertRule[];
|
||||
searchQuery: string;
|
||||
isLoading: boolean;
|
||||
notificationChannels: NotificationChannel[];
|
||||
}
|
||||
|
||||
export interface AlertNotification {
|
||||
|
||||
@@ -3,7 +3,7 @@ import { PayloadAction } from '@reduxjs/toolkit';
|
||||
import { NavIndex } from '@grafana/data';
|
||||
|
||||
import { LocationState } from './location';
|
||||
import { AlertRulesState } from './alerting';
|
||||
import { AlertRulesState, NotificationChannelState } from './alerting';
|
||||
import { TeamsState, TeamState } from './teams';
|
||||
import { FolderState } from './folders';
|
||||
import { DashboardState } from './dashboard';
|
||||
@@ -44,6 +44,7 @@ export interface StoreState {
|
||||
userListAdmin: UserListAdminState;
|
||||
templating: TemplatingState;
|
||||
importDashboard: ImportDashboardState;
|
||||
notificationChannel: NotificationChannelState;
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
Reference in New Issue
Block a user