Skip to content

Commit

Permalink
[Security Solution] Refactor Timeline Notes to use EuiCommentList (#8…
Browse files Browse the repository at this point in the history
…5256) (#85716)

* [Security Solution] Refactor Timeline Notes to use EuiCommentList

* notes

* fix types

* unit tests

* selector

* uncomment Pinned tab

* note event details

* cleanup

* cleanup

* transparent background

* don't display elastic as an owner when note is created

* review + bugs fixed found

Co-authored-by: Xavier Mouligneau <189600+XavierM@users.noreply.github.com>

Co-authored-by: Patryk Kopyciński <patryk.kopycinski@elastic.co>
  • Loading branch information
XavierM and patrykkopycinski authored Dec 13, 2020
1 parent 6e8ca9c commit a2771a3
Show file tree
Hide file tree
Showing 71 changed files with 648 additions and 1,929 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ const AddToCaseActionComponent: React.FC<AddToCaseActionProps> = ({ ecsRowData,

return (
<>
<ActionIconItem id="attachAlertToCase">
<ActionIconItem>
<EuiPopover
id="attachAlertToCasePanel"
button={button}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { getOr, get, isNumber } from 'lodash/fp';
import deepmerge from 'deepmerge';
import uuid from 'uuid';
import styled from 'styled-components';
import deepEqual from 'fast-deep-equal';

import { escapeDataProviderId } from '../drag_and_drop/helpers';
import { useTimeZone } from '../../lib/kibana';
Expand Down Expand Up @@ -193,4 +194,11 @@ export const BarChartComponent: React.FC<BarChartComponentProps> = ({
);
};

export const BarChart = React.memo(BarChartComponent);
export const BarChart = React.memo(
BarChartComponent,
(prevProps, nextProps) =>
prevProps.stackByField === nextProps.stackByField &&
prevProps.timelineId === nextProps.timelineId &&
deepEqual(prevProps.configs, nextProps.configs) &&
deepEqual(prevProps.barChart, nextProps.barChart)
);

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 @@ -98,7 +98,7 @@ export const EventFieldsBrowser = React.memo<Props>(
}
const linkFieldData = (data ?? []).find((d) => d.field === linkField);
const linkFieldValue = getOr(null, 'originalValue', linkFieldData);
return linkFieldValue;
return Array.isArray(linkFieldValue) ? linkFieldValue[0] : linkFieldValue;
},
[data, columnHeaders]
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,10 @@ interface Props {
data: TimelineEventsDetailsItem[];
}

const StyledEuiCodeEditor = styled(EuiCodeEditor)`
flex: 1;
const EuiCodeEditorContainer = styled.div`
.euiCodeEditorWrapper {
position: absolute;
}
`;

const EDITOR_SET_OPTIONS = { fontSize: '12px' };
Expand All @@ -34,19 +36,29 @@ export const JsonView = React.memo<Props>(({ data }) => {
);

return (
<StyledEuiCodeEditor
data-test-subj="jsonView"
isReadOnly
mode="javascript"
setOptions={EDITOR_SET_OPTIONS}
value={value}
width="100%"
height="100%"
/>
<EuiCodeEditorContainer>
<EuiCodeEditor
data-test-subj="jsonView"
isReadOnly
mode="javascript"
setOptions={EDITOR_SET_OPTIONS}
value={value}
width="100%"
height="100%"
/>
</EuiCodeEditorContainer>
);
});

JsonView.displayName = 'JsonView';

export const buildJsonView = (data: TimelineEventsDetailsItem[]) =>
data.reduce((accumulator, item) => set(item.field, item.originalValue, accumulator), {});
data.reduce(
(accumulator, item) =>
set(
item.field,
Array.isArray(item.originalValue) ? item.originalValue.join() : item.originalValue,
accumulator
),
{}
);
Original file line number Diff line number Diff line change
Expand Up @@ -167,15 +167,12 @@ export const SummaryViewComponent: React.FC<{
eventId: string;
timelineId: string;
}> = ({ data, eventId, timelineId, browserFields }) => {
const ruleId = useMemo(
() =>
getOr(
null,
'originalValue',
data.find((d) => d.field === 'signal.rule.id')
),
[data]
);
const ruleId = useMemo(() => {
const item = data.find((d) => d.field === 'signal.rule.id');
return Array.isArray(item?.originalValue)
? item?.originalValue[0]
: item?.originalValue ?? null;
}, [data]);
const { rule: maybeRule } = useRuleAsync(ruleId);
const summaryList = useMemo(() => getSummary({ browserFields, data, eventId, timelineId }), [
browserFields,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ const EventDetailsFlyoutComponent: React.FC<EventDetailsFlyoutProps> = ({
const dispatch = useDispatch();
const getTimeline = useMemo(() => timelineSelectors.getTimelineByIdSelector(), []);
const expandedEvent = useDeepEqualSelector(
(state) => (getTimeline(state, timelineId) ?? timelineDefaults).expandedEvent
(state) => (getTimeline(state, timelineId) ?? timelineDefaults)?.expandedEvent ?? {}
);

const handleClearSelection = useCallback(() => {
Expand All @@ -48,8 +48,8 @@ const EventDetailsFlyoutComponent: React.FC<EventDetailsFlyoutProps> = ({

const [loading, detailsData] = useTimelineEventsDetails({
docValueFields,
indexName: expandedEvent.indexName!,
eventId: expandedEvent.eventId!,
indexName: expandedEvent?.indexName ?? '',
eventId: expandedEvent?.eventId ?? '',
skip: !expandedEvent.eventId,
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
*/

import { EuiFlexGroup, EuiFlexItem, EuiPanel } from '@elastic/eui';
import { isEmpty, some } from 'lodash/fp';
import React, { useEffect, useMemo, useState } from 'react';
import { isEmpty } from 'lodash/fp';
import React, { useEffect, useMemo, useState, useRef } from 'react';
import styled from 'styled-components';
import deepEqual from 'fast-deep-equal';
import { useDispatch } from 'react-redux';
Expand Down Expand Up @@ -180,6 +180,9 @@ const EventsViewerComponent: React.FC<Props> = ({
[justTitle]
);

const prevCombinedQueries = useRef<{
filterQuery: string;
} | null>(null);
const combinedQueries = combineQueries({
config: esQuery.getEsQueryConfig(kibana.services.uiSettings),
dataProviders,
Expand Down Expand Up @@ -232,10 +235,11 @@ const EventsViewerComponent: React.FC<Props> = ({
});

useEffect(() => {
if (!events || (expandedEvent.eventId && !some(['_id', expandedEvent.eventId], events))) {
if (!deepEqual(prevCombinedQueries.current, combinedQueries)) {
prevCombinedQueries.current = combinedQueries;
dispatch(timelineActions.toggleExpandedEvent({ timelineId: id }));
}
}, [dispatch, events, expandedEvent, id]);
}, [combinedQueries, dispatch, id]);

const totalCountMinusDeleted = useMemo(
() => (totalCount > 0 ? totalCount - deletedEventIds.length : 0),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -327,6 +327,20 @@ const ReputationLinkComponent: React.FC<{
[ipReputationLinksSetting, domain, defaultNameMapping, allItemsLimit]
);

const renderCallback = useCallback(
(rowItem) =>
isReputationLink(rowItem) && (
<ExternalLink
url={rowItem.url_template}
overflowIndexStart={overflowIndexStart}
allItemsLimit={allItemsLimit}
>
<>{rowItem.name ?? domain}</>
</ExternalLink>
),
[allItemsLimit, domain, overflowIndexStart]
);

return ipReputationLinks?.length > 0 ? (
<section>
<EuiFlexGroup
Expand Down Expand Up @@ -357,19 +371,7 @@ const ReputationLinkComponent: React.FC<{
<DefaultFieldRendererOverflow
rowItems={ipReputationLinks}
idPrefix="moreReputationLink"
render={(rowItem) => {
return (
isReputationLink(rowItem) && (
<ExternalLink
url={rowItem.url_template}
overflowIndexStart={overflowIndexStart}
allItemsLimit={allItemsLimit}
>
<>{rowItem.name ?? domain}</>
</ExternalLink>
)
);
}}
render={renderCallback}
moreMaxHeight={DEFAULT_MORE_MAX_HEIGHT}
overflowIndexStart={overflowIndexStart}
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,20 +58,17 @@ export interface Props extends Pick<GlobalTimeArgs, 'from' | 'to' | 'deleteQuery
value?: string[] | string | null;
}

const NO_FILTERS: Filter[] = [];
const DEFAULT_QUERY: Query = { query: '', language: 'kuery' };

const TopNComponent: React.FC<Props> = ({
combinedQueries,
defaultView,
deleteQuery,
filters = NO_FILTERS,
filters,
field,
from,
indexPattern,
indexNames,
options,
query = DEFAULT_QUERY,
query,
setAbsoluteRangeDatePickerTarget,
setQuery,
timelineId,
Expand Down Expand Up @@ -132,7 +129,6 @@ const TopNComponent: React.FC<Props> = ({
filters={filters}
from={from}
headerChildren={headerChildren}
indexPattern={indexPattern}
onlyField={field}
query={query}
setAbsoluteRangeDatePickerTarget={setAbsoluteRangeDatePickerTarget}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/*
* 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, { useState } from 'react';
import { createPortalNode, OutPortal } from 'react-reverse-portal';

/**
* A singleton portal for rendering content in the global header
*/
const timelineEventsCountPortalNodeSingleton = createPortalNode();

export const useTimelineEventsCountPortal = () => {
const [timelineEventsCountPortalNode] = useState(timelineEventsCountPortalNodeSingleton);

return { timelineEventsCountPortalNode };
};

export const TimelineEventsCountBadge = React.memo(() => {
const { timelineEventsCountPortalNode } = useTimelineEventsCountPortal();

return <OutPortal node={timelineEventsCountPortalNode} />;
});

TimelineEventsCountBadge.displayName = 'TimelineEventsCountBadge';
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
export interface Note {
/** When the note was created */
created: Date;
eventId?: string | null;
/** Uniquely identifies the note */
id: string;
/** When not `null`, this represents the last edit */
Expand All @@ -18,5 +19,6 @@ export interface Note {
user: string;
/** SaveObjectID for note */
saveObjectId: string | null | undefined;
timelineId?: string | null;
version: string | null | undefined;
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,13 @@
* you may not use this file except in compliance with the Elastic License.
*/

import { keys } from 'lodash/fp';
import { keys, values } from 'lodash/fp';
import memoizeOne from 'memoize-one';
import { createSelector } from 'reselect';
import { Note } from '../../lib/note';
import { ErrorModel, NotesById } from './model';
import { State } from '../types';
import { TimelineResultNote } from '../../../timelines/components/open_timeline/types';

const selectNotesById = (state: State): NotesById => state.app.notesById;

Expand All @@ -25,6 +26,16 @@ export const getNotes = memoizeOne((notesById: NotesById, noteIds: string[]): No
}, [])
);

export const getNotesAsCommentsList = (notesById: NotesById): TimelineResultNote[] =>
values(notesById).map((note) => ({
eventId: note.eventId,
savedObjectId: note.saveObjectId,
note: note.note,
noteId: note.id,
updated: (note.lastEdit ?? note.created).getTime(),
updatedBy: note.user,
}));

export const selectNotesByIdSelector = createSelector(
selectNotesById,
(notesById: NotesById) => notesById
Expand All @@ -33,4 +44,7 @@ export const selectNotesByIdSelector = createSelector(
export const notesByIdsSelector = () =>
createSelector(selectNotesById, (notesById: NotesById) => notesById);

export const selectNotesAsCommentsListSelector = () =>
createSelector(selectNotesById, getNotesAsCommentsList);

export const errorsSelector = () => createSelector(getErrors, (errors) => errors);
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

import { createSelector } from 'reselect';

import { Filter, Query } from '../../../../../../../src/plugins/data/public';
import { State } from '../types';

import { InputsModel, InputsRange, GlobalQuery } from './model';
Expand Down Expand Up @@ -64,21 +65,18 @@ export const timelineQueryByIdSelector = () =>

export const globalSelector = () => createSelector(selectGlobal, (global) => global);

const DEFAULT_QUERY: Query = { query: '', language: 'kuery' };

export const globalQuerySelector = () =>
createSelector(
selectGlobal,
(global) =>
global.query || {
query: '',
language: 'kuery',
}
);
createSelector(selectGlobal, (global) => global.query || DEFAULT_QUERY);

export const globalSavedQuerySelector = () =>
createSelector(selectGlobal, (global) => global.savedQuery || null);

const NO_FILTERS: Filter[] = [];

export const globalFiltersQuerySelector = () =>
createSelector(selectGlobal, (global) => global.filters || []);
createSelector(selectGlobal, (global) => global.filters || NO_FILTERS);

export const getTimelineSelector = () => createSelector(selectTimeline, (timeline) => timeline);

Expand Down
Loading

0 comments on commit a2771a3

Please sign in to comment.