Merge pull request #1724 from influxdata/feature/pushover_support-1680

Add Pushover alert support
pull/10616/head
Jared Scheib 2017-07-21 14:04:22 -07:00 committed by GitHub
commit d6db7ee084
19 changed files with 792 additions and 283 deletions

View File

@ -11,6 +11,7 @@
1. [#1681](https://github.com/influxdata/chronograf/pull/1681): Add the ability to select Custom Time Ranges in the Hostpages, Data Explorer, and Dashboards
1. [#1752](https://github.com/influxdata/chronograf/pull/1752): Clarify BoltPath server flag help text by making example the default path
1. [#1738](https://github.com/influxdata/chronograf/pull/1738): Add shared secret JWT authorization to InfluxDB
1. [#1724](https://github.com/influxdata/chronograf/pull/1724): Add Pushover alert support
### UI Improvements
1. [#1707](https://github.com/influxdata/chronograf/pull/1707): Polish alerts table in status page to wrap text less

View File

@ -21,7 +21,7 @@ func kapaHandler(handler string) (string, error) {
return "email", nil
case "http":
return "post", nil
case "alerta", "sensu", "slack", "email", "talk", "telegram", "post", "tcp", "exec", "log":
case "alerta", "sensu", "slack", "email", "talk", "telegram", "post", "tcp", "exec", "log", "pushover":
return handler, nil
default:
return "", fmt.Errorf("Unsupported alert handler %s", handler)

View File

@ -412,7 +412,6 @@ func Reverse(script chronograf.TICKScript) (chronograf.AlertRule, error) {
rule.Query.RetentionPolicy = commonVars.RP
rule.Query.Measurement = commonVars.Measurement
rule.Query.GroupBy.Tags = commonVars.GroupBy
if commonVars.Filter.Operator == "==" {
rule.Query.AreTagsAccepted = true
}
@ -492,6 +491,7 @@ func extractAlertNodes(p *pipeline.Pipeline, rule *chronograf.AlertRule) {
extractSlack(t, rule)
extractTalk(t, rule)
extractTelegram(t, rule)
extractPushover(t, rule)
extractTCP(t, rule)
extractLog(t, rule)
extractExec(t, rule)
@ -501,7 +501,7 @@ func extractAlertNodes(p *pipeline.Pipeline, rule *chronograf.AlertRule) {
}
func extractHipchat(node *pipeline.AlertNode, rule *chronograf.AlertRule) {
if node.HipChatHandlers == nil {
if len(node.HipChatHandlers) == 0 {
return
}
rule.Alerts = append(rule.Alerts, "hipchat")
@ -527,7 +527,7 @@ func extractHipchat(node *pipeline.AlertNode, rule *chronograf.AlertRule) {
}
func extractOpsgenie(node *pipeline.AlertNode, rule *chronograf.AlertRule) {
if node.OpsGenieHandlers == nil {
if len(node.OpsGenieHandlers) == 0 {
return
}
rule.Alerts = append(rule.Alerts, "opsgenie")
@ -553,7 +553,7 @@ func extractOpsgenie(node *pipeline.AlertNode, rule *chronograf.AlertRule) {
}
func extractPagerduty(node *pipeline.AlertNode, rule *chronograf.AlertRule) {
if node.PagerDutyHandlers == nil {
if len(node.PagerDutyHandlers) == 0 {
return
}
rule.Alerts = append(rule.Alerts, "pagerduty")
@ -572,7 +572,7 @@ func extractPagerduty(node *pipeline.AlertNode, rule *chronograf.AlertRule) {
}
func extractVictorops(node *pipeline.AlertNode, rule *chronograf.AlertRule) {
if node.VictorOpsHandlers == nil {
if len(node.VictorOpsHandlers) == 0 {
return
}
rule.Alerts = append(rule.Alerts, "victorops")
@ -591,7 +591,7 @@ func extractVictorops(node *pipeline.AlertNode, rule *chronograf.AlertRule) {
}
func extractEmail(node *pipeline.AlertNode, rule *chronograf.AlertRule) {
if node.EmailHandlers == nil {
if len(node.EmailHandlers) == 0 {
return
}
rule.Alerts = append(rule.Alerts, "smtp")
@ -607,7 +607,7 @@ func extractEmail(node *pipeline.AlertNode, rule *chronograf.AlertRule) {
}
func extractPost(node *pipeline.AlertNode, rule *chronograf.AlertRule) {
if node.HTTPPostHandlers == nil {
if len(node.HTTPPostHandlers) == 0 {
return
}
rule.Alerts = append(rule.Alerts, "http")
@ -640,7 +640,7 @@ func extractPost(node *pipeline.AlertNode, rule *chronograf.AlertRule) {
}
func extractAlerta(node *pipeline.AlertNode, rule *chronograf.AlertRule) {
if node.AlertaHandlers == nil {
if len(node.AlertaHandlers) == 0 {
return
}
rule.Alerts = append(rule.Alerts, "alerta")
@ -709,7 +709,7 @@ func extractAlerta(node *pipeline.AlertNode, rule *chronograf.AlertRule) {
}
func extractSensu(node *pipeline.AlertNode, rule *chronograf.AlertRule) {
if node.SensuHandlers == nil {
if len(node.SensuHandlers) == 0 {
return
}
rule.Alerts = append(rule.Alerts, "sensu")
@ -721,7 +721,7 @@ func extractSensu(node *pipeline.AlertNode, rule *chronograf.AlertRule) {
}
func extractSlack(node *pipeline.AlertNode, rule *chronograf.AlertRule) {
if node.SlackHandlers == nil {
if len(node.SlackHandlers) == 0 {
return
}
rule.Alerts = append(rule.Alerts, "slack")
@ -753,7 +753,7 @@ func extractSlack(node *pipeline.AlertNode, rule *chronograf.AlertRule) {
rule.AlertNodes = append(rule.AlertNodes, alert)
}
func extractTalk(node *pipeline.AlertNode, rule *chronograf.AlertRule) {
if node.TalkHandlers == nil {
if len(node.TalkHandlers) == 0 {
return
}
rule.Alerts = append(rule.Alerts, "talk")
@ -764,7 +764,7 @@ func extractTalk(node *pipeline.AlertNode, rule *chronograf.AlertRule) {
rule.AlertNodes = append(rule.AlertNodes, alert)
}
func extractTelegram(node *pipeline.AlertNode, rule *chronograf.AlertRule) {
if node.TelegramHandlers == nil {
if len(node.TelegramHandlers) == 0 {
return
}
rule.Alerts = append(rule.Alerts, "telegram")
@ -802,7 +802,7 @@ func extractTelegram(node *pipeline.AlertNode, rule *chronograf.AlertRule) {
}
func extractTCP(node *pipeline.AlertNode, rule *chronograf.AlertRule) {
if node.TcpHandlers == nil {
if len(node.TcpHandlers) == 0 {
return
}
rule.Alerts = append(rule.Alerts, "tcp")
@ -819,7 +819,7 @@ func extractTCP(node *pipeline.AlertNode, rule *chronograf.AlertRule) {
}
func extractLog(node *pipeline.AlertNode, rule *chronograf.AlertRule) {
if node.LogHandlers == nil {
if len(node.LogHandlers) == 0 {
return
}
rule.Alerts = append(rule.Alerts, "log")
@ -836,7 +836,7 @@ func extractLog(node *pipeline.AlertNode, rule *chronograf.AlertRule) {
}
func extractExec(node *pipeline.AlertNode, rule *chronograf.AlertRule) {
if node.ExecHandlers == nil {
if len(node.ExecHandlers) == 0 {
return
}
rule.Alerts = append(rule.Alerts, "exec")
@ -851,3 +851,51 @@ func extractExec(node *pipeline.AlertNode, rule *chronograf.AlertRule) {
rule.AlertNodes = append(rule.AlertNodes, alert)
}
func extractPushover(node *pipeline.AlertNode, rule *chronograf.AlertRule) {
if len(node.PushoverHandlers) == 0 {
return
}
rule.Alerts = append(rule.Alerts, "pushover")
a := node.PushoverHandlers[0]
alert := chronograf.KapacitorNode{
Name: "pushover",
}
if a.Device != "" {
alert.Properties = append(alert.Properties, chronograf.KapacitorProperty{
Name: "device",
Args: []string{a.Device},
})
}
if a.Title != "" {
alert.Properties = append(alert.Properties, chronograf.KapacitorProperty{
Name: "title",
Args: []string{a.Title},
})
}
if a.URL != "" {
alert.Properties = append(alert.Properties, chronograf.KapacitorProperty{
Name: "URL",
Args: []string{a.URL},
})
}
if a.URLTitle != "" {
alert.Properties = append(alert.Properties, chronograf.KapacitorProperty{
Name: "URLTitle",
Args: []string{a.URLTitle},
})
}
if a.Sound != "" {
alert.Properties = append(alert.Properties, chronograf.KapacitorProperty{
Name: "sound",
Args: []string{a.Sound},
})
}
rule.AlertNodes = append(rule.AlertNodes, alert)
}

View File

@ -2714,6 +2714,7 @@
"hipchat",
"opsgenie",
"pagerduty",
"pushover",
"victorops",
"smtp",
"email",

View File

@ -11,6 +11,7 @@ import {
updateMessage,
updateAlerts,
updateAlertNodes,
updateAlertProperty,
updateRuleName,
deleteRuleSuccess,
updateRuleStatusSuccess,
@ -200,6 +201,106 @@ describe('Kapacitor.Reducers.rules', () => {
expect(newState[ruleID].details).to.equal(details)
})
it('can update properties', () => {
const ruleID = 1
const alertNodeName = 'pushover'
const alertProperty1_Name = 'device'
const alertProperty1_ArgsOrig =
'pineapple_kingdom_control_room,bob_cOreos_watch'
const alertProperty1_ArgsDiff = 'pineapple_kingdom_control_tower'
const alertProperty2_Name = 'URLTitle'
const alertProperty2_ArgsOrig = 'Cubeapple Rising'
const alertProperty2_ArgsDiff = 'Cubeapple Falling'
const alertProperty1_Orig = {
name: alertProperty1_Name,
args: [alertProperty1_ArgsOrig],
}
const alertProperty1_Diff = {
name: alertProperty1_Name,
args: [alertProperty1_ArgsDiff],
}
const alertProperty2_Orig = {
name: alertProperty2_Name,
args: [alertProperty2_ArgsOrig],
}
const alertProperty2_Diff = {
name: alertProperty2_Name,
args: [alertProperty2_ArgsDiff],
}
const initialState = {
[ruleID]: {
id: ruleID,
alertNodes: [
{
name: 'pushover',
args: null,
properties: null,
},
],
},
}
const getAlertPropertyArgs = (matchState, propertyName) =>
matchState[ruleID].alertNodes
.find(node => node.name === alertNodeName)
.properties.find(property => property.name === propertyName).args[0]
// add first property
let newState = reducer(
initialState,
updateAlertProperty(ruleID, alertNodeName, alertProperty1_Orig)
)
expect(getAlertPropertyArgs(newState, alertProperty1_Name)).to.equal(
alertProperty1_ArgsOrig
)
// change first property
newState = reducer(
initialState,
updateAlertProperty(ruleID, alertNodeName, alertProperty1_Diff)
)
expect(getAlertPropertyArgs(newState, alertProperty1_Name)).to.equal(
alertProperty1_ArgsDiff
)
// add second property
newState = reducer(
initialState,
updateAlertProperty(ruleID, alertNodeName, alertProperty2_Orig)
)
expect(getAlertPropertyArgs(newState, alertProperty1_Name)).to.equal(
alertProperty1_ArgsDiff
)
expect(getAlertPropertyArgs(newState, alertProperty2_Name)).to.equal(
alertProperty2_ArgsOrig
)
expect(
newState[ruleID].alertNodes.find(node => node.name === alertNodeName)
.properties.length
).to.equal(2)
// change second property
newState = reducer(
initialState,
updateAlertProperty(ruleID, alertNodeName, alertProperty2_Diff)
)
expect(getAlertPropertyArgs(newState, alertProperty1_Name)).to.equal(
alertProperty1_ArgsDiff
)
expect(getAlertPropertyArgs(newState, alertProperty2_Name)).to.equal(
alertProperty2_ArgsDiff
)
expect(
newState[ruleID].alertNodes.find(node => node.name === alertNodeName)
.properties.length
).to.equal(2)
})
it('can update status', () => {
const ruleID = 1
const status = 'enabled'

View File

@ -114,6 +114,15 @@ export function updateDetails(ruleID, details) {
}
}
export const updateAlertProperty = (ruleID, alertNodeName, alertProperty) => ({
type: 'UPDATE_RULE_ALERT_PROPERTY',
payload: {
ruleID,
alertNodeName,
alertProperty,
},
})
export function updateAlerts(ruleID, alerts) {
return {
type: 'UPDATE_RULE_ALERTS',
@ -124,12 +133,12 @@ export function updateAlerts(ruleID, alerts) {
}
}
export function updateAlertNodes(ruleID, alertType, alertNodesText) {
export function updateAlertNodes(ruleID, alertNodeName, alertNodesText) {
return {
type: 'UPDATE_RULE_ALERT_NODES',
payload: {
ruleID,
alertType,
alertNodeName,
alertNodesText,
},
}

View File

@ -13,6 +13,7 @@ import {
HipChatConfig,
OpsGenieConfig,
PagerDutyConfig,
PushoverConfig,
SensuConfig,
SlackConfig,
SMTPConfig,
@ -124,99 +125,97 @@ class AlertTabs extends Component {
this.handleTest('slack', properties)
}
const tabs = [
{
const supportedConfigs = {
alerta: {
type: 'Alerta',
component: (
renderComponent: () =>
<AlertaConfig
onSave={p => this.handleSaveConfig('alerta', p)}
config={this.getSection(configSections, 'alerta')}
/>
),
/>,
},
{
type: 'SMTP',
component: (
<SMTPConfig
onSave={p => this.handleSaveConfig('smtp', p)}
config={this.getSection(configSections, 'smtp')}
/>
),
hipchat: {
type: 'HipChat',
renderComponent: () =>
<HipChatConfig
onSave={p => this.handleSaveConfig('hipchat', p)}
config={this.getSection(configSections, 'hipchat')}
/>,
},
{
opsgenie: {
type: 'OpsGenie',
renderComponent: () =>
<OpsGenieConfig
onSave={p => this.handleSaveConfig('opsgenie', p)}
config={this.getSection(configSections, 'opsgenie')}
/>,
},
pagerduty: {
type: 'PagerDuty',
renderComponent: () =>
<PagerDutyConfig
onSave={p => this.handleSaveConfig('pagerduty', p)}
config={this.getSection(configSections, 'pagerduty')}
/>,
},
pushover: {
type: 'Pushover',
renderComponent: () =>
<PushoverConfig
onSave={p => this.handleSaveConfig('pushover', p)}
config={this.getSection(configSections, 'pushover')}
/>,
},
sensu: {
type: 'Sensu',
renderComponent: () =>
<SensuConfig
onSave={p => this.handleSaveConfig('sensu', p)}
config={this.getSection(configSections, 'sensu')}
/>,
},
slack: {
type: 'Slack',
component: (
renderComponent: () =>
<SlackConfig
onSave={p => this.handleSaveConfig('slack', p)}
onTest={test}
config={this.getSection(configSections, 'slack')}
/>
),
/>,
},
{
type: 'VictorOps',
component: (
<VictorOpsConfig
onSave={p => this.handleSaveConfig('victorops', p)}
config={this.getSection(configSections, 'victorops')}
/>
),
smtp: {
type: 'SMTP',
renderComponent: () =>
<SMTPConfig
onSave={p => this.handleSaveConfig('smtp', p)}
config={this.getSection(configSections, 'smtp')}
/>,
},
{
type: 'Telegram',
component: (
<TelegramConfig
onSave={p => this.handleSaveConfig('telegram', p)}
config={this.getSection(configSections, 'telegram')}
/>
),
},
{
type: 'OpsGenie',
component: (
<OpsGenieConfig
onSave={p => this.handleSaveConfig('opsgenie', p)}
config={this.getSection(configSections, 'opsgenie')}
/>
),
},
{
type: 'PagerDuty',
component: (
<PagerDutyConfig
onSave={p => this.handleSaveConfig('pagerduty', p)}
config={this.getSection(configSections, 'pagerduty')}
/>
),
},
{
type: 'HipChat',
component: (
<HipChatConfig
onSave={p => this.handleSaveConfig('hipchat', p)}
config={this.getSection(configSections, 'hipchat')}
/>
),
},
{
type: 'Sensu',
component: (
<SensuConfig
onSave={p => this.handleSaveConfig('sensu', p)}
config={this.getSection(configSections, 'sensu')}
/>
),
},
{
talk: {
type: 'Talk',
component: (
renderComponent: () =>
<TalkConfig
onSave={p => this.handleSaveConfig('talk', p)}
config={this.getSection(configSections, 'talk')}
/>
),
/>,
},
]
telegram: {
type: 'Telegram',
renderComponent: () =>
<TelegramConfig
onSave={p => this.handleSaveConfig('telegram', p)}
config={this.getSection(configSections, 'telegram')}
/>,
},
victorops: {
type: 'VictorOps',
renderComponent: () =>
<VictorOpsConfig
onSave={p => this.handleSaveConfig('victorops', p)}
config={this.getSection(configSections, 'victorops')}
/>,
},
}
return (
<div>
@ -228,17 +227,31 @@ class AlertTabs extends Component {
<Tabs tabContentsClass="config-endpoint">
<TabList customClass="config-endpoint--tabs">
{tabs.map((t, i) =>
<Tab key={tabs[i].type}>
{tabs[i].type}
</Tab>
{_.reduce(
configSections,
(acc, _cur, k) =>
supportedConfigs[k]
? acc.concat(
<Tab key={supportedConfigs[k].type}>
{supportedConfigs[k].type}
</Tab>
)
: acc,
[]
)}
</TabList>
<TabPanels customClass="config-endpoint--tab-contents">
{tabs.map((t, i) =>
<TabPanel key={tabs[i].type}>
{t.component}
</TabPanel>
{_.reduce(
configSections,
(acc, _cur, k) =>
supportedConfigs[k]
? acc.concat(
<TabPanel key={supportedConfigs[k].type}>
{supportedConfigs[k].renderComponent()}
</TabPanel>
)
: acc,
[]
)}
</TabPanels>
</Tabs>

View File

@ -0,0 +1,22 @@
import React, {PropTypes} from 'react'
const CodeData = ({onClickTemplate, template}) =>
<code
className="rule-builder--message-template"
data-tip={template.text}
onClick={onClickTemplate}
>
{template.label}
</code>
const {func, shape, string} = PropTypes
CodeData.propTypes = {
onClickTemplate: func,
template: shape({
label: string,
text: string,
}),
}
export default CodeData

View File

@ -1,41 +1,35 @@
import React, {PropTypes} from 'react'
import React, {Component, PropTypes} from 'react'
import classnames from 'classnames'
import ReactTooltip from 'react-tooltip'
import RuleMessageAlertConfig from 'src/kapacitor/components/RuleMessageAlertConfig'
import RuleMessageOptions from 'src/kapacitor/components/RuleMessageOptions'
import RuleMessageText from 'src/kapacitor/components/RuleMessageText'
import RuleMessageTemplates from 'src/kapacitor/components/RuleMessageTemplates'
import {RULE_MESSAGE_TEMPLATES as templates, DEFAULT_ALERTS} from '../constants'
import {DEFAULT_ALERTS, RULE_ALERT_OPTIONS} from 'src/kapacitor/constants'
const {arrayOf, func, shape, string} = PropTypes
class RuleMessage extends Component {
constructor(props) {
super(props)
export const RuleMessage = React.createClass({
propTypes: {
rule: shape({}).isRequired,
actions: shape({
updateMessage: func.isRequired,
updateDetails: func.isRequired,
}).isRequired,
enabledAlerts: arrayOf(string.isRequired).isRequired,
},
getInitialState() {
return {
selectedAlert: null,
selectedAlertProperty: null,
this.state = {
selectedAlertNodeName: null,
}
},
this.handleChangeMessage = ::this.handleChangeMessage
this.handleChooseAlert = ::this.handleChooseAlert
}
handleChangeMessage() {
const {actions, rule} = this.props
actions.updateMessage(rule.id, this.message.value)
},
}
handleChooseAlert(item) {
const {actions} = this.props
actions.updateAlerts(item.ruleID, [item.text])
actions.updateAlertNodes(item.ruleID, item.text, '')
this.setState({selectedAlert: item.text})
},
this.setState({selectedAlertNodeName: item.text})
}
render() {
const {rule, actions, enabledAlerts} = this.props
@ -43,13 +37,14 @@ export const RuleMessage = React.createClass({
return {text, ruleID: rule.id}
})
const alerts = enabledAlerts
.map(text => {
const alerts = [
...defaultAlertEndpoints,
...enabledAlerts.map(text => {
return {text, ruleID: rule.id}
})
.concat(defaultAlertEndpoints)
}),
]
const selectedAlert = rule.alerts[0] || alerts[0].text
const selectedAlertNodeName = rule.alerts[0] || alerts[0].text
return (
<div className="rule-section">
@ -58,96 +53,53 @@ export const RuleMessage = React.createClass({
<div className="rule-section--row rule-section--row-first rule-section--border-bottom">
<p>Send this Alert to:</p>
<ul className="nav nav-tablist nav-tablist-sm nav-tablist-malachite">
{alerts.map(alert =>
<li
key={alert.text}
className={classnames({
active: alert.text === selectedAlert,
})}
onClick={() => this.handleChooseAlert(alert)}
>
{alert.text}
</li>
)}
{alerts
// only display alert endpoints that have rule alert options configured
.filter(alert =>
Object.keys(RULE_ALERT_OPTIONS).includes(alert.text)
)
.map(alert =>
<li
key={alert.text}
className={classnames({
active: alert.text === selectedAlertNodeName,
})}
onClick={() => this.handleChooseAlert(alert)}
>
{alert.text}
</li>
)}
</ul>
</div>
<RuleMessageAlertConfig
updateAlertNodes={actions.updateAlertNodes}
alert={selectedAlert}
<RuleMessageOptions
rule={rule}
alertNodeName={selectedAlertNodeName}
updateAlertNodes={actions.updateAlertNodes}
updateDetails={actions.updateDetails}
updateAlertProperty={actions.updateAlertProperty}
/>
{selectedAlert === 'smtp'
? <div className="rule-section--border-bottom">
<textarea
className="form-control form-malachite monotype rule-builder--message"
placeholder="Email body text goes here"
ref={r => (this.details = r)}
onChange={() =>
actions.updateDetails(rule.id, this.details.value)}
value={rule.details}
spellCheck={false}
/>
</div>
: null}
<textarea
className="form-control form-malachite monotype rule-builder--message"
ref={r => (this.message = r)}
onChange={() => actions.updateMessage(rule.id, this.message.value)}
placeholder="Example: {{ .ID }} is {{ .Level }} value: {{ index .Fields &quot;value&quot; }}"
value={rule.message}
spellCheck={false}
<RuleMessageText rule={rule} updateMessage={actions.updateMessage} />
<RuleMessageTemplates
rule={rule}
updateMessage={actions.updateMessage}
/>
<div className="rule-section--row rule-section--row-last rule-section--border-top">
<p>Templates:</p>
{Object.keys(templates).map(t => {
return (
<CodeData
key={t}
template={templates[t]}
onClickTemplate={() =>
actions.updateMessage(
rule.id,
`${this.message.value} ${templates[t].label}`
)}
/>
)
})}
<ReactTooltip
effect="solid"
html={true}
offset={{top: -4}}
class="influx-tooltip kapacitor-tooltip"
/>
</div>
</div>
</div>
)
},
})
}
}
const CodeData = React.createClass({
propTypes: {
onClickTemplate: func,
template: shape({
label: string,
text: string,
}),
},
const {arrayOf, func, shape, string} = PropTypes
render() {
const {onClickTemplate, template} = this.props
const {label, text} = template
return (
<code
className="rule-builder--message-template"
data-tip={text}
onClick={onClickTemplate}
>
{label}
</code>
)
},
})
RuleMessage.propTypes = {
rule: shape({}).isRequired,
actions: shape({
updateAlertNodes: func.isRequired,
updateMessage: func.isRequired,
updateDetails: func.isRequired,
updateAlertProperty: func.isRequired,
}).isRequired,
enabledAlerts: arrayOf(string.isRequired).isRequired,
}
export default RuleMessage

View File

@ -1,44 +0,0 @@
import React, {PropTypes} from 'react'
import {
DEFAULT_ALERT_PLACEHOLDERS,
DEFAULT_ALERT_LABELS,
ALERT_NODES_ACCESSORS,
} from '../constants'
const RuleMessageAlertConfig = ({updateAlertNodes, alert, rule}) => {
if (!Object.keys(DEFAULT_ALERT_PLACEHOLDERS).find(a => a === alert)) {
return null
}
if (!Object.keys(DEFAULT_ALERT_LABELS).find(a => a === alert)) {
return null
}
return (
<div className="rule-section--row rule-section--border-bottom">
<p>
{DEFAULT_ALERT_LABELS[alert]}
</p>
<input
id="alert-input"
className="form-control input-sm form-malachite"
style={{flex: '1 0 0'}}
type="text"
placeholder={DEFAULT_ALERT_PLACEHOLDERS[alert]}
onChange={e => updateAlertNodes(rule.id, alert, e.target.value)}
value={ALERT_NODES_ACCESSORS[alert](rule)}
autoComplete="off"
spellCheck="false"
/>
</div>
)
}
const {func, shape, string} = PropTypes
RuleMessageAlertConfig.propTypes = {
updateAlertNodes: func.isRequired,
alert: string,
rule: shape({}).isRequired,
}
export default RuleMessageAlertConfig

View File

@ -0,0 +1,135 @@
import React, {Component, PropTypes} from 'react'
import {
RULE_ALERT_OPTIONS,
ALERT_NODES_ACCESSORS,
} from 'src/kapacitor/constants'
class RuleMessageOptions extends Component {
constructor(props) {
super(props)
this.getAlertPropertyValue = ::this.getAlertPropertyValue
}
getAlertPropertyValue(properties, name) {
if (properties) {
const alertNodeProperty = properties.find(
property => property.name === name
)
if (alertNodeProperty) {
return alertNodeProperty.args
}
}
return ''
}
render() {
const {
rule,
alertNodeName,
updateAlertNodes,
updateDetails,
updateAlertProperty,
} = this.props
const {args, details, properties} = RULE_ALERT_OPTIONS[alertNodeName]
return (
<div>
{args
? <div className="rule-section--row rule-section--border-bottom">
<p>
{args.label}
</p>
<input
id="alert-input"
className="form-control input-sm form-malachite"
style={{flex: '1 0 0'}}
type="text"
placeholder={args.placeholder}
onChange={e =>
updateAlertNodes(rule.id, alertNodeName, e.target.value)}
value={ALERT_NODES_ACCESSORS[alertNodeName](rule)}
autoComplete="off"
spellCheck="false"
/>
</div>
: null}
{properties && properties.length
? <div
className="rule-section--row rule-section--border-bottom"
style={{display: 'block'}}
>
<p>Optional Alert Parameters</p>
<div style={{display: 'flex', flexWrap: 'wrap'}}>
{properties.map(({name: propertyName, label, placeholder}) =>
<div
key={propertyName}
style={{display: 'block', flex: '0 0 33.33%'}}
>
<label
htmlFor={label}
style={{
display: 'flex',
width: '100%',
alignItems: 'center',
}}
>
<span style={{flex: '0 0 auto'}}>
{label}
</span>
<input
name={label}
className="form-control input-sm form-malachite"
style={{
margin: '0 15px 0 5px',
flex: '1 0 0',
}}
type="text"
placeholder={placeholder}
onChange={e =>
updateAlertProperty(rule.id, alertNodeName, {
name: propertyName,
args: [e.target.value],
})}
value={this.getAlertPropertyValue(
rule.alertNodes[0].properties,
propertyName
)}
autoComplete="off"
spellCheck="false"
/>
</label>
</div>
)}
</div>
</div>
: null}
{details
? <div className="rule-section--border-bottom">
<textarea
className="form-control form-malachite monotype rule-builder--message"
placeholder={details.placeholder ? details.placeholder : ''}
ref={r => (this.details = r)}
onChange={() => updateDetails(rule.id, this.details.value)}
value={rule.details}
spellCheck={false}
/>
</div>
: null}
</div>
)
}
}
const {func, shape, string} = PropTypes
RuleMessageOptions.propTypes = {
rule: shape({}).isRequired,
alertNodeName: string,
updateAlertNodes: func.isRequired,
updateDetails: func.isRequired,
updateAlertProperty: func.isRequired,
}
export default RuleMessageOptions

View File

@ -0,0 +1,49 @@
import React, {Component, PropTypes} from 'react'
import _ from 'lodash'
import ReactTooltip from 'react-tooltip'
import CodeData from 'src/kapacitor/components/CodeData'
import {RULE_MESSAGE_TEMPLATES} from 'src/kapacitor/constants'
// needs to be React Component for CodeData click handler to work
class RuleMessageTemplates extends Component {
constructor(props) {
super(props)
}
render() {
const {rule, updateMessage} = this.props
return (
<div className="rule-section--row rule-section--row-last rule-section--border-top">
<p>Templates:</p>
{_.map(RULE_MESSAGE_TEMPLATES, (template, key) => {
return (
<CodeData
key={key}
template={template}
onClickTemplate={() =>
updateMessage(rule.id, `${rule.message} ${template.label}`)}
/>
)
})}
<ReactTooltip
effect="solid"
html={true}
offset={{top: -4}}
class="influx-tooltip kapacitor-tooltip"
/>
</div>
)
}
}
const {func, shape} = PropTypes
RuleMessageTemplates.propTypes = {
rule: shape().isRequired,
updateMessage: func.isRequired,
}
export default RuleMessageTemplates

View File

@ -0,0 +1,31 @@
import React, {Component, PropTypes} from 'react'
class RuleMessageText extends Component {
constructor(props) {
super(props)
}
render() {
const {rule, updateMessage} = this.props
return (
<textarea
className="form-control form-malachite monotype rule-builder--message"
ref={r => (this.message = r)}
onChange={() => updateMessage(rule.id, this.message.value)}
placeholder="Example: {{ .ID }} is {{ .Level }} value: {{ index .Fields &quot;value&quot; }}"
value={rule.message}
spellCheck={false}
/>
)
}
}
const {func, shape} = PropTypes
RuleMessageText.propTypes = {
rule: shape().isRequired,
updateMessage: func.isRequired,
}
export default RuleMessageText

View File

@ -0,0 +1,98 @@
import React, {Component, PropTypes} from 'react'
import QuestionMarkTooltip from 'shared/components/QuestionMarkTooltip'
import RedactedInput from './RedactedInput'
import {PUSHOVER_DOCS_LINK} from 'src/kapacitor/copy'
class PushoverConfig extends Component {
constructor(props) {
super(props)
this.handleSaveAlert = ::this.handleSaveAlert
}
handleSaveAlert(e) {
e.preventDefault()
const properties = {
token: this.token.value,
url: this.url.value,
'user-key': this.userKey.value,
}
this.props.onSave(properties)
}
render() {
const {options} = this.props.config
const {token, url} = options
const userKey = options['user-key']
return (
<form onSubmit={this.handleSaveAlert}>
<div className="form-group col-xs-12">
<label htmlFor="user-key">
User Key
<QuestionMarkTooltip
tipID="token"
tipContent={PUSHOVER_DOCS_LINK}
/>
</label>
<RedactedInput
defaultValue={userKey}
id="user-key"
refFunc={r => (this.userKey = r)}
/>
</div>
<div className="form-group col-xs-12">
<label htmlFor="token">
Token
<QuestionMarkTooltip
tipID="token"
tipContent={PUSHOVER_DOCS_LINK}
/>
</label>
<RedactedInput
defaultValue={token}
id="token"
refFunc={r => (this.token = r)}
/>
</div>
<div className="form-group col-xs-12">
<label htmlFor="url">Pushover URL</label>
<input
className="form-control"
id="url"
type="text"
ref={r => (this.url = r)}
defaultValue={url || ''}
/>
</div>
<div className="form-group-submit col-xs-12 text-center">
<button className="btn btn-primary" type="submit">
Update Pushover Config
</button>
</div>
</form>
)
}
}
const {bool, func, shape, string} = PropTypes
PushoverConfig.propTypes = {
config: shape({
options: shape({
token: bool.isRequired,
'user-key': bool.isRequired,
url: string.isRequired,
}).isRequired,
}).isRequired,
onSave: func.isRequired,
}
export default PushoverConfig

View File

@ -2,6 +2,7 @@ import AlertaConfig from './AlertaConfig'
import HipChatConfig from './HipChatConfig'
import OpsGenieConfig from './OpsGenieConfig'
import PagerDutyConfig from './PagerDutyConfig'
import PushoverConfig from './PushoverConfig'
import SensuConfig from './SensuConfig'
import SlackConfig from './SlackConfig'
import SMTPConfig from './SMTPConfig'
@ -14,6 +15,7 @@ export {
HipChatConfig,
OpsGenieConfig,
PagerDutyConfig,
PushoverConfig,
SensuConfig,
SlackConfig,
SMTPConfig,

View File

@ -40,6 +40,7 @@ export const ALERTS = [
'hipchat',
'opsgenie',
'pagerduty',
'pushover',
'sensu',
'slack',
'smtp',
@ -82,23 +83,64 @@ export const RULE_MESSAGE_TEMPLATES = {
export const DEFAULT_ALERTS = ['http', 'tcp', 'exec', 'log']
export const DEFAULT_ALERT_LABELS = {
http: 'URL:',
tcp: 'Address:',
exec: 'Add Command (Arguments separated by Spaces):',
log: 'File:',
smtp: 'Email Addresses (Separated by Spaces):',
slack: 'Send alerts to Slack channel:',
alerta: 'Paste Alerta TICKscript:',
}
export const DEFAULT_ALERT_PLACEHOLDERS = {
http: 'Ex: http://example.com/api/alert',
tcp: 'Ex: exampleendpoint.com:5678',
exec: 'Ex: woogie boogie',
log: 'Ex: /tmp/alerts.log',
smtp: 'Ex: benedict@domain.com delaney@domain.com susan@domain.com',
slack: '#alerts',
alerta: 'alerta()',
export const RULE_ALERT_OPTIONS = {
http: {
args: {
label: 'URL:',
placeholder: 'Ex: http://example.com/api/alert',
},
},
tcp: {
args: {
label: 'Address:',
placeholder: 'Ex: exampleendpoint.com:5678',
},
},
exec: {
args: {
label: 'Add Command (Arguments separated by Spaces):',
placeholder: 'Ex: woogie boogie',
},
},
log: {
args: {
label: 'File:',
placeholder: 'Ex: /tmp/alerts.log',
},
},
smtp: {
args: {
label: 'Email Addresses (Separated by Spaces):',
placeholder:
'Ex: benedict@domain.com delaney@domain.com susan@domain.com',
},
details: {placeholder: 'Email body text goes here'},
},
slack: {
args: {
label: 'Send alerts to Slack channel:',
placeholder: '#alerts',
},
},
alerta: {
args: {
label: 'Paste Alerta TICKscript:',
placeholder: 'alerta()',
},
},
pushover: {
properties: [
{
name: 'device',
label: 'Device:',
placeholder: 'dv1,dv2 (Comma Separated)',
},
{name: 'title', label: 'Title:', placeholder: 'Important Message'},
{name: 'URL', label: 'URL:', placeholder: 'https://influxdata.com'},
{name: 'URLTitle', label: 'URL Title:', placeholder: 'InfluxData'},
{name: 'sound', label: 'Sound:', placeholder: 'alien'},
],
},
}
export const ALERT_NODES_ACCESSORS = {

View File

@ -82,7 +82,6 @@ class KapacitorRulePage extends Component {
if (!query) {
return <div className="page-spinner" />
}
return (
<KapacitorRule
source={source}

View File

@ -1,3 +1,14 @@
// HipChat
const hipchatTokenLink =
'https://docs.influxdata.com/kapacitor/latest/guides/event-handler-setup/#hipchat-api-access-token'
export const HIPCHAT_TOKEN_TIP = `<p>Need help creating a token?<br/>Check out <a href='${hipchatTokenLink}' target='_blank'>these steps</a>.</p>`
// Pushover
const pushoverDocsLink =
'https://docs.influxdata.com/kapacitor/latest/nodes/alert_node/#pushover'
export const PUSHOVER_DOCS_LINK = `<p>Need help setting up Pushover?<br/>Check out <a href='${pushoverDocsLink}' target='_blank'>the docs here</a>.</p>`
// Telegram
const telegramChatIDLink =
'https://docs.influxdata.com/kapacitor/latest/guides/event-handler-setup/#telegram-chat-id'
export const TELEGRAM_CHAT_ID_TIP = `<p>Need help finding your chat id?<br/>Check out <a target='_blank' href='${telegramChatIDLink}'>these steps</a>.</p>`
@ -5,7 +16,3 @@ export const TELEGRAM_CHAT_ID_TIP = `<p>Need help finding your chat id?<br/>Chec
const telegramTokenLink =
'https://docs.influxdata.com/kapacitor/latest/guides/event-handler-setup/#telegram-api-access-token'
export const TELEGRAM_TOKEN_TIP = `<p>Need help finding your token?<br/>Check out <a target='_blank' href='${telegramTokenLink}'>these steps</a>.</p>`
const hipchatTokenLink =
'https://docs.influxdata.com/kapacitor/latest/guides/event-handler-setup/#hipchat-api-access-token'
export const HIPCHAT_TOKEN_TIP = `<p>Need help creating a token?<br/>Check out <a href='${hipchatTokenLink}' target='_blank'>these steps</a>.</p>`

View File

@ -83,18 +83,22 @@ export default function rules(state = {}, action) {
})
}
// TODO: refactor to allow multiple alert nodes, and change name + refactor
// functionality to clearly disambiguate creating an alert node, changing its
// type, adding other alert nodes to a single rule, and updating an alert node's
// properties vs args vs details vs message.
case 'UPDATE_RULE_ALERT_NODES': {
const {ruleID, alertType, alertNodesText} = action.payload
const {ruleID, alertNodeName, alertNodesText} = action.payload
let alertNodesByType
switch (alertType) {
switch (alertNodeName) {
case 'http':
case 'tcp':
case 'log':
alertNodesByType = [
{
name: alertType,
name: alertNodeName,
args: [alertNodesText],
properties: [],
},
@ -104,7 +108,7 @@ export default function rules(state = {}, action) {
case 'smtp':
alertNodesByType = [
{
name: alertType,
name: alertNodeName,
args: alertNodesText.split(' '),
properties: [],
},
@ -113,7 +117,7 @@ export default function rules(state = {}, action) {
case 'slack':
alertNodesByType = [
{
name: alertType,
name: alertNodeName,
properties: [
{
name: 'channel',
@ -126,14 +130,21 @@ export default function rules(state = {}, action) {
case 'alerta':
alertNodesByType = [
{
name: alertType,
name: alertNodeName,
args: [],
properties: parseAlerta(alertNodesText),
},
]
break
case 'pushover':
default:
alertNodesByType = []
alertNodesByType = [
{
name: alertNodeName,
args: [],
properties: [],
},
]
}
return Object.assign({}, state, {
@ -143,6 +154,38 @@ export default function rules(state = {}, action) {
})
}
case 'UPDATE_RULE_ALERT_PROPERTY': {
const {ruleID, alertNodeName, alertProperty} = action.payload
const newAlertNodes = state[ruleID].alertNodes.map(alertNode => {
if (alertNode.name !== alertNodeName) {
return alertNode
}
let matched = false
if (!alertNode.properties) {
alertNode.properties = []
}
alertNode.properties = alertNode.properties.map(property => {
if (property.name === alertProperty.name) {
matched = true
return alertProperty
}
return property
})
if (!matched) {
alertNode.properties.push(alertProperty)
}
return alertNode
})
return {
...state,
[ruleID]: {...state[ruleID]},
alertNodes: newAlertNodes,
}
}
case 'UPDATE_RULE_NAME': {
const {ruleID, name} = action.payload
return Object.assign({}, state, {