// @ts-nocheck // Libraries import _ from 'lodash'; import $ from 'jquery'; import tinycolor from 'tinycolor2'; import React, { PureComponent } from 'react'; import uniqBy from 'lodash/uniqBy'; import flattenDeep from 'lodash/flattenDeep'; import cloneDeep from 'lodash/cloneDeep'; import appEvents from 'grafana/app/core/app_events'; import { getDataSourceSrv, getBackendSrv } from '@grafana/runtime'; import { getValueFormat, formattedValueToString, AnnotationEvent, DataSourceApi, AppEvents } from '@grafana/data'; import { Graph, OK_COLOR, ALERTING_COLOR, NO_DATA_COLOR, PENDING_COLOR, DEFAULT_ANNOTATION_COLOR, REGION_FILL_ALPHA } from '@grafana/ui'; export class Graph2 extends Graph { dashboard: any; datasourcePromises: any; globalAnnotationsPromise: any; annotations: AnnotationEvent[]; constructor(props) { super(props); const promises = []; const dsPromises = []; const range = props.timeRange; this.annotations = []; if (props.panelChrome) { this.dashboard = props.panelChrome.props.dashboard; Promise.all([this.getGlobalAnnotations(range)]) .then(results => { this.annotations = flattenDeep(results[0]); // filter out annotations that do not belong to requesting panel this.annotations = this.annotations.filter(item => { // if event has panel id and query is of type dashboard then panel and requesting panel id must match if (item.panelId && item.source.type === 'dashboard') { return item.panelId === props.panelChrome.props.panel.id; } return true; }); this.annotations = this.dedupAnnotations(this.annotations); this.draw(); }) .catch(err => { if (!err.message && err.data && err.data.message) { err.message = err.data.message; } console.log('AnnotationSrv.query error', err); appEvents.emit(AppEvents.alertError, ['Annotation Query Failed', err.message || err]); return []; }); } } getGlobalAnnotations(range: any) { const promises = []; const dsPromises = []; for (const annotation of this.dashboard.annotations.list) { if (!annotation.enable) { continue; } if (annotation.snapshotData) { return this.translateQueryResult(annotation, annotation.snapshotData); } const datasourcePromise = getDataSourceSrv().get(annotation.datasource); dsPromises.push(datasourcePromise); promises.push( datasourcePromise .then((datasource: DataSourceApi) => { // issue query against data source return datasource.annotationQuery({ range, rangeRaw: range.raw, annotation: annotation, dashboard: this.dashboard, }); }) .then(results => { // store response in annotation object if this is a snapshot call if (this.dashboard.snapshot) { annotation.snapshotData = cloneDeep(results); } // translate result return this.translateQueryResult(annotation, results); }) ); } this.datasourcePromises = Promise.all(dsPromises); this.globalAnnotationsPromise = Promise.all(promises); return this.globalAnnotationsPromise; } dedupAnnotations(annotations: any) { let dedup = []; // Split events by annotationId property existence const events = _.partition(annotations, 'id'); const eventsById = _.groupBy(events[0], 'id'); dedup = _.map(eventsById, eventGroup => { if (eventGroup.length > 1 && !_.every(eventGroup, this.isPanelAlert)) { // Get first non-panel alert return _.find(eventGroup, event => { return event.eventType !== 'panel-alert'; }); } else { return _.head(eventGroup); } }); dedup = _.concat(dedup, events[1]); return dedup; } isPanelAlert(event: { eventType: string }) { return event.eventType === 'panel-alert'; } translateQueryResult(annotation: any, results: any) { // if annotation has snapshotData // make clone and remove it if (annotation.snapshotData) { annotation = cloneDeep(annotation); delete annotation.snapshotData; } for (const item of results) { item.source = annotation; item.isRegion = item.timeEnd && item.time !== item.timeEnd; } return results; } tickFormatter(val, axis) { const formatter = getValueFormat('short'); if (!formatter) { throw new Error(`Unit '${format}' is not supported`); } return formattedValueToString(formatter(val, axis.tickDecimals, axis.scaledDecimals)); } getYAxes(series: GraphSeriesXY[]) { if (series.length === 0) { return [{ show: true, min: -1, max: 1 }]; } return uniqBy( series.map(s => { const index = s.yAxis ? s.yAxis.index : 1; const min = s.yAxis && !isNaN(s.yAxis.min as number) ? s.yAxis.min : null; const tickDecimals = s.yAxis && !isNaN(s.yAxis.tickDecimals as number) ? s.yAxis.tickDecimals : null; return { show: true, index, position: index === 1 ? 'left' : 'right', min, tickDecimals, tickFormatter: this.tickFormatter, }; }), yAxisConfig => yAxisConfig.index ); } getFillGradient(amount: number) { if (!amount) { return null; } return { colors: [{ opacity: 0.0 }, { opacity: amount / 10 }], }; } translateFillOption(fill: number) { if (this.props.stack) { return fill === 0 ? 0.001 : fill / 10; } else { return fill / 10; } } draw() { if (this.element === null) { return; } const { width, series, timeRange, showLines, showBars, showPoints, isStacked, lineWidth, fill, fillGradient, timeZone, onHorizontalRegionSelected, } = this.props; if (!width) { return; } const ticks = width / 100; const min = timeRange.from.valueOf(); const max = timeRange.to.valueOf(); const yaxes = this.getYAxes(series); const flotOptions: any = { legend: { show: false, }, series: { stack: isStacked, lines: { show: showLines, fill: this.translateFillOption(fill), fillColor: this.getFillGradient(fillGradient), lineWidth: lineWidth, zero: false, }, points: { show: showPoints, fill: 1, fillColor: false, radius: 2, }, bars: { show: showBars, fill: 1, // Dividig the width by 1.5 to make the bars not touch each other barWidth: showBars ? this.getBarWidth() / 1.5 : 1, zero: false, lineWidth: lineWidth, }, shadowSize: 0, }, xaxis: { show: true, mode: 'time', min: min, max: max, label: 'Datetime', ticks: ticks, timeformat: timeFormat(ticks, min, max), timezone: timeZone ?? DefaultTimeZone, }, yaxes, grid: { minBorderMargin: 0, markings: [], backgroundColor: null, borderWidth: 0, hoverable: true, clickable: true, color: '#a1a1a1', margin: { left: 0, right: 0 }, labelMarginX: 0, mouseActiveRadius: 30, }, selection: { mode: onHorizontalRegionSelected ? 'x' : null, color: '#666', }, crosshair: { mode: 'x', }, }; this.addFlotEvents(this.annotations, flotOptions); try { $.plot( this.element, series.filter(s => s.isVisible), flotOptions ); } catch (err) { console.log('Graph rendering error', err, flotOptions, series); throw new Error('Error rendering panel'); } } render() { const { height, width, series } = this.props; const noDataToBeDisplayed = series.length === 0; const tooltip = this.renderTooltip(); const context = this.renderContextMenu(); return ( <div className="graph-panel"> <div className="graph-panel__chart" ref={e => (this.element = e)} style={{ height, width }} onMouseLeave={() => { this.setState({ isTooltipVisible: false }); }} /> {noDataToBeDisplayed && <div className="datapoints-warning">No data</div>} {tooltip} {context} </div> ); } addFlotEvents(annotations: any, flotOptions: any) { if (!annotations) { return; } const types: any = { $__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, }, $__pending: { color: PENDING_COLOR, position: 'BOTTOM', markerSize: 5, }, $__editing: { color: DEFAULT_ANNOTATION_COLOR, position: 'BOTTOM', markerSize: 5, }, }; // annotations from query annotations.map(item => { // 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; } else { if (!types[item.source.name]) { types[item.source.name] = { color: item.source.iconColor, position: 'BOTTOM', markerSize: 5, }; } } }); const regions = getRegions(annotations); addRegionMarking(regions, flotOptions); const eventSectionHeight = 20; const 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: AnnotationEvent[]) { return _.filter(events, 'isRegion'); } function addRegionMarking(regions: any[], flotOptions: { grid: { markings: any } }) { const markings = flotOptions.grid.markings; const 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 { const color = tinycolor(colorString); if (color.isValid()) { color.setAlpha(alpha); return color.toRgbString(); } else { return colorString; } } // Copied from graph.ts function timeFormat(ticks: number, min: number, max: number): string { if (min && max && ticks) { const range = max - min; const secPerTick = range / ticks / 1000; const oneDay = 86400000; const oneYear = 31536000000; if (secPerTick <= 45) { return '%H:%M:%S'; } if (secPerTick <= 7200 || range <= oneDay) { return '%H:%M'; } if (secPerTick <= 80000) { return '%m/%d %H:%M'; } if (secPerTick <= 2419200 || range <= oneYear) { return '%m/%d'; } return '%Y-%m'; } return '%H:%M'; } export default Graph2;