From 9b92f8423c753e605730f5620420a819a1c11f60 Mon Sep 17 00:00:00 2001 From: Frank Hassanabad Date: Fri, 16 Aug 2019 19:35:33 -0600 Subject: [PATCH] [SIEM] Adds performance enhancements such by removing wasted renderers and adding incremental DOM rendering (#43157) ## Summary Whenever you wiggle the "resize" bar on the timeline or any other action within the UI which alters the redux store, all the components `mapStateProps` will and redux will re-render if it sees any of the properties have a shallow compare that returns false. Ref: https://react-redux.js.org/using-react-redux/connect-mapstate This PR fixes Super Date Selector which unnecessarily causes re-renders due to not using selectors or memoizations per guidance from the react-redux project. --- Super Date Selector will re-render on every single change to the redux store regardless of if it needs to render or not. Screen Shot 2019-08-12 at 11 49 13 AM You can see that with something as simple as a slight wiggle of the resizer of timeline causes around 23'ish re-renders Screen Shot 2019-08-12 at 11 52 54 AM After the fixes it no longer shows it being re-rendered on a redux change and the number or renders drop significantly when resizing timeline. It renders only once: Screen Shot 2019-08-12 at 3 51 20 PM --- Some parts of the application are expensive to render and are wasted renders by something such as anonymous functions. By swapping out the `pure` for `React.memo` and using the property compare we can cut out those renders and speed up the application considerably. Before with all the flyout sections causing re-renders when state did not change: Screen Shot 2019-08-12 at 4 46 29 PM After with the fix to where the ms is really cheap and the expensive pieces are not rendered unless needed now: Screen Shot 2019-08-13 at 10 32 55 AM --- For the timeline and its properties section it contained a large volume of JSX logic called "PropertiesLeft" and "PropertiesRight" in which both were having re-renders involving expensive calculations such as width changes to the date time when you did something as simple as type in a new title or description into the timeline. I broke those two out into PropertiesLeft and PropertiesRight subsections utilizing `React.Memo` for performance improvements. Before when typing a title of timeline, PropertiesRight renders 49+ times: properties-right-before Now it renders only 2 times and most of the time it is skipped with something as simple as a title changing: properties-right-after --- When rending feature rich renderers on the timeline it was having a difficult time rendering them all within a reasonable amount of time. This introduces a simple scheduler of incremental loading using `requestIdleCallback` when it is available from the browser, otherwise this will fall back on a `setTimeout` as a polyfill/shim. Ref: https://developer.mozilla.org/en-US/docs/Web/API/Window/requestIdleCallback https://developers.google.com/web/updates/2015/08/using-requestidlecallback https://www.w3.org/TR/requestidlecallback/ Tested on Safari, Firefox, and Chrome. IE-11 looked like it didn't crash :-) Gif of each of the browsers incrementally rendering the rows: Chrome: ![incremental-load-1](https://user-images.githubusercontent.com/1151048/63057036-8fec7e00-bea6-11e9-83f9-7bad3f79746f.gif) ![incremental-load-2](https://user-images.githubusercontent.com/1151048/63057043-92e76e80-bea6-11e9-8f0d-95ea2559b6c7.gif) Firefox: ![firefox-incremental-1](https://user-images.githubusercontent.com/1151048/63057052-97138c00-bea6-11e9-8e15-19f5b7cb0a5e.gif) ![firefox-incremental-2](https://user-images.githubusercontent.com/1151048/63057060-98dd4f80-bea6-11e9-9bf3-c8ae798db9e4.gif) Safari (polyfil/shim only working): ![safari-polyfill-1](https://user-images.githubusercontent.com/1151048/63057099-adb9e300-bea6-11e9-9b94-32a08b5905ad.gif) ![safari-polyfill-2](https://user-images.githubusercontent.com/1151048/63057101-b01c3d00-bea6-11e9-9261-77444272992d.gif) Before Performance picture of the full load. You can see that it will render all rows of 25+ and that causes a lag of up to 2 seconds depending on how complex the renderers are: Screen Shot 2019-08-13 at 4 24 56 PM Afterwards you can see it chops it more evenly up between the rendering and tries to get in 5 at a time and maintain a 60 FPS: Screen Shot 2019-08-14 at 3 59 31 PM ### Checklist Use ~~strikethroughs~~ to remove checklist items you don't feel are applicable to this PR. ~~- [ ] This was checked for cross-browser compatibility, [including a check against IE11](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#cross-browser-compatibility)~~ ~~- [ ] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/master/packages/kbn-i18n/README.md)~~ ~~- [ ] [Documentation](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#writing-documentation) was added for features that require explanation or tutorials~~ - [x] [Unit or functional tests](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#cross-browser-compatibility) were updated or added to match the most common scenarios ~~- [ ] This was checked for [keyboard-only and screenreader accessibility](https://developer.mozilla.org/en-US/docs/Learn/Tools_and_testing/Cross_browser_testing/Accessibility#Accessibility_testing_checklist)~~ ### For maintainers - [x] This was checked for breaking API changes and was [labeled appropriately](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#release-notes-process) - [x] This includes a feature addition or change that requires a release note and was [labeled appropriately](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#release-notes-process) --- .../__snapshots__/event_details.test.tsx.snap | 1 - .../components/event_details/columns.tsx | 3 - .../event_details/event_details.test.tsx | 3 - .../event_details/event_details.tsx | 6 +- .../event_fields_browser.test.tsx | 9 - .../event_details/event_fields_browser.tsx | 16 +- .../event_details/stateful_event_details.tsx | 4 - .../fields_browser/categories_pane.test.tsx | 2 - .../fields_browser/categories_pane.tsx | 11 +- .../fields_browser/category.test.tsx | 3 - .../fields_browser/category_columns.test.tsx | 6 - .../fields_browser/category_columns.tsx | 61 +- .../fields_browser/field_browser.test.tsx | 7 - .../fields_browser/field_browser.tsx | 11 +- .../fields_browser/field_items.test.tsx | 7 - .../components/fields_browser/field_items.tsx | 3 - .../fields_browser/field_name.test.tsx | 5 - .../components/fields_browser/field_name.tsx | 57 +- .../fields_browser/fields_pane.test.tsx | 4 - .../components/fields_browser/fields_pane.tsx | 7 +- .../components/fields_browser/index.test.tsx | 4 - .../components/fields_browser/index.tsx | 2 - .../public/components/fields_browser/types.ts | 2 - .../public/components/flyout/button/index.tsx | 56 +- .../public/components/flyout/header/index.tsx | 7 +- .../public/components/flyout/index.test.tsx | 2 + .../siem/public/components/flyout/index.tsx | 19 +- .../public/components/flyout/pane/index.tsx | 38 +- .../super_date_picker/index.test.tsx | 119 +- .../components/super_date_picker/index.tsx | 51 +- .../super_date_picker/selectors.test.ts | 399 ++++++ .../components/super_date_picker/selectors.ts | 68 + .../__snapshots__/timeline.test.tsx.snap | 792 +----------- .../timeline/body/actions/index.tsx | 18 +- .../__snapshots__/index.test.tsx.snap | 1098 ++++++++++------- .../body/column_headers/actions/index.tsx | 52 +- .../body/column_headers/header/index.test.tsx | 12 - .../body/column_headers/header/index.tsx | 46 +- .../body/column_headers/index.test.tsx | 4 - .../timeline/body/column_headers/index.tsx | 8 +- .../body/events/event_column_view.tsx | 143 ++- .../components/timeline/body/events/index.tsx | 116 +- .../timeline/body/events/stateful_event.tsx | 105 +- .../body/events/stateful_event_child.tsx | 114 ++ .../components/timeline/body/index.test.tsx | 10 +- .../public/components/timeline/body/index.tsx | 7 +- .../timeline/body/stateful_body.tsx | 3 - .../timeline/expandable_event/index.tsx | 79 +- .../components/timeline/footer/index.test.tsx | 22 +- .../components/timeline/footer/index.tsx | 13 +- .../header/__snapshots__/index.test.tsx.snap | 432 +++---- .../components/timeline/header/index.tsx | 3 +- .../components/timeline/properties/index.tsx | 236 +--- .../timeline/properties/properties_left.tsx | 164 +++ .../timeline/properties/properties_right.tsx | 193 +++ .../components/timeline/properties/styles.tsx | 14 +- .../public/components/timeline/timeline.tsx | 69 +- .../siem/public/lib/helpers/scheduler.ts | 95 ++ .../siem/public/pages/timelines/index.tsx | 3 +- 59 files changed, 2581 insertions(+), 2263 deletions(-) create mode 100644 x-pack/legacy/plugins/siem/public/components/super_date_picker/selectors.test.ts create mode 100644 x-pack/legacy/plugins/siem/public/components/super_date_picker/selectors.ts create mode 100644 x-pack/legacy/plugins/siem/public/components/timeline/body/events/stateful_event_child.tsx create mode 100644 x-pack/legacy/plugins/siem/public/components/timeline/properties/properties_left.tsx create mode 100644 x-pack/legacy/plugins/siem/public/components/timeline/properties/properties_right.tsx create mode 100644 x-pack/legacy/plugins/siem/public/lib/helpers/scheduler.ts diff --git a/x-pack/legacy/plugins/siem/public/components/event_details/__snapshots__/event_details.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/event_details/__snapshots__/event_details.test.tsx.snap index af0b6cea7dcc81..e8606c89a56dc3 100644 --- a/x-pack/legacy/plugins/siem/public/components/event_details/__snapshots__/event_details.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/event_details/__snapshots__/event_details.test.tsx.snap @@ -681,7 +681,6 @@ In other use cases the message field can be used to concatenate different values ] } id="Y-6TfmcB0WOhS6qyMv3s" - isLoading={false} onUpdateColumns={[MockFunction]} onViewSelected={[MockFunction]} timelineId="test" diff --git a/x-pack/legacy/plugins/siem/public/components/event_details/columns.tsx b/x-pack/legacy/plugins/siem/public/components/event_details/columns.tsx index c2675c21e1454a..fc985b3c360bdb 100644 --- a/x-pack/legacy/plugins/siem/public/components/event_details/columns.tsx +++ b/x-pack/legacy/plugins/siem/public/components/event_details/columns.tsx @@ -65,7 +65,6 @@ export const getColumns = ({ browserFields, columnHeaders, eventId, - isLoading, onUpdateColumns, timelineId, toggleColumn, @@ -73,7 +72,6 @@ export const getColumns = ({ browserFields: BrowserFields; columnHeaders: ColumnHeader[]; eventId: string; - isLoading: boolean; onUpdateColumns: OnUpdateColumns; timelineId: string; toggleColumn: (column: ColumnHeader) => void; @@ -146,7 +144,6 @@ export const getColumns = ({ })} data-test-subj="field-name" fieldId={field} - isLoading={isLoading} onUpdateColumns={onUpdateColumns} /> ) : ( diff --git a/x-pack/legacy/plugins/siem/public/components/event_details/event_details.test.tsx b/x-pack/legacy/plugins/siem/public/components/event_details/event_details.test.tsx index 80f605a9c412f6..3e970c86e0b17e 100644 --- a/x-pack/legacy/plugins/siem/public/components/event_details/event_details.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/event_details/event_details.test.tsx @@ -25,7 +25,6 @@ describe('EventDetails', () => { columnHeaders={defaultHeaders} data={mockDetailItemData} id={mockDetailItemDataId} - isLoading={false} view="table-view" onUpdateColumns={jest.fn()} onViewSelected={jest.fn()} @@ -48,7 +47,6 @@ describe('EventDetails', () => { columnHeaders={defaultHeaders} data={mockDetailItemData} id={mockDetailItemDataId} - isLoading={false} view="table-view" onUpdateColumns={jest.fn()} onViewSelected={jest.fn()} @@ -75,7 +73,6 @@ describe('EventDetails', () => { columnHeaders={defaultHeaders} data={mockDetailItemData} id={mockDetailItemDataId} - isLoading={false} view="table-view" onUpdateColumns={jest.fn()} onViewSelected={jest.fn()} diff --git a/x-pack/legacy/plugins/siem/public/components/event_details/event_details.tsx b/x-pack/legacy/plugins/siem/public/components/event_details/event_details.tsx index a4986973ec4f77..fbea96e3c9b3ef 100644 --- a/x-pack/legacy/plugins/siem/public/components/event_details/event_details.tsx +++ b/x-pack/legacy/plugins/siem/public/components/event_details/event_details.tsx @@ -6,7 +6,6 @@ import { EuiTabbedContent, EuiTabbedContentTab } from '@elastic/eui'; import * as React from 'react'; -import { pure } from 'recompose'; import styled from 'styled-components'; import { BrowserFields } from '../../containers/source'; @@ -25,7 +24,6 @@ interface Props { columnHeaders: ColumnHeader[]; data: DetailItem[]; id: string; - isLoading: boolean; view: View; onUpdateColumns: OnUpdateColumns; onViewSelected: (selected: View) => void; @@ -40,13 +38,12 @@ const Details = styled.div` Details.displayName = 'Details'; -export const EventDetails = pure( +export const EventDetails = React.memo( ({ browserFields, columnHeaders, data, id, - isLoading, view, onUpdateColumns, onViewSelected, @@ -63,7 +60,6 @@ export const EventDetails = pure( columnHeaders={columnHeaders} data={data} eventId={id} - isLoading={isLoading} onUpdateColumns={onUpdateColumns} timelineId={timelineId} toggleColumn={toggleColumn} diff --git a/x-pack/legacy/plugins/siem/public/components/event_details/event_fields_browser.test.tsx b/x-pack/legacy/plugins/siem/public/components/event_details/event_fields_browser.test.tsx index 3cda3debe7efbf..66ec391057d99b 100644 --- a/x-pack/legacy/plugins/siem/public/components/event_details/event_fields_browser.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/event_details/event_fields_browser.test.tsx @@ -25,7 +25,6 @@ describe('EventFieldsBrowser', () => { columnHeaders={defaultHeaders} data={mockDetailItemData} eventId={mockDetailItemDataId} - isLoading={false} onUpdateColumns={jest.fn()} timelineId="test" toggleColumn={jest.fn()} @@ -47,7 +46,6 @@ describe('EventFieldsBrowser', () => { columnHeaders={defaultHeaders} data={mockDetailItemData} eventId={mockDetailItemDataId} - isLoading={false} onUpdateColumns={jest.fn()} timelineId="test" toggleColumn={jest.fn()} @@ -74,7 +72,6 @@ describe('EventFieldsBrowser', () => { columnHeaders={defaultHeaders} data={mockDetailItemData} eventId={eventId} - isLoading={false} onUpdateColumns={jest.fn()} timelineId="test" toggleColumn={jest.fn()} @@ -100,7 +97,6 @@ describe('EventFieldsBrowser', () => { columnHeaders={defaultHeaders} data={mockDetailItemData} eventId={eventId} - isLoading={false} onUpdateColumns={jest.fn()} timelineId="test" toggleColumn={jest.fn()} @@ -127,7 +123,6 @@ describe('EventFieldsBrowser', () => { columnHeaders={defaultHeaders} data={mockDetailItemData} eventId={eventId} - isLoading={false} onUpdateColumns={jest.fn()} timelineId="test" toggleColumn={toggleColumn} @@ -161,7 +156,6 @@ describe('EventFieldsBrowser', () => { columnHeaders={defaultHeaders} data={mockDetailItemData} eventId={mockDetailItemDataId} - isLoading={false} onUpdateColumns={jest.fn()} timelineId="test" toggleColumn={jest.fn()} @@ -189,7 +183,6 @@ describe('EventFieldsBrowser', () => { columnHeaders={defaultHeaders} data={mockDetailItemData} eventId={mockDetailItemDataId} - isLoading={false} onUpdateColumns={jest.fn()} timelineId="test" toggleColumn={jest.fn()} @@ -214,7 +207,6 @@ describe('EventFieldsBrowser', () => { columnHeaders={defaultHeaders} data={mockDetailItemData} eventId={mockDetailItemDataId} - isLoading={false} onUpdateColumns={jest.fn()} timelineId="test" toggleColumn={jest.fn()} @@ -239,7 +231,6 @@ describe('EventFieldsBrowser', () => { columnHeaders={defaultHeaders} data={mockDetailItemData} eventId={mockDetailItemDataId} - isLoading={false} onUpdateColumns={jest.fn()} timelineId="test" toggleColumn={jest.fn()} diff --git a/x-pack/legacy/plugins/siem/public/components/event_details/event_fields_browser.tsx b/x-pack/legacy/plugins/siem/public/components/event_details/event_fields_browser.tsx index 3d4bef53fd2439..f796a9e69cd969 100644 --- a/x-pack/legacy/plugins/siem/public/components/event_details/event_fields_browser.tsx +++ b/x-pack/legacy/plugins/siem/public/components/event_details/event_fields_browser.tsx @@ -10,7 +10,6 @@ import { EuiInMemoryTable, } from '@elastic/eui'; import * as React from 'react'; -import { pure } from 'recompose'; import { ColumnHeader } from '../timeline/body/column_headers/column_header'; import { BrowserFields, getAllFieldsByName } from '../../containers/source'; @@ -25,24 +24,14 @@ interface Props { columnHeaders: ColumnHeader[]; data: DetailItem[]; eventId: string; - isLoading: boolean; onUpdateColumns: OnUpdateColumns; timelineId: string; toggleColumn: (column: ColumnHeader) => void; } /** Renders a table view or JSON view of the `ECS` `data` */ -export const EventFieldsBrowser = pure( - ({ - browserFields, - columnHeaders, - data, - eventId, - isLoading, - onUpdateColumns, - timelineId, - toggleColumn, - }) => { +export const EventFieldsBrowser = React.memo( + ({ browserFields, columnHeaders, data, eventId, onUpdateColumns, timelineId, toggleColumn }) => { const fieldsByName = getAllFieldsByName(browserFields); return ( ( browserFields, columnHeaders, eventId, - isLoading, onUpdateColumns, timelineId, toggleColumn, diff --git a/x-pack/legacy/plugins/siem/public/components/event_details/stateful_event_details.tsx b/x-pack/legacy/plugins/siem/public/components/event_details/stateful_event_details.tsx index 02d2b28664a3ee..ec76d8f90c3de9 100644 --- a/x-pack/legacy/plugins/siem/public/components/event_details/stateful_event_details.tsx +++ b/x-pack/legacy/plugins/siem/public/components/event_details/stateful_event_details.tsx @@ -18,7 +18,6 @@ interface Props { columnHeaders: ColumnHeader[]; data: DetailItem[]; id: string; - isLoading: boolean; onUpdateColumns: OnUpdateColumns; timelineId: string; toggleColumn: (column: ColumnHeader) => void; @@ -45,19 +44,16 @@ export class StatefulEventDetails extends React.PureComponent { columnHeaders, data, id, - isLoading, onUpdateColumns, timelineId, toggleColumn, } = this.props; - return ( { browserFields={mockBrowserFields} filteredBrowserFields={mockBrowserFields} width={CATEGORY_PANE_WIDTH} - isLoading={false} onCategorySelected={jest.fn()} onUpdateColumns={jest.fn()} selectedCategoryId={''} @@ -45,7 +44,6 @@ describe('CategoriesPane', () => { browserFields={mockBrowserFields} filteredBrowserFields={{}} width={CATEGORY_PANE_WIDTH} - isLoading={false} onCategorySelected={jest.fn()} onUpdateColumns={jest.fn()} selectedCategoryId={''} diff --git a/x-pack/legacy/plugins/siem/public/components/fields_browser/categories_pane.tsx b/x-pack/legacy/plugins/siem/public/components/fields_browser/categories_pane.tsx index 7423565e420877..81b62ebf2c860e 100644 --- a/x-pack/legacy/plugins/siem/public/components/fields_browser/categories_pane.tsx +++ b/x-pack/legacy/plugins/siem/public/components/fields_browser/categories_pane.tsx @@ -6,7 +6,6 @@ import { EuiInMemoryTable, EuiTitle } from '@elastic/eui'; import * as React from 'react'; -import { pure } from 'recompose'; import styled from 'styled-components'; import { BrowserFields } from '../../containers/source'; @@ -35,10 +34,7 @@ const Title = styled(EuiTitle)` Title.displayName = 'Title'; -type Props = Pick< - FieldBrowserProps, - 'browserFields' | 'isLoading' | 'timelineId' | 'onUpdateColumns' -> & { +type Props = Pick & { /** * A map of categoryId -> metadata about the fields in that category, * filtered such that the name of every field in the category includes @@ -55,11 +51,11 @@ type Props = Pick< /** The width of the categories pane */ width: number; }; -export const CategoriesPane = pure( + +export const CategoriesPane = React.memo( ({ browserFields, filteredBrowserFields, - isLoading, onCategorySelected, onUpdateColumns, selectedCategoryId, @@ -76,7 +72,6 @@ export const CategoriesPane = pure( columns={getCategoryColumns({ browserFields, filteredBrowserFields, - isLoading, onCategorySelected, onUpdateColumns, selectedCategoryId, diff --git a/x-pack/legacy/plugins/siem/public/components/fields_browser/category.test.tsx b/x-pack/legacy/plugins/siem/public/components/fields_browser/category.test.tsx index 3a7e170bde6a06..ce95deca54bfc7 100644 --- a/x-pack/legacy/plugins/siem/public/components/fields_browser/category.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/fields_browser/category.test.tsx @@ -33,7 +33,6 @@ describe('Category', () => { categoryId: selectedCategoryId, columnHeaders: [], highlight: '', - isLoading: false, onUpdateColumns: jest.fn(), timelineId, toggleColumn: jest.fn(), @@ -66,7 +65,6 @@ describe('Category', () => { categoryId: selectedCategoryId, columnHeaders: [], highlight: '', - isLoading: false, onUpdateColumns: jest.fn(), timelineId, toggleColumn: jest.fn(), @@ -99,7 +97,6 @@ describe('Category', () => { categoryId: selectedCategoryId, columnHeaders: [], highlight: '', - isLoading: false, onUpdateColumns: jest.fn(), timelineId, toggleColumn: jest.fn(), diff --git a/x-pack/legacy/plugins/siem/public/components/fields_browser/category_columns.test.tsx b/x-pack/legacy/plugins/siem/public/components/fields_browser/category_columns.test.tsx index 3ae931b2b222da..4a096a33f9a691 100644 --- a/x-pack/legacy/plugins/siem/public/components/fields_browser/category_columns.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/fields_browser/category_columns.test.tsx @@ -24,7 +24,6 @@ describe('getCategoryColumns', () => { browserFields={mockBrowserFields} filteredBrowserFields={mockBrowserFields} width={CATEGORY_PANE_WIDTH} - isLoading={false} onCategorySelected={jest.fn()} onUpdateColumns={jest.fn()} selectedCategoryId={''} @@ -50,7 +49,6 @@ describe('getCategoryColumns', () => { browserFields={mockBrowserFields} filteredBrowserFields={mockBrowserFields} width={CATEGORY_PANE_WIDTH} - isLoading={false} onCategorySelected={jest.fn()} onUpdateColumns={jest.fn()} selectedCategoryId={''} @@ -75,7 +73,6 @@ describe('getCategoryColumns', () => { browserFields={mockBrowserFields} filteredBrowserFields={mockBrowserFields} width={CATEGORY_PANE_WIDTH} - isLoading={false} onCategorySelected={jest.fn()} onUpdateColumns={jest.fn()} selectedCategoryId={''} @@ -103,7 +100,6 @@ describe('getCategoryColumns', () => { browserFields={mockBrowserFields} filteredBrowserFields={mockBrowserFields} width={CATEGORY_PANE_WIDTH} - isLoading={false} onCategorySelected={jest.fn()} onUpdateColumns={jest.fn()} selectedCategoryId={selectedCategoryId} @@ -127,7 +123,6 @@ describe('getCategoryColumns', () => { browserFields={mockBrowserFields} filteredBrowserFields={mockBrowserFields} width={CATEGORY_PANE_WIDTH} - isLoading={false} onCategorySelected={jest.fn()} onUpdateColumns={jest.fn()} selectedCategoryId={selectedCategoryId} @@ -153,7 +148,6 @@ describe('getCategoryColumns', () => { browserFields={mockBrowserFields} filteredBrowserFields={mockBrowserFields} width={CATEGORY_PANE_WIDTH} - isLoading={false} onCategorySelected={onCategorySelected} onUpdateColumns={jest.fn()} selectedCategoryId={selectedCategoryId} diff --git a/x-pack/legacy/plugins/siem/public/components/fields_browser/category_columns.tsx b/x-pack/legacy/plugins/siem/public/components/fields_browser/category_columns.tsx index 0cbf188a7f27f1..94b16e61010f53 100644 --- a/x-pack/legacy/plugins/siem/public/components/fields_browser/category_columns.tsx +++ b/x-pack/legacy/plugins/siem/public/components/fields_browser/category_columns.tsx @@ -8,6 +8,7 @@ import { EuiIcon, EuiFlexGroup, EuiFlexItem, EuiLink, EuiPanel, EuiToolTip } fro import * as React from 'react'; import styled from 'styled-components'; +import { useContext } from 'react'; import { BrowserFields } from '../../containers/source'; import { getColumnsWithTimestamp } from '../event_details/helpers'; import { OnUpdateColumns } from '../timeline/events'; @@ -16,6 +17,7 @@ import { WithHoverActions } from '../with_hover_actions'; import * as i18n from './translations'; import { CountBadge } from '../page'; import { LoadingSpinner, getCategoryPaneCategoryClassName, getFieldCount } from './helpers'; +import { TimelineContext } from '../timeline/timeline_context'; const CategoryName = styled.span<{ bold: boolean }>` font-weight: ${({ bold }) => (bold ? 'bold' : 'normal')}; @@ -56,13 +58,45 @@ export interface CategoryItem { categoryId: string; } +interface ToolTipProps { + categoryId: string; + browserFields: BrowserFields; + onUpdateColumns: OnUpdateColumns; +} + +const ToolTip = React.memo(({ categoryId, browserFields, onUpdateColumns }) => { + const { isLoading } = useContext(TimelineContext); + return ( + + {!isLoading ? ( + { + onUpdateColumns( + getColumnsWithTimestamp({ + browserFields, + category: categoryId, + }) + ); + }} + type="visTable" + /> + ) : ( + + )} + + ); +}); + +ToolTip.displayName = 'ToolTip'; + /** * Returns the column definition for the (single) column that displays all the * category names in the field browser */ export const getCategoryColumns = ({ browserFields, filteredBrowserFields, - isLoading, onCategorySelected, onUpdateColumns, selectedCategoryId, @@ -70,7 +104,6 @@ export const getCategoryColumns = ({ }: { browserFields: BrowserFields; filteredBrowserFields: BrowserFields; - isLoading: boolean; onCategorySelected: (categoryId: string) => void; onUpdateColumns: OnUpdateColumns; selectedCategoryId: string; @@ -96,25 +129,11 @@ export const getCategoryColumns = ({ justifyContent="spaceBetween" > - - {!isLoading ? ( - { - onUpdateColumns( - getColumnsWithTimestamp({ - browserFields, - category: categoryId, - }) - ); - }} - type="visTable" - /> - ) : ( - - )} - + diff --git a/x-pack/legacy/plugins/siem/public/components/fields_browser/field_browser.test.tsx b/x-pack/legacy/plugins/siem/public/components/fields_browser/field_browser.test.tsx index 91b561a156f9ea..c43d5833fe1da2 100644 --- a/x-pack/legacy/plugins/siem/public/components/fields_browser/field_browser.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/fields_browser/field_browser.test.tsx @@ -45,7 +45,6 @@ describe('FieldsBrowser', () => { filteredBrowserFields={mockBrowserFields} searchInput={''} height={FIELD_BROWSER_HEIGHT} - isLoading={false} isSearching={false} onCategorySelected={jest.fn()} onHideFieldBrowser={jest.fn()} @@ -83,7 +82,6 @@ describe('FieldsBrowser', () => { filteredBrowserFields={mockBrowserFields} searchInput={''} height={FIELD_BROWSER_HEIGHT} - isLoading={false} isSearching={false} onCategorySelected={jest.fn()} onFieldSelected={jest.fn()} @@ -115,7 +113,6 @@ describe('FieldsBrowser', () => { filteredBrowserFields={mockBrowserFields} searchInput={''} height={FIELD_BROWSER_HEIGHT} - isLoading={false} isSearching={false} onCategorySelected={jest.fn()} onHideFieldBrowser={jest.fn()} @@ -142,7 +139,6 @@ describe('FieldsBrowser', () => { filteredBrowserFields={mockBrowserFields} searchInput={''} height={FIELD_BROWSER_HEIGHT} - isLoading={false} isSearching={false} onCategorySelected={jest.fn()} onHideFieldBrowser={jest.fn()} @@ -169,7 +165,6 @@ describe('FieldsBrowser', () => { filteredBrowserFields={mockBrowserFields} searchInput={''} height={FIELD_BROWSER_HEIGHT} - isLoading={false} isSearching={false} onCategorySelected={jest.fn()} onHideFieldBrowser={jest.fn()} @@ -196,7 +191,6 @@ describe('FieldsBrowser', () => { filteredBrowserFields={mockBrowserFields} searchInput={''} height={FIELD_BROWSER_HEIGHT} - isLoading={false} isSearching={false} onCategorySelected={jest.fn()} onHideFieldBrowser={jest.fn()} @@ -231,7 +225,6 @@ describe('FieldsBrowser', () => { filteredBrowserFields={mockBrowserFields} searchInput={''} height={FIELD_BROWSER_HEIGHT} - isLoading={false} isSearching={false} onCategorySelected={jest.fn()} onHideFieldBrowser={jest.fn()} diff --git a/x-pack/legacy/plugins/siem/public/components/fields_browser/field_browser.tsx b/x-pack/legacy/plugins/siem/public/components/fields_browser/field_browser.tsx index 1453ab56e3df67..90dc21fe366c4e 100644 --- a/x-pack/legacy/plugins/siem/public/components/fields_browser/field_browser.tsx +++ b/x-pack/legacy/plugins/siem/public/components/fields_browser/field_browser.tsx @@ -51,13 +51,7 @@ PanesFlexGroup.displayName = 'PanesFlexGroup'; type Props = Pick< FieldBrowserProps, - | 'browserFields' - | 'height' - | 'isLoading' - | 'onFieldSelected' - | 'onUpdateColumns' - | 'timelineId' - | 'width' + 'browserFields' | 'height' | 'onFieldSelected' | 'onUpdateColumns' | 'timelineId' | 'width' > & { /** * The current timeline column headers @@ -124,7 +118,6 @@ export class FieldsBrowser extends React.PureComponent { browserFields, filteredBrowserFields, searchInput, - isLoading, isSearching, onCategorySelected, onFieldSelected, @@ -165,7 +158,6 @@ export class FieldsBrowser extends React.PureComponent { data-test-subj="left-categories-pane" filteredBrowserFields={filteredBrowserFields} width={CATEGORY_PANE_WIDTH} - isLoading={isLoading} onCategorySelected={onCategorySelected} onUpdateColumns={onUpdateColumns} selectedCategoryId={selectedCategoryId} @@ -178,7 +170,6 @@ export class FieldsBrowser extends React.PureComponent { columnHeaders={columnHeaders} data-test-subj="fields-pane" filteredBrowserFields={filteredBrowserFields} - isLoading={isLoading} onCategorySelected={onCategorySelected} onFieldSelected={this.selectFieldAndHide} onUpdateColumns={onUpdateColumns} diff --git a/x-pack/legacy/plugins/siem/public/components/fields_browser/field_items.test.tsx b/x-pack/legacy/plugins/siem/public/components/fields_browser/field_items.test.tsx index c7283fb6d68425..a569fc42e550fd 100644 --- a/x-pack/legacy/plugins/siem/public/components/fields_browser/field_items.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/fields_browser/field_items.test.tsx @@ -53,7 +53,6 @@ describe('field_items', () => { categoryId: selectedCategoryId, columnHeaders: [], highlight: '', - isLoading: false, onUpdateColumns: jest.fn(), timelineId, toggleColumn: jest.fn(), @@ -88,7 +87,6 @@ describe('field_items', () => { categoryId: selectedCategoryId, columnHeaders: [], highlight: '', - isLoading: false, onUpdateColumns: jest.fn(), timelineId, toggleColumn: jest.fn(), @@ -122,7 +120,6 @@ describe('field_items', () => { categoryId: selectedCategoryId, columnHeaders, highlight: '', - isLoading: false, onUpdateColumns: jest.fn(), timelineId, toggleColumn: jest.fn(), @@ -155,7 +152,6 @@ describe('field_items', () => { categoryId: selectedCategoryId, columnHeaders: columnHeaders.filter(header => header.id !== timestampFieldId), highlight: '', - isLoading: false, onUpdateColumns: jest.fn(), timelineId, toggleColumn: jest.fn(), @@ -190,7 +186,6 @@ describe('field_items', () => { categoryId: selectedCategoryId, columnHeaders: [], highlight: '', - isLoading: false, onUpdateColumns: jest.fn(), timelineId, toggleColumn, @@ -230,7 +225,6 @@ describe('field_items', () => { categoryId: selectedCategoryId, columnHeaders, highlight: '', - isLoading: false, onUpdateColumns: jest.fn(), timelineId, toggleColumn: jest.fn(), @@ -263,7 +257,6 @@ describe('field_items', () => { categoryId: selectedCategoryId, columnHeaders, highlight: '', - isLoading: false, onUpdateColumns: jest.fn(), timelineId, toggleColumn: jest.fn(), diff --git a/x-pack/legacy/plugins/siem/public/components/fields_browser/field_items.tsx b/x-pack/legacy/plugins/siem/public/components/fields_browser/field_items.tsx index 068c545721c380..4a8e635cf28b28 100644 --- a/x-pack/legacy/plugins/siem/public/components/fields_browser/field_items.tsx +++ b/x-pack/legacy/plugins/siem/public/components/fields_browser/field_items.tsx @@ -61,7 +61,6 @@ export const getFieldItems = ({ categoryId, columnHeaders, highlight = '', - isLoading, onUpdateColumns, timelineId, toggleColumn, @@ -71,7 +70,6 @@ export const getFieldItems = ({ categoryId: string; columnHeaders: ColumnHeader[]; highlight?: string; - isLoading: boolean; timelineId: string; toggleColumn: (column: ColumnHeader) => void; onUpdateColumns: OnUpdateColumns; @@ -148,7 +146,6 @@ export const getFieldItems = ({ })} fieldId={field.name || ''} highlight={highlight} - isLoading={isLoading} onUpdateColumns={onUpdateColumns} /> diff --git a/x-pack/legacy/plugins/siem/public/components/fields_browser/field_name.test.tsx b/x-pack/legacy/plugins/siem/public/components/fields_browser/field_name.test.tsx index afca98b70a6252..1ec4313e08d0b1 100644 --- a/x-pack/legacy/plugins/siem/public/components/fields_browser/field_name.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/fields_browser/field_name.test.tsx @@ -27,7 +27,6 @@ describe('FieldName', () => { category: categoryId, })} fieldId={timestampFieldId} - isLoading={false} onUpdateColumns={jest.fn()} /> @@ -51,7 +50,6 @@ describe('FieldName', () => { category: categoryId, })} fieldId={timestampFieldId} - isLoading={false} onUpdateColumns={jest.fn()} /> @@ -72,7 +70,6 @@ describe('FieldName', () => { category: categoryId, })} fieldId={timestampFieldId} - isLoading={false} onUpdateColumns={jest.fn()} /> @@ -95,7 +92,6 @@ describe('FieldName', () => { category: categoryId, })} fieldId={timestampFieldId} - isLoading={false} onUpdateColumns={onUpdateColumns} /> @@ -136,7 +132,6 @@ describe('FieldName', () => { })} fieldId={timestampFieldId} highlight={highlight} - isLoading={false} onUpdateColumns={jest.fn()} /> diff --git a/x-pack/legacy/plugins/siem/public/components/fields_browser/field_name.tsx b/x-pack/legacy/plugins/siem/public/components/fields_browser/field_name.tsx index 1e2fe28b811d4f..41844db7154181 100644 --- a/x-pack/legacy/plugins/siem/public/components/fields_browser/field_name.tsx +++ b/x-pack/legacy/plugins/siem/public/components/fields_browser/field_name.tsx @@ -14,9 +14,9 @@ import { EuiToolTip, } from '@elastic/eui'; import * as React from 'react'; -import { pure } from 'recompose'; import styled from 'styled-components'; +import { useContext } from 'react'; import { WithCopyToClipboard } from '../../lib/clipboard/with_copy_to_clipboard'; import { ColumnHeader } from '../timeline/body/column_headers/column_header'; import { OnUpdateColumns } from '../timeline/events'; @@ -24,6 +24,7 @@ import { WithHoverActions } from '../with_hover_actions'; import { LoadingSpinner } from './helpers'; import * as i18n from './translations'; +import { TimelineContext } from '../timeline/timeline_context'; /** * The name of a (draggable) field @@ -63,15 +64,43 @@ const ViewCategoryIcon = styled(EuiIcon)` ViewCategoryIcon.displayName = 'ViewCategoryIcon'; +interface ToolTipProps { + categoryId: string; + onUpdateColumns: OnUpdateColumns; + categoryColumns: ColumnHeader[]; +} + +const ToolTip = React.memo(({ categoryId, onUpdateColumns, categoryColumns }) => { + const { isLoading } = useContext(TimelineContext); + return ( + + {!isLoading ? ( + { + onUpdateColumns(categoryColumns); + }} + type="visTable" + /> + ) : ( + + )} + + ); +}); + +ToolTip.displayName = 'ToolTip'; + /** Renders a field name in it's non-dragging state */ -export const FieldName = pure<{ +export const FieldName = React.memo<{ categoryId: string; categoryColumns: ColumnHeader[]; fieldId: string; highlight?: string; - isLoading: boolean; onUpdateColumns: OnUpdateColumns; -}>(({ categoryId, categoryColumns, fieldId, highlight = '', isLoading, onUpdateColumns }) => ( +}>(({ categoryId, categoryColumns, fieldId, highlight = '', onUpdateColumns }) => ( @@ -93,21 +122,11 @@ export const FieldName = pure<{ {categoryColumns.length > 0 && ( - - {!isLoading ? ( - { - onUpdateColumns(categoryColumns); - }} - type="visTable" - /> - ) : ( - - )} - + )} diff --git a/x-pack/legacy/plugins/siem/public/components/fields_browser/fields_pane.test.tsx b/x-pack/legacy/plugins/siem/public/components/fields_browser/fields_pane.test.tsx index 3be4fd356ee2d9..2193d0c661fb79 100644 --- a/x-pack/legacy/plugins/siem/public/components/fields_browser/fields_pane.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/fields_browser/fields_pane.test.tsx @@ -24,7 +24,6 @@ describe('FieldsPane', () => { { { { & { +type Props = Pick & { columnHeaders: ColumnHeader[]; /** * A map of categoryId -> metadata about the fields in that category, @@ -66,7 +63,6 @@ export const FieldsPane = pure( ({ columnHeaders, filteredBrowserFields, - isLoading, onCategorySelected, onUpdateColumns, searchInput, @@ -87,7 +83,6 @@ export const FieldsPane = pure( categoryId: selectedCategoryId, columnHeaders, highlight: searchInput, - isLoading, onUpdateColumns, timelineId, toggleColumn, diff --git a/x-pack/legacy/plugins/siem/public/components/fields_browser/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/fields_browser/index.test.tsx index 03017ce145e6db..02f8548b896ad9 100644 --- a/x-pack/legacy/plugins/siem/public/components/fields_browser/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/fields_browser/index.test.tsx @@ -25,7 +25,6 @@ describe('StatefulFieldsBrowser', () => { browserFields={mockBrowserFields} columnHeaders={[]} height={FIELD_BROWSER_HEIGHT} - isLoading={false} onUpdateColumns={jest.fn()} timelineId={timelineId} toggleColumn={jest.fn()} @@ -50,7 +49,6 @@ describe('StatefulFieldsBrowser', () => { browserFields={mockBrowserFields} columnHeaders={[]} height={FIELD_BROWSER_HEIGHT} - isLoading={false} onUpdateColumns={jest.fn()} timelineId={timelineId} toggleColumn={jest.fn()} @@ -69,7 +67,6 @@ describe('StatefulFieldsBrowser', () => { browserFields={mockBrowserFields} columnHeaders={[]} height={FIELD_BROWSER_HEIGHT} - isLoading={false} onUpdateColumns={jest.fn()} timelineId={timelineId} toggleColumn={jest.fn()} @@ -95,7 +92,6 @@ describe('StatefulFieldsBrowser', () => { browserFields={mockBrowserFields} columnHeaders={[]} height={FIELD_BROWSER_HEIGHT} - isLoading={false} onUpdateColumns={jest.fn()} timelineId={timelineId} toggleColumn={jest.fn()} diff --git a/x-pack/legacy/plugins/siem/public/components/fields_browser/index.tsx b/x-pack/legacy/plugins/siem/public/components/fields_browser/index.tsx index 54197defffa9a4..f50497086e25f1 100644 --- a/x-pack/legacy/plugins/siem/public/components/fields_browser/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/fields_browser/index.tsx @@ -97,7 +97,6 @@ export class StatefulFieldsBrowserComponent extends React.PureComponent< columnHeaders, browserFields, height, - isLoading, onFieldSelected, timelineId, toggleColumn, @@ -144,7 +143,6 @@ export class StatefulFieldsBrowserComponent extends React.PureComponent< } searchInput={filterInput} height={height} - isLoading={isLoading} isSearching={isSearching} onCategorySelected={this.updateSelectedCategoryId} onFieldSelected={onFieldSelected} diff --git a/x-pack/legacy/plugins/siem/public/components/fields_browser/types.ts b/x-pack/legacy/plugins/siem/public/components/fields_browser/types.ts index 39b4d68ff35616..d22ca9eb548775 100644 --- a/x-pack/legacy/plugins/siem/public/components/fields_browser/types.ts +++ b/x-pack/legacy/plugins/siem/public/components/fields_browser/types.ts @@ -18,8 +18,6 @@ export interface FieldBrowserProps { browserFields: BrowserFields; /** The height of the field browser */ height: number; - /** When true, the timeline is loading data */ - isLoading: boolean; /** * Overrides the default behavior of the `FieldBrowser` to enable * "selection" mode, where a field is selected by clicking a button diff --git a/x-pack/legacy/plugins/siem/public/components/flyout/button/index.tsx b/x-pack/legacy/plugins/siem/public/components/flyout/button/index.tsx index 0046e525f2c268..d55411c15b1121 100644 --- a/x-pack/legacy/plugins/siem/public/components/flyout/button/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/flyout/button/index.tsx @@ -6,7 +6,6 @@ import { EuiBadge, EuiBadgeProps, EuiPanel, EuiText } from '@elastic/eui'; import * as React from 'react'; -import { pure } from 'recompose'; import styled from 'styled-components'; import { DroppableWrapper } from '../../drag_and_drop/droppable_wrapper'; @@ -93,31 +92,36 @@ interface FlyoutButtonProps { timelineId: string; } -export const FlyoutButton = pure(({ onOpen, show, dataProviders, timelineId }) => - show ? ( - - - - {dataProviders.length !== 0 && ( - - {dataProviders.length} - - )} - - - - - ) : null +export const FlyoutButton = React.memo( + ({ onOpen, show, dataProviders, timelineId }) => + show ? ( + + + + {dataProviders.length !== 0 && ( + + {dataProviders.length} + + )} + + + + + ) : null, + (prevProps, nextProps) => + prevProps.show === nextProps.show && + prevProps.dataProviders === nextProps.dataProviders && + prevProps.timelineId === nextProps.timelineId ); FlyoutButton.displayName = 'FlyoutButton'; diff --git a/x-pack/legacy/plugins/siem/public/components/flyout/header/index.tsx b/x-pack/legacy/plugins/siem/public/components/flyout/header/index.tsx index e09aea4a6481fc..c03703bab0c892 100644 --- a/x-pack/legacy/plugins/siem/public/components/flyout/header/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/flyout/header/index.tsx @@ -6,7 +6,6 @@ import * as React from 'react'; import { connect } from 'react-redux'; -import { pure } from 'recompose'; import { Dispatch } from 'redux'; import { ActionCreator } from 'typescript-fsa'; @@ -71,7 +70,7 @@ interface DispatchProps { type Props = OwnProps & StateReduxProps & DispatchProps; -const statefulFlyoutHeader = pure( +const statefulFlyoutHeader = React.memo( ({ associateNote, createTimeline, @@ -117,6 +116,8 @@ statefulFlyoutHeader.displayName = 'statefulFlyoutHeader'; const emptyHistory: History[] = []; // stable reference +const emptyNotesId: string[] = []; //stable reference + const makeMapStateToProps = () => { const getTimeline = timelineSelectors.getTimelineByIdSelector(); const getNotesByIds = appSelectors.notesByIdsSelector(); @@ -130,7 +131,7 @@ const makeMapStateToProps = () => { isFavorite = false, kqlQuery, title = '', - noteIds = [], + noteIds = emptyNotesId, width = DEFAULT_TIMELINE_WIDTH, } = timeline; diff --git a/x-pack/legacy/plugins/siem/public/components/flyout/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/flyout/index.test.tsx index 7d9f176c88d156..a0e4218153b950 100644 --- a/x-pack/legacy/plugins/siem/public/components/flyout/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/flyout/index.test.tsx @@ -199,6 +199,7 @@ describe('Flyout', () => { show={false} showTimeline={showTimeline} timelineId="test" + width={100} usersViewing={usersViewing} /> @@ -224,6 +225,7 @@ describe('Flyout', () => { flyoutHeight={testFlyoutHeight} headerHeight={flyoutHeaderHeight} show={true} + width={100} showTimeline={showTimeline} timelineId="test" usersViewing={usersViewing} diff --git a/x-pack/legacy/plugins/siem/public/components/flyout/index.tsx b/x-pack/legacy/plugins/siem/public/components/flyout/index.tsx index b6ee726afaf157..a73b3d213503b8 100644 --- a/x-pack/legacy/plugins/siem/public/components/flyout/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/flyout/index.tsx @@ -8,7 +8,6 @@ import { EuiBadge } from '@elastic/eui'; import { defaultTo, getOr } from 'lodash/fp'; import * as React from 'react'; import { connect } from 'react-redux'; -import { pure } from 'recompose'; import styled from 'styled-components'; import { ActionCreator } from 'typescript-fsa'; @@ -34,7 +33,7 @@ export const Badge = styled(EuiBadge)` Badge.displayName = 'Badge'; -const Visible = styled.div<{ show: boolean }>` +const Visible = styled.div<{ show?: boolean }>` visibility: ${({ show }) => (show ? 'visible' : 'hidden')}; `; @@ -49,7 +48,7 @@ interface OwnProps { } interface DispatchProps { - showTimeline?: ActionCreator<{ id: string; show: boolean }>; + showTimeline: ActionCreator<{ id: string; show: boolean }>; applyDeltaToWidth?: ({ id, delta, @@ -67,13 +66,13 @@ interface DispatchProps { interface StateReduxProps { dataProviders?: DataProvider[]; - show?: boolean; - width?: number; + show: boolean; + width: number; } type Props = OwnProps & DispatchProps & StateReduxProps; -export const FlyoutComponent = pure( +export const FlyoutComponent = React.memo( ({ children, dataProviders, @@ -86,14 +85,14 @@ export const FlyoutComponent = pure( width, }) => ( <> - + showTimeline!({ id: timelineId, show: false })} + onClose={() => showTimeline({ id: timelineId, show: false })} timelineId={timelineId} usersViewing={usersViewing} - width={width!} + width={width} > {children} @@ -104,7 +103,7 @@ export const FlyoutComponent = pure( timelineId={timelineId} onOpen={() => { track(METRIC_TYPE.LOADED, 'open_timeline'); - showTimeline!({ id: timelineId, show: true }); + showTimeline({ id: timelineId, show: true }); }} /> diff --git a/x-pack/legacy/plugins/siem/public/components/flyout/pane/index.tsx b/x-pack/legacy/plugins/siem/public/components/flyout/pane/index.tsx index 820c6318247d40..15ce42c6a16b64 100644 --- a/x-pack/legacy/plugins/siem/public/components/flyout/pane/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/flyout/pane/index.tsx @@ -7,7 +7,6 @@ import { EuiButtonIcon, EuiFlyout, EuiFlyoutBody, EuiFlyoutHeader, EuiToolTip } from '@elastic/eui'; import * as React from 'react'; import { connect } from 'react-redux'; -import { pure } from 'recompose'; import styled from 'styled-components'; import { ActionCreator } from 'typescript-fsa'; @@ -85,25 +84,30 @@ const WrappedCloseButton = styled.div` WrappedCloseButton.displayName = 'WrappedCloseButton'; -const FlyoutHeaderWithCloseButton = pure<{ +const FlyoutHeaderWithCloseButton = React.memo<{ onClose: () => void; timelineId: string; usersViewing: string[]; -}>(({ onClose, timelineId, usersViewing }) => ( - - - - - - - - -)); +}>( + ({ onClose, timelineId, usersViewing }) => ( + + + + + + + + + ), + (prevProps, nextProps) => + prevProps.timelineId === nextProps.timelineId && + prevProps.usersViewing === nextProps.usersViewing +); FlyoutHeaderWithCloseButton.displayName = 'FlyoutHeaderWithCloseButton'; diff --git a/x-pack/legacy/plugins/siem/public/components/super_date_picker/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/super_date_picker/index.test.tsx index 01b0ec1ba19ded..4cf5b4e9026ecb 100644 --- a/x-pack/legacy/plugins/siem/public/components/super_date_picker/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/super_date_picker/index.test.tsx @@ -11,7 +11,8 @@ import { Provider as ReduxStoreProvider } from 'react-redux'; import { apolloClientObservable, mockGlobalState } from '../../mock'; import { createStore, State } from '../../store'; -import { SuperDatePicker } from '.'; +import { SuperDatePicker, makeMapStateToProps } from '.'; +import { cloneDeep } from 'lodash/fp'; describe('SIEM Super Date Picker', () => { describe('#SuperDatePicker', () => { @@ -347,17 +348,115 @@ describe('SIEM Super Date Picker', () => { .simulate('click'); wrapper.update(); }); - test.skip('Make sure it is an absolute Date', () => { - expect(store.getState().inputs.global.timerange.kind).toBe('absolute'); + }); + + describe('#makeMapStateToProps', () => { + test('it should return the same shallow references given the same input twice', () => { + const mapStateToProps = makeMapStateToProps(); + const props1 = mapStateToProps(state, { id: 'global' }); + const props2 = mapStateToProps(state, { id: 'global' }); + Object.keys(props1).forEach(key => { + expect((props1 as Record)[key]).toBe((props2 as Record)[key]); + }); }); - test.skip('Make sure that the date in store match with the one selected', () => { - const selectedDate = - wrapper.find('input[data-test-subj="superDatePickerAbsoluteDateInput"]').props().value || - ''; - expect(new Date(store.getState().inputs.global.timerange.from).toISOString()).toBe( - new Date(selectedDate as string).toISOString() - ); + test('it should not return the same reference if policy kind is different', () => { + const mapStateToProps = makeMapStateToProps(); + const props1 = mapStateToProps(state, { id: 'global' }); + const clone = cloneDeep(state); + clone.inputs.global.policy.kind = 'interval'; + const props2 = mapStateToProps(clone, { id: 'global' }); + expect(props1.policy).not.toBe(props2.policy); + }); + + test('it should not return the same reference if duration is different', () => { + const mapStateToProps = makeMapStateToProps(); + const props1 = mapStateToProps(state, { id: 'global' }); + const clone = cloneDeep(state); + clone.inputs.global.policy.duration = 99999; + const props2 = mapStateToProps(clone, { id: 'global' }); + expect(props1.duration).not.toBe(props2.duration); + }); + + test('it should not return the same reference if timerange kind is different', () => { + const mapStateToProps = makeMapStateToProps(); + const props1 = mapStateToProps(state, { id: 'global' }); + const clone = cloneDeep(state); + clone.inputs.global.timerange.kind = 'absolute'; + const props2 = mapStateToProps(clone, { id: 'global' }); + expect(props1.kind).not.toBe(props2.kind); + }); + + test('it should not return the same reference if timerange from is different', () => { + const mapStateToProps = makeMapStateToProps(); + const props1 = mapStateToProps(state, { id: 'global' }); + const clone = cloneDeep(state); + clone.inputs.global.timerange.from = 999; + const props2 = mapStateToProps(clone, { id: 'global' }); + expect(props1.start).not.toBe(props2.start); + }); + + test('it should not return the same reference if timerange to is different', () => { + const mapStateToProps = makeMapStateToProps(); + const props1 = mapStateToProps(state, { id: 'global' }); + const clone = cloneDeep(state); + clone.inputs.global.timerange.to = 999; + const props2 = mapStateToProps(clone, { id: 'global' }); + expect(props1.end).not.toBe(props2.end); + }); + + test('it should not return the same reference of toStr if toStr different', () => { + const mapStateToProps = makeMapStateToProps(); + const props1 = mapStateToProps(state, { id: 'global' }); + const clone = cloneDeep(state); + clone.inputs.global.timerange.toStr = 'some other string'; + const props2 = mapStateToProps(clone, { id: 'global' }); + expect(props1.toStr).not.toBe(props2.toStr); + }); + + test('it should not return the same reference of fromStr if fromStr different', () => { + const mapStateToProps = makeMapStateToProps(); + const props1 = mapStateToProps(state, { id: 'global' }); + const clone = cloneDeep(state); + clone.inputs.global.timerange.fromStr = 'some other string'; + const props2 = mapStateToProps(clone, { id: 'global' }); + expect(props1.fromStr).not.toBe(props2.fromStr); + }); + + test('it should not return the same reference of isLoadingSelector if the query different', () => { + const mapStateToProps = makeMapStateToProps(); + const props1 = mapStateToProps(state, { id: 'global' }); + const clone = cloneDeep(state); + clone.inputs.global.query = [ + { + loading: true, + id: '1', + inspect: { dsl: [], response: [] }, + isInspected: false, + refetch: null, + selectedInspectIndex: 0, + }, + ]; + const props2 = mapStateToProps(clone, { id: 'global' }); + expect(props1.isLoading).not.toBe(props2.isLoading); + }); + + test('it should not return the same reference of refetchSelector if the query different', () => { + const mapStateToProps = makeMapStateToProps(); + const props1 = mapStateToProps(state, { id: 'global' }); + const clone = cloneDeep(state); + clone.inputs.global.query = [ + { + loading: true, + id: '1', + inspect: { dsl: [], response: [] }, + isInspected: false, + refetch: null, + selectedInspectIndex: 0, + }, + ]; + const props2 = mapStateToProps(clone, { id: 'global' }); + expect(props1.refetch).not.toBe(props2.refetch); }); }); }); diff --git a/x-pack/legacy/plugins/siem/public/components/super_date_picker/index.tsx b/x-pack/legacy/plugins/siem/public/components/super_date_picker/index.tsx index 8a071c7a86c2aa..636b6acea54f4f 100644 --- a/x-pack/legacy/plugins/siem/public/components/super_date_picker/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/super_date_picker/index.tsx @@ -12,7 +12,7 @@ import { OnRefreshProps, OnTimeChangeProps, } from '@elastic/eui'; -import { get, getOr, take } from 'lodash/fp'; +import { getOr, take } from 'lodash/fp'; import React, { Component } from 'react'; import { connect } from 'react-redux'; import { ActionCreator } from 'typescript-fsa'; @@ -20,6 +20,18 @@ import { ActionCreator } from 'typescript-fsa'; import { inputsModel, State } from '../../store'; import { inputsActions, timelineActions } from '../../store/actions'; import { InputsModelId } from '../../store/inputs/constants'; +import { + policySelector, + durationSelector, + kindSelector, + startSelector, + endSelector, + fromStrSelector, + toStrSelector, + isLoadingSelector, + refetchSelector, +} from './selectors'; +import { InputsRange } from '../../store/inputs/model'; const MAX_RECENTLY_USED_RANGES = 9; @@ -239,23 +251,34 @@ export const SuperDatePickerComponent = class extends Component< }; }; -const mapStateToProps = (state: State, { id }: OwnProps) => { - const myState = getOr({}, `inputs.${id}`, state); - return { - policy: get('policy.kind', myState), - duration: get('policy.duration', myState), - kind: get('timerange.kind', myState), - start: get('timerange.from', myState), - end: get('timerange.to', myState), - fromStr: get('timerange.fromStr', myState), - toStr: get('timerange.toStr', myState), - isLoading: myState.query.filter((i: inputsModel.GlobalQuery) => i.loading === true).length > 0, - refetch: myState.query.map((i: inputsModel.GlobalQuery) => i.refetch), +export const makeMapStateToProps = () => { + const getPolicySelector = policySelector(); + const getDurationSelector = durationSelector(); + const getKindSelector = kindSelector(); + const getStartSelector = startSelector(); + const getEndSelector = endSelector(); + const getFromStrSelector = fromStrSelector(); + const getToStrSelector = toStrSelector(); + const getIsLoadingSelector = isLoadingSelector(); + const getRefetchQuerySelector = refetchSelector(); + return (state: State, { id }: OwnProps) => { + const inputsRange: InputsRange = getOr({}, `inputs.${id}`, state); + return { + policy: getPolicySelector(inputsRange), + duration: getDurationSelector(inputsRange), + kind: getKindSelector(inputsRange), + start: getStartSelector(inputsRange), + end: getEndSelector(inputsRange), + fromStr: getFromStrSelector(inputsRange), + toStr: getToStrSelector(inputsRange), + isLoading: getIsLoadingSelector(inputsRange), + refetch: getRefetchQuerySelector(inputsRange), + }; }; }; export const SuperDatePicker = connect( - mapStateToProps, + makeMapStateToProps, { setAbsoluteSuperDatePicker: inputsActions.setAbsoluteRangeDatePicker, setRelativeSuperDatePicker: inputsActions.setRelativeRangeDatePicker, diff --git a/x-pack/legacy/plugins/siem/public/components/super_date_picker/selectors.test.ts b/x-pack/legacy/plugins/siem/public/components/super_date_picker/selectors.test.ts new file mode 100644 index 00000000000000..194e77075fb556 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/components/super_date_picker/selectors.test.ts @@ -0,0 +1,399 @@ +/* + * 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 { + policySelector, + durationSelector, + kindSelector, + startSelector, + endSelector, + fromStrSelector, + toStrSelector, + isLoadingSelector, + refetchSelector, +} from './selectors'; +import { InputsRange, AbsoluteTimeRange, RelativeTimeRange } from '../../store/inputs/model'; +import { cloneDeep } from 'lodash/fp'; + +describe('selectors', () => { + let absoluteTime: AbsoluteTimeRange = { + kind: 'absolute', + fromStr: undefined, + toStr: undefined, + from: 0, + to: 0, + }; + + let inputState: InputsRange = { + timerange: absoluteTime, + policy: { + kind: 'manual', + duration: 0, + }, + query: [], + linkTo: [], + }; + + const getPolicySelector = policySelector(); + const getDurationSelector = durationSelector(); + const getKindSelector = kindSelector(); + const getStartSelector = startSelector(); + const getEndSelector = endSelector(); + const getFromStrSelector = fromStrSelector(); + const getToStrSelector = toStrSelector(); + const getIsLoadingSelector = isLoadingSelector(); + const getRefetchSelector = refetchSelector(); + + beforeEach(() => { + absoluteTime = { + kind: 'absolute', + fromStr: undefined, + toStr: undefined, + from: 0, + to: 0, + }; + + inputState = { + timerange: absoluteTime, + policy: { + kind: 'manual', + duration: 0, + }, + query: [], + linkTo: [], + }; + }); + + describe('#policySelector', () => { + test('returns the same reference given the same identical input twice', () => { + const result1 = getPolicySelector(inputState); + const result2 = getPolicySelector(inputState); + expect(result1).toBe(result2); + }); + + test('returns the same reference given different input twice but with different deep copies', () => { + const clone = cloneDeep(inputState); + const result1 = getPolicySelector(inputState); + const result2 = getPolicySelector(clone); + expect(result1).toBe(result2); + }); + + test('returns a different reference given different policy kind', () => { + const result1 = getPolicySelector(inputState); + const change: InputsRange = { + ...inputState, + policy: { ...inputState.policy, kind: 'interval' }, + }; + const result2 = getPolicySelector(change); + expect(result1).not.toBe(result2); + }); + }); + + describe('#durationSelector', () => { + test('returns the same reference given the same identical input twice', () => { + const result1 = getDurationSelector(inputState); + const result2 = getDurationSelector(inputState); + expect(result1).toBe(result2); + }); + + test('returns the same reference given different input twice but with different deep copies', () => { + const clone = cloneDeep(inputState); + const result1 = getDurationSelector(inputState); + const result2 = getDurationSelector(clone); + expect(result1).toBe(result2); + }); + + test('returns a different reference given different duration', () => { + const result1 = getDurationSelector(inputState); + const change: InputsRange = { + ...inputState, + policy: { ...inputState.policy, duration: 1 }, + }; + const result2 = getDurationSelector(change); + expect(result1).not.toBe(result2); + }); + }); + + describe('#kindSelector', () => { + test('returns the same reference given the same identical input twice', () => { + const result1 = getKindSelector(inputState); + const result2 = getKindSelector(inputState); + expect(result1).toBe(result2); + }); + + test('returns the same reference given different input twice but with different deep copies', () => { + const clone = cloneDeep(inputState); + const result1 = getKindSelector(inputState); + const result2 = getKindSelector(clone); + expect(result1).toBe(result2); + }); + + test('returns a different reference given different time range', () => { + const result1 = getKindSelector(inputState); + const relativeTime: RelativeTimeRange = { + kind: 'relative', + fromStr: '', + toStr: '', + from: 1, + to: 0, + }; + const change: InputsRange = { + ...inputState, + timerange: { ...relativeTime }, + }; + const result2 = getKindSelector(change); + expect(result1).not.toBe(result2); + }); + }); + + describe('#startSelector', () => { + test('returns the same reference given the same identical input twice', () => { + const result1 = getStartSelector(inputState); + const result2 = getStartSelector(inputState); + expect(result1).toBe(result2); + }); + + test('returns the same reference given different input twice but with different deep copies', () => { + const clone = cloneDeep(inputState); + const result1 = getStartSelector(inputState); + const result2 = getStartSelector(clone); + expect(result1).toBe(result2); + }); + + test('returns a different reference given different time range', () => { + const result1 = getStartSelector(inputState); + const relativeTime: RelativeTimeRange = { + kind: 'relative', + fromStr: '', + toStr: '', + from: 1, + to: 0, + }; + const change: InputsRange = { + ...inputState, + timerange: { ...relativeTime }, + }; + const result2 = getStartSelector(change); + expect(result1).not.toBe(result2); + }); + }); + + describe('#endSelector', () => { + test('returns the same reference given the same identical input twice', () => { + const result1 = getEndSelector(inputState); + const result2 = getEndSelector(inputState); + expect(result1).toBe(result2); + }); + + test('returns the same reference given different input twice but with different deep copies', () => { + const clone = cloneDeep(inputState); + const result1 = getEndSelector(inputState); + const result2 = getEndSelector(clone); + expect(result1).toBe(result2); + }); + + test('returns a different reference given different time range', () => { + const result1 = getEndSelector(inputState); + const relativeTime: RelativeTimeRange = { + kind: 'relative', + fromStr: '', + toStr: '', + from: 0, + to: 1, + }; + const change: InputsRange = { + ...inputState, + timerange: { ...relativeTime }, + }; + const result2 = getEndSelector(change); + expect(result1).not.toBe(result2); + }); + }); + + describe('#fromStrSelector', () => { + test('returns the same reference given the same identical input twice', () => { + const result1 = getFromStrSelector(inputState); + const result2 = getFromStrSelector(inputState); + expect(result1).toBe(result2); + }); + + test('returns the same reference given different input twice but with different deep copies', () => { + const clone = cloneDeep(inputState); + const result1 = getFromStrSelector(inputState); + const result2 = getFromStrSelector(clone); + expect(result1).toBe(result2); + }); + + test('returns a different reference given different time range', () => { + const result1 = getFromStrSelector(inputState); + const relativeTime: RelativeTimeRange = { + kind: 'relative', + fromStr: '', + toStr: '', + from: 0, + to: 0, + }; + const change: InputsRange = { + ...inputState, + timerange: { ...relativeTime }, + }; + const result2 = getFromStrSelector(change); + expect(result1).not.toBe(result2); + }); + }); + + describe('#toStrSelector', () => { + test('returns the same reference given the same identical input twice', () => { + const result1 = getToStrSelector(inputState); + const result2 = getToStrSelector(inputState); + expect(result1).toBe(result2); + }); + + test('returns the same reference given different input twice but with different deep copies', () => { + const clone = cloneDeep(inputState); + const result1 = getToStrSelector(inputState); + const result2 = getToStrSelector(clone); + expect(result1).toBe(result2); + }); + + test('returns a different reference given different time range', () => { + const result1 = getToStrSelector(inputState); + const relativeTime: RelativeTimeRange = { + kind: 'relative', + fromStr: '', + toStr: '', + from: 0, + to: 0, + }; + const change: InputsRange = { + ...inputState, + timerange: { ...relativeTime }, + }; + const result2 = getToStrSelector(change); + expect(result1).not.toBe(result2); + }); + }); + + describe('#isLoadingSelector', () => { + test('returns the same reference given the same identical input twice', () => { + const result1 = getIsLoadingSelector(inputState); + const result2 = getIsLoadingSelector(inputState); + expect(result1).toBe(result2); + }); + + test('returns the same reference given different input twice but with different deep copies', () => { + const clone = cloneDeep(inputState); + const result1 = getIsLoadingSelector(inputState); + const result2 = getIsLoadingSelector(clone); + expect(result1).toBe(result2); + }); + + test('returns a different reference given different loading', () => { + const result1 = getIsLoadingSelector(inputState); + const change: InputsRange = { + ...inputState, + query: [ + { + loading: true, + id: '1', + inspect: { dsl: [], response: [] }, + isInspected: false, + refetch: null, + selectedInspectIndex: 0, + }, + ], + }; + const result2 = getIsLoadingSelector(change); + expect(result1).not.toBe(result2); + }); + + test('returns false if there are no queries loading', () => { + const inputsRange: InputsRange = { + ...inputState, + query: [ + { + loading: false, + id: '1', + inspect: { dsl: [], response: [] }, + isInspected: false, + refetch: null, + selectedInspectIndex: 0, + }, + { + loading: false, + id: '1', + inspect: { dsl: [], response: [] }, + isInspected: false, + refetch: null, + selectedInspectIndex: 0, + }, + ], + }; + const result = getIsLoadingSelector(inputsRange); + expect(result).toBe(false); + }); + + test('returns true if at least one query is loading', () => { + const inputsRange: InputsRange = { + ...inputState, + query: [ + { + loading: false, + id: '1', + inspect: { dsl: [], response: [] }, + isInspected: false, + refetch: null, + selectedInspectIndex: 0, + }, + { + loading: true, + id: '1', + inspect: { dsl: [], response: [] }, + isInspected: false, + refetch: null, + selectedInspectIndex: 0, + }, + ], + }; + const result = getIsLoadingSelector(inputsRange); + expect(result).toBe(true); + }); + }); + + describe('#refetchSelector', () => { + test('returns the same reference given the same identical input twice', () => { + const result1 = getRefetchSelector(inputState); + const result2 = getRefetchSelector(inputState); + expect(result1).toBe(result2); + }); + + test('DOES NOT return the same reference given different input twice but with different deep copies since the query is not a primitive', () => { + const clone = cloneDeep(inputState); + const result1 = getRefetchSelector(inputState); + const result2 = getRefetchSelector(clone); + expect(result1).not.toBe(result2); + }); + + test('returns a different reference even if the contents are the same since query is an array and not a primitive', () => { + const result1 = getRefetchSelector(inputState); + const change: InputsRange = { + ...inputState, + query: [ + { + loading: false, + id: '1', + inspect: { dsl: [], response: [] }, + isInspected: false, + refetch: null, + selectedInspectIndex: 0, + }, + ], + }; + const result2 = getRefetchSelector(change); + expect(result1).not.toBe(result2); + }); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/components/super_date_picker/selectors.ts b/x-pack/legacy/plugins/siem/public/components/super_date_picker/selectors.ts new file mode 100644 index 00000000000000..b0ebefb98ea30d --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/components/super_date_picker/selectors.ts @@ -0,0 +1,68 @@ +/* + * 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 { createSelector } from 'reselect'; +import { Policy, InputsRange, TimeRange, GlobalQuery } from '../../store/inputs/model'; + +export const getPolicy = (inputState: InputsRange): Policy => inputState.policy; + +export const getTimerange = (inputState: InputsRange): TimeRange => inputState.timerange; + +export const getQuery = (inputState: InputsRange): GlobalQuery[] => inputState.query; + +export const policySelector = () => + createSelector( + getPolicy, + policy => policy.kind + ); + +export const durationSelector = () => + createSelector( + getPolicy, + policy => policy.duration + ); + +export const kindSelector = () => + createSelector( + getTimerange, + timerange => timerange.kind + ); + +export const startSelector = () => + createSelector( + getTimerange, + timerange => timerange.from + ); + +export const endSelector = () => + createSelector( + getTimerange, + timerange => timerange.to + ); + +export const fromStrSelector = () => + createSelector( + getTimerange, + timerange => timerange.fromStr + ); + +export const toStrSelector = () => + createSelector( + getTimerange, + timerange => timerange.toStr + ); + +export const isLoadingSelector = () => + createSelector( + getQuery, + query => query.some(i => i.loading === true) + ); + +export const refetchSelector = () => + createSelector( + getQuery, + query => query.map(i => i.refetch) + ); diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/__snapshots__/timeline.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/timeline/__snapshots__/timeline.test.tsx.snap index f2e097a10b7b6f..67d266d1cbf398 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/__snapshots__/timeline.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/timeline/__snapshots__/timeline.test.tsx.snap @@ -1,790 +1,10 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`Timeline rendering renders correctly against snapshot 1`] = ` - + + + `; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/actions/index.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/actions/index.tsx index 8753cf7c20a461..210e030c56d59c 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/actions/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/actions/index.tsx @@ -14,7 +14,6 @@ import { } from '@elastic/eui'; import { noop } from 'lodash/fp'; import * as React from 'react'; -import { pure } from 'recompose'; import styled from 'styled-components'; import { Note } from '../../../../lib/note'; @@ -89,7 +88,7 @@ NotesButtonContainer.displayName = 'NotesButtonContainer'; const emptyNotes: string[] = []; -export const Actions = pure( +export const Actions = React.memo( ({ actionsColumnWidth, associateNote, @@ -180,7 +179,20 @@ export const Actions = pure( - ) + ), + (nextProps, prevProps) => { + return ( + prevProps.actionsColumnWidth === nextProps.actionsColumnWidth && + prevProps.checked === nextProps.checked && + prevProps.expanded === nextProps.expanded && + prevProps.eventId === nextProps.eventId && + prevProps.eventIsPinned === nextProps.eventIsPinned && + prevProps.loading === nextProps.loading && + prevProps.noteIds === nextProps.noteIds && + prevProps.showCheckboxes === nextProps.showCheckboxes && + prevProps.showNotes === nextProps.showNotes + ); + } ); Actions.displayName = 'Actions'; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/__snapshots__/index.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/__snapshots__/index.test.tsx.snap index 3e1c286f5fcb83..15629482547bd7 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/__snapshots__/index.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/__snapshots__/index.test.tsx.snap @@ -1,476 +1,632 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`ColumnHeaders rendering renders correctly against snapshot 1`] = ` - +> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + `; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/actions/index.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/actions/index.tsx index 2ce8e1aab2597b..cec148d16164d9 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/actions/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/actions/index.tsx @@ -9,12 +9,14 @@ import * as React from 'react'; import { pure } from 'recompose'; import styled from 'styled-components'; +import { useContext } from 'react'; import { OnColumnRemoved } from '../../../events'; import { Sort } from '../../sort'; import { SortIndicator } from '../../sort/sort_indicator'; import { ColumnHeader } from '../column_header'; import { getSortDirection } from '../header/helpers'; import * as i18n from '../translations'; +import { TimelineContext } from '../../../timeline_context'; const CLOSE_BUTTON_SIZE = 25; // px const SORT_INDICATOR_SIZE = 25; // px @@ -35,7 +37,6 @@ WrappedCloseButton.displayName = 'WrappedCloseButton'; interface Props { header: ColumnHeader; - isLoading: boolean; onColumnRemoved: OnColumnRemoved; show: boolean; sort: Sort; @@ -65,30 +66,33 @@ export const CloseButton = pure<{ CloseButton.displayName = 'CloseButton'; -export const Actions = pure(({ header, isLoading, onColumnRemoved, show, sort }) => ( - - - - - - {sort.columnId === header.id && isLoading ? ( - - - - ) : ( +export const Actions = React.memo(({ header, onColumnRemoved, show, sort }) => { + const { isLoading } = useContext(TimelineContext); + return ( + - + - )} - -)); + + {sort.columnId === header.id && isLoading ? ( + + + + ) : ( + + + + )} + + ); +}); Actions.displayName = 'Actions'; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/header/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/header/index.test.tsx index 57be95b5d2c0d3..860f30ea8a9a8b 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/header/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/header/index.test.tsx @@ -33,7 +33,6 @@ describe('Header', () => { const wrapper = shallow(
{
{
{
{
{
{
{
{
{
{
{
` HeaderDiv.displayName = 'HeaderDiv'; +interface HeaderCompProps { + children: React.ReactNode; + isResizing: boolean; + onClick: () => void; +} + +const HeaderComp = React.memo(({ children, onClick, isResizing }) => { + const { isLoading } = useContext(TimelineContext); + return ( + + {children} + + ); +}); + +HeaderComp.displayName = 'HeaderComp'; + const TruncatableHeaderText = styled(TruncatableText)` font-weight: bold; padding: 5px; @@ -64,7 +87,6 @@ TruncatableHeaderText.displayName = 'TruncatableHeaderText'; interface Props { header: ColumnHeader; - isLoading: boolean; onColumnRemoved: OnColumnRemoved; onColumnResized: OnColumnResized; onColumnSorted: OnColumnSorted; @@ -102,14 +124,7 @@ export class Header extends React.PureComponent { } private renderActions = (isResizing: boolean) => { - const { - header, - isLoading, - onColumnRemoved, - onFilterChange = noop, - setIsResizing, - sort, - } = this.props; + const { header, onColumnRemoved, onFilterChange = noop, setIsResizing, sort } = this.props; setIsResizing(isResizing); @@ -118,11 +133,7 @@ export class Header extends React.PureComponent { ( <> - + } @@ -147,7 +158,6 @@ export class Header extends React.PureComponent { { - + )} @@ -165,9 +175,9 @@ export class Header extends React.PureComponent { }; private onClick = () => { - const { header, isLoading, onColumnSorted, sort } = this.props; + const { header, onColumnSorted, sort } = this.props; - if (!isLoading && header.aggregatable) { + if (header.aggregatable) { onColumnSorted!({ columnId: header.id, sortDirection: getNewSortDirectionOnClick({ diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/index.test.tsx index b7220a8e98d815..5553914c018aed 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/index.test.tsx @@ -38,7 +38,6 @@ describe('ColumnHeaders', () => { actionsColumnWidth={ACTIONS_COLUMN_WIDTH} browserFields={mockBrowserFields} columnHeaders={defaultHeaders} - isLoading={false} minWidth={1000} onColumnSorted={jest.fn()} onColumnRemoved={jest.fn()} @@ -60,7 +59,6 @@ describe('ColumnHeaders', () => { actionsColumnWidth={ACTIONS_COLUMN_WIDTH} browserFields={mockBrowserFields} columnHeaders={defaultHeaders} - isLoading={false} minWidth={1000} onColumnSorted={jest.fn()} onColumnRemoved={jest.fn()} @@ -89,7 +87,6 @@ describe('ColumnHeaders', () => { actionsColumnWidth={ACTIONS_COLUMN_WIDTH} browserFields={mockBrowserFields} columnHeaders={defaultHeaders} - isLoading={false} minWidth={1000} onColumnSorted={jest.fn()} onColumnRemoved={jest.fn()} @@ -120,7 +117,6 @@ describe('ColumnHeaders', () => { actionsColumnWidth={ACTIONS_COLUMN_WIDTH} browserFields={mockBrowserFields} columnHeaders={defaultHeaders} - isLoading={false} minWidth={1000} onColumnSorted={jest.fn()} onColumnRemoved={jest.fn()} diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/index.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/index.tsx index 9e5cb3dc38b88c..8020ac7fe61116 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/index.tsx @@ -8,7 +8,6 @@ import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { noop } from 'lodash/fp'; import * as React from 'react'; import { Draggable } from 'react-beautiful-dnd'; -import { pure } from 'recompose'; import styled from 'styled-components'; import { BrowserFields } from '../../../../containers/source'; @@ -47,7 +46,6 @@ interface Props { actionsColumnWidth: number; browserFields: BrowserFields; columnHeaders: ColumnHeader[]; - isLoading: boolean; onColumnRemoved: OnColumnRemoved; onColumnResized: OnColumnResized; onColumnSorted: OnColumnSorted; @@ -88,12 +86,11 @@ const EventsSelectContainer = styled(EuiFlexItem)` EventsSelectContainer.displayName = 'EventsSelectContainer'; /** Renders the timeline header columns */ -export const ColumnHeaders = pure( +export const ColumnHeaders = React.memo( ({ actionsColumnWidth, browserFields, columnHeaders, - isLoading, onColumnRemoved, onColumnResized, onColumnSorted, @@ -106,7 +103,6 @@ export const ColumnHeaders = pure( minWidth, }) => { const { isResizing, setIsResizing } = isContainerResizing(); - return ( ( columnHeaders={columnHeaders} data-test-subj="field-browser" height={FIELD_BROWSER_HEIGHT} - isLoading={isLoading} onUpdateColumns={onUpdateColumns} timelineId={timelineId} toggleColumn={toggleColumn} @@ -173,7 +168,6 @@ export const ColumnHeaders = pure(
uuid.v4(); const emptyNotes: string[] = []; -export class EventColumnView extends React.PureComponent { - public render() { - const { - _id, - actionsColumnWidth, - associateNote, - columnHeaders, - columnRenderers, - data, - eventIdToNoteIds, - expanded, - getNotesByIds, - loading, - onColumnResized, - onEventToggled, - onPinEvent, - onUnPinEvent, - pinnedEventIds, - showNotes, - toggleShowNotes, - updateNote, - } = this.props; - return ( - - - - +export const EventColumnView = React.memo( + ({ + id, + actionsColumnWidth, + associateNote, + columnHeaders, + columnRenderers, + data, + eventIdToNoteIds, + expanded, + getNotesByIds, + loading, + onColumnResized, + onEventToggled, + onPinEvent, + onUnPinEvent, + pinnedEventIds, + showNotes, + toggleShowNotes, + updateNote, + }) => ( + + + + - - - - + + + + + ), + (prevProps, nextProps) => { + return ( + prevProps.id === nextProps.id && + prevProps.actionsColumnWidth === nextProps.actionsColumnWidth && + prevProps.columnHeaders === nextProps.columnHeaders && + prevProps.columnRenderers === nextProps.columnRenderers && + prevProps.data === nextProps.data && + prevProps.eventIdToNoteIds === nextProps.eventIdToNoteIds && + prevProps.expanded === nextProps.expanded && + prevProps.loading === nextProps.loading && + prevProps.pinnedEventIds === nextProps.pinnedEventIds && + prevProps.showNotes === nextProps.showNotes ); } -} +); + +EventColumnView.displayName = 'EventColumnView'; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/events/index.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/events/index.tsx index fb378e0288c377..add984e8c1c7b6 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/events/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/events/index.tsx @@ -7,7 +7,6 @@ import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import * as React from 'react'; import styled from 'styled-components'; -import uuid from 'uuid'; import { BrowserFields } from '../../../../containers/source'; import { TimelineItem } from '../../../../graphql/types'; @@ -19,6 +18,7 @@ import { ColumnHeader } from '../column_headers/column_header'; import { StatefulEvent } from './stateful_event'; import { ColumnRenderer } from '../renderers/column_renderer'; import { RowRenderer } from '../renderers/row_renderer'; +import { maxDelay } from '../../../../lib/helpers/scheduler'; const EventsContainer = styled.div<{ minWidth: number; @@ -40,7 +40,6 @@ interface Props { eventIdToNoteIds: Readonly>; getNotesByIds: (noteIds: string[]) => Note[]; id: string; - isLoading: boolean; onColumnResized: OnColumnResized; onPinEvent: OnPinEvent; onUpdateColumns: OnUpdateColumns; @@ -53,63 +52,58 @@ interface Props { width: number; } -export const getNewNoteId = (): string => uuid.v4(); +export const Events = React.memo( + ({ + actionsColumnWidth, + addNoteToEvent, + browserFields, + columnHeaders, + columnRenderers, + data, + eventIdToNoteIds, + getNotesByIds, + id, + minWidth, + onColumnResized, + onPinEvent, + onUpdateColumns, + onUnPinEvent, + pinnedEventIds, + rowRenderers, + toggleColumn, + updateNote, + width, + }) => ( + + + {data.map((event, i) => ( + + + + ))} + + + ) +); -export class Events extends React.PureComponent { - public render() { - const { - actionsColumnWidth, - addNoteToEvent, - browserFields, - columnHeaders, - columnRenderers, - data, - eventIdToNoteIds, - getNotesByIds, - id, - isLoading, - minWidth, - onColumnResized, - onPinEvent, - onUpdateColumns, - onUnPinEvent, - pinnedEventIds, - rowRenderers, - toggleColumn, - updateNote, - width, - } = this.props; - - return ( - - - {data.map(event => ( - - - - ))} - - - ); - } -} +Events.displayName = 'Events'; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/events/stateful_event.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/events/stateful_event.tsx index 24ab838dc5f8ba..6dd9a08a707432 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/events/stateful_event.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/events/stateful_event.tsx @@ -4,24 +4,24 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { EuiFlexItem } from '@elastic/eui'; import * as React from 'react'; import uuid from 'uuid'; import { BrowserFields } from '../../../../containers/source'; import { TimelineDetailsComponentQuery } from '../../../../containers/timeline/details'; -import { TimelineItem } from '../../../../graphql/types'; +import { TimelineItem, DetailItem } from '../../../../graphql/types'; import { Note } from '../../../../lib/note'; import { AddNoteToEvent, UpdateNote } from '../../../notes/helpers'; -import { NoteCards } from '../../../notes/note_cards'; import { OnColumnResized, OnPinEvent, OnUnPinEvent, OnUpdateColumns } from '../../events'; import { ExpandableEvent } from '../../expandable_event'; import { ColumnHeader } from '../column_headers/column_header'; -import { EventColumnView } from './event_column_view'; import { ColumnRenderer } from '../renderers/column_renderer'; import { RowRenderer } from '../renderers/row_renderer'; import { getRowRenderer } from '../renderers/get_row_renderer'; +import { requestIdleCallbackViaScheduler } from '../../../../lib/helpers/scheduler'; +import { StatefulEventChild } from './stateful_event_child'; interface Props { actionsColumnWidth: number; @@ -32,7 +32,6 @@ interface Props { event: TimelineItem; eventIdToNoteIds: Readonly>; getNotesByIds: (noteIds: string[]) => Note[]; - isLoading: boolean; onColumnResized: OnColumnResized; onPinEvent: OnPinEvent; onUpdateColumns: OnUpdateColumns; @@ -43,23 +42,43 @@ interface Props { toggleColumn: (column: ColumnHeader) => void; updateNote: UpdateNote; width: number; + maxDelay?: number; } interface State { expanded: { [eventId: string]: boolean }; showNotes: { [eventId: string]: boolean }; + initialRender: boolean; } export const getNewNoteId = (): string => uuid.v4(); -const emptyNotes: string[] = []; +const emptyDetails: DetailItem[] = []; -export class StatefulEvent extends React.PureComponent { +export class StatefulEvent extends React.Component { public readonly state: State = { expanded: {}, showNotes: {}, + initialRender: false, }; + /** + * Incrementally loads the events when it mounts by trying to + * see if it resides within a window frame and if it is it will + * indicate to React that it should render its self by setting + * its initialRender to true. + */ + public componentDidMount() { + requestIdleCallbackViaScheduler( + () => { + if (!this.state.initialRender) { + this.setState({ initialRender: true }); + } + }, + { timeout: this.props.maxDelay ? this.props.maxDelay : 0 } + ); + } + public render() { const { actionsColumnWidth, @@ -70,7 +89,6 @@ export class StatefulEvent extends React.PureComponent { event, eventIdToNoteIds, getNotesByIds, - isLoading, onColumnResized, onPinEvent, onUpdateColumns, @@ -83,6 +101,12 @@ export class StatefulEvent extends React.PureComponent { width, } = this.props; + // If we are not ready to render yet, just return null + // see componentDidMount() for when it schedules the first + // time this stateful component should be rendered. + if (!this.state.initialRender) { + return null; + } return ( { data: event.ecs, width, children: ( - <> - - - - - - - - - - + ), })} @@ -144,8 +150,7 @@ export class StatefulEvent extends React.PureComponent { browserFields={browserFields} columnHeaders={columnHeaders} id={event._id} - isLoading={isLoading} - event={detailsData || []} + event={detailsData || emptyDetails} forceExpand={!!this.state.expanded[event._id] && !loading} onUpdateColumns={onUpdateColumns} timelineId={timelineId} diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/events/stateful_event_child.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/events/stateful_event_child.tsx new file mode 100644 index 00000000000000..89aecca198fbaa --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/events/stateful_event_child.tsx @@ -0,0 +1,114 @@ +/* + * 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 * as React from 'react'; +import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import uuid from 'uuid'; +import { EventColumnView } from './event_column_view'; +import { AddNoteToEvent, UpdateNote } from '../../../notes/helpers'; +import { OnPinEvent, OnColumnResized, OnUnPinEvent } from '../../events'; +import { ColumnHeader } from '../column_headers/column_header'; +import { ColumnRenderer } from '../renderers/column_renderer'; +import { TimelineNonEcsData } from '../../../../graphql/types'; +import { Note } from '../../../../lib/note'; +import { NoteCards } from '../../../notes/note_cards'; + +interface Props { + id: string; + actionsColumnWidth: number; + addNoteToEvent: AddNoteToEvent; + onPinEvent: OnPinEvent; + columnHeaders: ColumnHeader[]; + columnRenderers: ColumnRenderer[]; + expanded: boolean; + data: TimelineNonEcsData[]; + eventIdToNoteIds: Readonly>; + loading: boolean; + onColumnResized: OnColumnResized; + onUnPinEvent: OnUnPinEvent; + pinnedEventIds: Readonly>; + showNotes: boolean; + updateNote: UpdateNote; + onToggleExpanded: (eventId: string) => () => void; + onToggleShowNotes: (eventId: string) => () => void; + getNotesByIds: (noteIds: string[]) => Note[]; + width: number; + associateNote: ( + eventId: string, + addNoteToEvent: AddNoteToEvent, + onPinEvent: OnPinEvent + ) => (noteId: string) => void; +} + +export const getNewNoteId = (): string => uuid.v4(); + +const emptyNotes: string[] = []; + +export const StatefulEventChild = React.memo( + ({ + id, + actionsColumnWidth, + associateNote, + addNoteToEvent, + onPinEvent, + columnHeaders, + columnRenderers, + expanded, + data, + eventIdToNoteIds, + getNotesByIds, + loading, + onColumnResized, + onToggleExpanded, + onUnPinEvent, + pinnedEventIds, + showNotes, + onToggleShowNotes, + updateNote, + width, + }) => ( + + + + + + + + + + ) +); + +StatefulEventChild.displayName = 'StatefulEventChild'; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/index.test.tsx index f213808a1d1f06..a6c21afd4eb0da 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/index.test.tsx @@ -16,6 +16,7 @@ import { TestProviders } from '../../../mock/test_providers'; import { Body } from '.'; import { columnRenderers, rowRenderers } from './renderers'; import { Sort } from './sort'; +import { wait } from '../../../lib/helpers'; const testBodyHeight = 700; const mockGetNotesByIds = (eventId: string[]) => []; @@ -38,7 +39,6 @@ describe('Body', () => { eventIdToNoteIds={{}} height={testBodyHeight} id={'timeline-test'} - isLoading={false} getNotesByIds={mockGetNotesByIds} onColumnRemoved={jest.fn()} onColumnResized={jest.fn()} @@ -78,7 +78,6 @@ describe('Body', () => { eventIdToNoteIds={{}} height={testBodyHeight} id={'timeline-test'} - isLoading={false} getNotesByIds={mockGetNotesByIds} onColumnRemoved={jest.fn()} onColumnResized={jest.fn()} @@ -118,7 +117,6 @@ describe('Body', () => { eventIdToNoteIds={{}} height={testBodyHeight} id={'timeline-test'} - isLoading={false} getNotesByIds={mockGetNotesByIds} onColumnRemoved={jest.fn()} onColumnResized={jest.fn()} @@ -146,7 +144,7 @@ describe('Body', () => { ).toEqual(true); }); - test('it renders a tooltip for timestamp', () => { + test('it renders a tooltip for timestamp', async () => { const headersJustTimestamp = defaultHeaders.filter(h => h.id === '@timestamp'); const wrapper = mount( @@ -160,7 +158,6 @@ describe('Body', () => { eventIdToNoteIds={{}} height={testBodyHeight} id={'timeline-test'} - isLoading={false} getNotesByIds={mockGetNotesByIds} onColumnRemoved={jest.fn()} onColumnResized={jest.fn()} @@ -179,7 +176,8 @@ describe('Body', () => { /> ); - + await wait(); + wrapper.update(); headersJustTimestamp.forEach(h => { expect( wrapper diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/index.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/index.tsx index 269ea02858de15..06c6eb823d6619 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/index.tsx @@ -6,7 +6,6 @@ import { EuiText } from '@elastic/eui'; import * as React from 'react'; -import { pure } from 'recompose'; import styled from 'styled-components'; import { BrowserFields } from '../../../containers/source'; @@ -41,7 +40,6 @@ interface Props { getNotesByIds: (noteIds: string[]) => Note[]; height: number; id: string; - isLoading: boolean; eventIdToNoteIds: Readonly>; onColumnRemoved: OnColumnRemoved; onColumnResized: OnColumnResized; @@ -85,7 +83,7 @@ const VerticalScrollContainer = styled.div<{ VerticalScrollContainer.displayName = 'VerticalScrollContainer'; /** Renders the timeline body */ -export const Body = pure( +export const Body = React.memo( ({ addNoteToEvent, browserFields, @@ -96,7 +94,6 @@ export const Body = pure( getNotesByIds, height, id, - isLoading, onColumnRemoved, onColumnResized, onColumnSorted, @@ -123,7 +120,6 @@ export const Body = pure( actionsColumnWidth={ACTIONS_COLUMN_WIDTH} browserFields={browserFields} columnHeaders={columnHeaders} - isLoading={isLoading} onColumnRemoved={onColumnRemoved} onColumnResized={onColumnResized} onColumnSorted={onColumnSorted} @@ -151,7 +147,6 @@ export const Body = pure( eventIdToNoteIds={eventIdToNoteIds} getNotesByIds={getNotesByIds} id={id} - isLoading={isLoading} onColumnResized={onColumnResized} onPinEvent={onPinEvent} onUpdateColumns={onUpdateColumns} diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/stateful_body.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/stateful_body.tsx index 799f9b29a55438..108ed1f4b5ad90 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/stateful_body.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/stateful_body.tsx @@ -36,7 +36,6 @@ interface OwnProps { browserFields: BrowserFields; data: TimelineItem[]; id: string; - isLoading: boolean; height: number; sort: Sort; toggleColumn: (column: ColumnHeader) => void; @@ -93,7 +92,6 @@ class StatefulBodyComponent extends React.PureComponent void; width?: number; } -export class ExpandableEvent extends React.PureComponent { - public render() { - const { - browserFields, - columnHeaders, - event, - forceExpand = false, - id, - isLoading, - timelineId, - toggleColumn, - onUpdateColumns, - width, - } = this.props; +export const ExpandableEvent = React.memo( + ({ + browserFields, + columnHeaders, + event, + forceExpand = false, + id, + timelineId, + toggleColumn, + onUpdateColumns, + width, + }) => ( + + ( + + )} + forceExpand={forceExpand} + paddingSize="none" + /> + + ) +); - return ( - - ( - - )} - forceExpand={forceExpand} - paddingSize="none" - /> - - ); - } -} +ExpandableEvent.displayName = 'ExpandableEvent'; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/footer/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/footer/index.test.tsx index a0f3d3890fca21..79f85103077b70 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/footer/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/footer/index.test.tsx @@ -18,7 +18,7 @@ describe('Footer Timeline Component', () => { const loadMore = jest.fn(); const onChangeItemsPerPage = jest.fn(); const getUpdatedAt = () => 1546878704036; - const width = 500; + const compact = true; describe('rendering', () => { test('it renders the default timeline footer', () => { @@ -37,7 +37,7 @@ describe('Footer Timeline Component', () => { nextCursor={getOr(null, 'endCursor.value', mockData.Events.pageInfo)!} tieBreaker={getOr(null, 'endCursor.tiebreaker', mockData.Events.pageInfo)} getUpdatedAt={getUpdatedAt} - width={width} + compact={compact} /> ); @@ -60,7 +60,7 @@ describe('Footer Timeline Component', () => { nextCursor={getOr(null, 'endCursor.value', mockData.Events.pageInfo)!} tieBreaker={getOr(null, 'endCursor.tiebreaker', mockData.Events.pageInfo)} getUpdatedAt={getUpdatedAt} - width={width} + compact={compact} /> ); @@ -84,7 +84,7 @@ describe('Footer Timeline Component', () => { nextCursor={getOr(null, 'endCursor.value', mockData.Events.pageInfo)!} tieBreaker={getOr(null, 'endCursor.tiebreaker', mockData.Events.pageInfo)} getUpdatedAt={getUpdatedAt} - width={width} + compact={compact} /> ); @@ -109,7 +109,7 @@ describe('Footer Timeline Component', () => { nextCursor={getOr(null, 'endCursor.value', mockData.Events.pageInfo)!} tieBreaker={getOr(null, 'endCursor.tiebreaker', mockData.Events.pageInfo)} getUpdatedAt={getUpdatedAt} - width={width} + compact={compact} /> ); @@ -143,7 +143,7 @@ describe('Footer Timeline Component', () => { nextCursor={getOr(null, 'endCursor.value', mockData.Events.pageInfo)!} tieBreaker={getOr(null, 'endCursor.tiebreaker', mockData.Events.pageInfo)} getUpdatedAt={getUpdatedAt} - width={width} + compact={compact} /> ); @@ -167,7 +167,7 @@ describe('Footer Timeline Component', () => { nextCursor={getOr(null, 'endCursor.value', mockData.Events.pageInfo)!} tieBreaker={getOr(null, 'endCursor.tiebreaker', mockData.Events.pageInfo)} getUpdatedAt={getUpdatedAt} - width={width} + compact={compact} /> ); @@ -198,7 +198,7 @@ describe('Footer Timeline Component', () => { nextCursor={getOr(null, 'endCursor.value', mockData.Events.pageInfo)!} tieBreaker={getOr(null, 'endCursor.tiebreaker', mockData.Events.pageInfo)} getUpdatedAt={getUpdatedAt} - width={width} + compact={compact} /> ); @@ -228,7 +228,7 @@ describe('Footer Timeline Component', () => { nextCursor={getOr(null, 'endCursor.value', mockData.Events.pageInfo)!} tieBreaker={getOr(null, 'endCursor.tiebreaker', mockData.Events.pageInfo)} getUpdatedAt={getUpdatedAt} - width={width} + compact={compact} /> ); @@ -262,7 +262,7 @@ describe('Footer Timeline Component', () => { nextCursor={getOr(null, 'endCursor.value', mockData.Events.pageInfo)!} tieBreaker={getOr(null, 'endCursor.tiebreaker', mockData.Events.pageInfo)} getUpdatedAt={getUpdatedAt} - width={width} + compact={compact} /> ); @@ -288,7 +288,7 @@ describe('Footer Timeline Component', () => { nextCursor={getOr(null, 'endCursor.value', mockData.Events.pageInfo)!} tieBreaker={getOr(null, 'endCursor.tiebreaker', mockData.Events.pageInfo)} getUpdatedAt={getUpdatedAt} - width={width} + compact={compact} /> ); diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/footer/index.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/footer/index.tsx index 6487bbc78a3748..30dad3e9147a28 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/footer/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/footer/index.tsx @@ -72,8 +72,6 @@ ServerSideEventCount.displayName = 'ServerSideEventCount'; /** The height of the footer, exported for use in height calculations */ export const footerHeight = 40; // px -export const isCompactFooter = (width: number): boolean => width < 600; - interface FooterProps { itemsCount: number; isLive: boolean; @@ -88,7 +86,7 @@ interface FooterProps { serverSideEventCount: number; tieBreaker: string; getUpdatedAt: () => number; - width: number; + compact: boolean; } interface FooterState { @@ -205,7 +203,7 @@ export class Footer extends React.PureComponent { serverSideEventCount, hasNextPage, getUpdatedAt, - width, + compact, } = this.props; if (isLoading && !this.state.paginationLoading) { @@ -301,13 +299,10 @@ export class Footer extends React.PureComponent { - + diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/header/__snapshots__/index.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/timeline/header/__snapshots__/index.test.tsx.snap index 332942c3e3a869..47f5bc1b04c3c9 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/header/__snapshots__/index.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/timeline/header/__snapshots__/index.test.tsx.snap @@ -1,241 +1,241 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`Header rendering renders correctly against snapshot 1`] = ` - + + + timelineId="foo" + /> + `; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/header/index.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/header/index.tsx index f4c893d6b2d0ec..f7802203d12581 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/header/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/header/index.tsx @@ -6,7 +6,6 @@ import { EuiCallOut } from '@elastic/eui'; import * as React from 'react'; -import { pure } from 'recompose'; import styled from 'styled-components'; import { StaticIndexPattern } from 'ui/index_patterns'; @@ -48,7 +47,7 @@ const TimelineHeaderContainer = styled.div` TimelineHeaderContainer.displayName = 'TimelineHeaderContainer'; -export const TimelineHeader = pure( +export const TimelineHeader = React.memo( ({ browserFields, id, diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/properties/index.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/properties/index.tsx index 5e1dd3363940bd..3c120f9d523263 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/properties/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/properties/index.tsx @@ -4,34 +4,17 @@ * you may not use this file except in compliance with the Elastic License. */ -import { - EuiAvatar, - EuiButtonIcon, - EuiFlexGroup, - EuiFlexItem, - EuiIcon, - EuiPopover, - EuiToolTip, -} from '@elastic/eui'; +import { EuiAvatar, EuiFlexItem, EuiIcon } from '@elastic/eui'; import * as React from 'react'; import styled, { injectGlobal } from 'styled-components'; import { Note } from '../../../lib/note'; import { InputsModelId } from '../../../store/inputs/constants'; -import { InspectButton } from '../../inspect'; import { AssociateNote, UpdateNote } from '../../notes/helpers'; -import { OpenTimelineModalButton } from '../../open_timeline/open_timeline_modal'; -import { SuperDatePicker } from '../../super_date_picker'; -import { Description, Name, NewTimeline, NotesButton, StarIcon } from './helpers'; -import { - DatePicker, - PropertiesLeft, - PropertiesRight, - TimelineProperties, - LockIconContainer, -} from './styles'; -import * as i18n from './translations'; +import { TimelineProperties } from './styles'; +import { PropertiesRight } from './properties_right'; +import { PropertiesLeft } from './properties_left'; type CreateTimeline = ({ id, show }: { id: string; show?: boolean }) => void; type UpdateIsFavorite = ({ id, isFavorite }: { id: string; isFavorite: boolean }) => void; @@ -170,175 +153,48 @@ export class Properties extends React.PureComponent { return ( - - - - - - - - {width >= showDescriptionThreshold ? ( - - - - ) : null} - - {width >= showNotesThreshold ? ( - - - - ) : null} - - - - - - - - - datePickerThreshold ? datePickerThreshold : datePickerWidth - } - > - - - - - - - - - - } - id="timelineSettingsPopover" - isOpen={this.state.showActions} - closePopover={this.onClosePopover} - > - - - - - - - - - - - - - - {width < showNotesThreshold ? ( - - - - ) : null} - - {width < showDescriptionThreshold ? ( - - - - - - ) : null} - - - - - {title != null && title.length - ? usersViewing.map(user => ( - // Hide the hard-coded elastic user avatar as the 7.2 release does not implement - // support for multi-user-collaboration as proposed in elastic/ingest-dev#395 - - - - - - )) - : null} - + = showDescriptionThreshold} + description={description} + title={title} + updateTitle={updateTitle} + updateDescription={updateDescription} + showNotes={this.state.showNotes} + showNotesFromWidth={width >= showNotesThreshold} + associateNote={associateNote} + getNotesByIds={getNotesByIds} + noteIds={noteIds} + onToggleShowNotes={this.onToggleShowNotes} + updateNote={updateNote} + isDatepickerLocked={isDatepickerLocked} + toggleLock={this.toggleLock} + datePickerWidth={ + datePickerWidth > datePickerThreshold ? datePickerThreshold : datePickerWidth + } + /> + 0} + usersViewing={usersViewing} + description={description} + updateDescription={updateDescription} + associateNote={associateNote} + getNotesByIds={getNotesByIds} + noteIds={noteIds} + onToggleShowNotes={this.onToggleShowNotes} + updateNote={updateNote} + /> ); } diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/properties/properties_left.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/properties/properties_left.tsx new file mode 100644 index 00000000000000..93e3f6f50847bf --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/components/timeline/properties/properties_left.tsx @@ -0,0 +1,164 @@ +/* + * 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 { EuiButtonIcon, EuiFlexGroup, EuiFlexItem, EuiToolTip } from '@elastic/eui'; + +import React from 'react'; +import styled from 'styled-components'; +import { Description, Name, NotesButton, StarIcon } from './helpers'; +import { AssociateNote, UpdateNote } from '../../notes/helpers'; +import { Note } from '../../../lib/note'; +import { SuperDatePicker } from '../../super_date_picker'; + +import * as i18n from './translations'; + +type UpdateIsFavorite = ({ id, isFavorite }: { id: string; isFavorite: boolean }) => void; +type UpdateTitle = ({ id, title }: { id: string; title: string }) => void; +type UpdateDescription = ({ id, description }: { id: string; description: string }) => void; + +interface Props { + isFavorite: boolean; + timelineId: string; + updateIsFavorite: UpdateIsFavorite; + showDescription: boolean; + description: string; + title: string; + updateTitle: UpdateTitle; + updateDescription: UpdateDescription; + showNotes: boolean; + associateNote: AssociateNote; + showNotesFromWidth: boolean; + getNotesByIds: (noteIds: string[]) => Note[]; + onToggleShowNotes: () => void; + noteIds: string[]; + updateNote: UpdateNote; + isDatepickerLocked: boolean; + toggleLock: () => void; + datePickerWidth: number; +} + +export const PropertiesLeftStyle = styled(EuiFlexGroup)` + width: 100%; +`; + +PropertiesLeftStyle.displayName = 'PropertiesLeftStyle'; + +export const LockIconContainer = styled(EuiFlexItem)` + margin-right: 2px; +`; + +LockIconContainer.displayName = 'LockIconContainer'; + +export const DatePicker = styled(EuiFlexItem)<{ width: number }>` + width: ${({ width }) => `${width}px`}; + .euiSuperDatePicker__flexWrapper { + max-width: none; + width: auto; + } +`; + +DatePicker.displayName = 'DatePicker'; + +export const PropertiesLeft = React.memo( + ({ + isFavorite, + timelineId, + updateIsFavorite, + showDescription, + description, + title, + updateTitle, + updateDescription, + showNotes, + showNotesFromWidth, + associateNote, + getNotesByIds, + noteIds, + onToggleShowNotes, + updateNote, + isDatepickerLocked, + toggleLock, + datePickerWidth, + }) => ( + + + + + + + + {showDescription ? ( + + + + ) : null} + + {showNotesFromWidth ? ( + + + + ) : null} + + + + + + + + + + + + + + + ) +); + +PropertiesLeft.displayName = 'PropertiesLeft'; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/properties/properties_right.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/properties/properties_right.tsx new file mode 100644 index 00000000000000..2eef253d7be7c5 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/components/timeline/properties/properties_right.tsx @@ -0,0 +1,193 @@ +/* + * 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 React from 'react'; +import styled from 'styled-components'; +import { + EuiFlexGroup, + EuiFlexItem, + EuiPopover, + EuiIcon, + EuiToolTip, + EuiAvatar, +} from '@elastic/eui'; +import { NewTimeline, Description, NotesButton } from './helpers'; +import { OpenTimelineModalButton } from '../../open_timeline/open_timeline_modal'; +import { InspectButton } from '../../inspect'; + +import * as i18n from './translations'; +import { AssociateNote } from '../../notes/helpers'; +import { Note } from '../../../lib/note'; + +export const PropertiesRightStyle = styled(EuiFlexGroup)` + margin-right: 5px; +`; + +PropertiesRightStyle.displayName = 'PropertiesRightStyle'; + +const DescriptionPopoverMenuContainer = styled.div` + margin-top: 15px; +`; + +DescriptionPopoverMenuContainer.displayName = 'DescriptionPopoverMenuContainer'; + +const SettingsIcon = styled(EuiIcon)` + margin-left: 4px; + cursor: pointer; +`; + +SettingsIcon.displayName = 'SettingsIcon'; + +const HiddenFlexItem = styled(EuiFlexItem)` + display: none; +`; + +HiddenFlexItem.displayName = 'HiddenFlexItem'; + +const Avatar = styled(EuiAvatar)` + margin-left: 5px; +`; + +Avatar.displayName = 'Avatar'; + +type CreateTimeline = ({ id, show }: { id: string; show?: boolean }) => void; +type UpdateDescription = ({ id, description }: { id: string; description: string }) => void; +export type UpdateNote = (note: Note) => void; + +interface Props { + onButtonClick: () => void; + onClosePopover: () => void; + showActions: boolean; + createTimeline: CreateTimeline; + timelineId: string; + isDataInTimeline: boolean; + showNotes: boolean; + showNotesFromWidth: boolean; + showDescription: boolean; + showUsersView: boolean; + usersViewing: string[]; + description: string; + updateDescription: UpdateDescription; + associateNote: AssociateNote; + getNotesByIds: (noteIds: string[]) => Note[]; + noteIds: string[]; + onToggleShowNotes: () => void; + updateNote: UpdateNote; +} + +export const PropertiesRight = React.memo( + ({ + onButtonClick, + showActions, + onClosePopover, + createTimeline, + timelineId, + isDataInTimeline, + showNotesFromWidth, + showNotes, + showDescription, + showUsersView, + usersViewing, + description, + updateDescription, + associateNote, + getNotesByIds, + noteIds, + onToggleShowNotes, + updateNote, + }) => ( + + + + } + id="timelineSettingsPopover" + isOpen={showActions} + closePopover={onClosePopover} + > + + + + + + + + + + + + + + {showNotesFromWidth ? ( + + + + ) : null} + + {showDescription ? ( + + + + + + ) : null} + + + + + {showUsersView + ? usersViewing.map(user => ( + // Hide the hard-coded elastic user avatar as the 7.2 release does not implement + // support for multi-user-collaboration as proposed in elastic/ingest-dev#395 + + + + + + )) + : null} + + ) +); + +PropertiesRight.displayName = 'PropertiesRight'; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/properties/styles.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/properties/styles.tsx index 369dff795dc507..e8b7a8e0f669d3 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/properties/styles.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/properties/styles.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiFieldText, EuiFlexGroup, EuiFlexItem, EuiIcon } from '@elastic/eui'; +import { EuiFieldText, EuiFlexItem, EuiIcon } from '@elastic/eui'; import styled, { keyframes } from 'styled-components'; const fadeInEffect = keyframes` @@ -74,18 +74,6 @@ export const StyledStar = styled(EuiIcon)` StyledStar.displayName = 'StyledStar'; -export const PropertiesLeft = styled(EuiFlexGroup)` - width: 100%; -`; - -PropertiesLeft.displayName = 'PropertiesLeft'; - -export const PropertiesRight = styled(EuiFlexGroup)` - margin-right: 5px; -`; - -PropertiesRight.displayName = 'PropertiesRight'; - export const Facet = styled.div` align-items: center; display: inline-flex; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/timeline.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/timeline.tsx index d47c962ba3de96..82e0c10d3304a3 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/timeline.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/timeline.tsx @@ -7,7 +7,6 @@ import { EuiFlexGroup } from '@elastic/eui'; import { getOr, isEmpty } from 'lodash/fp'; import * as React from 'react'; -import { pure } from 'recompose'; import styled from 'styled-components'; import { StaticIndexPattern } from 'ui/index_patterns'; @@ -53,6 +52,8 @@ const TimelineContainer = styled(EuiFlexGroup)` TimelineContainer.displayName = 'TimelineContainer'; +export const isCompactFooter = (width: number): boolean => width < 600; + interface Props { browserFields: BrowserFields; columns: ColumnHeader[]; @@ -82,7 +83,7 @@ interface Props { } /** The parent Timeline component */ -export const Timeline = pure( +export const Timeline = React.memo( ({ browserFields, columns, @@ -170,38 +171,38 @@ export const Timeline = pure( refetch, }) => ( - - -