From d894adffa6fdebc9b2b74393a0e086cabf9a3b4b Mon Sep 17 00:00:00 2001 From: FrankHassanabad Date: Mon, 12 Aug 2019 15:39:01 -0600 Subject: [PATCH 01/10] Removed extra renderers for super date picker --- .../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 +++ 4 files changed, 613 insertions(+), 24 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 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..a489d018f25a52 --- /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) + ); From 0f134a025c3b56cb924b41daa582c44bc413ed0f Mon Sep 17 00:00:00 2001 From: FrankHassanabad Date: Tue, 13 Aug 2019 10:35:03 -0600 Subject: [PATCH 02/10] Optimizations of flyoutbutton --- .../public/components/flyout/button/index.tsx | 56 ++++++++++--------- .../public/components/flyout/index.test.tsx | 2 + .../siem/public/components/flyout/index.tsx | 19 +++---- .../public/components/flyout/pane/index.tsx | 38 +++++++------ 4 files changed, 62 insertions(+), 53 deletions(-) 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/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'; From e54651aa248f8f2cdc21b27ec738a26dd45cc8f3 Mon Sep 17 00:00:00 2001 From: FrankHassanabad Date: Tue, 13 Aug 2019 15:22:17 -0600 Subject: [PATCH 03/10] left and right property optimization change --- .../public/components/flyout/header/index.tsx | 7 +- .../components/timeline/properties/index.tsx | 236 ++++-------------- .../timeline/properties/properties_left.tsx | 164 ++++++++++++ .../timeline/properties/properties_right.tsx | 193 ++++++++++++++ .../components/timeline/properties/styles.tsx | 14 +- 5 files changed, 408 insertions(+), 206 deletions(-) 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 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/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; From 6bf728030c8f6f56c444ec9765d7db8ce979c8d4 Mon Sep 17 00:00:00 2001 From: FrankHassanabad Date: Wed, 14 Aug 2019 14:55:13 -0600 Subject: [PATCH 04/10] Incremental loading through requestIdleCallback which works on Chrome and Firefox --- .../components/timeline/body/events/index.tsx | 54 ++++++----- .../timeline/body/events/stateful_event.tsx | 19 ++++ .../components/timeline/body/index.test.tsx | 6 +- .../siem/public/lib/helpers/scheduler.ts | 95 +++++++++++++++++++ 4 files changed, 147 insertions(+), 27 deletions(-) create mode 100644 x-pack/legacy/plugins/siem/public/lib/helpers/scheduler.ts 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..d8c2c41107cb01 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 @@ -19,6 +19,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; @@ -83,31 +84,34 @@ export class Events extends React.PureComponent { return ( - {data.map(event => ( - - - - ))} + {data.map((event, i) => { + return ( + + + + ); + })} ); 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..9491461454a6e0 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 @@ -22,6 +22,7 @@ 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'; interface Props { actionsColumnWidth: number; @@ -43,11 +44,13 @@ 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(); @@ -58,8 +61,20 @@ export class StatefulEvent extends React.PureComponent { public readonly state: State = { expanded: {}, showNotes: {}, + initialRender: false, }; + public componentDidMount() { + requestIdleCallbackViaScheduler( + () => { + if (!this.state.initialRender) { + this.setState({ initialRender: true }); + } + }, + { timeout: this.props.maxDelay } + ); + } + public render() { const { actionsColumnWidth, @@ -83,6 +98,10 @@ export class StatefulEvent extends React.PureComponent { width, } = this.props; + if (!this.state.initialRender) { + return null; + } + return ( []; @@ -146,7 +147,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( @@ -179,7 +180,8 @@ describe('Body', () => { /> ); - + await wait(); + wrapper.update(); headersJustTimestamp.forEach(h => { expect( wrapper diff --git a/x-pack/legacy/plugins/siem/public/lib/helpers/scheduler.ts b/x-pack/legacy/plugins/siem/public/lib/helpers/scheduler.ts new file mode 100644 index 00000000000000..ff0c1b61607818 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/lib/helpers/scheduler.ts @@ -0,0 +1,95 @@ +/* + * 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 { scaleLog } from 'd3-scale'; + +// Types are from: https://github.com/Microsoft/TypeScript/issues/21309 +// TODO: Once this is no longer an experimental web API, remove these below +// as they should be Typed by TypeScript +type RequestIdleCallbackHandle = number; +interface RequestIdleCallbackOptions { + timeout: number; +} +interface RequestIdleCallbackDeadline { + readonly didTimeout: boolean; + timeRemaining: () => number; +} + +declare global { + interface Window { + requestIdleCallback: ( + callback: (deadline: RequestIdleCallbackDeadline) => void, + opts?: RequestIdleCallbackOptions + ) => RequestIdleCallbackHandle; + cancelIdleCallback: (handle: RequestIdleCallbackHandle) => void; + } +} + +/** + * Polyfill is from: https://developers.google.com/web/updates/2015/08/using-requestidlecallback + * This is for Safari 12.1.2 and IE-11 + */ +export const polyFillRequestIdleCallback = ( + callback: (deadline: RequestIdleCallbackDeadline) => void +) => { + const start = Date.now(); + return setTimeout(() => { + callback({ + didTimeout: false, + timeRemaining: () => { + return Math.max(0, 50 - (Date.now() - start)); + }, + }); + }, 1); +}; + +/** + * This is a polyfill for "requestIdleCallback" but since it is an + * experimental web API and TypeScript does not even support the API + * properly, I left it as is within this file as a utility for us to change + * tune as needed instead of pushing the experimental API to all users + * of Kibana. + * + * NOTE: This might become obsolete once React releases its own + * scheduler with fibers (Concurrent React) and we would then remove + * this and all usages. Otherwise, just remove this note + */ +export const requestIdleCallbackViaScheduler = ( + callback: (deadline: RequestIdleCallbackDeadline) => void, + opts?: RequestIdleCallbackOptions +) => { + if ('requestIdleCallback' in window) { + window.requestIdleCallback(callback, opts); + } else { + polyFillRequestIdleCallback(callback); + } +}; + +/** + * Use this with any maxDelay such as + * + * data.map((event, i) => { + * const maxDelay = maxDelay(i); + * } + * + * where i is within a loop of elements. You can start at 0 and go to Infinity and it + * will clamp you at 100 milliseconds through 2000 milliseconds (2 seconds) + * to delay a render. + * + * This function guarantees that your first element gets a chance to run up to + * 100 milliseconds through the range of .range([100, 2000]). + * + * NOTE: ScaleLog cannot be given a zero, so the domain starting at 1 + * like so: domain([1, 100]) is intentional. + * + * NOTE: If you go above 25 elements this will at most return 2000 milliseconds for a + * delayMax setting value meaning at most beyond 25 elements to display, they will take at most + * 2 seconds to delay before show up. + */ +export const maxDelay = scaleLog() + .domain([1, 25]) + .range([100, 2000]) + .clamp(true); From 19725d03cf4662a8cff8d2058ef80acdf55db73e Mon Sep 17 00:00:00 2001 From: FrankHassanabad Date: Wed, 14 Aug 2019 15:38:30 -0600 Subject: [PATCH 05/10] Change over to using React.memo for the events component and removed dead function --- .../components/timeline/body/events/index.tsx | 121 +++++++++--------- .../timeline/body/events/stateful_event.tsx | 9 ++ 2 files changed, 66 insertions(+), 64 deletions(-) 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 d8c2c41107cb01..b2b63fcb8a0531 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'; @@ -54,66 +53,60 @@ interface Props { width: number; } -export const getNewNoteId = (): string => uuid.v4(); - -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, i) => { - return ( - - - - ); - })} - - - ); - } -} +export const Events = React.memo( + ({ + actionsColumnWidth, + addNoteToEvent, + browserFields, + columnHeaders, + columnRenderers, + data, + eventIdToNoteIds, + getNotesByIds, + id, + isLoading, + minWidth, + onColumnResized, + onPinEvent, + onUpdateColumns, + onUnPinEvent, + pinnedEventIds, + rowRenderers, + toggleColumn, + updateNote, + width, + }) => ( + + + {data.map((event, i) => { + return ( + + + + ); + })} + + + ) +); 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 9491461454a6e0..01e6b92a98369b 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 @@ -64,6 +64,12 @@ export class StatefulEvent extends React.PureComponent { 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( () => { @@ -98,6 +104,9 @@ 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; } From 52e4cb032b54b15472f634704996a421377c67e2 Mon Sep 17 00:00:00 2001 From: FrankHassanabad Date: Fri, 16 Aug 2019 10:52:44 -0600 Subject: [PATCH 06/10] Changed isLoading to be a non-drill prop for performance reasons and added more React.memo for more performance gains --- .../__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 | 55 +- .../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 - .../__snapshots__/timeline.test.tsx.snap | 756 +----------- .../timeline/body/actions/index.tsx | 18 +- .../__snapshots__/index.test.tsx.snap | 1048 ++++++++++------- .../body/column_headers/actions/index.tsx | 52 +- .../body/column_headers/header/index.test.tsx | 12 - .../body/column_headers/header/index.tsx | 44 +- .../body/column_headers/index.test.tsx | 4 - .../timeline/body/column_headers/index.tsx | 8 +- .../body/events/event_column_view.tsx | 141 ++- .../components/timeline/body/events/index.tsx | 57 +- .../timeline/body/events/stateful_event.tsx | 79 +- .../body/events/stateful_event_child.tsx | 112 ++ .../components/timeline/body/index.test.tsx | 4 - .../public/components/timeline/body/index.tsx | 7 +- .../timeline/body/stateful_body.tsx | 3 - .../timeline/expandable_event/index.tsx | 79 +- .../header/__snapshots__/index.test.tsx.snap | 432 +++---- .../components/timeline/header/index.tsx | 3 +- .../public/components/timeline/timeline.tsx | 67 +- 42 files changed, 1293 insertions(+), 1865 deletions(-) create mode 100644 x-pack/legacy/plugins/siem/public/components/timeline/body/events/stateful_event_child.tsx 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 1b6fe9e3a4f957..e83ba17e27d422 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 @@ -645,7 +645,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 d22c5e1255a647..326d2adae89758 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..69c0a16acefd97 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,41 @@ 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" + /> + ) : ( + + )} + + ); +}); + /** 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 +120,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/timeline/__snapshots__/timeline.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/timeline/__snapshots__/timeline.test.tsx.snap index 8747ef80944737..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,754 +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 8aa88f06d921b1..620b33832f4b4c 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,451 +1,607 @@ // 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} + + ); +}); + const TruncatableHeaderText = styled(TruncatableText)` font-weight: bold; padding: 5px; @@ -64,7 +85,6 @@ TruncatableHeaderText.displayName = 'TruncatableHeaderText'; interface Props { header: ColumnHeader; - isLoading: boolean; onColumnRemoved: OnColumnRemoved; onColumnResized: OnColumnResized; onColumnSorted: OnColumnSorted; @@ -102,14 +122,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 +131,7 @@ export class Header extends React.PureComponent { ( <> - + } @@ -147,7 +156,6 @@ export class Header extends React.PureComponent { { - + )} @@ -165,9 +173,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 ); } -} +); 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 b2b63fcb8a0531..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 @@ -40,7 +40,6 @@ interface Props { eventIdToNoteIds: Readonly>; getNotesByIds: (noteIds: string[]) => Note[]; id: string; - isLoading: boolean; onColumnResized: OnColumnResized; onPinEvent: OnPinEvent; onUpdateColumns: OnUpdateColumns; @@ -64,7 +63,6 @@ export const Events = React.memo( eventIdToNoteIds, getNotesByIds, id, - isLoading, minWidth, onColumnResized, onPinEvent, @@ -78,35 +76,34 @@ export const Events = React.memo( }) => ( - {data.map((event, i) => { - return ( - - - - ); - })} + {data.map((event, i) => ( + + + + ))} ) ); + +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 01e6b92a98369b..7c581a2de7d2d1 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,25 +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; @@ -33,7 +32,6 @@ interface Props { event: TimelineItem; eventIdToNoteIds: Readonly>; getNotesByIds: (noteIds: string[]) => Note[]; - isLoading: boolean; onColumnResized: OnColumnResized; onPinEvent: OnPinEvent; onUpdateColumns: OnUpdateColumns; @@ -55,9 +53,9 @@ interface State { 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: {}, @@ -91,7 +89,6 @@ export class StatefulEvent extends React.PureComponent { event, eventIdToNoteIds, getNotesByIds, - isLoading, onColumnResized, onPinEvent, onUpdateColumns, @@ -110,7 +107,6 @@ export class StatefulEvent extends React.PureComponent { if (!this.state.initialRender) { return null; } - return ( { data: event.ecs, width, children: ( - <> - - - - - - - - - - + ), })} @@ -172,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..34dd2ce7010659 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/events/stateful_event_child.tsx @@ -0,0 +1,112 @@ +/* + * 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, + }) => ( + + + + + + + + + + ) +); 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 c1490e81e2bf80..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 @@ -39,7 +39,6 @@ describe('Body', () => { eventIdToNoteIds={{}} height={testBodyHeight} id={'timeline-test'} - isLoading={false} getNotesByIds={mockGetNotesByIds} onColumnRemoved={jest.fn()} onColumnResized={jest.fn()} @@ -79,7 +78,6 @@ describe('Body', () => { eventIdToNoteIds={{}} height={testBodyHeight} id={'timeline-test'} - isLoading={false} getNotesByIds={mockGetNotesByIds} onColumnRemoved={jest.fn()} onColumnResized={jest.fn()} @@ -119,7 +117,6 @@ describe('Body', () => { eventIdToNoteIds={{}} height={testBodyHeight} id={'timeline-test'} - isLoading={false} getNotesByIds={mockGetNotesByIds} onColumnRemoved={jest.fn()} onColumnResized={jest.fn()} @@ -161,7 +158,6 @@ describe('Body', () => { eventIdToNoteIds={{}} height={testBodyHeight} id={'timeline-test'} - isLoading={false} getNotesByIds={mockGetNotesByIds} onColumnRemoved={jest.fn()} onColumnResized={jest.fn()} 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/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/timeline.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/timeline.tsx index d47c962ba3de96..0fae2b3e79734e 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'; @@ -82,7 +81,7 @@ interface Props { } /** The parent Timeline component */ -export const Timeline = pure( +export const Timeline = React.memo( ({ browserFields, columns, @@ -170,38 +169,38 @@ export const Timeline = pure( refetch, }) => ( - - -