diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/store/actions.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/store/actions.ts index ceb5da2ca9098..0860c9c62aca4 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/store/actions.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/store/actions.ts @@ -43,6 +43,7 @@ interface UserChangedSelectedEvent { interface AppRequestedResolverData { readonly type: 'appRequestedResolverData'; } + /** * When the user switches the active descendent of the Resolver. */ @@ -50,7 +51,7 @@ interface UserFocusedOnResolverNode { readonly type: 'userFocusedOnResolverNode'; readonly payload: { /** - * Used to identify the process node that should be brought into view. + * Used to identify the process node that the user focused on (in the DOM) */ readonly nodeId: string; }; diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/view/defs.tsx b/x-pack/plugins/endpoint/public/embeddables/resolver/view/defs.tsx index 3295ef0289235..911cda1be6517 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/view/defs.tsx +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/view/defs.tsx @@ -13,6 +13,7 @@ import { euiPaletteForStatus, colorPalette, } from '@elastic/eui'; +import styled from 'styled-components'; /** * Generating from `colorPalette` function: This could potentially @@ -396,17 +397,25 @@ const SymbolsAndShapes = memo(() => ( )); /** - * This element is used to define the reusable assets for the Resolver - * It confers sevral advantages, including but not limited to: - * 1) Freedom of form for creative assets (beyond box-model constraints) - * 2) Separation of concerns between creative assets and more functional areas of the app - * 3) elements can be handled by compositor (faster) + * This `` element is used to define the reusable assets for the Resolver + * It confers several advantages, including but not limited to: + * 1. Freedom of form for creative assets (beyond box-model constraints) + * 2. Separation of concerns between creative assets and more functional areas of the app + * 3. `` elements can be handled by compositor (faster) */ -export const SymbolDefinitions = memo(() => ( - - - - - - -)); +export const SymbolDefinitions = styled( + memo(({ className }: { className?: string }) => ( + + + + + + + )) +)` + position: absolute; + left: 100%; + top: 100%; + width: 0; + height: 0; +`; diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/view/index.tsx b/x-pack/plugins/endpoint/public/embeddables/resolver/view/index.tsx index 22e9d05ad98ff..58ce9b963de5d 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/view/index.tsx +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/view/index.tsx @@ -88,15 +88,22 @@ export const Resolver = styled( projectionMatrix={projectionMatrix} /> ))} - {Array.from(processNodePositions).map(([processEvent, position], index) => ( - - ))} + {[...processNodePositions].map(([processEvent, position], index) => { + const adjacentNodeMap = processToAdjacencyMap.get(processEvent); + if (!adjacentNodeMap) { + // This should never happen + throw new Error('Issue calculating adjacency node map.'); + } + return ( + + ); + })} )} diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/view/process_event_dot.tsx b/x-pack/plugins/endpoint/public/embeddables/resolver/view/process_event_dot.tsx index 603521e2d9bb3..b5f8f49877853 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/view/process_event_dot.tsx +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/view/process_event_dot.tsx @@ -82,7 +82,7 @@ export const ProcessEventDot = styled( /** * map of what nodes are "adjacent" to this one in "up, down, previous, next" directions */ - adjacentNodeMap?: AdjacentProcessMap; + adjacentNodeMap: AdjacentProcessMap; }) => { /** * Convert the position, which is in 'world' coordinates, to screen coordinates. @@ -91,7 +91,7 @@ export const ProcessEventDot = styled( const [magFactorX] = projectionMatrix; - const selfId = adjacentNodeMap?.self; + const selfId = adjacentNodeMap.self; const nodeViewportStyle = useMemo( () => ({ @@ -117,27 +117,31 @@ export const ProcessEventDot = styled( const labelYHeight = markerSize / 1.7647; - const levelAttribute = adjacentNodeMap?.level - ? { - 'aria-level': adjacentNodeMap.level, - } - : {}; - - const flowToAttribute = adjacentNodeMap?.nextSibling - ? { - 'aria-flowto': adjacentNodeMap.nextSibling, - } - : {}; + /** + * An element that should be animated when the node is clicked. + */ + const animationTarget: { + current: + | (SVGAnimationElement & { + /** + * `beginElement` is by [w3](https://www.w3.org/TR/SVG11/animate.html#__smil__ElementTimeControl__beginElement) + * but missing in [TSJS-lib-generator](https://github.com/microsoft/TSJS-lib-generator/blob/15a4678e0ef6de308e79451503e444e9949ee849/inputfiles/addedTypes.json#L1819) + */ + beginElement: () => void; + }) + | null; + } = React.createRef(); + const { cubeSymbol, labelFill, descriptionFill, descriptionText } = nodeAssets[ + nodeType(event) + ]; + const resolverNodeIdGenerator = useMemo(() => htmlIdGenerator('resolverNode'), []); - const nodeType = getNodeType(event); - const clickTargetRef: { current: SVGAnimationElement | null } = React.createRef(); - const { cubeSymbol, labelFill, descriptionFill, descriptionText } = nodeAssets[nodeType]; - const resolverNodeIdGenerator = htmlIdGenerator('resolverNode'); - const [nodeId, labelId, descriptionId] = [ - !!selfId ? resolverNodeIdGenerator(String(selfId)) : resolverNodeIdGenerator(), - resolverNodeIdGenerator(), - resolverNodeIdGenerator(), - ] as string[]; + const nodeId = useMemo(() => resolverNodeIdGenerator(selfId), [ + resolverNodeIdGenerator, + selfId, + ]); + const labelId = useMemo(() => resolverNodeIdGenerator(), [resolverNodeIdGenerator]); + const descriptionId = useMemo(() => resolverNodeIdGenerator(), [resolverNodeIdGenerator]); const dispatch = useResolverDispatch(); @@ -154,14 +158,11 @@ export const ProcessEventDot = styled( [dispatch, nodeId] ); - const handleClick = useCallback( - (clickEvent: React.MouseEvent) => { - if (clickTargetRef.current !== null) { - (clickTargetRef.current as any).beginElement(); - } - }, - [clickTargetRef] - ); + const handleClick = useCallback(() => { + if (animationTarget.current !== null) { + animationTarget.current.beginElement(); + } + }, [animationTarget]); return ( @@ -171,8 +172,10 @@ export const ProcessEventDot = styled( viewBox="-15 -15 90 30" preserveAspectRatio="xMidYMid meet" role="treeitem" - {...levelAttribute} - {...flowToAttribute} + aria-level={adjacentNodeMap.level} + aria-flowto={ + adjacentNodeMap.nextSibling === null ? undefined : adjacentNodeMap.nextSibling + } aria-labelledby={labelId} aria-describedby={descriptionId} aria-haspopup={'true'} @@ -202,7 +205,7 @@ export const ProcessEventDot = styled( begin="click" repeatCount="1" className="squish" - ref={clickTargetRef} + ref={animationTarget} /> = unknownEvent: 'runningProcessCube', }; -function getNodeType(processEvent: ResolverEvent): keyof typeof nodeAssets { +function nodeType(processEvent: ResolverEvent): keyof typeof nodeAssets { const processType = processModel.eventType(processEvent); if (processType in processTypeToCube) {