From 68fe807e27ed950e9854a716abe2c91244064111 Mon Sep 17 00:00:00 2001 From: Nathan L Smith Date: Mon, 23 Mar 2020 16:13:56 -0500 Subject: [PATCH] Simplify service map layout (#60949) Clean up the cytoscape component and event handlers to simplify the layout logic. Make all centering animations animated. Add logging of cytoscape events when we're in debug mode. Add Elasticsearch icon. --- .../app/ServiceMap/Cytoscape.stories.tsx | 8 ++ .../components/app/ServiceMap/Cytoscape.tsx | 94 +++++++++---------- .../app/ServiceMap/Popover/index.tsx | 6 +- .../app/ServiceMap/cytoscapeOptions.ts | 5 - .../public/components/app/ServiceMap/icons.ts | 9 +- .../app/ServiceMap/icons/elasticsearch.svg | 1 + 6 files changed, 69 insertions(+), 54 deletions(-) create mode 100644 x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons/elasticsearch.svg diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Cytoscape.stories.tsx b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Cytoscape.stories.tsx index 155695f7596dd..7a066b520cc3b 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Cytoscape.stories.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Cytoscape.stories.tsx @@ -77,6 +77,14 @@ storiesOf('app/ServiceMap/Cytoscape', module) { data: { id: 'default' } }, { data: { id: 'cache', label: 'cache', 'span.type': 'cache' } }, { data: { id: 'database', label: 'database', 'span.type': 'db' } }, + { + data: { + id: 'elasticsearch', + label: 'elasticsearch', + 'span.type': 'db', + 'span.subtype': 'elasticsearch' + } + }, { data: { id: 'external', label: 'external', 'span.type': 'external' } }, diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Cytoscape.tsx b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Cytoscape.tsx index e0a188b4915a2..a4cd6f4ed09a9 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Cytoscape.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Cytoscape.tsx @@ -9,7 +9,6 @@ import React, { createContext, CSSProperties, ReactNode, - useCallback, useEffect, useRef, useState @@ -109,23 +108,26 @@ export function Cytoscape({ serviceName, style }: CytoscapeProps) { - const initialElements = elements.map(element => ({ - ...element, - // prevents flash of unstyled elements - classes: [element.classes, 'invisible'].join(' ').trim() - })); - const [ref, cy] = useCytoscape({ ...cytoscapeOptions, - elements: initialElements + elements }); // Add the height to the div style. The height is a separate prop because it // is required and can trigger rendering when changed. const divStyle = { ...style, height }; - const resetConnectedEdgeStyle = useCallback( - (node?: cytoscape.NodeSingular) => { + // Trigger a custom "data" event when data changes + useEffect(() => { + if (cy && elements.length > 0) { + cy.add(elements); + cy.trigger('data'); + } + }, [cy, elements]); + + // Set up cytoscape event handlers + useEffect(() => { + const resetConnectedEdgeStyle = (node?: cytoscape.NodeSingular) => { if (cy) { cy.edges().removeClass('highlight'); @@ -133,12 +135,9 @@ export function Cytoscape({ node.connectedEdges().addClass('highlight'); } } - }, - [cy] - ); + }; - const dataHandler = useCallback( - event => { + const dataHandler: cytoscape.EventHandler = event => { if (cy) { if (serviceName) { resetConnectedEdgeStyle(cy.getElementById(serviceName)); @@ -150,36 +149,25 @@ export function Cytoscape({ } else { resetConnectedEdgeStyle(); } - if (event.cy.elements().length > 0) { - const selectedRoots = selectRoots(event.cy); - const layout = cy.layout( - getLayoutOptions(selectedRoots, height, width) - ); - layout.one('layoutstop', () => { - if (serviceName) { - const focusedNode = cy.getElementById(serviceName); - cy.center(focusedNode); - } - // show elements after layout is applied - cy.elements().removeClass('invisible'); - }); - layout.run(); - } - } - }, - [cy, resetConnectedEdgeStyle, serviceName, height, width] - ); - // Trigger a custom "data" event when data changes - useEffect(() => { - if (cy) { - cy.add(elements); - cy.trigger('data'); - } - }, [cy, elements]); + const selectedRoots = selectRoots(event.cy); + const layout = cy.layout( + getLayoutOptions(selectedRoots, height, width) + ); - // Set up cytoscape event handlers - useEffect(() => { + layout.run(); + } + }; + const layoutstopHandler: cytoscape.EventHandler = event => { + event.cy.animate({ + ...animationOptions, + center: { + eles: serviceName + ? event.cy.getElementById(serviceName) + : event.cy.collection() + } + }); + }; const mouseoverHandler: cytoscape.EventHandler = event => { event.target.addClass('hover'); event.target.connectedEdges().addClass('nodeHover'); @@ -194,10 +182,18 @@ export function Cytoscape({ const unselectHandler: cytoscape.EventHandler = event => { resetConnectedEdgeStyle(); }; + const debugHandler: cytoscape.EventHandler = event => { + const debugEnabled = sessionStorage.getItem('apm_debug') === 'true'; + if (debugEnabled) { + // eslint-disable-next-line no-console + console.debug('cytoscape:', event); + } + }; if (cy) { + cy.on('data layoutstop select unselect', debugHandler); cy.on('data', dataHandler); - cy.ready(dataHandler); + cy.on('layoutstop', layoutstopHandler); cy.on('mouseover', 'edge, node', mouseoverHandler); cy.on('mouseout', 'edge, node', mouseoutHandler); cy.on('select', 'node', selectHandler); @@ -207,15 +203,19 @@ export function Cytoscape({ return () => { if (cy) { cy.removeListener( - 'data', + 'data layoutstop select unselect', undefined, - dataHandler as cytoscape.EventHandler + debugHandler ); + cy.removeListener('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, dataHandler, resetConnectedEdgeStyle, serviceName]); + }, [cy, height, serviceName, width]); return ( diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Popover/index.tsx b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Popover/index.tsx index 35f68b84fabed..a9cc6457b4e31 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Popover/index.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Popover/index.tsx @@ -17,6 +17,7 @@ import React, { import { SERVICE_NAME } from '../../../../../../../../plugins/apm/common/elasticsearch_fieldnames'; import { CytoscapeContext } from '../Cytoscape'; import { Contents } from './Contents'; +import { animationOptions } from '../cytoscapeOptions'; interface PopoverProps { focusedServiceName?: string; @@ -89,7 +90,10 @@ export function Popover({ focusedServiceName }: PopoverProps) { const centerSelectedNode = useCallback(() => { if (cy) { - cy.center(cy.getElementById(selectedNodeServiceName)); + cy.animate({ + ...animationOptions, + center: { eles: cy.getElementById(selectedNodeServiceName) } + }); } }, [cy, selectedNodeServiceName]); diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/cytoscapeOptions.ts b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/cytoscapeOptions.ts index e19cb8ae4b646..3681725665797 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/cytoscapeOptions.ts +++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/cytoscapeOptions.ts @@ -113,11 +113,6 @@ const style: cytoscape.Stylesheet[] = [ selector: 'edge[isInverseEdge]', style: { visibility: 'hidden' } }, - // @ts-ignore - { - selector: '.invisible', - style: { visibility: 'hidden' } - }, { selector: 'edge.nodeHover', style: { diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons.ts b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons.ts index 5102dfc02f757..4925ffba310b5 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons.ts +++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons.ts @@ -8,12 +8,14 @@ import cytoscape from 'cytoscape'; import { AGENT_NAME, SERVICE_NAME, - SPAN_TYPE + SPAN_TYPE, + SPAN_SUBTYPE } from '../../../../../../../plugins/apm/common/elasticsearch_fieldnames'; import databaseIcon from './icons/database.svg'; import defaultIconImport from './icons/default.svg'; import documentsIcon from './icons/documents.svg'; import dotNetIcon from './icons/dot-net.svg'; +import elasticsearchIcon from './icons/elasticsearch.svg'; import globeIcon from './icons/globe.svg'; import goIcon from './icons/go.svg'; import javaIcon from './icons/java.svg'; @@ -63,6 +65,11 @@ export function iconForNode(node: cytoscape.NodeSingular) { return serviceIcons[node.data(AGENT_NAME) as string]; } else if (isIE11) { return defaultIcon; + } else if ( + node.data(SPAN_TYPE) === 'db' && + node.data(SPAN_SUBTYPE) === 'elasticsearch' + ) { + return elasticsearchIcon; } else if (icons[type]) { return icons[type]; } else { diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons/elasticsearch.svg b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons/elasticsearch.svg new file mode 100644 index 0000000000000..4f9fda36ba06a --- /dev/null +++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons/elasticsearch.svg @@ -0,0 +1 @@ +