From 5b6887dd3df7e92dce72bf58ff80e999d501a721 Mon Sep 17 00:00:00 2001
From: christineweng <18648970+christineweng@users.noreply.github.com>
Date: Tue, 10 Dec 2024 15:43:28 -0600
Subject: [PATCH] [Security Solution][Expandable flyout] Introducing Flyout
history in document flyout (#184970)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
## Summary
This PR introduced flyout history in expandable flyouts to keep tracked
of previously opened flyouts. The history button is available when
feature flag `newExpandableFlyoutNavigationEnabled` is enabled.
Flag is currently default `False`
### Changes in
[kbn-expandable-flyout](https://github.com/elastic/kibana/tree/main/packages/kbn-expandable-flyout)
package
- When `openFlyout` is called, the **right** panel will be appended to
the `history` slice in redux.
- History can be accessed via `useExpandableFlyoutHistory` API
![image](https://github.com/user-attachments/assets/081d6d6f-3c10-40f0-8882-73bc8c275e68)
### Changes to expandable flyouts in security solution
- When feature flag is on, opening more than 1 flyout will show a
history icon. Currently max at 10 entries
- When user clicks a flyout from the history, it does not add on top on
history, instead the position will be moved up. There is no duplicate
entries.
![image](https://github.com/user-attachments/assets/3bc68519-5eea-4fb7-9386-f6688b28b525)
**To illustrate how ordering works:**
-> History: [host1, user1, alert1]
-> clicks alert1
-> History: [alert1, host1, user1]
Keep in mind this is slightly different in the actual implementation, as
we do not display the current entry (i.e. alert1 in this example)
### Other changes in order to support flyout history
- Added a preview panel for network. Previously we reused the panel for
both network flyout and network preview. A dedicated network preview
with out history is now available
- Replaced `openRightPanel` with `openFlyout` in applicable places
- Added `isPreview` and `isPreviewMode` checks in EA flyouts
## How to test
- Enable feature flag `newExpandableFlyoutNavigationEnabled`
✅ Alerts page
Available for alert, host, user, rule name and ip's
✅ Explore pages (event table)
Available for events, host, user, rule name and ip's
✅ Cases
Note: the rule and entity link still go to a page, this will be
addressed in a separate PR
✅ Discover in severless
- enable `discover.experimental.enabledProfiles:
['security-root-profile']`
❌ Disabled in alert preview
❌ Disabled in preview mode
## WIP
- [x] Investigate performance with process history
- [ ] Final ui of the entries - pending UIUX team
### Checklist
- [x] Any text added follows [EUI's writing
guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses
sentence case text and includes [i18n
support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md)
- [x]
[Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html)
was added for features that require explanation or tutorials
- [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
---
packages/kbn-expandable-flyout/README.md | 2 +
packages/kbn-expandable-flyout/index.ts | 1 +
.../src/components/container.test.tsx | 5 +
.../src/components/preview_section.test.tsx | 1 +
.../hooks/use_expandable_flyout_history.ts | 22 ++
.../src/index.stories.tsx | 9 +
.../kbn-expandable-flyout/src/index.test.tsx | 1 +
.../src/provider.test.tsx | 2 +
.../src/store/reducers.test.ts | 64 ++++-
.../src/store/reducers.ts | 8 +
.../kbn-expandable-flyout/src/store/redux.ts | 2 +
.../kbn-expandable-flyout/src/store/state.ts | 4 +
.../translations/translations/fr-FR.json | 3 -
.../translations/translations/ja-JP.json | 3 -
.../translations/translations/zh-CN.json | 3 -
.../common/experimental_features.ts | 5 +
.../left/components/host_details.test.tsx | 5 +-
.../left/components/user_details.test.tsx | 5 +-
.../components/alert_header_title.test.tsx | 5 -
.../right/components/alert_header_title.tsx | 26 +-
.../right/components/event_header_title.tsx | 34 +--
.../highlighted_fields_cell.test.tsx | 5 +-
.../table_field_value_cell.test.tsx | 5 +-
.../document_details/right/navigation.tsx | 4 +-
.../right/utils/event_utils.tsx | 22 --
.../document_details/shared/utils.test.tsx | 46 ++-
.../flyout/document_details/shared/utils.tsx | 73 +++++
.../entity_details/host_right/index.test.tsx | 25 +-
.../entity_details/host_right/index.tsx | 13 +-
.../entity_details/user_right/index.test.tsx | 25 +-
.../entity_details/user_right/index.tsx | 6 +-
.../security_solution/public/flyout/index.tsx | 8 +-
.../public/flyout/network_details/header.tsx | 2 +-
.../public/flyout/network_details/index.tsx | 37 ++-
.../flyout/rule_details/right/content.tsx | 23 +-
.../flyout/rule_details/right/header.tsx | 6 +-
.../flyout/rule_details/right/index.test.tsx | 28 ++
.../flyout/rule_details/right/index.tsx | 5 +-
.../shared/components/flyout_history.test.tsx | 52 ++++
.../shared/components/flyout_history.tsx | 97 +++++++
.../components/flyout_history_row.test.tsx | 270 ++++++++++++++++++
.../shared/components/flyout_history_row.tsx | 186 ++++++++++++
.../components/flyout_navigation.test.tsx | 68 ++++-
.../shared/components/flyout_navigation.tsx | 59 +++-
.../shared/components/preview_link.test.tsx | 5 +-
.../flyout/shared/components/preview_link.tsx | 5 +-
.../flyout/shared/components/test_ids.ts | 15 +
.../flyout/shared/utils/history_utils.test.ts | 56 ++++
.../flyout/shared/utils/history_utils.ts | 31 ++
.../components/formatted_ip/index.test.tsx | 1 +
.../components/formatted_ip/index.tsx | 1 +
.../renderers/formatted_field_helpers.tsx | 14 +-
.../body/renderers/host_name.test.tsx | 42 +--
.../timeline/body/renderers/host_name.tsx | 21 +-
.../body/renderers/user_name.test.tsx | 40 +--
.../timeline/body/renderers/user_name.tsx | 20 +-
56 files changed, 1320 insertions(+), 206 deletions(-)
create mode 100644 packages/kbn-expandable-flyout/src/hooks/use_expandable_flyout_history.ts
create mode 100644 x-pack/plugins/security_solution/public/flyout/shared/components/flyout_history.test.tsx
create mode 100644 x-pack/plugins/security_solution/public/flyout/shared/components/flyout_history.tsx
create mode 100644 x-pack/plugins/security_solution/public/flyout/shared/components/flyout_history_row.test.tsx
create mode 100644 x-pack/plugins/security_solution/public/flyout/shared/components/flyout_history_row.tsx
create mode 100644 x-pack/plugins/security_solution/public/flyout/shared/utils/history_utils.test.ts
create mode 100644 x-pack/plugins/security_solution/public/flyout/shared/utils/history_utils.ts
diff --git a/packages/kbn-expandable-flyout/README.md b/packages/kbn-expandable-flyout/README.md
index 2bdd7ae3dfc48..930bf00334c56 100644
--- a/packages/kbn-expandable-flyout/README.md
+++ b/packages/kbn-expandable-flyout/README.md
@@ -61,6 +61,8 @@ To control (or mutate) flyout's layout, you can utilize [useExpandableFlyoutApi]
> The expandable flyout propagates the `onClose` callback from the EuiFlyout component. As we recommend having a single instance of the flyout in your application, it's up to the application's code to dispatch the event (through Redux, window events, observable, prop drilling...).
+When calling `openFlyout`, the right panel state is automatically appended in the `history` slice in the redux context. To access the flyout's history, you can use the [useExpandableFlyoutHistory](https://github.com/elastic/kibana/blob/main/packages/kbn-expandable-flyout/src/hooks/use_expandable_flyout_history.ts) hook.
+
## Usage
To use the expandable flyout in your plugin, first you need wrap your code with the [context provider](https://github.com/elastic/kibana/blob/main/packages/kbn-expandable-flyout/src/context.tsx) at a high enough level as follows:
diff --git a/packages/kbn-expandable-flyout/index.ts b/packages/kbn-expandable-flyout/index.ts
index 5816b6673dbc1..48478334b6590 100644
--- a/packages/kbn-expandable-flyout/index.ts
+++ b/packages/kbn-expandable-flyout/index.ts
@@ -11,6 +11,7 @@ export { ExpandableFlyout } from './src';
export { useExpandableFlyoutApi } from './src/hooks/use_expandable_flyout_api';
export { useExpandableFlyoutState } from './src/hooks/use_expandable_flyout_state';
+export { useExpandableFlyoutHistory } from './src/hooks/use_expandable_flyout_history';
export { type FlyoutPanels as ExpandableFlyoutState } from './src/store/state';
diff --git a/packages/kbn-expandable-flyout/src/components/container.test.tsx b/packages/kbn-expandable-flyout/src/components/container.test.tsx
index fa27d81fa4437..5482d73893c3a 100644
--- a/packages/kbn-expandable-flyout/src/components/container.test.tsx
+++ b/packages/kbn-expandable-flyout/src/components/container.test.tsx
@@ -58,6 +58,7 @@ describe('Container', () => {
},
left: undefined,
preview: undefined,
+ history: [{ id: 'key' }],
},
},
},
@@ -85,6 +86,7 @@ describe('Container', () => {
id: 'key',
},
preview: undefined,
+ history: [],
},
},
},
@@ -112,6 +114,7 @@ describe('Container', () => {
id: 'key',
},
],
+ history: [],
},
},
},
@@ -137,6 +140,7 @@ describe('Container', () => {
},
left: undefined,
preview: undefined,
+ history: [],
},
},
},
@@ -163,6 +167,7 @@ describe('Container', () => {
},
left: undefined,
preview: undefined,
+ history: [],
},
},
},
diff --git a/packages/kbn-expandable-flyout/src/components/preview_section.test.tsx b/packages/kbn-expandable-flyout/src/components/preview_section.test.tsx
index 6476ac91c0031..a6f927ca4eb0d 100644
--- a/packages/kbn-expandable-flyout/src/components/preview_section.test.tsx
+++ b/packages/kbn-expandable-flyout/src/components/preview_section.test.tsx
@@ -30,6 +30,7 @@ describe('PreviewSection', () => {
id: 'key',
},
],
+ history: [],
},
},
},
diff --git a/packages/kbn-expandable-flyout/src/hooks/use_expandable_flyout_history.ts b/packages/kbn-expandable-flyout/src/hooks/use_expandable_flyout_history.ts
new file mode 100644
index 0000000000000..415703a8811e4
--- /dev/null
+++ b/packages/kbn-expandable-flyout/src/hooks/use_expandable_flyout_history.ts
@@ -0,0 +1,22 @@
+/*
+ * 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", the "GNU Affero General Public License v3.0 only", and the "Server Side
+ * Public License v 1"; you may not use this file except in compliance with, at
+ * your election, the "Elastic License 2.0", the "GNU Affero General Public
+ * License v3.0 only", or the "Server Side Public License, v 1".
+ */
+
+import { REDUX_ID_FOR_MEMORY_STORAGE } from '../constants';
+import { useExpandableFlyoutContext } from '../context';
+import { selectHistoryById, useSelector } from '../store/redux';
+
+/**
+ * This hook allows you to access the flyout state, read open right, left and preview panels.
+ */
+export const useExpandableFlyoutHistory = () => {
+ const { urlKey } = useExpandableFlyoutContext();
+ // if no urlKey is provided, we are in memory storage mode and use the reserved word 'memory'
+ const id = urlKey || REDUX_ID_FOR_MEMORY_STORAGE;
+ return useSelector(selectHistoryById(id));
+};
diff --git a/packages/kbn-expandable-flyout/src/index.stories.tsx b/packages/kbn-expandable-flyout/src/index.stories.tsx
index 1e8e08d96c073..b827d02f1da08 100644
--- a/packages/kbn-expandable-flyout/src/index.stories.tsx
+++ b/packages/kbn-expandable-flyout/src/index.stories.tsx
@@ -111,6 +111,7 @@ export const Right: Story = () => {
},
left: undefined,
preview: undefined,
+ history: [{ id: 'right' }],
},
},
},
@@ -139,6 +140,7 @@ export const Left: Story = () => {
id: 'left',
},
preview: undefined,
+ history: [{ id: 'right' }],
},
},
},
@@ -171,6 +173,7 @@ export const Preview: Story = () => {
id: 'preview1',
},
],
+ history: [{ id: 'right' }],
},
},
},
@@ -206,6 +209,7 @@ export const MultiplePreviews: Story = () => {
id: 'preview2',
},
],
+ history: [{ id: 'right' }],
},
},
},
@@ -232,6 +236,7 @@ export const CollapsedPushMode: Story = () => {
},
left: undefined,
preview: undefined,
+ history: [{ id: 'right' }],
},
},
},
@@ -260,6 +265,7 @@ export const ExpandedPushMode: Story = () => {
id: 'left',
},
preview: undefined,
+ history: [{ id: 'right' }],
},
},
},
@@ -288,6 +294,7 @@ export const DisableTypeSelection: Story = () => {
id: 'left',
},
preview: undefined,
+ history: [{ id: 'right' }],
},
},
},
@@ -318,6 +325,7 @@ export const ResetWidths: Story = () => {
id: 'left',
},
preview: undefined,
+ history: [{ id: 'right' }],
},
},
},
@@ -343,6 +351,7 @@ export const DisableResizeWidthSelection: Story = () => {
id: 'left',
},
preview: undefined,
+ history: [{ id: 'right' }],
},
},
},
diff --git a/packages/kbn-expandable-flyout/src/index.test.tsx b/packages/kbn-expandable-flyout/src/index.test.tsx
index 8ee4ff32a9821..5f0bada8653a1 100644
--- a/packages/kbn-expandable-flyout/src/index.test.tsx
+++ b/packages/kbn-expandable-flyout/src/index.test.tsx
@@ -51,6 +51,7 @@ describe('ExpandableFlyout', () => {
},
left: undefined,
preview: undefined,
+ history: [{ id: 'key' }],
},
},
},
diff --git a/packages/kbn-expandable-flyout/src/provider.test.tsx b/packages/kbn-expandable-flyout/src/provider.test.tsx
index 7d7e6f8ab10c0..0d8e935098a3f 100644
--- a/packages/kbn-expandable-flyout/src/provider.test.tsx
+++ b/packages/kbn-expandable-flyout/src/provider.test.tsx
@@ -34,6 +34,7 @@ describe('UrlSynchronizer', () => {
right: { id: 'key1' },
left: { id: 'key11' },
preview: undefined,
+ history: [{ id: 'key1' }],
},
},
needsSync: true,
@@ -93,6 +94,7 @@ describe('UrlSynchronizer', () => {
right: { id: 'key1' },
left: { id: 'key2' },
preview: undefined,
+ history: [{ id: 'key1' }],
},
},
needsSync: true,
diff --git a/packages/kbn-expandable-flyout/src/store/reducers.test.ts b/packages/kbn-expandable-flyout/src/store/reducers.test.ts
index 1a887333daca8..e77a53c911319 100644
--- a/packages/kbn-expandable-flyout/src/store/reducers.test.ts
+++ b/packages/kbn-expandable-flyout/src/store/reducers.test.ts
@@ -74,19 +74,21 @@ describe('panelsReducer', () => {
left: leftPanel1,
right: rightPanel1,
preview: [previewPanel1],
+ history: [rightPanel1],
},
},
needsSync: true,
});
});
- it('should override all panels in the state', () => {
+ it('should override all panels in the state and update history', () => {
const state: PanelsState = {
byId: {
[id1]: {
left: leftPanel1,
right: rightPanel1,
preview: [previewPanel1, { id: 'preview' }],
+ history: [rightPanel1],
},
},
};
@@ -104,6 +106,7 @@ describe('panelsReducer', () => {
left: leftPanel2,
right: rightPanel2,
preview: [previewPanel2],
+ history: [rightPanel1, rightPanel2],
},
},
needsSync: true,
@@ -117,6 +120,7 @@ describe('panelsReducer', () => {
left: leftPanel1,
right: rightPanel1,
preview: [previewPanel1],
+ history: [rightPanel1],
},
},
};
@@ -132,6 +136,7 @@ describe('panelsReducer', () => {
left: undefined,
right: rightPanel2,
preview: undefined,
+ history: [rightPanel1, rightPanel2],
},
},
needsSync: true,
@@ -145,6 +150,7 @@ describe('panelsReducer', () => {
left: leftPanel1,
right: rightPanel1,
preview: [previewPanel1],
+ history: [rightPanel1],
},
},
};
@@ -160,11 +166,13 @@ describe('panelsReducer', () => {
left: leftPanel1,
right: rightPanel1,
preview: [previewPanel1],
+ history: [rightPanel1],
},
[id2]: {
left: undefined,
right: rightPanel2,
preview: undefined,
+ history: [rightPanel2],
},
},
needsSync: true,
@@ -173,7 +181,7 @@ describe('panelsReducer', () => {
});
describe('should handle openRightPanel action', () => {
- it('should add right panel to empty state', () => {
+ it('should add right panel to empty state but does not update history', () => {
const state: PanelsState = initialPanelsState;
const action = openRightPanelAction({ right: rightPanel1, id: id1 });
const newState: PanelsState = panelsReducer(state, action);
@@ -184,19 +192,21 @@ describe('panelsReducer', () => {
left: undefined,
right: rightPanel1,
preview: undefined,
+ history: [],
},
},
needsSync: true,
});
});
- it('should replace right panel', () => {
+ it('should replace right panel but does not update history', () => {
const state: PanelsState = {
byId: {
[id1]: {
left: leftPanel1,
right: rightPanel1,
preview: [previewPanel1],
+ history: [rightPanel1],
},
},
};
@@ -209,6 +219,7 @@ describe('panelsReducer', () => {
left: leftPanel1,
right: rightPanel2,
preview: [previewPanel1],
+ history: [rightPanel1],
},
},
needsSync: true,
@@ -222,6 +233,7 @@ describe('panelsReducer', () => {
left: leftPanel1,
right: rightPanel1,
preview: [previewPanel1],
+ history: [rightPanel1],
},
},
};
@@ -234,11 +246,13 @@ describe('panelsReducer', () => {
left: leftPanel1,
right: rightPanel1,
preview: [previewPanel1],
+ history: [rightPanel1],
},
[id2]: {
left: undefined,
right: rightPanel2,
preview: undefined,
+ history: [],
},
},
needsSync: true,
@@ -258,6 +272,7 @@ describe('panelsReducer', () => {
left: leftPanel1,
right: undefined,
preview: undefined,
+ history: [],
},
},
needsSync: true,
@@ -271,6 +286,7 @@ describe('panelsReducer', () => {
left: leftPanel1,
right: rightPanel1,
preview: [previewPanel1],
+ history: [],
},
},
};
@@ -283,6 +299,7 @@ describe('panelsReducer', () => {
left: leftPanel2,
right: rightPanel1,
preview: [previewPanel1],
+ history: [],
},
},
needsSync: true,
@@ -296,6 +313,7 @@ describe('panelsReducer', () => {
left: leftPanel1,
right: rightPanel1,
preview: [previewPanel1],
+ history: [],
},
},
};
@@ -308,11 +326,13 @@ describe('panelsReducer', () => {
left: leftPanel1,
right: rightPanel1,
preview: [previewPanel1],
+ history: [],
},
[id2]: {
left: leftPanel2,
right: undefined,
preview: undefined,
+ history: [],
},
},
needsSync: true,
@@ -332,6 +352,7 @@ describe('panelsReducer', () => {
left: undefined,
right: undefined,
preview: [previewPanel1],
+ history: [],
},
},
needsSync: true,
@@ -345,6 +366,7 @@ describe('panelsReducer', () => {
left: leftPanel1,
right: rightPanel1,
preview: [previewPanel1],
+ history: [],
},
},
};
@@ -357,6 +379,7 @@ describe('panelsReducer', () => {
left: leftPanel1,
right: rightPanel1,
preview: [previewPanel1, previewPanel2],
+ history: [],
},
},
needsSync: true,
@@ -370,6 +393,7 @@ describe('panelsReducer', () => {
left: leftPanel1,
right: rightPanel1,
preview: [previewPanel1],
+ history: [],
},
},
};
@@ -382,11 +406,13 @@ describe('panelsReducer', () => {
left: leftPanel1,
right: rightPanel1,
preview: [previewPanel1],
+ history: [],
},
[id2]: {
left: undefined,
right: undefined,
preview: [previewPanel2],
+ history: [],
},
},
needsSync: true,
@@ -413,6 +439,7 @@ describe('panelsReducer', () => {
left: leftPanel1,
right: undefined,
preview: [previewPanel1],
+ history: [],
},
},
};
@@ -432,6 +459,7 @@ describe('panelsReducer', () => {
left: leftPanel1,
right: rightPanel1,
preview: [previewPanel1],
+ history: [rightPanel1],
},
},
};
@@ -445,6 +473,7 @@ describe('panelsReducer', () => {
left: leftPanel1,
right: undefined,
preview: [previewPanel1],
+ history: [rightPanel1],
},
},
needsSync: true,
@@ -458,6 +487,7 @@ describe('panelsReducer', () => {
left: leftPanel1,
right: rightPanel1,
preview: [previewPanel1],
+ history: [rightPanel1],
},
},
};
@@ -471,6 +501,7 @@ describe('panelsReducer', () => {
left: leftPanel1,
right: rightPanel1,
preview: [previewPanel1],
+ history: [rightPanel1],
},
},
needsSync: true,
@@ -497,6 +528,7 @@ describe('panelsReducer', () => {
left: undefined,
right: rightPanel1,
preview: [previewPanel1],
+ history: [rightPanel1],
},
},
};
@@ -516,6 +548,7 @@ describe('panelsReducer', () => {
left: leftPanel1,
right: rightPanel1,
preview: [previewPanel1],
+ history: [rightPanel1],
},
},
};
@@ -528,6 +561,7 @@ describe('panelsReducer', () => {
left: undefined,
right: rightPanel1,
preview: [previewPanel1],
+ history: [rightPanel1],
},
},
needsSync: true,
@@ -541,6 +575,7 @@ describe('panelsReducer', () => {
left: leftPanel1,
right: rightPanel1,
preview: [previewPanel1],
+ history: [rightPanel1],
},
},
};
@@ -553,6 +588,7 @@ describe('panelsReducer', () => {
left: leftPanel1,
right: rightPanel1,
preview: [previewPanel1],
+ history: [rightPanel1],
},
},
needsSync: true,
@@ -579,6 +615,7 @@ describe('panelsReducer', () => {
left: leftPanel1,
right: rightPanel1,
preview: undefined,
+ history: [rightPanel1],
},
},
};
@@ -598,6 +635,7 @@ describe('panelsReducer', () => {
left: rightPanel1,
right: leftPanel1,
preview: [previewPanel1, previewPanel2],
+ history: [rightPanel1],
},
},
};
@@ -610,6 +648,7 @@ describe('panelsReducer', () => {
left: rightPanel1,
right: leftPanel1,
preview: undefined,
+ history: [rightPanel1],
},
},
needsSync: true,
@@ -623,6 +662,7 @@ describe('panelsReducer', () => {
left: leftPanel1,
right: rightPanel1,
preview: [previewPanel1],
+ history: [rightPanel1],
},
},
};
@@ -635,6 +675,7 @@ describe('panelsReducer', () => {
left: leftPanel1,
right: rightPanel1,
preview: [previewPanel1],
+ history: [rightPanel1],
},
},
needsSync: true,
@@ -661,6 +702,7 @@ describe('panelsReducer', () => {
left: leftPanel1,
right: rightPanel1,
preview: undefined,
+ history: [rightPanel1],
},
},
};
@@ -677,9 +719,10 @@ describe('panelsReducer', () => {
const state: PanelsState = {
byId: {
[id1]: {
- left: rightPanel1,
- right: leftPanel1,
+ left: leftPanel1,
+ right: rightPanel1,
preview: [previewPanel1, previewPanel2],
+ history: [rightPanel1],
},
},
};
@@ -689,9 +732,10 @@ describe('panelsReducer', () => {
expect(newState).toEqual({
byId: {
[id1]: {
- left: rightPanel1,
- right: leftPanel1,
+ left: leftPanel1,
+ right: rightPanel1,
preview: [previewPanel1],
+ history: [rightPanel1],
},
},
needsSync: false,
@@ -705,6 +749,7 @@ describe('panelsReducer', () => {
left: leftPanel1,
right: rightPanel1,
preview: [previewPanel1],
+ history: [rightPanel1],
},
},
};
@@ -717,6 +762,7 @@ describe('panelsReducer', () => {
left: leftPanel1,
right: rightPanel1,
preview: [previewPanel1],
+ history: [rightPanel1],
},
},
needsSync: false,
@@ -743,6 +789,7 @@ describe('panelsReducer', () => {
left: leftPanel1,
right: rightPanel1,
preview: [previewPanel1],
+ history: [rightPanel1],
},
},
};
@@ -755,6 +802,7 @@ describe('panelsReducer', () => {
left: undefined,
right: undefined,
preview: undefined,
+ history: [rightPanel1],
},
},
needsSync: true,
@@ -768,6 +816,7 @@ describe('panelsReducer', () => {
left: leftPanel1,
right: rightPanel1,
preview: [previewPanel1],
+ history: [rightPanel1],
},
},
};
@@ -780,6 +829,7 @@ describe('panelsReducer', () => {
left: leftPanel1,
right: rightPanel1,
preview: [previewPanel1],
+ history: [rightPanel1],
},
},
needsSync: true,
diff --git a/packages/kbn-expandable-flyout/src/store/reducers.ts b/packages/kbn-expandable-flyout/src/store/reducers.ts
index b14aa0b1b703b..be2c47344526e 100644
--- a/packages/kbn-expandable-flyout/src/store/reducers.ts
+++ b/packages/kbn-expandable-flyout/src/store/reducers.ts
@@ -35,11 +35,15 @@ export const panelsReducer = createReducer(initialPanelsState, (builder) => {
state.byId[id].right = right;
state.byId[id].left = left;
state.byId[id].preview = preview ? [preview] : undefined;
+ if (right) {
+ state.byId[id].history?.push(right);
+ }
} else {
state.byId[id] = {
left,
right,
preview: preview ? [preview] : undefined,
+ history: right ? [right] : [],
};
}
@@ -54,6 +58,7 @@ export const panelsReducer = createReducer(initialPanelsState, (builder) => {
left,
right: undefined,
preview: undefined,
+ history: [],
};
}
@@ -68,6 +73,7 @@ export const panelsReducer = createReducer(initialPanelsState, (builder) => {
right,
left: undefined,
preview: undefined,
+ history: [],
};
}
@@ -90,6 +96,7 @@ export const panelsReducer = createReducer(initialPanelsState, (builder) => {
right: undefined,
left: undefined,
preview: preview ? [preview] : undefined,
+ history: [],
};
}
@@ -149,6 +156,7 @@ export const panelsReducer = createReducer(initialPanelsState, (builder) => {
right,
left,
preview: preview ? [preview] : undefined,
+ history: right ? [right] : [], // update history only when loading flyout on refresh
};
}
diff --git a/packages/kbn-expandable-flyout/src/store/redux.ts b/packages/kbn-expandable-flyout/src/store/redux.ts
index d68b4a0295769..7f37017652bbc 100644
--- a/packages/kbn-expandable-flyout/src/store/redux.ts
+++ b/packages/kbn-expandable-flyout/src/store/redux.ts
@@ -48,6 +48,8 @@ const panelsSelector = createSelector(stateSelector, (state) => state.panels);
export const selectPanelsById = (id: string) =>
createSelector(panelsSelector, (state) => state.byId[id] || {});
export const selectNeedsSync = () => createSelector(panelsSelector, (state) => state.needsSync);
+export const selectHistoryById = (id: string) =>
+ createSelector(stateSelector, (state) => state.panels.byId[id].history || []);
const uiSelector = createSelector(stateSelector, (state) => state.ui);
export const selectPushVsOverlay = createSelector(uiSelector, (state) => state.pushVsOverlay);
diff --git a/packages/kbn-expandable-flyout/src/store/state.ts b/packages/kbn-expandable-flyout/src/store/state.ts
index e158f61aaccd5..46326c311fbeb 100644
--- a/packages/kbn-expandable-flyout/src/store/state.ts
+++ b/packages/kbn-expandable-flyout/src/store/state.ts
@@ -22,6 +22,10 @@ export interface FlyoutPanels {
* Panels to render in the preview section
*/
preview: FlyoutPanelProps[] | undefined;
+ /*
+ * History of the right panels that were opened
+ */
+ history: FlyoutPanelProps[];
}
export interface PanelsState {
diff --git a/x-pack/platform/plugins/private/translations/translations/fr-FR.json b/x-pack/platform/plugins/private/translations/translations/fr-FR.json
index 414a9a7ee12a9..898aac0bf0860 100644
--- a/x-pack/platform/plugins/private/translations/translations/fr-FR.json
+++ b/x-pack/platform/plugins/private/translations/translations/fr-FR.json
@@ -40777,9 +40777,6 @@
"xpack.securitySolution.flyout.right.response.responseButtonLabel": "Réponse",
"xpack.securitySolution.flyout.right.response.sectionTitle": "Réponse",
"xpack.securitySolution.flyout.right.rule.rulePreviewTitle": "Afficher les détails de la règle",
- "xpack.securitySolution.flyout.right.title.alertEventTitle": "Détails d'alerte externe",
- "xpack.securitySolution.flyout.right.title.eventTitle": "Détails de l'événement",
- "xpack.securitySolution.flyout.right.title.otherEventTitle": "Détails de {eventKind}",
"xpack.securitySolution.flyout.right.user.userPreviewTitle": "Aperçu des détails de l'utilisateur",
"xpack.securitySolution.flyout.right.visualizations.analyzerPreview.analyzerPreviewInvestigateTooltip": "Investiguer dans la chronologie",
"xpack.securitySolution.flyout.right.visualizations.analyzerPreview.analyzerPreviewOpenAnalyzerTooltip": "Ouvrir l'analyseur de graphe",
diff --git a/x-pack/platform/plugins/private/translations/translations/ja-JP.json b/x-pack/platform/plugins/private/translations/translations/ja-JP.json
index 6c3164faad045..1e4409f185464 100644
--- a/x-pack/platform/plugins/private/translations/translations/ja-JP.json
+++ b/x-pack/platform/plugins/private/translations/translations/ja-JP.json
@@ -40634,9 +40634,6 @@
"xpack.securitySolution.flyout.right.response.responseButtonLabel": "応答",
"xpack.securitySolution.flyout.right.response.sectionTitle": "応答",
"xpack.securitySolution.flyout.right.rule.rulePreviewTitle": "ルール詳細をプレビュー",
- "xpack.securitySolution.flyout.right.title.alertEventTitle": "外部アラート詳細",
- "xpack.securitySolution.flyout.right.title.eventTitle": "イベントの詳細",
- "xpack.securitySolution.flyout.right.title.otherEventTitle": "{eventKind}詳細",
"xpack.securitySolution.flyout.right.user.userPreviewTitle": "ユーザー詳細をプレビュー",
"xpack.securitySolution.flyout.right.visualizations.analyzerPreview.analyzerPreviewInvestigateTooltip": "タイムラインで調査",
"xpack.securitySolution.flyout.right.visualizations.analyzerPreview.analyzerPreviewOpenAnalyzerTooltip": "アナライザーグラフを開く",
diff --git a/x-pack/platform/plugins/private/translations/translations/zh-CN.json b/x-pack/platform/plugins/private/translations/translations/zh-CN.json
index 3fb56f88c24e0..c1508435745b9 100644
--- a/x-pack/platform/plugins/private/translations/translations/zh-CN.json
+++ b/x-pack/platform/plugins/private/translations/translations/zh-CN.json
@@ -40032,9 +40032,6 @@
"xpack.securitySolution.flyout.right.response.responseButtonLabel": "响应",
"xpack.securitySolution.flyout.right.response.sectionTitle": "响应",
"xpack.securitySolution.flyout.right.rule.rulePreviewTitle": "预览规则详情",
- "xpack.securitySolution.flyout.right.title.alertEventTitle": "外部告警详情",
- "xpack.securitySolution.flyout.right.title.eventTitle": "事件详情",
- "xpack.securitySolution.flyout.right.title.otherEventTitle": "{eventKind} 详情",
"xpack.securitySolution.flyout.right.user.userPreviewTitle": "预览用户详情",
"xpack.securitySolution.flyout.right.visualizations.analyzerPreview.analyzerPreviewInvestigateTooltip": "在时间线中调查",
"xpack.securitySolution.flyout.right.visualizations.analyzerPreview.analyzerPreviewOpenAnalyzerTooltip": "打开分析器图表",
diff --git a/x-pack/plugins/security_solution/common/experimental_features.ts b/x-pack/plugins/security_solution/common/experimental_features.ts
index 05b3c2ac9af51..428a48cf4b7be 100644
--- a/x-pack/plugins/security_solution/common/experimental_features.ts
+++ b/x-pack/plugins/security_solution/common/experimental_features.ts
@@ -258,6 +258,11 @@ export const allowedExperimentalValues = Object.freeze({
*/
defendInsights: false,
+ /**
+ * Enables flyout history and new preview navigation
+ */
+ newExpandableFlyoutNavigationEnabled: false,
+
/**
* Enables CrowdStrike's RunScript RTR command
*/
diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/left/components/host_details.test.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/left/components/host_details.test.tsx
index a5b2307dd9ca3..e08a794665222 100644
--- a/x-pack/plugins/security_solution/public/flyout/document_details/left/components/host_details.test.tsx
+++ b/x-pack/plugins/security_solution/public/flyout/document_details/left/components/host_details.test.tsx
@@ -39,7 +39,7 @@ import { HostPreviewPanelKey } from '../../../entity_details/host_right';
import { HOST_PREVIEW_BANNER } from '../../right/components/host_entity_overview';
import { UserPreviewPanelKey } from '../../../entity_details/user_right';
import { USER_PREVIEW_BANNER } from '../../right/components/user_entity_overview';
-import { NetworkPanelKey, NETWORK_PREVIEW_BANNER } from '../../../network_details';
+import { NetworkPreviewPanelKey, NETWORK_PREVIEW_BANNER } from '../../../network_details';
import { useAlertsByStatus } from '../../../../overview/components/detection_response/alerts_by_status/use_alerts_by_status';
jest.mock('@kbn/expandable-flyout');
@@ -313,10 +313,11 @@ describe('', () => {
getAllByTestId(HOST_DETAILS_RELATED_USERS_IP_LINK_TEST_ID)[0].click();
expect(mockFlyoutApi.openPreviewPanel).toHaveBeenCalledWith({
- id: NetworkPanelKey,
+ id: NetworkPreviewPanelKey,
params: {
ip: '100.XXX.XXX',
flowTarget: 'source',
+ scopeId: defaultProps.scopeId,
banner: NETWORK_PREVIEW_BANNER,
},
});
diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/left/components/user_details.test.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/left/components/user_details.test.tsx
index 28389919dec87..5bfb8a7df50db 100644
--- a/x-pack/plugins/security_solution/public/flyout/document_details/left/components/user_details.test.tsx
+++ b/x-pack/plugins/security_solution/public/flyout/document_details/left/components/user_details.test.tsx
@@ -37,7 +37,7 @@ import { HostPreviewPanelKey } from '../../../entity_details/host_right';
import { HOST_PREVIEW_BANNER } from '../../right/components/host_entity_overview';
import { UserPreviewPanelKey } from '../../../entity_details/user_right';
import { USER_PREVIEW_BANNER } from '../../right/components/user_entity_overview';
-import { NetworkPanelKey, NETWORK_PREVIEW_BANNER } from '../../../network_details';
+import { NetworkPreviewPanelKey, NETWORK_PREVIEW_BANNER } from '../../../network_details';
import { useAlertsByStatus } from '../../../../overview/components/detection_response/alerts_by_status/use_alerts_by_status';
jest.mock('@kbn/expandable-flyout');
@@ -291,10 +291,11 @@ describe('', () => {
getAllByTestId(USER_DETAILS_RELATED_HOSTS_IP_LINK_TEST_ID)[0].click();
expect(mockFlyoutApi.openPreviewPanel).toHaveBeenCalledWith({
- id: NetworkPanelKey,
+ id: NetworkPreviewPanelKey,
params: {
ip: '100.XXX.XXX',
flowTarget: 'source',
+ scopeId: defaultProps.scopeId,
banner: NETWORK_PREVIEW_BANNER,
},
});
diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/alert_header_title.test.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/alert_header_title.test.tsx
index 8a8293badb6af..b2d8e64c34b45 100644
--- a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/alert_header_title.test.tsx
+++ b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/alert_header_title.test.tsx
@@ -83,9 +83,4 @@ describe('', () => {
const { getByTestId } = renderHeader(mockContextValue);
expect(getByTestId(NOTES_TITLE_TEST_ID)).toBeInTheDocument();
});
-
- it('should render fall back values if document is not alert', () => {
- const { getByTestId } = renderHeader({ ...mockContextValue, dataFormattedForFieldBrowser: [] });
- expect(getByTestId(HEADER_TEXT_TEST_ID)).toHaveTextContent('Document details');
- });
});
diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/alert_header_title.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/alert_header_title.tsx
index cc7ef14585833..529e3d43b6056 100644
--- a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/alert_header_title.tsx
+++ b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/alert_header_title.tsx
@@ -8,7 +8,6 @@
import React, { memo, useCallback, useMemo } from 'react';
import { EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiLink } from '@elastic/eui';
import { ALERT_WORKFLOW_ASSIGNEE_IDS } from '@kbn/rule-data-utils';
-import { i18n } from '@kbn/i18n';
import { useIsExperimentalFeatureEnabled } from '../../../../common/hooks/use_experimental_features';
import { Notes } from './notes';
import { useRuleDetailsLink } from '../../shared/hooks/use_rule_details_link';
@@ -22,6 +21,7 @@ import { PreferenceFormattedDate } from '../../../../common/components/formatted
import { FLYOUT_ALERT_HEADER_TITLE_TEST_ID, ALERT_SUMMARY_PANEL_TEST_ID } from './test_ids';
import { Assignees } from './assignees';
import { FlyoutTitle } from '../../../shared/components/flyout_title';
+import { getAlertTitle } from '../../shared/utils';
// minWidth for each block, allows to switch for a 1 row 4 blocks to 2 rows with 2 block each
const blockStyles = {
@@ -44,17 +44,15 @@ export const AlertHeaderTitle = memo(() => {
'securitySolutionNotesDisabled'
);
- const { isAlert, ruleName, timestamp, ruleId } = useBasicDataFromDetailsData(
- dataFormattedForFieldBrowser
- );
-
+ const { ruleName, timestamp, ruleId } = useBasicDataFromDetailsData(dataFormattedForFieldBrowser);
+ const title = useMemo(() => getAlertTitle({ ruleName }), [ruleName]);
const href = useRuleDetailsLink({ ruleId: !isPreview ? ruleId : null });
const ruleTitle = useMemo(
() =>
href ? (
{
) : (
),
- [ruleName, href]
+ [title, href]
);
const { refetch } = useRefetchByScope({ scopeId });
@@ -86,17 +84,7 @@ export const AlertHeaderTitle = memo(() => {
{timestamp && }
- {isAlert && ruleName ? (
- ruleTitle
- ) : (
-
- )}
+ {ruleTitle}
{securitySolutionNotesDisabled ? (
{
const eventKind = getField(getFieldsData('event.kind'));
const eventCategory = getField(getFieldsData('event.category'));
- const title = useMemo(() => {
- const defaultTitle = i18n.translate('xpack.securitySolution.flyout.right.title.eventTitle', {
- defaultMessage: `Event details`,
- });
-
- if (eventKind === 'event' && eventCategory) {
- const fieldName = EVENT_CATEGORY_TO_FIELD[eventCategory];
- return getField(getFieldsData(fieldName)) ?? defaultTitle;
- }
-
- if (eventKind === 'alert') {
- return i18n.translate('xpack.securitySolution.flyout.right.title.alertEventTitle', {
- defaultMessage: 'External alert details',
- });
- }
-
- return eventKind
- ? i18n.translate('xpack.securitySolution.flyout.right.title.otherEventTitle', {
- defaultMessage: '{eventKind} details',
- values: {
- eventKind: startCase(eventKind),
- },
- })
- : defaultTitle;
- }, [eventKind, getFieldsData, eventCategory]);
+ const title = useMemo(
+ () => getEventTitle({ eventKind, eventCategory, getFieldsData }),
+ [eventKind, eventCategory, getFieldsData]
+ );
return (
<>
diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/highlighted_fields_cell.test.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/highlighted_fields_cell.test.tsx
index d819365da00b7..ff003813f260d 100644
--- a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/highlighted_fields_cell.test.tsx
+++ b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/highlighted_fields_cell.test.tsx
@@ -26,7 +26,7 @@ import { HostPreviewPanelKey } from '../../../entity_details/host_right';
import { HOST_PREVIEW_BANNER } from './host_entity_overview';
import { UserPreviewPanelKey } from '../../../entity_details/user_right';
import { USER_PREVIEW_BANNER } from './user_entity_overview';
-import { NetworkPanelKey, NETWORK_PREVIEW_BANNER } from '../../../network_details';
+import { NetworkPreviewPanelKey, NETWORK_PREVIEW_BANNER } from '../../../network_details';
import { createTelemetryServiceMock } from '../../../../common/lib/telemetry/telemetry_service.mock';
jest.mock('../../../../management/hooks');
@@ -137,10 +137,11 @@ describe('', () => {
getByTestId(HIGHLIGHTED_FIELDS_LINKED_CELL_TEST_ID).click();
expect(mockFlyoutApi.openPreviewPanel).toHaveBeenCalledWith({
- id: NetworkPanelKey,
+ id: NetworkPreviewPanelKey,
params: {
ip: '100:XXX:XXX',
flowTarget: 'source',
+ scopeId: panelContextValue.scopeId,
banner: NETWORK_PREVIEW_BANNER,
},
});
diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/table_field_value_cell.test.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/table_field_value_cell.test.tsx
index ee680f1061621..eec53dbe3d262 100644
--- a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/table_field_value_cell.test.tsx
+++ b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/table_field_value_cell.test.tsx
@@ -14,7 +14,7 @@ import { useExpandableFlyoutApi } from '@kbn/expandable-flyout';
import type { EventFieldsData } from '../../../../common/components/event_details/types';
import { TableFieldValueCell } from './table_field_value_cell';
import { TestProviders } from '../../../../common/mock';
-import { NetworkPanelKey, NETWORK_PREVIEW_BANNER } from '../../../network_details';
+import { NetworkPreviewPanelKey, NETWORK_PREVIEW_BANNER } from '../../../network_details';
import { mockFlyoutApi } from '../../shared/mocks/mock_flyout_context';
import { FLYOUT_TABLE_PREVIEW_LINK_FIELD_TEST_ID } from './test_ids';
import { createTelemetryServiceMock } from '../../../../common/lib/telemetry/telemetry_service.mock';
@@ -217,10 +217,11 @@ describe('TableFieldValueCell', () => {
screen.getByTestId(`${FLYOUT_TABLE_PREVIEW_LINK_FIELD_TEST_ID}-0`).click();
expect(mockFlyoutApi.openPreviewPanel).toHaveBeenCalledWith({
- id: NetworkPanelKey,
+ id: NetworkPreviewPanelKey,
params: {
ip: '127.0.0.1',
flowTarget: 'source',
+ scopeId: 'scopeId',
banner: NETWORK_PREVIEW_BANNER,
},
});
diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/navigation.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/right/navigation.tsx
index c3ee6a7d7a51a..2a17b76c03855 100644
--- a/x-pack/plugins/security_solution/public/flyout/document_details/right/navigation.tsx
+++ b/x-pack/plugins/security_solution/public/flyout/document_details/right/navigation.tsx
@@ -25,7 +25,7 @@ interface PanelNavigationProps {
export const PanelNavigation: FC = memo(({ flyoutIsExpandable }) => {
const { telemetry } = useKibana().services;
const { openLeftPanel } = useExpandableFlyoutApi();
- const { eventId, indexName, scopeId } = useDocumentDetailsContext();
+ const { eventId, indexName, scopeId, isPreview } = useDocumentDetailsContext();
const expandDetails = useCallback(() => {
openLeftPanel({
@@ -47,6 +47,8 @@ export const PanelNavigation: FC = memo(({ flyoutIsExpanda
flyoutIsExpandable={flyoutIsExpandable}
expandDetails={expandDetails}
actions={}
+ isPreviewMode={false}
+ isPreview={isPreview}
/>
);
});
diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/utils/event_utils.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/right/utils/event_utils.tsx
index 59c06629e2a4c..5f39d73bd31f9 100644
--- a/x-pack/plugins/security_solution/public/flyout/document_details/right/utils/event_utils.tsx
+++ b/x-pack/plugins/security_solution/public/flyout/document_details/right/utils/event_utils.tsx
@@ -48,25 +48,3 @@ export const getEcsAllowedValueDescription = (fieldName: FieldName, value: strin
})
);
};
-
-// mapping of event category to the field displayed as title
-export const EVENT_CATEGORY_TO_FIELD: Record = {
- authentication: 'user.name',
- configuration: '',
- database: '',
- driver: '',
- email: '',
- file: 'file.name',
- host: 'host.name',
- iam: '',
- intrusion_detection: '',
- malware: '',
- network: '',
- package: '',
- process: 'process.name',
- registry: '',
- session: '',
- threat: '',
- vulnerability: '',
- web: '',
-};
diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/shared/utils.test.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/shared/utils.test.tsx
index 531bc1b57df51..6c9e9917679fd 100644
--- a/x-pack/plugins/security_solution/public/flyout/document_details/shared/utils.test.tsx
+++ b/x-pack/plugins/security_solution/public/flyout/document_details/shared/utils.test.tsx
@@ -4,9 +4,9 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
-import { getField, getFieldArray } from './utils';
+import { getField, getFieldArray, getEventTitle, getAlertTitle } from './utils';
-describe('test getField', () => {
+describe('getField', () => {
it('should return the string value if field is a string', () => {
expect(getField('test string')).toBe('test string');
});
@@ -29,7 +29,7 @@ describe('test getField', () => {
});
});
-describe('test getFieldArray', () => {
+describe('getFieldArray', () => {
it('should return the string value in an array if field is a string', () => {
expect(getFieldArray('test string')).toStrictEqual(['test string']);
});
@@ -47,3 +47,43 @@ describe('test getFieldArray', () => {
expect(getFieldArray(null)).toStrictEqual([]);
});
});
+
+describe('getEventTitle', () => {
+ it('should return event title based on category when event kind is event', () => {
+ expect(
+ getEventTitle({
+ eventKind: 'event',
+ eventCategory: 'process',
+ getFieldsData: (field) => (field === 'process.name' ? 'process name' : ''),
+ })
+ ).toBe('process name');
+ });
+
+ it('should return External alert details when event kind is alert', () => {
+ expect(
+ getEventTitle({ eventKind: 'alert', eventCategory: null, getFieldsData: jest.fn() })
+ ).toBe('External alert details');
+ });
+
+ it('should return generic event details when event kind is not event or alert', () => {
+ expect(
+ getEventTitle({ eventKind: 'metric', eventCategory: null, getFieldsData: jest.fn() })
+ ).toBe('Metric details');
+ });
+
+ it('should return Event details when event kind is null', () => {
+ expect(getEventTitle({ eventKind: null, eventCategory: null, getFieldsData: jest.fn() })).toBe(
+ 'Event details'
+ );
+ });
+});
+
+describe('getAlertTitle', () => {
+ it('should return Document details when ruleName is undefined', () => {
+ expect(getAlertTitle({ ruleName: undefined })).toBe('Document details');
+ });
+
+ it('should return ruleName when ruleName is defined', () => {
+ expect(getAlertTitle({ ruleName: 'test rule' })).toBe('test rule');
+ });
+});
diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/shared/utils.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/shared/utils.tsx
index 72d568325676e..9953fa0fbbfb1 100644
--- a/x-pack/plugins/security_solution/public/flyout/document_details/shared/utils.tsx
+++ b/x-pack/plugins/security_solution/public/flyout/document_details/shared/utils.tsx
@@ -4,6 +4,9 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
+import { i18n } from '@kbn/i18n';
+import { startCase } from 'lodash';
+import type { GetFieldsData } from './hooks/use_get_fields_data';
/**
* Helper function to retrieve a field's value (used in combination with the custom hook useGetFieldsData (https://github.com/elastic/kibana/blob/main/x-pack/plugins/security_solution/public/common/hooks/use_get_fields_data.ts)
@@ -33,3 +36,73 @@ export const getFieldArray = (field: unknown | unknown[]) => {
}
return [];
};
+
+// mapping of event category to the field displayed as title
+export const EVENT_CATEGORY_TO_FIELD: Record = {
+ authentication: 'user.name',
+ configuration: '',
+ database: '',
+ driver: '',
+ email: '',
+ file: 'file.name',
+ host: 'host.name',
+ iam: '',
+ intrusion_detection: '',
+ malware: '',
+ network: '',
+ package: '',
+ process: 'process.name',
+ registry: '',
+ session: '',
+ threat: '',
+ vulnerability: '',
+ web: '',
+};
+
+/**
+ * Helper function to retrieve the alert title
+ */
+export const getAlertTitle = ({ ruleName }: { ruleName?: string | null }) => {
+ const defaultAlertTitle = i18n.translate(
+ 'xpack.securitySolution.flyout.right.header.headerTitle',
+ { defaultMessage: 'Document details' }
+ );
+ return ruleName ?? defaultAlertTitle;
+};
+
+/**
+ * Helper function to retrieve the event title
+ */
+export const getEventTitle = ({
+ eventKind,
+ eventCategory,
+ getFieldsData,
+}: {
+ eventKind: string | null;
+ eventCategory: string | null;
+ getFieldsData: GetFieldsData;
+}) => {
+ const defaultTitle = i18n.translate('xpack.securitySolution.flyout.title.eventTitle', {
+ defaultMessage: `Event details`,
+ });
+
+ if (eventKind === 'event' && eventCategory) {
+ const fieldName = EVENT_CATEGORY_TO_FIELD[eventCategory];
+ return getField(getFieldsData(fieldName)) ?? defaultTitle;
+ }
+
+ if (eventKind === 'alert') {
+ return i18n.translate('xpack.securitySolution.flyout.title.alertEventTitle', {
+ defaultMessage: 'External alert details',
+ });
+ }
+
+ return eventKind
+ ? i18n.translate('xpack.securitySolution.flyout.title.otherEventTitle', {
+ defaultMessage: '{eventKind} details',
+ values: {
+ eventKind: startCase(eventKind),
+ },
+ })
+ : defaultTitle;
+};
diff --git a/x-pack/plugins/security_solution/public/flyout/entity_details/host_right/index.test.tsx b/x-pack/plugins/security_solution/public/flyout/entity_details/host_right/index.test.tsx
index 14bc3d3bd35db..f66ff0883a4ad 100644
--- a/x-pack/plugins/security_solution/public/flyout/entity_details/host_right/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/flyout/entity_details/host_right/index.test.tsx
@@ -9,7 +9,16 @@ import { render } from '@testing-library/react';
import React from 'react';
import { TestProviders } from '../../../common/mock';
import { mockHostRiskScoreState, mockObservedHostData } from '../mocks';
-
+import type {
+ FlyoutPanelProps,
+ ExpandableFlyoutState,
+ ExpandableFlyoutApi,
+} from '@kbn/expandable-flyout';
+import {
+ useExpandableFlyoutApi,
+ useExpandableFlyoutState,
+ useExpandableFlyoutHistory,
+} from '@kbn/expandable-flyout';
import type { HostPanelProps } from '.';
import { HostPanel } from '.';
@@ -34,10 +43,24 @@ jest.mock('./hooks/use_observed_host', () => ({
useObservedHost: () => mockedUseObservedHost(),
}));
+const flyoutContextValue = {
+ closeLeftPanel: jest.fn(),
+} as unknown as ExpandableFlyoutApi;
+
+const flyoutHistory = [{ id: 'id1', params: {} }] as unknown as FlyoutPanelProps[];
+jest.mock('@kbn/expandable-flyout', () => ({
+ useExpandableFlyoutApi: jest.fn(),
+ useExpandableFlyoutHistory: jest.fn(),
+ useExpandableFlyoutState: jest.fn(),
+}));
+
describe('HostPanel', () => {
beforeEach(() => {
mockedHostRiskScore.mockReturnValue(mockHostRiskScoreState);
mockedUseObservedHost.mockReturnValue(mockObservedHostData);
+ jest.mocked(useExpandableFlyoutHistory).mockReturnValue(flyoutHistory);
+ jest.mocked(useExpandableFlyoutState).mockReturnValue({} as unknown as ExpandableFlyoutState);
+ jest.mocked(useExpandableFlyoutApi).mockReturnValue(flyoutContextValue);
});
it('renders', () => {
diff --git a/x-pack/plugins/security_solution/public/flyout/entity_details/host_right/index.tsx b/x-pack/plugins/security_solution/public/flyout/entity_details/host_right/index.tsx
index 64c8e74d62714..abf7d5cf591dd 100644
--- a/x-pack/plugins/security_solution/public/flyout/entity_details/host_right/index.tsx
+++ b/x-pack/plugins/security_solution/public/flyout/entity_details/host_right/index.tsx
@@ -11,6 +11,7 @@ import { useExpandableFlyoutApi } from '@kbn/expandable-flyout';
import { useHasMisconfigurations } from '@kbn/cloud-security-posture/src/hooks/use_has_misconfigurations';
import { useHasVulnerabilities } from '@kbn/cloud-security-posture/src/hooks/use_has_vulnerabilities';
+import { TableId } from '@kbn/securitysolution-data-table';
import { useNonClosedAlerts } from '../../../cloud_security_posture/hooks/use_non_closed_alerts';
import { DETECTION_RESPONSE_ALERTS_BY_STATUS_ID } from '../../../overview/components/detection_response/alerts_by_status/types';
import { useRefetchQueryById } from '../../../entity_analytics/api/hooks/use_refetch_query_by_id';
@@ -36,7 +37,6 @@ import { HostDetailsPanelKey } from '../host_details_left';
import { EntityDetailsLeftPanelTab } from '../shared/components/left_panel/left_panel_header';
import { HostPreviewPanelFooter } from '../host_preview/footer';
import { EntityEventTypes } from '../../../common/lib/telemetry';
-
export interface HostPanelProps extends Record {
contextID: string;
scopeId: string;
@@ -187,13 +187,14 @@ export const HostPanel = ({
<>
({
useIsExperimentalFeatureEnabled: () => mockedUseIsExperimentalFeatureEnabled(),
}));
+const flyoutContextValue = {
+ closeLeftPanel: jest.fn(),
+} as unknown as ExpandableFlyoutApi;
+
+const flyoutHistory = [{ id: 'id1', params: {} }] as unknown as FlyoutPanelProps[];
+jest.mock('@kbn/expandable-flyout', () => ({
+ useExpandableFlyoutApi: jest.fn(),
+ useExpandableFlyoutHistory: jest.fn(),
+ useExpandableFlyoutState: jest.fn(),
+}));
+
describe('UserPanel', () => {
beforeEach(() => {
mockedUseRiskScore.mockReturnValue(mockRiskScoreState);
mockedUseManagedUser.mockReturnValue(mockManagedUserData);
mockedUseObservedUser.mockReturnValue(mockObservedUser);
+ jest.mocked(useExpandableFlyoutHistory).mockReturnValue(flyoutHistory);
+ jest.mocked(useExpandableFlyoutState).mockReturnValue({} as unknown as ExpandableFlyoutState);
+ jest.mocked(useExpandableFlyoutApi).mockReturnValue(flyoutContextValue);
});
it('renders', () => {
diff --git a/x-pack/plugins/security_solution/public/flyout/entity_details/user_right/index.tsx b/x-pack/plugins/security_solution/public/flyout/entity_details/user_right/index.tsx
index 1a97c691f373f..182740a5afa57 100644
--- a/x-pack/plugins/security_solution/public/flyout/entity_details/user_right/index.tsx
+++ b/x-pack/plugins/security_solution/public/flyout/entity_details/user_right/index.tsx
@@ -9,6 +9,7 @@ import React, { useCallback, useMemo } from 'react';
import type { FlyoutPanelProps } from '@kbn/expandable-flyout';
import { useExpandableFlyoutApi } from '@kbn/expandable-flyout';
import { useHasMisconfigurations } from '@kbn/cloud-security-posture/src/hooks/use_has_misconfigurations';
+import { TableId } from '@kbn/securitysolution-data-table';
import { useNonClosedAlerts } from '../../../cloud_security_posture/hooks/use_non_closed_alerts';
import { useRefetchQueryById } from '../../../entity_analytics/api/hooks/use_refetch_query_by_id';
import type { Refetch } from '../../../common/types';
@@ -191,10 +192,11 @@ export const UserPanel = ({
<>
,
},
+ {
+ key: NetworkPreviewPanelKey,
+ component: (props) => (
+
+ ),
+ },
];
export const SECURITY_SOLUTION_ON_CLOSE_EVENT = `expandable-flyout-on-close-${Flyouts.securitySolution}`;
diff --git a/x-pack/plugins/security_solution/public/flyout/network_details/header.tsx b/x-pack/plugins/security_solution/public/flyout/network_details/header.tsx
index 8ffceb345b1e0..5ebdf9c9e8660 100644
--- a/x-pack/plugins/security_solution/public/flyout/network_details/header.tsx
+++ b/x-pack/plugins/security_solution/public/flyout/network_details/header.tsx
@@ -28,7 +28,7 @@ export interface PanelHeaderProps extends React.ComponentProps = memo(
({ ip, flowTarget, ...flyoutHeaderProps }: PanelHeaderProps) => {
diff --git a/x-pack/plugins/security_solution/public/flyout/network_details/index.tsx b/x-pack/plugins/security_solution/public/flyout/network_details/index.tsx
index d7e9d3519e4b2..37727e8074f0b 100644
--- a/x-pack/plugins/security_solution/public/flyout/network_details/index.tsx
+++ b/x-pack/plugins/security_solution/public/flyout/network_details/index.tsx
@@ -5,19 +5,23 @@
* 2.0.
*/
+import type { FC } from 'react';
import React, { memo } from 'react';
import type { FlyoutPanelProps } from '@kbn/expandable-flyout';
import { i18n } from '@kbn/i18n';
+import { TableId } from '@kbn/securitysolution-data-table';
import type { FlowTargetSourceDest } from '../../../common/search_strategy';
import { PanelHeader } from './header';
import { PanelContent } from './content';
+import { FlyoutNavigation } from '../shared/components/flyout_navigation';
export interface NetworkExpandableFlyoutProps extends FlyoutPanelProps {
- key: 'network-details';
+ key: 'network-details' | 'network-preview';
params: NetworkPanelProps;
}
export const NetworkPanelKey: NetworkExpandableFlyoutProps['key'] = 'network-details';
+export const NetworkPreviewPanelKey: NetworkExpandableFlyoutProps['key'] = 'network-preview';
export const NETWORK_PREVIEW_BANNER = {
title: i18n.translate('xpack.securitySolution.flyout.right.network.networkPreviewTitle', {
@@ -36,18 +40,33 @@ export interface NetworkPanelProps extends Record {
* Destination or source information
*/
flowTarget: FlowTargetSourceDest;
+ /**
+ * Scope ID
+ */
+ scopeId: string;
+ /**
+ * If in preview mode, show preview banner and hide navigation
+ */
+ isPreviewMode?: boolean;
}
/**
* Panel to be displayed in the network details expandable flyout right section
*/
-export const NetworkPanel = memo(({ ip, flowTarget }: NetworkPanelProps) => {
- return (
- <>
-
-
- >
- );
-});
+export const NetworkPanel: FC = memo(
+ ({ ip, flowTarget, scopeId, isPreviewMode }) => {
+ return (
+ <>
+
+
+
+ >
+ );
+ }
+);
NetworkPanel.displayName = 'NetworkPanel';
diff --git a/x-pack/plugins/security_solution/public/flyout/rule_details/right/content.tsx b/x-pack/plugins/security_solution/public/flyout/rule_details/right/content.tsx
index 8acc6cfe9b715..54e6824ce5052 100644
--- a/x-pack/plugins/security_solution/public/flyout/rule_details/right/content.tsx
+++ b/x-pack/plugins/security_solution/public/flyout/rule_details/right/content.tsx
@@ -4,7 +4,7 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
-import React, { memo } from 'react';
+import React, { memo, useMemo } from 'react';
import { EuiText, EuiHorizontalRule, EuiSpacer, EuiPanel } from '@elastic/eui';
import { css } from '@emotion/css';
import { FormattedMessage } from '@kbn/i18n-react';
@@ -50,12 +50,23 @@ export interface RuleDetailsProps {
* Rule details content on the right section of expandable flyout
*/
export const PanelContent = memo(({ rule }: RuleDetailsProps) => {
- const { ruleActionsData } =
- rule != null ? getStepsData({ rule, detailsView: true }) : { ruleActionsData: null };
+ const { ruleActionsData } = useMemo(
+ () => (rule != null ? getStepsData({ rule, detailsView: true }) : { ruleActionsData: null }),
+ [rule]
+ );
- const hasNotificationActions = Boolean(ruleActionsData?.actions?.length);
- const hasResponseActions = Boolean(ruleActionsData?.responseActions?.length);
- const hasActions = ruleActionsData != null && (hasNotificationActions || hasResponseActions);
+ const hasNotificationActions = useMemo(
+ () => Boolean(ruleActionsData?.actions?.length),
+ [ruleActionsData]
+ );
+ const hasResponseActions = useMemo(
+ () => Boolean(ruleActionsData?.responseActions?.length),
+ [ruleActionsData]
+ );
+ const hasActions = useMemo(
+ () => ruleActionsData != null && (hasNotificationActions || hasResponseActions),
+ [ruleActionsData, hasNotificationActions, hasResponseActions]
+ );
return (
diff --git a/x-pack/plugins/security_solution/public/flyout/rule_details/right/header.tsx b/x-pack/plugins/security_solution/public/flyout/rule_details/right/header.tsx
index 294870d6eebb7..027fc29479548 100644
--- a/x-pack/plugins/security_solution/public/flyout/rule_details/right/header.tsx
+++ b/x-pack/plugins/security_solution/public/flyout/rule_details/right/header.tsx
@@ -5,7 +5,7 @@
* 2.0.
*/
-import React from 'react';
+import React, { memo } from 'react';
import {
EuiTitle,
EuiText,
@@ -43,7 +43,7 @@ export interface PanelHeaderProps {
/**
* Title component that shows basic information of a rule. This is displayed above rule overview body
*/
-export const PanelHeader: React.FC = ({ rule, isSuppressed }) => {
+export const PanelHeader: React.FC = memo(({ rule, isSuppressed }) => {
const href = useRuleDetailsLink({ ruleId: rule.id });
return (
@@ -86,6 +86,6 @@ export const PanelHeader: React.FC = ({ rule, isSuppressed })
);
-};
+});
PanelHeader.displayName = 'PanelHeader';
diff --git a/x-pack/plugins/security_solution/public/flyout/rule_details/right/index.test.tsx b/x-pack/plugins/security_solution/public/flyout/rule_details/right/index.test.tsx
index 146da2be34346..c1a629f881710 100644
--- a/x-pack/plugins/security_solution/public/flyout/rule_details/right/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/flyout/rule_details/right/index.test.tsx
@@ -22,6 +22,16 @@ import {
import type { RuleResponse } from '../../../../common/api/detection_engine';
import { BODY_TEST_ID, LOADING_TEST_ID } from './test_ids';
import { RULE_PREVIEW_FOOTER_TEST_ID } from '../preview/test_ids';
+import type {
+ FlyoutPanelProps,
+ ExpandableFlyoutState,
+ ExpandableFlyoutApi,
+} from '@kbn/expandable-flyout';
+import {
+ useExpandableFlyoutApi,
+ useExpandableFlyoutState,
+ useExpandableFlyoutHistory,
+} from '@kbn/expandable-flyout';
jest.mock('../../document_details/shared/hooks/use_rule_details_link');
@@ -31,6 +41,18 @@ jest.mock('../hooks/use_rule_details');
const mockGetStepsData = getStepsData as jest.Mock;
jest.mock('../../../detections/pages/detection_engine/rules/helpers');
+jest.mock('@kbn/expandable-flyout', () => ({
+ useExpandableFlyoutApi: jest.fn(),
+ useExpandableFlyoutState: jest.fn(),
+ useExpandableFlyoutHistory: jest.fn(),
+}));
+
+const flyoutContextValue = {
+ closeLeftPanel: jest.fn(),
+} as unknown as ExpandableFlyoutApi;
+
+const flyoutHistory = [{ id: 'id1', params: {} }] as unknown as FlyoutPanelProps[];
+
const mockTheme = getMockTheme({ eui: { euiColorMediumShade: '#ece' } });
const rule = { name: 'rule name', description: 'rule description' } as RuleResponse;
const ERROR_MESSAGE = 'There was an error displaying data.';
@@ -45,6 +67,12 @@ const renderRulePanel = (isPreviewMode = false) =>
);
describe('', () => {
+ beforeEach(() => {
+ jest.mocked(useExpandableFlyoutHistory).mockReturnValue(flyoutHistory);
+ jest.mocked(useExpandableFlyoutState).mockReturnValue({} as unknown as ExpandableFlyoutState);
+ jest.mocked(useExpandableFlyoutApi).mockReturnValue(flyoutContextValue);
+ });
+
it('should render rule details and its sub sections', () => {
mockUseRuleDetails.mockReturnValue({
rule,
diff --git a/x-pack/plugins/security_solution/public/flyout/rule_details/right/index.tsx b/x-pack/plugins/security_solution/public/flyout/rule_details/right/index.tsx
index 10b22e22a575c..dfe5863f6a85b 100644
--- a/x-pack/plugins/security_solution/public/flyout/rule_details/right/index.tsx
+++ b/x-pack/plugins/security_solution/public/flyout/rule_details/right/index.tsx
@@ -5,6 +5,7 @@
* 2.0.
*/
+import type { FC } from 'react';
import React, { memo } from 'react';
import type { FlyoutPanelProps } from '@kbn/expandable-flyout';
import { i18n } from '@kbn/i18n';
@@ -50,14 +51,14 @@ export interface RulePanelProps extends Record {
/**
* Displays a rule overview panel
*/
-export const RulePanel = memo(({ ruleId, isPreviewMode }: RulePanelProps) => {
+export const RulePanel: FC = memo(({ ruleId, isPreviewMode }) => {
const { rule, loading, isExistingRule } = useRuleDetails({ ruleId });
return loading ? (
) : rule ? (
<>
-
+
{isPreviewMode && }
diff --git a/x-pack/plugins/security_solution/public/flyout/shared/components/flyout_history.test.tsx b/x-pack/plugins/security_solution/public/flyout/shared/components/flyout_history.test.tsx
new file mode 100644
index 0000000000000..f922190da3e73
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/flyout/shared/components/flyout_history.test.tsx
@@ -0,0 +1,52 @@
+/*
+ * 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 { render, fireEvent } from '@testing-library/react';
+import { TestProviders } from '../../../common/mock';
+import {
+ FLYOUT_HISTORY_TEST_ID,
+ FLYOUT_HISTORY_BUTTON_TEST_ID,
+ FLYOUT_HISTORY_CONTEXT_PANEL_TEST_ID,
+ NO_DATA_HISTORY_ROW_TEST_ID,
+} from './test_ids';
+import { FlyoutHistory } from './flyout_history';
+
+const mockedHistory = [{ id: '1' }, { id: '2' }];
+
+describe('FlyoutHistory', () => {
+ it('renders', () => {
+ const { getByTestId, queryByTestId } = render(
+
+
+
+ );
+ expect(getByTestId(FLYOUT_HISTORY_TEST_ID)).toBeInTheDocument();
+ expect(queryByTestId(FLYOUT_HISTORY_CONTEXT_PANEL_TEST_ID)).not.toBeInTheDocument();
+ });
+
+ it('renders context menu when clicking the popover', () => {
+ const { getByTestId } = render(
+
+
+
+ );
+
+ fireEvent.click(getByTestId(FLYOUT_HISTORY_BUTTON_TEST_ID));
+ expect(getByTestId(FLYOUT_HISTORY_CONTEXT_PANEL_TEST_ID)).toBeInTheDocument();
+ });
+
+ it('render empty history message if history is empty', () => {
+ const { getByTestId } = render(
+
+
+
+ );
+ fireEvent.click(getByTestId(FLYOUT_HISTORY_BUTTON_TEST_ID));
+ expect(getByTestId(NO_DATA_HISTORY_ROW_TEST_ID)).toBeInTheDocument();
+ });
+});
diff --git a/x-pack/plugins/security_solution/public/flyout/shared/components/flyout_history.tsx b/x-pack/plugins/security_solution/public/flyout/shared/components/flyout_history.tsx
new file mode 100644
index 0000000000000..933106e28ed10
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/flyout/shared/components/flyout_history.tsx
@@ -0,0 +1,97 @@
+/*
+ * 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 type { FC } from 'react';
+import React, { memo, useMemo, useState } from 'react';
+import {
+ EuiFlexItem,
+ EuiButtonEmpty,
+ EuiPopover,
+ EuiContextMenuPanel,
+ EuiText,
+ EuiContextMenuItem,
+ EuiTextColor,
+} from '@elastic/eui';
+import { FormattedMessage } from '@kbn/i18n-react';
+import type { FlyoutPanelProps } from '@kbn/expandable-flyout';
+import { FlyoutHistoryRow } from './flyout_history_row';
+import {
+ FLYOUT_HISTORY_TEST_ID,
+ FLYOUT_HISTORY_BUTTON_TEST_ID,
+ FLYOUT_HISTORY_CONTEXT_PANEL_TEST_ID,
+ NO_DATA_HISTORY_ROW_TEST_ID,
+} from './test_ids';
+
+export interface HistoryProps {
+ /**
+ * A list of flyouts that have been opened
+ */
+ history: FlyoutPanelProps[];
+}
+
+/**
+ * History of flyouts shown in top navigation
+ * Shows the title of previously opened flyout, and count of history of more than 1 flyout was opened
+ */
+export const FlyoutHistory: FC = memo(({ history }) => {
+ const [isPopoverOpen, setPopover] = useState(false);
+ const togglePopover = () => setPopover(!isPopoverOpen);
+
+ const emptyHistoryMessage = useMemo(() => {
+ return (
+
+
+
+
+
+
+
+
+
+ );
+ }, []);
+
+ const historyDropdownPanels = useMemo(
+ () =>
+ history.length > 0
+ ? history.map((item, index) => {
+ return ;
+ })
+ : [emptyHistoryMessage],
+ [history, emptyHistoryMessage]
+ );
+
+ return (
+
+
+ }
+ isOpen={isPopoverOpen}
+ closePopover={togglePopover}
+ panelPaddingSize="none"
+ anchorPosition="downLeft"
+ >
+
+
+
+ );
+});
+
+FlyoutHistory.displayName = 'FlyoutHistory';
diff --git a/x-pack/plugins/security_solution/public/flyout/shared/components/flyout_history_row.test.tsx b/x-pack/plugins/security_solution/public/flyout/shared/components/flyout_history_row.test.tsx
new file mode 100644
index 0000000000000..f3dcaefd536e0
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/flyout/shared/components/flyout_history_row.test.tsx
@@ -0,0 +1,270 @@
+/*
+ * 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 { render, fireEvent } from '@testing-library/react';
+import {
+ FlyoutHistoryRow,
+ RuleHistoryRow,
+ DocumentDetailsHistoryRow,
+ GenericHistoryRow,
+} from './flyout_history_row';
+import { TestProviders } from '../../../common/mock';
+import type { RuleResponse } from '../../../../common/api/detection_engine';
+import { useExpandableFlyoutApi, type ExpandableFlyoutApi } from '@kbn/expandable-flyout';
+import { useRuleDetails } from '../../rule_details/hooks/use_rule_details';
+import { useBasicDataFromDetailsData } from '../../document_details/shared/hooks/use_basic_data_from_details_data';
+import { DocumentDetailsRightPanelKey } from '../../document_details/shared/constants/panel_keys';
+import { RulePanelKey } from '../../rule_details/right';
+import { UserPanelKey } from '../../entity_details/user_right';
+import { HostPanelKey } from '../../entity_details/host_right';
+import { NetworkPanelKey } from '../../network_details';
+import {
+ DOCUMENT_DETAILS_HISTORY_ROW_TEST_ID,
+ RULE_HISTORY_ROW_TEST_ID,
+ HOST_HISTORY_ROW_TEST_ID,
+ USER_HISTORY_ROW_TEST_ID,
+ NETWORK_HISTORY_ROW_TEST_ID,
+ GENERIC_HISTORY_ROW_TEST_ID,
+} from './test_ids';
+
+jest.mock('@kbn/expandable-flyout', () => ({
+ useExpandableFlyoutApi: jest.fn(),
+ useExpandableFlyoutState: jest.fn(),
+ useExpandableFlyoutHistory: jest.fn(),
+ ExpandableFlyoutProvider: ({ children }: React.PropsWithChildren<{}>) => <>{children}>,
+}));
+
+jest.mock('../../../detection_engine/rule_management/logic/use_rule_with_fallback');
+jest.mock('../../document_details/shared/hooks/use_basic_data_from_details_data');
+jest.mock('../../rule_details/hooks/use_rule_details');
+
+const flyoutContextValue = {
+ openFlyout: jest.fn(),
+} as unknown as ExpandableFlyoutApi;
+
+const rowItems = {
+ alert: {
+ id: DocumentDetailsRightPanelKey,
+ params: {
+ id: 'eventId',
+ indexName: 'indexName',
+ scopeId: 'scopeId',
+ },
+ },
+ rule: {
+ id: RulePanelKey,
+ params: { ruleId: 'ruleId' },
+ },
+ host: {
+ id: HostPanelKey,
+ params: { hostName: 'host name' },
+ },
+ user: {
+ id: UserPanelKey,
+ params: { userName: 'user name' },
+ },
+ network: {
+ id: NetworkPanelKey,
+ params: { ip: 'ip' },
+ },
+};
+
+const mockedRuleResponse = {
+ rule: null,
+ loading: false,
+ isExistingRule: false,
+ error: null,
+ refresh: jest.fn(),
+};
+
+describe('FlyoutHistoryRow', () => {
+ beforeEach(() => {
+ jest.mocked(useExpandableFlyoutApi).mockReturnValue(flyoutContextValue);
+ jest.mocked(useRuleDetails).mockReturnValue({
+ ...mockedRuleResponse,
+ rule: { name: 'rule name' } as RuleResponse,
+ });
+ (useBasicDataFromDetailsData as jest.Mock).mockReturnValue({ isAlert: false });
+ });
+
+ it('renders document details history row when key is alert', () => {
+ (useBasicDataFromDetailsData as jest.Mock).mockReturnValue({
+ isAlert: true,
+ ruleName: 'rule name',
+ });
+
+ const { getByTestId } = render(
+
+
+
+ );
+ expect(getByTestId(`${0}-${DOCUMENT_DETAILS_HISTORY_ROW_TEST_ID}`)).toBeInTheDocument();
+ });
+
+ it('renders rule history row when key is rule', () => {
+ const { getByTestId } = render(
+
+
+
+ );
+ expect(getByTestId(`${1}-${RULE_HISTORY_ROW_TEST_ID}`)).toBeInTheDocument();
+ });
+
+ it('renders generic host history row when key is host', () => {
+ const { getByTestId } = render(
+
+
+
+ );
+ expect(getByTestId(`${2}-${HOST_HISTORY_ROW_TEST_ID}`)).toBeInTheDocument();
+ expect(getByTestId(`${2}-${HOST_HISTORY_ROW_TEST_ID}`)).toHaveTextContent('Host: host name');
+ });
+
+ it('renders generic user history row when key is user', () => {
+ const { getByTestId } = render(
+
+
+
+ );
+ expect(getByTestId(`${3}-${USER_HISTORY_ROW_TEST_ID}`)).toBeInTheDocument();
+ expect(getByTestId(`${3}-${USER_HISTORY_ROW_TEST_ID}`)).toHaveTextContent('User: user name');
+ });
+
+ it('renders generic network history row when key is network', () => {
+ const { getByTestId } = render(
+
+
+
+ );
+ expect(getByTestId(`${4}-${NETWORK_HISTORY_ROW_TEST_ID}`)).toBeInTheDocument();
+ expect(getByTestId(`${4}-${NETWORK_HISTORY_ROW_TEST_ID}`)).toHaveTextContent('Network: ip');
+ });
+
+ it('renders null when key is not supported', () => {
+ const { container } = render(
+
+
+
+ );
+ expect(container).toBeEmptyDOMElement();
+ });
+});
+
+describe('DocumentDetailsHistoryRow', () => {
+ beforeEach(() => {
+ jest.mocked(useExpandableFlyoutApi).mockReturnValue(flyoutContextValue);
+ });
+
+ it('renders alert title when isAlert is true and rule name is defined', () => {
+ (useBasicDataFromDetailsData as jest.Mock).mockReturnValue({
+ isAlert: true,
+ ruleName: 'rule name',
+ });
+
+ const { getByTestId } = render(
+
+
+
+ );
+ expect(getByTestId(`${0}-${DOCUMENT_DETAILS_HISTORY_ROW_TEST_ID}`)).toHaveTextContent(
+ 'Alert: rule name'
+ );
+ });
+
+ it('renders default alert title when isAlert is true and rule name is undefined', () => {
+ (useBasicDataFromDetailsData as jest.Mock).mockReturnValue({ isAlert: true });
+
+ const { getByTestId } = render(
+
+
+
+ );
+ expect(getByTestId(`${0}-${DOCUMENT_DETAILS_HISTORY_ROW_TEST_ID}`)).toHaveTextContent(
+ 'Alert: Document details'
+ );
+ });
+
+ it('renders event title when isAlert is false', () => {
+ (useBasicDataFromDetailsData as jest.Mock).mockReturnValue({ isAlert: false });
+
+ const { getByTestId } = render(
+
+
+
+ );
+ expect(getByTestId(`${0}-${DOCUMENT_DETAILS_HISTORY_ROW_TEST_ID}`)).toHaveTextContent(
+ 'Event details'
+ );
+ });
+
+ it('opens document details flyout when clicked', () => {
+ (useBasicDataFromDetailsData as jest.Mock).mockReturnValue({ isAlert: true });
+
+ const { getByTestId } = render(
+
+
+
+ );
+ fireEvent.click(getByTestId(`${0}-${DOCUMENT_DETAILS_HISTORY_ROW_TEST_ID}`));
+ expect(flyoutContextValue.openFlyout).toHaveBeenCalledWith({ right: rowItems.alert });
+ });
+});
+
+describe('RuleHistoryRow', () => {
+ beforeEach(() => {
+ jest.mocked(useExpandableFlyoutApi).mockReturnValue(flyoutContextValue);
+ jest.mocked(useRuleDetails).mockReturnValue({
+ rule: { name: 'rule name' } as RuleResponse,
+ loading: false,
+ isExistingRule: false,
+ });
+ });
+
+ it('renders', () => {
+ const { getByTestId } = render(
+
+
+
+ );
+ expect(getByTestId(`${0}-${RULE_HISTORY_ROW_TEST_ID}`)).toHaveTextContent('Rule: rule name');
+ expect(useRuleDetails).toHaveBeenCalledWith({ ruleId: rowItems.rule.params.ruleId });
+ });
+
+ it('opens rule details flyout when clicked', () => {
+ const { getByTestId } = render(
+
+
+
+ );
+ fireEvent.click(getByTestId(`${0}-${RULE_HISTORY_ROW_TEST_ID}`));
+ expect(flyoutContextValue.openFlyout).toHaveBeenCalledWith({ right: rowItems.rule });
+ });
+});
+
+describe('GenericHistoryRow', () => {
+ beforeEach(() => {
+ jest.mocked(useExpandableFlyoutApi).mockReturnValue(flyoutContextValue);
+ });
+
+ it('renders', () => {
+ const { getByTestId } = render(
+
+
+
+ );
+ expect(getByTestId(`${0}-${GENERIC_HISTORY_ROW_TEST_ID}`)).toHaveTextContent('Row name: title');
+ fireEvent.click(getByTestId(`${0}-${GENERIC_HISTORY_ROW_TEST_ID}`));
+ expect(flyoutContextValue.openFlyout).toHaveBeenCalledWith({ right: rowItems.host });
+ });
+});
diff --git a/x-pack/plugins/security_solution/public/flyout/shared/components/flyout_history_row.tsx b/x-pack/plugins/security_solution/public/flyout/shared/components/flyout_history_row.tsx
new file mode 100644
index 0000000000000..1081cae88e31d
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/flyout/shared/components/flyout_history_row.tsx
@@ -0,0 +1,186 @@
+/*
+ * 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 type { FC } from 'react';
+import React, { memo, useMemo, useCallback } from 'react';
+import { EuiContextMenuItem, type EuiIconProps } from '@elastic/eui';
+import { useExpandableFlyoutApi } from '@kbn/expandable-flyout';
+import type { FlyoutPanelProps } from '@kbn/expandable-flyout';
+import { DocumentDetailsRightPanelKey } from '../../document_details/shared/constants/panel_keys';
+import { useBasicDataFromDetailsData } from '../../document_details/shared/hooks/use_basic_data_from_details_data';
+import { useEventDetails } from '../../document_details/shared/hooks/use_event_details';
+import { getField, getAlertTitle, getEventTitle } from '../../document_details/shared/utils';
+import { RulePanelKey } from '../../rule_details/right';
+import { UserPanelKey } from '../../entity_details/user_right';
+import { HostPanelKey } from '../../entity_details/host_right';
+import { NetworkPanelKey } from '../../network_details';
+import { useRuleDetails } from '../../rule_details/hooks/use_rule_details';
+import {
+ DOCUMENT_DETAILS_HISTORY_ROW_TEST_ID,
+ RULE_HISTORY_ROW_TEST_ID,
+ GENERIC_HISTORY_ROW_TEST_ID,
+ HOST_HISTORY_ROW_TEST_ID,
+ USER_HISTORY_ROW_TEST_ID,
+ NETWORK_HISTORY_ROW_TEST_ID,
+} from './test_ids';
+
+export interface FlyoutHistoryRowProps {
+ /**
+ * Flyout item to display
+ */
+ item: FlyoutPanelProps;
+ /**
+ * Index of the flyout in the list
+ */
+ index: number;
+}
+
+/**
+ * Row item for a flyout history row
+ */
+export const FlyoutHistoryRow: FC = memo(({ item, index }) => {
+ switch (item.id) {
+ case DocumentDetailsRightPanelKey:
+ return ;
+ case RulePanelKey:
+ return ;
+ case HostPanelKey:
+ return (
+
+ );
+ case UserPanelKey:
+ return (
+
+ );
+ case NetworkPanelKey:
+ return (
+
+ );
+ default:
+ return null;
+ }
+});
+
+/**
+ * Row item for a document details
+ */
+export const DocumentDetailsHistoryRow: FC = memo(({ item, index }) => {
+ const { dataFormattedForFieldBrowser, getFieldsData } = useEventDetails({
+ eventId: String(item?.params?.id),
+ indexName: String(item?.params?.indexName),
+ });
+ const { ruleName, isAlert } = useBasicDataFromDetailsData(dataFormattedForFieldBrowser);
+ const eventKind = useMemo(() => getField(getFieldsData('event.kind')), [getFieldsData]);
+ const eventCategory = useMemo(() => getField(getFieldsData('event.category')), [getFieldsData]);
+
+ const title = useMemo(
+ () =>
+ isAlert
+ ? getAlertTitle({ ruleName })
+ : getEventTitle({ eventKind, eventCategory, getFieldsData }),
+ [isAlert, ruleName, eventKind, eventCategory, getFieldsData]
+ );
+
+ return (
+
+ );
+});
+
+/**
+ * Row item for a rule details flyout
+ */
+export const RuleHistoryRow: FC = memo(({ item, index }) => {
+ const ruleId = String(item?.params?.ruleId);
+ const { rule } = useRuleDetails({ ruleId });
+
+ return (
+
+ );
+});
+
+interface GenericHistoryRowProps extends FlyoutHistoryRowProps {
+ /**
+ * Icon to display
+ */
+ icon: EuiIconProps['type'];
+ /**
+ * Title to display
+ */
+ title: string;
+ /**
+ * Name to display
+ */
+ name: string;
+ /**
+ * Data test subject
+ */
+ dataTestSubj?: string;
+}
+
+/**
+ * Row item for a generic history row where the title is accessible in flyout params
+ */
+export const GenericHistoryRow: FC = memo(
+ ({ item, index, title, icon, name, dataTestSubj }) => {
+ const { openFlyout } = useExpandableFlyoutApi();
+ const onClick = useCallback(() => {
+ openFlyout({ right: item });
+ }, [openFlyout, item]);
+
+ return (
+
+ {`${name}: `}
+ {title}
+
+ );
+ }
+);
+
+FlyoutHistoryRow.displayName = 'FlyoutHistoryRow';
+DocumentDetailsHistoryRow.displayName = 'DocumentDetailsHistoryRow';
+RuleHistoryRow.displayName = 'RuleHistoryRow';
+GenericHistoryRow.displayName = 'GenericHistoryRow';
diff --git a/x-pack/plugins/security_solution/public/flyout/shared/components/flyout_navigation.test.tsx b/x-pack/plugins/security_solution/public/flyout/shared/components/flyout_navigation.test.tsx
index 321245ccde86e..372b11bcc9ef4 100644
--- a/x-pack/plugins/security_solution/public/flyout/shared/components/flyout_navigation.test.tsx
+++ b/x-pack/plugins/security_solution/public/flyout/shared/components/flyout_navigation.test.tsx
@@ -13,13 +13,16 @@ import { FlyoutNavigation } from './flyout_navigation';
import {
COLLAPSE_DETAILS_BUTTON_TEST_ID,
EXPAND_DETAILS_BUTTON_TEST_ID,
+ FLYOUT_HISTORY_BUTTON_TEST_ID,
HEADER_ACTIONS_TEST_ID,
} from './test_ids';
-import type { ExpandableFlyoutState } from '@kbn/expandable-flyout';
+import type { ExpandableFlyoutState, FlyoutPanelProps } from '@kbn/expandable-flyout';
+import { useIsExperimentalFeatureEnabled } from '../../../common/hooks/use_experimental_features';
import {
useExpandableFlyoutApi,
type ExpandableFlyoutApi,
useExpandableFlyoutState,
+ useExpandableFlyoutHistory,
} from '@kbn/expandable-flyout';
const expandDetails = jest.fn();
@@ -31,9 +34,12 @@ const ExpandableFlyoutTestProviders: FC> = ({ children })
jest.mock('@kbn/expandable-flyout', () => ({
useExpandableFlyoutApi: jest.fn(),
useExpandableFlyoutState: jest.fn(),
+ useExpandableFlyoutHistory: jest.fn(),
ExpandableFlyoutProvider: ({ children }: React.PropsWithChildren<{}>) => <>{children}>,
}));
+jest.mock('../../../common/hooks/use_experimental_features');
+
const flyoutContextValue = {
closeLeftPanel: jest.fn(),
} as unknown as ExpandableFlyoutApi;
@@ -42,6 +48,8 @@ describe('', () => {
beforeEach(() => {
jest.mocked(useExpandableFlyoutApi).mockReturnValue(flyoutContextValue);
jest.mocked(useExpandableFlyoutState).mockReturnValue({} as unknown as ExpandableFlyoutState);
+ jest.mocked(useExpandableFlyoutHistory).mockReturnValue([]);
+ jest.mocked(useIsExperimentalFeatureEnabled).mockReturnValue(false);
});
describe('when flyout is expandable', () => {
@@ -114,4 +122,62 @@ describe('', () => {
expect(container).toBeEmptyDOMElement();
});
});
+
+ it('should render empty component if isPreviewMode is true', () => {
+ const { container } = render(
+
+
+
+ );
+ expect(container).toBeEmptyDOMElement();
+ });
+
+ const flyoutHistory = [
+ { id: 'id1', params: {} },
+ { id: 'id2', params: {} },
+ ] as unknown as FlyoutPanelProps[];
+
+ describe('when flyout history is enabled', () => {
+ beforeEach(() => {
+ jest.mocked(useIsExperimentalFeatureEnabled).mockReturnValue(true);
+ jest.mocked(useExpandableFlyoutHistory).mockReturnValue(flyoutHistory);
+ });
+
+ it('should render history button when there is no item in history', () => {
+ jest.mocked(useExpandableFlyoutHistory).mockReturnValue([]);
+ const { getByTestId } = render(
+
+
+
+ );
+ expect(getByTestId(FLYOUT_HISTORY_BUTTON_TEST_ID)).toBeInTheDocument();
+ });
+
+ it('should render history button when there are more than 1 unqie item in history', () => {
+ const { getByTestId } = render(
+
+
+
+ );
+ expect(getByTestId(FLYOUT_HISTORY_BUTTON_TEST_ID)).toBeInTheDocument();
+ });
+
+ it('should not render history button if in rule preview', () => {
+ const { container } = render(
+
+
+
+ );
+ expect(container).toBeEmptyDOMElement();
+ });
+
+ it('should render empty component if isPreviewMode is true', () => {
+ const { container } = render(
+
+
+
+ );
+ expect(container).toBeEmptyDOMElement();
+ });
+ });
});
diff --git a/x-pack/plugins/security_solution/public/flyout/shared/components/flyout_navigation.tsx b/x-pack/plugins/security_solution/public/flyout/shared/components/flyout_navigation.tsx
index 1915c5a4484a4..89798687c3e62 100644
--- a/x-pack/plugins/security_solution/public/flyout/shared/components/flyout_navigation.tsx
+++ b/x-pack/plugins/security_solution/public/flyout/shared/components/flyout_navigation.tsx
@@ -15,9 +15,16 @@ import {
EuiButtonEmpty,
} from '@elastic/eui';
import { css } from '@emotion/react';
-import { useExpandableFlyoutApi, useExpandableFlyoutState } from '@kbn/expandable-flyout';
+import {
+ useExpandableFlyoutApi,
+ useExpandableFlyoutState,
+ useExpandableFlyoutHistory,
+} from '@kbn/expandable-flyout';
import { FormattedMessage } from '@kbn/i18n-react';
import { i18n } from '@kbn/i18n';
+import { FlyoutHistory } from './flyout_history';
+import { getProcessedHistory } from '../utils/history_utils';
+import { useIsExperimentalFeatureEnabled } from '../../../common/hooks/use_experimental_features';
import {
HEADER_ACTIONS_TEST_ID,
COLLAPSE_DETAILS_BUTTON_TEST_ID,
@@ -37,6 +44,14 @@ export interface FlyoutNavigationProps {
* Optional actions to be placed on the right hand side of navigation
*/
actions?: React.ReactElement;
+ /**
+ * Boolean indicating the panel is shown in preview panel
+ */
+ isPreviewMode?: boolean;
+ /**
+ * Boolean indicating the panel is shown in rule preview
+ */
+ isPreview?: boolean;
}
/**
@@ -44,9 +59,17 @@ export interface FlyoutNavigationProps {
* pass in a list of actions to be displayed on top.
*/
export const FlyoutNavigation: FC = memo(
- ({ flyoutIsExpandable = false, expandDetails, actions }) => {
+ ({ flyoutIsExpandable = false, expandDetails, actions, isPreviewMode, isPreview }) => {
const { euiTheme } = useEuiTheme();
+ const history = useExpandableFlyoutHistory();
+ const isFlyoutHistoryEnabled = useIsExperimentalFeatureEnabled(
+ 'newExpandableFlyoutNavigationEnabled'
+ );
+ const historyArray = useMemo(() => getProcessedHistory({ history, maxCount: 10 }), [history]);
+ // Don't show history in rule preview
+ const hasHistory = !isPreview && isFlyoutHistoryEnabled;
+
const panels = useExpandableFlyoutState();
const isExpanded: boolean = !!panels.left;
@@ -101,7 +124,12 @@ export const FlyoutNavigation: FC = memo(
[expandDetails]
);
- return flyoutIsExpandable || actions ? (
+ // do not show navigation in preview mode
+ if (isPreviewMode) {
+ return null;
+ }
+
+ return flyoutIsExpandable || actions || hasHistory ? (
= memo(
`}
>
- {flyoutIsExpandable && expandDetails && (isExpanded ? collapseButton : expandButton)}
+
+ {flyoutIsExpandable && expandDetails && (
+
+ {isExpanded ? collapseButton : expandButton}
+
+ )}
+ {hasHistory && (
+
+
+
+ )}
+
{actions && (
diff --git a/x-pack/plugins/security_solution/public/flyout/shared/components/preview_link.test.tsx b/x-pack/plugins/security_solution/public/flyout/shared/components/preview_link.test.tsx
index f1dedc18d3b1c..0dcda8d523392 100644
--- a/x-pack/plugins/security_solution/public/flyout/shared/components/preview_link.test.tsx
+++ b/x-pack/plugins/security_solution/public/flyout/shared/components/preview_link.test.tsx
@@ -16,7 +16,7 @@ import { HostPreviewPanelKey } from '../../entity_details/host_right';
import { HOST_PREVIEW_BANNER } from '../../document_details/right/components/host_entity_overview';
import { UserPreviewPanelKey } from '../../entity_details/user_right';
import { USER_PREVIEW_BANNER } from '../../document_details/right/components/user_entity_overview';
-import { NetworkPanelKey, NETWORK_PREVIEW_BANNER } from '../../network_details';
+import { NetworkPreviewPanelKey, NETWORK_PREVIEW_BANNER } from '../../network_details';
import { RulePreviewPanelKey, RULE_PREVIEW_BANNER } from '../../rule_details/right';
import { createTelemetryServiceMock } from '../../../common/lib/telemetry/telemetry_service.mock';
@@ -105,10 +105,11 @@ describe('', () => {
getByTestId('ip-link').click();
expect(mockFlyoutApi.openPreviewPanel).toHaveBeenCalledWith({
- id: NetworkPanelKey,
+ id: NetworkPreviewPanelKey,
params: {
ip: '100:XXX:XXX',
flowTarget: 'source',
+ scopeId: 'scopeId',
banner: NETWORK_PREVIEW_BANNER,
},
});
diff --git a/x-pack/plugins/security_solution/public/flyout/shared/components/preview_link.tsx b/x-pack/plugins/security_solution/public/flyout/shared/components/preview_link.tsx
index b6a4ea33ba4bc..712156243d4cb 100644
--- a/x-pack/plugins/security_solution/public/flyout/shared/components/preview_link.tsx
+++ b/x-pack/plugins/security_solution/public/flyout/shared/components/preview_link.tsx
@@ -22,7 +22,7 @@ import { HostPreviewPanelKey } from '../../entity_details/host_right';
import { HOST_PREVIEW_BANNER } from '../../document_details/right/components/host_entity_overview';
import { UserPreviewPanelKey } from '../../entity_details/user_right';
import { USER_PREVIEW_BANNER } from '../../document_details/right/components/user_entity_overview';
-import { NetworkPanelKey, NETWORK_PREVIEW_BANNER } from '../../network_details';
+import { NetworkPreviewPanelKey, NETWORK_PREVIEW_BANNER } from '../../network_details';
import { RulePreviewPanelKey, RULE_PREVIEW_BANNER } from '../../rule_details/right';
import { DocumentEventTypes } from '../../../common/lib/telemetry';
@@ -46,9 +46,10 @@ const getPreviewParams = (
): PreviewParams | null => {
if (getEcsField(field)?.type === IP_FIELD_TYPE) {
return {
- id: NetworkPanelKey,
+ id: NetworkPreviewPanelKey,
params: {
ip: value,
+ scopeId,
flowTarget: field.includes(FlowTargetSourceDest.destination)
? FlowTargetSourceDest.destination
: FlowTargetSourceDest.source,
diff --git a/x-pack/plugins/security_solution/public/flyout/shared/components/test_ids.ts b/x-pack/plugins/security_solution/public/flyout/shared/components/test_ids.ts
index f8a589f31561e..7f6be4ef2fa1d 100644
--- a/x-pack/plugins/security_solution/public/flyout/shared/components/test_ids.ts
+++ b/x-pack/plugins/security_solution/public/flyout/shared/components/test_ids.ts
@@ -36,3 +36,18 @@ export const HEADER_ACTIONS_TEST_ID = `${FLYOUT_NAVIGATION_TEST_ID}Actions` as c
export const TITLE_HEADER_ICON_TEST_ID = (dataTestSubj: string) => `${dataTestSubj}Icon`;
export const TITLE_HEADER_TEXT_TEST_ID = (dataTestSubj: string) => `${dataTestSubj}Text`;
export const TITLE_LINK_ICON_TEST_ID = (dataTestSubj: string) => `${dataTestSubj}LinkIcon`;
+
+/* History */
+export const FLYOUT_HISTORY_TEST_ID = `${PREFIX}History` as const;
+export const FLYOUT_HISTORY_BUTTON_TEST_ID = `${FLYOUT_HISTORY_TEST_ID}Button` as const;
+export const FLYOUT_HISTORY_CONTEXT_PANEL_TEST_ID =
+ `${FLYOUT_HISTORY_TEST_ID}ContextPanel` as const;
+
+export const DOCUMENT_DETAILS_HISTORY_ROW_TEST_ID =
+ `${FLYOUT_HISTORY_TEST_ID}DocumentDetailsRow` as const;
+export const RULE_HISTORY_ROW_TEST_ID = `${FLYOUT_HISTORY_TEST_ID}RuleRow` as const;
+export const HOST_HISTORY_ROW_TEST_ID = `${FLYOUT_HISTORY_TEST_ID}HostRow` as const;
+export const USER_HISTORY_ROW_TEST_ID = `${FLYOUT_HISTORY_TEST_ID}UserRow` as const;
+export const NETWORK_HISTORY_ROW_TEST_ID = `${FLYOUT_HISTORY_TEST_ID}NetworkRow` as const;
+export const GENERIC_HISTORY_ROW_TEST_ID = `${FLYOUT_HISTORY_TEST_ID}GenericRow` as const;
+export const NO_DATA_HISTORY_ROW_TEST_ID = `${FLYOUT_HISTORY_TEST_ID}NoDataRow` as const;
diff --git a/x-pack/plugins/security_solution/public/flyout/shared/utils/history_utils.test.ts b/x-pack/plugins/security_solution/public/flyout/shared/utils/history_utils.test.ts
new file mode 100644
index 0000000000000..97257fa84dd8a
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/flyout/shared/utils/history_utils.test.ts
@@ -0,0 +1,56 @@
+/*
+ * 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 { getProcessedHistory } from './history_utils';
+
+describe('getProcessedHistory', () => {
+ const simpleHistory = [{ id: '1' }, { id: '2' }, { id: '3' }, { id: '4' }];
+ const complexHistory = [
+ { id: '1' },
+ { id: '2' },
+ { id: '1' },
+ { id: '3' },
+ { id: '4' },
+ { id: '2' },
+ ];
+
+ it('returns a reversed history array and removes latest entry', () => {
+ // input: 1, 2, 3, 4
+ // reverse: 4, 3, 2, 1
+ // remove latest: 4, 3, 2
+ const processedHistory = getProcessedHistory({ history: simpleHistory, maxCount: 5 });
+ expect(processedHistory).toEqual([{ id: '3' }, { id: '2' }, { id: '1' }]);
+ });
+
+ it('returns processed history with the maxCount', () => {
+ // input: 1, 2, 3, 4
+ // reverse: 4, 3, 2, 1
+ // remove latest: 3, 2, 1
+ // keep maxCount: 3, 2
+ const processedHistory = getProcessedHistory({ history: simpleHistory, maxCount: 2 });
+ expect(processedHistory).toEqual([{ id: '3' }, { id: '2' }]);
+ });
+
+ it('removes duplicates and reverses', () => {
+ // input: 1, 2, 1, 3, 4, 2
+ // reverse: 2, 4, 3, 1, 2, 1
+ // remove duplicates: 2, 4, 3, 1
+ // remove latest: 4, 3, 1
+ const processedHistory = getProcessedHistory({ history: complexHistory, maxCount: 5 });
+ expect(processedHistory).toEqual([{ id: '4' }, { id: '3' }, { id: '1' }]);
+ });
+
+ it('returns empty array if history only has one entry', () => {
+ const processedHistory = getProcessedHistory({ history: [{ id: '1' }], maxCount: 5 });
+ expect(processedHistory).toEqual([]);
+ });
+
+ it('returns empty array if history is empty', () => {
+ const processedHistory = getProcessedHistory({ history: [], maxCount: 5 });
+ expect(processedHistory).toEqual([]);
+ });
+});
diff --git a/x-pack/plugins/security_solution/public/flyout/shared/utils/history_utils.ts b/x-pack/plugins/security_solution/public/flyout/shared/utils/history_utils.ts
new file mode 100644
index 0000000000000..ef31daa7f83f6
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/flyout/shared/utils/history_utils.ts
@@ -0,0 +1,31 @@
+/*
+ * 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 type { FlyoutPanelProps } from '@kbn/expandable-flyout';
+
+/**
+ * Helper function that reverses the history array,
+ * removes duplicates and the most recent item
+ * @returns a history array of maxCount length
+ */
+export const getProcessedHistory = ({
+ history,
+ maxCount,
+}: {
+ history: FlyoutPanelProps[];
+ maxCount: number;
+}): FlyoutPanelProps[] => {
+ // Step 1: reverse history so the most recent is first
+ const reversedHistory = history.slice().reverse();
+
+ // Step 2: remove duplicates
+ const historyArray = Array.from(new Set(reversedHistory.map((i) => JSON.stringify(i)))).map((i) =>
+ JSON.parse(i)
+ );
+
+ // Omit the first (current) entry and return array of maxCount length
+ return historyArray.slice(1, maxCount + 1);
+};
diff --git a/x-pack/plugins/security_solution/public/timelines/components/formatted_ip/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/formatted_ip/index.test.tsx
index 339a55a8b6d56..a132a1745af63 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/formatted_ip/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/formatted_ip/index.test.tsx
@@ -113,6 +113,7 @@ describe('FormattedIp', () => {
params: {
ip: props.value,
flowTarget: 'source',
+ scopeId: TimelineId.active,
},
},
});
diff --git a/x-pack/plugins/security_solution/public/timelines/components/formatted_ip/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/formatted_ip/index.tsx
index fc03759e17b14..3871a20f6b695 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/formatted_ip/index.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/formatted_ip/index.tsx
@@ -195,6 +195,7 @@ const AddressLinksItemComponent: React.FC = ({
id: NetworkPanelKey,
params: {
ip: address,
+ scopeId: eventContext.timelineID,
flowTarget: fieldName.includes(FlowTargetSourceDest.destination)
? FlowTargetSourceDest.destination
: FlowTargetSourceDest.source,
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/formatted_field_helpers.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/formatted_field_helpers.tsx
index d41c0238ce592..057f108834ee6 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/formatted_field_helpers.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/formatted_field_helpers.tsx
@@ -67,7 +67,7 @@ export const RenderRuleName: React.FC = ({
title,
value,
}) => {
- const { openRightPanel } = useExpandableFlyoutApi();
+ const { openFlyout } = useExpandableFlyoutApi();
const eventContext = useContext(StatefulEventContext);
const ruleName = `${value}`;
@@ -91,14 +91,16 @@ export const RenderRuleName: React.FC = ({
return;
}
- openRightPanel({
- id: RulePanelKey,
- params: {
- ruleId,
+ openFlyout({
+ right: {
+ id: RulePanelKey,
+ params: {
+ ruleId,
+ },
},
});
},
- [navigateToApp, ruleId, search, openInNewTab, openRightPanel, eventContext, isInTimelineContext]
+ [navigateToApp, ruleId, search, openInNewTab, openFlyout, eventContext, isInTimelineContext]
);
const href = useMemo(
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/host_name.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/host_name.test.tsx
index a7769069ff197..24f704e5f846d 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/host_name.test.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/host_name.test.tsx
@@ -16,7 +16,7 @@ import { TableId } from '@kbn/securitysolution-data-table';
import { createExpandableFlyoutApiMock } from '../../../../../common/mock/expandable_flyout';
import { useExpandableFlyoutApi } from '@kbn/expandable-flyout';
-const mockOpenRightPanel = jest.fn();
+const mockOpenFlyout = jest.fn();
jest.mock('@kbn/expandable-flyout');
@@ -28,7 +28,7 @@ describe('HostName', () => {
beforeEach(() => {
jest.mocked(useExpandableFlyoutApi).mockReturnValue({
...createExpandableFlyoutApiMock(),
- openRightPanel: mockOpenRightPanel,
+ openFlyout: mockOpenFlyout,
});
});
@@ -81,7 +81,7 @@ describe('HostName', () => {
wrapper.find('[data-test-subj="host-details-button"]').last().simulate('click');
await waitFor(() => {
- expect(mockOpenRightPanel).not.toHaveBeenCalled();
+ expect(mockOpenFlyout).not.toHaveBeenCalled();
});
});
@@ -103,7 +103,7 @@ describe('HostName', () => {
wrapper.find('[data-test-subj="host-details-button"]').last().simulate('click');
await waitFor(() => {
- expect(mockOpenRightPanel).not.toHaveBeenCalled();
+ expect(mockOpenFlyout).not.toHaveBeenCalled();
});
});
@@ -125,7 +125,7 @@ describe('HostName', () => {
wrapper.find('[data-test-subj="host-details-button"]').last().simulate('click');
await waitFor(() => {
- expect(mockOpenRightPanel).not.toHaveBeenCalled();
+ expect(mockOpenFlyout).not.toHaveBeenCalled();
});
});
@@ -146,13 +146,15 @@ describe('HostName', () => {
wrapper.find('[data-test-subj="host-details-button"]').last().simulate('click');
await waitFor(() => {
- expect(mockOpenRightPanel).toHaveBeenCalledWith({
- id: 'host-panel',
- params: {
- hostName: props.value,
- contextID: props.contextId,
- scopeId: TableId.alertsOnAlertsPage,
- isDraggable: false,
+ expect(mockOpenFlyout).toHaveBeenCalledWith({
+ right: {
+ id: 'host-panel',
+ params: {
+ hostName: props.value,
+ contextID: props.contextId,
+ scopeId: TableId.alertsOnAlertsPage,
+ isDraggable: false,
+ },
},
});
});
@@ -175,13 +177,15 @@ describe('HostName', () => {
wrapper.find('[data-test-subj="host-details-button"]').last().simulate('click');
await waitFor(() => {
- expect(mockOpenRightPanel).toHaveBeenCalledWith({
- id: 'host-panel',
- params: {
- hostName: props.value,
- contextID: props.contextId,
- scopeId: 'timeline-1',
- isDraggable: false,
+ expect(mockOpenFlyout).toHaveBeenCalledWith({
+ right: {
+ id: 'host-panel',
+ params: {
+ hostName: props.value,
+ contextID: props.contextId,
+ scopeId: 'timeline-1',
+ isDraggable: false,
+ },
},
});
});
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/host_name.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/host_name.tsx
index 41d403b3f2c5b..0597b3e99d207 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/host_name.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/host_name.tsx
@@ -44,7 +44,7 @@ const HostNameComponent: React.FC = ({
title,
value,
}) => {
- const { openRightPanel } = useExpandableFlyoutApi();
+ const { openFlyout } = useExpandableFlyoutApi();
const isInSecurityApp = useIsInSecurityApp();
@@ -70,18 +70,19 @@ const HostNameComponent: React.FC = ({
}
const { timelineID } = eventContext;
-
- openRightPanel({
- id: HostPanelKey,
- params: {
- hostName,
- contextID: contextId,
- scopeId: timelineID,
- isDraggable,
+ openFlyout({
+ right: {
+ id: HostPanelKey,
+ params: {
+ hostName,
+ contextID: contextId,
+ scopeId: timelineID,
+ isDraggable,
+ },
},
});
},
- [contextId, eventContext, hostName, isDraggable, isInTimelineContext, onClick, openRightPanel]
+ [contextId, eventContext, hostName, isDraggable, isInTimelineContext, onClick, openFlyout]
);
// The below is explicitly defined this way as the onClick takes precedence when it and the href are both defined
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/user_name.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/user_name.test.tsx
index bdb53f5850ec3..c3211d5fd776e 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/user_name.test.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/user_name.test.tsx
@@ -16,7 +16,7 @@ import { TableId } from '@kbn/securitysolution-data-table';
import { useExpandableFlyoutApi } from '@kbn/expandable-flyout';
import { createExpandableFlyoutApiMock } from '../../../../../common/mock/expandable_flyout';
-const mockOpenRightPanel = jest.fn();
+const mockOpenFlyout = jest.fn();
jest.mock('@kbn/expandable-flyout');
@@ -28,7 +28,7 @@ describe('UserName', () => {
beforeEach(() => {
jest.mocked(useExpandableFlyoutApi).mockReturnValue({
...createExpandableFlyoutApiMock(),
- openRightPanel: mockOpenRightPanel,
+ openFlyout: mockOpenFlyout,
});
});
afterEach(() => {
@@ -78,7 +78,7 @@ describe('UserName', () => {
wrapper.find('[data-test-subj="users-link-anchor"]').last().simulate('click');
await waitFor(() => {
- expect(mockOpenRightPanel).not.toHaveBeenCalled();
+ expect(mockOpenFlyout).not.toHaveBeenCalled();
});
});
@@ -100,7 +100,7 @@ describe('UserName', () => {
wrapper.find('[data-test-subj="users-link-anchor"]').last().simulate('click');
await waitFor(() => {
- expect(mockOpenRightPanel).not.toHaveBeenCalled();
+ expect(mockOpenFlyout).not.toHaveBeenCalled();
});
});
@@ -121,13 +121,15 @@ describe('UserName', () => {
wrapper.find('[data-test-subj="users-link-anchor"]').last().simulate('click');
await waitFor(() => {
- expect(mockOpenRightPanel).toHaveBeenCalledWith({
- id: 'user-panel',
- params: {
- userName: props.value,
- contextID: props.contextId,
- scopeId: TableId.alertsOnAlertsPage,
- isDraggable: false,
+ expect(mockOpenFlyout).toHaveBeenCalledWith({
+ right: {
+ id: 'user-panel',
+ params: {
+ userName: props.value,
+ contextID: props.contextId,
+ scopeId: TableId.alertsOnAlertsPage,
+ isDraggable: false,
+ },
},
});
});
@@ -150,13 +152,15 @@ describe('UserName', () => {
wrapper.find('[data-test-subj="users-link-anchor"]').last().simulate('click');
await waitFor(() => {
- expect(mockOpenRightPanel).toHaveBeenCalledWith({
- id: 'user-panel',
- params: {
- userName: props.value,
- contextID: props.contextId,
- scopeId: 'timeline-1',
- isDraggable: false,
+ expect(mockOpenFlyout).toHaveBeenCalledWith({
+ right: {
+ id: 'user-panel',
+ params: {
+ userName: props.value,
+ contextID: props.contextId,
+ scopeId: 'timeline-1',
+ isDraggable: false,
+ },
},
});
});
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/user_name.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/user_name.tsx
index 31a8424e5ea0c..88e8889353a03 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/user_name.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/user_name.tsx
@@ -47,7 +47,7 @@ const UserNameComponent: React.FC = ({
const eventContext = useContext(StatefulEventContext);
const userName = `${value}`;
const isInTimelineContext = userName && eventContext?.timelineID;
- const { openRightPanel } = useExpandableFlyoutApi();
+ const { openFlyout } = useExpandableFlyoutApi();
const isInSecurityApp = useIsInSecurityApp();
@@ -65,17 +65,19 @@ const UserNameComponent: React.FC = ({
const { timelineID } = eventContext;
- openRightPanel({
- id: UserPanelKey,
- params: {
- userName,
- contextID: contextId,
- scopeId: timelineID,
- isDraggable,
+ openFlyout({
+ right: {
+ id: UserPanelKey,
+ params: {
+ userName,
+ contextID: contextId,
+ scopeId: timelineID,
+ isDraggable,
+ },
},
});
},
- [contextId, eventContext, isDraggable, isInTimelineContext, onClick, openRightPanel, userName]
+ [contextId, eventContext, isDraggable, isInTimelineContext, onClick, openFlyout, userName]
);
// The below is explicitly defined this way as the onClick takes precedence when it and the href are both defined