Merge branch 'master' into feature/persistent-legend

pull/10616/head
deniz kusefoglu 2018-02-06 10:52:02 -08:00
commit 39eca759b4
52 changed files with 1500 additions and 1136 deletions

View File

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

View File

@ -1,13 +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
1. [#2746](https://github.com/influxdata/chronograf/pull/2746): Separate saving TICKscript from exiting editor page
### 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
## v1.4.0.1 [unreleased]
### UI Improvements
1. [#2690](https://github.com/influxdata/chronograf/pull/2690): Add separate CLI flag for canned sources, kapacitors, dashboards, and organizations
1. [#2664](https://github.com/influxdata/chronograf/pull/2664): Fix positioning of custom time indicator
## v1.4.0.0 [2017-12-22]
### UI Improvements
@ -103,7 +114,6 @@
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
1. [#2409](https://github.com/influxdata/chronograf/pull/2409): Add multiple event handlers to rules
### UI Improvements

View File

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

View File

@ -274,6 +274,10 @@ func MarshalDashboard(d chronograf.Dashboard) ([]byte, error) {
Type: c.Type,
Axes: axes,
Colors: colors,
Legend: &Legend{
Type: c.Legend.Type,
Orientation: c.Legend.Orientation,
},
}
}
templates := make([]*Template, len(d.Templates))
@ -394,6 +398,12 @@ func UnmarshalDashboard(data []byte, d *chronograf.Dashboard) error {
}
}
legend := chronograf.Legend{}
if c.Legend != nil {
legend.Type = c.Legend.Type
legend.Orientation = c.Legend.Orientation
}
cells[i] = chronograf.DashboardCell{
ID: c.ID,
X: c.X,
@ -405,6 +415,7 @@ func UnmarshalDashboard(data []byte, d *chronograf.Dashboard) error {
Type: c.Type,
Axes: axes,
CellColors: colors,
Legend: legend,
}
}
@ -570,10 +581,7 @@ func UnmarshalRole(data []byte, r *chronograf.Role) error {
// UnmarshalRolePB decodes a role from binary protobuf data.
func UnmarshalRolePB(data []byte, r *Role) error {
if err := proto.Unmarshal(data, r); err != nil {
return err
}
return nil
return proto.Unmarshal(data, r)
}
// MarshalOrganization encodes a organization to binary protobuf format.
@ -607,10 +615,7 @@ func UnmarshalOrganization(data []byte, o *chronograf.Organization) error {
// UnmarshalOrganizationPB decodes a organization from binary protobuf data.
func UnmarshalOrganizationPB(data []byte, o *Organization) error {
if err := proto.Unmarshal(data, o); err != nil {
return err
}
return nil
return proto.Unmarshal(data, o)
}
// MarshalConfig encodes a config to binary protobuf format.
@ -643,8 +648,5 @@ func UnmarshalConfig(data []byte, c *chronograf.Config) error {
// UnmarshalConfigPB decodes a config from binary protobuf data.
func UnmarshalConfigPB(data []byte, c *Config) error {
if err := proto.Unmarshal(data, c); err != nil {
return err
}
return nil
return proto.Unmarshal(data, c)
}

File diff suppressed because it is too large Load Diff

View File

@ -36,6 +36,7 @@ message DashboardCell {
string ID = 8; // id is the unique id of the dashboard. MIGRATED FIELD added in 1.2.0-beta6
map<string, Axis> axes = 9; // Axes represent the graphical viewport for a cell's visualizations
repeated Color colors = 10; // Colors represent encoding data values to color
Legend legend = 11; // Legend is summary information for a cell
}
message Color {
@ -46,6 +47,11 @@ message Color {
string Value = 5; // Value is the data value mapped to this color
}
message Legend {
string Type = 1; // Type is how the legend is used
string Orientation = 2; // Orientation is the location of the legend on the cell
}
message Axis {
repeated int64 legacyBounds = 1; // legacyBounds are an ordered 2-tuple consisting of lower and upper axis extents, respectively
repeated string bounds = 2; // bounds are an arbitrary list of client-defined bounds.

View File

@ -251,6 +251,10 @@ func Test_MarshalDashboard_WithLegacyBounds(t *testing.T) {
Value: "100",
},
},
Legend: chronograf.Legend{
Type: "static",
Orientation: "bottom",
},
Type: "line",
},
},
@ -301,6 +305,10 @@ func Test_MarshalDashboard_WithLegacyBounds(t *testing.T) {
Value: "100",
},
},
Legend: chronograf.Legend{
Type: "static",
Orientation: "bottom",
},
Type: "line",
},
},

View File

@ -25,6 +25,9 @@ const (
ErrInvalidAxis = Error("Unexpected axis in cell. Valid axes are 'x', 'y', and 'y2'")
ErrInvalidColorType = Error("Invalid color type. Valid color types are 'min', 'max', 'threshold', 'text', and 'background'")
ErrInvalidColor = Error("Invalid color. Accepted color format is #RRGGBB")
ErrInvalidLegend = Error("Invalid legend. Both type and orientation must be set")
ErrInvalidLegendType = Error("Invalid legend type. Valid legend type is 'static'")
ErrInvalidLegendOrient = Error("Invalid orientation type. Valid orientation types are 'top', 'bottom', 'right', 'left'")
ErrUserAlreadyExists = Error("user already exists")
ErrOrganizationNotFound = Error("organization not found")
ErrOrganizationAlreadyExists = Error("organization already exists")
@ -531,6 +534,12 @@ type CellColor struct {
Value string `json:"value"` // Value is the data value mapped to this color
}
// Legend represents the encoding of data into a legend
type Legend struct {
Type string `json:"type,omitempty"`
Orientation string `json:"orientation,omitempty"`
}
// DashboardCell holds visual and query information for a cell
type DashboardCell struct {
ID string `json:"i"`
@ -543,6 +552,7 @@ type DashboardCell struct {
Axes map[string]Axis `json:"axes"`
Type string `json:"type"`
CellColors []CellColor `json:"colors"`
Legend Legend `json:"legend"`
}
// DashboardsStore is the storage and retrieval of dashboards

View File

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

View File

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

View File

@ -539,6 +539,10 @@ func TestServer(t *testing.T) {
"value": "100"
}
],
"legend":{
"type": "static",
"orientation": "bottom"
},
"links": {
"self": "/chronograf/v1/dashboards/1000/cells/8f61c619-dd9b-4761-8aa8-577f27247093"
}
@ -778,6 +782,10 @@ func TestServer(t *testing.T) {
"value": "100"
}
],
"legend":{
"type": "static",
"orientation": "bottom"
},
"links": {
"self": "/chronograf/v1/dashboards/1000/cells/8f61c619-dd9b-4761-8aa8-577f27247093"
}

View File

@ -86,7 +86,11 @@
"name": "comet",
"value": "100"
}
]
],
"legend": {
"type": "static",
"orientation": "bottom"
}
}
],
"templates": [

View File

@ -1293,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
@ -1348,7 +1498,7 @@ var period = 1d
var name = 'myname\'s'
var idVar = name + ':{{.Group}}'
var idVar = name
var message = ''

View File

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

View File

@ -28,37 +28,31 @@ type dashboardCellResponse struct {
func newCellResponse(dID chronograf.DashboardID, cell chronograf.DashboardCell) dashboardCellResponse {
base := "/chronograf/v1/dashboards"
newCell := chronograf.DashboardCell{}
newCell.Queries = make([]chronograf.DashboardQuery, len(cell.Queries))
copy(newCell.Queries, cell.Queries)
newCell.CellColors = make([]chronograf.CellColor, len(cell.CellColors))
copy(newCell.CellColors, cell.CellColors)
// ensure x, y, and y2 axes always returned
labels := []string{"x", "y", "y2"}
newCell.Axes = make(map[string]chronograf.Axis, len(labels))
newCell.X = cell.X
newCell.Y = cell.Y
newCell.W = cell.W
newCell.H = cell.H
newCell.Name = cell.Name
newCell.ID = cell.ID
newCell.Type = cell.Type
for _, lbl := range labels {
if axis, found := cell.Axes[lbl]; !found {
newCell.Axes[lbl] = chronograf.Axis{
Bounds: []string{},
}
} else {
newCell.Axes[lbl] = axis
}
if cell.Queries == nil {
cell.Queries = []chronograf.DashboardQuery{}
}
if cell.CellColors == nil {
cell.CellColors = []chronograf.CellColor{}
}
// Copy to handle race condition
newAxes := make(map[string]chronograf.Axis, len(cell.Axes))
for k, v := range cell.Axes {
newAxes[k] = v
}
// ensure x, y, and y2 axes always returned
for _, lbl := range []string{"x", "y", "y2"} {
if _, found := newAxes[lbl]; !found {
newAxes[lbl] = chronograf.Axis{
Bounds: []string{},
}
}
}
cell.Axes = newAxes
return dashboardCellResponse{
DashboardCell: newCell,
DashboardCell: cell,
Links: dashboardCellLinks{
Self: fmt.Sprintf("%s/%d/cells/%s", base, dID, cell.ID),
},
@ -91,7 +85,10 @@ func ValidDashboardCellRequest(c *chronograf.DashboardCell) error {
if err != nil {
return err
}
return HasCorrectColors(c)
if err = HasCorrectColors(c); err != nil {
return err
}
return HasCorrectLegend(c)
}
// HasCorrectAxes verifies that only permitted axes exist within a DashboardCell
@ -126,6 +123,27 @@ func HasCorrectColors(c *chronograf.DashboardCell) error {
return nil
}
// HasCorrectLegend verifies that the format of the legend is correct
func HasCorrectLegend(c *chronograf.DashboardCell) error {
// No legend set
if c.Legend.Type == "" && c.Legend.Orientation == "" {
return nil
}
if c.Legend.Type == "" || c.Legend.Orientation == "" {
return chronograf.ErrInvalidLegend
}
if !oneOf(c.Legend.Orientation, "top", "bottom", "right", "left") {
return chronograf.ErrInvalidLegendOrient
}
// Remember! if we add other types, update ErrInvalidLegendType
if !oneOf(c.Legend.Type, "static") {
return chronograf.ErrInvalidLegendType
}
return nil
}
// oneOf reports whether a provided string is a member of a variadic list of
// valid options
func oneOf(prop string, validOpts ...string) bool {

View File

@ -532,7 +532,7 @@ func TestService_ReplaceDashboardCell(t *testing.T) {
}
}
`))),
want: `{"i":"3c5c4102-fa40-4585-a8f9-917c77e37192","x":0,"y":0,"w":4,"h":4,"name":"Untitled Cell","queries":[{"query":"SELECT mean(\"usage_user\") AS \"mean_usage_user\" FROM \"telegraf\".\"autogen\".\"cpu\" WHERE time \u003e :dashboardTime: AND \"cpu\"=:cpu: GROUP BY :interval: FILL(null)","queryConfig":{"id":"3cd3eaa4-a4b8-44b3-b69e-0c7bf6b91d9e","database":"telegraf","measurement":"cpu","retentionPolicy":"autogen","fields":[{"value":"mean","type":"func","alias":"mean_usage_user","args":[{"value":"usage_user","type":"field","alias":""}]}],"tags":{"cpu":["ChristohersMBP2.lan"]},"groupBy":{"time":"2s","tags":[]},"areTagsAccepted":true,"fill":"null","rawText":"SELECT mean(\"usage_user\") AS \"mean_usage_user\" FROM \"telegraf\".\"autogen\".\"cpu\" WHERE time \u003e :dashboardTime: AND \"cpu\"=:cpu: GROUP BY :interval: FILL(null)","range":{"upper":"","lower":"now() - 15m"},"shifts":[]},"source":""}],"axes":{"x":{"bounds":[],"label":"","prefix":"","suffix":"","base":"","scale":""},"y":{"bounds":[],"label":"","prefix":"","suffix":"","base":"","scale":""},"y2":{"bounds":[],"label":"","prefix":"","suffix":"","base":"","scale":""}},"type":"line","colors":[{"id":"0","type":"min","hex":"#00C9FF","name":"laser","value":"0"},{"id":"1","type":"max","hex":"#9394FF","name":"comet","value":"100"}],"links":{"self":"/chronograf/v1/dashboards/1/cells/3c5c4102-fa40-4585-a8f9-917c77e37192"}}
want: `{"i":"3c5c4102-fa40-4585-a8f9-917c77e37192","x":0,"y":0,"w":4,"h":4,"name":"Untitled Cell","queries":[{"query":"SELECT mean(\"usage_user\") AS \"mean_usage_user\" FROM \"telegraf\".\"autogen\".\"cpu\" WHERE time \u003e :dashboardTime: AND \"cpu\"=:cpu: GROUP BY :interval: FILL(null)","queryConfig":{"id":"3cd3eaa4-a4b8-44b3-b69e-0c7bf6b91d9e","database":"telegraf","measurement":"cpu","retentionPolicy":"autogen","fields":[{"value":"mean","type":"func","alias":"mean_usage_user","args":[{"value":"usage_user","type":"field","alias":""}]}],"tags":{"cpu":["ChristohersMBP2.lan"]},"groupBy":{"time":"2s","tags":[]},"areTagsAccepted":true,"fill":"null","rawText":"SELECT mean(\"usage_user\") AS \"mean_usage_user\" FROM \"telegraf\".\"autogen\".\"cpu\" WHERE time \u003e :dashboardTime: AND \"cpu\"=:cpu: GROUP BY :interval: FILL(null)","range":{"upper":"","lower":"now() - 15m"},"shifts":[]},"source":""}],"axes":{"x":{"bounds":[],"label":"","prefix":"","suffix":"","base":"","scale":""},"y":{"bounds":[],"label":"","prefix":"","suffix":"","base":"","scale":""},"y2":{"bounds":[],"label":"","prefix":"","suffix":"","base":"","scale":""}},"type":"line","colors":[{"id":"0","type":"min","hex":"#00C9FF","name":"laser","value":"0"},{"id":"1","type":"max","hex":"#9394FF","name":"comet","value":"100"}],"legend":{},"links":{"self":"/chronograf/v1/dashboards/1/cells/3c5c4102-fa40-4585-a8f9-917c77e37192"}}
`,
},
{
@ -695,7 +695,7 @@ func Test_newCellResponses(t *testing.T) {
want []dashboardCellResponse
}{
{
name: "foo",
name: "all fields set",
dID: chronograf.DashboardID(1),
dcells: []chronograf.DashboardCell{
chronograf.DashboardCell{
@ -752,6 +752,10 @@ func Test_newCellResponses(t *testing.T) {
chronograf.CellColor{ID: "0", Type: "min", Hex: "#00C9FF", Name: "laser", Value: "0"},
chronograf.CellColor{ID: "1", Type: "max", Hex: "#9394FF", Name: "comet", Value: "100"},
},
Legend: chronograf.Legend{
Type: "static",
Orientation: "bottom",
},
},
},
want: []dashboardCellResponse{
@ -817,6 +821,50 @@ func Test_newCellResponses(t *testing.T) {
Value: "100",
},
},
Legend: chronograf.Legend{
Type: "static",
Orientation: "bottom",
},
},
Links: dashboardCellLinks{
Self: "/chronograf/v1/dashboards/1/cells/445f8dc0-4d73-4168-8477-f628690d18a3"},
},
},
},
{
name: "nothing set",
dID: chronograf.DashboardID(1),
dcells: []chronograf.DashboardCell{
chronograf.DashboardCell{
ID: "445f8dc0-4d73-4168-8477-f628690d18a3",
X: 0,
Y: 0,
W: 4,
H: 4,
Name: "Untitled Cell",
},
},
want: []dashboardCellResponse{
{
DashboardCell: chronograf.DashboardCell{
ID: "445f8dc0-4d73-4168-8477-f628690d18a3",
W: 4,
H: 4,
Name: "Untitled Cell",
Queries: []chronograf.DashboardQuery{},
Axes: map[string]chronograf.Axis{
"x": chronograf.Axis{
Bounds: []string{},
},
"y": chronograf.Axis{
Bounds: []string{},
},
"y2": chronograf.Axis{
Bounds: []string{},
},
},
CellColors: []chronograf.CellColor{},
Legend: chronograf.Legend{},
},
Links: dashboardCellLinks{
Self: "/chronograf/v1/dashboards/1/cells/445f8dc0-4d73-4168-8477-f628690d18a3"},
@ -832,3 +880,97 @@ func Test_newCellResponses(t *testing.T) {
})
}
}
func TestHasCorrectLegend(t *testing.T) {
tests := []struct {
name string
c *chronograf.DashboardCell
wantErr bool
}{
{
name: "empty legend is ok",
c: &chronograf.DashboardCell{},
},
{
name: "must have both an orientation and type",
c: &chronograf.DashboardCell{
Legend: chronograf.Legend{
Type: "static",
},
},
wantErr: true,
},
{
name: "must have both a type and orientation",
c: &chronograf.DashboardCell{
Legend: chronograf.Legend{
Orientation: "bottom",
},
},
wantErr: true,
},
{
name: "invalid types",
c: &chronograf.DashboardCell{
Legend: chronograf.Legend{
Type: "no such type",
Orientation: "bottom",
},
},
wantErr: true,
},
{
name: "invalid orientation",
c: &chronograf.DashboardCell{
Legend: chronograf.Legend{
Type: "static",
Orientation: "no such orientation",
},
},
wantErr: true,
},
{
name: "orientation bottom valid",
c: &chronograf.DashboardCell{
Legend: chronograf.Legend{
Type: "static",
Orientation: "bottom",
},
},
},
{
name: "orientation top valid",
c: &chronograf.DashboardCell{
Legend: chronograf.Legend{
Type: "static",
Orientation: "top",
},
},
},
{
name: "orientation right valid",
c: &chronograf.DashboardCell{
Legend: chronograf.Legend{
Type: "static",
Orientation: "right",
},
},
},
{
name: "orientation left valid",
c: &chronograf.DashboardCell{
Legend: chronograf.Legend{
Type: "static",
Orientation: "left",
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if err := HasCorrectLegend(tt.c); (err != nil) != tt.wantErr {
t.Errorf("HasCorrectLegend() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}

View File

@ -3,7 +3,7 @@
"info": {
"title": "Chronograf",
"description": "API endpoints for Chronograf",
"version": "1.4.0.0"
"version": "1.4.0.1"
},
"schemes": ["http"],
"basePath": "/chronograf/v1",
@ -3970,6 +3970,24 @@
"$ref": "#/definitions/DashboardColor"
}
},
"legend": {
"description":
"Legend define encoding of the data into a cell's legend",
"type": "object",
"properties": {
"type": {
"description": "type is the style of the legend",
"type": "string",
"enum": ["static"]
},
"orientation": {
"description":
"orientation is the location of the legend with respect to the cell graph",
"type": "string",
"enum": ["top", "bottom", "left", "right"]
}
}
},
"links": {
"type": "object",
"properties": {

View File

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

View File

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

View File

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

View File

@ -195,16 +195,10 @@ export const updateRuleStatus = (rule, status) => dispatch => {
})
}
export const createTask = (
kapacitor,
task,
router,
sourceID
) => async dispatch => {
export const createTask = (kapacitor, task) => async dispatch => {
try {
const {data} = await createTaskAJAX(kapacitor, task)
router.push(`/sources/${sourceID}/alert-rules`)
dispatch(publishNotification('success', 'You made a TICKscript!'))
dispatch(publishNotification('success', 'TICKscript successfully created'))
return data
} catch (error) {
if (!error) {
@ -220,20 +214,17 @@ export const updateTask = (
kapacitor,
task,
ruleID,
router,
sourceID
) => async dispatch => {
try {
const {data} = await updateTaskAJAX(kapacitor, task, ruleID, sourceID)
router.push(`/sources/${sourceID}/alert-rules`)
dispatch(publishNotification('success', 'TICKscript updated successully'))
dispatch(publishNotification('success', 'TICKscript saved'))
return data
} catch (error) {
if (!error) {
dispatch(errorThrown('Could not communicate with server'))
return
}
return error.data
}
}

View File

@ -2,7 +2,11 @@ import React, {Component, PropTypes} from 'react'
import _ from 'lodash'
import {Tab, Tabs, TabPanel, TabPanels, TabList} from 'shared/components/Tabs'
import {getKapacitorConfig, updateKapacitorConfigSection} from 'shared/apis'
import {
getKapacitorConfig,
updateKapacitorConfigSection,
testAlertOutput,
} from 'shared/apis'
import {
AlertaConfig,
@ -23,7 +27,6 @@ class AlertTabs extends Component {
super(props)
this.state = {
selectedHandler: 'smtp',
configSections: null,
}
}
@ -38,18 +41,17 @@ 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) => {
@ -68,23 +70,43 @@ class AlertTabs extends Component {
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}.`,
})
}
}
@ -102,8 +124,14 @@ 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
@ -116,6 +144,8 @@ class AlertTabs extends Component {
<AlertaConfig
onSave={this.handleSaveConfig('alerta')}
config={this.getSection(configSections, 'alerta')}
onTest={this.handleTestConfig('alerta')}
enabled={this.getEnabled(configSections, 'alerta')}
/>,
},
hipchat: {
@ -125,6 +155,8 @@ class AlertTabs extends Component {
<HipChatConfig
onSave={this.handleSaveConfig('hipchat')}
config={this.getSection(configSections, 'hipchat')}
onTest={this.handleTestConfig('hipchat')}
enabled={this.getEnabled(configSections, 'hipchat')}
/>,
},
opsgenie: {
@ -134,6 +166,8 @@ class AlertTabs extends Component {
<OpsGenieConfig
onSave={this.handleSaveConfig('opsgenie')}
config={this.getSection(configSections, 'opsgenie')}
onTest={this.handleTestConfig('opsgenie')}
enabled={this.getEnabled(configSections, 'opsgenie')}
/>,
},
pagerduty: {
@ -143,6 +177,8 @@ class AlertTabs extends Component {
<PagerDutyConfig
onSave={this.handleSaveConfig('pagerduty')}
config={this.getSection(configSections, 'pagerduty')}
onTest={this.handleTestConfig('pagerduty')}
enabled={this.getEnabled(configSections, 'pagerduty')}
/>,
},
pushover: {
@ -152,6 +188,8 @@ class AlertTabs extends Component {
<PushoverConfig
onSave={this.handleSaveConfig('pushover')}
config={this.getSection(configSections, 'pushover')}
onTest={this.handleTestConfig('pushover')}
enabled={this.getEnabled(configSections, 'pushover')}
/>,
},
sensu: {
@ -161,6 +199,8 @@ class AlertTabs extends Component {
<SensuConfig
onSave={this.handleSaveConfig('sensu')}
config={this.getSection(configSections, 'sensu')}
onTest={this.handleTestConfig('sensu')}
enabled={this.getEnabled(configSections, 'sensu')}
/>,
},
slack: {
@ -170,6 +210,8 @@ class AlertTabs extends Component {
<SlackConfig
onSave={this.handleSaveConfig('slack')}
config={this.getSection(configSections, 'slack')}
onTest={this.handleTestConfig('slack')}
enabled={this.getEnabled(configSections, 'slack')}
/>,
},
smtp: {
@ -179,6 +221,8 @@ class AlertTabs extends Component {
<SMTPConfig
onSave={this.handleSaveConfig('smtp')}
config={this.getSection(configSections, 'smtp')}
onTest={this.handleTestConfig('smtp')}
enabled={this.getEnabled(configSections, 'smtp')}
/>,
},
talk: {
@ -188,6 +232,8 @@ class AlertTabs extends Component {
<TalkConfig
onSave={this.handleSaveConfig('talk')}
config={this.getSection(configSections, 'talk')}
onTest={this.handleTestConfig('talk')}
enabled={this.getEnabled(configSections, 'talk')}
/>,
},
telegram: {
@ -197,6 +243,8 @@ class AlertTabs extends Component {
<TelegramConfig
onSave={this.handleSaveConfig('telegram')}
config={this.getSection(configSections, 'telegram')}
onTest={this.handleTestConfig('telegram')}
enabled={this.getEnabled(configSections, 'telegram')}
/>,
},
victorops: {
@ -206,10 +254,11 @@ class AlertTabs extends Component {
<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">
@ -218,7 +267,10 @@ 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,
@ -269,6 +321,7 @@ AlertTabs.propTypes = {
}).isRequired,
}),
addFlashMessage: func.isRequired,
hash: string.isRequired,
}
export default AlertTabs

View File

@ -65,7 +65,7 @@ class HandlerOptions extends Component {
<EmailHandler
selectedHandler={selectedHandler}
handleModifyHandler={handleModifyHandler}
onGoToConfig={onGoToConfig}
onGoToConfig={onGoToConfig('smtp')}
validationError={validationError}
updateDetails={updateDetails}
rule={rule}
@ -76,7 +76,7 @@ class HandlerOptions extends Component {
<AlertaHandler
selectedHandler={selectedHandler}
handleModifyHandler={handleModifyHandler}
onGoToConfig={onGoToConfig}
onGoToConfig={onGoToConfig('alerta')}
validationError={validationError}
/>
)
@ -85,7 +85,7 @@ class HandlerOptions extends Component {
<HipchatHandler
selectedHandler={selectedHandler}
handleModifyHandler={handleModifyHandler}
onGoToConfig={onGoToConfig}
onGoToConfig={onGoToConfig('hipchat')}
validationError={validationError}
/>
)
@ -94,7 +94,7 @@ class HandlerOptions extends Component {
<OpsgenieHandler
selectedHandler={selectedHandler}
handleModifyHandler={handleModifyHandler}
onGoToConfig={onGoToConfig}
onGoToConfig={onGoToConfig('opsgenie')}
validationError={validationError}
/>
)
@ -103,7 +103,7 @@ class HandlerOptions extends Component {
<PagerdutyHandler
selectedHandler={selectedHandler}
handleModifyHandler={handleModifyHandler}
onGoToConfig={onGoToConfig}
onGoToConfig={onGoToConfig('pagerduty')}
validationError={validationError}
/>
)
@ -112,7 +112,7 @@ class HandlerOptions extends Component {
<PushoverHandler
selectedHandler={selectedHandler}
handleModifyHandler={handleModifyHandler}
onGoToConfig={onGoToConfig}
onGoToConfig={onGoToConfig('pushover')}
validationError={validationError}
/>
)
@ -121,7 +121,7 @@ class HandlerOptions extends Component {
<SensuHandler
selectedHandler={selectedHandler}
handleModifyHandler={handleModifyHandler}
onGoToConfig={onGoToConfig}
onGoToConfig={onGoToConfig('sensu')}
validationError={validationError}
/>
)
@ -130,7 +130,7 @@ class HandlerOptions extends Component {
<SlackHandler
selectedHandler={selectedHandler}
handleModifyHandler={handleModifyHandler}
onGoToConfig={onGoToConfig}
onGoToConfig={onGoToConfig('slack')}
validationError={validationError}
/>
)
@ -139,7 +139,7 @@ class HandlerOptions extends Component {
<TalkHandler
selectedHandler={selectedHandler}
handleModifyHandler={handleModifyHandler}
onGoToConfig={onGoToConfig}
onGoToConfig={onGoToConfig('talk')}
validationError={validationError}
/>
)
@ -148,7 +148,7 @@ class HandlerOptions extends Component {
<TelegramHandler
selectedHandler={selectedHandler}
handleModifyHandler={handleModifyHandler}
onGoToConfig={onGoToConfig}
onGoToConfig={onGoToConfig('telegram')}
validationError={validationError}
/>
)
@ -157,7 +157,7 @@ class HandlerOptions extends Component {
<VictoropsHandler
selectedHandler={selectedHandler}
handleModifyHandler={handleModifyHandler}
onGoToConfig={onGoToConfig}
onGoToConfig={onGoToConfig('victorops')}
validationError={validationError}
/>
)

View File

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

View File

@ -75,10 +75,12 @@ class KapacitorRule extends Component {
})
}
handleSaveToConfig = () => {
handleSaveToConfig = configName => () => {
const {rule, configLink, router} = this.props
if (this.validationError()) {
router.push(configLink)
router.push({
pathname: `${configLink}#${configName}`,
})
} else if (rule.id === DEFAULT_RULE_ID) {
this.handleCreate(configLink)
} else {

View File

@ -1,16 +1,16 @@
import React, {PropTypes} from 'react'
const LogsToggle = ({areLogsVisible, onToggleLogsVisbility}) =>
const LogsToggle = ({areLogsVisible, onToggleLogsVisibility}) =>
<ul className="nav nav-tablist nav-tablist-sm nav-tablist-malachite logs-toggle">
<li
className={areLogsVisible ? null : 'active'}
onClick={onToggleLogsVisbility}
onClick={onToggleLogsVisibility}
>
Editor
</li>
<li
className={areLogsVisible ? 'active' : null}
onClick={onToggleLogsVisbility}
onClick={onToggleLogsVisibility}
>
Editor + Logs
</li>
@ -20,7 +20,7 @@ const {bool, func} = PropTypes
LogsToggle.propTypes = {
areLogsVisible: bool,
onToggleLogsVisbility: func.isRequired,
onToggleLogsVisibility: func.isRequired,
}
export default LogsToggle

View File

@ -8,25 +8,29 @@ import LogsTable from 'src/kapacitor/components/LogsTable'
const Tickscript = ({
onSave,
onExit,
task,
logs,
validation,
consoleMessage,
onSelectDbrps,
onChangeScript,
onChangeType,
onChangeID,
unsavedChanges,
isNewTickscript,
areLogsVisible,
areLogsEnabled,
onToggleLogsVisbility,
onToggleLogsVisibility,
}) =>
<div className="page">
<TickscriptHeader
task={task}
onSave={onSave}
onExit={onExit}
unsavedChanges={unsavedChanges}
areLogsVisible={areLogsVisible}
areLogsEnabled={areLogsEnabled}
onToggleLogsVisbility={onToggleLogsVisbility}
onToggleLogsVisibility={onToggleLogsVisibility}
isNewTickscript={isNewTickscript}
/>
<div className="page-contents--split">
@ -38,11 +42,14 @@ const Tickscript = ({
onChangeID={onChangeID}
task={task}
/>
<TickscriptEditorConsole validation={validation} />
<TickscriptEditor
script={task.tickscript}
onChangeScript={onChangeScript}
/>
<TickscriptEditorConsole
consoleMessage={consoleMessage}
unsavedChanges={unsavedChanges}
/>
</div>
{areLogsVisible ? <LogsTable logs={logs} /> : null}
</div>
@ -53,12 +60,13 @@ const {arrayOf, bool, func, shape, string} = PropTypes
Tickscript.propTypes = {
logs: arrayOf(shape()).isRequired,
onSave: func.isRequired,
onExit: func.isRequired,
source: shape({
id: string,
}),
areLogsVisible: bool,
areLogsEnabled: bool,
onToggleLogsVisbility: func.isRequired,
onToggleLogsVisibility: func.isRequired,
task: shape({
id: string,
script: string,
@ -66,10 +74,11 @@ Tickscript.propTypes = {
}).isRequired,
onChangeScript: func.isRequired,
onSelectDbrps: func.isRequired,
validation: string,
consoleMessage: string,
onChangeType: func.isRequired,
onChangeID: func.isRequired,
isNewTickscript: bool.isRequired,
unsavedChanges: bool,
}
export default Tickscript

View File

@ -1,22 +1,31 @@
import React, {PropTypes} from 'react'
const TickscriptEditorConsole = ({validation}) =>
<div className="tickscript-console">
<div className="tickscript-console--output">
{validation
? <p>
{validation}
</p>
: <p className="tickscript-console--default">
Save your TICKscript to validate it
</p>}
</div>
</div>
const TickscriptEditorConsole = ({consoleMessage, unsavedChanges}) => {
let consoleOutput = 'TICKscript is valid'
let consoleClass = 'tickscript-console--valid'
const {string} = PropTypes
if (consoleMessage) {
consoleOutput = consoleMessage
consoleClass = 'tickscript-console--error'
} else if (unsavedChanges) {
consoleOutput = 'You have unsaved changes, save to validate TICKscript'
consoleClass = 'tickscript-console--default'
}
return (
<div className="tickscript-console">
<p className={consoleClass}>
{consoleOutput}
</p>
</div>
)
}
const {bool, string} = PropTypes
TickscriptEditorConsole.propTypes = {
validation: string,
consoleMessage: string,
unsavedChanges: bool,
}
export default TickscriptEditorConsole

View File

@ -2,14 +2,17 @@ import React, {PropTypes} from 'react'
import SourceIndicator from 'shared/components/SourceIndicator'
import LogsToggle from 'src/kapacitor/components/LogsToggle'
import ConfirmButton from 'src/shared/components/ConfirmButton'
const TickscriptHeader = ({
task: {id},
onSave,
onExit,
unsavedChanges,
areLogsVisible,
areLogsEnabled,
isNewTickscript,
onToggleLogsVisbility,
onToggleLogsVisibility,
}) =>
<div className="page-header full-width">
<div className="page-header__container">
@ -20,18 +23,40 @@ const TickscriptHeader = ({
<LogsToggle
areLogsVisible={areLogsVisible}
areLogsEnabled={areLogsEnabled}
onToggleLogsVisbility={onToggleLogsVisbility}
onToggleLogsVisibility={onToggleLogsVisibility}
/>}
<div className="page-header__right">
<SourceIndicator />
<button
className="btn btn-success btn-sm"
title={id ? '' : 'ID your TICKscript to save'}
onClick={onSave}
disabled={!id}
>
{isNewTickscript ? 'Save New TICKscript' : 'Save TICKscript'}
</button>
{isNewTickscript
? <button
className="btn btn-success btn-sm"
title="Name your TICKscript to save"
onClick={onSave}
disabled={!id}
>
Save New TICKscript
</button>
: <button
className="btn btn-success btn-sm"
title="You have unsaved changes"
onClick={onSave}
disabled={!unsavedChanges}
>
Save Changes
</button>}
{unsavedChanges
? <ConfirmButton
text="Exit"
confirmText="Discard unsaved changes?"
confirmAction={onExit}
/>
: <button
className="btn btn-default btn-sm"
title="Return to Alert Rules"
onClick={onExit}
>
Exit
</button>}
</div>
</div>
</div>
@ -41,9 +66,10 @@ const {arrayOf, bool, func, shape, string} = PropTypes
TickscriptHeader.propTypes = {
isNewTickscript: bool,
onSave: func,
onExit: func.isRequired,
areLogsVisible: bool,
areLogsEnabled: bool,
onToggleLogsVisbility: func.isRequired,
onToggleLogsVisibility: func.isRequired,
task: shape({
dbrps: arrayOf(
shape({
@ -52,6 +78,7 @@ TickscriptHeader.propTypes = {
})
),
}),
unsavedChanges: bool,
}
export default TickscriptHeader

View File

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

View File

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

View File

@ -12,10 +12,11 @@ class OpsGenieConfig extends Component {
this.state = {
currentTeams: teams || [],
currentRecipients: recipients || [],
testEnabled: this.props.enabled,
}
}
handleSaveAlert = e => {
handleSubmit = e => {
e.preventDefault()
const properties = {
@ -25,6 +26,11 @@ class OpsGenieConfig extends Component {
}
this.props.onSave(properties)
this.setState({testEnabled: true})
}
disableTest = () => {
this.setState({testEnabled: false})
}
handleAddTeam = team => {
@ -59,13 +65,14 @@ 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}
/>
</div>
@ -74,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>
@ -103,6 +125,8 @@ OpsGenieConfig.propTypes = {
}).isRequired,
}).isRequired,
onSave: func.isRequired,
onTest: func.isRequired,
enabled: bool.isRequired,
}
class TagInput extends Component {
@ -121,6 +145,7 @@ class TagInput extends Component {
this.input.value = ''
onAddTag(newItem)
this.props.disableTest()
}
}
@ -156,6 +181,7 @@ TagInput.propTypes = {
onDeleteTag: func.isRequired,
tags: arrayOf(string).isRequired,
title: string.isRequired,
disableTest: func.isRequired,
}
const Tags = ({tags, onDeleteTag}) =>

View File

@ -4,9 +4,12 @@ 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 = {
@ -15,6 +18,11 @@ class PagerDutyConfig extends Component {
}
this.props.onSave(properties)
this.setState({testEnabled: true})
}
disableTest = () => {
this.setState({testEnabled: false})
}
render() {
@ -23,13 +31,14 @@ class PagerDutyConfig extends Component {
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>
<RedactedInput
defaultValue={serviceKey || ''}
id="service-key"
refFunc={refFunc}
disableTest={this.disableTest}
/>
</div>
@ -41,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>
@ -64,6 +87,8 @@ PagerDutyConfig.propTypes = {
}).isRequired,
}).isRequired,
onSave: func.isRequired,
onTest: func.isRequired,
enabled: bool.isRequired,
}
export default PagerDutyConfig

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -24,11 +24,12 @@ class TickscriptPage extends Component {
dbrps: [],
type: 'stream',
},
validation: '',
consoleMessage: '',
isEditingID: true,
logs: [],
areLogsEnabled: false,
failStr: '',
unsavedChanges: false,
}
}
@ -172,9 +173,10 @@ class TickscriptPage extends Component {
} else {
response = await createTask(kapacitor, task, router, sourceID)
}
if (response && response.code === 500) {
return this.setState({validation: response.message})
if (response.code) {
this.setState({unsavedChanges: true, consoleMessage: response.message})
} else {
this.setState({unsavedChanges: false, consoleMessage: ''})
}
} catch (error) {
console.error(error)
@ -182,37 +184,57 @@ class TickscriptPage extends Component {
}
}
handleExit = () => {
const {source: {id: sourceID}, router} = this.props
return router.push(`/sources/${sourceID}/alert-rules`)
}
handleChangeScript = tickscript => {
this.setState({task: {...this.state.task, tickscript}})
this.setState({
task: {...this.state.task, tickscript},
unsavedChanges: true,
})
}
handleSelectDbrps = dbrps => {
this.setState({task: {...this.state.task, dbrps}})
this.setState({task: {...this.state.task, dbrps}, unsavedChanges: true})
}
handleChangeType = type => () => {
this.setState({task: {...this.state.task, type}})
this.setState({task: {...this.state.task, type}, unsavedChanges: true})
}
handleChangeID = e => {
this.setState({task: {...this.state.task, id: e.target.value}})
this.setState({
task: {...this.state.task, id: e.target.value},
unsavedChanges: true,
})
}
handleToggleLogsVisbility = () => {
handleToggleLogsVisibility = () => {
this.setState({areLogsVisible: !this.state.areLogsVisible})
}
render() {
const {source} = this.props
const {task, validation, logs, areLogsVisible, areLogsEnabled} = this.state
const {
task,
logs,
areLogsVisible,
areLogsEnabled,
unsavedChanges,
consoleMessage,
} = this.state
return (
<Tickscript
task={task}
logs={logs}
source={source}
validation={validation}
consoleMessage={consoleMessage}
onSave={this.handleSave}
unsavedChanges={unsavedChanges}
onExit={this.handleExit}
isNewTickscript={!this._isEditing()}
onSelectDbrps={this.handleSelectDbrps}
onChangeScript={this.handleChangeScript}
@ -220,7 +242,7 @@ class TickscriptPage extends Component {
onChangeID={this.handleChangeID}
areLogsVisible={areLogsVisible}
areLogsEnabled={areLogsEnabled}
onToggleLogsVisbility={this.handleToggleLogsVisbility}
onToggleLogsVisibility={this.handleToggleLogsVisibility}
/>
)
}

View File

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

View File

@ -0,0 +1,110 @@
import React, {Component, PropTypes} from 'react'
import OnClickOutside from 'shared/components/OnClickOutside'
class ConfirmButton extends Component {
constructor(props) {
super(props)
this.state = {
expanded: false,
}
}
handleButtonClick = () => {
if (this.props.disabled) {
return
}
this.setState({expanded: !this.state.expanded})
}
handleConfirmClick = () => {
this.setState({expanded: false})
this.props.confirmAction()
}
handleClickOutside = () => {
this.setState({expanded: false})
}
calculatePosition = () => {
if (!this.buttonDiv || !this.tooltipDiv) {
return ''
}
const windowWidth = window.innerWidth
const buttonRect = this.buttonDiv.getBoundingClientRect()
const tooltipRect = this.tooltipDiv.getBoundingClientRect()
const rightGap = windowWidth - buttonRect.right
if (tooltipRect.width / 2 > rightGap) {
return 'left'
}
return 'bottom'
}
render() {
const {
text,
confirmText,
type,
size,
square,
icon,
disabled,
customClass,
} = this.props
const {expanded} = this.state
const customClassString = customClass ? ` ${customClass}` : ''
const squareString = square ? ' btn-square' : ''
const expandedString = expanded ? ' active' : ''
const disabledString = disabled ? ' disabled' : ''
const classname = `confirm-button btn ${type} ${size}${customClassString}${squareString}${expandedString}${disabledString}`
return (
<div
className={classname}
onClick={this.handleButtonClick}
ref={r => (this.buttonDiv = r)}
>
{icon && <span className={`icon ${icon}`} />}
{text && text}
<div className={`confirm-button--tooltip ${this.calculatePosition()}`}>
<div
className="confirm-button--confirmation"
onClick={this.handleConfirmClick}
ref={r => (this.tooltipDiv = r)}
>
{confirmText}
</div>
</div>
</div>
)
}
}
const {bool, func, string} = PropTypes
ConfirmButton.defaultProps = {
confirmText: 'Confirm',
type: 'btn-default',
size: 'btn-sm',
square: false,
}
ConfirmButton.propTypes = {
text: string,
confirmText: string,
confirmAction: func.isRequired,
type: string,
size: string,
square: bool,
icon: string,
disabled: bool,
customClass: string,
}
export default OnClickOutside(ConfirmButton)

View File

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

View File

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

View File

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

View File

@ -28,6 +28,7 @@
// Components
@import 'components/ceo-display-options';
@import 'components/confirm-button';
@import 'components/confirm-buttons';
@import 'components/code-mirror-theme';
@import 'components/color-dropdown';

View File

@ -0,0 +1,79 @@
/*
Confirm Button
----------------------------------------------------------------------------
This button requires a second click to confirm the action
*/
.confirm-button {
.confirm-button--tooltip {
visibility: hidden;
transition: all;
position: absolute;
&.bottom {
top: calc(100% + 4px);
left: 50%;
transform: translateX(-50%);
}
&.left {
top: 50%;
right: calc(100% + 4px);
transform: translateY(-50%);
}
}
}
.confirm-button--confirmation {
border-radius: 3px;
background-color: $c-curacao;
opacity: 0;
padding: 0 7px;
color: $g20-white;
font-size: 13px;
font-weight: 600;
text-align: center;
transition: opacity 0.25s ease, background-color 0.25s ease;
&:after {
content: '';
border: 8px solid transparent;
position: absolute;
transition: border-color 0.25s ease;
z-index: 100;
}
&:hover {
background-color: $c-dreamsicle;
cursor: pointer;
}
}
.confirm-button--tooltip.bottom .confirm-button--confirmation:after {
bottom: 100%;
left: 50%;
border-bottom-color: $c-curacao;
transform: translateX(-50%);
}
.confirm-button--tooltip.bottom .confirm-button--confirmation:hover:after {
border-bottom-color: $c-dreamsicle;
}
.confirm-button--tooltip.left .confirm-button--confirmation:after {
left: 100%;
top: 50%;
border-left-color: $c-curacao;
transform: translateY(-50%);
}
.confirm-button--tooltip.left .confirm-button--confirmation:hover:after {
border-left-color: $c-dreamsicle;
}
.confirm-button.active {
z-index: 999;
.confirm-button--tooltip {
visibility: visible;
}
.confirm-button--confirmation {
opacity: 1;
}
}

View File

@ -3,28 +3,26 @@
----------------------------------------------------------------------------
*/
$tickscript-console-height: 60px;
$tickscript-controls-height: 60px;
.tickscript {
flex: 1 0 0;
position: relative;
}
.tickscript-controls,
.tickscript-console,
.tickscript-editor {
padding: 0;
margin: 0;
width: 100%;
position: relative;
}
.tickscript-controls,
.tickscript-console {
height: $tickscript-console-height;
.tickscript-console,
.tickscript-controls {
padding: 0 60px;
display: flex;
}
.tickscript-controls {
display: flex;
align-items: center;
height: $tickscript-controls-height;
justify-content: space-between;
padding: 0 60px;
background-color: $g3-castle;
}
.tickscript-controls--name {
@ -42,29 +40,42 @@ $tickscript-console-height: 60px;
> * {margin-left: 8px;}
}
.tickscript-console--output {
padding: 0 60px;
font-family: $code-font;
font-weight: 600;
display: flex;
align-items: center;
background-color: $g2-kevlar;
border-bottom: 2px solid $g3-castle;
position: relative;
height: 100%;
width: 100%;
border-radius: $radius $radius 0 0;
.tickscript-console {
align-items: flex-start;
height: $tickscript-controls-height * 2.25;
border-top: 2px solid $g3-castle;
background-color: $g0-obsidian;
overflow-y: scroll;
@include custom-scrollbar($g0-obsidian,$g4-onyx);
> p {
margin: 0;
position: relative;
padding-left: 16px;
font-family: $code-font;
margin: 11px 0;
font-weight: 700;
word-wrap: break-word;
word-break: break-word;
&:before {
content: '>';
position: absolute;
top: 0;
left: 0;
}
}
}
.tickscript-console--default {
color: $g10-wolf;
font-style: italic;
color: $g13-mist;
}
.tickscript-console--valid {
color: $c-rainforest;
}
.tickscript-console--error {
color: $c-dreamsicle;
}
.tickscript-editor {
height: calc(100% - #{$tickscript-console-height * 2});
height: calc(100% - #{$tickscript-controls-height * 3.25});
}
/*

View File

@ -6,7 +6,7 @@
margin: 0 0 0 4px;
}
.text-center .btn {
margin: 0 2px;
margin: 0 6px;
}
.default-source-label {
display: inline-block;