Skip to content

Commit

Permalink
[Security Solution] Refactor Timeline flyout to take a full page (ela…
Browse files Browse the repository at this point in the history
  • Loading branch information
patrykkopycinski committed Nov 22, 2020
1 parent e1a07c4 commit 5ee6233
Show file tree
Hide file tree
Showing 49 changed files with 618 additions and 832 deletions.
11 changes: 11 additions & 0 deletions x-pack/plugins/security_solution/common/types/timeline/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -401,3 +401,14 @@ export const importTimelineResultSchema = runtimeTypes.exact(
export type ImportTimelineResultSchema = runtimeTypes.TypeOf<typeof importTimelineResultSchema>;

export type TimelineEventsType = 'all' | 'raw' | 'alert' | 'signal' | 'custom';

export interface TimelineExpandedEventType {
eventId: string;
indexName: string;
loading: boolean;
}

// eslint-disable-next-line @typescript-eslint/no-explicit-any
type EmptyObject = Record<any, never>;

export type TimelineExpandedEvent = TimelineExpandedEventType | EmptyObject;
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,9 @@ export const openTimelineUsingToggle = () => {
cy.get(TIMELINE_TOGGLE_BUTTON).click();
};

export const openTimelineIfClosed = () => {
export const openTimelineIfClosed = () =>
cy.get(MAIN_PAGE).then(($page) => {
if ($page.find(TIMELINE_TOGGLE_BUTTON).length === 1) {
openTimelineUsingToggle();
}
});
};

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ import {
EuiFlexItem,
EuiIcon,
EuiPanel,
EuiText,
EuiToolTip,
EuiIconTip,
} from '@elastic/eui';
import React from 'react';
import { Draggable } from 'react-beautiful-dnd';
Expand All @@ -27,7 +27,6 @@ import { DroppableWrapper } from '../drag_and_drop/droppable_wrapper';
import { getDroppableId, getDraggableFieldId, DRAG_TYPE_FIELD } from '../drag_and_drop/helpers';
import { DraggableFieldBadge } from '../draggables/field_badge';
import { FieldName } from '../../../timelines/components/fields_browser/field_name';
import { SelectableText } from '../selectable_text';
import { OverflowField } from '../tables/helpers';
import { defaultColumnHeaderType } from '../../../timelines/components/timeline/body/column_headers/default_headers';
import { DEFAULT_COLUMN_MIN_WIDTH } from '../../../timelines/components/timeline/body/constants';
Expand Down Expand Up @@ -90,6 +89,21 @@ export const getColumns = ({
</EuiToolTip>
),
},
{
field: 'description',
name: '',
render: (description: string | null | undefined, data: EventFieldsData) => (
<EuiIconTip
aria-label={i18n.DESCRIPTION}
type="iInCircle"
color="subdued"
content={`${description || ''} ${getExampleText(data.example)}`}
/>
),
sortable: true,
truncateText: true,
width: '30px',
},
{
field: 'field',
name: i18n.FIELD,
Expand Down Expand Up @@ -187,18 +201,6 @@ export const getColumns = ({
</EuiFlexGroup>
),
},
{
field: 'description',
name: i18n.DESCRIPTION,
render: (description: string | null | undefined, data: EventFieldsData) => (
<SelectableText>
<EuiText size="xs">{`${description || ''} ${getExampleText(data.example)}`}</EuiText>
</SelectableText>
),
sortable: true,
truncateText: true,
width: '50%',
},
{
field: 'valuesConcatenated',
name: i18n.BLANK,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,12 @@ import { useMountAppended } from '../../utils/use_mount_appended';
jest.mock('../link_to');
describe('EventDetails', () => {
const mount = useMountAppended();
const onEventToggled = jest.fn();
const defaultProps = {
browserFields: mockBrowserFields,
columnHeaders: defaultHeaders,
data: mockDetailItemData,
id: mockDetailItemDataId,
view: 'table-view' as View,
onEventToggled,
onUpdateColumns: jest.fn(),
onViewSelected: jest.fn(),
timelineId: 'test',
Expand Down Expand Up @@ -66,12 +64,5 @@ describe('EventDetails', () => {
wrapper.find('[data-test-subj="eventDetails"]').find('.euiTab-isSelected').first().text()
).toEqual('Table');
});

test('it invokes `onEventToggled` when the collapse button is clicked', () => {
wrapper.find('[data-test-subj="collapse"]').first().simulate('click');
wrapper.update();

expect(onEventToggled).toHaveBeenCalled();
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
*/

import { EuiLink, EuiTabbedContent, EuiTabbedContentTab } from '@elastic/eui';
import React, { useMemo } from 'react';
import React, { useCallback, useMemo } from 'react';
import styled from 'styled-components';

import { BrowserFields } from '../../containers/source';
Expand All @@ -15,9 +15,12 @@ import { OnUpdateColumns } from '../../../timelines/components/timeline/events';
import { EventFieldsBrowser } from './event_fields_browser';
import { JsonView } from './json_view';
import * as i18n from './translations';
import { COLLAPSE, COLLAPSE_EVENT } from '../../../timelines/components/timeline/body/translations';

export type View = 'table-view' | 'json-view';
export type View = EventsViewType.tableView | EventsViewType.jsonView;
export enum EventsViewType {
tableView = 'table-view',
jsonView = 'json-view',
}

const CollapseLink = styled(EuiLink)`
margin: 20px 0;
Expand All @@ -30,10 +33,9 @@ interface Props {
columnHeaders: ColumnHeaderOptions[];
data: TimelineEventsDetailsItem[];
id: string;
view: View;
onEventToggled: () => void;
view: EventsViewType;
onUpdateColumns: OnUpdateColumns;
onViewSelected: (selected: View) => void;
onViewSelected: (selected: EventsViewType) => void;
timelineId: string;
toggleColumn: (column: ColumnHeaderOptions) => void;
}
Expand All @@ -51,16 +53,19 @@ export const EventDetails = React.memo<Props>(
data,
id,
view,
onEventToggled,
onUpdateColumns,
onViewSelected,
timelineId,
toggleColumn,
}) => {
const handleTabClick = useCallback((e) => onViewSelected(e.id as EventsViewType), [
onViewSelected,
]);

const tabs: EuiTabbedContentTab[] = useMemo(
() => [
{
id: 'table-view',
id: EventsViewType.tableView,
name: i18n.TABLE,
content: (
<EventFieldsBrowser
Expand All @@ -75,7 +80,7 @@ export const EventDetails = React.memo<Props>(
),
},
{
id: 'json-view',
id: EventsViewType.jsonView,
name: i18n.JSON_VIEW,
content: <JsonView data={data} />,
},
Expand All @@ -88,11 +93,8 @@ export const EventDetails = React.memo<Props>(
<EuiTabbedContent
tabs={tabs}
selectedTab={view === 'table-view' ? tabs[0] : tabs[1]}
onTabClick={(e) => onViewSelected(e.id as View)}
onTabClick={handleTabClick}
/>
<CollapseLink aria-label={COLLAPSE} data-test-subj="collapse" onClick={onEventToggled}>
{COLLAPSE_EVENT}
</CollapseLink>
</Details>
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ describe('EventFieldsBrowser', () => {
const mount = useMountAppended();

describe('column headers', () => {
['Field', 'Value', 'Description'].forEach((header) => {
['Field', 'Value'].forEach((header) => {
test(`it renders the ${header} column header`, () => {
const wrapper = mount(
<TestProviders>
Expand Down Expand Up @@ -229,8 +229,15 @@ describe('EventFieldsBrowser', () => {
</TestProviders>
);

expect(wrapper.find('.euiTableRow').find('.euiTableRowCell').at(3).text()).toContain(
'DescriptionDate/time when the event originated. For log events this is the date/time when the event was generated, and not when it was read. Required field for all events. Example: 2016-05-23T08:05:34.853Z'
expect(
wrapper
.find('.euiTableRow')
.find('.euiTableRowCell')
.at(1)
.find('EuiIconTip')
.prop('content')
).toContain(
'Date/time when the event originated. For log events this is the date/time when the event was generated, and not when it was read. Required field for all events. Example: 2016-05-23T08:05:34.853Z'
);
});
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,49 +4,38 @@
* you may not use this file except in compliance with the Elastic License.
*/

import React, { useCallback, useState } from 'react';
import React, { useState } from 'react';

import { BrowserFields } from '../../containers/source';
import { TimelineEventsDetailsItem } from '../../../../common/search_strategy/timeline';
import { ColumnHeaderOptions } from '../../../timelines/store/timeline/model';
import { OnUpdateColumns } from '../../../timelines/components/timeline/events';

import { EventDetails, View } from './event_details';
import { EventDetails, EventsViewType, View } from './event_details';

interface Props {
browserFields: BrowserFields;
columnHeaders: ColumnHeaderOptions[];
data: TimelineEventsDetailsItem[];
id: string;
onEventToggled: () => void;
onUpdateColumns: OnUpdateColumns;
timelineId: string;
toggleColumn: (column: ColumnHeaderOptions) => void;
}

export const StatefulEventDetails = React.memo<Props>(
({
browserFields,
columnHeaders,
data,
id,
onEventToggled,
onUpdateColumns,
timelineId,
toggleColumn,
}) => {
const [view, setView] = useState<View>('table-view');
({ browserFields, columnHeaders, data, id, onUpdateColumns, timelineId, toggleColumn }) => {
// TODO: Move to the store
const [view, setView] = useState<View>(EventsViewType.tableView);

const handleSetView = useCallback((newView) => setView(newView), []);
return (
<EventDetails
browserFields={browserFields}
columnHeaders={columnHeaders}
data={data}
id={id}
onEventToggled={onEventToggled}
onUpdateColumns={onUpdateColumns}
onViewSelected={handleSetView}
onViewSelected={setView}
timelineId={timelineId}
toggleColumn={toggleColumn}
view={view}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
/*
* 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 { EuiFlyout, EuiFlyoutBody, EuiFlyoutHeader } from '@elastic/eui';
import React, { useCallback } from 'react';
import styled from 'styled-components';
import deepEqual from 'fast-deep-equal';
import { useDispatch } from 'react-redux';

import { ColumnHeaderOptions } from '../../../timelines/store/timeline/model';
import { timelineActions } from '../../../timelines/store/timeline';
import { BrowserFields, DocValueFields } from '../../containers/source';
import {
ExpandableEvent,
ExpandableEventTitle,
} from '../../../timelines/components/timeline/expandable_event';
import { useDeepEqualSelector } from '../../hooks/use_selector';

const StyledEuiFlyout = styled(EuiFlyout)`
z-index: 9999;
`;

interface EventDetailsFlyoutProps {
browserFields: BrowserFields;
docValueFields: DocValueFields[];
timelineId: string;
toggleColumn: (column: ColumnHeaderOptions) => void;
}

const EventDetailsFlyoutComponent: React.FC<EventDetailsFlyoutProps> = ({
browserFields,
docValueFields,
timelineId,
toggleColumn,
}) => {
const dispatch = useDispatch();
const expandedEvent = useDeepEqualSelector(
(state) => state.timeline.timelineById[timelineId]?.expandedEvent ?? {}
);

const handleClearSelection = useCallback(() => {
dispatch(
timelineActions.toggleExpandedEvent({
timelineId,
event: {},
})
);
}, [dispatch, timelineId]);

if (!expandedEvent.eventId) {
return null;
}

return (
<StyledEuiFlyout size="s" onClose={handleClearSelection}>
<EuiFlyoutHeader hasBorder>
<ExpandableEventTitle />
</EuiFlyoutHeader>
<EuiFlyoutBody>
<ExpandableEvent
browserFields={browserFields}
docValueFields={docValueFields}
event={expandedEvent}
timelineId={timelineId}
toggleColumn={toggleColumn}
/>
</EuiFlyoutBody>
</StyledEuiFlyout>
);
};

export const EventDetailsFlyout = React.memo(
EventDetailsFlyoutComponent,
(prevProps, nextProps) =>
deepEqual(prevProps.browserFields, nextProps.browserFields) &&
deepEqual(prevProps.docValueFields, nextProps.docValueFields) &&
prevProps.timelineId === nextProps.timelineId &&
prevProps.toggleColumn === nextProps.toggleColumn
);
Loading

0 comments on commit 5ee6233

Please sign in to comment.