diff --git a/pkg/api/dashboard.go b/pkg/api/dashboard.go index 7b5c1f9f764..3ca7a406832 100644 --- a/pkg/api/dashboard.go +++ b/pkg/api/dashboard.go @@ -151,15 +151,13 @@ func PostDashboard(c *middleware.Context, cmd m.SaveDashboardCommand) { } if setting.AlertingEnabled { - saveAlertCommand := m.SaveAlertsCommand{ - DashboardId: cmd.Result.Id, - OrgId: c.OrgId, - UserId: c.UserId, - Alerts: alerting.ParseAlertsFromDashboard(&cmd), + alertCmd := alerting.UpdateDashboardAlertsCommand{ + OrgId: c.OrgId, + UserId: c.UserId, + Dashboard: cmd.Result, } - err = bus.Dispatch(&saveAlertCommand) - if err != nil { + if err := bus.Dispatch(&alertCmd); err != nil { c.JsonApiErr(500, "Failed to save alerts", err) return } diff --git a/pkg/models/alerts.go b/pkg/models/alerts.go index e5cc76d198c..2ba21fd17cf 100644 --- a/pkg/models/alerts.go +++ b/pkg/models/alerts.go @@ -14,6 +14,9 @@ type AlertRuleModel struct { Name string Description string State string + Scheduler int64 + Enabled bool + Frequency int Created time.Time Updated time.Time @@ -21,6 +24,8 @@ type AlertRuleModel struct { Expression *simplejson.Json } +type AlertRules []*AlertRuleModel + func (this AlertRuleModel) TableName() string { return "alert_rule" } @@ -83,7 +88,7 @@ type SaveAlertsCommand struct { UserId int64 OrgId int64 - Alerts []*AlertRuleModel + Alerts AlertRules } type DeleteAlertCommand struct { diff --git a/pkg/services/alerting/alert_rule.go b/pkg/services/alerting/alert_rule.go new file mode 100644 index 00000000000..867f88861e9 --- /dev/null +++ b/pkg/services/alerting/alert_rule.go @@ -0,0 +1,76 @@ +package alerting + +import ( + "fmt" + + "github.com/grafana/grafana/pkg/components/simplejson" + + m "github.com/grafana/grafana/pkg/models" +) + +type AlertRule struct { + Id int64 + OrgId int64 + DashboardId int64 + PanelId int64 + Frequency int64 + Name string + Description string + State string + Warning Level + Critical Level + Query AlertQuery + Transform string + TransformParams simplejson.Json + Transformer Transformer +} + +func NewAlertRuleFromDBModel(ruleDef *m.AlertRuleModel) (*AlertRule, error) { + model := &AlertRule{} + model.Id = ruleDef.Id + model.OrgId = ruleDef.OrgId + model.Name = ruleDef.Name + model.Description = ruleDef.Description + model.State = ruleDef.State + + critical := ruleDef.Expression.Get("critical") + model.Critical = Level{ + Operator: critical.Get("op").MustString(), + Level: critical.Get("level").MustFloat64(), + } + + warning := ruleDef.Expression.Get("warning") + model.Warning = Level{ + Operator: warning.Get("op").MustString(), + Level: warning.Get("level").MustFloat64(), + } + + model.Frequency = ruleDef.Expression.Get("frequency").MustInt64() + model.Transform = ruleDef.Expression.Get("transform").Get("type").MustString() + model.TransformParams = *ruleDef.Expression.Get("transform") + + if model.Transform == "aggregation" { + model.Transformer = &AggregationTransformer{ + Method: ruleDef.Expression.Get("transform").Get("method").MustString(), + } + } + + query := ruleDef.Expression.Get("query") + model.Query = AlertQuery{ + Query: query.Get("query").MustString(), + DatasourceId: query.Get("datasourceId").MustInt64(), + From: query.Get("from").MustString(), + To: query.Get("to").MustString(), + Aggregator: query.Get("agg").MustString(), + } + + if model.Query.Query == "" { + return nil, fmt.Errorf("missing query.query") + } + + if model.Query.DatasourceId == 0 { + return nil, fmt.Errorf("missing query.datasourceId") + } + + return model, nil +} diff --git a/pkg/services/alerting/commands.go b/pkg/services/alerting/commands.go new file mode 100644 index 00000000000..83d906f41fb --- /dev/null +++ b/pkg/services/alerting/commands.go @@ -0,0 +1,89 @@ +package alerting + +import ( + "fmt" + + "github.com/grafana/grafana/pkg/bus" + m "github.com/grafana/grafana/pkg/models" +) + +type UpdateDashboardAlertsCommand struct { + UserId int64 + OrgId int64 + Dashboard *m.Dashboard +} + +func init() { + bus.AddHandler("alerting", updateDashboardAlerts) +} + +func updateDashboardAlerts(cmd *UpdateDashboardAlertsCommand) error { + saveRulesCmd := m.SaveAlertsCommand{ + OrgId: cmd.OrgId, + UserId: cmd.UserId, + } + + extractor := NewAlertRuleExtractor(cmd.Dashboard, cmd.OrgId) + + rules, err := extractor.GetRuleModels() + if err != nil { + return err + } + + saveRulesCmd.Alerts = rules + if bus.Dispatch(&saveRulesCmd); err != nil { + return err + } + + return nil +} + +func ConvetAlertModelToAlertRule(ruleDef *m.AlertRuleModel) (*AlertRule, error) { + model := &AlertRule{} + model.Id = ruleDef.Id + model.OrgId = ruleDef.OrgId + model.Name = ruleDef.Name + model.Description = ruleDef.Description + model.State = ruleDef.State + + critical := ruleDef.Expression.Get("critical") + model.Critical = Level{ + Operator: critical.Get("op").MustString(), + Level: critical.Get("level").MustFloat64(), + } + + warning := ruleDef.Expression.Get("warning") + model.Warning = Level{ + Operator: warning.Get("op").MustString(), + Level: warning.Get("level").MustFloat64(), + } + + model.Frequency = ruleDef.Expression.Get("frequency").MustInt64() + model.Transform = ruleDef.Expression.Get("transform").Get("type").MustString() + model.TransformParams = *ruleDef.Expression.Get("transform") + + if model.Transform == "aggregation" { + model.Transformer = &AggregationTransformer{ + Method: ruleDef.Expression.Get("transform").Get("method").MustString(), + } + } + + query := ruleDef.Expression.Get("query") + model.Query = AlertQuery{ + Query: query.Get("query").MustString(), + DatasourceId: query.Get("datasourceId").MustInt64(), + From: query.Get("from").MustString(), + To: query.Get("to").MustString(), + Aggregator: query.Get("agg").MustString(), + } + + if model.Query.Query == "" { + return nil, fmt.Errorf("missing query.query") + } + + if model.Query.DatasourceId == 0 { + return nil, fmt.Errorf("missing query.datasourceId") + } + + return model, nil +} diff --git a/pkg/services/alerting/dashboard_parser.go b/pkg/services/alerting/dashboard_parser.go deleted file mode 100644 index c3c9b9b643b..00000000000 --- a/pkg/services/alerting/dashboard_parser.go +++ /dev/null @@ -1,135 +0,0 @@ -package alerting - -import ( - "fmt" - - "github.com/grafana/grafana/pkg/bus" - "github.com/grafana/grafana/pkg/components/simplejson" - "github.com/grafana/grafana/pkg/log" - m "github.com/grafana/grafana/pkg/models" -) - -func ParseAlertsFromDashboard(cmd *m.SaveDashboardCommand) []*m.AlertRuleModel { - alerts := make([]*m.AlertRuleModel, 0) - - for _, rowObj := range cmd.Dashboard.Get("rows").MustArray() { - row := simplejson.NewFromAny(rowObj) - - for _, panelObj := range row.Get("panels").MustArray() { - panel := simplejson.NewFromAny(panelObj) - - alerting := panel.Get("alerting") - alert := &m.AlertRuleModel{ - DashboardId: cmd.Result.Id, - OrgId: cmd.Result.OrgId, - PanelId: panel.Get("id").MustInt64(), - Id: alerting.Get("id").MustInt64(), - Name: alerting.Get("name").MustString(), - Description: alerting.Get("description").MustString(), - } - - log.Info("Alertrule: %v", alert.Name) - - valueQuery := alerting.Get("query") - valueQueryRef := valueQuery.Get("refId").MustString() - for _, targetsObj := range panel.Get("targets").MustArray() { - target := simplejson.NewFromAny(targetsObj) - - if target.Get("refId").MustString() == valueQueryRef { - datsourceName := "" - if target.Get("datasource").MustString() != "" { - datsourceName = target.Get("datasource").MustString() - } else if panel.Get("datasource").MustString() != "" { - datsourceName = panel.Get("datasource").MustString() - } - - if datsourceName == "" { - query := &m.GetDataSourcesQuery{OrgId: cmd.OrgId} - if err := bus.Dispatch(query); err == nil { - for _, ds := range query.Result { - if ds.IsDefault { - alerting.SetPath([]string{"query", "datasourceId"}, ds.Id) - } - } - } - } else { - query := &m.GetDataSourceByNameQuery{ - Name: panel.Get("datasource").MustString(), - OrgId: cmd.OrgId, - } - bus.Dispatch(query) - alerting.SetPath([]string{"query", "datasourceId"}, query.Result.Id) - } - - targetQuery := target.Get("target").MustString() - if targetQuery != "" { - alerting.SetPath([]string{"query", "query"}, targetQuery) - } - } - } - - alert.Expression = alerting - - _, err := ConvetAlertModelToAlertRule(alert) - - if err == nil && alert.ValidToSave() { - alerts = append(alerts, alert) - } else { - log.Error2("Failed to parse model from expression", "error", err) - } - - } - } - - return alerts -} - -func ConvetAlertModelToAlertRule(ruleDef *m.AlertRuleModel) (*AlertRule, error) { - model := &AlertRule{} - model.Id = ruleDef.Id - model.OrgId = ruleDef.OrgId - model.Name = ruleDef.Name - model.Description = ruleDef.Description - model.State = ruleDef.State - - critical := ruleDef.Expression.Get("critical") - model.Critical = Level{ - Operator: critical.Get("op").MustString(), - Level: critical.Get("level").MustFloat64(), - } - - warning := ruleDef.Expression.Get("warning") - model.Warning = Level{ - Operator: warning.Get("op").MustString(), - Level: warning.Get("level").MustFloat64(), - } - - model.Frequency = ruleDef.Expression.Get("frequency").MustInt64() - model.Transform = ruleDef.Expression.Get("transform").Get("type").MustString() - model.TransformParams = *ruleDef.Expression.Get("transform") - - if model.Transform == "aggregation" { - model.Transformer = &AggregationTransformer{ - Method: ruleDef.Expression.Get("transform").Get("method").MustString(), - } - } - - query := ruleDef.Expression.Get("query") - model.Query = AlertQuery{ - Query: query.Get("query").MustString(), - DatasourceId: query.Get("datasourceId").MustInt64(), - From: query.Get("from").MustString(), - To: query.Get("to").MustString(), - Aggregator: query.Get("agg").MustString(), - } - - if model.Query.Query == "" { - return nil, fmt.Errorf("missing query.query") - } - - if model.Query.DatasourceId == 0 { - return nil, fmt.Errorf("missing query.datasourceId") - } - - return model, nil -} diff --git a/pkg/services/alerting/extractor.go b/pkg/services/alerting/extractor.go new file mode 100644 index 00000000000..9731f1eb324 --- /dev/null +++ b/pkg/services/alerting/extractor.go @@ -0,0 +1,120 @@ +package alerting + +import ( + "errors" + + "github.com/grafana/grafana/pkg/bus" + "github.com/grafana/grafana/pkg/components/simplejson" + "github.com/grafana/grafana/pkg/log" + m "github.com/grafana/grafana/pkg/models" +) + +type AlertRuleExtractor struct { + Dash *m.Dashboard + OrgId int64 + log log.Logger +} + +func NewAlertRuleExtractor(dash *m.Dashboard, orgId int64) *AlertRuleExtractor { + return &AlertRuleExtractor{ + Dash: dash, + OrgId: orgId, + log: log.New("alerting.extractor"), + } +} + +func (e *AlertRuleExtractor) lookupDatasourceId(dsName string) (int64, error) { + if dsName == "" { + query := &m.GetDataSourcesQuery{OrgId: e.OrgId} + if err := bus.Dispatch(query); err != nil { + return 0, err + } else { + for _, ds := range query.Result { + if ds.IsDefault { + return ds.Id, nil + } + } + } + } else { + query := &m.GetDataSourceByNameQuery{Name: dsName, OrgId: e.OrgId} + if err := bus.Dispatch(query); err != nil { + return 0, err + } else { + return query.Result.Id, nil + } + } + + return 0, errors.New("Could not find datasource id for " + dsName) +} + +func (e *AlertRuleExtractor) GetRuleModels() (m.AlertRules, error) { + + rules := make(m.AlertRules, 0) + + for _, rowObj := range e.Dash.Data.Get("rows").MustArray() { + row := simplejson.NewFromAny(rowObj) + + for _, panelObj := range row.Get("panels").MustArray() { + panel := simplejson.NewFromAny(panelObj) + jsonRule := panel.Get("alerting") + + // check if marked for deletion + deleted := jsonRule.Get("deleted").MustBool() + if deleted { + e.log.Info("Deleted alert rule found") + continue + } + + ruleModel := &m.AlertRuleModel{ + DashboardId: e.Dash.Id, + OrgId: e.OrgId, + PanelId: panel.Get("id").MustInt64(), + Id: jsonRule.Get("id").MustInt64(), + Name: jsonRule.Get("name").MustString(), + Scheduler: jsonRule.Get("scheduler").MustInt64(), + Enabled: jsonRule.Get("enabled").MustBool(), + Description: jsonRule.Get("description").MustString(), + } + + valueQuery := jsonRule.Get("query") + valueQueryRef := valueQuery.Get("refId").MustString() + for _, targetsObj := range panel.Get("targets").MustArray() { + target := simplejson.NewFromAny(targetsObj) + + if target.Get("refId").MustString() == valueQueryRef { + dsName := "" + if target.Get("datasource").MustString() != "" { + dsName = target.Get("datasource").MustString() + } else if panel.Get("datasource").MustString() != "" { + dsName = panel.Get("datasource").MustString() + } + + if datasourceId, err := e.lookupDatasourceId(dsName); err != nil { + return nil, err + } else { + valueQuery.SetPath([]string{"datasourceId"}, datasourceId) + } + + targetQuery := target.Get("target").MustString() + if targetQuery != "" { + jsonRule.SetPath([]string{"query", "query"}, targetQuery) + } + } + } + + ruleModel.Expression = jsonRule + + // validate + _, err := NewAlertRuleFromDBModel(ruleModel) + if err == nil && ruleModel.ValidToSave() { + rules = append(rules, ruleModel) + } else { + e.log.Error("Failed to extract alert rules from dashboard", "error", err) + return nil, errors.New("Failed to extract alert rules from dashboard") + } + + } + } + + return rules, nil +} diff --git a/pkg/services/sqlstore/dashboard_parser_test.go b/pkg/services/alerting/extractor_test.go similarity index 63% rename from pkg/services/sqlstore/dashboard_parser_test.go rename to pkg/services/alerting/extractor_test.go index b7266a926c8..2f40c7e401c 100644 --- a/pkg/services/sqlstore/dashboard_parser_test.go +++ b/pkg/services/alerting/extractor_test.go @@ -1,17 +1,17 @@ -package sqlstore +package alerting import ( "testing" + "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/components/simplejson" m "github.com/grafana/grafana/pkg/models" - "github.com/grafana/grafana/pkg/services/alerting" . "github.com/smartystreets/goconvey/convey" ) -func TestAlertModelParsing(t *testing.T) { +func TestAlertRuleExtraction(t *testing.T) { - Convey("Parsing alert info from json", t, func() { + Convey("Parsing alert rules from dashboard json", t, func() { Convey("Parsing and validating alerts from dashboards", func() { json := `{ "id": 57, @@ -37,13 +37,15 @@ func TestAlertModelParsing(t *testing.T) { ], "datasource": null, "alerting": { - "name": "Alerting Panel Title alert", - "description": "description", + "name": "name1", + "description": "desc1", + "scheduler": 1, + "enabled": true, "critical": { "level": 20, "op": ">" }, - "frequency": 10, + "frequency": "60s", "query": { "from": "5m", "refId": "A", @@ -51,12 +53,12 @@ func TestAlertModelParsing(t *testing.T) { }, "transform": { "method": "avg", - "name": "aggregation" + "type": "aggregation" }, "warning": { "level": 10, "op": ">" - } + } } }, { @@ -70,13 +72,15 @@ func TestAlertModelParsing(t *testing.T) { ], "datasource": "graphite2", "alerting": { - "name": "Alerting Panel Title alert", - "description": "description", + "name": "name2", + "description": "desc2", + "scheduler": 0, + "enabled": true, "critical": { "level": 20, "op": ">" }, - "frequency": 10, + "frequency": "60s", "query": { "from": "5m", "refId": "A", @@ -145,7 +149,10 @@ func TestAlertModelParsing(t *testing.T) { ], "title": "Broken influxdb panel", "transform": "table", - "type": "table" + "type": "table", + "alerting": { + "deleted": true + } } ], "title": "New row" @@ -153,51 +160,62 @@ func TestAlertModelParsing(t *testing.T) { ] }` - dashboardJSON, _ := simplejson.NewJson([]byte(json)) - cmd := &m.SaveDashboardCommand{ - Dashboard: dashboardJSON, - UserId: 1, - OrgId: 1, - Overwrite: true, - Result: &m.Dashboard{ - Id: 1, - }, - } + dashJson, err := simplejson.NewJson([]byte(json)) + So(err, ShouldBeNil) - InitTestDB(t) + dash := m.NewDashboardFromJson(dashJson) + extractor := NewAlertRuleExtractor(dash, 1) - AddDataSource(&m.AddDataSourceCommand{ - Name: "graphite2", - OrgId: 1, - Type: m.DS_INFLUXDB, - Access: m.DS_ACCESS_DIRECT, - Url: "http://test", - IsDefault: false, - Database: "site", + // mock data + defaultDs := &m.DataSource{Id: 12, OrgId: 2, Name: "I am default", IsDefault: true} + graphite2Ds := &m.DataSource{Id: 15, OrgId: 2, Name: "graphite2"} + + bus.AddHandler("test", func(query *m.GetDataSourcesQuery) error { + query.Result = []*m.DataSource{defaultDs, graphite2Ds} + return nil }) - AddDataSource(&m.AddDataSourceCommand{ - Name: "InfluxDB", - OrgId: 1, - Type: m.DS_GRAPHITE, - Access: m.DS_ACCESS_DIRECT, - Url: "http://test", - IsDefault: true, + bus.AddHandler("test", func(query *m.GetDataSourceByNameQuery) error { + if query.Name == defaultDs.Name { + query.Result = defaultDs + } + if query.Name == graphite2Ds.Name { + query.Result = graphite2Ds + } + return nil }) - alerts := alerting.ParseAlertsFromDashboard(cmd) + alerts, err := extractor.GetRuleModels() + + Convey("Get rules without error", func() { + So(err, ShouldBeNil) + }) Convey("all properties have been set", func() { - So(alerts, ShouldNotBeEmpty) So(len(alerts), ShouldEqual, 2) for _, v := range alerts { - So(v.DashboardId, ShouldEqual, 1) - So(v.PanelId, ShouldNotEqual, 0) - + So(v.DashboardId, ShouldEqual, 57) So(v.Name, ShouldNotBeEmpty) So(v.Description, ShouldNotBeEmpty) } + + Convey("should extract scheduler property", func() { + So(alerts[0].Scheduler, ShouldEqual, 1) + So(alerts[1].Scheduler, ShouldEqual, 0) + }) + + Convey("should extract panel idc", func() { + So(alerts[0].PanelId, ShouldEqual, 3) + So(alerts[1].PanelId, ShouldEqual, 4) + }) + + Convey("should extract name and desc", func() { + So(alerts[0].Name, ShouldEqual, "name1") + So(alerts[0].Description, ShouldEqual, "desc1") + So(alerts[1].Name, ShouldEqual, "name2") + So(alerts[1].Description, ShouldEqual, "desc2") + }) }) }) }) diff --git a/pkg/services/alerting/models.go b/pkg/services/alerting/models.go index c13669ea3d7..364950ee9fa 100644 --- a/pkg/services/alerting/models.go +++ b/pkg/services/alerting/models.go @@ -1,9 +1,5 @@ package alerting -import ( - "github.com/grafana/grafana/pkg/components/simplejson" -) - type AlertJob struct { Offset int64 Delay bool @@ -21,23 +17,6 @@ type AlertResult struct { AlertJob *AlertJob } -type AlertRule struct { - Id int64 - OrgId int64 - DashboardId int64 - PanelId int64 - Frequency int64 - Name string - Description string - State string - Warning Level - Critical Level - Query AlertQuery - Transform string - TransformParams simplejson.Json - Transformer Transformer -} - type Level struct { Operator string Level float64 diff --git a/pkg/services/sqlstore/migrations/alert_mig.go b/pkg/services/sqlstore/migrations/alert_mig.go index 42cb9b78d39..855bd92b568 100644 --- a/pkg/services/sqlstore/migrations/alert_mig.go +++ b/pkg/services/sqlstore/migrations/alert_mig.go @@ -17,6 +17,9 @@ func addAlertMigrations(mg *Migrator) { {Name: "description", Type: DB_NVarchar, Length: 255, Nullable: false}, {Name: "state", Type: DB_NVarchar, Length: 255, Nullable: false}, {Name: "expression", Type: DB_Text, Nullable: false}, + {Name: "scheduler", Type: DB_BigInt, Nullable: false}, + {Name: "frequency", Type: DB_BigInt, Nullable: false}, + {Name: "enabled", Type: DB_Bool, Nullable: false}, {Name: "created", Type: DB_DateTime, Nullable: false}, {Name: "updated", Type: DB_DateTime, Nullable: false}, }, diff --git a/public/app/plugins/panel/graph/alert_tab_ctrl.ts b/public/app/plugins/panel/graph/alert_tab_ctrl.ts index 4f7e9d8d834..970e60d6d61 100644 --- a/public/app/plugins/panel/graph/alert_tab_ctrl.ts +++ b/public/app/plugins/panel/graph/alert_tab_ctrl.ts @@ -24,6 +24,7 @@ export class AlertTabCtrl { panelCtrl: any; alerting: any; metricTargets = [{ refId: '- select query -' } ]; + schedulers = [{text: 'Grafana', value: 1}, {text: 'External', value: 0}]; transforms = [ { text: 'Aggregation', @@ -33,24 +34,23 @@ export class AlertTabCtrl { text: 'Linear Forecast', type: 'forecast', }, - { - text: 'Percent Change', - type: 'percent_change', - }, - { - text: 'Query diff', - type: 'query_diff', - }, ]; aggregators = ['avg', 'sum', 'min', 'max', 'last']; rule: any; query: any; queryParams: any; transformDef: any; - trasnformQuery: any; + levelOpList = [ + {text: '>', value: '>'}, + {text: '<', value: '<'}, + {text: '=', value: '='}, + ]; defaultValues = { - frequency: 10, + frequency: '60s', + notify: [], + enabled: false, + scheduler: 1, warning: { op: '>', level: undefined }, critical: { op: '>', level: undefined }, query: { @@ -139,8 +139,18 @@ export class AlertTabCtrl { } } - markAsDeleted() { - this.panel.alerting = this.defaultValues; + delete() { + this.rule = this.panel.alerting = this.defaultValues; + this.rule.deleted = true; + } + + enable() { + delete this.rule.deleted; + this.rule.enabled = true; + } + + disable() { + this.rule.enabled = false; } thresholdsUpdated() { diff --git a/public/app/plugins/panel/graph/partials/tab_alerting.html b/public/app/plugins/panel/graph/partials/tab_alerting.html index ab688bb003e..06b8c35e632 100644 --- a/public/app/plugins/panel/graph/partials/tab_alerting.html +++ b/public/app/plugins/panel/graph/partials/tab_alerting.html @@ -1,15 +1,13 @@ - -
-
Alert Rule
+
+
+
Alert Query
- -
Transform using @@ -33,77 +31,86 @@
Timespan - +
-
+
Levels
- Warn if value + Warn if - - > - - + +
- Critcal if value + Critcal if - - > - - + +
+
- - - - - - - - - - - - - - - - - - - - - -
+
-
Alert info
-
- Alert name - -
+
Execution
- Alert description + Scheduler +
+ +
- + Evaluate every + +
+
+
+
+
Notifications
+
+
+ Groups + +
-
-
- + + +
+
Information
+
+ Alert name + +
+
+
+ Alert description +
+
+ +
+
+
+ +
+
+ + +
diff --git a/public/sass/components/_tagsinput.scss b/public/sass/components/_tagsinput.scss index 8092446e2e5..698302e6f1e 100644 --- a/public/sass/components/_tagsinput.scss +++ b/public/sass/components/_tagsinput.scss @@ -7,6 +7,7 @@ background-color: $input-bg; input { + display: inline-block; border: none; border-right: 1px solid $tight-form-border; margin: 0px; diff --git a/public/vendor/tagsinput/bootstrap-tagsinput.js b/public/vendor/tagsinput/bootstrap-tagsinput.js index b3a3c3bc386..702b6416962 100644 --- a/public/vendor/tagsinput/bootstrap-tagsinput.js +++ b/public/vendor/tagsinput/bootstrap-tagsinput.js @@ -35,7 +35,7 @@ this.inputSize = Math.max(1, this.placeholderText.length); this.$container = $('
'); - this.$input = $('').appendTo(this.$container); this.$element.after(this.$container);