diff --git a/pkg/services/alerting/alert_rule.go b/pkg/services/alerting/alert_rule.go index 7c9dee551e8..26d8fab092a 100644 --- a/pkg/services/alerting/alert_rule.go +++ b/pkg/services/alerting/alert_rule.go @@ -26,6 +26,10 @@ type AlertRule struct { Transformer transformer.Transformer } +func getTimeDurationStringToSeconds(str string) int64 { + return 60 +} + func NewAlertRuleFromDBModel(ruleDef *m.Alert) (*AlertRule, error) { model := &AlertRule{} model.Id = ruleDef.Id @@ -40,13 +44,13 @@ func NewAlertRuleFromDBModel(ruleDef *m.Alert) (*AlertRule, error) { Level: critical.Get("level").MustFloat64(), } - warning := ruleDef.Expression.Get("warning") + warning := ruleDef.Expression.Get("warn") model.Warning = Level{ Operator: warning.Get("op").MustString(), Level: warning.Get("level").MustFloat64(), } - model.Frequency = ruleDef.Expression.Get("frequency").MustInt64() + model.Frequency = getTimeDurationStringToSeconds(ruleDef.Expression.Get("frequency").MustString()) model.Transform = ruleDef.Expression.Get("transform").Get("type").MustString() model.TransformParams = *ruleDef.Expression.Get("transform") diff --git a/pkg/services/alerting/commands.go b/pkg/services/alerting/commands.go index 9577366afaf..4e269aca695 100644 --- a/pkg/services/alerting/commands.go +++ b/pkg/services/alerting/commands.go @@ -1,11 +1,8 @@ package alerting import ( - "fmt" - "github.com/grafana/grafana/pkg/bus" m "github.com/grafana/grafana/pkg/models" - "github.com/grafana/grafana/pkg/services/alerting/transformers" ) type UpdateDashboardAlertsCommand struct { @@ -39,56 +36,3 @@ func updateDashboardAlerts(cmd *UpdateDashboardAlertsCommand) error { return nil } - -func getTimeDurationStringToSeconds(str string) int64 { - return 60 -} - -func ConvetAlertModelToAlertRule(ruleDef *m.Alert) (*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 = getTimeDurationStringToSeconds(ruleDef.Expression.Get("frequency").MustString()) - model.Transform = ruleDef.Expression.Get("transform").Get("type").MustString() - model.TransformParams = *ruleDef.Expression.Get("transform") - - if model.Transform == "aggregation" { - method := ruleDef.Expression.Get("transform").Get("method").MustString() - model.Transformer = transformer.NewAggregationTransformer(method) - } - - 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 index 6a0883c16fa..ae360973fad 100644 --- a/pkg/services/alerting/extractor.go +++ b/pkg/services/alerting/extractor.go @@ -57,12 +57,9 @@ func (e *DashAlertExtractor) GetAlerts() ([]*m.Alert, error) { for _, panelObj := range row.Get("panels").MustArray() { panel := simplejson.NewFromAny(panelObj) - jsonAlert := panel.Get("alert") + jsonAlert, hasAlert := panel.CheckGet("alert") - // check if marked for deletion - deleted := jsonAlert.Get("deleted").MustBool() - if deleted { - e.log.Info("Deleted alert rule found") + if !hasAlert { continue } diff --git a/pkg/services/alerting/extractor_test.go b/pkg/services/alerting/extractor_test.go index 069489dfb23..7cab1d94c7c 100644 --- a/pkg/services/alerting/extractor_test.go +++ b/pkg/services/alerting/extractor_test.go @@ -55,7 +55,7 @@ func TestAlertRuleExtraction(t *testing.T) { "method": "avg", "type": "aggregation" }, - "warning": { + "warn": { "level": 10, "op": ">" } @@ -90,7 +90,7 @@ func TestAlertRuleExtraction(t *testing.T) { "method": "avg", "name": "aggregation" }, - "warning": { + "warn": { "level": 10, "op": ">" } @@ -149,10 +149,7 @@ func TestAlertRuleExtraction(t *testing.T) { ], "title": "Broken influxdb panel", "transform": "table", - "type": "table", - "alert": { - "deleted": true - } + "type": "table" } ], "title": "New row" @@ -185,7 +182,7 @@ func TestAlertRuleExtraction(t *testing.T) { return nil }) - alerts, err := extractor.GetRuleModels() + alerts, err := extractor.GetAlerts() Convey("Get rules without error", func() { So(err, ShouldBeNil) diff --git a/pkg/services/alerting/reader.go b/pkg/services/alerting/reader.go index 7f8f6b2c5de..db7da930746 100644 --- a/pkg/services/alerting/reader.go +++ b/pkg/services/alerting/reader.go @@ -49,7 +49,7 @@ func (arr *AlertRuleReader) Fetch() []*AlertRule { res := make([]*AlertRule, len(cmd.Result)) for i, ruleDef := range cmd.Result { - model, _ := ConvetAlertModelToAlertRule(ruleDef) + model, _ := NewAlertRuleFromDBModel(ruleDef) res[i] = model } diff --git a/pkg/services/sqlstore/alert_rule_parser_test.go b/pkg/services/sqlstore/alert_rule_parser_test.go deleted file mode 100644 index 8ec7c24429b..00000000000 --- a/pkg/services/sqlstore/alert_rule_parser_test.go +++ /dev/null @@ -1,55 +0,0 @@ -package sqlstore - -import ( - "testing" - - "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 TestAlertRuleModelParsing(t *testing.T) { - - Convey("Parsing alertRule from expression", t, func() { - alertRuleDAO := &m.Alert{} - json, _ := simplejson.NewJson([]byte(` - { - "frequency": 10, - "warning": { - "op": ">", - "level": 10 - }, - "critical": { - "op": ">", - "level": 20 - }, - "query": { - "refId": "A", - "from": "5m", - "to": "now", - "datasourceId": 1, - "query": "aliasByNode(statsd.fakesite.counters.session_start.*.count, 4)" - }, - "transform": { - "type": "aggregation", - "method": "avg" - } - }`)) - - alertRuleDAO.Name = "Test" - alertRuleDAO.Expression = json - rule, _ := alerting.ConvetAlertModelToAlertRule(alertRuleDAO) - - Convey("Confirm that all properties are set", func() { - So(rule.Query.Query, ShouldEqual, "aliasByNode(statsd.fakesite.counters.session_start.*.count, 4)") - So(rule.Query.From, ShouldEqual, "5m") - So(rule.Query.To, ShouldEqual, "now") - So(rule.Query.DatasourceId, ShouldEqual, 1) - So(rule.Warning.Level, ShouldEqual, 10) - So(rule.Warning.Operator, ShouldEqual, ">") - So(rule.Critical.Level, ShouldEqual, 20) - So(rule.Critical.Operator, ShouldEqual, ">") - }) - }) -} diff --git a/public/app/features/dashboard/viewStateSrv.js b/public/app/features/dashboard/viewStateSrv.js index 035bfb6ae6e..fe1277c59c0 100644 --- a/public/app/features/dashboard/viewStateSrv.js +++ b/public/app/features/dashboard/viewStateSrv.js @@ -120,25 +120,28 @@ function (angular, _, $) { if (this.panelScopes.length === 0) { return; } if (this.dashboard.meta.fullscreen) { - if (this.fullscreenPanel) { - this.leaveFullscreen(false); - } var panelScope = this.getPanelScope(this.state.panelId); - // panel could be about to be created/added and scope does - // not exist yet if (!panelScope) { return; } + if (this.fullscreenPanel) { + // if already fullscreen + if (this.fullscreenPanel === panelScope) { + return; + } else { + this.leaveFullscreen(false); + } + } + if (!panelScope.ctrl.editModeInitiated) { panelScope.ctrl.initEditMode(); } - this.enterFullscreen(panelScope); - return; - } - - if (this.fullscreenPanel) { + if (!panelScope.ctrl.fullscreen) { + this.enterFullscreen(panelScope); + } + } else if (this.fullscreenPanel) { this.leaveFullscreen(true); } }; diff --git a/public/app/features/panel/panel_ctrl.ts b/public/app/features/panel/panel_ctrl.ts index 0f253b5048a..bcb1980f854 100644 --- a/public/app/features/panel/panel_ctrl.ts +++ b/public/app/features/panel/panel_ctrl.ts @@ -152,8 +152,8 @@ export class PanelCtrl { calculatePanelHeight() { if (this.fullscreen) { var docHeight = $(window).height(); - var editHeight = Math.floor(docHeight * 0.3); - var fullscreenHeight = Math.floor(docHeight * 0.7); + var editHeight = Math.floor(docHeight * 0.4); + var fullscreenHeight = Math.floor(docHeight * 0.6); this.containerHeight = this.editMode ? editHeight : fullscreenHeight; } else { this.containerHeight = this.panel.height || this.row.height; diff --git a/public/app/plugins/panel/graph/alert_handle.ts b/public/app/plugins/panel/graph/alert_handle.ts new file mode 100644 index 00000000000..c1914196e89 --- /dev/null +++ b/public/app/plugins/panel/graph/alert_handle.ts @@ -0,0 +1,135 @@ +/// + +import 'jquery.flot'; +import $ from 'jquery'; +import _ from 'lodash'; + +export class AlertHandleManager { + plot: any; + placeholder: any; + height: any; + alert: any; + + constructor(private panelCtrl) { + this.alert = panelCtrl.panel.alert; + } + + getHandleInnerHtml(type, op, value) { + if (op === '>') { op = '>'; } + if (op === '<') { op = '<'; } + + return ` +
+
+
+ + ${op} ${value} +
`; + } + + getFullHandleHtml(type, op, value) { + var innerTemplate = this.getHandleInnerHtml(type, op, value); + return ` +
+ ${innerTemplate} +
+ `; + } + + setupDragging(handleElem, levelModel) { + var isMoving = false; + var lastY = null; + var posTop; + var plot = this.plot; + var panelCtrl = this.panelCtrl; + + function dragging(evt) { + if (lastY === null) { + lastY = evt.clientY; + } else { + var diff = evt.clientY - lastY; + posTop = posTop + diff; + lastY = evt.clientY; + handleElem.css({top: posTop + diff}); + } + } + + function stopped() { + isMoving = false; + // calculate graph level + var graphLevel = plot.c2p({left: 0, top: posTop}).y; + console.log('canvasPos:' + posTop + ' Graph level: ' + graphLevel); + graphLevel = parseInt(graphLevel.toFixed(0)); + levelModel.level = graphLevel; + console.log(levelModel); + + var levelCanvasPos = plot.p2c({x: 0, y: graphLevel}); + console.log('canvas pos', levelCanvasPos); + + console.log('stopped'); + handleElem.off("mousemove", dragging); + handleElem.off("mouseup", dragging); + + // trigger digest and render + panelCtrl.$scope.$apply(function() { + panelCtrl.render(); + }); + } + + handleElem.bind('mousedown', function() { + isMoving = true; + lastY = null; + posTop = handleElem.position().top; + console.log('start pos', posTop); + + handleElem.on("mousemove", dragging); + handleElem.on("mouseup", stopped); + }); + } + + cleanUp() { + if (this.placeholder) { + this.placeholder.find(".alert-handle-wrapper").remove(); + } + } + + renderHandle(type, model, defaultHandleTopPos) { + var handleElem = this.placeholder.find(`.alert-handle-wrapper--${type}`); + var level = model.level; + var levelStr = level; + var handleTopPos = 0; + + // handle no value + if (!_.isNumber(level)) { + levelStr = ''; + handleTopPos = defaultHandleTopPos; + } else { + var levelCanvasPos = this.plot.p2c({x: 0, y: level}); + handleTopPos = Math.min(Math.max(levelCanvasPos.top, 0), this.height) - 6; + } + + if (handleElem.length === 0) { + console.log('creating handle'); + handleElem = $(this.getFullHandleHtml(type, model.op, levelStr)); + this.placeholder.append(handleElem); + this.setupDragging(handleElem, model); + } else { + console.log('reusing handle!'); + handleElem.html(this.getHandleInnerHtml(type, model.op, levelStr)); + } + + handleElem.toggleClass('alert-handle-wrapper--no-value', levelStr === ''); + handleElem.css({top: handleTopPos}); + } + + draw(plot) { + this.plot = plot; + this.placeholder = plot.getPlaceholder(); + this.height = plot.height(); + + this.renderHandle('critical', this.alert.critical, 10); + this.renderHandle('warn', this.alert.warn, this.height-30); + } + +} + diff --git a/public/app/plugins/panel/graph/alert_tab_ctrl.ts b/public/app/plugins/panel/graph/alert_tab_ctrl.ts index 3f83e6db38e..82ac33e3d47 100644 --- a/public/app/plugins/panel/graph/alert_tab_ctrl.ts +++ b/public/app/plugins/panel/graph/alert_tab_ctrl.ts @@ -50,7 +50,7 @@ export class AlertTabCtrl { notify: [], enabled: false, scheduler: 1, - warning: { op: '>', level: undefined }, + warn: { op: '>', level: undefined }, critical: { op: '>', level: undefined }, query: { refId: 'A', @@ -70,12 +70,21 @@ export class AlertTabCtrl { $scope.ctrl = this; this.metricTargets = this.panel.targets.map(val => val); - this.initAlertModel(); + + // set panel alert edit mode + $scope.$on("$destroy", () => { + this.panelCtrl.editingAlert = false; + this.panelCtrl.render(); + }); } initAlertModel() { - this.alert = this.panel.alert = this.panel.alert || {}; + if (!this.panel.alert) { + return; + } + + this.alert = this.panel.alert; // set defaults _.defaults(this.alert, this.defaultValues); @@ -97,6 +106,9 @@ export class AlertTabCtrl { this.query = new QueryPart(this.queryParams, alertQueryDef); this.convertThresholdsToAlertThresholds(); this.transformDef = _.findWhere(this.transforms, {type: this.alert.transform.type}); + + this.panelCtrl.editingAlert = true; + this.panelCtrl.render(); } queryUpdated() { @@ -125,36 +137,36 @@ export class AlertTabCtrl { } convertThresholdsToAlertThresholds() { - if (this.panel.grid - && this.panel.grid.threshold1 - && this.alert.warnLevel === undefined - ) { - this.alert.warning.op = '>'; - this.alert.warning.level = this.panel.grid.threshold1; - } - - if (this.panel.grid - && this.panel.grid.threshold2 - && this.alert.critical.level === undefined - ) { - this.alert.critical.op = '>'; - this.alert.critical.level = this.panel.grid.threshold2; - } + // if (this.panel.grid + // && this.panel.grid.threshold1 + // && this.alert.warnLevel === undefined + // ) { + // this.alert.warning.op = '>'; + // this.alert.warning.level = this.panel.grid.threshold1; + // } + // + // if (this.panel.grid + // && this.panel.grid.threshold2 + // && this.alert.critical.level === undefined + // ) { + // this.alert.critical.op = '>'; + // this.alert.critical.level = this.panel.grid.threshold2; + // } } delete() { - this.alert = this.panel.alert = {}; - this.alert.deleted = true; - this.initAlertModel(); + delete this.panel.alert; + this.panelCtrl.editingAlert = false; + this.panelCtrl.render(); } enable() { - delete this.alert.deleted; - this.alert.enabled = true; + this.panel.alert = {}; + this.initAlertModel(); } - disable() { - this.alert.enabled = false; + levelsUpdated() { + this.panelCtrl.render(); } } diff --git a/public/app/plugins/panel/graph/graph.js b/public/app/plugins/panel/graph/graph.js index 3a42df657a5..599bbe554bf 100755 --- a/public/app/plugins/panel/graph/graph.js +++ b/public/app/plugins/panel/graph/graph.js @@ -5,6 +5,7 @@ define([ 'lodash', 'app/core/utils/kbn', './graph_tooltip', + './alert_handle', 'jquery.flot', 'jquery.flot.selection', 'jquery.flot.time', @@ -14,13 +15,16 @@ define([ 'jquery.flot.crosshair', './jquery.flot.events', ], -function (angular, $, moment, _, kbn, GraphTooltip) { +function (angular, $, moment, _, kbn, GraphTooltip, AlertHandle) { 'use strict'; var module = angular.module('grafana.directives'); var labelWidthCache = {}; var panelWidthCache = {}; + // systemjs export + var AlertHandleManager = AlertHandle.AlertHandleManager; + module.directive('grafanaGraph', function($rootScope, timeSrv) { return { restrict: 'A', @@ -34,6 +38,7 @@ function (angular, $, moment, _, kbn, GraphTooltip) { var legendSideLastValue = null; var rootScope = scope.$root; var panelWidth = 0; + var alertHandles; rootScope.onAppEvent('setCrosshair', function(event, info) { // do not need to to this if event is from this panel @@ -161,6 +166,10 @@ function (angular, $, moment, _, kbn, GraphTooltip) { rightLabel[0].style.marginTop = (getLabelWidth(panel.yaxes[1].label, rightLabel) / 2) + 'px'; } + + if (alertHandles) { + alertHandles.draw(plot); + } } function processOffsetHook(plot, gridMargin) { @@ -181,6 +190,18 @@ function (angular, $, moment, _, kbn, GraphTooltip) { return; } + // give space to alert editing + if (ctrl.editingAlert) { + if (!alertHandles) { + elem.css('margin-right', '220px'); + alertHandles = new AlertHandleManager(ctrl); + } + } else if (alertHandles) { + elem.css('margin-right', '0'); + alertHandles.cleanUp(); + alertHandles = null; + } + var stack = panel.stack ? true : null; // Populate element @@ -259,6 +280,7 @@ function (angular, $, moment, _, kbn, GraphTooltip) { function callPlot(incrementRenderCounter) { try { + console.log('rendering'); $.plot(elem, sortedSeries, options); } catch (e) { console.log('flotcharts error', e); @@ -311,6 +333,50 @@ function (angular, $, moment, _, kbn, GraphTooltip) { } function addGridThresholds(options, panel) { + if (panel.alert) { + var crit = panel.alert.critical; + var warn = panel.alert.warn; + var critEdge = Infinity; + var warnEdge = crit.level; + + if (_.isNumber(crit.level)) { + if (crit.op === '<') { + critEdge = -Infinity; + } + + // fill + options.grid.markings.push({ + yaxis: {from: crit.level, to: critEdge}, + color: 'rgba(234, 112, 112, 0.10)', + }); + + // line + options.grid.markings.push({ + yaxis: {from: crit.level, to: crit.level}, + color: '#ed2e18' + }); + } + + if (_.isNumber(warn.level)) { + // if (warn.op === '<') { + // } + + // fill + options.grid.markings.push({ + yaxis: {from: warn.level, to: warnEdge}, + color: 'rgba(216, 200, 27, 0.10)', + }); + + // line + options.grid.markings.push({ + yaxis: {from: warn.level, to: warn.level}, + color: '#F79520' + }); + } + + return; + } + if (_.isNumber(panel.grid.threshold1)) { var limit1 = panel.grid.thresholdLine ? panel.grid.threshold1 : (panel.grid.threshold2 || null); options.grid.markings.push({ diff --git a/public/app/plugins/panel/graph/partials/tab_alerting.html b/public/app/plugins/panel/graph/partials/tab_alerting.html index 2b7b4e112e3..c2cce48a073 100644 --- a/public/app/plugins/panel/graph/partials/tab_alerting.html +++ b/public/app/plugins/panel/graph/partials/tab_alerting.html @@ -1,116 +1,120 @@ -
-
-
Alert Query
-
-
- - -
-
- Transform using -
- + +
+
+
+
Alert Query
+
+
+ + +
+
+ Transform using +
+ +
+
+
+ Method +
+ +
+
+
+ Timespan +
-
- Method -
- +
+ +
+
Levels
+
+
+ + + Warn if + + + +
+
+ + + Critcal if + + +
-
- Timespan - +
+
+ +
+
+
Execution
+
+
+ Scheduler +
+ +
+
+
+ Evaluate every + +
+
+
+
+
Notifications
+
+
+ Groups + + +
-
Levels
-
-
- - - Warn if - - - -
-
- - - Critcal if - - - -
-
-
-
- -
-
-
Execution
-
-
- Scheduler -
- -
-
-
- Evaluate every - -
-
-
-
-
Notifications
-
-
- Groups - - -
-
-
-
- - -
-
Information
-
- Alert name - -
-
+
Information
- Alert description + Alert name +
-
- +
+
+ Alert description +
+
+ +
- - - + +
diff --git a/public/sass/_variables.dark.scss b/public/sass/_variables.dark.scss index 047e50faa73..99070cad837 100644 --- a/public/sass/_variables.dark.scss +++ b/public/sass/_variables.dark.scss @@ -44,7 +44,7 @@ $brand-text-highlight: #f7941d; // Status colors // ------------------------- $online: #10a345; -$warn: #ffc03c; +$warn: #F79520; $critical: #ed2e18; // Scaffolding diff --git a/public/sass/components/_panel_graph.scss b/public/sass/components/_panel_graph.scss index b830561f816..5c85ac4d465 100644 --- a/public/sass/components/_panel_graph.scss +++ b/public/sass/components/_panel_graph.scss @@ -315,3 +315,67 @@ font-size: 12px; } +.alert-handle-wrapper { + position: absolute; + user-select: none; + + .alert-handle { + z-index: 10; + position: relative; + float: right; + padding: 0.4rem 0.6rem 0.4rem 0.4rem; + background-color: $btn-inverse-bg; + box-shadow: $search-shadow; + cursor: row-resize; + width: 100px; + font-size: $font-size-sm; + box-shadow: 4px 4px 3px 0px $body-bg; + border-radius: 4px; + border-width: 0 1px 1px 0; + border-style: solid; + border-color: $black; + text-align: right; + color: $text-muted; + + .icon-gf { + font-size: 17px; + position: relative; + top: 0px; + float: left; + } + } + + .alert-handle-line { + float: left; + height: 2px; + margin-top: 13px; + z-index: 0; + position: relative; + } + + &--warn { + right: -222px; + width: 238px; + + .alert-handle-line { + width: 138px; + background-color: $warn; + } + } + + &--critical { + right: -105px; + width: 123px; + + .alert-handle-line { + width: 23px; + background-color: $critical; + } + } + + &--no-value { + .alert-handle-line { + display: none; + } + } +} diff --git a/public/sass/pages/_dashboard.scss b/public/sass/pages/_dashboard.scss index 8fb1e6bcdaa..eec86e78234 100644 --- a/public/sass/pages/_dashboard.scss +++ b/public/sass/pages/_dashboard.scss @@ -197,12 +197,6 @@ div.flot-text { bottom: 0; } -.panel-fullscreen { - .panel-title-container { - padding: 8px; - } -} - .panel-full-edit { margin-top: 20px; margin-bottom: 20px; diff --git a/public/vendor/flot/jquery.flot.js b/public/vendor/flot/jquery.flot.js index 380030a77c9..e2c460ddbd0 100644 --- a/public/vendor/flot/jquery.flot.js +++ b/public/vendor/flot/jquery.flot.js @@ -1322,7 +1322,7 @@ Licensed under the MIT license. placeholder.css("padding", 0) // padding messes up the positioning .children().filter(function(){ - return !$(this).hasClass("flot-overlay") && !$(this).hasClass('flot-base'); + return $(this).hasClass("flot-text"); }).remove(); if (placeholder.css("position") == 'static')