Browse Source

Merge pull request #12371 from marefr/xaxis_thresholds

Time regions support in graph panel
Marcus Efraimsson 7 years ago
parent
commit
0810aa2e60

+ 511 - 0
devenv/dev-dashboards/panel_tests_graph_time_regions.json

@@ -0,0 +1,511 @@
+{
+  "annotations": {
+    "list": [
+      {
+        "builtIn": 1,
+        "datasource": "-- Grafana --",
+        "enable": true,
+        "hide": true,
+        "iconColor": "rgba(0, 211, 255, 1)",
+        "name": "Annotations & Alerts",
+        "type": "dashboard"
+      }
+    ]
+  },
+  "editable": true,
+  "gnetId": null,
+  "graphTooltip": 0,
+  "links": [],
+  "panels": [
+    {
+      "aliasColors": {},
+      "bars": false,
+      "dashLength": 10,
+      "dashes": false,
+      "datasource": "gdev-testdata",
+      "fill": 2,
+      "gridPos": {
+        "h": 8,
+        "w": 24,
+        "x": 0,
+        "y": 0
+      },
+      "id": 2,
+      "legend": {
+        "avg": false,
+        "current": false,
+        "max": false,
+        "min": false,
+        "show": true,
+        "total": false,
+        "values": false
+      },
+      "lines": true,
+      "linewidth": 2,
+      "links": [],
+      "nullPointMode": "null",
+      "percentage": false,
+      "pointradius": 5,
+      "points": false,
+      "renderer": "flot",
+      "seriesOverrides": [],
+      "spaceLength": 10,
+      "stack": false,
+      "steppedLine": false,
+      "targets": [
+        {
+          "refId": "A",
+          "scenarioId": "random_walk",
+          "target": ""
+        }
+      ],
+      "thresholds": [],
+      "timeFrom": null,
+      "timeRegions": [
+        {
+          "colorMode": "gray",
+          "fill": true,
+          "fillColor": "rgba(255, 255, 255, 0.03)",
+          "from": "08:30",
+          "fromDayOfWeek": 1,
+          "line": false,
+          "lineColor": "rgba(255, 255, 255, 0.2)",
+          "op": "time",
+          "to": "16:45",
+          "toDayOfWeek": 5
+        }
+      ],
+      "timeShift": null,
+      "title": "Business Hours",
+      "tooltip": {
+        "shared": true,
+        "sort": 0,
+        "value_type": "individual"
+      },
+      "type": "graph",
+      "xaxis": {
+        "buckets": null,
+        "mode": "time",
+        "name": null,
+        "show": true,
+        "values": []
+      },
+      "yaxes": [
+        {
+          "format": "short",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": null,
+          "show": true
+        },
+        {
+          "format": "short",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": null,
+          "show": true
+        }
+      ],
+      "yaxis": {
+        "align": false,
+        "alignLevel": null
+      }
+    },
+    {
+      "aliasColors": {},
+      "bars": false,
+      "dashLength": 10,
+      "dashes": false,
+      "datasource": "gdev-testdata",
+      "fill": 2,
+      "gridPos": {
+        "h": 8,
+        "w": 24,
+        "x": 0,
+        "y": 8
+      },
+      "id": 4,
+      "legend": {
+        "avg": false,
+        "current": false,
+        "max": false,
+        "min": false,
+        "show": true,
+        "total": false,
+        "values": false
+      },
+      "lines": true,
+      "linewidth": 2,
+      "links": [],
+      "nullPointMode": "null",
+      "percentage": false,
+      "pointradius": 5,
+      "points": false,
+      "renderer": "flot",
+      "seriesOverrides": [],
+      "spaceLength": 10,
+      "stack": false,
+      "steppedLine": false,
+      "targets": [
+        {
+          "expr": "",
+          "format": "time_series",
+          "intervalFactor": 1,
+          "refId": "A",
+          "scenarioId": "random_walk",
+          "target": ""
+        }
+      ],
+      "thresholds": [],
+      "timeFrom": null,
+      "timeRegions": [
+        {
+          "colorMode": "red",
+          "fill": true,
+          "fillColor": "rgba(255, 255, 255, 0.03)",
+          "from": "20:00",
+          "fromDayOfWeek": 7,
+          "line": false,
+          "lineColor": "rgba(255, 255, 255, 0.2)",
+          "op": "time",
+          "to": "23:00",
+          "toDayOfWeek": 7
+        }
+      ],
+      "timeShift": null,
+      "title": "Sunday's 20-23",
+      "tooltip": {
+        "shared": true,
+        "sort": 0,
+        "value_type": "individual"
+      },
+      "type": "graph",
+      "xaxis": {
+        "buckets": null,
+        "mode": "time",
+        "name": null,
+        "show": true,
+        "values": []
+      },
+      "yaxes": [
+        {
+          "format": "short",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": null,
+          "show": true
+        },
+        {
+          "format": "short",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": null,
+          "show": true
+        }
+      ],
+      "yaxis": {
+        "align": false,
+        "alignLevel": null
+      }
+    },
+    {
+      "aliasColors": {
+        "A-series": "#d683ce"
+      },
+      "bars": false,
+      "dashLength": 10,
+      "dashes": false,
+      "datasource": "gdev-testdata",
+      "fill": 2,
+      "gridPos": {
+        "h": 8,
+        "w": 24,
+        "x": 0,
+        "y": 16
+      },
+      "id": 3,
+      "legend": {
+        "avg": false,
+        "current": false,
+        "max": false,
+        "min": false,
+        "show": true,
+        "total": false,
+        "values": false
+      },
+      "lines": true,
+      "linewidth": 2,
+      "links": [],
+      "nullPointMode": "null",
+      "percentage": false,
+      "pointradius": 0.5,
+      "points": false,
+      "renderer": "flot",
+      "seriesOverrides": [],
+      "spaceLength": 10,
+      "stack": false,
+      "steppedLine": false,
+      "targets": [
+        {
+          "refId": "A",
+          "scenarioId": "random_walk",
+          "target": ""
+        }
+      ],
+      "thresholds": [],
+      "timeFrom": null,
+      "timeRegions": [
+        {
+          "colorMode": "custom",
+          "fill": true,
+          "fillColor": "rgba(255, 0, 0, 0.22)",
+          "from": "",
+          "fromDayOfWeek": 1,
+          "line": true,
+          "lineColor": "rgba(255, 0, 0, 0.32)",
+          "op": "time",
+          "to": "",
+          "toDayOfWeek": 1
+        },
+        {
+          "colorMode": "custom",
+          "fill": true,
+          "fillColor": "rgba(255, 127, 0, 0.22)",
+          "fromDayOfWeek": 2,
+          "line": true,
+          "lineColor": "rgba(255, 127, 0, 0.32)",
+          "op": "time",
+          "toDayOfWeek": 2
+        },
+        {
+          "colorMode": "custom",
+          "fill": true,
+          "fillColor": "rgba(255, 255, 0, 0.22)",
+          "fromDayOfWeek": 3,
+          "line": true,
+          "lineColor": "rgba(255, 255, 0, 0.22)",
+          "op": "time",
+          "toDayOfWeek": 3
+        },
+        {
+          "colorMode": "custom",
+          "fill": true,
+          "fillColor": "rgba(0, 255, 0, 0.22)",
+          "fromDayOfWeek": 4,
+          "line": true,
+          "lineColor": "rgba(0, 255, 0, 0.32)",
+          "op": "time",
+          "toDayOfWeek": 4
+        },
+        {
+          "colorMode": "custom",
+          "fill": true,
+          "fillColor": "rgba(0, 0, 255, 0.22)",
+          "fromDayOfWeek": 5,
+          "line": true,
+          "lineColor": "rgba(0, 0, 255, 0.32)",
+          "op": "time",
+          "toDayOfWeek": 5
+        },
+        {
+          "colorMode": "custom",
+          "fill": true,
+          "fillColor": "rgba(75, 0, 130, 0.22)",
+          "fromDayOfWeek": 6,
+          "line": true,
+          "lineColor": "rgba(75, 0, 130, 0.32)",
+          "op": "time",
+          "toDayOfWeek": 6
+        },
+        {
+          "colorMode": "custom",
+          "fill": true,
+          "fillColor": "rgba(148, 0, 211, 0.22)",
+          "fromDayOfWeek": 7,
+          "line": true,
+          "lineColor": "rgba(148, 0, 211, 0.32)",
+          "op": "time",
+          "toDayOfWeek": 7
+        }
+      ],
+      "timeShift": null,
+      "title": "Each day of week",
+      "tooltip": {
+        "shared": true,
+        "sort": 0,
+        "value_type": "individual"
+      },
+      "type": "graph",
+      "xaxis": {
+        "buckets": null,
+        "mode": "time",
+        "name": null,
+        "show": true,
+        "values": []
+      },
+      "yaxes": [
+        {
+          "format": "short",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": null,
+          "show": true
+        },
+        {
+          "format": "short",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": null,
+          "show": true
+        }
+      ],
+      "yaxis": {
+        "align": false,
+        "alignLevel": null
+      }
+    },
+    {
+      "aliasColors": {},
+      "bars": false,
+      "dashLength": 10,
+      "dashes": false,
+      "datasource": "gdev-testdata",
+      "fill": 2,
+      "gridPos": {
+        "h": 8,
+        "w": 24,
+        "x": 0,
+        "y": 24
+      },
+      "id": 5,
+      "legend": {
+        "avg": false,
+        "current": false,
+        "max": false,
+        "min": false,
+        "show": true,
+        "total": false,
+        "values": false
+      },
+      "lines": true,
+      "linewidth": 2,
+      "links": [],
+      "nullPointMode": "null",
+      "percentage": false,
+      "pointradius": 5,
+      "points": false,
+      "renderer": "flot",
+      "seriesOverrides": [],
+      "spaceLength": 10,
+      "stack": false,
+      "steppedLine": false,
+      "targets": [
+        {
+          "expr": "",
+          "format": "time_series",
+          "intervalFactor": 1,
+          "refId": "A",
+          "scenarioId": "random_walk",
+          "target": ""
+        }
+      ],
+      "thresholds": [],
+      "timeFrom": null,
+      "timeRegions": [
+        {
+          "colorMode": "red",
+          "fill": false,
+          "from": "05:00",
+          "line": true,
+          "op": "time"
+        }
+      ],
+      "timeShift": null,
+      "title": "05:00",
+      "tooltip": {
+        "shared": true,
+        "sort": 0,
+        "value_type": "individual"
+      },
+      "type": "graph",
+      "xaxis": {
+        "buckets": null,
+        "mode": "time",
+        "name": null,
+        "show": true,
+        "values": []
+      },
+      "yaxes": [
+        {
+          "format": "short",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": null,
+          "show": true
+        },
+        {
+          "format": "short",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": null,
+          "show": true
+        }
+      ],
+      "yaxis": {
+        "align": false,
+        "alignLevel": null
+      }
+    }
+  ],
+  "refresh": false,
+  "schemaVersion": 16,
+  "style": "dark",
+  "tags": [
+    "gdev",
+    "panel-tests"
+  ],
+  "templating": {
+    "list": []
+  },
+  "time": {
+    "from": "now-30d",
+    "to": "now"
+  },
+  "timepicker": {
+    "refresh_intervals": [
+      "5s",
+      "10s",
+      "30s",
+      "1m",
+      "5m",
+      "15m",
+      "30m",
+      "1h",
+      "2h",
+      "1d"
+    ],
+    "time_options": [
+      "5m",
+      "15m",
+      "1h",
+      "6h",
+      "12h",
+      "24h",
+      "2d",
+      "7d",
+      "30d"
+    ]
+  },
+  "timezone": "browser",
+  "title": "Panel Tests - Graph (Time Regions)",
+  "uid": "XMjIZPmik",
+  "version": 1
+}

+ 8 - 0
docs/sources/features/panels/graph.md

@@ -186,6 +186,14 @@ There is an option under Series overrides to draw lines as dashes. Set Dashes to
 Thresholds allow you to add arbitrary lines or sections to the graph to make it easier to see when
 the graph crosses a particular threshold.
 
+### Time Regions
+
+> Only available in Grafana v5.4 and above.
+
+{{< docs-imagebox img="/img/docs/v54/graph_time_regions.png" max-width= "800px" >}}
+
+Time regions allow you to highlight certain time regions of the graph to make it easier to see for example weekends, business hours and/or off work hours.
+
 ## Time Range
 
 {{< docs-imagebox img="/img/docs/v51/graph-time-range.png"  max-width= "900px" >}}

+ 6 - 0
public/app/plugins/panel/graph/graph.ts

@@ -16,6 +16,7 @@ import { tickStep } from 'app/core/utils/ticks';
 import { appEvents, coreModule, updateLegendValues } from 'app/core/core';
 import GraphTooltip from './graph_tooltip';
 import { ThresholdManager } from './threshold_manager';
+import { TimeRegionManager } from './time_region_manager';
 import { EventManager } from 'app/features/annotations/all';
 import { convertToHistogramData } from './histogram';
 import { alignYLevel } from './align_yaxes';
@@ -38,6 +39,7 @@ class GraphElement {
   panelWidth: number;
   eventManager: EventManager;
   thresholdManager: ThresholdManager;
+  timeRegionManager: TimeRegionManager;
   legendElem: HTMLElement;
 
   constructor(private scope, private elem, private timeSrv) {
@@ -49,6 +51,7 @@ class GraphElement {
     this.panelWidth = 0;
     this.eventManager = new EventManager(this.ctrl);
     this.thresholdManager = new ThresholdManager(this.ctrl);
+    this.timeRegionManager = new TimeRegionManager(this.ctrl);
     this.tooltip = new GraphTooltip(this.elem, this.ctrl.dashboard, this.scope, () => {
       return this.sortedSeries;
     });
@@ -125,6 +128,7 @@ class GraphElement {
 
   onPanelTeardown() {
     this.thresholdManager = null;
+    this.timeRegionManager = null;
 
     if (this.plot) {
       this.plot.destroy();
@@ -215,6 +219,7 @@ class GraphElement {
     }
 
     this.thresholdManager.draw(plot);
+    this.timeRegionManager.draw(plot);
   }
 
   processOffsetHook(plot, gridMargin) {
@@ -293,6 +298,7 @@ class GraphElement {
     this.prepareXAxis(options, this.panel);
     this.configureYAxisOptions(this.data, options);
     this.thresholdManager.addFlotOptions(options, this.panel);
+    this.timeRegionManager.addFlotOptions(options, this.panel);
     this.eventManager.addFlotEvents(this.annotations, options);
 
     this.sortedSeries = this.sortSeries(this.data, this.panel);

+ 2 - 0
public/app/plugins/panel/graph/module.ts

@@ -1,6 +1,7 @@
 import './graph';
 import './series_overrides_ctrl';
 import './thresholds_form';
+import './time_regions_form';
 
 import template from './template';
 import _ from 'lodash';
@@ -111,6 +112,7 @@ class GraphCtrl extends MetricsPanelCtrl {
     // other style overrides
     seriesOverrides: [],
     thresholds: [],
+    timeRegions: [],
   };
 
   /** @ngInject */

+ 262 - 0
public/app/plugins/panel/graph/specs/time_region_manager.test.ts

@@ -0,0 +1,262 @@
+import { TimeRegionManager, colorModes } from '../time_region_manager';
+import moment from 'moment';
+
+describe('TimeRegionManager', () => {
+  function plotOptionsScenario(desc, func) {
+    describe(desc, () => {
+      const ctx: any = {
+        panel: {
+          timeRegions: [],
+        },
+        options: {
+          grid: { markings: [] },
+        },
+        panelCtrl: {
+          range: {},
+          dashboard: {
+            isTimezoneUtc: () => false,
+          },
+        },
+      };
+
+      ctx.setup = (regions, from, to) => {
+        ctx.panel.timeRegions = regions;
+        ctx.panelCtrl.range.from = from;
+        ctx.panelCtrl.range.to = to;
+        const manager = new TimeRegionManager(ctx.panelCtrl);
+        manager.addFlotOptions(ctx.options, ctx.panel);
+      };
+
+      ctx.printScenario = () => {
+        console.log(
+          `Time range: from=${ctx.panelCtrl.range.from.format()}, to=${ctx.panelCtrl.range.to.format()}`,
+          ctx.panelCtrl.range.from._isUTC
+        );
+        ctx.options.grid.markings.forEach((m, i) => {
+          console.log(
+            `Marking (${i}): from=${moment(m.xaxis.from).format()}, to=${moment(m.xaxis.to).format()}, color=${m.color}`
+          );
+        });
+      };
+
+      func(ctx);
+    });
+  }
+
+  describe('When creating plot markings using local time', () => {
+    plotOptionsScenario('for day of week region', ctx => {
+      const regions = [{ fromDayOfWeek: 1, toDayOfWeek: 1, fill: true, line: true, colorMode: 'red' }];
+      const from = moment('2018-01-01T00:00:00+01:00');
+      const to = moment('2018-01-01T23:59:00+01:00');
+      ctx.setup(regions, from, to);
+
+      it('should add 3 markings', () => {
+        expect(ctx.options.grid.markings.length).toBe(3);
+      });
+
+      it('should add fill', () => {
+        const markings = ctx.options.grid.markings;
+        expect(moment(markings[0].xaxis.from).format()).toBe(moment('2018-01-01T01:00:00+01:00').format());
+        expect(moment(markings[0].xaxis.to).format()).toBe(moment('2018-01-02T00:59:59+01:00').format());
+        expect(markings[0].color).toBe(colorModes.red.color.fill);
+      });
+
+      it('should add line before', () => {
+        const markings = ctx.options.grid.markings;
+        expect(moment(markings[1].xaxis.from).format()).toBe(moment('2018-01-01T01:00:00+01:00').format());
+        expect(moment(markings[1].xaxis.to).format()).toBe(moment('2018-01-01T01:00:00+01:00').format());
+        expect(markings[1].color).toBe(colorModes.red.color.line);
+      });
+
+      it('should add line after', () => {
+        const markings = ctx.options.grid.markings;
+        expect(moment(markings[2].xaxis.from).format()).toBe(moment('2018-01-02T00:59:59+01:00').format());
+        expect(moment(markings[2].xaxis.to).format()).toBe(moment('2018-01-02T00:59:59+01:00').format());
+        expect(markings[2].color).toBe(colorModes.red.color.line);
+      });
+    });
+
+    plotOptionsScenario('for time from region', ctx => {
+      const regions = [{ from: '05:00', fill: true, colorMode: 'red' }];
+      const from = moment('2018-01-01T00:00+01:00');
+      const to = moment('2018-01-03T23:59+01:00');
+      ctx.setup(regions, from, to);
+
+      it('should add 3 markings', () => {
+        expect(ctx.options.grid.markings.length).toBe(3);
+      });
+
+      it('should add one fill at 05:00 each day', () => {
+        const markings = ctx.options.grid.markings;
+
+        expect(moment(markings[0].xaxis.from).format()).toBe(moment('2018-01-01T06:00:00+01:00').format());
+        expect(moment(markings[0].xaxis.to).format()).toBe(moment('2018-01-01T06:00:00+01:00').format());
+        expect(markings[0].color).toBe(colorModes.red.color.fill);
+
+        expect(moment(markings[1].xaxis.from).format()).toBe(moment('2018-01-02T06:00:00+01:00').format());
+        expect(moment(markings[1].xaxis.to).format()).toBe(moment('2018-01-02T06:00:00+01:00').format());
+        expect(markings[1].color).toBe(colorModes.red.color.fill);
+
+        expect(moment(markings[2].xaxis.from).format()).toBe(moment('2018-01-03T06:00:00+01:00').format());
+        expect(moment(markings[2].xaxis.to).format()).toBe(moment('2018-01-03T06:00:00+01:00').format());
+        expect(markings[2].color).toBe(colorModes.red.color.fill);
+      });
+    });
+
+    plotOptionsScenario('for time to region', ctx => {
+      const regions = [{ to: '05:00', fill: true, colorMode: 'red' }];
+      const from = moment('2018-02-01T00:00+01:00');
+      const to = moment('2018-02-03T23:59+01:00');
+      ctx.setup(regions, from, to);
+
+      it('should add 3 markings', () => {
+        expect(ctx.options.grid.markings.length).toBe(3);
+      });
+
+      it('should add one fill at 05:00 each day', () => {
+        const markings = ctx.options.grid.markings;
+
+        expect(moment(markings[0].xaxis.from).format()).toBe(moment('2018-02-01T06:00:00+01:00').format());
+        expect(moment(markings[0].xaxis.to).format()).toBe(moment('2018-02-01T06:00:00+01:00').format());
+        expect(markings[0].color).toBe(colorModes.red.color.fill);
+
+        expect(moment(markings[1].xaxis.from).format()).toBe(moment('2018-02-02T06:00:00+01:00').format());
+        expect(moment(markings[1].xaxis.to).format()).toBe(moment('2018-02-02T06:00:00+01:00').format());
+        expect(markings[1].color).toBe(colorModes.red.color.fill);
+
+        expect(moment(markings[2].xaxis.from).format()).toBe(moment('2018-02-03T06:00:00+01:00').format());
+        expect(moment(markings[2].xaxis.to).format()).toBe(moment('2018-02-03T06:00:00+01:00').format());
+        expect(markings[2].color).toBe(colorModes.red.color.fill);
+      });
+    });
+
+    plotOptionsScenario('for day of week from/to region', ctx => {
+      const regions = [{ fromDayOfWeek: 7, toDayOfWeek: 7, fill: true, colorMode: 'red' }];
+      const from = moment('2018-01-01T18:45:05+01:00');
+      const to = moment('2018-01-22T08:27:00+01:00');
+      ctx.setup(regions, from, to);
+
+      it('should add 3 markings', () => {
+        expect(ctx.options.grid.markings.length).toBe(3);
+      });
+
+      it('should add one fill at each sunday', () => {
+        const markings = ctx.options.grid.markings;
+
+        expect(moment(markings[0].xaxis.from).format()).toBe(moment('2018-01-07T01:00:00+01:00').format());
+        expect(moment(markings[0].xaxis.to).format()).toBe(moment('2018-01-08T00:59:59+01:00').format());
+        expect(markings[0].color).toBe(colorModes.red.color.fill);
+
+        expect(moment(markings[1].xaxis.from).format()).toBe(moment('2018-01-14T01:00:00+01:00').format());
+        expect(moment(markings[1].xaxis.to).format()).toBe(moment('2018-01-15T00:59:59+01:00').format());
+        expect(markings[1].color).toBe(colorModes.red.color.fill);
+
+        expect(moment(markings[2].xaxis.from).format()).toBe(moment('2018-01-21T01:00:00+01:00').format());
+        expect(moment(markings[2].xaxis.to).format()).toBe(moment('2018-01-22T00:59:59+01:00').format());
+        expect(markings[2].color).toBe(colorModes.red.color.fill);
+      });
+    });
+
+    plotOptionsScenario('for day of week from region', ctx => {
+      const regions = [{ fromDayOfWeek: 7, fill: true, colorMode: 'red' }];
+      const from = moment('2018-01-01T18:45:05+01:00');
+      const to = moment('2018-01-22T08:27:00+01:00');
+      ctx.setup(regions, from, to);
+
+      it('should add 3 markings', () => {
+        expect(ctx.options.grid.markings.length).toBe(3);
+      });
+
+      it('should add one fill at each sunday', () => {
+        const markings = ctx.options.grid.markings;
+
+        expect(moment(markings[0].xaxis.from).format()).toBe(moment('2018-01-07T01:00:00+01:00').format());
+        expect(moment(markings[0].xaxis.to).format()).toBe(moment('2018-01-08T00:59:59+01:00').format());
+        expect(markings[0].color).toBe(colorModes.red.color.fill);
+
+        expect(moment(markings[1].xaxis.from).format()).toBe(moment('2018-01-14T01:00:00+01:00').format());
+        expect(moment(markings[1].xaxis.to).format()).toBe(moment('2018-01-15T00:59:59+01:00').format());
+        expect(markings[1].color).toBe(colorModes.red.color.fill);
+
+        expect(moment(markings[2].xaxis.from).format()).toBe(moment('2018-01-21T01:00:00+01:00').format());
+        expect(moment(markings[2].xaxis.to).format()).toBe(moment('2018-01-22T00:59:59+01:00').format());
+        expect(markings[2].color).toBe(colorModes.red.color.fill);
+      });
+    });
+
+    plotOptionsScenario('for day of week to region', ctx => {
+      const regions = [{ toDayOfWeek: 7, fill: true, colorMode: 'red' }];
+      const from = moment('2018-01-01T18:45:05+01:00');
+      const to = moment('2018-01-22T08:27:00+01:00');
+      ctx.setup(regions, from, to);
+
+      it('should add 3 markings', () => {
+        expect(ctx.options.grid.markings.length).toBe(3);
+      });
+
+      it('should add one fill at each sunday', () => {
+        const markings = ctx.options.grid.markings;
+
+        expect(moment(markings[0].xaxis.from).format()).toBe(moment('2018-01-07T01:00:00+01:00').format());
+        expect(moment(markings[0].xaxis.to).format()).toBe(moment('2018-01-08T00:59:59+01:00').format());
+        expect(markings[0].color).toBe(colorModes.red.color.fill);
+
+        expect(moment(markings[1].xaxis.from).format()).toBe(moment('2018-01-14T01:00:00+01:00').format());
+        expect(moment(markings[1].xaxis.to).format()).toBe(moment('2018-01-15T00:59:59+01:00').format());
+        expect(markings[1].color).toBe(colorModes.red.color.fill);
+
+        expect(moment(markings[2].xaxis.from).format()).toBe(moment('2018-01-21T01:00:00+01:00').format());
+        expect(moment(markings[2].xaxis.to).format()).toBe(moment('2018-01-22T00:59:59+01:00').format());
+        expect(markings[2].color).toBe(colorModes.red.color.fill);
+      });
+    });
+
+    plotOptionsScenario('for day of week from/to time region with daylight saving time', ctx => {
+      const regions = [{ fromDayOfWeek: 7, from: '20:00', toDayOfWeek: 7, to: '23:00', fill: true, colorMode: 'red' }];
+      const from = moment('2018-03-17T06:00:00+01:00');
+      const to = moment('2018-04-03T06:00:00+02:00');
+      ctx.setup(regions, from, to);
+
+      it('should add 3 markings', () => {
+        expect(ctx.options.grid.markings.length).toBe(3);
+      });
+
+      it('should add one fill at each sunday between 20:00 and 23:00', () => {
+        const markings = ctx.options.grid.markings;
+
+        expect(moment(markings[0].xaxis.from).format()).toBe(moment('2018-03-18T21:00:00+01:00').format());
+        expect(moment(markings[0].xaxis.to).format()).toBe(moment('2018-03-19T00:00:00+01:00').format());
+
+        expect(moment(markings[1].xaxis.from).format()).toBe(moment('2018-03-25T22:00:00+02:00').format());
+        expect(moment(markings[1].xaxis.to).format()).toBe(moment('2018-03-26T01:00:00+02:00').format());
+
+        expect(moment(markings[2].xaxis.from).format()).toBe(moment('2018-04-01T22:00:00+02:00').format());
+        expect(moment(markings[2].xaxis.to).format()).toBe(moment('2018-04-02T01:00:00+02:00').format());
+      });
+    });
+
+    plotOptionsScenario('for each day of week with winter time', ctx => {
+      const regions = [{ fromDayOfWeek: 7, toDayOfWeek: 7, fill: true, colorMode: 'red' }];
+      const from = moment('2018-10-20T14:50:11+02:00');
+      const to = moment('2018-11-07T12:56:23+01:00');
+      ctx.setup(regions, from, to);
+
+      it('should add 3 markings', () => {
+        expect(ctx.options.grid.markings.length).toBe(3);
+      });
+
+      it('should add one fill at each sunday', () => {
+        const markings = ctx.options.grid.markings;
+
+        expect(moment(markings[0].xaxis.from).format()).toBe(moment('2018-10-21T02:00:00+02:00').format());
+        expect(moment(markings[0].xaxis.to).format()).toBe(moment('2018-10-22T01:59:59+02:00').format());
+
+        expect(moment(markings[1].xaxis.from).format()).toBe(moment('2018-10-28T02:00:00+02:00').format());
+        expect(moment(markings[1].xaxis.to).format()).toBe(moment('2018-10-29T00:59:59+01:00').format());
+
+        expect(moment(markings[2].xaxis.from).format()).toBe(moment('2018-11-04T01:00:00+01:00').format());
+        expect(moment(markings[2].xaxis.to).format()).toBe(moment('2018-11-05T00:59:59+01:00').format());
+      });
+    });
+  });
+});

+ 9 - 0
public/app/plugins/panel/graph/tab_display.html

@@ -14,6 +14,11 @@
 					Thresholds <span class="muted">({{ctrl.panel.thresholds.length}})</span>
 				</a>
 			</li>
+			<li ng-class="{active: ctrl.subTabIndex === 3}">
+				<a ng-click="ctrl.subTabIndex = 3">
+					Time regions <span class="muted">({{ctrl.panel.timeRegions.length}})</span>
+				</a>
+			</li>
 		</ul>
 	</aside>
 
@@ -132,4 +137,8 @@
 		<graph-threshold-form panel-ctrl="ctrl"></graph-threshold-form>
 	</div>
 
+	<div class="edit-tab-content" ng-if="ctrl.subTabIndex === 3">
+		<graph-time-region-form panel-ctrl="ctrl"></graph-time-region-form>
+	</div>
+
 </div>

+ 77 - 0
public/app/plugins/panel/graph/thresholds_form.html

@@ -0,0 +1,77 @@
+<div class="gf-form-group">
+  <h5>Thresholds</h5>
+  <p class="muted" ng-show="ctrl.disabled">
+    Visual thresholds options <strong>disabled.</strong>
+    Visit the Alert tab update your thresholds. <br>
+    To re-enable thresholds, the alert rule must be deleted from this panel.
+  </p>
+  <div ng-class="{'thresholds-form-disabled': ctrl.disabled}">
+    <div class="gf-form-inline" ng-repeat="threshold in ctrl.panel.thresholds">
+      <div class="gf-form">
+        <label class="gf-form-label">T{{$index+1}}</label>
+      </div>
+
+      <div class="gf-form">
+        <div class="gf-form-select-wrapper">
+          <select class="gf-form-input" ng-model="threshold.op"
+                  ng-options="f for f in ['gt', 'lt']" ng-change="ctrl.render()" ng-disabled="ctrl.disabled"></select>
+        </div>
+        <input type="number" ng-model="threshold.value" class="gf-form-input width-8"
+               ng-change="ctrl.render()" placeholder="value" ng-disabled="ctrl.disabled">
+      </div>
+
+      <div class="gf-form">
+        <label class="gf-form-label">Color</label>
+        <div class="gf-form-select-wrapper">
+          <select class="gf-form-input" ng-model="threshold.colorMode"
+                  ng-options="f for f in ['custom', 'critical', 'warning', 'ok']" ng-change="ctrl.render()" ng-disabled="ctrl.disabled">
+          </select>
+        </div>
+      </div>
+
+      <gf-form-switch class="gf-form" label="Fill" checked="threshold.fill"
+                      on-change="ctrl.render()" ng-disabled="ctrl.disabled"></gf-form-switch>
+
+      <div class="gf-form" ng-if="threshold.fill && threshold.colorMode === 'custom'">
+        <label class="gf-form-label">Fill color</label>
+        <span class="gf-form-label">
+          <color-picker color="threshold.fillColor" onChange="ctrl.onFillColorChange($index)"></color-picker>
+        </span>
+      </div>
+
+      <gf-form-switch class="gf-form" label="Line" checked="threshold.line"
+                      on-change="ctrl.render()" ng-disabled="ctrl.disabled"></gf-form-switch>
+
+      <div class="gf-form" ng-if="threshold.line && threshold.colorMode === 'custom'">
+        <label class="gf-form-label">Line color</label>
+        <span class="gf-form-label">
+          <color-picker color="threshold.lineColor" onChange="ctrl.onLineColorChange($index)"></color-picker>
+        </span>
+      </div>
+
+      <div class="gf-form">
+        <label class="gf-form-label">Y-Axis</label>
+        <div class="gf-form-select-wrapper">
+          <select class="gf-form-input" ng-model="threshold.yaxis"
+                  ng-init="threshold.yaxis = threshold.yaxis === 'left' || threshold.yaxis === 'right' ? threshold.yaxis : 'left'"
+                  ng-options="f for f in ['left', 'right']" ng-change="ctrl.render()" ng-disabled="ctrl.disabled">
+          </select>
+        </div>
+      </div>
+
+      <div class="gf-form">
+        <label class="gf-form-label">
+          <a class="pointer" ng-click="ctrl.removeThreshold($index)" ng-disabled="ctrl.disabled">
+            <i class="fa fa-trash"></i>
+          </a>
+        </label>
+      </div>
+    </div>
+
+    <div class="gf-form-button-row">
+      <button class="btn btn-inverse" ng-click="ctrl.addThreshold()" ng-disabled="ctrl.disabled">
+        <i class="fa fa-plus"></i>&nbsp;Add Threshold
+      </button>
+    </div>
+  </div>
+</div>

+ 1 - 81
public/app/plugins/panel/graph/thresholds_form.ts

@@ -58,90 +58,10 @@ export class ThresholdFormCtrl {
   }
 }
 
-const template = `
-<div class="gf-form-group">
-  <h5>Thresholds</h5>
-  <p class="muted" ng-show="ctrl.disabled">
-    Visual thresholds options <strong>disabled.</strong>
-    Visit the Alert tab update your thresholds. <br>
-    To re-enable thresholds, the alert rule must be deleted from this panel.
-  </p>
-  <div ng-class="{'thresholds-form-disabled': ctrl.disabled}">
-    <div class="gf-form-inline" ng-repeat="threshold in ctrl.panel.thresholds">
-      <div class="gf-form">
-        <label class="gf-form-label">T{{$index+1}}</label>
-      </div>
-
-      <div class="gf-form">
-        <div class="gf-form-select-wrapper">
-          <select class="gf-form-input" ng-model="threshold.op"
-                  ng-options="f for f in ['gt', 'lt']" ng-change="ctrl.render()" ng-disabled="ctrl.disabled"></select>
-        </div>
-        <input type="number" ng-model="threshold.value" class="gf-form-input width-8"
-               ng-change="ctrl.render()" placeholder="value" ng-disabled="ctrl.disabled">
-      </div>
-
-      <div class="gf-form">
-        <label class="gf-form-label">Color</label>
-        <div class="gf-form-select-wrapper">
-          <select class="gf-form-input" ng-model="threshold.colorMode"
-                  ng-options="f for f in ['custom', 'critical', 'warning', 'ok']" ng-change="ctrl.render()" ng-disabled="ctrl.disabled">
-          </select>
-        </div>
-      </div>
-
-      <gf-form-switch class="gf-form" label="Fill" checked="threshold.fill"
-                      on-change="ctrl.render()" ng-disabled="ctrl.disabled"></gf-form-switch>
-
-      <div class="gf-form" ng-if="threshold.fill && threshold.colorMode === 'custom'">
-        <label class="gf-form-label">Fill color</label>
-        <span class="gf-form-label">
-          <color-picker color="threshold.fillColor" onChange="ctrl.onFillColorChange($index)"></color-picker>
-        </span>
-      </div>
-
-      <gf-form-switch class="gf-form" label="Line" checked="threshold.line"
-                      on-change="ctrl.render()" ng-disabled="ctrl.disabled"></gf-form-switch>
-
-      <div class="gf-form" ng-if="threshold.line && threshold.colorMode === 'custom'">
-        <label class="gf-form-label">Line color</label>
-        <span class="gf-form-label">
-          <color-picker color="threshold.lineColor" onChange="ctrl.onLineColorChange($index)"></color-picker>
-        </span>
-      </div>
-
-      <div class="gf-form">
-        <label class="gf-form-label">Y-Axis</label>
-        <div class="gf-form-select-wrapper">
-          <select class="gf-form-input" ng-model="threshold.yaxis"
-                  ng-init="threshold.yaxis = threshold.yaxis === 'left' || threshold.yaxis === 'right' ? threshold.yaxis : 'left'"
-                  ng-options="f for f in ['left', 'right']" ng-change="ctrl.render()" ng-disabled="ctrl.disabled">
-          </select>
-        </div>
-      </div>
-
-      <div class="gf-form">
-        <label class="gf-form-label">
-          <a class="pointer" ng-click="ctrl.removeThreshold($index)" ng-disabled="ctrl.disabled">
-            <i class="fa fa-trash"></i>
-          </a>
-        </label>
-      </div>
-    </div>
-
-    <div class="gf-form-button-row">
-      <button class="btn btn-inverse" ng-click="ctrl.addThreshold()" ng-disabled="ctrl.disabled">
-        <i class="fa fa-plus"></i>&nbsp;Add Threshold
-      </button>
-    </div>
-  </div>
-</div>
-`;
-
 coreModule.directive('graphThresholdForm', () => {
   return {
     restrict: 'E',
-    template: template,
+    templateUrl: 'public/app/plugins/panel/graph/thresholds_form.html',
     controller: ThresholdFormCtrl,
     bindToController: true,
     controllerAs: 'ctrl',

+ 248 - 0
public/app/plugins/panel/graph/time_region_manager.ts

@@ -0,0 +1,248 @@
+import 'vendor/flot/jquery.flot';
+import _ from 'lodash';
+import moment from 'moment';
+import config from 'app/core/config';
+
+export const colorModes = {
+  gray: {
+    themeDependent: true,
+    title: 'Gray',
+    darkColor: { fill: 'rgba(255, 255, 255, 0.09)', line: 'rgba(255, 255, 255, 0.2)' },
+    lightColor: { fill: 'rgba(0, 0, 0, 0.09)', line: 'rgba(0, 0, 0, 0.2)' },
+  },
+  red: {
+    title: 'Red',
+    color: { fill: 'rgba(234, 112, 112, 0.12)', line: 'rgba(237, 46, 24, 0.60)' },
+  },
+  green: {
+    title: 'Green',
+    color: { fill: 'rgba(11, 237, 50, 0.090)', line: 'rgba(6,163,69, 0.60)' },
+  },
+  blue: {
+    title: 'Blue',
+    color: { fill: 'rgba(11, 125, 238, 0.12)', line: 'rgba(11, 125, 238, 0.60)' },
+  },
+  yellow: {
+    title: 'Yellow',
+    color: { fill: 'rgba(235, 138, 14, 0.12)', line: 'rgba(247, 149, 32, 0.60)' },
+  },
+  custom: { title: 'Custom' },
+};
+
+export function getColorModes() {
+  return _.map(Object.keys(colorModes), key => {
+    return {
+      key: key,
+      value: colorModes[key].title,
+    };
+  });
+}
+
+function getColor(timeRegion) {
+  if (Object.keys(colorModes).indexOf(timeRegion.colorMode) === -1) {
+    timeRegion.colorMode = 'red';
+  }
+
+  if (timeRegion.colorMode === 'custom') {
+    return {
+      fill: timeRegion.fillColor,
+      line: timeRegion.lineColor,
+    };
+  }
+
+  const colorMode = colorModes[timeRegion.colorMode];
+  if (colorMode.themeDependent === true) {
+    return config.bootData.user.lightTheme ? colorMode.lightColor : colorMode.darkColor;
+  }
+
+  return colorMode.color;
+}
+
+export class TimeRegionManager {
+  plot: any;
+  timeRegions: any;
+
+  constructor(private panelCtrl) {}
+
+  draw(plot) {
+    this.timeRegions = this.panelCtrl.panel.timeRegions;
+    this.plot = plot;
+  }
+
+  addFlotOptions(options, panel) {
+    if (!panel.timeRegions || panel.timeRegions.length === 0) {
+      return;
+    }
+
+    const tRange = { from: moment(this.panelCtrl.range.from).utc(), to: moment(this.panelCtrl.range.to).utc() };
+
+    let i, hRange, timeRegion, regions, fromStart, fromEnd, timeRegionColor;
+
+    const timeRegionsCopy = panel.timeRegions.map(a => ({ ...a }));
+
+    for (i = 0; i < timeRegionsCopy.length; i++) {
+      timeRegion = timeRegionsCopy[i];
+
+      if (!(timeRegion.fromDayOfWeek || timeRegion.from) && !(timeRegion.toDayOfWeek || timeRegion.to)) {
+        continue;
+      }
+
+      hRange = {
+        from: this.parseTimeRange(timeRegion.from),
+        to: this.parseTimeRange(timeRegion.to),
+      };
+
+      if (!timeRegion.fromDayOfWeek && timeRegion.toDayOfWeek) {
+        timeRegion.fromDayOfWeek = timeRegion.toDayOfWeek;
+      }
+
+      if (!timeRegion.toDayOfWeek && timeRegion.fromDayOfWeek) {
+        timeRegion.toDayOfWeek = timeRegion.fromDayOfWeek;
+      }
+
+      if (timeRegion.fromDayOfWeek) {
+        hRange.from.dayOfWeek = Number(timeRegion.fromDayOfWeek);
+      }
+
+      if (timeRegion.toDayOfWeek) {
+        hRange.to.dayOfWeek = Number(timeRegion.toDayOfWeek);
+      }
+
+      if (!hRange.from.h && hRange.to.h) {
+        hRange.from = hRange.to;
+      }
+
+      if (hRange.from.h && !hRange.to.h) {
+        hRange.to = hRange.from;
+      }
+
+      if (hRange.from.dayOfWeek && !hRange.from.h && !hRange.from.m) {
+        hRange.from.h = 0;
+        hRange.from.m = 0;
+        hRange.from.s = 0;
+      }
+
+      if (hRange.to.dayOfWeek && !hRange.to.h && !hRange.to.m) {
+        hRange.to.h = 23;
+        hRange.to.m = 59;
+        hRange.to.s = 59;
+      }
+
+      if (!hRange.from || !hRange.to) {
+        continue;
+      }
+
+      regions = [];
+
+      if (
+        hRange.from.h >= tRange.from.hour() &&
+        hRange.from.h <= tRange.from.hour() &&
+        hRange.from.m >= tRange.from.minute() &&
+        hRange.from.m <= tRange.from.minute() &&
+        hRange.to.h >= tRange.to.hour() &&
+        hRange.to.h <= tRange.to.hour() &&
+        hRange.to.m >= tRange.to.minute() &&
+        hRange.to.m <= tRange.to.minute()
+      ) {
+        regions.push({ from: tRange.from.valueOf(), to: tRange.to.startOf('hour').valueOf() });
+      } else {
+        fromStart = moment(tRange.from);
+        fromStart.set('hour', 0);
+        fromStart.set('minute', 0);
+        fromStart.set('second', 0);
+        fromStart.add(hRange.from.h, 'hours');
+        fromStart.add(hRange.from.m, 'minutes');
+        fromStart.add(hRange.from.s, 'seconds');
+
+        while (fromStart.unix() <= tRange.to.unix()) {
+          while (hRange.from.dayOfWeek && hRange.from.dayOfWeek !== fromStart.isoWeekday()) {
+            fromStart.add(24, 'hours');
+          }
+
+          if (fromStart.unix() > tRange.to.unix()) {
+            break;
+          }
+
+          fromEnd = moment(fromStart);
+
+          if (hRange.from.h <= hRange.to.h) {
+            fromEnd.add(hRange.to.h - hRange.from.h, 'hours');
+          } else if (hRange.from.h + hRange.to.h < 23) {
+            fromEnd.add(hRange.to.h, 'hours');
+          } else {
+            fromEnd.add(24 - hRange.from.h, 'hours');
+          }
+
+          fromEnd.set('minute', hRange.to.m);
+          fromEnd.set('second', hRange.to.s);
+
+          while (hRange.to.dayOfWeek && hRange.to.dayOfWeek !== fromEnd.isoWeekday()) {
+            fromEnd.add(24, 'hours');
+          }
+
+          const outsideRange =
+            (fromStart.unix() < tRange.from.unix() && fromEnd.unix() < tRange.from.unix()) ||
+            (fromStart.unix() > tRange.to.unix() && fromEnd.unix() > tRange.to.unix());
+
+          if (!outsideRange) {
+            regions.push({ from: fromStart.valueOf(), to: fromEnd.valueOf() });
+          }
+
+          fromStart.add(24, 'hours');
+        }
+      }
+
+      timeRegionColor = getColor(timeRegion);
+
+      for (let j = 0; j < regions.length; j++) {
+        const r = regions[j];
+        if (timeRegion.fill) {
+          options.grid.markings.push({
+            xaxis: { from: r.from, to: r.to },
+            color: timeRegionColor.fill,
+          });
+        }
+
+        if (timeRegion.line) {
+          options.grid.markings.push({
+            xaxis: { from: r.from, to: r.from },
+            color: timeRegionColor.line,
+          });
+          options.grid.markings.push({
+            xaxis: { from: r.to, to: r.to },
+            color: timeRegionColor.line,
+          });
+        }
+      }
+    }
+  }
+
+  parseTimeRange(str) {
+    const timeRegex = /^([\d]+):?(\d{2})?/;
+    const result = { h: null, m: null };
+    const match = timeRegex.exec(str);
+
+    if (!match) {
+      return result;
+    }
+
+    if (match.length > 1) {
+      result.h = Number(match[1]);
+      result.m = 0;
+
+      if (match.length > 2 && match[2] !== undefined) {
+        result.m = Number(match[2]);
+      }
+
+      if (result.h > 23) {
+        result.h = 23;
+      }
+
+      if (result.m > 59) {
+        result.m = 59;
+      }
+    }
+
+    return result;
+  }
+}

+ 64 - 0
public/app/plugins/panel/graph/time_regions_form.html

@@ -0,0 +1,64 @@
+<div class="gf-form-group">
+  <h5>Time regions <tip>All configured time regions refers to UTC time</tip></h5>
+  <div class="gf-form-inline" ng-repeat="timeRegion in ctrl.panel.timeRegions">
+    <div class="gf-form">
+      <label class="gf-form-label">T{{$index+1}}</label>
+    </div>
+
+    <div class="gf-form">
+      <label class="gf-form-label">From</label>
+      <div class="gf-form-select-wrapper">
+        <select class="gf-form-input width-6" ng-model="timeRegion.fromDayOfWeek" ng-options="f.d as f.value for f in [{d: undefined, value: 'Any'}, {d:1, value: 'Mon'}, {d:2, value: 'Tue'}, {d:3, value: 'Wed'}, {d:4, value: 'Thu'}, {d:5, value: 'Fri'}, {d:6, value: 'Sat'}, {d:7, value: 'Sun'}]"
+          ng-change="ctrl.render()"></select>
+      </div>
+      <input type="text" ng-maxlength="5" ng-model="timeRegion.from" class="gf-form-input width-5" ng-change="ctrl.render()" placeholder="hh:mm">
+      <label class="gf-form-label">To</label>
+      <div class="gf-form-select-wrapper">
+        <select class="gf-form-input width-6" ng-model="timeRegion.toDayOfWeek" ng-options="f.d as f.value for f in [{d: undefined, value: 'Any'}, {d:1, value: 'Mon'}, {d:2, value: 'Tue'}, {d:3, value: 'Wed'}, {d:4, value: 'Thu'}, {d:5, value: 'Fri'}, {d:6, value: 'Sat'}, {d:7, value: 'Sun'}]"
+          ng-change="ctrl.render()"></select>
+      </div>
+      <input type="text" ng-maxlength="5" ng-model="timeRegion.to" class="gf-form-input width-5" ng-change="ctrl.render()" placeholder="hh:mm"
+      >
+    </div>
+
+    <div class="gf-form">
+      <label class="gf-form-label">Color</label>
+      <div class="gf-form-select-wrapper">
+        <select class="gf-form-input" ng-model="timeRegion.colorMode" ng-options="f.key as f.value for f in ctrl.colorModes" ng-change="ctrl.render()">
+        </select>
+      </div>
+    </div>
+
+    <gf-form-switch class="gf-form" label="Fill" checked="timeRegion.fill" on-change="ctrl.render()"></gf-form-switch>
+
+    <div class="gf-form" ng-if="timeRegion.fill && timeRegion.colorMode === 'custom'">
+      <label class="gf-form-label">Fill color</label>
+      <span class="gf-form-label">
+        <color-picker color="timeRegion.fillColor" onChange="ctrl.onFillColorChange($index)"></color-picker>
+      </span>
+    </div>
+
+    <gf-form-switch class="gf-form" label="Line" checked="timeRegion.line" on-change="ctrl.render()"></gf-form-switch>
+
+    <div class="gf-form" ng-if="timeRegion.line && timeRegion.colorMode === 'custom'">
+      <label class="gf-form-label">Line color</label>
+      <span class="gf-form-label">
+        <color-picker color="timeRegion.lineColor" onChange="ctrl.onLineColorChange($index)"></color-picker>
+      </span>
+    </div>
+
+    <div class="gf-form">
+      <label class="gf-form-label">
+        <a class="pointer" ng-click="ctrl.removeTimeRegion($index)">
+          <i class="fa fa-trash"></i>
+        </a>
+      </label>
+    </div>
+  </div>
+
+  <div class="gf-form-button-row">
+    <button class="btn btn-inverse" ng-click="ctrl.addTimeRegion()">
+      <i class="fa fa-plus"></i>&nbsp;Add time region
+    </button>
+  </div>
+</div>

+ 73 - 0
public/app/plugins/panel/graph/time_regions_form.ts

@@ -0,0 +1,73 @@
+import coreModule from 'app/core/core_module';
+import { getColorModes } from './time_region_manager';
+
+export class TimeRegionFormCtrl {
+  panelCtrl: any;
+  panel: any;
+  disabled: boolean;
+  colorModes: any;
+
+  /** @ngInject */
+  constructor($scope) {
+    this.panel = this.panelCtrl.panel;
+
+    const unbindDestroy = $scope.$on('$destroy', () => {
+      this.panelCtrl.editingTimeRegions = false;
+      this.panelCtrl.render();
+      unbindDestroy();
+    });
+
+    this.colorModes = getColorModes();
+    this.panelCtrl.editingTimeRegions = true;
+  }
+
+  render() {
+    this.panelCtrl.render();
+  }
+
+  addTimeRegion() {
+    this.panel.timeRegions.push({
+      op: 'time',
+      fromDayOfWeek: undefined,
+      from: undefined,
+      toDayOfWeek: undefined,
+      to: undefined,
+      colorMode: 'background6',
+      fill: true,
+      line: false,
+    });
+    this.panelCtrl.render();
+  }
+
+  removeTimeRegion(index) {
+    this.panel.timeRegions.splice(index, 1);
+    this.panelCtrl.render();
+  }
+
+  onFillColorChange(index) {
+    return newColor => {
+      this.panel.timeRegions[index].fillColor = newColor;
+      this.render();
+    };
+  }
+
+  onLineColorChange(index) {
+    return newColor => {
+      this.panel.timeRegions[index].lineColor = newColor;
+      this.render();
+    };
+  }
+}
+
+coreModule.directive('graphTimeRegionForm', () => {
+  return {
+    restrict: 'E',
+    templateUrl: 'public/app/plugins/panel/graph/time_regions_form.html',
+    controller: TimeRegionFormCtrl,
+    bindToController: true,
+    controllerAs: 'ctrl',
+    scope: {
+      panelCtrl: '=',
+    },
+  };
+});