-
Notifications
You must be signed in to change notification settings - Fork 8k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[Security Solution] Unified Timeline - Add Expandable Flyout + fixes …
…old flyout (#181793) ## Summary This feature must be enabled with below feature flag: ```yaml xpack.securitySolution.enableExperimental: - unifiedComponentsInTimelineEnabled ``` This PR enables expandable Flyout in unified timeline and fixes the `z-index` issue with previous flyout. This is essentially a workaround until either #180646 or #180645 is resolved. After that #179520 will make sure to remove this workaround in favour of a permanent solution. This is how it looks after the fix: https://github.com/elastic/kibana/assets/7485038/21952311-92bf-49a4-a8fd-1d12c126bd5c
- Loading branch information
Showing
4 changed files
with
255 additions
and
15 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
121 changes: 121 additions & 0 deletions
121
...ponents/timeline/unified_components/hooks/use_unified_timeline_expandable_flyout.test.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,121 @@ | ||
/* | ||
* 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 { useUiSetting$ } from '@kbn/kibana-react-plugin/public'; | ||
import { useIsExperimentalFeatureEnabled } from '../../../../../common/hooks/use_experimental_features'; | ||
import { renderHook } from '@testing-library/react-hooks'; | ||
import { useUnifiedTableExpandableFlyout } from './use_unified_timeline_expandable_flyout'; | ||
import { useLocation } from 'react-router-dom'; | ||
import { URL_PARAM_KEY } from '../../../../../common/hooks/use_url_state'; | ||
import { useExpandableFlyoutApi } from '@kbn/expandable-flyout'; | ||
|
||
jest.mock('../../../../../common/hooks/use_experimental_features'); | ||
jest.mock('@kbn/kibana-react-plugin/public'); | ||
jest.mock('react-router-dom', () => { | ||
return { | ||
useLocation: jest.fn(), | ||
}; | ||
}); | ||
jest.mock('@kbn/expandable-flyout'); | ||
|
||
const onFlyoutCloseMock = jest.fn(); | ||
|
||
describe('useUnifiedTimelineExpandableFlyout', () => { | ||
beforeEach(() => { | ||
(useIsExperimentalFeatureEnabled as jest.Mock).mockReturnValue(true); | ||
(useUiSetting$ as jest.Mock).mockReturnValue([true, jest.fn()]); | ||
(useLocation as jest.Mock).mockReturnValue({ | ||
search: `?${URL_PARAM_KEY.timelineFlyout}=(test:value)`, | ||
}); | ||
(useExpandableFlyoutApi as jest.Mock).mockReturnValue({ | ||
openFlyout: jest.fn(), | ||
closeFlyout: jest.fn(), | ||
}); | ||
}); | ||
|
||
afterEach(() => { | ||
jest.clearAllMocks(); | ||
}); | ||
|
||
it('should have expandable flyout disabled when flyout is disabled in Advanced Settings', () => { | ||
(useUiSetting$ as jest.Mock).mockReturnValue([false, jest.fn()]); | ||
const { result } = renderHook(() => | ||
useUnifiedTableExpandableFlyout({ | ||
onClose: onFlyoutCloseMock, | ||
}) | ||
); | ||
|
||
expect(result.current.isTimelineExpandableFlyoutEnabled).toBe(false); | ||
}); | ||
it('should have expandable flyout disabled when flyout is disabled in Experimental Features', () => { | ||
(useIsExperimentalFeatureEnabled as jest.Mock).mockReturnValue(false); | ||
const { result } = renderHook(() => | ||
useUnifiedTableExpandableFlyout({ | ||
onClose: onFlyoutCloseMock, | ||
}) | ||
); | ||
|
||
expect(result.current.isTimelineExpandableFlyoutEnabled).toBe(false); | ||
}); | ||
describe('when flyout is enabled', () => { | ||
it('should mark flyout as closed when location is empty', () => { | ||
(useLocation as jest.Mock).mockReturnValue({ | ||
search: '', | ||
}); | ||
|
||
const { result } = renderHook(() => | ||
useUnifiedTableExpandableFlyout({ | ||
onClose: onFlyoutCloseMock, | ||
}) | ||
); | ||
|
||
expect(result.current.isTimelineExpandableFlyoutOpen).toBe(false); | ||
}); | ||
|
||
it('should mark flyout as open when location has `timelineFlyout`', () => { | ||
(useLocation as jest.Mock).mockReturnValue({ | ||
search: `${URL_PARAM_KEY.timelineFlyout}=(test:value)`, | ||
}); | ||
const { result } = renderHook(() => | ||
useUnifiedTableExpandableFlyout({ | ||
onClose: onFlyoutCloseMock, | ||
}) | ||
); | ||
|
||
expect(result.current.isTimelineExpandableFlyoutOpen).toBe(true); | ||
}); | ||
|
||
it('should mark flyout as close when location has empty `timelineFlyout`', () => { | ||
const { result, rerender } = renderHook(() => | ||
useUnifiedTableExpandableFlyout({ | ||
onClose: onFlyoutCloseMock, | ||
}) | ||
); | ||
expect(result.current.isTimelineExpandableFlyoutOpen).toBe(true); | ||
|
||
(useLocation as jest.Mock).mockReturnValue({ | ||
search: `${URL_PARAM_KEY.timelineFlyout}=()`, | ||
}); | ||
|
||
rerender(); | ||
|
||
expect(result.current.isTimelineExpandableFlyoutOpen).toBe(false); | ||
expect(onFlyoutCloseMock).toHaveBeenCalledTimes(1); | ||
}); | ||
|
||
it('should call user provided close handler when flyout is closed', () => { | ||
const { result } = renderHook(() => | ||
useUnifiedTableExpandableFlyout({ | ||
onClose: onFlyoutCloseMock, | ||
}) | ||
); | ||
|
||
result.current.closeFlyout(); | ||
expect(onFlyoutCloseMock).toHaveBeenCalledTimes(1); | ||
}); | ||
}); | ||
}); |
82 changes: 82 additions & 0 deletions
82
...s/components/timeline/unified_components/hooks/use_unified_timeline_expandable_flyout.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,82 @@ | ||
/* | ||
* 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 { useCallback, useEffect, useMemo, useState } from 'react'; | ||
import { useUiSetting$ } from '@kbn/kibana-react-plugin/public'; | ||
import { useExpandableFlyoutApi } from '@kbn/expandable-flyout'; | ||
import { useLocation } from 'react-router-dom'; | ||
import { useIsExperimentalFeatureEnabled } from '../../../../../common/hooks/use_experimental_features'; | ||
import { ENABLE_EXPANDABLE_FLYOUT_SETTING } from '../../../../../../common/constants'; | ||
import { URL_PARAM_KEY } from '../../../../../common/hooks/use_url_state'; | ||
|
||
const EMPTY_TIMELINE_FLYOUT_SEARCH_PARAMS = '()'; | ||
|
||
interface UseUnifiedTableExpandableFlyoutArgs { | ||
onClose?: () => void; | ||
} | ||
|
||
export const useUnifiedTableExpandableFlyout = ({ | ||
onClose, | ||
}: UseUnifiedTableExpandableFlyoutArgs) => { | ||
const expandableTimelineFlyoutEnabled = useIsExperimentalFeatureEnabled( | ||
'expandableTimelineFlyoutEnabled' | ||
); | ||
|
||
const [isSecurityFlyoutEnabled] = useUiSetting$<boolean>(ENABLE_EXPANDABLE_FLYOUT_SETTING); | ||
|
||
const location = useLocation(); | ||
|
||
const { openFlyout, closeFlyout } = useExpandableFlyoutApi(); | ||
|
||
const closeFlyoutWithEffect = useCallback(() => { | ||
closeFlyout(); | ||
onClose?.(); | ||
}, [onClose, closeFlyout]); | ||
|
||
const isFlyoutOpen = useMemo(() => { | ||
/** | ||
* Currently, if new expanable flyout is closed, there is not way for | ||
* consumer to trigger an effect `onClose` of flyout. So, we are using | ||
* this hack to know if flyout is open or not. | ||
* | ||
* Raised: https://github.com/elastic/kibana/issues/179520 | ||
* | ||
* */ | ||
const searchParams = new URLSearchParams(location.search); | ||
return ( | ||
searchParams.has(URL_PARAM_KEY.timelineFlyout) && | ||
searchParams.get(URL_PARAM_KEY.timelineFlyout) !== EMPTY_TIMELINE_FLYOUT_SEARCH_PARAMS | ||
); | ||
}, [location.search]); | ||
|
||
const [isTimelineExpandableFlyoutOpen, setIsTimelineExpandableFlyoutOpen] = | ||
useState(isFlyoutOpen); | ||
|
||
useEffect(() => { | ||
setIsTimelineExpandableFlyoutOpen((prev) => { | ||
if (prev === isFlyoutOpen) { | ||
return prev; | ||
} | ||
if (!isFlyoutOpen && onClose) { | ||
// run onClose only when isFlyoutOpen changed from true to false | ||
// should not be needed when | ||
// https://github.com/elastic/kibana/issues/179520 | ||
// is resolved | ||
|
||
onClose(); | ||
} | ||
return isFlyoutOpen; | ||
}); | ||
}, [isFlyoutOpen, onClose]); | ||
|
||
return { | ||
isTimelineExpandableFlyoutOpen, | ||
openFlyout, | ||
closeFlyout: closeFlyoutWithEffect, | ||
isTimelineExpandableFlyoutEnabled: expandableTimelineFlyoutEnabled && isSecurityFlyoutEnabled, | ||
}; | ||
}; |