Resolver/nodedesign 25 (#60630)

* PR base

* adds designed resolver nodes

* adjust distance between nodes

* WIP remove stroke

* WIP changes to meet mocks

* new boxes

* remove animation

* new box assets

* baby resolver running nodes complete

* cleanup defs, add running trigger cube

* added 2 more defs for process cubes

* adding switched for assets on node component

* vacuuming defs file

* adjusting types and references to new event model

* switch background to full shade for contrast

* switch background to full shade for contrast

* cube, animation and a11y changes to 25% nodes

* PR base

* adds designed resolver nodes

* adjust distance between nodes

* WIP remove stroke

* WIP changes to meet mocks

* new boxes

* remove animation

* new box assets

* baby resolver running nodes complete

* cleanup defs, add running trigger cube

* added 2 more defs for process cubes

* adding switched for assets on node component

* vacuuming defs file

* adjusting types and references to new event model

* switch background to full shade for contrast

* cube, animation and a11y changes to 25% nodes

* merge upstream

* change from Legacy to new Resolver event

* cleaning up unused styles

* fix adjacency map issues

* fix process type to cube mapping

* fix typing on selctor

* set viewport to strict

* remove unused types

* fixes ci / testing issues

* feedback from Jon Buttner

* fix index from Jon Buttner comment

* reset focus state on nodes

* Robert review: changing adjacency map property names for better semantics

* Robert Austin review: changing var name

* Robert Austin review: rearrange code for readability

* Robert Austin review: change const name

* Robert Austin review: rearranging code for readability

* Robert Austin review: adjustments to process_event_dot

* Robert Austin review: replace level getter

* Robert Austin review: removing unnecessary casting

* Robert Austin review: adjust selector

* Robert Austin review: fix setting parent map

* Robert Austin review: replace function with consts

* K Qualters review: change return type of function

Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
This commit is contained in:
Brent Kimmel 2020-03-23 17:17:52 -04:00 committed by GitHub
parent a0a85dbb90
commit dd93a14fef
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 940 additions and 170 deletions

View file

@ -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<string | undefined, ResolverEvent[]>();
const idToValue = new Map<string, ResolverEvent>();
const idToAdjacent = new Map<string, AdjacentProcessMap>();
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;
}
/**

View file

@ -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;

View file

@ -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,
],
},
}

View file

@ -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<ResolverEvent, AdjacentProcessMap>();
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.
*/

View file

@ -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<ResolverUIState, ResolverAction> = (
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<ResolverState, ResolverAction> = (state, action) => {

View file

@ -54,6 +54,11 @@ export const processNodePositionsAndEdgeLineSegments = composeSelectors(
dataSelectors.processNodePositionsAndEdgeLineSegments
);
export const processAdjacencies = composeSelectors(
dataStateSelector,
dataSelectors.processAdjacencies
);
/**
* Returns the camera state from within ResolverState
*/

View file

@ -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<string, ResolverEvent>;
/**
* Map of ID to adjacent processes
*/
idToAdjacent: Map<string, AdjacentProcessMap>;
}
/**

View file

@ -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<string, string | string[]> = {
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<ResolverColorNames, string> = {
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(() => (
<>
<linearGradient
id={PaintServerIds.runningProcess}
x1="0"
y1="0"
x2="1"
y2="0"
spreadMethod="reflect"
gradientUnits="objectBoundingBox"
>
<stop
offset="0%"
stopColor={saturate(0.7, lighten(0.05, NamedColors.runningProcessStart))}
stopOpacity="1"
/>
<stop
offset="100%"
stopColor={saturate(0.7, lighten(0.05, NamedColors.runningProcessEnd))}
stopOpacity="1"
/>
</linearGradient>
<linearGradient
id={PaintServerIds.runningTrigger}
x1="0"
y1="0"
x2="1"
y2="0"
spreadMethod="reflect"
gradientUnits="objectBoundingBox"
>
<stop
offset="0%"
stopColor={saturate(0.7, lighten(0.05, NamedColors.runningTriggerStart))}
stopOpacity="1"
/>
<stop
offset="100%"
stopColor={saturate(0.7, lighten(0.05, NamedColors.runningTriggerEnd))}
stopOpacity="1"
/>
</linearGradient>
<linearGradient
id={PaintServerIds.runningProcessCube}
x1="-382.33074"
y1="265.24689"
x2="-381.88086"
y2="264.46019"
gradientTransform="matrix(88, 0, 0, -100, 33669, 26535)"
gradientUnits="userSpaceOnUse"
>
<stop offset="0" stopColor={NamedColors.runningProcessStart} />
<stop offset="1" stopColor={NamedColors.runningProcessEnd} />
</linearGradient>
<linearGradient
id={PaintServerIds.runningTriggerCube}
x1="-382.32713"
y1="265.24057"
x2="-381.88108"
y2="264.46057"
gradientTransform="matrix(88, 0, 0, -100, 33669, 26535)"
gradientUnits="userSpaceOnUse"
>
<stop offset="0" stopColor="#bd281f" />
<stop offset="1" stopColor="#dc0b72" />
</linearGradient>
<linearGradient
id={PaintServerIds.terminatedProcessCube}
x1="-382.33074"
y1="265.24689"
x2="-381.88086"
y2="264.46019"
gradientTransform="matrix(88, 0, 0, -100, 33669, 26535)"
gradientUnits="userSpaceOnUse"
>
<stop offset="0" stopColor="#006bb4" />
<stop offset="1" stopColor="#017d73" />
</linearGradient>
<linearGradient
id={PaintServerIds.terminatedTriggerCube}
x1="-382.33074"
y1="265.24689"
x2="-381.88086"
y2="264.46019"
gradientTransform="matrix(88, 0, 0, -100, 33669, 26535)"
gradientUnits="userSpaceOnUse"
>
<stop offset="0" stopColor="#be2820" />
<stop offset="1" stopColor="#dc0b72" />
</linearGradient>
</>
));
/**
* Ids of symbols to be linked by <use> 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(() => (
<>
<symbol id={SymbolIds.processNode} viewBox="0 0 144 25" preserveAspectRatio="xMidYMid meet">
<rect
x="1"
y="1"
width="142"
height="23"
fill="inherit"
strokeWidth="0"
paintOrder="normal"
/>
</symbol>
<symbol id={SymbolIds.solidHexagon} viewBox="0 0 200 200" preserveAspectRatio="xMidYMid meet">
<g transform="translate(0,-97)">
<path
transform="matrix(1.6461 0 0 1.6596 -56.401 -64.183)"
d="m95.148 97.617 28.238 16.221 23.609 13.713 0.071 32.566-0.071 27.302-28.167 16.344-23.68 13.59-28.238-16.221-23.609-13.713-0.07098-32.566 0.07098-27.302 28.167-16.344z"
fill="inherit"
strokeWidth="15"
stroke="inherit"
/>
</g>
</symbol>
<symbol id={SymbolIds.runningProcessCube} viewBox="0 0 88 100">
<title>Running Process</title>
<g>
<polygon
points="0 25.839 44.23 0 88 25.839 88 74.688 44.23 100 0 74.688 0 25.839"
fill="#1d1e24"
/>
<polygon
points="44.23 100 44 50 0 25.839 0 74.664 44.23 100"
opacity="0.25179"
style={{ isolation: 'isolate' }}
/>
<polygon
points="27 41.077 44.089 31 61 41.077 61 60.128 44.089 70 27 60.128 27 41.077"
fill="#fff"
/>
<polygon points="44 31 61 41.077 61 60.128 44 50 44 31" fill="#d8d8d8" />
<polygon points="27 60.128 27 41.077 44 31 44 50 27 60.128" fill="#959595" />
<polygon
points="0 25.839 44.23 0 88 25.839 88 74.688 44.23 100 0 74.688 0 25.839"
opacity="0.74744"
fill={`url(#${PaintServerIds.runningProcessCube})`}
style={{ isolation: 'isolate' }}
/>
<polygon
points="88 25.839 44.23 0 0 25.839 44 50 88 25.839"
fill="#fff"
opacity="0.13893"
style={{ isolation: 'isolate' }}
/>
<polygon
points="44.23 100 44 50 0 25.839 0 74.664 44.23 100"
opacity="0.25179"
style={{ isolation: 'isolate' }}
/>
</g>
</symbol>
<symbol id={SymbolIds.runningTriggerCube} viewBox="0 0 88 100">
<title>Running Trigger Process</title>
<g>
<polygon
points="0 25.839 44.23 0 88 25.839 88 74.688 44.23 100 0 74.688 0 25.839"
fill="#1d1e24"
/>
<polygon
points="44.23 100 44 50 0 25.839 0 74.664 44.23 100"
opacity="0.25179"
style={{ isolation: 'isolate' }}
/>
<polygon
points="27 41.077 44.089 31 61 41.077 61 60.128 44.089 70 27 60.128 27 41.077"
fill="#fff"
/>
<polygon points="44 31 61 41.077 61 60.128 44 50 44 31" fill="#d8d8d8" />
<polygon points="27 60.128 27 41.077 44 31 44 50 27 60.128" fill="#959595" />
<polygon
points="0 25.839 44.23 0 88 25.839 88 74.688 44.23 100 0 74.688 0 25.839"
opacity="0.75"
fill={`url(#${PaintServerIds.runningTriggerCube})`}
style={{ isolation: 'isolate' }}
/>
<polygon
points="88 25.839 44.23 0 0 25.839 44 50 88 25.839"
fill="#fff"
opacity="0.13893"
style={{ isolation: 'isolate' }}
/>
<polygon
points="44.23 100 44 50 0 25.839 0 74.664 44.23 100"
opacity="0.25179"
style={{ isolation: 'isolate' }}
/>
</g>
</symbol>
<symbol viewBox="0 0 88 100" id={SymbolIds.terminatedProcessCube}>
<title>Terminated Process</title>
<g>
<polygon
points="0 25.839 44.23 0 88 25.839 88 74.688 44.23 100 0 74.688 0 25.839"
fill="#1d1e24"
/>
<polygon
points="44.23 100 44 50 0 25.839 0 74.664 44.23 100"
opacity="0.25179"
style={{ isolation: 'isolate' }}
/>
<polygon
id="Path-4-Copy-15"
points="0 25.839 44.23 0 88 25.839 88 74.688 44.23 100 0 74.688 0 25.839"
opacity="0.35"
fill={`url(#${PaintServerIds.terminatedProcessCube})`}
style={{ isolation: 'isolate' }}
/>
<polygon
id="Path-Copy-20"
points="88 25.839 44.23 0 0 25.839 44 50 88 25.839"
fill="#fff"
opacity="0.13893"
style={{ isolation: 'isolate' }}
/>
<polygon
id="Path-Copy-21"
points="44.23 100 44 50 0 25.839 0 74.664 44.23 100"
opacity="0.25179"
style={{ isolation: 'isolate' }}
/>
</g>
</symbol>
<svg id={SymbolIds.terminatedTriggerCube} viewBox="0 0 88 100">
<title>Terminated Trigger Process</title>
<g>
<polygon
points="0 25.839 44.23 0 88 25.839 88 74.688 44.23 100 0 74.688 0 25.839"
fill="#1d1e24"
/>
<polygon
points="44.23 100 44 50 0 25.839 0 74.664 44.23 100"
opacity="0.25179"
style={{ isolation: 'isolate' }}
/>
<polygon
points="0 25.839 44.23 0 88 25.839 88 74.688 44.23 100 0 74.688 0 25.839"
opacity="0.35"
fill={`url(#${PaintServerIds.terminatedTriggerCube})`}
style={{ isolation: 'isolate' }}
/>
<polygon
points="88 25.839 44.23 0 0 25.839 44 50 88 25.839"
fill="#fff"
opacity="0.13893"
style={{ isolation: 'isolate' }}
/>
<polygon
points="44.23 100 44 50 0 25.839 0 74.664 44.23 100"
opacity="0.25179"
style={{ isolation: 'isolate' }}
/>
</g>
</svg>
</>
));
/**
* This <defs> 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) <use> elements can be handled by compositor (faster)
*/
export const SymbolDefinitions = memo(() => (
<svg>
<defs>
<PaintServers />
<SymbolsAndShapes />
</defs>
</svg>
));

View file

@ -66,7 +66,7 @@ export const EdgeLine = styled(
*/
transform: `translateY(-50%) rotateZ(${angle(screenStart, screenEnd)}rad)`,
};
return <div className={className} style={style} />;
return <div role="presentation" className={className} style={style} />;
}
)
)`
@ -74,4 +74,5 @@ export const EdgeLine = styled(
height: 3px;
background-color: #d4d4d4;
color: #333333;
contain: strict;
`;

View file

@ -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(
<EuiLoadingSpinner size="xl" />
</div>
) : (
<>
<div className="resolver-graph" onMouseDown={onMouseDown} ref={ref}>
{Array.from(processNodePositions).map(([processEvent, position], index) => (
<ProcessEventDot
key={index}
position={position}
projectionMatrix={projectionMatrix}
event={processEvent}
/>
))}
{edgeLineSegments.map(([startPosition, endPosition], index) => (
<EdgeLine
key={index}
startPosition={startPosition}
endPosition={endPosition}
projectionMatrix={projectionMatrix}
/>
))}
</div>
<StyledPanel />
<StyledGraphControls />
</>
<StyledResolverContainer
className="resolver-graph kbn-resetFocusState"
onMouseDown={onMouseDown}
ref={ref}
role="tree"
tabIndex={0}
>
{edgeLineSegments.map(([startPosition, endPosition], index) => (
<EdgeLine
key={index}
startPosition={startPosition}
endPosition={endPosition}
projectionMatrix={projectionMatrix}
/>
))}
{Array.from(processNodePositions).map(([processEvent, position], index) => (
<ProcessEventDot
key={index}
position={position}
projectionMatrix={projectionMatrix}
event={processEvent}
adjacentNodeMap={processToAdjacencyMap.get(processEvent)}
/>
))}
</StyledResolverContainer>
)}
<StyledPanel />
<StyledGraphControls />
<SymbolDefinitions />
</div>
);
})
@ -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};
`;

View file

@ -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<SVGSVGElement>) => {
dispatch({
type: 'userFocusedOnResolverNode',
payload: {
nodeId,
},
});
focusEvent.currentTarget.setAttribute('aria-current', 'true');
},
[dispatch, nodeId]
);
const handleClick = useCallback(
(clickEvent: React.MouseEvent<SVGSVGElement, MouseEvent>) => {
if (clickTargetRef.current !== null) {
(clickTargetRef.current as any).beginElement();
}
},
[clickTargetRef]
);
return (
<span className={className} style={style} data-test-subj={'resolverNode'}>
name: {eventModel.eventName(event)}
<br />
x: {position[0]}
<br />
y: {position[1]}
</span>
<EuiKeyboardAccessible>
<svg
data-test-subj={'resolverNode'}
className={className + ' kbn-resetFocusState'}
viewBox="-15 -15 90 30"
preserveAspectRatio="xMidYMid meet"
role="treeitem"
{...levelAttribute}
{...flowToAttribute}
aria-labelledby={labelId}
aria-describedby={descriptionId}
aria-haspopup={'true'}
style={nodeViewportStyle}
id={nodeId}
onClick={handleClick}
onFocus={handleFocus}
tabIndex={-1}
>
<g>
<use
role="presentation"
xlinkHref={cubeSymbol}
x={markerPositionOffset}
y={markerPositionOffset}
width={markerSize}
height={markerSize}
opacity="1"
className="cube"
>
<animateTransform
attributeType="XML"
attributeName="transform"
type="scale"
values="1 1; 1 .83; 1 .8; 1 .83; 1 1"
dur="0.2s"
begin="click"
repeatCount="1"
className="squish"
ref={clickTargetRef}
/>
</use>
<use
role="presentation"
xlinkHref={`#${SymbolIds.processNode}`}
x={markerPositionOffset + markerSize - 0.5}
y={labelYOffset}
width={(markerSize / 1.7647) * 5}
height={markerSize / 1.7647}
opacity="1"
fill={labelFill}
/>
<text
x={markerPositionOffset + 0.7 * markerSize + 50 / 2}
y={labelYOffset + labelYHeight / 2}
textAnchor="middle"
dominantBaseline="middle"
fontSize="3.75"
fontWeight="bold"
fill={NamedColors.empty}
paintOrder="stroke"
tabIndex={-1}
style={{ letterSpacing: '-0.02px' }}
id={labelId}
>
{eventModel.eventName(event)}
</text>
<text
x={markerPositionOffset + markerSize}
y={labelYOffset - 1}
textAnchor="start"
dominantBaseline="middle"
fontSize="2.67"
fill={descriptionFill}
id={descriptionId}
paintOrder="stroke"
fontWeight="bold"
style={{ textTransform: 'uppercase', letterSpacing: '-0.01px' }}
>
{descriptionText}
</text>
</g>
</svg>
</EuiKeyboardAccessible>
);
}
)
)`
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<ResolverProcessType, keyof typeof nodeAssets> = {
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';
}