From 181e00b908921c3759fa1fafe327cb71481cc607 Mon Sep 17 00:00:00 2001 From: Michael Olorunnisola Date: Mon, 1 Jun 2020 10:12:43 -0400 Subject: [PATCH 01/15] WIP: transition dropdowns and cleanup --- .../public/resolver/store/data/selectors.ts | 41 ++++- .../public/resolver/types.ts | 2 +- .../public/resolver/view/defs.tsx | 155 ++++++++++-------- .../public/resolver/view/edge_line.tsx | 61 ++++++- .../public/resolver/view/graph_controls.tsx | 5 +- .../public/resolver/view/index.tsx | 10 +- .../resolver/view/process_event_dot.tsx | 85 +++------- .../plugins/siem/public/resolver/lib/dates.ts | 68 ++++++++ 8 files changed, 270 insertions(+), 157 deletions(-) create mode 100644 x-pack/plugins/siem/public/resolver/lib/dates.ts diff --git a/x-pack/plugins/security_solution/public/resolver/store/data/selectors.ts b/x-pack/plugins/security_solution/public/resolver/store/data/selectors.ts index 6904107eeebb51..da8c34363d02f9 100644 --- a/x-pack/plugins/security_solution/public/resolver/store/data/selectors.ts +++ b/x-pack/plugins/security_solution/public/resolver/store/data/selectors.ts @@ -17,7 +17,7 @@ import { Vector2, } from '../../types'; import { ResolverEvent } from '../../../../common/endpoint/types'; - +import { eventTimestamp } from '../../../../common/endpoint/models/event'; import { add as vector2Add, applyMatrix3 } from '../../lib/vector2'; import { isGraphableProcess, uniquePidForProcess } from '../../models/process_event'; import { @@ -27,6 +27,7 @@ import { size, levelOrder, } from '../../models/indexed_process_tree'; +import { getRelativeTimeDifference } from '../../lib/dates'; const unit = 100; const distanceBetweenNodesInUnits = 2; @@ -154,6 +155,7 @@ function processEdgeLineSegments( positions: ProcessPositions ): EdgeLineSegment[] { const edgeLineSegments: EdgeLineSegment[] = []; + let elapsedTime: string = ''; for (const metadata of levelOrderWithWidths(indexedProcessTree, widths)) { /** * We only handle children, drawing lines back to their parents. The root has no parent, so we skip it @@ -173,6 +175,12 @@ function processEdgeLineSegments( throw new Error(); } + const parentTime = eventTimestamp(parent); + const processTime = eventTimestamp(process); + if (parentTime && processTime) { + elapsedTime = getRelativeTimeDifference(parentTime, processTime); + } + /** * The point halfway between the parent and child on the y axis, we sometimes have a hard angle here in the edge line */ @@ -188,14 +196,18 @@ function processEdgeLineSegments( * | | * B C */ - const lineFromProcessToMidwayLine: EdgeLineSegment = [[position[0], midwayY], position]; + const lineFromProcessToMidwayLine: EdgeLineSegment = [ + [position[0], midwayY], + position, + elapsedTime, + ]; const siblings = indexedProcessTreeChildren(indexedProcessTree, parent); const isFirstChild = process === siblings[0]; if (metadata.isOnlyChild) { // add a single line segment directly from parent to child. We don't do the 'pitchfork' in this case. - edgeLineSegments.push([parentPosition, position]); + edgeLineSegments.push([parentPosition, position, elapsedTime]); } else if (isFirstChild) { /** * If the parent has multiple children, we draw the 'midway' line, and the line from the @@ -347,7 +359,7 @@ function processPositions( */ positions.set(process, [0, 0]); } else { - const { process, parent, width, parentWidth } = metadata; + const { process, parent, isOnlyChild, width, parentWidth } = metadata; // Reinit counters when parent changes if (lastProcessedParentNode !== parent) { @@ -387,7 +399,14 @@ function processPositions( /** * The y axis gains `-distanceBetweenNodes` as we move down the screen 1 unit at a time. */ - const position = vector2Add([xOffset, -distanceBetweenNodes], parentPosition); + let yDistanceBetweenNodes = -distanceBetweenNodes; + + if (!isOnlyChild) { + // Make space on leaves to show elapsed time + yDistanceBetweenNodes *= 2; + } + + const position = vector2Add([xOffset, yDistanceBetweenNodes], parentPosition); positions.set(process, position); @@ -470,10 +489,14 @@ export const processNodePositionsAndEdgeLineSegments = createSelector( } for (const edgeLineSegment of edgeLineSegments) { - const transformedSegment = []; - for (const point of edgeLineSegment) { - transformedSegment.push(applyMatrix3(point, isometricTransformMatrix)); - } + // const transformedSegment = []; + const [startPoint, endPoint, elapsedTime] = edgeLineSegment; + const transformedSegment: EdgeLineSegment = [ + applyMatrix3(startPoint, isometricTransformMatrix), + applyMatrix3(endPoint, isometricTransformMatrix), + elapsedTime, + ]; + transformedEdgeLineSegments.push(transformedSegment); } diff --git a/x-pack/plugins/security_solution/public/resolver/types.ts b/x-pack/plugins/security_solution/public/resolver/types.ts index ecdd032a6c1d7c..cabe7a7eacec94 100644 --- a/x-pack/plugins/security_solution/public/resolver/types.ts +++ b/x-pack/plugins/security_solution/public/resolver/types.ts @@ -244,7 +244,7 @@ export type ProcessPositions = Map; /** * An array of vectors2 forming an polyline. Used to connect process nodes in the graph. */ -export type EdgeLineSegment = Vector2[]; +export type EdgeLineSegment = [Vector2, Vector2, string?]; /** * Used to provide precalculated info from `widthsOfProcessSubtrees`. These 'width' values are used in the layout of the graph. diff --git a/x-pack/plugins/security_solution/public/resolver/view/defs.tsx b/x-pack/plugins/security_solution/public/resolver/view/defs.tsx index d70ee7bc235875..29f4d43168c0b2 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/defs.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/defs.tsx @@ -5,81 +5,38 @@ */ import React, { memo } from 'react'; -import { saturate } from 'polished'; - -import { - htmlIdGenerator, - euiPaletteForTemperature, - euiPaletteForStatus, - colorPalette, -} from '@elastic/eui'; +import euiThemeAmsterdamDark from '@elastic/eui/dist/eui_theme_amsterdam_dark.json'; +import euiThemeAmsterdamLight from '@elastic/eui/dist/eui_theme_amsterdam_light.json'; +import { htmlIdGenerator, ButtonColor } from '@elastic/eui'; import styled from 'styled-components'; +import { i18n } from '@kbn/i18n'; +import { useUiSetting } from '../../common/lib/kibana'; +import { DEFAULT_DARK_MODE } from '../../../common/constants'; -/** - * 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' + | 'descriptionText' | 'full' - | 'warning' - | 'strokeBehindEmpty' + | 'graphControls' + | 'graphControlsBackground' | 'resolverBackground' - | 'runningProcessStart' - | 'runningProcessEnd' - | 'runningTriggerStart' - | 'runningTriggerEnd' - | 'activeNoWarning' - | 'activeWarning' - | 'fullLabelBackground' - | 'inertDescription' - | 'labelBackgroundTerminatedProcess' - | 'labelBackgroundTerminatedTrigger' - | 'labelBackgroundRunningProcess' - | 'labelBackgroundRunningTrigger'; + | 'resolverEdge' + | 'resolverEdgeText'; -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', - labelBackgroundTerminatedProcess: '#8A96A8', - labelBackgroundTerminatedTrigger: '#8A96A8', - labelBackgroundRunningProcess: '#8A96A8', - labelBackgroundRunningTrigger: '#8A96A8', - inertDescription: '#747474', -}; +type ColorMap = Record; +interface NodeStyleConfig { + cubeSymbol: string; + descriptionFill: string; + descriptionText: string; + isLabelFilled: boolean; + labelButtonFill: ButtonColor; +} + +export interface NodeStyleMap { + runningProcessCube: NodeStyleConfig; + runningTriggerCube: NodeStyleConfig; + terminatedProcessCube: NodeStyleConfig; + terminatedTriggerCube: NodeStyleConfig; +} const idGenerator = htmlIdGenerator(); @@ -401,3 +358,63 @@ export const SymbolDefinitions = styled(SymbolDefinitionsComponent)` width: 0; height: 0; `; + +export const useResolverTheme = (): { colorMap: ColorMap; nodeAssets: NodeStyleMap } => { + const isDarkMode = useUiSetting(DEFAULT_DARK_MODE); + const theme = isDarkMode ? euiThemeAmsterdamDark : euiThemeAmsterdamLight; + + const getColor = (lightOption: string, darkOption: string): string => { + return isDarkMode ? darkOption : lightOption; + }; + + const colorMap = { + descriptionText: theme.euiColorDarkestShade, + full: theme.euiColorFullShade, + graphControls: theme.euiColorDarkestShade, + graphControlsBackground: theme.euiColorEmptyShade, + resolverBackground: theme.euiColorEmptyShade, + resolverEdge: getColor(theme.euiColorLightestShade, theme.euiColorDarkestShade), + resolverEdgeText: getColor(theme.euiColorDarkShade, theme.euiColorLightShade), + }; + + const nodeAssets = { + runningProcessCube: { + cubeSymbol: `#${SymbolIds.runningProcessCube}`, + descriptionFill: colorMap.descriptionText, + descriptionText: i18n.translate('xpack.siem.endpoint.resolver.runningProcess', { + defaultMessage: 'Running Process', + }), + isLabelFilled: true, + labelButtonFill: 'primary', + }, + runningTriggerCube: { + cubeSymbol: `#${SymbolIds.runningTriggerCube}`, + descriptionFill: colorMap.descriptionText, + descriptionText: i18n.translate('xpack.siem.endpoint.resolver.runningTrigger', { + defaultMessage: 'Running Trigger', + }), + isLabelFilled: true, + labelButtonFill: 'danger', + }, + terminatedProcessCube: { + cubeSymbol: `#${SymbolIds.terminatedProcessCube}`, + descriptionFill: colorMap.descriptionText, + descriptionText: i18n.translate('xpack.siem.endpoint.resolver.terminatedProcess', { + defaultMessage: 'Terminated Process', + }), + isLabelFilled: false, + labelButtonFill: 'primary', + }, + terminatedTriggerCube: { + cubeSymbol: `#${SymbolIds.terminatedTriggerCube}`, + descriptionFill: colorMap.descriptionText, + descriptionText: i18n.translate('xpack.siem.endpoint.resolver.terminatedTrigger', { + defaultMessage: 'Terminated Trigger', + }), + isLabelFilled: false, + labelButtonFill: 'danger', + }, + }; + + return { colorMap, nodeAssets }; +}; diff --git a/x-pack/plugins/security_solution/public/resolver/view/edge_line.tsx b/x-pack/plugins/security_solution/public/resolver/view/edge_line.tsx index 2192422b7d31d4..dc84707002c3ab 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/edge_line.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/edge_line.tsx @@ -8,6 +8,27 @@ import React from 'react'; import styled from 'styled-components'; import { applyMatrix3, distance, angle } from '../lib/vector2'; import { Vector2, Matrix3 } from '../types'; +import { useResolverTheme } from './defs'; + +interface ElapsedTimeProps { + readonly scaledTypeSize: number; + readonly backgroundColor: string; +} + +const StyledElapsedTime = styled.div` + background-color: ${(props) => props.backgroundColor}; + color: ${() => useResolverTheme().colorMap.resolverEdgeText}; + font-size: ${(props) => `${props.scaledTypeSize}px`}; + font-weight: bold; + position: absolute; + top: 50%; + white-space: nowrap; + left: 50%; + padding: 6px 8px; + border-radius: 999px; + transform: translate(-50%, -50%) rotateX(35deg); + user-select: none; +`; /** * A placeholder line segment view that connects process nodes. @@ -15,6 +36,7 @@ import { Vector2, Matrix3 } from '../types'; const EdgeLineComponent = React.memo( ({ className, + elapsedTime, startPosition, endPosition, projectionMatrix, @@ -23,6 +45,10 @@ const EdgeLineComponent = React.memo( * A className string provided by `styled` */ className?: string; + /** + * Time elapsed betweeen process nodes + */ + elapsedTime?: string; /** * The postion of first point in the line segment. In 'world' coordinates. */ @@ -42,6 +68,8 @@ const EdgeLineComponent = React.memo( */ const screenStart = applyMatrix3(startPosition, projectionMatrix); const screenEnd = applyMatrix3(endPosition, projectionMatrix); + const [magFactorX] = projectionMatrix; + const { colorMap } = useResolverTheme(); /** * We render the line using a short, long, `div` element. The length of this `div` @@ -49,6 +77,11 @@ const EdgeLineComponent = React.memo( */ const length = distance(screenStart, screenEnd); + const minimumFontSize = 10; + const slopeOfFontScale = 7.5; + const fontSizeAdjustmentForScale = magFactorX > 1 ? slopeOfFontScale * (magFactorX - 1) : 0; + const scaledTypeSize = minimumFontSize + fontSizeAdjustmentForScale; + const style = { left: `${screenStart[0]}px`, top: `${screenStart[1]}px`, @@ -65,7 +98,19 @@ const EdgeLineComponent = React.memo( */ transform: `translateY(-50%) rotateZ(${angle(screenStart, screenEnd)}rad)`, }; - return
; + + return ( +
+ {elapsedTime && ( + + {elapsedTime} + + )} +
+ ); } ); @@ -73,8 +118,14 @@ EdgeLineComponent.displayName = 'EdgeLine'; export const EdgeLine = styled(EdgeLineComponent)` position: absolute; - height: 3px; - background-color: #d4d4d4; - color: #333333; - contain: strict; + height: ${({ projectionMatrix }) => { + const [magFactorX] = projectionMatrix; + const minimumFontSize = 12; + const slopeOfFontScale = 8.5; + const fontSizeAdjustmentForScale = magFactorX > 1 ? slopeOfFontScale * (magFactorX - 1) : 0; + const scaledTypeSize = minimumFontSize + fontSizeAdjustmentForScale; + return `${scaledTypeSize}px`; + }}; + background-color: ${() => useResolverTheme().colorMap.resolverEdge}; + /* contain: strict; */ `; diff --git a/x-pack/plugins/security_solution/public/resolver/view/graph_controls.tsx b/x-pack/plugins/security_solution/public/resolver/view/graph_controls.tsx index 544dd7143ca280..70117499ddbf5f 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/graph_controls.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/graph_controls.tsx @@ -13,6 +13,7 @@ import { useSelector, useDispatch } from 'react-redux'; import { SideEffectContext } from './side_effect_context'; import { ResolverAction, Vector2 } from '../types'; import * as selectors from '../store/selectors'; +import { useResolverTheme } from './defs'; /** * Controls for zooming, panning, and centering in Resolver @@ -160,8 +161,8 @@ export const GraphControls = styled(GraphControlsComponent)` position: absolute; top: 5px; right: 5px; - background-color: #d4d4d4; - color: #333333; + background-color: ${() => useResolverTheme().colorMap.graphControlsBackground}; + color: ${() => useResolverTheme().colorMap.graphControls}; .zoom-controls { display: flex; diff --git a/x-pack/plugins/security_solution/public/resolver/view/index.tsx b/x-pack/plugins/security_solution/public/resolver/view/index.tsx index 9b336d6916bdf3..86ace933524451 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/index.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/index.tsx @@ -15,7 +15,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 { SymbolDefinitions, useResolverTheme } from './defs'; import { ResolverAction } from '../types'; import { ResolverEvent } from '../../../common/endpoint/types'; import * as eventModel from '../../../common/endpoint/models/event'; @@ -36,8 +36,6 @@ const StyledResolverContainer = styled.div` contain: layout; `; -const bgColor = NamedColors.resolverBackground; - export const Resolver = styled( React.memo(function Resolver({ className, @@ -46,6 +44,7 @@ export const Resolver = styled( className?: string; selectedEvent?: ResolverEvent; }) { + const { colorMap } = useResolverTheme(); const { processNodePositions, edgeLineSegments } = useSelector( selectors.processNodePositionsAndEdgeLineSegments ); @@ -90,8 +89,9 @@ export const Resolver = styled( tabIndex={0} aria-activedescendant={activeDescendantId || undefined} > - {edgeLineSegments.map(([startPosition, endPosition], index) => ( + {edgeLineSegments.map(([startPosition, endPosition, elapsedTime], index) => ( useResolverTheme().colorMap.resolverBackground}; `; diff --git a/x-pack/plugins/security_solution/public/resolver/view/process_event_dot.tsx b/x-pack/plugins/security_solution/public/resolver/view/process_event_dot.tsx index 03903b722427ae..de1ee1f5c64efb 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/process_event_dot.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/process_event_dot.tsx @@ -9,6 +9,7 @@ import styled from 'styled-components'; import { i18n } from '@kbn/i18n'; import { htmlIdGenerator, + EuiButton, EuiI18nNumber, EuiKeyboardAccessible, EuiFlexGroup, @@ -18,48 +19,13 @@ import { useSelector } from 'react-redux'; import { NodeSubMenu, subMenuAssets } from './submenu'; import { applyMatrix3 } from '../lib/vector2'; import { Vector2, Matrix3, AdjacentProcessMap, ResolverProcessType } from '../types'; -import { SymbolIds, NamedColors } from './defs'; +import { SymbolIds, useResolverTheme, NodeStyleMap } from './defs'; import { ResolverEvent, ResolverNodeStats } from '../../../common/endpoint/types'; import { useResolverDispatch } from './use_resolver_dispatch'; import * as eventModel from '../../../common/endpoint/models/event'; import * as processModel from '../models/process_event'; import * as selectors from '../store/selectors'; -const nodeAssets = { - runningProcessCube: { - cubeSymbol: `#${SymbolIds.runningProcessCube}`, - labelBackground: NamedColors.labelBackgroundRunningProcess, - descriptionFill: NamedColors.empty, - descriptionText: i18n.translate('xpack.securitySolution.endpoint.resolver.runningProcess', { - defaultMessage: 'Running Process', - }), - }, - runningTriggerCube: { - cubeSymbol: `#${SymbolIds.runningTriggerCube}`, - labelBackground: NamedColors.labelBackgroundRunningTrigger, - descriptionFill: NamedColors.empty, - descriptionText: i18n.translate('xpack.securitySolution.endpoint.resolver.runningTrigger', { - defaultMessage: 'Running Trigger', - }), - }, - terminatedProcessCube: { - cubeSymbol: `#${SymbolIds.terminatedProcessCube}`, - labelBackground: NamedColors.labelBackgroundTerminatedProcess, - descriptionFill: NamedColors.empty, - descriptionText: i18n.translate('xpack.securitySolution.endpoint.resolver.terminatedProcess', { - defaultMessage: 'Terminated Process', - }), - }, - terminatedTriggerCube: { - cubeSymbol: `#${SymbolIds.terminatedTriggerCube}`, - labelBackground: NamedColors.labelBackgroundTerminatedTrigger, - descriptionFill: NamedColors.empty, - descriptionText: i18n.translate('xpack.securitySolution.endpoint.resolver.terminatedTrigger', { - defaultMessage: 'Terminated Trigger', - }), - }, -}; - /** * Take a gross `schemaName` and return a beautiful translated one. */ @@ -317,7 +283,8 @@ const ProcessEventDotComponents = React.memo( const markerBaseSize = 15; const markerSize = markerBaseSize; - const markerPositionOffset = -markerBaseSize / 2; + const markerPositionYOffset = -markerBaseSize / 2 + 3; // + 3 to align nodes centrally on edge + const markerPositionXOffset = -markerBaseSize / 2; /** * An element that should be animated when the node is clicked. @@ -333,7 +300,10 @@ const ProcessEventDotComponents = React.memo( }) | null; } = React.createRef(); - const { cubeSymbol, labelBackground, descriptionText } = nodeAssets[nodeType(event)]; + const { colorMap, nodeAssets } = useResolverTheme(); + const { cubeSymbol, descriptionText, isLabelFilled, labelButtonFill } = nodeAssets[ + nodeType(event) + ]; const resolverNodeIdGenerator = useMemo(() => htmlIdGenerator('resolverNode'), []); const nodeId = useMemo(() => resolverNodeIdGenerator(selfId), [ @@ -460,7 +430,7 @@ const ProcessEventDotComponents = React.memo( @@ -509,7 +479,7 @@ const ProcessEventDotComponents = React.memo( style={{ textTransform: 'uppercase', letterSpacing: '-0.01px', - backgroundColor: NamedColors.resolverBackground, + backgroundColor: colorMap.resolverBackground, lineHeight: '1', fontWeight: 'bold', fontSize: '0.8rem', @@ -517,35 +487,18 @@ const ProcessEventDotComponents = React.memo( margin: '0', textAlign: 'left', padding: '0', - color: NamedColors.empty, + color: colorMap.descriptionText, }} > {descriptionText}
-
= 2 ? 'euiButton' : 'euiButton euiButton--small'} - data-test-subject="nodeLabel" - id={labelId} - style={{ - backgroundColor: labelBackground, - padding: '.15rem 0', - textAlign: 'center', - maxWidth: '20rem', - minWidth: '12rem', - width: '60%', - overflow: 'hidden', - whiteSpace: 'nowrap', - textOverflow: 'ellipsis', - contain: 'content', - margin: '.25rem 0 .35rem 0', - }} - > + {eventModel.eventName(event)} -
+ {magFactorX >= 2 && ( @@ -619,7 +572,7 @@ export const ProcessEventDot = styled(ProcessEventDotComponents)` } `; -const processTypeToCube: Record = { +const processTypeToCube: Record = { processCreated: 'runningProcessCube', processRan: 'runningProcessCube', processTerminated: 'terminatedProcessCube', @@ -628,7 +581,7 @@ const processTypeToCube: Record = unknownEvent: 'runningProcessCube', }; -function nodeType(processEvent: ResolverEvent): keyof typeof nodeAssets { +function nodeType(processEvent: ResolverEvent): keyof NodeStyleMap { const processType = processModel.eventType(processEvent); if (processType in processTypeToCube) { diff --git a/x-pack/plugins/siem/public/resolver/lib/dates.ts b/x-pack/plugins/siem/public/resolver/lib/dates.ts new file mode 100644 index 00000000000000..307d17f307e9a6 --- /dev/null +++ b/x-pack/plugins/siem/public/resolver/lib/dates.ts @@ -0,0 +1,68 @@ +/* + * 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 moment from 'moment'; +import { i18n } from '@kbn/i18n'; + +type DateFormat = Date | number | string; +export const getRelativeTimeDifference = (startDate: DateFormat, endDate: DateFormat): string => { + // Set the threshold above which pluralization takes place https://momentjs.com/docs/#/customization/relative-time-threshold/ + moment.relativeTimeThreshold('s', 60); // Least number of seconds to be considered minute + moment.relativeTimeThreshold('ss', 1); + moment.relativeTimeThreshold('m', 60); // Least number of minutes to be considered an hour + moment.relativeTimeThreshold('mm', 1); + moment.relativeTimeThreshold('h', 24); // Least number of hours to be considered a day + moment.relativeTimeThreshold('hh', 1); + moment.relativeTimeThreshold('d', 30); // Least number of days to be considered a month + moment.relativeTimeThreshold('dd', 1); + moment.relativeTimeThreshold('M', 12); // Least number of months to be considered a year + moment.relativeTimeThreshold('MM', 1); + moment.relativeTimeThreshold('yy', 1); + + /** Currently on version 2.24.0 of moment. Weeks were added in 2.25.0. Will need to update 'd' and these when updated */ + + // moment.relativeTimeThreshold('w', 4); // Least number of days to be considered a month + // moment.relativeTimeThreshold('ww', 1); + + // Explicitly configure the text format being used + moment.updateLocale('en', { + relativeTime: { + future: 'in %s', + past: '%s ago', + s: '%d second', + ss: '%d seconds', + m: '%d minute', + mm: '%d minutes', + h: '%d hour', + // w: '%d week', + // ww: '%d weeks', + hh: '%d hours', + d: '%d day', + dd: '%d days', + M: '%d month', + MM: '%d months', + y: '%d year', + yy: '%d years', + }, + }); + + moment.locale(i18n.getLocale()); + + const momentEndDate = moment(endDate); + const momentStartDate = moment(startDate); + let elapsedTime = momentEndDate.from(momentStartDate, true); + + if (elapsedTime.includes('days')) { + // This can be removed when updated to version 2.25.0 of moment.js + const [numStr] = elapsedTime.split(' '); + const weekVal = Math.floor(parseInt(numStr, 10) / 7); + if (weekVal > 0) { + elapsedTime = `${weekVal} ${weekVal > 1 ? 'weeks' : ' week'}`; + } + } + + return elapsedTime; +}; From 0cb51e886f6d67019a487a8b25d2b7087183c6bb Mon Sep 17 00:00:00 2001 From: Michael Olorunnisola Date: Mon, 1 Jun 2020 11:56:03 -0400 Subject: [PATCH 02/15] WIP: initial pass at submeenu styling --- .../public/resolver/view/index.tsx | 1 - .../resolver/view/process_event_dot.tsx | 2 + .../public/resolver/view/submenu.tsx | 70 ++++++++++++++----- 3 files changed, 54 insertions(+), 19 deletions(-) diff --git a/x-pack/plugins/security_solution/public/resolver/view/index.tsx b/x-pack/plugins/security_solution/public/resolver/view/index.tsx index 86ace933524451..334a12c4ae3670 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/index.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/index.tsx @@ -44,7 +44,6 @@ export const Resolver = styled( className?: string; selectedEvent?: ResolverEvent; }) { - const { colorMap } = useResolverTheme(); const { processNodePositions, edgeLineSegments } = useSelector( selectors.processNodePositionsAndEdgeLineSegments ); diff --git a/x-pack/plugins/security_solution/public/resolver/view/process_event_dot.tsx b/x-pack/plugins/security_solution/public/resolver/view/process_event_dot.tsx index de1ee1f5c64efb..12640380c65001 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/process_event_dot.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/process_event_dot.tsx @@ -503,6 +503,7 @@ const ProcessEventDotComponents = React.memo( diff --git a/x-pack/plugins/security_solution/public/resolver/view/submenu.tsx b/x-pack/plugins/security_solution/public/resolver/view/submenu.tsx index 6f69725c8677a9..9629cf377a5c35 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/submenu.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/submenu.tsx @@ -6,7 +6,7 @@ import { i18n } from '@kbn/i18n'; import React, { ReactNode, useState, useMemo, useCallback } from 'react'; -import { EuiSelectable, EuiButton } from '@elastic/eui'; +import { EuiSelectable, EuiButton, EuiPopover, ButtonColor } from '@elastic/eui'; import styled from 'styled-components'; /** @@ -98,11 +98,17 @@ OptionList.displayName = 'OptionList'; */ const NodeSubMenuComponents = React.memo( ({ + buttonColor, menuTitle, menuAction, optionsWithActions, className, - }: { menuTitle: string; className?: string; menuAction: () => unknown } & { + }: { + menuTitle: string; + className?: string; + menuAction: () => unknown; + buttonColor: ButtonColor; + } & { optionsWithActions?: ResolverSubmenuOptionList | string | undefined; }) => { const [menuIsOpen, setMenuOpen] = useState(false); @@ -126,6 +132,8 @@ const NodeSubMenuComponents = React.memo( [menuAction] ); + const closePopover = () => setMenuOpen(false); + const isMenuLoading = optionsWithActions === 'waitingForRelatedEventData'; if (!optionsWithActions) { @@ -135,7 +143,7 @@ const NodeSubMenuComponents = React.memo( */ return (
- + {menuTitle}
@@ -145,23 +153,35 @@ const NodeSubMenuComponents = React.memo( * When called with a set of `optionsWithActions`: * Render with a panel of options that appear when the menu host button is clicked */ + + const button = ( + + {menuTitle} + + ); + return (
- - {menuTitle} - - {menuIsOpen && typeof optionsWithActions === 'object' && ( - - )} + {menuIsOpen && typeof optionsWithActions === 'object' && ( + + )} +
); } @@ -170,11 +190,25 @@ const NodeSubMenuComponents = React.memo( NodeSubMenuComponents.displayName = 'NodeSubMenu'; export const NodeSubMenu = styled(NodeSubMenuComponents)` - margin: 0; + margin: 2px 0 0 0; padding: 0; border: none; display: flex; flex-flow: column; + + .euiButton { + background-color: transparent; + border-color: ${(props) => props.buttonColor}; + border-style: solid; + border-width: 1px; + width: 100%; + } + + .euiPopover__anchor { + display: flex; + width: 100%; + } + &.is-open .euiButton { border-bottom-left-radius: 0; border-bottom-right-radius: 0; From 697fc2c5ae806854d6338c80dc6e25bcb723e694 Mon Sep 17 00:00:00 2001 From: Michael Olorunnisola Date: Wed, 3 Jun 2020 10:23:02 -0400 Subject: [PATCH 03/15] WIP: style changes, renaming --- .../public/resolver/store/data/selectors.ts | 19 +- .../public/resolver/types.ts | 12 +- .../public/resolver/view/assets.tsx | 472 ++++++++++++++++++ .../public/resolver/view/defs.tsx | 156 ++++-- .../public/resolver/view/edge_line.tsx | 12 +- .../public/resolver/view/graph_controls.tsx | 4 +- .../public/resolver/view/index.tsx | 6 +- .../resolver/view/process_event_dot.tsx | 44 +- .../public/resolver/view/submenu.tsx | 38 +- .../public/resolver/lib/{dates.ts => date.ts} | 0 10 files changed, 668 insertions(+), 95 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/resolver/view/assets.tsx rename x-pack/plugins/siem/public/resolver/lib/{dates.ts => date.ts} (100%) diff --git a/x-pack/plugins/security_solution/public/resolver/store/data/selectors.ts b/x-pack/plugins/security_solution/public/resolver/store/data/selectors.ts index da8c34363d02f9..ab2716e4b5bb86 100644 --- a/x-pack/plugins/security_solution/public/resolver/store/data/selectors.ts +++ b/x-pack/plugins/security_solution/public/resolver/store/data/selectors.ts @@ -15,8 +15,11 @@ import { Matrix3, AdjacentProcessMap, Vector2, + RelatedEventData, + RelatedEventDataEntryWithStats, + EdgeLineMetadata, } from '../../types'; -import { ResolverEvent } from '../../../../common/endpoint/types'; +import { ResolverEvent, EndpointEvent } from '../../../../common/endpoint/types'; import { eventTimestamp } from '../../../../common/endpoint/models/event'; import { add as vector2Add, applyMatrix3 } from '../../lib/vector2'; import { isGraphableProcess, uniquePidForProcess } from '../../models/process_event'; @@ -27,7 +30,7 @@ import { size, levelOrder, } from '../../models/indexed_process_tree'; -import { getRelativeTimeDifference } from '../../lib/dates'; +import { getRelativeTimeDifference } from '../../lib/date'; const unit = 100; const distanceBetweenNodesInUnits = 2; @@ -155,8 +158,8 @@ function processEdgeLineSegments( positions: ProcessPositions ): EdgeLineSegment[] { const edgeLineSegments: EdgeLineSegment[] = []; - let elapsedTime: string = ''; for (const metadata of levelOrderWithWidths(indexedProcessTree, widths)) { + const edgeLineMetadata: EdgeLineMetadata = {}; /** * We only handle children, drawing lines back to their parents. The root has no parent, so we skip it */ @@ -178,7 +181,7 @@ function processEdgeLineSegments( const parentTime = eventTimestamp(parent); const processTime = eventTimestamp(process); if (parentTime && processTime) { - elapsedTime = getRelativeTimeDifference(parentTime, processTime); + edgeLineMetadata.elapsedTime = getRelativeTimeDifference(parentTime, processTime); } /** @@ -199,7 +202,7 @@ function processEdgeLineSegments( const lineFromProcessToMidwayLine: EdgeLineSegment = [ [position[0], midwayY], position, - elapsedTime, + edgeLineMetadata, ]; const siblings = indexedProcessTreeChildren(indexedProcessTree, parent); @@ -207,7 +210,7 @@ function processEdgeLineSegments( if (metadata.isOnlyChild) { // add a single line segment directly from parent to child. We don't do the 'pitchfork' in this case. - edgeLineSegments.push([parentPosition, position, elapsedTime]); + edgeLineSegments.push([parentPosition, position, edgeLineMetadata]); } else if (isFirstChild) { /** * If the parent has multiple children, we draw the 'midway' line, and the line from the @@ -490,12 +493,12 @@ export const processNodePositionsAndEdgeLineSegments = createSelector( for (const edgeLineSegment of edgeLineSegments) { // const transformedSegment = []; - const [startPoint, endPoint, elapsedTime] = edgeLineSegment; + const [startPoint, endPoint, edgeLineMetadata] = edgeLineSegment; const transformedSegment: EdgeLineSegment = [ applyMatrix3(startPoint, isometricTransformMatrix), applyMatrix3(endPoint, isometricTransformMatrix), - elapsedTime, ]; + if (edgeLineMetadata) transformedSegment.push(edgeLineMetadata); transformedEdgeLineSegments.push(transformedSegment); } diff --git a/x-pack/plugins/security_solution/public/resolver/types.ts b/x-pack/plugins/security_solution/public/resolver/types.ts index cabe7a7eacec94..2bca15ff1908db 100644 --- a/x-pack/plugins/security_solution/public/resolver/types.ts +++ b/x-pack/plugins/security_solution/public/resolver/types.ts @@ -241,10 +241,18 @@ export type ProcessWidths = Map; * Map of ProcessEvents (representing process nodes) to their positions. Calculated by `processPositions` */ export type ProcessPositions = Map; + +/** + * Values shared between two vertices joined by an edge line. + */ +export interface EdgeLineMetadata { + elapsedTime?: string; +} + /** - * An array of vectors2 forming an polyline. Used to connect process nodes in the graph. + * A tuple of 2 vector2 points and optional EdgeLineMetadata forming a polyline. Used to connect process nodes in the graph and display relevant information. */ -export type EdgeLineSegment = [Vector2, Vector2, string?]; +export type EdgeLineSegment = [Vector2, Vector2, EdgeLineMetadata?]; /** * Used to provide precalculated info from `widthsOfProcessSubtrees`. These 'width' values are used in the layout of the graph. diff --git a/x-pack/plugins/security_solution/public/resolver/view/assets.tsx b/x-pack/plugins/security_solution/public/resolver/view/assets.tsx new file mode 100644 index 00000000000000..5c92253c74eaa1 --- /dev/null +++ b/x-pack/plugins/security_solution/public/resolver/view/assets.tsx @@ -0,0 +1,472 @@ +/* + * 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 euiThemeAmsterdamDark from '@elastic/eui/dist/eui_theme_amsterdam_dark.json'; +import euiThemeAmsterdamLight from '@elastic/eui/dist/eui_theme_amsterdam_light.json'; +import { htmlIdGenerator, ButtonColor } from '@elastic/eui'; +import styled from 'styled-components'; +import { i18n } from '@kbn/i18n'; +import { useUiSetting } from '../../common/lib/kibana'; +import { DEFAULT_DARK_MODE } from '../../../common/constants'; + +type ResolverColorNames = + | 'descriptionText' + | 'full' + | 'graphControls' + | 'graphControlsBackground' + | 'resolverBackground' + | 'resolverEdge' + | 'resolverEdgeText'; + +type ColorMap = Record; +interface NodeStyleConfig { + backingFill: string; + cubeSymbol: string; + descriptionFill: string; + descriptionText: string; + isLabelFilled: boolean; + labelButtonFill: ButtonColor; +} + +export interface NodeStyleMap { + runningProcessCube: NodeStyleConfig; + runningTriggerCube: NodeStyleConfig; + terminatedProcessCube: NodeStyleConfig; + terminatedTriggerCube: NodeStyleConfig; +} + +const idGenerator = htmlIdGenerator(); + +/** + * Ids of paint servers to be referenced by fill and stroke attributes + */ +export const PaintServerIds = { + 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(({ isDarkMode }: { isDarkMode: boolean }) => ( + <> + + + + + + + + + {isDarkMode ? ( + <> + + + + + + + + + + ) : ( + <> + + + + + + + + + + )} + +)); + +PaintServers.displayName = 'PaintServers'; + +/** + * Ids of symbols to be linked by elements + */ +export const SymbolIds = { + processNodeLabel: idGenerator('nodeSymbol'), + runningProcessCube: idGenerator('runningCube'), + runningTriggerCube: idGenerator('runningTriggerCube'), + terminatedProcessCube: idGenerator('terminatedCube'), + terminatedTriggerCube: idGenerator('terminatedTriggerCube'), + processCubeActiveBacking: idGenerator('activeBacking'), +}; + +/** + * Defs entries that define shapes, masks and other spatial elements + */ +const SymbolsAndShapes = memo(({ isDarkMode }: { isDarkMode: boolean }) => ( + <> + + + + + {'Running Process'} + + + + + + + + + + + + + + {'resolver_dark process running'} + + + + + + + + + + + + + + {'Terminated Process'} + + + + + + + + + {'Terminated Trigger Process'} + {isDarkMode && ( + + )} + + {!isDarkMode && ( + + )} + + + + + + + {'resolver active backing'} + + + +)); + +SymbolsAndShapes.displayName = 'SymbolsAndShapes'; + +/** + * 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) + */ +const SymbolDefinitionsComponent = memo(({ className }: { className?: string }) => { + const isDarkMode = useUiSetting(DEFAULT_DARK_MODE); + return ( + + + + + + + ); +}); + +SymbolDefinitionsComponent.displayName = 'SymbolDefinitions'; + +export const SymbolDefinitions = styled(SymbolDefinitionsComponent)` + position: absolute; + left: 100%; + top: 100%; + width: 0; + height: 0; +`; + +export const useResolverTheme = (): { colorMap: ColorMap; nodeAssets: NodeStyleMap } => { + const isDarkMode = useUiSetting(DEFAULT_DARK_MODE); + const theme = isDarkMode ? euiThemeAmsterdamDark : euiThemeAmsterdamLight; + + const getThemedOption = (lightOption: string, darkOption: string): string => { + return isDarkMode ? darkOption : lightOption; + }; + + const colorMap = { + descriptionText: theme.euiColorDarkestShade, + full: theme.euiColorFullShade, + graphControls: theme.euiColorDarkestShade, + graphControlsBackground: theme.euiColorEmptyShade, + processBackingFill: theme.euiColorPrimary, + resolverBackground: theme.euiColorEmptyShade, + resolverEdge: getThemedOption(theme.euiColorLightestShade, theme.euiColorLightShade), + resolverEdgeText: getThemedOption(theme.euiColorDarkShade, theme.euiColorFullShade), + triggerBackingFill: theme.euiColorDanger, + }; + + const nodeAssets: NodeStyleMap = { + runningProcessCube: { + backingFill: colorMap.processBackingFill, + cubeSymbol: `#${SymbolIds.runningProcessCube}`, + descriptionFill: colorMap.descriptionText, + descriptionText: i18n.translate('xpack.siem.endpoint.resolver.runningProcess', { + defaultMessage: 'Running Process', + }), + isLabelFilled: true, + labelButtonFill: 'primary', + }, + runningTriggerCube: { + backingFill: colorMap.triggerBackingFill, + cubeSymbol: `#${SymbolIds.runningTriggerCube}`, + descriptionFill: colorMap.descriptionText, + descriptionText: i18n.translate('xpack.siem.endpoint.resolver.runningTrigger', { + defaultMessage: 'Running Trigger', + }), + isLabelFilled: true, + labelButtonFill: 'danger', + }, + terminatedProcessCube: { + backingFill: colorMap.processBackingFill, + cubeSymbol: `#${SymbolIds.terminatedProcessCube}`, + descriptionFill: colorMap.descriptionText, + descriptionText: i18n.translate('xpack.siem.endpoint.resolver.terminatedProcess', { + defaultMessage: 'Terminated Process', + }), + isLabelFilled: false, + labelButtonFill: 'primary', + }, + terminatedTriggerCube: { + backingFill: colorMap.triggerBackingFill, + cubeSymbol: `#${SymbolIds.terminatedTriggerCube}`, + descriptionFill: colorMap.descriptionText, + descriptionText: i18n.translate('xpack.siem.endpoint.resolver.terminatedTrigger', { + defaultMessage: 'Terminated Trigger', + }), + isLabelFilled: false, + labelButtonFill: 'danger', + }, + }; + + return { colorMap, nodeAssets }; +}; diff --git a/x-pack/plugins/security_solution/public/resolver/view/defs.tsx b/x-pack/plugins/security_solution/public/resolver/view/defs.tsx index 29f4d43168c0b2..5c92253c74eaa1 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/defs.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/defs.tsx @@ -24,6 +24,7 @@ type ResolverColorNames = type ColorMap = Record; interface NodeStyleConfig { + backingFill: string; cubeSymbol: string; descriptionFill: string; descriptionText: string; @@ -54,32 +55,9 @@ export const PaintServerIds = { * PaintServers: Where color palettes, grandients, patterns and other similar concerns * are exposed to the component */ -const PaintServers = memo(() => ( + +const PaintServers = memo(({ isDarkMode }: { isDarkMode: boolean }) => ( <> - - - - - - - - ( + {isDarkMode ? ( + <> + + + + + + + + + + ) : ( + <> + + + + + + + + + + )} )); @@ -124,7 +155,7 @@ export const SymbolIds = { /** * Defs entries that define shapes, masks and other spatial elements */ -const SymbolsAndShapes = memo(() => ( +const SymbolsAndShapes = memo(({ isDarkMode }: { isDarkMode: boolean }) => ( <> ( {'Terminated Trigger Process'} - + {isDarkMode && ( + + )} + {!isDarkMode && ( + + )} ( {'resolver active backing'} @@ -340,14 +383,17 @@ SymbolsAndShapes.displayName = 'SymbolsAndShapes'; * 2. Separation of concerns between creative assets and more functional areas of the app * 3. `` elements can be handled by compositor (faster) */ -const SymbolDefinitionsComponent = memo(({ className }: { className?: string }) => ( - - - - - - -)); +const SymbolDefinitionsComponent = memo(({ className }: { className?: string }) => { + const isDarkMode = useUiSetting(DEFAULT_DARK_MODE); + return ( + + + + + + + ); +}); SymbolDefinitionsComponent.displayName = 'SymbolDefinitions'; @@ -363,7 +409,7 @@ export const useResolverTheme = (): { colorMap: ColorMap; nodeAssets: NodeStyleM const isDarkMode = useUiSetting(DEFAULT_DARK_MODE); const theme = isDarkMode ? euiThemeAmsterdamDark : euiThemeAmsterdamLight; - const getColor = (lightOption: string, darkOption: string): string => { + const getThemedOption = (lightOption: string, darkOption: string): string => { return isDarkMode ? darkOption : lightOption; }; @@ -372,13 +418,16 @@ export const useResolverTheme = (): { colorMap: ColorMap; nodeAssets: NodeStyleM full: theme.euiColorFullShade, graphControls: theme.euiColorDarkestShade, graphControlsBackground: theme.euiColorEmptyShade, + processBackingFill: theme.euiColorPrimary, resolverBackground: theme.euiColorEmptyShade, - resolverEdge: getColor(theme.euiColorLightestShade, theme.euiColorDarkestShade), - resolverEdgeText: getColor(theme.euiColorDarkShade, theme.euiColorLightShade), + resolverEdge: getThemedOption(theme.euiColorLightestShade, theme.euiColorLightShade), + resolverEdgeText: getThemedOption(theme.euiColorDarkShade, theme.euiColorFullShade), + triggerBackingFill: theme.euiColorDanger, }; - const nodeAssets = { + const nodeAssets: NodeStyleMap = { runningProcessCube: { + backingFill: colorMap.processBackingFill, cubeSymbol: `#${SymbolIds.runningProcessCube}`, descriptionFill: colorMap.descriptionText, descriptionText: i18n.translate('xpack.siem.endpoint.resolver.runningProcess', { @@ -388,6 +437,7 @@ export const useResolverTheme = (): { colorMap: ColorMap; nodeAssets: NodeStyleM labelButtonFill: 'primary', }, runningTriggerCube: { + backingFill: colorMap.triggerBackingFill, cubeSymbol: `#${SymbolIds.runningTriggerCube}`, descriptionFill: colorMap.descriptionText, descriptionText: i18n.translate('xpack.siem.endpoint.resolver.runningTrigger', { @@ -397,6 +447,7 @@ export const useResolverTheme = (): { colorMap: ColorMap; nodeAssets: NodeStyleM labelButtonFill: 'danger', }, terminatedProcessCube: { + backingFill: colorMap.processBackingFill, cubeSymbol: `#${SymbolIds.terminatedProcessCube}`, descriptionFill: colorMap.descriptionText, descriptionText: i18n.translate('xpack.siem.endpoint.resolver.terminatedProcess', { @@ -406,6 +457,7 @@ export const useResolverTheme = (): { colorMap: ColorMap; nodeAssets: NodeStyleM labelButtonFill: 'primary', }, terminatedTriggerCube: { + backingFill: colorMap.triggerBackingFill, cubeSymbol: `#${SymbolIds.terminatedTriggerCube}`, descriptionFill: colorMap.descriptionText, descriptionText: i18n.translate('xpack.siem.endpoint.resolver.terminatedTrigger', { diff --git a/x-pack/plugins/security_solution/public/resolver/view/edge_line.tsx b/x-pack/plugins/security_solution/public/resolver/view/edge_line.tsx index dc84707002c3ab..c8e77e7351bc1a 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/edge_line.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/edge_line.tsx @@ -7,8 +7,8 @@ import React from 'react'; import styled from 'styled-components'; import { applyMatrix3, distance, angle } from '../lib/vector2'; -import { Vector2, Matrix3 } from '../types'; -import { useResolverTheme } from './defs'; +import { Vector2, Matrix3, EdgeLineMetadata } from '../types'; +import { useResolverTheme } from './assets'; interface ElapsedTimeProps { readonly scaledTypeSize: number; @@ -36,7 +36,7 @@ const StyledElapsedTime = styled.div` const EdgeLineComponent = React.memo( ({ className, - elapsedTime, + edgeLineMetadata, startPosition, endPosition, projectionMatrix, @@ -48,7 +48,7 @@ const EdgeLineComponent = React.memo( /** * Time elapsed betweeen process nodes */ - elapsedTime?: string; + edgeLineMetadata?: EdgeLineMetadata; /** * The postion of first point in the line segment. In 'world' coordinates. */ @@ -70,6 +70,7 @@ const EdgeLineComponent = React.memo( const screenEnd = applyMatrix3(endPosition, projectionMatrix); const [magFactorX] = projectionMatrix; const { colorMap } = useResolverTheme(); + const elapsedTime = edgeLineMetadata?.elapsedTime; /** * We render the line using a short, long, `div` element. The length of this `div` @@ -98,10 +99,11 @@ const EdgeLineComponent = React.memo( */ transform: `translateY(-50%) rotateZ(${angle(screenStart, screenEnd)}rad)`, }; + const showElapsedTime = elapsedTime && magFactorX >= 0.75 && magFactorX <= 2.25; return (
- {elapsedTime && ( + {showElapsedTime && ( - {edgeLineSegments.map(([startPosition, endPosition, elapsedTime], index) => ( + {edgeLineSegments.map(([startPosition, endPosition, edgeLineMetadata], index) => ( htmlIdGenerator('resolverNode'), []); @@ -429,6 +431,7 @@ const ProcessEventDotComponents = React.memo( @@ -483,7 +486,7 @@ const ProcessEventDotComponents = React.memo( lineHeight: '1', fontWeight: 'bold', fontSize: '0.8rem', - width: '100%', + width: 'fit-content', margin: '0', textAlign: 'left', padding: '0', @@ -492,18 +495,26 @@ const ProcessEventDotComponents = React.memo( > {descriptionText}
- + {eventModel.eventName(event)} - {magFactorX >= 2 && ( + {magFactorX >= 1.3 && ( @@ -549,16 +561,24 @@ export const ProcessEventDot = styled(ProcessEventDotComponents)` & .backing { stroke-dasharray: 500; stroke-dashoffset: 500; + fill-opacity: 0; + } + &:hover:not([aria-current]) .backing { + transition-property: fill-opacity; + transition-duration: 0.25s; + fill-opacity: 0.06; } + &[aria-current] .backing { transition-property: stroke-dashoffset; transition-duration: 1s; stroke-dashoffset: 0; } - & .related-dropdown { - width: 4.5em; + & .euiButton { + width: fit-content; } + & .euiSelectableList-bordered { border-top-right-radius: 0px; border-top-left-radius: 0px; diff --git a/x-pack/plugins/security_solution/public/resolver/view/submenu.tsx b/x-pack/plugins/security_solution/public/resolver/view/submenu.tsx index 9629cf377a5c35..49f542cee60b5b 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/submenu.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/submenu.tsx @@ -44,6 +44,10 @@ interface ResolverSubmenuOption { export type ResolverSubmenuOptionList = ResolverSubmenuOption[] | string; +const OptionListItem = styled.div` + width: 175px; +`; + const OptionList = React.memo( ({ subMenuOptions, @@ -81,7 +85,7 @@ const OptionList = React.memo( listProps={{ showIcons: true, bordered: true }} isLoading={isLoading} > - {(list) => list} + {(list) => {list}} ), [isLoading, options] @@ -98,7 +102,7 @@ OptionList.displayName = 'OptionList'; */ const NodeSubMenuComponents = React.memo( ({ - buttonColor, + buttonBorderColor, menuTitle, menuAction, optionsWithActions, @@ -107,7 +111,8 @@ const NodeSubMenuComponents = React.memo( menuTitle: string; className?: string; menuAction: () => unknown; - buttonColor: ButtonColor; + buttonBorderColor: ButtonColor; + buttonFill: string; } & { optionsWithActions?: ResolverSubmenuOptionList | string | undefined; }) => { @@ -143,7 +148,12 @@ const NodeSubMenuComponents = React.memo( */ return (
- + {menuTitle}
@@ -159,7 +169,7 @@ const NodeSubMenuComponents = React.memo( onClick={ typeof optionsWithActions === 'object' ? handleMenuOpenClick : handleMenuActionClick } - color={buttonColor} + color={buttonBorderColor} size="s" iconType={menuIsOpen ? 'arrowUp' : 'arrowDown'} iconSide="right" @@ -196,17 +206,23 @@ export const NodeSubMenu = styled(NodeSubMenuComponents)` display: flex; flex-flow: column; - .euiButton { - background-color: transparent; - border-color: ${(props) => props.buttonColor}; + & .euiButton { + background-color: ${(props) => props.buttonFill}; + border-color: ${(props) => props.buttonBorderColor}; border-style: solid; border-width: 1px; - width: 100%; + + &:hover, + &:active, + &:focus { + background-color: ${(props) => props.buttonFill}; + } } - .euiPopover__anchor { + & .euiPopover__anchor { display: flex; - width: 100%; + } + } &.is-open .euiButton { diff --git a/x-pack/plugins/siem/public/resolver/lib/dates.ts b/x-pack/plugins/siem/public/resolver/lib/date.ts similarity index 100% rename from x-pack/plugins/siem/public/resolver/lib/dates.ts rename to x-pack/plugins/siem/public/resolver/lib/date.ts From abb136dec617b3e9518263f6a5d0f9bf2ddfec0e Mon Sep 17 00:00:00 2001 From: Michael Olorunnisola Date: Wed, 3 Jun 2020 14:10:34 -0400 Subject: [PATCH 04/15] WIP: button var name change --- .../security_solution/public/resolver/view/submenu.tsx | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/x-pack/plugins/security_solution/public/resolver/view/submenu.tsx b/x-pack/plugins/security_solution/public/resolver/view/submenu.tsx index 49f542cee60b5b..c6298b17f2a695 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/submenu.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/submenu.tsx @@ -164,7 +164,7 @@ const NodeSubMenuComponents = React.memo( * Render with a panel of options that appear when the menu host button is clicked */ - const button = ( + const submenuPopoverButton = ( @@ -223,8 +223,6 @@ export const NodeSubMenu = styled(NodeSubMenuComponents)` display: flex; } - } - &.is-open .euiButton { border-bottom-left-radius: 0; border-bottom-right-radius: 0; From dea3be51adaa844437f38e444f868a1d688d2af0 Mon Sep 17 00:00:00 2001 From: Michael Olorunnisola Date: Mon, 8 Jun 2020 15:18:17 -0400 Subject: [PATCH 05/15] code cleanup, update styles --- .../view/test_helpers/render_alert_page.tsx | 3 +- .../store/camera/scaling_constants.ts | 4 +- .../resolver/store/camera/zooming.test.ts | 8 +- .../data/__snapshots__/graphing.test.ts.snap | 144 +++++++------ .../public/resolver/store/data/selectors.ts | 4 +- .../public/resolver/view/assets.tsx | 19 +- .../public/resolver/view/edge_line.tsx | 40 ++-- .../public/resolver/view/graph_controls.tsx | 2 +- .../resolver/view/process_event_dot.tsx | 202 ++++++++++++------ .../public/resolver/view/submenu.tsx | 2 +- .../public/resolver/view/use_camera.test.tsx | 4 +- .../siem/public/resolver/lib/date.test.ts | 31 +++ .../plugins/siem/public/resolver/lib/date.ts | 2 +- 13 files changed, 292 insertions(+), 173 deletions(-) create mode 100644 x-pack/plugins/siem/public/resolver/lib/date.test.ts diff --git a/x-pack/plugins/security_solution/public/endpoint_alerts/view/test_helpers/render_alert_page.tsx b/x-pack/plugins/security_solution/public/endpoint_alerts/view/test_helpers/render_alert_page.tsx index f52d854d986ff9..11ddc1f2529723 100644 --- a/x-pack/plugins/security_solution/public/endpoint_alerts/view/test_helpers/render_alert_page.tsx +++ b/x-pack/plugins/security_solution/public/endpoint_alerts/view/test_helpers/render_alert_page.tsx @@ -29,6 +29,7 @@ export const alertPageTestRender = () => { const depsStart = depsStartMock(); depsStart.data.ui.SearchBar.mockImplementation(() =>
); + const uiSettings = new Map(); return { store, @@ -47,7 +48,7 @@ export const alertPageTestRender = () => { */ return reactTestingLibrary.render( - + diff --git a/x-pack/plugins/security_solution/public/resolver/store/camera/scaling_constants.ts b/x-pack/plugins/security_solution/public/resolver/store/camera/scaling_constants.ts index 243d8877a8b0d7..5e92290b4c97b4 100644 --- a/x-pack/plugins/security_solution/public/resolver/store/camera/scaling_constants.ts +++ b/x-pack/plugins/security_solution/public/resolver/store/camera/scaling_constants.ts @@ -12,12 +12,12 @@ export const minimum = 0.5; /** * The maximum allowed value for the camera scale. This is greatest scale that we will ever render something at. */ -export const maximum = 6; +export const maximum = 2; /** * The curve of the zoom function growth rate. The higher the scale factor is, the higher the zoom rate will be. */ -export const zoomCurveRate = 4; +export const zoomCurveRate = 2; /** * The size, in world units, of a 'nudge' as caused by clicking the up, right, down, or left panning buttons. diff --git a/x-pack/plugins/security_solution/public/resolver/store/camera/zooming.test.ts b/x-pack/plugins/security_solution/public/resolver/store/camera/zooming.test.ts index fb38c2f526e0b3..ff03b0baf01aaf 100644 --- a/x-pack/plugins/security_solution/public/resolver/store/camera/zooming.test.ts +++ b/x-pack/plugins/security_solution/public/resolver/store/camera/zooming.test.ts @@ -70,12 +70,12 @@ describe('zooming', () => { expect(actual).toMatchInlineSnapshot(` Object { "maximum": Array [ - 25.000000000000007, - 16.666666666666668, + 75, + 50, ], "minimum": Array [ - -25, - -16.666666666666668, + -75, + -50, ], } `); diff --git a/x-pack/plugins/security_solution/public/resolver/store/data/__snapshots__/graphing.test.ts.snap b/x-pack/plugins/security_solution/public/resolver/store/data/__snapshots__/graphing.test.ts.snap index 00abc27b25a83d..4118953ed6d54e 100644 --- a/x-pack/plugins/security_solution/public/resolver/store/data/__snapshots__/graphing.test.ts.snap +++ b/x-pack/plugins/security_solution/public/resolver/store/data/__snapshots__/graphing.test.ts.snap @@ -41,129 +41,136 @@ Object { -0.8164965809277259, ], Array [ - 70.71067811865476, - -41.641325627314025, + 190.91883092036784, + -111.04353500617071, ], ], Array [ Array [ - -70.71067811865476, - -123.29098372008661, + 0, + -221.2705734314137, ], Array [ - 212.13203435596427, - 40.00833246545857, + 381.8376618407357, + -0.8164965809277259, ], ], Array [ Array [ - -70.71067811865476, - -123.29098372008661, + 0, + -221.2705734314137, ], Array [ - 0, - -164.1158127664729, + 190.91883092036784, + -331.4976118566567, ], + Object {}, ], Array [ Array [ - 212.13203435596427, - 40.00833246545857, + 381.8376618407357, + -0.8164965809277259, ], Array [ - 282.842712474619, - -0.8164965809277259, + 572.7564927611036, + -111.04353500617071, ], + Object {}, ], Array [ Array [ - 0, - -164.1158127664729, + 190.91883092036784, + -331.4976118566567, ], Array [ - 70.71067811865476, - -204.9406418128592, + 381.83766184073573, + -441.7246502818997, ], ], Array [ Array [ - 0, - -245.76547085924548, + 286.3782463805518, + -496.8381694945212, ], Array [ - 141.4213562373095, - -164.1158127664729, + 477.2970773009197, + -386.6111310692782, ], ], Array [ Array [ - 0, - -245.76547085924548, + 286.3782463805518, + -496.8381694945212, ], Array [ - 70.71067811865476, - -286.5902999056318, + 477.29707730091957, + -607.0652079197642, ], + Object {}, ], Array [ Array [ - 141.4213562373095, - -164.1158127664729, + 477.2970773009197, + -386.6111310692782, ], Array [ - 212.13203435596427, - -204.9406418128592, + 668.2159082212875, + -496.8381694945212, ], + Object {}, ], Array [ Array [ - 282.842712474619, - -0.8164965809277259, + 572.7564927611036, + -111.04353500617071, ], Array [ - 353.5533905932738, - -41.64132562731401, + 763.6753236814714, + -221.2705734314137, ], ], Array [ Array [ - 282.842712474619, - -82.4661546737003, + 668.2159082212875, + -276.3840926440352, ], Array [ - 424.26406871192853, - -0.8164965809277259, + 859.1347391416554, + -166.1570542187922, ], ], Array [ Array [ - 282.842712474619, - -82.4661546737003, + 668.2159082212875, + -276.3840926440352, ], Array [ - 353.5533905932738, - -123.29098372008661, + 859.1347391416552, + -386.6111310692782, ], + Object {}, ], Array [ Array [ - 424.26406871192853, - -0.8164965809277259, + 859.1347391416554, + -166.1570542187922, ], Array [ - 494.9747468305833, - -41.64132562731404, + 1050.053570062023, + -276.3840926440352, ], + Object {}, ], Array [ Array [ - 494.9747468305833, - -41.64132562731404, + 1050.053570062023, + -276.3840926440352, ], Array [ - 636.3961030678928, - -123.2909837200866, + 1240.972400982391, + -386.6111310692782, ], + Object {}, ], ], "processNodePositions": Map { @@ -198,8 +205,8 @@ Object { "unique_ppid": 0, }, } => Array [ - 0, - -164.1158127664729, + 190.91883092036784, + -331.4976118566567, ], Object { "@timestamp": 1582233383000, @@ -215,8 +222,8 @@ Object { "unique_ppid": 0, }, } => Array [ - 282.842712474619, - -0.8164965809277259, + 572.7564927611036, + -111.04353500617071, ], Object { "@timestamp": 1582233383000, @@ -232,8 +239,8 @@ Object { "unique_ppid": 1, }, } => Array [ - 70.71067811865476, - -286.5902999056318, + 477.29707730091957, + -607.0652079197642, ], Object { "@timestamp": 1582233383000, @@ -249,8 +256,8 @@ Object { "unique_ppid": 1, }, } => Array [ - 212.13203435596427, - -204.9406418128592, + 668.2159082212875, + -496.8381694945212, ], Object { "@timestamp": 1582233383000, @@ -266,8 +273,8 @@ Object { "unique_ppid": 2, }, } => Array [ - 353.5533905932738, - -123.29098372008661, + 859.1347391416552, + -386.6111310692782, ], Object { "@timestamp": 1582233383000, @@ -283,8 +290,8 @@ Object { "unique_ppid": 2, }, } => Array [ - 494.9747468305833, - -41.64132562731404, + 1050.053570062023, + -276.3840926440352, ], Object { "@timestamp": 1582233383000, @@ -300,8 +307,8 @@ Object { "unique_ppid": 6, }, } => Array [ - 636.3961030678928, - -123.2909837200866, + 1240.972400982391, + -386.6111310692782, ], }, } @@ -316,9 +323,10 @@ Object { -0.8164965809277259, ], Array [ - 141.4213562373095, - -82.46615467370032, + 190.91883092036784, + -111.04353500617071, ], + Object {}, ], ], "processNodePositions": Map { @@ -353,8 +361,8 @@ Object { "unique_ppid": 0, }, } => Array [ - 141.4213562373095, - -82.46615467370032, + 190.91883092036784, + -111.04353500617071, ], }, } diff --git a/x-pack/plugins/security_solution/public/resolver/store/data/selectors.ts b/x-pack/plugins/security_solution/public/resolver/store/data/selectors.ts index ab2716e4b5bb86..80a07ea41418f5 100644 --- a/x-pack/plugins/security_solution/public/resolver/store/data/selectors.ts +++ b/x-pack/plugins/security_solution/public/resolver/store/data/selectors.ts @@ -19,7 +19,7 @@ import { RelatedEventDataEntryWithStats, EdgeLineMetadata, } from '../../types'; -import { ResolverEvent, EndpointEvent } from '../../../../common/endpoint/types'; +import { ResolverEvent } from '../../../../common/endpoint/types'; import { eventTimestamp } from '../../../../common/endpoint/models/event'; import { add as vector2Add, applyMatrix3 } from '../../lib/vector2'; import { isGraphableProcess, uniquePidForProcess } from '../../models/process_event'; @@ -32,7 +32,7 @@ import { } from '../../models/indexed_process_tree'; import { getRelativeTimeDifference } from '../../lib/date'; -const unit = 100; +const unit = 135; const distanceBetweenNodesInUnits = 2; export function isLoading(state: DataState) { diff --git a/x-pack/plugins/security_solution/public/resolver/view/assets.tsx b/x-pack/plugins/security_solution/public/resolver/view/assets.tsx index 5c92253c74eaa1..c83dfab0463041 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/assets.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/assets.tsx @@ -30,6 +30,7 @@ interface NodeStyleConfig { descriptionText: string; isLabelFilled: boolean; labelButtonFill: ButtonColor; + strokeColor: string; } export interface NodeStyleMap { @@ -368,7 +369,6 @@ const SymbolsAndShapes = memo(({ isDarkMode }: { isDarkMode: boolean }) => ( @@ -418,11 +418,11 @@ export const useResolverTheme = (): { colorMap: ColorMap; nodeAssets: NodeStyleM full: theme.euiColorFullShade, graphControls: theme.euiColorDarkestShade, graphControlsBackground: theme.euiColorEmptyShade, - processBackingFill: theme.euiColorPrimary, + processBackingFill: `${theme.euiColorPrimary}${getThemedOption('0F', '1F')}`, // Add opacity 0F = 6% , 1F = 12% resolverBackground: theme.euiColorEmptyShade, resolverEdge: getThemedOption(theme.euiColorLightestShade, theme.euiColorLightShade), resolverEdgeText: getThemedOption(theme.euiColorDarkShade, theme.euiColorFullShade), - triggerBackingFill: theme.euiColorDanger, + triggerBackingFill: `${theme.euiColorDanger}${getThemedOption('0F', '1F')}`, }; const nodeAssets: NodeStyleMap = { @@ -435,6 +435,7 @@ export const useResolverTheme = (): { colorMap: ColorMap; nodeAssets: NodeStyleM }), isLabelFilled: true, labelButtonFill: 'primary', + strokeColor: theme.euiColorPrimary, }, runningTriggerCube: { backingFill: colorMap.triggerBackingFill, @@ -445,6 +446,7 @@ export const useResolverTheme = (): { colorMap: ColorMap; nodeAssets: NodeStyleM }), isLabelFilled: true, labelButtonFill: 'danger', + strokeColor: theme.euiColorDanger, }, terminatedProcessCube: { backingFill: colorMap.processBackingFill, @@ -455,6 +457,7 @@ export const useResolverTheme = (): { colorMap: ColorMap; nodeAssets: NodeStyleM }), isLabelFilled: false, labelButtonFill: 'primary', + strokeColor: `${theme.euiColorPrimary}33`, // 33 = 20% opacity }, terminatedTriggerCube: { backingFill: colorMap.triggerBackingFill, @@ -465,8 +468,18 @@ export const useResolverTheme = (): { colorMap: ColorMap; nodeAssets: NodeStyleM }), isLabelFilled: false, labelButtonFill: 'danger', + strokeColor: `${theme.euiColorDanger}33`, }, }; return { colorMap, nodeAssets }; }; + +export const calculateResolverFontSize = ( + magFactorX: number, + minFontSize: number, + slopeOfFontScale: number +): number => { + const fontSizeAdjustmentForScale = magFactorX > 1 ? slopeOfFontScale * (magFactorX - 1) : 0; + return minFontSize + fontSizeAdjustmentForScale; +}; diff --git a/x-pack/plugins/security_solution/public/resolver/view/edge_line.tsx b/x-pack/plugins/security_solution/public/resolver/view/edge_line.tsx index c8e77e7351bc1a..dcb275cbf43474 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/edge_line.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/edge_line.tsx @@ -8,24 +8,26 @@ import React from 'react'; import styled from 'styled-components'; import { applyMatrix3, distance, angle } from '../lib/vector2'; import { Vector2, Matrix3, EdgeLineMetadata } from '../types'; -import { useResolverTheme } from './assets'; +import { useResolverTheme, calculateResolverFontSize } from './assets'; interface ElapsedTimeProps { - readonly scaledTypeSize: number; readonly backgroundColor: string; + readonly leftPct: number; + readonly scaledTypeSize: number; + readonly textColor: string; } const StyledElapsedTime = styled.div` background-color: ${(props) => props.backgroundColor}; - color: ${() => useResolverTheme().colorMap.resolverEdgeText}; + color: ${(props) => props.textColor}; font-size: ${(props) => `${props.scaledTypeSize}px`}; font-weight: bold; position: absolute; top: 50%; white-space: nowrap; - left: 50%; + left: ${(props) => `${props.leftPct}%`}; padding: 6px 8px; - border-radius: 999px; + border-radius: 999px; // generate pill shape transform: translate(-50%, -50%) rotateX(35deg); user-select: none; `; @@ -77,11 +79,7 @@ const EdgeLineComponent = React.memo( * should be the same as the distance between the start and end points. */ const length = distance(screenStart, screenEnd); - - const minimumFontSize = 10; - const slopeOfFontScale = 7.5; - const fontSizeAdjustmentForScale = magFactorX > 1 ? slopeOfFontScale * (magFactorX - 1) : 0; - const scaledTypeSize = minimumFontSize + fontSizeAdjustmentForScale; + const scaledTypeSize = calculateResolverFontSize(magFactorX, 10, 7.5); const style = { left: `${screenStart[0]}px`, @@ -99,14 +97,22 @@ const EdgeLineComponent = React.memo( */ transform: `translateY(-50%) rotateZ(${angle(screenStart, screenEnd)}rad)`, }; - const showElapsedTime = elapsedTime && magFactorX >= 0.75 && magFactorX <= 2.25; + + let elapsedTimeLeftPosPct = 50; + + if (magFactorX < 1) { + const fractionalOffset = (1 / magFactorX) * ((1 - magFactorX) * 10); + elapsedTimeLeftPosPct += fractionalOffset; + } return (
- {showElapsedTime && ( + {elapsedTime && ( {elapsedTime} @@ -120,14 +126,8 @@ EdgeLineComponent.displayName = 'EdgeLine'; export const EdgeLine = styled(EdgeLineComponent)` position: absolute; - height: ${({ projectionMatrix }) => { - const [magFactorX] = projectionMatrix; - const minimumFontSize = 12; - const slopeOfFontScale = 8.5; - const fontSizeAdjustmentForScale = magFactorX > 1 ? slopeOfFontScale * (magFactorX - 1) : 0; - const scaledTypeSize = minimumFontSize + fontSizeAdjustmentForScale; - return `${scaledTypeSize}px`; + height: ${({ projectionMatrix: [magFactorX] }) => { + return `${calculateResolverFontSize(magFactorX, 12, 8.5)}px`; }}; background-color: ${() => useResolverTheme().colorMap.resolverEdge}; - /* contain: strict; */ `; diff --git a/x-pack/plugins/security_solution/public/resolver/view/graph_controls.tsx b/x-pack/plugins/security_solution/public/resolver/view/graph_controls.tsx index 4d905a30796e92..4a206c657e9432 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/graph_controls.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/graph_controls.tsx @@ -141,7 +141,7 @@ const GraphControlsComponent = React.memo( className="zoom-slider" data-test-subj="zoom-slider" min={0} - max={0.8} + max={1} step={0.01} value={scalingFactor} onChange={handleZoomAmountChange} diff --git a/x-pack/plugins/security_solution/public/resolver/view/process_event_dot.tsx b/x-pack/plugins/security_solution/public/resolver/view/process_event_dot.tsx index 8a7184dec27342..a8859c99c3aeb2 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/process_event_dot.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/process_event_dot.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useCallback, useMemo } from 'react'; +import React, { useCallback, useMemo, useState, useEffect } from 'react'; import styled from 'styled-components'; import { i18n } from '@kbn/i18n'; import { @@ -18,8 +18,8 @@ import { import { useSelector } from 'react-redux'; import { NodeSubMenu, subMenuAssets } from './submenu'; import { applyMatrix3 } from '../lib/vector2'; -import { Vector2, Matrix3, AdjacentProcessMap, ResolverProcessType } from '../types'; -import { SymbolIds, useResolverTheme, NodeStyleMap } from './assets'; +import { Vector2, Matrix3, AdjacentProcessMap } from '../types'; +import { SymbolIds, useResolverTheme, NodeStyleMap, calculateResolverFontSize } from './assets'; import { ResolverEvent, ResolverNodeStats } from '../../../common/endpoint/types'; import { useResolverDispatch } from './use_resolver_dispatch'; import * as eventModel from '../../../common/endpoint/models/event'; @@ -198,6 +198,45 @@ const getDisplayName: (schemaName: string) => string = function nameInSchemaToDi ); }; +interface StyledActionsContainer { + readonly color: string; + readonly fontSize: number; + readonly topPct: number; +} + +const StyledActionsContainer = styled.div` + background-color: transparent; + color: ${(props) => props.color}; + display: flex; + flex-flow: column; + font-size: ${(props) => `${props.fontSize}px`}; + left: 25%; + line-height: 140%; + padding: 0.25rem 0 0 0.1rem; + position: absolute; + top: ${(props) => `${props.topPct}%`}; + width: auto; +`; + +interface StyledDescriptionText { + readonly backgroundColor: string; + readonly color: string; +} + +const StyledDescriptionText = styled.div` + text-transform: uppercase; + letter-spacing: -0.01px; + background-color: ${(props) => props.backgroundColor}; + line-height: 1; + font-weight: bold; + font-size: 0.8rem; + width: fit-content; + margin: 0; + text-align: left; + padding: 1px 0 1px 0; + color: ${(props) => props.color}; +`; + /** * An artifact that represents a process node and the things associated with it in the Resolver */ @@ -255,8 +294,32 @@ const ProcessEventDotComponents = React.memo( * position to accomodate for that. This aligns the logical center of the process node * with the correct position on the map. */ - const processNodeViewXOffset = -0.172413 * logicalProcessNodeViewWidth * magFactorX; - const processNodeViewYOffset = -0.73684 * logicalProcessNodeViewHeight * magFactorX; + const isZoomedOut = magFactorX < 0.95; + const isShowingDescriptionText = magFactorX > 0.6; + + const nodeXOffsetValue = isZoomedOut + ? -0.177413 + (-magFactorX * (1 - magFactorX)) / 5 + : -0.172413; + const nodeYOffsetValue = isZoomedOut + ? -0.73684 + (-magFactorX * 0.5 * (1 - magFactorX)) / magFactorX + : -0.73684; + + const actionsBaseYOffsetPct = 30; + let actionsYOffsetPct; + switch (true) { + case !isZoomedOut: + actionsYOffsetPct = actionsBaseYOffsetPct + 3.5 * magFactorX; + break; + case isShowingDescriptionText: + actionsYOffsetPct = actionsBaseYOffsetPct + magFactorX; + break; + default: + actionsYOffsetPct = actionsBaseYOffsetPct + 21 * magFactorX; + break; + } + + const processNodeViewXOffset = nodeXOffsetValue * logicalProcessNodeViewWidth * magFactorX; + const processNodeViewYOffset = nodeYOffsetValue * logicalProcessNodeViewHeight * magFactorX; const nodeViewportStyle = useMemo( () => ({ @@ -276,18 +339,13 @@ const ProcessEventDotComponents = React.memo( * 18.75 : The smallest readable font size at which labels/descriptions can be read. Font size will not scale below this. * 12.5 : A 'slope' at which the font size will scale w.r.t. to zoom level otherwise */ - const minimumFontSize = 18.75; - const slopeOfFontScale = 12.5; - const fontSizeAdjustmentForScale = magFactorX > 1 ? slopeOfFontScale * (magFactorX - 1) : 0; - const scaledTypeSize = minimumFontSize + fontSizeAdjustmentForScale; + const scaledTypeSize = calculateResolverFontSize(magFactorX, 18.75, 12.5); const markerBaseSize = 15; const markerSize = markerBaseSize; const markerPositionYOffset = -markerBaseSize / 2 + 3; // + 3 to align nodes centrally on edge const markerPositionXOffset = -markerBaseSize / 2; - const markerActionsBaseYOffsetPct = 30; - const markerActionsYOffsetPct = markerActionsBaseYOffsetPct + 7 * magFactorX; /** * An element that should be animated when the node is clicked. */ @@ -303,9 +361,14 @@ const ProcessEventDotComponents = React.memo( | null; } = React.createRef(); const { colorMap, nodeAssets } = useResolverTheme(); - const { backingFill, cubeSymbol, descriptionText, isLabelFilled, labelButtonFill } = nodeAssets[ - nodeType(event) - ]; + const { + backingFill, + cubeSymbol, + descriptionText, + isLabelFilled, + labelButtonFill, + strokeColor, + } = nodeAssets[nodeType(event)]; const resolverNodeIdGenerator = useMemo(() => htmlIdGenerator('resolverNode'), []); const nodeId = useMemo(() => resolverNodeIdGenerator(selfId), [ @@ -341,7 +404,7 @@ const ProcessEventDotComponents = React.memo( }); }, [animationTarget, dispatch, nodeId]); - const handleRelatedEventRequest = useCallback(() => { + useEffect(() => { dispatch({ type: 'userRequestedRelatedEventData', payload: event, @@ -359,6 +422,7 @@ const ProcessEventDotComponents = React.memo( * generally in the form `number of related events in category` `category title` * e.g. "10 DNS", "230 File" */ + const [isShowingRelatedEvents, updateIsShowingRelatedEvents] = useState(false); const relatedEventOptions = useMemo(() => { if (!relatedEventsStats) { // Return an empty set of options if there are no stats to report @@ -434,6 +498,7 @@ const ProcessEventDotComponents = React.memo( fill={backingFill} // Only visible on hover x={-11.35} y={-8.35} + stroke={strokeColor} width={markerSize * 1.5} height={markerSize * 1.5} className="backing" @@ -462,64 +527,65 @@ const ProcessEventDotComponents = React.memo( -
+ {isShowingDescriptionText && ( + + {descriptionText} + + )}
- {descriptionText} -
- - - - {eventModel.eventName(event)} + + + + {eventModel.eventName(event)} + - - - {magFactorX >= 1.3 && ( - - - - + +
+ {!isZoomedOut && ( + + {isShowingRelatedEvents && ( + + + + )} )} -
+
); @@ -566,7 +632,7 @@ export const ProcessEventDot = styled(ProcessEventDotComponents)` &:hover:not([aria-current]) .backing { transition-property: fill-opacity; transition-duration: 0.25s; - fill-opacity: 0.06; + fill-opacity: 1; // actual color opacity handled in the fill hex } &[aria-current] .backing { diff --git a/x-pack/plugins/security_solution/public/resolver/view/submenu.tsx b/x-pack/plugins/security_solution/public/resolver/view/submenu.tsx index c6298b17f2a695..80091c90287f8e 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/submenu.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/submenu.tsx @@ -110,7 +110,7 @@ const NodeSubMenuComponents = React.memo( }: { menuTitle: string; className?: string; - menuAction: () => unknown; + menuAction?: () => unknown; buttonBorderColor: ButtonColor; buttonFill: string; } & { diff --git a/x-pack/plugins/security_solution/public/resolver/view/use_camera.test.tsx b/x-pack/plugins/security_solution/public/resolver/view/use_camera.test.tsx index 4f66322a247a68..8ed9f00d51af8b 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/use_camera.test.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/use_camera.test.tsx @@ -138,11 +138,11 @@ describe('useCamera on an unpainted element', () => { it('should zoom in', () => { expect(projectionMatrix).toMatchInlineSnapshot(` Array [ - 1.0635255481707058, + 1.0292841801261479, 0, 400, 0, - -1.0635255481707058, + -1.0292841801261479, 300, 0, 0, diff --git a/x-pack/plugins/siem/public/resolver/lib/date.test.ts b/x-pack/plugins/siem/public/resolver/lib/date.test.ts new file mode 100644 index 00000000000000..778eab6af57521 --- /dev/null +++ b/x-pack/plugins/siem/public/resolver/lib/date.test.ts @@ -0,0 +1,31 @@ +/* + * 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 moment from 'moment'; +import { getRelativeTimeDifference } from './date'; + +describe('date', () => { + describe('getRelativeTimeDifference', () => { + const initialTime = new Date('6/1/2020'); + const momentDate = moment(initialTime); + const fiveSeconds = momentDate.add(5, 's').toDate(); + const fiveMinutes = momentDate.add(5, 'm').toDate(); + const fiveHours = momentDate.add(5, 'h').toDate(); + const fiveDays = momentDate.add(5, 'd').toDate(); + const threeWeeks = momentDate.add(3, 'w').toDate(); + const threeMonths = momentDate.add(3, 'M').toDate(); + const threeYears = momentDate.add(3, 'y').toDate(); + + it('should return the correct relative time', () => { + expect(getRelativeTimeDifference(initialTime, fiveSeconds)).toBe('5 seconds'); + expect(getRelativeTimeDifference(initialTime, fiveMinutes)).toBe('5 minutes'); + expect(getRelativeTimeDifference(initialTime, fiveHours)).toBe('5 hours'); + expect(getRelativeTimeDifference(initialTime, fiveDays)).toBe('5 days'); + expect(getRelativeTimeDifference(initialTime, threeWeeks)).toBe('3 weeks'); + expect(getRelativeTimeDifference(initialTime, threeMonths)).toBe('4 months'); + expect(getRelativeTimeDifference(initialTime, threeYears)).toBe('3 years'); + }); + }); +}); diff --git a/x-pack/plugins/siem/public/resolver/lib/date.ts b/x-pack/plugins/siem/public/resolver/lib/date.ts index 307d17f307e9a6..e7be1ff0661968 100644 --- a/x-pack/plugins/siem/public/resolver/lib/date.ts +++ b/x-pack/plugins/siem/public/resolver/lib/date.ts @@ -16,7 +16,7 @@ export const getRelativeTimeDifference = (startDate: DateFormat, endDate: DateFo moment.relativeTimeThreshold('mm', 1); moment.relativeTimeThreshold('h', 24); // Least number of hours to be considered a day moment.relativeTimeThreshold('hh', 1); - moment.relativeTimeThreshold('d', 30); // Least number of days to be considered a month + moment.relativeTimeThreshold('d', 28); // Least number of days to be considered a month moment.relativeTimeThreshold('dd', 1); moment.relativeTimeThreshold('M', 12); // Least number of months to be considered a year moment.relativeTimeThreshold('MM', 1); From ed6a4243e57e2793659bd339c8590d707bcc27bf Mon Sep 17 00:00:00 2001 From: Michael Olorunnisola Date: Mon, 8 Jun 2020 16:22:08 -0400 Subject: [PATCH 06/15] minor stylistic changes --- .../public/resolver/view/process_event_dot.tsx | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/security_solution/public/resolver/view/process_event_dot.tsx b/x-pack/plugins/security_solution/public/resolver/view/process_event_dot.tsx index a8859c99c3aeb2..70fc00c92d8c78 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/process_event_dot.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/process_event_dot.tsx @@ -210,7 +210,7 @@ const StyledActionsContainer = styled.div` display: flex; flex-flow: column; font-size: ${(props) => `${props.fontSize}px`}; - left: 25%; + left: 26%; line-height: 140%; padding: 0.25rem 0 0 0.1rem; position: absolute; @@ -233,7 +233,7 @@ const StyledDescriptionText = styled.div` width: fit-content; margin: 0; text-align: left; - padding: 1px 0 1px 0; + padding: 4px 0 0 2px; color: ${(props) => props.color}; `; @@ -295,7 +295,7 @@ const ProcessEventDotComponents = React.memo( * with the correct position on the map. */ const isZoomedOut = magFactorX < 0.95; - const isShowingDescriptionText = magFactorX > 0.6; + const isShowingDescriptionText = magFactorX >= 0.55; const nodeXOffsetValue = isZoomedOut ? -0.177413 + (-magFactorX * (1 - magFactorX)) / 5 @@ -554,6 +554,7 @@ const ProcessEventDotComponents = React.memo( id={labelId} size="s" style={{ + maxHeight: `${Math.min(26 + magFactorX * 3, 32)}px`, maxWidth: `${Math.min(150 * magFactorX, 400)}px`, }} tabIndex={-1} From 44320690afa6aaf8b8b59c5c7792b1bb0127d9af Mon Sep 17 00:00:00 2001 From: Michael Olorunnisola Date: Mon, 8 Jun 2020 16:54:08 -0400 Subject: [PATCH 07/15] revert hiding events dropdown --- .../resolver/view/process_event_dot.tsx | 24 +++++++++---------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/x-pack/plugins/security_solution/public/resolver/view/process_event_dot.tsx b/x-pack/plugins/security_solution/public/resolver/view/process_event_dot.tsx index 70fc00c92d8c78..b13b3308590070 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/process_event_dot.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/process_event_dot.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useCallback, useMemo, useState, useEffect } from 'react'; +import React, { useCallback, useMemo } from 'react'; import styled from 'styled-components'; import { i18n } from '@kbn/i18n'; import { @@ -404,7 +404,7 @@ const ProcessEventDotComponents = React.memo( }); }, [animationTarget, dispatch, nodeId]); - useEffect(() => { + const handleRelatedEventRequest = useCallback(() => { dispatch({ type: 'userRequestedRelatedEventData', payload: event, @@ -422,7 +422,6 @@ const ProcessEventDotComponents = React.memo( * generally in the form `number of related events in category` `category title` * e.g. "10 DNS", "230 File" */ - const [isShowingRelatedEvents, updateIsShowingRelatedEvents] = useState(false); const relatedEventOptions = useMemo(() => { if (!relatedEventsStats) { // Return an empty set of options if there are no stats to report @@ -577,16 +576,15 @@ const ProcessEventDotComponents = React.memo( margin: 0, }} > - {isShowingRelatedEvents && ( - - - - )} + + + Date: Tue, 9 Jun 2020 10:59:29 -0400 Subject: [PATCH 08/15] update event node positioning --- .../public/resolver/store/data/selectors.ts | 2 +- .../resolver/view/process_event_dot.tsx | 25 ++++++++++--------- 2 files changed, 14 insertions(+), 13 deletions(-) diff --git a/x-pack/plugins/security_solution/public/resolver/store/data/selectors.ts b/x-pack/plugins/security_solution/public/resolver/store/data/selectors.ts index 80a07ea41418f5..c22411f0085d0c 100644 --- a/x-pack/plugins/security_solution/public/resolver/store/data/selectors.ts +++ b/x-pack/plugins/security_solution/public/resolver/store/data/selectors.ts @@ -32,7 +32,7 @@ import { } from '../../models/indexed_process_tree'; import { getRelativeTimeDifference } from '../../lib/date'; -const unit = 135; +const unit = 140; const distanceBetweenNodesInUnits = 2; export function isLoading(state: DataState) { diff --git a/x-pack/plugins/security_solution/public/resolver/view/process_event_dot.tsx b/x-pack/plugins/security_solution/public/resolver/view/process_event_dot.tsx index b13b3308590070..4a01b37e88a542 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/process_event_dot.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/process_event_dot.tsx @@ -210,7 +210,7 @@ const StyledActionsContainer = styled.div` display: flex; flex-flow: column; font-size: ${(props) => `${props.fontSize}px`}; - left: 26%; + left: 20.8%; line-height: 140%; padding: 0.25rem 0 0 0.1rem; position: absolute; @@ -294,17 +294,18 @@ const ProcessEventDotComponents = React.memo( * position to accomodate for that. This aligns the logical center of the process node * with the correct position on the map. */ - const isZoomedOut = magFactorX < 0.95; + + const isZoomedOut = magFactorX < 0.8; const isShowingDescriptionText = magFactorX >= 0.55; const nodeXOffsetValue = isZoomedOut - ? -0.177413 + (-magFactorX * (1 - magFactorX)) / 5 - : -0.172413; + ? -0.143413 + (-magFactorX * (1 - magFactorX)) / 10 + : -0.147413; const nodeYOffsetValue = isZoomedOut - ? -0.73684 + (-magFactorX * 0.5 * (1 - magFactorX)) / magFactorX - : -0.73684; + ? -0.53684 + (-magFactorX * 0.2 * (1 - magFactorX)) / magFactorX + : -0.53684; - const actionsBaseYOffsetPct = 30; + const actionsBaseYOffsetPct = 5; let actionsYOffsetPct; switch (true) { case !isZoomedOut: @@ -343,8 +344,8 @@ const ProcessEventDotComponents = React.memo( const markerBaseSize = 15; const markerSize = markerBaseSize; - const markerPositionYOffset = -markerBaseSize / 2 + 3; // + 3 to align nodes centrally on edge - const markerPositionXOffset = -markerBaseSize / 2; + const markerPositionYOffset = -markerBaseSize / 2 - 4; + const markerPositionXOffset = -markerBaseSize / 2 - 4; /** * An element that should be animated when the node is clicked. @@ -495,8 +496,8 @@ const ProcessEventDotComponents = React.memo( From 9cc6243e42d6a1bcd4950ddbb654dcf1f66914c9 Mon Sep 17 00:00:00 2001 From: Michael Olorunnisola Date: Thu, 11 Jun 2020 15:51:15 -0400 Subject: [PATCH 09/15] rebase and pr feedback changes --- .../public/resolver/lib/date.test.ts | 98 ++++++++ .../public/resolver/lib/date.ts | 77 +++++++ .../public/resolver/store/data/selectors.ts | 67 +++--- .../public/resolver/types.ts | 38 +++- .../public/resolver/view/edge_line.tsx | 48 +++- .../public/resolver/view/graph_controls.tsx | 75 +++--- .../public/resolver/view/index.tsx | 215 +++++++++--------- .../resolver/view/process_event_dot.tsx | 150 ++++++------ .../siem/public/resolver/lib/date.test.ts | 31 --- .../plugins/siem/public/resolver/lib/date.ts | 68 ------ 10 files changed, 517 insertions(+), 350 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/resolver/lib/date.test.ts create mode 100644 x-pack/plugins/security_solution/public/resolver/lib/date.ts delete mode 100644 x-pack/plugins/siem/public/resolver/lib/date.test.ts delete mode 100644 x-pack/plugins/siem/public/resolver/lib/date.ts diff --git a/x-pack/plugins/security_solution/public/resolver/lib/date.test.ts b/x-pack/plugins/security_solution/public/resolver/lib/date.test.ts new file mode 100644 index 00000000000000..0cc116a85fa578 --- /dev/null +++ b/x-pack/plugins/security_solution/public/resolver/lib/date.test.ts @@ -0,0 +1,98 @@ +/* + * 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 { getFriendlyElapsedTime } from './date'; + +describe('date', () => { + describe('getFriendlyElapsedTime', () => { + const second = 1000; + const minute = second * 60; + const hour = minute * 60; + const day = hour * 24; + const week = day * 7; + const month = day * 30; + const year = day * 365; + + const initialTime = new Date('6/1/2020').getTime(); + + const oneSecond = new Date(initialTime + 1 * second).getTime(); + const oneMinute = new Date(initialTime + 1 * minute).getTime(); + const oneHour = new Date(initialTime + 1 * hour).getTime(); + const oneDay = new Date(initialTime + 1 * day).getTime(); + const oneWeek = new Date(initialTime + 1 * week).getTime(); + const oneMonth = new Date(initialTime + 1 * month).getTime(); + const oneYear = new Date(initialTime + 1 * year).getTime(); + + const almostAMinute = new Date(initialTime + 59.9 * second).getTime(); + const almostAnHour = new Date(initialTime + 59.9 * minute).getTime(); + const almostADay = new Date(initialTime + 23.9 * hour).getTime(); + const almostAWeek = new Date(initialTime + 6.9 * day).getTime(); + const almostAMonth = new Date(initialTime + 3.9 * week).getTime(); + const almostAYear = new Date(initialTime + 11.9 * month).getTime(); + const threeYears = new Date(initialTime + 3 * year).getTime(); + + it('should return the correct singular relative time', () => { + expect(getFriendlyElapsedTime(initialTime, oneSecond)).toEqual({ + duration: 1, + durationType: 'second', + }); + expect(getFriendlyElapsedTime(initialTime, oneMinute)).toEqual({ + duration: 1, + durationType: 'minute', + }); + expect(getFriendlyElapsedTime(initialTime, oneHour)).toEqual({ + duration: 1, + durationType: 'hour', + }); + expect(getFriendlyElapsedTime(initialTime, oneDay)).toEqual({ + duration: 1, + durationType: 'day', + }); + expect(getFriendlyElapsedTime(initialTime, oneWeek)).toEqual({ + duration: 1, + durationType: 'week', + }); + expect(getFriendlyElapsedTime(initialTime, oneMonth)).toEqual({ + duration: 1, + durationType: 'month', + }); + expect(getFriendlyElapsedTime(initialTime, oneYear)).toEqual({ + duration: 1, + durationType: 'year', + }); + }); + + it('should return the correct pluralized relative time', () => { + expect(getFriendlyElapsedTime(initialTime, almostAMinute)).toEqual({ + duration: 59, + durationType: 'seconds', + }); + expect(getFriendlyElapsedTime(initialTime, almostAnHour)).toEqual({ + duration: 59, + durationType: 'minutes', + }); + expect(getFriendlyElapsedTime(initialTime, almostADay)).toEqual({ + duration: 23, + durationType: 'hours', + }); + expect(getFriendlyElapsedTime(initialTime, almostAWeek)).toEqual({ + duration: 6, + durationType: 'days', + }); + expect(getFriendlyElapsedTime(initialTime, almostAMonth)).toEqual({ + duration: 3, + durationType: 'weeks', + }); + expect(getFriendlyElapsedTime(initialTime, almostAYear)).toEqual({ + duration: 11, + durationType: 'months', + }); + expect(getFriendlyElapsedTime(initialTime, threeYears)).toEqual({ + duration: 3, + durationType: 'years', + }); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/resolver/lib/date.ts b/x-pack/plugins/security_solution/public/resolver/lib/date.ts new file mode 100644 index 00000000000000..806dadbb355580 --- /dev/null +++ b/x-pack/plugins/security_solution/public/resolver/lib/date.ts @@ -0,0 +1,77 @@ +/* + * 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 { DurationDetails, DurationTypes } from '../types'; + +export const getFriendlyElapsedTime = ( + from: number | string, + to: number | string +): DurationDetails | null => { + const startTime = typeof from === 'number' ? from : parseInt(from, 10); + const endTime = typeof to === 'number' ? to : parseInt(to, 10); + const elapsedTimeInMs = endTime - startTime; + + if (Number.isNaN(elapsedTimeInMs)) { + return null; + } + + const second = 1000; + const minute = second * 60; + const hour = minute * 60; + const day = hour * 24; + const week = day * 7; + const month = day * 30; + const year = day * 365; + + let duration: number; + let singularType: DurationTypes; + let pluralType: DurationTypes; + switch (true) { + case elapsedTimeInMs >= year: + duration = elapsedTimeInMs / year; + singularType = 'year'; + pluralType = 'years'; + break; + case elapsedTimeInMs >= month: + duration = elapsedTimeInMs / month; + singularType = 'month'; + pluralType = 'months'; + break; + case elapsedTimeInMs >= week: + duration = elapsedTimeInMs / week; + singularType = 'week'; + pluralType = 'weeks'; + break; + case elapsedTimeInMs >= day: + duration = elapsedTimeInMs / day; + singularType = 'day'; + pluralType = 'days'; + break; + case elapsedTimeInMs >= hour: + duration = elapsedTimeInMs / hour; + singularType = 'hour'; + pluralType = 'hours'; + break; + case elapsedTimeInMs >= minute: + duration = elapsedTimeInMs / minute; + singularType = 'minute'; + pluralType = 'minutes'; + break; + case elapsedTimeInMs >= second: + duration = elapsedTimeInMs / second; + singularType = 'second'; + pluralType = 'seconds'; + break; + default: + duration = elapsedTimeInMs; + singularType = 'millisecond'; + pluralType = 'milliseconds'; + break; + } + + const durationType = duration > 1 ? pluralType : singularType; + return { duration: Math.floor(duration), durationType }; +}; diff --git a/x-pack/plugins/security_solution/public/resolver/store/data/selectors.ts b/x-pack/plugins/security_solution/public/resolver/store/data/selectors.ts index c22411f0085d0c..fbb106600282f0 100644 --- a/x-pack/plugins/security_solution/public/resolver/store/data/selectors.ts +++ b/x-pack/plugins/security_solution/public/resolver/store/data/selectors.ts @@ -30,7 +30,7 @@ import { size, levelOrder, } from '../../models/indexed_process_tree'; -import { getRelativeTimeDifference } from '../../lib/date'; +import { getFriendlyElapsedTime } from '../../lib/date'; const unit = 140; const distanceBetweenNodesInUnits = 2; @@ -181,7 +181,8 @@ function processEdgeLineSegments( const parentTime = eventTimestamp(parent); const processTime = eventTimestamp(process); if (parentTime && processTime) { - edgeLineMetadata.elapsedTime = getRelativeTimeDifference(parentTime, processTime); + const elapsedTime = getFriendlyElapsedTime(parentTime, processTime); + if (elapsedTime) edgeLineMetadata.elapsedTime = elapsedTime; } /** @@ -199,18 +200,17 @@ function processEdgeLineSegments( * | | * B C */ - const lineFromProcessToMidwayLine: EdgeLineSegment = [ - [position[0], midwayY], - position, - edgeLineMetadata, - ]; + const lineFromProcessToMidwayLine: EdgeLineSegment = { + points: [[position[0], midwayY], position], + metadata: edgeLineMetadata, + }; const siblings = indexedProcessTreeChildren(indexedProcessTree, parent); const isFirstChild = process === siblings[0]; if (metadata.isOnlyChild) { // add a single line segment directly from parent to child. We don't do the 'pitchfork' in this case. - edgeLineSegments.push([parentPosition, position, edgeLineMetadata]); + edgeLineSegments.push({ points: [parentPosition, position], metadata: edgeLineMetadata }); } else if (isFirstChild) { /** * If the parent has multiple children, we draw the 'midway' line, and the line from the @@ -226,28 +226,29 @@ function processEdgeLineSegments( */ const { firstChildWidth, lastChildWidth } = metadata; - const lineFromParentToMidwayLine: EdgeLineSegment = [ - parentPosition, - [parentPosition[0], midwayY], - ]; + const lineFromParentToMidwayLine: EdgeLineSegment = { + points: [parentPosition, [parentPosition[0], midwayY]], + }; const widthOfMidline = parentWidth - firstChildWidth / 2 - lastChildWidth / 2; const minX = parentWidth / -2 + firstChildWidth / 2; const maxX = minX + widthOfMidline; - const midwayLine: EdgeLineSegment = [ - [ - // Position line relative to the parent's x component - parentPosition[0] + minX, - midwayY, + const midwayLine: EdgeLineSegment = { + points: [ + [ + // Position line relative to the parent's x component + parentPosition[0] + minX, + midwayY, + ], + [ + // Position line relative to the parent's x component + parentPosition[0] + maxX, + midwayY, + ], ], - [ - // Position line relative to the parent's x component - parentPosition[0] + maxX, - midwayY, - ], - ]; + }; edgeLineSegments.push( /* line from parent to midway line */ @@ -492,13 +493,19 @@ export const processNodePositionsAndEdgeLineSegments = createSelector( } for (const edgeLineSegment of edgeLineSegments) { - // const transformedSegment = []; - const [startPoint, endPoint, edgeLineMetadata] = edgeLineSegment; - const transformedSegment: EdgeLineSegment = [ - applyMatrix3(startPoint, isometricTransformMatrix), - applyMatrix3(endPoint, isometricTransformMatrix), - ]; - if (edgeLineMetadata) transformedSegment.push(edgeLineMetadata); + const { + points: [startPoint, endPoint], + metadata, + } = edgeLineSegment; + + const transformedSegment: EdgeLineSegment = { + points: [ + applyMatrix3(startPoint, isometricTransformMatrix), + applyMatrix3(endPoint, isometricTransformMatrix), + ], + }; + + if (metadata) transformedSegment.metadata = metadata; transformedEdgeLineSegments.push(transformedSegment); } diff --git a/x-pack/plugins/security_solution/public/resolver/types.ts b/x-pack/plugins/security_solution/public/resolver/types.ts index 2bca15ff1908db..765b2b2a26adac 100644 --- a/x-pack/plugins/security_solution/public/resolver/types.ts +++ b/x-pack/plugins/security_solution/public/resolver/types.ts @@ -242,17 +242,49 @@ export type ProcessWidths = Map; */ export type ProcessPositions = Map; +export type DurationTypes = + | 'millisecond' + | 'milliseconds' + | 'second' + | 'seconds' + | 'minute' + | 'minutes' + | 'hour' + | 'hours' + | 'day' + | 'days' + | 'week' + | 'weeks' + | 'month' + | 'months' + | 'year' + | 'years'; + +/** + * duration value and description string + */ +export interface DurationDetails { + duration: number; + durationType: DurationTypes; +} /** * Values shared between two vertices joined by an edge line. */ export interface EdgeLineMetadata { - elapsedTime?: string; + elapsedTime?: DurationDetails; } +/** + * A tuple of 2 vector2 points forming a polyline. Used to connect process nodes in the graph. + */ +export type EdgeLinePoints = Vector2[]; /** - * A tuple of 2 vector2 points and optional EdgeLineMetadata forming a polyline. Used to connect process nodes in the graph and display relevant information. + * Edge line components including the points joining the edgeline and any optional associated metadata */ -export type EdgeLineSegment = [Vector2, Vector2, EdgeLineMetadata?]; +export interface EdgeLineSegment { + points: EdgeLinePoints; + metadata?: EdgeLineMetadata; +} /** * Used to provide precalculated info from `widthsOfProcessSubtrees`. These 'width' values are used in the layout of the graph. diff --git a/x-pack/plugins/security_solution/public/resolver/view/edge_line.tsx b/x-pack/plugins/security_solution/public/resolver/view/edge_line.tsx index dcb275cbf43474..1b7f99ddfec1ea 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/edge_line.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/edge_line.tsx @@ -6,23 +6,40 @@ import React from 'react'; import styled from 'styled-components'; +import { FormattedMessage } from '@kbn/i18n/react'; import { applyMatrix3, distance, angle } from '../lib/vector2'; import { Vector2, Matrix3, EdgeLineMetadata } from '../types'; import { useResolverTheme, calculateResolverFontSize } from './assets'; -interface ElapsedTimeProps { +interface StyledEdgeLine { + readonly resolverEdgeColor: string; + readonly magFactorX: number; +} + +const StyledEdgeLine = styled.div` + position: absolute; + height: ${(props) => { + return `${calculateResolverFontSize(props.magFactorX, 12, 8.5)}px`; + }}; + background-color: ${(props) => props.resolverEdgeColor}; +`; + +interface StyledElapsedTime { readonly backgroundColor: string; readonly leftPct: number; readonly scaledTypeSize: number; readonly textColor: string; } -const StyledElapsedTime = styled.div` +const StyledElapsedTime = styled.div` background-color: ${(props) => props.backgroundColor}; color: ${(props) => props.textColor}; font-size: ${(props) => `${props.scaledTypeSize}px`}; font-weight: bold; + max-width: 75%; + overflow: hidden; position: absolute; + text-overflow: ellipsis; top: 50%; white-space: nowrap; left: ${(props) => `${props.leftPct}%`}; @@ -106,7 +123,13 @@ const EdgeLineComponent = React.memo( } return ( -
+ {elapsedTime && ( - {elapsedTime} + )} -
+
); } ); EdgeLineComponent.displayName = 'EdgeLine'; -export const EdgeLine = styled(EdgeLineComponent)` - position: absolute; - height: ${({ projectionMatrix: [magFactorX] }) => { - return `${calculateResolverFontSize(magFactorX, 12, 8.5)}px`; - }}; - background-color: ${() => useResolverTheme().colorMap.resolverEdge}; -`; +export const EdgeLine = EdgeLineComponent; diff --git a/x-pack/plugins/security_solution/public/resolver/view/graph_controls.tsx b/x-pack/plugins/security_solution/public/resolver/view/graph_controls.tsx index 4a206c657e9432..67c091627741ad 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/graph_controls.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/graph_controls.tsx @@ -15,6 +15,41 @@ import { ResolverAction, Vector2 } from '../types'; import * as selectors from '../store/selectors'; import { useResolverTheme } from './assets'; +interface StyledGraphControls { + graphControlsBackground: string; + graphControlsIconColor: string; +} + +const StyledGraphControls = styled.div` + position: absolute; + top: 5px; + right: 5px; + background-color: ${(props) => props.graphControlsBackground}; + color: ${(props) => props.graphControlsIconColor}; + + .zoom-controls { + display: flex; + flex-direction: column; + align-items: center; + padding: 5px 0px; + + .zoom-slider { + width: 20px; + height: 150px; + margin: 5px 0px 2px 0px; + + input[type='range'] { + width: 150px; + height: 20px; + transform-origin: 75px 75px; + transform: rotate(-90deg); + } + } + } + .panning-controls { + text-align: center; + } +`; /** * Controls for zooming, panning, and centering in Resolver */ @@ -30,6 +65,7 @@ const GraphControlsComponent = React.memo( const dispatch: (action: ResolverAction) => unknown = useDispatch(); const scalingFactor = useSelector(selectors.scalingFactor); const { timestamp } = useContext(SideEffectContext); + const { colorMap } = useResolverTheme(); const handleZoomAmountChange = useCallback( (event: React.ChangeEvent | React.MouseEvent) => { @@ -84,7 +120,11 @@ const GraphControlsComponent = React.memo( }, [dispatch, timestamp]); return ( -
+
-
+
); } ); GraphControlsComponent.displayName = 'GraphControlsComponent'; -export const GraphControls = styled(GraphControlsComponent)` - position: absolute; - top: 5px; - right: 5px; - background-color: ${() => useResolverTheme().colorMap.graphControlsBackground}; - color: ${() => useResolverTheme().colorMap.graphControls}; - - .zoom-controls { - display: flex; - flex-direction: column; - align-items: center; - padding: 5px 0px; - - .zoom-slider { - width: 20px; - height: 150px; - margin: 5px 0px 2px 0px; - - input[type='range'] { - width: 150px; - height: 20px; - transform-origin: 75px 75px; - transform: rotate(-90deg); - } - } - } - .panning-controls { - text-align: center; - } -`; +export const GraphControls = GraphControlsComponent; diff --git a/x-pack/plugins/security_solution/public/resolver/view/index.tsx b/x-pack/plugins/security_solution/public/resolver/view/index.tsx index 30919f10eb26b6..0e15cd5c4e1dac 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/index.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/index.tsx @@ -20,109 +20,11 @@ import { ResolverAction } from '../types'; import { ResolverEvent } from '../../../common/endpoint/types'; import * as eventModel from '../../../common/endpoint/models/event'; -const StyledPanel = styled(Panel)` - position: absolute; - left: 1em; - top: 1em; - max-height: calc(100% - 2em); - overflow: auto; - width: 25em; - max-width: 50%; -`; - -const StyledResolverContainer = styled.div` - display: flex; - flex-grow: 1; - contain: layout; -`; - -export const Resolver = styled( - React.memo(function Resolver({ - className, - selectedEvent, - }: { - className?: string; - selectedEvent?: ResolverEvent; - }) { - const { processNodePositions, edgeLineSegments } = useSelector( - selectors.processNodePositionsAndEdgeLineSegments - ); - - const dispatch: (action: ResolverAction) => unknown = useDispatch(); - const { processToAdjacencyMap } = useSelector(selectors.processAdjacencies); - const relatedEventsStats = useSelector(selectors.relatedEventsStats); - const { projectionMatrix, ref, onMouseDown } = useCamera(); - const isLoading = useSelector(selectors.isLoading); - const hasError = useSelector(selectors.hasError); - const activeDescendantId = useSelector(selectors.uiActiveDescendantId); - - useLayoutEffect(() => { - dispatch({ - type: 'userChangedSelectedEvent', - payload: { selectedEvent }, - }); - }, [dispatch, selectedEvent]); +interface StyledResolver { + backgroundColor: string; +} - return ( -
- {isLoading ? ( -
- -
- ) : hasError ? ( -
-
- {' '} - -
-
- ) : ( - - {edgeLineSegments.map(([startPosition, endPosition, edgeLineMetadata], 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 ( - - ); - })} - - )} - - - -
- ); - }) -)` +const StyledResolver = styled.div` /** * Take up all availble space */ @@ -146,5 +48,112 @@ export const Resolver = styled( */ overflow: hidden; contain: strict; - background-color: ${() => useResolverTheme().colorMap.resolverBackground}; + background-color: ${(props) => props.backgroundColor}; `; + +const StyledPanel = styled(Panel)` + position: absolute; + left: 1em; + top: 1em; + max-height: calc(100% - 2em); + overflow: auto; + width: 25em; + max-width: 50%; +`; + +const StyledResolverContainer = styled.div` + display: flex; + flex-grow: 1; + contain: layout; +`; + +export const Resolver = React.memo(function Resolver({ + className, + selectedEvent, +}: { + className?: string; + selectedEvent?: ResolverEvent; +}) { + const { processNodePositions, edgeLineSegments } = useSelector( + selectors.processNodePositionsAndEdgeLineSegments + ); + + const dispatch: (action: ResolverAction) => unknown = useDispatch(); + const { processToAdjacencyMap } = useSelector(selectors.processAdjacencies); + const relatedEventsStats = useSelector(selectors.relatedEventsStats); + const { projectionMatrix, ref, onMouseDown } = useCamera(); + const isLoading = useSelector(selectors.isLoading); + const hasError = useSelector(selectors.hasError); + const activeDescendantId = useSelector(selectors.uiActiveDescendantId); + const { colorMap } = useResolverTheme(); + + useLayoutEffect(() => { + dispatch({ + type: 'userChangedSelectedEvent', + payload: { selectedEvent }, + }); + }, [dispatch, selectedEvent]); + + return ( + + {isLoading ? ( +
+ +
+ ) : hasError ? ( +
+
+ {' '} + +
+
+ ) : ( + + {edgeLineSegments.map(({ points: [startPosition, endPosition], metadata }, 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/security_solution/public/resolver/view/process_event_dot.tsx b/x-pack/plugins/security_solution/public/resolver/view/process_event_dot.tsx index 4a01b37e88a542..b7a1e797b9cec8 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/process_event_dot.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/process_event_dot.tsx @@ -210,7 +210,7 @@ const StyledActionsContainer = styled.div` display: flex; flex-flow: column; font-size: ${(props) => `${props.fontSize}px`}; - left: 20.8%; + left: 20.9%; line-height: 140%; padding: 0.25rem 0 0 0.1rem; position: absolute; @@ -221,20 +221,22 @@ const StyledActionsContainer = styled.div` interface StyledDescriptionText { readonly backgroundColor: string; readonly color: string; + readonly isDisplaying: boolean; } const StyledDescriptionText = styled.div` - text-transform: uppercase; - letter-spacing: -0.01px; background-color: ${(props) => props.backgroundColor}; - line-height: 1; - font-weight: bold; + color: ${(props) => props.color}; + display: ${(props) => (props.isDisplaying ? 'block' : 'none')}; font-size: 0.8rem; - width: fit-content; + font-weight: bold; + letter-spacing: -0.01px; + line-height: 1; margin: 0; - text-align: left; padding: 4px 0 0 2px; - color: ${(props) => props.color}; + text-align: left; + text-transform: uppercase; + width: fit-content; `; /** @@ -286,39 +288,46 @@ const ProcessEventDotComponents = React.memo( const activeDescendantId = useSelector(selectors.uiActiveDescendantId); const selectedDescendantId = useSelector(selectors.uiSelectedDescendantId); - const logicalProcessNodeViewWidth = 360; - const logicalProcessNodeViewHeight = 120; - /** - * The `left` and `top` values represent the 'center' point of the process node. - * Since the view has content to the left and above the 'center' point, offset the - * position to accomodate for that. This aligns the logical center of the process node - * with the correct position on the map. - */ - - const isZoomedOut = magFactorX < 0.8; + const isShowingEventActions = magFactorX > 0.8; const isShowingDescriptionText = magFactorX >= 0.55; - const nodeXOffsetValue = isZoomedOut - ? -0.143413 + (-magFactorX * (1 - magFactorX)) / 10 - : -0.147413; - const nodeYOffsetValue = isZoomedOut - ? -0.53684 + (-magFactorX * 0.2 * (1 - magFactorX)) / magFactorX - : -0.53684; - - const actionsBaseYOffsetPct = 5; - let actionsYOffsetPct; + /** + * As the resolver zooms and buttons and text change visibility, we look to keep the overall container properly vertically aligned + */ + const actionalButtonsBaseTopOffset = 5; + let actionableButtonsTopOffset; switch (true) { - case !isZoomedOut: - actionsYOffsetPct = actionsBaseYOffsetPct + 3.5 * magFactorX; + case isShowingEventActions: + actionableButtonsTopOffset = actionalButtonsBaseTopOffset + 3.5 * magFactorX; break; case isShowingDescriptionText: - actionsYOffsetPct = actionsBaseYOffsetPct + magFactorX; + actionableButtonsTopOffset = actionalButtonsBaseTopOffset + magFactorX; break; default: - actionsYOffsetPct = actionsBaseYOffsetPct + 21 * magFactorX; + actionableButtonsTopOffset = actionalButtonsBaseTopOffset + 21 * magFactorX; break; } + /** + * The `left` and `top` values represent the 'center' point of the process node. + * Since the view has content to the left and above the 'center' point, offset the + * position to accomodate for that. This aligns the logical center of the process node + * with the correct position on the map. + */ + + const logicalProcessNodeViewWidth = 360; + const logicalProcessNodeViewHeight = 120; + + /** + * As the scale changes and button visibility toggles on the graph, these offsets help scale to keep the nodes centered on the edge + */ + const nodeXOffsetValue = isShowingEventActions + ? -0.147413 + : -0.147413 - (magFactorX - 0.5) * 0.08; + const nodeYOffsetValue = isShowingEventActions + ? -0.53684 + : -0.53684 + (-magFactorX * 0.2 * (1 - magFactorX)) / magFactorX; + const processNodeViewXOffset = nodeXOffsetValue * logicalProcessNodeViewWidth * magFactorX; const processNodeViewYOffset = nodeYOffsetValue * logicalProcessNodeViewHeight * magFactorX; @@ -530,16 +539,15 @@ const ProcessEventDotComponents = React.memo( - {isShowingDescriptionText && ( - - {descriptionText} - - )} + + {descriptionText} +
@@ -566,36 +575,35 @@ const ProcessEventDotComponents = React.memo(
- {!isZoomedOut && ( - - - - - - - - - )} + + + + + + + +
diff --git a/x-pack/plugins/siem/public/resolver/lib/date.test.ts b/x-pack/plugins/siem/public/resolver/lib/date.test.ts deleted file mode 100644 index 778eab6af57521..00000000000000 --- a/x-pack/plugins/siem/public/resolver/lib/date.test.ts +++ /dev/null @@ -1,31 +0,0 @@ -/* - * 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 moment from 'moment'; -import { getRelativeTimeDifference } from './date'; - -describe('date', () => { - describe('getRelativeTimeDifference', () => { - const initialTime = new Date('6/1/2020'); - const momentDate = moment(initialTime); - const fiveSeconds = momentDate.add(5, 's').toDate(); - const fiveMinutes = momentDate.add(5, 'm').toDate(); - const fiveHours = momentDate.add(5, 'h').toDate(); - const fiveDays = momentDate.add(5, 'd').toDate(); - const threeWeeks = momentDate.add(3, 'w').toDate(); - const threeMonths = momentDate.add(3, 'M').toDate(); - const threeYears = momentDate.add(3, 'y').toDate(); - - it('should return the correct relative time', () => { - expect(getRelativeTimeDifference(initialTime, fiveSeconds)).toBe('5 seconds'); - expect(getRelativeTimeDifference(initialTime, fiveMinutes)).toBe('5 minutes'); - expect(getRelativeTimeDifference(initialTime, fiveHours)).toBe('5 hours'); - expect(getRelativeTimeDifference(initialTime, fiveDays)).toBe('5 days'); - expect(getRelativeTimeDifference(initialTime, threeWeeks)).toBe('3 weeks'); - expect(getRelativeTimeDifference(initialTime, threeMonths)).toBe('4 months'); - expect(getRelativeTimeDifference(initialTime, threeYears)).toBe('3 years'); - }); - }); -}); diff --git a/x-pack/plugins/siem/public/resolver/lib/date.ts b/x-pack/plugins/siem/public/resolver/lib/date.ts deleted file mode 100644 index e7be1ff0661968..00000000000000 --- a/x-pack/plugins/siem/public/resolver/lib/date.ts +++ /dev/null @@ -1,68 +0,0 @@ -/* - * 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 moment from 'moment'; -import { i18n } from '@kbn/i18n'; - -type DateFormat = Date | number | string; -export const getRelativeTimeDifference = (startDate: DateFormat, endDate: DateFormat): string => { - // Set the threshold above which pluralization takes place https://momentjs.com/docs/#/customization/relative-time-threshold/ - moment.relativeTimeThreshold('s', 60); // Least number of seconds to be considered minute - moment.relativeTimeThreshold('ss', 1); - moment.relativeTimeThreshold('m', 60); // Least number of minutes to be considered an hour - moment.relativeTimeThreshold('mm', 1); - moment.relativeTimeThreshold('h', 24); // Least number of hours to be considered a day - moment.relativeTimeThreshold('hh', 1); - moment.relativeTimeThreshold('d', 28); // Least number of days to be considered a month - moment.relativeTimeThreshold('dd', 1); - moment.relativeTimeThreshold('M', 12); // Least number of months to be considered a year - moment.relativeTimeThreshold('MM', 1); - moment.relativeTimeThreshold('yy', 1); - - /** Currently on version 2.24.0 of moment. Weeks were added in 2.25.0. Will need to update 'd' and these when updated */ - - // moment.relativeTimeThreshold('w', 4); // Least number of days to be considered a month - // moment.relativeTimeThreshold('ww', 1); - - // Explicitly configure the text format being used - moment.updateLocale('en', { - relativeTime: { - future: 'in %s', - past: '%s ago', - s: '%d second', - ss: '%d seconds', - m: '%d minute', - mm: '%d minutes', - h: '%d hour', - // w: '%d week', - // ww: '%d weeks', - hh: '%d hours', - d: '%d day', - dd: '%d days', - M: '%d month', - MM: '%d months', - y: '%d year', - yy: '%d years', - }, - }); - - moment.locale(i18n.getLocale()); - - const momentEndDate = moment(endDate); - const momentStartDate = moment(startDate); - let elapsedTime = momentEndDate.from(momentStartDate, true); - - if (elapsedTime.includes('days')) { - // This can be removed when updated to version 2.25.0 of moment.js - const [numStr] = elapsedTime.split(' '); - const weekVal = Math.floor(parseInt(numStr, 10) / 7); - if (weekVal > 0) { - elapsedTime = `${weekVal} ${weekVal > 1 ? 'weeks' : ' week'}`; - } - } - - return elapsedTime; -}; From 900d241cab3c16026600b58ed7ccdc34fc806daa Mon Sep 17 00:00:00 2001 From: Michael Olorunnisola Date: Fri, 12 Jun 2020 09:06:42 -0400 Subject: [PATCH 10/15] fix tests --- .../data/__snapshots__/graphing.test.ts.snap | 328 ++++++++++-------- .../public/resolver/view/assets.tsx | 22 +- .../public/resolver/view/edge_line.tsx | 2 +- 3 files changed, 193 insertions(+), 159 deletions(-) diff --git a/x-pack/plugins/security_solution/public/resolver/store/data/__snapshots__/graphing.test.ts.snap b/x-pack/plugins/security_solution/public/resolver/store/data/__snapshots__/graphing.test.ts.snap index 4118953ed6d54e..f21d3b21068127 100644 --- a/x-pack/plugins/security_solution/public/resolver/store/data/__snapshots__/graphing.test.ts.snap +++ b/x-pack/plugins/security_solution/public/resolver/store/data/__snapshots__/graphing.test.ts.snap @@ -35,143 +35,169 @@ Object { exports[`resolver graph layout when rendering two forks, and one fork has an extra long tine renders right 1`] = ` Object { "edgeLineSegments": Array [ - Array [ - Array [ - 0, - -0.8164965809277259, - ], - Array [ - 190.91883092036784, - -111.04353500617071, - ], - ], - Array [ - Array [ - 0, - -221.2705734314137, - ], - Array [ - 381.8376618407357, - -0.8164965809277259, - ], - ], - Array [ - Array [ - 0, - -221.2705734314137, - ], - Array [ - 190.91883092036784, - -331.4976118566567, - ], - Object {}, - ], - Array [ - Array [ - 381.8376618407357, - -0.8164965809277259, - ], - Array [ - 572.7564927611036, - -111.04353500617071, - ], - Object {}, - ], - Array [ - Array [ - 190.91883092036784, - -331.4976118566567, - ], - Array [ - 381.83766184073573, - -441.7246502818997, - ], - ], - Array [ - Array [ - 286.3782463805518, - -496.8381694945212, - ], - Array [ - 477.2970773009197, - -386.6111310692782, - ], - ], - Array [ - Array [ - 286.3782463805518, - -496.8381694945212, - ], - Array [ - 477.29707730091957, - -607.0652079197642, + Object { + "points": Array [ + Array [ + 0, + -0.8164965809277259, + ], + Array [ + 197.9898987322333, + -115.12601791080935, + ], ], - Object {}, - ], - Array [ - Array [ - 477.2970773009197, - -386.6111310692782, + }, + Object { + "points": Array [ + Array [ + 0, + -229.43553924069099, + ], + Array [ + 395.9797974644666, + -0.8164965809277259, + ], ], - Array [ - 668.2159082212875, - -496.8381694945212, + }, + Object { + "metadata": Object {}, + "points": Array [ + Array [ + 0, + -229.43553924069099, + ], + Array [ + 197.9898987322333, + -343.7450605705726, + ], ], - Object {}, - ], - Array [ - Array [ - 572.7564927611036, - -111.04353500617071, + }, + Object { + "metadata": Object {}, + "points": Array [ + Array [ + 395.9797974644666, + -0.8164965809277259, + ], + Array [ + 593.9696961966999, + -115.12601791080935, + ], ], - Array [ - 763.6753236814714, - -221.2705734314137, + }, + Object { + "points": Array [ + Array [ + 197.9898987322333, + -343.7450605705726, + ], + Array [ + 395.9797974644666, + -458.05458190045425, + ], ], - ], - Array [ - Array [ - 668.2159082212875, - -276.3840926440352, + }, + Object { + "points": Array [ + Array [ + 296.98484809834997, + -515.2093425653951, + ], + Array [ + 494.9747468305833, + -400.8998212355134, + ], ], - Array [ - 859.1347391416554, - -166.1570542187922, + }, + Object { + "metadata": Object {}, + "points": Array [ + Array [ + 296.98484809834997, + -515.2093425653951, + ], + Array [ + 494.9747468305833, + -629.5188638952767, + ], ], - ], - Array [ - Array [ - 668.2159082212875, - -276.3840926440352, + }, + Object { + "metadata": Object {}, + "points": Array [ + Array [ + 494.9747468305833, + -400.8998212355134, + ], + Array [ + 692.9646455628166, + -515.2093425653951, + ], ], - Array [ - 859.1347391416552, - -386.6111310692782, + }, + Object { + "points": Array [ + Array [ + 593.9696961966999, + -115.12601791080935, + ], + Array [ + 791.9595949289333, + -229.43553924069096, + ], ], - Object {}, - ], - Array [ - Array [ - 859.1347391416554, - -166.1570542187922, + }, + Object { + "points": Array [ + Array [ + 692.9646455628166, + -286.5902999056318, + ], + Array [ + 890.9545442950499, + -172.28077857575016, + ], ], - Array [ - 1050.053570062023, - -276.3840926440352, + }, + Object { + "metadata": Object {}, + "points": Array [ + Array [ + 692.9646455628166, + -286.5902999056318, + ], + Array [ + 890.9545442950499, + -400.89982123551346, + ], ], - Object {}, - ], - Array [ - Array [ - 1050.053570062023, - -276.3840926440352, + }, + Object { + "metadata": Object {}, + "points": Array [ + Array [ + 890.9545442950499, + -172.28077857575016, + ], + Array [ + 1088.9444430272833, + -286.5902999056318, + ], ], - Array [ - 1240.972400982391, - -386.6111310692782, + }, + Object { + "metadata": Object {}, + "points": Array [ + Array [ + 1088.9444430272833, + -286.5902999056318, + ], + Array [ + 1286.9343417595164, + -400.89982123551346, + ], ], - Object {}, - ], + }, ], "processNodePositions": Map { Object { @@ -205,8 +231,8 @@ Object { "unique_ppid": 0, }, } => Array [ - 190.91883092036784, - -331.4976118566567, + 197.9898987322333, + -343.7450605705726, ], Object { "@timestamp": 1582233383000, @@ -222,8 +248,8 @@ Object { "unique_ppid": 0, }, } => Array [ - 572.7564927611036, - -111.04353500617071, + 593.9696961966999, + -115.12601791080935, ], Object { "@timestamp": 1582233383000, @@ -239,8 +265,8 @@ Object { "unique_ppid": 1, }, } => Array [ - 477.29707730091957, - -607.0652079197642, + 494.9747468305833, + -629.5188638952767, ], Object { "@timestamp": 1582233383000, @@ -256,8 +282,8 @@ Object { "unique_ppid": 1, }, } => Array [ - 668.2159082212875, - -496.8381694945212, + 692.9646455628166, + -515.2093425653951, ], Object { "@timestamp": 1582233383000, @@ -273,8 +299,8 @@ Object { "unique_ppid": 2, }, } => Array [ - 859.1347391416552, - -386.6111310692782, + 890.9545442950499, + -400.89982123551346, ], Object { "@timestamp": 1582233383000, @@ -290,8 +316,8 @@ Object { "unique_ppid": 2, }, } => Array [ - 1050.053570062023, - -276.3840926440352, + 1088.9444430272833, + -286.5902999056318, ], Object { "@timestamp": 1582233383000, @@ -307,8 +333,8 @@ Object { "unique_ppid": 6, }, } => Array [ - 1240.972400982391, - -386.6111310692782, + 1286.9343417595164, + -400.89982123551346, ], }, } @@ -317,17 +343,19 @@ Object { exports[`resolver graph layout when rendering two nodes, one being the parent of the other renders right 1`] = ` Object { "edgeLineSegments": Array [ - Array [ - Array [ - 0, - -0.8164965809277259, - ], - Array [ - 190.91883092036784, - -111.04353500617071, + Object { + "metadata": Object {}, + "points": Array [ + Array [ + 0, + -0.8164965809277259, + ], + Array [ + 197.9898987322333, + -115.12601791080935, + ], ], - Object {}, - ], + }, ], "processNodePositions": Map { Object { @@ -361,8 +389,8 @@ Object { "unique_ppid": 0, }, } => Array [ - 190.91883092036784, - -111.04353500617071, + 197.9898987322333, + -115.12601791080935, ], }, } diff --git a/x-pack/plugins/security_solution/public/resolver/view/assets.tsx b/x-pack/plugins/security_solution/public/resolver/view/assets.tsx index c83dfab0463041..150ab3d93a8c7f 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/assets.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/assets.tsx @@ -430,7 +430,7 @@ export const useResolverTheme = (): { colorMap: ColorMap; nodeAssets: NodeStyleM backingFill: colorMap.processBackingFill, cubeSymbol: `#${SymbolIds.runningProcessCube}`, descriptionFill: colorMap.descriptionText, - descriptionText: i18n.translate('xpack.siem.endpoint.resolver.runningProcess', { + descriptionText: i18n.translate('xpack.securitySolution.endpoint.resolver.runningProcess', { defaultMessage: 'Running Process', }), isLabelFilled: true, @@ -441,7 +441,7 @@ export const useResolverTheme = (): { colorMap: ColorMap; nodeAssets: NodeStyleM backingFill: colorMap.triggerBackingFill, cubeSymbol: `#${SymbolIds.runningTriggerCube}`, descriptionFill: colorMap.descriptionText, - descriptionText: i18n.translate('xpack.siem.endpoint.resolver.runningTrigger', { + descriptionText: i18n.translate('xpack.securitySolution.endpoint.resolver.runningTrigger', { defaultMessage: 'Running Trigger', }), isLabelFilled: true, @@ -452,9 +452,12 @@ export const useResolverTheme = (): { colorMap: ColorMap; nodeAssets: NodeStyleM backingFill: colorMap.processBackingFill, cubeSymbol: `#${SymbolIds.terminatedProcessCube}`, descriptionFill: colorMap.descriptionText, - descriptionText: i18n.translate('xpack.siem.endpoint.resolver.terminatedProcess', { - defaultMessage: 'Terminated Process', - }), + descriptionText: i18n.translate( + 'xpack.securitySolution.endpoint.resolver.terminatedProcess', + { + defaultMessage: 'Terminated Process', + } + ), isLabelFilled: false, labelButtonFill: 'primary', strokeColor: `${theme.euiColorPrimary}33`, // 33 = 20% opacity @@ -463,9 +466,12 @@ export const useResolverTheme = (): { colorMap: ColorMap; nodeAssets: NodeStyleM backingFill: colorMap.triggerBackingFill, cubeSymbol: `#${SymbolIds.terminatedTriggerCube}`, descriptionFill: colorMap.descriptionText, - descriptionText: i18n.translate('xpack.siem.endpoint.resolver.terminatedTrigger', { - defaultMessage: 'Terminated Trigger', - }), + descriptionText: i18n.translate( + 'xpack.securitySolution.endpoint.resolver.terminatedTrigger', + { + defaultMessage: 'Terminated Trigger', + } + ), isLabelFilled: false, labelButtonFill: 'danger', strokeColor: `${theme.euiColorDanger}33`, diff --git a/x-pack/plugins/security_solution/public/resolver/view/edge_line.tsx b/x-pack/plugins/security_solution/public/resolver/view/edge_line.tsx index 1b7f99ddfec1ea..c4b9811f632329 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/edge_line.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/edge_line.tsx @@ -138,7 +138,7 @@ const EdgeLineComponent = React.memo( textColor={colorMap.resolverEdgeText} > Date: Fri, 12 Jun 2020 11:04:35 -0400 Subject: [PATCH 11/15] removed old defs file --- .../public/resolver/view/defs.tsx | 472 ------------------ 1 file changed, 472 deletions(-) delete mode 100644 x-pack/plugins/security_solution/public/resolver/view/defs.tsx diff --git a/x-pack/plugins/security_solution/public/resolver/view/defs.tsx b/x-pack/plugins/security_solution/public/resolver/view/defs.tsx deleted file mode 100644 index 5c92253c74eaa1..00000000000000 --- a/x-pack/plugins/security_solution/public/resolver/view/defs.tsx +++ /dev/null @@ -1,472 +0,0 @@ -/* - * 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 euiThemeAmsterdamDark from '@elastic/eui/dist/eui_theme_amsterdam_dark.json'; -import euiThemeAmsterdamLight from '@elastic/eui/dist/eui_theme_amsterdam_light.json'; -import { htmlIdGenerator, ButtonColor } from '@elastic/eui'; -import styled from 'styled-components'; -import { i18n } from '@kbn/i18n'; -import { useUiSetting } from '../../common/lib/kibana'; -import { DEFAULT_DARK_MODE } from '../../../common/constants'; - -type ResolverColorNames = - | 'descriptionText' - | 'full' - | 'graphControls' - | 'graphControlsBackground' - | 'resolverBackground' - | 'resolverEdge' - | 'resolverEdgeText'; - -type ColorMap = Record; -interface NodeStyleConfig { - backingFill: string; - cubeSymbol: string; - descriptionFill: string; - descriptionText: string; - isLabelFilled: boolean; - labelButtonFill: ButtonColor; -} - -export interface NodeStyleMap { - runningProcessCube: NodeStyleConfig; - runningTriggerCube: NodeStyleConfig; - terminatedProcessCube: NodeStyleConfig; - terminatedTriggerCube: NodeStyleConfig; -} - -const idGenerator = htmlIdGenerator(); - -/** - * Ids of paint servers to be referenced by fill and stroke attributes - */ -export const PaintServerIds = { - 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(({ isDarkMode }: { isDarkMode: boolean }) => ( - <> - - - - - - - - - {isDarkMode ? ( - <> - - - - - - - - - - ) : ( - <> - - - - - - - - - - )} - -)); - -PaintServers.displayName = 'PaintServers'; - -/** - * Ids of symbols to be linked by elements - */ -export const SymbolIds = { - processNodeLabel: idGenerator('nodeSymbol'), - runningProcessCube: idGenerator('runningCube'), - runningTriggerCube: idGenerator('runningTriggerCube'), - terminatedProcessCube: idGenerator('terminatedCube'), - terminatedTriggerCube: idGenerator('terminatedTriggerCube'), - processCubeActiveBacking: idGenerator('activeBacking'), -}; - -/** - * Defs entries that define shapes, masks and other spatial elements - */ -const SymbolsAndShapes = memo(({ isDarkMode }: { isDarkMode: boolean }) => ( - <> - - - - - {'Running Process'} - - - - - - - - - - - - - - {'resolver_dark process running'} - - - - - - - - - - - - - - {'Terminated Process'} - - - - - - - - - {'Terminated Trigger Process'} - {isDarkMode && ( - - )} - - {!isDarkMode && ( - - )} - - - - - - - {'resolver active backing'} - - - -)); - -SymbolsAndShapes.displayName = 'SymbolsAndShapes'; - -/** - * 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) - */ -const SymbolDefinitionsComponent = memo(({ className }: { className?: string }) => { - const isDarkMode = useUiSetting(DEFAULT_DARK_MODE); - return ( - - - - - - - ); -}); - -SymbolDefinitionsComponent.displayName = 'SymbolDefinitions'; - -export const SymbolDefinitions = styled(SymbolDefinitionsComponent)` - position: absolute; - left: 100%; - top: 100%; - width: 0; - height: 0; -`; - -export const useResolverTheme = (): { colorMap: ColorMap; nodeAssets: NodeStyleMap } => { - const isDarkMode = useUiSetting(DEFAULT_DARK_MODE); - const theme = isDarkMode ? euiThemeAmsterdamDark : euiThemeAmsterdamLight; - - const getThemedOption = (lightOption: string, darkOption: string): string => { - return isDarkMode ? darkOption : lightOption; - }; - - const colorMap = { - descriptionText: theme.euiColorDarkestShade, - full: theme.euiColorFullShade, - graphControls: theme.euiColorDarkestShade, - graphControlsBackground: theme.euiColorEmptyShade, - processBackingFill: theme.euiColorPrimary, - resolverBackground: theme.euiColorEmptyShade, - resolverEdge: getThemedOption(theme.euiColorLightestShade, theme.euiColorLightShade), - resolverEdgeText: getThemedOption(theme.euiColorDarkShade, theme.euiColorFullShade), - triggerBackingFill: theme.euiColorDanger, - }; - - const nodeAssets: NodeStyleMap = { - runningProcessCube: { - backingFill: colorMap.processBackingFill, - cubeSymbol: `#${SymbolIds.runningProcessCube}`, - descriptionFill: colorMap.descriptionText, - descriptionText: i18n.translate('xpack.siem.endpoint.resolver.runningProcess', { - defaultMessage: 'Running Process', - }), - isLabelFilled: true, - labelButtonFill: 'primary', - }, - runningTriggerCube: { - backingFill: colorMap.triggerBackingFill, - cubeSymbol: `#${SymbolIds.runningTriggerCube}`, - descriptionFill: colorMap.descriptionText, - descriptionText: i18n.translate('xpack.siem.endpoint.resolver.runningTrigger', { - defaultMessage: 'Running Trigger', - }), - isLabelFilled: true, - labelButtonFill: 'danger', - }, - terminatedProcessCube: { - backingFill: colorMap.processBackingFill, - cubeSymbol: `#${SymbolIds.terminatedProcessCube}`, - descriptionFill: colorMap.descriptionText, - descriptionText: i18n.translate('xpack.siem.endpoint.resolver.terminatedProcess', { - defaultMessage: 'Terminated Process', - }), - isLabelFilled: false, - labelButtonFill: 'primary', - }, - terminatedTriggerCube: { - backingFill: colorMap.triggerBackingFill, - cubeSymbol: `#${SymbolIds.terminatedTriggerCube}`, - descriptionFill: colorMap.descriptionText, - descriptionText: i18n.translate('xpack.siem.endpoint.resolver.terminatedTrigger', { - defaultMessage: 'Terminated Trigger', - }), - isLabelFilled: false, - labelButtonFill: 'danger', - }, - }; - - return { colorMap, nodeAssets }; -}; From e269a1af7ad3e7b410622005ba01cdaa184b290e Mon Sep 17 00:00:00 2001 From: Michael Olorunnisola Date: Fri, 12 Jun 2020 13:18:36 -0400 Subject: [PATCH 12/15] added comment --- x-pack/plugins/security_solution/public/resolver/lib/date.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/x-pack/plugins/security_solution/public/resolver/lib/date.ts b/x-pack/plugins/security_solution/public/resolver/lib/date.ts index 806dadbb355580..de0f9dcd7efbea 100644 --- a/x-pack/plugins/security_solution/public/resolver/lib/date.ts +++ b/x-pack/plugins/security_solution/public/resolver/lib/date.ts @@ -6,6 +6,11 @@ import { DurationDetails, DurationTypes } from '../types'; +/* + * Given two unix timestamps, it will return an object containing the time difference and properly pluralized friendly version of the time difference. + * i.e. a time difference of 1000ms will yield => { duration: 1, durationType: 'second' } and 10000ms will yield => { duration: 10, durationType: 'seconds' } + * + */ export const getFriendlyElapsedTime = ( from: number | string, to: number | string From bdd87fd1081fc821048aba0f0978740a0e0e473b Mon Sep 17 00:00:00 2001 From: Michael Olorunnisola Date: Fri, 12 Jun 2020 14:34:44 -0400 Subject: [PATCH 13/15] added comment and popover id --- .../security_solution/public/resolver/view/edge_line.tsx | 3 +++ .../security_solution/public/resolver/view/submenu.tsx | 9 +++++---- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/x-pack/plugins/security_solution/public/resolver/view/edge_line.tsx b/x-pack/plugins/security_solution/public/resolver/view/edge_line.tsx index c4b9811f632329..4eccb4f5602209 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/edge_line.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/edge_line.tsx @@ -117,6 +117,9 @@ const EdgeLineComponent = React.memo( let elapsedTimeLeftPosPct = 50; + /** + * Calculates a fractional offset from 0 -> 5% as magFactorX decreases from 1 to a min of .5 + */ if (magFactorX < 1) { const fractionalOffset = (1 / magFactorX) * ((1 - magFactorX) * 10); elapsedTimeLeftPosPct += fractionalOffset; diff --git a/x-pack/plugins/security_solution/public/resolver/view/submenu.tsx b/x-pack/plugins/security_solution/public/resolver/view/submenu.tsx index 80091c90287f8e..861c170b8b0b8f 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/submenu.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/submenu.tsx @@ -6,7 +6,7 @@ import { i18n } from '@kbn/i18n'; import React, { ReactNode, useState, useMemo, useCallback } from 'react'; -import { EuiSelectable, EuiButton, EuiPopover, ButtonColor } from '@elastic/eui'; +import { EuiSelectable, EuiButton, EuiPopover, ButtonColor, htmlIdGenerator } from '@elastic/eui'; import styled from 'styled-components'; /** @@ -35,7 +35,7 @@ export const subMenuAssets = { }), }, }; - +const idGenerator = htmlIdGenerator(); interface ResolverSubmenuOption { optionTitle: string; action: () => unknown; @@ -137,7 +137,8 @@ const NodeSubMenuComponents = React.memo( [menuAction] ); - const closePopover = () => setMenuOpen(false); + const closePopover = useCallback(() => setMenuOpen(false), []); + const popoverId = idGenerator('submenu-popover'); const isMenuLoading = optionsWithActions === 'waitingForRelatedEventData'; @@ -182,7 +183,7 @@ const NodeSubMenuComponents = React.memo( return (
Date: Fri, 12 Jun 2020 18:13:19 -0400 Subject: [PATCH 14/15] rebase and minor fix --- .../resolver/view/process_event_dot.tsx | 40 +++++++++++-------- 1 file changed, 23 insertions(+), 17 deletions(-) diff --git a/x-pack/plugins/security_solution/public/resolver/view/process_event_dot.tsx b/x-pack/plugins/security_solution/public/resolver/view/process_event_dot.tsx index b7a1e797b9cec8..7b463f0bed26af 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/process_event_dot.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/process_event_dot.tsx @@ -18,7 +18,7 @@ import { import { useSelector } from 'react-redux'; import { NodeSubMenu, subMenuAssets } from './submenu'; import { applyMatrix3 } from '../lib/vector2'; -import { Vector2, Matrix3, AdjacentProcessMap } from '../types'; +import { Vector2, Matrix3, AdjacentProcessMap, ResolverProcessType } from '../types'; import { SymbolIds, useResolverTheme, NodeStyleMap, calculateResolverFontSize } from './assets'; import { ResolverEvent, ResolverNodeStats } from '../../../common/endpoint/types'; import { useResolverDispatch } from './use_resolver_dispatch'; @@ -433,27 +433,33 @@ const ProcessEventDotComponents = React.memo( * e.g. "10 DNS", "230 File" */ const relatedEventOptions = useMemo(() => { + const relatedStatsList = []; + if (!relatedEventsStats) { // Return an empty set of options if there are no stats to report return []; } // If we have entries to show, map them into options to display in the selectable list - return Object.entries(relatedEventsStats.events.byCategory).map(([category, total]) => { - const displayName = getDisplayName(category); - return { - prefix: , - optionTitle: `${displayName}`, - action: () => { - dispatch({ - type: 'userSelectedRelatedEventCategory', - payload: { - subject: event, - category, - }, - }); - }, - }; - }); + for (const category in relatedEventsStats.events.byCategory) { + if (Object.hasOwnProperty.call(relatedEventsStats.events.byCategory, category)) { + const total = relatedEventsStats.events.byCategory[category]; + const displayName = getDisplayName(category); + relatedStatsList.push({ + prefix: , + optionTitle: `${displayName}`, + action: () => { + dispatch({ + type: 'userSelectedRelatedEventCategory', + payload: { + subject: event, + category, + }, + }); + }, + }); + } + } + return relatedStatsList; }, [relatedEventsStats, dispatch, event]); const relatedEventStatusOrOptions = (() => { From 91a344028871e7801f2e4407cd6ce480e59baf0c Mon Sep 17 00:00:00 2001 From: Michael Olorunnisola Date: Fri, 12 Jun 2020 18:20:52 -0400 Subject: [PATCH 15/15] remove unused imports --- .../security_solution/public/resolver/store/data/selectors.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/x-pack/plugins/security_solution/public/resolver/store/data/selectors.ts b/x-pack/plugins/security_solution/public/resolver/store/data/selectors.ts index fbb106600282f0..672b3fb2c72938 100644 --- a/x-pack/plugins/security_solution/public/resolver/store/data/selectors.ts +++ b/x-pack/plugins/security_solution/public/resolver/store/data/selectors.ts @@ -15,8 +15,6 @@ import { Matrix3, AdjacentProcessMap, Vector2, - RelatedEventData, - RelatedEventDataEntryWithStats, EdgeLineMetadata, } from '../../types'; import { ResolverEvent } from '../../../../common/endpoint/types';