[SIEM] Improves the timeline performance by optimizing the width, removing wasted renderers, and adding a visibility sensor (#43560)

## Summary

This improves the timeline performance by:

* Optimizing the widths using ContextProvider when the AutoSizer is moved.
* Optimizing the renderers found within the Stats component by using React.Memo
* Fixes a small mistake with a ContextProvider which would make the `Header.Div's` re-render on redraws of the width
* Changed the width to use inline in areas as that is recommended by `StyledComponents` as a performance improvement when you have a fast amount of CSS changes dynamically.
* Adds a visibility sensor to so we can perform "windowing" and remove heavy DOM elements from the timeline 

Windowing over 300 items will show "grey" placeholders now until they load
![windowing-up-and-down](https://user-images.githubusercontent.com/1151048/63405479-31db0180-c3a4-11e9-8444-7472e6265b8e.gif)

Another shot of the windowing:
![place-holders-300](https://user-images.githubusercontent.com/1151048/63405598-9b5b1000-c3a4-11e9-927a-4d3e761df0d7.gif)

### Checklist

Use ~~strikethroughs~~ to remove checklist items you don't feel are applicable to this PR.

- [x] This was checked for cross-browser compatibility, [including a check against IE11](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#cross-browser-compatibility)
- [x] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/master/packages/kbn-i18n/README.md)
- [x] [Documentation](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#writing-documentation) was added for features that require explanation or tutorials
- [x] [Unit or functional tests](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#cross-browser-compatibility) were updated or added to match the most common scenarios
- [ ] This was checked for [keyboard-only and screenreader accessibility](https://developer.mozilla.org/en-US/docs/Learn/Tools_and_testing/Cross_browser_testing/Accessibility#Accessibility_testing_checklist)

### For maintainers

- [x] This was checked for breaking API changes and was [labeled appropriately](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#release-notes-process)
- [x] This includes a feature addition or change that requires a release note and was [labeled appropriately](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#release-notes-process)
This commit is contained in:
Frank Hassanabad 2019-08-22 14:01:06 -06:00 committed by GitHub
parent daa86da4cf
commit a69a3f7dd2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
41 changed files with 335 additions and 289 deletions

View file

@ -65,7 +65,7 @@ interface ToolTipProps {
}
const ToolTip = React.memo<ToolTipProps>(({ categoryId, browserFields, onUpdateColumns }) => {
const { isLoading } = useContext(TimelineContext);
const isLoading = useContext(TimelineContext);
return (
<EuiToolTip content={i18n.VIEW_CATEGORY(categoryId)}>
{!isLoading ? (

View file

@ -71,7 +71,7 @@ interface ToolTipProps {
}
const ToolTip = React.memo<ToolTipProps>(({ categoryId, onUpdateColumns, categoryColumns }) => {
const { isLoading } = useContext(TimelineContext);
const isLoading = useContext(TimelineContext);
return (
<EuiToolTip content={i18n.VIEW_CATEGORY(categoryId)}>
{!isLoading ? (

View file

@ -70,7 +70,7 @@ interface DispatchProps {
type Props = OwnProps & StateReduxProps & DispatchProps;
const statefulFlyoutHeader = React.memo<Props>(
const StatefulFlyoutHeader = React.memo<Props>(
({
associateNote,
createTimeline,
@ -112,7 +112,7 @@ const statefulFlyoutHeader = React.memo<Props>(
)
);
statefulFlyoutHeader.displayName = 'statefulFlyoutHeader';
StatefulFlyoutHeader.displayName = 'StatefulFlyoutHeader';
const emptyHistory: History[] = []; // stable reference
@ -212,4 +212,4 @@ const mapDispatchToProps = (dispatch: Dispatch, { timelineId }: OwnProps) => ({
export const FlyoutHeader = connect(
makeMapStateToProps,
mapDispatchToProps
)(statefulFlyoutHeader);
)(StatefulFlyoutHeader);

View file

@ -45,7 +45,6 @@ describe('NoteCards', () => {
showAddNote={true}
toggleShowAddNote={jest.fn()}
updateNote={jest.fn()}
width="100%"
/>
);
@ -62,7 +61,6 @@ describe('NoteCards', () => {
showAddNote={true}
toggleShowAddNote={jest.fn()}
updateNote={jest.fn()}
width="100%"
/>
);
@ -79,7 +77,6 @@ describe('NoteCards', () => {
showAddNote={true}
toggleShowAddNote={jest.fn()}
updateNote={jest.fn()}
width="100%"
/>
);
@ -103,7 +100,6 @@ describe('NoteCards', () => {
showAddNote={true}
toggleShowAddNote={jest.fn()}
updateNote={jest.fn()}
width="100%"
/>
);
@ -120,7 +116,6 @@ describe('NoteCards', () => {
showAddNote={false}
toggleShowAddNote={jest.fn()}
updateNote={jest.fn()}
width="100%"
/>
);

View file

@ -8,10 +8,12 @@ import { EuiFlexGroup, EuiPanel } from '@elastic/eui';
import * as React from 'react';
import styled from 'styled-components';
import { useContext } from 'react';
import { Note } from '../../../lib/note';
import { AddNote } from '../add_note';
import { AssociateNote, GetNewNoteId, UpdateNote } from '../helpers';
import { NoteCard } from '../note_card';
import { TimelineWidthContext } from '../../timeline/timeline_context';
const AddNoteContainer = styled.div``;
@ -23,12 +25,29 @@ const NoteContainer = styled.div`
NoteContainer.displayName = 'NoteContainer';
const NoteCardsContainer = styled(EuiPanel)<{ width?: string }>`
border: none;
width: ${({ width = '100%' }) => width};
`;
interface NoteCardsCompProps {
children: React.ReactNode;
}
NoteCardsContainer.displayName = 'NoteCardsContainer';
const NoteCardsComp = React.memo<NoteCardsCompProps>(({ children }) => {
const width = useContext(TimelineWidthContext);
// Passing the styles directly to the component because the width is
// being calculated and is recommended by Styled Components for performance
// https://github.com/styled-components/styled-components/issues/134#issuecomment-312415291
return (
<EuiPanel
data-test-subj="note-cards"
hasShadow={false}
paddingSize="none"
style={{ width: `${width - 10}px`, border: 'none' }}
>
{children}
</EuiPanel>
);
});
NoteCardsComp.displayName = 'NoteCardsComp';
const NotesContainer = styled(EuiFlexGroup)`
padding: 0 5px;
@ -45,7 +64,6 @@ interface Props {
showAddNote: boolean;
toggleShowAddNote: () => void;
updateNote: UpdateNote;
width?: string;
}
interface State {
@ -68,16 +86,10 @@ export class NoteCards extends React.PureComponent<Props, State> {
showAddNote,
toggleShowAddNote,
updateNote,
width,
} = this.props;
return (
<NoteCardsContainer
data-test-subj="note-cards"
hasShadow={false}
paddingSize="none"
width={width}
>
<NoteCardsComp>
{noteIds.length ? (
<NotesContainer data-test-subj="notes" direction="column" gutterSize="none">
{getNotesByIds(noteIds).map(note => (
@ -100,7 +112,7 @@ export class NoteCards extends React.PureComponent<Props, State> {
/>
</AddNoteContainer>
) : null}
</NoteCardsContainer>
</NoteCardsComp>
);
}

View file

@ -41,35 +41,30 @@ const FlexGroupSpinner = styled(EuiFlexGroup)`
FlexGroupSpinner.displayName = 'FlexGroupSpinner';
export const KpiHostsComponent = ({
data,
from,
loading,
id,
to,
narrowDateRange,
}: KpiHostsProps | KpiHostDetailsProps) => {
const mappings =
(data as KpiHostsData).hosts !== undefined ? kpiHostsMapping : kpiHostDetailsMapping;
const statItemsProps: StatItemsProps[] = useKpiMatrixStatus(
mappings,
data,
id,
from,
to,
narrowDateRange
);
return loading ? (
<FlexGroupSpinner justifyContent="center" alignItems="center">
<EuiFlexItem grow={false}>
<EuiLoadingSpinner size="xl" />
</EuiFlexItem>
</FlexGroupSpinner>
) : (
<EuiFlexGroup>
{statItemsProps.map((mappedStatItemProps, idx) => {
return <StatItemsComponent {...mappedStatItemProps} />;
})}
</EuiFlexGroup>
);
};
export const KpiHostsComponent = React.memo<KpiHostsProps | KpiHostDetailsProps>(
({ data, from, loading, id, to, narrowDateRange }) => {
const mappings =
(data as KpiHostsData).hosts !== undefined ? kpiHostsMapping : kpiHostDetailsMapping;
const statItemsProps: StatItemsProps[] = useKpiMatrixStatus(
mappings,
data,
id,
from,
to,
narrowDateRange
);
return loading ? (
<FlexGroupSpinner justifyContent="center" alignItems="center">
<EuiFlexItem grow={false}>
<EuiLoadingSpinner size="xl" />
</EuiFlexItem>
</FlexGroupSpinner>
) : (
<EuiFlexGroup>
{statItemsProps.map((mappedStatItemProps, idx) => {
return <StatItemsComponent {...mappedStatItemProps} />;
})}
</EuiFlexGroup>
);
}
);

View file

@ -122,21 +122,14 @@ const FlexGroup = styled(EuiFlexGroup)`
FlexGroup.displayName = 'FlexGroup';
export const KpiNetworkBaseComponent = ({
fieldsMapping,
data,
id,
from,
to,
narrowDateRange,
}: {
export const KpiNetworkBaseComponent = React.memo<{
fieldsMapping: Readonly<StatItems[]>;
data: KpiNetworkData;
id: string;
from: number;
to: number;
narrowDateRange: UpdateDateRange;
}) => {
}>(({ fieldsMapping, data, id, from, to, narrowDateRange }) => {
const statItemsProps: StatItemsProps[] = useKpiMatrixStatus(
fieldsMapping,
data,
@ -153,7 +146,7 @@ export const KpiNetworkBaseComponent = ({
})}
</EuiFlexGroup>
);
};
});
export const KpiNetworkComponent = React.memo<KpiNetworkProps>(
({ data, from, id, loading, to, narrowDateRange }) => {

View file

@ -67,7 +67,7 @@ export const CloseButton = pure<{
CloseButton.displayName = 'CloseButton';
export const Actions = React.memo<Props>(({ header, onColumnRemoved, show, sort }) => {
const { isLoading } = useContext(TimelineContext);
const isLoading = useContext(TimelineContext);
return (
<ActionsContainer
alignItems="center"

View file

@ -64,7 +64,7 @@ interface HeaderCompProps {
}
const HeaderComp = React.memo<HeaderCompProps>(({ children, onClick, isResizing }) => {
const { isLoading } = useContext(TimelineContext);
const isLoading = useContext(TimelineContext);
return (
<HeaderDiv
data-test-subj="header"

View file

@ -49,7 +49,6 @@ interface Props {
rowRenderers: RowRenderer[];
toggleColumn: (column: ColumnHeader) => void;
updateNote: UpdateNote;
width: number;
}
export const Events = React.memo<Props>(
@ -72,7 +71,6 @@ export const Events = React.memo<Props>(
rowRenderers,
toggleColumn,
updateNote,
width,
}) => (
<EventsContainer data-test-subj="events" minWidth={minWidth}>
<EuiFlexGroup data-test-subj="events-flex-group" direction="column" gutterSize="none">
@ -96,7 +94,6 @@ export const Events = React.memo<Props>(
timelineId={id}
toggleColumn={toggleColumn}
updateNote={updateNote}
width={width}
maxDelay={maxDelay(i)}
/>
</EuiFlexItem>

View file

@ -8,6 +8,8 @@ import { EuiFlexItem } from '@elastic/eui';
import * as React from 'react';
import uuid from 'uuid';
import VisibilitySensor from 'react-visibility-sensor';
import styled from 'styled-components';
import { BrowserFields } from '../../../../containers/source';
import { TimelineDetailsComponentQuery } from '../../../../containers/timeline/details';
import { TimelineItem, DetailItem } from '../../../../graphql/types';
@ -41,7 +43,6 @@ interface Props {
timelineId: string;
toggleColumn: (column: ColumnHeader) => void;
updateNote: UpdateNote;
width: number;
maxDelay?: number;
}
@ -55,6 +56,54 @@ export const getNewNoteId = (): string => uuid.v4();
const emptyDetails: DetailItem[] = [];
/**
* This is the default row height whenever it is a plain row renderer and not a custom row height.
* We use this value when we do not know the height of a particular row.
*/
const DEFAULT_ROW_HEIGHT = '27px';
/**
* This is the default margin size from the EmptyRow below and is the top and bottom
* margin added together.
* If you change margin: 5px 0 5px 0; within EmptyRow styled component, then please
* update this value
*/
const EMPTY_ROW_MARGIN_TOP_BOTTOM = 10;
/**
* This is the top offset in pixels of the top part of the timeline. The UI area where you do your
* drag and drop and filtering. It is a positive number in pixels of _PART_ of the header but not
* the entire header. We leave room for some rows to render behind the drag and drop so they might be
* visible by the time the user scrolls upwards. All other DOM elements are replaced with their "blank"
* rows.
*/
const TOP_OFFSET = 50;
/**
* This is the bottom offset in pixels of the bottom part of the timeline. The UI area right below the
* timeline which is the footer. Since the footer is so incredibly small we don't have enough room to
* render around 5 rows below the timeline to get the user the best chance of always scrolling without seeing
* "blank rows". The negative number is to give the bottom of the browser window a bit of invisible space to
* keep around 5 rows rendering below it. All other DOM elements are replaced with their "blank"
* rows.
*/
const BOTTOM_OFFSET = -500;
/**
* This is missing the height props intentionally for performance reasons that
* you can see here:
* https://github.com/styled-components/styled-components/issues/134#issuecomment-312415291
*
* We inline the height style for performance reasons directly on the component.
*/
export const EmptyRow = styled.div`
background-color: ${props => props.theme.eui.euiColorLightestShade};
width: 100%;
border: ${props => `1px solid ${props.theme.eui.euiColorLightShade}`};
border-radius: 5px;
margin: 5px 0 5px 0;
`;
export class StatefulEvent extends React.Component<Props, State> {
public readonly state: State = {
expanded: {},
@ -62,6 +111,8 @@ export class StatefulEvent extends React.Component<Props, State> {
initialRender: false,
};
public divElement: HTMLDivElement | null = null;
/**
* Incrementally loads the events when it mounts by trying to
* see if it resides within a window frame and if it is it will
@ -98,69 +149,99 @@ export class StatefulEvent extends React.Component<Props, State> {
timelineId,
toggleColumn,
updateNote,
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;
// height is being inlined directly in here because of performance with StyledComponents
// involving quick and constant changes to the DOM.
// https://github.com/styled-components/styled-components/issues/134#issuecomment-312415291
return <EmptyRow style={{ height: DEFAULT_ROW_HEIGHT }}></EmptyRow>;
}
return (
<TimelineDetailsComponentQuery
sourceId="default"
indexName={event._index!}
eventId={event._id}
executeQuery={!!this.state.expanded[event._id]}
<VisibilitySensor
partialVisibility={true}
scrollCheck={true}
offset={{ top: TOP_OFFSET, bottom: BOTTOM_OFFSET }}
>
{({ detailsData, loading }) => (
<div data-test-subj="event">
{getRowRenderer(event.ecs, rowRenderers).renderRow({
browserFields,
data: event.ecs,
width,
children: (
<StatefulEventChild
id={event._id}
actionsColumnWidth={actionsColumnWidth}
associateNote={this.associateNote}
addNoteToEvent={addNoteToEvent}
onPinEvent={onPinEvent}
columnHeaders={columnHeaders}
columnRenderers={columnRenderers}
expanded={!!this.state.expanded[event._id]}
data={event.data}
eventIdToNoteIds={eventIdToNoteIds}
getNotesByIds={getNotesByIds}
loading={loading}
onColumnResized={onColumnResized}
onToggleExpanded={this.onToggleExpanded}
onUnPinEvent={onUnPinEvent}
pinnedEventIds={pinnedEventIds}
showNotes={!!this.state.showNotes[event._id]}
onToggleShowNotes={this.onToggleShowNotes}
updateNote={updateNote}
width={width}
/>
),
})}
<EuiFlexItem data-test-subj="event-details" grow={true}>
<ExpandableEvent
browserFields={browserFields}
columnHeaders={columnHeaders}
id={event._id}
event={detailsData || emptyDetails}
forceExpand={!!this.state.expanded[event._id] && !loading}
onUpdateColumns={onUpdateColumns}
timelineId={timelineId}
toggleColumn={toggleColumn}
width={width}
/>
</EuiFlexItem>
</div>
)}
</TimelineDetailsComponentQuery>
{({ isVisible }) => {
if (isVisible) {
return (
<TimelineDetailsComponentQuery
sourceId="default"
indexName={event._index!}
eventId={event._id}
executeQuery={!!this.state.expanded[event._id]}
>
{({ detailsData, loading }) => (
<div
data-test-subj="event"
ref={divElement => {
if (divElement != null) {
this.divElement = divElement;
}
}}
>
{getRowRenderer(event.ecs, rowRenderers).renderRow({
browserFields,
data: event.ecs,
children: (
<StatefulEventChild
id={event._id}
actionsColumnWidth={actionsColumnWidth}
associateNote={this.associateNote}
addNoteToEvent={addNoteToEvent}
onPinEvent={onPinEvent}
columnHeaders={columnHeaders}
columnRenderers={columnRenderers}
expanded={!!this.state.expanded[event._id]}
data={event.data}
eventIdToNoteIds={eventIdToNoteIds}
getNotesByIds={getNotesByIds}
loading={loading}
onColumnResized={onColumnResized}
onToggleExpanded={this.onToggleExpanded}
onUnPinEvent={onUnPinEvent}
pinnedEventIds={pinnedEventIds}
showNotes={!!this.state.showNotes[event._id]}
onToggleShowNotes={this.onToggleShowNotes}
updateNote={updateNote}
/>
),
})}
<EuiFlexItem data-test-subj="event-details" grow={true}>
<ExpandableEvent
browserFields={browserFields}
columnHeaders={columnHeaders}
id={event._id}
event={detailsData || emptyDetails}
forceExpand={!!this.state.expanded[event._id] && !loading}
onUpdateColumns={onUpdateColumns}
timelineId={timelineId}
toggleColumn={toggleColumn}
/>
</EuiFlexItem>
</div>
)}
</TimelineDetailsComponentQuery>
);
} else {
// Height place holder for visibility detection as well as re-rendering sections.
const height =
this.divElement != null
? `${this.divElement.clientHeight - EMPTY_ROW_MARGIN_TOP_BOTTOM}px`
: DEFAULT_ROW_HEIGHT;
// height is being inlined directly in here because of performance with StyledComponents
// involving quick and constant changes to the DOM.
// https://github.com/styled-components/styled-components/issues/134#issuecomment-312415291
return <EmptyRow style={{ height }}></EmptyRow>;
}
}}
</VisibilitySensor>
);
}

View file

@ -35,7 +35,6 @@ interface Props {
onToggleExpanded: (eventId: string) => () => void;
onToggleShowNotes: (eventId: string) => () => void;
getNotesByIds: (noteIds: string[]) => Note[];
width: number;
associateNote: (
eventId: string,
addNoteToEvent: AddNoteToEvent,
@ -68,7 +67,6 @@ export const StatefulEventChild = React.memo<Props>(
showNotes,
onToggleShowNotes,
updateNote,
width,
}) => (
<EuiFlexGroup data-test-subj="event-rows" direction="column" gutterSize="none">
<EuiFlexItem data-test-subj="event-column-data" grow={false}>
@ -104,7 +102,6 @@ export const StatefulEventChild = React.memo<Props>(
showAddNote={showNotes}
toggleShowAddNote={onToggleShowNotes(id)}
updateNote={updateNote}
width={`${width - 10}px`}
/>
</EuiFlexItem>
</EuiFlexGroup>

View file

@ -27,6 +27,12 @@ const mockSort: Sort = {
sortDirection: Direction.desc,
};
jest.mock(
'react-visibility-sensor',
() => ({ children }: { children: (args: { isVisible: boolean }) => React.ReactNode }) =>
children({ isVisible: true })
);
describe('Body', () => {
describe('rendering', () => {
test('it renders the column headers', () => {
@ -55,7 +61,6 @@ describe('Body', () => {
sort={mockSort}
toggleColumn={jest.fn()}
updateNote={jest.fn()}
width={100}
/>
</TestProviders>
);
@ -94,7 +99,6 @@ describe('Body', () => {
sort={mockSort}
toggleColumn={jest.fn()}
updateNote={jest.fn()}
width={100}
/>
</TestProviders>
);
@ -133,7 +137,6 @@ describe('Body', () => {
sort={mockSort}
toggleColumn={jest.fn()}
updateNote={jest.fn()}
width={100}
/>
</TestProviders>
);
@ -174,13 +177,13 @@ describe('Body', () => {
sort={mockSort}
toggleColumn={jest.fn()}
updateNote={jest.fn()}
width={100}
/>
</TestProviders>
);
wrapper.update();
await wait();
wrapper.update();
headersJustTimestamp.forEach(h => {
headersJustTimestamp.forEach(() => {
expect(
wrapper
.find('[data-test-subj="data-driven-columns"]')

View file

@ -54,7 +54,6 @@ interface Props {
sort: Sort;
toggleColumn: (column: ColumnHeader) => void;
updateNote: UpdateNote;
width: number;
}
const HorizontalScroll = styled.div<{
@ -106,7 +105,6 @@ export const Body = React.memo<Props>(
sort,
toggleColumn,
updateNote,
width,
}) => {
const columnWidths = columnHeaders.reduce(
(totalWidth, header) => totalWidth + header.width,
@ -156,7 +154,6 @@ export const Body = React.memo<Props>(
toggleColumn={toggleColumn}
updateNote={updateNote}
minWidth={columnWidths}
width={width}
/>
</VerticalScrollContainer>
</EuiText>

View file

@ -6,9 +6,7 @@ exports[`GenericRowRenderer #createGenericAuditRowRenderer renders correctly aga
<span>
some children
</span>
<RowRendererContainer
width={100}
>
<RowRendererContainer>
<AuditdGenericDetails
browserFields={Object {}}
contextId="connected-to"
@ -132,9 +130,7 @@ exports[`GenericRowRenderer #createGenericFileRowRenderer renders correctly agai
<span>
some children
</span>
<RowRendererContainer
width={100}
>
<RowRendererContainer>
<AuditdGenericFileDetails
browserFields={Object {}}
contextId="opened-file"

View file

@ -38,7 +38,6 @@ describe('GenericRowRenderer', () => {
const children = connectedToRenderer.renderRow({
browserFields,
data: auditd,
width: 100,
children: <span>{'some children'}</span>,
});
@ -68,7 +67,6 @@ describe('GenericRowRenderer', () => {
const children = connectedToRenderer.renderRow({
browserFields: mockBrowserFields,
data: nonAuditd,
width: 100,
children: <span>{'some children'}</span>,
});
const wrapper = mount(
@ -83,7 +81,6 @@ describe('GenericRowRenderer', () => {
const children = connectedToRenderer.renderRow({
browserFields: mockBrowserFields,
data: auditd,
width: 100,
children: <span>{'some children '}</span>,
});
const wrapper = mount(
@ -117,7 +114,6 @@ describe('GenericRowRenderer', () => {
const children = fileToRenderer.renderRow({
browserFields,
data: auditdFile,
width: 100,
children: <span>{'some children'}</span>,
});
@ -147,7 +143,6 @@ describe('GenericRowRenderer', () => {
const children = fileToRenderer.renderRow({
browserFields: mockBrowserFields,
data: nonAuditd,
width: 100,
children: <span>{'some children'}</span>,
});
const wrapper = mount(
@ -162,7 +157,6 @@ describe('GenericRowRenderer', () => {
const children = fileToRenderer.renderRow({
browserFields: mockBrowserFields,
data: auditdFile,
width: 100,
children: <span>{'some children '}</span>,
});
const wrapper = mount(

View file

@ -31,10 +31,10 @@ export const createGenericAuditRowRenderer = ({
action.toLowerCase() === actionName
);
},
renderRow: ({ browserFields, data, width, children }) => (
renderRow: ({ browserFields, data, children }) => (
<Row>
{children}
<RowRendererContainer width={width}>
<RowRendererContainer>
<AuditdGenericDetails
browserFields={browserFields}
data={data}
@ -65,10 +65,10 @@ export const createGenericFileRowRenderer = ({
action.toLowerCase() === actionName
);
},
renderRow: ({ browserFields, data, width, children }) => (
renderRow: ({ browserFields, data, children }) => (
<Row>
{children}
<RowRendererContainer width={width}>
<RowRendererContainer>
<AuditdGenericFileDetails
browserFields={browserFields}
data={data}

View file

@ -31,7 +31,6 @@ describe('get_column_renderer', () => {
const row = rowRenderer.renderRow({
browserFields: mockBrowserFields,
data: nonSuricata,
width: 100,
children: <span>{'some child'}</span>,
});
@ -44,7 +43,6 @@ describe('get_column_renderer', () => {
const row = rowRenderer.renderRow({
browserFields: mockBrowserFields,
data: nonSuricata,
width: 100,
children: <span>{'some child'}</span>,
});
const wrapper = mount(
@ -60,7 +58,6 @@ describe('get_column_renderer', () => {
const row = rowRenderer.renderRow({
browserFields: mockBrowserFields,
data: suricata,
width: 100,
children: <span>{'some child '}</span>,
});
const wrapper = mount(

View file

@ -6,9 +6,7 @@ exports[`netflowRowRenderer renders correctly against snapshot 1`] = `
<span>
some children
</span>
<RowRendererContainer
width={500}
>
<RowRendererContainer>
<Details>
<Netflow
contextId="network_flow"

View file

@ -32,7 +32,6 @@ describe('netflowRowRenderer', () => {
const children = netflowRowRenderer.renderRow({
browserFields,
data: getMockNetflowData(),
width: 500,
children: <span>{'some children'}</span>,
});
@ -102,7 +101,6 @@ describe('netflowRowRenderer', () => {
const children = netflowRowRenderer.renderRow({
browserFields: mockBrowserFields,
data: justIdAndTimestamp,
width: 500,
children: <span>{'some children'}</span>,
});
const wrapper = mount(
@ -117,7 +115,6 @@ describe('netflowRowRenderer', () => {
const children = netflowRowRenderer.renderRow({
browserFields: mockBrowserFields,
data: getMockNetflowData(),
width: 500,
children: <span>{'some children'}</span>,
});
const wrapper = mount(

View file

@ -84,10 +84,10 @@ export const netflowRowRenderer: RowRenderer = {
eventActionMatches(get(EVENT_ACTION_FIELD, ecs))
);
},
renderRow: ({ data, width, children }) => (
renderRow: ({ data, children }) => (
<Row>
{children}
<RowRendererContainer width={width}>
<RowRendererContainer>
<Details>
<Netflow
contextId={NETWORK_FLOW}

View file

@ -26,7 +26,6 @@ describe('plain_row_renderer', () => {
const children = plainRowRenderer.renderRow({
browserFields: mockBrowserFields,
data: mockDatum,
width: 100,
children: <span>{'some children'}</span>,
});
const wrapper = shallow(<span>{children}</span>);
@ -41,7 +40,6 @@ describe('plain_row_renderer', () => {
const children = plainRowRenderer.renderRow({
browserFields: mockBrowserFields,
data: mockDatum,
width: 100,
children: <span>{'some children'}</span>,
});
const wrapper = mount(

View file

@ -4,14 +4,23 @@
* you may not use this file except in compliance with the Elastic License.
*/
import styled from 'styled-components';
import React, { useContext } from 'react';
import { BrowserFields } from '../../../../containers/source';
import { Ecs } from '../../../../graphql/types';
import { TimelineWidthContext } from '../../timeline_context';
export const RowRendererContainer = styled.div<{ width: number }>`
width: ${({ width }) => `${width}px`};
`;
interface RowRendererContainerProps {
children: React.ReactNode;
}
export const RowRendererContainer = React.memo<RowRendererContainerProps>(({ children }) => {
const width = useContext(TimelineWidthContext);
// Passing the styles directly to the component because the width is
// being calculated and is recommended by Styled Components for performance
// https://github.com/styled-components/styled-components/issues/134#issuecomment-312415291
return <div style={{ width: `${width}px` }}>{children}</div>;
});
RowRendererContainer.displayName = 'RowRendererContainer';
@ -20,12 +29,10 @@ export interface RowRenderer {
renderRow: ({
browserFields,
data,
width,
children,
}: {
browserFields: BrowserFields;
data: Ecs;
width: number;
children: React.ReactNode;
}) => React.ReactNode;
}

View file

@ -6,9 +6,7 @@ exports[`suricata_row_renderer renders correctly against snapshot 1`] = `
<span>
some children
</span>
<RowRendererContainer
width={100}
>
<RowRendererContainer>
<SuricataDetails
browserFields={
Object {

View file

@ -28,7 +28,6 @@ describe('suricata_row_renderer', () => {
const children = suricataRowRenderer.renderRow({
browserFields: mockBrowserFields,
data: nonSuricata,
width: 100,
children: <span>{'some children'}</span>,
});
@ -48,7 +47,6 @@ describe('suricata_row_renderer', () => {
const children = suricataRowRenderer.renderRow({
browserFields: mockBrowserFields,
data: nonSuricata,
width: 100,
children: <span>{'some children'}</span>,
});
const wrapper = mount(
@ -63,7 +61,6 @@ describe('suricata_row_renderer', () => {
const children = suricataRowRenderer.renderRow({
browserFields: mockBrowserFields,
data: suricata,
width: 100,
children: <span>{'some children '}</span>,
});
const wrapper = mount(
@ -81,7 +78,6 @@ describe('suricata_row_renderer', () => {
const children = suricataRowRenderer.renderRow({
browserFields: mockBrowserFields,
data: suricata,
width: 100,
children: <span>{'some children'}</span>,
});
const wrapper = mount(

View file

@ -16,11 +16,11 @@ export const suricataRowRenderer: RowRenderer = {
const module: string | null | undefined = get('event.module[0]', ecs);
return module != null && module.toLowerCase() === 'suricata';
},
renderRow: ({ browserFields, data, width, children }) => {
renderRow: ({ browserFields, data, children }) => {
return (
<Row>
{children}
<RowRendererContainer width={width}>
<RowRendererContainer>
<SuricataDetails data={data} browserFields={browserFields} />
</RowRendererContainer>
</Row>

View file

@ -6,9 +6,7 @@ exports[`GenericRowRenderer #createGenericFileRowRenderer renders correctly agai
<span>
some children
</span>
<RowRendererContainer
width={100}
>
<RowRendererContainer>
<SystemGenericFileDetails
browserFields={Object {}}
contextId="user_login"
@ -109,9 +107,7 @@ exports[`GenericRowRenderer #createGenericSystemRowRenderer renders correctly ag
<span>
some children
</span>
<RowRendererContainer
width={100}
>
<RowRendererContainer>
<SystemGenericDetails
browserFields={Object {}}
contextId="process_started"

View file

@ -38,7 +38,6 @@ describe('GenericRowRenderer', () => {
const children = connectedToRenderer.renderRow({
browserFields,
data: system,
width: 100,
children: <span>{'some children'}</span>,
});
@ -68,7 +67,6 @@ describe('GenericRowRenderer', () => {
const children = connectedToRenderer.renderRow({
browserFields: mockBrowserFields,
data: system,
width: 100,
children: <span>{'some children '}</span>,
});
const wrapper = mount(
@ -102,7 +100,6 @@ describe('GenericRowRenderer', () => {
const children = fileToRenderer.renderRow({
browserFields,
data: systemFile,
width: 100,
children: <span>{'some children'}</span>,
});
@ -131,7 +128,6 @@ describe('GenericRowRenderer', () => {
const children = fileToRenderer.renderRow({
browserFields: mockBrowserFields,
data: systemFile,
width: 100,
children: <span>{'some children '}</span>,
});
const wrapper = mount(

View file

@ -30,10 +30,10 @@ export const createGenericSystemRowRenderer = ({
action.toLowerCase() === actionName
);
},
renderRow: ({ browserFields, data, width, children }) => (
renderRow: ({ browserFields, data, children }) => (
<Row>
{children}
<RowRendererContainer width={width}>
<RowRendererContainer>
<SystemGenericDetails
browserFields={browserFields}
data={data}
@ -62,10 +62,10 @@ export const createGenericFileRowRenderer = ({
action.toLowerCase() === actionName
);
},
renderRow: ({ browserFields, data, width, children }) => (
renderRow: ({ browserFields, data, children }) => (
<Row>
{children}
<RowRendererContainer width={width}>
<RowRendererContainer>
<SystemGenericFileDetails
browserFields={browserFields}
data={data}

View file

@ -6,9 +6,7 @@ exports[`zeek_row_renderer renders correctly against snapshot 1`] = `
<span>
some children
</span>
<RowRendererContainer
width={100}
>
<RowRendererContainer>
<ZeekDetails
browserFields={
Object {

View file

@ -27,7 +27,6 @@ describe('zeek_row_renderer', () => {
const children = zeekRowRenderer.renderRow({
browserFields: mockBrowserFields,
data: nonZeek,
width: 100,
children: <span>{'some children'}</span>,
});
@ -47,7 +46,6 @@ describe('zeek_row_renderer', () => {
const children = zeekRowRenderer.renderRow({
browserFields: mockBrowserFields,
data: nonZeek,
width: 100,
children: <span>{'some children'}</span>,
});
const wrapper = mount(
@ -62,7 +60,6 @@ describe('zeek_row_renderer', () => {
const children = zeekRowRenderer.renderRow({
browserFields: mockBrowserFields,
data: zeek,
width: 100,
children: <span>{'some children '}</span>,
});
const wrapper = mount(

View file

@ -16,10 +16,10 @@ export const zeekRowRenderer: RowRenderer = {
const module: string | null | undefined = get('event.module[0]', ecs);
return module != null && module.toLowerCase() === 'zeek';
},
renderRow: ({ browserFields, data, width, children }) => (
renderRow: ({ browserFields, data, children }) => (
<Row>
{children}
<RowRendererContainer width={width}>
<RowRendererContainer>
<ZeekDetails data={data} browserFields={browserFields} />
</RowRendererContainer>
</Row>

View file

@ -39,14 +39,13 @@ interface OwnProps {
height: number;
sort: Sort;
toggleColumn: (column: ColumnHeader) => void;
width: number;
}
interface ReduxProps {
columnHeaders: ColumnHeader[];
eventIdToNoteIds?: Readonly<Record<string, string[]>>;
eventIdToNoteIds: Readonly<Record<string, string[]>>;
getNotesByIds: (noteIds: string[]) => Note[];
pinnedEventIds?: Readonly<Record<string, boolean>>;
pinnedEventIds: Readonly<Record<string, boolean>>;
range?: string;
}
@ -82,6 +81,8 @@ interface DispatchProps {
type StatefulBodyComponentProps = OwnProps & ReduxProps & DispatchProps;
export const emptyColumnHeaders: ColumnHeader[] = [];
class StatefulBodyComponent extends React.PureComponent<StatefulBodyComponentProps> {
public render() {
const {
@ -96,7 +97,6 @@ class StatefulBodyComponent extends React.PureComponent<StatefulBodyComponentPro
range,
sort,
toggleColumn,
width,
} = this.props;
return (
@ -104,10 +104,10 @@ class StatefulBodyComponent extends React.PureComponent<StatefulBodyComponentPro
addNoteToEvent={this.onAddNoteToEvent}
browserFields={browserFields}
id={id}
columnHeaders={columnHeaders || []}
columnHeaders={columnHeaders || emptyColumnHeaders}
columnRenderers={columnRenderers}
data={data}
eventIdToNoteIds={eventIdToNoteIds!}
eventIdToNoteIds={eventIdToNoteIds}
getNotesByIds={getNotesByIds}
height={height}
onColumnResized={this.onColumnResized}
@ -117,13 +117,12 @@ class StatefulBodyComponent extends React.PureComponent<StatefulBodyComponentPro
onPinEvent={this.onPinEvent}
onUpdateColumns={this.onUpdateColumns}
onUnPinEvent={this.onUnPinEvent}
pinnedEventIds={pinnedEventIds!}
pinnedEventIds={pinnedEventIds}
range={range!}
rowRenderers={rowRenderers}
sort={sort}
toggleColumn={toggleColumn}
updateNote={this.onUpdateNote}
width={width}
/>
);
}

View file

@ -87,7 +87,7 @@ export const DataProviders = pure<Props>(
}) => (
<DropTargetDataProviders data-test-subj="dataProviders">
<TimelineContext.Consumer>
{({ isLoading }) => (
{isLoading => (
<DroppableWrapper isDropDisabled={!show || isLoading} droppableId={getDroppableId(id)}>
{dataProviders != null && dataProviders.length ? (
<Providers

View file

@ -59,7 +59,7 @@ export class ProviderItemBadge extends PureComponent<ProviderItemBadgeProps, Own
return (
<TimelineContext.Consumer>
{({ isLoading }) => (
{isLoading => (
<ProviderItemActions
andProviderId={andProviderId}
browserFields={browserFields}

View file

@ -95,7 +95,7 @@ describe('Providers', () => {
const mockOnDataProviderRemoved = jest.fn();
const wrapper = mount(
<TestProviders>
<TimelineContext.Provider value={{ isLoading: true }}>
<TimelineContext.Provider value={true}>
<DroppableWrapper droppableId="unitTest">
<Providers
browserFields={{}}
@ -158,7 +158,7 @@ describe('Providers', () => {
const mockOnDataProviderRemoved = jest.fn();
const wrapper = mount(
<TestProviders>
<TimelineContext.Provider value={{ isLoading: true }}>
<TimelineContext.Provider value={true}>
<DroppableWrapper droppableId="unitTest">
<Providers
browserFields={{}}
@ -240,7 +240,7 @@ describe('Providers', () => {
const mockOnToggleDataProviderEnabled = jest.fn();
const wrapper = mount(
<TestProviders>
<TimelineContext.Provider value={{ isLoading: true }}>
<TimelineContext.Provider value={true}>
<DroppableWrapper droppableId="unitTest">
<Providers
browserFields={{}}
@ -318,7 +318,7 @@ describe('Providers', () => {
const wrapper = mount(
<TestProviders>
<TimelineContext.Provider value={{ isLoading: true }}>
<TimelineContext.Provider value={true}>
<DroppableWrapper droppableId="unitTest">
<Providers
browserFields={{}}
@ -427,7 +427,7 @@ describe('Providers', () => {
const wrapper = mount(
<TestProviders>
<TimelineContext.Provider value={{ isLoading: true }}>
<TimelineContext.Provider value={true}>
<DroppableWrapper droppableId="unitTest">
<Providers
browserFields={{}}
@ -508,7 +508,7 @@ describe('Providers', () => {
const wrapper = mount(
<TestProviders>
<TimelineContext.Provider value={{ isLoading: true }}>
<TimelineContext.Provider value={true}>
<DroppableWrapper droppableId="unitTest">
<Providers
browserFields={{}}
@ -594,7 +594,7 @@ describe('Providers', () => {
const wrapper = mount(
<TestProviders>
<TimelineContext.Provider value={{ isLoading: true }}>
<TimelineContext.Provider value={true}>
<DroppableWrapper droppableId="unitTest">
<Providers
browserFields={{}}

View file

@ -7,23 +7,24 @@
import * as React from 'react';
import styled from 'styled-components';
import { useContext } from 'react';
import { BrowserFields } from '../../../containers/source';
import { ColumnHeader } from '../body/column_headers/column_header';
import { DetailItem } from '../../../graphql/types';
import { StatefulEventDetails } from '../../event_details/stateful_event_details';
import { LazyAccordion } from '../../lazy_accordion';
import { OnUpdateColumns } from '../events';
import { TimelineWidthContext } from '../timeline_context';
const ExpandableDetails = styled.div<{ hideExpandButton: boolean; width?: number }>`
width: ${({ width }) => (width != null ? `${width}px;` : '100%')}
${({ hideExpandButton }) =>
hideExpandButton
? `
const ExpandableDetails = styled.div<{ hideExpandButton: boolean }>`
${({ hideExpandButton }) =>
hideExpandButton
? `
.euiAccordion__button {
display: none;
}
`
: ''};
: ''};
`;
ExpandableDetails.displayName = 'ExpandableDetails';
@ -38,7 +39,6 @@ interface Props {
onUpdateColumns: OnUpdateColumns;
timelineId: string;
toggleColumn: (column: ColumnHeader) => void;
width?: number;
}
export const ExpandableEvent = React.memo<Props>(
@ -51,31 +51,33 @@ export const ExpandableEvent = React.memo<Props>(
timelineId,
toggleColumn,
onUpdateColumns,
width,
}) => (
<ExpandableDetails
data-test-subj="timeline-expandable-details"
hideExpandButton={true}
width={width}
>
<LazyAccordion
id={`timeline-${timelineId}-row-${id}`}
renderExpandedContent={() => (
<StatefulEventDetails
browserFields={browserFields}
columnHeaders={columnHeaders}
data={event}
id={id}
onUpdateColumns={onUpdateColumns}
timelineId={timelineId}
toggleColumn={toggleColumn}
/>
)}
forceExpand={forceExpand}
paddingSize="none"
/>
</ExpandableDetails>
)
}) => {
const width = useContext(TimelineWidthContext);
// Passing the styles directly to the component of LazyAccordion because the width is
// being calculated and is recommended by Styled Components for performance
// https://github.com/styled-components/styled-components/issues/134#issuecomment-312415291
return (
<ExpandableDetails hideExpandButton={true}>
<LazyAccordion
style={{ width: `${width}px` }}
id={`timeline-${timelineId}-row-${id}`}
renderExpandedContent={() => (
<StatefulEventDetails
browserFields={browserFields}
columnHeaders={columnHeaders}
data={event}
id={id}
onUpdateColumns={onUpdateColumns}
timelineId={timelineId}
toggleColumn={toggleColumn}
/>
)}
forceExpand={forceExpand}
paddingSize="none"
/>
</ExpandableDetails>
);
}
);
ExpandableEvent.displayName = 'ExpandableEvent';

View file

@ -34,7 +34,7 @@ import { Footer, footerHeight } from './footer';
import { TimelineHeader } from './header';
import { calculateBodyHeight, combineQueries } from './helpers';
import { TimelineRefetch } from './refetch_timeline';
import { TimelineContext } from './timeline_context';
import { TimelineContext, TimelineWidthContext } from './timeline_context';
const WrappedByAutoSizer = styled.div`
width: 100%;
@ -171,37 +171,38 @@ export const Timeline = React.memo<Props>(
refetch,
}) => (
<TimelineRefetch loading={loading} id={id} inspect={inspect} refetch={refetch}>
<TimelineContext.Provider value={{ isLoading: loading }}>
<StatefulBody
browserFields={browserFields}
data={events}
id={id}
height={calculateBodyHeight({
flyoutHeight,
flyoutHeaderHeight,
timelineHeaderHeight,
timelineFooterHeight: footerHeight,
})}
sort={sort}
toggleColumn={toggleColumn}
width={width}
/>
<Footer
serverSideEventCount={totalCount}
hasNextPage={getOr(false, 'hasNextPage', pageInfo)!}
height={footerHeight}
isLive={isLive}
isLoading={loading}
itemsCount={events.length}
itemsPerPage={itemsPerPage}
itemsPerPageOptions={itemsPerPageOptions}
onChangeItemsPerPage={onChangeItemsPerPage}
onLoadMore={loadMore}
nextCursor={getOr(null, 'endCursor.value', pageInfo)!}
tieBreaker={getOr(null, 'endCursor.tiebreaker', pageInfo)}
getUpdatedAt={getUpdatedAt}
compact={isCompactFooter(width)}
/>
<TimelineContext.Provider value={loading}>
<TimelineWidthContext.Provider value={width}>
<StatefulBody
browserFields={browserFields}
data={events}
id={id}
height={calculateBodyHeight({
flyoutHeight,
flyoutHeaderHeight,
timelineHeaderHeight,
timelineFooterHeight: footerHeight,
})}
sort={sort}
toggleColumn={toggleColumn}
/>
<Footer
serverSideEventCount={totalCount}
hasNextPage={getOr(false, 'hasNextPage', pageInfo)!}
height={footerHeight}
isLive={isLive}
isLoading={loading}
itemsCount={events.length}
itemsPerPage={itemsPerPage}
itemsPerPageOptions={itemsPerPageOptions}
onChangeItemsPerPage={onChangeItemsPerPage}
onLoadMore={loadMore}
nextCursor={getOr(null, 'endCursor.value', pageInfo)!}
tieBreaker={getOr(null, 'endCursor.tiebreaker', pageInfo)}
getUpdatedAt={getUpdatedAt}
compact={isCompactFooter(width)}
/>
</TimelineWidthContext.Provider>
</TimelineContext.Provider>
</TimelineRefetch>
)}

View file

@ -6,8 +6,6 @@
import * as React from 'react';
export interface TimelineContextData {
isLoading: boolean;
}
export const TimelineContext = React.createContext<boolean>(false);
export const TimelineContext = React.createContext<TimelineContextData>({ isLoading: false });
export const TimelineWidthContext = React.createContext<number>(0);

View file

@ -329,6 +329,7 @@
"react-sticky": "^6.0.3",
"react-syntax-highlighter": "^5.7.0",
"react-vis": "^1.8.1",
"react-visibility-sensor": "^5.1.1",
"recompose": "^0.26.0",
"reduce-reducers": "^0.4.3",
"redux": "4.0.0",

View file

@ -23640,6 +23640,13 @@ react-vis@^1.8.1:
prop-types "^15.5.8"
react-motion "^0.4.8"
react-visibility-sensor@^5.1.1:
version "5.1.1"
resolved "https://registry.yarnpkg.com/react-visibility-sensor/-/react-visibility-sensor-5.1.1.tgz#5238380960d3a0b2be0b7faddff38541e337f5a9"
integrity sha512-cTUHqIK+zDYpeK19rzW6zF9YfT4486TIgizZW53wEZ+/GPBbK7cNS0EHyJVyHYacwFEvvHLEKfgJndbemWhB/w==
dependencies:
prop-types "^15.7.2"
react@^16.2.0, react@^16.6.0, react@^16.8.0:
version "16.8.2"
resolved "https://registry.yarnpkg.com/react/-/react-16.8.2.tgz#83064596feaa98d9c2857c4deae1848b542c9c0c"