Commit 0810aa2e by Marcus Efraimsson Committed by GitHub

Merge pull request #12371 from marefr/xaxis_thresholds

Time regions support in graph panel
parents fe45cb9a 3b4a224a
......@@ -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" >}}
......
......@@ -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);
......
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 */
......
......@@ -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>
<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>
\ No newline at end of file
......@@ -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',
......
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;
}
}
<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>
\ No newline at end of file
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: '=',
},
};
});
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment