The open and composable observability and data visualization platform. Visualize metrics, logs, and traces from multiple sources like Prometheus, Loki, Elasticsearch, InfluxDB, Postgres and many more.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
grafana/public/app/plugins/panel/singlestat/module.ts

725 lines
20 KiB

import _ from 'lodash';
import $ from 'jquery';
import 'vendor/flot/jquery.flot';
import 'vendor/flot/jquery.flot.gauge';
import 'app/features/panellinks/link_srv';
import kbn from 'app/core/utils/kbn';
import config from 'app/core/config';
import TimeSeries from 'app/core/time_series2';
import { MetricsPanelCtrl } from 'app/plugins/sdk';
class SingleStatCtrl extends MetricsPanelCtrl {
static templateUrl = 'module.html';
dataType = 'timeseries';
series: any[];
data: any;
fontSizes: any[];
unitFormats: any[];
invalidGaugeRange: boolean;
panel: any;
events: any;
valueNameOptions: any[] = [
{ value: 'min', text: 'Min' },
{ value: 'max', text: 'Max' },
{ value: 'avg', text: 'Average' },
{ value: 'current', text: 'Current' },
{ value: 'total', text: 'Total' },
{ value: 'name', text: 'Name' },
{ value: 'first', text: 'First' },
{ value: 'delta', text: 'Delta' },
{ value: 'diff', text: 'Difference' },
{ value: 'range', text: 'Range' },
{ value: 'last_time', text: 'Time of last point' },
];
tableColumnOptions: any;
// Set and populate defaults
panelDefaults = {
links: [],
datasource: null,
maxDataPoints: 100,
interval: null,
targets: [{}],
cacheTimeout: null,
format: 'none',
prefix: '',
postfix: '',
nullText: null,
valueMaps: [{ value: 'null', op: '=', text: 'N/A' }],
mappingTypes: [{ name: 'value to text', value: 1 }, { name: 'range to text', value: 2 }],
rangeMaps: [{ from: 'null', to: 'null', text: 'N/A' }],
mappingType: 1,
nullPointMode: 'connected',
valueName: 'avg',
prefixFontSize: '50%',
valueFontSize: '80%',
postfixFontSize: '50%',
thresholds: '',
colorBackground: false,
colorValue: false,
colors: ['#299c46', 'rgba(237, 129, 40, 0.89)', '#d44a3a'],
sparkline: {
show: false,
full: false,
lineColor: 'rgb(31, 120, 193)',
fillColor: 'rgba(31, 118, 189, 0.18)',
},
gauge: {
show: false,
minValue: 0,
maxValue: 100,
thresholdMarkers: true,
thresholdLabels: false,
},
tableColumn: '',
};
/** @ngInject */
constructor($scope, $injector, private $location, private linkSrv) {
super($scope, $injector);
_.defaults(this.panel, this.panelDefaults);
this.events.on('data-received', this.onDataReceived.bind(this));
this.events.on('data-error', this.onDataError.bind(this));
this.events.on('data-snapshot-load', this.onDataReceived.bind(this));
this.events.on('init-edit-mode', this.onInitEditMode.bind(this));
this.onSparklineColorChange = this.onSparklineColorChange.bind(this);
this.onSparklineFillChange = this.onSparklineFillChange.bind(this);
}
onInitEditMode() {
this.fontSizes = ['20%', '30%', '50%', '70%', '80%', '100%', '110%', '120%', '150%', '170%', '200%'];
this.addEditorTab('Options', 'public/app/plugins/panel/singlestat/editor.html', 2);
this.addEditorTab('Value Mappings', 'public/app/plugins/panel/singlestat/mappings.html', 3);
this.unitFormats = kbn.getUnitFormats();
}
setUnitFormat(subItem) {
this.panel.format = subItem.value;
this.refresh();
}
onDataError(err) {
this.onDataReceived([]);
}
onDataReceived(dataList) {
const data: any = {};
if (dataList.length > 0 && dataList[0].type === 'table') {
this.dataType = 'table';
const tableData = dataList.map(this.tableHandler.bind(this));
this.setTableValues(tableData, data);
} else {
this.dataType = 'timeseries';
this.series = dataList.map(this.seriesHandler.bind(this));
this.setValues(data);
}
this.data = data;
this.render();
}
seriesHandler(seriesData) {
var series = new TimeSeries({
8 years ago
datapoints: seriesData.datapoints || [],
alias: seriesData.target,
});
series.flotpairs = series.getFlotPairs(this.panel.nullPointMode);
return series;
}
tableHandler(tableData) {
const datapoints = [];
const columnNames = {};
tableData.columns.forEach((column, columnIndex) => {
columnNames[columnIndex] = column.text;
});
this.tableColumnOptions = columnNames;
if (!_.find(tableData.columns, ['text', this.panel.tableColumn])) {
this.setTableColumnToSensibleDefault(tableData);
}
tableData.rows.forEach(row => {
const datapoint = {};
row.forEach((value, columnIndex) => {
const key = columnNames[columnIndex];
datapoint[key] = value;
});
datapoints.push(datapoint);
});
return datapoints;
}
setTableColumnToSensibleDefault(tableData) {
if (this.tableColumnOptions.length === 1) {
this.panel.tableColumn = this.tableColumnOptions[0];
} else {
this.panel.tableColumn = _.find(tableData.columns, col => {
return col.type !== 'time';
}).text;
}
}
setTableValues(tableData, data) {
if (!tableData || tableData.length === 0) {
return;
}
if (tableData[0].length === 0 || tableData[0][0][this.panel.tableColumn] === undefined) {
return;
}
const datapoint = tableData[0][0];
data.value = datapoint[this.panel.tableColumn];
if (_.isString(data.value)) {
data.valueFormatted = _.escape(data.value);
data.value = 0;
data.valueRounded = 0;
} else {
const decimalInfo = this.getDecimalsForValue(data.value);
const formatFunc = kbn.valueFormats[this.panel.format];
data.valueFormatted = formatFunc(
datapoint[this.panel.tableColumn],
decimalInfo.decimals,
decimalInfo.scaledDecimals
);
data.valueRounded = kbn.roundValue(data.value, this.panel.decimals || 0);
}
this.setValueMapping(data);
}
canChangeFontSize() {
return this.panel.gauge.show;
}
setColoring(options) {
if (options.background) {
this.panel.colorValue = false;
this.panel.colors = ['rgba(71, 212, 59, 0.4)', 'rgba(245, 150, 40, 0.73)', 'rgba(225, 40, 40, 0.59)'];
} else {
this.panel.colorBackground = false;
this.panel.colors = ['rgba(50, 172, 45, 0.97)', 'rgba(237, 129, 40, 0.89)', 'rgba(245, 54, 54, 0.9)'];
}
this.render();
}
invertColorOrder() {
var tmp = this.panel.colors[0];
this.panel.colors[0] = this.panel.colors[2];
this.panel.colors[2] = tmp;
this.render();
}
onColorChange(panelColorIndex) {
return color => {
this.panel.colors[panelColorIndex] = color;
this.render();
};
}
onSparklineColorChange(newColor) {
this.panel.sparkline.lineColor = newColor;
this.render();
}
onSparklineFillChange(newColor) {
this.panel.sparkline.fillColor = newColor;
this.render();
}
getDecimalsForValue(value) {
if (_.isNumber(this.panel.decimals)) {
return { decimals: this.panel.decimals, scaledDecimals: null };
}
var delta = value / 2;
var dec = -Math.floor(Math.log(delta) / Math.LN10);
var magn = Math.pow(10, -dec),
norm = delta / magn, // norm is between 1.0 and 10.0
size;
if (norm < 1.5) {
size = 1;
} else if (norm < 3) {
size = 2;
// special case for 2.5, requires an extra decimal
if (norm > 2.25) {
size = 2.5;
++dec;
}
} else if (norm < 7.5) {
size = 5;
} else {
size = 10;
}
size *= magn;
// reduce starting decimals if not needed
if (Math.floor(value) === value) {
dec = 0;
}
var result: any = {};
result.decimals = Math.max(0, dec);
result.scaledDecimals = result.decimals - Math.floor(Math.log(size) / Math.LN10) + 2;
return result;
}
setValues(data) {
data.flotpairs = [];
if (this.series.length > 1) {
var error: any = new Error();
error.message = 'Multiple Series Error';
error.data =
'Metric query returns ' +
this.series.length +
' series. Single Stat Panel expects a single series.\n\nResponse:\n' +
JSON.stringify(this.series);
throw error;
}
if (this.series && this.series.length > 0) {
let lastPoint = _.last(this.series[0].datapoints);
let lastValue = _.isArray(lastPoint) ? lastPoint[0] : null;
if (this.panel.valueName === 'name') {
data.value = 0;
data.valueRounded = 0;
data.valueFormatted = this.series[0].alias;
} else if (_.isString(lastValue)) {
data.value = 0;
data.valueFormatted = _.escape(lastValue);
data.valueRounded = 0;
} else if (this.panel.valueName === 'last_time') {
let formatFunc = kbn.valueFormats[this.panel.format];
data.value = lastPoint[1];
data.valueRounded = data.value;
data.valueFormatted = formatFunc(data.value, 0, 0);
} else {
data.value = this.series[0].stats[this.panel.valueName];
data.flotpairs = this.series[0].flotpairs;
let decimalInfo = this.getDecimalsForValue(data.value);
let formatFunc = kbn.valueFormats[this.panel.format];
data.valueFormatted = formatFunc(data.value, decimalInfo.decimals, decimalInfo.scaledDecimals);
data.valueRounded = kbn.roundValue(data.value, decimalInfo.decimals);
}
// Add $__name variable for using in prefix or postfix
data.scopedVars = _.extend({}, this.panel.scopedVars);
data.scopedVars['__name'] = { value: this.series[0].label };
}
this.setValueMapping(data);
}
setValueMapping(data) {
// check value to text mappings if its enabled
if (this.panel.mappingType === 1) {
for (let i = 0; i < this.panel.valueMaps.length; i++) {
let map = this.panel.valueMaps[i];
// special null case
if (map.value === 'null') {
if (data.value === null || data.value === void 0) {
data.valueFormatted = map.text;
return;
}
continue;
}
// value/number to text mapping
var value = parseFloat(map.value);
if (value === data.valueRounded) {
data.valueFormatted = map.text;
return;
}
}
} else if (this.panel.mappingType === 2) {
for (let i = 0; i < this.panel.rangeMaps.length; i++) {
let map = this.panel.rangeMaps[i];
// special null case
if (map.from === 'null' && map.to === 'null') {
if (data.value === null || data.value === void 0) {
data.valueFormatted = map.text;
return;
}
continue;
}
// value/number to range mapping
var from = parseFloat(map.from);
var to = parseFloat(map.to);
if (to >= data.valueRounded && from <= data.valueRounded) {
data.valueFormatted = map.text;
return;
}
}
}
if (data.value === null || data.value === void 0) {
data.valueFormatted = 'no value';
}
}
removeValueMap(map) {
var index = _.indexOf(this.panel.valueMaps, map);
this.panel.valueMaps.splice(index, 1);
this.render();
}
addValueMap() {
this.panel.valueMaps.push({ value: '', op: '=', text: '' });
}
removeRangeMap(rangeMap) {
var index = _.indexOf(this.panel.rangeMaps, rangeMap);
this.panel.rangeMaps.splice(index, 1);
this.render();
}
addRangeMap() {
this.panel.rangeMaps.push({ from: '', to: '', text: '' });
}
link(scope, elem, attrs, ctrl) {
var $location = this.$location;
var linkSrv = this.linkSrv;
var $timeout = this.$timeout;
var panel = ctrl.panel;
var templateSrv = this.templateSrv;
var data, linkInfo;
var $panelContainer = elem.find('.panel-container');
elem = elem.find('.singlestat-panel');
function applyColoringThresholds(value, valueString) {
if (!panel.colorValue) {
return valueString;
}
var color = getColorForValue(data, value);
if (color) {
return '<span style="color:' + color + '">' + valueString + '</span>';
}
return valueString;
}
function getSpan(className, fontSize, value) {
value = templateSrv.replace(value, data.scopedVars);
return '<span class="' + className + '" style="font-size:' + fontSize + '">' + value + '</span>';
}
function getBigValueHtml() {
var body = '<div class="singlestat-panel-value-container">';
if (panel.prefix) {
var prefix = applyColoringThresholds(data.value, panel.prefix);
body += getSpan('singlestat-panel-prefix', panel.prefixFontSize, prefix);
}
var value = applyColoringThresholds(data.value, data.valueFormatted);
body += getSpan('singlestat-panel-value', panel.valueFontSize, value);
if (panel.postfix) {
var postfix = applyColoringThresholds(data.value, panel.postfix);
body += getSpan('singlestat-panel-postfix', panel.postfixFontSize, postfix);
}
body += '</div>';
return body;
}
function getValueText() {
var result = panel.prefix ? templateSrv.replace(panel.prefix, data.scopedVars) : '';
result += data.valueFormatted;
result += panel.postfix ? templateSrv.replace(panel.postfix, data.scopedVars) : '';
return result;
}
function addGauge() {
var width = elem.width();
var height = elem.height();
// Allow to use a bit more space for wide gauges
var dimension = Math.min(width, height * 1.3);
ctrl.invalidGaugeRange = false;
if (panel.gauge.minValue > panel.gauge.maxValue) {
ctrl.invalidGaugeRange = true;
return;
}
var plotCanvas = $('<div></div>');
var plotCss = {
top: '10px',
margin: 'auto',
position: 'relative',
height: height * 0.9 + 'px',
width: dimension + 'px',
};
plotCanvas.css(plotCss);
var thresholds = [];
for (var i = 0; i < data.thresholds.length; i++) {
thresholds.push({
value: data.thresholds[i],
color: data.colorMap[i],
});
}
thresholds.push({
value: panel.gauge.maxValue,
color: data.colorMap[data.colorMap.length - 1],
});
var bgColor = config.bootData.user.lightTheme ? 'rgb(230,230,230)' : 'rgb(38,38,38)';
var fontScale = parseInt(panel.valueFontSize) / 100;
var fontSize = Math.min(dimension / 5, 100) * fontScale;
// Reduce gauge width if threshold labels enabled
var gaugeWidthReduceRatio = panel.gauge.thresholdLabels ? 1.5 : 1;
var gaugeWidth = Math.min(dimension / 6, 60) / gaugeWidthReduceRatio;
var thresholdMarkersWidth = gaugeWidth / 5;
var thresholdLabelFontSize = fontSize / 2.5;
var options = {
series: {
gauges: {
gauge: {
min: panel.gauge.minValue,
max: panel.gauge.maxValue,
background: { color: bgColor },
border: { color: null },
shadow: { show: false },
width: gaugeWidth,
},
frame: { show: false },
label: { show: false },
layout: { margin: 0, thresholdWidth: 0 },
cell: { border: { width: 0 } },
threshold: {
values: thresholds,
label: {
show: panel.gauge.thresholdLabels,
margin: thresholdMarkersWidth + 1,
font: { size: thresholdLabelFontSize },
},
show: panel.gauge.thresholdMarkers,
width: thresholdMarkersWidth,
},
value: {
color: panel.colorValue ? getColorForValue(data, data.valueRounded) : null,
formatter: function() {
return getValueText();
},
font: {
size: fontSize,
family: '"Helvetica Neue", Helvetica, Arial, sans-serif',
},
},
show: true,
},
},
};
elem.append(plotCanvas);
var plotSeries = {
data: [[0, data.valueRounded]],
};
$.plot(plotCanvas, [plotSeries], options);
}
function addSparkline() {
var width = elem.width() + 20;
if (width < 30) {
// element has not gotten it's width yet
// delay sparkline render
setTimeout(addSparkline, 30);
return;
}
var height = ctrl.height;
var plotCanvas = $('<div></div>');
var plotCss: any = {};
plotCss.position = 'absolute';
if (panel.sparkline.full) {
plotCss.bottom = '5px';
plotCss.left = '-5px';
plotCss.width = width - 10 + 'px';
var dynamicHeightMargin = height <= 100 ? 5 : Math.round(height / 100) * 15 + 5;
plotCss.height = height - dynamicHeightMargin + 'px';
} else {
plotCss.bottom = '0px';
plotCss.left = '-5px';
plotCss.width = width - 10 + 'px';
plotCss.height = Math.floor(height * 0.25) + 'px';
}
plotCanvas.css(plotCss);
var options = {
legend: { show: false },
series: {
lines: {
show: true,
fill: 1,
lineWidth: 1,
fillColor: panel.sparkline.fillColor,
},
},
yaxes: { show: false },
xaxis: {
show: false,
mode: 'time',
min: ctrl.range.from.valueOf(),
max: ctrl.range.to.valueOf(),
},
grid: { hoverable: false, show: false },
};
elem.append(plotCanvas);
var plotSeries = {
data: data.flotpairs,
color: panel.sparkline.lineColor,
};
$.plot(plotCanvas, [plotSeries], options);
}
function render() {
if (!ctrl.data) {
return;
}
data = ctrl.data;
// get thresholds
data.thresholds = panel.thresholds.split(',').map(function(strVale) {
return Number(strVale.trim());
});
data.colorMap = panel.colors;
var body = panel.gauge.show ? '' : getBigValueHtml();
if (panel.colorBackground) {
var color = getColorForValue(data, data.value);
if (color) {
$panelContainer.css('background-color', color);
if (scope.fullscreen) {
elem.css('background-color', color);
} else {
elem.css('background-color', '');
}
}
} else {
$panelContainer.css('background-color', '');
elem.css('background-color', '');
}
elem.html(body);
if (panel.sparkline.show) {
addSparkline();
}
if (panel.gauge.show) {
addGauge();
}
elem.toggleClass('pointer', panel.links.length > 0);
if (panel.links.length > 0) {
linkInfo = linkSrv.getPanelLinkAnchorInfo(panel.links[0], data.scopedVars);
} else {
linkInfo = null;
}
}
function hookupDrilldownLinkTooltip() {
// drilldown link tooltip
var drilldownTooltip = $('<div id="tooltip" class="">hello</div>"');
elem.mouseleave(function() {
if (panel.links.length === 0) {
return;
}
$timeout(function() {
drilldownTooltip.detach();
});
});
elem.click(function(evt) {
if (!linkInfo) {
return;
}
// ignore title clicks in title
if ($(evt).parents('.panel-header').length > 0) {
return;
}
if (linkInfo.target === '_blank') {
window.open(linkInfo.href, '_blank');
return;
}
if (linkInfo.href.indexOf('http') === 0) {
window.location.href = linkInfo.href;
} else {
$timeout(function() {
$location.url(linkInfo.href);
});
}
drilldownTooltip.detach();
});
elem.mousemove(function(e) {
if (!linkInfo) {
return;
}
drilldownTooltip.text('click to go to: ' + linkInfo.title);
drilldownTooltip.place_tt(e.pageX, e.pageY - 50);
});
}
hookupDrilldownLinkTooltip();
this.events.on('render', function() {
render();
ctrl.renderingCompleted();
});
}
}
function getColorForValue(data, value) {
if (!_.isFinite(value)) {
return null;
}
for (var i = data.thresholds.length; i > 0; i--) {
if (value >= data.thresholds[i - 1]) {
return data.colorMap[i];
}
}
return _.first(data.colorMap);
}
export { SingleStatCtrl, SingleStatCtrl as PanelCtrl, getColorForValue };