diff --git a/x-pack/plugins/security_solution/public/app/home/template_wrapper/global_kql_header/index.tsx b/x-pack/plugins/security_solution/public/app/home/template_wrapper/global_kql_header/index.tsx index 3e3c91133eab6..ac49d8e90498d 100644 --- a/x-pack/plugins/security_solution/public/app/home/template_wrapper/global_kql_header/index.tsx +++ b/x-pack/plugins/security_solution/public/app/home/template_wrapper/global_kql_header/index.tsx @@ -11,7 +11,7 @@ import { useGlobalHeaderPortal } from '../../../../common/hooks/use_global_heade const StyledStickyWrapper = styled.div` position: sticky; - z-index: ${(props) => props.theme.eui.euiZLevel2}; + z-index: ${(props) => props.theme.eui.euiZHeaderBelowDataGrid}; // TOP location is declared in src/public/rendering/_base.scss to keep in line with Kibana Chrome `; diff --git a/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.tsx b/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.tsx index b8b6b9766bdde..057d28b0112ad 100644 --- a/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.tsx +++ b/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.tsx @@ -321,7 +321,7 @@ const EventsViewerComponent: React.FC = ({ refetch={refetch} /> - {graphEventId && } + {graphEventId && } = ({ const trailingControlColumns: ControlColumnProps[] = EMPTY_CONTROL_COLUMNS; const graphOverlay = useMemo( () => - graphEventId != null && graphEventId.length > 0 ? ( - - ) : null, + graphEventId != null && graphEventId.length > 0 ? : null, [graphEventId, id] ); const setQuery = useCallback( diff --git a/x-pack/plugins/security_solution/public/common/containers/use_full_screen/index.tsx b/x-pack/plugins/security_solution/public/common/containers/use_full_screen/index.tsx index e19db8bb94b46..bd9c16e3d88e6 100644 --- a/x-pack/plugins/security_solution/public/common/containers/use_full_screen/index.tsx +++ b/x-pack/plugins/security_solution/public/common/containers/use_full_screen/index.tsx @@ -28,7 +28,6 @@ export const resetScroll = () => { } }, 0); }; - interface GlobalFullScreen { globalFullScreen: boolean; setGlobalFullScreen: (fullScreen: boolean) => void; @@ -46,10 +45,10 @@ export const useGlobalFullScreen = (): GlobalFullScreen => { const setGlobalFullScreen = useCallback( (fullScreen: boolean) => { if (fullScreen) { - document.body.classList.add(SCROLLING_DISABLED_CLASS_NAME); + document.body.classList.add(SCROLLING_DISABLED_CLASS_NAME, 'euiDataGrid__restrictBody'); resetScroll(); } else { - document.body.classList.remove(SCROLLING_DISABLED_CLASS_NAME); + document.body.classList.remove(SCROLLING_DISABLED_CLASS_NAME, 'euiDataGrid__restrictBody'); resetScroll(); } @@ -71,9 +70,15 @@ export const useTimelineFullScreen = (): TimelineFullScreen => { const dispatch = useDispatch(); const timelineFullScreen = useShallowEqualSelector(inputsSelectors.timelineFullScreenSelector) ?? false; - const setTimelineFullScreen = useCallback( - (fullScreen: boolean) => dispatch(inputsActions.setFullScreen({ id: 'timeline', fullScreen })), + (fullScreen: boolean) => { + if (fullScreen) { + document.body.classList.add('euiDataGrid__restrictBody'); + } else { + document.body.classList.remove('euiDataGrid__restrictBody'); + } + dispatch(inputsActions.setFullScreen({ id: 'timeline', fullScreen })); + }, [dispatch] ); const memoizedReturn = useMemo( diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/investigate_in_resolver.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/investigate_in_resolver.tsx index 2e23ecc648aee..21d0e132599fb 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/investigate_in_resolver.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/investigate_in_resolver.tsx @@ -13,6 +13,10 @@ import { setActiveTabTimeline, updateTimelineGraphEventId, } from '../../../../timelines/store/timeline/actions'; +import { + useGlobalFullScreen, + useTimelineFullScreen, +} from '../../../../common/containers/use_full_screen'; import { TimelineId, TimelineTabs } from '../../../../../common'; import { ACTION_INVESTIGATE_IN_RESOLVER } from '../../../../timelines/components/timeline/body/translations'; import { Ecs } from '../../../../../common/ecs'; @@ -35,13 +39,23 @@ export const useInvestigateInResolverContextItem = ({ }: InvestigateInResolverProps) => { const dispatch = useDispatch(); const isDisabled = useMemo(() => !isInvestigateInResolverActionEnabled(ecsData), [ecsData]); + const { setGlobalFullScreen } = useGlobalFullScreen(); + const { setTimelineFullScreen } = useTimelineFullScreen(); const handleClick = useCallback(() => { + const dataGridIsFullScreen = document.querySelector('.euiDataGrid--fullScreen'); dispatch(updateTimelineGraphEventId({ id: timelineId, graphEventId: ecsData._id })); if (timelineId === TimelineId.active) { + if (dataGridIsFullScreen) { + setTimelineFullScreen(true); + } dispatch(setActiveTabTimeline({ id: timelineId, activeTab: TimelineTabs.graph })); + } else { + if (dataGridIsFullScreen) { + setGlobalFullScreen(true); + } } onClose(); - }, [dispatch, ecsData._id, onClose, timelineId]); + }, [dispatch, ecsData._id, onClose, timelineId, setGlobalFullScreen, setTimelineFullScreen]); return isDisabled ? [] : [ diff --git a/x-pack/plugins/security_solution/public/timelines/components/graph_overlay/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/graph_overlay/index.test.tsx index 1286208bff9e6..d672a3c699707 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/graph_overlay/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/graph_overlay/index.test.tsx @@ -44,12 +44,10 @@ describe('GraphOverlay', () => { }); describe('when used in an events viewer (i.e. in the Detections view, or the Host > Events view)', () => { - const isEventViewer = true; - - test('it has 100% width when isEventViewer is true and NOT in full screen mode', async () => { + test('it has 100% width when NOT in full screen mode', async () => { const wrapper = mount( - + ); @@ -59,9 +57,9 @@ describe('GraphOverlay', () => { }); }); - test('it has a calculated width that makes room for the Timeline flyout button when isEventViewer is true in full screen mode', async () => { + test('it has a fixed position when in full screen mode', async () => { (useGlobalFullScreen as jest.Mock).mockReturnValue({ - globalFullScreen: true, // <-- true when an events viewer is in full screen mode + globalFullScreen: true, setGlobalFullScreen: jest.fn(), }); (useTimelineFullScreen as jest.Mock).mockReturnValue({ @@ -71,25 +69,24 @@ describe('GraphOverlay', () => { const wrapper = mount( - + ); await waitFor(() => { const overlayContainer = wrapper.find('[data-test-subj="overlayContainer"]').first(); - expect(overlayContainer).toHaveStyleRule('width', 'calc(100% - 36px)'); + expect(overlayContainer).toHaveStyleRule('position', 'fixed'); }); }); }); describe('when used in the active timeline', () => { - const isEventViewer = false; const timelineId = TimelineId.active; - test('it has 100% width when isEventViewer is false and NOT in full screen mode', async () => { + test('it has 100% width when NOT in full screen mode', async () => { const wrapper = mount( - + ); @@ -99,7 +96,7 @@ describe('GraphOverlay', () => { }); }); - test('it has 100% width when isEventViewer is false and the active timeline is in full screen mode', async () => { + test('it has 100% width when the active timeline is in full screen mode', async () => { (useGlobalFullScreen as jest.Mock).mockReturnValue({ globalFullScreen: false, setGlobalFullScreen: jest.fn(), @@ -111,7 +108,7 @@ describe('GraphOverlay', () => { const wrapper = mount( - + ); diff --git a/x-pack/plugins/security_solution/public/timelines/components/graph_overlay/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/graph_overlay/index.tsx index a8cfea1de8e74..16459381a8431 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/graph_overlay/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/graph_overlay/index.tsx @@ -41,13 +41,19 @@ import { import * as i18n from './translations'; const OverlayContainer = styled.div` - ${({ $restrictWidth }: { $restrictWidth: boolean }) => - ` - display: flex; - flex-direction: column; - flex: 1; - width: ${$restrictWidth ? 'calc(100% - 36px)' : '100%'}; - `} + display: flex; + flex-direction: column; + flex: 1; + width: 100%; +`; + +const FullScreenOverlayContainer = styled.div` + position: fixed; + top: 0; + bottom: 0; + left: 0; + right: 0; + z-index: ${(props) => props.theme.eui.euiZLevel3}; `; const StyledResolver = styled(Resolver)` @@ -59,7 +65,6 @@ const FullScreenButtonIcon = styled(EuiButtonIcon)` `; interface OwnProps { - isEventViewer: boolean; timelineId: TimelineId; } @@ -111,18 +116,15 @@ NavigationComponent.displayName = 'NavigationComponent'; const Navigation = React.memo(NavigationComponent); -const GraphOverlayComponent: React.FC = ({ isEventViewer, timelineId }) => { +const GraphOverlayComponent: React.FC = ({ timelineId }) => { const dispatch = useDispatch(); - const onCloseOverlay = useCallback(() => { - dispatch(updateTimelineGraphEventId({ id: timelineId, graphEventId: '' })); - }, [dispatch, timelineId]); + const { globalFullScreen, setGlobalFullScreen } = useGlobalFullScreen(); + const { timelineFullScreen, setTimelineFullScreen } = useTimelineFullScreen(); + const getTimeline = useMemo(() => timelineSelectors.getTimelineByIdSelector(), []); const graphEventId = useDeepEqualSelector( (state) => (getTimeline(state, timelineId) ?? timelineDefaults).graphEventId ); - - const { globalFullScreen, setGlobalFullScreen } = useGlobalFullScreen(); - const { timelineFullScreen, setTimelineFullScreen } = useTimelineFullScreen(); const getStartSelector = useMemo(() => startSelector(), []); const getEndSelector = useMemo(() => endSelector(), []); const getIsLoadingSelector = useMemo(() => isLoadingSelector(), []); @@ -154,6 +156,16 @@ const GraphOverlayComponent: React.FC = ({ isEventViewer, timelineId } [globalFullScreen, timelineId, timelineFullScreen] ); + const isInTimeline = timelineId === TimelineId.active; + const onCloseOverlay = useCallback(() => { + if (timelineId === TimelineId.active) { + setTimelineFullScreen(false); + } else { + setGlobalFullScreen(false); + } + dispatch(updateTimelineGraphEventId({ id: timelineId, graphEventId: '' })); + }, [dispatch, timelineId, setTimelineFullScreen, setGlobalFullScreen]); + const toggleFullScreen = useCallback(() => { if (timelineId === TimelineId.active) { setTimelineFullScreen(!timelineFullScreen); @@ -173,41 +185,71 @@ const GraphOverlayComponent: React.FC = ({ isEventViewer, timelineId } [] ); const existingIndexNames = useDeepEqualSelector(existingIndexNamesSelector); - - return ( - - - - - + + + + + + + + {graphEventId !== undefined ? ( + - - - - {graphEventId !== undefined ? ( - - ) : ( - - + ) : ( + + + + )} + + ); + } else { + return ( + + + + + + - )} - - ); + + {graphEventId !== undefined ? ( + + ) : ( + + + + )} + + ); + } }; export const GraphOverlay = React.memo(GraphOverlayComponent); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/graph_tab_content/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/graph_tab_content/index.tsx index 1678a92c4cdaa..64d3f01cff265 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/graph_tab_content/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/graph_tab_content/index.tsx @@ -26,7 +26,7 @@ const GraphTabContentComponent: React.FC = ({ timelineId } return null; } - return ; + return ; }; GraphTabContentComponent.displayName = 'GraphTabContentComponent'; diff --git a/x-pack/plugins/timelines/public/components/t_grid/integrated/index.tsx b/x-pack/plugins/timelines/public/components/t_grid/integrated/index.tsx index e98d9fff04a0c..c3c83f6be72c8 100644 --- a/x-pack/plugins/timelines/public/components/t_grid/integrated/index.tsx +++ b/x-pack/plugins/timelines/public/components/t_grid/integrated/index.tsx @@ -8,7 +8,13 @@ import type { AlertConsumers as AlertConsumersTyped } from '@kbn/rule-data-utils'; // @ts-expect-error import { AlertConsumers as AlertConsumersNonTyped } from '@kbn/rule-data-utils/target_node/alerts_as_data_rbac'; -import { EuiEmptyPrompt, EuiLoadingContent, EuiPanel } from '@elastic/eui'; +import { + EuiEmptyPrompt, + EuiFlexGroup, + EuiFlexItem, + EuiPanel, + EuiLoadingContent, +} from '@elastic/eui'; import { isEmpty } from 'lodash/fp'; import React, { useEffect, useMemo, useRef, useState } from 'react'; import styled from 'styled-components'; @@ -80,6 +86,16 @@ const EventsContainerLoading = styled.div.attrs(({ className = '' }) => ({ flex-direction: column; `; +const FullWidthFlexGroup = styled(EuiFlexGroup)<{ $visible: boolean }>` + overflow: hidden; + margin: 0; + display: ${({ $visible }) => ($visible ? 'flex' : 'none')}; +`; + +const ScrollableFlexItem = styled(EuiFlexItem)` + overflow: auto; +`; + const SECURITY_ALERTS_CONSUMERS = [AlertConsumers.SIEM]; export interface TGridIntegratedProps { @@ -309,56 +325,61 @@ const TGridIntegratedComponent: React.FC = ({ {!graphEventId && graphOverlay == null && ( - <> - {totalCountMinusDeleted === 0 && loading === false && ( - - - - } - titleSize="s" - body={ -

- -

- } - /> - )} - {totalCountMinusDeleted > 0 && ( - - )} - + + + {totalCountMinusDeleted === 0 && loading === false && ( + + + + } + titleSize="s" + body={ +

+ +

+ } + /> + )} + {totalCountMinusDeleted > 0 && ( + + )} +
+
)} )}