diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Popover/Contents.tsx b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Popover/Contents.tsx new file mode 100644 index 00000000000000..378ad9509c2170 --- /dev/null +++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Popover/Contents.tsx @@ -0,0 +1,66 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + EuiFlexGroup, + EuiFlexItem, + EuiHorizontalRule, + EuiTitle +} from '@elastic/eui'; +import cytoscape from 'cytoscape'; +import React from 'react'; +import { Buttons } from './Buttons'; +import { Info } from './Info'; +import { ServiceMetricList } from './ServiceMetricList'; + +const popoverMinWidth = 280; + +interface ContentsProps { + focusedServiceName?: string; + isService: boolean; + label: string; + onFocusClick: () => void; + selectedNodeData: cytoscape.NodeDataDefinition; + selectedNodeServiceName: string; +} + +export function Contents({ + selectedNodeData, + focusedServiceName, + isService, + label, + onFocusClick, + selectedNodeServiceName +}: ContentsProps) { + return ( + + + +

{label}

+
+ +
+ + {isService ? ( + + ) : ( + + )} + + {isService && ( + + )} +
+ ); +} diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Popover/Info.tsx b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Popover/Info.tsx index 1c5443e404f9b3..d432119505382a 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Popover/Info.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Popover/Info.tsx @@ -4,10 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; +import lightTheme from '@elastic/eui/dist/eui_theme_light.json'; import { i18n } from '@kbn/i18n'; +import cytoscape from 'cytoscape'; +import React from 'react'; import styled from 'styled-components'; -import lightTheme from '@elastic/eui/dist/eui_theme_light.json'; const ItemRow = styled.div` line-height: 2; @@ -19,8 +20,8 @@ const ItemTitle = styled.dt` const ItemDescription = styled.dd``; -interface InfoProps { - type: string; +interface InfoProps extends cytoscape.NodeDataDefinition { + type?: string; subtype?: string; } diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Popover/Popover.stories.tsx b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Popover/Popover.stories.tsx new file mode 100644 index 00000000000000..b26488c5ef7de9 --- /dev/null +++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Popover/Popover.stories.tsx @@ -0,0 +1,49 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { storiesOf } from '@storybook/react'; +import React from 'react'; +import { + ApmPluginContext, + ApmPluginContextValue +} from '../../../../context/ApmPluginContext'; +import { Contents } from './Contents'; + +const selectedNodeData = { + id: 'opbeans-node', + label: 'opbeans-node', + href: + '#/services/opbeans-node/service-map?rangeFrom=now-24h&rangeTo=now&refreshPaused=true&refreshInterval=0', + agentName: 'nodejs', + type: 'service' +}; + +storiesOf('app/ServiceMap/Popover/Contents', module).add( + 'example', + () => { + return ( + + {}} + selectedNodeServiceName="opbeans-node" + /> + + ); + }, + { + info: { + propTablesExclude: [ApmPluginContext.Provider], + source: false + } + } +); 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 dfb78aaa0214c4..e8e37cfdfb1f0f 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 @@ -4,27 +4,18 @@ * you may not use this file except in compliance with the Elastic License. */ -import { - EuiFlexGroup, - EuiFlexItem, - EuiHorizontalRule, - EuiPopover, - EuiTitle -} from '@elastic/eui'; +import { EuiPopover } from '@elastic/eui'; import cytoscape from 'cytoscape'; import React, { CSSProperties, + useCallback, useContext, useEffect, - useState, - useCallback + useRef, + useState } from 'react'; import { CytoscapeContext } from '../Cytoscape'; -import { Buttons } from './Buttons'; -import { Info } from './Info'; -import { ServiceMetricList } from './ServiceMetricList'; - -const popoverMinWidth = 280; +import { Contents } from './Contents'; interface PopoverProps { focusedServiceName?: string; @@ -35,56 +26,62 @@ export function Popover({ focusedServiceName }: PopoverProps) { const [selectedNode, setSelectedNode] = useState< cytoscape.NodeSingular | undefined >(undefined); - const onFocusClick = useCallback(() => setSelectedNode(undefined), [ + const deselect = useCallback(() => setSelectedNode(undefined), [ setSelectedNode ]); - - useEffect(() => { - const selectHandler: cytoscape.EventHandler = event => { - setSelectedNode(event.target); - }; - const unselectHandler: cytoscape.EventHandler = () => { - setSelectedNode(undefined); - }; - - if (cy) { - cy.on('select', 'node', selectHandler); - cy.on('unselect', 'node', unselectHandler); - cy.on('data viewport', unselectHandler); - } - - return () => { - if (cy) { - cy.removeListener('select', 'node', selectHandler); - cy.removeListener('unselect', 'node', unselectHandler); - cy.removeListener('data viewport', undefined, unselectHandler); - } - }; - }, [cy]); - const renderedHeight = selectedNode?.renderedHeight() ?? 0; const renderedWidth = selectedNode?.renderedWidth() ?? 0; const { x, y } = selectedNode?.renderedPosition() ?? { x: 0, y: 0 }; const isOpen = !!selectedNode; - const selectedNodeServiceName: string = selectedNode?.data('id'); const isService = selectedNode?.data('type') === 'service'; const triggerStyle: CSSProperties = { background: 'transparent', height: renderedHeight, position: 'absolute', - width: renderedWidth + width: renderedWidth, + border: '3px dotted red' }; - const trigger =
; - + const trigger =
; const zoom = cy?.zoom() ?? 1; const height = selectedNode?.height() ?? 0; - const translateY = y - (zoom + 1) * (height / 2); + const translateY = y - ((zoom + 1) * height) / 4; const popoverStyle: CSSProperties = { position: 'absolute', transform: `translate(${x}px, ${translateY}px)` }; - const data = selectedNode?.data() ?? {}; - const label = data.label || selectedNodeServiceName; + const selectedNodeData = selectedNode?.data() ?? {}; + const selectedNodeServiceName = selectedNodeData.id; + const label = selectedNodeData.label || selectedNodeServiceName; + const popoverRef = useRef(null); + + // Set up Cytoscape event handlers + useEffect(() => { + const selectHandler: cytoscape.EventHandler = event => { + setSelectedNode(event.target); + }; + + if (cy) { + cy.on('select', 'node', selectHandler); + cy.on('unselect', 'node', deselect); + cy.on('data viewport', deselect); + } + + return () => { + if (cy) { + cy.removeListener('select', 'node', selectHandler); + cy.removeListener('unselect', 'node', deselect); + cy.removeListener('data viewport', undefined, deselect); + } + }; + }, [cy, deselect]); + + // Handle positioning of popover. This makes it so the popover positions + // itself correctly and the arrows are always pointing to where they should. + useEffect(() => { + if (popoverRef.current) { + popoverRef.current.positionPopoverFluid(); + } + }, [popoverRef, x, y]); return ( {}} isOpen={isOpen} + ref={popoverRef} style={popoverStyle} > - - - -

{label}

-
- -
- - - {isService ? ( - - ) : ( - - )} - - {isService && ( - - )} -
+
); }