import cytoscape from 'cytoscape' ; import { debounce } from 'lodash' ; import { useEffect } from 'react' ; import { EuiTheme } from '../../../../../../../src/plugins/kibana_react/common' ; import { useUiTracker } from '../../../../../observability/public' ; import { getAnimationOptions , getNodeHeight } from './cytoscape_options' ; function applyCubicBezierStyles ( edges ) { edges . forEach ( ( edge ) => { const { x : x0 , y : y0 } = edge . source ( ) . position ( ) ; const { x : x1 , y : y1 } = edge . target ( ) . position ( ) ; const x = x1 - x0 ; const y = y1 - y0 ; const z = Math . sqrt ( x * x + y * y ) ; const costheta = z === 0 ? 0 : x / z ; const alpha = 0.25 ; edge . style ( 'control-point-distances' , [ - alpha * y * costheta , alpha * y * costheta , ] ) ; edge . style ( 'control-point-weights' , [ alpha , 1 - alpha ] ) ; } ) ; } function getLayoutOptions ( { fit = false , nodeHeight , theme , } ) { const animationOptions = getAnimationOptions ( theme ) ; return { animationDuration : animationOptions . duration , animationEasing : animationOptions . easing , fit , name : 'dagre' , animate : ! fit , padding : nodeHeight , spacingFactor : 1.2 , nodeSep : nodeHeight , edgeSep : 32 , rankSep : 128 , rankDir : 'LR' , ranker : 'network-simplex' , } ; } function setCursor ( cursor , event ) { const container = event . cy . container ( ) ; if ( container ) { container . style . cursor = cursor ; } } function resetConnectedEdgeStyle ( cytoscapeInstance , node ? ) { cytoscapeInstance . edges ( ) . removeClass ( 'highlight' ) ; if ( node ) { node . connectedEdges ( ) . addClass ( 'highlight' ) ; } } export function useCytoscapeEventHandlers ( { cy , serviceName , theme , } ) { const trackApmEvent = useUiTracker ( { app : 'apm' } ) ; useEffect ( ( ) => { const nodeHeight = getNodeHeight ( theme ) ; const dataHandler = ( event , fit ) => { if ( serviceName ) { const node = event . cy . getElementById ( serviceName ) ; resetConnectedEdgeStyle ( event . cy , node ) ; if ( event . cy . nodes ( ) . length > 0 ) { event . cy . nodes ( ) . removeClass ( 'primary' ) ; node . addClass ( 'primary' ) ; } } else { resetConnectedEdgeStyle ( event . cy ) ; } event . cy . elements ( '[!hasBeenDragged]' ) . difference ( 'node:selected' ) . layout ( getLayoutOptions ( { fit , nodeHeight , theme } ) ) . run ( ) ; } ; const layoutstopHandler = ( event ) => { applyCubicBezierStyles ( event . cy . edges ( ) ) ; } ; const trackNodeEdgeHover = debounce ( ( ) => trackApmEvent ( { metric : 'service_map_node_or_edge_hover' } ) , 1000 ) ; const mouseoverHandler = ( event ) => { if ( event . target . isNode ( ) ) { setCursor ( 'pointer' , event ) ; } trackNodeEdgeHover ( ) ; event . target . addClass ( 'hover' ) ; event . target . connectedEdges ( ) . addClass ( 'nodeHover' ) ; } ; const mouseoutHandler = ( event ) => { setCursor ( 'grab' , event ) ; event . target . removeClass ( 'hover' ) ; event . target . connectedEdges ( ) . removeClass ( 'nodeHover' ) ; } ; const selectHandler = ( event ) => { trackApmEvent ( { metric : 'service_map_node_select' } ) ; resetConnectedEdgeStyle ( event . cy , event . target ) ; } ; const unselectHandler = ( event ) => { resetConnectedEdgeStyle ( event . cy , serviceName ? event . cy . getElementById ( serviceName ) : undefined ) ; } ; const debugHandler = ( event ) => { const debugEnabled = sessionStorage . getItem ( 'apm_debug' ) === 'true' ; if ( debugEnabled ) { console . debug ( 'cytoscape:' , event ) ; } } ; const dragHandler = ( event ) => { setCursor ( 'grabbing' , event ) ; applyCubicBezierStyles ( event . target . connectedEdges ( ) ) ; if ( ! event . target . data ( 'hasBeenDragged' ) ) { event . target . data ( 'hasBeenDragged' , true ) ; } } ; const dragfreeHandler = ( event ) => { setCursor ( 'pointer' , event ) ; } ; const tapstartHandler = ( event ) => { if ( ! event . target . isNode || ! event . target . isNode ( ) ) { setCursor ( 'grabbing' , event ) ; } } ; const tapendHandler = ( event ) => { if ( ! event . target . isNode || ! event . target . isNode ( ) ) { setCursor ( 'grab' , event ) ; } } ; if ( cy ) { cy . on ( 'custom:data drag dragfree layoutstop select tapstart tapend unselect' , debugHandler ) ; cy . on ( 'custom:data' , dataHandler ) ; cy . on ( 'layoutstop' , layoutstopHandler ) ; cy . on ( 'mouseover' , 'edge, node' , mouseoverHandler ) ; cy . on ( 'mouseout' , 'edge, node' , mouseoutHandler ) ; cy . on ( 'select' , 'node' , selectHandler ) ; cy . on ( 'unselect' , 'node' , unselectHandler ) ; cy . on ( 'drag' , 'node' , dragHandler ) ; cy . on ( 'dragfree' , 'node' , dragfreeHandler ) ; cy . on ( 'tapstart' , tapstartHandler ) ; cy . on ( 'tapend' , tapendHandler ) ; } return ( ) => { if ( cy ) { cy . removeListener ( 'custom:data drag dragfree layoutstop select tapstart tapend unselect' , undefined , debugHandler ) ; cy . removeListener ( 'custom:data' , undefined , dataHandler ) ; cy . removeListener ( 'layoutstop' , undefined , layoutstopHandler ) ; cy . removeListener ( 'mouseover' , 'edge, node' , mouseoverHandler ) ; cy . removeListener ( 'mouseout' , 'edge, node' , mouseoutHandler ) ; cy . removeListener ( 'select' , 'node' , selectHandler ) ; cy . removeListener ( 'unselect' , 'node' , unselectHandler ) ; cy . removeListener ( 'drag' , 'node' , dragHandler ) ; cy . removeListener ( 'dragfree' , 'node' , dragfreeHandler ) ; cy . removeListener ( 'tapstart' , undefined , tapstartHandler ) ; cy . removeListener ( 'tapend' , undefined , tapendHandler ) ; } } ; } , [ cy , serviceName , trackApmEvent , theme ] ) ; }