Skip to content

Commit 26ecbb6

Browse files
[Investigations] - Show detection alerts only guidance (#239975)
## Summary With the removal of the show detection alerts only feature, we want to provide guidance to users on how to update their timelines. The UI looks like this <img width="1197" height="955" alt="Screenshot 2025-11-12 at 10 55 14 PM" src="https://github.com/user-attachments/assets/9a5de15e-10cd-458a-a06c-2b17973bcb14" /> The callout here only displays when the saved index patterns are just the alerts index `.alerts-security.alerts-{space}`, but the data view is the default data view. The user can either - add an `Alerts only` filter (`_index: .alerts-security.alerts-{space}`) - filter for the `Security solution alerts` data view These 2 paths are shown in the following video https://github.com/user-attachments/assets/910fd069-a31c-4e4d-8e45-a88e7fd6e102 For Timelines that were not saved with the `Show detection alerts only` option, the callout is not displayed. ## How to test - start with the `newDataViewPickerEnabled` feature flag turned off - save a Timeline after making sure that you select the `Show detection alerts only` checbox and save the dataview - turn the `newDataViewPickerEnabled` feature flag back on - open the saved Timeline ### Checklist - [x] [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 - [x] The PR description includes the appropriate Release Notes section, and the correct `release_note:*` label is applied per the [guidelines](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process) - [x] Review the [backport guidelines](https://docs.google.com/document/d/1VyN5k91e5OVumlc0Gb9RPa3h1ewuPE705nRtioPiTvY/edit?usp=sharing) and apply applicable `backport:*` labels. --------- Co-authored-by: PhilippeOberti <[email protected]>
1 parent fd8a21a commit 26ecbb6

13 files changed

+659
-11
lines changed

x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/index.tsx

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ import { useDispatch, useSelector } from 'react-redux';
1212
import styled from 'styled-components';
1313

1414
import { isTab } from '@kbn/timelines-plugin/public';
15+
import { useSpaceId } from '../../../common/hooks/use_space_id';
16+
import { DEFAULT_ALERTS_INDEX, DEFAULT_DATA_VIEW_ID } from '../../../../common/constants';
1517
import { useIsExperimentalFeatureEnabled } from '../../../common/hooks/use_experimental_features';
1618
import { timelineActions, timelineSelectors } from '../../store';
1719
import { timelineDefaults } from '../../store/defaults';
@@ -101,6 +103,7 @@ const StatefulTimelineComponent: React.FC<Props> = ({
101103
const { timelineFullScreen } = useTimelineFullScreen();
102104

103105
const newDataViewPickerEnabled = useIsExperimentalFeatureEnabled('newDataViewPickerEnabled');
106+
const spaceId = useSpaceId();
104107
const experimentalSelectedPatterns = useSelectedPatterns(SourcererScopeName.timeline);
105108
const { dataView: experimentalDataView, status } = useDataView(SourcererScopeName.timeline);
106109

@@ -143,6 +146,15 @@ const StatefulTimelineComponent: React.FC<Props> = ({
143146
) {
144147
return;
145148
}
149+
// TODO: newDataViewPickerEnabled: With the new data view picker, we should not update the selected patterns
150+
// on timeline, as that prevents us from guiding the user to duplicate the data view or using the new alerts only dv
151+
if (
152+
selectedDataViewIdTimeline === `${DEFAULT_DATA_VIEW_ID}-${spaceId}` &&
153+
selectedPatternsTimeline.length === 1 &&
154+
selectedPatternsTimeline[0].includes(DEFAULT_ALERTS_INDEX)
155+
) {
156+
return;
157+
}
146158
dispatch(
147159
timelineActions.updateDataView({
148160
dataViewId: selectedDataViewId,
@@ -157,6 +169,7 @@ const StatefulTimelineComponent: React.FC<Props> = ({
157169
selectedDataViewIdTimeline,
158170
selectedPatterns,
159171
selectedPatternsTimeline,
172+
spaceId,
160173
timelineId,
161174
]);
162175

x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/tabs/query/header/index.test.tsx

Lines changed: 25 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,22 +6,23 @@
66
*/
77

88
import React from 'react';
9-
109
import { coreMock } from '@kbn/core/public/mocks';
1110
import { mockIndexPattern } from '../../../../../../common/mock';
1211
import { TestProviders } from '../../../../../../common/mock/test_providers';
1312
import { FilterManager } from '@kbn/data-plugin/public';
1413
import { mockDataProviders } from '../../../data_providers/mock/mock_data_providers';
1514
import { useMountAppended } from '../../../../../../common/utils/use_mount_appended';
16-
1715
import { QueryTabHeader } from '.';
1816
import { TimelineStatusEnum, TimelineTypeEnum } from '../../../../../../../common/api/timeline';
1917
import { waitFor } from '@testing-library/react';
2018
import { TimelineId, TimelineTabs } from '../../../../../../../common/types';
19+
import { useShouldShowAlertsOnlyMigrationMessage } from '../hooks/use_show_alerts_only_migration_message';
20+
import { CALLOUT_TEST_ID } from './migration_message_callout';
2121

2222
const mockUiSettingsForFilterManager = coreMock.createStart().uiSettings;
2323

2424
jest.mock('../../../../../../common/lib/kibana');
25+
jest.mock('../hooks/use_show_alerts_only_migration_message');
2526

2627
describe('Header', () => {
2728
const indexPattern = mockIndexPattern;
@@ -33,6 +34,8 @@ describe('Header', () => {
3334
};
3435
const props = {
3536
activeTab: TimelineTabs.query,
37+
currentIndices: ['index-1', 'index-2'],
38+
dataViewId: '',
3639
showEventsCountBadge: true,
3740
totalCount: 1,
3841
browserFields: {},
@@ -51,8 +54,8 @@ describe('Header', () => {
5154
timelineType: TimelineTypeEnum.default,
5255
};
5356

54-
describe('rendering', () => {
55-
test('it renders the data providers when show is true', async () => {
57+
describe('QueryTabHeader', () => {
58+
test('should render the data providers when show is true', async () => {
5659
const testProps = { ...props, show: true };
5760
const wrapper = await getWrapper(
5861
<TestProviders>
@@ -63,7 +66,7 @@ describe('Header', () => {
6366
expect(wrapper.find('[data-test-subj="dataProviders"]').exists()).toEqual(true);
6467
});
6568

66-
test('it renders the unauthorized call out providers', async () => {
69+
test('should render the unauthorized call out providers', async () => {
6770
const testProps = {
6871
...props,
6972
filterManager: new FilterManager(mockUiSettingsForFilterManager),
@@ -79,7 +82,7 @@ describe('Header', () => {
7982
expect(wrapper.find('[data-test-subj="timelineCallOutUnauthorized"]').exists()).toEqual(true);
8083
});
8184

82-
test('it renders the unauthorized call out with correct icon', async () => {
85+
test('should render the unauthorized call out with correct icon', async () => {
8386
const testProps = {
8487
...props,
8588
filterManager: new FilterManager(mockUiSettingsForFilterManager),
@@ -97,7 +100,7 @@ describe('Header', () => {
97100
).toEqual('warning');
98101
});
99102

100-
test('it renders the unauthorized call out with correct message', async () => {
103+
test('should render the unauthorized call out with correct message', async () => {
101104
const testProps = {
102105
...props,
103106
filterManager: new FilterManager(mockUiSettingsForFilterManager),
@@ -117,7 +120,7 @@ describe('Header', () => {
117120
);
118121
});
119122

120-
test('it renders the immutable timeline call out providers', async () => {
123+
test('should render the immutable timeline call out providers', async () => {
121124
const testProps = {
122125
...props,
123126
filterManager: new FilterManager(mockUiSettingsForFilterManager),
@@ -134,7 +137,7 @@ describe('Header', () => {
134137
expect(wrapper.find('[data-test-subj="timelineImmutableCallOut"]').exists()).toEqual(true);
135138
});
136139

137-
test('it renders the immutable timeline call out with correct icon', async () => {
140+
test('should render the immutable timeline call out with correct icon', async () => {
138141
const testProps = {
139142
...props,
140143
filterManager: new FilterManager(mockUiSettingsForFilterManager),
@@ -153,7 +156,7 @@ describe('Header', () => {
153156
).toEqual('warning');
154157
});
155158

156-
test('it renders the immutable timeline call out with correct message', async () => {
159+
test('should render the immutable timeline call out with correct message', async () => {
157160
const testProps = {
158161
...props,
159162
filterManager: new FilterManager(mockUiSettingsForFilterManager),
@@ -174,4 +177,16 @@ describe('Header', () => {
174177
);
175178
});
176179
});
180+
181+
test('should render the migration callout', async () => {
182+
(useShouldShowAlertsOnlyMigrationMessage as jest.Mock).mockReturnValue(true);
183+
184+
const wrapper = await getWrapper(
185+
<TestProviders>
186+
<QueryTabHeader {...props} />
187+
</TestProviders>
188+
);
189+
190+
expect(wrapper.find(`[data-test-subj="${CALLOUT_TEST_ID}"]`).exists()).toEqual(true);
191+
});
177192
});

x-pack/solutions/security/plugins/security_solution/public/timelines/components/timeline/tabs/query/header/index.tsx

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ import type { FilterManager } from '@kbn/data-plugin/public';
1111
import { InPortal } from 'react-reverse-portal';
1212
import { IS_DRAGGING_CLASS_NAME } from '@kbn/securitysolution-t-grid';
1313
import { css } from '@emotion/react';
14+
import { MigrationMessageCallout } from './migration_message_callout';
15+
import { useShouldShowAlertsOnlyMigrationMessage } from '../hooks/use_show_alerts_only_migration_message';
1416
import { useTimelineEventsCountPortal } from '../../../../../../common/hooks/use_timeline_events_count';
1517
import {
1618
type TimelineStatus,
@@ -28,6 +30,14 @@ import { EventsCountBadge, StyledEuiFlyoutHeader, TabHeaderContainer } from '../
2830

2931
interface Props {
3032
activeTab: TimelineTabs;
33+
/**
34+
* The currently selected timeline indices
35+
*/
36+
currentIndices: string[];
37+
/**
38+
* The id of the dataview
39+
*/
40+
dataViewId: string | null;
3141
filterManager: FilterManager;
3242
show: boolean;
3343
showCallOutUnauthorizedMsg: boolean;
@@ -61,6 +71,8 @@ const useStyles = (shouldShowQueryBuilder: boolean) => {
6171

6272
const QueryTabHeaderComponent: React.FC<Props> = ({
6373
activeTab,
74+
currentIndices,
75+
dataViewId,
6476
filterManager,
6577
show,
6678
showCallOutUnauthorizedMsg,
@@ -89,6 +101,11 @@ const QueryTabHeaderComponent: React.FC<Props> = ({
89101
);
90102
const dataProviderStyles = useStyles(shouldShowQueryBuilder);
91103

104+
const showAlertsOnlyMigrationMessage = useShouldShowAlertsOnlyMigrationMessage({
105+
currentTimelineIndices: currentIndices,
106+
dataViewId,
107+
});
108+
92109
return (
93110
<StyledEuiFlyoutHeader data-test-subj={`${activeTab}-tab-flyout-header`} hasBorder={false}>
94111
<InPortal node={timelineEventsCountPortalNode}>
@@ -103,6 +120,11 @@ const QueryTabHeaderComponent: React.FC<Props> = ({
103120
<EuiFlexItem>
104121
<StatefulSearchOrFilter filterManager={filterManager} timelineId={timelineId} />
105122
</EuiFlexItem>
123+
{showAlertsOnlyMigrationMessage && (
124+
<EuiFlexItem>
125+
<MigrationMessageCallout timelineId={timelineId} />
126+
</EuiFlexItem>
127+
)}
106128
{showCallOutUnauthorizedMsg && (
107129
<EuiFlexItem>
108130
<EuiCallOut
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0; you may not use this file except in compliance with the Elastic License
5+
* 2.0.
6+
*/
7+
8+
import React from 'react';
9+
import { render } from '@testing-library/react';
10+
import {
11+
ADD_ALERTS_FILTER_BUTTON_TEST_ID,
12+
ALERTS_ONLY_DATA_VIEW_BUTTON_TEST_ID,
13+
CALLOUT_TEST_ID,
14+
MigrationMessageCallout,
15+
} from './migration_message_callout';
16+
import { useTimelineSelectAlertsOnlyDataView } from '../hooks/use_timeline_select_alerts_only_data_view';
17+
import { useAddAlertsOnlyFilter } from '../hooks/use_add_alerts_only_filter';
18+
import {
19+
CALL_OUT_ALERTS_ONLY_MIGRATION_CONTENT,
20+
CALL_OUT_ALERTS_ONLY_MIGRATION_SWITCH_BUTTON,
21+
CALL_OUT_FILTER_FOR_ALERTS_BUTTON,
22+
} from './translations';
23+
24+
jest.mock('../hooks/use_timeline_select_alerts_only_data_view');
25+
jest.mock('../hooks/use_add_alerts_only_filter');
26+
27+
describe('MigrationMessageCallout', () => {
28+
beforeEach(() => {
29+
jest.clearAllMocks();
30+
});
31+
32+
it('should render correctly', () => {
33+
(useTimelineSelectAlertsOnlyDataView as jest.Mock).mockReturnValue(jest.fn());
34+
(useAddAlertsOnlyFilter as jest.Mock).mockReturnValue(jest.fn());
35+
36+
const { getByTestId } = render(<MigrationMessageCallout timelineId={'test'} />);
37+
38+
expect(getByTestId(CALLOUT_TEST_ID)).toHaveTextContent(CALL_OUT_ALERTS_ONLY_MIGRATION_CONTENT);
39+
expect(getByTestId(ALERTS_ONLY_DATA_VIEW_BUTTON_TEST_ID)).toHaveTextContent(
40+
CALL_OUT_ALERTS_ONLY_MIGRATION_SWITCH_BUTTON
41+
);
42+
expect(getByTestId(ADD_ALERTS_FILTER_BUTTON_TEST_ID)).toHaveTextContent(
43+
CALL_OUT_FILTER_FOR_ALERTS_BUTTON
44+
);
45+
});
46+
47+
it('should call the selectAlertsDataView callback', () => {
48+
const selectAlertsDataView = jest.fn();
49+
(useTimelineSelectAlertsOnlyDataView as jest.Mock).mockReturnValue(selectAlertsDataView);
50+
(useAddAlertsOnlyFilter as jest.Mock).mockReturnValue(jest.fn());
51+
52+
const { getByTestId } = render(<MigrationMessageCallout timelineId={'test'} />);
53+
54+
getByTestId(ALERTS_ONLY_DATA_VIEW_BUTTON_TEST_ID).click();
55+
expect(selectAlertsDataView).toHaveBeenCalled();
56+
});
57+
58+
it('should call the addAlertsFilter callback', () => {
59+
const addAlertsFilter = jest.fn();
60+
(useAddAlertsOnlyFilter as jest.Mock).mockReturnValue(addAlertsFilter);
61+
(useTimelineSelectAlertsOnlyDataView as jest.Mock).mockReturnValue(jest.fn());
62+
63+
const { getByTestId } = render(<MigrationMessageCallout timelineId={'test'} />);
64+
65+
getByTestId(ADD_ALERTS_FILTER_BUTTON_TEST_ID).click();
66+
expect(addAlertsFilter).toHaveBeenCalled();
67+
});
68+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0; you may not use this file except in compliance with the Elastic License
5+
* 2.0.
6+
*/
7+
8+
import {
9+
EuiButton,
10+
EuiButtonEmpty,
11+
EuiCallOut,
12+
EuiFlexGroup,
13+
EuiFlexItem,
14+
EuiText,
15+
} from '@elastic/eui';
16+
import React, { memo } from 'react';
17+
import { useTimelineSelectAlertsOnlyDataView } from '../hooks/use_timeline_select_alerts_only_data_view';
18+
import { useAddAlertsOnlyFilter } from '../hooks/use_add_alerts_only_filter';
19+
import * as i18n from './translations';
20+
21+
export const CALLOUT_TEST_ID = 'timelineAlertsOnlyCallOut';
22+
export const ALERTS_ONLY_DATA_VIEW_BUTTON_TEST_ID = 'timelineAlertsOnlyDataViewButton';
23+
export const ADD_ALERTS_FILTER_BUTTON_TEST_ID = 'timelineAddAlertsFitlerButton';
24+
25+
export interface MigrationMessageProps {
26+
/**
27+
* Id of the timeline the callout is being displayed in
28+
*/
29+
timelineId: string;
30+
}
31+
32+
/**
33+
* Callout message displayed in timelines to inform users that with the new data view picker
34+
* we don't support the "show detection alerts only" option we had with sourcerer.
35+
* So their option is to either swithc to use the alerts data view
36+
* or use the default data view and add an alerts-only filter.
37+
*/
38+
export const MigrationMessageCallout = memo(({ timelineId }: MigrationMessageProps) => {
39+
const selectAlertsDataView = useTimelineSelectAlertsOnlyDataView();
40+
const addAlertsFilter = useAddAlertsOnlyFilter({ timelineId });
41+
42+
return (
43+
<EuiCallOut
44+
announceOnMount={false}
45+
color="warning"
46+
data-test-subj={CALLOUT_TEST_ID}
47+
iconType="warning"
48+
size="m"
49+
title={i18n.CALL_OUT_ALERTS_ONLY_MIGRATION_TITLE}
50+
>
51+
<EuiFlexGroup justifyContent="spaceBetween" responsive={false}>
52+
<EuiFlexItem>
53+
<EuiText size="s">{i18n.CALL_OUT_ALERTS_ONLY_MIGRATION_CONTENT}</EuiText>
54+
</EuiFlexItem>
55+
<EuiFlexItem grow={false}>
56+
<EuiFlexGroup alignItems="center" gutterSize="xs" responsive={false}>
57+
<EuiFlexItem grow={false}>
58+
<EuiButtonEmpty
59+
aria-label={i18n.CALL_OUT_ALERTS_ONLY_MIGRATION_SWITCH_BUTTON}
60+
color="text"
61+
data-test-subj={ALERTS_ONLY_DATA_VIEW_BUTTON_TEST_ID}
62+
onClick={selectAlertsDataView}
63+
size="s"
64+
>
65+
{i18n.CALL_OUT_ALERTS_ONLY_MIGRATION_SWITCH_BUTTON}
66+
</EuiButtonEmpty>
67+
</EuiFlexItem>
68+
<EuiFlexItem grow={false}>
69+
<EuiButton
70+
aria-label={i18n.CALL_OUT_FILTER_FOR_ALERTS_BUTTON}
71+
color="warning"
72+
data-test-subj={ADD_ALERTS_FILTER_BUTTON_TEST_ID}
73+
fill
74+
onClick={addAlertsFilter}
75+
size="s"
76+
>
77+
{i18n.CALL_OUT_FILTER_FOR_ALERTS_BUTTON}
78+
</EuiButton>
79+
</EuiFlexItem>
80+
</EuiFlexGroup>
81+
</EuiFlexItem>
82+
</EuiFlexGroup>
83+
</EuiCallOut>
84+
);
85+
});
86+
87+
MigrationMessageCallout.displayName = 'MigrationMessageCallout';

0 commit comments

Comments
 (0)