From 60c61f1dc1bd037ec2cef61a267ec5d5780adf03 Mon Sep 17 00:00:00 2001 From: PhilippeOberti Date: Fri, 7 Feb 2025 17:10:53 -0600 Subject: [PATCH 1/3] [Security Solution][Expandable flyout] show last open in the history --- .../packages/expandable-flyout/README.md | 4 + .../packages/expandable-flyout/index.ts | 7 +- .../src/components/container.test.tsx | 4 +- .../expandable-flyout/src/index.stories.tsx | 18 +-- .../expandable-flyout/src/index.test.tsx | 2 +- .../expandable-flyout/src/provider.test.tsx | 4 +- .../src/store/reducers.test.ts | 88 +++++++------ .../expandable-flyout/src/store/reducers.ts | 23 +++- .../expandable-flyout/src/store/state.ts | 5 +- .../packages/expandable-flyout/src/types.ts | 11 ++ .../entity_details/host_right/index.test.tsx | 6 +- .../service_right/index.test.tsx | 6 +- .../entity_details/user_right/index.test.tsx | 6 +- .../flyout/rule_details/right/index.test.tsx | 10 +- .../shared/components/flyout_history.test.tsx | 10 +- .../shared/components/flyout_history.tsx | 12 +- .../components/flyout_history_row.test.tsx | 118 ++++++++++++------ .../shared/components/flyout_history_row.tsx | 72 ++++++++--- .../components/flyout_navigation.test.tsx | 16 +-- .../flyout/shared/utils/history_utils.test.ts | 65 +++++++--- .../flyout/shared/utils/history_utils.ts | 28 +++-- 21 files changed, 348 insertions(+), 167 deletions(-) diff --git a/x-pack/solutions/security/packages/expandable-flyout/README.md b/x-pack/solutions/security/packages/expandable-flyout/README.md index a3f0acb5ce428..6514bbdc2e7fc 100644 --- a/x-pack/solutions/security/packages/expandable-flyout/README.md +++ b/x-pack/solutions/security/packages/expandable-flyout/README.md @@ -46,6 +46,10 @@ The ExpandableFlyout [React component](https://github.com/elastic/kibana/tree/ma To retrieve the flyout's layout (left, right and preview panels), you can utilize [useExpandableFlyoutState](https://github.com/elastic/kibana/blob/main/x-pack/solutions/security/packages/expandable-flyout/src/hooks/use_expandable_flyout_state.ts). +The flyout also stores a history of all the opened panels (we push values via the `openPanelsAction` action only) for +the current session. That history can be retrieved +using [useExpandableFlyoutHistory](https://github.com/elastic/kibana/blob/main/x-pack/solutions/security/packages/expandable-flyout/src/hooks/use_expandable_flyout_history.ts). + To control (or mutate) flyout's layout, you can utilize [useExpandableFlyoutApi](https://github.com/elastic/kibana/blob/main/x-pack/solutions/security/packages/expandable-flyout/src/hooks/use_expandable_flyout_api.ts). **Expandable Flyout API** exposes the following methods: diff --git a/x-pack/solutions/security/packages/expandable-flyout/index.ts b/x-pack/solutions/security/packages/expandable-flyout/index.ts index 4b5cd448428c9..38a33d660c7aa 100644 --- a/x-pack/solutions/security/packages/expandable-flyout/index.ts +++ b/x-pack/solutions/security/packages/expandable-flyout/index.ts @@ -16,4 +16,9 @@ export { type FlyoutPanels as ExpandableFlyoutState } from './src/store/state'; export { ExpandableFlyoutProvider } from './src/provider'; export type { ExpandableFlyoutProps } from './src'; -export type { FlyoutPanelProps, PanelPath, ExpandableFlyoutApi } from './src/types'; +export type { + FlyoutPanelProps, + PanelPath, + ExpandableFlyoutApi, + FlyoutPanelHistory, +} from './src/types'; diff --git a/x-pack/solutions/security/packages/expandable-flyout/src/components/container.test.tsx b/x-pack/solutions/security/packages/expandable-flyout/src/components/container.test.tsx index 8cf8466901bad..9ef3465d1c4ef 100644 --- a/x-pack/solutions/security/packages/expandable-flyout/src/components/container.test.tsx +++ b/x-pack/solutions/security/packages/expandable-flyout/src/components/container.test.tsx @@ -12,8 +12,8 @@ import { Panel } from '../types'; import { LEFT_SECTION_TEST_ID, PREVIEW_SECTION_TEST_ID, - SETTINGS_MENU_BUTTON_TEST_ID, RIGHT_SECTION_TEST_ID, + SETTINGS_MENU_BUTTON_TEST_ID, } from './test_ids'; import { initialUiState, type State } from '../store/state'; import { TestProvider } from '../test/provider'; @@ -56,7 +56,7 @@ describe('Container', () => { }, left: undefined, preview: undefined, - history: [{ id: 'key' }], + history: [{ lastOpen: Date.now(), panel: { id: 'key1' } }], }, }, }, diff --git a/x-pack/solutions/security/packages/expandable-flyout/src/index.stories.tsx b/x-pack/solutions/security/packages/expandable-flyout/src/index.stories.tsx index 4667cf64e2991..acd904a5678ad 100644 --- a/x-pack/solutions/security/packages/expandable-flyout/src/index.stories.tsx +++ b/x-pack/solutions/security/packages/expandable-flyout/src/index.stories.tsx @@ -109,7 +109,7 @@ export const Right: Story = () => { }, left: undefined, preview: undefined, - history: [{ id: 'right' }], + history: [{ lastOpen: Date.now(), panel: { id: 'right' } }], }, }, }, @@ -138,7 +138,7 @@ export const Left: Story = () => { id: 'left', }, preview: undefined, - history: [{ id: 'right' }], + history: [{ lastOpen: Date.now(), panel: { id: 'right' } }], }, }, }, @@ -171,7 +171,7 @@ export const Preview: Story = () => { id: 'preview1', }, ], - history: [{ id: 'right' }], + history: [{ lastOpen: Date.now(), panel: { id: 'right' } }], }, }, }, @@ -207,7 +207,7 @@ export const MultiplePreviews: Story = () => { id: 'preview2', }, ], - history: [{ id: 'right' }], + history: [{ lastOpen: Date.now(), panel: { id: 'right' } }], }, }, }, @@ -234,7 +234,7 @@ export const CollapsedPushMode: Story = () => { }, left: undefined, preview: undefined, - history: [{ id: 'right' }], + history: [{ lastOpen: Date.now(), panel: { id: 'right' } }], }, }, }, @@ -263,7 +263,7 @@ export const ExpandedPushMode: Story = () => { id: 'left', }, preview: undefined, - history: [{ id: 'right' }], + history: [{ lastOpen: Date.now(), panel: { id: 'right' } }], }, }, }, @@ -292,7 +292,7 @@ export const DisableTypeSelection: Story = () => { id: 'left', }, preview: undefined, - history: [{ id: 'right' }], + history: [{ lastOpen: Date.now(), panel: { id: 'right' } }], }, }, }, @@ -323,7 +323,7 @@ export const ResetWidths: Story = () => { id: 'left', }, preview: undefined, - history: [{ id: 'right' }], + history: [{ lastOpen: Date.now(), panel: { id: 'right' } }], }, }, }, @@ -349,7 +349,7 @@ export const DisableResizeWidthSelection: Story = () => { id: 'left', }, preview: undefined, - history: [{ id: 'right' }], + history: [{ lastOpen: Date.now(), panel: { id: 'right' } }], }, }, }, diff --git a/x-pack/solutions/security/packages/expandable-flyout/src/index.test.tsx b/x-pack/solutions/security/packages/expandable-flyout/src/index.test.tsx index 4ac5d79462374..20854304450d4 100644 --- a/x-pack/solutions/security/packages/expandable-flyout/src/index.test.tsx +++ b/x-pack/solutions/security/packages/expandable-flyout/src/index.test.tsx @@ -49,7 +49,7 @@ describe('ExpandableFlyout', () => { }, left: undefined, preview: undefined, - history: [{ id: 'key' }], + history: [{ lastOpen: Date.now(), panel: { id: 'key' } }], }, }, }, diff --git a/x-pack/solutions/security/packages/expandable-flyout/src/provider.test.tsx b/x-pack/solutions/security/packages/expandable-flyout/src/provider.test.tsx index 9005a95003d96..79a5b6cdaa0a2 100644 --- a/x-pack/solutions/security/packages/expandable-flyout/src/provider.test.tsx +++ b/x-pack/solutions/security/packages/expandable-flyout/src/provider.test.tsx @@ -32,7 +32,7 @@ describe('UrlSynchronizer', () => { right: { id: 'key1' }, left: { id: 'key11' }, preview: undefined, - history: [{ id: 'key1' }], + history: [{ lastOpen: Date.now(), panel: { id: 'key1' } }], }, }, needsSync: true, @@ -92,7 +92,7 @@ describe('UrlSynchronizer', () => { right: { id: 'key1' }, left: { id: 'key2' }, preview: undefined, - history: [{ id: 'key1' }], + history: [{ lastOpen: Date.now(), panel: { id: 'key1' } }], }, }, needsSync: true, diff --git a/x-pack/solutions/security/packages/expandable-flyout/src/store/reducers.test.ts b/x-pack/solutions/security/packages/expandable-flyout/src/store/reducers.test.ts index 2ce8e4e210456..b3cc9f899c545 100644 --- a/x-pack/solutions/security/packages/expandable-flyout/src/store/reducers.test.ts +++ b/x-pack/solutions/security/packages/expandable-flyout/src/store/reducers.test.ts @@ -53,6 +53,7 @@ const previewPanel2: FlyoutPanelProps = { id: 'preview2', params: { id: 'id' }, }; +const rightPanel1LastOpen = Date.now(); describe('panelsReducer', () => { describe('should handle openFlyout action', () => { @@ -72,7 +73,7 @@ describe('panelsReducer', () => { left: leftPanel1, right: rightPanel1, preview: [previewPanel1], - history: [rightPanel1], + history: [{ lastOpen: expect.any(Number), panel: rightPanel1 }], }, }, needsSync: true, @@ -86,7 +87,7 @@ describe('panelsReducer', () => { left: leftPanel1, right: rightPanel1, preview: [previewPanel1, { id: 'preview' }], - history: [rightPanel1], + history: [{ lastOpen: rightPanel1LastOpen, panel: rightPanel1 }], }, }, }; @@ -104,7 +105,13 @@ describe('panelsReducer', () => { left: leftPanel2, right: rightPanel2, preview: [previewPanel2], - history: [rightPanel1, rightPanel2], + history: [ + { lastOpen: rightPanel1LastOpen, panel: rightPanel1 }, + { + lastOpen: expect.any(Number), + panel: rightPanel2, + }, + ], }, }, needsSync: true, @@ -118,7 +125,7 @@ describe('panelsReducer', () => { left: leftPanel1, right: rightPanel1, preview: [previewPanel1], - history: [rightPanel1], + history: [{ lastOpen: rightPanel1LastOpen, panel: rightPanel1 }], }, }, }; @@ -134,7 +141,13 @@ describe('panelsReducer', () => { left: undefined, right: rightPanel2, preview: undefined, - history: [rightPanel1, rightPanel2], + history: [ + { lastOpen: rightPanel1LastOpen, panel: rightPanel1 }, + { + lastOpen: expect.any(Number), + panel: rightPanel2, + }, + ], }, }, needsSync: true, @@ -148,7 +161,7 @@ describe('panelsReducer', () => { left: leftPanel1, right: rightPanel1, preview: [previewPanel1], - history: [rightPanel1], + history: [{ lastOpen: rightPanel1LastOpen, panel: rightPanel1 }], }, }, }; @@ -164,13 +177,18 @@ describe('panelsReducer', () => { left: leftPanel1, right: rightPanel1, preview: [previewPanel1], - history: [rightPanel1], + history: [{ lastOpen: rightPanel1LastOpen, panel: rightPanel1 }], }, [id2]: { left: undefined, right: rightPanel2, preview: undefined, - history: [rightPanel2], + history: [ + { + lastOpen: expect.any(Number), + panel: rightPanel2, + }, + ], }, }, needsSync: true, @@ -204,7 +222,7 @@ describe('panelsReducer', () => { left: leftPanel1, right: rightPanel1, preview: [previewPanel1], - history: [rightPanel1], + history: [{ lastOpen: rightPanel1LastOpen, panel: rightPanel1 }], }, }, }; @@ -217,7 +235,7 @@ describe('panelsReducer', () => { left: leftPanel1, right: rightPanel2, preview: [previewPanel1], - history: [rightPanel1], + history: [{ lastOpen: rightPanel1LastOpen, panel: rightPanel1 }], }, }, needsSync: true, @@ -231,7 +249,7 @@ describe('panelsReducer', () => { left: leftPanel1, right: rightPanel1, preview: [previewPanel1], - history: [rightPanel1], + history: [{ lastOpen: rightPanel1LastOpen, panel: rightPanel1 }], }, }, }; @@ -244,7 +262,7 @@ describe('panelsReducer', () => { left: leftPanel1, right: rightPanel1, preview: [previewPanel1], - history: [rightPanel1], + history: [{ lastOpen: rightPanel1LastOpen, panel: rightPanel1 }], }, [id2]: { left: undefined, @@ -457,7 +475,7 @@ describe('panelsReducer', () => { left: leftPanel1, right: rightPanel1, preview: [previewPanel1], - history: [rightPanel1], + history: [{ lastOpen: rightPanel1LastOpen, panel: rightPanel1 }], }, }, }; @@ -471,7 +489,7 @@ describe('panelsReducer', () => { left: leftPanel1, right: undefined, preview: [previewPanel1], - history: [rightPanel1], + history: [{ lastOpen: rightPanel1LastOpen, panel: rightPanel1 }], }, }, needsSync: true, @@ -485,7 +503,7 @@ describe('panelsReducer', () => { left: leftPanel1, right: rightPanel1, preview: [previewPanel1], - history: [rightPanel1], + history: [{ lastOpen: rightPanel1LastOpen, panel: rightPanel1 }], }, }, }; @@ -499,7 +517,7 @@ describe('panelsReducer', () => { left: leftPanel1, right: rightPanel1, preview: [previewPanel1], - history: [rightPanel1], + history: [{ lastOpen: rightPanel1LastOpen, panel: rightPanel1 }], }, }, needsSync: true, @@ -526,7 +544,7 @@ describe('panelsReducer', () => { left: undefined, right: rightPanel1, preview: [previewPanel1], - history: [rightPanel1], + history: [{ lastOpen: rightPanel1LastOpen, panel: rightPanel1 }], }, }, }; @@ -546,7 +564,7 @@ describe('panelsReducer', () => { left: leftPanel1, right: rightPanel1, preview: [previewPanel1], - history: [rightPanel1], + history: [{ lastOpen: rightPanel1LastOpen, panel: rightPanel1 }], }, }, }; @@ -559,7 +577,7 @@ describe('panelsReducer', () => { left: undefined, right: rightPanel1, preview: [previewPanel1], - history: [rightPanel1], + history: [{ lastOpen: rightPanel1LastOpen, panel: rightPanel1 }], }, }, needsSync: true, @@ -573,7 +591,7 @@ describe('panelsReducer', () => { left: leftPanel1, right: rightPanel1, preview: [previewPanel1], - history: [rightPanel1], + history: [{ lastOpen: rightPanel1LastOpen, panel: rightPanel1 }], }, }, }; @@ -586,7 +604,7 @@ describe('panelsReducer', () => { left: leftPanel1, right: rightPanel1, preview: [previewPanel1], - history: [rightPanel1], + history: [{ lastOpen: rightPanel1LastOpen, panel: rightPanel1 }], }, }, needsSync: true, @@ -613,7 +631,7 @@ describe('panelsReducer', () => { left: leftPanel1, right: rightPanel1, preview: undefined, - history: [rightPanel1], + history: [{ lastOpen: rightPanel1LastOpen, panel: rightPanel1 }], }, }, }; @@ -633,7 +651,7 @@ describe('panelsReducer', () => { left: rightPanel1, right: leftPanel1, preview: [previewPanel1, previewPanel2], - history: [rightPanel1], + history: [{ lastOpen: rightPanel1LastOpen, panel: rightPanel1 }], }, }, }; @@ -646,7 +664,7 @@ describe('panelsReducer', () => { left: rightPanel1, right: leftPanel1, preview: undefined, - history: [rightPanel1], + history: [{ lastOpen: rightPanel1LastOpen, panel: rightPanel1 }], }, }, needsSync: true, @@ -660,7 +678,7 @@ describe('panelsReducer', () => { left: leftPanel1, right: rightPanel1, preview: [previewPanel1], - history: [rightPanel1], + history: [{ lastOpen: rightPanel1LastOpen, panel: rightPanel1 }], }, }, }; @@ -673,7 +691,7 @@ describe('panelsReducer', () => { left: leftPanel1, right: rightPanel1, preview: [previewPanel1], - history: [rightPanel1], + history: [{ lastOpen: rightPanel1LastOpen, panel: rightPanel1 }], }, }, needsSync: true, @@ -700,7 +718,7 @@ describe('panelsReducer', () => { left: leftPanel1, right: rightPanel1, preview: undefined, - history: [rightPanel1], + history: [{ lastOpen: rightPanel1LastOpen, panel: rightPanel1 }], }, }, }; @@ -720,7 +738,7 @@ describe('panelsReducer', () => { left: leftPanel1, right: rightPanel1, preview: [previewPanel1, previewPanel2], - history: [rightPanel1], + history: [{ lastOpen: rightPanel1LastOpen, panel: rightPanel1 }], }, }, }; @@ -733,7 +751,7 @@ describe('panelsReducer', () => { left: leftPanel1, right: rightPanel1, preview: [previewPanel1], - history: [rightPanel1], + history: [{ lastOpen: rightPanel1LastOpen, panel: rightPanel1 }], }, }, needsSync: false, @@ -747,7 +765,7 @@ describe('panelsReducer', () => { left: leftPanel1, right: rightPanel1, preview: [previewPanel1], - history: [rightPanel1], + history: [{ lastOpen: rightPanel1LastOpen, panel: rightPanel1 }], }, }, }; @@ -760,7 +778,7 @@ describe('panelsReducer', () => { left: leftPanel1, right: rightPanel1, preview: [previewPanel1], - history: [rightPanel1], + history: [{ lastOpen: rightPanel1LastOpen, panel: rightPanel1 }], }, }, needsSync: false, @@ -787,7 +805,7 @@ describe('panelsReducer', () => { left: leftPanel1, right: rightPanel1, preview: [previewPanel1], - history: [rightPanel1], + history: [{ lastOpen: rightPanel1LastOpen, panel: rightPanel1 }], }, }, }; @@ -800,7 +818,7 @@ describe('panelsReducer', () => { left: undefined, right: undefined, preview: undefined, - history: [rightPanel1], + history: [{ lastOpen: rightPanel1LastOpen, panel: rightPanel1 }], }, }, needsSync: true, @@ -814,7 +832,7 @@ describe('panelsReducer', () => { left: leftPanel1, right: rightPanel1, preview: [previewPanel1], - history: [rightPanel1], + history: [{ lastOpen: rightPanel1LastOpen, panel: rightPanel1 }], }, }, }; @@ -827,7 +845,7 @@ describe('panelsReducer', () => { left: leftPanel1, right: rightPanel1, preview: [previewPanel1], - history: [rightPanel1], + history: [{ lastOpen: rightPanel1LastOpen, panel: rightPanel1 }], }, }, needsSync: true, diff --git a/x-pack/solutions/security/packages/expandable-flyout/src/store/reducers.ts b/x-pack/solutions/security/packages/expandable-flyout/src/store/reducers.ts index 5e1023d9afe21..a3febf9c9eb95 100644 --- a/x-pack/solutions/security/packages/expandable-flyout/src/store/reducers.ts +++ b/x-pack/solutions/security/packages/expandable-flyout/src/store/reducers.ts @@ -34,14 +34,24 @@ export const panelsReducer = createReducer(initialPanelsState, (builder) => { state.byId[id].left = left; state.byId[id].preview = preview ? [preview] : undefined; if (right) { - state.byId[id].history?.push(right); + state.byId[id].history?.push({ + lastOpen: Date.now(), + panel: right, + }); } } else { state.byId[id] = { left, right, preview: preview ? [preview] : undefined, - history: right ? [right] : [], + history: right + ? [ + { + lastOpen: Date.now(), + panel: right, + }, + ] + : [], }; } @@ -154,7 +164,14 @@ export const panelsReducer = createReducer(initialPanelsState, (builder) => { right, left, preview: preview ? [preview] : undefined, - history: right ? [right] : [], // update history only when loading flyout on refresh + history: right + ? [ + { + lastOpen: Date.now(), + panel: right, + }, + ] + : [], // update history only when loading flyout on refresh }; } diff --git a/x-pack/solutions/security/packages/expandable-flyout/src/store/state.ts b/x-pack/solutions/security/packages/expandable-flyout/src/store/state.ts index 9fd476cad24dc..36b58687ad550 100644 --- a/x-pack/solutions/security/packages/expandable-flyout/src/store/state.ts +++ b/x-pack/solutions/security/packages/expandable-flyout/src/store/state.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { FlyoutPanelHistory } from '../types'; import { FlyoutPanelProps } from '../..'; export interface FlyoutPanels { @@ -20,10 +21,10 @@ export interface FlyoutPanels { * Panels to render in the preview section */ preview: FlyoutPanelProps[] | undefined; - /* + /** * History of the right panels that were opened */ - history: FlyoutPanelProps[]; + history: FlyoutPanelHistory[]; } export interface PanelsState { diff --git a/x-pack/solutions/security/packages/expandable-flyout/src/types.ts b/x-pack/solutions/security/packages/expandable-flyout/src/types.ts index 024dd050ad5b1..999504b1eb205 100644 --- a/x-pack/solutions/security/packages/expandable-flyout/src/types.ts +++ b/x-pack/solutions/security/packages/expandable-flyout/src/types.ts @@ -86,3 +86,14 @@ export interface Panel { */ component: (props: FlyoutPanelProps) => React.ReactElement; } + +export interface FlyoutPanelHistory { + /** + * Time at which the flyout was last opened + */ + lastOpen: number; + /** + * Panel that was opened + */ + panel: FlyoutPanelProps; +} diff --git a/x-pack/solutions/security/plugins/security_solution/public/flyout/entity_details/host_right/index.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/flyout/entity_details/host_right/index.test.tsx index feff2cb54fa35..7bd53873bdff1 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/flyout/entity_details/host_right/index.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/flyout/entity_details/host_right/index.test.tsx @@ -12,7 +12,7 @@ import { mockHostRiskScoreState, mockObservedHostData } from '../mocks'; import type { ExpandableFlyoutApi, ExpandableFlyoutState, - FlyoutPanelProps, + FlyoutPanelHistory, } from '@kbn/expandable-flyout'; import { useExpandableFlyoutApi, @@ -46,7 +46,9 @@ const flyoutContextValue = { closeLeftPanel: jest.fn(), } as unknown as ExpandableFlyoutApi; -const flyoutHistory = [{ id: 'id1', params: {} }] as unknown as FlyoutPanelProps[]; +const flyoutHistory: FlyoutPanelHistory[] = [ + { lastOpen: Date.now(), panel: { id: 'id1', params: {} } }, +]; jest.mock('@kbn/expandable-flyout', () => ({ useExpandableFlyoutApi: jest.fn(), useExpandableFlyoutHistory: jest.fn(), diff --git a/x-pack/solutions/security/plugins/security_solution/public/flyout/entity_details/service_right/index.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/flyout/entity_details/service_right/index.test.tsx index f39a736d50328..894e6ebe150bf 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/flyout/entity_details/service_right/index.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/flyout/entity_details/service_right/index.test.tsx @@ -13,7 +13,7 @@ import { ServicePanel } from '.'; import type { ExpandableFlyoutApi, ExpandableFlyoutState, - FlyoutPanelProps, + FlyoutPanelHistory, } from '@kbn/expandable-flyout'; import { useExpandableFlyoutApi, @@ -51,7 +51,9 @@ const flyoutContextValue = { closeLeftPanel: jest.fn(), } as unknown as ExpandableFlyoutApi; -const flyoutHistory = [{ id: 'id1', params: {} }] as unknown as FlyoutPanelProps[]; +const flyoutHistory: FlyoutPanelHistory[] = [ + { lastOpen: Date.now(), panel: { id: 'id1', params: {} } }, +]; jest.mock('@kbn/expandable-flyout', () => ({ useExpandableFlyoutApi: jest.fn(), useExpandableFlyoutHistory: jest.fn(), diff --git a/x-pack/solutions/security/plugins/security_solution/public/flyout/entity_details/user_right/index.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/flyout/entity_details/user_right/index.test.tsx index 737f28364d2c0..da777442762f2 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/flyout/entity_details/user_right/index.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/flyout/entity_details/user_right/index.test.tsx @@ -13,7 +13,7 @@ import { UserPanel } from '.'; import type { ExpandableFlyoutApi, ExpandableFlyoutState, - FlyoutPanelProps, + FlyoutPanelHistory, } from '@kbn/expandable-flyout'; import { useExpandableFlyoutApi, @@ -56,7 +56,9 @@ const flyoutContextValue = { closeLeftPanel: jest.fn(), } as unknown as ExpandableFlyoutApi; -const flyoutHistory = [{ id: 'id1', params: {} }] as unknown as FlyoutPanelProps[]; +const flyoutHistory: FlyoutPanelHistory[] = [ + { lastOpen: Date.now(), panel: { id: 'id1', params: {} } }, +]; jest.mock('@kbn/expandable-flyout', () => ({ useExpandableFlyoutApi: jest.fn(), useExpandableFlyoutHistory: jest.fn(), diff --git a/x-pack/solutions/security/plugins/security_solution/public/flyout/rule_details/right/index.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/flyout/rule_details/right/index.test.tsx index c1a629f881710..e3b429a59cf1c 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/flyout/rule_details/right/index.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/flyout/rule_details/right/index.test.tsx @@ -23,14 +23,14 @@ 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, + ExpandableFlyoutState, + FlyoutPanelHistory, } from '@kbn/expandable-flyout'; import { useExpandableFlyoutApi, - useExpandableFlyoutState, useExpandableFlyoutHistory, + useExpandableFlyoutState, } from '@kbn/expandable-flyout'; jest.mock('../../document_details/shared/hooks/use_rule_details_link'); @@ -51,7 +51,9 @@ const flyoutContextValue = { closeLeftPanel: jest.fn(), } as unknown as ExpandableFlyoutApi; -const flyoutHistory = [{ id: 'id1', params: {} }] as unknown as FlyoutPanelProps[]; +const flyoutHistory: FlyoutPanelHistory[] = [ + { lastOpen: Date.now(), panel: { id: 'id1', params: {} } }, +]; const mockTheme = getMockTheme({ eui: { euiColorMediumShade: '#ece' } }); const rule = { name: 'rule name', description: 'rule description' } as RuleResponse; diff --git a/x-pack/solutions/security/plugins/security_solution/public/flyout/shared/components/flyout_history.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/flyout/shared/components/flyout_history.test.tsx index f922190da3e73..7f7a009fa5fee 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/flyout/shared/components/flyout_history.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/flyout/shared/components/flyout_history.test.tsx @@ -6,17 +6,21 @@ */ import React from 'react'; -import { render, fireEvent } from '@testing-library/react'; +import { fireEvent, render } from '@testing-library/react'; +import type { FlyoutPanelHistory } from '@kbn/expandable-flyout'; import { TestProviders } from '../../../common/mock'; import { - FLYOUT_HISTORY_TEST_ID, FLYOUT_HISTORY_BUTTON_TEST_ID, FLYOUT_HISTORY_CONTEXT_PANEL_TEST_ID, + FLYOUT_HISTORY_TEST_ID, NO_DATA_HISTORY_ROW_TEST_ID, } from './test_ids'; import { FlyoutHistory } from './flyout_history'; -const mockedHistory = [{ id: '1' }, { id: '2' }]; +const mockedHistory: FlyoutPanelHistory[] = [ + { lastOpen: Date.now(), panel: { id: '1' } }, + { lastOpen: Date.now(), panel: { id: '2' } }, +]; describe('FlyoutHistory', () => { it('renders', () => { diff --git a/x-pack/solutions/security/plugins/security_solution/public/flyout/shared/components/flyout_history.tsx b/x-pack/solutions/security/plugins/security_solution/public/flyout/shared/components/flyout_history.tsx index 933106e28ed10..bd6063b85c0e1 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/flyout/shared/components/flyout_history.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/flyout/shared/components/flyout_history.tsx @@ -8,21 +8,21 @@ import type { FC } from 'react'; import React, { memo, useMemo, useState } from 'react'; import { - EuiFlexItem, EuiButtonEmpty, - EuiPopover, + EuiContextMenuItem, EuiContextMenuPanel, + EuiFlexItem, + EuiPopover, EuiText, - EuiContextMenuItem, EuiTextColor, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; -import type { FlyoutPanelProps } from '@kbn/expandable-flyout'; +import type { FlyoutPanelHistory } 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, + FLYOUT_HISTORY_TEST_ID, NO_DATA_HISTORY_ROW_TEST_ID, } from './test_ids'; @@ -30,7 +30,7 @@ export interface HistoryProps { /** * A list of flyouts that have been opened */ - history: FlyoutPanelProps[]; + history: FlyoutPanelHistory[]; } /** diff --git a/x-pack/solutions/security/plugins/security_solution/public/flyout/shared/components/flyout_history_row.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/flyout/shared/components/flyout_history_row.test.tsx index 9579d5ff0d161..471cbae65427b 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/flyout/shared/components/flyout_history_row.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/flyout/shared/components/flyout_history_row.test.tsx @@ -6,16 +6,20 @@ */ import React from 'react'; -import { render, fireEvent } from '@testing-library/react'; +import { fireEvent, render } from '@testing-library/react'; import { - FlyoutHistoryRow, - RuleHistoryRow, DocumentDetailsHistoryRow, + FlyoutHistoryRow, GenericHistoryRow, + RuleHistoryRow, } 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 { + type ExpandableFlyoutApi, + type FlyoutPanelHistory, + useExpandableFlyoutApi, +} 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'; @@ -23,11 +27,11 @@ import { RulePanelKey } from '../../rule_details/right'; import { NetworkPanelKey } from '../../network_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, - GENERIC_HISTORY_ROW_TEST_ID, + RULE_HISTORY_ROW_TEST_ID, + USER_HISTORY_ROW_TEST_ID, } from './test_ids'; import { HostPanelKey, UserPanelKey } from '../../entity_details/shared/constants'; @@ -46,30 +50,51 @@ const flyoutContextValue = { openFlyout: jest.fn(), } as unknown as ExpandableFlyoutApi; -const rowItems = { +const rowItems: { [id: string]: FlyoutPanelHistory } = { alert: { - id: DocumentDetailsRightPanelKey, - params: { - id: 'eventId', - indexName: 'indexName', - scopeId: 'scopeId', + lastOpen: Date.now(), + panel: { + id: DocumentDetailsRightPanelKey, + params: { + id: 'eventId', + indexName: 'indexName', + scopeId: 'scopeId', + }, }, }, rule: { - id: RulePanelKey, - params: { ruleId: 'ruleId' }, + lastOpen: Date.now(), + panel: { + id: RulePanelKey, + params: { ruleId: 'ruleId' }, + }, }, host: { - id: HostPanelKey, - params: { hostName: 'host name' }, + lastOpen: Date.now(), + panel: { + id: HostPanelKey, + params: { hostName: 'host name' }, + }, }, user: { - id: UserPanelKey, - params: { userName: 'user name' }, + lastOpen: Date.now(), + panel: { + id: UserPanelKey, + params: { userName: 'user name' }, + }, }, network: { - id: NetworkPanelKey, - params: { ip: 'ip' }, + lastOpen: Date.now(), + panel: { + id: NetworkPanelKey, + params: { ip: 'ip' }, + }, + }, + unsupported: { + lastOpen: Date.now(), + panel: { + id: 'key', + }, }, }; @@ -91,7 +116,7 @@ describe('FlyoutHistoryRow', () => { (useBasicDataFromDetailsData as jest.Mock).mockReturnValue({ isAlert: false }); }); - it('renders document details history row when key is alert', () => { + it('should render document details history row when key is alert', () => { (useBasicDataFromDetailsData as jest.Mock).mockReturnValue({ isAlert: true, ruleName: 'rule name', @@ -105,7 +130,7 @@ describe('FlyoutHistoryRow', () => { expect(getByTestId(`${0}-${DOCUMENT_DETAILS_HISTORY_ROW_TEST_ID}`)).toBeInTheDocument(); }); - it('renders rule history row when key is rule', () => { + it('should render rule history row when key is rule', () => { const { getByTestId } = render( @@ -114,7 +139,7 @@ describe('FlyoutHistoryRow', () => { expect(getByTestId(`${1}-${RULE_HISTORY_ROW_TEST_ID}`)).toBeInTheDocument(); }); - it('renders generic host history row when key is host', () => { + it('should render generic host history row when key is host', () => { const { getByTestId } = render( @@ -124,7 +149,7 @@ describe('FlyoutHistoryRow', () => { expect(getByTestId(`${2}-${HOST_HISTORY_ROW_TEST_ID}`)).toHaveTextContent('Host: host name'); }); - it('renders generic user history row when key is user', () => { + it('should render generic user history row when key is user', () => { const { getByTestId } = render( @@ -134,7 +159,7 @@ describe('FlyoutHistoryRow', () => { expect(getByTestId(`${3}-${USER_HISTORY_ROW_TEST_ID}`)).toHaveTextContent('User: user name'); }); - it('renders generic network history row when key is network', () => { + it('should render generic network history row when key is network', () => { const { getByTestId } = render( @@ -144,10 +169,10 @@ describe('FlyoutHistoryRow', () => { expect(getByTestId(`${4}-${NETWORK_HISTORY_ROW_TEST_ID}`)).toHaveTextContent('Network: ip'); }); - it('renders null when key is not supported', () => { + it('should render null when key is not supported', () => { const { container } = render( - + ); expect(container).toBeEmptyDOMElement(); @@ -159,7 +184,7 @@ describe('DocumentDetailsHistoryRow', () => { jest.mocked(useExpandableFlyoutApi).mockReturnValue(flyoutContextValue); }); - it('renders alert title when isAlert is true and rule name is defined', () => { + it('should render alert title when isAlert is true and rule name is defined', () => { (useBasicDataFromDetailsData as jest.Mock).mockReturnValue({ isAlert: true, ruleName: 'rule name', @@ -175,7 +200,7 @@ describe('DocumentDetailsHistoryRow', () => { ); }); - it('renders default alert title when isAlert is true and rule name is undefined', () => { + it('should render default alert title when isAlert is true and rule name is undefined', () => { (useBasicDataFromDetailsData as jest.Mock).mockReturnValue({ isAlert: true }); const { getByTestId } = render( @@ -188,7 +213,7 @@ describe('DocumentDetailsHistoryRow', () => { ); }); - it('renders event title when isAlert is false', () => { + it('should render event title when isAlert is false', () => { (useBasicDataFromDetailsData as jest.Mock).mockReturnValue({ isAlert: false }); const { getByTestId } = render( @@ -201,7 +226,7 @@ describe('DocumentDetailsHistoryRow', () => { ); }); - it('opens document details flyout when clicked', () => { + it('should open document details flyout when clicked', () => { (useBasicDataFromDetailsData as jest.Mock).mockReturnValue({ isAlert: true }); const { getByTestId } = render( @@ -210,7 +235,7 @@ describe('DocumentDetailsHistoryRow', () => { ); fireEvent.click(getByTestId(`${0}-${DOCUMENT_DETAILS_HISTORY_ROW_TEST_ID}`)); - expect(flyoutContextValue.openFlyout).toHaveBeenCalledWith({ right: rowItems.alert }); + expect(flyoutContextValue.openFlyout).toHaveBeenCalledWith({ right: rowItems.alert.panel }); }); }); @@ -224,24 +249,24 @@ describe('RuleHistoryRow', () => { }); }); - it('renders', () => { + it('should render the rule row component', () => { const { getByTestId } = render( ); expect(getByTestId(`${0}-${RULE_HISTORY_ROW_TEST_ID}`)).toHaveTextContent('Rule: rule name'); - expect(useRuleDetails).toHaveBeenCalledWith({ ruleId: rowItems.rule.params.ruleId }); + expect(useRuleDetails).toHaveBeenCalledWith({ ruleId: rowItems.rule.panel.params?.ruleId }); }); - it('opens rule details flyout when clicked', () => { + it('should open rule details flyout when clicked', () => { const { getByTestId } = render( ); fireEvent.click(getByTestId(`${0}-${RULE_HISTORY_ROW_TEST_ID}`)); - expect(flyoutContextValue.openFlyout).toHaveBeenCalledWith({ right: rowItems.rule }); + expect(flyoutContextValue.openFlyout).toHaveBeenCalledWith({ right: rowItems.rule.panel }); }); }); @@ -250,7 +275,7 @@ describe('GenericHistoryRow', () => { jest.mocked(useExpandableFlyoutApi).mockReturnValue(flyoutContextValue); }); - it('renders', () => { + it('should render the generic row component', () => { const { getByTestId } = render( { ); expect(getByTestId(`${0}-${GENERIC_HISTORY_ROW_TEST_ID}`)).toHaveTextContent('Row name: title'); fireEvent.click(getByTestId(`${0}-${GENERIC_HISTORY_ROW_TEST_ID}`)); - expect(flyoutContextValue.openFlyout).toHaveBeenCalledWith({ right: rowItems.host }); + }); + + it('should open the flyout when clicked', () => { + const { getByTestId } = render( + + + + ); + fireEvent.click(getByTestId(`${0}-${GENERIC_HISTORY_ROW_TEST_ID}`)); + expect(flyoutContextValue.openFlyout).toHaveBeenCalledWith({ right: rowItems.host.panel }); }); }); diff --git a/x-pack/solutions/security/plugins/security_solution/public/flyout/shared/components/flyout_history_row.tsx b/x-pack/solutions/security/plugins/security_solution/public/flyout/shared/components/flyout_history_row.tsx index 3569d3d8d9ae6..7334fcf1950d0 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/flyout/shared/components/flyout_history_row.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/flyout/shared/components/flyout_history_row.tsx @@ -6,24 +6,32 @@ */ import type { FC } from 'react'; -import React, { memo, useMemo, useCallback } from 'react'; -import { EuiContextMenuItem, type EuiIconProps } from '@elastic/eui'; +import React, { memo, useCallback, useMemo } from 'react'; +import { css } from '@emotion/react'; +import { + EuiContextMenuItem, + EuiFlexGroup, + EuiFlexItem, + type EuiIconProps, + useEuiTheme, +} from '@elastic/eui'; +import type { FlyoutPanelHistory } from '@kbn/expandable-flyout'; import { useExpandableFlyoutApi } from '@kbn/expandable-flyout'; -import type { FlyoutPanelProps } from '@kbn/expandable-flyout'; +import { FormattedRelativePreferenceDate } from '../../../common/components/formatted_date'; 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 { getAlertTitle, getEventTitle, getField } from '../../document_details/shared/utils'; import { RulePanelKey } from '../../rule_details/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, + RULE_HISTORY_ROW_TEST_ID, + USER_HISTORY_ROW_TEST_ID, } from './test_ids'; import { HostPanelKey, UserPanelKey } from '../../entity_details/shared/constants'; @@ -31,7 +39,7 @@ export interface FlyoutHistoryRowProps { /** * Flyout item to display */ - item: FlyoutPanelProps; + item: FlyoutPanelHistory; /** * Index of the flyout in the list */ @@ -42,7 +50,7 @@ export interface FlyoutHistoryRowProps { * Row item for a flyout history row */ export const FlyoutHistoryRow: FC = memo(({ item, index }) => { - switch (item.id) { + switch (item.panel.id) { case DocumentDetailsRightPanelKey: return ; case RulePanelKey: @@ -52,7 +60,7 @@ export const FlyoutHistoryRow: FC = memo(({ item, index } = memo(({ item, index } = memo(({ item, index } = memo(({ item, index } */ export const DocumentDetailsHistoryRow: FC = memo(({ item, index }) => { const { dataFormattedForFieldBrowser, getFieldsData } = useEventDetails({ - eventId: String(item?.params?.id), - indexName: String(item?.params?.indexName), + eventId: String(item?.panel?.params?.id), + indexName: String(item?.panel?.params?.indexName), }); const { ruleName, isAlert } = useBasicDataFromDetailsData(dataFormattedForFieldBrowser); const eventKind = useMemo(() => getField(getFieldsData('event.kind')), [getFieldsData]); @@ -121,7 +129,7 @@ export const DocumentDetailsHistoryRow: FC = memo(({ item * Row item for a rule details flyout */ export const RuleHistoryRow: FC = memo(({ item, index }) => { - const ruleId = String(item?.params?.ruleId); + const ruleId = String(item?.panel?.params?.ruleId); const { rule } = useRuleDetails({ ruleId }); return ( @@ -137,6 +145,10 @@ export const RuleHistoryRow: FC = memo(({ item, index }) }); interface GenericHistoryRowProps extends FlyoutHistoryRowProps { + /** + * Index to put in the html key attribute + */ + index: number; /** * Icon to display */ @@ -160,20 +172,44 @@ interface GenericHistoryRowProps extends FlyoutHistoryRowProps { */ export const GenericHistoryRow: FC = memo( ({ item, index, title, icon, name, dataTestSubj }) => { + const { euiTheme } = useEuiTheme(); const { openFlyout } = useExpandableFlyoutApi(); const onClick = useCallback(() => { - openFlyout({ right: item }); - }, [openFlyout, item]); + openFlyout({ right: item.panel }); + }, [openFlyout, item.panel]); return ( - {`${name}: `} - {title} + + + {`${name}:`} +   + {title} + + + + + ); } diff --git a/x-pack/solutions/security/plugins/security_solution/public/flyout/shared/components/flyout_navigation.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/flyout/shared/components/flyout_navigation.test.tsx index 372b11bcc9ef4..53a31c734e379 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/flyout/shared/components/flyout_navigation.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/flyout/shared/components/flyout_navigation.test.tsx @@ -16,14 +16,14 @@ import { FLYOUT_HISTORY_BUTTON_TEST_ID, HEADER_ACTIONS_TEST_ID, } from './test_ids'; -import type { ExpandableFlyoutState, FlyoutPanelProps } from '@kbn/expandable-flyout'; -import { useIsExperimentalFeatureEnabled } from '../../../common/hooks/use_experimental_features'; +import type { ExpandableFlyoutState, FlyoutPanelHistory } from '@kbn/expandable-flyout'; import { - useExpandableFlyoutApi, type ExpandableFlyoutApi, - useExpandableFlyoutState, + useExpandableFlyoutApi, useExpandableFlyoutHistory, + useExpandableFlyoutState, } from '@kbn/expandable-flyout'; +import { useIsExperimentalFeatureEnabled } from '../../../common/hooks/use_experimental_features'; const expandDetails = jest.fn(); @@ -132,10 +132,10 @@ describe('', () => { expect(container).toBeEmptyDOMElement(); }); - const flyoutHistory = [ - { id: 'id1', params: {} }, - { id: 'id2', params: {} }, - ] as unknown as FlyoutPanelProps[]; + const flyoutHistory: FlyoutPanelHistory[] = [ + { lastOpen: Date.now(), panel: { id: 'id1', params: {} } }, + { lastOpen: Date.now(), panel: { id: 'id2', params: {} } }, + ]; describe('when flyout history is enabled', () => { beforeEach(() => { diff --git a/x-pack/solutions/security/plugins/security_solution/public/flyout/shared/utils/history_utils.test.ts b/x-pack/solutions/security/plugins/security_solution/public/flyout/shared/utils/history_utils.test.ts index 97257fa84dd8a..cfeaab927dcdc 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/flyout/shared/utils/history_utils.test.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/flyout/shared/utils/history_utils.test.ts @@ -5,51 +5,76 @@ * 2.0. */ +import type { FlyoutPanelHistory } from '@kbn/expandable-flyout'; import { getProcessedHistory } from './history_utils'; +const date1 = Date.now(); // oldest +const date2 = date1 + 1; +const date3 = date2 + 1; +const date4 = date3 + 1; +const date5 = date4 + 1; +const date6 = date5 + 1; // newest + 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' }, + const singleEntryHistory: FlyoutPanelHistory[] = [{ lastOpen: date1, panel: { id: '1' } }]; + const simpleHistory: FlyoutPanelHistory[] = [ + { lastOpen: date1, panel: { id: '1' } }, + { lastOpen: date2, panel: { id: '2' } }, + { lastOpen: date3, panel: { id: '3' } }, + { lastOpen: date4, panel: { id: '4' } }, + ]; + const complexHistory: FlyoutPanelHistory[] = [ + { lastOpen: date1, panel: { id: '1' } }, + { lastOpen: date2, panel: { id: '2' } }, + { lastOpen: date3, panel: { id: '1' } }, + { lastOpen: date4, panel: { id: '3' } }, + { lastOpen: date5, panel: { id: '4' } }, + { lastOpen: date6, panel: { id: '2' } }, ]; - it('returns a reversed history array and removes latest entry', () => { + it('should return a reversed history array and remove newest entry', () => { // input: 1, 2, 3, 4 // reverse: 4, 3, 2, 1 - // remove latest: 4, 3, 2 + // remove newest: 3, 2, 1 const processedHistory = getProcessedHistory({ history: simpleHistory, maxCount: 5 }); - expect(processedHistory).toEqual([{ id: '3' }, { id: '2' }, { id: '1' }]); + expect(processedHistory).toEqual([ + { lastOpen: date3, panel: { id: '3' } }, + { lastOpen: date2, panel: { id: '2' } }, + { lastOpen: date1, panel: { id: '1' } }, + ]); }); - it('returns processed history with the maxCount', () => { + it('should return only the amount of entries requested', () => { // input: 1, 2, 3, 4 // reverse: 4, 3, 2, 1 - // remove latest: 3, 2, 1 + // remove newest: 3, 2, 1 // keep maxCount: 3, 2 const processedHistory = getProcessedHistory({ history: simpleHistory, maxCount: 2 }); - expect(processedHistory).toEqual([{ id: '3' }, { id: '2' }]); + expect(processedHistory).toEqual([ + { lastOpen: date3, panel: { id: '3' } }, + { lastOpen: date2, panel: { id: '2' } }, + ]); }); - it('removes duplicates and reverses', () => { + it('should remove all duplicates', () => { // 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 + // remove newest: 4, 3, 1 const processedHistory = getProcessedHistory({ history: complexHistory, maxCount: 5 }); - expect(processedHistory).toEqual([{ id: '4' }, { id: '3' }, { id: '1' }]); + expect(processedHistory).toEqual([ + { lastOpen: date5, panel: { id: '4' } }, + { lastOpen: date4, panel: { id: '3' } }, + { lastOpen: date3, panel: { id: '1' } }, + ]); }); - it('returns empty array if history only has one entry', () => { - const processedHistory = getProcessedHistory({ history: [{ id: '1' }], maxCount: 5 }); + it('should return empty array if history only has one entry', () => { + const processedHistory = getProcessedHistory({ history: singleEntryHistory, maxCount: 5 }); expect(processedHistory).toEqual([]); }); - it('returns empty array if history is empty', () => { + it('should return empty array if history is empty', () => { const processedHistory = getProcessedHistory({ history: [], maxCount: 5 }); expect(processedHistory).toEqual([]); }); diff --git a/x-pack/solutions/security/plugins/security_solution/public/flyout/shared/utils/history_utils.ts b/x-pack/solutions/security/plugins/security_solution/public/flyout/shared/utils/history_utils.ts index ef31daa7f83f6..0e14ce8ce2c84 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/flyout/shared/utils/history_utils.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/flyout/shared/utils/history_utils.ts @@ -4,7 +4,8 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import type { FlyoutPanelProps } from '@kbn/expandable-flyout'; + +import type { FlyoutPanelHistory } from '@kbn/expandable-flyout'; /** * Helper function that reverses the history array, @@ -15,17 +16,28 @@ export const getProcessedHistory = ({ history, maxCount, }: { - history: FlyoutPanelProps[]; + history: FlyoutPanelHistory[]; maxCount: number; -}): FlyoutPanelProps[] => { +}): FlyoutPanelHistory[] => { // Step 1: reverse history so the most recent is first + // We need to do this step first because we want to make sure that during step 2 + // we are removing only older duplicates. 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) - ); + // Because the lastOpen value will always be different, we're manually removing duplicates + // by looking at the panel's information only. + const uniqueHistory: FlyoutPanelHistory[] = []; + reversedHistory.forEach((hist) => { + const entryDoesNotExists = + uniqueHistory + .map((h: FlyoutPanelHistory) => JSON.stringify(h.panel)) + .indexOf(JSON.stringify(hist.panel)) === -1; + if (entryDoesNotExists) { + uniqueHistory.push(hist); + } + }); - // Omit the first (current) entry and return array of maxCount length - return historyArray.slice(1, maxCount + 1); + // Omit the first (current opened) entry and return array of maxCount length + return uniqueHistory.slice(1, maxCount + 1); }; From 9b544e880c35ea587ebbae14cf4ced642ef7bfc7 Mon Sep 17 00:00:00 2001 From: PhilippeOberti Date: Thu, 13 Feb 2025 11:04:21 -0600 Subject: [PATCH 2/3] add separator and limit witdh for very long names --- .../flyout/shared/components/flyout_history.tsx | 8 +++++++- .../shared/components/flyout_history_row.tsx | 14 +++++++++++++- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/x-pack/solutions/security/plugins/security_solution/public/flyout/shared/components/flyout_history.tsx b/x-pack/solutions/security/plugins/security_solution/public/flyout/shared/components/flyout_history.tsx index bd6063b85c0e1..0da79178c1b81 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/flyout/shared/components/flyout_history.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/flyout/shared/components/flyout_history.tsx @@ -12,6 +12,7 @@ import { EuiContextMenuItem, EuiContextMenuPanel, EuiFlexItem, + EuiHorizontalRule, EuiPopover, EuiText, EuiTextColor, @@ -62,7 +63,12 @@ export const FlyoutHistory: FC = memo(({ history }) => { () => history.length > 0 ? history.map((item, index) => { - return ; + return ( + <> + + + + ); }) : [emptyHistoryMessage], [history, emptyHistoryMessage] diff --git a/x-pack/solutions/security/plugins/security_solution/public/flyout/shared/components/flyout_history_row.tsx b/x-pack/solutions/security/plugins/security_solution/public/flyout/shared/components/flyout_history_row.tsx index 7334fcf1950d0..dae808d313822 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/flyout/shared/components/flyout_history_row.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/flyout/shared/components/flyout_history_row.tsx @@ -35,6 +35,8 @@ import { } from './test_ids'; import { HostPanelKey, UserPanelKey } from '../../entity_details/shared/constants'; +const MAX_WIDTH = 300; // px + export interface FlyoutHistoryRowProps { /** * Flyout item to display @@ -185,6 +187,7 @@ export const GenericHistoryRow: FC = memo( icon={icon} css={css` align-items: flex-start; + padding: ${euiTheme.size.s} ${euiTheme.size.m}; `} data-test-subj={`${index}-${dataTestSubj ?? GENERIC_HISTORY_ROW_TEST_ID}`} > @@ -200,7 +203,16 @@ export const GenericHistoryRow: FC = memo( `} >{`${name}:`}   - {title} + + {title} + Date: Thu, 13 Feb 2025 18:00:17 -0600 Subject: [PATCH 3/3] PR comments --- .../shared/components/flyout_history_row.tsx | 57 +++++++++++++------ .../flyout/shared/utils/history_utils.ts | 14 ++--- 2 files changed, 48 insertions(+), 23 deletions(-) diff --git a/x-pack/solutions/security/plugins/security_solution/public/flyout/shared/components/flyout_history_row.tsx b/x-pack/solutions/security/plugins/security_solution/public/flyout/shared/components/flyout_history_row.tsx index dae808d313822..ed36c14326555 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/flyout/shared/components/flyout_history_row.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/flyout/shared/components/flyout_history_row.tsx @@ -127,6 +127,45 @@ export const DocumentDetailsHistoryRow: FC = memo(({ item ); }); +interface RowTitleProps { + /** + * alert, event, host, user, network, rule... + */ + type: string; + /** + * Actual value of the rule, host, user... + */ + value: string; +} + +/** + * Populates the generic row main text + */ +const RowTitle: FC = memo(({ type, value }) => { + const { euiTheme } = useEuiTheme(); + + return ( + <> + {`${type}:`} +   + + {value} + + + ); +}); + /** * Row item for a rule details flyout */ @@ -197,22 +236,7 @@ export const GenericHistoryRow: FC = memo( flex-direction: row; `} > - {`${name}:`} -   - - {title} - + = memo( FlyoutHistoryRow.displayName = 'FlyoutHistoryRow'; DocumentDetailsHistoryRow.displayName = 'DocumentDetailsHistoryRow'; RuleHistoryRow.displayName = 'RuleHistoryRow'; +RowTitle.displayName = 'RowTitle'; GenericHistoryRow.displayName = 'GenericHistoryRow'; diff --git a/x-pack/solutions/security/plugins/security_solution/public/flyout/shared/utils/history_utils.ts b/x-pack/solutions/security/plugins/security_solution/public/flyout/shared/utils/history_utils.ts index 0e14ce8ce2c84..0e28d76dfe208 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/flyout/shared/utils/history_utils.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/flyout/shared/utils/history_utils.ts @@ -27,15 +27,15 @@ export const getProcessedHistory = ({ // Step 2: remove duplicates // Because the lastOpen value will always be different, we're manually removing duplicates // by looking at the panel's information only. - const uniqueHistory: FlyoutPanelHistory[] = []; - reversedHistory.forEach((hist) => { - const entryDoesNotExists = - uniqueHistory - .map((h: FlyoutPanelHistory) => JSON.stringify(h.panel)) - .indexOf(JSON.stringify(hist.panel)) === -1; + const uniquePanels = new Set(); + const uniqueHistory = reversedHistory.filter((hist) => { + const panelString = JSON.stringify(hist.panel); + const entryDoesNotExists = !uniquePanels.has(panelString); if (entryDoesNotExists) { - uniqueHistory.push(hist); + uniquePanels.add(panelString); + return true; } + return false; }); // Omit the first (current opened) entry and return array of maxCount length