From d3a325ab29dd766c77b8f60ef52711ae0eacb336 Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Fri, 20 Dec 2024 05:16:10 +1100 Subject: [PATCH] [8.x] [Cloud Security] Update graph appearance (#204610) (#204974) # Backport This will backport the following commits from `main` to `8.x`: - [[Cloud Security] Update graph appearance (#204610)](https://github.com/elastic/kibana/pull/204610) ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sqren/backport) Co-authored-by: Kfir Peled <61654899+kfirpeled@users.noreply.github.com> --- .../common/schema/graph/v1.ts | 1 + .../components/edge/deafult_edge.stories.tsx | 173 +++++++++++------- .../src/components/edge/default_edge.tsx | 36 ++-- .../graph/src/components/edge/markers.tsx | 96 ++++++++++ .../graph/src/components/edge/styles.tsx | 50 ----- .../graph/src/components/graph/graph.tsx | 4 +- .../src/components/graph/graph_popover.tsx | 16 +- .../src/components/node/button.stories.tsx | 5 +- .../src/components/node/diamond_node.tsx | 6 +- .../src/components/node/edge_group_node.tsx | 35 ++-- .../src/components/node/ellipse_node.tsx | 6 +- .../src/components/node/hexagon_node.tsx | 6 +- .../components/node/node_expand_button.tsx | 6 +- .../src/components/node/pentagon_node.tsx | 6 +- .../src/components/node/rectangle_node.tsx | 6 +- .../graph/src/components/node/styles.tsx | 17 +- .../graph/src/components/types.ts | 3 + .../server/routes/graph/v1.ts | 48 ++++- .../routes/graph.ts | 12 +- 19 files changed, 337 insertions(+), 195 deletions(-) create mode 100644 x-pack/solutions/security/packages/kbn-cloud-security-posture/graph/src/components/edge/markers.tsx diff --git a/x-pack/platform/packages/shared/kbn-cloud-security-posture/common/schema/graph/v1.ts b/x-pack/platform/packages/shared/kbn-cloud-security-posture/common/schema/graph/v1.ts index 5b1a48cf940b7..114ff1aec9568 100644 --- a/x-pack/platform/packages/shared/kbn-cloud-security-posture/common/schema/graph/v1.ts +++ b/x-pack/platform/packages/shared/kbn-cloud-security-posture/common/schema/graph/v1.ts @@ -99,4 +99,5 @@ export const edgeDataSchema = schema.object({ source: schema.string(), target: schema.string(), color: colorSchema, + type: schema.maybe(schema.oneOf([schema.literal('solid'), schema.literal('dashed')])), }); diff --git a/x-pack/solutions/security/packages/kbn-cloud-security-posture/graph/src/components/edge/deafult_edge.stories.tsx b/x-pack/solutions/security/packages/kbn-cloud-security-posture/graph/src/components/edge/deafult_edge.stories.tsx index dd1c956a55e55..b4f35af2054f4 100644 --- a/x-pack/solutions/security/packages/kbn-cloud-security-posture/graph/src/components/edge/deafult_edge.stories.tsx +++ b/x-pack/solutions/security/packages/kbn-cloud-security-posture/graph/src/components/edge/deafult_edge.stories.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React from 'react'; +import React, { useEffect, useMemo, useRef } from 'react'; import { ThemeProvider } from '@emotion/react'; import { ReactFlow, @@ -17,14 +17,18 @@ import { useEdgesState, type BuiltInNode, type NodeProps, + type Node as xyNode, + type Edge as xyEdge, } from '@xyflow/react'; +import { isEmpty, isEqual, pick, size, xorWith } from 'lodash'; import { Story } from '@storybook/react'; -import { SvgDefsMarker } from './styles'; import { DefaultEdge } from '.'; +import { LabelNode } from '../node'; +import type { EdgeViewModel } from '../types'; +import { SvgDefsMarker } from './markers'; import '@xyflow/react/dist/style.css'; -import { LabelNode } from '../node'; -import type { NodeViewModel } from '../types'; +import { HandleStyleOverride } from '../node/styles'; export default { title: 'Components/Graph Components/Default Edge', @@ -34,22 +38,32 @@ export default { options: ['primary', 'danger', 'warning'], control: { type: 'radio' }, }, + type: { + options: ['solid', 'dashed'], + control: { type: 'radio' }, + }, }, }; const nodeTypes = { - default: ((props: NodeProps) => { - const handleStyle = { - width: 0, - height: 0, - 'min-width': 0, - 'min-height': 0, - border: 'none', - }; + // eslint-disable-next-line react/display-name + default: React.memo((props: NodeProps) => { return (
- - + + {props.data.label}
); @@ -61,66 +75,87 @@ const edgeTypes = { default: DefaultEdge, }; -const Template: Story = (args: NodeViewModel) => { - const initialNodes = [ - { - id: 'source', - type: 'default', - data: { label: 'source' }, - position: { x: 0, y: 0 }, - draggable: true, - }, - { - id: 'target', - type: 'default', - data: { label: 'target' }, - position: { x: 320, y: 100 }, - draggable: true, - }, - { - id: args.id, - type: 'label', - data: args, - position: { x: 160, y: 50 }, - draggable: true, - }, - ]; +const Template: Story = (args: EdgeViewModel) => { + const nodes = useMemo( + () => [ + { + id: 'source', + type: 'default', + data: { + label: 'source', + }, + position: { x: 0, y: 0 }, + }, + { + id: 'target', + type: 'default', + data: { + label: 'target', + }, + position: { x: 420, y: 0 }, + }, + { + id: args.id, + type: 'label', + data: pick(args, ['id', 'label', 'interactive', 'source', 'target', 'color', 'type']), + position: { x: 230, y: 6 }, + }, + ], + [args] + ); - const initialEdges = [ - { - id: `source-${args.id}`, - source: 'source', - target: args.id, - data: { + const edges = useMemo( + () => [ + { id: `source-${args.id}`, source: 'source', - sourceShape: 'rectangle', target: args.id, - targetShape: 'label', - color: args.color, - interactive: true, + data: { + id: `source-${args.id}`, + source: 'source', + sourceShape: 'custom', + target: args.id, + targetShape: 'label', + color: args.color, + type: args.type, + }, + type: 'default', }, - type: 'default', - }, - { - id: `${args.id}-target`, - source: args.id, - target: 'target', - data: { + { id: `${args.id}-target`, source: args.id, - sourceShape: 'label', target: 'target', - targetShape: 'rectangle', - color: args.color, - interactive: true, + data: { + id: `${args.id}-target`, + source: args.id, + sourceShape: 'label', + target: 'target', + targetShape: 'custom', + color: args.color, + type: args.type, + }, + type: 'default', }, - type: 'default', - }, - ]; + ], + [args] + ); - const [nodes, _setNodes, onNodesChange] = useNodesState(initialNodes); - const [edges, _setEdges, onEdgesChange] = useEdgesState(initialEdges); + const [nodesState, setNodes, onNodesChange] = useNodesState(nodes); + const [edgesState, setEdges, onEdgesChange] = useEdgesState>(edges); + const currNodesRef = useRef(nodes); + const currEdgesRef = useRef(edges); + + useEffect(() => { + if ( + !isArrayOfObjectsEqual(nodes, currNodesRef.current) || + !isArrayOfObjectsEqual(edges, currEdgesRef.current) + ) { + setNodes(nodes); + setEdges(edges); + currNodesRef.current = nodes; + currEdgesRef.current = edges; + } + }, [setNodes, setEdges, nodes, edges]); return ( @@ -128,12 +163,13 @@ const Template: Story = (args: NodeViewModel) => { @@ -148,6 +184,9 @@ Edge.args = { id: 'siem-windows', label: 'User login to OKTA', color: 'primary', - icon: 'okta', interactive: true, + type: 'solid', }; + +const isArrayOfObjectsEqual = (x: object[], y: object[]) => + size(x) === size(y) && isEmpty(xorWith(x, y, isEqual)); diff --git a/x-pack/solutions/security/packages/kbn-cloud-security-posture/graph/src/components/edge/default_edge.tsx b/x-pack/solutions/security/packages/kbn-cloud-security-posture/graph/src/components/edge/default_edge.tsx index 6826c47b270ce..370c7b3909973 100644 --- a/x-pack/solutions/security/packages/kbn-cloud-security-posture/graph/src/components/edge/default_edge.tsx +++ b/x-pack/solutions/security/packages/kbn-cloud-security-posture/graph/src/components/edge/default_edge.tsx @@ -6,12 +6,13 @@ */ import React from 'react'; -import { BaseEdge, getBezierPath } from '@xyflow/react'; +import { BaseEdge, getSmoothStepPath } from '@xyflow/react'; import { useEuiTheme } from '@elastic/eui'; -import type { Color } from '@kbn/cloud-security-posture-common/types/graph/latest'; -import type { EdgeProps } from '../types'; -import { getMarker } from './styles'; +import type { EdgeProps, EdgeViewModel } from '../types'; import { getShapeHandlePosition } from './utils'; +import { getMarkerStart, getMarkerEnd } from './markers'; + +type EdgeColor = EdgeViewModel['color']; export function DefaultEdge({ id, @@ -25,9 +26,9 @@ export function DefaultEdge({ data, }: EdgeProps) { const { euiTheme } = useEuiTheme(); - const color: Color = data?.color ?? 'primary'; + const color: EdgeColor = data?.color ?? 'primary'; - const [edgePath] = getBezierPath({ + const [edgePath] = getSmoothStepPath({ // sourceX and targetX are adjusted to account for the shape handle position sourceX: sourceX - getShapeHandlePosition(data?.sourceShape), sourceY, @@ -35,12 +36,8 @@ export function DefaultEdge({ targetX: targetX + getShapeHandlePosition(data?.targetShape), targetY, targetPosition, - curvature: - 0.1 * - (data?.sourceShape === 'group' || - (data?.sourceShape === 'label' && data?.targetShape === 'group') - ? -1 // We flip direction when the edge is between parent node to child nodes (groups always contain children in our graph) - : 1), + borderRadius: 15, + offset: 0, }); return ( @@ -50,12 +47,19 @@ export function DefaultEdge({ style={{ stroke: euiTheme.colors[color], }} - css={{ - strokeDasharray: '2,2', - }} + css={ + (!data?.type || data?.type === 'dashed') && { + strokeDasharray: '2,2', + } + } + markerStart={ + data?.sourceShape !== 'label' && data?.sourceShape !== 'group' + ? getMarkerStart(color) + : undefined + } markerEnd={ data?.targetShape !== 'label' && data?.targetShape !== 'group' - ? getMarker(color) + ? getMarkerEnd(color) : undefined } /> diff --git a/x-pack/solutions/security/packages/kbn-cloud-security-posture/graph/src/components/edge/markers.tsx b/x-pack/solutions/security/packages/kbn-cloud-security-posture/graph/src/components/edge/markers.tsx new file mode 100644 index 0000000000000..06dcaf29c63d6 --- /dev/null +++ b/x-pack/solutions/security/packages/kbn-cloud-security-posture/graph/src/components/edge/markers.tsx @@ -0,0 +1,96 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { useEuiTheme } from '@elastic/eui'; + +const getArrowPoints = (width: number, height: number): string => { + return `${-width},${-height} 0,0 ${-width},${height} ${-width},${-height}`; +}; + +const ArrowMarker = ({ + id, + color, + width = 5, + height = 4, +}: { + id: string; + color: string; + width?: number; + height?: number; +}) => { + const points = getArrowPoints(width, height); + + return ( + + + + ); +}; + +const DotMarker = ({ id, color }: { id: string; color: string }) => { + return ( + + + + ); +}; + +const MarkerStartType = { + primary: 'url(#dotPrimary)', + danger: 'url(#dotDanger)', + warning: 'url(#dotWarning)', +}; + +const MarkerEndType = { + primary: 'url(#arrowPrimary)', + danger: 'url(#arrowDanger)', + warning: 'url(#arrowWarning)', +}; + +export const getMarkerStart = (color: string) => { + const colorKey = color as keyof typeof MarkerStartType; + return MarkerStartType[colorKey] ?? MarkerStartType.primary; +}; + +export const getMarkerEnd = (color: string) => { + const colorKey = color as keyof typeof MarkerEndType; + return MarkerEndType[colorKey] ?? MarkerEndType.primary; +}; + +export const SvgDefsMarker = () => { + const { euiTheme } = useEuiTheme(); + + return ( + + + + + + + + + + + ); +}; diff --git a/x-pack/solutions/security/packages/kbn-cloud-security-posture/graph/src/components/edge/styles.tsx b/x-pack/solutions/security/packages/kbn-cloud-security-posture/graph/src/components/edge/styles.tsx index 5a3e2f8b72b21..66b7eca015caf 100644 --- a/x-pack/solutions/security/packages/kbn-cloud-security-posture/graph/src/components/edge/styles.tsx +++ b/x-pack/solutions/security/packages/kbn-cloud-security-posture/graph/src/components/edge/styles.tsx @@ -5,7 +5,6 @@ * 2.0. */ -import React from 'react'; import styled from '@emotion/styled'; import { rgba } from 'polished'; import { @@ -88,52 +87,3 @@ export const EdgeLabelOnHover = styled(EdgeLabel) { - return ( - - - - ); -}; - -export const MarkerType = { - primary: 'url(#primary)', - danger: 'url(#danger)', - warning: 'url(#warning)', -}; - -export const getMarker = (color: string) => { - const colorKey = color as keyof typeof MarkerType; - return MarkerType[colorKey] ?? MarkerType.primary; -}; - -export const SvgDefsMarker = () => { - const { euiTheme } = useEuiTheme(); - - return ( - - - - - - - - ); -}; diff --git a/x-pack/solutions/security/packages/kbn-cloud-security-posture/graph/src/components/graph/graph.tsx b/x-pack/solutions/security/packages/kbn-cloud-security-posture/graph/src/components/graph/graph.tsx index a97a1c74698ca..f08e40111b7f8 100644 --- a/x-pack/solutions/security/packages/kbn-cloud-security-posture/graph/src/components/graph/graph.tsx +++ b/x-pack/solutions/security/packages/kbn-cloud-security-posture/graph/src/components/graph/graph.tsx @@ -18,7 +18,7 @@ import { import type { Edge, FitViewOptions, Node, ReactFlowInstance } from '@xyflow/react'; import { useGeneratedHtmlId } from '@elastic/eui'; import type { CommonProps } from '@elastic/eui'; -import { SvgDefsMarker } from '../edge/styles'; +import { SvgDefsMarker } from '../edge/markers'; import { HexagonNode, PentagonNode, @@ -243,7 +243,9 @@ const processGraph = ( data: { ...edgeData, sourceShape: nodesById[edgeData.source].shape, + sourceColor: nodesById[edgeData.source].color, targetShape: nodesById[edgeData.target].shape, + targetColor: nodesById[edgeData.target].color, }, }; }); diff --git a/x-pack/solutions/security/packages/kbn-cloud-security-posture/graph/src/components/graph/graph_popover.tsx b/x-pack/solutions/security/packages/kbn-cloud-security-posture/graph/src/components/graph/graph_popover.tsx index 570c1332a8834..65d0b5a2b89b8 100644 --- a/x-pack/solutions/security/packages/kbn-cloud-security-posture/graph/src/components/graph/graph_popover.tsx +++ b/x-pack/solutions/security/packages/kbn-cloud-security-posture/graph/src/components/graph/graph_popover.tsx @@ -40,20 +40,8 @@ export const GraphPopover: React.FC = ({ {...rest} panelProps={{ css: css` - .euiPopover__arrow[data-popover-arrow='left']:before { - border-inline-start-color: ${euiTheme.colors?.body}; - } - - .euiPopover__arrow[data-popover-arrow='right']:before { - border-inline-end-color: ${euiTheme.colors?.body}; - } - - .euiPopover__arrow[data-popover-arrow='bottom']:before { - border-block-end-color: ${euiTheme.colors?.body}; - } - - .euiPopover__arrow[data-popover-arrow='top']:before { - border-block-start-color: ${euiTheme.colors?.body}; + .euiPopover__arrow { + --euiPopoverBackgroundColor: ${euiTheme.colors?.body}; } background-color: ${euiTheme.colors?.body}; diff --git a/x-pack/solutions/security/packages/kbn-cloud-security-posture/graph/src/components/node/button.stories.tsx b/x-pack/solutions/security/packages/kbn-cloud-security-posture/graph/src/components/node/button.stories.tsx index 7dc46ac6eb82c..bea6f851bab17 100644 --- a/x-pack/solutions/security/packages/kbn-cloud-security-posture/graph/src/components/node/button.stories.tsx +++ b/x-pack/solutions/security/packages/kbn-cloud-security-posture/graph/src/components/node/button.stories.tsx @@ -10,7 +10,8 @@ import React from 'react'; import { ThemeProvider } from '@emotion/react'; import { Story } from '@storybook/react'; -import { NodeButton, type NodeButtonProps, NodeShapeContainer } from './styles'; +import { type NodeButtonProps, NodeShapeContainer } from './styles'; +import { NodeExpandButton } from './node_expand_button'; export default { title: 'Components/Graph Components', @@ -24,7 +25,7 @@ const Template: Story = (args) => ( Hover me - + ); diff --git a/x-pack/solutions/security/packages/kbn-cloud-security-posture/graph/src/components/node/diamond_node.tsx b/x-pack/solutions/security/packages/kbn-cloud-security-posture/graph/src/components/node/diamond_node.tsx index ac6f51284a98d..b46c44c69d1b8 100644 --- a/x-pack/solutions/security/packages/kbn-cloud-security-posture/graph/src/components/node/diamond_node.tsx +++ b/x-pack/solutions/security/packages/kbn-cloud-security-posture/graph/src/components/node/diamond_node.tsx @@ -6,7 +6,7 @@ */ import React, { memo } from 'react'; -import { useEuiBackgroundColor, useEuiTheme } from '@elastic/eui'; +import { useEuiTheme } from '@elastic/eui'; import { Handle, Position } from '@xyflow/react'; import type { EntityNodeViewModel, NodeProps } from '../types'; import { @@ -16,6 +16,7 @@ import { NodeIcon, NodeButton, HandleStyleOverride, + useNodeFillColor, } from './styles'; import { DiamondHoverShape, DiamondShape } from './shapes/diamond_shape'; import { NodeExpandButton } from './node_expand_button'; @@ -51,7 +52,7 @@ export const DiamondNode: React.FC = memo((props: NodeProps) => { xmlns="http://www.w3.org/2000/svg" > {icon && } @@ -60,6 +61,7 @@ export const DiamondNode: React.FC = memo((props: NodeProps) => { <> nodeClick?.(e, props)} /> expandButtonClick?.(e, props, unToggleCallback)} x={`${NODE_WIDTH - NodeExpandButton.ExpandButtonSize}px`} y={`${(NODE_HEIGHT - NodeExpandButton.ExpandButtonSize) / 2 - 4}px`} diff --git a/x-pack/solutions/security/packages/kbn-cloud-security-posture/graph/src/components/node/edge_group_node.tsx b/x-pack/solutions/security/packages/kbn-cloud-security-posture/graph/src/components/node/edge_group_node.tsx index 05a61977cdcb1..10ac415398717 100644 --- a/x-pack/solutions/security/packages/kbn-cloud-security-posture/graph/src/components/node/edge_group_node.tsx +++ b/x-pack/solutions/security/packages/kbn-cloud-security-posture/graph/src/components/node/edge_group_node.tsx @@ -6,7 +6,7 @@ */ import React, { memo } from 'react'; -import { Handle, NodeResizeControl, Position } from '@xyflow/react'; +import { Handle, Position } from '@xyflow/react'; import { HandleStyleOverride } from './styles'; import type { NodeProps } from '../types'; @@ -15,25 +15,20 @@ export const EdgeGroupNode: React.FC = memo((props: NodeProps) => { // Handles order horizontally is: in > inside > out > outside return ( <> - - - - + + = memo((props: NodeProps) => { xmlns="http://www.w3.org/2000/svg" > {icon && } @@ -59,6 +60,7 @@ export const EllipseNode: React.FC = memo((props: NodeProps) => { <> nodeClick?.(e, props)} /> expandButtonClick?.(e, props, unToggleCallback)} x={`${NODE_WIDTH - NodeExpandButton.ExpandButtonSize / 2}px`} y={`${(NODE_HEIGHT - NodeExpandButton.ExpandButtonSize) / 2}px`} diff --git a/x-pack/solutions/security/packages/kbn-cloud-security-posture/graph/src/components/node/hexagon_node.tsx b/x-pack/solutions/security/packages/kbn-cloud-security-posture/graph/src/components/node/hexagon_node.tsx index f5ee7d92605cc..ebde4e2334e21 100644 --- a/x-pack/solutions/security/packages/kbn-cloud-security-posture/graph/src/components/node/hexagon_node.tsx +++ b/x-pack/solutions/security/packages/kbn-cloud-security-posture/graph/src/components/node/hexagon_node.tsx @@ -6,7 +6,7 @@ */ import React, { memo } from 'react'; -import { useEuiBackgroundColor, useEuiTheme } from '@elastic/eui'; +import { useEuiTheme } from '@elastic/eui'; import { Handle, Position } from '@xyflow/react'; import { NodeShapeContainer, @@ -15,6 +15,7 @@ import { NodeIcon, NodeButton, HandleStyleOverride, + useNodeFillColor, } from './styles'; import type { EntityNodeViewModel, NodeProps } from '../types'; import { HexagonHoverShape, HexagonShape } from './shapes/hexagon_shape'; @@ -51,7 +52,7 @@ export const HexagonNode: React.FC = memo((props: NodeProps) => { xmlns="http://www.w3.org/2000/svg" > {icon && } @@ -60,6 +61,7 @@ export const HexagonNode: React.FC = memo((props: NodeProps) => { <> nodeClick?.(e, props)} /> expandButtonClick?.(e, props, unToggleCallback)} x={`${NODE_WIDTH - NodeExpandButton.ExpandButtonSize / 2 + 2}px`} y={`${(NODE_HEIGHT - NodeExpandButton.ExpandButtonSize) / 2 - 2}px`} diff --git a/x-pack/solutions/security/packages/kbn-cloud-security-posture/graph/src/components/node/node_expand_button.tsx b/x-pack/solutions/security/packages/kbn-cloud-security-posture/graph/src/components/node/node_expand_button.tsx index 07581c1e3d3dd..80d354cd77d6b 100644 --- a/x-pack/solutions/security/packages/kbn-cloud-security-posture/graph/src/components/node/node_expand_button.tsx +++ b/x-pack/solutions/security/packages/kbn-cloud-security-posture/graph/src/components/node/node_expand_button.tsx @@ -7,14 +7,16 @@ import React, { useCallback, useState } from 'react'; import { StyledNodeExpandButton, RoundEuiButtonIcon, ExpandButtonSize } from './styles'; +import type { EntityNodeViewModel, LabelNodeViewModel } from '..'; export interface NodeExpandButtonProps { x?: string; y?: string; + color?: EntityNodeViewModel['color'] | LabelNodeViewModel['color']; onClick?: (e: React.MouseEvent, unToggleCallback: () => void) => void; } -export const NodeExpandButton = ({ x, y, onClick }: NodeExpandButtonProps) => { +export const NodeExpandButton = ({ x, y, color, onClick }: NodeExpandButtonProps) => { // State to track whether the icon is "plus" or "minus" const [isToggled, setIsToggled] = useState(false); @@ -30,7 +32,7 @@ export const NodeExpandButton = ({ x, y, onClick }: NodeExpandButtonProps) => { return ( = memo((props: NodeProps) => { xmlns="http://www.w3.org/2000/svg" > {icon && } @@ -65,6 +66,7 @@ export const PentagonNode: React.FC = memo((props: NodeProps) => { <> nodeClick?.(e, props)} /> expandButtonClick?.(e, props, unToggleCallback)} x={`${NODE_WIDTH - NodeExpandButton.ExpandButtonSize / 2}px`} y={`${(NODE_HEIGHT - NodeExpandButton.ExpandButtonSize) / 2}px`} diff --git a/x-pack/solutions/security/packages/kbn-cloud-security-posture/graph/src/components/node/rectangle_node.tsx b/x-pack/solutions/security/packages/kbn-cloud-security-posture/graph/src/components/node/rectangle_node.tsx index 8b55a0898586c..f923641a25a50 100644 --- a/x-pack/solutions/security/packages/kbn-cloud-security-posture/graph/src/components/node/rectangle_node.tsx +++ b/x-pack/solutions/security/packages/kbn-cloud-security-posture/graph/src/components/node/rectangle_node.tsx @@ -6,7 +6,7 @@ */ import React, { memo } from 'react'; -import { useEuiBackgroundColor, useEuiTheme } from '@elastic/eui'; +import { useEuiTheme } from '@elastic/eui'; import { Handle, Position } from '@xyflow/react'; import { NodeShapeContainer, @@ -15,6 +15,7 @@ import { NodeIcon, NodeButton, HandleStyleOverride, + useNodeFillColor, } from './styles'; import type { EntityNodeViewModel, NodeProps } from '../types'; import { RectangleHoverShape, RectangleShape } from './shapes/rectangle_shape'; @@ -51,7 +52,7 @@ export const RectangleNode: React.FC = memo((props: NodeProps) => { xmlns="http://www.w3.org/2000/svg" > {icon && } @@ -60,6 +61,7 @@ export const RectangleNode: React.FC = memo((props: NodeProps) => { <> nodeClick?.(e, props)} /> expandButtonClick?.(e, props, unToggleCallback)} x={`${NODE_WIDTH - NodeExpandButton.ExpandButtonSize / 4}px`} y={`${(NODE_HEIGHT - NodeExpandButton.ExpandButtonSize / 2) / 2}px`} diff --git a/x-pack/solutions/security/packages/kbn-cloud-security-posture/graph/src/components/node/styles.tsx b/x-pack/solutions/security/packages/kbn-cloud-security-posture/graph/src/components/node/styles.tsx index 2982c4145370e..c4305c6fded6b 100644 --- a/x-pack/solutions/security/packages/kbn-cloud-security-posture/graph/src/components/node/styles.tsx +++ b/x-pack/solutions/security/packages/kbn-cloud-security-posture/graph/src/components/node/styles.tsx @@ -9,7 +9,7 @@ import React from 'react'; import styled from '@emotion/styled'; import { type EuiIconProps, - type _EuiBackgroundColor, + type EuiTextProps, EuiButtonIcon, EuiIcon, EuiText, @@ -19,12 +19,14 @@ import { import { rgba } from 'polished'; import { getSpanIcon } from './get_span_icon'; import type { NodeExpandButtonProps } from './node_expand_button'; +import type { EntityNodeViewModel, LabelNodeViewModel } from '..'; export const LABEL_PADDING_X = 15; export const LABEL_BORDER_WIDTH = 1; export const NODE_WIDTH = 90; export const NODE_HEIGHT = 90; export const NODE_LABEL_WIDTH = 160; +type NodeColor = EntityNodeViewModel['color'] | LabelNodeViewModel['color']; export const LabelNodeContainer = styled.div` text-wrap: nowrap; @@ -32,8 +34,12 @@ export const LabelNodeContainer = styled.div` height: 24px; `; -export const LabelShape = styled(EuiText)` - background: ${(props) => useEuiBackgroundColor(props.color as _EuiBackgroundColor)}; +interface LabelShapeProps extends EuiTextProps { + color: LabelNodeViewModel['color']; +} + +export const LabelShape = styled(EuiText)` + background: ${(props) => useNodeFillColor(props.color)}; border: ${(props) => { const { euiTheme } = useEuiTheme(); return `solid ${ @@ -209,6 +215,11 @@ export const HandleStyleOverride: React.CSSProperties = { border: 'none', }; +export const useNodeFillColor = (color: NodeColor | undefined) => { + const fillColor = (color === 'danger' ? 'primary' : color) ?? 'primary'; + return useEuiBackgroundColor(fillColor); +}; + export const GroupStyleOverride = (size?: { width: number; height: number; diff --git a/x-pack/solutions/security/packages/kbn-cloud-security-posture/graph/src/components/types.ts b/x-pack/solutions/security/packages/kbn-cloud-security-posture/graph/src/components/types.ts index b09f6a29f6c62..32e34a212af59 100644 --- a/x-pack/solutions/security/packages/kbn-cloud-security-posture/graph/src/components/types.ts +++ b/x-pack/solutions/security/packages/kbn-cloud-security-posture/graph/src/components/types.ts @@ -12,6 +12,7 @@ import type { LabelNodeDataModel, EdgeDataModel, NodeShape, + Color as NodeColor, } from '@kbn/cloud-security-posture-common/types/graph/latest'; import type { Node, NodeProps as xyNodeProps, Edge, EdgeProps as xyEdgeProps } from '@xyflow/react'; @@ -62,7 +63,9 @@ export type EdgeProps = xyEdgeProps< Edge< EdgeViewModel & { sourceShape: NodeShape; + sourceColor: NodeColor; targetShape: NodeShape; + targetColor: NodeColor; } > >; diff --git a/x-pack/solutions/security/plugins/cloud_security_posture/server/routes/graph/v1.ts b/x-pack/solutions/security/plugins/cloud_security_posture/server/routes/graph/v1.ts index d506bb856e766..5fb39c9be4993 100644 --- a/x-pack/solutions/security/plugins/cloud_security_posture/server/routes/graph/v1.ts +++ b/x-pack/solutions/security/plugins/cloud_security_posture/server/routes/graph/v1.ts @@ -40,6 +40,7 @@ interface GraphEdge { interface LabelEdges { source: string; target: string; + edgeType: EdgeDataModel['type']; } interface GraphContextServices { @@ -259,7 +260,17 @@ const createNodes = (records: GraphEdge[], context: Omit { // If actor is a user return ellipse if (users.includes(actorId)) { @@ -337,7 +352,7 @@ const determineEntityNodeShape = ( return { shape: 'diamond', icon: 'globe' }; } - return { shape: 'hexagon', icon: 'questionInCircle' }; + return { shape: 'hexagon' }; }; const sortNodes = (nodesMap: Record) => { @@ -368,7 +383,8 @@ const createEdgesAndGroups = (context: ParseContext) => { nodesMap, labelEdges[edgeLabelId].source, edgeLabelId, - labelEdges[edgeLabelId].target + labelEdges[edgeLabelId].target, + labelEdges[edgeLabelId].edgeType ); } else { const groupNode: GroupNodeDataModel = { @@ -377,10 +393,18 @@ const createEdgesAndGroups = (context: ParseContext) => { }; nodesMap[groupNode.id] = groupNode; let groupEdgesColor: Color = 'primary'; + let groupEdgesType: EdgeDataModel['type'] = 'dashed'; edgeLabelsIds.forEach((edgeLabelId) => { (nodesMap[edgeLabelId] as Writable).parentId = groupNode.id; - connectEntitiesAndLabelNode(edgesMap, nodesMap, groupNode.id, edgeLabelId, groupNode.id); + connectEntitiesAndLabelNode( + edgesMap, + nodesMap, + groupNode.id, + edgeLabelId, + groupNode.id, + labelEdges[edgeLabelId].edgeType + ); if ((nodesMap[edgeLabelId] as LabelNodeDataModel).color === 'danger') { groupEdgesColor = 'danger'; @@ -391,6 +415,10 @@ const createEdgesAndGroups = (context: ParseContext) => { // Use warning only if there's no danger color groupEdgesColor = 'warning'; } + + if (labelEdges[edgeLabelId].edgeType === 'solid') { + groupEdgesType = 'solid'; + } }); connectEntitiesAndLabelNode( @@ -399,6 +427,7 @@ const createEdgesAndGroups = (context: ParseContext) => { labelEdges[edgeLabelsIds[0]].source, groupNode.id, labelEdges[edgeLabelsIds[0]].target, + groupEdgesType, groupEdgesColor ); } @@ -411,11 +440,12 @@ const connectEntitiesAndLabelNode = ( sourceNodeId: string, labelNodeId: string, targetNodeId: string, + edgeType: EdgeDataModel['type'] = 'solid', colorOverride?: Color ) => { [ - connectNodes(nodesMap, sourceNodeId, labelNodeId, colorOverride), - connectNodes(nodesMap, labelNodeId, targetNodeId, colorOverride), + connectNodes(nodesMap, sourceNodeId, labelNodeId, edgeType, colorOverride), + connectNodes(nodesMap, labelNodeId, targetNodeId, edgeType, colorOverride), ].forEach((edge) => { edgesMap[edge.id] = edge; }); @@ -425,6 +455,7 @@ const connectNodes = ( nodesMap: Record, sourceNodeId: string, targetNodeId: string, + edgeType: EdgeDataModel['type'] = 'solid', colorOverride?: Color ): EdgeDataModel => { const sourceNode = nodesMap[sourceNodeId]; @@ -441,5 +472,6 @@ const connectNodes = ( source: sourceNodeId, target: targetNodeId, color: colorOverride ?? color, + type: edgeType, }; }; diff --git a/x-pack/test/cloud_security_posture_api/routes/graph.ts b/x-pack/test/cloud_security_posture_api/routes/graph.ts index e2be81a7d40e5..18a61b85c5f40 100644 --- a/x-pack/test/cloud_security_posture_api/routes/graph.ts +++ b/x-pack/test/cloud_security_posture_api/routes/graph.ts @@ -75,7 +75,7 @@ export default function (providerContext: FtrProviderContext) { }); describe('Validation', () => { - it('should return 400 when missing `eventIds` field', async () => { + it('should return 400 when missing `originEventIds` field', async () => { await postGraph(supertest, { // @ts-expect-error ignore error for testing query: { @@ -171,6 +171,7 @@ export default function (providerContext: FtrProviderContext) { 'primary', `edge color mismatched [edge: ${edge.id}] [actual: ${edge.color}]` ); + expect(edge.type).equal('dashed'); }); }); @@ -201,6 +202,7 @@ export default function (providerContext: FtrProviderContext) { 'danger', `edge color mismatched [edge: ${edge.id}] [actual: ${edge.color}]` ); + expect(edge.type).equal('solid'); }); }); @@ -231,6 +233,7 @@ export default function (providerContext: FtrProviderContext) { 'primary', `edge color mismatched [edge: ${edge.id}] [actual: ${edge.color}]` ); + expect(edge.type).equal('solid'); }); }); @@ -261,6 +264,7 @@ export default function (providerContext: FtrProviderContext) { 'danger', `edge color mismatched [edge: ${edge.id}] [actual: ${edge.color}]` ); + expect(edge.type).equal('solid'); }); }); @@ -303,6 +307,7 @@ export default function (providerContext: FtrProviderContext) { 'warning', `edge color mismatched [edge: ${edge.id}] [actual: ${edge.color}]` ); + expect(edge.type).equal('dashed'); }); }); @@ -351,10 +356,11 @@ export default function (providerContext: FtrProviderContext) { : 'primary', `edge color mismatched [edge: ${edge.id}] [actual: ${edge.color}]` ); + expect(edge.type).equal('dashed'); }); }); - it('should support more than 1 eventIds', async () => { + it('should support more than 1 originEventIds', async () => { const response = await postGraph(supertest, { query: { originEventIds: [ @@ -384,6 +390,7 @@ export default function (providerContext: FtrProviderContext) { 'danger', `edge color mismatched [edge: ${edge.id}] [actual: ${edge.color}]` ); + expect(edge.type).equal('solid'); }); }); @@ -429,6 +436,7 @@ export default function (providerContext: FtrProviderContext) { idx <= 1 ? 'danger' : 'warning', `edge color mismatched [edge: ${edge.id}] [actual: ${edge.color}]` ); + expect(edge.type).equal(idx <= 1 ? 'solid' : 'dashed'); }); });