From a0a85dbb90a6aca8dc17081ed9168219ab90de36 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 13aa53a8cf4b2..102b135f3cd1f 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; @@ -88,7 +89,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 e92a4fe797855..413458f336e6f 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 @@ -115,11 +115,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 @@ +