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 b8b6b9766bdde3..b18e90034af10e 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,9 @@ const EventsViewerComponent: React.FC = ({ refetch={refetch} /> - {graphEventId && } + {graphEventId && ( + + )} = ({ const globalFilters = useMemo(() => [...filters, ...(pageFilters ?? [])], [filters, pageFilters]); const trailingControlColumns: ControlColumnProps[] = EMPTY_CONTROL_COLUMNS; - const graphOverlay = useMemo( - () => - graphEventId != null && graphEventId.length > 0 ? ( - - ) : null, - [graphEventId, id] - ); + const { isDataGridExpanded, onMutation } = useIsDataGridExpanded(); const setQuery = useCallback( (inspect, loading, refetch) => { dispatch(inputsActions.setQuery({ id, inputId: 'global', inspect, loading, refetch })); }, [dispatch, id] ); + const graphOverlay = useMemo( + () => + graphEventId != null && graphEventId.length > 0 ? ( + + ) : null, + [graphEventId, id, isDataGridExpanded] + ); + + const tGridProps = useMemo(() => { + const type: TGridType = 'embedded'; + return { + id, + type, + browserFields, + columns, + dataProviders: dataProviders!, + defaultCellActions, + deletedEventIds, + docValueFields, + end, + entityType, + filters: globalFilters, + globalFullScreen, + graphOverlay, + hasAlertsCrud, + indexNames: selectedPatterns, + indexPattern, + isLive, + isLoadingIndexPattern, + itemsPerPage, + itemsPerPageOptions: itemsPerPageOptions!, + kqlMode, + query, + onRuleChange, + renderCellValue, + rowRenderers, + setQuery, + start, + sort, + additionalFilters, + graphEventId, + filterStatus: currentFilter, + leadingControlColumns, + trailingControlColumns, + tGridEventRenderedViewEnabled, + unit, + }; + }, [ + additionalFilters, + browserFields, + columns, + currentFilter, + dataProviders, + defaultCellActions, + deletedEventIds, + docValueFields, + end, + entityType, + globalFilters, + globalFullScreen, + graphEventId, + graphOverlay, + hasAlertsCrud, + id, + indexPattern, + isLive, + isLoadingIndexPattern, + itemsPerPage, + itemsPerPageOptions, + kqlMode, + onRuleChange, + query, + renderCellValue, + rowRenderers, + selectedPatterns, + setQuery, + sort, + start, + tGridEventRenderedViewEnabled, + trailingControlColumns, + unit, + ]); return ( - <> - - - {tGridEnabled ? ( - timelinesUi.getTGrid<'embedded'>({ - id, - type: 'embedded', - browserFields, - columns, - dataProviders: dataProviders!, - defaultCellActions, - deletedEventIds, - docValueFields, - end, - entityType, - filters: globalFilters, - globalFullScreen, - graphOverlay, - hasAlertsCrud, - indexNames: selectedPatterns, - indexPattern, - isLive, - isLoadingIndexPattern, - itemsPerPage, - itemsPerPageOptions: itemsPerPageOptions!, - kqlMode, - query, - onRuleChange, - renderCellValue, - rowRenderers, - setQuery, - start, - sort, - additionalFilters, - graphEventId, - filterStatus: currentFilter, - leadingControlColumns, - trailingControlColumns, - tGridEventRenderedViewEnabled, - unit, - }) - ) : ( - - )} - - - - + + {(mutationRef) => ( + <> + + + {tGridEnabled ? ( + timelinesUi.getTGrid<'embedded'>(tGridProps) + ) : ( + + )} + + + + + )} + ); }; 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 e19db8bb94b466..5271bdfd47bb22 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 @@ -5,7 +5,7 @@ * 2.0. */ -import { useCallback, useMemo } from 'react'; +import { useCallback, useMemo, useState } from 'react'; import { useDispatch } from 'react-redux'; import { SCROLLING_DISABLED_CLASS_NAME } from '../../../../common/constants'; @@ -29,6 +29,25 @@ export const resetScroll = () => { }, 0); }; +export const setDataGridFullScreen = (isExpanded: boolean) => { + const clickEvent = new KeyboardEvent('click', { + view: window, + bubbles: true, + cancelable: true, + }); + const dataGridIsFullScreen = document.querySelector('.euiDataGrid--fullScreen') !== null; + const dataGridFullScreenButtonNew = document.querySelector( + '[data-test-subj="dataGridFullScrenButton"]' + ); + const dataGridFullScreenButtonOld = document.querySelector( + '[data-test-subj="dataGridFullScreenButton"]' + ); + const dataGridFullScreenButton = dataGridFullScreenButtonNew || dataGridFullScreenButtonOld; + if (dataGridIsFullScreen !== isExpanded && dataGridFullScreenButton) { + dataGridFullScreenButton.dispatchEvent(clickEvent); + } +}; + interface GlobalFullScreen { globalFullScreen: boolean; setGlobalFullScreen: (fullScreen: boolean) => void; @@ -46,9 +65,11 @@ export const useGlobalFullScreen = (): GlobalFullScreen => { const setGlobalFullScreen = useCallback( (fullScreen: boolean) => { if (fullScreen) { + setDataGridFullScreen(true); document.body.classList.add(SCROLLING_DISABLED_CLASS_NAME); resetScroll(); } else { + setDataGridFullScreen(false); document.body.classList.remove(SCROLLING_DISABLED_CLASS_NAME); resetScroll(); } @@ -71,9 +92,10 @@ 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) => { + dispatch(inputsActions.setFullScreen({ id: 'timeline', fullScreen })); + }, [dispatch] ); const memoizedReturn = useMemo( @@ -85,3 +107,14 @@ export const useTimelineFullScreen = (): TimelineFullScreen => { ); return memoizedReturn; }; + +export const useIsDataGridExpanded = () => { + const [isDataGridExpanded, setDataGridExpanded] = useState(false); + const onMutation = useCallback(() => { + const isExpanded = document.querySelector('.euiDataGrid--fullScreen') !== null; + setDataGridExpanded(isExpanded); + }, [setDataGridExpanded]); + return useMemo(() => { + return { onMutation, isDataGridExpanded }; + }, [onMutation, isDataGridExpanded]); +}; 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 2e23ecc648aee4..21d0e132599fbd 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 1286208bff9e6a..51beafd4c8cf8b 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 @@ -49,7 +49,11 @@ describe('GraphOverlay', () => { test('it has 100% width when isEventViewer is true and NOT in full screen mode', async () => { const wrapper = mount( - + ); @@ -59,7 +63,7 @@ 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 isEventViewer is true in full screen mode', async () => { (useGlobalFullScreen as jest.Mock).mockReturnValue({ globalFullScreen: true, // <-- true when an events viewer is in full screen mode setGlobalFullScreen: jest.fn(), @@ -71,13 +75,17 @@ 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'); }); }); }); @@ -89,7 +97,11 @@ describe('GraphOverlay', () => { test('it has 100% width when isEventViewer is false and NOT in full screen mode', async () => { const wrapper = mount( - + ); @@ -111,7 +123,11 @@ 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 a8cfea1de8e74c..eb8a1acea6b25c 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 @@ -24,6 +24,7 @@ import { FULL_SCREEN_TOGGLED_CLASS_NAME } from '../../../../common/constants'; import { useGlobalFullScreen, useTimelineFullScreen, + setDataGridFullScreen, } from '../../../common/containers/use_full_screen'; import { useDeepEqualSelector } from '../../../common/hooks/use_selector'; import { TimelineId } from '../../../../common/types/timeline'; @@ -50,6 +51,15 @@ const OverlayContainer = styled.div` `} `; +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)` height: 100%; `; @@ -61,6 +71,7 @@ const FullScreenButtonIcon = styled(EuiButtonIcon)` interface OwnProps { isEventViewer: boolean; timelineId: TimelineId; + isDataGridExpanded: boolean; } interface NavigationProps { @@ -111,18 +122,19 @@ NavigationComponent.displayName = 'NavigationComponent'; const Navigation = React.memo(NavigationComponent); -const GraphOverlayComponent: React.FC = ({ isEventViewer, timelineId }) => { +const GraphOverlayComponent: React.FC = ({ + isEventViewer, + timelineId, + isDataGridExpanded, +}) => { 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(), []); @@ -149,11 +161,27 @@ const GraphOverlayComponent: React.FC = ({ isEventViewer, timelineId } } }); - const fullScreen = useMemo( + const isResolverExpanded = useMemo( () => isFullScreen({ globalFullScreen, timelineId, timelineFullScreen }), [globalFullScreen, timelineId, timelineFullScreen] ); + const fullScreen = useMemo(() => { + return isResolverExpanded && isDataGridExpanded; + }, [isResolverExpanded, isDataGridExpanded]); + + const onCloseOverlay = useCallback(() => { + if (isResolverExpanded === false) { + setDataGridFullScreen(false); + } + if (timelineId === TimelineId.active) { + setTimelineFullScreen(false); + } else { + setGlobalFullScreen(false); + } + dispatch(updateTimelineGraphEventId({ id: timelineId, graphEventId: '' })); + }, [dispatch, timelineId, setTimelineFullScreen, setGlobalFullScreen, isResolverExpanded]); + const toggleFullScreen = useCallback(() => { if (timelineId === TimelineId.active) { setTimelineFullScreen(!timelineFullScreen); @@ -173,41 +201,74 @@ 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 1678a92c4cdaa8..f1d0db0d3ef553 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/body/index.tsx b/x-pack/plugins/timelines/public/components/t_grid/body/index.tsx index 24f2718fbc1cfe..dbef74b4380e0c 100644 --- a/x-pack/plugins/timelines/public/components/t_grid/body/index.tsx +++ b/x-pack/plugins/timelines/public/components/t_grid/body/index.tsx @@ -734,7 +734,6 @@ export const BodyComponent = React.memo( ); const height = useDataGridHeightHack(pageSize, data.length); - // Store context in state rather than creating object in provider value={} to prevent re-renders caused by a new object being created const [activeStatefulEventContext] = useState({ timelineID: id, 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 e98d9fff04a0c9..e7dea4ff30bf9d 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,9 +8,15 @@ 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 React, { useEffect, useMemo, useState, useRef } from 'react'; import styled from 'styled-components'; import { useDispatch } from 'react-redux'; @@ -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 { @@ -142,6 +158,7 @@ const TGridIntegratedComponent: React.FC = ({ id, indexNames, indexPattern, + isLive, isLoadingIndexPattern, itemsPerPage, itemsPerPageOptions, @@ -262,8 +279,6 @@ const TGridIntegratedComponent: React.FC = ({ setIsQueryLoading(loading); }, [loading]); - const alignItems = tableView === 'gridView' ? 'baseline' : 'center'; - const isFirstUpdate = useRef(true); useEffect(() => { if (isFirstUpdate.current && !loading) { @@ -271,6 +286,7 @@ const TGridIntegratedComponent: React.FC = ({ } }, [loading]); + const alignItems = tableView === 'gridView' ? 'baseline' : 'center'; useEffect(() => { setQuery(inspect, loading, refetch); }, [inspect, loading, refetch, setQuery]); @@ -287,81 +303,86 @@ const TGridIntegratedComponent: React.FC = ({ {isFirstUpdate.current && } {graphOverlay} + {canQueryTimeline ? ( + <> + + + + + + + {!resolverIsShowing(graphEventId) && additionalFilters} + + {tGridEventRenderedViewEnabled && + ['detections-page', 'detections-rules-details-page'].includes(id) && ( + + + + )} + - {canQueryTimeline && ( - - - - - - - {!resolverIsShowing(graphEventId) && additionalFilters} - - {tGridEventRenderedViewEnabled && - ['detections-page', 'detections-rules-details-page'].includes(id) && ( - - - - )} - - - {!graphEventId && graphOverlay == null && ( - <> - {totalCountMinusDeleted === 0 && loading === false && ( - - - - } - titleSize="s" - body={ -

- -

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

+ +

+ } + /> + ) : ( + <> + + + )} +
+
+
+ + ) : null} );