mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[Security Solution][Expandable flyout] Introducing Flyout history in document flyout (#184970)
## 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  ### 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.  **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` <details> <summary>✅ Alerts page</summary> Available for alert, host, user, rule name and ip's <img src="https://github.com/user-attachments/assets/e74a6444-763f-4e18-8370-f6c0c83e0d4c" /> </details> <details> <summary>✅ Explore pages (event table)</summary> Available for events, host, user, rule name and ip's <img src="https://github.com/user-attachments/assets/d2b9f0b9-a788-4174-bc80-8ac9c51fb16a" /> </details> <details> <summary>✅ Cases</summary> Note: the rule and entity link still go to a page, this will be addressed in a separate PR <img src="https://github.com/user-attachments/assets/fa7a5c86-d1e3-4dad-80ed-405c52efc486" /> </details> <details> <summary>✅ Discover in severless</summary> - enable `discover.experimental.enabledProfiles: ['security-root-profile']` <img src="https://github.com/user-attachments/assets/ebd5de5d-1ed3-42ad-bb6f-1beccdc48e62" /> </details> <details> <summary>❌ Disabled in alert preview </summary> <img src="https://github.com/user-attachments/assets/53e82ded-0db8-4639-afa1-c5cf224cf193" /> </details> <details> <summary>❌ Disabled in preview mode </summary> <img src="https://github.com/user-attachments/assets/a12b741f-2558-4fb5-852f-282af6e10f93" /> </details> ## 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
This commit is contained in:
parent
a5c9ed7bb8
commit
5b6887dd3d
56 changed files with 1320 additions and 206 deletions
|
@ -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:
|
||||
|
|
|
@ -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';
|
||||
|
||||
|
|
|
@ -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: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
|
@ -30,6 +30,7 @@ describe('PreviewSection', () => {
|
|||
id: 'key',
|
||||
},
|
||||
],
|
||||
history: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
|
@ -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));
|
||||
};
|
|
@ -111,6 +111,7 @@ export const Right: Story<void> = () => {
|
|||
},
|
||||
left: undefined,
|
||||
preview: undefined,
|
||||
history: [{ id: 'right' }],
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -139,6 +140,7 @@ export const Left: Story<void> = () => {
|
|||
id: 'left',
|
||||
},
|
||||
preview: undefined,
|
||||
history: [{ id: 'right' }],
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -171,6 +173,7 @@ export const Preview: Story<void> = () => {
|
|||
id: 'preview1',
|
||||
},
|
||||
],
|
||||
history: [{ id: 'right' }],
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -206,6 +209,7 @@ export const MultiplePreviews: Story<void> = () => {
|
|||
id: 'preview2',
|
||||
},
|
||||
],
|
||||
history: [{ id: 'right' }],
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -232,6 +236,7 @@ export const CollapsedPushMode: Story<void> = () => {
|
|||
},
|
||||
left: undefined,
|
||||
preview: undefined,
|
||||
history: [{ id: 'right' }],
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -260,6 +265,7 @@ export const ExpandedPushMode: Story<void> = () => {
|
|||
id: 'left',
|
||||
},
|
||||
preview: undefined,
|
||||
history: [{ id: 'right' }],
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -288,6 +294,7 @@ export const DisableTypeSelection: Story<void> = () => {
|
|||
id: 'left',
|
||||
},
|
||||
preview: undefined,
|
||||
history: [{ id: 'right' }],
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -318,6 +325,7 @@ export const ResetWidths: Story<void> = () => {
|
|||
id: 'left',
|
||||
},
|
||||
preview: undefined,
|
||||
history: [{ id: 'right' }],
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -343,6 +351,7 @@ export const DisableResizeWidthSelection: Story<void> = () => {
|
|||
id: 'left',
|
||||
},
|
||||
preview: undefined,
|
||||
history: [{ id: 'right' }],
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
|
@ -51,6 +51,7 @@ describe('ExpandableFlyout', () => {
|
|||
},
|
||||
left: undefined,
|
||||
preview: undefined,
|
||||
history: [{ id: 'key' }],
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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": "アナライザーグラフを開く",
|
||||
|
|
|
@ -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": "打开分析器图表",
|
||||
|
|
|
@ -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
|
||||
*/
|
||||
|
|
|
@ -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('<HostDetails />', () => {
|
|||
|
||||
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,
|
||||
},
|
||||
});
|
||||
|
|
|
@ -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('<UserDetails />', () => {
|
|||
|
||||
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,
|
||||
},
|
||||
});
|
||||
|
|
|
@ -83,9 +83,4 @@ describe('<AlertHeaderTitle />', () => {
|
|||
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');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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 ? (
|
||||
<EuiLink href={href} target="_blank" external={false}>
|
||||
<FlyoutTitle
|
||||
title={ruleName}
|
||||
title={title}
|
||||
iconType={'warning'}
|
||||
isLink
|
||||
data-test-subj={FLYOUT_ALERT_HEADER_TITLE_TEST_ID}
|
||||
|
@ -62,12 +60,12 @@ export const AlertHeaderTitle = memo(() => {
|
|||
</EuiLink>
|
||||
) : (
|
||||
<FlyoutTitle
|
||||
title={ruleName}
|
||||
title={title}
|
||||
iconType={'warning'}
|
||||
data-test-subj={FLYOUT_ALERT_HEADER_TITLE_TEST_ID}
|
||||
/>
|
||||
),
|
||||
[ruleName, href]
|
||||
[title, href]
|
||||
);
|
||||
|
||||
const { refetch } = useRefetchByScope({ scopeId });
|
||||
|
@ -86,17 +84,7 @@ export const AlertHeaderTitle = memo(() => {
|
|||
<EuiSpacer size="m" />
|
||||
{timestamp && <PreferenceFormattedDate value={new Date(timestamp)} />}
|
||||
<EuiSpacer size="xs" />
|
||||
{isAlert && ruleName ? (
|
||||
ruleTitle
|
||||
) : (
|
||||
<FlyoutTitle
|
||||
title={i18n.translate('xpack.securitySolution.flyout.right.header.headerTitle', {
|
||||
defaultMessage: 'Document details',
|
||||
})}
|
||||
iconType={'warning'}
|
||||
data-test-subj={FLYOUT_ALERT_HEADER_TITLE_TEST_ID}
|
||||
/>
|
||||
)}
|
||||
{ruleTitle}
|
||||
<EuiSpacer size="m" />
|
||||
{securitySolutionNotesDisabled ? (
|
||||
<EuiFlexGroup
|
||||
|
|
|
@ -6,17 +6,14 @@
|
|||
*/
|
||||
|
||||
import React, { memo, useMemo } from 'react';
|
||||
import { startCase } from 'lodash';
|
||||
import { EuiSpacer } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FlyoutTitle } from '../../../shared/components/flyout_title';
|
||||
import { DocumentSeverity } from './severity';
|
||||
import { useBasicDataFromDetailsData } from '../../shared/hooks/use_basic_data_from_details_data';
|
||||
import { useDocumentDetailsContext } from '../../shared/context';
|
||||
import { PreferenceFormattedDate } from '../../../../common/components/formatted_date';
|
||||
import { FLYOUT_EVENT_HEADER_TITLE_TEST_ID } from './test_ids';
|
||||
import { getField } from '../../shared/utils';
|
||||
import { EVENT_CATEGORY_TO_FIELD } from '../utils/event_utils';
|
||||
import { getField, getEventTitle } from '../../shared/utils';
|
||||
|
||||
/**
|
||||
* Event details flyout right section header
|
||||
|
@ -28,31 +25,10 @@ export const EventHeaderTitle = memo(() => {
|
|||
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 (
|
||||
<>
|
||||
|
|
|
@ -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('<HighlightedFieldsCell />', () => {
|
|||
|
||||
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,
|
||||
},
|
||||
});
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
});
|
||||
|
|
|
@ -25,7 +25,7 @@ interface PanelNavigationProps {
|
|||
export const PanelNavigation: FC<PanelNavigationProps> = 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<PanelNavigationProps> = memo(({ flyoutIsExpanda
|
|||
flyoutIsExpandable={flyoutIsExpandable}
|
||||
expandDetails={expandDetails}
|
||||
actions={<HeaderActions />}
|
||||
isPreviewMode={false}
|
||||
isPreview={isPreview}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
|
|
@ -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<string, string> = {
|
||||
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: '',
|
||||
};
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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<string, string> = {
|
||||
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;
|
||||
};
|
||||
|
|
|
@ -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', () => {
|
||||
|
|
|
@ -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<string, unknown> {
|
||||
contextID: string;
|
||||
scopeId: string;
|
||||
|
@ -187,13 +187,14 @@ export const HostPanel = ({
|
|||
<>
|
||||
<FlyoutNavigation
|
||||
flyoutIsExpandable={
|
||||
!isPreviewMode &&
|
||||
(isRiskScoreExist ||
|
||||
hasMisconfigurationFindings ||
|
||||
hasVulnerabilitiesFindings ||
|
||||
hasNonClosedAlerts)
|
||||
isRiskScoreExist ||
|
||||
hasMisconfigurationFindings ||
|
||||
hasVulnerabilitiesFindings ||
|
||||
hasNonClosedAlerts
|
||||
}
|
||||
expandDetails={openDefaultPanel}
|
||||
isPreviewMode={isPreviewMode}
|
||||
isPreview={scopeId === TableId.rulePreview}
|
||||
/>
|
||||
<HostPanelHeader hostName={hostName} observedHost={observedHostWithAnomalies} />
|
||||
<HostPanelContent
|
||||
|
|
|
@ -10,7 +10,16 @@ import React from 'react';
|
|||
import { TestProviders } from '../../../common/mock';
|
||||
import type { UserPanelProps } from '.';
|
||||
import { UserPanel } from '.';
|
||||
|
||||
import type {
|
||||
FlyoutPanelProps,
|
||||
ExpandableFlyoutState,
|
||||
ExpandableFlyoutApi,
|
||||
} from '@kbn/expandable-flyout';
|
||||
import {
|
||||
useExpandableFlyoutApi,
|
||||
useExpandableFlyoutState,
|
||||
useExpandableFlyoutHistory,
|
||||
} from '@kbn/expandable-flyout';
|
||||
import { mockManagedUserData, mockObservedUser } from './mocks';
|
||||
import { mockRiskScoreState } from '../../shared/mocks';
|
||||
|
||||
|
@ -44,11 +53,25 @@ jest.mock('../../../common/hooks/use_experimental_features', () => ({
|
|||
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', () => {
|
||||
|
|
|
@ -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 = ({
|
|||
<>
|
||||
<FlyoutNavigation
|
||||
flyoutIsExpandable={
|
||||
!isPreviewMode &&
|
||||
(hasUserDetailsData || hasMisconfigurationFindings || hasNonClosedAlerts)
|
||||
hasUserDetailsData || hasMisconfigurationFindings || hasNonClosedAlerts
|
||||
}
|
||||
expandDetails={openPanelFirstTab}
|
||||
isPreviewMode={isPreviewMode}
|
||||
isPreview={scopeId === TableId.rulePreview}
|
||||
/>
|
||||
<UserPanelHeader
|
||||
userName={userName}
|
||||
|
|
|
@ -39,7 +39,7 @@ import type { HostPanelExpandableFlyoutProps } from './entity_details/host_right
|
|||
import { HostPanel, HostPanelKey, HostPreviewPanelKey } from './entity_details/host_right';
|
||||
import type { HostDetailsExpandableFlyoutProps } from './entity_details/host_details_left';
|
||||
import { HostDetailsPanel, HostDetailsPanelKey } from './entity_details/host_details_left';
|
||||
import { NetworkPanel, NetworkPanelKey } from './network_details';
|
||||
import { NetworkPanel, NetworkPanelKey, NetworkPreviewPanelKey } from './network_details';
|
||||
import type { AnalyzerPanelExpandableFlyoutProps } from './document_details/analyzer_panels';
|
||||
import { AnalyzerPanel } from './document_details/analyzer_panels';
|
||||
|
||||
|
@ -140,6 +140,12 @@ const expandableFlyoutDocumentsPanels: ExpandableFlyoutProps['registeredPanels']
|
|||
key: NetworkPanelKey,
|
||||
component: (props) => <NetworkPanel {...(props as NetworkExpandableFlyoutProps).params} />,
|
||||
},
|
||||
{
|
||||
key: NetworkPreviewPanelKey,
|
||||
component: (props) => (
|
||||
<NetworkPanel {...(props as NetworkExpandableFlyoutProps).params} isPreviewMode />
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
export const SECURITY_SOLUTION_ON_CLOSE_EVENT = `expandable-flyout-on-close-${Flyouts.securitySolution}`;
|
||||
|
|
|
@ -28,7 +28,7 @@ export interface PanelHeaderProps extends React.ComponentProps<typeof EuiFlyoutH
|
|||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* Header component for the network details flyout
|
||||
*/
|
||||
export const PanelHeader: FC<PanelHeaderProps> = memo(
|
||||
({ ip, flowTarget, ...flyoutHeaderProps }: PanelHeaderProps) => {
|
||||
|
|
|
@ -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<string, unknown> {
|
|||
* 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 (
|
||||
<>
|
||||
<PanelHeader ip={ip} flowTarget={flowTarget} />
|
||||
<PanelContent ip={ip} flowTarget={flowTarget} />
|
||||
</>
|
||||
);
|
||||
});
|
||||
export const NetworkPanel: FC<NetworkPanelProps> = memo(
|
||||
({ ip, flowTarget, scopeId, isPreviewMode }) => {
|
||||
return (
|
||||
<>
|
||||
<FlyoutNavigation
|
||||
flyoutIsExpandable={false}
|
||||
isPreviewMode={isPreviewMode}
|
||||
isPreview={scopeId === TableId.rulePreview}
|
||||
/>
|
||||
<PanelHeader ip={ip} flowTarget={flowTarget} />
|
||||
<PanelContent ip={ip} flowTarget={flowTarget} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
NetworkPanel.displayName = 'NetworkPanel';
|
||||
|
|
|
@ -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 (
|
||||
<FlyoutBody>
|
||||
|
|
|
@ -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<PanelHeaderProps> = ({ rule, isSuppressed }) => {
|
||||
export const PanelHeader: React.FC<PanelHeaderProps> = memo(({ rule, isSuppressed }) => {
|
||||
const href = useRuleDetailsLink({ ruleId: rule.id });
|
||||
|
||||
return (
|
||||
|
@ -86,6 +86,6 @@ export const PanelHeader: React.FC<PanelHeaderProps> = ({ rule, isSuppressed })
|
|||
</EuiFlexGroup>
|
||||
</FlyoutHeader>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
PanelHeader.displayName = 'PanelHeader';
|
||||
|
|
|
@ -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('<RulePanel />', () => {
|
||||
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,
|
||||
|
|
|
@ -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<string, unknown> {
|
|||
/**
|
||||
* Displays a rule overview panel
|
||||
*/
|
||||
export const RulePanel = memo(({ ruleId, isPreviewMode }: RulePanelProps) => {
|
||||
export const RulePanel: FC<RulePanelProps> = memo(({ ruleId, isPreviewMode }) => {
|
||||
const { rule, loading, isExistingRule } = useRuleDetails({ ruleId });
|
||||
|
||||
return loading ? (
|
||||
<FlyoutLoading data-test-subj={LOADING_TEST_ID} />
|
||||
) : rule ? (
|
||||
<>
|
||||
<FlyoutNavigation flyoutIsExpandable={false} />
|
||||
<FlyoutNavigation flyoutIsExpandable={false} isPreviewMode={isPreviewMode} />
|
||||
<PanelHeader rule={rule} isSuppressed={!isExistingRule} />
|
||||
<PanelContent rule={rule} />
|
||||
{isPreviewMode && <PreviewFooter ruleId={ruleId} />}
|
||||
|
|
|
@ -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(
|
||||
<TestProviders>
|
||||
<FlyoutHistory history={mockedHistory} />
|
||||
</TestProviders>
|
||||
);
|
||||
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(
|
||||
<TestProviders>
|
||||
<FlyoutHistory history={mockedHistory} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
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(
|
||||
<TestProviders>
|
||||
<FlyoutHistory history={[]} />
|
||||
</TestProviders>
|
||||
);
|
||||
fireEvent.click(getByTestId(FLYOUT_HISTORY_BUTTON_TEST_ID));
|
||||
expect(getByTestId(NO_DATA_HISTORY_ROW_TEST_ID)).toBeInTheDocument();
|
||||
});
|
||||
});
|
|
@ -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<HistoryProps> = memo(({ history }) => {
|
||||
const [isPopoverOpen, setPopover] = useState(false);
|
||||
const togglePopover = () => setPopover(!isPopoverOpen);
|
||||
|
||||
const emptyHistoryMessage = useMemo(() => {
|
||||
return (
|
||||
<EuiContextMenuItem key={0} data-test-subj={NO_DATA_HISTORY_ROW_TEST_ID}>
|
||||
<EuiText size="s">
|
||||
<EuiTextColor color="subdued">
|
||||
<i>
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.flyout.history.noData"
|
||||
defaultMessage="No history"
|
||||
/>
|
||||
</i>
|
||||
</EuiTextColor>
|
||||
</EuiText>
|
||||
</EuiContextMenuItem>
|
||||
);
|
||||
}, []);
|
||||
|
||||
const historyDropdownPanels = useMemo(
|
||||
() =>
|
||||
history.length > 0
|
||||
? history.map((item, index) => {
|
||||
return <FlyoutHistoryRow item={item} index={index} />;
|
||||
})
|
||||
: [emptyHistoryMessage],
|
||||
[history, emptyHistoryMessage]
|
||||
);
|
||||
|
||||
return (
|
||||
<EuiFlexItem grow={false} data-test-subj={FLYOUT_HISTORY_TEST_ID}>
|
||||
<EuiPopover
|
||||
button={
|
||||
<EuiButtonEmpty
|
||||
onClick={togglePopover}
|
||||
size="m"
|
||||
iconType={'clockCounter'}
|
||||
data-test-subj={FLYOUT_HISTORY_BUTTON_TEST_ID}
|
||||
/>
|
||||
}
|
||||
isOpen={isPopoverOpen}
|
||||
closePopover={togglePopover}
|
||||
panelPaddingSize="none"
|
||||
anchorPosition="downLeft"
|
||||
>
|
||||
<EuiContextMenuPanel
|
||||
size="s"
|
||||
items={historyDropdownPanels}
|
||||
data-test-subj={FLYOUT_HISTORY_CONTEXT_PANEL_TEST_ID}
|
||||
/>
|
||||
</EuiPopover>
|
||||
</EuiFlexItem>
|
||||
);
|
||||
});
|
||||
|
||||
FlyoutHistory.displayName = 'FlyoutHistory';
|
|
@ -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(
|
||||
<TestProviders>
|
||||
<FlyoutHistoryRow item={rowItems.alert} index={0} />
|
||||
</TestProviders>
|
||||
);
|
||||
expect(getByTestId(`${0}-${DOCUMENT_DETAILS_HISTORY_ROW_TEST_ID}`)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders rule history row when key is rule', () => {
|
||||
const { getByTestId } = render(
|
||||
<TestProviders>
|
||||
<FlyoutHistoryRow item={rowItems.rule} index={1} />
|
||||
</TestProviders>
|
||||
);
|
||||
expect(getByTestId(`${1}-${RULE_HISTORY_ROW_TEST_ID}`)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders generic host history row when key is host', () => {
|
||||
const { getByTestId } = render(
|
||||
<TestProviders>
|
||||
<FlyoutHistoryRow item={rowItems.host} index={2} />
|
||||
</TestProviders>
|
||||
);
|
||||
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(
|
||||
<TestProviders>
|
||||
<FlyoutHistoryRow item={rowItems.user} index={3} />
|
||||
</TestProviders>
|
||||
);
|
||||
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(
|
||||
<TestProviders>
|
||||
<FlyoutHistoryRow item={rowItems.network} index={4} />
|
||||
</TestProviders>
|
||||
);
|
||||
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(
|
||||
<TestProviders>
|
||||
<FlyoutHistoryRow item={{ id: 'key' }} index={5} />
|
||||
</TestProviders>
|
||||
);
|
||||
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(
|
||||
<TestProviders>
|
||||
<DocumentDetailsHistoryRow item={rowItems.alert} index={0} />
|
||||
</TestProviders>
|
||||
);
|
||||
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(
|
||||
<TestProviders>
|
||||
<DocumentDetailsHistoryRow item={rowItems.alert} index={0} />
|
||||
</TestProviders>
|
||||
);
|
||||
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(
|
||||
<TestProviders>
|
||||
<DocumentDetailsHistoryRow item={rowItems.alert} index={0} />
|
||||
</TestProviders>
|
||||
);
|
||||
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(
|
||||
<TestProviders>
|
||||
<DocumentDetailsHistoryRow item={rowItems.alert} index={0} />
|
||||
</TestProviders>
|
||||
);
|
||||
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(
|
||||
<TestProviders>
|
||||
<RuleHistoryRow item={rowItems.rule} index={0} />
|
||||
</TestProviders>
|
||||
);
|
||||
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(
|
||||
<TestProviders>
|
||||
<RuleHistoryRow item={rowItems.rule} index={0} />
|
||||
</TestProviders>
|
||||
);
|
||||
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(
|
||||
<TestProviders>
|
||||
<GenericHistoryRow
|
||||
item={rowItems.host}
|
||||
name="Row name"
|
||||
icon={'user'}
|
||||
title="title"
|
||||
index={0}
|
||||
/>
|
||||
</TestProviders>
|
||||
);
|
||||
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 });
|
||||
});
|
||||
});
|
|
@ -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<FlyoutHistoryRowProps> = memo(({ item, index }) => {
|
||||
switch (item.id) {
|
||||
case DocumentDetailsRightPanelKey:
|
||||
return <DocumentDetailsHistoryRow item={item} index={index} />;
|
||||
case RulePanelKey:
|
||||
return <RuleHistoryRow item={item} index={index} />;
|
||||
case HostPanelKey:
|
||||
return (
|
||||
<GenericHistoryRow
|
||||
item={item}
|
||||
index={index}
|
||||
title={String(item?.params?.hostName)}
|
||||
icon={'storage'}
|
||||
name={'Host'}
|
||||
dataTestSubj={HOST_HISTORY_ROW_TEST_ID}
|
||||
/>
|
||||
);
|
||||
case UserPanelKey:
|
||||
return (
|
||||
<GenericHistoryRow
|
||||
item={item}
|
||||
index={index}
|
||||
title={String(item?.params?.userName)}
|
||||
icon={'user'}
|
||||
name={'User'}
|
||||
dataTestSubj={USER_HISTORY_ROW_TEST_ID}
|
||||
/>
|
||||
);
|
||||
case NetworkPanelKey:
|
||||
return (
|
||||
<GenericHistoryRow
|
||||
item={item}
|
||||
index={index}
|
||||
title={String(item?.params?.ip)}
|
||||
icon={'globe'}
|
||||
name={'Network'}
|
||||
dataTestSubj={NETWORK_HISTORY_ROW_TEST_ID}
|
||||
/>
|
||||
);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Row item for a document details
|
||||
*/
|
||||
export const DocumentDetailsHistoryRow: FC<FlyoutHistoryRowProps> = 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 (
|
||||
<GenericHistoryRow
|
||||
item={item}
|
||||
index={index}
|
||||
title={title}
|
||||
icon={isAlert ? 'warning' : 'analyzeEvent'}
|
||||
name={isAlert ? 'Alert' : 'Event'}
|
||||
dataTestSubj={DOCUMENT_DETAILS_HISTORY_ROW_TEST_ID}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
/**
|
||||
* Row item for a rule details flyout
|
||||
*/
|
||||
export const RuleHistoryRow: FC<FlyoutHistoryRowProps> = memo(({ item, index }) => {
|
||||
const ruleId = String(item?.params?.ruleId);
|
||||
const { rule } = useRuleDetails({ ruleId });
|
||||
|
||||
return (
|
||||
<GenericHistoryRow
|
||||
item={item}
|
||||
index={index}
|
||||
title={rule?.name ?? ''}
|
||||
icon={'indexSettings'}
|
||||
name={'Rule'}
|
||||
dataTestSubj={RULE_HISTORY_ROW_TEST_ID}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
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<GenericHistoryRowProps> = memo(
|
||||
({ item, index, title, icon, name, dataTestSubj }) => {
|
||||
const { openFlyout } = useExpandableFlyoutApi();
|
||||
const onClick = useCallback(() => {
|
||||
openFlyout({ right: item });
|
||||
}, [openFlyout, item]);
|
||||
|
||||
return (
|
||||
<EuiContextMenuItem
|
||||
key={index}
|
||||
onClick={onClick}
|
||||
icon={icon}
|
||||
data-test-subj={`${index}-${dataTestSubj ?? GENERIC_HISTORY_ROW_TEST_ID}`}
|
||||
>
|
||||
<i>{`${name}: `}</i>
|
||||
{title}
|
||||
</EuiContextMenuItem>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
FlyoutHistoryRow.displayName = 'FlyoutHistoryRow';
|
||||
DocumentDetailsHistoryRow.displayName = 'DocumentDetailsHistoryRow';
|
||||
RuleHistoryRow.displayName = 'RuleHistoryRow';
|
||||
GenericHistoryRow.displayName = 'GenericHistoryRow';
|
|
@ -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<PropsWithChildren<{}>> = ({ 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('<FlyoutNavigation />', () => {
|
|||
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('<FlyoutNavigation />', () => {
|
|||
expect(container).toBeEmptyDOMElement();
|
||||
});
|
||||
});
|
||||
|
||||
it('should render empty component if isPreviewMode is true', () => {
|
||||
const { container } = render(
|
||||
<ExpandableFlyoutTestProviders>
|
||||
<FlyoutNavigation isPreviewMode={true} flyoutIsExpandable={true} />
|
||||
</ExpandableFlyoutTestProviders>
|
||||
);
|
||||
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(
|
||||
<ExpandableFlyoutTestProviders>
|
||||
<FlyoutNavigation flyoutIsExpandable={false} />
|
||||
</ExpandableFlyoutTestProviders>
|
||||
);
|
||||
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(
|
||||
<ExpandableFlyoutTestProviders>
|
||||
<FlyoutNavigation flyoutIsExpandable={false} />
|
||||
</ExpandableFlyoutTestProviders>
|
||||
);
|
||||
expect(getByTestId(FLYOUT_HISTORY_BUTTON_TEST_ID)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should not render history button if in rule preview', () => {
|
||||
const { container } = render(
|
||||
<ExpandableFlyoutTestProviders>
|
||||
<FlyoutNavigation flyoutIsExpandable={false} isPreview={true} />
|
||||
</ExpandableFlyoutTestProviders>
|
||||
);
|
||||
expect(container).toBeEmptyDOMElement();
|
||||
});
|
||||
|
||||
it('should render empty component if isPreviewMode is true', () => {
|
||||
const { container } = render(
|
||||
<ExpandableFlyoutTestProviders>
|
||||
<FlyoutNavigation isPreviewMode={true} flyoutIsExpandable={true} />
|
||||
</ExpandableFlyoutTestProviders>
|
||||
);
|
||||
expect(container).toBeEmptyDOMElement();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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<FlyoutNavigationProps> = 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<FlyoutNavigationProps> = memo(
|
|||
[expandDetails]
|
||||
);
|
||||
|
||||
return flyoutIsExpandable || actions ? (
|
||||
// do not show navigation in preview mode
|
||||
if (isPreviewMode) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return flyoutIsExpandable || actions || hasHistory ? (
|
||||
<EuiFlyoutHeader hasBorder>
|
||||
<EuiFlexGroup
|
||||
direction="row"
|
||||
|
@ -116,7 +144,30 @@ export const FlyoutNavigation: FC<FlyoutNavigationProps> = memo(
|
|||
`}
|
||||
>
|
||||
<EuiFlexItem grow={false}>
|
||||
{flyoutIsExpandable && expandDetails && (isExpanded ? collapseButton : expandButton)}
|
||||
<EuiFlexGroup
|
||||
direction="row"
|
||||
justifyContent="flexStart"
|
||||
alignItems="center"
|
||||
responsive={false}
|
||||
gutterSize="none"
|
||||
>
|
||||
{flyoutIsExpandable && expandDetails && (
|
||||
<EuiFlexItem
|
||||
grow={false}
|
||||
css={css`
|
||||
border-right: 1px ${euiTheme.colors.lightShade} solid;
|
||||
padding-right: -${euiTheme.size.m};
|
||||
`}
|
||||
>
|
||||
{isExpanded ? collapseButton : expandButton}
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
{hasHistory && (
|
||||
<EuiFlexItem>
|
||||
<FlyoutHistory history={historyArray} />
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
{actions && (
|
||||
<EuiFlexItem grow={false} data-test-subj={HEADER_ACTIONS_TEST_ID}>
|
||||
|
|
|
@ -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('<PreviewLink />', () => {
|
|||
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,
|
||||
},
|
||||
});
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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([]);
|
||||
});
|
||||
});
|
|
@ -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);
|
||||
};
|
|
@ -113,6 +113,7 @@ describe('FormattedIp', () => {
|
|||
params: {
|
||||
ip: props.value,
|
||||
flowTarget: 'source',
|
||||
scopeId: TimelineId.active,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
|
@ -195,6 +195,7 @@ const AddressLinksItemComponent: React.FC<AddressLinksItemProps> = ({
|
|||
id: NetworkPanelKey,
|
||||
params: {
|
||||
ip: address,
|
||||
scopeId: eventContext.timelineID,
|
||||
flowTarget: fieldName.includes(FlowTargetSourceDest.destination)
|
||||
? FlowTargetSourceDest.destination
|
||||
: FlowTargetSourceDest.source,
|
||||
|
|
|
@ -67,7 +67,7 @@ export const RenderRuleName: React.FC<RenderRuleNameProps> = ({
|
|||
title,
|
||||
value,
|
||||
}) => {
|
||||
const { openRightPanel } = useExpandableFlyoutApi();
|
||||
const { openFlyout } = useExpandableFlyoutApi();
|
||||
const eventContext = useContext(StatefulEventContext);
|
||||
|
||||
const ruleName = `${value}`;
|
||||
|
@ -91,14 +91,16 @@ export const RenderRuleName: React.FC<RenderRuleNameProps> = ({
|
|||
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(
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
|
|
@ -44,7 +44,7 @@ const HostNameComponent: React.FC<Props> = ({
|
|||
title,
|
||||
value,
|
||||
}) => {
|
||||
const { openRightPanel } = useExpandableFlyoutApi();
|
||||
const { openFlyout } = useExpandableFlyoutApi();
|
||||
|
||||
const isInSecurityApp = useIsInSecurityApp();
|
||||
|
||||
|
@ -70,18 +70,19 @@ const HostNameComponent: React.FC<Props> = ({
|
|||
}
|
||||
|
||||
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
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
|
|
@ -47,7 +47,7 @@ const UserNameComponent: React.FC<Props> = ({
|
|||
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<Props> = ({
|
|||
|
||||
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
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue