Skip to content

Commit 56641a0

Browse files
[8.19] [Security solution][Alerts]Fix flyout highlighted table cell renderer (#234222) (#235105)
# Backport This will backport the following commits from `main` to `8.19`: - [[Security solution][Alerts]Fix flyout highlighted table cell renderer (#234222)](#234222) <!--- Backport version: 10.0.2 --> ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sorenlouv/backport) <!--BACKPORT [{"author":{"name":"Nicholas Peretti","email":"[email protected]"},"sourceCommit":{"committedDate":"2025-09-11T13:43:13Z","message":"[Security solution][Alerts]Fix flyout highlighted table cell renderer (#234222)\n\n## Summary\n\nFixes #231974\n\n### 🛑 The problem\n\nAs shown in the ticket, when an alert is generated via EQL sequence rule\nit might have multiple values for the \"source event\" field.\n\nWhen that happen, the UI is suboptimal.\n\nAnother issue with this type of rules is that they keep in the ancestors\nalso the ID of the alerts in the previous sequence on top of the IDs of\nthe documents that have generated the alerts in the first place.\n\nIdeally, we would like to only keep the IDs of the original documents\n(events) and skip the alerts (signals)\n\n### 💡 The solution\n\n- There was a bug in the highlighted table where a bit of CSS was\napplied to a react fragment\n- That style was not being applied as react fragments get stripped from\nthe app tree\n- This has caused the UI to improve, by rendering multiple values on top\nof each other rather then horizontally\n- As we don't really know how many values there might be, a \"show more\"\nsolution has been implemented\n- The values for the highlighted field get filtered by cross referencing\neach value with its \"depth\" from the `kibana.alert.ancestors.depth`\n - Only the ancestors with depth `0` are kept and shown in the table\n \n ### 🎬 Video\n \n\n\nhttps://github.com/user-attachments/assets/0bbe93b6-bf0e-4d51-887a-7a043a7ffbec","sha":"7b5852cc9a179ca06040f3b93f0f739a42c2a45f","branchLabelMapping":{"^v9.2.0$":"main","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["release_note:fix","backport missing","Team: SecuritySolution","Team:Threat Hunting:Investigations","backport:version","v9.1.0","v8.19.0","v9.2.0"],"title":"[Security solution][Alerts] Fix flyout highlighted table","number":234222,"url":"https://github.com/elastic/kibana/pull/234222","mergeCommit":{"message":"[Security solution][Alerts]Fix flyout highlighted table cell renderer (#234222)\n\n## Summary\n\nFixes #231974\n\n### 🛑 The problem\n\nAs shown in the ticket, when an alert is generated via EQL sequence rule\nit might have multiple values for the \"source event\" field.\n\nWhen that happen, the UI is suboptimal.\n\nAnother issue with this type of rules is that they keep in the ancestors\nalso the ID of the alerts in the previous sequence on top of the IDs of\nthe documents that have generated the alerts in the first place.\n\nIdeally, we would like to only keep the IDs of the original documents\n(events) and skip the alerts (signals)\n\n### 💡 The solution\n\n- There was a bug in the highlighted table where a bit of CSS was\napplied to a react fragment\n- That style was not being applied as react fragments get stripped from\nthe app tree\n- This has caused the UI to improve, by rendering multiple values on top\nof each other rather then horizontally\n- As we don't really know how many values there might be, a \"show more\"\nsolution has been implemented\n- The values for the highlighted field get filtered by cross referencing\neach value with its \"depth\" from the `kibana.alert.ancestors.depth`\n - Only the ancestors with depth `0` are kept and shown in the table\n \n ### 🎬 Video\n \n\n\nhttps://github.com/user-attachments/assets/0bbe93b6-bf0e-4d51-887a-7a043a7ffbec","sha":"7b5852cc9a179ca06040f3b93f0f739a42c2a45f"}},"sourceBranch":"main","suggestedTargetBranches":["9.1","8.19"],"targetPullRequestStates":[{"branch":"9.1","label":"v9.1.0","branchLabelMappingKey":"^v(\\d+).(\\d+).\\d+$","isSourceBranch":false,"state":"NOT_CREATED"},{"branch":"8.19","label":"v8.19.0","branchLabelMappingKey":"^v(\\d+).(\\d+).\\d+$","isSourceBranch":false,"state":"NOT_CREATED"},{"branch":"main","label":"v9.2.0","branchLabelMappingKey":"^v9.2.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/234222","number":234222,"mergeCommit":{"message":"[Security solution][Alerts]Fix flyout highlighted table cell renderer (#234222)\n\n## Summary\n\nFixes #231974\n\n### 🛑 The problem\n\nAs shown in the ticket, when an alert is generated via EQL sequence rule\nit might have multiple values for the \"source event\" field.\n\nWhen that happen, the UI is suboptimal.\n\nAnother issue with this type of rules is that they keep in the ancestors\nalso the ID of the alerts in the previous sequence on top of the IDs of\nthe documents that have generated the alerts in the first place.\n\nIdeally, we would like to only keep the IDs of the original documents\n(events) and skip the alerts (signals)\n\n### 💡 The solution\n\n- There was a bug in the highlighted table where a bit of CSS was\napplied to a react fragment\n- That style was not being applied as react fragments get stripped from\nthe app tree\n- This has caused the UI to improve, by rendering multiple values on top\nof each other rather then horizontally\n- As we don't really know how many values there might be, a \"show more\"\nsolution has been implemented\n- The values for the highlighted field get filtered by cross referencing\neach value with its \"depth\" from the `kibana.alert.ancestors.depth`\n - Only the ancestors with depth `0` are kept and shown in the table\n \n ### 🎬 Video\n \n\n\nhttps://github.com/user-attachments/assets/0bbe93b6-bf0e-4d51-887a-7a043a7ffbec","sha":"7b5852cc9a179ca06040f3b93f0f739a42c2a45f"}}]}] BACKPORT-->
1 parent ac7260b commit 56641a0

File tree

8 files changed

+232
-34
lines changed

8 files changed

+232
-34
lines changed

x-pack/solutions/security/plugins/security_solution/public/common/components/event_details/translations.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,3 +46,8 @@ export const EVENT_SUMMARY_VIEW_CONTEXT_TOOLTIP = i18n.translate(
4646
defaultMessage: 'Add this event as context',
4747
}
4848
);
49+
50+
export const EVENT_SOURCE_FIELD_DESCRIPTOR = i18n.translate(
51+
'xpack.securitySolution.detections.alerts.ancestorsId',
52+
{ defaultMessage: 'Source event' }
53+
);

x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/components/highlighted_fields_cell.test.tsx

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,11 @@
66
*/
77

88
import React from 'react';
9-
import { render } from '@testing-library/react';
9+
import { fireEvent, render } from '@testing-library/react';
1010
import {
1111
HIGHLIGHTED_FIELDS_AGENT_STATUS_CELL_TEST_ID,
1212
HIGHLIGHTED_FIELDS_BASIC_CELL_TEST_ID,
13+
HIGHLIGHTED_FIELDS_CELL_TEST_ID,
1314
HIGHLIGHTED_FIELDS_LINKED_CELL_TEST_ID,
1415
} from './test_ids';
1516
import { HighlightedFieldsCell } from './highlighted_fields_cell';
@@ -176,4 +177,35 @@ describe('<HighlightedFieldsCell />', () => {
176177

177178
expect(container).toBeEmptyDOMElement();
178179
});
180+
181+
it('should truncate content if too large', () => {
182+
const values = new Array(5).fill(null).map((_, i) => i.toString());
183+
const { container } = render(
184+
<TestProviders>
185+
<HighlightedFieldsCell values={values} field={'field'} scopeId={SCOPE_ID} />
186+
</TestProviders>
187+
);
188+
189+
function getDisplayedValues() {
190+
return container.querySelectorAll(`[data-test-subj$=${HIGHLIGHTED_FIELDS_CELL_TEST_ID}]`);
191+
}
192+
193+
function getToggleButton() {
194+
return container.querySelector('[data-test-subj="toggle-show-more-button"]');
195+
}
196+
197+
// Only the first 2 values should be displayed by default
198+
expect(getDisplayedValues().length).toBe(2);
199+
200+
// The toggle button to see all the values should be visible
201+
expect(getToggleButton()).toBeVisible();
202+
203+
// Click the toggle button and check that the rest of the elements are visible
204+
fireEvent.click(getToggleButton()!);
205+
expect(getDisplayedValues().length).toBe(values.length);
206+
207+
// Click the toggle button again and check that the rest of the elements has been hidden
208+
fireEvent.click(getToggleButton()!);
209+
expect(getDisplayedValues().length).toBe(2);
210+
});
179211
});

x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/components/highlighted_fields_cell.tsx

Lines changed: 110 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,10 @@
66
*/
77

88
import type { FC } from 'react';
9-
import React, { useMemo } from 'react';
9+
import React, { useCallback, useMemo, useState } from 'react';
1010
import { css } from '@emotion/react';
11-
import { useEuiTheme } from '@elastic/eui';
11+
import { EuiButtonEmpty, useEuiTheme } from '@elastic/eui';
12+
import { FormattedMessage } from '@kbn/i18n-react';
1213
import { getAgentTypeForAgentIdField } from '../../../../common/lib/endpoint/utils/get_agent_type_for_agent_id_field';
1314
import type { ResponseActionAgentType } from '../../../../../common/endpoint/service/response_actions/constants';
1415
import { AgentStatus } from '../../../../common/components/endpoint/agents/agent_status';
@@ -22,6 +23,8 @@ import {
2223
import { isFlyoutLink } from '../../../shared/utils/link_utils';
2324
import { PreviewLink } from '../../../shared/components/preview_link';
2425

26+
const EMPTY_ARRAY: string[] = [];
27+
2528
export interface HighlightedFieldsCellProps {
2629
/**
2730
* Highlighted field's name used to know what component to display
@@ -45,6 +48,16 @@ export interface HighlightedFieldsCellProps {
4548
* This is false by default (for the AI for SOC alert summary page) and will be true for the alerts page.
4649
*/
4750
showPreview?: boolean;
51+
/**
52+
* The indexName to be passed to the flyout preview panel
53+
* when clicking on "Source event" id
54+
*/
55+
ancestorsIndexName?: string;
56+
/**
57+
* Caps the amount of values displayed in the cell.
58+
* If the limit is reached a "show more" button is being rendered
59+
*/
60+
displayValuesLimit?: number;
4861
}
4962

5063
/**
@@ -56,14 +69,85 @@ export const HighlightedFieldsCell: FC<HighlightedFieldsCellProps> = ({
5669
originalField = '',
5770
scopeId = '',
5871
showPreview = false,
72+
ancestorsIndexName,
73+
displayValuesLimit = 2,
5974
}) => {
6075
const agentType: ResponseActionAgentType = useMemo(() => {
6176
return getAgentTypeForAgentIdField(originalField);
6277
}, [originalField]);
6378
const { euiTheme } = useEuiTheme();
79+
const [isContentExpanded, setIsContentExpanded] = useState(false);
80+
const toggleContentExpansion = useCallback(
81+
() => setIsContentExpanded((currentIsOpen) => !currentIsOpen),
82+
[]
83+
);
84+
85+
const visibleValues = useMemo(() => {
86+
/**
87+
* Check if a limit was set and if the limit
88+
* is within the values size
89+
*/
90+
if (
91+
displayValuesLimit &&
92+
displayValuesLimit > 0 &&
93+
displayValuesLimit < (values?.length ?? 0)
94+
) {
95+
return values?.slice(0, displayValuesLimit);
96+
}
97+
98+
return values;
99+
}, [values, displayValuesLimit]);
100+
101+
const overflownValues = useMemo(() => {
102+
/**
103+
* Check if a limit was set and if the limit
104+
* is within the values size
105+
*/
106+
if (
107+
displayValuesLimit &&
108+
displayValuesLimit > 0 &&
109+
displayValuesLimit < (values?.length ?? 0)
110+
) {
111+
return values?.slice(displayValuesLimit);
112+
}
113+
114+
return EMPTY_ARRAY;
115+
}, [values, displayValuesLimit]);
116+
117+
const isContentTooLarge = useMemo(
118+
() => !!displayValuesLimit && displayValuesLimit < (values?.length ?? 0),
119+
[displayValuesLimit, values]
120+
);
121+
122+
const renderValue = useCallback(
123+
(value: string, i: number) => (
124+
<div key={`${i}-${value}`} data-test-subj={`${value}-${HIGHLIGHTED_FIELDS_CELL_TEST_ID}`}>
125+
{showPreview && isFlyoutLink({ field, scopeId }) ? (
126+
<PreviewLink
127+
field={field}
128+
value={value}
129+
scopeId={scopeId}
130+
data-test-subj={HIGHLIGHTED_FIELDS_LINKED_CELL_TEST_ID}
131+
ancestorsIndexName={ancestorsIndexName}
132+
/>
133+
) : field === AGENT_STATUS_FIELD_NAME ? (
134+
<AgentStatus
135+
agentId={String(value ?? '')}
136+
agentType={agentType}
137+
data-test-subj={HIGHLIGHTED_FIELDS_AGENT_STATUS_CELL_TEST_ID}
138+
/>
139+
) : (
140+
<span data-test-subj={HIGHLIGHTED_FIELDS_BASIC_CELL_TEST_ID}>{value}</span>
141+
)}
142+
</div>
143+
),
144+
[agentType, ancestorsIndexName, field, scopeId, showPreview]
145+
);
146+
147+
if (values === null) return null;
64148

65149
return (
66-
<React.Fragment
150+
<div
67151
css={css`
68152
div {
69153
margin-bottom: ${euiTheme.size.xs};
@@ -74,32 +158,28 @@ export const HighlightedFieldsCell: FC<HighlightedFieldsCellProps> = ({
74158
}
75159
`}
76160
>
77-
{values != null &&
78-
values.map((value, i) => {
79-
return (
80-
<div
81-
key={`${i}-${value}`}
82-
data-test-subj={`${value}-${HIGHLIGHTED_FIELDS_CELL_TEST_ID}`}
83-
>
84-
{showPreview && isFlyoutLink({ field, scopeId }) ? (
85-
<PreviewLink
86-
field={field}
87-
value={value}
88-
scopeId={scopeId}
89-
data-test-subj={HIGHLIGHTED_FIELDS_LINKED_CELL_TEST_ID}
90-
/>
91-
) : field === AGENT_STATUS_FIELD_NAME ? (
92-
<AgentStatus
93-
agentId={String(value ?? '')}
94-
agentType={agentType}
95-
data-test-subj={HIGHLIGHTED_FIELDS_AGENT_STATUS_CELL_TEST_ID}
96-
/>
97-
) : (
98-
<span data-test-subj={HIGHLIGHTED_FIELDS_BASIC_CELL_TEST_ID}>{value}</span>
99-
)}
100-
</div>
101-
);
102-
})}
103-
</React.Fragment>
161+
{visibleValues != null && visibleValues.map((value, i) => renderValue(value, i))}
162+
{isContentExpanded && overflownValues?.map(renderValue)}
163+
{isContentTooLarge && (
164+
<EuiButtonEmpty
165+
size="xs"
166+
flush="both"
167+
onClick={toggleContentExpansion}
168+
data-test-subj="toggle-show-more-button"
169+
>
170+
{isContentExpanded ? (
171+
<FormattedMessage
172+
id="xpack.securitySolution.flyout.alertsHighlightedField.showMore"
173+
defaultMessage="Show less"
174+
/>
175+
) : (
176+
<FormattedMessage
177+
id="xpack.securitySolution.flyout.alertsHighlightedField.showLess"
178+
defaultMessage="Show more"
179+
/>
180+
)}
181+
</EuiButtonEmpty>
182+
)}
183+
</div>
104184
);
105185
};

x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/shared/hooks/use_highlighted_fields.test.tsx

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -265,4 +265,45 @@ describe('useHighlightedFields', () => {
265265
},
266266
});
267267
});
268+
269+
it('should only include ancestors with depth 0 in the "source event" field', () => {
270+
const hookResult = renderHook(() =>
271+
useHighlightedFields({
272+
dataFormattedForFieldBrowser: dataFormattedForFieldBrowser
273+
.filter((item) => item.field !== 'kibana.alert.ancestors.id')
274+
.concat([
275+
{
276+
category: 'kibana',
277+
field: 'kibana.alert.ancestors.depth',
278+
values: ['0', '1', '0', '1'],
279+
originalValue: ['0', '1', '0', '1'],
280+
isObjectArray: false,
281+
},
282+
{
283+
category: 'kibana',
284+
field: 'kibana.alert.ancestors.id',
285+
values: [
286+
'AZkupz0BWCNsCtptscaJ',
287+
'e7f11264eb2dbcdd2588ebd64f3cdbfc04d47d364db700b817ded6503f995b75',
288+
'AZkupz0BWCNsCtptscaU',
289+
'3c8211dd158a96ef6884fc50267fd778ae36704fa8b8a448399d4832e93cac3b',
290+
],
291+
originalValue: [
292+
'AZkupz0BWCNsCtptscaJ',
293+
'e7f11264eb2dbcdd2588ebd64f3cdbfc04d47d364db700b817ded6503f995b75',
294+
'AZkupz0BWCNsCtptscaU',
295+
'3c8211dd158a96ef6884fc50267fd778ae36704fa8b8a448399d4832e93cac3b',
296+
],
297+
isObjectArray: false,
298+
},
299+
]),
300+
investigationFields: ['kibana.alert.ancestors.id'],
301+
})
302+
);
303+
304+
expect(hookResult.result.current['kibana.alert.ancestors.id'].values).toEqual([
305+
'AZkupz0BWCNsCtptscaJ',
306+
'AZkupz0BWCNsCtptscaU',
307+
]);
308+
});
268309
});

x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/shared/hooks/use_highlighted_fields.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
getEventCategoriesFromData,
1515
getHighlightedFieldsToDisplay,
1616
} from '../../../../common/components/event_details/get_alert_summary_rows';
17+
import { EVENT_SOURCE_FIELD_NAME } from '../../../../timelines/components/timeline/body/renderers/constants';
1718

1819
export interface UseHighlightedFieldsParams {
1920
/**
@@ -92,7 +93,7 @@ export const useHighlightedFields = ({
9293
}
9394

9495
// if there aren't any values we can skip this highlighted field
95-
const fieldValues = item.values;
96+
let fieldValues = item.values;
9697
if (!fieldValues || isEmpty(fieldValues)) {
9798
return acc;
9899
}
@@ -112,6 +113,17 @@ export const useHighlightedFields = ({
112113
return acc;
113114
}
114115

116+
/**
117+
* Source event use-case.
118+
* In this case we only want to show ancestors of level "0".
119+
* We can obtain those by using the `kibana.alert.ancestors.depth` field
120+
*/
121+
if (item.field === EVENT_SOURCE_FIELD_NAME) {
122+
const depts = dataFormattedForFieldBrowser.find(
123+
(_item) => _item.field === `kibana.alert.ancestors.depth`
124+
);
125+
fieldValues = fieldValues.filter((_, idx) => (depts?.values?.[idx] ?? '0') === '0');
126+
}
115127
return {
116128
...acc,
117129
[field.id]: {

x-pack/solutions/security/plugins/security_solution/public/flyout/shared/components/preview_link.tsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,11 @@ interface PreviewLinkProps {
3838
* React components to render, if none provided, the value will be rendered
3939
*/
4040
children?: React.ReactNode;
41+
/**
42+
* The indexName to be passed to the flyout preview panel
43+
* when clicking on "Source event" id
44+
*/
45+
ancestorsIndexName?: string;
4146
}
4247

4348
/**
@@ -50,6 +55,7 @@ export const PreviewLink: FC<PreviewLinkProps> = ({
5055
scopeId,
5156
ruleId,
5257
children,
58+
ancestorsIndexName,
5359
'data-test-subj': dataTestSubj = FLYOUT_PREVIEW_LINK_TEST_ID,
5460
}) => {
5561
const { openPreviewPanel } = useExpandableFlyoutApi();
@@ -62,8 +68,9 @@ export const PreviewLink: FC<PreviewLinkProps> = ({
6268
field,
6369
scopeId,
6470
ruleId,
71+
ancestorsIndexName,
6572
}),
66-
[value, field, scopeId, ruleId]
73+
[value, field, scopeId, ruleId, ancestorsIndexName]
6774
);
6875

6976
const onClick = useCallback(() => {

0 commit comments

Comments
 (0)