From f31661255b366bb2f50a47b9821874bdc0cd1b8e Mon Sep 17 00:00:00 2001
From: Kfir Peled <61654899+kfirpeled@users.noreply.github.com>
Date: Thu, 19 Dec 2024 12:51:11 +0000
Subject: [PATCH] [Cloud Security] Improve graph node label ellipsis logic
(#204580)
## Summary
This PR improves the logic of node's label truncation. It follows the
guidelines from design:
> - Set the maximum width for names to 160px
> - Truncate names to max two lines
> - I recommend truncating names in the middle, as entity IDs often
differ only in their last characters while starting similarly. This
approach will make it easier to identify differences between names
> - Display the full name in a tooltip on hover for better visibility
Before:
After:
https://github.com/user-attachments/assets/2ab04809-7599-45ab-9aaa-ae1fcabdf969
**How to test**
To test this PR you can run
```
yarn storybook cloud_security_posture_packages
```
### Checklist
- [ ] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios
---------
Co-authored-by: Sean Rathier
Co-authored-by: Brad White
---
.../src/components/node/diamond_node.tsx | 4 +-
.../src/components/node/ellipse_node.tsx | 9 +-
.../src/components/node/hexagon_node.tsx | 4 +-
.../src/components/node/label.stories.tsx | 117 ++++++++++++++++++
.../graph/src/components/node/label.tsx | 104 ++++++++++++++++
.../src/components/node/pentagon_node.tsx | 4 +-
.../src/components/node/rectangle_node.tsx | 4 +-
.../graph/src/components/node/styles.tsx | 15 +--
8 files changed, 234 insertions(+), 27 deletions(-)
create mode 100644 x-pack/solutions/security/packages/kbn-cloud-security-posture/graph/src/components/node/label.stories.tsx
create mode 100644 x-pack/solutions/security/packages/kbn-cloud-security-posture/graph/src/components/node/label.tsx
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 c974c0c9a60e6..ac6f51284a98d 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
@@ -11,7 +11,6 @@ import { Handle, Position } from '@xyflow/react';
import type { EntityNodeViewModel, NodeProps } from '../types';
import {
NodeShapeContainer,
- NodeLabel,
NodeShapeOnHoverSvg,
NodeShapeSvg,
NodeIcon,
@@ -20,6 +19,7 @@ import {
} from './styles';
import { DiamondHoverShape, DiamondShape } from './shapes/diamond_shape';
import { NodeExpandButton } from './node_expand_button';
+import { Label } from './label';
const NODE_WIDTH = 99;
const NODE_HEIGHT = 98;
@@ -81,7 +81,7 @@ export const DiamondNode: React.FC = memo((props: NodeProps) => {
style={HandleStyleOverride}
/>
- {label ? label : id}
+
>
);
});
diff --git a/x-pack/solutions/security/packages/kbn-cloud-security-posture/graph/src/components/node/ellipse_node.tsx b/x-pack/solutions/security/packages/kbn-cloud-security-posture/graph/src/components/node/ellipse_node.tsx
index 7fccea3b6bcf6..c8de632e893a0 100644
--- a/x-pack/solutions/security/packages/kbn-cloud-security-posture/graph/src/components/node/ellipse_node.tsx
+++ b/x-pack/solutions/security/packages/kbn-cloud-security-posture/graph/src/components/node/ellipse_node.tsx
@@ -10,19 +10,18 @@ import { useEuiBackgroundColor, useEuiTheme } from '@elastic/eui';
import { Handle, Position } from '@xyflow/react';
import {
NodeShapeContainer,
- NodeLabel,
NodeShapeOnHoverSvg,
NodeShapeSvg,
NodeIcon,
NodeButton,
HandleStyleOverride,
+ NODE_WIDTH,
+ NODE_HEIGHT,
} from './styles';
import type { EntityNodeViewModel, NodeProps } from '../types';
import { EllipseHoverShape, EllipseShape } from './shapes/ellipse_shape';
import { NodeExpandButton } from './node_expand_button';
-
-const NODE_WIDTH = 90;
-const NODE_HEIGHT = 90;
+import { Label } from './label';
// eslint-disable-next-line react/display-name
export const EllipseNode: React.FC = memo((props: NodeProps) => {
@@ -81,7 +80,7 @@ export const EllipseNode: React.FC = memo((props: NodeProps) => {
style={HandleStyleOverride}
/>
- {label ? label : id}
+
>
);
});
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 ca90094344072..f5ee7d92605cc 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
@@ -10,7 +10,6 @@ import { useEuiBackgroundColor, useEuiTheme } from '@elastic/eui';
import { Handle, Position } from '@xyflow/react';
import {
NodeShapeContainer,
- NodeLabel,
NodeShapeOnHoverSvg,
NodeShapeSvg,
NodeIcon,
@@ -20,6 +19,7 @@ import {
import type { EntityNodeViewModel, NodeProps } from '../types';
import { HexagonHoverShape, HexagonShape } from './shapes/hexagon_shape';
import { NodeExpandButton } from './node_expand_button';
+import { Label } from './label';
const NODE_WIDTH = 87;
const NODE_HEIGHT = 96;
@@ -81,7 +81,7 @@ export const HexagonNode: React.FC = memo((props: NodeProps) => {
style={HandleStyleOverride}
/>
- {label ? label : id}
+
>
);
});
diff --git a/x-pack/solutions/security/packages/kbn-cloud-security-posture/graph/src/components/node/label.stories.tsx b/x-pack/solutions/security/packages/kbn-cloud-security-posture/graph/src/components/node/label.stories.tsx
new file mode 100644
index 0000000000000..97a55f9b88f64
--- /dev/null
+++ b/x-pack/solutions/security/packages/kbn-cloud-security-posture/graph/src/components/node/label.stories.tsx
@@ -0,0 +1,117 @@
+/*
+ * 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 { ThemeProvider } from '@emotion/react';
+import { pick } from 'lodash';
+import { ReactFlow, Controls, Background } from '@xyflow/react';
+import { Story } from '@storybook/react';
+import { NodeViewModel } from '../types';
+import { HexagonNode, PentagonNode, EllipseNode, RectangleNode, DiamondNode, LabelNode } from '.';
+
+import '@xyflow/react/dist/style.css';
+
+export default {
+ title: 'Components/Graph Components/Labels',
+ description: 'CDR - Graph visualization',
+ argTypes: {
+ color: {
+ options: ['primary', 'danger', 'warning'],
+ control: { type: 'radio' },
+ },
+ shape: {
+ options: ['ellipse', 'hexagon', 'pentagon', 'rectangle', 'diamond', 'label'],
+ control: { type: 'radio' },
+ },
+ expandButtonClick: { action: 'expandButtonClick' },
+ },
+};
+
+const nodeTypes = {
+ hexagon: HexagonNode,
+ pentagon: PentagonNode,
+ ellipse: EllipseNode,
+ rectangle: RectangleNode,
+ diamond: DiamondNode,
+ label: LabelNode,
+};
+
+const Template: Story = (args: NodeViewModel) => (
+
+
+
+
+
+
+);
+
+export const ShortLabel = Template.bind({});
+
+ShortLabel.args = {
+ id: 'siem-windows',
+ label: '',
+ color: 'primary',
+ shape: 'hexagon',
+ icon: 'okta',
+ interactive: true,
+};
+
+export const ArnLabel = Template.bind({});
+
+ArnLabel.args = {
+ id: 'siem-windows',
+ label: 'arn:aws:iam::1234567890:user/lorem-ipsumdol-sitamet-user-1234',
+ color: 'primary',
+ shape: 'hexagon',
+ icon: 'okta',
+ interactive: true,
+};
+
+export const DashedLabel = Template.bind({});
+
+DashedLabel.args = {
+ id: 'siem-windows',
+ label: 'lore-ipsumdol-sitameta-consectetu-adipis342',
+ color: 'primary',
+ shape: 'hexagon',
+ icon: 'okta',
+ interactive: true,
+};
+
+export const NoSpacesLabel = Template.bind({});
+
+NoSpacesLabel.args = {
+ id: 'siem-windows',
+ label: 'LoremIpsumDolorSitAmetConsectetur123',
+ color: 'primary',
+ shape: 'hexagon',
+ icon: 'okta',
+ interactive: true,
+};
+
+export const NoSpacesAllLoweredLabel = Template.bind({});
+
+NoSpacesAllLoweredLabel.args = {
+ id: 'siem-windows',
+ label: 'loremipsumdolorsitametconsectetur123',
+ color: 'primary',
+ shape: 'hexagon',
+ icon: 'okta',
+ interactive: true,
+};
diff --git a/x-pack/solutions/security/packages/kbn-cloud-security-posture/graph/src/components/node/label.tsx b/x-pack/solutions/security/packages/kbn-cloud-security-posture/graph/src/components/node/label.tsx
new file mode 100644
index 0000000000000..098a3e0dd89c7
--- /dev/null
+++ b/x-pack/solutions/security/packages/kbn-cloud-security-posture/graph/src/components/node/label.tsx
@@ -0,0 +1,104 @@
+/*
+ * 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, { memo, type PropsWithChildren } from 'react';
+import { EuiText, EuiTextTruncate, EuiToolTip } from '@elastic/eui';
+import { css } from '@emotion/react';
+import styled from '@emotion/styled';
+import { NODE_LABEL_WIDTH, NODE_WIDTH } from './styles';
+
+const WORD_BOUNDARIES_REGEX = /\b/;
+const FORCE_BREAK_REGEX = /(.{10})/;
+
+/**
+ * A component that renders an element with breaking opportunities (``s)
+ * spliced into text children at word boundaries.
+ * Copied from x-pack/plugins/security_solution/public/resolver/view/generated_text.tsx
+ */
+const GeneratedText = memo>(function ({ children }) {
+ return <>{processedValue()}>;
+
+ function processedValue() {
+ return React.Children.map(children, (child) => {
+ if (typeof child === 'string') {
+ let valueSplitByWordBoundaries = child.split(WORD_BOUNDARIES_REGEX);
+
+ if (valueSplitByWordBoundaries.length < 2) {
+ valueSplitByWordBoundaries = child.split(FORCE_BREAK_REGEX);
+
+ if (valueSplitByWordBoundaries.length < 2) {
+ return valueSplitByWordBoundaries[0];
+ }
+ }
+
+ return [
+ valueSplitByWordBoundaries[0],
+ ...valueSplitByWordBoundaries
+ .splice(1)
+ .reduce((generatedTextMemo: Array, value) => {
+ if (
+ generatedTextMemo.length > 0 &&
+ typeof generatedTextMemo[generatedTextMemo.length - 1] === 'object'
+ ) {
+ return [...generatedTextMemo, value];
+ }
+ return [...generatedTextMemo, value, ];
+ }, []),
+ ];
+ } else {
+ return child;
+ }
+ });
+ }
+});
+
+GeneratedText.displayName = 'GeneratedText';
+
+export interface LabelProps {
+ text?: string;
+}
+
+const LabelComponent: React.FC = ({ text = '' }: LabelProps) => {
+ const [isTruncated, setIsTruncated] = React.useState(false);
+
+ return (
+
+
+
+ {(truncatedText) => (
+ <>
+ {setIsTruncated(truncatedText.length !== text.length)}
+ {{truncatedText}}
+ >
+ )}
+
+
+
+ );
+};
+
+export const Label = styled(LabelComponent)`
+ width: ${NODE_LABEL_WIDTH}px;
+ margin-left: ${-(NODE_LABEL_WIDTH - NODE_WIDTH) / 2}px;
+ text-overflow: ellipsis;
+ overflow: hidden;
+`;
diff --git a/x-pack/solutions/security/packages/kbn-cloud-security-posture/graph/src/components/node/pentagon_node.tsx b/x-pack/solutions/security/packages/kbn-cloud-security-posture/graph/src/components/node/pentagon_node.tsx
index 159f78a83b279..6888f1c83c558 100644
--- a/x-pack/solutions/security/packages/kbn-cloud-security-posture/graph/src/components/node/pentagon_node.tsx
+++ b/x-pack/solutions/security/packages/kbn-cloud-security-posture/graph/src/components/node/pentagon_node.tsx
@@ -11,7 +11,6 @@ import styled from '@emotion/styled';
import { Handle, Position } from '@xyflow/react';
import {
NodeShapeContainer,
- NodeLabel,
NodeShapeOnHoverSvg,
NodeShapeSvg,
NodeIcon,
@@ -21,6 +20,7 @@ import {
import type { EntityNodeViewModel, NodeProps } from '../types';
import { PentagonHoverShape, PentagonShape } from './shapes/pentagon_shape';
import { NodeExpandButton } from './node_expand_button';
+import { Label } from './label';
const PentagonShapeOnHover = styled(NodeShapeOnHoverSvg)`
transform: translate(-50%, -51.5%);
@@ -86,7 +86,7 @@ export const PentagonNode: React.FC = memo((props: NodeProps) => {
style={HandleStyleOverride}
/>
- {label ? label : id}
+
>
);
});
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 6884974982838..8b55a0898586c 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
@@ -10,7 +10,6 @@ import { useEuiBackgroundColor, useEuiTheme } from '@elastic/eui';
import { Handle, Position } from '@xyflow/react';
import {
NodeShapeContainer,
- NodeLabel,
NodeShapeOnHoverSvg,
NodeShapeSvg,
NodeIcon,
@@ -20,6 +19,7 @@ import {
import type { EntityNodeViewModel, NodeProps } from '../types';
import { RectangleHoverShape, RectangleShape } from './shapes/rectangle_shape';
import { NodeExpandButton } from './node_expand_button';
+import { Label } from './label';
const NODE_WIDTH = 81;
const NODE_HEIGHT = 80;
@@ -81,7 +81,7 @@ export const RectangleNode: React.FC = memo((props: NodeProps) => {
style={HandleStyleOverride}
/>
- {label ? label : id}
+
>
);
});
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 eed8b50c9038c..2982c4145370e 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
@@ -24,7 +24,7 @@ export const LABEL_PADDING_X = 15;
export const LABEL_BORDER_WIDTH = 1;
export const NODE_WIDTH = 90;
export const NODE_HEIGHT = 90;
-const NODE_LABEL_WIDTH = 120;
+export const NODE_LABEL_WIDTH = 160;
export const LabelNodeContainer = styled.div`
text-wrap: nowrap;
@@ -185,19 +185,6 @@ export const NodeIcon = ({ icon, color, x, y }: NodeIconProps) => {
);
};
-export const NodeLabel = styled(EuiText)`
- width: ${NODE_LABEL_WIDTH}px;
- margin-left: ${-(NODE_LABEL_WIDTH - NODE_WIDTH) / 2}px;
- text-overflow: ellipsis;
- // white-space: nowrap;
- overflow: hidden;
-`;
-
-NodeLabel.defaultProps = {
- size: 'xs',
- textAlign: 'center',
-};
-
export const ExpandButtonSize = 18;
export const RoundEuiButtonIcon = styled(EuiButtonIcon)`