Merge branch 'master' into feature/global-users

pull/2703/head
Iris Scholten 2018-02-01 16:40:14 -08:00
commit 5fbfc12f11
322 changed files with 23321 additions and 7946 deletions

View File

@ -1,5 +1,5 @@
[bumpversion] [bumpversion]
current_version = 1.4.0.0 current_version = 1.4.0.1
files = README.md server/swagger.json files = README.md server/swagger.json
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)\.(?P<release>\d+) parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)\.(?P<release>\d+)
serialize = {major}.{minor}.{patch}.{release} serialize = {major}.{minor}.{patch}.{release}

View File

@ -1,3 +1,24 @@
## v1.4.1.0 [unreleased]
### Features
1. [#2409](https://github.com/influxdata/chronograf/pull/2409): Allow adding multiple event handlers to a rule
1. [#2709](https://github.com/influxdata/chronograf/pull/2709): Add "send test alert" button to test kapacitor alert configurations"
1. [#2708](https://github.com/influxdata/chronograf/pull/2708): Link to specified kapacitor config panel from rule builder alert handlers
1. [#2722](https://github.com/influxdata/chronograf/pull/2722): Add auto refresh widget to hosts list page
### UI Improvements
1. [#2698](https://github.com/influxdata/chronograf/pull/2698): Improve clarity of terminology surrounding InfluxDB & Kapacitor connections
### Bug Fixes
1. [#2684](https://github.com/influxdata/chronograf/pull/2684): Fix TICKscript Sensu alerts when no group by tags selected
1. [#2735](https://github.com/influxdata/chronograf/pull/2735): Remove cli options from systemd service file
## v1.4.0.1 [2017-1-9]
### Features
1. [#2690](https://github.com/influxdata/chronograf/pull/2690): Add separate CLI flag for canned sources, kapacitors, dashboards, and organizations
1. [#2672](https://github.com/influxdata/chronograf/pull/2672): Add telegraf interval configuration
### Bug Fixes
1. [#2689](https://github.com/influxdata/chronograf/pull/2689): Allow insecure (self-signed) certificates for kapacitor and influxdb
1. [#2664](https://github.com/influxdata/chronograf/pull/2664): Fix positioning of custom time indicator
## v1.4.0.0 [2017-12-22] ## v1.4.0.0 [2017-12-22]
### UI Improvements ### UI Improvements
1. [#2652](https://github.com/influxdata/chronograf/pull/2652): Add page header with instructional copy when adding initial source for consistency and clearer UX 1. [#2652](https://github.com/influxdata/chronograf/pull/2652): Add page header with instructional copy when adding initial source for consistency and clearer UX
@ -5,6 +26,7 @@
### Bug Fixes ### Bug Fixes
1. [#2652](https://github.com/influxdata/chronograf/pull/2652): Make page render successfully when attempting to edit a source 1. [#2652](https://github.com/influxdata/chronograf/pull/2652): Make page render successfully when attempting to edit a source
1. [#2664](https://github.com/influxdata/chronograf/pull/2664): Fix CustomTimeIndicator positioning 1. [#2664](https://github.com/influxdata/chronograf/pull/2664): Fix CustomTimeIndicator positioning
1. [#2687](https://github.com/influxdata/chronograf/pull/2687): Remove series with "no value" from legend
## v1.4.0.0-rc2 [2017-12-21] ## v1.4.0.0-rc2 [2017-12-21]
### UI Improvements ### UI Improvements
@ -81,6 +103,19 @@
1. [#2477](https://github.com/influxdata/chronograf/pull/2477): Fix hoverline intermittently not rendering 1. [#2477](https://github.com/influxdata/chronograf/pull/2477): Fix hoverline intermittently not rendering
1. [#2483](https://github.com/influxdata/chronograf/pull/2483): Update MySQL pre-canned dashboard to have query derivative correctly 1. [#2483](https://github.com/influxdata/chronograf/pull/2483): Update MySQL pre-canned dashboard to have query derivative correctly
### Features
1. [#2188](https://github.com/influxdata/chronograf/pull/2188): Add Kapacitor logs to the TICKscript editor
1. [#2384](https://github.com/influxdata/chronograf/pull/2384): Add filtering by name to Dashboard index page
1. [#2385](https://github.com/influxdata/chronograf/pull/2385): Add time shift feature to DataExplorer and Dashboards
1. [#2400](https://github.com/influxdata/chronograf/pull/2400): Allow override of generic oauth2 keys for email
1. [#2426](https://github.com/influxdata/chronograf/pull/2426): Add auto group by time to Data Explorer
1. [#2456](https://github.com/influxdata/chronograf/pull/2456): Add boolean thresholds for kapacitor threshold alerts
1. [#2460](https://github.com/influxdata/chronograf/pull/2460): Update kapacitor alerts to cast to float before sending to influx
1. [#2479](https://github.com/influxdata/chronograf/pull/2479): Support authentication for Enterprise Meta Nodes
1. [#2477](https://github.com/influxdata/chronograf/pull/2477): Improve performance of hoverline rendering
### UI Improvements
## v1.3.10.0 [2017-10-24] ## v1.3.10.0 [2017-10-24]
### Bug Fixes ### Bug Fixes
1. [#2095](https://github.com/influxdata/chronograf/pull/2095): Improve the copy in the retention policy edit page 1. [#2095](https://github.com/influxdata/chronograf/pull/2095): Improve the copy in the retention policy edit page

10
Gopkg.lock generated
View File

@ -65,13 +65,13 @@
[[projects]] [[projects]]
name = "github.com/influxdata/influxdb" name = "github.com/influxdata/influxdb"
packages = ["influxql","influxql/internal","influxql/neldermead","models","pkg/escape"] packages = ["influxql","influxql/internal","influxql/neldermead","models","pkg/escape"]
revision = "af72d9b0e4ebe95be30e89b160f43eabaf0529ed" revision = "cd9363b52cac452113b95554d98a6be51beda24e"
version = "v1.1.5"
[[projects]] [[projects]]
name = "github.com/influxdata/kapacitor" name = "github.com/influxdata/kapacitor"
packages = ["client/v1","pipeline","services/k8s/client","tick","tick/ast","tick/stateful","udf/agent"] packages = ["client/v1","pipeline","pipeline/tick","services/k8s/client","tick","tick/ast","tick/stateful","udf/agent"]
revision = "3b5512f7276483326577907803167e4bb213c613" revision = "6de30070b39afde111fea5e041281126fe8aae31"
version = "v1.3.1"
[[projects]] [[projects]]
name = "github.com/influxdata/usage-client" name = "github.com/influxdata/usage-client"
@ -140,6 +140,6 @@
[solve-meta] [solve-meta]
analyzer-name = "dep" analyzer-name = "dep"
analyzer-version = 1 analyzer-version = 1
inputs-digest = "85a5451fc9e0596e486a676204eb2de0b12900522341ee0804cf9ec86fb2765e" inputs-digest = "a5bd1aa82919723ff8ec5dd9d520329862de8181ca9dba75c6acb3a34df5f1a4"
solver-name = "gps-cdcl" solver-name = "gps-cdcl"
solver-version = 1 solver-version = 1

View File

@ -32,14 +32,6 @@ required = ["github.com/jteeuwen/go-bindata","github.com/gogo/protobuf/proto","g
name = "github.com/google/go-github" name = "github.com/google/go-github"
revision = "1bc362c7737e51014af7299e016444b654095ad9" revision = "1bc362c7737e51014af7299e016444b654095ad9"
[[constraint]]
name = "github.com/influxdata/influxdb"
revision = "af72d9b0e4ebe95be30e89b160f43eabaf0529ed"
[[constraint]]
name = "github.com/influxdata/kapacitor"
version = "^1.2.0"
[[constraint]] [[constraint]]
name = "github.com/influxdata/usage-client" name = "github.com/influxdata/usage-client"
revision = "6d3895376368aa52a3a81d2a16e90f0f52371967" revision = "6d3895376368aa52a3a81d2a16e90f0f52371967"
@ -75,3 +67,12 @@ required = ["github.com/jteeuwen/go-bindata","github.com/gogo/protobuf/proto","g
[[constraint]] [[constraint]]
name = "google.golang.org/api" name = "google.golang.org/api"
revision = "bc20c61134e1d25265dd60049f5735381e79b631" revision = "bc20c61134e1d25265dd60049f5735381e79b631"
[[constraint]]
name = "github.com/influxdata/influxdb"
version = "~1.1.0"
[[constraint]]
name = "github.com/influxdata/kapacitor"
revision = "6de30070b39afde111fea5e041281126fe8aae31"

View File

@ -136,7 +136,7 @@ option.
## Versions ## Versions
The most recent version of Chronograf is The most recent version of Chronograf is
[v1.4.0.0](https://www.influxdata.com/downloads/). [v1.4.0.1](https://www.influxdata.com/downloads/).
Spotted a bug or have a feature request? Please open Spotted a bug or have a feature request? Please open
[an issue](https://github.com/influxdata/chronograf/issues/new)! [an issue](https://github.com/influxdata/chronograf/issues/new)!
@ -156,7 +156,7 @@ The Chronograf team has identified and is working on the following issues:
## Installation ## Installation
Check out the Check out the
[INSTALLATION](https://docs.influxdata.com/chronograf/v1.3/introduction/installation/) [INSTALLATION](https://docs.influxdata.com/chronograf/v1.4/introduction/installation/)
guide to get up and running with Chronograf with as little configuration and guide to get up and running with Chronograf with as little configuration and
code as possible. code as possible.
@ -178,7 +178,7 @@ By default, chronograf runs on port `8888`.
To get started right away with Docker, you can pull down our latest release: To get started right away with Docker, you can pull down our latest release:
```sh ```sh
docker pull chronograf:1.4.0.0 docker pull chronograf:1.4.0.1
``` ```
### From Source ### From Source
@ -198,10 +198,10 @@ docker pull chronograf:1.4.0.0
## Documentation ## Documentation
[Getting Started](https://docs.influxdata.com/chronograf/v1.3/introduction/getting-started/) [Getting Started](https://docs.influxdata.com/chronograf/v1.4/introduction/getting-started/)
will get you up and running with Chronograf with as little configuration and will get you up and running with Chronograf with as little configuration and
code as possible. See our code as possible. See our
[guides](https://docs.influxdata.com/chronograf/v1.3/guides/) to get familiar [guides](https://docs.influxdata.com/chronograf/v1.4/guides/) to get familiar
with Chronograf's main features. with Chronograf's main features.
Documentation for Telegraf, InfluxDB, and Kapacitor are available at Documentation for Telegraf, InfluxDB, and Kapacitor are available at

View File

@ -231,6 +231,7 @@ type SourcesStore interface {
Update(context.Context, Source) error Update(context.Context, Source) error
} }
// DBRP is a database and retention policy for a kapacitor task
type DBRP struct { type DBRP struct {
DB string `json:"db"` DB string `json:"db"`
RP string `json:"rp"` RP string `json:"rp"`
@ -238,25 +239,24 @@ type DBRP struct {
// AlertRule represents rules for building a tickscript alerting task // AlertRule represents rules for building a tickscript alerting task
type AlertRule struct { type AlertRule struct {
ID string `json:"id,omitempty"` // ID is the unique ID of the alert ID string `json:"id,omitempty"` // ID is the unique ID of the alert
TICKScript TICKScript `json:"tickscript"` // TICKScript is the raw tickscript associated with this Alert TICKScript TICKScript `json:"tickscript"` // TICKScript is the raw tickscript associated with this Alert
Query *QueryConfig `json:"query"` // Query is the filter of data for the alert. Query *QueryConfig `json:"query"` // Query is the filter of data for the alert.
Every string `json:"every"` // Every how often to check for the alerting criteria Every string `json:"every"` // Every how often to check for the alerting criteria
Alerts []string `json:"alerts"` // Alerts name all the services to notify (e.g. pagerduty) AlertNodes AlertNodes `json:"alertNodes"` // AlertNodes defines the destinations for the alert
AlertNodes []KapacitorNode `json:"alertNodes,omitempty"` // AlertNodes define additional arguments to alerts Message string `json:"message"` // Message included with alert
Message string `json:"message"` // Message included with alert Details string `json:"details"` // Details is generally used for the Email alert. If empty will not be added.
Details string `json:"details"` // Details is generally used for the Email alert. If empty will not be added. Trigger string `json:"trigger"` // Trigger is a type that defines when to trigger the alert
Trigger string `json:"trigger"` // Trigger is a type that defines when to trigger the alert TriggerValues TriggerValues `json:"values"` // Defines the values that cause the alert to trigger
TriggerValues TriggerValues `json:"values"` // Defines the values that cause the alert to trigger Name string `json:"name"` // Name is the user-defined name for the alert
Name string `json:"name"` // Name is the user-defined name for the alert Type string `json:"type"` // Represents the task type where stream is data streamed to kapacitor and batch is queried by kapacitor
Type string `json:"type"` // Represents the task type where stream is data streamed to kapacitor and batch is queried by kapacitor DBRPs []DBRP `json:"dbrps"` // List of database retention policy pairs the task is allowed to access
DBRPs []DBRP `json:"dbrps"` // List of database retention policy pairs the task is allowed to access Status string `json:"status"` // Represents if this rule is enabled or disabled in kapacitor
Status string `json:"status"` // Represents if this rule is enabled or disabled in kapacitor Executing bool `json:"executing"` // Whether the task is currently executing
Executing bool `json:"executing"` // Whether the task is currently executing Error string `json:"error"` // Any error encountered when kapacitor executes the task
Error string `json:"error"` // Any error encountered when kapacitor executes the task Created time.Time `json:"created"` // Date the task was first created
Created time.Time `json:"created"` // Date the task was first created Modified time.Time `json:"modified"` // Date the task was last modified
Modified time.Time `json:"modified"` // Date the task was last modified LastEnabled time.Time `json:"last-enabled,omitempty"` // Date the task was last set to status enabled
LastEnabled time.Time `json:"last-enabled,omitempty"` // Date the task was last set to status enabled
} }
// TICKScript task to be used by kapacitor // TICKScript task to be used by kapacitor

View File

@ -24,6 +24,7 @@ DATA_DIR = "/var/lib/chronograf"
SCRIPT_DIR = "/usr/lib/chronograf/scripts" SCRIPT_DIR = "/usr/lib/chronograf/scripts"
LOGROTATE_DIR = "/etc/logrotate.d" LOGROTATE_DIR = "/etc/logrotate.d"
CANNED_DIR = "/usr/share/chronograf/canned" CANNED_DIR = "/usr/share/chronograf/canned"
RESOURCES_DIR = "/usr/share/chronograf/resources"
INIT_SCRIPT = "etc/scripts/init.sh" INIT_SCRIPT = "etc/scripts/init.sh"
SYSTEMD_SCRIPT = "etc/scripts/chronograf.service" SYSTEMD_SCRIPT = "etc/scripts/chronograf.service"
@ -115,7 +116,8 @@ def create_package_fs(build_root):
DATA_DIR[1:], DATA_DIR[1:],
SCRIPT_DIR[1:], SCRIPT_DIR[1:],
LOGROTATE_DIR[1:], LOGROTATE_DIR[1:],
CANNED_DIR[1:] CANNED_DIR[1:],
RESOURCES_DIR[1:]
] ]
for d in dirs: for d in dirs:
os.makedirs(os.path.join(build_root, d)) os.makedirs(os.path.join(build_root, d))

View File

@ -8,8 +8,12 @@ After=network-online.target
[Service] [Service]
User=chronograf User=chronograf
Group=chronograf Group=chronograf
Environment="HOST=0.0.0.0"
Environment="PORT=8888"
Environment="BOLT_PATH=/var/lib/chronograf/chronograf-v1.db"
Environment="CANNED_PATH=/usr/share/chronograf/canned"
EnvironmentFile=-/etc/default/chronograf EnvironmentFile=-/etc/default/chronograf
ExecStart=/usr/bin/chronograf --host 0.0.0.0 --port 8888 -b /var/lib/chronograf/chronograf-v1.db -c /usr/share/chronograf/canned $CHRONOGRAF_OPTS ExecStart=/usr/bin/chronograf $CHRONOGRAF_OPTS
KillMode=control-group KillMode=control-group
Restart=on-failure Restart=on-failure

View File

@ -2289,6 +2289,7 @@ func TestServer(t *testing.T) {
// Use testdata directory for the canned data // Use testdata directory for the canned data
tt.args.server.CannedPath = "testdata" tt.args.server.CannedPath = "testdata"
tt.args.server.ResourcesPath = "testdata"
// This is so that we can use staticly generate jwts // This is so that we can use staticly generate jwts
tt.args.server.TokenSecret = "secret" tt.args.server.TokenSecret = "secret"

148
kapacitor.go Normal file
View File

@ -0,0 +1,148 @@
package chronograf
import "encoding/json"
// AlertNodes defines all possible kapacitor interactions with an alert.
type AlertNodes struct {
IsStateChangesOnly bool `json:"stateChangesOnly"` // IsStateChangesOnly will only send alerts on state changes.
UseFlapping bool `json:"useFlapping"` // UseFlapping enables flapping detection. Flapping occurs when a service or host changes state too frequently, resulting in a storm of problem and recovery notification
Posts []*Post `json:"post"` // HTTPPost will post the JSON alert data to the specified URLs.
TCPs []*TCP `json:"tcp"` // TCP will send the JSON alert data to the specified endpoint via TCP.
Email []*Email `json:"email"` // Email will send alert data to the specified emails.
Exec []*Exec `json:"exec"` // Exec will run shell commandss when an alert triggers
Log []*Log `json:"log"` // Log will log JSON alert data to files in JSON lines format.
VictorOps []*VictorOps `json:"victorOps"` // VictorOps will send alert to all VictorOps
PagerDuty []*PagerDuty `json:"pagerDuty"` // PagerDuty will send alert to all PagerDuty
Pushover []*Pushover `json:"pushover"` // Pushover will send alert to all Pushover
Sensu []*Sensu `json:"sensu"` // Sensu will send alert to all Sensu
Slack []*Slack `json:"slack"` // Slack will send alert to Slack
Telegram []*Telegram `json:"telegram"` // Telegram will send alert to all Telegram
HipChat []*HipChat `json:"hipChat"` // HipChat will send alert to all HipChat
Alerta []*Alerta `json:"alerta"` // Alerta will send alert to all Alerta
OpsGenie []*OpsGenie `json:"opsGenie"` // OpsGenie will send alert to all OpsGenie
Talk []*Talk `json:"talk"` // Talk will send alert to all Talk
}
// Post will POST alerts to a destination URL
type Post struct {
URL string `json:"url"` // URL is the destination of the POST.
Headers map[string]string `json:"headers"` // Headers are added to the output POST
}
// Log sends the output of the alert to a file
type Log struct {
FilePath string `json:"filePath"` // Absolute path the the log file; it will be created if it does not exist.
}
// Alerta sends the output of the alert to an alerta service
type Alerta struct {
Token string `json:"token"` // Token is the authentication token that overrides the global configuration.
Resource string `json:"resource"` // Resource under alarm, deliberately not host-centric
Event string `json:"event"` // Event is the event name eg. NodeDown, QUEUE:LENGTH:EXCEEDED
Environment string `json:"environment"` // Environment is the effected environment; used to namespace the resource
Group string `json:"group"` // Group is an event group used to group events of similar type
Value string `json:"value"` // Value is the event value eg. 100%, Down, PingFail, 55ms, ORA-1664
Origin string `json:"origin"` // Origin is the name of monitoring component that generated the alert
Service []string `json:"service"` // Service is the list of affected services
}
// Exec executes a shell command on an alert
type Exec struct {
Command []string `json:"command"` // Command is the space separated command and args to execute.
}
// TCP sends the alert to the address
type TCP struct {
Address string `json:"address"` // Endpoint is the Address and port to send the alert
}
// Email sends the alert to a list of email addresses
type Email struct {
To []string `json:"to"` // ToList is the list of email recipients.
}
// VictorOps sends alerts to the victorops.com service
type VictorOps struct {
RoutingKey string `json:"routingKey"` // RoutingKey is what is used to map the alert to a team
}
// PagerDuty sends alerts to the pagerduty.com service
type PagerDuty struct {
ServiceKey string `json:"serviceKey"` // ServiceKey is the GUID of one of the "Generic API" integrations
}
// HipChat sends alerts to stride.com
type HipChat struct {
Room string `json:"room"` // Room is the HipChat room to post messages.
Token string `json:"token"` // Token is the HipChat authentication token.
}
// Sensu sends alerts to sensu or sensuapp.org
type Sensu struct {
Source string `json:"source"` // Source is the check source, used to create a proxy client for an external resource
Handlers []string `json:"handlers"` // Handlers are Sensu event handlers are for taking action on events
}
// Pushover sends alerts to pushover.net
type Pushover struct {
// UserKey is the User/Group key of your user (or you), viewable when logged
// into the Pushover dashboard. Often referred to as USER_KEY
// in the Pushover documentation.
UserKey string `json:"userKey"`
// Device is the users device name to send message directly to that device,
// rather than all of a user's devices (multiple device names may
// be separated by a comma)
Device string `json:"device"`
// Title is your message's title, otherwise your apps name is used
Title string `json:"title"`
// URL is a supplementary URL to show with your message
URL string `json:"url"`
// URLTitle is a title for your supplementary URL, otherwise just URL is shown
URLTitle string `json:"urlTitle"`
// Sound is the name of one of the sounds supported by the device clients to override
// the user's default sound choice
Sound string `json:"sound"`
}
// Slack sends alerts to a slack.com channel
type Slack struct {
Channel string `json:"channel"` // Slack channel in which to post messages.
Username string `json:"username"` // Username of the Slack bot.
IconEmoji string `json:"iconEmoji"` // IconEmoji is an emoji name surrounded in ':' characters; The emoji image will replace the normal user icon for the slack bot.
}
// Telegram sends alerts to telegram.org
type Telegram struct {
ChatID string `json:"chatId"` // ChatID is the Telegram user/group ID to post messages to.
ParseMode string `json:"parseMode"` // ParseMode tells telegram how to render the message (Markdown or HTML)
DisableWebPagePreview bool `json:"disableWebPagePreview"` // IsDisableWebPagePreview will disables link previews in alert messages.
DisableNotification bool `json:"disableNotification"` // IsDisableNotification will disables notifications on iOS devices and disables sounds on Android devices. Android users continue to receive notifications.
}
// OpsGenie sends alerts to opsgenie.com
type OpsGenie struct {
Teams []string `json:"teams"` // Teams that the alert will be routed to send notifications
Recipients []string `json:"recipients"` // Recipients can be a single user, group, escalation, or schedule (https://docs.opsgenie.com/docs/alert-recipients-and-teams)
}
// Talk sends alerts to Jane Talk (https://jianliao.com/site)
type Talk struct{}
// MarshalJSON converts AlertNodes to JSON
func (n *AlertNodes) MarshalJSON() ([]byte, error) {
type Alias AlertNodes
var raw = &struct {
Type string `json:"typeOf"`
*Alias
}{
Type: "alert",
Alias: (*Alias)(n),
}
return json.Marshal(raw)
}

View File

@ -1,90 +1,19 @@
package kapacitor package kapacitor
import ( import (
"fmt" "bytes"
"encoding/json"
"regexp"
"strings" "strings"
"github.com/influxdata/chronograf" "github.com/influxdata/chronograf"
"github.com/influxdata/kapacitor/pipeline"
"github.com/influxdata/kapacitor/pipeline/tick"
) )
func kapaHandler(handler string) (string, error) {
switch handler {
case "hipchat":
return "hipChat", nil
case "opsgenie":
return "opsGenie", nil
case "pagerduty":
return "pagerDuty", nil
case "victorops":
return "victorOps", nil
case "smtp":
return "email", nil
case "http":
return "post", nil
case "alerta", "sensu", "slack", "email", "talk", "telegram", "post", "tcp", "exec", "log", "pushover":
return handler, nil
default:
return "", fmt.Errorf("Unsupported alert handler %s", handler)
}
}
func toKapaFunc(method string, args []string) (string, error) {
if len(args) == 0 {
return fmt.Sprintf(".%s()", method), nil
}
params := make([]string, len(args))
copy(params, args)
// Kapacitor strings are quoted
for i, p := range params {
params[i] = fmt.Sprintf("'%s'", p)
}
return fmt.Sprintf(".%s(%s)", method, strings.Join(params, ",")), nil
}
func addAlertNodes(rule chronograf.AlertRule) (string, error) {
alert := ""
// Using a map to try to combine older API in .Alerts with .AlertNodes
nodes := map[string]chronograf.KapacitorNode{}
for _, node := range rule.AlertNodes {
handler, err := kapaHandler(node.Name)
if err != nil {
return "", err
}
nodes[handler] = node
}
for _, a := range rule.Alerts {
handler, err := kapaHandler(a)
if err != nil {
return "", err
}
// If the this handler is not in nodes, then there are
// there are no arguments or properties
if _, ok := nodes[handler]; !ok {
alert = alert + fmt.Sprintf(".%s()", handler)
}
}
for handler, node := range nodes {
service, err := toKapaFunc(handler, node.Args)
if err != nil {
return "", nil
}
alert = alert + service
for _, prop := range node.Properties {
alertProperty, err := toKapaFunc(prop.Name, prop.Args)
if err != nil {
return "", nil
}
alert = alert + alertProperty
}
}
return alert, nil
}
// AlertServices generates alert chaining methods to be attached to an alert from all rule Services // AlertServices generates alert chaining methods to be attached to an alert from all rule Services
func AlertServices(rule chronograf.AlertRule) (string, error) { func AlertServices(rule chronograf.AlertRule) (string, error) {
node, err := addAlertNodes(rule) node, err := addAlertNodes(rule.AlertNodes)
if err != nil { if err != nil {
return "", err return "", err
} }
@ -94,3 +23,45 @@ func AlertServices(rule chronograf.AlertRule) (string, error) {
} }
return node, nil return node, nil
} }
func addAlertNodes(handlers chronograf.AlertNodes) (string, error) {
octets, err := json.Marshal(&handlers)
if err != nil {
return "", err
}
stream := &pipeline.StreamNode{}
pipe := pipeline.CreatePipelineSources(stream)
from := stream.From()
node := from.Alert()
if err = json.Unmarshal(octets, node); err != nil {
return "", err
}
aster := tick.AST{}
err = aster.Build(pipe)
if err != nil {
return "", err
}
var buf bytes.Buffer
aster.Program.Format(&buf, "", false)
rawTick := buf.String()
return toOldSchema(rawTick), nil
}
var (
removeID = regexp.MustCompile(`(?m)\s*\.id\(.*\)$`) // Remove to use ID variable
removeMessage = regexp.MustCompile(`(?m)\s*\.message\(.*\)$`) // Remove to use message variable
removeDetails = regexp.MustCompile(`(?m)\s*\.details\(.*\)$`) // Remove to use details variable
removeHistory = regexp.MustCompile(`(?m)\s*\.history\(21\)$`) // Remove default history
)
func toOldSchema(rawTick string) string {
rawTick = strings.Replace(rawTick, "stream\n |from()\n |alert()", "", -1)
rawTick = removeID.ReplaceAllString(rawTick, "")
rawTick = removeMessage.ReplaceAllString(rawTick, "")
rawTick = removeDetails.ReplaceAllString(rawTick, "")
rawTick = removeHistory.ReplaceAllString(rawTick, "")
return rawTick
}

View File

@ -16,51 +16,60 @@ func TestAlertServices(t *testing.T) {
{ {
name: "Test several valid services", name: "Test several valid services",
rule: chronograf.AlertRule{ rule: chronograf.AlertRule{
Alerts: []string{"slack", "victorops", "email"}, AlertNodes: chronograf.AlertNodes{
Slack: []*chronograf.Slack{{}},
VictorOps: []*chronograf.VictorOps{{}},
Email: []*chronograf.Email{{}},
},
}, },
want: `alert() want: `alert()
.slack()
.victorOps()
.email() .email()
.victorOps()
.slack()
`, `,
}, },
{
name: "Test single invalid services amongst several valid",
rule: chronograf.AlertRule{
Alerts: []string{"slack", "invalid", "email"},
},
want: ``,
wantErr: true,
},
{
name: "Test single invalid service",
rule: chronograf.AlertRule{
Alerts: []string{"invalid"},
},
want: ``,
wantErr: true,
},
{ {
name: "Test single valid service", name: "Test single valid service",
rule: chronograf.AlertRule{ rule: chronograf.AlertRule{
Alerts: []string{"slack"}, AlertNodes: chronograf.AlertNodes{
Slack: []*chronograf.Slack{{}},
},
}, },
want: `alert() want: `alert()
.slack() .slack()
`,
},
{
name: "Test pushoverservice",
rule: chronograf.AlertRule{
AlertNodes: chronograf.AlertNodes{
Pushover: []*chronograf.Pushover{
{
Device: "asdf",
Title: "asdf",
Sound: "asdf",
URL: "http://moo.org",
URLTitle: "influxdata",
},
},
},
},
want: `alert()
.pushover()
.device('asdf')
.title('asdf')
.uRL('http://moo.org')
.uRLTitle('influxdata')
.sound('asdf')
`, `,
}, },
{ {
name: "Test single valid service and property", name: "Test single valid service and property",
rule: chronograf.AlertRule{ rule: chronograf.AlertRule{
Alerts: []string{"slack"}, AlertNodes: chronograf.AlertNodes{
AlertNodes: []chronograf.KapacitorNode{ Slack: []*chronograf.Slack{
{ {
Name: "slack", Channel: "#general",
Properties: []chronograf.KapacitorProperty{
{
Name: "channel",
Args: []string{"#general"},
},
}, },
}, },
}, },
@ -73,10 +82,11 @@ func TestAlertServices(t *testing.T) {
{ {
name: "Test tcp", name: "Test tcp",
rule: chronograf.AlertRule{ rule: chronograf.AlertRule{
AlertNodes: []chronograf.KapacitorNode{ AlertNodes: chronograf.AlertNodes{
{ TCPs: []*chronograf.TCP{
Name: "tcp", {
Args: []string{"myaddress:22"}, Address: "myaddress:22",
},
}, },
}, },
}, },
@ -84,24 +94,14 @@ func TestAlertServices(t *testing.T) {
.tcp('myaddress:22') .tcp('myaddress:22')
`, `,
}, },
{
name: "Test tcp no argument",
rule: chronograf.AlertRule{
AlertNodes: []chronograf.KapacitorNode{
{
Name: "tcp",
},
},
},
wantErr: true,
},
{ {
name: "Test log", name: "Test log",
rule: chronograf.AlertRule{ rule: chronograf.AlertRule{
AlertNodes: []chronograf.KapacitorNode{ AlertNodes: chronograf.AlertNodes{
{ Log: []*chronograf.Log{
Name: "log", {
Args: []string{"/tmp/alerts.log"}, FilePath: "/tmp/alerts.log",
},
}, },
}, },
}, },
@ -109,82 +109,29 @@ func TestAlertServices(t *testing.T) {
.log('/tmp/alerts.log') .log('/tmp/alerts.log')
`, `,
}, },
{
name: "Test log no argument",
rule: chronograf.AlertRule{
AlertNodes: []chronograf.KapacitorNode{
{
Name: "log",
},
},
},
wantErr: true,
},
{
name: "Test tcp no argument with other services",
rule: chronograf.AlertRule{
Alerts: []string{"slack", "tcp", "email"},
AlertNodes: []chronograf.KapacitorNode{
{
Name: "tcp",
},
},
},
wantErr: true,
},
{ {
name: "Test http as post", name: "Test http as post",
rule: chronograf.AlertRule{ rule: chronograf.AlertRule{
AlertNodes: []chronograf.KapacitorNode{ AlertNodes: chronograf.AlertNodes{
{ Posts: []*chronograf.Post{
Name: "http", {
Args: []string{"http://myaddress"}, URL: "http://myaddress",
},
}, },
}, },
}, },
want: `alert() want: `alert()
.post('http://myaddress') .post('http://myaddress')
`,
},
{
name: "Test post",
rule: chronograf.AlertRule{
AlertNodes: []chronograf.KapacitorNode{
{
Name: "post",
Args: []string{"http://myaddress"},
},
},
},
want: `alert()
.post('http://myaddress')
`,
},
{
name: "Test http no arguments",
rule: chronograf.AlertRule{
AlertNodes: []chronograf.KapacitorNode{
{
Name: "http",
},
},
},
want: `alert()
.post()
`, `,
}, },
{ {
name: "Test post with headers", name: "Test post with headers",
rule: chronograf.AlertRule{ rule: chronograf.AlertRule{
AlertNodes: []chronograf.KapacitorNode{ AlertNodes: chronograf.AlertNodes{
{ Posts: []*chronograf.Post{
Name: "post", {
Args: []string{"http://myaddress"}, URL: "http://myaddress",
Properties: []chronograf.KapacitorProperty{ Headers: map[string]string{"key": "value"},
{
Name: "header",
Args: []string{"key", "value"},
},
}, },
}, },
}, },
@ -192,27 +139,6 @@ func TestAlertServices(t *testing.T) {
want: `alert() want: `alert()
.post('http://myaddress') .post('http://myaddress')
.header('key', 'value') .header('key', 'value')
`,
},
{
name: "Test post with headers",
rule: chronograf.AlertRule{
AlertNodes: []chronograf.KapacitorNode{
{
Name: "post",
Args: []string{"http://myaddress"},
Properties: []chronograf.KapacitorProperty{
{
Name: "endpoint",
Args: []string{"myendpoint"},
},
},
},
},
},
want: `alert()
.post('http://myaddress')
.endpoint('myendpoint')
`, `,
}, },
} }
@ -235,3 +161,68 @@ func TestAlertServices(t *testing.T) {
} }
} }
} }
func Test_addAlertNodes(t *testing.T) {
tests := []struct {
name string
handlers chronograf.AlertNodes
want string
wantErr bool
}{
{
name: "test email alerts",
handlers: chronograf.AlertNodes{
IsStateChangesOnly: true,
Email: []*chronograf.Email{
{
To: []string{
"me@me.com", "you@you.com",
},
},
},
},
want: `
.stateChangesOnly()
.email()
.to('me@me.com')
.to('you@you.com')
`,
},
{
name: "test pushover alerts",
handlers: chronograf.AlertNodes{
IsStateChangesOnly: true,
Pushover: []*chronograf.Pushover{
{
Device: "asdf",
Title: "asdf",
Sound: "asdf",
URL: "http://moo.org",
URLTitle: "influxdata",
},
},
},
want: `
.stateChangesOnly()
.pushover()
.device('asdf')
.title('asdf')
.uRL('http://moo.org')
.uRLTitle('influxdata')
.sound('asdf')
`,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := addAlertNodes(tt.handlers)
if (err != nil) != tt.wantErr {
t.Errorf("addAlertNodes() error = %v, wantErr %v", err, tt.wantErr)
return
}
if got != tt.want {
t.Errorf("addAlertNodes() =\n%v\n, want\n%v", got, tt.want)
}
})
}
}

View File

@ -1,6 +1,7 @@
package kapacitor package kapacitor
import ( import (
"encoding/json"
"regexp" "regexp"
"strconv" "strconv"
"strings" "strings"
@ -367,8 +368,7 @@ func alertType(script chronograf.TICKScript) (string, error) {
// Reverse converts tickscript to an AlertRule // Reverse converts tickscript to an AlertRule
func Reverse(script chronograf.TICKScript) (chronograf.AlertRule, error) { func Reverse(script chronograf.TICKScript) (chronograf.AlertRule, error) {
rule := chronograf.AlertRule{ rule := chronograf.AlertRule{
Alerts: []string{}, Query: &chronograf.QueryConfig{},
Query: &chronograf.QueryConfig{},
} }
t, err := alertType(script) t, err := alertType(script)
if err != nil { if err != nil {
@ -483,432 +483,20 @@ func Reverse(script chronograf.TICKScript) (chronograf.AlertRule, error) {
return chronograf.AlertRule{}, err return chronograf.AlertRule{}, err
} }
extractAlertNodes(p, &rule) err = extractAlertNodes(p, &rule)
return rule, err return rule, err
} }
func extractAlertNodes(p *pipeline.Pipeline, rule *chronograf.AlertRule) { func extractAlertNodes(p *pipeline.Pipeline, rule *chronograf.AlertRule) error {
p.Walk(func(n pipeline.Node) error { return p.Walk(func(n pipeline.Node) error {
switch t := n.(type) { switch node := n.(type) {
case *pipeline.AlertNode: case *pipeline.AlertNode:
extractHipchat(t, rule) octets, err := json.MarshalIndent(node, "", " ")
extractOpsgenie(t, rule) if err != nil {
extractPagerduty(t, rule) return err
extractVictorops(t, rule) }
extractEmail(t, rule) return json.Unmarshal(octets, &rule.AlertNodes)
extractPost(t, rule)
extractAlerta(t, rule)
extractSensu(t, rule)
extractSlack(t, rule)
extractTalk(t, rule)
extractTelegram(t, rule)
extractPushover(t, rule)
extractTCP(t, rule)
extractLog(t, rule)
extractExec(t, rule)
} }
return nil return nil
}) })
} }
func extractHipchat(node *pipeline.AlertNode, rule *chronograf.AlertRule) {
if len(node.HipChatHandlers) == 0 {
return
}
rule.Alerts = append(rule.Alerts, "hipchat")
h := node.HipChatHandlers[0]
alert := chronograf.KapacitorNode{
Name: "hipchat",
}
if h.Room != "" {
alert.Properties = append(alert.Properties, chronograf.KapacitorProperty{
Name: "room",
Args: []string{h.Room},
})
}
if h.Token != "" {
alert.Properties = append(alert.Properties, chronograf.KapacitorProperty{
Name: "token",
Args: []string{h.Token},
})
}
rule.AlertNodes = append(rule.AlertNodes, alert)
}
func extractOpsgenie(node *pipeline.AlertNode, rule *chronograf.AlertRule) {
if len(node.OpsGenieHandlers) == 0 {
return
}
rule.Alerts = append(rule.Alerts, "opsgenie")
o := node.OpsGenieHandlers[0]
alert := chronograf.KapacitorNode{
Name: "opsgenie",
}
if len(o.RecipientsList) != 0 {
alert.Properties = append(alert.Properties, chronograf.KapacitorProperty{
Name: "recipients",
Args: o.RecipientsList,
})
}
if len(o.TeamsList) != 0 {
alert.Properties = append(alert.Properties, chronograf.KapacitorProperty{
Name: "teams",
Args: o.TeamsList,
})
}
rule.AlertNodes = append(rule.AlertNodes, alert)
}
func extractPagerduty(node *pipeline.AlertNode, rule *chronograf.AlertRule) {
if len(node.PagerDutyHandlers) == 0 {
return
}
rule.Alerts = append(rule.Alerts, "pagerduty")
p := node.PagerDutyHandlers[0]
alert := chronograf.KapacitorNode{
Name: "pagerduty",
}
if p.ServiceKey != "" {
alert.Properties = append(alert.Properties, chronograf.KapacitorProperty{
Name: "serviceKey",
Args: []string{p.ServiceKey},
})
}
rule.AlertNodes = append(rule.AlertNodes, alert)
}
func extractVictorops(node *pipeline.AlertNode, rule *chronograf.AlertRule) {
if len(node.VictorOpsHandlers) == 0 {
return
}
rule.Alerts = append(rule.Alerts, "victorops")
v := node.VictorOpsHandlers[0]
alert := chronograf.KapacitorNode{
Name: "victorops",
}
if v.RoutingKey != "" {
alert.Properties = append(alert.Properties, chronograf.KapacitorProperty{
Name: "routingKey",
Args: []string{v.RoutingKey},
})
}
rule.AlertNodes = append(rule.AlertNodes, alert)
}
func extractEmail(node *pipeline.AlertNode, rule *chronograf.AlertRule) {
if len(node.EmailHandlers) == 0 {
return
}
rule.Alerts = append(rule.Alerts, "smtp")
e := node.EmailHandlers[0]
alert := chronograf.KapacitorNode{
Name: "smtp",
}
if len(e.ToList) != 0 {
alert.Args = e.ToList
}
rule.AlertNodes = append(rule.AlertNodes, alert)
}
func extractPost(node *pipeline.AlertNode, rule *chronograf.AlertRule) {
if len(node.HTTPPostHandlers) == 0 {
return
}
rule.Alerts = append(rule.Alerts, "http")
p := node.HTTPPostHandlers[0]
alert := chronograf.KapacitorNode{
Name: "http",
}
if p.URL != "" {
alert.Args = []string{p.URL}
}
if p.Endpoint != "" {
alert.Properties = append(alert.Properties, chronograf.KapacitorProperty{
Name: "endpoint",
Args: []string{p.Endpoint},
})
}
if len(p.Headers) > 0 {
for k, v := range p.Headers {
alert.Properties = append(alert.Properties, chronograf.KapacitorProperty{
Name: "header",
Args: []string{k, v},
})
}
}
rule.AlertNodes = append(rule.AlertNodes, alert)
}
func extractAlerta(node *pipeline.AlertNode, rule *chronograf.AlertRule) {
if len(node.AlertaHandlers) == 0 {
return
}
rule.Alerts = append(rule.Alerts, "alerta")
a := node.AlertaHandlers[0]
alert := chronograf.KapacitorNode{
Name: "alerta",
}
if a.Token != "" {
alert.Properties = append(alert.Properties, chronograf.KapacitorProperty{
Name: "token",
Args: []string{a.Token},
})
}
if a.Resource != "" {
alert.Properties = append(alert.Properties, chronograf.KapacitorProperty{
Name: "resource",
Args: []string{a.Resource},
})
}
if a.Event != "" {
alert.Properties = append(alert.Properties, chronograf.KapacitorProperty{
Name: "event",
Args: []string{a.Event},
})
}
if a.Environment != "" {
alert.Properties = append(alert.Properties, chronograf.KapacitorProperty{
Name: "environment",
Args: []string{a.Environment},
})
}
if a.Group != "" {
alert.Properties = append(alert.Properties, chronograf.KapacitorProperty{
Name: "group",
Args: []string{a.Group},
})
}
if a.Value != "" {
alert.Properties = append(alert.Properties, chronograf.KapacitorProperty{
Name: "value",
Args: []string{a.Value},
})
}
if a.Origin != "" {
alert.Properties = append(alert.Properties, chronograf.KapacitorProperty{
Name: "origin",
Args: []string{a.Origin},
})
}
if a.Service != nil {
alert.Properties = append(alert.Properties, chronograf.KapacitorProperty{
Name: "services",
Args: a.Service,
})
}
rule.AlertNodes = append(rule.AlertNodes, alert)
}
func extractSensu(node *pipeline.AlertNode, rule *chronograf.AlertRule) {
if len(node.SensuHandlers) == 0 {
return
}
rule.Alerts = append(rule.Alerts, "sensu")
alert := chronograf.KapacitorNode{
Name: "sensu",
}
rule.AlertNodes = append(rule.AlertNodes, alert)
}
func extractSlack(node *pipeline.AlertNode, rule *chronograf.AlertRule) {
if len(node.SlackHandlers) == 0 {
return
}
rule.Alerts = append(rule.Alerts, "slack")
s := node.SlackHandlers[0]
alert := chronograf.KapacitorNode{
Name: "slack",
}
if s.Channel != "" {
alert.Properties = append(alert.Properties, chronograf.KapacitorProperty{
Name: "channel",
Args: []string{s.Channel},
})
}
if s.IconEmoji != "" {
alert.Properties = append(alert.Properties, chronograf.KapacitorProperty{
Name: "iconEmoji",
Args: []string{s.IconEmoji},
})
}
if s.Username != "" {
alert.Properties = append(alert.Properties, chronograf.KapacitorProperty{
Name: "username",
Args: []string{s.Username},
})
}
rule.AlertNodes = append(rule.AlertNodes, alert)
}
func extractTalk(node *pipeline.AlertNode, rule *chronograf.AlertRule) {
if len(node.TalkHandlers) == 0 {
return
}
rule.Alerts = append(rule.Alerts, "talk")
alert := chronograf.KapacitorNode{
Name: "talk",
}
rule.AlertNodes = append(rule.AlertNodes, alert)
}
func extractTelegram(node *pipeline.AlertNode, rule *chronograf.AlertRule) {
if len(node.TelegramHandlers) == 0 {
return
}
rule.Alerts = append(rule.Alerts, "telegram")
t := node.TelegramHandlers[0]
alert := chronograf.KapacitorNode{
Name: "telegram",
}
if t.ChatId != "" {
alert.Properties = append(alert.Properties, chronograf.KapacitorProperty{
Name: "chatId",
Args: []string{t.ChatId},
})
}
if t.ParseMode != "" {
alert.Properties = append(alert.Properties, chronograf.KapacitorProperty{
Name: "parseMode",
Args: []string{t.ParseMode},
})
}
if t.IsDisableWebPagePreview {
alert.Properties = append(alert.Properties, chronograf.KapacitorProperty{
Name: "disableWebPagePreview",
})
}
if t.IsDisableNotification {
alert.Properties = append(alert.Properties, chronograf.KapacitorProperty{
Name: "disableNotification",
})
}
rule.AlertNodes = append(rule.AlertNodes, alert)
}
func extractTCP(node *pipeline.AlertNode, rule *chronograf.AlertRule) {
if len(node.TcpHandlers) == 0 {
return
}
rule.Alerts = append(rule.Alerts, "tcp")
t := node.TcpHandlers[0]
alert := chronograf.KapacitorNode{
Name: "tcp",
}
if t.Address != "" {
alert.Args = []string{t.Address}
}
rule.AlertNodes = append(rule.AlertNodes, alert)
}
func extractLog(node *pipeline.AlertNode, rule *chronograf.AlertRule) {
if len(node.LogHandlers) == 0 {
return
}
rule.Alerts = append(rule.Alerts, "log")
log := node.LogHandlers[0]
alert := chronograf.KapacitorNode{
Name: "log",
}
if log.FilePath != "" {
alert.Args = []string{log.FilePath}
}
rule.AlertNodes = append(rule.AlertNodes, alert)
}
func extractExec(node *pipeline.AlertNode, rule *chronograf.AlertRule) {
if len(node.ExecHandlers) == 0 {
return
}
rule.Alerts = append(rule.Alerts, "exec")
exec := node.ExecHandlers[0]
alert := chronograf.KapacitorNode{
Name: "exec",
}
if len(exec.Command) != 0 {
alert.Args = exec.Command
}
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

@ -59,7 +59,7 @@ func TestReverse(t *testing.T) {
.durationField(durationField) .durationField(durationField)
.slack() .slack()
.victorOps() .victorOps()
.email('howdy@howdy.com') .email('howdy@howdy.com', 'doody@doody.com')
.log('/tmp/alerts.log') .log('/tmp/alerts.log')
.post('http://backin.tm') .post('http://backin.tm')
.endpoint('myendpoint') .endpoint('myendpoint')
@ -69,35 +69,29 @@ func TestReverse(t *testing.T) {
want: chronograf.AlertRule{ want: chronograf.AlertRule{
Name: "name", Name: "name",
Trigger: "threshold", Trigger: "threshold",
Alerts: []string{"victorops", "smtp", "http", "slack", "log"}, AlertNodes: chronograf.AlertNodes{
AlertNodes: []chronograf.KapacitorNode{ IsStateChangesOnly: true,
{ Slack: []*chronograf.Slack{
Name: "victorops", {},
}, },
{ VictorOps: []*chronograf.VictorOps{
Name: "smtp", {},
Args: []string{"howdy@howdy.com"},
}, },
{ Email: []*chronograf.Email{
Name: "http", {
Args: []string{"http://backin.tm"}, To: []string{"howdy@howdy.com", "doody@doody.com"},
Properties: []chronograf.KapacitorProperty{
{
Name: "endpoint",
Args: []string{"myendpoint"},
},
{
Name: "header",
Args: []string{"key", "value"},
},
}, },
}, },
{ Log: []*chronograf.Log{
Name: "slack", {
FilePath: "/tmp/alerts.log",
},
}, },
{ Posts: []*chronograf.Post{
Name: "log", {
Args: []string{"/tmp/alerts.log"}, URL: "http://backin.tm",
Headers: map[string]string{"key": "value"},
},
}, },
}, },
TriggerValues: chronograf.TriggerValues{ TriggerValues: chronograf.TriggerValues{
@ -247,20 +241,19 @@ func TestReverse(t *testing.T) {
AreTagsAccepted: true, AreTagsAccepted: true,
}, },
Every: "30s", Every: "30s",
Alerts: []string{ AlertNodes: chronograf.AlertNodes{
"victorops", IsStateChangesOnly: true,
"smtp",
"slack", Slack: []*chronograf.Slack{
}, {},
AlertNodes: []chronograf.KapacitorNode{
chronograf.KapacitorNode{
Name: "victorops",
}, },
chronograf.KapacitorNode{ VictorOps: []*chronograf.VictorOps{
Name: "smtp", {},
}, },
chronograf.KapacitorNode{ Email: []*chronograf.Email{
Name: "slack", {
To: []string{},
},
}, },
}, },
Message: "message", Message: "message",
@ -354,10 +347,14 @@ func TestReverse(t *testing.T) {
|httpOut('output') |httpOut('output')
`, `,
want: chronograf.AlertRule{ want: chronograf.AlertRule{
Name: "haproxy", Name: "haproxy",
Trigger: "threshold", Trigger: "threshold",
Alerts: []string{"smtp"}, AlertNodes: chronograf.AlertNodes{
AlertNodes: []chronograf.KapacitorNode{chronograf.KapacitorNode{Name: "smtp"}}, IsStateChangesOnly: true,
Email: []*chronograf.Email{
{To: []string{}},
},
},
TriggerValues: chronograf.TriggerValues{ TriggerValues: chronograf.TriggerValues{
Operator: "equal to", Operator: "equal to",
Value: "DOWN", Value: "DOWN",
@ -473,10 +470,10 @@ func TestReverse(t *testing.T) {
want: chronograf.AlertRule{ want: chronograf.AlertRule{
Name: "haproxy", Name: "haproxy",
Trigger: "threshold", Trigger: "threshold",
Alerts: []string{"smtp"}, AlertNodes: chronograf.AlertNodes{
AlertNodes: []chronograf.KapacitorNode{ IsStateChangesOnly: true,
chronograf.KapacitorNode{ Email: []*chronograf.Email{
Name: "smtp", {To: []string{}},
}, },
}, },
TriggerValues: chronograf.TriggerValues{ TriggerValues: chronograf.TriggerValues{
@ -596,11 +593,17 @@ func TestReverse(t *testing.T) {
want: chronograf.AlertRule{ want: chronograf.AlertRule{
Name: "name", Name: "name",
Trigger: "threshold", Trigger: "threshold",
Alerts: []string{"victorops", "smtp", "slack"}, AlertNodes: chronograf.AlertNodes{
AlertNodes: []chronograf.KapacitorNode{ IsStateChangesOnly: true,
{Name: "victorops"}, Slack: []*chronograf.Slack{
{Name: "smtp"}, {},
{Name: "slack"}, },
VictorOps: []*chronograf.VictorOps{
{},
},
Email: []*chronograf.Email{
{To: []string{}},
},
}, },
TriggerValues: chronograf.TriggerValues{ TriggerValues: chronograf.TriggerValues{
Operator: "greater than", Operator: "greater than",
@ -727,11 +730,17 @@ func TestReverse(t *testing.T) {
want: chronograf.AlertRule{ want: chronograf.AlertRule{
Name: "name", Name: "name",
Trigger: "threshold", Trigger: "threshold",
Alerts: []string{"victorops", "smtp", "slack"}, AlertNodes: chronograf.AlertNodes{
AlertNodes: []chronograf.KapacitorNode{ IsStateChangesOnly: true,
{Name: "victorops"}, Slack: []*chronograf.Slack{
{Name: "smtp"}, {},
{Name: "slack"}, },
VictorOps: []*chronograf.VictorOps{
{},
},
Email: []*chronograf.Email{
{To: []string{}},
},
}, },
TriggerValues: chronograf.TriggerValues{ TriggerValues: chronograf.TriggerValues{
Operator: "inside range", Operator: "inside range",
@ -858,11 +867,17 @@ func TestReverse(t *testing.T) {
want: chronograf.AlertRule{ want: chronograf.AlertRule{
Name: "name", Name: "name",
Trigger: "threshold", Trigger: "threshold",
Alerts: []string{"victorops", "smtp", "slack"}, AlertNodes: chronograf.AlertNodes{
AlertNodes: []chronograf.KapacitorNode{ IsStateChangesOnly: true,
{Name: "victorops"}, Slack: []*chronograf.Slack{
{Name: "smtp"}, {},
{Name: "slack"}, },
VictorOps: []*chronograf.VictorOps{
{},
},
Email: []*chronograf.Email{
{To: []string{}},
},
}, },
TriggerValues: chronograf.TriggerValues{ TriggerValues: chronograf.TriggerValues{
Operator: "outside range", Operator: "outside range",
@ -979,11 +994,17 @@ func TestReverse(t *testing.T) {
want: chronograf.AlertRule{ want: chronograf.AlertRule{
Name: "name", Name: "name",
Trigger: "threshold", Trigger: "threshold",
Alerts: []string{"victorops", "smtp", "slack"}, AlertNodes: chronograf.AlertNodes{
AlertNodes: []chronograf.KapacitorNode{ IsStateChangesOnly: true,
{Name: "victorops"}, Slack: []*chronograf.Slack{
{Name: "smtp"}, {},
{Name: "slack"}, },
VictorOps: []*chronograf.VictorOps{
{},
},
Email: []*chronograf.Email{
{To: []string{}},
},
}, },
TriggerValues: chronograf.TriggerValues{ TriggerValues: chronograf.TriggerValues{
Operator: "greater than", Operator: "greater than",
@ -1111,11 +1132,17 @@ trigger
want: chronograf.AlertRule{ want: chronograf.AlertRule{
Name: "name", Name: "name",
Trigger: "relative", Trigger: "relative",
Alerts: []string{"victorops", "smtp", "slack"}, AlertNodes: chronograf.AlertNodes{
AlertNodes: []chronograf.KapacitorNode{ IsStateChangesOnly: true,
{Name: "victorops"}, Slack: []*chronograf.Slack{
{Name: "smtp"}, {},
{Name: "slack"}, },
VictorOps: []*chronograf.VictorOps{
{},
},
Email: []*chronograf.Email{
{To: []string{}},
},
}, },
TriggerValues: chronograf.TriggerValues{ TriggerValues: chronograf.TriggerValues{
Change: "% change", Change: "% change",
@ -1253,11 +1280,17 @@ trigger
want: chronograf.AlertRule{ want: chronograf.AlertRule{
Name: "name", Name: "name",
Trigger: "relative", Trigger: "relative",
Alerts: []string{"victorops", "smtp", "slack"}, AlertNodes: chronograf.AlertNodes{
AlertNodes: []chronograf.KapacitorNode{ IsStateChangesOnly: true,
{Name: "victorops"}, Slack: []*chronograf.Slack{
{Name: "smtp"}, {},
{Name: "slack"}, },
VictorOps: []*chronograf.VictorOps{
{},
},
Email: []*chronograf.Email{
{To: []string{}},
},
}, },
TriggerValues: chronograf.TriggerValues{ TriggerValues: chronograf.TriggerValues{
Change: "change", Change: "change",
@ -1377,11 +1410,17 @@ trigger
want: chronograf.AlertRule{ want: chronograf.AlertRule{
Name: "name", Name: "name",
Trigger: "deadman", Trigger: "deadman",
Alerts: []string{"victorops", "smtp", "slack"}, AlertNodes: chronograf.AlertNodes{
AlertNodes: []chronograf.KapacitorNode{ IsStateChangesOnly: true,
{Name: "victorops"}, Slack: []*chronograf.Slack{
{Name: "smtp"}, {},
{Name: "slack"}, },
VictorOps: []*chronograf.VictorOps{
{},
},
Email: []*chronograf.Email{
{To: []string{}},
},
}, },
TriggerValues: chronograf.TriggerValues{ TriggerValues: chronograf.TriggerValues{
Period: "10m0s", Period: "10m0s",
@ -1480,7 +1519,6 @@ trigger
want: chronograf.AlertRule{ want: chronograf.AlertRule{
Name: "rule 1", Name: "rule 1",
Trigger: "threshold", Trigger: "threshold",
Alerts: []string{},
TriggerValues: chronograf.TriggerValues{ TriggerValues: chronograf.TriggerValues{
Operator: "greater than", Operator: "greater than",
Value: "90000", Value: "90000",
@ -1488,6 +1526,9 @@ trigger
Every: "", Every: "",
Message: "", Message: "",
Details: "", Details: "",
AlertNodes: chronograf.AlertNodes{
IsStateChangesOnly: true,
},
Query: &chronograf.QueryConfig{ Query: &chronograf.QueryConfig{
Database: "_internal", Database: "_internal",
RetentionPolicy: "monitor", RetentionPolicy: "monitor",
@ -1514,7 +1555,7 @@ trigger
return return
} }
if !reflect.DeepEqual(got, tt.want) { if !reflect.DeepEqual(got, tt.want) {
t.Errorf("Reverse() = \n%#v\n, want \n%#v\n", got, tt.want) t.Errorf("Reverse() = %s", cmp.Diff(got, tt.want))
if tt.want.Query != nil { if tt.want.Query != nil {
if got.Query == nil { if got.Query == nil {
t.Errorf("Reverse() = got nil QueryConfig") t.Errorf("Reverse() = got nil QueryConfig")

View File

@ -131,7 +131,7 @@ func TestClient_All(t *testing.T) {
ID: "howdy", ID: "howdy",
Name: "howdy", Name: "howdy",
TICKScript: "", TICKScript: "",
Type: "unknown TaskType 0", Type: "invalid",
Status: "enabled", Status: "enabled",
DBRPs: []chronograf.DBRP{}, DBRPs: []chronograf.DBRP{},
}, },
@ -318,11 +318,13 @@ trigger
|httpOut('output') |httpOut('output')
`, `,
Trigger: "threshold", Trigger: "threshold",
Alerts: []string{},
TriggerValues: chronograf.TriggerValues{ TriggerValues: chronograf.TriggerValues{
Operator: "greater than", Operator: "greater than",
Value: "90000", Value: "90000",
}, },
AlertNodes: chronograf.AlertNodes{
IsStateChangesOnly: true,
},
Query: &chronograf.QueryConfig{ Query: &chronograf.QueryConfig{
Database: "_internal", Database: "_internal",
RetentionPolicy: "monitor", RetentionPolicy: "monitor",
@ -647,11 +649,13 @@ trigger
|httpOut('output') |httpOut('output')
`, `,
Trigger: "threshold", Trigger: "threshold",
Alerts: []string{},
TriggerValues: chronograf.TriggerValues{ TriggerValues: chronograf.TriggerValues{
Operator: "greater than", Operator: "greater than",
Value: "90000", Value: "90000",
}, },
AlertNodes: chronograf.AlertNodes{
IsStateChangesOnly: true,
},
Query: &chronograf.QueryConfig{ Query: &chronograf.QueryConfig{
Database: "_internal", Database: "_internal",
RetentionPolicy: "monitor", RetentionPolicy: "monitor",
@ -1124,7 +1128,7 @@ func TestClient_Update(t *testing.T) {
}, },
Trigger: Relative, Trigger: Relative,
TriggerValues: chronograf.TriggerValues{ TriggerValues: chronograf.TriggerValues{
Operator: InsideRange, Operator: insideRange,
}, },
}, },
}, },
@ -1289,7 +1293,157 @@ func TestClient_Create(t *testing.T) {
createTaskOptions *client.CreateTaskOptions createTaskOptions *client.CreateTaskOptions
}{ }{
{ {
name: "create alert rule", name: "create alert rule with tags",
fields: fields{
kapaClient: func(url, username, password string, insecureSkipVerify bool) (KapaClient, error) {
return kapa, nil
},
Ticker: &Alert{},
ID: &MockID{
ID: "howdy",
},
},
args: args{
ctx: context.Background(),
rule: chronograf.AlertRule{
ID: "howdy",
Name: "myname's",
Query: &chronograf.QueryConfig{
Database: "db",
RetentionPolicy: "rp",
Measurement: "meas",
GroupBy: chronograf.GroupBy{
Tags: []string{
"tag1",
"tag2",
},
},
},
Trigger: Deadman,
TriggerValues: chronograf.TriggerValues{
Period: "1d",
},
},
},
resTask: client.Task{
ID: "chronograf-v1-howdy",
Status: client.Enabled,
Type: client.StreamTask,
DBRPs: []client.DBRP{
{
Database: "db",
RetentionPolicy: "rp",
},
},
Link: client.Link{
Href: "/kapacitor/v1/tasks/chronograf-v1-howdy",
},
},
createTaskOptions: &client.CreateTaskOptions{
TICKscript: `var db = 'db'
var rp = 'rp'
var measurement = 'meas'
var groupBy = ['tag1', 'tag2']
var whereFilter = lambda: TRUE
var period = 1d
var name = 'myname\'s'
var idVar = name + ':{{.Group}}'
var message = ''
var idTag = 'alertID'
var levelTag = 'level'
var messageField = 'message'
var durationField = 'duration'
var outputDB = 'chronograf'
var outputRP = 'autogen'
var outputMeasurement = 'alerts'
var triggerType = 'deadman'
var threshold = 0.0
var data = stream
|from()
.database(db)
.retentionPolicy(rp)
.measurement(measurement)
.groupBy(groupBy)
.where(whereFilter)
var trigger = data
|deadman(threshold, period)
.stateChangesOnly()
.message(message)
.id(idVar)
.idTag(idTag)
.levelTag(levelTag)
.messageField(messageField)
.durationField(durationField)
trigger
|eval(lambda: "emitted")
.as('value')
.keep('value', messageField, durationField)
|eval(lambda: float("value"))
.as('value')
.keep()
|influxDBOut()
.create()
.database(outputDB)
.retentionPolicy(outputRP)
.measurement(outputMeasurement)
.tag('alertName', name)
.tag('triggerType', triggerType)
trigger
|httpOut('output')
`,
ID: "chronograf-v1-howdy",
Type: client.StreamTask,
Status: client.Enabled,
DBRPs: []client.DBRP{
{
Database: "db",
RetentionPolicy: "rp",
},
},
},
want: &Task{
ID: "chronograf-v1-howdy",
Href: "/kapacitor/v1/tasks/chronograf-v1-howdy",
HrefOutput: "/kapacitor/v1/tasks/chronograf-v1-howdy/output",
Rule: chronograf.AlertRule{
Type: "stream",
DBRPs: []chronograf.DBRP{
{
DB: "db",
RP: "rp",
},
},
Status: "enabled",
ID: "chronograf-v1-howdy",
Name: "chronograf-v1-howdy",
},
},
},
{
name: "create alert rule with no tags",
fields: fields{ fields: fields{
kapaClient: func(url, username, password string, insecureSkipVerify bool) (KapaClient, error) { kapaClient: func(url, username, password string, insecureSkipVerify bool) (KapaClient, error) {
return kapa, nil return kapa, nil
@ -1344,7 +1498,7 @@ var period = 1d
var name = 'myname\'s' var name = 'myname\'s'
var idVar = name + ':{{.Group}}' var idVar = name
var message = '' var message = ''

View File

@ -40,7 +40,10 @@ func benchmark_PaginatingKapaClient(taskCount int, b *testing.B) {
}, },
} }
pkap := kapacitor.PaginatingKapaClient{mockClient, 50} pkap := kapacitor.PaginatingKapaClient{
KapaClient: mockClient,
FetchRate: 50,
}
opts := &client.ListTasksOptions{} opts := &client.ListTasksOptions{}

View File

@ -34,7 +34,10 @@ func Test_Kapacitor_PaginatingKapaClient(t *testing.T) {
}, },
} }
pkap := kapacitor.PaginatingKapaClient{mockClient, 50} pkap := kapacitor.PaginatingKapaClient{
KapaClient: mockClient,
FetchRate: 50,
}
opts := &client.ListTasksOptions{ opts := &client.ListTasksOptions{
Limit: 100, Limit: 100,

View File

@ -7,12 +7,12 @@ import (
const ( const (
greaterThan = "greater than" greaterThan = "greater than"
lessThan = "less than" lessThan = "less than"
LessThanEqual = "equal to or less than" lessThanEqual = "equal to or less than"
GreaterThanEqual = "equal to or greater" greaterThanEqual = "equal to or greater"
Equal = "equal to" equal = "equal to"
NotEqual = "not equal to" notEqual = "not equal to"
InsideRange = "inside range" insideRange = "inside range"
OutsideRange = "outside range" outsideRange = "outside range"
) )
// kapaOperator converts UI strings to kapacitor operators // kapaOperator converts UI strings to kapacitor operators
@ -22,13 +22,13 @@ func kapaOperator(operator string) (string, error) {
return ">", nil return ">", nil
case lessThan: case lessThan:
return "<", nil return "<", nil
case LessThanEqual: case lessThanEqual:
return "<=", nil return "<=", nil
case GreaterThanEqual: case greaterThanEqual:
return ">=", nil return ">=", nil
case Equal: case equal:
return "==", nil return "==", nil
case NotEqual: case notEqual:
return "!=", nil return "!=", nil
default: default:
return "", fmt.Errorf("invalid operator: %s is unknown", operator) return "", fmt.Errorf("invalid operator: %s is unknown", operator)
@ -42,13 +42,13 @@ func chronoOperator(operator string) (string, error) {
case "<": case "<":
return lessThan, nil return lessThan, nil
case "<=": case "<=":
return LessThanEqual, nil return lessThanEqual, nil
case ">=": case ">=":
return GreaterThanEqual, nil return greaterThanEqual, nil
case "==": case "==":
return Equal, nil return equal, nil
case "!=": case "!=":
return NotEqual, nil return notEqual, nil
default: default:
return "", fmt.Errorf("invalid operator: %s is unknown", operator) return "", fmt.Errorf("invalid operator: %s is unknown", operator)
} }
@ -56,9 +56,9 @@ func chronoOperator(operator string) (string, error) {
func rangeOperators(operator string) ([]string, error) { func rangeOperators(operator string) ([]string, error) {
switch operator { switch operator {
case InsideRange: case insideRange:
return []string{">=", "AND", "<="}, nil return []string{">=", "AND", "<="}, nil
case OutsideRange: case outsideRange:
return []string{"<", "OR", ">"}, nil return []string{"<", "OR", ">"}, nil
default: default:
return nil, fmt.Errorf("invalid operator: %s is unknown", operator) return nil, fmt.Errorf("invalid operator: %s is unknown", operator)
@ -70,9 +70,9 @@ func chronoRangeOperators(ops []string) (string, error) {
return "", fmt.Errorf("Unknown operators") return "", fmt.Errorf("Unknown operators")
} }
if ops[0] == ">=" && ops[1] == "AND" && ops[2] == "<=" { if ops[0] == ">=" && ops[1] == "AND" && ops[2] == "<=" {
return InsideRange, nil return insideRange, nil
} else if ops[0] == "<" && ops[1] == "OR" && ops[2] == ">" { } else if ops[0] == "<" && ops[1] == "OR" && ops[2] == ">" {
return OutsideRange, nil return outsideRange, nil
} }
return "", fmt.Errorf("Unknown operators") return "", fmt.Errorf("Unknown operators")
} }

37
kapacitor/pipeline.go Normal file
View File

@ -0,0 +1,37 @@
package kapacitor
import (
"bytes"
"encoding/json"
"github.com/influxdata/chronograf"
"github.com/influxdata/kapacitor/pipeline"
totick "github.com/influxdata/kapacitor/pipeline/tick"
)
// MarshalTICK converts tickscript to JSON representation
func MarshalTICK(script string) ([]byte, error) {
pipeline, err := newPipeline(chronograf.TICKScript(script))
if err != nil {
return nil, err
}
return json.MarshalIndent(pipeline, "", " ")
}
// UnmarshalTICK converts JSON to tickscript
func UnmarshalTICK(octets []byte) (string, error) {
pipe := &pipeline.Pipeline{}
if err := pipe.Unmarshal(octets); err != nil {
return "", err
}
ast := totick.AST{}
err := ast.Build(pipe)
if err != nil {
return "", err
}
var buf bytes.Buffer
ast.Program.Format(&buf, "", false)
return buf.String(), nil
}

341
kapacitor/pipeline_test.go Normal file
View File

@ -0,0 +1,341 @@
package kapacitor
import (
"fmt"
"testing"
"github.com/sergi/go-diff/diffmatchpatch"
)
func TestPipelineJSON(t *testing.T) {
script := `var db = 'telegraf'
var rp = 'autogen'
var measurement = 'cpu'
var groupBy = ['host', 'cluster_id']
var whereFilter = lambda: ("cpu" == 'cpu_total') AND ("host" == 'acc-0eabc309-eu-west-1-data-3' OR "host" == 'prod')
var period = 10m
var every = 30s
var name = 'name'
var idVar = name + ':{{.Group}}'
var message = 'message'
var idTag = 'alertID'
var levelTag = 'level'
var messageField = 'message'
var durationField = 'duration'
var outputDB = 'chronograf'
var outputRP = 'autogen'
var outputMeasurement = 'alerts'
var triggerType = 'threshold'
var crit = 90
var data = stream
|from()
.database(db)
.retentionPolicy(rp)
.measurement(measurement)
.groupBy(groupBy)
.where(whereFilter)
|window()
.period(period)
.every(every)
.align()
|mean('usage_user')
.as('value')
var trigger = data
|alert()
.crit(lambda: "value" > crit)
.stateChangesOnly()
.message(message)
.id(idVar)
.idTag(idTag)
.levelTag(levelTag)
.messageField(messageField)
.durationField(durationField)
.slack()
.victorOps()
.email()
trigger
|influxDBOut()
.create()
.database(outputDB)
.retentionPolicy(outputRP)
.measurement(outputMeasurement)
.tag('alertName', name)
.tag('triggerType', triggerType)
trigger
|httpOut('output')
`
want := `var alert4 = stream
|from()
.database('telegraf')
.retentionPolicy('autogen')
.measurement('cpu')
.where(lambda: "cpu" == 'cpu_total' AND "host" == 'acc-0eabc309-eu-west-1-data-3' OR "host" == 'prod')
.groupBy('host', 'cluster_id')
|window()
.period(10m)
.every(30s)
.align()
|mean('usage_user')
.as('value')
|alert()
.id('name:{{.Group}}')
.message('message')
.details('{{ json . }}')
.crit(lambda: "value" > 90)
.history(21)
.levelTag('level')
.messageField('message')
.durationField('duration')
.idTag('alertID')
.stateChangesOnly()
.email()
.victorOps()
.slack()
alert4
|httpOut('output')
alert4
|influxDBOut()
.database('chronograf')
.retentionPolicy('autogen')
.measurement('alerts')
.buffer(1000)
.flushInterval(10s)
.create()
.tag('alertName', 'name')
.tag('triggerType', 'threshold')
`
octets, err := MarshalTICK(script)
if err != nil {
t.Fatalf("MarshalTICK unexpected error %v", err)
}
got, err := UnmarshalTICK(octets)
if err != nil {
t.Fatalf("UnmarshalTICK unexpected error %v", err)
}
if got != want {
fmt.Println(got)
diff := diffmatchpatch.New()
delta := diff.DiffMain(want, got, true)
t.Errorf("%s", diff.DiffPrettyText(delta))
}
}
func TestPipelineJSONDeadman(t *testing.T) {
script := `var db = 'telegraf'
var rp = 'autogen'
var measurement = 'cpu'
var groupBy = ['host', 'cluster_id']
var whereFilter = lambda: ("cpu" == 'cpu_total') AND ("host" == 'acc-0eabc309-eu-west-1-data-3' OR "host" == 'prod')
var period = 10m
var name = 'name'
var idVar = name + ':{{.Group}}'
var message = 'message'
var idTag = 'alertID'
var levelTag = 'level'
var messageField = 'message'
var durationField = 'duration'
var outputDB = 'chronograf'
var outputRP = 'autogen'
var outputMeasurement = 'alerts'
var triggerType = 'deadman'
var threshold = 0.0
var data = stream
|from()
.database(db)
.retentionPolicy(rp)
.measurement(measurement)
.groupBy(groupBy)
.where(whereFilter)
var trigger = data
|deadman(threshold, period)
.stateChangesOnly()
.message(message)
.id(idVar)
.idTag(idTag)
.levelTag(levelTag)
.messageField(messageField)
.durationField(durationField)
.slack()
.victorOps()
.email()
trigger
|eval(lambda: "emitted")
.as('value')
.keep('value', messageField, durationField)
|influxDBOut()
.create()
.database(outputDB)
.retentionPolicy(outputRP)
.measurement(outputMeasurement)
.tag('alertName', name)
.tag('triggerType', triggerType)
trigger
|httpOut('output')
`
wantA := `var from1 = stream
|from()
.database('telegraf')
.retentionPolicy('autogen')
.measurement('cpu')
.where(lambda: "cpu" == 'cpu_total' AND "host" == 'acc-0eabc309-eu-west-1-data-3' OR "host" == 'prod')
.groupBy('host', 'cluster_id')
var alert5 = from1
|stats(10m)
.align()
|derivative('emitted')
.as('emitted')
.unit(10m)
.nonNegative()
|alert()
.id('name:{{.Group}}')
.message('message')
.details('{{ json . }}')
.crit(lambda: "emitted" <= 0.0)
.history(21)
.levelTag('level')
.messageField('message')
.durationField('duration')
.idTag('alertID')
.stateChangesOnly()
.email()
.victorOps()
.slack()
alert5
|httpOut('output')
alert5
|eval(lambda: "emitted")
.as('value')
.tags()
.keep('value', 'message', 'duration')
|influxDBOut()
.database('chronograf')
.retentionPolicy('autogen')
.measurement('alerts')
.buffer(1000)
.flushInterval(10s)
.create()
.tag('alertName', 'name')
.tag('triggerType', 'deadman')
`
wantB := `var from1 = stream
|from()
.database('telegraf')
.retentionPolicy('autogen')
.measurement('cpu')
.where(lambda: "cpu" == 'cpu_total' AND "host" == 'acc-0eabc309-eu-west-1-data-3' OR "host" == 'prod')
.groupBy('host', 'cluster_id')
var alert5 = from1
|stats(10m)
.align()
|derivative('emitted')
.as('emitted')
.unit(10m)
.nonNegative()
|alert()
.id('name:{{.Group}}')
.message('message')
.details('{{ json . }}')
.crit(lambda: "emitted" <= 0.0)
.history(21)
.levelTag('level')
.messageField('message')
.durationField('duration')
.idTag('alertID')
.stateChangesOnly()
.email()
.victorOps()
.slack()
alert5
|eval(lambda: "emitted")
.as('value')
.tags()
.keep('value', 'message', 'duration')
|influxDBOut()
.database('chronograf')
.retentionPolicy('autogen')
.measurement('alerts')
.buffer(1000)
.flushInterval(10s)
.create()
.tag('alertName', 'name')
.tag('triggerType', 'deadman')
alert5
|httpOut('output')
`
octets, err := MarshalTICK(script)
if err != nil {
t.Fatalf("MarshalTICK unexpected error %v", err)
}
got, err := UnmarshalTICK(octets)
if err != nil {
t.Fatalf("UnmarshalTICK unexpected error %v", err)
}
if got != wantA && got != wantB {
want := wantA
fmt.Println("got")
fmt.Println(got)
fmt.Println("want")
fmt.Println(want)
diff := diffmatchpatch.New()
delta := diff.DiffMain(want, got, true)
t.Errorf("%s", diff.DiffPrettyText(delta))
}
}

View File

@ -13,7 +13,11 @@ func TestGenerate(t *testing.T) {
alert := chronograf.AlertRule{ alert := chronograf.AlertRule{
Name: "name", Name: "name",
Trigger: "relative", Trigger: "relative",
Alerts: []string{"slack", "victorops", "email"}, AlertNodes: chronograf.AlertNodes{
Slack: []*chronograf.Slack{{}},
VictorOps: []*chronograf.VictorOps{{}},
Email: []*chronograf.Email{{}},
},
TriggerValues: chronograf.TriggerValues{ TriggerValues: chronograf.TriggerValues{
Change: "change", Change: "change",
Shift: "1m", Shift: "1m",
@ -65,7 +69,11 @@ func TestThreshold(t *testing.T) {
alert := chronograf.AlertRule{ alert := chronograf.AlertRule{
Name: "name", Name: "name",
Trigger: "threshold", Trigger: "threshold",
Alerts: []string{"slack", "victorops", "email"}, AlertNodes: chronograf.AlertNodes{
Slack: []*chronograf.Slack{{}},
VictorOps: []*chronograf.VictorOps{{}},
Email: []*chronograf.Email{{}},
},
TriggerValues: chronograf.TriggerValues{ TriggerValues: chronograf.TriggerValues{
Operator: "greater than", Operator: "greater than",
Value: "90", Value: "90",
@ -176,9 +184,9 @@ var trigger = data
.levelTag(levelTag) .levelTag(levelTag)
.messageField(messageField) .messageField(messageField)
.durationField(durationField) .durationField(durationField)
.slack()
.victorOps()
.email() .email()
.victorOps()
.slack()
trigger trigger
|eval(lambda: float("value")) |eval(lambda: float("value"))
@ -217,7 +225,9 @@ func TestThresholdStringCrit(t *testing.T) {
alert := chronograf.AlertRule{ alert := chronograf.AlertRule{
Name: "haproxy", Name: "haproxy",
Trigger: "threshold", Trigger: "threshold",
Alerts: []string{"email"}, AlertNodes: chronograf.AlertNodes{
Email: []*chronograf.Email{{}},
},
TriggerValues: chronograf.TriggerValues{ TriggerValues: chronograf.TriggerValues{
Operator: "equal to", Operator: "equal to",
Value: "DOWN", Value: "DOWN",
@ -364,7 +374,9 @@ func TestThresholdStringCritGreater(t *testing.T) {
alert := chronograf.AlertRule{ alert := chronograf.AlertRule{
Name: "haproxy", Name: "haproxy",
Trigger: "threshold", Trigger: "threshold",
Alerts: []string{"email"}, AlertNodes: chronograf.AlertNodes{
Email: []*chronograf.Email{{}},
},
TriggerValues: chronograf.TriggerValues{ TriggerValues: chronograf.TriggerValues{
Operator: "greater than", Operator: "greater than",
Value: "DOWN", Value: "DOWN",
@ -509,7 +521,11 @@ func TestThresholdDetail(t *testing.T) {
alert := chronograf.AlertRule{ alert := chronograf.AlertRule{
Name: "name", Name: "name",
Trigger: "threshold", Trigger: "threshold",
Alerts: []string{"slack", "victorops", "email"}, AlertNodes: chronograf.AlertNodes{
Slack: []*chronograf.Slack{{}},
VictorOps: []*chronograf.VictorOps{{}},
Email: []*chronograf.Email{{}},
},
TriggerValues: chronograf.TriggerValues{ TriggerValues: chronograf.TriggerValues{
Operator: "greater than", Operator: "greater than",
Value: "90", Value: "90",
@ -624,9 +640,9 @@ var trigger = data
.messageField(messageField) .messageField(messageField)
.durationField(durationField) .durationField(durationField)
.details(details) .details(details)
.slack()
.victorOps()
.email() .email()
.victorOps()
.slack()
trigger trigger
|eval(lambda: float("value")) |eval(lambda: float("value"))
@ -665,7 +681,11 @@ func TestThresholdInsideRange(t *testing.T) {
alert := chronograf.AlertRule{ alert := chronograf.AlertRule{
Name: "name", Name: "name",
Trigger: "threshold", Trigger: "threshold",
Alerts: []string{"slack", "victorops", "email"}, AlertNodes: chronograf.AlertNodes{
Slack: []*chronograf.Slack{{}},
VictorOps: []*chronograf.VictorOps{{}},
Email: []*chronograf.Email{{}},
},
TriggerValues: chronograf.TriggerValues{ TriggerValues: chronograf.TriggerValues{
Operator: "inside range", Operator: "inside range",
Value: "90", Value: "90",
@ -779,9 +799,9 @@ var trigger = data
.levelTag(levelTag) .levelTag(levelTag)
.messageField(messageField) .messageField(messageField)
.durationField(durationField) .durationField(durationField)
.slack()
.victorOps()
.email() .email()
.victorOps()
.slack()
trigger trigger
|eval(lambda: float("value")) |eval(lambda: float("value"))
@ -820,7 +840,11 @@ func TestThresholdOutsideRange(t *testing.T) {
alert := chronograf.AlertRule{ alert := chronograf.AlertRule{
Name: "name", Name: "name",
Trigger: "threshold", Trigger: "threshold",
Alerts: []string{"slack", "victorops", "email"}, AlertNodes: chronograf.AlertNodes{
Slack: []*chronograf.Slack{{}},
VictorOps: []*chronograf.VictorOps{{}},
Email: []*chronograf.Email{{}},
},
TriggerValues: chronograf.TriggerValues{ TriggerValues: chronograf.TriggerValues{
Operator: "outside range", Operator: "outside range",
Value: "90", Value: "90",
@ -934,9 +958,9 @@ var trigger = data
.levelTag(levelTag) .levelTag(levelTag)
.messageField(messageField) .messageField(messageField)
.durationField(durationField) .durationField(durationField)
.slack()
.victorOps()
.email() .email()
.victorOps()
.slack()
trigger trigger
|eval(lambda: float("value")) |eval(lambda: float("value"))
@ -975,7 +999,11 @@ func TestThresholdNoAggregate(t *testing.T) {
alert := chronograf.AlertRule{ alert := chronograf.AlertRule{
Name: "name", Name: "name",
Trigger: "threshold", Trigger: "threshold",
Alerts: []string{"slack", "victorops", "email"}, AlertNodes: chronograf.AlertNodes{
Slack: []*chronograf.Slack{{}},
VictorOps: []*chronograf.VictorOps{{}},
Email: []*chronograf.Email{{}},
},
TriggerValues: chronograf.TriggerValues{ TriggerValues: chronograf.TriggerValues{
Operator: "greater than", Operator: "greater than",
Value: "90", Value: "90",
@ -1072,9 +1100,9 @@ var trigger = data
.levelTag(levelTag) .levelTag(levelTag)
.messageField(messageField) .messageField(messageField)
.durationField(durationField) .durationField(durationField)
.slack()
.victorOps()
.email() .email()
.victorOps()
.slack()
trigger trigger
|eval(lambda: float("value")) |eval(lambda: float("value"))
@ -1113,7 +1141,11 @@ func TestRelative(t *testing.T) {
alert := chronograf.AlertRule{ alert := chronograf.AlertRule{
Name: "name", Name: "name",
Trigger: "relative", Trigger: "relative",
Alerts: []string{"slack", "victorops", "email"}, AlertNodes: chronograf.AlertNodes{
Slack: []*chronograf.Slack{{}},
VictorOps: []*chronograf.VictorOps{{}},
Email: []*chronograf.Email{{}},
},
TriggerValues: chronograf.TriggerValues{ TriggerValues: chronograf.TriggerValues{
Change: "% change", Change: "% change",
Shift: "1m", Shift: "1m",
@ -1238,9 +1270,9 @@ var trigger = past
.levelTag(levelTag) .levelTag(levelTag)
.messageField(messageField) .messageField(messageField)
.durationField(durationField) .durationField(durationField)
.slack()
.victorOps()
.email() .email()
.victorOps()
.slack()
trigger trigger
|eval(lambda: float("value")) |eval(lambda: float("value"))
@ -1279,7 +1311,11 @@ func TestRelativeChange(t *testing.T) {
alert := chronograf.AlertRule{ alert := chronograf.AlertRule{
Name: "name", Name: "name",
Trigger: "relative", Trigger: "relative",
Alerts: []string{"slack", "victorops", "email"}, AlertNodes: chronograf.AlertNodes{
Slack: []*chronograf.Slack{{}},
VictorOps: []*chronograf.VictorOps{{}},
Email: []*chronograf.Email{{}},
},
TriggerValues: chronograf.TriggerValues{ TriggerValues: chronograf.TriggerValues{
Change: "change", Change: "change",
Shift: "1m", Shift: "1m",
@ -1404,9 +1440,9 @@ var trigger = past
.levelTag(levelTag) .levelTag(levelTag)
.messageField(messageField) .messageField(messageField)
.durationField(durationField) .durationField(durationField)
.slack()
.victorOps()
.email() .email()
.victorOps()
.slack()
trigger trigger
|eval(lambda: float("value")) |eval(lambda: float("value"))
@ -1445,7 +1481,11 @@ func TestDeadman(t *testing.T) {
alert := chronograf.AlertRule{ alert := chronograf.AlertRule{
Name: "name", Name: "name",
Trigger: "deadman", Trigger: "deadman",
Alerts: []string{"slack", "victorops", "email"}, AlertNodes: chronograf.AlertNodes{
Slack: []*chronograf.Slack{{}},
VictorOps: []*chronograf.VictorOps{{}},
Email: []*chronograf.Email{{}},
},
TriggerValues: chronograf.TriggerValues{ TriggerValues: chronograf.TriggerValues{
Period: "10m", Period: "10m",
}, },
@ -1546,9 +1586,9 @@ var trigger = data
.levelTag(levelTag) .levelTag(levelTag)
.messageField(messageField) .messageField(messageField)
.durationField(durationField) .durationField(durationField)
.slack()
.victorOps()
.email() .email()
.victorOps()
.slack()
trigger trigger
|eval(lambda: "emitted") |eval(lambda: "emitted")

View File

@ -3,6 +3,7 @@ package kapacitor
import ( import (
"bytes" "bytes"
"fmt" "fmt"
"strings"
"time" "time"
"github.com/influxdata/chronograf" "github.com/influxdata/chronograf"
@ -33,10 +34,19 @@ func formatTick(tickscript string) (chronograf.TICKScript, error) {
} }
func validateTick(script chronograf.TICKScript) error { func validateTick(script chronograf.TICKScript) error {
_, err := newPipeline(script)
return err
}
func newPipeline(script chronograf.TICKScript) (*pipeline.Pipeline, error) {
edge := pipeline.StreamEdge
if strings.Contains(string(script), "batch") {
edge = pipeline.BatchEdge
}
scope := stateful.NewScope() scope := stateful.NewScope()
predefinedVars := map[string]tick.Var{} predefinedVars := map[string]tick.Var{}
_, err := pipeline.CreatePipeline(string(script), pipeline.StreamEdge, scope, &deadman{}, predefinedVars) return pipeline.CreatePipeline(string(script), edge, scope, &deadman{}, predefinedVars)
return err
} }
// deadman is an empty implementation of a kapacitor DeadmanService to allow CreatePipeline // deadman is an empty implementation of a kapacitor DeadmanService to allow CreatePipeline

View File

@ -123,7 +123,7 @@ func commonVars(rule chronograf.AlertRule) (string, error) {
%s %s
var name = '%s' var name = '%s'
var idVar = name + ':{{.Group}}' var idVar = %s
var message = '%s' var message = '%s'
var idTag = '%s' var idTag = '%s'
var levelTag = '%s' var levelTag = '%s'
@ -143,6 +143,7 @@ func commonVars(rule chronograf.AlertRule) (string, error) {
whereFilter(rule.Query), whereFilter(rule.Query),
wind, wind,
Escape(rule.Name), Escape(rule.Name),
idVar(rule.Query),
Escape(rule.Message), Escape(rule.Message),
IDTag, IDTag,
LevelTag, LevelTag,
@ -197,6 +198,13 @@ func groupBy(q *chronograf.QueryConfig) string {
return "[" + strings.Join(groups, ",") + "]" return "[" + strings.Join(groups, ",") + "]"
} }
func idVar(q *chronograf.QueryConfig) string {
if len(q.GroupBy.Tags) > 0 {
return `name + ':{{.Group}}'`
}
return "name"
}
func field(q *chronograf.QueryConfig) (string, error) { func field(q *chronograf.QueryConfig) (string, error) {
if q == nil { if q == nil {
return "", fmt.Errorf("No fields set in query") return "", fmt.Errorf("No fields set in query")

View File

@ -1,11 +1,14 @@
package server package server
import ( import (
"crypto/tls"
"encoding/json" "encoding/json"
"fmt" "fmt"
"net"
"net/http" "net/http"
"net/http/httputil" "net/http/httputil"
"net/url" "net/url"
"time"
"github.com/influxdata/chronograf" "github.com/influxdata/chronograf"
"github.com/influxdata/chronograf/influx" "github.com/influxdata/chronograf/influx"
@ -111,8 +114,29 @@ func (s *Service) Write(w http.ResponseWriter, r *http.Request) {
auth := influx.DefaultAuthorization(&src) auth := influx.DefaultAuthorization(&src)
auth.Set(req) auth.Set(req)
} }
proxy := &httputil.ReverseProxy{ proxy := &httputil.ReverseProxy{
Director: director, Director: director,
} }
// The connection to influxdb is using a self-signed certificate.
// This modifies uses the same values as http.DefaultTransport but specifies
// InsecureSkipVerify
if src.InsecureSkipVerify {
proxy.Transport = &http.Transport{
Proxy: http.ProxyFromEnvironment,
DialContext: (&net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second,
DualStack: true,
}).DialContext,
MaxIdleConns: 100,
IdleConnTimeout: 90 * time.Second,
TLSHandshakeTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
}
}
proxy.ServeHTTP(w, r) proxy.ServeHTTP(w, r)
} }

View File

@ -328,7 +328,7 @@ func (s *Service) KapacitorRulesPost(w http.ResponseWriter, r *http.Request) {
var req chronograf.AlertRule var req chronograf.AlertRule
if err = json.NewDecoder(r.Body).Decode(&req); err != nil { if err = json.NewDecoder(r.Body).Decode(&req); err != nil {
invalidJSON(w, s.Logger) invalidData(w, err, s.Logger)
return return
} }
// TODO: validate this data // TODO: validate this data
@ -341,7 +341,7 @@ func (s *Service) KapacitorRulesPost(w http.ResponseWriter, r *http.Request) {
task, err := c.Create(ctx, req) task, err := c.Create(ctx, req)
if err != nil { if err != nil {
Error(w, http.StatusInternalServerError, err.Error(), s.Logger) invalidData(w, err, s.Logger)
return return
} }
res := newAlertResponse(task, srv.SrcID, srv.ID) res := newAlertResponse(task, srv.SrcID, srv.ID)
@ -371,26 +371,111 @@ func newAlertResponse(task *kapa.Task, srcID, kapaID int) *alertResponse {
}, },
} }
if res.Alerts == nil { if res.AlertNodes.Alerta == nil {
res.Alerts = make([]string, 0) res.AlertNodes.Alerta = []*chronograf.Alerta{}
} }
if res.AlertNodes == nil { for i, a := range res.AlertNodes.Alerta {
res.AlertNodes = make([]chronograf.KapacitorNode, 0) if a.Service == nil {
a.Service = []string{}
res.AlertNodes.Alerta[i] = a
}
} }
for _, n := range res.AlertNodes { if res.AlertNodes.Email == nil {
if n.Args == nil { res.AlertNodes.Email = []*chronograf.Email{}
n.Args = make([]string, 0) }
for i, a := range res.AlertNodes.Email {
if a.To == nil {
a.To = []string{}
res.AlertNodes.Email[i] = a
} }
if n.Properties == nil { }
n.Properties = make([]chronograf.KapacitorProperty, 0)
if res.AlertNodes.Exec == nil {
res.AlertNodes.Exec = []*chronograf.Exec{}
}
for i, a := range res.AlertNodes.Exec {
if a.Command == nil {
a.Command = []string{}
res.AlertNodes.Exec[i] = a
} }
for _, p := range n.Properties { }
if p.Args == nil {
p.Args = make([]string, 0) if res.AlertNodes.HipChat == nil {
} res.AlertNodes.HipChat = []*chronograf.HipChat{}
}
if res.AlertNodes.Log == nil {
res.AlertNodes.Log = []*chronograf.Log{}
}
if res.AlertNodes.OpsGenie == nil {
res.AlertNodes.OpsGenie = []*chronograf.OpsGenie{}
}
for i, a := range res.AlertNodes.OpsGenie {
if a.Teams == nil {
a.Teams = []string{}
res.AlertNodes.OpsGenie[i] = a
} }
if a.Recipients == nil {
a.Recipients = []string{}
res.AlertNodes.OpsGenie[i] = a
}
}
if res.AlertNodes.PagerDuty == nil {
res.AlertNodes.PagerDuty = []*chronograf.PagerDuty{}
}
if res.AlertNodes.Posts == nil {
res.AlertNodes.Posts = []*chronograf.Post{}
}
for i, a := range res.AlertNodes.Posts {
if a.Headers == nil {
a.Headers = map[string]string{}
res.AlertNodes.Posts[i] = a
}
}
if res.AlertNodes.Pushover == nil {
res.AlertNodes.Pushover = []*chronograf.Pushover{}
}
if res.AlertNodes.Sensu == nil {
res.AlertNodes.Sensu = []*chronograf.Sensu{}
}
for i, a := range res.AlertNodes.Sensu {
if a.Handlers == nil {
a.Handlers = []string{}
res.AlertNodes.Sensu[i] = a
}
}
if res.AlertNodes.Slack == nil {
res.AlertNodes.Slack = []*chronograf.Slack{}
}
if res.AlertNodes.Talk == nil {
res.AlertNodes.Talk = []*chronograf.Talk{}
}
if res.AlertNodes.TCPs == nil {
res.AlertNodes.TCPs = []*chronograf.TCP{}
}
if res.AlertNodes.Telegram == nil {
res.AlertNodes.Telegram = []*chronograf.Telegram{}
}
if res.AlertNodes.VictorOps == nil {
res.AlertNodes.VictorOps = []*chronograf.VictorOps{}
} }
if res.Query != nil { if res.Query != nil {
@ -457,7 +542,7 @@ func (s *Service) KapacitorRulesPut(w http.ResponseWriter, r *http.Request) {
c := kapa.NewClient(srv.URL, srv.Username, srv.Password, srv.InsecureSkipVerify) c := kapa.NewClient(srv.URL, srv.Username, srv.Password, srv.InsecureSkipVerify)
var req chronograf.AlertRule var req chronograf.AlertRule
if err = json.NewDecoder(r.Body).Decode(&req); err != nil { if err = json.NewDecoder(r.Body).Decode(&req); err != nil {
invalidJSON(w, s.Logger) invalidData(w, err, s.Logger)
return return
} }
// TODO: validate this data // TODO: validate this data
@ -482,7 +567,7 @@ func (s *Service) KapacitorRulesPut(w http.ResponseWriter, r *http.Request) {
req.ID = tid req.ID = tid
task, err := c.Update(ctx, c.Href(tid), req) task, err := c.Update(ctx, c.Href(tid), req)
if err != nil { if err != nil {
Error(w, http.StatusInternalServerError, err.Error(), s.Logger) invalidData(w, err, s.Logger)
return return
} }
res := newAlertResponse(task, srv.SrcID, srv.ID) res := newAlertResponse(task, srv.SrcID, srv.ID)

View File

@ -94,9 +94,9 @@ func Test_KapacitorRulesGet(t *testing.T) {
expected []chronograf.AlertRule expected []chronograf.AlertRule
}{ }{
{ {
"basic", name: "basic",
"/chronograf/v1/sources/1/kapacitors/1/rules", requestPath: "/chronograf/v1/sources/1/kapacitors/1/rules",
[]chronograf.AlertRule{ mockAlerts: []chronograf.AlertRule{
{ {
ID: "cpu_alert", ID: "cpu_alert",
Name: "cpu_alert", Name: "cpu_alert",
@ -106,15 +106,31 @@ func Test_KapacitorRulesGet(t *testing.T) {
TICKScript: tickScript, TICKScript: tickScript,
}, },
}, },
[]chronograf.AlertRule{ expected: []chronograf.AlertRule{
{ {
ID: "cpu_alert", ID: "cpu_alert",
Name: "cpu_alert", Name: "cpu_alert",
Status: "enabled", Status: "enabled",
Type: "stream", Type: "stream",
DBRPs: []chronograf.DBRP{{DB: "telegraf", RP: "autogen"}}, DBRPs: []chronograf.DBRP{{DB: "telegraf", RP: "autogen"}},
Alerts: []string{},
TICKScript: tickScript, TICKScript: tickScript,
AlertNodes: chronograf.AlertNodes{
Posts: []*chronograf.Post{},
TCPs: []*chronograf.TCP{},
Email: []*chronograf.Email{},
Exec: []*chronograf.Exec{},
Log: []*chronograf.Log{},
VictorOps: []*chronograf.VictorOps{},
PagerDuty: []*chronograf.PagerDuty{},
Pushover: []*chronograf.Pushover{},
Sensu: []*chronograf.Sensu{},
Slack: []*chronograf.Slack{},
Telegram: []*chronograf.Telegram{},
HipChat: []*chronograf.HipChat{},
Alerta: []*chronograf.Alerta{},
OpsGenie: []*chronograf.OpsGenie{},
Talk: []*chronograf.Talk{},
},
}, },
}, },
}, },

View File

@ -1,7 +1,9 @@
package server package server
import ( import (
"crypto/tls"
"fmt" "fmt"
"net"
"net/http" "net/http"
"net/http/httputil" "net/http/httputil"
"net/url" "net/url"
@ -64,6 +66,25 @@ func (s *Service) KapacitorProxy(w http.ResponseWriter, r *http.Request) {
Director: director, Director: director,
FlushInterval: time.Second, FlushInterval: time.Second,
} }
// The connection to kapacitor is using a self-signed certificate.
// This modifies uses the same values as http.DefaultTransport but specifies
// InsecureSkipVerify
if srv.InsecureSkipVerify {
proxy.Transport = &http.Transport{
Proxy: http.ProxyFromEnvironment,
DialContext: (&net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second,
DualStack: true,
}).DialContext,
MaxIdleConns: 100,
IdleConnTimeout: 90 * time.Second,
TLSHandshakeTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
}
}
proxy.ServeHTTP(w, r) proxy.ServeHTTP(w, r)
} }

View File

@ -52,11 +52,12 @@ type Server struct {
NewSources string `long:"new-sources" description:"Config for adding a new InfluxDB source and Kapacitor server, in JSON as an array of objects, and surrounded by single quotes. E.g. --new-sources='[{\"influxdb\":{\"name\":\"Influx 1\",\"username\":\"user1\",\"password\":\"pass1\",\"url\":\"http://localhost:8086\",\"metaUrl\":\"http://metaurl.com\",\"type\":\"influx-enterprise\",\"insecureSkipVerify\":false,\"default\":true,\"telegraf\":\"telegraf\",\"sharedSecret\":\"cubeapples\"},\"kapacitor\":{\"name\":\"Kapa 1\",\"url\":\"http://localhost:9092\",\"active\":true}}]'" env:"NEW_SOURCES" hidden:"true"` NewSources string `long:"new-sources" description:"Config for adding a new InfluxDB source and Kapacitor server, in JSON as an array of objects, and surrounded by single quotes. E.g. --new-sources='[{\"influxdb\":{\"name\":\"Influx 1\",\"username\":\"user1\",\"password\":\"pass1\",\"url\":\"http://localhost:8086\",\"metaUrl\":\"http://metaurl.com\",\"type\":\"influx-enterprise\",\"insecureSkipVerify\":false,\"default\":true,\"telegraf\":\"telegraf\",\"sharedSecret\":\"cubeapples\"},\"kapacitor\":{\"name\":\"Kapa 1\",\"url\":\"http://localhost:9092\",\"active\":true}}]'" env:"NEW_SOURCES" hidden:"true"`
Develop bool `short:"d" long:"develop" description:"Run server in develop mode."` Develop bool `short:"d" long:"develop" description:"Run server in develop mode."`
BoltPath string `short:"b" long:"bolt-path" description:"Full path to boltDB file (e.g. './chronograf-v1.db')" env:"BOLT_PATH" default:"chronograf-v1.db"` BoltPath string `short:"b" long:"bolt-path" description:"Full path to boltDB file (e.g. './chronograf-v1.db')" env:"BOLT_PATH" default:"chronograf-v1.db"`
CannedPath string `short:"c" long:"canned-path" description:"Path to directory of pre-canned dashboards and application layouts (/usr/share/chronograf/canned)" env:"CANNED_PATH" default:"canned"` CannedPath string `short:"c" long:"canned-path" description:"Path to directory of pre-canned application layouts (/usr/share/chronograf/canned)" env:"CANNED_PATH" default:"canned"`
TokenSecret string `short:"t" long:"token-secret" description:"Secret to sign tokens" env:"TOKEN_SECRET"` ResourcesPath string `long:"resources-path" description:"Path to directory of pre-canned dashboards, sources, kapacitors, and organizations (/usr/share/chronograf/resources)" env:"RESOURCES_PATH" default:"canned"`
AuthDuration time.Duration `long:"auth-duration" default:"720h" description:"Total duration of cookie life for authentication (in hours). 0 means authentication expires on browser close." env:"AUTH_DURATION"` TokenSecret string `short:"t" long:"token-secret" description:"Secret to sign tokens" env:"TOKEN_SECRET"`
AuthDuration time.Duration `long:"auth-duration" default:"720h" description:"Total duration of cookie life for authentication (in hours). 0 means authentication expires on browser close." env:"AUTH_DURATION"`
GithubClientID string `short:"i" long:"github-client-id" description:"Github Client ID for OAuth 2 support" env:"GH_CLIENT_ID"` GithubClientID string `short:"i" long:"github-client-id" description:"Github Client ID for OAuth 2 support" env:"GH_CLIENT_ID"`
GithubClientSecret string `short:"s" long:"github-client-secret" description:"Github Client Secret for OAuth 2 support" env:"GH_CLIENT_SECRET"` GithubClientSecret string `short:"s" long:"github-client-secret" description:"Github Client Secret for OAuth 2 support" env:"GH_CLIENT_SECRET"`
@ -289,7 +290,7 @@ func (s *Server) newBuilders(logger chronograf.Logger) builders {
Dashboards: &MultiDashboardBuilder{ Dashboards: &MultiDashboardBuilder{
Logger: logger, Logger: logger,
ID: idgen.NewTime(), ID: idgen.NewTime(),
Path: s.CannedPath, Path: s.ResourcesPath,
}, },
Sources: &MultiSourceBuilder{ Sources: &MultiSourceBuilder{
InfluxDBURL: s.InfluxDBURL, InfluxDBURL: s.InfluxDBURL,
@ -297,7 +298,7 @@ func (s *Server) newBuilders(logger chronograf.Logger) builders {
InfluxDBPassword: s.InfluxDBPassword, InfluxDBPassword: s.InfluxDBPassword,
Logger: logger, Logger: logger,
ID: idgen.NewTime(), ID: idgen.NewTime(),
Path: s.CannedPath, Path: s.ResourcesPath,
}, },
Kapacitors: &MultiKapacitorBuilder{ Kapacitors: &MultiKapacitorBuilder{
KapacitorURL: s.KapacitorURL, KapacitorURL: s.KapacitorURL,
@ -305,11 +306,11 @@ func (s *Server) newBuilders(logger chronograf.Logger) builders {
KapacitorPassword: s.KapacitorPassword, KapacitorPassword: s.KapacitorPassword,
Logger: logger, Logger: logger,
ID: idgen.NewTime(), ID: idgen.NewTime(),
Path: s.CannedPath, Path: s.ResourcesPath,
}, },
Organizations: &MultiOrganizationBuilder{ Organizations: &MultiOrganizationBuilder{
Logger: logger, Logger: logger,
Path: s.CannedPath, Path: s.ResourcesPath,
}, },
} }
} }

View File

@ -3,7 +3,7 @@
"info": { "info": {
"title": "Chronograf", "title": "Chronograf",
"description": "API endpoints for Chronograf", "description": "API endpoints for Chronograf",
"version": "1.4.0.0" "version": "1.4.0.1"
}, },
"schemes": ["http"], "schemes": ["http"],
"basePath": "/chronograf/v1", "basePath": "/chronograf/v1",

View File

@ -1,6 +1,6 @@
{ {
"name": "chronograf-ui", "name": "chronograf-ui",
"version": "1.4.0-0", "version": "1.4.0-1",
"private": false, "private": false,
"license": "AGPL-3.0", "license": "AGPL-3.0",
"description": "", "description": "",

View File

@ -1,6 +1,5 @@
import reducer from 'src/kapacitor/reducers/rules' import reducer from 'src/kapacitor/reducers/rules'
import {defaultRuleConfigs} from 'src/kapacitor/constants' import {defaultRuleConfigs} from 'src/kapacitor/constants'
import {ALERT_NODES_ACCESSORS} from 'src/kapacitor/constants'
import { import {
chooseTrigger, chooseTrigger,
@ -9,9 +8,7 @@ import {
updateRuleValues, updateRuleValues,
updateDetails, updateDetails,
updateMessage, updateMessage,
updateAlerts,
updateAlertNodes, updateAlertNodes,
updateAlertProperty,
updateRuleName, updateRuleName,
deleteRuleSuccess, deleteRuleSuccess,
updateRuleStatusSuccess, updateRuleStatusSuccess,
@ -100,56 +97,33 @@ describe('Kapacitor.Reducers.rules', () => {
expect(newState[ruleID].message).to.equal(message) expect(newState[ruleID].message).to.equal(message)
}) })
it('can update the alerts', () => { it('can update a slack alert', () => {
const ruleID = 1 const ruleID = 1
const initialState = { const initialState = {
[ruleID]: { [ruleID]: {
id: ruleID, id: ruleID,
queryID: 988, queryID: 988,
alerts: [], alertNodes: {},
}, },
} }
const updatedSlack = {
const alerts = ['slack'] alias: 'slack-1',
const newState = reducer(initialState, updateAlerts(ruleID, alerts)) username: 'testname',
expect(newState[ruleID].alerts).to.equal(alerts) iconEmoji: 'testemoji',
}) enabled: true,
text: 'slack',
it('can update an alerta alert', () => { type: 'slack',
const ruleID = 1 url: true,
const initialState = {
[ruleID]: {
id: ruleID,
queryID: 988,
alerts: [],
alertNodes: [],
},
} }
const expectedSlack = {
const tickScript = `stream username: 'testname',
|alert() iconEmoji: 'testemoji',
.alerta() }
.resource('Hostname or service') const newState = reducer(
.event('Something went wrong')
.environment('Development')
.group('Dev. Servers')
.services('a b c')
`
let newState = reducer(
initialState, initialState,
updateAlertNodes(ruleID, 'alerta', tickScript) updateAlertNodes(ruleID, [updatedSlack])
) )
const expectedStr = `alerta().resource('Hostname or service').event('Something went wrong').environment('Development').group('Dev. Servers').services('a b c')` expect(newState[ruleID].alertNodes.slack[0]).to.deep.equal(expectedSlack)
let actualStr = ALERT_NODES_ACCESSORS.alerta(newState[ruleID])
// Test both data structure and accessor string
expect(actualStr).to.equal(expectedStr)
// Test that accessor string is the same if fed back in
newState = reducer(newState, updateAlertNodes(ruleID, 'alerta', actualStr))
actualStr = ALERT_NODES_ACCESSORS.alerta(newState[ruleID])
expect(actualStr).to.equal(expectedStr)
}) })
it('can update the name', () => { it('can update the name', () => {
@ -201,106 +175,6 @@ describe('Kapacitor.Reducers.rules', () => {
expect(newState[ruleID].details).to.equal(details) 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', () => { it('can update status', () => {
const ruleID = 1 const ruleID = 1
const status = 'enabled' const status = 'enabled'

File diff suppressed because it is too large Load Diff

View File

@ -1,46 +0,0 @@
import {parseAlerta} from 'shared/parsing/parseAlerta'
it('can parse an alerta tick script', () => {
const tickScript = `stream
|alert()
.alerta()
.resource('Hostname or service')
.event('Something went wrong')
.environment('Development')
.group('Dev. Servers')
.services('a b c')
`
let actualObj = parseAlerta(tickScript)
const expectedObj = [
{
name: 'resource',
args: ['Hostname or service'],
},
{
name: 'event',
args: ['Something went wrong'],
},
{
name: 'environment',
args: ['Development'],
},
{
name: 'group',
args: ['Dev. Servers'],
},
{
name: 'services',
args: ['a', 'b', 'c'],
},
]
// Test data structure
expect(actualObj).to.deep.equal(expectedObj)
// Test that data structure is the same if fed back in
const expectedStr = `alerta().resource('Hostname or service').event('Something went wrong').environment('Development').group('Dev. Servers').services('a b c')`
actualObj = parseAlerta(expectedStr)
expect(actualObj).to.deep.equal(expectedObj)
})

View File

@ -0,0 +1,25 @@
import parseHandlersFromConfig from 'shared/parsing/parseHandlersFromConfig'
import {
config,
configResponse,
emptyConfig,
emptyConfigResponse,
} from './constants'
describe('parseHandlersFromConfig', () => {
it('returns an array', () => {
const input = config
const actual = parseHandlersFromConfig(input)
expect(actual).to.be.a('array')
})
it('returns the right response', () => {
const input = config
const actual = parseHandlersFromConfig(input)
expect(actual).to.deep.equal(configResponse)
})
it('returns the right response even if config is empty', () => {
const input = emptyConfig
const actual = parseHandlersFromConfig(input)
expect(actual).to.deep.equal(emptyConfigResponse)
})
})

View File

@ -0,0 +1,42 @@
import {parseHandlersFromRule} from 'shared/parsing/parseHandlersFromRule'
import {
emptyRule,
emptyConfigResponse,
rule,
handlersFromConfig,
handlersOfKind_expected,
selectedHandler_expected,
handlersOnThisAlert_expected,
} from './constants'
describe('parseHandlersFromRule', () => {
it('returns empty things if rule is new and config is empty', () => {
const input1 = emptyRule
const input2 = emptyConfigResponse
const {
handlersOnThisAlert,
selectedHandler,
handlersOfKind,
} = parseHandlersFromRule(input1, input2)
const handlersOnThisAlert_expected = []
const selectedHandler_expected = null
const handlersOfKind_expected = {}
expect(handlersOnThisAlert).to.deep.equal(handlersOnThisAlert_expected)
expect(selectedHandler).to.deep.equal(selectedHandler_expected)
expect(handlersOfKind).to.deep.equal(handlersOfKind_expected)
})
it('returns values if rule and config are not empty', () => {
const input1 = rule
const input2 = handlersFromConfig
const {
handlersOnThisAlert,
selectedHandler,
handlersOfKind,
} = parseHandlersFromRule(input1, input2)
expect(handlersOnThisAlert).to.deep.equal(handlersOnThisAlert_expected)
expect(selectedHandler).to.deep.equal(selectedHandler_expected)
expect(handlersOfKind).to.deep.equal(handlersOfKind_expected)
})
})

View File

@ -1,12 +1,16 @@
import React, {PropTypes, Component} from 'react' import React, {PropTypes, Component} from 'react'
import {connect} from 'react-redux' import {connect} from 'react-redux'
import {bindActionCreators} from 'redux'
import _ from 'lodash' import _ from 'lodash'
import HostsTable from 'src/hosts/components/HostsTable' import HostsTable from 'src/hosts/components/HostsTable'
import SourceIndicator from 'shared/components/SourceIndicator' import SourceIndicator from 'shared/components/SourceIndicator'
import AutoRefreshDropdown from 'shared/components/AutoRefreshDropdown'
import ManualRefresh from 'src/shared/components/ManualRefresh'
import {getCpuAndLoadForHosts, getLayouts, getAppsForHosts} from '../apis' import {getCpuAndLoadForHosts, getLayouts, getAppsForHosts} from '../apis'
import {getEnv} from 'src/shared/apis/env' import {getEnv} from 'src/shared/apis/env'
import {setAutoRefresh} from 'shared/actions/app'
class HostsPage extends Component { class HostsPage extends Component {
constructor(props) { constructor(props) {
@ -19,59 +23,26 @@ class HostsPage extends Component {
} }
} }
async componentDidMount() { async fetchHostsData() {
const {source, links, addFlashMessage} = this.props const {source, links, addFlashMessage} = this.props
const {telegrafSystemInterval} = await getEnv(links.environment) const {telegrafSystemInterval} = await getEnv(links.environment)
const hostsError = 'Unable to get hosts'
const hostsError = 'Unable to get apps for hosts'
let hosts, layouts
try {
const [h, {data}] = await Promise.all([
getCpuAndLoadForHosts(
source.links.proxy,
source.telegraf,
telegrafSystemInterval
),
getLayouts(),
new Promise(resolve => {
this.setState({hostsLoading: true})
resolve()
}),
])
hosts = h
layouts = data.layouts
this.setState({
hosts,
hostsLoading: false,
})
} catch (error) {
this.setState({
hostsError: error.toString(),
hostsLoading: false,
})
console.error(error)
}
if (!hosts || !layouts) {
addFlashMessage({type: 'error', text: hostsError})
return this.setState({
hostsError,
hostsLoading: false,
})
}
try { try {
const hosts = await getCpuAndLoadForHosts(
source.links.proxy,
source.telegraf,
telegrafSystemInterval
)
if (!hosts) {
throw new Error(hostsError)
}
const newHosts = await getAppsForHosts( const newHosts = await getAppsForHosts(
source.links.proxy, source.links.proxy,
hosts, hosts,
layouts, this.layouts,
source.telegraf source.telegraf
) )
this.setState({ this.setState({
hosts: newHosts, hosts: newHosts,
hostsError: '', hostsError: '',
@ -87,8 +58,50 @@ class HostsPage extends Component {
} }
} }
async componentDidMount() {
const {addFlashMessage, autoRefresh} = this.props
this.setState({hostsLoading: true}) // Only print this once
const {data} = await getLayouts()
this.layouts = data.layouts
if (!this.layouts) {
const layoutError = 'Unable to get apps for hosts'
addFlashMessage({type: 'error', text: layoutError})
this.setState({
hostsError: layoutError,
hostsLoading: false,
})
return
}
await this.fetchHostsData()
if (autoRefresh) {
this.intervalID = setInterval(() => this.fetchHostsData(), autoRefresh)
}
}
componentWillReceiveProps(nextProps) {
if (this.props.manualRefresh !== nextProps.manualRefresh) {
this.fetchHostsData()
}
if (this.props.autoRefresh !== nextProps.autoRefresh) {
clearInterval(this.intervalID)
if (nextProps.autoRefresh) {
this.intervalID = setInterval(
() => this.fetchHostsData(),
nextProps.autoRefresh
)
}
}
}
render() { render() {
const {source} = this.props const {
source,
autoRefresh,
onChooseAutoRefresh,
onManualRefresh,
} = this.props
const {hosts, hostsLoading, hostsError} = this.state const {hosts, hostsLoading, hostsError} = this.state
return ( return (
<div className="page hosts-list-page"> <div className="page hosts-list-page">
@ -99,6 +112,12 @@ class HostsPage extends Component {
</div> </div>
<div className="page-header__right"> <div className="page-header__right">
<SourceIndicator /> <SourceIndicator />
<AutoRefreshDropdown
iconName="refresh"
selected={autoRefresh}
onChoose={onChooseAutoRefresh}
onManualRefresh={onManualRefresh}
/>
</div> </div>
</div> </div>
</div> </div>
@ -119,13 +138,20 @@ class HostsPage extends Component {
</div> </div>
) )
} }
componentWillUnmount() {
clearInterval(this.intervalID)
this.intervalID = false
}
} }
const {func, shape, string} = PropTypes const {func, shape, string, number} = PropTypes
const mapStateToProps = ({links}) => { const mapStateToProps = state => {
const {app: {persisted: {autoRefresh}}, links} = state
return { return {
links, links,
autoRefresh,
} }
} }
@ -143,6 +169,20 @@ HostsPage.propTypes = {
environment: string.isRequired, environment: string.isRequired,
}), }),
addFlashMessage: func, addFlashMessage: func,
autoRefresh: number.isRequired,
manualRefresh: number,
onChooseAutoRefresh: func.isRequired,
onManualRefresh: func.isRequired,
} }
export default connect(mapStateToProps, null)(HostsPage) HostsPage.defaultProps = {
manualRefresh: 0,
}
const mapDispatchToProps = dispatch => ({
onChooseAutoRefresh: bindActionCreators(setAutoRefresh, dispatch),
})
export default connect(mapStateToProps, mapDispatchToProps)(
ManualRefresh(HostsPage)
)

View File

@ -141,6 +141,10 @@ const Root = React.createClass({
<Route path="tickscript/:ruleID" component={TickscriptPage} /> <Route path="tickscript/:ruleID" component={TickscriptPage} />
<Route path="kapacitors/new" component={KapacitorPage} /> <Route path="kapacitors/new" component={KapacitorPage} />
<Route path="kapacitors/:id/edit" component={KapacitorPage} /> <Route path="kapacitors/:id/edit" component={KapacitorPage} />
<Route
path="kapacitors/:id/edit:hash"
component={KapacitorPage}
/>
<Route path="kapacitor-tasks" component={KapacitorTasksPage} /> <Route path="kapacitor-tasks" component={KapacitorTasksPage} />
<Route path="admin-chronograf" component={AdminChronografPage} /> <Route path="admin-chronograf" component={AdminChronografPage} />
<Route path="admin-influxdb" component={AdminInfluxDBPage} /> <Route path="admin-influxdb" component={AdminInfluxDBPage} />

View File

@ -136,31 +136,12 @@ export const updateDetails = (ruleID, details) => ({
}, },
}) })
export const updateAlertProperty = (ruleID, alertNodeName, alertProperty) => ({ export function updateAlertNodes(ruleID, alerts) {
type: 'UPDATE_RULE_ALERT_PROPERTY', return {
payload: { type: 'UPDATE_RULE_ALERT_NODES',
ruleID, payload: {ruleID, alerts},
alertNodeName, }
alertProperty, }
},
})
export const updateAlerts = (ruleID, alerts) => ({
type: 'UPDATE_RULE_ALERTS',
payload: {
ruleID,
alerts,
},
})
export const updateAlertNodes = (ruleID, alertNodeName, alertNodesText) => ({
type: 'UPDATE_RULE_ALERT_NODES',
payload: {
ruleID,
alertNodeName,
alertNodesText,
},
})
export const updateRuleName = (ruleID, name) => ({ export const updateRuleName = (ruleID, name) => ({
type: 'UPDATE_RULE_NAME', type: 'UPDATE_RULE_NAME',

View File

@ -2,7 +2,11 @@ import React, {Component, PropTypes} from 'react'
import _ from 'lodash' import _ from 'lodash'
import {Tab, Tabs, TabPanel, TabPanels, TabList} from 'shared/components/Tabs' import {Tab, Tabs, TabPanel, TabPanels, TabList} from 'shared/components/Tabs'
import {getKapacitorConfig, updateKapacitorConfigSection} from 'shared/apis' import {
getKapacitorConfig,
updateKapacitorConfigSection,
testAlertOutput,
} from 'shared/apis'
import { import {
AlertaConfig, AlertaConfig,
@ -23,7 +27,6 @@ class AlertTabs extends Component {
super(props) super(props)
this.state = { this.state = {
selectedEndpoint: 'smtp',
configSections: null, configSections: null,
} }
} }
@ -38,45 +41,72 @@ class AlertTabs extends Component {
} }
} }
refreshKapacitorConfig = kapacitor => { refreshKapacitorConfig = async kapacitor => {
getKapacitorConfig(kapacitor) try {
.then(({data: {sections}}) => { const {data: {sections}} = await getKapacitorConfig(kapacitor)
this.setState({configSections: sections}) this.setState({configSections: sections})
}) } catch (error) {
.catch(() => { this.setState({configSections: null})
this.setState({configSections: null}) this.props.addFlashMessage({
this.props.addFlashMessage({ type: 'error',
type: 'error', text: 'There was an error getting the Kapacitor config',
text: 'There was an error getting the Kapacitor config',
})
}) })
}
} }
getSection = (sections, section) => { getSection = (sections, section) => {
return _.get(sections, [section, 'elements', '0'], null) return _.get(sections, [section, 'elements', '0'], null)
} }
getEnabled = (sections, section) => {
return _.get(
sections,
[section, 'elements', '0', 'options', 'enabled'],
null
)
}
handleGetSection = (sections, section) => () => { handleGetSection = (sections, section) => () => {
return this.getSection(sections, section) return this.getSection(sections, section)
} }
handleSaveConfig = section => properties => { handleSaveConfig = section => async properties => {
if (section !== '') { if (section !== '') {
const propsToSend = this.sanitizeProperties(section, properties) const propsToSend = this.sanitizeProperties(section, properties)
updateKapacitorConfigSection(this.props.kapacitor, section, propsToSend) try {
.then(() => { await updateKapacitorConfigSection(
this.refreshKapacitorConfig(this.props.kapacitor) this.props.kapacitor,
this.props.addFlashMessage({ section,
type: 'success', propsToSend
text: `Alert for ${section} successfully saved`, )
}) this.refreshKapacitorConfig(this.props.kapacitor)
this.props.addFlashMessage({
type: 'success',
text: `Alert configuration for ${section} successfully saved.`,
}) })
.catch(() => { } catch (error) {
this.props.addFlashMessage({ this.props.addFlashMessage({
type: 'error', type: 'error',
text: 'There was an error saving the kapacitor config', text: `There was an error saving the alert configuration for ${section}.`,
})
}) })
}
}
}
handleTestConfig = section => async e => {
e.preventDefault()
try {
await testAlertOutput(this.props.kapacitor, section)
this.props.addFlashMessage({
type: 'success',
text: `Successfully triggered an alert to ${section}. If the alert does not reach its destination, please check your configuration settings.`,
})
} catch (error) {
this.props.addFlashMessage({
type: 'error',
text: `There was an error sending an alert to ${section}.`,
})
} }
} }
@ -94,104 +124,141 @@ class AlertTabs extends Component {
return cleanProps return cleanProps
} }
getInitialIndex = (supportedConfigs, hash) => {
const index = _.indexOf(_.keys(supportedConfigs), _.replace(hash, '#', ''))
return index >= 0 ? index : 0
}
render() { render() {
const {configSections} = this.state const {configSections} = this.state
const {hash} = this.props
if (!configSections) { if (!configSections) {
return null return null
} }
const supportedConfigs = { const supportedConfigs = {
alerta: { alerta: {
type: 'Alerta', type: 'Alerta',
enabled: this.getEnabled(configSections, 'alerta'),
renderComponent: () => renderComponent: () =>
<AlertaConfig <AlertaConfig
onSave={this.handleSaveConfig('alerta')} onSave={this.handleSaveConfig('alerta')}
config={this.getSection(configSections, 'alerta')} config={this.getSection(configSections, 'alerta')}
onTest={this.handleTestConfig('alerta')}
enabled={this.getEnabled(configSections, 'alerta')}
/>, />,
}, },
hipchat: { hipchat: {
type: 'HipChat', type: 'HipChat',
enabled: this.getEnabled(configSections, 'hipchat'),
renderComponent: () => renderComponent: () =>
<HipChatConfig <HipChatConfig
onSave={this.handleSaveConfig('hipchat')} onSave={this.handleSaveConfig('hipchat')}
config={this.getSection(configSections, 'hipchat')} config={this.getSection(configSections, 'hipchat')}
onTest={this.handleTestConfig('hipchat')}
enabled={this.getEnabled(configSections, 'hipchat')}
/>, />,
}, },
opsgenie: { opsgenie: {
type: 'OpsGenie', type: 'OpsGenie',
enabled: this.getEnabled(configSections, 'opsgenie'),
renderComponent: () => renderComponent: () =>
<OpsGenieConfig <OpsGenieConfig
onSave={this.handleSaveConfig('opsgenie')} onSave={this.handleSaveConfig('opsgenie')}
config={this.getSection(configSections, 'opsgenie')} config={this.getSection(configSections, 'opsgenie')}
onTest={this.handleTestConfig('opsgenie')}
enabled={this.getEnabled(configSections, 'opsgenie')}
/>, />,
}, },
pagerduty: { pagerduty: {
type: 'PagerDuty', type: 'PagerDuty',
enabled: this.getEnabled(configSections, 'pagerduty'),
renderComponent: () => renderComponent: () =>
<PagerDutyConfig <PagerDutyConfig
onSave={this.handleSaveConfig('pagerduty')} onSave={this.handleSaveConfig('pagerduty')}
config={this.getSection(configSections, 'pagerduty')} config={this.getSection(configSections, 'pagerduty')}
onTest={this.handleTestConfig('pagerduty')}
enabled={this.getEnabled(configSections, 'pagerduty')}
/>, />,
}, },
pushover: { pushover: {
type: 'Pushover', type: 'Pushover',
enabled: this.getEnabled(configSections, 'pushover'),
renderComponent: () => renderComponent: () =>
<PushoverConfig <PushoverConfig
onSave={this.handleSaveConfig('pushover')} onSave={this.handleSaveConfig('pushover')}
config={this.getSection(configSections, 'pushover')} config={this.getSection(configSections, 'pushover')}
onTest={this.handleTestConfig('pushover')}
enabled={this.getEnabled(configSections, 'pushover')}
/>, />,
}, },
sensu: { sensu: {
type: 'Sensu', type: 'Sensu',
enabled: this.getEnabled(configSections, 'sensu'),
renderComponent: () => renderComponent: () =>
<SensuConfig <SensuConfig
onSave={this.handleSaveConfig('sensu')} onSave={this.handleSaveConfig('sensu')}
config={this.getSection(configSections, 'sensu')} config={this.getSection(configSections, 'sensu')}
onTest={this.handleTestConfig('sensu')}
enabled={this.getEnabled(configSections, 'sensu')}
/>, />,
}, },
slack: { slack: {
type: 'Slack', type: 'Slack',
enabled: this.getEnabled(configSections, 'slack'),
renderComponent: () => renderComponent: () =>
<SlackConfig <SlackConfig
onSave={this.handleSaveConfig('slack')} onSave={this.handleSaveConfig('slack')}
config={this.getSection(configSections, 'slack')} config={this.getSection(configSections, 'slack')}
onTest={this.handleTestConfig('slack')}
enabled={this.getEnabled(configSections, 'slack')}
/>, />,
}, },
smtp: { smtp: {
type: 'SMTP', type: 'SMTP',
enabled: this.getEnabled(configSections, 'smtp'),
renderComponent: () => renderComponent: () =>
<SMTPConfig <SMTPConfig
onSave={this.handleSaveConfig('smtp')} onSave={this.handleSaveConfig('smtp')}
config={this.getSection(configSections, 'smtp')} config={this.getSection(configSections, 'smtp')}
onTest={this.handleTestConfig('smtp')}
enabled={this.getEnabled(configSections, 'smtp')}
/>, />,
}, },
talk: { talk: {
type: 'Talk', type: 'Talk',
enabled: this.getEnabled(configSections, 'talk'),
renderComponent: () => renderComponent: () =>
<TalkConfig <TalkConfig
onSave={this.handleSaveConfig('talk')} onSave={this.handleSaveConfig('talk')}
config={this.getSection(configSections, 'talk')} config={this.getSection(configSections, 'talk')}
onTest={this.handleTestConfig('talk')}
enabled={this.getEnabled(configSections, 'talk')}
/>, />,
}, },
telegram: { telegram: {
type: 'Telegram', type: 'Telegram',
enabled: this.getEnabled(configSections, 'telegram'),
renderComponent: () => renderComponent: () =>
<TelegramConfig <TelegramConfig
onSave={this.handleSaveConfig('telegram')} onSave={this.handleSaveConfig('telegram')}
config={this.getSection(configSections, 'telegram')} config={this.getSection(configSections, 'telegram')}
onTest={this.handleTestConfig('telegram')}
enabled={this.getEnabled(configSections, 'telegram')}
/>, />,
}, },
victorops: { victorops: {
type: 'VictorOps', type: 'VictorOps',
enabled: this.getEnabled(configSections, 'victorops'),
renderComponent: () => renderComponent: () =>
<VictorOpsConfig <VictorOpsConfig
onSave={this.handleSaveConfig('victorops')} onSave={this.handleSaveConfig('victorops')}
config={this.getSection(configSections, 'victorops')} config={this.getSection(configSections, 'victorops')}
onTest={this.handleTestConfig('victorops')}
enabled={this.getEnabled(configSections, 'victorops')}
/>, />,
}, },
} }
return ( return (
<div> <div>
<div className="panel panel-minimal"> <div className="panel panel-minimal">
@ -200,14 +267,20 @@ class AlertTabs extends Component {
</div> </div>
</div> </div>
<Tabs tabContentsClass="config-endpoint"> <Tabs
tabContentsClass="config-endpoint"
initialIndex={this.getInitialIndex(supportedConfigs, hash)}
>
<TabList customClass="config-endpoint--tabs"> <TabList customClass="config-endpoint--tabs">
{_.reduce( {_.reduce(
configSections, configSections,
(acc, _cur, k) => (acc, _cur, k) =>
supportedConfigs[k] supportedConfigs[k]
? acc.concat( ? acc.concat(
<Tab key={supportedConfigs[k].type}> <Tab
key={supportedConfigs[k].type}
isConfigured={supportedConfigs[k].enabled}
>
{supportedConfigs[k].type} {supportedConfigs[k].type}
</Tab> </Tab>
) )
@ -248,6 +321,7 @@ AlertTabs.propTypes = {
}).isRequired, }).isRequired,
}), }),
addFlashMessage: func.isRequired, addFlashMessage: func.isRequired,
hash: string.isRequired,
} }
export default AlertTabs export default AlertTabs

View File

@ -0,0 +1,34 @@
import React, {PropTypes} from 'react'
const HandlerCheckbox = ({
fieldName,
fieldDisplay,
selectedHandler,
handleModifyHandler,
}) =>
<div className="form-group ">
<div className="form-control-static handler-checkbox">
<input
name={fieldName}
id={fieldName}
type="checkbox"
defaultChecked={selectedHandler[fieldName]}
onClick={handleModifyHandler(selectedHandler, fieldName)}
/>
<label htmlFor={fieldName}>
{fieldDisplay}
</label>
</div>
</div>
const {func, shape, string, bool} = PropTypes
HandlerCheckbox.propTypes = {
fieldName: string,
fieldDisplay: string,
defaultChecked: bool,
selectedHandler: shape({}).isRequired,
handleModifyHandler: func.isRequired,
}
export default HandlerCheckbox

View File

@ -0,0 +1,30 @@
import React, {PropTypes} from 'react'
const HandlerEmpty = ({onGoToConfig, validationError}) =>
<div className="endpoint-tab-contents">
<div className="endpoint-tab--parameters">
<div className="endpoint-tab--parameters--empty">
<p>This handler has not been configured</p>
<div className="form-group-submit col-xs-12 text-center">
<button
className="btn btn-primary"
type="submit"
onClick={onGoToConfig}
>
{validationError
? 'Exit Rule and Configure this Alert Handler'
: 'Save Rule and Configure this Alert Handler'}
</button>
</div>
</div>
</div>
</div>
const {string, func} = PropTypes
HandlerEmpty.propTypes = {
onGoToConfig: func.isRequired,
validationError: string.isRequired,
}
export default HandlerEmpty

View File

@ -0,0 +1,69 @@
import React, {PropTypes} from 'react'
import _ from 'lodash'
const HandlerInput = ({
fieldName,
fieldDisplay,
placeholder,
selectedHandler,
handleModifyHandler,
redacted = false,
disabled = false,
fieldColumns = 'col-md-6',
parseToArray = false,
headerIndex = 0,
}) => {
const formGroupClass = `form-group ${fieldColumns}`
return (
<div className={formGroupClass}>
<label htmlFor={fieldName}>
{fieldDisplay}
</label>
<div className={redacted ? 'form-control-static redacted-handler' : null}>
<input
name={fieldName}
id={selectedHandler.alias + fieldName}
className="form-control input-sm form-malachite"
type={redacted ? 'hidden' : 'text'}
placeholder={placeholder}
onChange={handleModifyHandler(
selectedHandler,
fieldName,
parseToArray,
headerIndex
)}
value={
parseToArray
? _.join(selectedHandler[fieldName], ' ')
: selectedHandler[fieldName] || ''
}
autoComplete="off"
spellCheck="false"
disabled={disabled}
/>
{redacted
? <span className="alert-value-set">
<span className="icon checkmark" /> Value set in Config
</span>
: null}
</div>
</div>
)
}
const {func, shape, string, bool, number} = PropTypes
HandlerInput.propTypes = {
fieldName: string.isRequired,
fieldDisplay: string,
placeholder: string,
disabled: bool,
redacted: bool,
selectedHandler: shape({}).isRequired,
handleModifyHandler: func.isRequired,
fieldColumns: string,
parseToArray: bool,
headerIndex: number,
}
export default HandlerInput

View File

@ -0,0 +1,181 @@
import React, {Component, PropTypes} from 'react'
import {
PostHandler,
TcpHandler,
ExecHandler,
LogHandler,
EmailHandler,
AlertaHandler,
HipchatHandler,
OpsgenieHandler,
PagerdutyHandler,
PushoverHandler,
SensuHandler,
SlackHandler,
TalkHandler,
TelegramHandler,
VictoropsHandler,
} from './handlers'
class HandlerOptions extends Component {
constructor(props) {
super(props)
}
render() {
const {
selectedHandler,
handleModifyHandler,
rule,
updateDetails,
onGoToConfig,
validationError,
} = this.props
switch (selectedHandler && selectedHandler.type) {
case 'post':
return (
<PostHandler
selectedHandler={selectedHandler}
handleModifyHandler={handleModifyHandler}
/>
)
case 'tcp':
return (
<TcpHandler
selectedHandler={selectedHandler}
handleModifyHandler={handleModifyHandler}
/>
)
case 'exec':
return (
<ExecHandler
selectedHandler={selectedHandler}
handleModifyHandler={handleModifyHandler}
/>
)
case 'log':
return (
<LogHandler
selectedHandler={selectedHandler}
handleModifyHandler={handleModifyHandler}
/>
)
case 'email':
return (
<EmailHandler
selectedHandler={selectedHandler}
handleModifyHandler={handleModifyHandler}
onGoToConfig={onGoToConfig('smtp')}
validationError={validationError}
updateDetails={updateDetails}
rule={rule}
/>
)
case 'alerta':
return (
<AlertaHandler
selectedHandler={selectedHandler}
handleModifyHandler={handleModifyHandler}
onGoToConfig={onGoToConfig('alerta')}
validationError={validationError}
/>
)
case 'hipChat':
return (
<HipchatHandler
selectedHandler={selectedHandler}
handleModifyHandler={handleModifyHandler}
onGoToConfig={onGoToConfig('hipchat')}
validationError={validationError}
/>
)
case 'opsGenie':
return (
<OpsgenieHandler
selectedHandler={selectedHandler}
handleModifyHandler={handleModifyHandler}
onGoToConfig={onGoToConfig('opsgenie')}
validationError={validationError}
/>
)
case 'pagerDuty':
return (
<PagerdutyHandler
selectedHandler={selectedHandler}
handleModifyHandler={handleModifyHandler}
onGoToConfig={onGoToConfig('pagerduty')}
validationError={validationError}
/>
)
case 'pushover':
return (
<PushoverHandler
selectedHandler={selectedHandler}
handleModifyHandler={handleModifyHandler}
onGoToConfig={onGoToConfig('pushover')}
validationError={validationError}
/>
)
case 'sensu':
return (
<SensuHandler
selectedHandler={selectedHandler}
handleModifyHandler={handleModifyHandler}
onGoToConfig={onGoToConfig('sensu')}
validationError={validationError}
/>
)
case 'slack':
return (
<SlackHandler
selectedHandler={selectedHandler}
handleModifyHandler={handleModifyHandler}
onGoToConfig={onGoToConfig('slack')}
validationError={validationError}
/>
)
case 'talk':
return (
<TalkHandler
selectedHandler={selectedHandler}
handleModifyHandler={handleModifyHandler}
onGoToConfig={onGoToConfig('talk')}
validationError={validationError}
/>
)
case 'telegram':
return (
<TelegramHandler
selectedHandler={selectedHandler}
handleModifyHandler={handleModifyHandler}
onGoToConfig={onGoToConfig('telegram')}
validationError={validationError}
/>
)
case 'victorOps':
return (
<VictoropsHandler
selectedHandler={selectedHandler}
handleModifyHandler={handleModifyHandler}
onGoToConfig={onGoToConfig('victorops')}
validationError={validationError}
/>
)
default:
return null
}
}
}
const {func, shape, string} = PropTypes
HandlerOptions.propTypes = {
selectedHandler: shape({}).isRequired,
handleModifyHandler: func.isRequired,
updateDetails: func,
rule: shape({}),
onGoToConfig: func.isRequired,
validationError: string.isRequired,
}
export default HandlerOptions

View File

@ -0,0 +1,40 @@
import React, {PropTypes} from 'react'
import classnames from 'classnames'
import uuid from 'node-uuid'
const HandlerTabs = ({
handlersOnThisAlert,
selectedHandler,
handleChooseHandler,
handleRemoveHandler,
}) =>
handlersOnThisAlert.length
? <ul className="endpoint-tabs">
{handlersOnThisAlert.map(ep =>
<li
key={uuid.v4()}
className={classnames('endpoint-tab', {
active: ep.alias === (selectedHandler && selectedHandler.alias),
})}
onClick={handleChooseHandler(ep)}
>
{ep.type}
<button
className="endpoint-tab--delete"
onClick={handleRemoveHandler(ep)}
/>
</li>
)}
</ul>
: null
const {shape, func, array} = PropTypes
HandlerTabs.propTypes = {
handlersOnThisAlert: array,
selectedHandler: shape({}),
handleChooseHandler: func.isRequired,
handleRemoveHandler: func.isRequired,
}
export default HandlerTabs

View File

@ -6,14 +6,15 @@ import FancyScrollbar from 'shared/components/FancyScrollbar'
class KapacitorForm extends Component { class KapacitorForm extends Component {
render() { render() {
const {onInputChange, onReset, kapacitor, onSubmit, exists} = this.props const {onInputChange, onReset, kapacitor, onSubmit, exists} = this.props
const {url, name, username, password} = kapacitor const {url: kapaUrl, name, username, password} = kapacitor
return ( return (
<div className="page"> <div className="page">
<div className="page-header"> <div className="page-header">
<div className="page-header__container"> <div className="page-header__container">
<div className="page-header__left"> <div className="page-header__left">
<h1 className="page-header__title">Configure Kapacitor</h1> <h1 className="page-header__title">{`${exists
? 'Configure'
: 'Add a New'} Kapacitor Connection`}</h1>
</div> </div>
</div> </div>
</div> </div>
@ -29,13 +30,13 @@ class KapacitorForm extends Component {
<form onSubmit={onSubmit}> <form onSubmit={onSubmit}>
<div> <div>
<div className="form-group"> <div className="form-group">
<label htmlFor="url">Kapacitor URL</label> <label htmlFor="kapaUrl">Kapacitor URL</label>
<input <input
className="form-control" className="form-control"
id="url" id="kapaUrl"
name="url" name="kapaUrl"
placeholder={url} placeholder={kapaUrl}
value={url} value={kapaUrl}
onChange={onInputChange} onChange={onInputChange}
spellCheck="false" spellCheck="false"
/> />
@ -60,7 +61,7 @@ class KapacitorForm extends Component {
id="username" id="username"
name="username" name="username"
placeholder="username" placeholder="username"
value={username} value={username || ''}
onChange={onInputChange} onChange={onInputChange}
spellCheck="false" spellCheck="false"
/> />
@ -73,7 +74,7 @@ class KapacitorForm extends Component {
type="password" type="password"
name="password" name="password"
placeholder="password" placeholder="password"
value={password} value={password || ''}
onChange={onInputChange} onChange={onInputChange}
spellCheck="false" spellCheck="false"
/> />
@ -108,7 +109,7 @@ class KapacitorForm extends Component {
// TODO: move these to another page. they dont belong on this page // TODO: move these to another page. they dont belong on this page
renderAlertOutputs() { renderAlertOutputs() {
const {exists, kapacitor, addFlashMessage, source} = this.props const {exists, kapacitor, addFlashMessage, source, hash} = this.props
if (exists) { if (exists) {
return ( return (
@ -116,6 +117,7 @@ class KapacitorForm extends Component {
source={source} source={source}
kapacitor={kapacitor} kapacitor={kapacitor}
addFlashMessage={addFlashMessage} addFlashMessage={addFlashMessage}
hash={hash}
/> />
) )
} }
@ -153,6 +155,7 @@ KapacitorForm.propTypes = {
source: shape({}).isRequired, source: shape({}).isRequired,
addFlashMessage: func.isRequired, addFlashMessage: func.isRequired,
exists: bool.isRequired, exists: bool.isRequired,
hash: string.isRequired,
} }
export default KapacitorForm export default KapacitorForm

View File

@ -3,12 +3,14 @@ import React, {PropTypes, Component} from 'react'
import NameSection from 'src/kapacitor/components/NameSection' import NameSection from 'src/kapacitor/components/NameSection'
import ValuesSection from 'src/kapacitor/components/ValuesSection' import ValuesSection from 'src/kapacitor/components/ValuesSection'
import RuleHeader from 'src/kapacitor/components/RuleHeader' import RuleHeader from 'src/kapacitor/components/RuleHeader'
import RuleHandlers from 'src/kapacitor/components/RuleHandlers'
import RuleMessage from 'src/kapacitor/components/RuleMessage' import RuleMessage from 'src/kapacitor/components/RuleMessage'
import FancyScrollbar from 'shared/components/FancyScrollbar' import FancyScrollbar from 'shared/components/FancyScrollbar'
import {createRule, editRule} from 'src/kapacitor/apis' import {createRule, editRule} from 'src/kapacitor/apis'
import buildInfluxQLQuery from 'utils/influxql' import buildInfluxQLQuery from 'utils/influxql'
import timeRanges from 'hson!shared/data/timeRanges.hson' import timeRanges from 'hson!shared/data/timeRanges.hson'
import {DEFAULT_RULE_ID} from 'src/kapacitor/constants'
class KapacitorRule extends Component { class KapacitorRule extends Component {
constructor(props) { constructor(props) {
@ -23,7 +25,7 @@ class KapacitorRule extends Component {
this.setState({timeRange}) this.setState({timeRange})
} }
handleCreate = () => { handleCreate = link => {
const { const {
addFlashMessage, addFlashMessage,
queryConfigs, queryConfigs,
@ -40,7 +42,7 @@ class KapacitorRule extends Component {
createRule(kapacitor, newRule) createRule(kapacitor, newRule)
.then(() => { .then(() => {
router.push(`/sources/${source.id}/alert-rules`) router.push(link || `/sources/${source.id}/alert-rules`)
addFlashMessage({type: 'success', text: 'Rule successfully created'}) addFlashMessage({type: 'success', text: 'Rule successfully created'})
}) })
.catch(() => { .catch(() => {
@ -51,7 +53,7 @@ class KapacitorRule extends Component {
}) })
} }
handleEdit = () => { handleEdit = link => {
const {addFlashMessage, queryConfigs, rule, router, source} = this.props const {addFlashMessage, queryConfigs, rule, router, source} = this.props
const updatedRule = Object.assign({}, rule, { const updatedRule = Object.assign({}, rule, {
query: queryConfigs[rule.queryID], query: queryConfigs[rule.queryID],
@ -59,20 +61,33 @@ class KapacitorRule extends Component {
editRule(updatedRule) editRule(updatedRule)
.then(() => { .then(() => {
router.push(`/sources/${source.id}/alert-rules`) router.push(link || `/sources/${source.id}/alert-rules`)
addFlashMessage({ addFlashMessage({
type: 'success', type: 'success',
text: `${rule.name} successfully saved!`, text: `${rule.name} successfully saved!`,
}) })
}) })
.catch(() => { .catch(e => {
addFlashMessage({ addFlashMessage({
type: 'error', type: 'error',
text: `There was a problem saving ${rule.name}`, text: `There was a problem saving ${rule.name}: ${e.data.message}`,
}) })
}) })
} }
handleSaveToConfig = configName => () => {
const {rule, configLink, router} = this.props
if (this.validationError()) {
router.push({
pathname: `${configLink}#${configName}`,
})
} else if (rule.id === DEFAULT_RULE_ID) {
this.handleCreate(configLink)
} else {
this.handleEdit(configLink)
}
}
handleAddEvery = frequency => { handleAddEvery = frequency => {
const {rule: {id: ruleID}, ruleActions: {addEvery}} = this.props const {rule: {id: ruleID}, ruleActions: {addEvery}} = this.props
addEvery(ruleID, frequency) addEvery(ruleID, frequency)
@ -137,20 +152,20 @@ class KapacitorRule extends Component {
const { const {
rule, rule,
source, source,
isEditing,
ruleActions, ruleActions,
queryConfigs, queryConfigs,
enabledAlerts, handlersFromConfig,
queryConfigActions, queryConfigActions,
} = this.props } = this.props
const {chooseTrigger, updateRuleValues} = ruleActions const {chooseTrigger, updateRuleValues} = ruleActions
const {timeRange} = this.state const {timeRange} = this.state
return ( return (
<div className="page"> <div className="page">
<RuleHeader <RuleHeader
source={source} source={source}
onSave={isEditing ? this.handleEdit : this.handleCreate} onSave={
rule.id === DEFAULT_RULE_ID ? this.handleCreate : this.handleEdit
}
validationError={this.validationError()} validationError={this.validationError()}
/> />
<FancyScrollbar className="page-contents fancy-scroll--kapacitor"> <FancyScrollbar className="page-contents fancy-scroll--kapacitor">
@ -159,10 +174,9 @@ class KapacitorRule extends Component {
<div className="col-xs-12"> <div className="col-xs-12">
<div className="rule-builder"> <div className="rule-builder">
<NameSection <NameSection
isEditing={isEditing} rule={rule}
defaultName={rule.name} defaultName={rule.name}
onRuleRename={ruleActions.updateRuleName} onRuleRename={ruleActions.updateRuleName}
ruleID={rule.id}
/> />
<ValuesSection <ValuesSection
rule={rule} rule={rule}
@ -179,11 +193,14 @@ class KapacitorRule extends Component {
onRuleTypeDropdownChange={this.handleRuleTypeDropdownChange} onRuleTypeDropdownChange={this.handleRuleTypeDropdownChange}
onChooseTimeRange={this.handleChooseTimeRange} onChooseTimeRange={this.handleChooseTimeRange}
/> />
<RuleMessage <RuleHandlers
rule={rule} rule={rule}
actions={ruleActions} ruleActions={ruleActions}
enabledAlerts={enabledAlerts} handlersFromConfig={handlersFromConfig}
onGoToConfig={this.handleSaveToConfig}
validationError={this.validationError()}
/> />
<RuleMessage rule={rule} ruleActions={ruleActions} />
</div> </div>
</div> </div>
</div> </div>
@ -194,22 +211,25 @@ class KapacitorRule extends Component {
} }
} }
const {arrayOf, func, shape, string} = PropTypes
KapacitorRule.propTypes = { KapacitorRule.propTypes = {
source: PropTypes.shape({}).isRequired, source: shape({}).isRequired,
rule: PropTypes.shape({ rule: shape({
values: PropTypes.shape({}), values: shape({}),
}).isRequired, }).isRequired,
query: PropTypes.shape({}).isRequired, query: shape({}).isRequired,
queryConfigs: PropTypes.shape({}).isRequired, queryConfigs: shape({}).isRequired,
queryConfigActions: PropTypes.shape({}).isRequired, queryConfigActions: shape({}).isRequired,
ruleActions: PropTypes.shape({}).isRequired, ruleActions: shape({}).isRequired,
addFlashMessage: PropTypes.func.isRequired, addFlashMessage: func.isRequired,
isEditing: PropTypes.bool.isRequired, ruleID: string.isRequired,
enabledAlerts: PropTypes.arrayOf(PropTypes.string.isRequired).isRequired, handlersFromConfig: arrayOf(shape({})).isRequired,
router: PropTypes.shape({ router: shape({
push: PropTypes.func.isRequired, push: func.isRequired,
}).isRequired, }).isRequired,
kapacitor: PropTypes.shape({}).isRequired, kapacitor: shape({}).isRequired,
configLink: string.isRequired,
} }
export default KapacitorRule export default KapacitorRule

View File

@ -59,7 +59,7 @@ const KapacitorRules = ({
className="btn btn-sm btn-primary" className="btn btn-sm btn-primary"
style={{marginRight: '4px'}} style={{marginRight: '4px'}}
> >
<span className="icon plus" /> Build Rule <span className="icon plus" /> Build Alert Rule
</Link> </Link>
</div> </div>
</div> </div>
@ -80,9 +80,10 @@ const KapacitorRules = ({
<div className="u-flex u-ai-center u-jc-space-between"> <div className="u-flex u-ai-center u-jc-space-between">
<Link <Link
to={`/sources/${source.id}/tickscript/new`} to={`/sources/${source.id}/tickscript/new`}
className="btn btn-sm btn-info" className="btn btn-sm btn-primary"
style={{marginRight: '4px'}}
> >
Write TICKscript <span className="icon plus" /> Write TICKscript
</Link> </Link>
</div> </div>
</div> </div>

View File

@ -2,6 +2,7 @@ import React, {PropTypes} from 'react'
import {Link} from 'react-router' import {Link} from 'react-router'
import _ from 'lodash' import _ from 'lodash'
import {parseAlertNodeList} from 'src/shared/parsing/parseHandlersFromRule'
import {KAPACITOR_RULES_TABLE} from 'src/kapacitor/constants/tableSizing' import {KAPACITOR_RULES_TABLE} from 'src/kapacitor/constants/tableSizing'
const { const {
colName, colName,
@ -62,7 +63,7 @@ const RuleRow = ({rule, source, onDelete, onChangeRuleStatus}) =>
</span> </span>
</td> </td>
<td style={{width: colAlerts}} className="monotype"> <td style={{width: colAlerts}} className="monotype">
{rule.alerts.join(', ')} {parseAlertNodeList(rule)}
</td> </td>
<td style={{width: colEnabled}} className="monotype text-center"> <td style={{width: colEnabled}} className="monotype text-center">
<div className="dark-checkbox"> <div className="dark-checkbox">

View File

@ -1,4 +1,5 @@
import React, {Component, PropTypes} from 'react' import React, {Component, PropTypes} from 'react'
import {DEFAULT_RULE_ID} from 'src/kapacitor/constants'
class NameSection extends Component { class NameSection extends Component {
constructor(props) { constructor(props) {
@ -10,9 +11,9 @@ class NameSection extends Component {
} }
handleInputBlur = reset => e => { handleInputBlur = reset => e => {
const {defaultName, onRuleRename, ruleID} = this.props const {defaultName, onRuleRename, rule} = this.props
onRuleRename(ruleID, reset ? defaultName : e.target.value) onRuleRename(rule.id, reset ? defaultName : e.target.value)
this.setState({reset: false}) this.setState({reset: false})
} }
@ -27,13 +28,13 @@ class NameSection extends Component {
} }
render() { render() {
const {isEditing, defaultName} = this.props const {rule, defaultName} = this.props
const {reset} = this.state const {reset} = this.state
return ( return (
<div className="rule-section"> <div className="rule-section">
<h3 className="rule-section--heading"> <h3 className="rule-section--heading">
{isEditing ? 'Name' : 'Name this Alert Rule'} {rule.id === DEFAULT_RULE_ID ? 'Name this Alert Rule' : 'Name'}
</h3> </h3>
<div className="rule-section--body"> <div className="rule-section--body">
<div className="rule-section--row rule-section--row-first rule-section--row-last"> <div className="rule-section--row rule-section--row-first rule-section--row-last">
@ -53,13 +54,12 @@ class NameSection extends Component {
} }
} }
const {bool, func, string} = PropTypes const {func, string, shape} = PropTypes
NameSection.propTypes = { NameSection.propTypes = {
isEditing: bool,
defaultName: string.isRequired, defaultName: string.isRequired,
onRuleRename: func.isRequired, onRuleRename: func.isRequired,
ruleID: string.isRequired, rule: shape({}).isRequired,
} }
export default NameSection export default NameSection

View File

@ -0,0 +1,36 @@
import React, {Component, PropTypes} from 'react'
class RuleDetailsText extends Component {
constructor(props) {
super(props)
}
handleUpdateDetails = e => {
const {rule, updateDetails} = this.props
updateDetails(rule.id, e.target.value)
}
render() {
const {rule} = this.props
return (
<div className="rule-builder--details">
<textarea
className="form-control form-malachite monotype"
onChange={this.handleUpdateDetails}
placeholder="Enter the body for your email here. Can contain html"
value={rule.details}
spellCheck={false}
/>
</div>
)
}
}
const {shape, func} = PropTypes
RuleDetailsText.propTypes = {
rule: shape().isRequired,
updateDetails: func.isRequired,
}
export default RuleDetailsText

View File

@ -0,0 +1,209 @@
import React, {Component, PropTypes} from 'react'
import _ from 'lodash'
import HandlerOptions from 'src/kapacitor/components/HandlerOptions'
import HandlerTabs from 'src/kapacitor/components/HandlerTabs'
import Dropdown from 'shared/components/Dropdown'
import {parseHandlersFromRule} from 'src/shared/parsing/parseHandlersFromRule'
import {DEFAULT_HANDLERS} from 'src/kapacitor/constants'
class RuleHandlers extends Component {
constructor(props) {
super(props)
const {handlersFromConfig} = this.props
const {
handlersOnThisAlert,
selectedHandler,
handlersOfKind,
} = parseHandlersFromRule(this.props.rule, handlersFromConfig)
this.state = {
selectedHandler,
handlersOnThisAlert,
handlersOfKind,
}
}
handleChangeMessage = e => {
const {ruleActions, rule} = this.props
ruleActions.updateMessage(rule.id, e.target.value)
}
handleChooseHandler = ep => () => {
this.setState({selectedHandler: ep})
}
handleAddHandler = selectedItem => {
const {handlersOnThisAlert, handlersOfKind} = this.state
const newItemNumbering = _.get(handlersOfKind, selectedItem.type, 0) + 1
const newItemName = `${selectedItem.type}-${newItemNumbering}`
const newEndpoint = {
...selectedItem,
alias: newItemName,
}
this.setState(
{
handlersOnThisAlert: [...handlersOnThisAlert, newEndpoint],
handlersOfKind: {
...handlersOfKind,
[selectedItem.type]: newItemNumbering,
},
selectedHandler: newEndpoint,
},
this.handleUpdateAllAlerts
)
}
handleRemoveHandler = removedHandler => e => {
e.stopPropagation()
const {handlersOnThisAlert, selectedHandler} = this.state
const removedIndex = _.findIndex(handlersOnThisAlert, [
'alias',
removedHandler.alias,
])
const remainingHandlers = _.reject(handlersOnThisAlert, [
'alias',
removedHandler.alias,
])
if (selectedHandler.alias === removedHandler.alias) {
const selectedIndex = removedIndex > 0 ? removedIndex - 1 : 0
const newSelected = remainingHandlers.length
? remainingHandlers[selectedIndex]
: null
this.setState({selectedHandler: newSelected})
}
this.setState(
{handlersOnThisAlert: remainingHandlers},
this.handleUpdateAllAlerts
)
}
handleUpdateAllAlerts = () => {
const {rule, ruleActions} = this.props
const {handlersOnThisAlert} = this.state
ruleActions.updateAlertNodes(rule.id, handlersOnThisAlert)
}
handleModifyHandler = (selectedHandler, fieldName, parseToArray) => e => {
const {handlersOnThisAlert} = this.state
let modifiedHandler
if (e.target.type === 'checkbox') {
modifiedHandler = {
...selectedHandler,
[fieldName]: !selectedHandler[fieldName],
}
} else if (parseToArray) {
modifiedHandler = {
...selectedHandler,
[fieldName]: _.split(e.target.value, ' '),
}
} else {
modifiedHandler = {
...selectedHandler,
[fieldName]: e.target.value,
}
}
const modifiedIndex = _.findIndex(handlersOnThisAlert, [
'alias',
modifiedHandler.alias,
])
handlersOnThisAlert[modifiedIndex] = modifiedHandler
this.setState(
{
selectedHandler: modifiedHandler,
handlersOnThisAlert: [...handlersOnThisAlert],
},
this.handleUpdateAllAlerts
)
}
render() {
const {
rule,
ruleActions,
onGoToConfig,
validationError,
handlersFromConfig,
} = this.props
const {handlersOnThisAlert, selectedHandler} = this.state
const mappedhandlers = _.map(
[...DEFAULT_HANDLERS, ...handlersFromConfig],
h => {
return {...h, text: h.type}
}
)
const handlers = _.flatten([
_.filter(mappedhandlers, ['enabled', true]),
{text: 'SEPARATOR'},
_.filter(mappedhandlers, ['enabled', false]),
])
const dropdownLabel = handlersOnThisAlert.length
? 'Add another Handler'
: 'Add a Handler'
const ruleSectionClassName = handlersOnThisAlert.length
? 'rule-section--row rule-section--row-first rule-section--border-bottom'
: 'rule-section--row rule-section--row-first rule-section--row-last'
return (
<div className="rule-section">
<h3 className="rule-section--heading">Alert Handlers</h3>
<div className="rule-section--body">
<div className={ruleSectionClassName}>
<p>Send this Alert to:</p>
<Dropdown
items={handlers}
menuClass="dropdown-malachite"
selected={dropdownLabel}
onChoose={this.handleAddHandler}
className="dropdown-170 rule-message--add-endpoint"
/>
</div>
{handlersOnThisAlert.length
? <div className="rule-message--endpoints">
<HandlerTabs
handlersOnThisAlert={handlersOnThisAlert}
selectedHandler={selectedHandler}
handleChooseHandler={this.handleChooseHandler}
handleRemoveHandler={this.handleRemoveHandler}
/>
<HandlerOptions
selectedHandler={selectedHandler}
handleModifyHandler={this.handleModifyHandler}
updateDetails={ruleActions.updateDetails}
rule={rule}
onGoToConfig={onGoToConfig}
validationError={validationError}
/>
</div>
: null}
</div>
</div>
)
}
}
const {arrayOf, func, shape, string} = PropTypes
RuleHandlers.propTypes = {
rule: shape({}).isRequired,
ruleActions: shape({
updateAlertNodes: func.isRequired,
updateMessage: func.isRequired,
updateDetails: func.isRequired,
}).isRequired,
handlersFromConfig: arrayOf(shape({})),
onGoToConfig: func.isRequired,
validationError: string.isRequired,
}
export default RuleHandlers

View File

@ -1,86 +1,32 @@
import React, {Component, PropTypes} from 'react' import React, {Component, PropTypes} from 'react'
import _ from 'lodash'
import classnames from 'classnames'
import RuleMessageOptions from 'src/kapacitor/components/RuleMessageOptions'
import RuleMessageText from 'src/kapacitor/components/RuleMessageText' import RuleMessageText from 'src/kapacitor/components/RuleMessageText'
import RuleMessageTemplates from 'src/kapacitor/components/RuleMessageTemplates' import RuleMessageTemplates from 'src/kapacitor/components/RuleMessageTemplates'
import {DEFAULT_ALERTS, RULE_ALERT_OPTIONS} from 'src/kapacitor/constants'
class RuleMessage extends Component { class RuleMessage extends Component {
constructor(props) { constructor(props) {
super(props) super(props)
this.state = {
selectedAlertNodeName: null,
}
} }
handleChangeMessage = e => { handleChangeMessage = e => {
const {actions, rule} = this.props const {ruleActions, rule} = this.props
actions.updateMessage(rule.id, e.target.value) ruleActions.updateMessage(rule.id, e.target.value)
}
handleChooseAlert = item => () => {
const {actions} = this.props
actions.updateAlerts(item.ruleID, [item.text])
actions.updateAlertNodes(item.ruleID, item.text, '')
this.setState({selectedAlertNodeName: item.text})
} }
render() { render() {
const {rule, actions, enabledAlerts} = this.props const {rule, ruleActions} = this.props
const defaultAlertEndpoints = DEFAULT_ALERTS.map(text => {
return {text, ruleID: rule.id}
})
const alerts = [
...defaultAlertEndpoints,
...enabledAlerts.map(text => {
return {text, ruleID: rule.id}
}),
]
const selectedAlertNodeName = rule.alerts[0] || alerts[0].text
return ( return (
<div className="rule-section"> <div className="rule-section">
<h3 className="rule-section--heading">Alert Message</h3> <h3 className="rule-section--heading">Message</h3>
<div className="rule-section--body"> <div className="rule-section--body">
<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
// only display alert endpoints that have rule alert options configured
.filter(alert => _.get(RULE_ALERT_OPTIONS, alert.text, false))
.map(alert =>
<li
key={alert.text}
className={classnames({
active: alert.text === selectedAlertNodeName,
})}
onClick={this.handleChooseAlert(alert)}
>
{alert.text}
</li>
)}
</ul>
</div>
<RuleMessageOptions
rule={rule}
alertNodeName={selectedAlertNodeName}
updateAlertNodes={actions.updateAlertNodes}
updateDetails={actions.updateDetails}
updateAlertProperty={actions.updateAlertProperty}
/>
<RuleMessageText <RuleMessageText
rule={rule} rule={rule}
updateMessage={this.handleChangeMessage} updateMessage={this.handleChangeMessage}
/> />
<RuleMessageTemplates <RuleMessageTemplates
rule={rule} rule={rule}
updateMessage={actions.updateMessage} updateMessage={ruleActions.updateMessage}
/> />
</div> </div>
</div> </div>
@ -88,17 +34,13 @@ class RuleMessage extends Component {
} }
} }
const {arrayOf, func, shape, string} = PropTypes const {func, shape} = PropTypes
RuleMessage.propTypes = { RuleMessage.propTypes = {
rule: shape({}).isRequired, rule: shape().isRequired,
actions: shape({ ruleActions: shape({
updateAlertNodes: func.isRequired,
updateMessage: func.isRequired, updateMessage: func.isRequired,
updateDetails: func.isRequired,
updateAlertProperty: func.isRequired,
}).isRequired, }).isRequired,
enabledAlerts: arrayOf(string.isRequired).isRequired,
} }
export default RuleMessage export default RuleMessage

View File

@ -1,125 +0,0 @@
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)
}
getAlertPropertyValue = name => {
const {rule} = this.props
const {properties} = rule.alertNodes[0]
if (properties) {
const alertNodeProperty = properties.find(
property => property.name === name
)
if (alertNodeProperty) {
return alertNodeProperty.args
}
}
return ''
}
handleUpdateDetails = e => {
const {updateDetails, rule} = this.props
updateDetails(rule.id, e.target.value)
}
handleUpdateAlertNodes = e => {
const {updateAlertNodes, alertNodeName, rule} = this.props
updateAlertNodes(rule.id, alertNodeName, e.target.value)
}
handleUpdateAlertProperty = propertyName => e => {
const {updateAlertProperty, alertNodeName, rule} = this.props
updateAlertProperty(rule.id, alertNodeName, {
name: propertyName,
args: [e.target.value],
})
}
render() {
const {rule, alertNodeName} = this.props
const {args, details, properties} = RULE_ALERT_OPTIONS[alertNodeName]
return (
<div>
{args
? <div className="rule-section--row rule-section--border-bottom">
<p>Optional Alert Parameters:</p>
<div className="optional-alert-parameters">
<div className="form-group">
<input
name={args.label}
id="alert-input"
className="form-control input-sm form-malachite"
type="text"
placeholder={args.placeholder}
onChange={this.handleUpdateAlertNodes}
value={ALERT_NODES_ACCESSORS[alertNodeName](rule)}
autoComplete="off"
spellCheck="false"
/>
<label htmlFor={args.label}>
{args.label}
</label>
</div>
</div>
</div>
: null}
{properties && properties.length
? <div className="rule-section--row rule-section--border-bottom">
<p>Optional Alert Parameters:</p>
<div className="optional-alert-parameters">
{properties.map(({name: propertyName, label, placeholder}) =>
<div key={propertyName} className="form-group">
<input
name={label}
className="form-control input-sm form-malachite"
type="text"
placeholder={placeholder}
onChange={this.handleUpdateAlertProperty(propertyName)}
value={this.getAlertPropertyValue(propertyName)}
autoComplete="off"
spellCheck="false"
/>
<label htmlFor={label}>
{label}
</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 : ''}
onChange={this.handleUpdateDetails}
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

@ -19,7 +19,7 @@ class RuleMessageTemplates extends Component {
render() { render() {
return ( return (
<div className="rule-section--row rule-section--row-last rule-section--border-top"> <div className="rule-section--row rule-section--row-last">
<p>Templates:</p> <p>Templates:</p>
{_.map(RULE_MESSAGE_TEMPLATES, (template, key) => { {_.map(RULE_MESSAGE_TEMPLATES, (template, key) => {
return ( return (

View File

@ -5,9 +5,12 @@ import RedactedInput from './RedactedInput'
class AlertaConfig extends Component { class AlertaConfig extends Component {
constructor(props) { constructor(props) {
super(props) super(props)
this.state = {
testEnabled: this.props.enabled,
}
} }
handleSaveAlert = e => { handleSubmit = e => {
e.preventDefault() e.preventDefault()
const properties = { const properties = {
@ -18,6 +21,11 @@ class AlertaConfig extends Component {
} }
this.props.onSave(properties) this.props.onSave(properties)
this.setState({testEnabled: true})
}
disableTest = () => {
this.setState({testEnabled: false})
} }
handleTokenRef = r => (this.token = r) handleTokenRef = r => (this.token = r)
@ -26,7 +34,7 @@ class AlertaConfig extends Component {
const {environment, origin, token, url} = this.props.config.options const {environment, origin, token, url} = this.props.config.options
return ( return (
<form onSubmit={this.handleSaveAlert}> <form onSubmit={this.handleSubmit}>
<div className="form-group col-xs-12"> <div className="form-group col-xs-12">
<label htmlFor="environment">Environment</label> <label htmlFor="environment">Environment</label>
<input <input
@ -35,6 +43,7 @@ class AlertaConfig extends Component {
type="text" type="text"
ref={r => (this.environment = r)} ref={r => (this.environment = r)}
defaultValue={environment || ''} defaultValue={environment || ''}
onChange={this.disableTest}
/> />
</div> </div>
@ -46,6 +55,7 @@ class AlertaConfig extends Component {
type="text" type="text"
ref={r => (this.origin = r)} ref={r => (this.origin = r)}
defaultValue={origin || ''} defaultValue={origin || ''}
onChange={this.disableTest}
/> />
</div> </div>
@ -55,6 +65,7 @@ class AlertaConfig extends Component {
defaultValue={token} defaultValue={token}
id="token" id="token"
refFunc={this.handleTokenRef} refFunc={this.handleTokenRef}
disableTest={this.disableTest}
/> />
</div> </div>
@ -66,12 +77,26 @@ class AlertaConfig extends Component {
type="text" type="text"
ref={r => (this.url = r)} ref={r => (this.url = r)}
defaultValue={url || ''} defaultValue={url || ''}
onChange={this.disableTest}
/> />
</div> </div>
<div className="form-group-submit col-xs-12 text-center"> <div className="form-group-submit col-xs-12 text-center">
<button className="btn btn-primary" type="submit"> <button
Update Alerta Config className="btn btn-primary"
type="submit"
disabled={this.state.testEnabled}
>
<span className="icon checkmark" />
Save Changes
</button>
<button
className="btn btn-primary"
disabled={!this.state.testEnabled}
onClick={this.props.onTest}
>
<span className="icon pulse-c" />
Send Test Alert
</button> </button>
</div> </div>
</form> </form>
@ -91,6 +116,8 @@ AlertaConfig.propTypes = {
}).isRequired, }).isRequired,
}).isRequired, }).isRequired,
onSave: func.isRequired, onSave: func.isRequired,
onTest: func.isRequired,
enabled: bool.isRequired,
} }
export default AlertaConfig export default AlertaConfig

View File

@ -7,9 +7,12 @@ import RedactedInput from './RedactedInput'
class HipchatConfig extends Component { class HipchatConfig extends Component {
constructor(props) { constructor(props) {
super(props) super(props)
this.state = {
testEnabled: this.props.enabled,
}
} }
handleSaveAlert = e => { handleSubmit = e => {
e.preventDefault() e.preventDefault()
const properties = { const properties = {
@ -19,6 +22,11 @@ class HipchatConfig extends Component {
} }
this.props.onSave(properties) this.props.onSave(properties)
this.setState({testEnabled: true})
}
disableTest = () => {
this.setState({testEnabled: false})
} }
handleTokenRef = r => (this.token = r) handleTokenRef = r => (this.token = r)
@ -32,7 +40,7 @@ class HipchatConfig extends Component {
.replace('.hipchat.com/v2/room', '') .replace('.hipchat.com/v2/room', '')
return ( return (
<form onSubmit={this.handleSaveAlert}> <form onSubmit={this.handleSubmit}>
<div className="form-group col-xs-12"> <div className="form-group col-xs-12">
<label htmlFor="url">Subdomain</label> <label htmlFor="url">Subdomain</label>
<input <input
@ -42,6 +50,7 @@ class HipchatConfig extends Component {
placeholder="your-subdomain" placeholder="your-subdomain"
ref={r => (this.url = r)} ref={r => (this.url = r)}
defaultValue={subdomain && subdomain.length ? subdomain : ''} defaultValue={subdomain && subdomain.length ? subdomain : ''}
onChange={this.disableTest}
/> />
</div> </div>
@ -54,6 +63,7 @@ class HipchatConfig extends Component {
placeholder="your-hipchat-room" placeholder="your-hipchat-room"
ref={r => (this.room = r)} ref={r => (this.room = r)}
defaultValue={room || ''} defaultValue={room || ''}
onChange={this.disableTest}
/> />
</div> </div>
@ -66,12 +76,26 @@ class HipchatConfig extends Component {
defaultValue={token} defaultValue={token}
id="token" id="token"
refFunc={this.handleTokenRef} refFunc={this.handleTokenRef}
disableTest={this.disableTest}
/> />
</div> </div>
<div className="form-group-submit col-xs-12 text-center"> <div className="form-group-submit col-xs-12 text-center">
<button className="btn btn-primary" type="submit"> <button
Update HipChat Config className="btn btn-primary"
type="submit"
disabled={this.state.testEnabled}
>
<span className="icon checkmark" />
Save Changes
</button>
<button
className="btn btn-primary"
disabled={!this.state.testEnabled}
onClick={this.props.onTest}
>
<span className="icon pulse-c" />
Send Test Alert
</button> </button>
</div> </div>
</form> </form>
@ -90,6 +114,8 @@ HipchatConfig.propTypes = {
}).isRequired, }).isRequired,
}).isRequired, }).isRequired,
onSave: func.isRequired, onSave: func.isRequired,
onTest: func.isRequired,
enabled: bool.isRequired,
} }
export default HipchatConfig export default HipchatConfig

View File

@ -12,10 +12,11 @@ class OpsGenieConfig extends Component {
this.state = { this.state = {
currentTeams: teams || [], currentTeams: teams || [],
currentRecipients: recipients || [], currentRecipients: recipients || [],
testEnabled: this.props.enabled,
} }
} }
handleSaveAlert = e => { handleSubmit = e => {
e.preventDefault() e.preventDefault()
const properties = { const properties = {
@ -25,6 +26,11 @@ class OpsGenieConfig extends Component {
} }
this.props.onSave(properties) this.props.onSave(properties)
this.setState({testEnabled: true})
}
disableTest = () => {
this.setState({testEnabled: false})
} }
handleAddTeam = team => { handleAddTeam = team => {
@ -59,18 +65,15 @@ class OpsGenieConfig extends Component {
const {currentTeams, currentRecipients} = this.state const {currentTeams, currentRecipients} = this.state
return ( return (
<form onSubmit={this.handleSaveAlert}> <form onSubmit={this.handleSubmit}>
<div className="form-group col-xs-12"> <div className="form-group col-xs-12">
<label htmlFor="api-key">API Key</label> <label htmlFor="api-key">API Key</label>
<RedactedInput <RedactedInput
defaultValue={apiKey} defaultValue={apiKey}
id="api-key" id="api-key"
refFunc={this.handleApiKeyRef} refFunc={this.handleApiKeyRef}
disableTest={this.disableTest}
/> />
<label className="form-helper">
Note: a value of <code>true</code> indicates the OpsGenie API key
has been set
</label>
</div> </div>
<TagInput <TagInput
@ -78,17 +81,32 @@ class OpsGenieConfig extends Component {
onAddTag={this.handleAddTeam} onAddTag={this.handleAddTeam}
onDeleteTag={this.handleDeleteTeam} onDeleteTag={this.handleDeleteTeam}
tags={currentTeams} tags={currentTeams}
disableTest={this.disableTest}
/> />
<TagInput <TagInput
title="Recipients" title="Recipients"
onAddTag={this.handleAddRecipient} onAddTag={this.handleAddRecipient}
onDeleteTag={this.handleDeleteRecipient} onDeleteTag={this.handleDeleteRecipient}
tags={currentRecipients} tags={currentRecipients}
disableTest={this.disableTest}
/> />
<div className="form-group-submit col-xs-12 text-center"> <div className="form-group-submit col-xs-12 text-center">
<button className="btn btn-primary" type="submit"> <button
Update OpsGenie Config className="btn btn-primary"
type="submit"
disabled={this.state.testEnabled}
>
<span className="icon checkmark" />
Save Changes
</button>
<button
className="btn btn-primary"
disabled={!this.state.testEnabled}
onClick={this.props.onTest}
>
<span className="icon pulse-c" />
Send Test Alert
</button> </button>
</div> </div>
</form> </form>
@ -107,6 +125,8 @@ OpsGenieConfig.propTypes = {
}).isRequired, }).isRequired,
}).isRequired, }).isRequired,
onSave: func.isRequired, onSave: func.isRequired,
onTest: func.isRequired,
enabled: bool.isRequired,
} }
export default OpsGenieConfig export default OpsGenieConfig

View File

@ -1,11 +1,15 @@
import React, {PropTypes, Component} from 'react' import React, {PropTypes, Component} from 'react'
import RedactedInput from './RedactedInput'
class PagerDutyConfig extends Component { class PagerDutyConfig extends Component {
constructor(props) { constructor(props) {
super(props) super(props)
this.state = {
testEnabled: this.props.enabled,
}
} }
handleSaveAlert = e => { handleSubmit = e => {
e.preventDefault() e.preventDefault()
const properties = { const properties = {
@ -14,28 +18,28 @@ class PagerDutyConfig extends Component {
} }
this.props.onSave(properties) this.props.onSave(properties)
this.setState({testEnabled: true})
}
disableTest = () => {
this.setState({testEnabled: false})
} }
render() { render() {
const {options} = this.props.config const {options} = this.props.config
const {url} = options const {url} = options
const serviceKey = options['service-key'] const serviceKey = options['service-key']
const refFunc = r => (this.serviceKey = r)
return ( return (
<form onSubmit={this.handleSaveAlert}> <form onSubmit={this.handleSubmit}>
<div className="form-group col-xs-12"> <div className="form-group col-xs-12">
<label htmlFor="service-key">Service Key</label> <label htmlFor="service-key">Service Key</label>
<input <RedactedInput
className="form-control"
id="service-key"
type="text"
ref={r => (this.serviceKey = r)}
defaultValue={serviceKey || ''} defaultValue={serviceKey || ''}
id="service-key"
refFunc={refFunc}
disableTest={this.disableTest}
/> />
<label className="form-helper">
Note: a value of <code>true</code> indicates the PagerDuty service
key has been set
</label>
</div> </div>
<div className="form-group col-xs-12"> <div className="form-group col-xs-12">
@ -46,12 +50,26 @@ class PagerDutyConfig extends Component {
type="text" type="text"
ref={r => (this.url = r)} ref={r => (this.url = r)}
defaultValue={url || ''} defaultValue={url || ''}
onChange={this.disableTest}
/> />
</div> </div>
<div className="form-group-submit col-xs-12 text-center"> <div className="form-group-submit col-xs-12 text-center">
<button className="btn btn-primary" type="submit"> <button
Update PagerDuty Config className="btn btn-primary"
type="submit"
disabled={this.state.testEnabled}
>
<span className="icon checkmark" />
Save Changes
</button>
<button
className="btn btn-primary"
disabled={!this.state.testEnabled}
onClick={this.props.onTest}
>
<span className="icon pulse-c" />
Send Test Alert
</button> </button>
</div> </div>
</form> </form>
@ -69,6 +87,8 @@ PagerDutyConfig.propTypes = {
}).isRequired, }).isRequired,
}).isRequired, }).isRequired,
onSave: func.isRequired, onSave: func.isRequired,
onTest: func.isRequired,
enabled: bool.isRequired,
} }
export default PagerDutyConfig export default PagerDutyConfig

View File

@ -8,9 +8,12 @@ import {PUSHOVER_DOCS_LINK} from 'src/kapacitor/copy'
class PushoverConfig extends Component { class PushoverConfig extends Component {
constructor(props) { constructor(props) {
super(props) super(props)
this.state = {
testEnabled: this.props.enabled,
}
} }
handleSaveAlert = e => { handleSubmit = e => {
e.preventDefault() e.preventDefault()
const properties = { const properties = {
@ -20,6 +23,11 @@ class PushoverConfig extends Component {
} }
this.props.onSave(properties) this.props.onSave(properties)
this.setState({testEnabled: true})
}
disableTest = () => {
this.setState({testEnabled: false})
} }
handleUserKeyRef = r => (this.userKey = r) handleUserKeyRef = r => (this.userKey = r)
@ -32,7 +40,7 @@ class PushoverConfig extends Component {
const userKey = options['user-key'] const userKey = options['user-key']
return ( return (
<form onSubmit={this.handleSaveAlert}> <form onSubmit={this.handleSubmit}>
<div className="form-group col-xs-12"> <div className="form-group col-xs-12">
<label htmlFor="user-key"> <label htmlFor="user-key">
User Key User Key
@ -45,6 +53,7 @@ class PushoverConfig extends Component {
defaultValue={userKey} defaultValue={userKey}
id="user-key" id="user-key"
refFunc={this.handleUserKeyRef} refFunc={this.handleUserKeyRef}
disableTest={this.disableTest}
/> />
</div> </div>
@ -60,6 +69,7 @@ class PushoverConfig extends Component {
defaultValue={token} defaultValue={token}
id="token" id="token"
refFunc={this.handleTokenRef} refFunc={this.handleTokenRef}
disableTest={this.disableTest}
/> />
</div> </div>
@ -71,12 +81,26 @@ class PushoverConfig extends Component {
type="text" type="text"
ref={r => (this.url = r)} ref={r => (this.url = r)}
defaultValue={url || ''} defaultValue={url || ''}
onChange={this.disableTest}
/> />
</div> </div>
<div className="form-group-submit col-xs-12 text-center"> <div className="form-group-submit col-xs-12 text-center">
<button className="btn btn-primary" type="submit"> <button
Update Pushover Config className="btn btn-primary"
type="submit"
disabled={this.state.testEnabled}
>
<span className="icon checkmark" />
Save Changes
</button>
<button
className="btn btn-primary"
disabled={!this.state.testEnabled}
onClick={this.props.onTest}
>
<span className="icon pulse-c" />
Send Test Alert
</button> </button>
</div> </div>
</form> </form>
@ -95,6 +119,8 @@ PushoverConfig.propTypes = {
}).isRequired, }).isRequired,
}).isRequired, }).isRequired,
onSave: func.isRequired, onSave: func.isRequired,
onTest: func.isRequired,
enabled: bool.isRequired,
} }
export default PushoverConfig export default PushoverConfig

View File

@ -13,7 +13,7 @@ class RedactedInput extends Component {
} }
render() { render() {
const {defaultValue, id, refFunc} = this.props const {defaultValue, id, refFunc, disableTest} = this.props
const {editing} = this.state const {editing} = this.state
if (defaultValue === true && !editing) { if (defaultValue === true && !editing) {
@ -43,6 +43,7 @@ class RedactedInput extends Component {
type="text" type="text"
ref={refFunc} ref={refFunc}
defaultValue={''} defaultValue={''}
onChange={disableTest}
/> />
) )
} }
@ -54,6 +55,7 @@ RedactedInput.propTypes = {
id: string.isRequired, id: string.isRequired,
defaultValue: bool, defaultValue: bool,
refFunc: func.isRequired, refFunc: func.isRequired,
disableTest: func,
} }
export default RedactedInput export default RedactedInput

View File

@ -3,9 +3,12 @@ import React, {PropTypes, Component} from 'react'
class SMTPConfig extends Component { class SMTPConfig extends Component {
constructor(props) { constructor(props) {
super(props) super(props)
this.state = {
testEnabled: this.props.enabled,
}
} }
handleSaveAlert = e => { handleSubmit = e => {
e.preventDefault() e.preventDefault()
const properties = { const properties = {
@ -17,13 +20,18 @@ class SMTPConfig extends Component {
} }
this.props.onSave(properties) this.props.onSave(properties)
this.setState({testEnabled: true})
}
disableTest = () => {
this.setState({testEnabled: false})
} }
render() { render() {
const {host, port, from, username, password} = this.props.config.options const {host, port, from, username, password} = this.props.config.options
return ( return (
<form onSubmit={this.handleSaveAlert}> <form onSubmit={this.handleSubmit}>
<div className="form-group col-xs-12 col-md-6"> <div className="form-group col-xs-12 col-md-6">
<label htmlFor="smtp-host">SMTP Host</label> <label htmlFor="smtp-host">SMTP Host</label>
<input <input
@ -32,6 +40,7 @@ class SMTPConfig extends Component {
type="text" type="text"
ref={r => (this.host = r)} ref={r => (this.host = r)}
defaultValue={host || ''} defaultValue={host || ''}
onChange={this.disableTest}
/> />
</div> </div>
@ -43,6 +52,7 @@ class SMTPConfig extends Component {
type="text" type="text"
ref={r => (this.port = r)} ref={r => (this.port = r)}
defaultValue={port || ''} defaultValue={port || ''}
onChange={this.disableTest}
/> />
</div> </div>
@ -55,6 +65,7 @@ class SMTPConfig extends Component {
type="text" type="text"
ref={r => (this.from = r)} ref={r => (this.from = r)}
defaultValue={from || ''} defaultValue={from || ''}
onChange={this.disableTest}
/> />
</div> </div>
@ -66,6 +77,7 @@ class SMTPConfig extends Component {
type="text" type="text"
ref={r => (this.username = r)} ref={r => (this.username = r)}
defaultValue={username || ''} defaultValue={username || ''}
onChange={this.disableTest}
/> />
</div> </div>
@ -77,12 +89,26 @@ class SMTPConfig extends Component {
type="password" type="password"
ref={r => (this.password = r)} ref={r => (this.password = r)}
defaultValue={`${password}`} defaultValue={`${password}`}
onChange={this.disableTest}
/> />
</div> </div>
<div className="form-group-submit col-xs-12 text-center"> <div className="form-group-submit col-xs-12 text-center">
<button className="btn btn-primary" type="submit"> <button
Update SMTP Config className="btn btn-primary"
type="submit"
disabled={this.state.testEnabled}
>
<span className="icon checkmark" />
Save Changes
</button>
<button
className="btn btn-primary"
disabled={!this.state.testEnabled}
onClick={this.props.onTest}
>
<span className="icon pulse-c" />
Send Test Alert
</button> </button>
</div> </div>
</form> </form>
@ -103,6 +129,8 @@ SMTPConfig.propTypes = {
}).isRequired, }).isRequired,
}).isRequired, }).isRequired,
onSave: func.isRequired, onSave: func.isRequired,
onTest: func.isRequired,
enabled: bool.isRequired,
} }
export default SMTPConfig export default SMTPConfig

View File

@ -3,9 +3,12 @@ import React, {PropTypes, Component} from 'react'
class SensuConfig extends Component { class SensuConfig extends Component {
constructor(props) { constructor(props) {
super(props) super(props)
this.state = {
testEnabled: this.props.enabled,
}
} }
handleSaveAlert = e => { handleSubmit = e => {
e.preventDefault() e.preventDefault()
const properties = { const properties = {
@ -14,13 +17,18 @@ class SensuConfig extends Component {
} }
this.props.onSave(properties) this.props.onSave(properties)
this.setState({testEnabled: true})
}
disableTest = () => {
this.setState({testEnabled: false})
} }
render() { render() {
const {source, addr} = this.props.config.options const {source, addr} = this.props.config.options
return ( return (
<form onSubmit={this.handleSaveAlert}> <form onSubmit={this.handleSubmit}>
<div className="form-group col-xs-12 col-md-6"> <div className="form-group col-xs-12 col-md-6">
<label htmlFor="source">Source</label> <label htmlFor="source">Source</label>
<input <input
@ -29,6 +37,7 @@ class SensuConfig extends Component {
type="text" type="text"
ref={r => (this.source = r)} ref={r => (this.source = r)}
defaultValue={source || ''} defaultValue={source || ''}
onChange={this.disableTest}
/> />
</div> </div>
@ -40,12 +49,26 @@ class SensuConfig extends Component {
type="text" type="text"
ref={r => (this.addr = r)} ref={r => (this.addr = r)}
defaultValue={addr || ''} defaultValue={addr || ''}
onChange={this.disableTest}
/> />
</div> </div>
<div className="form-group-submit col-xs-12 text-center"> <div className="form-group-submit col-xs-12 text-center">
<button className="btn btn-primary" type="submit"> <button
Update Sensu Config className="btn btn-primary"
type="submit"
disabled={this.state.testEnabled}
>
<span className="icon checkmark" />
Save Changes
</button>
<button
className="btn btn-primary"
disabled={!this.state.testEnabled}
onClick={this.props.onTest}
>
<span className="icon pulse-c" />
Send Test Alert
</button> </button>
</div> </div>
</form> </form>
@ -53,7 +76,7 @@ class SensuConfig extends Component {
} }
} }
const {func, shape, string} = PropTypes const {bool, func, shape, string} = PropTypes
SensuConfig.propTypes = { SensuConfig.propTypes = {
config: shape({ config: shape({
@ -63,6 +86,8 @@ SensuConfig.propTypes = {
}).isRequired, }).isRequired,
}).isRequired, }).isRequired,
onSave: func.isRequired, onSave: func.isRequired,
onTest: func.isRequired,
enabled: bool.isRequired,
} }
export default SensuConfig export default SensuConfig

View File

@ -6,25 +6,21 @@ class SlackConfig extends Component {
constructor(props) { constructor(props) {
super(props) super(props)
this.state = { this.state = {
testEnabled: !!this.props.config.options.url, testEnabled: this.props.enabled,
} }
} }
componentWillReceiveProps(nextProps) { handleSubmit = e => {
this.setState({
testEnabled: !!nextProps.config.options.url,
})
}
handleSaveAlert = e => {
e.preventDefault() e.preventDefault()
const properties = { const properties = {
url: this.url.value, url: this.url.value,
channel: this.channel.value, channel: this.channel.value,
} }
this.props.onSave(properties) this.props.onSave(properties)
this.setState({testEnabled: true})
}
disableTest = () => {
this.setState({testEnabled: false})
} }
handleUrlRef = r => (this.url = r) handleUrlRef = r => (this.url = r)
@ -33,7 +29,7 @@ class SlackConfig extends Component {
const {url, channel} = this.props.config.options const {url, channel} = this.props.config.options
return ( return (
<form onSubmit={this.handleSaveAlert}> <form onSubmit={this.handleSubmit}>
<div className="form-group col-xs-12"> <div className="form-group col-xs-12">
<label htmlFor="slack-url"> <label htmlFor="slack-url">
Slack Webhook URL ( Slack Webhook URL (
@ -46,6 +42,7 @@ class SlackConfig extends Component {
defaultValue={url} defaultValue={url}
id="url" id="url"
refFunc={this.handleUrlRef} refFunc={this.handleUrlRef}
disableTest={this.disableTest}
/> />
</div> </div>
@ -58,14 +55,30 @@ class SlackConfig extends Component {
placeholder="#alerts" placeholder="#alerts"
ref={r => (this.channel = r)} ref={r => (this.channel = r)}
defaultValue={channel || ''} defaultValue={channel || ''}
onChange={this.disableTest}
/> />
</div> </div>
<div className="form-group-submit col-xs-12 text-center"> <div className="form-group-submit col-xs-12 text-center">
<button className="btn btn-primary" type="submit"> <button
Update Slack Config className="btn btn-primary"
type="submit"
disabled={this.state.testEnabled}
>
<span className="icon checkmark" />
Save Changes
</button>
<button
className="btn btn-primary"
disabled={!this.state.testEnabled}
onClick={this.props.onTest}
>
<span className="icon pulse-c" />
Send Test Alert
</button> </button>
</div> </div>
<br />
<br />
</form> </form>
) )
} }
@ -81,6 +94,8 @@ SlackConfig.propTypes = {
}).isRequired, }).isRequired,
}).isRequired, }).isRequired,
onSave: func.isRequired, onSave: func.isRequired,
onTest: func.isRequired,
enabled: bool.isRequired,
} }
export default SlackConfig export default SlackConfig

View File

@ -5,9 +5,12 @@ import RedactedInput from './RedactedInput'
class TalkConfig extends Component { class TalkConfig extends Component {
constructor(props) { constructor(props) {
super(props) super(props)
this.state = {
testEnabled: this.props.enabled,
}
} }
handleSaveAlert = e => { handleSubmit = e => {
e.preventDefault() e.preventDefault()
const properties = { const properties = {
@ -16,6 +19,11 @@ class TalkConfig extends Component {
} }
this.props.onSave(properties) this.props.onSave(properties)
this.setState({testEnabled: true})
}
disableTest = () => {
this.setState({testEnabled: false})
} }
handleUrlRef = r => (this.url = r) handleUrlRef = r => (this.url = r)
@ -24,13 +32,14 @@ class TalkConfig extends Component {
const {url, author_name: author} = this.props.config.options const {url, author_name: author} = this.props.config.options
return ( return (
<form onSubmit={this.handleSaveAlert}> <form onSubmit={this.handleSubmit}>
<div className="form-group col-xs-12"> <div className="form-group col-xs-12">
<label htmlFor="url">URL</label> <label htmlFor="url">URL</label>
<RedactedInput <RedactedInput
defaultValue={url} defaultValue={url}
id="url" id="url"
refFunc={this.handleUrlRef} refFunc={this.handleUrlRef}
disableTest={this.disableTest}
/> />
</div> </div>
@ -42,12 +51,26 @@ class TalkConfig extends Component {
type="text" type="text"
ref={r => (this.author = r)} ref={r => (this.author = r)}
defaultValue={author || ''} defaultValue={author || ''}
onChange={this.disableTest}
/> />
</div> </div>
<div className="form-group-submit col-xs-12 text-center"> <div className="form-group-submit col-xs-12 text-center">
<button className="btn btn-primary" type="submit"> <button
Update Talk Config className="btn btn-primary"
type="submit"
disabled={this.state.testEnabled}
>
<span className="icon checkmark" />
Save Changes
</button>
<button
className="btn btn-primary"
disabled={!this.state.testEnabled}
onClick={this.props.onTest}
>
<span className="icon pulse-c" />
Send Test Alert
</button> </button>
</div> </div>
</form> </form>
@ -65,6 +88,8 @@ TalkConfig.propTypes = {
}).isRequired, }).isRequired,
}).isRequired, }).isRequired,
onSave: func.isRequired, onSave: func.isRequired,
onTest: func.isRequired,
enabled: bool.isRequired,
} }
export default TalkConfig export default TalkConfig

View File

@ -7,9 +7,12 @@ import RedactedInput from './RedactedInput'
class TelegramConfig extends Component { class TelegramConfig extends Component {
constructor(props) { constructor(props) {
super(props) super(props)
this.state = {
testEnabled: this.props.enabled,
}
} }
handleSaveAlert = e => { handleSubmit = e => {
e.preventDefault() e.preventDefault()
let parseMode let parseMode
@ -29,6 +32,11 @@ class TelegramConfig extends Component {
} }
this.props.onSave(properties) this.props.onSave(properties)
this.setState({testEnabled: true})
}
disableTest = () => {
this.setState({testEnabled: false})
} }
handleTokenRef = r => (this.token = r) handleTokenRef = r => (this.token = r)
@ -42,7 +50,7 @@ class TelegramConfig extends Component {
const parseMode = options['parse-mode'] const parseMode = options['parse-mode']
return ( return (
<form onSubmit={this.handleSaveAlert}> <form onSubmit={this.handleSubmit}>
<div className="form-group col-xs-12"> <div className="form-group col-xs-12">
<div className="alert alert-warning alert-icon no-user-select"> <div className="alert alert-warning alert-icon no-user-select">
<span className="icon triangle" /> <span className="icon triangle" />
@ -68,6 +76,7 @@ class TelegramConfig extends Component {
defaultValue={token} defaultValue={token}
id="token" id="token"
refFunc={this.handleTokenRef} refFunc={this.handleTokenRef}
disableTest={this.disableTest}
/> />
</div> </div>
@ -86,6 +95,7 @@ class TelegramConfig extends Component {
placeholder="your-telegram-chat-id" placeholder="your-telegram-chat-id"
ref={r => (this.chatID = r)} ref={r => (this.chatID = r)}
defaultValue={chatID || ''} defaultValue={chatID || ''}
onChange={this.disableTest}
/> />
</div> </div>
@ -100,6 +110,7 @@ class TelegramConfig extends Component {
value="markdown" value="markdown"
defaultChecked={parseMode !== 'HTML'} defaultChecked={parseMode !== 'HTML'}
ref={r => (this.parseModeMarkdown = r)} ref={r => (this.parseModeMarkdown = r)}
onChange={this.disableTest}
/> />
<label htmlFor="parseModeMarkdown">Markdown</label> <label htmlFor="parseModeMarkdown">Markdown</label>
</div> </div>
@ -111,6 +122,7 @@ class TelegramConfig extends Component {
value="html" value="html"
defaultChecked={parseMode === 'HTML'} defaultChecked={parseMode === 'HTML'}
ref={r => (this.parseModeHTML = r)} ref={r => (this.parseModeHTML = r)}
onChange={this.disableTest}
/> />
<label htmlFor="parseModeHTML">HTML</label> <label htmlFor="parseModeHTML">HTML</label>
</div> </div>
@ -124,6 +136,7 @@ class TelegramConfig extends Component {
type="checkbox" type="checkbox"
defaultChecked={disableWebPagePreview} defaultChecked={disableWebPagePreview}
ref={r => (this.disableWebPagePreview = r)} ref={r => (this.disableWebPagePreview = r)}
onChange={this.disableTest}
/> />
<label htmlFor="disableWebPagePreview"> <label htmlFor="disableWebPagePreview">
Disable{' '} Disable{' '}
@ -142,6 +155,7 @@ class TelegramConfig extends Component {
type="checkbox" type="checkbox"
defaultChecked={disableNotification} defaultChecked={disableNotification}
ref={r => (this.disableNotification = r)} ref={r => (this.disableNotification = r)}
onChange={this.disableTest}
/> />
<label htmlFor="disableNotification"> <label htmlFor="disableNotification">
Disable notifications on iOS devices and disable sounds on Android Disable notifications on iOS devices and disable sounds on Android
@ -151,8 +165,21 @@ class TelegramConfig extends Component {
</div> </div>
<div className="form-group-submit col-xs-12 text-center"> <div className="form-group-submit col-xs-12 text-center">
<button className="btn btn-primary" type="submit"> <button
Update Telegram Config className="btn btn-primary"
type="submit"
disabled={this.state.testEnabled}
>
<span className="icon checkmark" />
Save Changes
</button>
<button
className="btn btn-primary"
disabled={!this.state.testEnabled}
onClick={this.props.onTest}
>
<span className="icon pulse-c" />
Send Test Alert
</button> </button>
</div> </div>
</form> </form>
@ -173,6 +200,8 @@ TelegramConfig.propTypes = {
}).isRequired, }).isRequired,
}).isRequired, }).isRequired,
onSave: func.isRequired, onSave: func.isRequired,
onTest: func.isRequired,
enabled: bool.isRequired,
} }
export default TelegramConfig export default TelegramConfig

View File

@ -5,9 +5,12 @@ import RedactedInput from './RedactedInput'
class VictorOpsConfig extends Component { class VictorOpsConfig extends Component {
constructor(props) { constructor(props) {
super(props) super(props)
this.state = {
testEnabled: this.props.enabled,
}
} }
handleSaveAlert = e => { handleSubmit = e => {
e.preventDefault() e.preventDefault()
const properties = { const properties = {
@ -17,6 +20,11 @@ class VictorOpsConfig extends Component {
} }
this.props.onSave(properties) this.props.onSave(properties)
this.setState({testEnabled: true})
}
disableTest = () => {
this.setState({testEnabled: false})
} }
handleApiRef = r => (this.apiKey = r) handleApiRef = r => (this.apiKey = r)
@ -28,13 +36,14 @@ class VictorOpsConfig extends Component {
const {url} = options const {url} = options
return ( return (
<form onSubmit={this.handleSaveAlert}> <form onSubmit={this.handleSubmit}>
<div className="form-group col-xs-12"> <div className="form-group col-xs-12">
<label htmlFor="api-key">API Key</label> <label htmlFor="api-key">API Key</label>
<RedactedInput <RedactedInput
defaultValue={apiKey} defaultValue={apiKey}
id="api-key" id="api-key"
refFunc={this.handleApiRef} refFunc={this.handleApiRef}
disableTest={this.disableTest}
/> />
</div> </div>
@ -46,6 +55,7 @@ class VictorOpsConfig extends Component {
type="text" type="text"
ref={r => (this.routingKey = r)} ref={r => (this.routingKey = r)}
defaultValue={routingKey || ''} defaultValue={routingKey || ''}
onChange={this.disableTest}
/> />
</div> </div>
@ -57,12 +67,26 @@ class VictorOpsConfig extends Component {
type="text" type="text"
ref={r => (this.url = r)} ref={r => (this.url = r)}
defaultValue={url || ''} defaultValue={url || ''}
onChange={this.disableTest}
/> />
</div> </div>
<div className="form-group-submit col-xs-12 text-center"> <div className="form-group-submit col-xs-12 text-center">
<button className="btn btn-primary" type="submit"> <button
Update VictorOps Config className="btn btn-primary"
type="submit"
disabled={this.state.testEnabled}
>
<span className="icon checkmark" />
Save Changes
</button>
<button
className="btn btn-primary"
disabled={!this.state.testEnabled}
onClick={this.props.onTest}
>
<span className="icon pulse-c" />
Send Test Alert
</button> </button>
</div> </div>
</form> </form>
@ -81,6 +105,8 @@ VictorOpsConfig.propTypes = {
}).isRequired, }).isRequired,
}).isRequired, }).isRequired,
onSave: func.isRequired, onSave: func.isRequired,
onTest: func.isRequired,
enabled: bool.isRequired,
} }
export default VictorOpsConfig export default VictorOpsConfig

View File

@ -0,0 +1,107 @@
import React, {PropTypes} from 'react'
import HandlerInput from 'src/kapacitor/components/HandlerInput'
import HandlerEmpty from 'src/kapacitor/components/HandlerEmpty'
const AlertaHandler = ({
selectedHandler,
handleModifyHandler,
onGoToConfig,
validationError,
}) =>
selectedHandler.enabled
? <div className="endpoint-tab-contents">
<div className="endpoint-tab--parameters">
<h4 className="u-flex u-jc-space-between">
Parameters from Kapacitor Configuration
<div className="btn btn-default btn-sm" onClick={onGoToConfig}>
<span className="icon cog-thick" />
{validationError
? 'Exit this Rule and Edit Configuration'
: 'Save this Rule and Edit Configuration'}
</div>
</h4>
<div className="faux-form">
<HandlerInput
selectedHandler={selectedHandler}
handleModifyHandler={handleModifyHandler}
fieldName="token"
fieldDisplay="Token"
placeholder="ex: my_token"
redacted={true}
fieldColumns="col-md-12"
/>
<HandlerInput
selectedHandler={selectedHandler}
handleModifyHandler={handleModifyHandler}
fieldName="environment"
fieldDisplay="Environment"
placeholder="ex: environment"
/>
<HandlerInput
selectedHandler={selectedHandler}
handleModifyHandler={handleModifyHandler}
fieldName="origin"
fieldDisplay="Origin"
placeholder="ex: origin"
/>
</div>
</div>
<div className="endpoint-tab--parameters">
<h4>Parameters for this Alert Handler</h4>
<div className="faux-form">
<HandlerInput
selectedHandler={selectedHandler}
handleModifyHandler={handleModifyHandler}
fieldName="resource"
fieldDisplay="Resource"
placeholder=""
/>
<HandlerInput
selectedHandler={selectedHandler}
handleModifyHandler={handleModifyHandler}
fieldName="event"
fieldDisplay="Event"
placeholder=""
/>
<HandlerInput
selectedHandler={selectedHandler}
handleModifyHandler={handleModifyHandler}
fieldName="group"
fieldDisplay="Group"
placeholder=""
/>
<HandlerInput
selectedHandler={selectedHandler}
handleModifyHandler={handleModifyHandler}
fieldName="value"
fieldDisplay="Value"
placeholder=""
/>
<HandlerInput
selectedHandler={selectedHandler}
handleModifyHandler={handleModifyHandler}
fieldName="service"
fieldDisplay="Service"
placeholder=""
parseToArray={true}
/>
</div>
</div>
</div>
: <HandlerEmpty
onGoToConfig={onGoToConfig}
validationError={validationError}
/>
const {func, shape, string} = PropTypes
AlertaHandler.propTypes = {
selectedHandler: shape({}).isRequired,
handleModifyHandler: func.isRequired,
onGoToConfig: func.isRequired,
validationError: string.isRequired,
}
export default AlertaHandler

View File

@ -0,0 +1,88 @@
import React, {PropTypes} from 'react'
import HandlerInput from 'src/kapacitor/components/HandlerInput'
import HandlerEmpty from 'src/kapacitor/components/HandlerEmpty'
import RuleDetailsText from 'src/kapacitor/components/RuleDetailsText'
const EmailHandler = ({
rule,
updateDetails,
selectedHandler,
handleModifyHandler,
onGoToConfig,
validationError,
}) =>
selectedHandler.enabled
? <div className="endpoint-tab-contents">
<div className="endpoint-tab--parameters">
<h4 className="u-flex u-jc-space-between">
Parameters from Kapacitor Configuration
<div className="btn btn-default btn-sm" onClick={onGoToConfig}>
<span className="icon cog-thick" />
{validationError
? 'Exit this Rule and Edit Configuration'
: 'Save this Rule and Edit Configuration'}
</div>
</h4>
<div className="faux-form">
<HandlerInput
selectedHandler={selectedHandler}
handleModifyHandler={handleModifyHandler}
fieldName="from"
fieldDisplay="From E-mail"
placeholder=""
disabled={true}
fieldColumns="col-md-4"
/>
<HandlerInput
selectedHandler={selectedHandler}
handleModifyHandler={handleModifyHandler}
fieldName="host"
fieldDisplay="SMTP Host"
placeholder=""
disabled={true}
fieldColumns="col-md-4"
/>
<HandlerInput
selectedHandler={selectedHandler}
handleModifyHandler={handleModifyHandler}
fieldName="port"
fieldDisplay="SMTP Port"
placeholder=""
disabled={true}
fieldColumns="col-md-4"
/>
</div>
</div>
<div className="endpoint-tab--parameters">
<h4>Parameters for this Alert Handler</h4>
<div className="faux-form">
<HandlerInput
selectedHandler={selectedHandler}
handleModifyHandler={handleModifyHandler}
fieldName="to"
fieldDisplay="Recipient E-mail Addresses: (separated by spaces)"
placeholder="ex: bob@domain.com susan@domain.com"
parseToArray={true}
fieldColumns="col-md-12"
/>
<RuleDetailsText rule={rule} updateDetails={updateDetails} />
</div>
</div>
</div>
: <HandlerEmpty
onGoToConfig={onGoToConfig}
validationError={validationError}
/>
const {func, shape, string} = PropTypes
EmailHandler.propTypes = {
selectedHandler: shape({}).isRequired,
handleModifyHandler: func.isRequired,
updateDetails: func,
rule: shape({}),
onGoToConfig: func.isRequired,
validationError: string.isRequired,
}
export default EmailHandler

View File

@ -0,0 +1,29 @@
import React, {PropTypes} from 'react'
import HandlerInput from 'src/kapacitor/components/HandlerInput'
const ExecHandler = ({selectedHandler, handleModifyHandler}) =>
<div className="endpoint-tab-contents">
<div className="endpoint-tab--parameters">
<h4>Parameters for this Alert Handler</h4>
<div className="faux-form">
<HandlerInput
selectedHandler={selectedHandler}
handleModifyHandler={handleModifyHandler}
fieldName="command"
fieldDisplay="Command (arguments separated by spaces):"
placeholder="ex: command argument"
fieldColumns="col-md-12"
parseToArray={true}
/>
</div>
</div>
</div>
const {func, shape} = PropTypes
ExecHandler.propTypes = {
selectedHandler: shape({}).isRequired,
handleModifyHandler: func.isRequired,
}
export default ExecHandler

View File

@ -0,0 +1,66 @@
import React, {PropTypes} from 'react'
import HandlerInput from 'src/kapacitor/components/HandlerInput'
import HandlerEmpty from 'src/kapacitor/components/HandlerEmpty'
const HipchatHandler = ({
selectedHandler,
handleModifyHandler,
onGoToConfig,
validationError,
}) =>
selectedHandler.enabled
? <div className="endpoint-tab-contents">
<div className="endpoint-tab--parameters">
<h4 className="u-flex u-jc-space-between">
Parameters from Kapacitor Configuration
<div className="btn btn-default btn-sm" onClick={onGoToConfig}>
<span className="icon cog-thick" />
{validationError
? 'Exit this Rule and Edit Configuration'
: 'Save this Rule and Edit Configuration'}
</div>
</h4>
<div className="faux-form">
<HandlerInput
selectedHandler={selectedHandler}
handleModifyHandler={handleModifyHandler}
fieldName="token"
fieldDisplay="Token:"
placeholder="ex: the_token"
redacted={true}
fieldColumns="col-md-12"
/>
<HandlerInput
selectedHandler={selectedHandler}
handleModifyHandler={handleModifyHandler}
fieldName="url"
fieldDisplay="Subdomain Url"
placeholder="ex: hipchat_subdomain"
disabled={true}
/>
<HandlerInput
selectedHandler={selectedHandler}
handleModifyHandler={handleModifyHandler}
fieldName="room"
fieldDisplay="Room:"
placeholder="ex: room_name"
/>
</div>
</div>
</div>
: <HandlerEmpty
onGoToConfig={onGoToConfig}
validationError={validationError}
/>
const {func, shape, string} = PropTypes
HipchatHandler.propTypes = {
selectedHandler: shape({}).isRequired,
handleModifyHandler: func.isRequired,
onGoToConfig: func.isRequired,
validationError: string.isRequired,
}
export default HipchatHandler

View File

@ -0,0 +1,28 @@
import React, {PropTypes} from 'react'
import HandlerInput from 'src/kapacitor/components/HandlerInput'
const LogHandler = ({selectedHandler, handleModifyHandler}) =>
<div className="endpoint-tab-contents">
<div className="endpoint-tab--parameters">
<h4>Parameters for this Alert Handler</h4>
<div className="faux-form">
<HandlerInput
selectedHandler={selectedHandler}
handleModifyHandler={handleModifyHandler}
fieldName="filePath"
fieldDisplay="File Path for Log File:"
placeholder="ex: /tmp/alerts.log"
fieldColumns="col-md-12"
/>
</div>
</div>
</div>
const {func, shape} = PropTypes
LogHandler.propTypes = {
selectedHandler: shape({}).isRequired,
handleModifyHandler: func.isRequired,
}
export default LogHandler

View File

@ -0,0 +1,72 @@
import React, {PropTypes} from 'react'
import HandlerInput from 'src/kapacitor/components/HandlerInput'
import HandlerEmpty from 'src/kapacitor/components/HandlerEmpty'
const OpsgenieHandler = ({
selectedHandler,
handleModifyHandler,
onGoToConfig,
validationError,
}) =>
selectedHandler.enabled
? <div className="endpoint-tab-contents">
<div className="endpoint-tab--parameters">
<h4 className="u-flex u-jc-space-between">
Parameters from Kapacitor Configuration
<div className="btn btn-default btn-sm" onClick={onGoToConfig}>
<span className="icon cog-thick" />
{validationError
? 'Exit this Rule and Edit Configuration'
: 'Save this Rule and Edit Configuration'}
</div>
</h4>
<div className="faux-form">
<HandlerInput
selectedHandler={selectedHandler}
handleModifyHandler={handleModifyHandler}
fieldName="api-key"
fieldDisplay="API-key"
placeholder=""
redacted={true}
disabled={true}
fieldColumns="col-md-12"
/>
</div>
</div>
<div className="endpoint-tab--parameters">
<h4>Parameters for this Alert Handler:</h4>
<div className="faux-form">
<HandlerInput
selectedHandler={selectedHandler}
handleModifyHandler={handleModifyHandler}
fieldName="teams"
fieldDisplay="Teams"
placeholder="ex: teams_name"
parseToArray={true}
/>
<HandlerInput
selectedHandler={selectedHandler}
handleModifyHandler={handleModifyHandler}
fieldName="recipients"
fieldDisplay="Recipients"
placeholder="ex: recipients_name"
parseToArray={true}
/>
</div>
</div>
</div>
: <HandlerEmpty
onGoToConfig={onGoToConfig}
validationError={validationError}
/>
const {func, shape, string} = PropTypes
OpsgenieHandler.propTypes = {
selectedHandler: shape({}).isRequired,
handleModifyHandler: func.isRequired,
onGoToConfig: func.isRequired,
validationError: string.isRequired,
}
export default OpsgenieHandler

View File

@ -0,0 +1,48 @@
import React, {PropTypes} from 'react'
import HandlerInput from 'src/kapacitor/components/HandlerInput'
import HandlerEmpty from 'src/kapacitor/components/HandlerEmpty'
const PagerdutyHandler = ({
selectedHandler,
handleModifyHandler,
onGoToConfig,
validationError,
}) =>
selectedHandler.enabled
? <div className="endpoint-tab-contents">
<div className="endpoint-tab--parameters">
<h4 className="u-flex u-jc-space-between">
Parameters from Kapacitor Configuration
<div className="btn btn-default btn-sm" onClick={onGoToConfig}>
<span className="icon cog-thick" />
{validationError
? 'Exit this Rule and Edit Configuration'
: 'Save this Rule and Edit Configuration'}
</div>
</h4>
<HandlerInput
selectedHandler={selectedHandler}
handleModifyHandler={handleModifyHandler}
fieldName="serviceKey"
fieldDisplay="Service Key:"
placeholder="ex: service_key"
redacted={true}
fieldColumns="col-md-12"
/>
</div>
</div>
: <HandlerEmpty
onGoToConfig={onGoToConfig}
validationError={validationError}
/>
const {func, shape, string} = PropTypes
PagerdutyHandler.propTypes = {
selectedHandler: shape({}).isRequired,
handleModifyHandler: func.isRequired,
onGoToConfig: func.isRequired,
validationError: string.isRequired,
}
export default PagerdutyHandler

View File

@ -0,0 +1,42 @@
import React, {PropTypes} from 'react'
import HandlerInput from 'src/kapacitor/components/HandlerInput'
const PostHandler = ({selectedHandler, handleModifyHandler}) =>
<div className="endpoint-tab-contents">
<div className="endpoint-tab--parameters">
<h4>Parameters for this Alert Handler</h4>
<div className="faux-form">
<HandlerInput
selectedHandler={selectedHandler}
handleModifyHandler={handleModifyHandler}
fieldName="url"
fieldDisplay="HTTP endpoint for POST request"
placeholder="ex: http://example.com/api/alert"
fieldColumns="col-md-12"
/>
<HandlerInput
selectedHandler={selectedHandler}
handleModifyHandler={handleModifyHandler}
fieldName="headerKey"
fieldDisplay="Header Key"
placeholder=""
/>
<HandlerInput
selectedHandler={selectedHandler}
handleModifyHandler={handleModifyHandler}
fieldName="headerValue"
fieldDisplay="Header Value"
placeholder=""
/>
</div>
</div>
</div>
const {func, shape} = PropTypes
PostHandler.propTypes = {
selectedHandler: shape({}).isRequired,
handleModifyHandler: func.isRequired,
}
export default PostHandler

View File

@ -0,0 +1,99 @@
import React, {PropTypes} from 'react'
import HandlerInput from 'src/kapacitor/components/HandlerInput'
import HandlerEmpty from 'src/kapacitor/components/HandlerEmpty'
const PushoverHandler = ({
selectedHandler,
handleModifyHandler,
onGoToConfig,
validationError,
}) =>
selectedHandler.enabled
? <div className="endpoint-tab-contents">
<div className="endpoint-tab--parameters">
<h4 className="u-flex u-jc-space-between">
Parameters from Kapacitor Configuration
<div className="btn btn-default btn-sm" onClick={onGoToConfig}>
<span className="icon cog-thick" />
{validationError
? 'Exit this Rule and Edit Configuration'
: 'Save this Rule and Edit Configuration'}
</div>
</h4>
<div className="faux-form">
<HandlerInput
selectedHandler={selectedHandler}
handleModifyHandler={handleModifyHandler}
fieldName="token"
fieldDisplay="Token"
placeholder=""
disabled={true}
redacted={true}
/>
<HandlerInput
selectedHandler={selectedHandler}
handleModifyHandler={handleModifyHandler}
fieldName="userKey"
fieldDisplay="User Key"
placeholder=""
redacted={true}
/>
</div>
</div>
<div className="endpoint-tab--parameters">
<h4>Parameters for this Alert Handler</h4>
<div className="faux-form">
<HandlerInput
selectedHandler={selectedHandler}
handleModifyHandler={handleModifyHandler}
fieldName="title"
fieldDisplay="Alert Title:"
placeholder="ex: Important Alert"
fieldColumns="col-md-12"
/>
<HandlerInput
selectedHandler={selectedHandler}
handleModifyHandler={handleModifyHandler}
fieldName="url"
fieldDisplay="URL:"
placeholder="ex: https://influxdata.com"
/>
<HandlerInput
selectedHandler={selectedHandler}
handleModifyHandler={handleModifyHandler}
fieldName="urlTitle"
fieldDisplay="URL Title:"
placeholder="ex: InfluxData"
/>
<HandlerInput
selectedHandler={selectedHandler}
handleModifyHandler={handleModifyHandler}
fieldName="device"
fieldDisplay="Devices: (comma separated)"
placeholder="ex: dv1, dv2"
/>
<HandlerInput
selectedHandler={selectedHandler}
handleModifyHandler={handleModifyHandler}
fieldName="sound"
fieldDisplay="Alert Sound:"
placeholder="ex: alien"
/>
</div>
</div>
</div>
: <HandlerEmpty
onGoToConfig={onGoToConfig}
validationError={validationError}
/>
const {func, shape, string} = PropTypes
PushoverHandler.propTypes = {
selectedHandler: shape({}).isRequired,
handleModifyHandler: func.isRequired,
onGoToConfig: func.isRequired,
validationError: string.isRequired,
}
export default PushoverHandler

View File

@ -0,0 +1,70 @@
import React, {PropTypes} from 'react'
import HandlerInput from 'src/kapacitor/components/HandlerInput'
import HandlerEmpty from 'src/kapacitor/components/HandlerEmpty'
const SensuHandler = ({
selectedHandler,
handleModifyHandler,
onGoToConfig,
validationError,
}) =>
selectedHandler.enabled
? <div className="endpoint-tab-contents">
<div className="endpoint-tab--parameters">
<h4 className="u-flex u-jc-space-between">
Parameters from Kapacitor Configuration
<div className="btn btn-default btn-sm" onClick={onGoToConfig}>
<span className="icon cog-thick" />
{validationError
? 'Exit this Rule and Edit Configuration'
: 'Save this Rule and Edit Configuration'}
</div>
</h4>
<div className="faux-form">
<HandlerInput
selectedHandler={selectedHandler}
handleModifyHandler={handleModifyHandler}
fieldName="addr"
fieldDisplay="Address"
placeholder=""
disabled={true}
/>
<HandlerInput
selectedHandler={selectedHandler}
handleModifyHandler={handleModifyHandler}
fieldName="source"
fieldDisplay="Source"
placeholder="ex: my_source"
/>
</div>
</div>
<div className="endpoint-tab--parameters">
<h4>Parameters for this Alert Handler</h4>
<div className="faux-form">
<HandlerInput
selectedHandler={selectedHandler}
handleModifyHandler={handleModifyHandler}
fieldName="handlers"
fieldDisplay="Handlers"
placeholder="ex: my_handlers"
fieldColumns="col-md-12"
parseToArray={true}
/>
</div>
</div>
</div>
: <HandlerEmpty
onGoToConfig={onGoToConfig}
validationError={validationError}
/>
const {func, shape, string} = PropTypes
SensuHandler.propTypes = {
selectedHandler: shape({}).isRequired,
handleModifyHandler: func.isRequired,
onGoToConfig: func.isRequired,
validationError: string.isRequired,
}
export default SensuHandler

View File

@ -0,0 +1,80 @@
import React, {PropTypes} from 'react'
import HandlerInput from 'src/kapacitor/components/HandlerInput'
import HandlerEmpty from 'src/kapacitor/components/HandlerEmpty'
const SlackHandler = ({
selectedHandler,
handleModifyHandler,
onGoToConfig,
validationError,
}) =>
selectedHandler.enabled
? <div className="endpoint-tab-contents">
<div className="endpoint-tab--parameters">
<h4 className="u-flex u-jc-space-between">
Parameters from Kapacitor Configuration
<div className="btn btn-default btn-sm" onClick={onGoToConfig}>
<span className="icon cog-thick" />
{validationError
? 'Exit this Rule and Edit Configuration'
: 'Save this Rule and Edit Configuration'}
</div>
</h4>
<div className="faux-form">
<HandlerInput
selectedHandler={selectedHandler}
handleModifyHandler={handleModifyHandler}
fieldName="url"
fieldDisplay="Webhook URL:"
placeholder=""
disabled={true}
redacted={true}
fieldColumns="col-md-12"
/>
</div>
</div>
<div className="endpoint-tab--parameters">
<h4>Parameters for this Alert Handler</h4>
<div className="faux-form">
<HandlerInput
selectedHandler={selectedHandler}
handleModifyHandler={handleModifyHandler}
fieldName="channel"
fieldDisplay="Channel:"
placeholder="ex: #my_favorite_channel"
fieldColumns="col-md-4"
/>
<HandlerInput
selectedHandler={selectedHandler}
handleModifyHandler={handleModifyHandler}
fieldName="username"
fieldDisplay="Username:"
placeholder="ex: my_favorite_username"
fieldColumns="col-md-4"
/>
<HandlerInput
selectedHandler={selectedHandler}
handleModifyHandler={handleModifyHandler}
fieldName="iconEmoji"
fieldDisplay="Emoji:"
placeholder="ex: :thumbsup:"
fieldColumns="col-md-4"
/>
</div>
</div>
</div>
: <HandlerEmpty
onGoToConfig={onGoToConfig}
validationError={validationError}
/>
const {func, shape, string} = PropTypes
SlackHandler.propTypes = {
selectedHandler: shape({}).isRequired,
handleModifyHandler: func.isRequired,
onGoToConfig: func.isRequired,
validationError: string.isRequired,
}
export default SlackHandler

View File

@ -0,0 +1,58 @@
import React, {PropTypes} from 'react'
import HandlerInput from 'src/kapacitor/components/HandlerInput'
import HandlerEmpty from 'src/kapacitor/components/HandlerEmpty'
const TalkHandler = ({
selectedHandler,
handleModifyHandler,
onGoToConfig,
validationError,
}) =>
selectedHandler.enabled
? <div className="endpoint-tab-contents">
<div className="endpoint-tab--parameters">
<h4 className="u-flex u-jc-space-between">
Parameters from Kapacitor Configuration
<div className="btn btn-default btn-sm" onClick={onGoToConfig}>
<span className="icon cog-thick" />
{validationError
? 'Exit this Rule and Edit Configuration'
: 'Save this Rule and Edit Configuration'}
</div>
</h4>
<div className="faux-form">
<HandlerInput
selectedHandler={selectedHandler}
handleModifyHandler={handleModifyHandler}
fieldName="url"
fieldDisplay="URL"
placeholder=""
disabled={true}
redacted={true}
/>
<HandlerInput
selectedHandler={selectedHandler}
handleModifyHandler={handleModifyHandler}
fieldName="author_name"
fieldDisplay="Author Name"
placeholder=""
disabled={true}
/>
</div>
</div>
</div>
: <HandlerEmpty
onGoToConfig={onGoToConfig}
validationError={validationError}
/>
const {func, shape, string} = PropTypes
TalkHandler.propTypes = {
selectedHandler: shape({}).isRequired,
handleModifyHandler: func.isRequired,
onGoToConfig: func.isRequired,
validationError: string.isRequired,
}
export default TalkHandler

View File

@ -0,0 +1,28 @@
import React, {PropTypes} from 'react'
import HandlerInput from 'src/kapacitor/components/HandlerInput'
const TcpHandler = ({selectedHandler, handleModifyHandler}) =>
<div className="endpoint-tab-contents">
<div className="endpoint-tab--parameters">
<h4>Parameters for this Alert Handler</h4>
<div className="faux-form">
<HandlerInput
selectedHandler={selectedHandler}
handleModifyHandler={handleModifyHandler}
fieldName="address"
fieldDisplay="Address"
placeholder="ex: exampleendpoint.com:5678"
fieldColumns="col-md-12"
/>
</div>
</div>
</div>
const {func, shape} = PropTypes
TcpHandler.propTypes = {
selectedHandler: shape({}).isRequired,
handleModifyHandler: func.isRequired,
}
export default TcpHandler

View File

@ -0,0 +1,83 @@
import React, {PropTypes} from 'react'
import HandlerInput from 'src/kapacitor/components/HandlerInput'
import HandlerCheckbox from 'src/kapacitor/components/HandlerCheckbox'
import HandlerEmpty from 'src/kapacitor/components/HandlerEmpty'
const TelegramHandler = ({
selectedHandler,
handleModifyHandler,
onGoToConfig,
validationError,
}) =>
selectedHandler.enabled
? <div className="endpoint-tab-contents">
<div className="endpoint-tab--parameters">
<h4 className="u-flex u-jc-space-between">
Parameters from Kapacitor Configuration
<div className="btn btn-default btn-sm" onClick={onGoToConfig}>
<span className="icon cog-thick" />
{validationError
? 'Exit this Rule and Edit Configuration'
: 'Save this Rule and Edit Configuration'}
</div>
</h4>
<div className="faux-form">
<HandlerInput
selectedHandler={selectedHandler}
handleModifyHandler={handleModifyHandler}
fieldName="token"
fieldDisplay="Token"
placeholder=""
disabled={true}
redacted={true}
fieldColumns="col-md-12"
/>
</div>
</div>
<div className="endpoint-tab--parameters">
<h4>Parameters for this Alert Handler</h4>
<div className="faux-form">
<HandlerInput
selectedHandler={selectedHandler}
handleModifyHandler={handleModifyHandler}
fieldName="chatId"
fieldDisplay="Chat ID:"
placeholder="ex: chat_id"
/>
<HandlerInput
selectedHandler={selectedHandler}
handleModifyHandler={handleModifyHandler}
fieldName="parseMode"
fieldDisplay="Parse Mode:"
placeholder="ex: Markdown or HTML"
/>
<HandlerCheckbox
selectedHandler={selectedHandler}
handleModifyHandler={handleModifyHandler}
fieldName="disableWebPagePreview"
fieldDisplay="Disable web page preview"
/>
<HandlerCheckbox
selectedHandler={selectedHandler}
handleModifyHandler={handleModifyHandler}
fieldName="disableNotification"
fieldDisplay="Disable notification"
/>
</div>
</div>
</div>
: <HandlerEmpty
onGoToConfig={onGoToConfig}
validationError={validationError}
/>
const {func, shape, string} = PropTypes
TelegramHandler.propTypes = {
selectedHandler: shape({}).isRequired,
handleModifyHandler: func.isRequired,
onGoToConfig: func.isRequired,
validationError: string.isRequired,
}
export default TelegramHandler

View File

@ -0,0 +1,64 @@
import React, {PropTypes} from 'react'
import HandlerInput from 'src/kapacitor/components/HandlerInput'
import HandlerEmpty from 'src/kapacitor/components/HandlerEmpty'
const VictoropsHandler = ({
selectedHandler,
handleModifyHandler,
onGoToConfig,
validationError,
}) =>
selectedHandler.enabled
? <div className="endpoint-tab-contents">
<div className="endpoint-tab--parameters">
<h4 className="u-flex u-jc-space-between">
Parameters from Kapacitor Configuration
<div className="btn btn-default btn-sm" onClick={onGoToConfig}>
<span className="icon cog-thick" />
{validationError
? 'Exit this Rule and Edit Configuration'
: 'Save this Rule and Edit Configuration'}
</div>
</h4>
<div className="faux-form">
<HandlerInput
selectedHandler={selectedHandler}
handleModifyHandler={handleModifyHandler}
fieldName="api-key"
fieldDisplay="API key"
placeholder="ex: api_key"
disabled={true}
redacted={true}
fieldColumns="col-md-12"
/>
</div>
</div>
<div className="endpoint-tab--parameters">
<h4>Parameters for this Alert Handler</h4>
<div className="faux-form">
<HandlerInput
selectedHandler={selectedHandler}
handleModifyHandler={handleModifyHandler}
fieldName="routingKey"
fieldDisplay="Routing Key:"
placeholder="ex: routing_key"
fieldColumns="col-md-12"
/>
</div>
</div>
</div>
: <HandlerEmpty
onGoToConfig={onGoToConfig}
validationError={validationError}
/>
const {func, shape, string} = PropTypes
VictoropsHandler.propTypes = {
selectedHandler: shape({}).isRequired,
handleModifyHandler: func.isRequired,
onGoToConfig: func.isRequired,
validationError: string.isRequired,
}
export default VictoropsHandler

View File

@ -0,0 +1,33 @@
import PostHandler from './PostHandler'
import TcpHandler from './TcpHandler'
import ExecHandler from './ExecHandler'
import LogHandler from './LogHandler'
import AlertaHandler from './AlertaHandler'
import HipchatHandler from './HipchatHandler'
import OpsgenieHandler from './OpsgenieHandler'
import PagerdutyHandler from './PagerdutyHandler'
import PushoverHandler from './PushoverHandler'
import SensuHandler from './SensuHandler'
import SlackHandler from './SlackHandler'
import EmailHandler from './EmailHandler'
import TalkHandler from './TalkHandler'
import TelegramHandler from './TelegramHandler'
import VictoropsHandler from './VictoropsHandler'
export {
PostHandler,
TcpHandler,
ExecHandler,
LogHandler,
EmailHandler,
AlertaHandler,
HipchatHandler,
OpsgenieHandler,
PagerdutyHandler,
PushoverHandler,
SensuHandler,
SlackHandler,
TalkHandler,
TelegramHandler,
VictoropsHandler,
}

View File

@ -1,5 +1,3 @@
import _ from 'lodash'
export const defaultRuleConfigs = { export const defaultRuleConfigs = {
deadman: { deadman: {
period: '10m', period: '10m',
@ -87,144 +85,104 @@ export const RULE_MESSAGE_TEMPLATES = {
text: 'The time of the point that triggered the event', text: 'The time of the point that triggered the event',
}, },
} }
// DEFAULT_HANDLERS are empty alert templates for handlers that don't exist in the kapacitor config
export const DEFAULT_HANDLERS = [
{
type: 'post',
enabled: true,
url: '',
headers: {},
headerKey: '',
headerValue: '',
},
{type: 'tcp', enabled: true, address: ''},
{type: 'exec', enabled: true, command: []},
{type: 'log', enabled: true, filePath: ''},
]
export const DEFAULT_ALERTS = ['http', 'tcp', 'exec', 'log'] export const MAP_KEYS_FROM_CONFIG = {
hipchat: 'hipChat',
opsgenie: 'opsGenie',
pagerduty: 'pagerDuty',
smtp: 'email',
victorops: 'victorOps',
}
export const RULE_ALERT_OPTIONS = { // ALERTS_FROM_CONFIG the array of fields to accept from Kapacitor Config
http: { export const ALERTS_FROM_CONFIG = {
args: { alerta: ['environment', 'origin', 'token'], // token = bool
label: 'URL:', hipChat: ['url', 'room', 'token'], // token = bool
placeholder: 'Ex: http://example.com/api/alert', opsGenie: ['api-key', 'teams', 'recipients'], // api-key = bool
}, pagerDuty: ['service-key'], // service-key = bool
// properties: [ pushover: ['token', 'user-key'], // token = bool, user-key = bool
// {name: 'endpoint', label: 'Endpoint:', placeholder: 'Endpoint'}, sensu: ['addr', 'source'],
// {name: 'header', label: 'Headers:', placeholder: 'Headers (Delimited)'}, // TODO: determine how to delimit slack: ['url', 'channel'], // url = bool
// ], email: ['from', 'host', 'password', 'port', 'username'], // password = bool
}, talk: ['url', 'author_name'], // url = bool
tcp: { telegram: [
args: { 'token',
label: 'Address:', 'chat-id',
placeholder: 'Ex: exampleendpoint.com:5678', 'parse-mode',
}, 'disable-web-page-preview',
}, 'disable-notification',
exec: { ], // token = bool
args: { victorOps: ['api-key', 'routing-key'], // api-key = bool
label: 'Command (Arguments separated by Spaces):', // snmpTrap: ['trapOid', 'data'], // [oid/type/value]
placeholder: 'Ex: woogie boogie', // influxdb:[],
}, // mqtt:[]
}, }
log: {
args: { export const MAP_FIELD_KEYS_FROM_CONFIG = {
label: 'File:', alerta: {},
placeholder: 'Ex: /tmp/alerts.log', hipChat: {},
}, opsGenie: {},
}, pagerDuty: {'service-key': 'serviceKey'},
alerta: { pushover: {'user-key': 'userKey'},
args: { sensu: {},
label: 'Paste Alerta TICKscript:', // TODO: remove this slack: {},
placeholder: 'alerta()', email: {},
},
// properties: [
// {name: 'token', label: 'Token:', placeholder: 'Token'},
// {name: 'resource', label: 'Resource:', placeholder: 'Resource'},
// {name: 'event', label: 'Event:', placeholder: 'Event'},
// {name: 'environment', label: 'Environment:', placeholder: 'Environment'},
// {name: 'group', label: 'Group:', placeholder: 'Group'},
// {name: 'value', label: 'Value:', placeholder: 'Value'},
// {name: 'origin', label: 'Origin:', placeholder: 'Origin'},
// {name: 'services', label: 'Services:', placeholder: 'Services'}, // TODO: what format?
// ],
},
hipchat: {
properties: [
{name: 'room', label: 'Room:', placeholder: 'happy_place'},
{name: 'token', label: 'Token:', placeholder: 'a_gilded_token'},
],
},
opsgenie: {
// properties: [
// {name: 'recipients', label: 'Recipients:', placeholder: 'happy_place'}, // TODO: what format?
// {name: 'teams', label: 'Teams:', placeholder: 'blue,yellow,maroon'}, // TODO: what format?
// ],
},
pagerduty: {
properties: [
{
name: 'serviceKey',
label: 'Service Key:',
placeholder: 'one_rad_key',
},
],
},
pushover: {
properties: [
{
name: 'device',
label: 'Device:',
placeholder: 'dv1,dv2 (Comma Separated)', // TODO: do these need to be parsed before sent?
},
{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'},
],
},
sensu: {
// TODO: apparently no args or properties, according to kapacitor/ast.go ?
},
slack: {
properties: [
{name: 'channel', label: 'Channel:', placeholder: '#cubeoctohedron'},
{name: 'iconEmoji', label: 'Emoji:', placeholder: ':cubeapple:'},
{name: 'username', label: 'Username:', placeholder: 'pineapple'},
],
},
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'},
},
talk: {}, talk: {},
telegram: { telegram: {
properties: [ 'chat-id': 'chatId',
{name: 'chatId', label: 'Chat ID:', placeholder: 'xxxxxxxxx'}, 'parse-mode': 'parseMode',
{name: 'parseMode', label: 'Emoji:', placeholder: 'Markdown'}, 'disable-web-page-preview': 'disableWebPagePreview',
// { 'disable-notification': 'disableNotification',
// name: 'disableWebPagePreview',
// label: 'Disable Web Page Preview:',
// placeholder: 'true', // TODO: format to bool
// },
// {
// name: 'disableNotification',
// label: 'Disable Notification:',
// placeholder: 'false', // TODO: format to bool
// },
],
},
victorops: {
properties: [
{name: 'routingKey', label: 'Channel:', placeholder: 'team_rocket'},
],
}, },
victorOps: {'routing-key': 'routingKey'},
// snmpTrap: {},
// influxd: {},
// mqtt: {}
} }
export const ALERT_NODES_ACCESSORS = { // HANDLERS_TO_RULE returns array of fields that may be updated for each alert on rule.
http: rule => _.get(rule, 'alertNodes[0].args[0]', ''), export const HANDLERS_TO_RULE = {
tcp: rule => _.get(rule, 'alertNodes[0].args[0]', ''), alerta: [
exec: rule => _.get(rule, 'alertNodes[0].args', []).join(' '), 'resource',
log: rule => _.get(rule, 'alertNodes[0].args[0]', ''), 'event',
smtp: rule => _.get(rule, 'alertNodes[0].args', []).join(' '), 'environment',
alerta: rule => 'group',
_.get(rule, 'alertNodes[0].properties', []) 'value',
.reduce( 'origin',
(strs, item) => { 'service',
strs.push(`${item.name}('${item.args.join(' ')}')`) ],
return strs hipChat: ['room'],
}, opsGenie: ['teams', 'recipients'],
['alerta()'] pagerDuty: [],
) pushover: ['device', 'title', 'sound', 'url', 'urlTitle'],
.join('.'), sensu: ['source', 'handlers'],
slack: ['channel', 'username', 'iconEmoji'],
email: ['to'],
talk: [],
telegram: [
'chatId',
'parseMode',
'disableWebPagePreview',
'disableNotification',
],
victorOps: ['routingKey'],
post: ['url', 'headers', 'captureResponse'],
tcp: ['address'],
exec: ['command'],
log: ['filePath'],
// snmpTrap: ['trapOid', 'data'], // [oid/type/value]
} }

View File

@ -136,9 +136,9 @@ class KapacitorPage extends Component {
} }
render() { render() {
const {source, addFlashMessage} = this.props const {source, addFlashMessage, location, params} = this.props
const hash = (location && location.hash) || (params && params.hash) || ''
const {kapacitor, exists} = this.state const {kapacitor, exists} = this.state
return ( return (
<KapacitorForm <KapacitorForm
onSubmit={this.handleSubmit} onSubmit={this.handleSubmit}
@ -148,6 +148,7 @@ class KapacitorPage extends Component {
source={source} source={source}
addFlashMessage={addFlashMessage} addFlashMessage={addFlashMessage}
exists={exists} exists={exists}
hash={hash}
/> />
) )
} }
@ -168,6 +169,7 @@ KapacitorPage.propTypes = {
url: string.isRequired, url: string.isRequired,
kapacitors: array, kapacitors: array,
}), }),
location: shape({pathname: string, hash: string}).isRequired,
} }
export default withRouter(KapacitorPage) export default withRouter(KapacitorPage)

View File

@ -1,31 +1,32 @@
import React, {PropTypes, Component} from 'react' import React, {PropTypes, Component} from 'react'
import {connect} from 'react-redux' import {connect} from 'react-redux'
import _ from 'lodash'
import * as kapacitorRuleActionCreators from 'src/kapacitor/actions/view' import * as kapacitorRuleActionCreators from 'src/kapacitor/actions/view'
import * as kapacitorQueryConfigActionCreators from 'src/kapacitor/actions/queryConfigs' import * as kapacitorQueryConfigActionCreators from 'src/kapacitor/actions/queryConfigs'
import {bindActionCreators} from 'redux' import {bindActionCreators} from 'redux'
import {getActiveKapacitor, getKapacitorConfig} from 'shared/apis/index' import {getActiveKapacitor, getKapacitorConfig} from 'shared/apis/index'
import {RULE_ALERT_OPTIONS, DEFAULT_RULE_ID} from 'src/kapacitor/constants' import {DEFAULT_RULE_ID} from 'src/kapacitor/constants'
import KapacitorRule from 'src/kapacitor/components/KapacitorRule' import KapacitorRule from 'src/kapacitor/components/KapacitorRule'
import parseHandlersFromConfig from 'src/shared/parsing/parseHandlersFromConfig'
class KapacitorRulePage extends Component { class KapacitorRulePage extends Component {
constructor(props) { constructor(props) {
super(props) super(props)
this.state = { this.state = {
enabledAlerts: [], handlersFromConfig: [],
kapacitor: {}, kapacitor: {},
} }
} }
async componentDidMount() { async componentDidMount() {
const {params, source, ruleActions, addFlashMessage} = this.props const {params, source, ruleActions, addFlashMessage} = this.props
if (this.isEditing()) {
ruleActions.fetchRule(source, params.ruleID) if (params.ruleID === 'new') {
} else {
ruleActions.loadDefaultRule() ruleActions.loadDefaultRule()
} else {
ruleActions.fetchRule(source, params.ruleID)
} }
const kapacitor = await getActiveKapacitor(this.props.source) const kapacitor = await getActiveKapacitor(this.props.source)
@ -37,17 +38,9 @@ class KapacitorRulePage extends Component {
} }
try { try {
const {data: {sections}} = await getKapacitorConfig(kapacitor) const kapacitorConfig = await getKapacitorConfig(kapacitor)
const enabledAlerts = Object.keys(sections).filter( const handlersFromConfig = parseHandlersFromConfig(kapacitorConfig)
section => this.setState({kapacitor, handlersFromConfig})
_.get(
sections,
[section, 'elements', '0', 'options', 'enabled'],
false
) && _.get(RULE_ALERT_OPTIONS, section, false)
)
this.setState({kapacitor, enabledAlerts})
} catch (error) { } catch (error) {
addFlashMessage({ addFlashMessage({
type: 'error', type: 'error',
@ -69,10 +62,9 @@ class KapacitorRulePage extends Component {
addFlashMessage, addFlashMessage,
queryConfigActions, queryConfigActions,
} = this.props } = this.props
const {enabledAlerts, kapacitor} = this.state const {handlersFromConfig, kapacitor} = this.state
const rule = this.isEditing() const rule =
? rules[params.ruleID] params.ruleID === 'new' ? rules[DEFAULT_RULE_ID] : rules[params.ruleID]
: rules[DEFAULT_RULE_ID]
const query = rule && queryConfigs[rule.queryID] const query = rule && queryConfigs[rule.queryID]
if (!query) { if (!query) {
@ -80,25 +72,21 @@ class KapacitorRulePage extends Component {
} }
return ( return (
<KapacitorRule <KapacitorRule
source={source}
rule={rule} rule={rule}
query={query} query={query}
router={router}
source={source}
kapacitor={kapacitor}
ruleActions={ruleActions}
queryConfigs={queryConfigs} queryConfigs={queryConfigs}
isEditing={this.isEditing()}
enabledAlerts={enabledAlerts}
addFlashMessage={addFlashMessage}
queryConfigActions={queryConfigActions} queryConfigActions={queryConfigActions}
ruleActions={ruleActions}
addFlashMessage={addFlashMessage}
handlersFromConfig={handlersFromConfig}
ruleID={params.ruleID}
router={router}
kapacitor={kapacitor}
configLink={`/sources/${source.id}/kapacitors/${kapacitor.id}/edit`}
/> />
) )
} }
isEditing = () => {
const {params} = this.props
return params.ruleID && params.ruleID !== 'new'
}
} }
const {func, shape, string} = PropTypes const {func, shape, string} = PropTypes
@ -121,7 +109,6 @@ KapacitorRulePage.propTypes = {
removeEvery: func.isRequired, removeEvery: func.isRequired,
updateRuleValues: func.isRequired, updateRuleValues: func.isRequired,
updateMessage: func.isRequired, updateMessage: func.isRequired,
updateAlerts: func.isRequired,
updateRuleName: func.isRequired, updateRuleName: func.isRequired,
}).isRequired, }).isRequired,
queryConfigActions: shape({}).isRequired, queryConfigActions: shape({}).isRequired,

View File

@ -70,7 +70,6 @@ KapacitorRulesPage.propTypes = {
name: string.isRequired, name: string.isRequired,
trigger: string.isRequired, trigger: string.isRequired,
message: string.isRequired, message: string.isRequired,
alerts: arrayOf(string.isRequired).isRequired,
}) })
).isRequired, ).isRequired,
actions: shape({ actions: shape({

View File

@ -1,6 +1,9 @@
import {defaultRuleConfigs, DEFAULT_RULE_ID} from 'src/kapacitor/constants' import {
defaultRuleConfigs,
DEFAULT_RULE_ID,
HANDLERS_TO_RULE,
} from 'src/kapacitor/constants'
import _ from 'lodash' import _ from 'lodash'
import {parseAlerta} from 'shared/parsing/parseAlerta'
export default function rules(state = {}, action) { export default function rules(state = {}, action) {
switch (action.type) { switch (action.type) {
@ -13,8 +16,7 @@ export default function rules(state = {}, action) {
trigger: 'threshold', trigger: 'threshold',
values: defaultRuleConfigs.threshold, values: defaultRuleConfigs.threshold,
message: '', message: '',
alerts: [], alertNodes: {},
alertNodes: [],
every: null, every: null,
name: 'Untitled Rule', name: 'Untitled Rule',
}, },
@ -74,71 +76,33 @@ export default function rules(state = {}, action) {
}) })
} }
case 'UPDATE_RULE_ALERTS': {
const {ruleID, alerts} = action.payload
return Object.assign({}, state, {
[ruleID]: Object.assign({}, state[ruleID], {
alerts,
}),
})
}
// 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': { case 'UPDATE_RULE_ALERT_NODES': {
const {ruleID, alertNodeName, alertNodesText} = action.payload const {ruleID, alerts} = action.payload
const alertNodesByType = {}
let alertNodesByType _.forEach(alerts, h => {
if (h.enabled) {
switch (alertNodeName) { if (h.type === 'post') {
case 'http': if (h.url === '') {
case 'tcp': return
case 'log': }
alertNodesByType = [ h.headers = {[h.headerKey]: h.headerValue}
{ }
name: alertNodeName, if (h.type === 'log' && h.filePath === '') {
args: [alertNodesText], return
properties: [], }
}, if (h.type === 'tcp' && h.address === '') {
return
}
if (h.type === 'exec' && h.command.length === 0) {
return
}
const existing = _.get(alertNodesByType, h.type, [])
alertNodesByType[h.type] = [
...existing,
_.pick(h, HANDLERS_TO_RULE[h.type]),
] ]
break }
case 'exec': })
case 'smtp':
alertNodesByType = [
{
name: alertNodeName,
args: alertNodesText.split(' '),
properties: [],
},
]
break
case 'alerta':
alertNodesByType = [
{
name: alertNodeName,
args: [],
properties: parseAlerta(alertNodesText),
},
]
break
case 'hipchat':
case 'opsgenie':
case 'pagerduty':
case 'slack':
case 'telegram':
case 'victorops':
case 'pushover':
default:
alertNodesByType = [
{
name: alertNodeName,
args: [],
properties: [],
},
]
}
return Object.assign({}, state, { return Object.assign({}, state, {
[ruleID]: Object.assign({}, state[ruleID], { [ruleID]: Object.assign({}, state[ruleID], {
alertNodes: alertNodesByType, alertNodes: alertNodesByType,
@ -146,39 +110,6 @@ 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': { case 'UPDATE_RULE_NAME': {
const {ruleID, name} = action.payload const {ruleID, name} = action.payload
return Object.assign({}, state, { return Object.assign({}, state, {

View File

@ -152,20 +152,18 @@ export function updateKapacitorConfigSection(kapacitor, section, properties) {
}) })
} }
export function testAlertOutput(kapacitor, outputName, properties) { export const testAlertOutput = async (kapacitor, outputName) => {
return kapacitorProxy( try {
kapacitor, const {data: {services}} = await kapacitorProxy(
'GET',
'/kapacitor/v1/service-tests'
).then(({data: {services}}) => {
const service = services.find(s => s.name === outputName)
return kapacitorProxy(
kapacitor, kapacitor,
'POST', 'GET',
service.link.href, '/kapacitor/v1/service-tests'
Object.assign({}, service.options, properties)
) )
}) const service = services.find(s => s.name === outputName)
return kapacitorProxy(kapacitor, 'POST', service.link.href, {})
} catch (error) {
console.error(error)
}
} }
export function createKapacitorTask(kapacitor, id, type, dbrps, script) { export function createKapacitorTask(kapacitor, id, type, dbrps, script) {

View File

@ -29,8 +29,9 @@ const DygraphLegend = ({
isFilterVisible, isFilterVisible,
onToggleFilter, onToggleFilter,
}) => { }) => {
const withValues = series.filter(s => s.y)
const sorted = _.sortBy( const sorted = _.sortBy(
series, withValues,
({y, label}) => (sortType === 'numeric' ? y : label) ({y, label}) => (sortType === 'numeric' ? y : label)
) )

View File

@ -9,6 +9,7 @@ export const Tab = React.createClass({
isDisabled: bool, isDisabled: bool,
isActive: bool, isActive: bool,
isKapacitorTab: bool, isKapacitorTab: bool,
isConfigured: bool,
}, },
render() { render() {
@ -24,7 +25,10 @@ export const Tab = React.createClass({
} }
return ( return (
<div <div
className={classnames('btn tab', {active: this.props.isActive})} className={classnames('btn tab', {
active: this.props.isActive,
configured: this.props.isConfigured,
})}
onClick={this.props.isDisabled ? null : this.props.onClick} onClick={this.props.isDisabled ? null : this.props.onClick}
> >
{this.props.children} {this.props.children}

View File

@ -15,6 +15,7 @@ class TagInput extends Component {
this.input.value = '' this.input.value = ''
onAddTag(newItem) onAddTag(newItem)
this.props.disableTest()
} }
} }
@ -56,6 +57,7 @@ TagInput.propTypes = {
onDeleteTag: func.isRequired, onDeleteTag: func.isRequired,
tags: arrayOf(string).isRequired, tags: arrayOf(string).isRequired,
title: string.isRequired, title: string.isRequired,
disableTest: func,
} }
export default TagInput export default TagInput

View File

@ -1,21 +0,0 @@
const alertaRegex = /(services)\('(.+?)'\)|(resource)\('(.+?)'\)|(event)\('(.+?)'\)|(environment)\('(.+?)'\)|(group)\('(.+?)'\)|(origin)\('(.+?)'\)|(token)\('(.+?)'\)/gi
export function parseAlerta(string) {
const properties = []
let match
while ((match = alertaRegex.exec(string))) {
// eslint-disable-line no-cond-assign
for (let m = 1; m < match.length; m += 2) {
if (match[m]) {
properties.push({
name: match[m],
args:
match[m] === 'services' ? match[m + 1].split(' ') : [match[m + 1]],
})
}
}
}
return properties
}

View File

@ -0,0 +1,46 @@
import _ from 'lodash'
import {
ALERTS_FROM_CONFIG,
MAP_FIELD_KEYS_FROM_CONFIG,
MAP_KEYS_FROM_CONFIG,
} from 'src/kapacitor/constants'
const parseHandlersFromConfig = config => {
const {data: {sections}} = config
const allHandlers = _.map(sections, (v, k) => {
const fromConfig = _.get(v, ['elements', '0', 'options'], {})
return {
// fill type with handler names in rule
type: _.get(MAP_KEYS_FROM_CONFIG, k, k),
...fromConfig,
}
})
// map handler names from config to handler names in rule
const mappedHandlers = _.mapKeys(allHandlers, (v, k) => {
return _.get(MAP_KEYS_FROM_CONFIG, k, k)
})
// filter out any handlers from config that are not allowed
const allowedHandlers = _.filter(
mappedHandlers,
h => h.type in ALERTS_FROM_CONFIG
)
// filter out any fields of handlers that are not allowed
const pickedHandlers = _.map(allowedHandlers, h => {
return _.pick(h, ['type', 'enabled', ...ALERTS_FROM_CONFIG[h.type]])
})
// map field names from config to field names in rule
const fieldKeyMappedHandlers = _.map(pickedHandlers, h => {
return _.mapKeys(h, (v, k) => {
return _.get(MAP_FIELD_KEYS_FROM_CONFIG[h.type], k, k)
})
})
return fieldKeyMappedHandlers
}
export default parseHandlersFromConfig

View File

@ -0,0 +1,57 @@
import _ from 'lodash'
import {HANDLERS_TO_RULE} from 'src/kapacitor/constants'
export const parseHandlersFromRule = (rule, handlersFromConfig) => {
const handlersOfKind = {}
const handlersOnThisAlert = []
const handlersFromRule = _.pickBy(rule.alertNodes, (v, k) => {
return k in HANDLERS_TO_RULE
})
_.forEach(handlersFromRule, (v, alertKind) => {
const thisAlertFromConfig = _.find(
handlersFromConfig,
h => h.type === alertKind
)
_.forEach(v, alertOptions => {
const count = _.get(handlersOfKind, alertKind, 0) + 1
handlersOfKind[alertKind] = count
if (alertKind === 'post') {
const headers = alertOptions.headers
alertOptions.headerKey = _.keys(headers)[0]
alertOptions.headerValue = _.values(headers)[0]
alertOptions = _.omit(alertOptions, 'headers')
}
const ep = {
enabled: true,
...thisAlertFromConfig,
...alertOptions,
alias: `${alertKind}-${count}`,
type: alertKind,
}
handlersOnThisAlert.push(ep)
})
})
const selectedHandler = handlersOnThisAlert.length
? handlersOnThisAlert[0]
: null
return {handlersOnThisAlert, selectedHandler, handlersOfKind}
}
export const parseAlertNodeList = rule => {
const nodeList = _.transform(
rule.alertNodes,
(acc, v, k) => {
if (k in HANDLERS_TO_RULE && v.length > 0) {
acc.push(k)
}
},
[]
)
const uniqNodeList = _.uniq(nodeList)
return _.join(uniqNodeList, ', ')
}

View File

@ -21,7 +21,7 @@ const kapacitorDropdown = (
to={`/sources/${source.id}/kapacitors/new`} to={`/sources/${source.id}/kapacitors/new`}
className="btn btn-xs btn-default" className="btn btn-xs btn-default"
> >
<span className="icon plus" /> Add Config <span className="icon plus" /> Add Kapacitor Connection
</Link> </Link>
</Authorized> </Authorized>
) )
@ -62,7 +62,7 @@ const kapacitorDropdown = (
onChoose={setActiveKapacitor} onChoose={setActiveKapacitor}
addNew={{ addNew={{
url: `/sources/${source.id}/kapacitors/new`, url: `/sources/${source.id}/kapacitors/new`,
text: 'Add Kapacitor', text: 'Add Kapacitor Connection',
}} }}
actions={[ actions={[
{ {
@ -105,16 +105,16 @@ const InfluxTable = ({
<h2 className="panel-title"> <h2 className="panel-title">
{isUsingAuth {isUsingAuth
? <span> ? <span>
InfluxDB Sources for <em>{me.currentOrganization.name}</em> Connections for <em>{me.currentOrganization.name}</em>
</span> </span>
: <span>InfluxDB Sources</span>} : <span>Connections</span>}
</h2> </h2>
<Authorized requiredRole={EDITOR_ROLE}> <Authorized requiredRole={EDITOR_ROLE}>
<Link <Link
to={`/sources/${source.id}/manage-sources/new`} to={`/sources/${source.id}/manage-sources/new`}
className="btn btn-sm btn-primary" className="btn btn-sm btn-primary"
> >
<span className="icon plus" /> Add Source <span className="icon plus" /> Add Connection
</Link> </Link>
</Authorized> </Authorized>
</div> </div>
@ -123,14 +123,14 @@ const InfluxTable = ({
<thead> <thead>
<tr> <tr>
<th className="source-table--connect-col" /> <th className="source-table--connect-col" />
<th>Source Name & Host</th> <th>InfluxDB Connection</th>
<th className="text-right" /> <th className="text-right" />
<th> <th>
Active Kapacitor{' '} Kapacitor Connection{' '}
<QuestionMarkTooltip <QuestionMarkTooltip
tipID="kapacitor-node-helper" tipID="kapacitor-node-helper"
tipContent={ tipContent={
'<p>Kapacitor Configurations are<br/>scoped per InfluxDB Source.<br/>Only one can be active at a time.</p>' '<p>Kapacitor Connections are<br/>scoped per InfluxDB Connection.<br/>Only one can be active at a time.</p>'
} }
/> />
</th> </th>
@ -189,7 +189,7 @@ const InfluxTable = ({
href="#" href="#"
onClick={handleDeleteSource(s)} onClick={handleDeleteSource(s)}
> >
Delete Source Delete Connection
</a> </a>
</Authorized> </Authorized>
</td> </td>

View File

@ -23,13 +23,14 @@ const SourceForm = ({
? <div className="text-center"> ? <div className="text-center">
{me.role === SUPERADMIN_ROLE {me.role === SUPERADMIN_ROLE
? <h3> ? <h3>
<strong>{me.currentOrganization.name}</strong> has no sources <strong>{me.currentOrganization.name}</strong> has no
connections
</h3> </h3>
: <h3> : <h3>
<strong>{me.currentOrganization.name}</strong> has no sources <strong>{me.currentOrganization.name}</strong> has no
available to <em>{me.role}s</em> connections available to <em>{me.role}s</em>
</h3>} </h3>}
<h6>Add a Source below:</h6> <h6>Add a Connection below:</h6>
</div> </div>
: null} : null}
@ -112,13 +113,13 @@ const SourceForm = ({
<div className="form-control-static"> <div className="form-control-static">
<input <input
type="checkbox" type="checkbox"
id="defaultSourceCheckbox" id="defaultConnectionCheckbox"
name="default" name="default"
checked={source.default} checked={source.default}
onChange={onInputChange} onChange={onInputChange}
/> />
<label htmlFor="defaultSourceCheckbox"> <label htmlFor="defaultConnectionCheckbox">
Make this the default source Make this the default connection
</label> </label>
</div> </div>
</div> </div>
@ -148,7 +149,7 @@ const SourceForm = ({
type="submit" type="submit"
> >
<span className={`icon ${editMode ? 'checkmark' : 'plus'}`} /> <span className={`icon ${editMode ? 'checkmark' : 'plus'}`} />
{editMode ? 'Save Changes' : 'Add Source'} {editMode ? 'Save Changes' : 'Add Connection'}
</button> </button>
<br /> <br />

View File

@ -131,7 +131,7 @@ class SourcePage extends Component {
.catch(err => { .catch(err => {
// dont want to flash this until they submit // dont want to flash this until they submit
const error = this._parseError(err) const error = this._parseError(err)
console.error('Error on source creation: ', error) console.error('Error creating InfluxDB connection: ', error)
}) })
} }
@ -142,10 +142,10 @@ class SourcePage extends Component {
.then(({data: sourceFromServer}) => { .then(({data: sourceFromServer}) => {
this.props.addSourceAction(sourceFromServer) this.props.addSourceAction(sourceFromServer)
this._redirect(sourceFromServer) this._redirect(sourceFromServer)
notify('success', `New source ${source.name} added`) notify('success', `InfluxDB ${source.name} available as a connection`)
}) })
.catch(error => { .catch(error => {
this.handleError('Unable to create source', error) this.handleError('Unable to create InfluxDB connection', error)
}) })
} }
@ -156,10 +156,10 @@ class SourcePage extends Component {
.then(({data: sourceFromServer}) => { .then(({data: sourceFromServer}) => {
this.props.updateSourceAction(sourceFromServer) this.props.updateSourceAction(sourceFromServer)
this._redirect(sourceFromServer) this._redirect(sourceFromServer)
notify('success', `Source ${source.name} updated`) notify('success', `InfluxDB connection ${source.name} updated`)
}) })
.catch(error => { .catch(error => {
this.handleError('Unable to update source', error) this.handleError('Unable to update InfluxDB connection', error)
}) })
} }
@ -208,7 +208,9 @@ class SourcePage extends Component {
<div className="page-header__col-md-8"> <div className="page-header__col-md-8">
<div className="page-header__left"> <div className="page-header__left">
<h1 className="page-header__title"> <h1 className="page-header__title">
{editMode ? 'Edit Source' : 'Add a New Source'} {editMode
? 'Configure InfluxDB Connection'
: 'Add a New InfluxDB Connection'}
</h1> </h1>
</div> </div>
{isInitialSource {isInitialSource

View File

@ -37,12 +37,13 @@ $config-endpoint-tab-bg-active: $g3-castle;
border-radius: 0; border-radius: 0;
height: $config-endpoint-tab-height; height: $config-endpoint-tab-height;
border: 0; border: 0;
padding: 0 40px 0 15px; padding: 0 50px 0 15px;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
font-weight: 500; font-weight: 500;
font-size: 16px; font-size: 16px;
position: relative;
&:first-child {border-top-left-radius: $radius;} &:first-child {border-top-left-radius: $radius;}
@ -54,6 +55,22 @@ $config-endpoint-tab-bg-active: $g3-castle;
color: $config-endpoint-tab-text-active; color: $config-endpoint-tab-text-active;
background-color: $config-endpoint-tab-bg-active; background-color: $config-endpoint-tab-bg-active;
} }
// Checkmark for configured state, hidden by default
&:after {
content: "\e918";
font-family: 'icomoon';
color: $c-rainforest;
position: absolute;
top: 50%;
right: 14px;
transform: translateY(-50%);
opacity: 0;
transition: opacity 0.25s ease;
}
&.configured:after {
opacity: 1;
}
} }
} }
} }

View File

@ -126,6 +126,7 @@ $rule-builder--radius-lg: 5px;
display: none; display: none;
} }
} }
// Generic re-usable classes for rule builder sections // Generic re-usable classes for rule builder sections
.rule-section--border-top { .rule-section--border-top {
border-top: 2px solid $rule-builder--section-border; border-top: 2px solid $rule-builder--section-border;
@ -260,7 +261,7 @@ $rule-builder--radius-lg: 5px;
top: ($rule-builder--padding-lg * 2); top: ($rule-builder--padding-lg * 2);
left: $rule-builder--padding-sm; left: $rule-builder--padding-sm;
width: calc(100% - #{$rule-builder--padding-sm * 2}); width: calc(100% - #{$rule-builder--padding-sm * 2});
height: calc(100% - #{$rule-builder--padding-lg * 2}) !important;; height: calc(100% - #{$rule-builder--padding-lg * 2}) !important;
} }
> .dygraph > .dygraph-child { > .dygraph > .dygraph-child {
position: absolute; position: absolute;
@ -290,7 +291,10 @@ $rule-builder--radius-lg: 5px;
} }
.rule-builder--graph-options { .rule-builder--graph-options {
width: 100%; width: 100%;
padding: $rule-builder--padding-sm ($rule-builder--padding-lg - $rule-builder--padding-sm); padding: $rule-builder--padding-sm
(
$rule-builder--padding-lg - $rule-builder--padding-sm
);
display: flex; display: flex;
align-items: center; align-items: center;
height: ($rule-builder--padding-lg * 2); height: ($rule-builder--padding-lg * 2);
@ -309,7 +313,9 @@ $rule-builder--radius-lg: 5px;
*/ */
.rule-builder--message { .rule-builder--message {
background-color: $rule-builder--section-bg; background-color: $rule-builder--section-bg;
padding: $rule-builder--padding-sm ($rule-builder--padding-lg - 2px); padding: $rule-builder--padding-lg - 2px;
padding-bottom: 0;
border-radius: $rule-builder--radius-lg $rule-builder--radius-lg 0 0;
} }
.rule-builder--message textarea { .rule-builder--message textarea {
height: 100px; height: 100px;
@ -404,3 +410,216 @@ $rule-builder--radius-lg: 5px;
color: $c-dreamsicle !important; color: $c-dreamsicle !important;
} }
} }
/*
Styles for Endpoints section
-----------------------------------------------------------------------------
*/
.rule-message--endpoints {
display: flex;
align-items: stretch;
flex-wrap: nowrap;
}
.rule-message--add-endpoint {
margin-left: 4px;
}
.rule-message--add-endpoint .dropdown-menu {
max-height: 233px;
}
.rule-message--add-endpoint .dropdown-menu.dropdown-malachite li.dropdown-divider {
background: linear-gradient(to right, #a8e1cf 0%, #23adf6 100%) !important;
}
.endpoint-tabs {
width: 150px;
background-color: $rule-builder--section-border;
border-bottom-left-radius: $rule-builder--radius-lg;
display: flex;
flex-direction: column;
margin: 0;
padding: 0;
overflow: hidden;
}
.endpoint-tab {
display: block;
list-style: none;
@include no-user-select();
position: relative;
height: 40px;
line-height: 40px;
padding: 0 $rule-builder--padding-lg;
margin: 0 0 2px 0;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
color: $g9-mountain;
font-size: 14.5px;
font-weight: 600;
border-right: 2px solid $rule-builder--section-border;
transition: color 0.25s ease, background-color 0.25s ease,
border-color 0.25s ease;
&:last-child {
margin-bottom: 0px;
}
&:hover {
cursor: pointer;
background-color: $rule-builder--section-bg;
color: $g15-platinum;
}
&.active {
color: $c-rainforest;
background-color: $rule-builder--section-bg;
border-color: $rule-builder--section-bg;
}
}
.endpoint-tab--delete {
position: absolute;
top: 50%;
right: 10px;
transform: translateY(-50%);
width: 20px;
height: 20px;
border-radius: 3px;
transition: background-color 0.25s ease;
outline: none;
background-color: transparent;
border: none;
&:before,
&:after {
content: '';
position: absolute;
top: 50%;
left: 50%;
background-color: $g8-storm;
border-radius: 1px;
transform: translate(-50%, -50%) rotate(45deg);
transition: background-color 0.25s ease;
}
&:before {
width: 12px;
height: 2px;
}
&:after {
width: 2px;
height: 12px;
}
&:hover {
background-color: $g5-pepper;
cursor: pointer;
&:before,
&:after {
background-color: $g20-white;
}
}
&:hover:active {
background-color: $c-curacao;
}
}
.endpoint-tab-contents {
flex: 1 0 0;
background-color: $rule-builder--section-bg;
border-bottom-right-radius: $rule-builder--radius-lg;
display: flex;
flex-direction: column;
align-items: stretch;
min-height: 350px;
h4 {
width: 100%;
margin: 0;
margin-bottom: 8px;
@include no-user-select();
font-size: 14.5px;
font-weight: 600;
color: $g15-platinum;
}
}
.endpoint-tab--parameters {
padding: $rule-builder--padding-lg;
padding-bottom: 0;
&:last-child {
padding-bottom: $rule-builder--padding-lg;
}
}
.endpoint-tab--parameters .faux-form {
margin-left: -6px;
margin-right: -6px;
width: calc(100% + 12px);
display: inline-block;
}
.endpoint-tab--parameters--empty {
align-items: center;
justify-content: center;
@include no-user-select();
p {
margin: 0;
font-size: 16px;
line-height: 23px;
text-align: center;
color: $g12-forge;
strong {
color: $g18-cloud;
font-weight: 900;
}
}
}
.endpoint-tab--parameters .form-control-static {
min-height: 30px;
height: 30px;
}
.endpoint-tab--parameters .handler-checkbox {
margin-left: 10px;
margin-right: 10px;
}
.redacted-handler {
height: 30px;
align-items: center;
justify-content: space-between;
}
.endpoint-tab--parameters h4 .btn {
margin-left: 6px;
}
/*
Rule Details
-----------------------------------------------------------------------------
*/
.rule-builder--details {
background-color: $rule-builder--section-bg;
padding-top: $rule-builder--padding-lg - 2px;
padding-left: 6px;
padding-right: 6px;
padding-bottom: 0;
border-radius: $rule-builder--radius-lg $rule-builder--radius-lg 0 0;
}
.rule-builder--details textarea {
height: 100px;
min-width: 100%;
max-width: 100%;
width: 100% !important;
@include custom-scrollbar($rule-builder--section-bg,$rule-builder--accent-color);
}
.rule-builder--details-template {
height: 30px;
line-height: 30px;
padding: 0 ($rule-builder--padding-sm - 2px);
margin: 2px;
transition: color 0.25s ease;
@include no-user-select();
&:hover {
color: $rule-builder--accent-color;
cursor: pointer;
}
}

View File

@ -4233,10 +4233,10 @@ p .label {
Kapacitor Theme Dropdowns Kapacitor Theme Dropdowns
*/ */
.dropdown .dropdown-menu.dropdown-malachite { .dropdown .dropdown-menu.dropdown-malachite {
background: #4ed8a0; background: #32b08c;
background: -moz-linear-gradient(left, #4ed8a0 0%, #22adf6 100%); background: -moz-linear-gradient(left, #32b08c 0%, #22adf6 100%);
background: -webkit-linear-gradient(left, #4ed8a0 0%, #22adf6 100%); background: -webkit-linear-gradient(left, #32b08c 0%, #22adf6 100%);
background: linear-gradient(to right, #4ed8a0 0%, #22adf6 100%); background: linear-gradient(to right, #32b08c 0%, #22adf6 100%);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='@color1', endColorstr='@color2', GradientType=1); filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='@color1', endColorstr='@color2', GradientType=1);
} }
.dropdown .dropdown-menu.dropdown-malachite li.dropdown-item:hover, .dropdown .dropdown-menu.dropdown-malachite li.dropdown-item:hover,
@ -4261,10 +4261,10 @@ p .label {
.dropdown .dropdown-menu.dropdown-malachite li.dropdown-item > a:focus:active, .dropdown .dropdown-menu.dropdown-malachite li.dropdown-item > a:focus:active,
.dropdown .dropdown-menu.dropdown-malachite li.dropdown-item > a:focus:active:hover { .dropdown .dropdown-menu.dropdown-malachite li.dropdown-item > a:focus:active:hover {
color: #fff; color: #fff;
background: #32b08c; background: #108174;
background: -moz-linear-gradient(left, #32b08c 0%, #22adf6 100%); background: -moz-linear-gradient(left, #108174 0%, #22adf6 100%);
background: -webkit-linear-gradient(left, #32b08c 0%, #22adf6 100%); background: -webkit-linear-gradient(left, #108174 0%, #22adf6 100%);
background: linear-gradient(to right, #32b08c 0%, #22adf6 100%); background: linear-gradient(to right, #108174 0%, #22adf6 100%);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='@color1', endColorstr='@color2', GradientType=1); filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='@color1', endColorstr='@color2', GradientType=1);
} }
.dropdown .dropdown-menu.dropdown-malachite li.dropdown-item .dropdown-action { .dropdown .dropdown-menu.dropdown-malachite li.dropdown-item .dropdown-action {
@ -4274,25 +4274,25 @@ p .label {
color: #fff; color: #fff;
} }
.dropdown .dropdown-menu.dropdown-malachite li.dropdown-item.active { .dropdown .dropdown-menu.dropdown-malachite li.dropdown-item.active {
background: #32b08c; background: #108174;
background: -moz-linear-gradient(left, #32b08c 0%, #22adf6 100%); background: -moz-linear-gradient(left, #108174 0%, #22adf6 100%);
background: -webkit-linear-gradient(left, #32b08c 0%, #22adf6 100%); background: -webkit-linear-gradient(left, #108174 0%, #22adf6 100%);
background: linear-gradient(to right, #32b08c 0%, #22adf6 100%); background: linear-gradient(to right, #108174 0%, #22adf6 100%);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='@color1', endColorstr='@color2', GradientType=1); filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='@color1', endColorstr='@color2', GradientType=1);
} }
.dropdown .dropdown-menu.dropdown-malachite li.dropdown-divider { .dropdown .dropdown-menu.dropdown-malachite li.dropdown-divider {
background: #32b08c; background: #108174;
background: -moz-linear-gradient(left, #32b08c 0%, #4591ed 100%); background: -moz-linear-gradient(left, #108174 0%, #4591ed 100%);
background: -webkit-linear-gradient(left, #32b08c 0%, #4591ed 100%); background: -webkit-linear-gradient(left, #108174 0%, #4591ed 100%);
background: linear-gradient(to right, #32b08c 0%, #4591ed 100%); background: linear-gradient(to right, #108174 0%, #4591ed 100%);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='@color1', endColorstr='@color2', GradientType=1); filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='@color1', endColorstr='@color2', GradientType=1);
} }
.dropdown .dropdown-menu.dropdown-malachite li.dropdown-header { .dropdown .dropdown-menu.dropdown-malachite li.dropdown-header {
color: #c6ffd0; color: #c6ffd0;
background: #32b08c; background: #108174;
background: -moz-linear-gradient(left, #32b08c 0%, #4591ed 100%); background: -moz-linear-gradient(left, #108174 0%, #4591ed 100%);
background: -webkit-linear-gradient(left, #32b08c 0%, #4591ed 100%); background: -webkit-linear-gradient(left, #108174 0%, #4591ed 100%);
background: linear-gradient(to right, #32b08c 0%, #4591ed 100%); background: linear-gradient(to right, #108174 0%, #4591ed 100%);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='@color1', endColorstr='@color2', GradientType=1); filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='@color1', endColorstr='@color2', GradientType=1);
} }
.dropdown .dropdown-menu.dropdown-malachite.dropdown-menu--no-highlight li.dropdown-item.highlight, .dropdown .dropdown-menu.dropdown-malachite.dropdown-menu--no-highlight li.dropdown-item.highlight,

Some files were not shown because too many files have changed in this diff Show More