diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/models/indexed_process_tree.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/models/indexed_process_tree.ts index c9a03f0a4796..967a2c10f14c 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/models/indexed_process_tree.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/models/indexed_process_tree.ts @@ -5,7 +5,7 @@ */ import { uniquePidForProcess, uniqueParentPidForProcess } from './process_event'; -import { IndexedProcessTree } from '../types'; +import { IndexedProcessTree, AdjacentProcessMap } from '../types'; import { ResolverEvent } from '../../../../common/types'; import { levelOrder as baseLevelOrder } from '../lib/tree_sequencers'; @@ -15,21 +15,89 @@ import { levelOrder as baseLevelOrder } from '../lib/tree_sequencers'; export function factory(processes: ResolverEvent[]): IndexedProcessTree { const idToChildren = new Map(); const idToValue = new Map(); + const idToAdjacent = new Map(); + + function emptyAdjacencyMap(id: string): AdjacentProcessMap { + return { + self: id, + parent: null, + firstChild: null, + previousSibling: null, + nextSibling: null, + level: 1, + }; + } + + const roots: ResolverEvent[] = []; for (const process of processes) { - idToValue.set(uniquePidForProcess(process), process); + const uniqueProcessPid = uniquePidForProcess(process); + idToValue.set(uniqueProcessPid, process); + + const currentProcessAdjacencyMap: AdjacentProcessMap = + idToAdjacent.get(uniqueProcessPid) || emptyAdjacencyMap(uniqueProcessPid); + idToAdjacent.set(uniqueProcessPid, currentProcessAdjacencyMap); + const uniqueParentPid = uniqueParentPidForProcess(process); - const processChildren = idToChildren.get(uniqueParentPid); - if (processChildren) { - processChildren.push(process); + const currentProcessSiblings = idToChildren.get(uniqueParentPid); + + if (currentProcessSiblings) { + const previousProcessId = uniquePidForProcess( + currentProcessSiblings[currentProcessSiblings.length - 1] + ); + currentProcessSiblings.push(process); + /** + * Update adjacency maps for current and previous entries + */ + idToAdjacent.get(previousProcessId)!.nextSibling = uniqueProcessPid; + currentProcessAdjacencyMap.previousSibling = previousProcessId; + if (uniqueParentPid) { + currentProcessAdjacencyMap.parent = uniqueParentPid; + } } else { idToChildren.set(uniqueParentPid, [process]); + + if (uniqueParentPid) { + /** + * Get the parent's map, otherwise set an empty one + */ + const parentAdjacencyMap = + idToAdjacent.get(uniqueParentPid) || + (idToAdjacent.set(uniqueParentPid, emptyAdjacencyMap(uniqueParentPid)), + idToAdjacent.get(uniqueParentPid))!; + // set firstChild for parent + parentAdjacencyMap.firstChild = uniqueProcessPid; + // set parent for current + currentProcessAdjacencyMap.parent = uniqueParentPid || null; + } else { + // In this case (no unique parent id), it must be a root + roots.push(process); + } } } + /** + * Scan adjacency maps from the top down and assign levels + */ + function traverseLevels(currentProcessMap: AdjacentProcessMap, level: number = 1): void { + const nextLevel = level + 1; + if (currentProcessMap.nextSibling) { + traverseLevels(idToAdjacent.get(currentProcessMap.nextSibling)!, level); + } + if (currentProcessMap.firstChild) { + traverseLevels(idToAdjacent.get(currentProcessMap.firstChild)!, nextLevel); + } + currentProcessMap.level = level; + } + + for (const treeRoot of roots) { + traverseLevels(idToAdjacent.get(uniquePidForProcess(treeRoot))!); + } + return { idToChildren, idToProcess: idToValue, + idToAdjacent, }; } @@ -38,8 +106,8 @@ export function factory(processes: ResolverEvent[]): IndexedProcessTree { */ export function children(tree: IndexedProcessTree, process: ResolverEvent): ResolverEvent[] { const id = uniquePidForProcess(process); - const processChildren = tree.idToChildren.get(id); - return processChildren === undefined ? [] : processChildren; + const currentProcessSiblings = tree.idToChildren.get(id); + return currentProcessSiblings === undefined ? [] : currentProcessSiblings; } /** 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 fec2078cc60c..ceb5da2ca909 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/store/actions.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/store/actions.ts @@ -43,10 +43,23 @@ interface UserChangedSelectedEvent { interface AppRequestedResolverData { readonly type: 'appRequestedResolverData'; } +/** + * When the user switches the active descendent of the Resolver. + */ +interface UserFocusedOnResolverNode { + readonly type: 'userFocusedOnResolverNode'; + readonly payload: { + /** + * Used to identify the process node that should be brought into view. + */ + readonly nodeId: string; + }; +} export type ResolverAction = | CameraAction | DataAction | UserBroughtProcessIntoView | UserChangedSelectedEvent - | AppRequestedResolverData; + | AppRequestedResolverData + | UserFocusedOnResolverNode; diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/__snapshots__/graphing.test.ts.snap b/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/__snapshots__/graphing.test.ts.snap index b88652097eb5..00abc27b25a8 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/__snapshots__/graphing.test.ts.snap +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/__snapshots__/graphing.test.ts.snap @@ -40,129 +40,129 @@ Object { 0, -0.8164965809277259, ], - Array [ - 35.35533905932738, - -21.228911104120876, - ], - ], - Array [ - Array [ - -35.35533905932738, - -62.053740150507174, - ], - Array [ - 106.06601717798213, - 19.595917942265423, - ], - ], - Array [ - Array [ - -35.35533905932738, - -62.053740150507174, - ], - Array [ - 0, - -82.46615467370032, - ], - ], - Array [ - Array [ - 106.06601717798213, - 19.595917942265423, - ], - Array [ - 141.4213562373095, - -0.8164965809277259, - ], - ], - Array [ - Array [ - 0, - -82.46615467370032, - ], - Array [ - 35.35533905932738, - -102.87856919689347, - ], - ], - Array [ - Array [ - 0, - -123.2909837200866, - ], Array [ 70.71067811865476, - -82.46615467370032, + -41.641325627314025, ], ], Array [ Array [ - 0, - -123.2909837200866, - ], - Array [ - 35.35533905932738, - -143.70339824327976, - ], - ], - Array [ - Array [ - 70.71067811865476, - -82.46615467370032, - ], - Array [ - 106.06601717798213, - -102.87856919689347, - ], - ], - Array [ - Array [ - 141.4213562373095, - -0.8164965809277259, - ], - Array [ - 176.7766952966369, - -21.22891110412087, - ], - ], - Array [ - Array [ - 141.4213562373095, - -41.64132562731402, + -70.71067811865476, + -123.29098372008661, ], Array [ 212.13203435596427, - -0.8164965809277259, + 40.00833246545857, ], ], Array [ Array [ - 141.4213562373095, - -41.64132562731402, + -70.71067811865476, + -123.29098372008661, ], Array [ - 176.7766952966369, - -62.053740150507174, + 0, + -164.1158127664729, ], ], Array [ Array [ 212.13203435596427, - -0.8164965809277259, + 40.00833246545857, ], Array [ - 247.48737341529164, - -21.228911104120883, + 282.842712474619, + -0.8164965809277259, ], ], Array [ Array [ - 247.48737341529164, - -21.228911104120883, + 0, + -164.1158127664729, ], Array [ - 318.1980515339464, - -62.05374015050717, + 70.71067811865476, + -204.9406418128592, + ], + ], + Array [ + Array [ + 0, + -245.76547085924548, + ], + Array [ + 141.4213562373095, + -164.1158127664729, + ], + ], + Array [ + Array [ + 0, + -245.76547085924548, + ], + Array [ + 70.71067811865476, + -286.5902999056318, + ], + ], + Array [ + Array [ + 141.4213562373095, + -164.1158127664729, + ], + Array [ + 212.13203435596427, + -204.9406418128592, + ], + ], + Array [ + Array [ + 282.842712474619, + -0.8164965809277259, + ], + Array [ + 353.5533905932738, + -41.64132562731401, + ], + ], + Array [ + Array [ + 282.842712474619, + -82.4661546737003, + ], + Array [ + 424.26406871192853, + -0.8164965809277259, + ], + ], + Array [ + Array [ + 282.842712474619, + -82.4661546737003, + ], + Array [ + 353.5533905932738, + -123.29098372008661, + ], + ], + Array [ + Array [ + 424.26406871192853, + -0.8164965809277259, + ], + Array [ + 494.9747468305833, + -41.64132562731404, + ], + ], + Array [ + Array [ + 494.9747468305833, + -41.64132562731404, + ], + Array [ + 636.3961030678928, + -123.2909837200866, ], ], ], @@ -199,7 +199,7 @@ Object { }, } => Array [ 0, - -82.46615467370032, + -164.1158127664729, ], Object { "@timestamp": 1582233383000, @@ -215,7 +215,7 @@ Object { "unique_ppid": 0, }, } => Array [ - 141.4213562373095, + 282.842712474619, -0.8164965809277259, ], Object { @@ -232,8 +232,8 @@ Object { "unique_ppid": 1, }, } => Array [ - 35.35533905932738, - -143.70339824327976, + 70.71067811865476, + -286.5902999056318, ], Object { "@timestamp": 1582233383000, @@ -249,8 +249,8 @@ Object { "unique_ppid": 1, }, } => Array [ - 106.06601717798213, - -102.87856919689347, + 212.13203435596427, + -204.9406418128592, ], Object { "@timestamp": 1582233383000, @@ -266,8 +266,8 @@ Object { "unique_ppid": 2, }, } => Array [ - 176.7766952966369, - -62.053740150507174, + 353.5533905932738, + -123.29098372008661, ], Object { "@timestamp": 1582233383000, @@ -283,8 +283,8 @@ Object { "unique_ppid": 2, }, } => Array [ - 247.48737341529164, - -21.228911104120883, + 494.9747468305833, + -41.64132562731404, ], Object { "@timestamp": 1582233383000, @@ -300,8 +300,8 @@ Object { "unique_ppid": 6, }, } => Array [ - 318.1980515339464, - -62.05374015050717, + 636.3961030678928, + -123.2909837200866, ], }, } @@ -316,8 +316,8 @@ Object { -0.8164965809277259, ], Array [ - 70.71067811865476, - -41.641325627314025, + 141.4213562373095, + -82.46615467370032, ], ], ], @@ -353,8 +353,8 @@ Object { "unique_ppid": 0, }, } => Array [ - 70.71067811865476, - -41.641325627314025, + 141.4213562373095, + -82.46615467370032, ], }, } diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/selectors.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/selectors.ts index e8007f82e30c..5dda54d4ed02 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/selectors.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/selectors.ts @@ -13,11 +13,12 @@ import { EdgeLineSegment, ProcessWithWidthMetadata, Matrix3, + AdjacentProcessMap, } from '../../types'; import { ResolverEvent } from '../../../../../common/types'; import { Vector2 } from '../../types'; import { add as vector2Add, applyMatrix3 } from '../../lib/vector2'; -import { isGraphableProcess } from '../../models/process_event'; +import { isGraphableProcess, uniquePidForProcess } from '../../models/process_event'; import { factory as indexedProcessTreeFactory, children as indexedProcessTreeChildren, @@ -27,7 +28,7 @@ import { } from '../../models/indexed_process_tree'; const unit = 100; -const distanceBetweenNodesInUnits = 1; +const distanceBetweenNodesInUnits = 2; export function isLoading(state: DataState) { return state.isLoading; @@ -392,17 +393,42 @@ function processPositions( return positions; } -export const processNodePositionsAndEdgeLineSegments = createSelector( +export const indexedProcessTree = createSelector(graphableProcesses, function indexedTree( + /* eslint-disable no-shadow */ + graphableProcesses + /* eslint-enable no-shadow */ +) { + return indexedProcessTreeFactory(graphableProcesses); +}); + +export const processAdjacencies = createSelector( + indexedProcessTree, graphableProcesses, - function processNodePositionsAndEdgeLineSegments( + function selectProcessAdjacencies( /* eslint-disable no-shadow */ + indexedProcessTree, graphableProcesses /* eslint-enable no-shadow */ ) { - /** - * Index the tree, creating maps from id -> node and id -> children - */ - const indexedProcessTree = indexedProcessTreeFactory(graphableProcesses); + const processToAdjacencyMap = new Map(); + const { idToAdjacent } = indexedProcessTree; + + for (const graphableProcess of graphableProcesses) { + const processPid = uniquePidForProcess(graphableProcess); + const adjacencyMap = idToAdjacent.get(processPid)!; + processToAdjacencyMap.set(graphableProcess, adjacencyMap); + } + return { processToAdjacencyMap }; + } +); + +export const processNodePositionsAndEdgeLineSegments = createSelector( + indexedProcessTree, + function processNodePositionsAndEdgeLineSegments( + /* eslint-disable no-shadow */ + indexedProcessTree + /* eslint-enable no-shadow */ + ) { /** * Walk the tree in reverse level order, calculating the 'width' of subtrees. */ diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/store/reducer.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/store/reducer.ts index 20c490b8998f..1c66a998a4c2 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/store/reducer.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/store/reducer.ts @@ -7,11 +7,25 @@ import { Reducer, combineReducers } from 'redux'; import { animateProcessIntoView } from './methods'; import { cameraReducer } from './camera/reducer'; import { dataReducer } from './data/reducer'; -import { ResolverState, ResolverAction } from '../types'; +import { ResolverState, ResolverAction, ResolverUIState } from '../types'; + +const uiReducer: Reducer = ( + uiState = { activeDescendentId: null }, + action +) => { + if (action.type === 'userFocusedOnResolverNode') { + return { + activeDescendentId: action.payload.nodeId, + }; + } else { + return uiState; + } +}; const concernReducers = combineReducers({ camera: cameraReducer, data: dataReducer, + ui: uiReducer, }); export const resolverReducer: Reducer = (state, action) => { diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/store/selectors.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/store/selectors.ts index 708eb684ebd3..37482916496e 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/store/selectors.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/store/selectors.ts @@ -54,6 +54,11 @@ export const processNodePositionsAndEdgeLineSegments = composeSelectors( dataSelectors.processNodePositionsAndEdgeLineSegments ); +export const processAdjacencies = composeSelectors( + dataStateSelector, + dataSelectors.processAdjacencies +); + /** * Returns the camera state from within ResolverState */ diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/types.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/types.ts index 4380d3ab9899..674553aba093 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/types.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/types.ts @@ -23,6 +23,21 @@ export interface ResolverState { * Contains the state associated with event data (process events and possibly other event types). */ readonly data: DataState; + + /** + * Contains the state needed to maintain Resolver UI elements. + */ + readonly ui: ResolverUIState; +} + +/** + * Piece of redux state that models an animation for the camera. + */ +export interface ResolverUIState { + /** + * The ID attribute of the resolver's aria-activedescendent. + */ + readonly activeDescendentId: string | null; } /** @@ -174,9 +189,26 @@ export interface ProcessEvent { source_id?: number; process_name: string; process_path: string; + signature_status?: string; }; } +/** + * A map of Process Ids that indicate which processes are adjacent to a given process along + * directions in two axes: up/down and previous/next. + */ +export interface AdjacentProcessMap { + readonly self: string; + parent: string | null; + firstChild: string | null; + previousSibling: string | null; + nextSibling: string | null; + /** + * To support aria-level, this must be >= 1 + */ + level: number; +} + /** * A represention of a process tree with indices for O(1) access to children and values by id. */ @@ -189,6 +221,10 @@ export interface IndexedProcessTree { * Map of ID to process */ idToProcess: Map; + /** + * Map of ID to adjacent processes + */ + idToAdjacent: Map; } /** diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/view/defs.tsx b/x-pack/plugins/endpoint/public/embeddables/resolver/view/defs.tsx new file mode 100644 index 000000000000..799f67123ba2 --- /dev/null +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/view/defs.tsx @@ -0,0 +1,381 @@ +/* + * 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 React, { memo } from 'react'; +import { saturate, lighten } from 'polished'; + +import { + htmlIdGenerator, + euiPaletteForTemperature, + euiPaletteForStatus, + colorPalette, +} from '@elastic/eui'; + +/** + * Generating from `colorPalette` function: This could potentially + * pick up a palette shift and decouple from raw hex + */ +const [euiColorEmptyShade, , , , , euiColor85Shade, euiColorFullShade] = colorPalette( + ['#ffffff', '#000000'], + 7 +); + +/** + * Base Colors - sourced from EUI + */ +const resolverPalette: Record = { + temperatures: euiPaletteForTemperature(7), + statii: euiPaletteForStatus(7), + fullShade: euiColorFullShade, + emptyShade: euiColorEmptyShade, +}; + +/** + * Defines colors by semantics like so: + * `danger`, `attention`, `enabled`, `disabled` + * Or by function like: + * `colorBlindBackground`, `subMenuForeground` + */ +type ResolverColorNames = + | 'ok' + | 'empty' + | 'full' + | 'warning' + | 'strokeBehindEmpty' + | 'resolverBackground' + | 'runningProcessStart' + | 'runningProcessEnd' + | 'runningTriggerStart' + | 'runningTriggerEnd' + | 'activeNoWarning' + | 'activeWarning' + | 'fullLabelBackground' + | 'inertDescription'; + +export const NamedColors: Record = { + ok: saturate(0.5, resolverPalette.temperatures[0]), + empty: euiColorEmptyShade, + full: euiColorFullShade, + strokeBehindEmpty: euiColor85Shade, + warning: resolverPalette.statii[3], + resolverBackground: euiColorFullShade, + runningProcessStart: '#006BB4', + runningProcessEnd: '#017D73', + runningTriggerStart: '#BD281E', + runningTriggerEnd: '#DD0A73', + activeNoWarning: '#0078FF', + activeWarning: '#C61F38', + fullLabelBackground: '#3B3C41', + inertDescription: '#747474', +}; + +const idGenerator = htmlIdGenerator(); + +/** + * Ids of paint servers to be referenced by fill and stroke attributes + */ +export const PaintServerIds = { + runningProcess: idGenerator('psRunningProcess'), + runningTrigger: idGenerator('psRunningTrigger'), + runningProcessCube: idGenerator('psRunningProcessCube'), + runningTriggerCube: idGenerator('psRunningTriggerCube'), + terminatedProcessCube: idGenerator('psTerminatedProcessCube'), + terminatedTriggerCube: idGenerator('psTerminatedTriggerCube'), +}; + +/** + * PaintServers: Where color palettes, grandients, patterns and other similar concerns + * are exposed to the component + */ +const PaintServers = memo(() => ( + <> + + + + + + + + + + + + + + + + + + + + + + + + + +)); + +/** + * Ids of symbols to be linked by elements + */ +export const SymbolIds = { + processNode: idGenerator('nodeSymbol'), + solidHexagon: idGenerator('hexagon'), + runningProcessCube: idGenerator('runningCube'), + runningTriggerCube: idGenerator('runningTriggerCube'), + terminatedProcessCube: idGenerator('terminatedCube'), + terminatedTriggerCube: idGenerator('terminatedTriggerCube'), +}; + +/** + * Defs entries that define shapes, masks and other spatial elements + */ +const SymbolsAndShapes = memo(() => ( + <> + + + + + + + + + + Running Process + + + + + + + + + + + + + Running Trigger Process + + + + + + + + + + + + + Terminated Process + + + + + + + + + + Terminated Trigger Process + + + + + + + + + +)); + +/** + * 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) + */ +export const SymbolDefinitions = memo(() => ( + + + + + + +)); diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/view/edge_line.tsx b/x-pack/plugins/endpoint/public/embeddables/resolver/view/edge_line.tsx index 3386ed4a448d..fbd40dda9adf 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/view/edge_line.tsx +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/view/edge_line.tsx @@ -66,7 +66,7 @@ export const EdgeLine = styled( */ transform: `translateY(-50%) rotateZ(${angle(screenStart, screenEnd)}rad)`, }; - return
; + return
; } ) )` @@ -74,4 +74,5 @@ export const EdgeLine = styled( height: 3px; background-color: #d4d4d4; color: #333333; + contain: strict; `; 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 eab22f993d0a..22e9d05ad98f 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/view/index.tsx +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/view/index.tsx @@ -14,6 +14,7 @@ import { Panel } from './panel'; import { GraphControls } from './graph_controls'; import { ProcessEventDot } from './process_event_dot'; import { useCamera } from './use_camera'; +import { SymbolDefinitions, NamedColors } from './defs'; import { ResolverAction } from '../types'; import { ResolverEvent } from '../../../../common/types'; @@ -33,6 +34,14 @@ const StyledGraphControls = styled(GraphControls)` right: 5px; `; +const StyledResolverContainer = styled.div` + display: flex; + flex-grow: 1; + contain: layout; +`; + +const bgColor = NamedColors.resolverBackground; + export const Resolver = styled( React.memo(function Resolver({ className, @@ -46,6 +55,8 @@ export const Resolver = styled( ); const dispatch: (action: ResolverAction) => unknown = useDispatch(); + const { processToAdjacencyMap } = useSelector(selectors.processAdjacencies); + const { projectionMatrix, ref, onMouseDown } = useCamera(); const isLoading = useSelector(selectors.isLoading); @@ -62,29 +73,35 @@ export const Resolver = styled(
) : ( - <> -
- {Array.from(processNodePositions).map(([processEvent, position], index) => ( - - ))} - {edgeLineSegments.map(([startPosition, endPosition], index) => ( - - ))} -
- - - + + {edgeLineSegments.map(([startPosition, endPosition], index) => ( + + ))} + {Array.from(processNodePositions).map(([processEvent, position], index) => ( + + ))} + )} + + +
); }) @@ -111,4 +128,6 @@ export const Resolver = styled( * Prevent partially visible components from showing up outside the bounds of Resolver. */ overflow: hidden; + contain: strict; + background-color: ${bgColor}; `; 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 2241df97291a..f3086be1598a 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 @@ -4,12 +4,52 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; +import React, { useCallback, useMemo } from 'react'; import styled from 'styled-components'; +import { i18n } from '@kbn/i18n'; +import { htmlIdGenerator, EuiKeyboardAccessible } from '@elastic/eui'; import { applyMatrix3 } from '../lib/vector2'; -import { Vector2, Matrix3 } from '../types'; +import { Vector2, Matrix3, AdjacentProcessMap, ResolverProcessType } from '../types'; +import { SymbolIds, NamedColors, PaintServerIds } from './defs'; import { ResolverEvent } from '../../../../common/types'; +import { useResolverDispatch } from './use_resolver_dispatch'; import * as eventModel from '../../../../common/models/event'; +import * as processModel from '../models/process_event'; + +const nodeAssets = { + runningProcessCube: { + cubeSymbol: `#${SymbolIds.runningProcessCube}`, + labelFill: `url(#${PaintServerIds.runningProcess})`, + descriptionFill: NamedColors.activeNoWarning, + descriptionText: i18n.translate('xpack.endpoint.resolver.runningProcess', { + defaultMessage: 'Running Process', + }), + }, + runningTriggerCube: { + cubeSymbol: `#${SymbolIds.runningTriggerCube}`, + labelFill: `url(#${PaintServerIds.runningTrigger})`, + descriptionFill: NamedColors.activeWarning, + descriptionText: i18n.translate('xpack.endpoint.resolver.runningTrigger', { + defaultMessage: 'Running Trigger', + }), + }, + terminatedProcessCube: { + cubeSymbol: `#${SymbolIds.terminatedProcessCube}`, + labelFill: NamedColors.fullLabelBackground, + descriptionFill: NamedColors.inertDescription, + descriptionText: i18n.translate('xpack.endpoint.resolver.terminatedProcess', { + defaultMessage: 'Terminated Process', + }), + }, + terminatedTriggerCube: { + cubeSymbol: `#${SymbolIds.terminatedTriggerCube}`, + labelFill: NamedColors.fullLabelBackground, + descriptionFill: NamedColors.inertDescription, + descriptionText: i18n.translate('xpack.endpoint.resolver.terminatedTrigger', { + defaultMessage: 'Terminated Trigger', + }), + }, +}; /** * A placeholder view for a process node. @@ -21,6 +61,7 @@ export const ProcessEventDot = styled( position, event, projectionMatrix, + adjacentNodeMap, }: { /** * A `className` string provided by `styled` @@ -38,39 +79,205 @@ export const ProcessEventDot = styled( * projectionMatrix which can be used to convert `position` to screen coordinates. */ projectionMatrix: Matrix3; + /** + * map of what nodes are "adjacent" to this one in "up, down, previous, next" directions + */ + adjacentNodeMap?: AdjacentProcessMap; }) => { /** * Convert the position, which is in 'world' coordinates, to screen coordinates. */ const [left, top] = applyMatrix3(position, projectionMatrix); - const style = { - left: (left - 20).toString() + 'px', - top: (top - 20).toString() + 'px', - }; + + const [magFactorX] = projectionMatrix; + + const selfId = adjacentNodeMap?.self; + + const nodeViewportStyle = useMemo( + () => ({ + left: `${left}px`, + top: `${top}px`, + // Width of symbol viewport scaled to fit + width: `${360 * magFactorX}px`, + // Height according to symbol viewbox AR + height: `${120 * magFactorX}px`, + // Adjusted to position/scale with camera + transform: `translateX(-${0.172413 * 360 * magFactorX + 10}px) translateY(-${0.73684 * + 120 * + magFactorX}px)`, + }), + [left, magFactorX, top] + ); + + const markerBaseSize = 15; + const markerSize = markerBaseSize; + const markerPositionOffset = -markerBaseSize / 2; + + const labelYOffset = markerPositionOffset + 0.25 * markerSize - 0.5; + + const labelYHeight = markerSize / 1.7647; + + const levelAttribute = adjacentNodeMap?.level + ? { + 'aria-level': adjacentNodeMap.level, + } + : {}; + + const flowToAttribute = adjacentNodeMap?.nextSibling + ? { + 'aria-flowto': adjacentNodeMap.nextSibling, + } + : {}; + + 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 dispatch = useResolverDispatch(); + + const handleFocus = useCallback( + (focusEvent: React.FocusEvent) => { + dispatch({ + type: 'userFocusedOnResolverNode', + payload: { + nodeId, + }, + }); + focusEvent.currentTarget.setAttribute('aria-current', 'true'); + }, + [dispatch, nodeId] + ); + + const handleClick = useCallback( + (clickEvent: React.MouseEvent) => { + if (clickTargetRef.current !== null) { + (clickTargetRef.current as any).beginElement(); + } + }, + [clickTargetRef] + ); + return ( - - name: {eventModel.eventName(event)} -
- x: {position[0]} -
- y: {position[1]} -
+ + + + + + + + + {eventModel.eventName(event)} + + + {descriptionText} + + + + ); } ) )` position: absolute; - width: 40px; - height: 40px; + display: block; text-align: left; font-size: 10px; - /** - * Give the element a button-like appearance. - */ user-select: none; - border: 1px solid black; box-sizing: border-box; border-radius: 10%; padding: 4px; white-space: nowrap; + will-change: left, top, width, height; + contain: strict; `; + +const processTypeToCube: Record = { + processCreated: 'terminatedProcessCube', + processRan: 'runningProcessCube', + processTerminated: 'terminatedProcessCube', + unknownProcessEvent: 'runningProcessCube', + processCausedAlert: 'runningTriggerCube', + unknownEvent: 'runningProcessCube', +}; + +function getNodeType(processEvent: ResolverEvent): keyof typeof nodeAssets { + const processType = processModel.eventType(processEvent); + + if (processType in processTypeToCube) { + return processTypeToCube[processType]; + } + return 'runningProcessCube'; +}