Merge remote-tracking branch 'upstream/master' into fix-11053

This commit is contained in:
Alexander Zobnin
2018-04-05 16:30:46 +03:00
86 changed files with 1622 additions and 767 deletions

View File

@@ -31,6 +31,7 @@ export class HelpCtrl {
{ keys: ['e'], description: 'Toggle panel edit view' },
{ keys: ['v'], description: 'Toggle panel fullscreen view' },
{ keys: ['p', 's'], description: 'Open Panel Share Modal' },
{ keys: ['p', 'd'], description: 'Duplicate Panel' },
{ keys: ['p', 'r'], description: 'Remove Panel' },
],
'Time Range': [

View File

@@ -20,7 +20,7 @@
<div class="search-section__header" ng-show="section.hideHeader"></div>
<div ng-if="section.expanded">
<a ng-repeat="item in section.items" class="search-item" ng-class="{'selected': item.selected}" ng-href="{{::item.url}}" >
<a ng-repeat="item in section.items" class="search-item search-item--indent" ng-class="{'selected': item.selected}" ng-href="{{::item.url}}" >
<div ng-click="ctrl.toggleSelection(item, $event)">
<gf-form-switch
ng-show="ctrl.editable"

View File

@@ -1,13 +0,0 @@
define([
'./alert_srv',
'./util_srv',
'./context_srv',
'./timer',
'./analytics',
'./popover_srv',
'./segment_srv',
'./backend_srv',
'./dynamic_directive_srv',
'./bridge_srv'
],
function () {});

View File

@@ -0,0 +1,10 @@
import './alert_srv';
import './util_srv';
import './context_srv';
import './timer';
import './analytics';
import './popover_srv';
import './segment_srv';
import './backend_srv';
import './dynamic_directive_srv';
import './bridge_srv';

View File

@@ -10,6 +10,7 @@ import 'mousetrap-global-bind';
export class KeybindingSrv {
helpModal: boolean;
modalOpen = false;
timepickerOpen = false;
/** @ngInject */
constructor(private $rootScope, private $location) {
@@ -22,6 +23,8 @@ export class KeybindingSrv {
this.setupGlobal();
appEvents.on('show-modal', () => (this.modalOpen = true));
$rootScope.onAppEvent('timepickerOpen', () => (this.timepickerOpen = true));
$rootScope.onAppEvent('timepickerClosed', () => (this.timepickerOpen = false));
}
setupGlobal() {
@@ -73,7 +76,12 @@ export class KeybindingSrv {
appEvents.emit('hide-modal');
if (!this.modalOpen) {
this.$rootScope.appEvent('panel-change-view', { fullscreen: false, edit: false });
if (this.timepickerOpen) {
this.$rootScope.appEvent('closeTimepicker');
this.timepickerOpen = false;
} else {
this.$rootScope.appEvent('panel-change-view', { fullscreen: false, edit: false });
}
} else {
this.modalOpen = false;
}

View File

@@ -447,6 +447,7 @@ kbn.valueFormats.currencyDKK = kbn.formatBuilders.currency('kr');
kbn.valueFormats.currencyISK = kbn.formatBuilders.currency('kr');
kbn.valueFormats.currencyNOK = kbn.formatBuilders.currency('kr');
kbn.valueFormats.currencySEK = kbn.formatBuilders.currency('kr');
kbn.valueFormats.currencyCZK = kbn.formatBuilders.currency('czk');
// Data (Binary)
kbn.valueFormats.bits = kbn.formatBuilders.binarySIPrefix('b');
@@ -869,6 +870,7 @@ kbn.getUnitFormats = function() {
{ text: 'Icelandic Króna (kr)', value: 'currencyISK' },
{ text: 'Norwegian Krone (kr)', value: 'currencyNOK' },
{ text: 'Swedish Krona (kr)', value: 'currencySEK' },
{ text: 'Czech koruna (czk)', value: 'currencyCZK' },
],
},
{

View File

@@ -43,6 +43,7 @@ export class AlertNotificationEditCtrl {
return this.backendSrv.get(`/api/alert-notifications/${this.$routeParams.id}`).then(result => {
this.navModel.breadcrumbs.push({ text: result.name });
this.navModel.node = { text: result.name };
result.settings = _.defaults(result.settings, this.defaults.settings);
return result;
});
})
@@ -89,7 +90,7 @@ export class AlertNotificationEditCtrl {
}
typeChanged() {
this.model.settings = {};
this.model.settings = _.defaults({}, this.defaults.settings);
this.notifierTemplateId = this.getNotifierTemplateId(this.model.type);
}

View File

@@ -1,9 +1,5 @@
export class ThresholdMapper {
static alertToGraphThresholds(panel) {
if (panel.type !== 'graph') {
return false;
}
for (var i = 0; i < panel.alert.conditions.length; i++) {
let condition = panel.alert.conditions[i];
if (condition.type !== 'query') {

View File

@@ -1,15 +0,0 @@
define([
'./panellinks/module',
'./dashlinks/module',
'./annotations/all',
'./templating/all',
'./plugins/all',
'./dashboard/all',
'./playlist/all',
'./snapshot/all',
'./panel/all',
'./org/all',
'./admin/admin',
'./alerting/all',
'./styleguide/styleguide',
], function () {});

View File

@@ -0,0 +1,13 @@
import './panellinks/module';
import './dashlinks/module';
import './annotations/all';
import './templating/all';
import './plugins/all';
import './dashboard/all';
import './playlist/all';
import './snapshot/all';
import './panel/all';
import './org/all';
import './admin/admin';
import './alerting/all';
import './styleguide/styleguide';

View File

@@ -22,7 +22,6 @@ export class TimePickerCtrl {
refresh: any;
isUtc: boolean;
firstDayOfWeek: number;
closeDropdown: any;
isOpen: boolean;
/** @ngInject */
@@ -32,6 +31,7 @@ export class TimePickerCtrl {
$rootScope.onAppEvent('shift-time-forward', () => this.move(1), $scope);
$rootScope.onAppEvent('shift-time-backward', () => this.move(-1), $scope);
$rootScope.onAppEvent('refresh', this.onRefresh.bind(this), $scope);
$rootScope.onAppEvent('closeTimepicker', this.openDropdown.bind(this), $scope);
// init options
this.panel = this.dashboard.timepicker;
@@ -96,7 +96,7 @@ export class TimePickerCtrl {
openDropdown() {
if (this.isOpen) {
this.isOpen = false;
this.closeDropdown();
return;
}
@@ -112,6 +112,12 @@ export class TimePickerCtrl {
this.refresh.options.unshift({ text: 'off' });
this.isOpen = true;
this.$rootScope.appEvent('timepickerOpen');
}
closeDropdown() {
this.isOpen = false;
this.$rootScope.appEvent('timepickerClosed');
}
applyCustom() {
@@ -120,7 +126,7 @@ export class TimePickerCtrl {
}
this.timeSrv.setTime(this.editTimeRaw);
this.isOpen = false;
this.closeDropdown();
}
absoluteFromChanged() {
@@ -143,7 +149,7 @@ export class TimePickerCtrl {
}
this.timeSrv.setTime(range);
this.isOpen = false;
this.closeDropdown();
}
}

View File

@@ -35,12 +35,12 @@ export class Tracker {
$window.onbeforeunload = () => {
if (this.ignoreChanges()) {
return null;
return undefined;
}
if (this.hasChanges()) {
return 'There are unsaved changes to this dashboard';
}
return null;
return undefined;
};
scope.$on('$locationChangeStart', (event, next) => {

View File

@@ -1,9 +0,0 @@
define([
'./panel_header',
'./panel_directive',
'./solo_panel_ctrl',
'./query_ctrl',
'./panel_editor_tab',
'./query_editor_row',
'./query_troubleshooter',
], function () {});

View File

@@ -0,0 +1,7 @@
import './panel_header';
import './panel_directive';
import './solo_panel_ctrl';
import './query_ctrl';
import './panel_editor_tab';
import './query_editor_row';
import './query_troubleshooter';

View File

@@ -190,6 +190,7 @@ export class PanelCtrl {
text: 'Duplicate',
click: 'ctrl.duplicate()',
role: 'Editor',
shortcut: 'p d',
});
menu.push({

View File

@@ -1,7 +0,0 @@
define([
'./playlists_ctrl',
'./playlist_search',
'./playlist_srv',
'./playlist_edit_ctrl',
'./playlist_routes'
], function () {});

View File

@@ -0,0 +1,5 @@
import './playlists_ctrl';
import './playlist_search';
import './playlist_srv';
import './playlist_edit_ctrl';
import './playlist_routes';

View File

@@ -1,39 +0,0 @@
define([
'angular',
'lodash'
],
function (angular) {
'use strict';
var module = angular.module('grafana.routes');
module.config(function($routeProvider) {
$routeProvider
.when('/playlists', {
templateUrl: 'public/app/features/playlist/partials/playlists.html',
controllerAs: 'ctrl',
controller : 'PlaylistsCtrl'
})
.when('/playlists/create', {
templateUrl: 'public/app/features/playlist/partials/playlist.html',
controllerAs: 'ctrl',
controller : 'PlaylistEditCtrl'
})
.when('/playlists/edit/:id', {
templateUrl: 'public/app/features/playlist/partials/playlist.html',
controllerAs: 'ctrl',
controller : 'PlaylistEditCtrl'
})
.when('/playlists/play/:id', {
templateUrl: 'public/app/features/playlist/partials/playlists.html',
controllerAs: 'ctrl',
controller : 'PlaylistsCtrl',
resolve: {
init: function(playlistSrv, $route) {
var playlistId = $route.current.params.id;
playlistSrv.start(playlistId);
}
}
});
});
});

View File

@@ -0,0 +1,33 @@
import angular from 'angular';
function grafanaRoutes($routeProvider) {
$routeProvider
.when('/playlists', {
templateUrl: 'public/app/features/playlist/partials/playlists.html',
controllerAs: 'ctrl',
controller: 'PlaylistsCtrl',
})
.when('/playlists/create', {
templateUrl: 'public/app/features/playlist/partials/playlist.html',
controllerAs: 'ctrl',
controller: 'PlaylistEditCtrl',
})
.when('/playlists/edit/:id', {
templateUrl: 'public/app/features/playlist/partials/playlist.html',
controllerAs: 'ctrl',
controller: 'PlaylistEditCtrl',
})
.when('/playlists/play/:id', {
templateUrl: 'public/app/features/playlist/partials/playlists.html',
controllerAs: 'ctrl',
controller: 'PlaylistsCtrl',
resolve: {
init: function(playlistSrv, $route) {
let playlistId = $route.current.params.id;
playlistSrv.start(playlistId);
},
},
});
}
angular.module('grafana.routes').config(grafanaRoutes);

View File

@@ -23,6 +23,8 @@ export class VariableEditorCtrl {
{ value: 2, text: 'Alphabetical (desc)' },
{ value: 3, text: 'Numerical (asc)' },
{ value: 4, text: 'Numerical (desc)' },
{ value: 5, text: 'Alphabetical (case-insensitive, asc)' },
{ value: 6, text: 'Alphabetical (case-insensitive, desc)' },
];
$scope.hideOptions = [{ value: 0, text: '' }, { value: 1, text: 'Label' }, { value: 2, text: 'Variable' }];

View File

@@ -197,6 +197,10 @@ export class QueryVariable implements Variable {
return parseInt(matches[1], 10);
}
});
} else if (sortType === 3) {
options = _.sortBy(options, opt => {
return _.toLower(opt.text);
});
}
if (reverseSort) {

View File

@@ -40,11 +40,11 @@ describe('QueryVariable', () => {
});
describe('can convert and sort metric names', () => {
var variable = new QueryVariable({}, null, null, null, null);
variable.sort = 3; // Numerical (asc)
const variable = new QueryVariable({}, null, null, null, null);
let input;
describe('can sort a mixed array of metric variables', () => {
var input = [
beforeEach(() => {
input = [
{ text: '0', value: '0' },
{ text: '1', value: '1' },
{ text: null, value: 3 },
@@ -58,11 +58,18 @@ describe('QueryVariable', () => {
{ text: '', value: undefined },
{ text: undefined, value: '' },
];
});
describe('can sort a mixed array of metric variables in numeric order', () => {
let result;
beforeEach(() => {
variable.sort = 3; // Numerical (asc)
result = variable.metricNamesToVariableValues(input);
});
var result = variable.metricNamesToVariableValues(input);
it('should return in same order', () => {
var i = 0;
expect(result.length).toBe(11);
expect(result[i++].text).toBe('');
expect(result[i++].text).toBe('0');
@@ -73,5 +80,27 @@ describe('QueryVariable', () => {
expect(result[i++].text).toBe('6');
});
});
describe('can sort a mixed array of metric variables in alphabetical order', () => {
let result;
beforeEach(() => {
variable.sort = 5; // Alphabetical CI (asc)
result = variable.metricNamesToVariableValues(input);
});
it('should return in same order', () => {
var i = 0;
console.log(result);
expect(result.length).toBe(11);
expect(result[i++].text).toBe('');
expect(result[i++].text).toBe('0');
expect(result[i++].text).toBe('1');
expect(result[i++].text).toBe('10');
expect(result[i++].text).toBe('3');
expect(result[i++].text).toBe('4');
expect(result[i++].text).toBe('5');
});
});
});
});

View File

@@ -4,6 +4,7 @@ import $ from 'jquery';
import rst2html from 'rst2html';
import Drop from 'tether-drop';
/** @ngInject */
export function graphiteAddFunc($compile) {
const inputTemplate =
'<input type="text"' + ' class="gf-form-input"' + ' spellcheck="false" style="display:none"></input>';

View File

@@ -3,6 +3,7 @@ import _ from 'lodash';
import $ from 'jquery';
import rst2html from 'rst2html';
/** @ngInject */
export function graphiteFuncEditor($compile, templateSrv, popoverSrv) {
const funcSpanTemplate = '<a ng-click="">{{func.def.name}}</a><span>(</span>';
const paramTemplate =

View File

@@ -28,7 +28,7 @@ An annotation is an event that is overlayed on top of graphs. The query can have
Macros:
- $__time(column) -&gt; column AS time
- $__timeEpoch(column) -&gt; DATEDIFF(second, '1970-01-01', column) AS time
- $__timeFilter(column) -&gt; column &gt;= DATEADD(s, 18446744066914186738, '1970-01-01') AND column &t;= DATEADD(s, 18446744066914187038, '1970-01-01')
- $__timeFilter(column) -&gt; column &gt;= DATEADD(s, 18446744066914186738, '1970-01-01') AND column &lt;= DATEADD(s, 18446744066914187038, '1970-01-01')
- $__unixEpochFilter(column) -&gt; column &gt;= 1492750877 AND column &lt;= 1492750877
Or build your own conditionals using these macros which just return the values:

View File

@@ -49,7 +49,7 @@ Table:
Macros:
- $__time(column) -&gt; column AS time
- $__timeEpoch(column) -&gt; DATEDIFF(second, '1970-01-01', column) AS time
- $__timeFilter(column) -&gt; column &gt;= DATEADD(s, 18446744066914186738, '1970-01-01') AND column &t;= DATEADD(s, 18446744066914187038, '1970-01-01')
- $__timeFilter(column) -&gt; column &gt;= DATEADD(s, 18446744066914186738, '1970-01-01') AND column &lt;= DATEADD(s, 18446744066914187038, '1970-01-01')
- $__unixEpochFilter(column) -&gt; column &gt;= 1492750877 AND column &lt;= 1492750877
- $__timeGroup(column, '5m'[, fillvalue]) -&gt; CAST(ROUND(DATEDIFF(second, '1970-01-01', column)/300.0, 0) as bigint)*300. Providing a <i>fillValue</i> of <i>NULL</i> or floating value will automatically fill empty series in timerange with that value.

View File

@@ -6,8 +6,12 @@ import * as dateMath from 'app/core/utils/datemath';
import PrometheusMetricFindQuery from './metric_find_query';
import { ResultTransformer } from './result_transformer';
function prometheusSpecialRegexEscape(value) {
return value.replace(/[\\^$*+?.()|[\]{}]/g, '\\\\$&');
export function prometheusRegularEscape(value) {
return value.replace(/'/g, "\\\\'");
}
export function prometheusSpecialRegexEscape(value) {
return prometheusRegularEscape(value.replace(/\\/g, '\\\\\\\\').replace(/[$^*{}\[\]+?.()]/g, '\\\\$&'));
}
export class PrometheusDatasource {
@@ -80,7 +84,7 @@ export class PrometheusDatasource {
interpolateQueryExpr(value, variable, defaultFormatFn) {
// if no multi or include all do not regexEscape
if (!variable.multi && !variable.includeAll) {
return value;
return prometheusRegularEscape(value);
}
if (typeof value === 'string') {

View File

@@ -1,7 +1,7 @@
import _ from 'lodash';
import moment from 'moment';
import q from 'q';
import { PrometheusDatasource } from '../datasource';
import { PrometheusDatasource, prometheusSpecialRegexEscape, prometheusRegularEscape } from '../datasource';
describe('PrometheusDatasource', () => {
let ctx: any = {};
@@ -101,4 +101,41 @@ describe('PrometheusDatasource', () => {
});
});
});
describe('Prometheus regular escaping', function() {
it('should not escape simple string', function() {
expect(prometheusRegularEscape('cryptodepression')).toEqual('cryptodepression');
});
it("should escape '", function() {
expect(prometheusRegularEscape("looking'glass")).toEqual("looking\\\\'glass");
});
it('should escape multiple characters', function() {
expect(prometheusRegularEscape("'looking'glass'")).toEqual("\\\\'looking\\\\'glass\\\\'");
});
});
describe('Prometheus regexes escaping', function() {
it('should not escape simple string', function() {
expect(prometheusSpecialRegexEscape('cryptodepression')).toEqual('cryptodepression');
});
it('should escape $^*+?.()\\', function() {
expect(prometheusSpecialRegexEscape("looking'glass")).toEqual("looking\\\\'glass");
expect(prometheusSpecialRegexEscape('looking{glass')).toEqual('looking\\\\{glass');
expect(prometheusSpecialRegexEscape('looking}glass')).toEqual('looking\\\\}glass');
expect(prometheusSpecialRegexEscape('looking[glass')).toEqual('looking\\\\[glass');
expect(prometheusSpecialRegexEscape('looking]glass')).toEqual('looking\\\\]glass');
expect(prometheusSpecialRegexEscape('looking$glass')).toEqual('looking\\\\$glass');
expect(prometheusSpecialRegexEscape('looking^glass')).toEqual('looking\\\\^glass');
expect(prometheusSpecialRegexEscape('looking*glass')).toEqual('looking\\\\*glass');
expect(prometheusSpecialRegexEscape('looking+glass')).toEqual('looking\\\\+glass');
expect(prometheusSpecialRegexEscape('looking?glass')).toEqual('looking\\\\?glass');
expect(prometheusSpecialRegexEscape('looking.glass')).toEqual('looking\\\\.glass');
expect(prometheusSpecialRegexEscape('looking(glass')).toEqual('looking\\\\(glass');
expect(prometheusSpecialRegexEscape('looking)glass')).toEqual('looking\\\\)glass');
expect(prometheusSpecialRegexEscape('looking\\glass')).toEqual('looking\\\\\\\\glass');
});
it('should escape multiple special characters', function() {
expect(prometheusSpecialRegexEscape('+looking$glass?')).toEqual('\\\\+looking\\\\$glass\\\\?');
});
});
});

View File

@@ -1,292 +0,0 @@
define([
'jquery',
'app/core/core',
],
function ($, core) {
'use strict';
var appEvents = core.appEvents;
function GraphTooltip(elem, dashboard, scope, getSeriesFn) {
var self = this;
var ctrl = scope.ctrl;
var panel = ctrl.panel;
var $tooltip = $('<div class="graph-tooltip">');
this.destroy = function() {
$tooltip.remove();
};
this.findHoverIndexFromDataPoints = function(posX, series, last) {
var ps = series.datapoints.pointsize;
var initial = last*ps;
var len = series.datapoints.points.length;
for (var j = initial; j < len; j += ps) {
// Special case of a non stepped line, highlight the very last point just before a null point
if ((!series.lines.steps && series.datapoints.points[initial] != null && series.datapoints.points[j] == null)
//normal case
|| series.datapoints.points[j] > posX) {
return Math.max(j - ps, 0)/ps;
}
}
return j/ps - 1;
};
this.findHoverIndexFromData = function(posX, series) {
var lower = 0;
var upper = series.data.length - 1;
var middle;
while (true) {
if (lower > upper) {
return Math.max(upper, 0);
}
middle = Math.floor((lower + upper) / 2);
if (series.data[middle][0] === posX) {
return middle;
} else if (series.data[middle][0] < posX) {
lower = middle + 1;
} else {
upper = middle - 1;
}
}
};
this.renderAndShow = function(absoluteTime, innerHtml, pos, xMode) {
if (xMode === 'time') {
innerHtml = '<div class="graph-tooltip-time">'+ absoluteTime + '</div>' + innerHtml;
}
$tooltip.html(innerHtml).place_tt(pos.pageX + 20, pos.pageY);
};
this.getMultiSeriesPlotHoverInfo = function(seriesList, pos) {
var value, i, series, hoverIndex, hoverDistance, pointTime, yaxis;
// 3 sub-arrays, 1st for hidden series, 2nd for left yaxis, 3rd for right yaxis.
var results = [[],[],[]];
//now we know the current X (j) position for X and Y values
var last_value = 0; //needed for stacked values
var minDistance, minTime;
for (i = 0; i < seriesList.length; i++) {
series = seriesList[i];
if (!series.data.length || (panel.legend.hideEmpty && series.allIsNull)) {
// Init value so that it does not brake series sorting
results[0].push({ hidden: true, value: 0 });
continue;
}
if (!series.data.length || (panel.legend.hideZero && series.allIsZero)) {
// Init value so that it does not brake series sorting
results[0].push({ hidden: true, value: 0 });
continue;
}
hoverIndex = this.findHoverIndexFromData(pos.x, series);
hoverDistance = pos.x - series.data[hoverIndex][0];
pointTime = series.data[hoverIndex][0];
// Take the closest point before the cursor, or if it does not exist, the closest after
if (! minDistance
|| (hoverDistance >=0 && (hoverDistance < minDistance || minDistance < 0))
|| (hoverDistance < 0 && hoverDistance > minDistance)) {
minDistance = hoverDistance;
minTime = pointTime;
}
if (series.stack) {
if (panel.tooltip.value_type === 'individual') {
value = series.data[hoverIndex][1];
} else if (!series.stack) {
value = series.data[hoverIndex][1];
} else {
last_value += series.data[hoverIndex][1];
value = last_value;
}
} else {
value = series.data[hoverIndex][1];
}
// Highlighting multiple Points depending on the plot type
if (series.lines.steps || series.stack) {
// stacked and steppedLine plots can have series with different length.
// Stacked series can increase its length on each new stacked serie if null points found,
// to speed the index search we begin always on the last found hoverIndex.
hoverIndex = this.findHoverIndexFromDataPoints(pos.x, series, hoverIndex);
}
// Be sure we have a yaxis so that it does not brake series sorting
yaxis = 0;
if (series.yaxis) {
yaxis = series.yaxis.n;
}
results[yaxis].push({
value: value,
hoverIndex: hoverIndex,
color: series.color,
label: series.aliasEscaped,
time: pointTime,
distance: hoverDistance,
index: i
});
}
// Contat the 3 sub-arrays
results = results[0].concat(results[1],results[2]);
// Time of the point closer to pointer
results.time = minTime;
return results;
};
elem.mouseleave(function () {
if (panel.tooltip.shared) {
var plot = elem.data().plot;
if (plot) {
$tooltip.detach();
plot.unhighlight();
}
}
appEvents.emit('graph-hover-clear');
});
elem.bind("plothover", function (event, pos, item) {
self.show(pos, item);
// broadcast to other graph panels that we are hovering!
pos.panelRelY = (pos.pageY - elem.offset().top) / elem.height();
appEvents.emit('graph-hover', {pos: pos, panel: panel});
});
elem.bind("plotclick", function (event, pos, item) {
appEvents.emit('graph-click', {pos: pos, panel: panel, item: item});
});
this.clear = function(plot) {
$tooltip.detach();
plot.clearCrosshair();
plot.unhighlight();
};
this.show = function(pos, item) {
var plot = elem.data().plot;
var plotData = plot.getData();
var xAxes = plot.getXAxes();
var xMode = xAxes[0].options.mode;
var seriesList = getSeriesFn();
var allSeriesMode = panel.tooltip.shared;
var group, value, absoluteTime, hoverInfo, i, series, seriesHtml, tooltipFormat;
// if panelRelY is defined another panel wants us to show a tooltip
// get pageX from position on x axis and pageY from relative position in original panel
if (pos.panelRelY) {
var pointOffset = plot.pointOffset({x: pos.x});
if (Number.isNaN(pointOffset.left) || pointOffset.left < 0 || pointOffset.left > elem.width()) {
self.clear(plot);
return;
}
pos.pageX = elem.offset().left + pointOffset.left;
pos.pageY = elem.offset().top + elem.height() * pos.panelRelY;
var isVisible = pos.pageY >= $(window).scrollTop() && pos.pageY <= $(window).innerHeight() + $(window).scrollTop();
if (!isVisible) {
self.clear(plot);
return;
}
plot.setCrosshair(pos);
allSeriesMode = true;
if (dashboard.sharedCrosshairModeOnly()) {
// if only crosshair mode we are done
return;
}
}
if (seriesList.length === 0) {
return;
}
if (seriesList[0].hasMsResolution) {
tooltipFormat = 'YYYY-MM-DD HH:mm:ss.SSS';
} else {
tooltipFormat = 'YYYY-MM-DD HH:mm:ss';
}
if (allSeriesMode) {
plot.unhighlight();
var seriesHoverInfo = self.getMultiSeriesPlotHoverInfo(plotData, pos);
seriesHtml = '';
absoluteTime = dashboard.formatDate(seriesHoverInfo.time, tooltipFormat);
// Dynamically reorder the hovercard for the current time point if the
// option is enabled.
if (panel.tooltip.sort === 2) {
seriesHoverInfo.sort(function(a, b) {
return b.value - a.value;
});
} else if (panel.tooltip.sort === 1) {
seriesHoverInfo.sort(function(a, b) {
return a.value - b.value;
});
}
for (i = 0; i < seriesHoverInfo.length; i++) {
hoverInfo = seriesHoverInfo[i];
if (hoverInfo.hidden) {
continue;
}
var highlightClass = '';
if (item && hoverInfo.index === item.seriesIndex) {
highlightClass = 'graph-tooltip-list-item--highlight';
}
series = seriesList[hoverInfo.index];
value = series.formatValue(hoverInfo.value);
seriesHtml += '<div class="graph-tooltip-list-item ' + highlightClass + '"><div class="graph-tooltip-series-name">';
seriesHtml += '<i class="fa fa-minus" style="color:' + hoverInfo.color +';"></i> ' + hoverInfo.label + ':</div>';
seriesHtml += '<div class="graph-tooltip-value">' + value + '</div></div>';
plot.highlight(hoverInfo.index, hoverInfo.hoverIndex);
}
self.renderAndShow(absoluteTime, seriesHtml, pos, xMode);
}
// single series tooltip
else if (item) {
series = seriesList[item.seriesIndex];
group = '<div class="graph-tooltip-list-item"><div class="graph-tooltip-series-name">';
group += '<i class="fa fa-minus" style="color:' + item.series.color +';"></i> ' + series.aliasEscaped + ':</div>';
if (panel.stack && panel.tooltip.value_type === 'individual') {
value = item.datapoint[1] - item.datapoint[2];
}
else {
value = item.datapoint[1];
}
value = series.formatValue(value);
absoluteTime = dashboard.formatDate(item.datapoint[0], tooltipFormat);
group += '<div class="graph-tooltip-value">' + value + '</div>';
self.renderAndShow(absoluteTime, group, pos, xMode);
}
// no hit
else {
$tooltip.detach();
}
};
}
return GraphTooltip;
});

View File

@@ -0,0 +1,289 @@
import $ from 'jquery';
import { appEvents } from 'app/core/core';
export default function GraphTooltip(elem, dashboard, scope, getSeriesFn) {
let self = this;
let ctrl = scope.ctrl;
let panel = ctrl.panel;
let $tooltip = $('<div class="graph-tooltip">');
this.destroy = function() {
$tooltip.remove();
};
this.findHoverIndexFromDataPoints = function(posX, series, last) {
let ps = series.datapoints.pointsize;
let initial = last * ps;
let len = series.datapoints.points.length;
let j;
for (j = initial; j < len; j += ps) {
// Special case of a non stepped line, highlight the very last point just before a null point
if (
(!series.lines.steps && series.datapoints.points[initial] != null && series.datapoints.points[j] == null) ||
//normal case
series.datapoints.points[j] > posX
) {
return Math.max(j - ps, 0) / ps;
}
}
return j / ps - 1;
};
this.findHoverIndexFromData = function(posX, series) {
let lower = 0;
let upper = series.data.length - 1;
let middle;
while (true) {
if (lower > upper) {
return Math.max(upper, 0);
}
middle = Math.floor((lower + upper) / 2);
if (series.data[middle][0] === posX) {
return middle;
} else if (series.data[middle][0] < posX) {
lower = middle + 1;
} else {
upper = middle - 1;
}
}
};
this.renderAndShow = function(absoluteTime, innerHtml, pos, xMode) {
if (xMode === 'time') {
innerHtml = '<div class="graph-tooltip-time">' + absoluteTime + '</div>' + innerHtml;
}
$tooltip.html(innerHtml).place_tt(pos.pageX + 20, pos.pageY);
};
this.getMultiSeriesPlotHoverInfo = function(seriesList, pos) {
let value, i, series, hoverIndex, hoverDistance, pointTime, yaxis;
// 3 sub-arrays, 1st for hidden series, 2nd for left yaxis, 3rd for right yaxis.
let results: any = [[], [], []];
//now we know the current X (j) position for X and Y values
let last_value = 0; //needed for stacked values
let minDistance, minTime;
for (i = 0; i < seriesList.length; i++) {
series = seriesList[i];
if (!series.data.length || (panel.legend.hideEmpty && series.allIsNull)) {
// Init value so that it does not brake series sorting
results[0].push({ hidden: true, value: 0 });
continue;
}
if (!series.data.length || (panel.legend.hideZero && series.allIsZero)) {
// Init value so that it does not brake series sorting
results[0].push({ hidden: true, value: 0 });
continue;
}
hoverIndex = this.findHoverIndexFromData(pos.x, series);
hoverDistance = pos.x - series.data[hoverIndex][0];
pointTime = series.data[hoverIndex][0];
// Take the closest point before the cursor, or if it does not exist, the closest after
if (
!minDistance ||
(hoverDistance >= 0 && (hoverDistance < minDistance || minDistance < 0)) ||
(hoverDistance < 0 && hoverDistance > minDistance)
) {
minDistance = hoverDistance;
minTime = pointTime;
}
if (series.stack) {
if (panel.tooltip.value_type === 'individual') {
value = series.data[hoverIndex][1];
} else if (!series.stack) {
value = series.data[hoverIndex][1];
} else {
last_value += series.data[hoverIndex][1];
value = last_value;
}
} else {
value = series.data[hoverIndex][1];
}
// Highlighting multiple Points depending on the plot type
if (series.lines.steps || series.stack) {
// stacked and steppedLine plots can have series with different length.
// Stacked series can increase its length on each new stacked serie if null points found,
// to speed the index search we begin always on the last found hoverIndex.
hoverIndex = this.findHoverIndexFromDataPoints(pos.x, series, hoverIndex);
}
// Be sure we have a yaxis so that it does not brake series sorting
yaxis = 0;
if (series.yaxis) {
yaxis = series.yaxis.n;
}
results[yaxis].push({
value: value,
hoverIndex: hoverIndex,
color: series.color,
label: series.aliasEscaped,
time: pointTime,
distance: hoverDistance,
index: i,
});
}
// Contat the 3 sub-arrays
results = results[0].concat(results[1], results[2]);
// Time of the point closer to pointer
results.time = minTime;
return results;
};
elem.mouseleave(function() {
if (panel.tooltip.shared) {
let plot = elem.data().plot;
if (plot) {
$tooltip.detach();
plot.unhighlight();
}
}
appEvents.emit('graph-hover-clear');
});
elem.bind('plothover', function(event, pos, item) {
self.show(pos, item);
// broadcast to other graph panels that we are hovering!
pos.panelRelY = (pos.pageY - elem.offset().top) / elem.height();
appEvents.emit('graph-hover', { pos: pos, panel: panel });
});
elem.bind('plotclick', function(event, pos, item) {
appEvents.emit('graph-click', { pos: pos, panel: panel, item: item });
});
this.clear = function(plot) {
$tooltip.detach();
plot.clearCrosshair();
plot.unhighlight();
};
this.show = function(pos, item) {
let plot = elem.data().plot;
let plotData = plot.getData();
let xAxes = plot.getXAxes();
let xMode = xAxes[0].options.mode;
let seriesList = getSeriesFn();
let allSeriesMode = panel.tooltip.shared;
let group, value, absoluteTime, hoverInfo, i, series, seriesHtml, tooltipFormat;
// if panelRelY is defined another panel wants us to show a tooltip
// get pageX from position on x axis and pageY from relative position in original panel
if (pos.panelRelY) {
let pointOffset = plot.pointOffset({ x: pos.x });
if (Number.isNaN(pointOffset.left) || pointOffset.left < 0 || pointOffset.left > elem.width()) {
self.clear(plot);
return;
}
pos.pageX = elem.offset().left + pointOffset.left;
pos.pageY = elem.offset().top + elem.height() * pos.panelRelY;
let isVisible =
pos.pageY >= $(window).scrollTop() && pos.pageY <= $(window).innerHeight() + $(window).scrollTop();
if (!isVisible) {
self.clear(plot);
return;
}
plot.setCrosshair(pos);
allSeriesMode = true;
if (dashboard.sharedCrosshairModeOnly()) {
// if only crosshair mode we are done
return;
}
}
if (seriesList.length === 0) {
return;
}
if (seriesList[0].hasMsResolution) {
tooltipFormat = 'YYYY-MM-DD HH:mm:ss.SSS';
} else {
tooltipFormat = 'YYYY-MM-DD HH:mm:ss';
}
if (allSeriesMode) {
plot.unhighlight();
let seriesHoverInfo = self.getMultiSeriesPlotHoverInfo(plotData, pos);
seriesHtml = '';
absoluteTime = dashboard.formatDate(seriesHoverInfo.time, tooltipFormat);
// Dynamically reorder the hovercard for the current time point if the
// option is enabled.
if (panel.tooltip.sort === 2) {
seriesHoverInfo.sort(function(a, b) {
return b.value - a.value;
});
} else if (panel.tooltip.sort === 1) {
seriesHoverInfo.sort(function(a, b) {
return a.value - b.value;
});
}
for (i = 0; i < seriesHoverInfo.length; i++) {
hoverInfo = seriesHoverInfo[i];
if (hoverInfo.hidden) {
continue;
}
let highlightClass = '';
if (item && hoverInfo.index === item.seriesIndex) {
highlightClass = 'graph-tooltip-list-item--highlight';
}
series = seriesList[hoverInfo.index];
value = series.formatValue(hoverInfo.value);
seriesHtml +=
'<div class="graph-tooltip-list-item ' + highlightClass + '"><div class="graph-tooltip-series-name">';
seriesHtml +=
'<i class="fa fa-minus" style="color:' + hoverInfo.color + ';"></i> ' + hoverInfo.label + ':</div>';
seriesHtml += '<div class="graph-tooltip-value">' + value + '</div></div>';
plot.highlight(hoverInfo.index, hoverInfo.hoverIndex);
}
self.renderAndShow(absoluteTime, seriesHtml, pos, xMode);
} else if (item) {
// single series tooltip
series = seriesList[item.seriesIndex];
group = '<div class="graph-tooltip-list-item"><div class="graph-tooltip-series-name">';
group +=
'<i class="fa fa-minus" style="color:' + item.series.color + ';"></i> ' + series.aliasEscaped + ':</div>';
if (panel.stack && panel.tooltip.value_type === 'individual') {
value = item.datapoint[1] - item.datapoint[2];
} else {
value = item.datapoint[1];
}
value = series.formatValue(value);
absoluteTime = dashboard.formatDate(item.datapoint[0], tooltipFormat);
group += '<div class="graph-tooltip-value">' + value + '</div>';
self.renderAndShow(absoluteTime, group, pos, xMode);
} else {
// no hit
$tooltip.detach();
}
};
}

View File

@@ -131,8 +131,11 @@ module.directive('graphLegend', function(popoverSrv, $timeout) {
elem.empty();
// Set min-width if side style and there is a value, otherwise remove the CSS propery
var width = panel.legend.rightSide && panel.legend.sideWidth ? panel.legend.sideWidth + 'px' : '';
// Set width so it works with IE11
var width: any = panel.legend.rightSide && panel.legend.sideWidth ? panel.legend.sideWidth + 'px' : '';
var ieWidth: any = panel.legend.rightSide && panel.legend.sideWidth ? panel.legend.sideWidth - 1 + 'px' : '';
elem.css('min-width', width);
elem.css('width', ieWidth);
elem.toggleClass('graph-legend-table', panel.legend.alignAsTable === true);

View File

@@ -11,6 +11,7 @@ var scope = {
var elem = $('<div></div>');
var dashboard = {};
var getSeriesFn;
function describeSharedTooltip(desc, fn) {
var ctx: any = {};
@@ -30,7 +31,7 @@ function describeSharedTooltip(desc, fn) {
describe(desc, function() {
beforeEach(function() {
ctx.setupFn();
var tooltip = new GraphTooltip(elem, dashboard, scope);
var tooltip = new GraphTooltip(elem, dashboard, scope, getSeriesFn);
ctx.results = tooltip.getMultiSeriesPlotHoverInfo(ctx.data, ctx.pos);
});
@@ -39,7 +40,7 @@ function describeSharedTooltip(desc, fn) {
}
describe('findHoverIndexFromData', function() {
var tooltip = new GraphTooltip(elem, dashboard, scope);
var tooltip = new GraphTooltip(elem, dashboard, scope, getSeriesFn);
var series = {
data: [[100, 0], [101, 0], [102, 0], [103, 0], [104, 0], [105, 0], [106, 0], [107, 0]],
};

View File

@@ -69,7 +69,59 @@
</div>
</div>
<div class="section gf-form-group" ng-if="style.type === 'number'">
<div class="section gf-form-group" ng-if="style.type === 'string'">
<h5 class="section-heading">Value Mappings</h5>
<div class="editor-row">
<div class="gf-form-group">
<div class="gf-form">
<span class="gf-form-label">
Type
</span>
<div class="gf-form-select-wrapper">
<select class="gf-form-input" ng-model="style.mappingType"
ng-options="c.value as c.text for c in editor.mappingTypes" ng-change="editor.render()"></select>
</div>
</div>
<div class="gf-form-group" ng-if="style.mappingType==1">
<div class="gf-form" ng-repeat="map in style.valueMaps">
<span class="gf-form-label">
<i class="fa fa-remove pointer" ng-click="editor.removeValueMap(style, $index)"></i>
</span>
<input type="text" class="gf-form-input max-width-6" ng-model="map.value" placeholder="Value" ng-blur="editor.render()">
<label class="gf-form-label">
<i class="fa fa-arrow-right"></i>
</label>
<input type="text" class="gf-form-input max-width-8" ng-model="map.text" placeholder="Text" ng-blur="editor.render()">
</div>
<div class="gf-form">
<label class="gf-form-label">
<a class="pointer" ng-click="editor.addValueMap(style)"><i class="fa fa-plus"></i></a>
</label>
</div>
</div>
<div class="gf-form-group" ng-if="style.mappingType==2">
<div class="gf-form" ng-repeat="rangeMap in style.rangeMaps">
<span class="gf-form-label">
<i class="fa fa-remove pointer" ng-click="editor.removeRangeMap(style, $index)"></i>
</span>
<span class="gf-form-label">From</span>
<input type="text" ng-model="rangeMap.from" class="gf-form-input max-width-6" ng-blur="editor.render()">
<span class="gf-form-label">To</span>
<input type="text" ng-model="rangeMap.to" class="gf-form-input max-width-6" ng-blur="editor.render()">
<span class="gf-form-label">Text</span>
<input type="text" ng-model="rangeMap.text" class="gf-form-input max-width-8" ng-blur="editor.render()">
</div>
<div class="gf-form">
<label class="gf-form-label">
<a class="pointer" ng-click="editor.addRangeMap(style)"><i class="fa fa-plus"></i></a>
</label>
</div>
</div>
</div>
</div>
</div>
<div class="section gf-form-group" ng-if="['number', 'string'].indexOf(style.type) !== -1">
<h5 class="section-heading">Thresholds</h5>
<div class="gf-form">
<label class="gf-form-label width-8">Thresholds
@@ -111,10 +163,10 @@
<span>
Use special variables to specify cell values:
<br>
<em>$__cell</em> refers to current cell value
<em>${__cell}</em> refers to current cell value
<br>
<em>$__cell_n</em> refers to Nth column value in current row. Column indexes are started from 0. For instance,
<em>$__cell_1</em> refers to second column's value.
<em>${__cell_n}</em> refers to Nth column value in current row. Column indexes are started from 0. For instance,
<em>${__cell_1}</em> refers to second column's value.
</span>
</info-popover>
</div>

View File

@@ -13,6 +13,7 @@ export class ColumnOptionsCtrl {
unitFormats: any;
getColumnNames: any;
activeStyleIndex: number;
mappingTypes: any;
/** @ngInject */
constructor($scope) {
@@ -41,6 +42,7 @@ export class ColumnOptionsCtrl {
{ text: 'MM/DD/YY h:mm:ss a', value: 'MM/DD/YY h:mm:ss a' },
{ text: 'MMMM D, YYYY LT', value: 'MMMM D, YYYY LT' },
];
this.mappingTypes = [{ text: 'Value to text', value: 1 }, { text: 'Range to text', value: 2 }];
this.getColumnNames = () => {
if (!this.panelCtrl.table) {
@@ -74,6 +76,7 @@ export class ColumnOptionsCtrl {
pattern: '',
dateFormat: 'YYYY-MM-DD HH:mm:ss',
thresholds: [],
mappingType: 1,
};
var styles = this.panel.styles;
@@ -110,6 +113,32 @@ export class ColumnOptionsCtrl {
this.render();
};
}
addValueMap(style) {
if (!style.valueMaps) {
style.valueMaps = [];
}
style.valueMaps.push({ value: '', text: '' });
this.panelCtrl.render();
}
removeValueMap(style, index) {
style.valueMaps.splice(index, 1);
this.panelCtrl.render();
}
addRangeMap(style) {
if (!style.rangeMaps) {
style.rangeMaps = [];
}
style.rangeMaps.push({ from: '', to: '', text: '' });
this.panelCtrl.render();
}
removeRangeMap(style, index) {
style.rangeMaps.splice(index, 1);
this.panelCtrl.render();
}
}
/** @ngInject */

View File

@@ -47,7 +47,6 @@ export class TableRenderer {
if (!style.thresholds) {
return null;
}
for (var i = style.thresholds.length; i > 0; i--) {
if (value >= style.thresholds[i - 1]) {
return style.colors[i];
@@ -100,6 +99,60 @@ export class TableRenderer {
};
}
if (column.style.type === 'string') {
return v => {
if (_.isArray(v)) {
v = v.join(', ');
}
const mappingType = column.style.mappingType || 0;
if (mappingType === 1 && column.style.valueMaps) {
for (let i = 0; i < column.style.valueMaps.length; i++) {
const map = column.style.valueMaps[i];
if (v === null) {
if (map.value === 'null') {
return map.text;
}
continue;
}
// Allow both numeric and string values to be mapped
if ((!_.isString(v) && Number(map.value) === Number(v)) || map.value === v) {
this.setColorState(v, column.style);
return this.defaultCellFormatter(map.text, column.style);
}
}
}
if (mappingType === 2 && column.style.rangeMaps) {
for (let i = 0; i < column.style.rangeMaps.length; i++) {
const map = column.style.rangeMaps[i];
if (v === null) {
if (map.from === 'null' && map.to === 'null') {
return map.text;
}
continue;
}
if (Number(map.from) <= Number(v) && Number(map.to) >= Number(v)) {
this.setColorState(v, column.style);
return this.defaultCellFormatter(map.text, column.style);
}
}
}
if (v === null || v === void 0) {
return '-';
}
this.setColorState(v, column.style);
return this.defaultCellFormatter(v, column.style);
};
}
if (column.style.type === 'number') {
let valueFormatter = kbn.valueFormats[column.unit || column.style.unit];
@@ -112,10 +165,7 @@ export class TableRenderer {
return this.defaultCellFormatter(v, column.style);
}
if (column.style.colorMode) {
this.colorState[column.style.colorMode] = this.getColorForValue(v, column.style);
}
this.setColorState(v, column.style);
return valueFormatter(v, column.style.decimals, null);
};
}
@@ -125,6 +175,23 @@ export class TableRenderer {
};
}
setColorState(value, style) {
if (!style.colorMode) {
return;
}
if (value === null || value === void 0 || _.isArray(value)) {
return;
}
var numericValue = Number(value);
if (numericValue === NaN) {
return;
}
this.colorState[style.colorMode] = this.getColorForValue(numericValue, style);
}
renderRowVariables(rowIndex) {
let scopedVars = {};
let cell_variable;

View File

@@ -3,7 +3,7 @@ import TableModel from 'app/core/table_model';
import { TableRenderer } from '../renderer';
describe('when rendering table', () => {
describe('given 2 columns', () => {
describe('given 13 columns', () => {
var table = new TableModel();
table.columns = [
{ text: 'Time' },
@@ -15,8 +15,14 @@ describe('when rendering table', () => {
{ text: 'Sanitized' },
{ text: 'Link' },
{ text: 'Array' },
{ text: 'Mapping' },
{ text: 'RangeMapping' },
{ text: 'MappingColored' },
{ text: 'RangeMappingColored' },
];
table.rows = [
[1388556366666, 1230, 40, undefined, '', '', 'my.host.com', 'host1', ['value1', 'value2'], 1, 2, 1, 2],
];
table.rows = [[1388556366666, 1230, 40, undefined, '', '', 'my.host.com', 'host1', ['value1', 'value2']]];
var panel = {
pageSize: 10,
@@ -47,6 +53,10 @@ describe('when rendering table', () => {
pattern: 'String',
type: 'string',
},
{
pattern: 'String',
type: 'string',
},
{
pattern: 'United',
type: 'number',
@@ -72,6 +82,84 @@ describe('when rendering table', () => {
unit: 'ms',
decimals: 3,
},
{
pattern: 'Mapping',
type: 'string',
mappingType: 1,
valueMaps: [
{
value: '1',
text: 'on',
},
{
value: '0',
text: 'off',
},
{
value: 'HELLO WORLD',
text: 'HELLO GRAFANA',
},
{
value: 'value1, value2',
text: 'value3, value4',
},
],
},
{
pattern: 'RangeMapping',
type: 'string',
mappingType: 2,
rangeMaps: [
{
from: '1',
to: '3',
text: 'on',
},
{
from: '3',
to: '6',
text: 'off',
},
],
},
{
pattern: 'MappingColored',
type: 'string',
mappingType: 1,
valueMaps: [
{
value: '1',
text: 'on',
},
{
value: '0',
text: 'off',
},
],
colorMode: 'value',
thresholds: [1, 2],
colors: ['green', 'orange', 'red'],
},
{
pattern: 'RangeMappingColored',
type: 'string',
mappingType: 2,
rangeMaps: [
{
from: '1',
to: '3',
text: 'on',
},
{
from: '3',
to: '6',
text: 'off',
},
],
colorMode: 'value',
thresholds: [2, 5],
colors: ['green', 'orange', 'red'],
},
],
};
@@ -192,6 +280,86 @@ describe('when rendering table', () => {
var html = renderer.renderCell(8, 0, ['value1', 'value2']);
expect(html).toBe('<td>value1, value2</td>');
});
it('numeric value should be mapped to text', () => {
var html = renderer.renderCell(9, 0, 1);
expect(html).toBe('<td>on</td>');
});
it('string numeric value should be mapped to text', () => {
var html = renderer.renderCell(9, 0, '0');
expect(html).toBe('<td>off</td>');
});
it('string value should be mapped to text', () => {
var html = renderer.renderCell(9, 0, 'HELLO WORLD');
expect(html).toBe('<td>HELLO GRAFANA</td>');
});
it('array column value should be mapped to text', () => {
var html = renderer.renderCell(9, 0, ['value1', 'value2']);
expect(html).toBe('<td>value3, value4</td>');
});
it('value should be mapped to text (range)', () => {
var html = renderer.renderCell(10, 0, 2);
expect(html).toBe('<td>on</td>');
});
it('value should be mapped to text (range)', () => {
var html = renderer.renderCell(10, 0, 5);
expect(html).toBe('<td>off</td>');
});
it('array column value should not be mapped to text', () => {
var html = renderer.renderCell(10, 0, ['value1', 'value2']);
expect(html).toBe('<td>value1, value2</td>');
});
it('value should be mapped to text and colored cell should have style', () => {
var html = renderer.renderCell(11, 0, 1);
expect(html).toBe('<td style="color:orange">on</td>');
});
it('value should be mapped to text and colored cell should have style', () => {
var html = renderer.renderCell(11, 0, '1');
expect(html).toBe('<td style="color:orange">on</td>');
});
it('value should be mapped to text and colored cell should have style', () => {
var html = renderer.renderCell(11, 0, 0);
expect(html).toBe('<td style="color:green">off</td>');
});
it('value should be mapped to text and colored cell should have style', () => {
var html = renderer.renderCell(11, 0, '0');
expect(html).toBe('<td style="color:green">off</td>');
});
it('value should be mapped to text and colored cell should have style', () => {
var html = renderer.renderCell(11, 0, '2.1');
expect(html).toBe('<td style="color:red">2.1</td>');
});
it('value should be mapped to text (range) and colored cell should have style', () => {
var html = renderer.renderCell(12, 0, 0);
expect(html).toBe('<td style="color:green">0</td>');
});
it('value should be mapped to text (range) and colored cell should have style', () => {
var html = renderer.renderCell(12, 0, 1);
expect(html).toBe('<td style="color:green">on</td>');
});
it('value should be mapped to text (range) and colored cell should have style', () => {
var html = renderer.renderCell(12, 0, 4);
expect(html).toBe('<td style="color:orange">off</td>');
});
it('value should be mapped to text (range) and colored cell should have style', () => {
var html = renderer.renderCell(12, 0, '7.1');
expect(html).toBe('<td style="color:red">7.1</td>');
});
});
});

View File

@@ -59,9 +59,8 @@ $critical: #ec2128;
$body-bg: $gray-7;
$page-bg: $gray-7;
$body-color: $gray-1;
//$text-color: $dark-4;
$text-color: $gray-1;
$text-color-strong: $white;
$text-color-strong: $dark-2;
$text-color-weak: $gray-2;
$text-color-faint: $gray-4;
$text-color-emphasis: $dark-5;

View File

@@ -1,8 +1,10 @@
.gicon {
line-height: 1;
display: inline-block;
width: 1.1057142857em;
height: 1.1057142857em;
//width: 1.1057142857em;
//height: 1.1057142857em;
height: 22px;
width: 22px;
text-align: center;
background-repeat: no-repeat;
background-position: center;

View File

@@ -31,7 +31,6 @@
//padding: 0.5rem 1.5rem 0.5rem 0;
padding: 1rem 1rem 0.75rem 1rem;
height: 51px;
line-height: 51px;
box-sizing: border-box;
outline: none;
background: $side-menu-bg;
@@ -62,7 +61,9 @@
flex-direction: column;
flex-grow: 1;
// overflow-y: scroll;
.search-item--indent {
margin-left: 14px;
}
}
.search-dropdown__col_2 {

View File

@@ -178,6 +178,7 @@ li.sidemenu-org-switcher {
padding: 0.4rem 1rem 0.4rem 0.65rem;
min-height: $navbarHeight;
position: relative;
height: $navbarHeight - 1px;
&:hover {
background: $navbarButtonBackgroundHighlight;

View File

@@ -43,7 +43,7 @@
font-size: 120%;
}
&:hover {
color: $white;
color: $text-color-strong;
}
}

View File

@@ -108,7 +108,8 @@
justify-content: center;
align-items: center;
width: 40px;
padding: 0 28px 0 16px;
//margin-right: 8px;
padding: 0 4px 0 2px;
.icon-gf,
.fa {
font-size: 200%;

View File

@@ -33,7 +33,7 @@ div.flot-text {
border: $panel-border;
position: relative;
border-radius: 3px;
height: 100%;
//height: 100%;
&.panel-transparent {
background-color: transparent;

View File

@@ -3,6 +3,7 @@ $login-border: #8daac5;
.login {
background-position: center;
min-height: 85vh;
height: 80vh;
background-repeat: no-repeat;
min-width: 100%;
margin-left: 0;
@@ -290,9 +291,14 @@ select:-webkit-autofill:focus {
}
@include media-breakpoint-up(md) {
.login-content {
flex: 1 0 100%;
}
.login-branding {
width: 45%;
padding: 2rem 4rem;
flex-grow: 1;
.logo-icon {
width: 130px;
@@ -371,7 +377,7 @@ select:-webkit-autofill:focus {
left: 0;
right: 0;
height: 100%;
content: "";
content: '';
display: block;
}