mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[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:  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:
parent
5e8e01fd0f
commit
dca4a23597
17 changed files with 206 additions and 34 deletions
|
@ -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';
|
||||
|
|
|
@ -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]')
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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`
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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)`
|
||||
|
|
|
@ -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(
|
||||
() => (
|
||||
|
|
|
@ -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]
|
||||
);
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -71,7 +71,7 @@ const OverviewComponent: React.FC<PropsFromRedux> = ({
|
|||
<>
|
||||
{indicesExist ? (
|
||||
<StickyContainer>
|
||||
<FiltersGlobal>
|
||||
<FiltersGlobal globalFullScreen={false}>
|
||||
<SiemSearchBar id="global" indexPattern={indexPattern} />
|
||||
</FiltersGlobal>
|
||||
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue