From 3a33b675404cdb66032db360ba5cfc33445a188d Mon Sep 17 00:00:00 2001 From: rozetko Date: Tue, 17 Jul 2018 19:51:34 +0300 Subject: [PATCH] Unsupported grafana version 5.2.1 #37 (#45) * Add Grafana's embedded modules * Include them from vendor/ instead of grafana/ --- src/data_processor.ts | 4 +- src/graph_renderer.ts | 31 +-- src/vendor/grafana/colors.ts | 93 ++++++++ src/vendor/grafana/event.ts | 11 + src/vendor/grafana/event_manager.ts | 177 ++++++++++++++ src/vendor/grafana/ticks.ts | 158 +++++++++++++ src/vendor/grafana/time_series2.ts | 349 ++++++++++++++++++++++++++++ 7 files changed, 794 insertions(+), 29 deletions(-) create mode 100644 src/vendor/grafana/colors.ts create mode 100644 src/vendor/grafana/event.ts create mode 100644 src/vendor/grafana/event_manager.ts create mode 100644 src/vendor/grafana/ticks.ts create mode 100644 src/vendor/grafana/time_series2.ts diff --git a/src/data_processor.ts b/src/data_processor.ts index 9c26923..d3ecb96 100644 --- a/src/data_processor.ts +++ b/src/data_processor.ts @@ -1,6 +1,6 @@ import _ from 'lodash'; -import TimeSeries from 'grafana/app/core/time_series2'; -import colors from './colors'; +import TimeSeries from './vendor/grafana/time_series2'; +import colors from './vendor/grafana/colors'; export class DataProcessor { constructor(private panel) {} diff --git a/src/graph_renderer.ts b/src/graph_renderer.ts index b688fa3..2da70ca 100644 --- a/src/graph_renderer.ts +++ b/src/graph_renderer.ts @@ -24,11 +24,10 @@ import 'grafana/vendor/flot/jquery.flot.gauge.js'; import 'grafana/vendor/flot/jquery.flot.pie.js'; import './vendor/flot/jquery.flot.events.js'; -// import { EventManager } from 'grafana/app/features/annotations/event_manager'; -import TimeSeries from 'grafana/app/core/time_series2'; -import { getFlotTickDecimals } from 'grafana/app/core/utils/ticks'; -import { tickStep } from 'grafana/app/core/utils/ticks'; -import { appEvents, coreModule } from 'grafana/app/core/core'; +// import { EventManager } from './vendor/grafana/event_manager'; +import { updateLegendValues } from './vendor/grafana/time_series2'; +import { tickStep } from './vendor/grafana/ticks'; +import { appEvents } from 'grafana/app/core/core'; import kbn from 'grafana/app/core/utils/kbn'; import * as $ from 'jquery'; @@ -821,25 +820,3 @@ export class GraphRenderer { } } - -function updateLegendValues(data: TimeSeries[], panel) { - for (let i = 0; i < data.length; i++) { - let series = data[i]; - let yaxes = panel.yaxes; - const seriesYAxis = series.yaxis || 1; - let axis = yaxes[seriesYAxis - 1]; - let { tickDecimals, scaledDecimals } = getFlotTickDecimals(data, axis); - let formater = kbn.valueFormats[panel.yaxes[seriesYAxis - 1].format]; - - // decimal override - if (_.isNumber(panel.decimals)) { - series.updateLegendValues(formater, panel.decimals, null); - } else { - // auto decimals - // legend and tooltip gets one more decimal precision - // than graph legend ticks - tickDecimals = (tickDecimals || -1) + 1; - series.updateLegendValues(formater, tickDecimals, scaledDecimals + 2); - } - } -} diff --git a/src/vendor/grafana/colors.ts b/src/vendor/grafana/colors.ts new file mode 100644 index 0000000..93d7004 --- /dev/null +++ b/src/vendor/grafana/colors.ts @@ -0,0 +1,93 @@ +import _ from 'lodash'; +import tinycolor from 'tinycolor2'; + +export const PALETTE_ROWS = 4; +export const PALETTE_COLUMNS = 14; +export const DEFAULT_ANNOTATION_COLOR = 'rgba(0, 211, 255, 1)'; +export const OK_COLOR = 'rgba(11, 237, 50, 1)'; +export const ALERTING_COLOR = 'rgba(237, 46, 24, 1)'; +export const NO_DATA_COLOR = 'rgba(150, 150, 150, 1)'; +export const REGION_FILL_ALPHA = 0.09; + +let colors = [ + '#7EB26D', + '#EAB839', + '#6ED0E0', + '#EF843C', + '#E24D42', + '#1F78C1', + '#BA43A9', + '#705DA0', + '#508642', + '#CCA300', + '#447EBC', + '#C15C17', + '#890F02', + '#0A437C', + '#6D1F62', + '#584477', + '#B7DBAB', + '#F4D598', + '#70DBED', + '#F9BA8F', + '#F29191', + '#82B5D8', + '#E5A8E2', + '#AEA2E0', + '#629E51', + '#E5AC0E', + '#64B0C8', + '#E0752D', + '#BF1B00', + '#0A50A1', + '#962D82', + '#614D93', + '#9AC48A', + '#F2C96D', + '#65C5DB', + '#F9934E', + '#EA6460', + '#5195CE', + '#D683CE', + '#806EB7', + '#3F6833', + '#967302', + '#2F575E', + '#99440A', + '#58140C', + '#052B51', + '#511749', + '#3F2B5B', + '#E0F9D7', + '#FCEACA', + '#CFFAFF', + '#F9E2D2', + '#FCE2DE', + '#BADFF4', + '#F9D9F9', + '#DEDAF7', +]; + +export function sortColorsByHue(hexColors) { + let hslColors = _.map(hexColors, hexToHsl); + + let sortedHSLColors: any = _.sortBy(hslColors, ['h']); + sortedHSLColors = _.chunk(sortedHSLColors, PALETTE_ROWS); + sortedHSLColors = _.map(sortedHSLColors, chunk => { + return _.sortBy(chunk, 'l'); + }); + sortedHSLColors = _.flattenDeep(_.zip(...sortedHSLColors)); + + return _.map(sortedHSLColors, hslToHex); +} + +export function hexToHsl(color) { + return tinycolor(color).toHsl(); +} + +export function hslToHex(color) { + return tinycolor(color).toHexString(); +} + +export let sortedColors = sortColorsByHue(colors); +export default colors; diff --git a/src/vendor/grafana/event.ts b/src/vendor/grafana/event.ts new file mode 100644 index 0000000..de86b72 --- /dev/null +++ b/src/vendor/grafana/event.ts @@ -0,0 +1,11 @@ +export class AnnotationEvent { + dashboardId: number; + panelId: number; + userId: number; + time: any; + timeEnd: any; + isRegion: boolean; + text: string; + type: string; + tags: string; +} diff --git a/src/vendor/grafana/event_manager.ts b/src/vendor/grafana/event_manager.ts new file mode 100644 index 0000000..2a702f9 --- /dev/null +++ b/src/vendor/grafana/event_manager.ts @@ -0,0 +1,177 @@ +import _ from 'lodash'; +import moment from 'moment'; +import tinycolor from 'tinycolor2'; +import { MetricsPanelCtrl } from 'grafana/app/plugins/sdk'; +import { AnnotationEvent } from './event'; +import { + DEFAULT_ANNOTATION_COLOR, + OK_COLOR, + ALERTING_COLOR, + NO_DATA_COLOR, + REGION_FILL_ALPHA +} from './colors'; + +export class EventManager { + event: AnnotationEvent; + editorOpen: boolean; + + constructor(private panelCtrl: MetricsPanelCtrl) { } + + editorClosed() { + this.event = null; + this.editorOpen = false; + this.panelCtrl.render(); + } + + editorOpened() { + this.editorOpen = true; + } + + updateTime(range) { + if (!this.event) { + this.event = new AnnotationEvent(); + this.event.dashboardId = this.panelCtrl.dashboard.id; + this.event.panelId = this.panelCtrl.panel.id; + } + + // update time + this.event.time = moment(range.from); + this.event.isRegion = false; + if (range.to) { + this.event.timeEnd = moment(range.to); + this.event.isRegion = true; + } + + this.panelCtrl.render(); + } + + editEvent(event, elem?) { + this.event = event; + this.panelCtrl.render(); + } + + addFlotEvents(annotations, flotOptions) { + if (!this.event && annotations.length === 0) { + return; + } + + var types = { + $__alerting: { + color: ALERTING_COLOR, + position: 'BOTTOM', + markerSize: 5, + }, + $__ok: { + color: OK_COLOR, + position: 'BOTTOM', + markerSize: 5, + }, + $__no_data: { + color: NO_DATA_COLOR, + position: 'BOTTOM', + markerSize: 5, + }, + $__editing: { + color: DEFAULT_ANNOTATION_COLOR, + position: 'BOTTOM', + markerSize: 5, + }, + }; + + if (this.event) { + if (this.event.isRegion) { + annotations = [ + { + isRegion: true, + min: this.event.time.valueOf(), + timeEnd: this.event.timeEnd.valueOf(), + text: this.event.text, + eventType: '$__editing', + editModel: this.event, + }, + ]; + } else { + annotations = [ + { + min: this.event.time.valueOf(), + text: this.event.text, + editModel: this.event, + eventType: '$__editing', + }, + ]; + } + } else { + // annotations from query + for (var i = 0; i < annotations.length; i++) { + var item = annotations[i]; + + // add properties used by jquery flot events + item.min = item.time; + item.max = item.time; + item.eventType = item.source.name; + + if (item.newState) { + item.eventType = '$__' + item.newState; + continue; + } + + if (!types[item.source.name]) { + types[item.source.name] = { + color: item.source.iconColor, + position: 'BOTTOM', + markerSize: 5, + }; + } + } + } + + let regions = getRegions(annotations); + addRegionMarking(regions, flotOptions); + + let eventSectionHeight = 20; + let eventSectionMargin = 7; + flotOptions.grid.eventSectionHeight = eventSectionMargin; + flotOptions.xaxis.eventSectionHeight = eventSectionHeight; + + flotOptions.events = { + levels: _.keys(types).length + 1, + data: annotations, + types: types, + manager: this, + }; + } +} + +function getRegions(events) { + return _.filter(events, 'isRegion'); +} + +function addRegionMarking(regions, flotOptions) { + let markings = flotOptions.grid.markings; + let defaultColor = DEFAULT_ANNOTATION_COLOR; + let fillColor; + + _.each(regions, region => { + if (region.source) { + fillColor = region.source.iconColor || defaultColor; + } else { + fillColor = defaultColor; + } + + fillColor = addAlphaToRGB(fillColor, REGION_FILL_ALPHA); + markings.push({ + xaxis: { from: region.min, to: region.timeEnd }, + color: fillColor, + }); + }); +} + +function addAlphaToRGB(colorString: string, alpha: number): string { + let color = tinycolor(colorString); + if (color.isValid()) { + color.setAlpha(alpha); + return color.toRgbString(); + } else { + return colorString; + } +} diff --git a/src/vendor/grafana/ticks.ts b/src/vendor/grafana/ticks.ts new file mode 100644 index 0000000..a243de0 --- /dev/null +++ b/src/vendor/grafana/ticks.ts @@ -0,0 +1,158 @@ +import { getDataMinMax } from './time_series2'; + +/** + * Calculate tick step. + * Implementation from d3-array (ticks.js) + * https://github.com/d3/d3-array/blob/master/src/ticks.js + * @param start Start value + * @param stop End value + * @param count Ticks count + */ +export function tickStep(start: number, stop: number, count: number): number { + let e10 = Math.sqrt(50), + e5 = Math.sqrt(10), + e2 = Math.sqrt(2); + + let step0 = Math.abs(stop - start) / Math.max(0, count), + step1 = Math.pow(10, Math.floor(Math.log(step0) / Math.LN10)), + error = step0 / step1; + + if (error >= e10) { + step1 *= 10; + } else if (error >= e5) { + step1 *= 5; + } else if (error >= e2) { + step1 *= 2; + } + + return stop < start ? -step1 : step1; +} + +export function getScaledDecimals(decimals, tick_size) { + return decimals - Math.floor(Math.log(tick_size) / Math.LN10); +} + +/** + * Calculate tick size based on min and max values, number of ticks and precision. + * Implementation from Flot. + * @param min Axis minimum + * @param max Axis maximum + * @param noTicks Number of ticks + * @param tickDecimals Tick decimal precision + */ +export function getFlotTickSize(min: number, max: number, noTicks: number, tickDecimals: number) { + var delta = (max - min) / noTicks, + dec = -Math.floor(Math.log(delta) / Math.LN10), + maxDec = tickDecimals; + + 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 && (maxDec == null || dec + 1 <= maxDec)) { + size = 2.5; + ++dec; + } + } else if (norm < 7.5) { + size = 5; + } else { + size = 10; + } + + size *= magn; + + return size; +} + +/** + * Calculate axis range (min and max). + * Implementation from Flot. + */ +export function getFlotRange(panelMin, panelMax, datamin, datamax) { + const autoscaleMargin = 0.02; + + let min = +(panelMin != null ? panelMin : datamin); + let max = +(panelMax != null ? panelMax : datamax); + let delta = max - min; + + if (delta === 0.0) { + // Grafana fix: wide Y min and max using increased wideFactor + // when all series values are the same + var wideFactor = 0.25; + var widen = Math.abs(max === 0 ? 1 : max * wideFactor); + + if (panelMin === null) { + min -= widen; + } + // always widen max if we couldn't widen min to ensure we + // don't fall into min == max which doesn't work + if (panelMax == null || panelMin != null) { + max += widen; + } + } else { + // consider autoscaling + var margin = autoscaleMargin; + if (margin != null) { + if (panelMin == null) { + min -= delta * margin; + // make sure we don't go below zero if all values + // are positive + if (min < 0 && datamin != null && datamin >= 0) { + min = 0; + } + } + if (panelMax == null) { + max += delta * margin; + if (max > 0 && datamax != null && datamax <= 0) { + max = 0; + } + } + } + } + return { min, max }; +} + +/** + * Calculate tick decimals. + * Implementation from Flot. + */ +export function getFlotTickDecimals(data, axis) { + let { datamin, datamax } = getDataMinMax(data); + let { min, max } = getFlotRange(axis.min, axis.max, datamin, datamax); + let noTicks = 3; + let tickDecimals, maxDec; + let delta = (max - min) / noTicks; + let dec = -Math.floor(Math.log(delta) / Math.LN10); + + let magn = Math.pow(10, -dec); + // norm is between 1.0 and 10.0 + let norm = delta / magn; + let 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 && (maxDec == null || dec + 1 <= maxDec)) { + size = 2.5; + ++dec; + } + } else if (norm < 7.5) { + size = 5; + } else { + size = 10; + } + + size *= magn; + + tickDecimals = Math.max(0, maxDec != null ? maxDec : dec); + // grafana addition + const scaledDecimals = tickDecimals - Math.floor(Math.log(size) / Math.LN10); + return { tickDecimals, scaledDecimals }; +} diff --git a/src/vendor/grafana/time_series2.ts b/src/vendor/grafana/time_series2.ts new file mode 100644 index 0000000..cef6f9e --- /dev/null +++ b/src/vendor/grafana/time_series2.ts @@ -0,0 +1,349 @@ +import kbn from 'grafana/app/core/utils/kbn'; +import { getFlotTickDecimals } from './ticks'; +import _ from 'lodash'; + +function matchSeriesOverride(aliasOrRegex, seriesAlias) { + if (!aliasOrRegex) { + return false; + } + + if (aliasOrRegex[0] === '/') { + var regex = kbn.stringToJsRegex(aliasOrRegex); + return seriesAlias.match(regex) != null; + } + + return aliasOrRegex === seriesAlias; +} + +function translateFillOption(fill) { + return fill === 0 ? 0.001 : fill / 10; +} + +/** + * Calculate decimals for legend and update values for each series. + * @param data series data + * @param panel + */ +export function updateLegendValues(data: TimeSeries[], panel) { + for (let i = 0; i < data.length; i++) { + let series = data[i]; + let yaxes = panel.yaxes; + const seriesYAxis = series.yaxis || 1; + let axis = yaxes[seriesYAxis - 1]; + let { tickDecimals, scaledDecimals } = getFlotTickDecimals(data, axis); + let formater = kbn.valueFormats[panel.yaxes[seriesYAxis - 1].format]; + + // decimal override + if (_.isNumber(panel.decimals)) { + series.updateLegendValues(formater, panel.decimals, null); + } else { + // auto decimals + // legend and tooltip gets one more decimal precision + // than graph legend ticks + tickDecimals = (tickDecimals || -1) + 1; + series.updateLegendValues(formater, tickDecimals, scaledDecimals + 2); + } + } +} + +export function getDataMinMax(data: TimeSeries[]) { + let datamin = null; + let datamax = null; + + for (let series of data) { + if (datamax === null || datamax < series.stats.max) { + datamax = series.stats.max; + } + if (datamin === null || datamin > series.stats.min) { + datamin = series.stats.min; + } + } + + return { datamin, datamax }; +} + +export default class TimeSeries { + datapoints: any; + id: string; + label: string; + alias: string; + aliasEscaped: string; + color: string; + valueFormater: any; + stats: any; + legend: boolean; + allIsNull: boolean; + allIsZero: boolean; + decimals: number; + scaledDecimals: number; + hasMsResolution: boolean; + isOutsideRange: boolean; + + lines: any; + dashes: any; + bars: any; + points: any; + yaxis: any; + zindex: any; + stack: any; + nullPointMode: any; + fillBelowTo: any; + transform: any; + flotpairs: any; + unit: any; + + constructor(opts) { + this.datapoints = opts.datapoints; + this.label = opts.alias; + this.id = opts.alias; + this.alias = opts.alias; + this.aliasEscaped = _.escape(opts.alias); + this.color = opts.color; + this.valueFormater = kbn.valueFormats.none; + this.stats = {}; + this.legend = true; + this.unit = opts.unit; + this.hasMsResolution = this.isMsResolutionNeeded(); + } + + applySeriesOverrides(overrides) { + this.lines = {}; + this.dashes = { + dashLength: [], + }; + this.points = {}; + this.bars = {}; + this.yaxis = 1; + this.zindex = 0; + this.nullPointMode = null; + delete this.stack; + + for (var i = 0; i < overrides.length; i++) { + var override = overrides[i]; + if (!matchSeriesOverride(override.alias, this.alias)) { + continue; + } + if (override.lines !== void 0) { + this.lines.show = override.lines; + } + if (override.dashes !== void 0) { + this.dashes.show = override.dashes; + this.lines.lineWidth = 0; + } + if (override.points !== void 0) { + this.points.show = override.points; + } + if (override.bars !== void 0) { + this.bars.show = override.bars; + } + if (override.fill !== void 0) { + this.lines.fill = translateFillOption(override.fill); + } + if (override.stack !== void 0) { + this.stack = override.stack; + } + if (override.linewidth !== void 0) { + this.lines.lineWidth = this.dashes.show ? 0 : override.linewidth; + this.dashes.lineWidth = override.linewidth; + } + if (override.dashLength !== void 0) { + this.dashes.dashLength[0] = override.dashLength; + } + if (override.spaceLength !== void 0) { + this.dashes.dashLength[1] = override.spaceLength; + } + if (override.nullPointMode !== void 0) { + this.nullPointMode = override.nullPointMode; + } + if (override.pointradius !== void 0) { + this.points.radius = override.pointradius; + } + if (override.steppedLine !== void 0) { + this.lines.steps = override.steppedLine; + } + if (override.zindex !== void 0) { + this.zindex = override.zindex; + } + if (override.fillBelowTo !== void 0) { + this.fillBelowTo = override.fillBelowTo; + } + if (override.color !== void 0) { + this.color = override.color; + } + if (override.transform !== void 0) { + this.transform = override.transform; + } + if (override.legend !== void 0) { + this.legend = override.legend; + } + + if (override.yaxis !== void 0) { + this.yaxis = override.yaxis; + } + } + } + + getFlotPairs(fillStyle) { + var result = []; + + this.stats.total = 0; + this.stats.max = -Number.MAX_VALUE; + this.stats.min = Number.MAX_VALUE; + this.stats.logmin = Number.MAX_VALUE; + this.stats.avg = null; + this.stats.current = null; + this.stats.first = null; + this.stats.delta = 0; + this.stats.diff = null; + this.stats.range = null; + this.stats.timeStep = Number.MAX_VALUE; + this.allIsNull = true; + this.allIsZero = true; + + var ignoreNulls = fillStyle === 'connected'; + var nullAsZero = fillStyle === 'null as zero'; + var currentTime; + var currentValue; + var nonNulls = 0; + var previousTime; + var previousValue = 0; + var previousDeltaUp = true; + + for (var i = 0; i < this.datapoints.length; i++) { + currentValue = this.datapoints[i][0]; + currentTime = this.datapoints[i][1]; + + // Due to missing values we could have different timeStep all along the series + // so we have to find the minimum one (could occur with aggregators such as ZimSum) + if (previousTime !== undefined) { + let timeStep = currentTime - previousTime; + if (timeStep < this.stats.timeStep) { + this.stats.timeStep = timeStep; + } + } + previousTime = currentTime; + + if (currentValue === null) { + if (ignoreNulls) { + continue; + } + if (nullAsZero) { + currentValue = 0; + } + } + + if (currentValue !== null) { + if (_.isNumber(currentValue)) { + this.stats.total += currentValue; + this.allIsNull = false; + nonNulls++; + } + + if (currentValue > this.stats.max) { + this.stats.max = currentValue; + } + + if (currentValue < this.stats.min) { + this.stats.min = currentValue; + } + + if (this.stats.first === null) { + this.stats.first = currentValue; + } else { + if (previousValue > currentValue) { + // counter reset + previousDeltaUp = false; + if (i === this.datapoints.length - 1) { + // reset on last + this.stats.delta += currentValue; + } + } else { + if (previousDeltaUp) { + this.stats.delta += currentValue - previousValue; // normal increment + } else { + this.stats.delta += currentValue; // account for counter reset + } + previousDeltaUp = true; + } + } + previousValue = currentValue; + + if (currentValue < this.stats.logmin && currentValue > 0) { + this.stats.logmin = currentValue; + } + + if (currentValue !== 0) { + this.allIsZero = false; + } + } + + result.push([currentTime, currentValue]); + } + + if (this.stats.max === -Number.MAX_VALUE) { + this.stats.max = null; + } + if (this.stats.min === Number.MAX_VALUE) { + this.stats.min = null; + } + + if (result.length && !this.allIsNull) { + this.stats.avg = this.stats.total / nonNulls; + this.stats.current = result[result.length - 1][1]; + if (this.stats.current === null && result.length > 1) { + this.stats.current = result[result.length - 2][1]; + } + } + if (this.stats.max !== null && this.stats.min !== null) { + this.stats.range = this.stats.max - this.stats.min; + } + if (this.stats.current !== null && this.stats.first !== null) { + this.stats.diff = this.stats.current - this.stats.first; + } + + this.stats.count = result.length; + return result; + } + + updateLegendValues(formater, decimals, scaledDecimals) { + this.valueFormater = formater; + this.decimals = decimals; + this.scaledDecimals = scaledDecimals; + } + + formatValue(value) { + if (!_.isFinite(value)) { + value = null; // Prevent NaN formatting + } + return this.valueFormater(value, this.decimals, this.scaledDecimals); + } + + isMsResolutionNeeded() { + for (var i = 0; i < this.datapoints.length; i++) { + if (this.datapoints[i][1] !== null) { + var timestamp = this.datapoints[i][1].toString(); + if (timestamp.length === 13 && timestamp % 1000 !== 0) { + return true; + } + } + } + return false; + } + + hideFromLegend(options) { + if (options.hideEmpty && this.allIsNull) { + return true; + } + // ignore series excluded via override + if (!this.legend) { + return true; + } + + // ignore zero series + if (options.hideZero && this.allIsZero) { + return true; + } + + return false; + } +}