[SIEM] Hide popover after choosing option from Timeline Settings popover (#49578) (#49746)

This commit is contained in:
patrykkopycinski 2019-10-30 18:00:17 +01:00 committed by GitHub
parent 1d6167a283
commit e88a3d48ca
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 173 additions and 232 deletions

View file

@ -22,7 +22,7 @@ import {
} from '../../store/timeline/actions';
import { OpenTimeline } from './open_timeline';
import { OPEN_TIMELINE_CLASS_NAME, queryTimelineById, dispatchUpdateTimeline } from './helpers';
import { OpenTimelineModal } from './open_timeline_modal/open_timeline_modal';
import { OpenTimelineModalBody } from './open_timeline_modal/open_timeline_modal_body';
import {
DeleteTimelines,
EuiSearchBarQuery,
@ -281,7 +281,7 @@ export const StatefulOpenTimelineComponent = React.memo<OpenTimelineOwnProps>(
totalSearchResultsCount={totalCount}
/>
) : (
<OpenTimelineModal
<OpenTimelineModalBody
data-test-subj={'open-timeline-modal'}
deleteTimelines={onDeleteOneTimeline}
defaultPageSize={defaultPageSize}

View file

@ -13,174 +13,32 @@ import { ThemeProvider } from 'styled-components';
import { wait } from '../../../lib/helpers';
import { TestProviderWithoutDragAndDrop } from '../../../mock/test_providers';
import { mockOpenTimelineQueryResults } from '../../../mock/timeline_results';
import * as i18n from '../translations';
import { OpenTimelineModalButton } from '.';
import { OpenTimelineModal } from '.';
jest.mock('../../../lib/settings/use_kibana_ui_setting');
jest.mock('../../../utils/apollo_context', () => ({
useApolloClient: () => ({}),
}));
describe('OpenTimelineModalButton', () => {
describe('OpenTimelineModal', () => {
const theme = () => ({ eui: euiDarkVars, darkMode: true });
test('it renders the expected button text', async () => {
test('it renders the expected modal', async () => {
const wrapper = mount(
<TestProviderWithoutDragAndDrop>
<MockedProvider mocks={mockOpenTimelineQueryResults} addTypename={false}>
<OpenTimelineModalButton onToggle={jest.fn()} />
</MockedProvider>
</TestProviderWithoutDragAndDrop>
<ThemeProvider theme={theme}>
<TestProviderWithoutDragAndDrop>
<MockedProvider mocks={mockOpenTimelineQueryResults} addTypename={false}>
<OpenTimelineModal onClose={jest.fn()} />
</MockedProvider>
</TestProviderWithoutDragAndDrop>
</ThemeProvider>
);
await wait();
wrapper.update();
expect(
wrapper
.find('[data-test-subj="open-timeline-button"]')
.first()
.text()
).toEqual(i18n.OPEN_TIMELINE);
});
describe('statefulness', () => {
test('defaults showModal to false', async () => {
const wrapper = mount(
<ThemeProvider theme={theme}>
<TestProviderWithoutDragAndDrop>
<MockedProvider mocks={mockOpenTimelineQueryResults} addTypename={false}>
<OpenTimelineModalButton onToggle={jest.fn()} />
</MockedProvider>
</TestProviderWithoutDragAndDrop>
</ThemeProvider>
);
await wait();
wrapper.update();
expect(wrapper.find('div[data-test-subj="open-timeline-modal"].euiModal').length).toEqual(0);
});
test('it sets showModal to true when the button is clicked', async () => {
const wrapper = mount(
<ThemeProvider theme={theme}>
<TestProviderWithoutDragAndDrop>
<MockedProvider mocks={mockOpenTimelineQueryResults} addTypename={false}>
<OpenTimelineModalButton onToggle={jest.fn()} />
</MockedProvider>
</TestProviderWithoutDragAndDrop>
</ThemeProvider>
);
await wait();
wrapper
.find('[data-test-subj="open-timeline-button"]')
.first()
.simulate('click');
wrapper.update();
expect(wrapper.find('div[data-test-subj="open-timeline-modal"].euiModal').length).toEqual(1);
});
test('it does NOT render the modal when showModal is false', async () => {
const wrapper = mount(
<TestProviderWithoutDragAndDrop>
<MockedProvider mocks={mockOpenTimelineQueryResults} addTypename={false}>
<OpenTimelineModalButton onToggle={jest.fn()} />
</MockedProvider>
</TestProviderWithoutDragAndDrop>
);
await wait();
wrapper.update();
expect(
wrapper
.find('[data-test-subj="open-timeline-modal"]')
.first()
.exists()
).toBe(false);
});
test('it renders the modal when showModal is true', async () => {
const wrapper = mount(
<ThemeProvider theme={theme}>
<TestProviderWithoutDragAndDrop>
<MockedProvider mocks={mockOpenTimelineQueryResults} addTypename={false}>
<OpenTimelineModalButton onToggle={jest.fn()} />
</MockedProvider>
</TestProviderWithoutDragAndDrop>
</ThemeProvider>
);
await wait();
wrapper.update();
wrapper
.find('[data-test-subj="open-timeline-button"]')
.first()
.simulate('click');
expect(
wrapper
.find('[data-test-subj="open-timeline-modal"]')
.first()
.exists()
).toBe(true);
});
});
describe('onToggle prop', () => {
test('it still correctly updates the showModal state if `onToggle` is not provided as a prop', async () => {
const wrapper = mount(
<ThemeProvider theme={theme}>
<TestProviderWithoutDragAndDrop>
<MockedProvider mocks={mockOpenTimelineQueryResults} addTypename={false}>
<OpenTimelineModalButton onToggle={jest.fn()} />
</MockedProvider>
</TestProviderWithoutDragAndDrop>
</ThemeProvider>
);
await wait();
wrapper
.find('[data-test-subj="open-timeline-button"]')
.first()
.simulate('click');
wrapper.update();
expect(wrapper.find('div[data-test-subj="open-timeline-modal"].euiModal').length).toEqual(1);
});
test('it invokes the optional onToggle function provided as a prop when the open timeline button is clicked', async () => {
const onToggle = jest.fn();
const wrapper = mount(
<ThemeProvider theme={theme}>
<TestProviderWithoutDragAndDrop>
<MockedProvider mocks={mockOpenTimelineQueryResults} addTypename={false}>
<OpenTimelineModalButton onToggle={onToggle} />
</MockedProvider>
</TestProviderWithoutDragAndDrop>
</ThemeProvider>
);
await wait();
wrapper
.find('[data-test-subj="open-timeline-button"]')
.first()
.simulate('click');
wrapper.update();
expect(onToggle).toBeCalled();
});
expect(wrapper.find('div[data-test-subj="open-timeline-modal"].euiModal').length).toEqual(1);
});
});

View file

@ -4,80 +4,42 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { EuiButtonEmpty, EuiModal, EuiOverlayMask } from '@elastic/eui';
import React, { useCallback, useState } from 'react';
import { EuiModal, EuiOverlayMask } from '@elastic/eui';
import React from 'react';
import { ApolloConsumer } from 'react-apollo';
import { useApolloClient } from '../../../utils/apollo_context';
import * as i18n from '../translations';
import { StatefulOpenTimeline } from '..';
export interface OpenTimelineModalButtonProps {
/**
* An optional callback that if specified, will perform arbitrary IO before
* this component updates its internal toggle state.
*/
onToggle?: () => void;
export interface OpenTimelineModalProps {
onClose: () => void;
}
const DEFAULT_SEARCH_RESULTS_PER_PAGE = 10;
const OPEN_TIMELINE_MODAL_WIDTH = 1000; // px
/**
* Renders a button that when clicked, displays the `Open Timelines` modal
*/
export const OpenTimelineModalButton = React.memo<OpenTimelineModalButtonProps>(({ onToggle }) => {
const [showModal, setShowModal] = useState(false);
export const OpenTimelineModal = React.memo<OpenTimelineModalProps>(({ onClose }) => {
const apolloClient = useApolloClient();
/** shows or hides the `Open Timeline` modal */
const openModal = useCallback(() => {
if (onToggle != null) {
onToggle();
}
setShowModal(true);
}, [onToggle]);
const closeModal = useCallback(() => {
if (onToggle != null) {
onToggle();
}
setShowModal(false);
}, [onToggle]);
if (!apolloClient) return null;
return (
<ApolloConsumer>
{client => (
<>
<EuiButtonEmpty
color="text"
data-test-subj="open-timeline-button"
iconSide="left"
iconType="folderOpen"
onClick={openModal}
>
{i18n.OPEN_TIMELINE}
</EuiButtonEmpty>
{showModal && (
<EuiOverlayMask>
<EuiModal
data-test-subj="open-timeline-modal"
maxWidth={OPEN_TIMELINE_MODAL_WIDTH}
onClose={closeModal}
>
<StatefulOpenTimeline
apolloClient={client}
closeModalTimeline={closeModal}
isModal={true}
defaultPageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
title={i18n.OPEN_TIMELINE_TITLE}
/>
</EuiModal>
</EuiOverlayMask>
)}
</>
)}
</ApolloConsumer>
<EuiOverlayMask>
<EuiModal
data-test-subj="open-timeline-modal"
maxWidth={OPEN_TIMELINE_MODAL_WIDTH}
onClose={onClose}
>
<StatefulOpenTimeline
apolloClient={apolloClient}
closeModalTimeline={onClose}
isModal={true}
defaultPageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
title={i18n.OPEN_TIMELINE_TITLE}
/>
</EuiModal>
</EuiOverlayMask>
);
});
OpenTimelineModalButton.displayName = 'OpenTimelineModalButton';
OpenTimelineModal.displayName = 'OpenTimelineModal';

View file

@ -14,7 +14,7 @@ import { DEFAULT_SEARCH_RESULTS_PER_PAGE } from '../../../pages/timelines/timeli
import { OpenTimelineResult } from '../types';
import { TimelinesTableProps } from '../timelines_table';
import { mockTimelineResults } from '../../../mock/timeline_results';
import { OpenTimelineModal } from './open_timeline_modal';
import { OpenTimelineModalBody } from './open_timeline_modal_body';
import { DEFAULT_SORT_DIRECTION, DEFAULT_SORT_FIELD } from '../constants';
jest.mock('../../../lib/settings/use_kibana_ui_setting');
@ -32,7 +32,7 @@ describe('OpenTimelineModal', () => {
test('it renders the title row', () => {
const wrapper = mountWithIntl(
<ThemeProvider theme={theme}>
<OpenTimelineModal
<OpenTimelineModalBody
deleteTimelines={jest.fn()}
defaultPageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
isLoading={false}
@ -70,7 +70,7 @@ describe('OpenTimelineModal', () => {
test('it renders the search row', () => {
const wrapper = mountWithIntl(
<ThemeProvider theme={theme}>
<OpenTimelineModal
<OpenTimelineModalBody
deleteTimelines={jest.fn()}
defaultPageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
isLoading={false}
@ -108,7 +108,7 @@ describe('OpenTimelineModal', () => {
test('it renders the timelines table', () => {
const wrapper = mountWithIntl(
<ThemeProvider theme={theme}>
<OpenTimelineModal
<OpenTimelineModalBody
deleteTimelines={jest.fn()}
defaultPageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
isLoading={false}
@ -146,7 +146,7 @@ describe('OpenTimelineModal', () => {
test('it shows extended columns and actions when onDeleteSelected and deleteTimelines are specified', () => {
const wrapper = mountWithIntl(
<ThemeProvider theme={theme}>
<OpenTimelineModal
<OpenTimelineModalBody
deleteTimelines={jest.fn()}
defaultPageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
isLoading={false}
@ -184,7 +184,7 @@ describe('OpenTimelineModal', () => {
test('it does NOT show extended columns and actions when is onDeleteSelected undefined and deleteTimelines is specified', () => {
const wrapper = mountWithIntl(
<ThemeProvider theme={theme}>
<OpenTimelineModal
<OpenTimelineModalBody
deleteTimelines={jest.fn()}
defaultPageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
isLoading={false}
@ -221,7 +221,7 @@ describe('OpenTimelineModal', () => {
test('it does NOT show extended columns and actions when is onDeleteSelected provided and deleteTimelines is undefined', () => {
const wrapper = mountWithIntl(
<ThemeProvider theme={theme}>
<OpenTimelineModal
<OpenTimelineModalBody
defaultPageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
isLoading={false}
itemIdToExpandedNotesRowMap={{}}
@ -258,7 +258,7 @@ describe('OpenTimelineModal', () => {
test('it does NOT show extended columns and actions when both onDeleteSelected and deleteTimelines are undefined', () => {
const wrapper = mountWithIntl(
<ThemeProvider theme={theme}>
<OpenTimelineModal
<OpenTimelineModalBody
defaultPageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE}
isLoading={false}
itemIdToExpandedNotesRowMap={{}}

View file

@ -20,7 +20,7 @@ export const HeaderContainer = styled.div`
HeaderContainer.displayName = 'HeaderContainer';
export const OpenTimelineModal = pure<OpenTimelineProps>(
export const OpenTimelineModalBody = pure<OpenTimelineProps>(
({
deleteTimelines,
defaultPageSize,
@ -91,4 +91,4 @@ export const OpenTimelineModal = pure<OpenTimelineProps>(
)
);
OpenTimelineModal.displayName = 'OpenTimelineModal';
OpenTimelineModalBody.displayName = 'OpenTimelineModalBody';

View file

@ -0,0 +1,71 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import euiDarkVars from '@elastic/eui/dist/eui_theme_dark.json';
import { mount } from 'enzyme';
import * as React from 'react';
import { MockedProvider } from 'react-apollo/test-utils';
import { ThemeProvider } from 'styled-components';
import { wait } from '../../../lib/helpers';
import { TestProviderWithoutDragAndDrop } from '../../../mock/test_providers';
import { mockOpenTimelineQueryResults } from '../../../mock/timeline_results';
import * as i18n from '../translations';
import { OpenTimelineModalButton } from './open_timeline_modal_button';
jest.mock('../../../lib/settings/use_kibana_ui_setting');
describe('OpenTimelineModalButton', () => {
const theme = () => ({ eui: euiDarkVars, darkMode: true });
test('it renders the expected button text', async () => {
const wrapper = mount(
<TestProviderWithoutDragAndDrop>
<MockedProvider mocks={mockOpenTimelineQueryResults} addTypename={false}>
<OpenTimelineModalButton onClick={jest.fn()} />
</MockedProvider>
</TestProviderWithoutDragAndDrop>
);
await wait();
wrapper.update();
expect(
wrapper
.find('[data-test-subj="open-timeline-button"]')
.first()
.text()
).toEqual(i18n.OPEN_TIMELINE);
});
describe('onClick prop', () => {
test('it invokes onClick function provided as a prop when the button is clicked', async () => {
const onClick = jest.fn();
const wrapper = mount(
<ThemeProvider theme={theme}>
<TestProviderWithoutDragAndDrop>
<MockedProvider mocks={mockOpenTimelineQueryResults} addTypename={false}>
<OpenTimelineModalButton onClick={onClick} />
</MockedProvider>
</TestProviderWithoutDragAndDrop>
</ThemeProvider>
);
await wait();
wrapper
.find('[data-test-subj="open-timeline-button"]')
.first()
.simulate('click');
wrapper.update();
expect(onClick).toBeCalled();
});
});
});

View file

@ -0,0 +1,28 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { EuiButtonEmpty } from '@elastic/eui';
import React from 'react';
import * as i18n from '../translations';
export interface OpenTimelineModalButtonProps {
onClick: () => void;
}
export const OpenTimelineModalButton = React.memo<OpenTimelineModalButtonProps>(({ onClick }) => (
<EuiButtonEmpty
color="text"
data-test-subj="open-timeline-button"
iconSide="left"
iconType="folderOpen"
onClick={onClick}
>
{i18n.OPEN_TIMELINE}
</EuiButtonEmpty>
));
OpenTimelineModalButton.displayName = 'OpenTimelineModalButton';

View file

@ -113,6 +113,7 @@ export const Properties = React.memo<Props>(
}) => {
const [showActions, setShowActions] = useState(false);
const [showNotes, setShowNotes] = useState(false);
const [showTimelineModal, setShowTimelineModal] = useState(false);
const onButtonClick = useCallback(() => {
setShowActions(!showActions);
@ -126,6 +127,15 @@ export const Properties = React.memo<Props>(
setShowActions(false);
}, []);
const onOpenTimelineModal = useCallback(() => {
onClosePopover();
setShowTimelineModal(true);
}, []);
const onCloseTimelineModal = useCallback(() => {
setShowTimelineModal(false);
}, []);
const datePickerWidth =
width -
rightGutter -
@ -173,11 +183,14 @@ export const Properties = React.memo<Props>(
noteIds={noteIds}
onButtonClick={onButtonClick}
onClosePopover={onClosePopover}
onCloseTimelineModal={onCloseTimelineModal}
onOpenTimelineModal={onOpenTimelineModal}
onToggleShowNotes={onToggleShowNotes}
showActions={showActions}
showDescription={width < showDescriptionThreshold}
showNotes={showNotes}
showNotesFromWidth={width < showNotesThreshold}
showTimelineModal={showTimelineModal}
showUsersView={title.length > 0}
timelineId={timelineId}
updateDescription={updateDescription}

View file

@ -15,7 +15,8 @@ import {
EuiAvatar,
} from '@elastic/eui';
import { NewTimeline, Description, NotesButton } from './helpers';
import { OpenTimelineModalButton } from '../../open_timeline/open_timeline_modal';
import { OpenTimelineModalButton } from '../../open_timeline/open_timeline_modal/open_timeline_modal_button';
import { OpenTimelineModal } from '../../open_timeline/open_timeline_modal';
import { InspectButton } from '../../inspect';
import * as i18n from './translations';
@ -75,6 +76,9 @@ interface Props {
getNotesByIds: (noteIds: string[]) => Note[];
noteIds: string[];
onToggleShowNotes: () => void;
onCloseTimelineModal: () => void;
onOpenTimelineModal: () => void;
showTimelineModal: boolean;
updateNote: UpdateNote;
}
@ -98,6 +102,9 @@ export const PropertiesRight = React.memo<Props>(
noteIds,
onToggleShowNotes,
updateNote,
showTimelineModal,
onCloseTimelineModal,
onOpenTimelineModal,
}) => (
<PropertiesRightStyle alignItems="flexStart" data-test-subj="properties-right" gutterSize="s">
<EuiFlexItem grow={false}>
@ -125,7 +132,7 @@ export const PropertiesRight = React.memo<Props>(
</EuiFlexItem>
<EuiFlexItem grow={false}>
<OpenTimelineModalButton />
<OpenTimelineModalButton onClick={onOpenTimelineModal} />
</EuiFlexItem>
<EuiFlexItem grow={false}>
@ -186,6 +193,8 @@ export const PropertiesRight = React.memo<Props>(
</HiddenFlexItem>
))
: null}
{showTimelineModal ? <OpenTimelineModal onClose={onCloseTimelineModal} /> : null}
</PropertiesRightStyle>
)
);