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 ] ) ; this . annotations = this . annotations . filter ( item => { 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 ) { 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 ) => { return datasource . annotationQuery ( { range , rangeRaw : range . raw , annotation : annotation , dashboard : this . dashboard , } ) ; } ) . then ( results => { if ( this . dashboard . snapshot ) { annotation . snapshotData = cloneDeep ( results ) ; } return this . translateQueryResult ( annotation , results ) ; } ) ) ; } this . datasourcePromises = Promise . all ( dsPromises ) ; this . globalAnnotationsPromise = Promise . all ( promises ) ; return this . globalAnnotationsPromise ; } dedupAnnotations ( annotations ) { let dedup = [ ] ; const events = _ . partition ( annotations , 'id' ) ; const eventsById = _ . groupBy ( events [ 0 ] , 'id' ) ; dedup = _ . map ( eventsById , eventGroup => { if ( eventGroup . length > 1 && ! _ . every ( eventGroup , this . isPanelAlert ) ) { return _ . find ( eventGroup , event => { return event . eventType !== 'panel-alert' ; } ) ; } else { return _ . head ( eventGroup ) ; } } ) ; dedup = _ . concat ( dedup , events [ 1 ] ) ; return dedup ; } isPanelAlert ( event ) { return event . eventType === 'panel-alert' ; } translateQueryResult ( annotation , results ) { 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 ) { 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 ) { if ( ! amount ) { return null ; } return { colors : [ { opacity : 0.0 } , { opacity : amount / 10 } ] , } ; } translateFillOption ( fill ) { 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 = { 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 , 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 , flotOptions ) { if ( ! annotations ) { return ; } const 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 , } , $__pending : { color : PENDING_COLOR , position : 'BOTTOM' , markerSize : 5 , } , $__editing : { color : DEFAULT_ANNOTATION_COLOR , position : 'BOTTOM' , markerSize : 5 , } , } ; annotations . map ( item => { 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 ) { return _ . filter ( events , 'isRegion' ) ; } function addRegionMarking ( regions , flotOptions ) { 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 , alpha ) { const color = tinycolor ( colorString ) ; if ( color . isValid ( ) ) { color . setAlpha ( alpha ) ; return color . toRgbString ( ) ; } else { return colorString ; } } function timeFormat ( ticks , min , max ) { 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 ;