[Security Solution] Full screen fixes for Timeline based views (#73421)

## Full screen fixes for Timeline based views

- Fixes an issue where sometimes, Global navigation is hidden until the page is scrolled when exiting full screen mode
- Improves performance by adding an intent delay before showing the draggable wrapper hover menu
- Removes an unnecessary CSS transition

### Sometimes, Global navigation is hidden until the page is scrolled when exiting full screen mode

Sometimes, after exiting `Full screen` mode in a page, for example, the `Detections` page, the global navigation, e.g. `Overview Detections Hosts...` is hidden until the page is scrolled.

To reproduce:

1) Navigate to the `Detections` page
2) Click the `Full screen` button in the table
3) Without scrolling the full screen view, click the `Exit full screen` button

**Expected result**
- [x] The global navigation e.g. `Overview Detections Hosts...` is visible above the search bar, per the screenshot below:

![correct-global-navigation](https://user-images.githubusercontent.com/4459398/87717870-571bef80-c76e-11ea-8b7b-1850094326b3.png)

4) Once again, click the `Full screen` button in the table
5) This time, expand an event, which will scroll the view
6) Once again, click the `Exit full screen` button

**Expected result**
- [x] The global navigation e.g. `Overview Detections Hosts...` is visible above the search bar

**Actual result**
- [ ] Sometimes, the global navigation e.g. `Overview Detections Hosts...` is **not** visible until the page is scrolled
This commit is contained in:
Andrew Goldstein 2020-07-28 08:09:35 -06:00 committed by GitHub
parent 5e8e01fd0f
commit dca4a23597
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 206 additions and 34 deletions

View file

@ -32,6 +32,7 @@ export const DEFAULT_INTERVAL_PAUSE = true;
export const DEFAULT_INTERVAL_TYPE = 'manual';
export const DEFAULT_INTERVAL_VALUE = 300000; // ms
export const DEFAULT_TIMEPICKER_QUICK_RANGES = 'timepicker:quickRanges';
export const SCROLLING_DISABLED_CLASS_NAME = 'scrolling-disabled';
export const FILTERS_GLOBAL_HEIGHT = 109; // px
export const FULL_SCREEN_TOGGLED_CLASS_NAME = 'fullScreenToggled';
export const NO_ALERT_INDEX = 'no-alert-index-049FC71A-4C2C-446F-9901-37XMC5024C51';

View file

@ -45,6 +45,7 @@ describe('AddFilterToGlobalSearchBar Component', () => {
);
beforeEach(() => {
jest.useFakeTimers();
store = createStore(
state,
SUB_PLUGINS_REDUCER,
@ -159,6 +160,8 @@ describe('AddFilterToGlobalSearchBar Component', () => {
wrapper.find('[data-test-subj="withHoverActionsButton"]').simulate('mouseenter');
wrapper.update();
jest.runAllTimers();
wrapper.update();
wrapper
.find('[data-test-subj="hover-actions-container"] [data-euiicon-type]')

View file

@ -22,6 +22,10 @@ describe('DraggableWrapper', () => {
const message = 'draggable wrapper content';
const mount = useMountAppended();
beforeEach(() => {
jest.useFakeTimers();
});
describe('rendering', () => {
test('it renders against the snapshot', () => {
const wrapper = shallow(
@ -78,6 +82,8 @@ describe('DraggableWrapper', () => {
wrapper.find('[data-test-subj="withHoverActionsButton"]').simulate('mouseenter');
wrapper.update();
jest.runAllTimers();
wrapper.update();
expect(wrapper.find('[data-test-subj="copy-to-clipboard"]').exists()).toBe(true);
});
});

View file

@ -13,13 +13,6 @@ interface ProviderContainerProps {
}
const ProviderContainerComponent = styled.div<ProviderContainerProps>`
&,
&::before,
&::after {
transition: background ${({ theme }) => theme.eui.euiAnimSpeedFast} ease,
color ${({ theme }) => theme.eui.euiAnimSpeedFast} ease;
}
${({ isDragging }) =>
!isDragging &&
css`

View file

@ -4,20 +4,120 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { shallow } from 'enzyme';
import { mount, ReactWrapper, shallow } from 'enzyme';
import React from 'react';
import { StickyContainer } from 'react-sticky';
import '../../mock/match_media';
import { FiltersGlobal } from './filters_global';
import { TestProviders } from '../../mock/test_providers';
describe('rendering', () => {
test('renders correctly', () => {
const wrapper = shallow(
<FiltersGlobal>
<FiltersGlobal globalFullScreen={false}>
<p>{'Additional filters here.'}</p>
</FiltersGlobal>
);
expect(wrapper).toMatchSnapshot();
});
describe('full screen mode', () => {
let wrapper: ReactWrapper;
beforeEach(() => {
wrapper = mount(
<TestProviders>
<StickyContainer>
<FiltersGlobal globalFullScreen={true}>
<p>{'Filter content'}</p>
</FiltersGlobal>
</StickyContainer>
</TestProviders>
);
});
test('it does NOT render the sticky container', () => {
expect(wrapper.find('[data-test-subj="sticky-filters-global-container"]').exists()).toBe(
false
);
});
test('it renders the non-sticky container', () => {
expect(wrapper.find('[data-test-subj="non-sticky-global-container"]').exists()).toBe(true);
});
test('it does NOT render the container with a `display: none` style when `show` is true (the default)', () => {
expect(
wrapper.find('[data-test-subj="non-sticky-global-container"]').first()
).not.toHaveStyleRule('display', 'none');
});
});
describe('non-full screen mode', () => {
let wrapper: ReactWrapper;
beforeEach(() => {
wrapper = mount(
<TestProviders>
<StickyContainer>
<FiltersGlobal globalFullScreen={false}>
<p>{'Filter content'}</p>
</FiltersGlobal>
</StickyContainer>
</TestProviders>
);
});
test('it renders the sticky container', () => {
expect(wrapper.find('[data-test-subj="sticky-filters-global-container"]').exists()).toBe(
true
);
});
test('it does NOT render the non-sticky container', () => {
expect(wrapper.find('[data-test-subj="non-sticky-global-container"]').exists()).toBe(false);
});
test('it does NOT render the container with a `display: none` style when `show` is true (the default)', () => {
expect(
wrapper.find('[data-test-subj="sticky-filters-global-container"]').first()
).not.toHaveStyleRule('display', 'none');
});
});
describe('when show is false', () => {
test('in full screen mode it renders the container with a `display: none` style', () => {
const wrapper = mount(
<TestProviders>
<StickyContainer>
<FiltersGlobal globalFullScreen={true} show={false}>
<p>{'Filter content'}</p>
</FiltersGlobal>
</StickyContainer>
</TestProviders>
);
expect(
wrapper.find('[data-test-subj="non-sticky-global-container"]').first()
).toHaveStyleRule('display', 'none');
});
test('in non-full screen mode it renders the container with a `display: none` style', () => {
const wrapper = mount(
<TestProviders>
<StickyContainer>
<FiltersGlobal globalFullScreen={false} show={false}>
<p>{'Filter content'}</p>
</FiltersGlobal>
</StickyContainer>
</TestProviders>
);
expect(
wrapper.find('[data-test-subj="sticky-filters-global-container"]').first()
).toHaveStyleRule('display', 'none');
});
});
});

View file

@ -47,20 +47,33 @@ const FiltersGlobalContainer = styled.header<{ show: boolean }>`
FiltersGlobalContainer.displayName = 'FiltersGlobalContainer';
const NO_STYLE: React.CSSProperties = {};
export interface FiltersGlobalProps {
children: React.ReactNode;
globalFullScreen: boolean;
show?: boolean;
}
export const FiltersGlobal = React.memo<FiltersGlobalProps>(({ children, show = true }) => (
<Sticky disableCompensation={disableStickyMq.matches} topOffset={-offsetChrome}>
{({ style, isSticky }) => (
<FiltersGlobalContainer show={show}>
<Wrapper className="siemFiltersGlobal" isSticky={isSticky} style={style}>
export const FiltersGlobal = React.memo<FiltersGlobalProps>(
({ children, globalFullScreen, show = true }) =>
globalFullScreen ? (
<FiltersGlobalContainer data-test-subj="non-sticky-global-container" show={show}>
<Wrapper className="siemFiltersGlobal" isSticky={false} style={NO_STYLE}>
{children}
</Wrapper>
</FiltersGlobalContainer>
)}
</Sticky>
));
) : (
<Sticky disableCompensation={disableStickyMq.matches} topOffset={-offsetChrome}>
{({ style, isSticky }) => (
<FiltersGlobalContainer data-test-subj="sticky-filters-global-container" show={show}>
<Wrapper className="siemFiltersGlobal" isSticky={isSticky} style={style}>
{children}
</Wrapper>
</FiltersGlobalContainer>
)}
</Sticky>
)
);
FiltersGlobal.displayName = 'FiltersGlobal';

View file

@ -7,7 +7,10 @@
import { EuiBadge, EuiDescriptionList, EuiFlexGroup, EuiIcon, EuiPage } from '@elastic/eui';
import styled, { createGlobalStyle } from 'styled-components';
import { FULL_SCREEN_TOGGLED_CLASS_NAME } from '../../../../common/constants';
import {
FULL_SCREEN_TOGGLED_CLASS_NAME,
SCROLLING_DISABLED_CLASS_NAME,
} from '../../../../common/constants';
/*
SIDE EFFECT: the following `createGlobalStyle` overrides default styling in angular code that was not theme-friendly
@ -63,6 +66,14 @@ export const AppGlobalStyle = createGlobalStyle<{ theme: { eui: { euiColorPrimar
.${FULL_SCREEN_TOGGLED_CLASS_NAME} {
${({ theme }) => `background-color: ${theme.eui.euiColorPrimary} !important`};
}
.${SCROLLING_DISABLED_CLASS_NAME} body {
overflow-y: hidden;
}
.${SCROLLING_DISABLED_CLASS_NAME} #kibana-body {
overflow-y: hidden;
}
`;
export const DescriptionListStyled = styled(EuiDescriptionList)`

View file

@ -10,6 +10,11 @@ import styled from 'styled-components';
import { IS_DRAGGING_CLASS_NAME } from '../drag_and_drop/helpers';
/**
* To avoid expensive changes to the DOM, delay showing the popover menu
*/
const HOVER_INTENT_DELAY = 100; // ms
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const WithHoverActionsPopover = (styled(EuiPopover as any)`
.euiPopover__anchor {
@ -51,18 +56,27 @@ export const WithHoverActions = React.memo<Props>(
({ alwaysShow = false, closePopOverTrigger, hoverContent, render }) => {
const [isOpen, setIsOpen] = useState(hoverContent != null && alwaysShow);
const [showHoverContent, setShowHoverContent] = useState(false);
const [hoverTimeout, setHoverTimeout] = useState<number | undefined>(undefined);
const onMouseEnter = useCallback(() => {
// NOTE: the following read from the DOM is expensive, but not as
// expensive as the default behavior, which adds a div to the body,
// which-in turn performs a more expensive change to the layout
if (!document.body.classList.contains(IS_DRAGGING_CLASS_NAME)) {
setShowHoverContent(true);
}
}, []);
setHoverTimeout(
Number(
setTimeout(() => {
// NOTE: the following read from the DOM is expensive, but not as
// expensive as the default behavior, which adds a div to the body,
// which-in turn performs a more expensive change to the layout
if (!document.body.classList.contains(IS_DRAGGING_CLASS_NAME)) {
setShowHoverContent(true);
}
}, HOVER_INTENT_DELAY)
)
);
}, [setHoverTimeout, setShowHoverContent]);
const onMouseLeave = useCallback(() => {
clearTimeout(hoverTimeout);
setShowHoverContent(false);
}, []);
}, [hoverTimeout, setShowHoverContent]);
const content = useMemo(
() => (

View file

@ -6,6 +6,7 @@
import { useCallback, useMemo } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { SCROLLING_DISABLED_CLASS_NAME } from '../../../../common/constants';
import { inputsSelectors } from '../../store';
import { inputsActions } from '../../store/actions';
@ -16,7 +17,16 @@ export const useFullScreen = () => {
const timelineFullScreen = useSelector(inputsSelectors.timelineFullScreenSelector) ?? false;
const setGlobalFullScreen = useCallback(
(fullScreen: boolean) => dispatch(inputsActions.setFullScreen({ id: 'global', fullScreen })),
(fullScreen: boolean) => {
if (fullScreen) {
document.body.classList.add(SCROLLING_DISABLED_CLASS_NAME);
} else {
document.body.classList.remove(SCROLLING_DISABLED_CLASS_NAME);
setTimeout(() => window.scrollTo(0, 0), 0);
}
dispatch(inputsActions.setFullScreen({ id: 'global', fullScreen }));
},
[dispatch]
);

View file

@ -156,7 +156,10 @@ export const DetectionEnginePageComponent: React.FC<PropsFromRedux> = ({
{indicesExist ? (
<StickyContainer>
<EuiWindowEvent event="resize" handler={noop} />
<FiltersGlobal show={showGlobalFilters({ globalFullScreen, graphEventId })}>
<FiltersGlobal
globalFullScreen={globalFullScreen}
show={showGlobalFilters({ globalFullScreen, graphEventId })}
>
<SiemSearchBar id="global" indexPattern={indexPattern} />
</FiltersGlobal>

View file

@ -366,7 +366,10 @@ export const RuleDetailsPageComponent: FC<PropsFromRedux> = ({
{indicesExist ? (
<StickyContainer>
<EuiWindowEvent event="resize" handler={noop} />
<FiltersGlobal show={showGlobalFilters({ globalFullScreen, graphEventId })}>
<FiltersGlobal
globalFullScreen={globalFullScreen}
show={showGlobalFilters({ globalFullScreen, graphEventId })}
>
<SiemSearchBar id="global" indexPattern={indexPattern} />
</FiltersGlobal>

View file

@ -104,7 +104,10 @@ const HostDetailsComponent = React.memo<HostDetailsProps & PropsFromRedux>(
{indicesExist ? (
<StickyContainer>
<EuiWindowEvent event="resize" handler={noop} />
<FiltersGlobal show={showGlobalFilters({ globalFullScreen, graphEventId })}>
<FiltersGlobal
globalFullScreen={globalFullScreen}
show={showGlobalFilters({ globalFullScreen, graphEventId })}
>
<SiemSearchBar indexPattern={indexPattern} id="global" />
</FiltersGlobal>

View file

@ -98,7 +98,10 @@ export const HostsComponent = React.memo<HostsComponentProps & PropsFromRedux>(
{indicesExist ? (
<StickyContainer>
<EuiWindowEvent event="resize" handler={noop} />
<FiltersGlobal show={showGlobalFilters({ globalFullScreen, graphEventId })}>
<FiltersGlobal
globalFullScreen={globalFullScreen}
show={showGlobalFilters({ globalFullScreen, graphEventId })}
>
<SiemSearchBar indexPattern={indexPattern} id="global" />
</FiltersGlobal>

View file

@ -90,7 +90,7 @@ export const IPDetailsComponent: React.FC<IPDetailsComponentProps & PropsFromRed
<div data-test-subj="ip-details-page">
{indicesExist ? (
<StickyContainer>
<FiltersGlobal>
<FiltersGlobal globalFullScreen={false}>
<SiemSearchBar indexPattern={indexPattern} id="global" />
</FiltersGlobal>

View file

@ -106,7 +106,10 @@ const NetworkComponent = React.memo<NetworkComponentProps & PropsFromRedux>(
{indicesExist ? (
<StickyContainer>
<EuiWindowEvent event="resize" handler={noop} />
<FiltersGlobal show={showGlobalFilters({ globalFullScreen, graphEventId })}>
<FiltersGlobal
globalFullScreen={globalFullScreen}
show={showGlobalFilters({ globalFullScreen, graphEventId })}
>
<SiemSearchBar indexPattern={indexPattern} id="global" />
</FiltersGlobal>

View file

@ -71,7 +71,7 @@ const OverviewComponent: React.FC<PropsFromRedux> = ({
<>
{indicesExist ? (
<StickyContainer>
<FiltersGlobal>
<FiltersGlobal globalFullScreen={false}>
<SiemSearchBar id="global" indexPattern={indexPattern} />
</FiltersGlobal>

View file

@ -28,6 +28,10 @@ const defaultProps = {
};
describe('FieldName', () => {
beforeEach(() => {
jest.useFakeTimers();
});
test('it renders the field name', () => {
const wrapper = mount(
<TestProviders>
@ -48,6 +52,8 @@ describe('FieldName', () => {
);
wrapper.find('[data-test-subj="withHoverActionsButton"]').at(0).simulate('mouseenter');
wrapper.update();
jest.runAllTimers();
wrapper.update();
expect(wrapper.find('[data-test-subj="copy-to-clipboard"]').exists()).toBe(true);
});