diff --git a/CHANGELOG.md b/CHANGELOG.md index 9f8910bc29..957a5638e8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/kapacitor/alerts.go b/kapacitor/alerts.go index 759b161fe1..638b6fb8b8 100644 --- a/kapacitor/alerts.go +++ b/kapacitor/alerts.go @@ -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) diff --git a/kapacitor/ast.go b/kapacitor/ast.go index bdff08eca4..82dc04a13c 100644 --- a/kapacitor/ast.go +++ b/kapacitor/ast.go @@ -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) +} diff --git a/server/swagger.json b/server/swagger.json index ddd452ea7a..058be7dadf 100644 --- a/server/swagger.json +++ b/server/swagger.json @@ -2714,6 +2714,7 @@ "hipchat", "opsgenie", "pagerduty", + "pushover", "victorops", "smtp", "email", diff --git a/ui/spec/kapacitor/reducers/rulesSpec.js b/ui/spec/kapacitor/reducers/rulesSpec.js index 3f981aca81..87223b9b83 100644 --- a/ui/spec/kapacitor/reducers/rulesSpec.js +++ b/ui/spec/kapacitor/reducers/rulesSpec.js @@ -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' diff --git a/ui/src/kapacitor/actions/view/index.js b/ui/src/kapacitor/actions/view/index.js index 1452c32945..06d16500f1 100644 --- a/ui/src/kapacitor/actions/view/index.js +++ b/ui/src/kapacitor/actions/view/index.js @@ -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, }, } diff --git a/ui/src/kapacitor/components/AlertTabs.js b/ui/src/kapacitor/components/AlertTabs.js index 51a0a81c17..afe2d7f537 100644 --- a/ui/src/kapacitor/components/AlertTabs.js +++ b/ui/src/kapacitor/components/AlertTabs.js @@ -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: () => this.handleSaveConfig('alerta', p)} config={this.getSection(configSections, 'alerta')} - /> - ), + />, }, - { - type: 'SMTP', - component: ( - this.handleSaveConfig('smtp', p)} - config={this.getSection(configSections, 'smtp')} - /> - ), + hipchat: { + type: 'HipChat', + renderComponent: () => + this.handleSaveConfig('hipchat', p)} + config={this.getSection(configSections, 'hipchat')} + />, }, - { + opsgenie: { + type: 'OpsGenie', + renderComponent: () => + this.handleSaveConfig('opsgenie', p)} + config={this.getSection(configSections, 'opsgenie')} + />, + }, + pagerduty: { + type: 'PagerDuty', + renderComponent: () => + this.handleSaveConfig('pagerduty', p)} + config={this.getSection(configSections, 'pagerduty')} + />, + }, + pushover: { + type: 'Pushover', + renderComponent: () => + this.handleSaveConfig('pushover', p)} + config={this.getSection(configSections, 'pushover')} + />, + }, + sensu: { + type: 'Sensu', + renderComponent: () => + this.handleSaveConfig('sensu', p)} + config={this.getSection(configSections, 'sensu')} + />, + }, + slack: { type: 'Slack', - component: ( + renderComponent: () => this.handleSaveConfig('slack', p)} onTest={test} config={this.getSection(configSections, 'slack')} - /> - ), + />, }, - { - type: 'VictorOps', - component: ( - this.handleSaveConfig('victorops', p)} - config={this.getSection(configSections, 'victorops')} - /> - ), + smtp: { + type: 'SMTP', + renderComponent: () => + this.handleSaveConfig('smtp', p)} + config={this.getSection(configSections, 'smtp')} + />, }, - { - type: 'Telegram', - component: ( - this.handleSaveConfig('telegram', p)} - config={this.getSection(configSections, 'telegram')} - /> - ), - }, - { - type: 'OpsGenie', - component: ( - this.handleSaveConfig('opsgenie', p)} - config={this.getSection(configSections, 'opsgenie')} - /> - ), - }, - { - type: 'PagerDuty', - component: ( - this.handleSaveConfig('pagerduty', p)} - config={this.getSection(configSections, 'pagerduty')} - /> - ), - }, - { - type: 'HipChat', - component: ( - this.handleSaveConfig('hipchat', p)} - config={this.getSection(configSections, 'hipchat')} - /> - ), - }, - { - type: 'Sensu', - component: ( - this.handleSaveConfig('sensu', p)} - config={this.getSection(configSections, 'sensu')} - /> - ), - }, - { + talk: { type: 'Talk', - component: ( + renderComponent: () => this.handleSaveConfig('talk', p)} config={this.getSection(configSections, 'talk')} - /> - ), + />, }, - ] + telegram: { + type: 'Telegram', + renderComponent: () => + this.handleSaveConfig('telegram', p)} + config={this.getSection(configSections, 'telegram')} + />, + }, + victorops: { + type: 'VictorOps', + renderComponent: () => + this.handleSaveConfig('victorops', p)} + config={this.getSection(configSections, 'victorops')} + />, + }, + } return (
@@ -228,17 +227,31 @@ class AlertTabs extends Component { - {tabs.map((t, i) => - - {tabs[i].type} - + {_.reduce( + configSections, + (acc, _cur, k) => + supportedConfigs[k] + ? acc.concat( + + {supportedConfigs[k].type} + + ) + : acc, + [] )} - {tabs.map((t, i) => - - {t.component} - + {_.reduce( + configSections, + (acc, _cur, k) => + supportedConfigs[k] + ? acc.concat( + + {supportedConfigs[k].renderComponent()} + + ) + : acc, + [] )} diff --git a/ui/src/kapacitor/components/CodeData.js b/ui/src/kapacitor/components/CodeData.js new file mode 100644 index 0000000000..23a4fd160d --- /dev/null +++ b/ui/src/kapacitor/components/CodeData.js @@ -0,0 +1,22 @@ +import React, {PropTypes} from 'react' + +const CodeData = ({onClickTemplate, template}) => + + {template.label} + + +const {func, shape, string} = PropTypes + +CodeData.propTypes = { + onClickTemplate: func, + template: shape({ + label: string, + text: string, + }), +} + +export default CodeData diff --git a/ui/src/kapacitor/components/RuleMessage.js b/ui/src/kapacitor/components/RuleMessage.js index 7e333cb43e..5112397d7e 100644 --- a/ui/src/kapacitor/components/RuleMessage.js +++ b/ui/src/kapacitor/components/RuleMessage.js @@ -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 (
@@ -58,96 +53,53 @@ export const RuleMessage = React.createClass({

Send this Alert to:

    - {alerts.map(alert => -
  • this.handleChooseAlert(alert)} - > - {alert.text} -
  • - )} + {alerts + // only display alert endpoints that have rule alert options configured + .filter(alert => + Object.keys(RULE_ALERT_OPTIONS).includes(alert.text) + ) + .map(alert => +
  • this.handleChooseAlert(alert)} + > + {alert.text} +
  • + )}
- - {selectedAlert === 'smtp' - ?
-