Просмотр исходного кода

feat(alerting): more work on alerting thresholds

Torkel Ödegaard 9 лет назад
Родитель
Сommit
e3b281dbac

+ 2 - 5
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
 			}
 

+ 2 - 5
pkg/services/alerting/extractor_test.go

@@ -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)

+ 135 - 0
public/app/plugins/panel/graph/alert_handle.ts

@@ -0,0 +1,135 @@
+///<reference path="../../../headers/common.d.ts" />
+
+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 = '&gt;'; }
+    if (op === '<') { op = '&lt;'; }
+
+    return `
+    <div class="alert-handle-line">
+    </div>
+    <div class="alert-handle">
+    <i class="icon-gf icon-gf-${type} alert-icon-${type}"></i>
+    ${op} ${value}
+    </div>`;
+  }
+
+  getFullHandleHtml(type, op, value) {
+    var innerTemplate = this.getHandleInnerHtml(type, op, value);
+    return `
+    <div class="alert-handle-wrapper alert-handle-wrapper--${type}">
+    ${innerTemplate}
+    </div>
+    `;
+  }
+
+  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);
+  }
+
+}
+

+ 13 - 13
public/app/plugins/panel/graph/alert_tab_ctrl.ts

@@ -73,9 +73,6 @@ export class AlertTabCtrl {
     this.initAlertModel();
 
     // set panel alert edit mode
-    this.panelCtrl.editingAlert = true;
-    this.panelCtrl.render();
-
     $scope.$on("$destroy", () => {
       this.panelCtrl.editingAlert = false;
       this.panelCtrl.render();
@@ -83,7 +80,11 @@ export class AlertTabCtrl {
   }
 
   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);
@@ -105,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() {
@@ -151,18 +155,14 @@ export class AlertTabCtrl {
   }
 
   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;
-  }
-
-  disable() {
-    this.alert.enabled = false;
+    this.panel.alert = {};
+    this.initAlertModel();
   }
 
   levelsUpdated() {

+ 23 - 13
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',
@@ -13,15 +14,17 @@ define([
   'jquery.flot.fillbelow',
   'jquery.flot.crosshair',
   './jquery.flot.events',
-  './jquery.flot.alerts',
 ],
-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',
@@ -35,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
@@ -162,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) {
@@ -178,24 +186,26 @@ function (angular, $, moment, _, kbn, GraphTooltip) {
             panelWidth = panelWidthCache[panel.span] = elem.width();
           }
 
-          if (ctrl.editingAlert) {
-            elem.css('margin-right', '220px');
-          } else {
-            elem.css('margin-right', '');
-          }
-
           if (shouldAbortRender()) {
             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
           var options = {
-            alerting: {
-              editing: ctrl.editingAlert,
-              alert: panel.alert,
-            },
             hooks: {
               draw: [drawHook],
               processOffset: [processOffsetHook],
@@ -323,7 +333,7 @@ function (angular, $, moment, _, kbn, GraphTooltip) {
         }
 
         function addGridThresholds(options, panel) {
-          if (panel.alert && panel.alert.enabled) {
+          if (panel.alert) {
             var crit = panel.alert.critical;
             var warn = panel.alert.warn;
             var critEdge = Infinity;

+ 0 - 97
public/app/plugins/panel/graph/jquery.flot.alerts.ts

@@ -1,97 +0,0 @@
-///<reference path="../../../headers/common.d.ts" />
-
-import 'jquery.flot';
-import $ from 'jquery';
-import _ from 'lodash';
-
-var options = {};
-
-function getHandleInnerHtml(type, op, value) {
-  if (op === '>') { op = '&gt;'; }
-  if (op === '<') { op = '&lt;'; }
-
-  return `
-  <div class="alert-handle-line">
-  </div>
-  <div class="alert-handle">
-  <i class="icon-gf icon-gf-${type} alert-icon-${type}"></i>
-  ${op} ${value}
-  </div>`;
-}
-
-function getFullHandleHtml(type, op, value) {
-  var innerTemplate = getHandleInnerHtml(type, op, value);
-  return `
-  <div class="alert-handle-wrapper alert-handle-wrapper--${type}">
-  ${innerTemplate}
-  </div>
-  `;
-}
-
-var dragGhostElem = document.createElement('div');
-
-function dragStartHandler(evt) {
-  evt.dataTransfer.setDragImage(dragGhostElem, -99999, -99999);
-}
-
-function dragEndHandler() {
-  console.log('drag end');
-}
-
-function drawAlertHandles(plot) {
-  var options = plot.getOptions();
-  var $placeholder = plot.getPlaceholder();
-
-  if (!options.alerting.editing) {
-    $placeholder.find(".alert-handle-wrapper").remove();
-    return;
-  }
-
-  var alert = options.alerting.alert;
-  var height = plot.height();
-
-  function renderHandle(type, model) {
-    var $handle = $placeholder.find(`.alert-handle-wrapper--${type}`);
-
-    if (!_.isNumber(model.level)) {
-      $handle.remove();
-      return;
-    }
-
-    if ($handle.length === 0) {
-      console.log('creating handle');
-      $handle = $(getFullHandleHtml(type, model.op, model.level));
-      $handle.attr('draggable', true);
-      $handle.bind('dragend', dragEndHandler);
-      $handle.bind('dragstart', dragStartHandler);
-      $placeholder.append($handle);
-    } else {
-      console.log('reusing handle!');
-      $handle.html(getHandleInnerHtml(type, model.op, model.level));
-    }
-
-    var levelCanvasPos = plot.p2c({x: 0, y: model.level});
-    var levelTopPos = Math.min(Math.max(levelCanvasPos.top, 0), height) - 6;
-    $handle.css({top: levelTopPos});
-  }
-
-  renderHandle('critical', alert.critical);
-  renderHandle('warn', alert.warn);
-}
-
-function shutdown() {
-  console.log('shutdown');
-}
-
-function init(plot, classes) {
-  plot.hooks.draw.push(drawAlertHandles);
-  plot.hooks.shutdown.push(shutdown);
-}
-
-$.plot.plugins.push({
-  init: init,
-  options: options,
-  name: 'navigationControl',
-  version: '1.4'
-});
-

+ 96 - 92
public/app/plugins/panel/graph/partials/tab_alerting.html

@@ -1,116 +1,120 @@
-<div class="editor-row">
-  <div class="gf-form-group section" >
-    <h5 class="section-heading">Alert Query</h5>
-    <div class="gf-form-inline">
-      <div class="gf-form">
-        <query-part-editor
-                    class="gf-form-label query-part"
-                    part="ctrl.query"
-                    part-updated="ctrl.queryUpdated()">
-        </query-part-editor>
-      </div>
-      <div class="gf-form">
-        <span class="gf-form-label">Transform using</span>
-        <div class="gf-form-select-wrapper">
-          <select   class="gf-form-input"
-                    ng-model="ctrl.alert.transform.type"
-                    ng-options="f.type as f.text for f in ctrl.transforms"
-                    ng-change="ctrl.transformChanged()"
-                    >
-          </select>
+
+<div ng-if="ctrl.panel.alert">
+  <div class="editor-row">
+    <div class="gf-form-group section" >
+      <h5 class="section-heading">Alert Query</h5>
+      <div class="gf-form-inline">
+        <div class="gf-form">
+          <query-part-editor
+             class="gf-form-label query-part"
+             part="ctrl.query"
+             part-updated="ctrl.queryUpdated()">
+          </query-part-editor>
         </div>
-      </div>
-      <div class="gf-form" ng-if="ctrl.transformDef.type === 'aggregation'">
-        <span class="gf-form-label">Method</span>
-        <div class="gf-form-select-wrapper">
-          <select   class="gf-form-input"
-                    ng-model="ctrl.alert.transform.method"
-                    ng-options="f for f in ctrl.aggregators">
-          </select>
+        <div class="gf-form">
+          <span class="gf-form-label">Transform using</span>
+          <div class="gf-form-select-wrapper">
+            <select   class="gf-form-input"
+                      ng-model="ctrl.alert.transform.type"
+                      ng-options="f.type as f.text for f in ctrl.transforms"
+                      ng-change="ctrl.transformChanged()"
+                      >
+            </select>
+          </div>
+        </div>
+        <div class="gf-form" ng-if="ctrl.transformDef.type === 'aggregation'">
+          <span class="gf-form-label">Method</span>
+          <div class="gf-form-select-wrapper">
+            <select   class="gf-form-input"
+                      ng-model="ctrl.alert.transform.method"
+                      ng-options="f for f in ctrl.aggregators">
+            </select>
+          </div>
+        </div>
+        <div class="gf-form" ng-if="ctrl.transformDef.type === 'forecast'">
+          <span class="gf-form-label">Timespan</span>
+          <input class="gf-form-input max-width-5" type="text" ng-model="ctrl.alert.transform.timespan" ng-change="ctrl.ruleUpdated()"></input>
         </div>
       </div>
-      <div class="gf-form" ng-if="ctrl.transformDef.type === 'forecast'">
-        <span class="gf-form-label">Timespan</span>
-        <input class="gf-form-input max-width-5" type="text" ng-model="ctrl.alert.transform.timespan" ng-change="ctrl.ruleUpdated()"></input>
+    </div>
+
+    <div class="gf-form-group section">
+      <h5 class="section-heading">Levels</h5>
+      <div class="gf-form-inline">
+        <div class="gf-form">
+          <span class="gf-form-label">
+            <i class="icon-gf icon-gf-warn alert-icon-warn"></i>
+            Warn if
+          </span>
+          <metric-segment-model property="ctrl.alert.warn.op" options="ctrl.levelOpList" custom="false" css-class="query-segment-operator"></metric-segment-model>
+          <input class="gf-form-input max-width-7" type="number" ng-model="ctrl.alert.warn.level" ng-change="ctrl.levelsUpdated()"></input>
+        </div>
+        <div class="gf-form">
+          <span class="gf-form-label">
+            <i class="icon-gf icon-gf-warn alert-icon-critical"></i>
+            Critcal if
+          </span>
+          <metric-segment-model property="ctrl.alert.critical.op" options="ctrl.levelOpList" custom="false" css-class="query-segment-operator"></metric-segment-model>
+          <input class="gf-form-input max-width-7" type="number" ng-model="ctrl.alert.critical.level" ng-change="ctrl.levelsUpdated()"></input>
+        </div>
       </div>
     </div>
   </div>
 
-<!--   <div class="gf&#45;form&#45;group section"> -->
-<!--     <h5 class="section&#45;heading">Levels</h5> -->
-<!--     <div class="gf&#45;form&#45;inline"> -->
-<!--       <div class="gf&#45;form"> -->
-<!--         <span class="gf&#45;form&#45;label"> -->
-<!--           <i class="icon&#45;gf icon&#45;gf&#45;warn alert&#45;icon&#45;warn"></i> -->
-<!--           Warn if -->
-<!--         </span> -->
-<!--         <metric&#45;segment&#45;model property="ctrl.alert.warn.op" options="ctrl.levelOpList" custom="false" css&#45;class="query&#45;segment&#45;operator"></metric&#45;segment&#45;model> -->
-<!--         <input class="gf&#45;form&#45;input max&#45;width&#45;7" type="number" ng&#45;model="ctrl.alert.warn.level" ng&#45;change="ctrl.levelsUpdated()"></input> -->
-<!--       </div> -->
-<!--       <div class="gf&#45;form"> -->
-<!--         <span class="gf&#45;form&#45;label"> -->
-<!--           <i class="icon&#45;gf icon&#45;gf&#45;warn alert&#45;icon&#45;critical"></i> -->
-<!--           Critcal if -->
-<!--         </span> -->
-<!--         <metric&#45;segment&#45;model property="ctrl.alert.critical.op" options="ctrl.levelOpList" custom="false" css&#45;class="query&#45;segment&#45;operator"></metric&#45;segment&#45;model> -->
-<!--         <input class="gf&#45;form&#45;input max&#45;width&#45;7" type="number" ng&#45;model="ctrl.alert.critical.level" ng&#45;change="ctrl.levelsUpdated()"></input> -->
-<!--       </div> -->
-<!--     </div> -->
-<!--   </div> -->
-<!-- </div> -->
-
-<div class="editor-row">
-  <div class="gf-form-group section">
-    <h5 class="section-heading">Execution</h5>
-    <div class="gf-form-inline">
-      <div class="gf-form">
-        <span class="gf-form-label">Scheduler</span>
-        <div class="gf-form-select-wrapper">
-          <select   class="gf-form-input"
-                    ng-model="ctrl.alert.scheduler"
-                    ng-options="f.value as f.text for f in ctrl.schedulers">
-          </select>
+  <div class="editor-row">
+    <div class="gf-form-group section">
+      <h5 class="section-heading">Execution</h5>
+      <div class="gf-form-inline">
+        <div class="gf-form">
+          <span class="gf-form-label">Scheduler</span>
+          <div class="gf-form-select-wrapper">
+            <select   class="gf-form-input"
+                      ng-model="ctrl.alert.scheduler"
+                      ng-options="f.value as f.text for f in ctrl.schedulers">
+            </select>
+          </div>
+        </div>
+        <div class="gf-form">
+          <span class="gf-form-label">Evaluate every</span>
+          <input class="gf-form-input max-width-7" type="text" ng-model="ctrl.alert.frequency"></input>
         </div>
-      </div>
-      <div class="gf-form">
-        <span class="gf-form-label">Evaluate every</span>
-        <input class="gf-form-input max-width-7" type="text" ng-model="ctrl.alert.frequency"></input>
       </div>
     </div>
-  </div>
-  <div class="gf-form-group section">
-    <h5 class="section-heading">Notifications</h5>
-    <div class="gf-form-inline">
-      <div class="gf-form">
-        <span class="gf-form-label">Groups</span>
-        <bootstrap-tagsinput ng-model="ctrl.alert.notify" tagclass="label label-tag" placeholder="add tags">
-				</bootstrap-tagsinput>
+    <div class="gf-form-group section">
+      <h5 class="section-heading">Notifications</h5>
+      <div class="gf-form-inline">
+        <div class="gf-form">
+          <span class="gf-form-label">Groups</span>
+          <bootstrap-tagsinput ng-model="ctrl.alert.notify" tagclass="label label-tag" placeholder="add tags">
+          </bootstrap-tagsinput>
+        </div>
       </div>
     </div>
   </div>
-</div>
 
-
-<div class="gf-form-group section">
-  <h5 class="section-heading">Information</h5>
-  <div class="gf-form">
-    <span class="gf-form-label width-10">Alert name</span>
-    <input type="text" class="gf-form-input width-22" ng-model="ctrl.panel.alerting.name">
-  </div>
-  <div class="gf-form-inline">
+  <div class="gf-form-group section">
+    <h5 class="section-heading">Information</h5>
     <div class="gf-form">
-      <span class="gf-form-label width-10" style="margin-top: -73px;">Alert description</span>
+      <span class="gf-form-label width-10">Alert name</span>
+      <input type="text" class="gf-form-input width-22" ng-model="ctrl.panel.alerting.name">
     </div>
-    <div class="gf-form">
-      <textarea rows="5" ng-model="ctrl.panel.alerting.description" class="gf-form-input width-22"></textarea>
+    <div class="gf-form-inline">
+      <div class="gf-form">
+        <span class="gf-form-label width-10" style="margin-top: -73px;">Alert description</span>
+      </div>
+      <div class="gf-form">
+        <textarea rows="5" ng-model="ctrl.panel.alerting.description" class="gf-form-input width-22"></textarea>
+      </div>
     </div>
   </div>
 </div>
 
 <div class="editor-row">
   <div class="gf-form-button-row">
-    <button class="btn btn-danger" ng-click="ctrl.delete()" ng-show="ctrl.alert.enabled">Delete</button>
-    <button class="btn btn-success" ng-click="ctrl.enable()" ng-hide="ctrl.alert.enabled">Enable</button>
-    <button class="btn btn-secondary" ng-click="ctrl.disable()" ng-show="ctrl.alert.enabled">Disable</button>
+    <button class="btn btn-danger" ng-click="ctrl.delete()" ng-show="ctrl.panel.alert">Delete</button>
+    <button class="btn btn-inverse" ng-click="ctrl.enable()" ng-hide="ctrl.panel.alert">
+      <i class="icon-gf icon-gf-alert"></i>
+      Add Alert
+    </button>
   </div>
 </div>

+ 35 - 32
public/sass/components/_panel_graph.scss

@@ -319,37 +319,6 @@
   position: absolute;
   user-select: none;
 
-  &--warn {
-    right: -222px;
-    width: 238px;
-
-    .alert-handle-line {
-      float: left;
-      height: 2px;
-      width: 138px;
-      margin-top: 14px;
-      background-color: $warn;
-      z-index: 0;
-      position: relative;
-    }
-  }
-
-  &--critical {
-    right: -105px;
-    width: 123px;
-
-    .alert-handle-line {
-      float: left;
-      height: 2px;
-      width: 23px;
-      margin-top: 14px;
-      background-color: $critical;
-      z-index: 0;
-      position: relative;
-    }
-  }
-
-
   .alert-handle {
     z-index: 10;
     position: relative;
@@ -357,7 +326,7 @@
     padding: 0.4rem 0.6rem 0.4rem 0.4rem;
     background-color: $btn-inverse-bg;
     box-shadow: $search-shadow;
-    cursor: pointer;
+    cursor: row-resize;
     width: 100px;
     font-size: $font-size-sm;
     box-shadow: 4px 4px 3px 0px $body-bg;
@@ -366,6 +335,7 @@
     border-style: solid;
     border-color: $black;
     text-align: right;
+    color: $text-muted;
 
     .icon-gf {
       font-size: 17px;
@@ -375,4 +345,37 @@
     }
   }
 
+  .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;
+    }
+  }
 }