Merge branch 'master' into feature/global-users
commit
5fbfc12f11
|
@ -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}
|
||||
|
|
35
CHANGELOG.md
35
CHANGELOG.md
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
17
Gopkg.toml
17
Gopkg.toml
|
@ -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"
|
||||
|
|
10
README.md
10
README.md
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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)
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
434
kapacitor/ast.go
434
kapacitor/ast.go
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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 = ''
|
||||
|
||||
|
|
|
@ -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{}
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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))
|
||||
}
|
||||
}
|
|
@ -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")
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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{},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "chronograf-ui",
|
||||
"version": "1.4.0-0",
|
||||
"version": "1.4.0-1",
|
||||
"private": false,
|
||||
"license": "AGPL-3.0",
|
||||
"description": "",
|
||||
|
|
|
@ -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
|
@ -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)
|
||||
})
|
|
@ -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)
|
||||
})
|
||||
})
|
|
@ -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)
|
||||
})
|
||||
})
|
|
@ -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)
|
||||
)
|
||||
|
|
|
@ -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} />
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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 (
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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,
|
||||
}
|
|
@ -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]
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -70,7 +70,6 @@ KapacitorRulesPage.propTypes = {
|
|||
name: string.isRequired,
|
||||
trigger: string.isRequired,
|
||||
message: string.isRequired,
|
||||
alerts: arrayOf(string.isRequired).isRequired,
|
||||
})
|
||||
).isRequired,
|
||||
actions: shape({
|
||||
|
|
|
@ -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, {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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)
|
||||
)
|
||||
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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
|
|
@ -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, ', ')
|
||||
}
|
|
@ -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>
|
||||
|
|
|
@ -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 />
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
Loading…
Reference in New Issue