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]
current_version = 1.4.0.0
current_version = 1.4.0.1
files = README.md server/swagger.json
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)\.(?P<release>\d+)
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]
### 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
@ -5,6 +26,7 @@
### Bug Fixes
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. [#2687](https://github.com/influxdata/chronograf/pull/2687): Remove series with "no value" from legend
## v1.4.0.0-rc2 [2017-12-21]
### UI Improvements
@ -81,6 +103,19 @@
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
### 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]
### Bug Fixes
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]]
name = "github.com/influxdata/influxdb"
packages = ["influxql","influxql/internal","influxql/neldermead","models","pkg/escape"]
revision = "af72d9b0e4ebe95be30e89b160f43eabaf0529ed"
revision = "cd9363b52cac452113b95554d98a6be51beda24e"
version = "v1.1.5"
[[projects]]
name = "github.com/influxdata/kapacitor"
packages = ["client/v1","pipeline","services/k8s/client","tick","tick/ast","tick/stateful","udf/agent"]
revision = "3b5512f7276483326577907803167e4bb213c613"
version = "v1.3.1"
packages = ["client/v1","pipeline","pipeline/tick","services/k8s/client","tick","tick/ast","tick/stateful","udf/agent"]
revision = "6de30070b39afde111fea5e041281126fe8aae31"
[[projects]]
name = "github.com/influxdata/usage-client"
@ -140,6 +140,6 @@
[solve-meta]
analyzer-name = "dep"
analyzer-version = 1
inputs-digest = "85a5451fc9e0596e486a676204eb2de0b12900522341ee0804cf9ec86fb2765e"
inputs-digest = "a5bd1aa82919723ff8ec5dd9d520329862de8181ca9dba75c6acb3a34df5f1a4"
solver-name = "gps-cdcl"
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"
revision = "1bc362c7737e51014af7299e016444b654095ad9"
[[constraint]]
name = "github.com/influxdata/influxdb"
revision = "af72d9b0e4ebe95be30e89b160f43eabaf0529ed"
[[constraint]]
name = "github.com/influxdata/kapacitor"
version = "^1.2.0"
[[constraint]]
name = "github.com/influxdata/usage-client"
revision = "6d3895376368aa52a3a81d2a16e90f0f52371967"
@ -75,3 +67,12 @@ required = ["github.com/jteeuwen/go-bindata","github.com/gogo/protobuf/proto","g
[[constraint]]
name = "google.golang.org/api"
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
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
[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
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
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:
```sh
docker pull chronograf:1.4.0.0
docker pull chronograf:1.4.0.1
```
### From Source
@ -198,10 +198,10 @@ docker pull chronograf:1.4.0.0
## 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
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.
Documentation for Telegraf, InfluxDB, and Kapacitor are available at

View File

@ -231,6 +231,7 @@ type SourcesStore interface {
Update(context.Context, Source) error
}
// DBRP is a database and retention policy for a kapacitor task
type DBRP struct {
DB string `json:"db"`
RP string `json:"rp"`
@ -238,25 +239,24 @@ type DBRP struct {
// AlertRule represents rules for building a tickscript alerting task
type AlertRule struct {
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
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
Alerts []string `json:"alerts"` // Alerts name all the services to notify (e.g. pagerduty)
AlertNodes []KapacitorNode `json:"alertNodes,omitempty"` // AlertNodes define additional arguments to alerts
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.
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
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
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
Executing bool `json:"executing"` // Whether the task is currently executing
Error string `json:"error"` // Any error encountered when kapacitor executes the task
Created time.Time `json:"created"` // Date the task was first created
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
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
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
AlertNodes AlertNodes `json:"alertNodes"` // AlertNodes defines the destinations for the 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.
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
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
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
Executing bool `json:"executing"` // Whether the task is currently executing
Error string `json:"error"` // Any error encountered when kapacitor executes the task
Created time.Time `json:"created"` // Date the task was first created
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
}
// TICKScript task to be used by kapacitor

View File

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

View File

@ -8,8 +8,12 @@ After=network-online.target
[Service]
User=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
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
Restart=on-failure

View File

@ -2289,6 +2289,7 @@ func TestServer(t *testing.T) {
// Use testdata directory for the canned data
tt.args.server.CannedPath = "testdata"
tt.args.server.ResourcesPath = "testdata"
// This is so that we can use staticly generate jwts
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
import (
"fmt"
"bytes"
"encoding/json"
"regexp"
"strings"
"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
func AlertServices(rule chronograf.AlertRule) (string, error) {
node, err := addAlertNodes(rule)
node, err := addAlertNodes(rule.AlertNodes)
if err != nil {
return "", err
}
@ -94,3 +23,45 @@ func AlertServices(rule chronograf.AlertRule) (string, error) {
}
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",
rule: chronograf.AlertRule{
Alerts: []string{"slack", "victorops", "email"},
AlertNodes: chronograf.AlertNodes{
Slack: []*chronograf.Slack{{}},
VictorOps: []*chronograf.VictorOps{{}},
Email: []*chronograf.Email{{}},
},
},
want: `alert()
.slack()
.victorOps()
.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",
rule: chronograf.AlertRule{
Alerts: []string{"slack"},
AlertNodes: chronograf.AlertNodes{
Slack: []*chronograf.Slack{{}},
},
},
want: `alert()
.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",
rule: chronograf.AlertRule{
Alerts: []string{"slack"},
AlertNodes: []chronograf.KapacitorNode{
{
Name: "slack",
Properties: []chronograf.KapacitorProperty{
{
Name: "channel",
Args: []string{"#general"},
},
AlertNodes: chronograf.AlertNodes{
Slack: []*chronograf.Slack{
{
Channel: "#general",
},
},
},
@ -73,10 +82,11 @@ func TestAlertServices(t *testing.T) {
{
name: "Test tcp",
rule: chronograf.AlertRule{
AlertNodes: []chronograf.KapacitorNode{
{
Name: "tcp",
Args: []string{"myaddress:22"},
AlertNodes: chronograf.AlertNodes{
TCPs: []*chronograf.TCP{
{
Address: "myaddress:22",
},
},
},
},
@ -84,24 +94,14 @@ func TestAlertServices(t *testing.T) {
.tcp('myaddress:22')
`,
},
{
name: "Test tcp no argument",
rule: chronograf.AlertRule{
AlertNodes: []chronograf.KapacitorNode{
{
Name: "tcp",
},
},
},
wantErr: true,
},
{
name: "Test log",
rule: chronograf.AlertRule{
AlertNodes: []chronograf.KapacitorNode{
{
Name: "log",
Args: []string{"/tmp/alerts.log"},
AlertNodes: chronograf.AlertNodes{
Log: []*chronograf.Log{
{
FilePath: "/tmp/alerts.log",
},
},
},
},
@ -109,82 +109,29 @@ func TestAlertServices(t *testing.T) {
.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",
rule: chronograf.AlertRule{
AlertNodes: []chronograf.KapacitorNode{
{
Name: "http",
Args: []string{"http://myaddress"},
AlertNodes: chronograf.AlertNodes{
Posts: []*chronograf.Post{
{
URL: "http://myaddress",
},
},
},
},
want: `alert()
.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",
rule: chronograf.AlertRule{
AlertNodes: []chronograf.KapacitorNode{
{
Name: "post",
Args: []string{"http://myaddress"},
Properties: []chronograf.KapacitorProperty{
{
Name: "header",
Args: []string{"key", "value"},
},
AlertNodes: chronograf.AlertNodes{
Posts: []*chronograf.Post{
{
URL: "http://myaddress",
Headers: map[string]string{"key": "value"},
},
},
},
@ -192,27 +139,6 @@ func TestAlertServices(t *testing.T) {
want: `alert()
.post('http://myaddress')
.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
import (
"encoding/json"
"regexp"
"strconv"
"strings"
@ -367,8 +368,7 @@ func alertType(script chronograf.TICKScript) (string, error) {
// Reverse converts tickscript to an AlertRule
func Reverse(script chronograf.TICKScript) (chronograf.AlertRule, error) {
rule := chronograf.AlertRule{
Alerts: []string{},
Query: &chronograf.QueryConfig{},
Query: &chronograf.QueryConfig{},
}
t, err := alertType(script)
if err != nil {
@ -483,432 +483,20 @@ func Reverse(script chronograf.TICKScript) (chronograf.AlertRule, error) {
return chronograf.AlertRule{}, err
}
extractAlertNodes(p, &rule)
err = extractAlertNodes(p, &rule)
return rule, err
}
func extractAlertNodes(p *pipeline.Pipeline, rule *chronograf.AlertRule) {
p.Walk(func(n pipeline.Node) error {
switch t := n.(type) {
func extractAlertNodes(p *pipeline.Pipeline, rule *chronograf.AlertRule) error {
return p.Walk(func(n pipeline.Node) error {
switch node := n.(type) {
case *pipeline.AlertNode:
extractHipchat(t, rule)
extractOpsgenie(t, rule)
extractPagerduty(t, rule)
extractVictorops(t, rule)
extractEmail(t, rule)
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)
octets, err := json.MarshalIndent(node, "", " ")
if err != nil {
return err
}
return json.Unmarshal(octets, &rule.AlertNodes)
}
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)
.slack()
.victorOps()
.email('howdy@howdy.com')
.email('howdy@howdy.com', 'doody@doody.com')
.log('/tmp/alerts.log')
.post('http://backin.tm')
.endpoint('myendpoint')
@ -69,35 +69,29 @@ func TestReverse(t *testing.T) {
want: chronograf.AlertRule{
Name: "name",
Trigger: "threshold",
Alerts: []string{"victorops", "smtp", "http", "slack", "log"},
AlertNodes: []chronograf.KapacitorNode{
{
Name: "victorops",
AlertNodes: chronograf.AlertNodes{
IsStateChangesOnly: true,
Slack: []*chronograf.Slack{
{},
},
{
Name: "smtp",
Args: []string{"howdy@howdy.com"},
VictorOps: []*chronograf.VictorOps{
{},
},
{
Name: "http",
Args: []string{"http://backin.tm"},
Properties: []chronograf.KapacitorProperty{
{
Name: "endpoint",
Args: []string{"myendpoint"},
},
{
Name: "header",
Args: []string{"key", "value"},
},
Email: []*chronograf.Email{
{
To: []string{"howdy@howdy.com", "doody@doody.com"},
},
},
{
Name: "slack",
Log: []*chronograf.Log{
{
FilePath: "/tmp/alerts.log",
},
},
{
Name: "log",
Args: []string{"/tmp/alerts.log"},
Posts: []*chronograf.Post{
{
URL: "http://backin.tm",
Headers: map[string]string{"key": "value"},
},
},
},
TriggerValues: chronograf.TriggerValues{
@ -247,20 +241,19 @@ func TestReverse(t *testing.T) {
AreTagsAccepted: true,
},
Every: "30s",
Alerts: []string{
"victorops",
"smtp",
"slack",
},
AlertNodes: []chronograf.KapacitorNode{
chronograf.KapacitorNode{
Name: "victorops",
AlertNodes: chronograf.AlertNodes{
IsStateChangesOnly: true,
Slack: []*chronograf.Slack{
{},
},
chronograf.KapacitorNode{
Name: "smtp",
VictorOps: []*chronograf.VictorOps{
{},
},
chronograf.KapacitorNode{
Name: "slack",
Email: []*chronograf.Email{
{
To: []string{},
},
},
},
Message: "message",
@ -354,10 +347,14 @@ func TestReverse(t *testing.T) {
|httpOut('output')
`,
want: chronograf.AlertRule{
Name: "haproxy",
Trigger: "threshold",
Alerts: []string{"smtp"},
AlertNodes: []chronograf.KapacitorNode{chronograf.KapacitorNode{Name: "smtp"}},
Name: "haproxy",
Trigger: "threshold",
AlertNodes: chronograf.AlertNodes{
IsStateChangesOnly: true,
Email: []*chronograf.Email{
{To: []string{}},
},
},
TriggerValues: chronograf.TriggerValues{
Operator: "equal to",
Value: "DOWN",
@ -473,10 +470,10 @@ func TestReverse(t *testing.T) {
want: chronograf.AlertRule{
Name: "haproxy",
Trigger: "threshold",
Alerts: []string{"smtp"},
AlertNodes: []chronograf.KapacitorNode{
chronograf.KapacitorNode{
Name: "smtp",
AlertNodes: chronograf.AlertNodes{
IsStateChangesOnly: true,
Email: []*chronograf.Email{
{To: []string{}},
},
},
TriggerValues: chronograf.TriggerValues{
@ -596,11 +593,17 @@ func TestReverse(t *testing.T) {
want: chronograf.AlertRule{
Name: "name",
Trigger: "threshold",
Alerts: []string{"victorops", "smtp", "slack"},
AlertNodes: []chronograf.KapacitorNode{
{Name: "victorops"},
{Name: "smtp"},
{Name: "slack"},
AlertNodes: chronograf.AlertNodes{
IsStateChangesOnly: true,
Slack: []*chronograf.Slack{
{},
},
VictorOps: []*chronograf.VictorOps{
{},
},
Email: []*chronograf.Email{
{To: []string{}},
},
},
TriggerValues: chronograf.TriggerValues{
Operator: "greater than",
@ -727,11 +730,17 @@ func TestReverse(t *testing.T) {
want: chronograf.AlertRule{
Name: "name",
Trigger: "threshold",
Alerts: []string{"victorops", "smtp", "slack"},
AlertNodes: []chronograf.KapacitorNode{
{Name: "victorops"},
{Name: "smtp"},
{Name: "slack"},
AlertNodes: chronograf.AlertNodes{
IsStateChangesOnly: true,
Slack: []*chronograf.Slack{
{},
},
VictorOps: []*chronograf.VictorOps{
{},
},
Email: []*chronograf.Email{
{To: []string{}},
},
},
TriggerValues: chronograf.TriggerValues{
Operator: "inside range",
@ -858,11 +867,17 @@ func TestReverse(t *testing.T) {
want: chronograf.AlertRule{
Name: "name",
Trigger: "threshold",
Alerts: []string{"victorops", "smtp", "slack"},
AlertNodes: []chronograf.KapacitorNode{
{Name: "victorops"},
{Name: "smtp"},
{Name: "slack"},
AlertNodes: chronograf.AlertNodes{
IsStateChangesOnly: true,
Slack: []*chronograf.Slack{
{},
},
VictorOps: []*chronograf.VictorOps{
{},
},
Email: []*chronograf.Email{
{To: []string{}},
},
},
TriggerValues: chronograf.TriggerValues{
Operator: "outside range",
@ -979,11 +994,17 @@ func TestReverse(t *testing.T) {
want: chronograf.AlertRule{
Name: "name",
Trigger: "threshold",
Alerts: []string{"victorops", "smtp", "slack"},
AlertNodes: []chronograf.KapacitorNode{
{Name: "victorops"},
{Name: "smtp"},
{Name: "slack"},
AlertNodes: chronograf.AlertNodes{
IsStateChangesOnly: true,
Slack: []*chronograf.Slack{
{},
},
VictorOps: []*chronograf.VictorOps{
{},
},
Email: []*chronograf.Email{
{To: []string{}},
},
},
TriggerValues: chronograf.TriggerValues{
Operator: "greater than",
@ -1111,11 +1132,17 @@ trigger
want: chronograf.AlertRule{
Name: "name",
Trigger: "relative",
Alerts: []string{"victorops", "smtp", "slack"},
AlertNodes: []chronograf.KapacitorNode{
{Name: "victorops"},
{Name: "smtp"},
{Name: "slack"},
AlertNodes: chronograf.AlertNodes{
IsStateChangesOnly: true,
Slack: []*chronograf.Slack{
{},
},
VictorOps: []*chronograf.VictorOps{
{},
},
Email: []*chronograf.Email{
{To: []string{}},
},
},
TriggerValues: chronograf.TriggerValues{
Change: "% change",
@ -1253,11 +1280,17 @@ trigger
want: chronograf.AlertRule{
Name: "name",
Trigger: "relative",
Alerts: []string{"victorops", "smtp", "slack"},
AlertNodes: []chronograf.KapacitorNode{
{Name: "victorops"},
{Name: "smtp"},
{Name: "slack"},
AlertNodes: chronograf.AlertNodes{
IsStateChangesOnly: true,
Slack: []*chronograf.Slack{
{},
},
VictorOps: []*chronograf.VictorOps{
{},
},
Email: []*chronograf.Email{
{To: []string{}},
},
},
TriggerValues: chronograf.TriggerValues{
Change: "change",
@ -1377,11 +1410,17 @@ trigger
want: chronograf.AlertRule{
Name: "name",
Trigger: "deadman",
Alerts: []string{"victorops", "smtp", "slack"},
AlertNodes: []chronograf.KapacitorNode{
{Name: "victorops"},
{Name: "smtp"},
{Name: "slack"},
AlertNodes: chronograf.AlertNodes{
IsStateChangesOnly: true,
Slack: []*chronograf.Slack{
{},
},
VictorOps: []*chronograf.VictorOps{
{},
},
Email: []*chronograf.Email{
{To: []string{}},
},
},
TriggerValues: chronograf.TriggerValues{
Period: "10m0s",
@ -1480,7 +1519,6 @@ trigger
want: chronograf.AlertRule{
Name: "rule 1",
Trigger: "threshold",
Alerts: []string{},
TriggerValues: chronograf.TriggerValues{
Operator: "greater than",
Value: "90000",
@ -1488,6 +1526,9 @@ trigger
Every: "",
Message: "",
Details: "",
AlertNodes: chronograf.AlertNodes{
IsStateChangesOnly: true,
},
Query: &chronograf.QueryConfig{
Database: "_internal",
RetentionPolicy: "monitor",
@ -1514,7 +1555,7 @@ trigger
return
}
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 got.Query == nil {
t.Errorf("Reverse() = got nil QueryConfig")

View File

@ -131,7 +131,7 @@ func TestClient_All(t *testing.T) {
ID: "howdy",
Name: "howdy",
TICKScript: "",
Type: "unknown TaskType 0",
Type: "invalid",
Status: "enabled",
DBRPs: []chronograf.DBRP{},
},
@ -318,11 +318,13 @@ trigger
|httpOut('output')
`,
Trigger: "threshold",
Alerts: []string{},
TriggerValues: chronograf.TriggerValues{
Operator: "greater than",
Value: "90000",
},
AlertNodes: chronograf.AlertNodes{
IsStateChangesOnly: true,
},
Query: &chronograf.QueryConfig{
Database: "_internal",
RetentionPolicy: "monitor",
@ -647,11 +649,13 @@ trigger
|httpOut('output')
`,
Trigger: "threshold",
Alerts: []string{},
TriggerValues: chronograf.TriggerValues{
Operator: "greater than",
Value: "90000",
},
AlertNodes: chronograf.AlertNodes{
IsStateChangesOnly: true,
},
Query: &chronograf.QueryConfig{
Database: "_internal",
RetentionPolicy: "monitor",
@ -1124,7 +1128,7 @@ func TestClient_Update(t *testing.T) {
},
Trigger: Relative,
TriggerValues: chronograf.TriggerValues{
Operator: InsideRange,
Operator: insideRange,
},
},
},
@ -1289,7 +1293,157 @@ func TestClient_Create(t *testing.T) {
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{
kapaClient: func(url, username, password string, insecureSkipVerify bool) (KapaClient, error) {
return kapa, nil
@ -1344,7 +1498,7 @@ var period = 1d
var name = 'myname\'s'
var idVar = name + ':{{.Group}}'
var idVar = name
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{}

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{
Limit: 100,

View File

@ -7,12 +7,12 @@ import (
const (
greaterThan = "greater than"
lessThan = "less than"
LessThanEqual = "equal to or less than"
GreaterThanEqual = "equal to or greater"
Equal = "equal to"
NotEqual = "not equal to"
InsideRange = "inside range"
OutsideRange = "outside range"
lessThanEqual = "equal to or less than"
greaterThanEqual = "equal to or greater"
equal = "equal to"
notEqual = "not equal to"
insideRange = "inside range"
outsideRange = "outside range"
)
// kapaOperator converts UI strings to kapacitor operators
@ -22,13 +22,13 @@ func kapaOperator(operator string) (string, error) {
return ">", nil
case lessThan:
return "<", nil
case LessThanEqual:
case lessThanEqual:
return "<=", nil
case GreaterThanEqual:
case greaterThanEqual:
return ">=", nil
case Equal:
case equal:
return "==", nil
case NotEqual:
case notEqual:
return "!=", nil
default:
return "", fmt.Errorf("invalid operator: %s is unknown", operator)
@ -42,13 +42,13 @@ func chronoOperator(operator string) (string, error) {
case "<":
return lessThan, nil
case "<=":
return LessThanEqual, nil
return lessThanEqual, nil
case ">=":
return GreaterThanEqual, nil
return greaterThanEqual, nil
case "==":
return Equal, nil
return equal, nil
case "!=":
return NotEqual, nil
return notEqual, nil
default:
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) {
switch operator {
case InsideRange:
case insideRange:
return []string{">=", "AND", "<="}, nil
case OutsideRange:
case outsideRange:
return []string{"<", "OR", ">"}, nil
default:
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")
}
if ops[0] == ">=" && ops[1] == "AND" && ops[2] == "<=" {
return InsideRange, nil
return insideRange, nil
} else if ops[0] == "<" && ops[1] == "OR" && ops[2] == ">" {
return OutsideRange, nil
return outsideRange, nil
}
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{
Name: "name",
Trigger: "relative",
Alerts: []string{"slack", "victorops", "email"},
AlertNodes: chronograf.AlertNodes{
Slack: []*chronograf.Slack{{}},
VictorOps: []*chronograf.VictorOps{{}},
Email: []*chronograf.Email{{}},
},
TriggerValues: chronograf.TriggerValues{
Change: "change",
Shift: "1m",
@ -65,7 +69,11 @@ func TestThreshold(t *testing.T) {
alert := chronograf.AlertRule{
Name: "name",
Trigger: "threshold",
Alerts: []string{"slack", "victorops", "email"},
AlertNodes: chronograf.AlertNodes{
Slack: []*chronograf.Slack{{}},
VictorOps: []*chronograf.VictorOps{{}},
Email: []*chronograf.Email{{}},
},
TriggerValues: chronograf.TriggerValues{
Operator: "greater than",
Value: "90",
@ -176,9 +184,9 @@ var trigger = data
.levelTag(levelTag)
.messageField(messageField)
.durationField(durationField)
.slack()
.victorOps()
.email()
.victorOps()
.slack()
trigger
|eval(lambda: float("value"))
@ -217,7 +225,9 @@ func TestThresholdStringCrit(t *testing.T) {
alert := chronograf.AlertRule{
Name: "haproxy",
Trigger: "threshold",
Alerts: []string{"email"},
AlertNodes: chronograf.AlertNodes{
Email: []*chronograf.Email{{}},
},
TriggerValues: chronograf.TriggerValues{
Operator: "equal to",
Value: "DOWN",
@ -364,7 +374,9 @@ func TestThresholdStringCritGreater(t *testing.T) {
alert := chronograf.AlertRule{
Name: "haproxy",
Trigger: "threshold",
Alerts: []string{"email"},
AlertNodes: chronograf.AlertNodes{
Email: []*chronograf.Email{{}},
},
TriggerValues: chronograf.TriggerValues{
Operator: "greater than",
Value: "DOWN",
@ -509,7 +521,11 @@ func TestThresholdDetail(t *testing.T) {
alert := chronograf.AlertRule{
Name: "name",
Trigger: "threshold",
Alerts: []string{"slack", "victorops", "email"},
AlertNodes: chronograf.AlertNodes{
Slack: []*chronograf.Slack{{}},
VictorOps: []*chronograf.VictorOps{{}},
Email: []*chronograf.Email{{}},
},
TriggerValues: chronograf.TriggerValues{
Operator: "greater than",
Value: "90",
@ -624,9 +640,9 @@ var trigger = data
.messageField(messageField)
.durationField(durationField)
.details(details)
.slack()
.victorOps()
.email()
.victorOps()
.slack()
trigger
|eval(lambda: float("value"))
@ -665,7 +681,11 @@ func TestThresholdInsideRange(t *testing.T) {
alert := chronograf.AlertRule{
Name: "name",
Trigger: "threshold",
Alerts: []string{"slack", "victorops", "email"},
AlertNodes: chronograf.AlertNodes{
Slack: []*chronograf.Slack{{}},
VictorOps: []*chronograf.VictorOps{{}},
Email: []*chronograf.Email{{}},
},
TriggerValues: chronograf.TriggerValues{
Operator: "inside range",
Value: "90",
@ -779,9 +799,9 @@ var trigger = data
.levelTag(levelTag)
.messageField(messageField)
.durationField(durationField)
.slack()
.victorOps()
.email()
.victorOps()
.slack()
trigger
|eval(lambda: float("value"))
@ -820,7 +840,11 @@ func TestThresholdOutsideRange(t *testing.T) {
alert := chronograf.AlertRule{
Name: "name",
Trigger: "threshold",
Alerts: []string{"slack", "victorops", "email"},
AlertNodes: chronograf.AlertNodes{
Slack: []*chronograf.Slack{{}},
VictorOps: []*chronograf.VictorOps{{}},
Email: []*chronograf.Email{{}},
},
TriggerValues: chronograf.TriggerValues{
Operator: "outside range",
Value: "90",
@ -934,9 +958,9 @@ var trigger = data
.levelTag(levelTag)
.messageField(messageField)
.durationField(durationField)
.slack()
.victorOps()
.email()
.victorOps()
.slack()
trigger
|eval(lambda: float("value"))
@ -975,7 +999,11 @@ func TestThresholdNoAggregate(t *testing.T) {
alert := chronograf.AlertRule{
Name: "name",
Trigger: "threshold",
Alerts: []string{"slack", "victorops", "email"},
AlertNodes: chronograf.AlertNodes{
Slack: []*chronograf.Slack{{}},
VictorOps: []*chronograf.VictorOps{{}},
Email: []*chronograf.Email{{}},
},
TriggerValues: chronograf.TriggerValues{
Operator: "greater than",
Value: "90",
@ -1072,9 +1100,9 @@ var trigger = data
.levelTag(levelTag)
.messageField(messageField)
.durationField(durationField)
.slack()
.victorOps()
.email()
.victorOps()
.slack()
trigger
|eval(lambda: float("value"))
@ -1113,7 +1141,11 @@ func TestRelative(t *testing.T) {
alert := chronograf.AlertRule{
Name: "name",
Trigger: "relative",
Alerts: []string{"slack", "victorops", "email"},
AlertNodes: chronograf.AlertNodes{
Slack: []*chronograf.Slack{{}},
VictorOps: []*chronograf.VictorOps{{}},
Email: []*chronograf.Email{{}},
},
TriggerValues: chronograf.TriggerValues{
Change: "% change",
Shift: "1m",
@ -1238,9 +1270,9 @@ var trigger = past
.levelTag(levelTag)
.messageField(messageField)
.durationField(durationField)
.slack()
.victorOps()
.email()
.victorOps()
.slack()
trigger
|eval(lambda: float("value"))
@ -1279,7 +1311,11 @@ func TestRelativeChange(t *testing.T) {
alert := chronograf.AlertRule{
Name: "name",
Trigger: "relative",
Alerts: []string{"slack", "victorops", "email"},
AlertNodes: chronograf.AlertNodes{
Slack: []*chronograf.Slack{{}},
VictorOps: []*chronograf.VictorOps{{}},
Email: []*chronograf.Email{{}},
},
TriggerValues: chronograf.TriggerValues{
Change: "change",
Shift: "1m",
@ -1404,9 +1440,9 @@ var trigger = past
.levelTag(levelTag)
.messageField(messageField)
.durationField(durationField)
.slack()
.victorOps()
.email()
.victorOps()
.slack()
trigger
|eval(lambda: float("value"))
@ -1445,7 +1481,11 @@ func TestDeadman(t *testing.T) {
alert := chronograf.AlertRule{
Name: "name",
Trigger: "deadman",
Alerts: []string{"slack", "victorops", "email"},
AlertNodes: chronograf.AlertNodes{
Slack: []*chronograf.Slack{{}},
VictorOps: []*chronograf.VictorOps{{}},
Email: []*chronograf.Email{{}},
},
TriggerValues: chronograf.TriggerValues{
Period: "10m",
},
@ -1546,9 +1586,9 @@ var trigger = data
.levelTag(levelTag)
.messageField(messageField)
.durationField(durationField)
.slack()
.victorOps()
.email()
.victorOps()
.slack()
trigger
|eval(lambda: "emitted")

View File

@ -3,6 +3,7 @@ package kapacitor
import (
"bytes"
"fmt"
"strings"
"time"
"github.com/influxdata/chronograf"
@ -33,10 +34,19 @@ func formatTick(tickscript string) (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()
predefinedVars := map[string]tick.Var{}
_, err := pipeline.CreatePipeline(string(script), pipeline.StreamEdge, scope, &deadman{}, predefinedVars)
return err
return pipeline.CreatePipeline(string(script), edge, scope, &deadman{}, predefinedVars)
}
// 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
var name = '%s'
var idVar = name + ':{{.Group}}'
var idVar = %s
var message = '%s'
var idTag = '%s'
var levelTag = '%s'
@ -143,6 +143,7 @@ func commonVars(rule chronograf.AlertRule) (string, error) {
whereFilter(rule.Query),
wind,
Escape(rule.Name),
idVar(rule.Query),
Escape(rule.Message),
IDTag,
LevelTag,
@ -197,6 +198,13 @@ func groupBy(q *chronograf.QueryConfig) string {
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) {
if q == nil {
return "", fmt.Errorf("No fields set in query")

View File

@ -1,11 +1,14 @@
package server
import (
"crypto/tls"
"encoding/json"
"fmt"
"net"
"net/http"
"net/http/httputil"
"net/url"
"time"
"github.com/influxdata/chronograf"
"github.com/influxdata/chronograf/influx"
@ -111,8 +114,29 @@ func (s *Service) Write(w http.ResponseWriter, r *http.Request) {
auth := influx.DefaultAuthorization(&src)
auth.Set(req)
}
proxy := &httputil.ReverseProxy{
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)
}

View File

@ -328,7 +328,7 @@ func (s *Service) KapacitorRulesPost(w http.ResponseWriter, r *http.Request) {
var req chronograf.AlertRule
if err = json.NewDecoder(r.Body).Decode(&req); err != nil {
invalidJSON(w, s.Logger)
invalidData(w, err, s.Logger)
return
}
// TODO: validate this data
@ -341,7 +341,7 @@ func (s *Service) KapacitorRulesPost(w http.ResponseWriter, r *http.Request) {
task, err := c.Create(ctx, req)
if err != nil {
Error(w, http.StatusInternalServerError, err.Error(), s.Logger)
invalidData(w, err, s.Logger)
return
}
res := newAlertResponse(task, srv.SrcID, srv.ID)
@ -371,26 +371,111 @@ func newAlertResponse(task *kapa.Task, srcID, kapaID int) *alertResponse {
},
}
if res.Alerts == nil {
res.Alerts = make([]string, 0)
if res.AlertNodes.Alerta == nil {
res.AlertNodes.Alerta = []*chronograf.Alerta{}
}
if res.AlertNodes == nil {
res.AlertNodes = make([]chronograf.KapacitorNode, 0)
for i, a := range res.AlertNodes.Alerta {
if a.Service == nil {
a.Service = []string{}
res.AlertNodes.Alerta[i] = a
}
}
for _, n := range res.AlertNodes {
if n.Args == nil {
n.Args = make([]string, 0)
if res.AlertNodes.Email == nil {
res.AlertNodes.Email = []*chronograf.Email{}
}
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 {
@ -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)
var req chronograf.AlertRule
if err = json.NewDecoder(r.Body).Decode(&req); err != nil {
invalidJSON(w, s.Logger)
invalidData(w, err, s.Logger)
return
}
// TODO: validate this data
@ -482,7 +567,7 @@ func (s *Service) KapacitorRulesPut(w http.ResponseWriter, r *http.Request) {
req.ID = tid
task, err := c.Update(ctx, c.Href(tid), req)
if err != nil {
Error(w, http.StatusInternalServerError, err.Error(), s.Logger)
invalidData(w, err, s.Logger)
return
}
res := newAlertResponse(task, srv.SrcID, srv.ID)

View File

@ -94,9 +94,9 @@ func Test_KapacitorRulesGet(t *testing.T) {
expected []chronograf.AlertRule
}{
{
"basic",
"/chronograf/v1/sources/1/kapacitors/1/rules",
[]chronograf.AlertRule{
name: "basic",
requestPath: "/chronograf/v1/sources/1/kapacitors/1/rules",
mockAlerts: []chronograf.AlertRule{
{
ID: "cpu_alert",
Name: "cpu_alert",
@ -106,15 +106,31 @@ func Test_KapacitorRulesGet(t *testing.T) {
TICKScript: tickScript,
},
},
[]chronograf.AlertRule{
expected: []chronograf.AlertRule{
{
ID: "cpu_alert",
Name: "cpu_alert",
Status: "enabled",
Type: "stream",
DBRPs: []chronograf.DBRP{{DB: "telegraf", RP: "autogen"}},
Alerts: []string{},
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
import (
"crypto/tls"
"fmt"
"net"
"net/http"
"net/http/httputil"
"net/url"
@ -64,6 +66,25 @@ func (s *Service) KapacitorProxy(w http.ResponseWriter, r *http.Request) {
Director: director,
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)
}

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"`
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"`
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"`
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"`
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"`
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"`
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"`
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"`
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{
Logger: logger,
ID: idgen.NewTime(),
Path: s.CannedPath,
Path: s.ResourcesPath,
},
Sources: &MultiSourceBuilder{
InfluxDBURL: s.InfluxDBURL,
@ -297,7 +298,7 @@ func (s *Server) newBuilders(logger chronograf.Logger) builders {
InfluxDBPassword: s.InfluxDBPassword,
Logger: logger,
ID: idgen.NewTime(),
Path: s.CannedPath,
Path: s.ResourcesPath,
},
Kapacitors: &MultiKapacitorBuilder{
KapacitorURL: s.KapacitorURL,
@ -305,11 +306,11 @@ func (s *Server) newBuilders(logger chronograf.Logger) builders {
KapacitorPassword: s.KapacitorPassword,
Logger: logger,
ID: idgen.NewTime(),
Path: s.CannedPath,
Path: s.ResourcesPath,
},
Organizations: &MultiOrganizationBuilder{
Logger: logger,
Path: s.CannedPath,
Path: s.ResourcesPath,
},
}
}

View File

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

View File

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

View File

@ -1,6 +1,5 @@
import reducer from 'src/kapacitor/reducers/rules'
import {defaultRuleConfigs} from 'src/kapacitor/constants'
import {ALERT_NODES_ACCESSORS} from 'src/kapacitor/constants'
import {
chooseTrigger,
@ -9,9 +8,7 @@ import {
updateRuleValues,
updateDetails,
updateMessage,
updateAlerts,
updateAlertNodes,
updateAlertProperty,
updateRuleName,
deleteRuleSuccess,
updateRuleStatusSuccess,
@ -100,56 +97,33 @@ describe('Kapacitor.Reducers.rules', () => {
expect(newState[ruleID].message).to.equal(message)
})
it('can update the alerts', () => {
it('can update a slack alert', () => {
const ruleID = 1
const initialState = {
[ruleID]: {
id: ruleID,
queryID: 988,
alerts: [],
alertNodes: {},
},
}
const alerts = ['slack']
const newState = reducer(initialState, updateAlerts(ruleID, alerts))
expect(newState[ruleID].alerts).to.equal(alerts)
})
it('can update an alerta alert', () => {
const ruleID = 1
const initialState = {
[ruleID]: {
id: ruleID,
queryID: 988,
alerts: [],
alertNodes: [],
},
const updatedSlack = {
alias: 'slack-1',
username: 'testname',
iconEmoji: 'testemoji',
enabled: true,
text: 'slack',
type: 'slack',
url: true,
}
const tickScript = `stream
|alert()
.alerta()
.resource('Hostname or service')
.event('Something went wrong')
.environment('Development')
.group('Dev. Servers')
.services('a b c')
`
let newState = reducer(
const expectedSlack = {
username: 'testname',
iconEmoji: 'testemoji',
}
const newState = reducer(
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')`
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)
expect(newState[ruleID].alertNodes.slack[0]).to.deep.equal(expectedSlack)
})
it('can update the name', () => {
@ -201,106 +175,6 @@ describe('Kapacitor.Reducers.rules', () => {
expect(newState[ruleID].details).to.equal(details)
})
it('can update properties', () => {
const ruleID = 1
const alertNodeName = 'pushover'
const alertProperty1_Name = 'device'
const alertProperty1_ArgsOrig =
'pineapple_kingdom_control_room,bob_cOreos_watch'
const alertProperty1_ArgsDiff = 'pineapple_kingdom_control_tower'
const alertProperty2_Name = 'URLTitle'
const alertProperty2_ArgsOrig = 'Cubeapple Rising'
const alertProperty2_ArgsDiff = 'Cubeapple Falling'
const alertProperty1_Orig = {
name: alertProperty1_Name,
args: [alertProperty1_ArgsOrig],
}
const alertProperty1_Diff = {
name: alertProperty1_Name,
args: [alertProperty1_ArgsDiff],
}
const alertProperty2_Orig = {
name: alertProperty2_Name,
args: [alertProperty2_ArgsOrig],
}
const alertProperty2_Diff = {
name: alertProperty2_Name,
args: [alertProperty2_ArgsDiff],
}
const initialState = {
[ruleID]: {
id: ruleID,
alertNodes: [
{
name: 'pushover',
args: null,
properties: null,
},
],
},
}
const getAlertPropertyArgs = (matchState, propertyName) =>
matchState[ruleID].alertNodes
.find(node => node.name === alertNodeName)
.properties.find(property => property.name === propertyName).args[0]
// add first property
let newState = reducer(
initialState,
updateAlertProperty(ruleID, alertNodeName, alertProperty1_Orig)
)
expect(getAlertPropertyArgs(newState, alertProperty1_Name)).to.equal(
alertProperty1_ArgsOrig
)
// change first property
newState = reducer(
initialState,
updateAlertProperty(ruleID, alertNodeName, alertProperty1_Diff)
)
expect(getAlertPropertyArgs(newState, alertProperty1_Name)).to.equal(
alertProperty1_ArgsDiff
)
// add second property
newState = reducer(
initialState,
updateAlertProperty(ruleID, alertNodeName, alertProperty2_Orig)
)
expect(getAlertPropertyArgs(newState, alertProperty1_Name)).to.equal(
alertProperty1_ArgsDiff
)
expect(getAlertPropertyArgs(newState, alertProperty2_Name)).to.equal(
alertProperty2_ArgsOrig
)
expect(
newState[ruleID].alertNodes.find(node => node.name === alertNodeName)
.properties.length
).to.equal(2)
// change second property
newState = reducer(
initialState,
updateAlertProperty(ruleID, alertNodeName, alertProperty2_Diff)
)
expect(getAlertPropertyArgs(newState, alertProperty1_Name)).to.equal(
alertProperty1_ArgsDiff
)
expect(getAlertPropertyArgs(newState, alertProperty2_Name)).to.equal(
alertProperty2_ArgsDiff
)
expect(
newState[ruleID].alertNodes.find(node => node.name === alertNodeName)
.properties.length
).to.equal(2)
})
it('can update status', () => {
const ruleID = 1
const status = 'enabled'

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 {connect} from 'react-redux'
import {bindActionCreators} from 'redux'
import _ from 'lodash'
import HostsTable from 'src/hosts/components/HostsTable'
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 {getEnv} from 'src/shared/apis/env'
import {setAutoRefresh} from 'shared/actions/app'
class HostsPage extends Component {
constructor(props) {
@ -19,59 +23,26 @@ class HostsPage extends Component {
}
}
async componentDidMount() {
async fetchHostsData() {
const {source, links, addFlashMessage} = this.props
const {telegrafSystemInterval} = await getEnv(links.environment)
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,
})
}
const hostsError = 'Unable to get hosts'
try {
const hosts = await getCpuAndLoadForHosts(
source.links.proxy,
source.telegraf,
telegrafSystemInterval
)
if (!hosts) {
throw new Error(hostsError)
}
const newHosts = await getAppsForHosts(
source.links.proxy,
hosts,
layouts,
this.layouts,
source.telegraf
)
this.setState({
hosts: newHosts,
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() {
const {source} = this.props
const {
source,
autoRefresh,
onChooseAutoRefresh,
onManualRefresh,
} = this.props
const {hosts, hostsLoading, hostsError} = this.state
return (
<div className="page hosts-list-page">
@ -99,6 +112,12 @@ class HostsPage extends Component {
</div>
<div className="page-header__right">
<SourceIndicator />
<AutoRefreshDropdown
iconName="refresh"
selected={autoRefresh}
onChoose={onChooseAutoRefresh}
onManualRefresh={onManualRefresh}
/>
</div>
</div>
</div>
@ -119,13 +138,20 @@ class HostsPage extends Component {
</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 {
links,
autoRefresh,
}
}
@ -143,6 +169,20 @@ HostsPage.propTypes = {
environment: string.isRequired,
}),
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="kapacitors/new" 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="admin-chronograf" component={AdminChronografPage} />
<Route path="admin-influxdb" component={AdminInfluxDBPage} />

View File

@ -136,31 +136,12 @@ export const updateDetails = (ruleID, details) => ({
},
})
export const updateAlertProperty = (ruleID, alertNodeName, alertProperty) => ({
type: 'UPDATE_RULE_ALERT_PROPERTY',
payload: {
ruleID,
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 function updateAlertNodes(ruleID, alerts) {
return {
type: 'UPDATE_RULE_ALERT_NODES',
payload: {ruleID, alerts},
}
}
export const updateRuleName = (ruleID, name) => ({
type: 'UPDATE_RULE_NAME',

View File

@ -2,7 +2,11 @@ import React, {Component, PropTypes} from 'react'
import _ from 'lodash'
import {Tab, Tabs, TabPanel, TabPanels, TabList} from 'shared/components/Tabs'
import {getKapacitorConfig, updateKapacitorConfigSection} from 'shared/apis'
import {
getKapacitorConfig,
updateKapacitorConfigSection,
testAlertOutput,
} from 'shared/apis'
import {
AlertaConfig,
@ -23,7 +27,6 @@ class AlertTabs extends Component {
super(props)
this.state = {
selectedEndpoint: 'smtp',
configSections: null,
}
}
@ -38,45 +41,72 @@ class AlertTabs extends Component {
}
}
refreshKapacitorConfig = kapacitor => {
getKapacitorConfig(kapacitor)
.then(({data: {sections}}) => {
this.setState({configSections: sections})
})
.catch(() => {
this.setState({configSections: null})
this.props.addFlashMessage({
type: 'error',
text: 'There was an error getting the Kapacitor config',
})
refreshKapacitorConfig = async kapacitor => {
try {
const {data: {sections}} = await getKapacitorConfig(kapacitor)
this.setState({configSections: sections})
} catch (error) {
this.setState({configSections: null})
this.props.addFlashMessage({
type: 'error',
text: 'There was an error getting the Kapacitor config',
})
}
}
getSection = (sections, section) => {
return _.get(sections, [section, 'elements', '0'], null)
}
getEnabled = (sections, section) => {
return _.get(
sections,
[section, 'elements', '0', 'options', 'enabled'],
null
)
}
handleGetSection = (sections, section) => () => {
return this.getSection(sections, section)
}
handleSaveConfig = section => properties => {
handleSaveConfig = section => async properties => {
if (section !== '') {
const propsToSend = this.sanitizeProperties(section, properties)
updateKapacitorConfigSection(this.props.kapacitor, section, propsToSend)
.then(() => {
this.refreshKapacitorConfig(this.props.kapacitor)
this.props.addFlashMessage({
type: 'success',
text: `Alert for ${section} successfully saved`,
})
try {
await updateKapacitorConfigSection(
this.props.kapacitor,
section,
propsToSend
)
this.refreshKapacitorConfig(this.props.kapacitor)
this.props.addFlashMessage({
type: 'success',
text: `Alert configuration for ${section} successfully saved.`,
})
.catch(() => {
this.props.addFlashMessage({
type: 'error',
text: 'There was an error saving the kapacitor config',
})
} catch (error) {
this.props.addFlashMessage({
type: 'error',
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
}
getInitialIndex = (supportedConfigs, hash) => {
const index = _.indexOf(_.keys(supportedConfigs), _.replace(hash, '#', ''))
return index >= 0 ? index : 0
}
render() {
const {configSections} = this.state
const {hash} = this.props
if (!configSections) {
return null
}
const supportedConfigs = {
alerta: {
type: 'Alerta',
enabled: this.getEnabled(configSections, 'alerta'),
renderComponent: () =>
<AlertaConfig
onSave={this.handleSaveConfig('alerta')}
config={this.getSection(configSections, 'alerta')}
onTest={this.handleTestConfig('alerta')}
enabled={this.getEnabled(configSections, 'alerta')}
/>,
},
hipchat: {
type: 'HipChat',
enabled: this.getEnabled(configSections, 'hipchat'),
renderComponent: () =>
<HipChatConfig
onSave={this.handleSaveConfig('hipchat')}
config={this.getSection(configSections, 'hipchat')}
onTest={this.handleTestConfig('hipchat')}
enabled={this.getEnabled(configSections, 'hipchat')}
/>,
},
opsgenie: {
type: 'OpsGenie',
enabled: this.getEnabled(configSections, 'opsgenie'),
renderComponent: () =>
<OpsGenieConfig
onSave={this.handleSaveConfig('opsgenie')}
config={this.getSection(configSections, 'opsgenie')}
onTest={this.handleTestConfig('opsgenie')}
enabled={this.getEnabled(configSections, 'opsgenie')}
/>,
},
pagerduty: {
type: 'PagerDuty',
enabled: this.getEnabled(configSections, 'pagerduty'),
renderComponent: () =>
<PagerDutyConfig
onSave={this.handleSaveConfig('pagerduty')}
config={this.getSection(configSections, 'pagerduty')}
onTest={this.handleTestConfig('pagerduty')}
enabled={this.getEnabled(configSections, 'pagerduty')}
/>,
},
pushover: {
type: 'Pushover',
enabled: this.getEnabled(configSections, 'pushover'),
renderComponent: () =>
<PushoverConfig
onSave={this.handleSaveConfig('pushover')}
config={this.getSection(configSections, 'pushover')}
onTest={this.handleTestConfig('pushover')}
enabled={this.getEnabled(configSections, 'pushover')}
/>,
},
sensu: {
type: 'Sensu',
enabled: this.getEnabled(configSections, 'sensu'),
renderComponent: () =>
<SensuConfig
onSave={this.handleSaveConfig('sensu')}
config={this.getSection(configSections, 'sensu')}
onTest={this.handleTestConfig('sensu')}
enabled={this.getEnabled(configSections, 'sensu')}
/>,
},
slack: {
type: 'Slack',
enabled: this.getEnabled(configSections, 'slack'),
renderComponent: () =>
<SlackConfig
onSave={this.handleSaveConfig('slack')}
config={this.getSection(configSections, 'slack')}
onTest={this.handleTestConfig('slack')}
enabled={this.getEnabled(configSections, 'slack')}
/>,
},
smtp: {
type: 'SMTP',
enabled: this.getEnabled(configSections, 'smtp'),
renderComponent: () =>
<SMTPConfig
onSave={this.handleSaveConfig('smtp')}
config={this.getSection(configSections, 'smtp')}
onTest={this.handleTestConfig('smtp')}
enabled={this.getEnabled(configSections, 'smtp')}
/>,
},
talk: {
type: 'Talk',
enabled: this.getEnabled(configSections, 'talk'),
renderComponent: () =>
<TalkConfig
onSave={this.handleSaveConfig('talk')}
config={this.getSection(configSections, 'talk')}
onTest={this.handleTestConfig('talk')}
enabled={this.getEnabled(configSections, 'talk')}
/>,
},
telegram: {
type: 'Telegram',
enabled: this.getEnabled(configSections, 'telegram'),
renderComponent: () =>
<TelegramConfig
onSave={this.handleSaveConfig('telegram')}
config={this.getSection(configSections, 'telegram')}
onTest={this.handleTestConfig('telegram')}
enabled={this.getEnabled(configSections, 'telegram')}
/>,
},
victorops: {
type: 'VictorOps',
enabled: this.getEnabled(configSections, 'victorops'),
renderComponent: () =>
<VictorOpsConfig
onSave={this.handleSaveConfig('victorops')}
config={this.getSection(configSections, 'victorops')}
onTest={this.handleTestConfig('victorops')}
enabled={this.getEnabled(configSections, 'victorops')}
/>,
},
}
return (
<div>
<div className="panel panel-minimal">
@ -200,14 +267,20 @@ class AlertTabs extends Component {
</div>
</div>
<Tabs tabContentsClass="config-endpoint">
<Tabs
tabContentsClass="config-endpoint"
initialIndex={this.getInitialIndex(supportedConfigs, hash)}
>
<TabList customClass="config-endpoint--tabs">
{_.reduce(
configSections,
(acc, _cur, k) =>
supportedConfigs[k]
? acc.concat(
<Tab key={supportedConfigs[k].type}>
<Tab
key={supportedConfigs[k].type}
isConfigured={supportedConfigs[k].enabled}
>
{supportedConfigs[k].type}
</Tab>
)
@ -248,6 +321,7 @@ AlertTabs.propTypes = {
}).isRequired,
}),
addFlashMessage: func.isRequired,
hash: string.isRequired,
}
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 {
render() {
const {onInputChange, onReset, kapacitor, onSubmit, exists} = this.props
const {url, name, username, password} = kapacitor
const {url: kapaUrl, name, username, password} = kapacitor
return (
<div className="page">
<div className="page-header">
<div className="page-header__container">
<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>
@ -29,13 +30,13 @@ class KapacitorForm extends Component {
<form onSubmit={onSubmit}>
<div>
<div className="form-group">
<label htmlFor="url">Kapacitor URL</label>
<label htmlFor="kapaUrl">Kapacitor URL</label>
<input
className="form-control"
id="url"
name="url"
placeholder={url}
value={url}
id="kapaUrl"
name="kapaUrl"
placeholder={kapaUrl}
value={kapaUrl}
onChange={onInputChange}
spellCheck="false"
/>
@ -60,7 +61,7 @@ class KapacitorForm extends Component {
id="username"
name="username"
placeholder="username"
value={username}
value={username || ''}
onChange={onInputChange}
spellCheck="false"
/>
@ -73,7 +74,7 @@ class KapacitorForm extends Component {
type="password"
name="password"
placeholder="password"
value={password}
value={password || ''}
onChange={onInputChange}
spellCheck="false"
/>
@ -108,7 +109,7 @@ class KapacitorForm extends Component {
// TODO: move these to another page. they dont belong on this page
renderAlertOutputs() {
const {exists, kapacitor, addFlashMessage, source} = this.props
const {exists, kapacitor, addFlashMessage, source, hash} = this.props
if (exists) {
return (
@ -116,6 +117,7 @@ class KapacitorForm extends Component {
source={source}
kapacitor={kapacitor}
addFlashMessage={addFlashMessage}
hash={hash}
/>
)
}
@ -153,6 +155,7 @@ KapacitorForm.propTypes = {
source: shape({}).isRequired,
addFlashMessage: func.isRequired,
exists: bool.isRequired,
hash: string.isRequired,
}
export default KapacitorForm

View File

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

View File

@ -59,7 +59,7 @@ const KapacitorRules = ({
className="btn btn-sm btn-primary"
style={{marginRight: '4px'}}
>
<span className="icon plus" /> Build Rule
<span className="icon plus" /> Build Alert Rule
</Link>
</div>
</div>
@ -80,9 +80,10 @@ const KapacitorRules = ({
<div className="u-flex u-ai-center u-jc-space-between">
<Link
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>
</div>
</div>

View File

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

View File

@ -1,4 +1,5 @@
import React, {Component, PropTypes} from 'react'
import {DEFAULT_RULE_ID} from 'src/kapacitor/constants'
class NameSection extends Component {
constructor(props) {
@ -10,9 +11,9 @@ class NameSection extends Component {
}
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})
}
@ -27,13 +28,13 @@ class NameSection extends Component {
}
render() {
const {isEditing, defaultName} = this.props
const {rule, defaultName} = this.props
const {reset} = this.state
return (
<div className="rule-section">
<h3 className="rule-section--heading">
{isEditing ? 'Name' : 'Name this Alert Rule'}
{rule.id === DEFAULT_RULE_ID ? 'Name this Alert Rule' : 'Name'}
</h3>
<div className="rule-section--body">
<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 = {
isEditing: bool,
defaultName: string.isRequired,
onRuleRename: func.isRequired,
ruleID: string.isRequired,
rule: shape({}).isRequired,
}
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 _ from 'lodash'
import classnames from 'classnames'
import RuleMessageOptions from 'src/kapacitor/components/RuleMessageOptions'
import RuleMessageText from 'src/kapacitor/components/RuleMessageText'
import RuleMessageTemplates from 'src/kapacitor/components/RuleMessageTemplates'
import {DEFAULT_ALERTS, RULE_ALERT_OPTIONS} from 'src/kapacitor/constants'
class RuleMessage extends Component {
constructor(props) {
super(props)
this.state = {
selectedAlertNodeName: null,
}
}
handleChangeMessage = e => {
const {actions, rule} = this.props
actions.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})
const {ruleActions, rule} = this.props
ruleActions.updateMessage(rule.id, e.target.value)
}
render() {
const {rule, actions, enabledAlerts} = 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
const {rule, ruleActions} = this.props
return (
<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--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
rule={rule}
updateMessage={this.handleChangeMessage}
/>
<RuleMessageTemplates
rule={rule}
updateMessage={actions.updateMessage}
updateMessage={ruleActions.updateMessage}
/>
</div>
</div>
@ -88,17 +34,13 @@ class RuleMessage extends Component {
}
}
const {arrayOf, func, shape, string} = PropTypes
const {func, shape} = PropTypes
RuleMessage.propTypes = {
rule: shape({}).isRequired,
actions: shape({
updateAlertNodes: func.isRequired,
rule: shape().isRequired,
ruleActions: shape({
updateMessage: func.isRequired,
updateDetails: func.isRequired,
updateAlertProperty: func.isRequired,
}).isRequired,
enabledAlerts: arrayOf(string.isRequired).isRequired,
}
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() {
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>
{_.map(RULE_MESSAGE_TEMPLATES, (template, key) => {
return (

View File

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

View File

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

View File

@ -12,10 +12,11 @@ class OpsGenieConfig extends Component {
this.state = {
currentTeams: teams || [],
currentRecipients: recipients || [],
testEnabled: this.props.enabled,
}
}
handleSaveAlert = e => {
handleSubmit = e => {
e.preventDefault()
const properties = {
@ -25,6 +26,11 @@ class OpsGenieConfig extends Component {
}
this.props.onSave(properties)
this.setState({testEnabled: true})
}
disableTest = () => {
this.setState({testEnabled: false})
}
handleAddTeam = team => {
@ -59,18 +65,15 @@ class OpsGenieConfig extends Component {
const {currentTeams, currentRecipients} = this.state
return (
<form onSubmit={this.handleSaveAlert}>
<form onSubmit={this.handleSubmit}>
<div className="form-group col-xs-12">
<label htmlFor="api-key">API Key</label>
<RedactedInput
defaultValue={apiKey}
id="api-key"
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>
<TagInput
@ -78,17 +81,32 @@ class OpsGenieConfig extends Component {
onAddTag={this.handleAddTeam}
onDeleteTag={this.handleDeleteTeam}
tags={currentTeams}
disableTest={this.disableTest}
/>
<TagInput
title="Recipients"
onAddTag={this.handleAddRecipient}
onDeleteTag={this.handleDeleteRecipient}
tags={currentRecipients}
disableTest={this.disableTest}
/>
<div className="form-group-submit col-xs-12 text-center">
<button className="btn btn-primary" type="submit">
Update OpsGenie Config
<button
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>
</div>
</form>
@ -107,6 +125,8 @@ OpsGenieConfig.propTypes = {
}).isRequired,
}).isRequired,
onSave: func.isRequired,
onTest: func.isRequired,
enabled: bool.isRequired,
}
export default OpsGenieConfig

View File

@ -1,11 +1,15 @@
import React, {PropTypes, Component} from 'react'
import RedactedInput from './RedactedInput'
class PagerDutyConfig extends Component {
constructor(props) {
super(props)
this.state = {
testEnabled: this.props.enabled,
}
}
handleSaveAlert = e => {
handleSubmit = e => {
e.preventDefault()
const properties = {
@ -14,28 +18,28 @@ class PagerDutyConfig extends Component {
}
this.props.onSave(properties)
this.setState({testEnabled: true})
}
disableTest = () => {
this.setState({testEnabled: false})
}
render() {
const {options} = this.props.config
const {url} = options
const serviceKey = options['service-key']
const refFunc = r => (this.serviceKey = r)
return (
<form onSubmit={this.handleSaveAlert}>
<form onSubmit={this.handleSubmit}>
<div className="form-group col-xs-12">
<label htmlFor="service-key">Service Key</label>
<input
className="form-control"
id="service-key"
type="text"
ref={r => (this.serviceKey = r)}
<RedactedInput
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 className="form-group col-xs-12">
@ -46,12 +50,26 @@ class PagerDutyConfig extends Component {
type="text"
ref={r => (this.url = r)}
defaultValue={url || ''}
onChange={this.disableTest}
/>
</div>
<div className="form-group-submit col-xs-12 text-center">
<button className="btn btn-primary" type="submit">
Update PagerDuty Config
<button
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>
</div>
</form>
@ -69,6 +87,8 @@ PagerDutyConfig.propTypes = {
}).isRequired,
}).isRequired,
onSave: func.isRequired,
onTest: func.isRequired,
enabled: bool.isRequired,
}
export default PagerDutyConfig

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -5,9 +5,12 @@ import RedactedInput from './RedactedInput'
class VictorOpsConfig extends Component {
constructor(props) {
super(props)
this.state = {
testEnabled: this.props.enabled,
}
}
handleSaveAlert = e => {
handleSubmit = e => {
e.preventDefault()
const properties = {
@ -17,6 +20,11 @@ class VictorOpsConfig extends Component {
}
this.props.onSave(properties)
this.setState({testEnabled: true})
}
disableTest = () => {
this.setState({testEnabled: false})
}
handleApiRef = r => (this.apiKey = r)
@ -28,13 +36,14 @@ class VictorOpsConfig extends Component {
const {url} = options
return (
<form onSubmit={this.handleSaveAlert}>
<form onSubmit={this.handleSubmit}>
<div className="form-group col-xs-12">
<label htmlFor="api-key">API Key</label>
<RedactedInput
defaultValue={apiKey}
id="api-key"
refFunc={this.handleApiRef}
disableTest={this.disableTest}
/>
</div>
@ -46,6 +55,7 @@ class VictorOpsConfig extends Component {
type="text"
ref={r => (this.routingKey = r)}
defaultValue={routingKey || ''}
onChange={this.disableTest}
/>
</div>
@ -57,12 +67,26 @@ class VictorOpsConfig extends Component {
type="text"
ref={r => (this.url = r)}
defaultValue={url || ''}
onChange={this.disableTest}
/>
</div>
<div className="form-group-submit col-xs-12 text-center">
<button className="btn btn-primary" type="submit">
Update VictorOps Config
<button
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>
</div>
</form>
@ -81,6 +105,8 @@ VictorOpsConfig.propTypes = {
}).isRequired,
}).isRequired,
onSave: func.isRequired,
onTest: func.isRequired,
enabled: bool.isRequired,
}
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 = {
deadman: {
period: '10m',
@ -87,144 +85,104 @@ export const RULE_MESSAGE_TEMPLATES = {
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 = {
http: {
args: {
label: 'URL:',
placeholder: 'Ex: http://example.com/api/alert',
},
// properties: [
// {name: 'endpoint', label: 'Endpoint:', placeholder: 'Endpoint'},
// {name: 'header', label: 'Headers:', placeholder: 'Headers (Delimited)'}, // TODO: determine how to delimit
// ],
},
tcp: {
args: {
label: 'Address:',
placeholder: 'Ex: exampleendpoint.com:5678',
},
},
exec: {
args: {
label: 'Command (Arguments separated by Spaces):',
placeholder: 'Ex: woogie boogie',
},
},
log: {
args: {
label: 'File:',
placeholder: 'Ex: /tmp/alerts.log',
},
},
alerta: {
args: {
label: 'Paste Alerta TICKscript:', // TODO: remove this
placeholder: 'alerta()',
},
// 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'},
},
// ALERTS_FROM_CONFIG the array of fields to accept from Kapacitor Config
export const ALERTS_FROM_CONFIG = {
alerta: ['environment', 'origin', 'token'], // token = bool
hipChat: ['url', 'room', 'token'], // token = bool
opsGenie: ['api-key', 'teams', 'recipients'], // api-key = bool
pagerDuty: ['service-key'], // service-key = bool
pushover: ['token', 'user-key'], // token = bool, user-key = bool
sensu: ['addr', 'source'],
slack: ['url', 'channel'], // url = bool
email: ['from', 'host', 'password', 'port', 'username'], // password = bool
talk: ['url', 'author_name'], // url = bool
telegram: [
'token',
'chat-id',
'parse-mode',
'disable-web-page-preview',
'disable-notification',
], // token = bool
victorOps: ['api-key', 'routing-key'], // api-key = bool
// snmpTrap: ['trapOid', 'data'], // [oid/type/value]
// influxdb:[],
// mqtt:[]
}
export const MAP_FIELD_KEYS_FROM_CONFIG = {
alerta: {},
hipChat: {},
opsGenie: {},
pagerDuty: {'service-key': 'serviceKey'},
pushover: {'user-key': 'userKey'},
sensu: {},
slack: {},
email: {},
talk: {},
telegram: {
properties: [
{name: 'chatId', label: 'Chat ID:', placeholder: 'xxxxxxxxx'},
{name: 'parseMode', label: 'Emoji:', placeholder: 'Markdown'},
// {
// 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'},
],
'chat-id': 'chatId',
'parse-mode': 'parseMode',
'disable-web-page-preview': 'disableWebPagePreview',
'disable-notification': 'disableNotification',
},
victorOps: {'routing-key': 'routingKey'},
// snmpTrap: {},
// influxd: {},
// mqtt: {}
}
export const ALERT_NODES_ACCESSORS = {
http: rule => _.get(rule, 'alertNodes[0].args[0]', ''),
tcp: rule => _.get(rule, 'alertNodes[0].args[0]', ''),
exec: rule => _.get(rule, 'alertNodes[0].args', []).join(' '),
log: rule => _.get(rule, 'alertNodes[0].args[0]', ''),
smtp: rule => _.get(rule, 'alertNodes[0].args', []).join(' '),
alerta: rule =>
_.get(rule, 'alertNodes[0].properties', [])
.reduce(
(strs, item) => {
strs.push(`${item.name}('${item.args.join(' ')}')`)
return strs
},
['alerta()']
)
.join('.'),
// HANDLERS_TO_RULE returns array of fields that may be updated for each alert on rule.
export const HANDLERS_TO_RULE = {
alerta: [
'resource',
'event',
'environment',
'group',
'value',
'origin',
'service',
],
hipChat: ['room'],
opsGenie: ['teams', 'recipients'],
pagerDuty: [],
pushover: ['device', 'title', 'sound', 'url', 'urlTitle'],
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() {
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
return (
<KapacitorForm
onSubmit={this.handleSubmit}
@ -148,6 +148,7 @@ class KapacitorPage extends Component {
source={source}
addFlashMessage={addFlashMessage}
exists={exists}
hash={hash}
/>
)
}
@ -168,6 +169,7 @@ KapacitorPage.propTypes = {
url: string.isRequired,
kapacitors: array,
}),
location: shape({pathname: string, hash: string}).isRequired,
}
export default withRouter(KapacitorPage)

View File

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

View File

@ -70,7 +70,6 @@ KapacitorRulesPage.propTypes = {
name: string.isRequired,
trigger: string.isRequired,
message: string.isRequired,
alerts: arrayOf(string.isRequired).isRequired,
})
).isRequired,
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 {parseAlerta} from 'shared/parsing/parseAlerta'
export default function rules(state = {}, action) {
switch (action.type) {
@ -13,8 +16,7 @@ export default function rules(state = {}, action) {
trigger: 'threshold',
values: defaultRuleConfigs.threshold,
message: '',
alerts: [],
alertNodes: [],
alertNodes: {},
every: null,
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': {
const {ruleID, alertNodeName, alertNodesText} = action.payload
let alertNodesByType
switch (alertNodeName) {
case 'http':
case 'tcp':
case 'log':
alertNodesByType = [
{
name: alertNodeName,
args: [alertNodesText],
properties: [],
},
const {ruleID, alerts} = action.payload
const alertNodesByType = {}
_.forEach(alerts, h => {
if (h.enabled) {
if (h.type === 'post') {
if (h.url === '') {
return
}
h.headers = {[h.headerKey]: h.headerValue}
}
if (h.type === 'log' && h.filePath === '') {
return
}
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, {
[ruleID]: Object.assign({}, state[ruleID], {
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': {
const {ruleID, name} = action.payload
return Object.assign({}, state, {

View File

@ -152,20 +152,18 @@ export function updateKapacitorConfigSection(kapacitor, section, properties) {
})
}
export function testAlertOutput(kapacitor, outputName, properties) {
return kapacitorProxy(
kapacitor,
'GET',
'/kapacitor/v1/service-tests'
).then(({data: {services}}) => {
const service = services.find(s => s.name === outputName)
return kapacitorProxy(
export const testAlertOutput = async (kapacitor, outputName) => {
try {
const {data: {services}} = await kapacitorProxy(
kapacitor,
'POST',
service.link.href,
Object.assign({}, service.options, properties)
'GET',
'/kapacitor/v1/service-tests'
)
})
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) {

View File

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

View File

@ -9,6 +9,7 @@ export const Tab = React.createClass({
isDisabled: bool,
isActive: bool,
isKapacitorTab: bool,
isConfigured: bool,
},
render() {
@ -24,7 +25,10 @@ export const Tab = React.createClass({
}
return (
<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}
>
{this.props.children}

View File

@ -15,6 +15,7 @@ class TagInput extends Component {
this.input.value = ''
onAddTag(newItem)
this.props.disableTest()
}
}
@ -56,6 +57,7 @@ TagInput.propTypes = {
onDeleteTag: func.isRequired,
tags: arrayOf(string).isRequired,
title: string.isRequired,
disableTest: func,
}
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`}
className="btn btn-xs btn-default"
>
<span className="icon plus" /> Add Config
<span className="icon plus" /> Add Kapacitor Connection
</Link>
</Authorized>
)
@ -62,7 +62,7 @@ const kapacitorDropdown = (
onChoose={setActiveKapacitor}
addNew={{
url: `/sources/${source.id}/kapacitors/new`,
text: 'Add Kapacitor',
text: 'Add Kapacitor Connection',
}}
actions={[
{
@ -105,16 +105,16 @@ const InfluxTable = ({
<h2 className="panel-title">
{isUsingAuth
? <span>
InfluxDB Sources for <em>{me.currentOrganization.name}</em>
Connections for <em>{me.currentOrganization.name}</em>
</span>
: <span>InfluxDB Sources</span>}
: <span>Connections</span>}
</h2>
<Authorized requiredRole={EDITOR_ROLE}>
<Link
to={`/sources/${source.id}/manage-sources/new`}
className="btn btn-sm btn-primary"
>
<span className="icon plus" /> Add Source
<span className="icon plus" /> Add Connection
</Link>
</Authorized>
</div>
@ -123,14 +123,14 @@ const InfluxTable = ({
<thead>
<tr>
<th className="source-table--connect-col" />
<th>Source Name & Host</th>
<th>InfluxDB Connection</th>
<th className="text-right" />
<th>
Active Kapacitor{' '}
Kapacitor Connection{' '}
<QuestionMarkTooltip
tipID="kapacitor-node-helper"
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>
@ -189,7 +189,7 @@ const InfluxTable = ({
href="#"
onClick={handleDeleteSource(s)}
>
Delete Source
Delete Connection
</a>
</Authorized>
</td>

View File

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

View File

@ -131,7 +131,7 @@ class SourcePage extends Component {
.catch(err => {
// dont want to flash this until they submit
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}) => {
this.props.addSourceAction(sourceFromServer)
this._redirect(sourceFromServer)
notify('success', `New source ${source.name} added`)
notify('success', `InfluxDB ${source.name} available as a connection`)
})
.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}) => {
this.props.updateSourceAction(sourceFromServer)
this._redirect(sourceFromServer)
notify('success', `Source ${source.name} updated`)
notify('success', `InfluxDB connection ${source.name} updated`)
})
.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__left">
<h1 className="page-header__title">
{editMode ? 'Edit Source' : 'Add a New Source'}
{editMode
? 'Configure InfluxDB Connection'
: 'Add a New InfluxDB Connection'}
</h1>
</div>
{isInitialSource

View File

@ -37,12 +37,13 @@ $config-endpoint-tab-bg-active: $g3-castle;
border-radius: 0;
height: $config-endpoint-tab-height;
border: 0;
padding: 0 40px 0 15px;
padding: 0 50px 0 15px;
display: flex;
align-items: center;
justify-content: space-between;
font-weight: 500;
font-size: 16px;
position: relative;
&:first-child {border-top-left-radius: $radius;}
@ -54,6 +55,22 @@ $config-endpoint-tab-bg-active: $g3-castle;
color: $config-endpoint-tab-text-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;
}
}
// Generic re-usable classes for rule builder sections
.rule-section--border-top {
border-top: 2px solid $rule-builder--section-border;
@ -260,7 +261,7 @@ $rule-builder--radius-lg: 5px;
top: ($rule-builder--padding-lg * 2);
left: $rule-builder--padding-sm;
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 {
position: absolute;
@ -290,7 +291,10 @@ $rule-builder--radius-lg: 5px;
}
.rule-builder--graph-options {
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;
align-items: center;
height: ($rule-builder--padding-lg * 2);
@ -309,7 +313,9 @@ $rule-builder--radius-lg: 5px;
*/
.rule-builder--message {
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 {
height: 100px;
@ -404,3 +410,216 @@ $rule-builder--radius-lg: 5px;
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
*/
.dropdown .dropdown-menu.dropdown-malachite {
background: #4ed8a0;
background: -moz-linear-gradient(left, #4ed8a0 0%, #22adf6 100%);
background: -webkit-linear-gradient(left, #4ed8a0 0%, #22adf6 100%);
background: linear-gradient(to right, #4ed8a0 0%, #22adf6 100%);
background: #32b08c;
background: -moz-linear-gradient(left, #32b08c 0%, #22adf6 100%);
background: -webkit-linear-gradient(left, #32b08c 0%, #22adf6 100%);
background: linear-gradient(to right, #32b08c 0%, #22adf6 100%);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='@color1', endColorstr='@color2', GradientType=1);
}
.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:hover {
color: #fff;
background: #32b08c;
background: -moz-linear-gradient(left, #32b08c 0%, #22adf6 100%);
background: -webkit-linear-gradient(left, #32b08c 0%, #22adf6 100%);
background: linear-gradient(to right, #32b08c 0%, #22adf6 100%);
background: #108174;
background: -moz-linear-gradient(left, #108174 0%, #22adf6 100%);
background: -webkit-linear-gradient(left, #108174 0%, #22adf6 100%);
background: linear-gradient(to right, #108174 0%, #22adf6 100%);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='@color1', endColorstr='@color2', GradientType=1);
}
.dropdown .dropdown-menu.dropdown-malachite li.dropdown-item .dropdown-action {
@ -4274,25 +4274,25 @@ p .label {
color: #fff;
}
.dropdown .dropdown-menu.dropdown-malachite li.dropdown-item.active {
background: #32b08c;
background: -moz-linear-gradient(left, #32b08c 0%, #22adf6 100%);
background: -webkit-linear-gradient(left, #32b08c 0%, #22adf6 100%);
background: linear-gradient(to right, #32b08c 0%, #22adf6 100%);
background: #108174;
background: -moz-linear-gradient(left, #108174 0%, #22adf6 100%);
background: -webkit-linear-gradient(left, #108174 0%, #22adf6 100%);
background: linear-gradient(to right, #108174 0%, #22adf6 100%);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='@color1', endColorstr='@color2', GradientType=1);
}
.dropdown .dropdown-menu.dropdown-malachite li.dropdown-divider {
background: #32b08c;
background: -moz-linear-gradient(left, #32b08c 0%, #4591ed 100%);
background: -webkit-linear-gradient(left, #32b08c 0%, #4591ed 100%);
background: linear-gradient(to right, #32b08c 0%, #4591ed 100%);
background: #108174;
background: -moz-linear-gradient(left, #108174 0%, #4591ed 100%);
background: -webkit-linear-gradient(left, #108174 0%, #4591ed 100%);
background: linear-gradient(to right, #108174 0%, #4591ed 100%);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='@color1', endColorstr='@color2', GradientType=1);
}
.dropdown .dropdown-menu.dropdown-malachite li.dropdown-header {
color: #c6ffd0;
background: #32b08c;
background: -moz-linear-gradient(left, #32b08c 0%, #4591ed 100%);
background: -webkit-linear-gradient(left, #32b08c 0%, #4591ed 100%);
background: linear-gradient(to right, #32b08c 0%, #4591ed 100%);
background: #108174;
background: -moz-linear-gradient(left, #108174 0%, #4591ed 100%);
background: -webkit-linear-gradient(left, #108174 0%, #4591ed 100%);
background: linear-gradient(to right, #108174 0%, #4591ed 100%);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='@color1', endColorstr='@color2', GradientType=1);
}
.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