diff --git a/CHANGELOG.md b/CHANGELOG.md index 3d1e42314..07d636094 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ### Bug Fixes 1. [#1530](https://github.com/influxdata/chronograf/pull/1530): Update query config field ordering to always match input query +1. [#1535](https://github.com/influxdata/chronograf/pull/1535): Fix add field functions to existing Kapacitor rules ### Features @@ -9,6 +10,7 @@ 1. [#1508](https://github.com/influxdata/chronograf/pull/1508): The enter and escape keys now perform as expected when renaming dashboard headers. 1. [#1524](https://github.com/influxdata/chronograf/pull/1524): Rewrite UI copy in Kapacitor Node configuration to be more clear 1. [#1549](https://github.com/influxdata/chronograf/pull/1549): Reset graph zoom when a new time range is selected + 1. [#1544](https://github.com/influxdata/chronograf/pull/1544): Upgrade to new version of Influx Theme, remove excess stylesheets ## v1.3.1.0 [2017-05-22] @@ -35,6 +37,7 @@ In versions 1.3.1+, installing a new version of Chronograf automatically clears ### UI Improvements 1. [#1451](https://github.com/influxdata/chronograf/pull/1451): Refactor scrollbars to support non-webkit browsers + 1. [#1453](https://github.com/influxdata/chronograf/pull/1453): Increase the query builder's default height in cell editor mode and in the data explorer 1. [#1453](https://github.com/influxdata/chronograf/pull/1453): Give QueryMaker a greater initial height than Visualization 1. [#1475](https://github.com/influxdata/chronograf/pull/1475): Add ability to toggle visibility of the Template Control Bar 1. [#1464](https://github.com/influxdata/chronograf/pull/1464): Make the [template variables](https://docs.influxdata.com/chronograf/v1.3/guides/dashboard-template-variables/) manager more space efficient diff --git a/LICENSE_OF_DEPENDENCIES.md b/LICENSE_OF_DEPENDENCIES.md index ae0806a23..3217a2566 100644 --- a/LICENSE_OF_DEPENDENCIES.md +++ b/LICENSE_OF_DEPENDENCIES.md @@ -219,7 +219,7 @@ * base64-arraybuffer 0.1.2 [MIT](https://github.com/niklasvh/base64-arraybuffer) * base64-arraybuffer 0.1.5 [MIT](https://github.com/niklasvh/base64-arraybuffer) * base64-js 1.2.0 [MIT](http://github.com/beatgammit/base64-js) -* base64id 0.1.0 [Unknown](https://github.com/faeldt/base64id) +* base64id 0.1.0 [MIT](https://github.com/faeldt/base64id/blob/master/LICENSE) * batch 0.5.3 [MIT](https://github.com/visionmedia/batch) * bcrypt-pbkdf 1.0.0 [BSD-4-Clause]((none)) * benchmark 1.0.0 [MIT](https://github.com/bestiejs/benchmark.js) diff --git a/kapacitor/client.go b/kapacitor/client.go index 9846e6417..378f0efab 100644 --- a/kapacitor/client.go +++ b/kapacitor/client.go @@ -99,7 +99,7 @@ func (c *Client) Create(ctx context.Context, rule chronograf.AlertRule) (*Task, Href: task.Link.Href, HrefOutput: c.HrefOutput(kapaID), TICKScript: script, - Rule: rule, + Rule: c.Reverse(kapaID, script), }, nil } @@ -215,6 +215,22 @@ func (c *Client) All(ctx context.Context) (map[string]chronograf.AlertRule, erro return alerts, nil } +// Reverse builds a chronograf.AlertRule and its QueryConfig from a tickscript +func (c *Client) Reverse(id string, script chronograf.TICKScript) chronograf.AlertRule { + rule, err := Reverse(script) + if err != nil { + return chronograf.AlertRule{ + ID: id, + Name: id, + Query: nil, + TICKScript: script, + } + } + rule.ID = id + rule.TICKScript = script + return rule +} + // Get returns a single alert in kapacitor func (c *Client) Get(ctx context.Context, id string) (chronograf.AlertRule, error) { kapa, err := c.kapaClient(c.URL, c.Username, c.Password) @@ -228,18 +244,7 @@ func (c *Client) Get(ctx context.Context, id string) (chronograf.AlertRule, erro } script := chronograf.TICKScript(task.TICKscript) - rule, err := Reverse(script) - if err != nil { - return chronograf.AlertRule{ - ID: task.ID, - Name: task.ID, - Query: nil, - TICKScript: script, - }, nil - } - rule.ID = task.ID - rule.TICKScript = script - return rule, nil + return c.Reverse(task.ID, script), nil } // Update changes the tickscript of a given id. @@ -282,7 +287,7 @@ func (c *Client) Update(ctx context.Context, href string, rule chronograf.AlertR Href: task.Link.Href, HrefOutput: c.HrefOutput(task.ID), TICKScript: script, - Rule: rule, + Rule: c.Reverse(task.ID, script), }, nil } diff --git a/kapacitor/client_test.go b/kapacitor/client_test.go index a395250c1..7a1c0bcec 100644 --- a/kapacitor/client_test.go +++ b/kapacitor/client_test.go @@ -15,15 +15,15 @@ type MockKapa struct { ResTasks []client.Task Error error - client.CreateTaskOptions + *client.CreateTaskOptions client.Link *client.TaskOptions *client.ListTasksOptions - client.UpdateTaskOptions + *client.UpdateTaskOptions } func (m *MockKapa) CreateTask(opt client.CreateTaskOptions) (client.Task, error) { - m.CreateTaskOptions = opt + m.CreateTaskOptions = &opt return m.ResTask, m.Error } @@ -40,7 +40,9 @@ func (m *MockKapa) ListTasks(opt *client.ListTasksOptions) ([]client.Task, error func (m *MockKapa) UpdateTask(link client.Link, opt client.UpdateTaskOptions) (client.Task, error) { m.Link = link - m.UpdateTaskOptions = opt + if m.UpdateTaskOptions == nil { + m.UpdateTaskOptions = &opt + } return m.ResTask, m.Error } @@ -49,6 +51,13 @@ func (m *MockKapa) DeleteTask(link client.Link) error { return m.Error } +type MockID struct { + ID string +} + +func (m *MockID) Generate() (string, error) { + return m.ID, nil +} func TestClient_AllStatus(t *testing.T) { type fields struct { URL string @@ -160,9 +169,6 @@ func TestClient_AllStatus(t *testing.T) { if !reflect.DeepEqual(got, tt.want) { t.Errorf("Client.AllStatus() = %v, want %v", got, tt.want) } - if !reflect.DeepEqual(kapa.CreateTaskOptions, tt.createTaskOptions) { - t.Errorf("Client.AllStatus() = createTaskOptions %v, want %v", kapa.CreateTaskOptions, tt.createTaskOptions) - } if !reflect.DeepEqual(kapa.ListTasksOptions, tt.listTasksOptions) { t.Errorf("Client.AllStatus() = listTasksOptions %v, want %v", kapa.ListTasksOptions, tt.listTasksOptions) } @@ -438,9 +444,6 @@ trigger if !reflect.DeepEqual(got, tt.want) { t.Errorf("Client.All() = %#v, want %#v", got, tt.want) } - if !reflect.DeepEqual(kapa.CreateTaskOptions, tt.createTaskOptions) { - t.Errorf("Client.All() = createTaskOptions %v, want %v", kapa.CreateTaskOptions, tt.createTaskOptions) - } if !reflect.DeepEqual(kapa.ListTasksOptions, tt.listTasksOptions) { t.Errorf("Client.All() = listTasksOptions %v, want %v", kapa.ListTasksOptions, tt.listTasksOptions) } @@ -725,9 +728,6 @@ trigger if !reflect.DeepEqual(got, tt.want) { t.Errorf("Client.Get() =\n%#v\nwant\n%#v", got, tt.want) } - if !reflect.DeepEqual(kapa.CreateTaskOptions, tt.createTaskOptions) { - t.Errorf("Client.Get() = createTaskOptions %v, want %v", kapa.CreateTaskOptions, tt.createTaskOptions) - } if !reflect.DeepEqual(kapa.ListTasksOptions, tt.listTasksOptions) { t.Errorf("Client.Get() = listTasksOptions %v, want %v", kapa.ListTasksOptions, tt.listTasksOptions) } @@ -743,3 +743,410 @@ trigger }) } } + +func TestClient_updateStatus(t *testing.T) { + type fields struct { + URL string + Username string + Password string + ID chronograf.ID + Ticker chronograf.Ticker + kapaClient func(url, username, password string) (KapaClient, error) + } + type args struct { + ctx context.Context + href string + status client.TaskStatus + } + kapa := &MockKapa{} + tests := []struct { + name string + fields fields + args args + resTask client.Task + want *Task + resError error + wantErr bool + updateTaskOptions *client.UpdateTaskOptions + }{ + { + name: "disable alert rule", + fields: fields{ + kapaClient: func(url, username, password string) (KapaClient, error) { + return kapa, nil + }, + Ticker: &Alert{}, + }, + args: args{ + ctx: context.Background(), + href: "/kapacitor/v1/tasks/howdy", + status: client.Disabled, + }, + resTask: client.Task{ + ID: "howdy", + Status: client.Disabled, + Link: client.Link{ + Href: "/kapacitor/v1/tasks/howdy", + }, + }, + updateTaskOptions: &client.UpdateTaskOptions{ + TICKscript: "", + Status: client.Disabled, + }, + want: &Task{ + ID: "howdy", + Href: "/kapacitor/v1/tasks/howdy", + HrefOutput: "/kapacitor/v1/tasks/howdy/output", + Rule: chronograf.AlertRule{}, + }, + }, + { + name: "fail to enable alert rule", + fields: fields{ + kapaClient: func(url, username, password string) (KapaClient, error) { + return kapa, nil + }, + Ticker: &Alert{}, + }, + args: args{ + ctx: context.Background(), + href: "/kapacitor/v1/tasks/howdy", + status: client.Enabled, + }, + updateTaskOptions: &client.UpdateTaskOptions{ + TICKscript: "", + Status: client.Enabled, + }, + resError: fmt.Errorf("error"), + wantErr: true, + }, + { + name: "enable alert rule", + fields: fields{ + kapaClient: func(url, username, password string) (KapaClient, error) { + return kapa, nil + }, + Ticker: &Alert{}, + }, + args: args{ + ctx: context.Background(), + href: "/kapacitor/v1/tasks/howdy", + status: client.Enabled, + }, + resTask: client.Task{ + ID: "howdy", + Status: client.Enabled, + Link: client.Link{ + Href: "/kapacitor/v1/tasks/howdy", + }, + }, + updateTaskOptions: &client.UpdateTaskOptions{ + TICKscript: "", + Status: client.Enabled, + }, + want: &Task{ + ID: "howdy", + Href: "/kapacitor/v1/tasks/howdy", + HrefOutput: "/kapacitor/v1/tasks/howdy/output", + Rule: chronograf.AlertRule{}, + }, + }, + } + for _, tt := range tests { + kapa.ResTask = tt.resTask + kapa.Error = tt.resError + kapa.UpdateTaskOptions = nil + t.Run(tt.name, func(t *testing.T) { + c := &Client{ + URL: tt.fields.URL, + Username: tt.fields.Username, + Password: tt.fields.Password, + ID: tt.fields.ID, + Ticker: tt.fields.Ticker, + kapaClient: tt.fields.kapaClient, + } + got, err := c.updateStatus(tt.args.ctx, tt.args.href, tt.args.status) + if (err != nil) != tt.wantErr { + t.Errorf("Client.updateStatus() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("Client.updateStatus() = %v, want %v", got, tt.want) + } + if !reflect.DeepEqual(kapa.UpdateTaskOptions, tt.updateTaskOptions) { + t.Errorf("Client.updateStatus() = %v, want %v", kapa.UpdateTaskOptions, tt.updateTaskOptions) + } + }) + } +} + +func TestClient_Update(t *testing.T) { + type fields struct { + URL string + Username string + Password string + ID chronograf.ID + Ticker chronograf.Ticker + kapaClient func(url, username, password string) (KapaClient, error) + } + type args struct { + ctx context.Context + href string + rule chronograf.AlertRule + } + kapa := &MockKapa{} + tests := []struct { + name string + fields fields + args args + resTask client.Task + want *Task + resError error + wantErr bool + updateTaskOptions *client.UpdateTaskOptions + }{ + { + name: "update alert rule error", + fields: fields{ + kapaClient: func(url, username, password string) (KapaClient, error) { + return kapa, nil + }, + Ticker: &Alert{}, + }, + args: args{ + ctx: context.Background(), + href: "/kapacitor/v1/tasks/howdy", + rule: chronograf.AlertRule{ + ID: "howdy", + Query: &chronograf.QueryConfig{ + Database: "db", + RetentionPolicy: "rp", + }, + }, + }, + resError: fmt.Errorf("error"), + updateTaskOptions: &client.UpdateTaskOptions{ + TICKscript: "", + Type: client.StreamTask, + Status: client.Disabled, + DBRPs: []client.DBRP{ + { + Database: "db", + RetentionPolicy: "rp", + }, + }, + }, + wantErr: true, + }, + { + name: "update alert rule", + fields: fields{ + kapaClient: func(url, username, password string) (KapaClient, error) { + return kapa, nil + }, + Ticker: &Alert{}, + }, + args: args{ + ctx: context.Background(), + href: "/kapacitor/v1/tasks/howdy", + rule: chronograf.AlertRule{ + ID: "howdy", + Query: &chronograf.QueryConfig{ + Database: "db", + RetentionPolicy: "rp", + }, + }, + }, + resTask: client.Task{ + ID: "howdy", + Status: client.Enabled, + Link: client.Link{ + Href: "/kapacitor/v1/tasks/howdy", + }, + }, + updateTaskOptions: &client.UpdateTaskOptions{ + TICKscript: "", + Type: client.StreamTask, + Status: client.Disabled, + DBRPs: []client.DBRP{ + { + Database: "db", + RetentionPolicy: "rp", + }, + }, + }, + want: &Task{ + ID: "howdy", + Href: "/kapacitor/v1/tasks/howdy", + HrefOutput: "/kapacitor/v1/tasks/howdy/output", + Rule: chronograf.AlertRule{ + ID: "howdy", + Name: "howdy", + }, + }, + }, + } + for _, tt := range tests { + kapa.ResTask = tt.resTask + kapa.Error = tt.resError + t.Run(tt.name, func(t *testing.T) { + c := &Client{ + URL: tt.fields.URL, + Username: tt.fields.Username, + Password: tt.fields.Password, + ID: tt.fields.ID, + Ticker: tt.fields.Ticker, + kapaClient: tt.fields.kapaClient, + } + got, err := c.Update(tt.args.ctx, tt.args.href, tt.args.rule) + if (err != nil) != tt.wantErr { + t.Errorf("Client.Update() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("Client.Update() =\n%#+v\n, want\n%#+v\n", got, tt.want) + } + if !reflect.DeepEqual(kapa.UpdateTaskOptions, tt.updateTaskOptions) { + t.Errorf("Client.Update() = %v, want %v", kapa.UpdateTaskOptions, tt.updateTaskOptions) + } + }) + } +} + +func TestClient_Create(t *testing.T) { + type fields struct { + URL string + Username string + Password string + ID chronograf.ID + Ticker chronograf.Ticker + kapaClient func(url, username, password string) (KapaClient, error) + } + type args struct { + ctx context.Context + rule chronograf.AlertRule + } + kapa := &MockKapa{} + tests := []struct { + name string + fields fields + args args + resTask client.Task + want *Task + resError error + wantErr bool + createTaskOptions *client.CreateTaskOptions + }{ + { + name: "create alert rule", + fields: fields{ + kapaClient: func(url, username, password string) (KapaClient, error) { + return kapa, nil + }, + Ticker: &Alert{}, + ID: &MockID{ + ID: "howdy", + }, + }, + args: args{ + ctx: context.Background(), + rule: chronograf.AlertRule{ + ID: "howdy", + Query: &chronograf.QueryConfig{ + Database: "db", + RetentionPolicy: "rp", + }, + }, + }, + resTask: client.Task{ + ID: "chronograf-v1-howdy", + Status: client.Enabled, + Link: client.Link{ + Href: "/kapacitor/v1/tasks/chronograf-v1-howdy", + }, + }, + createTaskOptions: &client.CreateTaskOptions{ + TICKscript: "", + 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{ + ID: "chronograf-v1-howdy", + Name: "chronograf-v1-howdy", + }, + }, + }, + { + name: "create alert rule", + fields: fields{ + kapaClient: func(url, username, password string) (KapaClient, error) { + return kapa, nil + }, + Ticker: &Alert{}, + ID: &MockID{ + ID: "howdy", + }, + }, + args: args{ + ctx: context.Background(), + rule: chronograf.AlertRule{ + ID: "howdy", + Query: &chronograf.QueryConfig{ + Database: "db", + RetentionPolicy: "rp", + }, + }, + }, + resError: fmt.Errorf("error"), + createTaskOptions: &client.CreateTaskOptions{ + TICKscript: "", + ID: "chronograf-v1-howdy", + Type: client.StreamTask, + Status: client.Enabled, + DBRPs: []client.DBRP{ + { + Database: "db", + RetentionPolicy: "rp", + }, + }, + }, + wantErr: true, + }, + } + for _, tt := range tests { + kapa.ResTask = tt.resTask + kapa.Error = tt.resError + t.Run(tt.name, func(t *testing.T) { + c := &Client{ + URL: tt.fields.URL, + Username: tt.fields.Username, + Password: tt.fields.Password, + ID: tt.fields.ID, + Ticker: tt.fields.Ticker, + kapaClient: tt.fields.kapaClient, + } + got, err := c.Create(tt.args.ctx, tt.args.rule) + if (err != nil) != tt.wantErr { + t.Errorf("Client.Create() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("Client.Create() =\n%v\n, want\n%v\n", got, tt.want) + } + if !reflect.DeepEqual(kapa.CreateTaskOptions, tt.createTaskOptions) { + t.Errorf("Client.Create() = %v, want %v", kapa.CreateTaskOptions, tt.createTaskOptions) + } + }) + } +} diff --git a/server/kapacitors.go b/server/kapacitors.go index de6fef6be..488d6f586 100644 --- a/server/kapacitors.go +++ b/server/kapacitors.go @@ -398,6 +398,26 @@ func newAlertResponse(rule chronograf.AlertRule, tickScript chronograf.TICKScrip return res } +// ValidRuleRequest checks if the requested rule change is valid +func ValidRuleRequest(rule chronograf.AlertRule) error { + if rule.Query == nil { + return fmt.Errorf("invalid alert rule: no query defined") + } + var hasFuncs bool + for _, f := range rule.Query.Fields { + if len(f.Funcs) > 0 { + hasFuncs = true + break + } + } + // All kapacitor rules with functions must have a window that is applied + // every amount of time + if rule.Every == "" && hasFuncs { + return fmt.Errorf(`invalid alert rule: functions require an "every" window`) + } + return nil +} + // KapacitorRulesPut proxies PATCH to kapacitor func (h *Service) KapacitorRulesPut(w http.ResponseWriter, r *http.Request) { id, err := paramID("kid", r) @@ -451,8 +471,7 @@ func (h *Service) KapacitorRulesPut(w http.ResponseWriter, r *http.Request) { Error(w, http.StatusInternalServerError, err.Error(), h.Logger) return } - - res := newAlertResponse(req, task.TICKScript, task.Href, task.HrefOutput, "enabled", srv.SrcID, srv.ID) + res := newAlertResponse(task.Rule, task.TICKScript, task.Href, task.HrefOutput, "enabled", srv.SrcID, srv.ID) encodeJSON(w, http.StatusOK, res, h.Logger) } diff --git a/server/kapacitors_test.go b/server/kapacitors_test.go new file mode 100644 index 000000000..116aca560 --- /dev/null +++ b/server/kapacitors_test.go @@ -0,0 +1,56 @@ +package server + +import ( + "testing" + + "github.com/influxdata/chronograf" +) + +func TestValidRuleRequest(t *testing.T) { + tests := []struct { + name string + rule chronograf.AlertRule + wantErr bool + }{ + { + name: "No every with functions", + rule: chronograf.AlertRule{ + Query: &chronograf.QueryConfig{ + Fields: []chronograf.Field{ + { + Field: "oldmanpeabody", + Funcs: []string{"max"}, + }, + }, + }, + }, + wantErr: true, + }, + { + name: "With every", + rule: chronograf.AlertRule{ + Every: "10s", + Query: &chronograf.QueryConfig{ + Fields: []chronograf.Field{ + { + Field: "oldmanpeabody", + Funcs: []string{"max"}, + }, + }, + }, + }, + }, + { + name: "No query config", + rule: chronograf.AlertRule{}, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := ValidRuleRequest(tt.rule); (err != nil) != tt.wantErr { + t.Errorf("ValidRuleRequest() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/ui/spec/kapacitor/reducers/rulesSpec.js b/ui/spec/kapacitor/reducers/rulesSpec.js index 5016fd5b0..3f981aca8 100644 --- a/ui/spec/kapacitor/reducers/rulesSpec.js +++ b/ui/spec/kapacitor/reducers/rulesSpec.js @@ -4,6 +4,8 @@ import {ALERT_NODES_ACCESSORS} from 'src/kapacitor/constants' import { chooseTrigger, + addEvery, + removeEvery, updateRuleValues, updateDetails, updateMessage, @@ -38,6 +40,23 @@ describe('Kapacitor.Reducers.rules', () => { expect(newState[ruleID].values).to.equal(defaultRuleConfigs.threshold) }) + it('can update the every', () => { + const ruleID = 1 + const initialState = { + [ruleID]: { + id: ruleID, + queryID: 988, + every: null, + }, + } + + let newState = reducer(initialState, addEvery(ruleID, '30s')) + expect(newState[ruleID].every).to.equal('30s') + + newState = reducer(newState, removeEvery(ruleID)) + expect(newState[ruleID].every).to.equal(null) + }) + it('can update the values', () => { const ruleID = 1 const initialState = { @@ -50,11 +69,17 @@ describe('Kapacitor.Reducers.rules', () => { } const newDeadmanValues = {duration: '5m'} - const newState = reducer(initialState, updateRuleValues(ruleID, 'deadman', newDeadmanValues)) + const newState = reducer( + initialState, + updateRuleValues(ruleID, 'deadman', newDeadmanValues) + ) expect(newState[ruleID].values).to.equal(newDeadmanValues) const newRelativeValues = {func: 'max', change: 'change'} - const finalState = reducer(newState, updateRuleValues(ruleID, 'relative', newRelativeValues)) + const finalState = reducer( + newState, + updateRuleValues(ruleID, 'relative', newRelativeValues) + ) expect(finalState[ruleID].trigger).to.equal('relative') expect(finalState[ruleID].values).to.equal(newRelativeValues) }) @@ -110,7 +135,10 @@ describe('Kapacitor.Reducers.rules', () => { .services('a b c') ` - let newState = reducer(initialState, updateAlertNodes(ruleID, 'alerta', tickScript)) + let newState = reducer( + initialState, + updateAlertNodes(ruleID, 'alerta', tickScript) + ) 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]) @@ -184,7 +212,10 @@ describe('Kapacitor.Reducers.rules', () => { }, } - const newState = reducer(initialState, updateRuleStatusSuccess(ruleID, status)) + const newState = reducer( + initialState, + updateRuleStatusSuccess(ruleID, status) + ) expect(newState[ruleID].status).to.equal(status) }) }) diff --git a/ui/src/admin/components/AdminTabs.js b/ui/src/admin/components/AdminTabs.js index ea4f35ba0..12bcf06c8 100644 --- a/ui/src/admin/components/AdminTabs.js +++ b/ui/src/admin/components/AdminTabs.js @@ -96,7 +96,7 @@ const AdminTabs = ({ {tabs.map((t, i) => {tabs[i].type})} - + {tabs.map((t, i) => ( {t.component} ))} diff --git a/ui/src/admin/components/ChangePassRow.js b/ui/src/admin/components/ChangePassRow.js index 64982f6fc..751a5d596 100644 --- a/ui/src/admin/components/ChangePassRow.js +++ b/ui/src/admin/components/ChangePassRow.js @@ -48,17 +48,17 @@ class ChangePassRow extends Component { } render() { - const {user} = this.props + const {user, buttonSize} = this.props if (this.state.showForm) { return ( -
+
) } return ( - +
+ + Change + +
) } } -const {shape, func} = PropTypes +const {func, shape, string} = PropTypes ChangePassRow.propTypes = { user: shape().isRequired, onApply: func.isRequired, onEdit: func.isRequired, + buttonSize: string, } export default OnClickOutside(ChangePassRow) diff --git a/ui/src/admin/components/DatabaseManager.js b/ui/src/admin/components/DatabaseManager.js index 5d4921818..11a89dbf5 100644 --- a/ui/src/admin/components/DatabaseManager.js +++ b/ui/src/admin/components/DatabaseManager.js @@ -25,7 +25,7 @@ const DatabaseManager = ({ onDeleteRetentionPolicy, }) => { return ( -
+

{databases.length === 1 diff --git a/ui/src/admin/components/DatabaseRow.js b/ui/src/admin/components/DatabaseRow.js index 7e56eaa3b..b5d9f38b1 100644 --- a/ui/src/admin/components/DatabaseRow.js +++ b/ui/src/admin/components/DatabaseRow.js @@ -1,7 +1,9 @@ import React, {PropTypes, Component} from 'react' +import onClickOutside from 'react-onclickoutside' + import {formatRPDuration} from 'utils/formatting' import YesNoButtons from 'src/shared/components/YesNoButtons' -import onClickOutside from 'react-onclickoutside' +import {DATABASE_TABLE} from 'src/admin/constants/tableSizing' class DatabaseRow extends Component { constructor(props) { @@ -46,51 +48,55 @@ class DatabaseRow extends Component { {isNew - ?
- this.handleKeyDown(e, database)} - ref={r => (this.name = r)} - autoFocus={true} - /> -
- :
- {name} -
} + ? this.handleKeyDown(e, database)} + ref={r => (this.name = r)} + autoFocus={true} + spellCheck={false} + autoComplete={false} + /> + : name} - -
- this.handleKeyDown(e, database)} - ref={r => (this.duration = r)} - autoFocus={!isNew} - /> -
+ + this.handleKeyDown(e, database)} + ref={r => (this.duration = r)} + autoFocus={!isNew} + spellCheck={false} + autoComplete={false} + /> - -
- this.handleKeyDown(e, database)} - ref={r => (this.replication = r)} - /> -
- - + {isRFDisplayed + ? + this.handleKeyDown(e, database)} + ref={r => (this.replication = r)} + spellCheck={false} + autoComplete={false} + /> + + : null} + - {name} - {' '} + {`${name} `} {isDefault ? default : null} - {formattedDuration} + + {formattedDuration} + {isRFDisplayed - ? {replication} + ? + {replication} + : null} - + {isDeleting ? onDelete(database, retentionPolicy)} onCancel={this.handleEndDelete} + buttonSize="btn-xs" /> :


-
) diff --git a/ui/src/dashboards/components/DashboardHeader.js b/ui/src/dashboards/components/DashboardHeader.js index f914493cc..66b4839eb 100644 --- a/ui/src/dashboards/components/DashboardHeader.js +++ b/ui/src/dashboards/components/DashboardHeader.js @@ -35,10 +35,10 @@ const DashboardHeader = ({ type="button" data-toggle="dropdown" > - {buttonText} + {buttonText} -
    +
      {children}
} @@ -55,7 +55,7 @@ const DashboardHeader = ({ : null} {dashboard ?
{dashboards && dashboards.length - ? + ?
@@ -109,6 +109,7 @@ const DashboardsPage = React.createClass({ ))} diff --git a/ui/src/data_explorer/components/FieldList.js b/ui/src/data_explorer/components/FieldList.js index 45d8e4bb7..9b0f7add0 100644 --- a/ui/src/data_explorer/components/FieldList.js +++ b/ui/src/data_explorer/components/FieldList.js @@ -1,7 +1,8 @@ import React, {PropTypes} from 'react' import FieldListItem from 'src/data_explorer/components/FieldListItem' -import GroupByTimeDropdown from 'src/data_explorer/components/GroupByTimeDropdown' +import GroupByTimeDropdown + from 'src/data_explorer/components/GroupByTimeDropdown' import FancyScrollbar from 'shared/components/FancyScrollbar' import {showFieldKeys} from 'shared/apis/metaQuery' @@ -77,7 +78,7 @@ const FieldList = React.createClass({ }, render() { - const {query} = this.props + const {query, isKapacitorRule} = this.props const hasAggregates = query.fields.some(f => f.funcs && f.funcs.length) const hasGroupByTime = query.groupBy.time @@ -90,6 +91,7 @@ const FieldList = React.createClass({ isOpen={!hasGroupByTime} selected={query.groupBy.time} onChooseGroupByTime={this.handleGroupByTime} + isInRuleBuilder={isKapacitorRule} /> : null} diff --git a/ui/src/data_explorer/components/FieldListItem.js b/ui/src/data_explorer/components/FieldListItem.js index 002d92e83..d892210c6 100644 --- a/ui/src/data_explorer/components/FieldListItem.js +++ b/ui/src/data_explorer/components/FieldListItem.js @@ -56,7 +56,9 @@ const FieldListItem = React.createClass({ if (isKapacitorRule) { return (
@@ -66,14 +68,16 @@ const FieldListItem = React.createClass({ {isSelected ? - : null - } + : null}
) } @@ -81,7 +85,9 @@ const FieldListItem = React.createClass({ return (
@@ -89,13 +95,17 @@ const FieldListItem = React.createClass({ {fieldText} {isSelected - ?
+ ?
Functions
- : null - } + : null}
- {(isSelected && isOpen) + {isSelected && isOpen ? -
- Group by {selected || 'time'} - -
- -
+ ({ + ...groupBy, + text: groupBy.menuOption, + }))} + onChoose={onChooseGroupByTime} + selected={selected ? `Group by ${selected}` : 'Group by Time'} + /> ) }, }) diff --git a/ui/src/data_explorer/components/MeasurementList.js b/ui/src/data_explorer/components/MeasurementList.js index 8bf56c9de..fba817da4 100644 --- a/ui/src/data_explorer/components/MeasurementList.js +++ b/ui/src/data_explorer/components/MeasurementList.js @@ -96,6 +96,8 @@ const MeasurementList = React.createClass({ value={this.state.filterText} onChange={this.handleFilterText} onKeyUp={this.handleEscape} + spellCheck={false} + autoComplete={false} />
diff --git a/ui/src/data_explorer/components/QueryEditor.js b/ui/src/data_explorer/components/QueryEditor.js index bfd038e42..26129b0b7 100644 --- a/ui/src/data_explorer/components/QueryEditor.js +++ b/ui/src/data_explorer/components/QueryEditor.js @@ -254,7 +254,8 @@ class QueryEditor extends Component { items={QUERY_TEMPLATES} selected={'Query Templates'} onChoose={this.handleChooseTemplate} - className="query-editor--templates" + className="dropdown-140 query-editor--templates" + buttonSize="btn-xs" /> : null} @@ -270,7 +271,8 @@ class QueryEditor extends Component { items={QUERY_TEMPLATES} selected={'Query Templates'} onChoose={this.handleChooseTemplate} - className="query-editor--templates" + className="dropdown-140 query-editor--templates" + buttonSize="btn-xs" /> : null} @@ -300,7 +302,8 @@ class QueryEditor extends Component { items={QUERY_TEMPLATES} selected={'Query Templates'} onChoose={this.handleChooseTemplate} - className="query-editor--templates" + className="dropdown-140 query-editor--templates" + buttonSize="btn-xs" /> : null} diff --git a/ui/src/data_explorer/components/TagListItem.js b/ui/src/data_explorer/components/TagListItem.js index c3653e122..5575e6f98 100644 --- a/ui/src/data_explorer/components/TagListItem.js +++ b/ui/src/data_explorer/components/TagListItem.js @@ -66,6 +66,8 @@ const TagListItem = React.createClass({ value={this.state.filterText} onChange={this.handleFilterText} onKeyUp={this.handleEscape} + spellCheck={false} + autoComplete={false} /> @@ -107,7 +109,7 @@ const TagListItem = React.createClass({ {tagItemLabel}
(
{views.length - ?
    + ?
      {views.map(v => (
    • onToggleView(v)} - className={classnames('toggle-btn ', {active: view === v})} + className={classnames({active: view === v})} > - {v} + {_.upperFirst(v)}
    • ))}
    diff --git a/ui/src/hosts/components/HostsTable.js b/ui/src/hosts/components/HostsTable.js index 069482198..049ab6c9d 100644 --- a/ui/src/hosts/components/HostsTable.js +++ b/ui/src/hosts/components/HostsTable.js @@ -108,8 +108,6 @@ const HostsTable = React.createClass({ hostsTitle = 'Loading Hosts...' } else if (hostsError.length) { hostsTitle = 'There was a problem loading hosts' - } else if (hosts.length === 0) { - hostsTitle = 'No hosts found' } else if (hostCount === 1) { hostsTitle = `${hostCount} Host` } else { @@ -123,45 +121,52 @@ const HostsTable = React.createClass({
-
Name
- - - - - - - - - - - {sortedHosts.map(h => { - return - })} - -
this.updateSort('name')} - className={this.sortableClasses('name')} - > - Host - this.updateSort('deltaUptime')} - className={this.sortableClasses('deltaUptime')} - style={{width: '74px'}} - > - Status - this.updateSort('cpu')} - className={this.sortableClasses('cpu')} - style={{width: '70px'}} - > - CPU - this.updateSort('load')} - className={this.sortableClasses('load')} - style={{width: '68px'}} - > - Load - Apps
+ {hostCount > 0 && !hostsError.length + ? + + + + + + + + + + + + {sortedHosts.map(h => { + return + })} + +
this.updateSort('name')} + className={this.sortableClasses('name')} + > + Host + this.updateSort('deltaUptime')} + className={this.sortableClasses('deltaUptime')} + style={{width: '74px'}} + > + Status + this.updateSort('cpu')} + className={this.sortableClasses('cpu')} + style={{width: '70px'}} + > + CPU + this.updateSort('load')} + className={this.sortableClasses('load')} + style={{width: '68px'}} + > + Load + Apps
+ :
+

+ No Hosts found +

+
}
) diff --git a/ui/src/hosts/containers/HostPage.js b/ui/src/hosts/containers/HostPage.js index 459eccaf4..42d4be250 100644 --- a/ui/src/hosts/containers/HostPage.js +++ b/ui/src/hosts/containers/HostPage.js @@ -190,21 +190,20 @@ export const HostPage = React.createClass({ > {Object.keys(hosts).map((host, i) => { return ( -
  • - +
  • + {host}
  • ) })} - +
    {layouts.length > 0 ? this.renderLayouts(layouts) : ''}
    diff --git a/ui/src/kapacitor/actions/view/index.js b/ui/src/kapacitor/actions/view/index.js index b9f1afff4..f288869c5 100644 --- a/ui/src/kapacitor/actions/view/index.js +++ b/ui/src/kapacitor/actions/view/index.js @@ -71,6 +71,21 @@ export function chooseTrigger(ruleID, trigger) { } } +export const addEvery = (ruleID, frequency) => ({ + type: 'ADD_EVERY', + payload: { + ruleID, + frequency, + }, +}) + +export const removeEvery = ruleID => ({ + type: 'REMOVE_EVERY', + payload: { + ruleID, + }, +}) + export function updateRuleValues(ruleID, trigger, values) { return { type: 'UPDATE_RULE_VALUES', diff --git a/ui/src/kapacitor/components/DataSection.js b/ui/src/kapacitor/components/DataSection.js index 3684a6cfc..af70d3bec 100644 --- a/ui/src/kapacitor/components/DataSection.js +++ b/ui/src/kapacitor/components/DataSection.js @@ -6,6 +6,8 @@ import DatabaseList from '../../data_explorer/components/DatabaseList' import MeasurementList from '../../data_explorer/components/MeasurementList' import FieldList from '../../data_explorer/components/FieldList' +import {defaultEveryFrequency} from 'src/kapacitor/constants' + export const DataSection = React.createClass({ propTypes: { source: PropTypes.shape({ @@ -27,6 +29,8 @@ export const DataSection = React.createClass({ groupByTime: PropTypes.func.isRequired, toggleTagAcceptance: PropTypes.func.isRequired, }).isRequired, + onAddEvery: PropTypes.func.isRequired, + onRemoveEvery: PropTypes.func.isRequired, timeRange: PropTypes.shape({}).isRequired, }, @@ -53,6 +57,11 @@ export const DataSection = React.createClass({ handleToggleField(field) { this.props.actions.toggleField(this.props.query.id, field, true) + // Every is only added when a function has been added to a field. + // Here, the field is selected without a function. + this.props.onRemoveEvery() + // Because there are no functions there is no group by time. + this.props.actions.groupByTime(this.props.query.id, null) }, handleGroupByTime(time) { @@ -61,6 +70,7 @@ export const DataSection = React.createClass({ handleApplyFuncsToField(fieldFunc) { this.props.actions.applyFuncsToField(this.props.query.id, fieldFunc) + this.props.onAddEvery(defaultEveryFrequency) }, handleChooseTag(tag) { @@ -80,13 +90,13 @@ export const DataSection = React.createClass({ const statement = query.rawText || buildInfluxQLQuery({lower}, query) return ( -
    -

    Select a Time Series

    -
    -
    +      
    +

    Select a Time Series

    +
    +
                 
                   {statement || 'Build a query below'}
    diff --git a/ui/src/kapacitor/components/KapacitorForm.js b/ui/src/kapacitor/components/KapacitorForm.js
    index 328c233e2..d1444ffbf 100644
    --- a/ui/src/kapacitor/components/KapacitorForm.js
    +++ b/ui/src/kapacitor/components/KapacitorForm.js
    @@ -83,7 +83,7 @@ class KapacitorForm extends Component {