Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP] Graph visualization quick fixes #201211

Closed
wants to merge 32 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
a982ad7
Added node button to support node click to open a popover.
kfirpeled Oct 29, 2024
c94c04a
Fixed SvgDefsMarker so it won't capture clicks
kfirpeled Oct 29, 2024
5c00b6e
Fixed missing padding when lazy loading the graph
kfirpeled Oct 29, 2024
1589ef8
Added graph visualization tab with expand capability
kfirpeled Oct 30, 2024
6f71b19
checks feature flag before showing graph tab
kfirpeled Oct 30, 2024
3dc4523
Added filterbar support
kfirpeled Oct 30, 2024
b88b074
fixed prevent scrolling when not interactive
kfirpeled Oct 31, 2024
dbb2688
Added filter support
kfirpeled Nov 4, 2024
008521f
lowered the minimum zoom
kfirpeled Nov 4, 2024
70ee31d
refactoring
kfirpeled Nov 4, 2024
dbab169
Merge branch 'main' into cspm/cdr-graph-viz-expanded
kfirpeled Nov 4, 2024
78246a7
Added open in timeline + time picking is around @timestamp
kfirpeled Nov 4, 2024
5318512
Color fixes and auto fitview
kfirpeled Nov 5, 2024
74ecb0e
Merge branch 'main' into cspm/cdr-graph-viz-expanded
kfirpeled Nov 5, 2024
b57dfac
Merge branch 'main' into cspm/cdr-graph-viz-expanded
kfirpeled Nov 11, 2024
2d0d360
Removed trace message that might contain sensitive info
kfirpeled Nov 11, 2024
f94db1e
Removed duplicated code due to merge
kfirpeled Nov 11, 2024
0899098
Merge branch 'main' into cspm/cdr-graph-viz-expanded
kfirpeled Nov 13, 2024
702ef00
merge cleanup
kfirpeled Nov 13, 2024
1ad2ce4
Merge branch 'main' into cspm/cdr-graph-viz-expanded
kfirpeled Nov 13, 2024
30f73c8
refactoring, removed investigate in timeline
kfirpeled Nov 18, 2024
3821ad9
Updated shared hooks
kfirpeled Nov 18, 2024
33134f6
Switched feature flag to an existing advanced setting
kfirpeled Nov 19, 2024
c98ab19
Merge branch 'main' into cspm/cdr-graph-viz-expanded
kfirpeled Nov 19, 2024
708300d
Added FTR to test e2e expanded flyout filtering
kfirpeled Nov 20, 2024
34656ae
Added technical preview tag
kfirpeled Nov 20, 2024
b0f73b9
Merge branch 'main' into cspm/cdr-graph-viz-expanded
kfirpeled Nov 21, 2024
c7fb622
post merge fixes
kfirpeled Nov 21, 2024
117b2ad
Added timeline investigation, fixed coloring of events. Added node li…
kfirpeled Nov 21, 2024
ad8a9d1
Using step edges, fixed ellipsis logic, changed danger fill color to …
kfirpeled Nov 21, 2024
c1e9a40
Added event popover and view event details
kfirpeled Nov 24, 2024
b381c46
bug fix event details
kfirpeled Nov 24, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,9 @@ export const labelNodeDataSchema = schema.allOf([
shape: schema.literal('label'),
parentId: schema.maybe(schema.string()),
color: colorSchema,
badge: schema.number(),
lastEventId: schema.string(),
lastEventIdx: schema.string(),
}),
]);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
*/

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';
Expand All @@ -27,14 +27,16 @@ export function DefaultEdge({
const { euiTheme } = useEuiTheme();
const color: Color = 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,
sourcePosition,
targetX: targetX + getShapeHandlePosition(data?.targetShape),
targetY,
targetPosition,
borderRadius: 15,
offset: 0,
curvature:
0.1 *
(data?.sourceShape === 'group' ||
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,7 @@ export const Graph: React.FC<GraphProps> = ({
minZoom={0.1}
>
{interactive && <Controls onInteractiveChange={onInteractiveStateChange} />}
<Background id={backgroundId} />{' '}
<Background id={backgroundId} />
</ReactFlow>
</div>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { ThemeProvider } from '@emotion/react';
import { Story } from '@storybook/react';
import { css } from '@emotion/react';
import { EuiListGroup, EuiHorizontalRule } from '@elastic/eui';
import type { EntityNodeViewModel, NodeProps } from '..';
import type { EntityNodeViewModel, LabelNodeViewModel, NodeProps } from '..';
import { Graph } from '..';
import { GraphPopover } from './graph_popover';
import { ExpandButtonClickCallback } from '../types';
Expand Down Expand Up @@ -173,18 +173,30 @@ const Template: Story = () => {
popoverOpenWrapper(expandNodePopover.onNodeExpandButtonClick, ...args);
const nodeClickHandler = (...args: any[]) => popoverOpenWrapper(nodePopover.onNodeClick, ...args);

const nodes: EntityNodeViewModel[] = useMemo(
() =>
(['hexagon', 'ellipse', 'rectangle', 'pentagon', 'diamond'] as const).map((shape, idx) => ({
id: `${idx}`,
label: `Node ${idx}`,
const nodes: Array<EntityNodeViewModel | LabelNodeViewModel> = useMemo(
() => [
...(['hexagon', 'ellipse', 'rectangle', 'pentagon', 'diamond'] as const).map(
(shape, idx) => ({
id: `${idx}`,
label: `Node ${idx}`,
color: 'primary',
icon: 'okta',
interactive: true,
shape,
expandButtonClick: expandButtonClickHandler,
nodeClick: nodeClickHandler,
})
),
{
id: 'label',
label: 'Label Node',
color: 'primary',
icon: 'okta',
interactive: true,
shape,
shape: 'label',
expandButtonClick: expandButtonClickHandler,
nodeClick: nodeClickHandler,
})),
},
],
// eslint-disable-next-line react-hooks/exhaustive-deps
[]
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,20 +40,8 @@ export const GraphPopover: React.FC<GraphPopoverProps> = ({
{...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};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,10 @@ export const layoutGraph = (
nodesById[node.id] = node;
}

if (node.parentId) {
return;
}

g.setNode(node.id, {
...node,
...size,
Expand All @@ -59,20 +63,22 @@ export const layoutGraph = (
Dagre.layout(g);

const layoutedNodes = nodes.map((node) => {
// For grouped nodes, we want to keep the original position relative to the parent
if (node.data.shape === 'label' && node.data.parentId) {
return {
...node,
position: nodesById[node.data.id].position,
};
}

const dagreNode = g.node(node.data.id);

// We are shifting the dagre node position (anchor=center center) to the top left
// so it matches the React Flow node anchor point (top left).
const x = dagreNode.x - (dagreNode.width ?? 0) / 2;
const y = dagreNode.y - (dagreNode.height ?? 0) / 2;

// For grouped nodes, we want to keep the original position relative to the parent
if (node.data.shape === 'label' && node.data.parentId) {
return {
...node,
position: nodesById[node.data.id].position,
};
} else if (node.data.shape === 'group') {
if (node.data.shape === 'group') {
return {
...node,
position: { x, y },
Expand Down Expand Up @@ -130,7 +136,7 @@ const layoutGroupChildren = (
const childSize = calcLabelSize(child.data.label);
child.position = {
x: groupNodeWidth / 2 - childSize.width / 2,
y: index * (childSize.height * 2 + space),
y: index * (childSize.height + space),
};
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
* 2.0.
*/

import { NodeViewModel } from './types';

export { Graph } from './graph/graph';
export { GraphPopover } from './graph/graph_popover';
export { useGraphPopover } from './graph/use_graph_popover';
Expand All @@ -17,3 +19,10 @@ export type {
EntityNodeViewModel,
NodeProps,
} from './types';

export const isEntityNode = (node: NodeViewModel) =>
node.shape === 'ellipse' ||
node.shape === 'pentagon' ||
node.shape === 'rectangle' ||
node.shape === 'diamond' ||
node.shape === 'hexagon';
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export const DiamondNode: React.FC<NodeProps> = memo((props: NodeProps) => {
const { id, color, icon, label, interactive, expandButtonClick, nodeClick } =
props.data as EntityNodeViewModel;
const { euiTheme } = useEuiTheme();
const fillColor = (color === 'danger' ? 'primary' : color) ?? 'primary';
return (
<>
<NodeShapeContainer>
Expand All @@ -50,7 +51,7 @@ export const DiamondNode: React.FC<NodeProps> = memo((props: NodeProps) => {
xmlns="http://www.w3.org/2000/svg"
>
<DiamondShape
fill={useEuiBackgroundColor(color ?? 'primary')}
fill={useEuiBackgroundColor(fillColor)}
stroke={euiTheme.colors[color ?? 'primary']}
/>
{icon && <NodeIcon x="14.5" y="14.5" icon={icon} color={color} />}
Expand Down Expand Up @@ -80,7 +81,7 @@ export const DiamondNode: React.FC<NodeProps> = memo((props: NodeProps) => {
style={HandleStyleOverride}
/>
</NodeShapeContainer>
<NodeLabel>{Boolean(label) ? label : id}</NodeLabel>
<NodeLabel text={Boolean(label) ? label : id} />
</>
);
});
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export const EllipseNode: React.FC<NodeProps> = memo((props: NodeProps) => {
const { id, color, icon, label, interactive, expandButtonClick, nodeClick } =
props.data as EntityNodeViewModel;
const { euiTheme } = useEuiTheme();
const fillColor = (color === 'danger' ? 'primary' : color) ?? 'primary';
return (
<>
<NodeShapeContainer>
Expand All @@ -50,7 +51,7 @@ export const EllipseNode: React.FC<NodeProps> = memo((props: NodeProps) => {
xmlns="http://www.w3.org/2000/svg"
>
<EllipseShape
fill={useEuiBackgroundColor(color ?? 'primary')}
fill={useEuiBackgroundColor(fillColor)}
stroke={euiTheme.colors[color ?? 'primary']}
/>
{icon && <NodeIcon x="11" y="12" icon={icon} color={color} />}
Expand Down Expand Up @@ -80,7 +81,7 @@ export const EllipseNode: React.FC<NodeProps> = memo((props: NodeProps) => {
style={HandleStyleOverride}
/>
</NodeShapeContainer>
<NodeLabel>{Boolean(label) ? label : id}</NodeLabel>
<NodeLabel text={Boolean(label) ? label : id} />
</>
);
});
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export const HexagonNode: React.FC<NodeProps> = memo((props: NodeProps) => {
const { id, color, icon, label, interactive, expandButtonClick, nodeClick } =
props.data as EntityNodeViewModel;
const { euiTheme } = useEuiTheme();
const fillColor = (color === 'danger' ? 'primary' : color) ?? 'primary';
return (
<>
<NodeShapeContainer>
Expand All @@ -50,7 +51,7 @@ export const HexagonNode: React.FC<NodeProps> = memo((props: NodeProps) => {
xmlns="http://www.w3.org/2000/svg"
>
<HexagonShape
fill={useEuiBackgroundColor(color ?? 'primary')}
fill={useEuiBackgroundColor(fillColor)}
stroke={euiTheme.colors[color ?? 'primary']}
/>
{icon && <NodeIcon x="11" y="15" icon={icon} color={color} />}
Expand Down Expand Up @@ -80,7 +81,7 @@ export const HexagonNode: React.FC<NodeProps> = memo((props: NodeProps) => {
style={HandleStyleOverride}
/>
</NodeShapeContainer>
<NodeLabel>{Boolean(label) ? label : id}</NodeLabel>
<NodeLabel text={Boolean(label) ? label : id} />
</>
);
});
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,52 @@

import React, { memo } from 'react';
import { Handle, Position } from '@xyflow/react';
import { LabelNodeContainer, LabelShape, HandleStyleOverride, LabelShapeOnHover } from './styles';
import { css } from '@emotion/react';
import {
LabelNodeContainer,
LabelShape,
HandleStyleOverride,
LabelShapeOnHover,
NodeButton,
LABEL_PADDING_X,
LABEL_BORDER_WIDTH,
} from './styles';
import type { LabelNodeViewModel, NodeProps } from '../types';
import { NodeExpandButton } from './node_expand_button';
import { getTextWidth } from '../graph/utils';

export const LabelNode: React.FC<NodeProps> = memo((props: NodeProps) => {
const { id, color, label, interactive } = props.data as LabelNodeViewModel;
const { id, color, label, interactive, nodeClick, expandButtonClick } =
props.data as LabelNodeViewModel;
const text = Boolean(label) ? label : id;
const labelWidth = Math.max(
100,
getTextWidth(text ?? '') + LABEL_PADDING_X * 2 + LABEL_BORDER_WIDTH * 2
);

return (
<LabelNodeContainer>
{interactive && <LabelShapeOnHover color={color} />}
<LabelShape color={color} textAlign="center">
{Boolean(label) ? label : id}
{text}
</LabelShape>
{interactive && (
<>
<NodeButton
css={css`
margin-top: -24px;
`}
height={24}
width={labelWidth}
onClick={(e) => nodeClick?.(e, props)}
/>
<NodeExpandButton
onClick={(e, unToggleCallback) => expandButtonClick?.(e, props, unToggleCallback)}
x={`${labelWidth}px`}
y={`${-24 + (24 - NodeExpandButton.ExpandButtonSize) / 2}px`}
/>
</>
)}
<Handle
type="target"
isConnectable={false}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ export const NodeExpandButton = ({ x, y, onClick }: NodeExpandButtonProps) => {
onClick={onClickHandler}
iconSize="m"
aria-label="Open or close node actions"
data-test-subj="nodeExpandButton"
/>
</StyledNodeExpandButton>
);
Expand Down
Loading