From b6768fe440eac047ed039c5b8caa774b02a8055c Mon Sep 17 00:00:00 2001 From: Patryk Kopycinski Date: Sun, 21 Jun 2020 10:35:06 +0200 Subject: [PATCH 01/19] WIP --- .../common/types/timeline/index.ts | 21 + .../public/common/mock/global_state.ts | 1 + .../public/common/mock/timeline_results.ts | 2 + .../public/graphql/introspection.json | 85 ++++ .../security_solution/public/graphql/types.ts | 22 + .../components/fields_browser/index.test.tsx | 29 -- .../components/fields_browser/index.tsx | 335 ++++++++-------- .../components/open_timeline/types.ts | 3 +- .../categories_pane.test.tsx | 61 +++ .../row_renderers_browser/categories_pane.tsx | 97 +++++ .../row_renderers_browser/category.test.tsx | 106 +++++ .../row_renderers_browser/category.tsx | 61 +++ .../category_columns.test.tsx | 137 +++++++ .../category_columns.tsx | 144 +++++++ .../category_title.test.tsx | 62 +++ .../row_renderers_browser/category_title.tsx | 59 +++ .../field_browser.test.tsx | 250 ++++++++++++ .../field_items.test.tsx | 281 +++++++++++++ .../row_renderers_browser/field_items.tsx | 193 +++++++++ .../row_renderers_browser/field_name.test.tsx | 66 +++ .../row_renderers_browser/field_name.tsx | 115 ++++++ .../fields_pane.test.tsx | 120 ++++++ .../row_renderers_browser/fields_pane.tsx | 106 +++++ .../row_renderers_browser/header.test.tsx | 258 ++++++++++++ .../row_renderers_browser/header.tsx | 123 ++++++ .../row_renderers_browser/helpers.test.tsx | 376 ++++++++++++++++++ .../row_renderers_browser/helpers.tsx | 143 +++++++ .../row_renderers_browser/index.test.tsx | 243 +++++++++++ .../row_renderers_browser/index.tsx | 208 ++++++++++ .../row_renderers_browser.tsx | 303 ++++++++++++++ .../row_renderers_browser/translations.ts | 88 ++++ .../components/row_renderers_browser/types.ts | 37 ++ .../body/column_headers/index.test.tsx | 10 - .../timeline/body/column_headers/index.tsx | 29 +- .../components/timeline/body/index.tsx | 31 +- .../renderers/auditd/generic_row_renderer.tsx | 2 + .../netflow/netflow_row_renderer.tsx | 1 + .../body/renderers/plain_row_renderer.tsx | 9 +- .../timeline/body/renderers/row_renderer.tsx | 2 + .../suricata/suricata_row_renderer.tsx | 1 + .../renderers/system/generic_row_renderer.tsx | 7 + .../body/renderers/zeek/zeek_row_renderer.tsx | 1 + .../timeline/body/stateful_body.tsx | 56 ++- .../containers/all/index.gql_query.ts | 1 + .../public/timelines/containers/all/index.tsx | 1 + .../containers/one/index.gql_query.ts | 1 + .../timelines/store/timeline/actions.ts | 7 +- .../timelines/store/timeline/defaults.ts | 1 + .../timelines/store/timeline/epic.test.ts | 2 + .../timelines/store/timeline/helpers.ts | 25 +- .../public/timelines/store/timeline/model.ts | 4 + .../timelines/store/timeline/reducer.test.ts | 8 + .../timelines/store/timeline/reducer.ts | 10 + .../server/graphql/timeline/schema.gql.ts | 17 + .../security_solution/server/graphql/types.ts | 29 ++ .../lib/timeline/pick_saved_timeline.ts | 2 +- .../lib/timeline/saved_object_mappings.ts | 3 + 57 files changed, 4121 insertions(+), 274 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/categories_pane.test.tsx create mode 100644 x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/categories_pane.tsx create mode 100644 x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/category.test.tsx create mode 100644 x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/category.tsx create mode 100644 x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/category_columns.test.tsx create mode 100644 x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/category_columns.tsx create mode 100644 x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/category_title.test.tsx create mode 100644 x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/category_title.tsx create mode 100644 x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/field_browser.test.tsx create mode 100644 x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/field_items.test.tsx create mode 100644 x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/field_items.tsx create mode 100644 x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/field_name.test.tsx create mode 100644 x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/field_name.tsx create mode 100644 x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/fields_pane.test.tsx create mode 100644 x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/fields_pane.tsx create mode 100644 x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/header.test.tsx create mode 100644 x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/header.tsx create mode 100644 x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/helpers.test.tsx create mode 100644 x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/helpers.tsx create mode 100644 x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/index.test.tsx create mode 100644 x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/index.tsx create mode 100644 x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/row_renderers_browser.tsx create mode 100644 x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/translations.ts create mode 100644 x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/types.ts diff --git a/x-pack/plugins/security_solution/common/types/timeline/index.ts b/x-pack/plugins/security_solution/common/types/timeline/index.ts index 4f255bb6d6834..a13417384c2d2 100644 --- a/x-pack/plugins/security_solution/common/types/timeline/index.ts +++ b/x-pack/plugins/security_solution/common/types/timeline/index.ts @@ -151,6 +151,25 @@ export type TimelineStatusLiteralWithNull = runtimeTypes.TypeOf< typeof TimelineStatusLiteralWithNullRt >; +export const RowRendererIdRuntimeType = runtimeTypes.keyof({ + auditd: null, + auditd_file: null, + netflow: null, + plain: null, + suricata: null, + system: null, + system_dns: null, + system_endgame_process: null, + system_file: null, + system_fin: null, + system_security_event: null, + system_socket: null, + zeek: null, + all: null, +}); + +export type RowRendererId = runtimeTypes.TypeOf; + /* * Timeline Types */ @@ -175,6 +194,7 @@ export const SavedTimelineRuntimeType = runtimeTypes.partial({ dataProviders: unionWithNullType(runtimeTypes.array(SavedDataProviderRuntimeType)), description: unionWithNullType(runtimeTypes.string), eventType: unionWithNullType(runtimeTypes.string), + excludedRowRendererIds: unionWithNullType(RowRendererIdRuntimeType), favorite: unionWithNullType(runtimeTypes.array(SavedFavoriteRuntimeType)), filters: unionWithNullType(runtimeTypes.array(SavedFilterRuntimeType)), kqlMode: unionWithNullType(runtimeTypes.string), @@ -246,6 +266,7 @@ export const TimelineSavedToReturnObjectRuntimeType = runtimeTypes.intersection( }), runtimeTypes.partial({ eventIdToNoteIds: runtimeTypes.array(NoteSavedObjectToReturnRuntimeType), + excludedRowRendererIds: runtimeTypes.array(RowRendererIdRuntimeType), noteIds: runtimeTypes.array(runtimeTypes.string), notes: runtimeTypes.array(NoteSavedObjectToReturnRuntimeType), pinnedEventIds: runtimeTypes.array(runtimeTypes.string), diff --git a/x-pack/plugins/security_solution/public/common/mock/global_state.ts b/x-pack/plugins/security_solution/public/common/mock/global_state.ts index 3e84e4035e15e..52da54b520d43 100644 --- a/x-pack/plugins/security_solution/public/common/mock/global_state.ts +++ b/x-pack/plugins/security_solution/public/common/mock/global_state.ts @@ -197,6 +197,7 @@ export const mockGlobalState: State = { dataProviders: [], description: '', eventIdToNoteIds: {}, + excludedRowRendererIds: [], highlightedDropAndProviderId: '', historyIds: [], isFavorite: false, diff --git a/x-pack/plugins/security_solution/public/common/mock/timeline_results.ts b/x-pack/plugins/security_solution/public/common/mock/timeline_results.ts index 4eb66acdfad65..9c4bc2d70e725 100644 --- a/x-pack/plugins/security_solution/public/common/mock/timeline_results.ts +++ b/x-pack/plugins/security_solution/public/common/mock/timeline_results.ts @@ -2098,6 +2098,7 @@ export const mockTimelineModel: TimelineModel = { description: 'This is a sample rule description', eventIdToNoteIds: {}, eventType: 'all', + excludedRowRendererIds: [], filters: [ { $state: { @@ -2217,6 +2218,7 @@ export const defaultTimelineProps: CreateTimelineProps = { description: '', eventIdToNoteIds: {}, eventType: 'all', + excludedRowRendererIds: [], filters: [], highlightedDropAndProviderId: '', historyIds: [], diff --git a/x-pack/plugins/security_solution/public/graphql/introspection.json b/x-pack/plugins/security_solution/public/graphql/introspection.json index 3c8c7c21d72a0..399745d7dc385 100644 --- a/x-pack/plugins/security_solution/public/graphql/introspection.json +++ b/x-pack/plugins/security_solution/public/graphql/introspection.json @@ -9586,6 +9586,22 @@ "isDeprecated": false, "deprecationReason": null }, + { + "name": "excludedRowRendererIds", + "description": "", + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "ENUM", "name": "RowRendererId", "ofType": null } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "favorite", "description": "", @@ -10060,6 +10076,75 @@ "enumValues": null, "possibleTypes": null }, + { + "kind": "ENUM", + "name": "RowRendererId", + "description": "", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": [ + { "name": "all", "description": "", "isDeprecated": false, "deprecationReason": null }, + { "name": "auditd", "description": "", "isDeprecated": false, "deprecationReason": null }, + { + "name": "auditd_file", + "description": "", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "netflow", + "description": "", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "suricata", + "description": "", + "isDeprecated": false, + "deprecationReason": null + }, + { "name": "system", "description": "", "isDeprecated": false, "deprecationReason": null }, + { + "name": "system_dns", + "description": "", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "system_endgame_process", + "description": "", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "system_file", + "description": "", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "system_fin", + "description": "", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "system_security_event", + "description": "", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "system_socket", + "description": "", + "isDeprecated": false, + "deprecationReason": null + }, + { "name": "zeek", "description": "", "isDeprecated": false, "deprecationReason": null } + ], + "possibleTypes": null + }, { "kind": "OBJECT", "name": "FavoriteTimelineResult", diff --git a/x-pack/plugins/security_solution/public/graphql/types.ts b/x-pack/plugins/security_solution/public/graphql/types.ts index dc4a8ae78bf46..d566a46037261 100644 --- a/x-pack/plugins/security_solution/public/graphql/types.ts +++ b/x-pack/plugins/security_solution/public/graphql/types.ts @@ -342,6 +342,22 @@ export enum TlsFields { _id = '_id', } +export enum RowRendererId { + all = 'all', + auditd = 'auditd', + auditd_file = 'auditd_file', + netflow = 'netflow', + suricata = 'suricata', + system = 'system', + system_dns = 'system_dns', + system_endgame_process = 'system_endgame_process', + system_file = 'system_file', + system_fin = 'system_fin', + system_security_event = 'system_security_event', + system_socket = 'system_socket', + zeek = 'zeek', +} + export enum TimelineStatus { active = 'active', draft = 'draft', @@ -1938,6 +1954,8 @@ export interface TimelineResult { eventType?: Maybe; + excludedRowRendererIds?: Maybe; + favorite?: Maybe; filters?: Maybe; @@ -4334,6 +4352,8 @@ export namespace GetAllTimeline { eventIdToNoteIds: Maybe; + excludedRowRendererIds: Maybe; + notes: Maybe; noteIds: Maybe; @@ -5391,6 +5411,8 @@ export namespace GetOneTimeline { eventIdToNoteIds: Maybe; + excludedRowRendererIds: Maybe; + favorite: Maybe; filters: Maybe; diff --git a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/index.test.tsx index 24dc806838d90..03b670190f263 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/index.test.tsx @@ -6,11 +6,9 @@ import { mount } from 'enzyme'; import React from 'react'; -import { ActionCreator } from 'typescript-fsa'; import { mockBrowserFields } from '../../../common/containers/source/mock'; import { TestProviders } from '../../../common/mock'; -import { ColumnHeaderOptions } from '../../../timelines/store/timeline/model'; import { FIELD_BROWSER_HEIGHT, FIELD_BROWSER_WIDTH } from './helpers'; @@ -29,17 +27,6 @@ afterAll(() => { console.warn = originalWarn; }); -const removeColumnMock = (jest.fn() as unknown) as ActionCreator<{ - id: string; - columnId: string; -}>; - -const upsertColumnMock = (jest.fn() as unknown) as ActionCreator<{ - column: ColumnHeaderOptions; - id: string; - index: number; -}>; - describe('StatefulFieldsBrowser', () => { const timelineId = 'test'; @@ -54,8 +41,6 @@ describe('StatefulFieldsBrowser', () => { timelineId={timelineId} toggleColumn={jest.fn()} width={FIELD_BROWSER_WIDTH} - removeColumn={removeColumnMock} - upsertColumn={upsertColumnMock} /> ); @@ -75,8 +60,6 @@ describe('StatefulFieldsBrowser', () => { timelineId={timelineId} toggleColumn={jest.fn()} width={FIELD_BROWSER_WIDTH} - removeColumn={removeColumnMock} - upsertColumn={upsertColumnMock} /> ); @@ -95,8 +78,6 @@ describe('StatefulFieldsBrowser', () => { timelineId={timelineId} toggleColumn={jest.fn()} width={FIELD_BROWSER_WIDTH} - removeColumn={removeColumnMock} - upsertColumn={upsertColumnMock} /> ); @@ -122,8 +103,6 @@ describe('StatefulFieldsBrowser', () => { timelineId={timelineId} toggleColumn={jest.fn()} width={FIELD_BROWSER_WIDTH} - removeColumn={removeColumnMock} - upsertColumn={upsertColumnMock} /> ); @@ -149,8 +128,6 @@ describe('StatefulFieldsBrowser', () => { timelineId={timelineId} toggleColumn={jest.fn()} width={FIELD_BROWSER_WIDTH} - removeColumn={removeColumnMock} - upsertColumn={upsertColumnMock} /> ); @@ -186,8 +163,6 @@ describe('StatefulFieldsBrowser', () => { timelineId={timelineId} toggleColumn={jest.fn()} width={FIELD_BROWSER_WIDTH} - removeColumn={removeColumnMock} - upsertColumn={upsertColumnMock} /> ); @@ -209,8 +184,6 @@ describe('StatefulFieldsBrowser', () => { timelineId={timelineId} toggleColumn={jest.fn()} width={FIELD_BROWSER_WIDTH} - removeColumn={removeColumnMock} - upsertColumn={upsertColumnMock} /> ); @@ -232,8 +205,6 @@ describe('StatefulFieldsBrowser', () => { timelineId={timelineId} toggleColumn={jest.fn()} width={FIELD_BROWSER_WIDTH} - removeColumn={removeColumnMock} - upsertColumn={upsertColumnMock} /> ); diff --git a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/index.tsx index a3e93ff3c90eb..355ba6d233669 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/index.tsx @@ -4,14 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiButtonEmpty, EuiButtonIcon, EuiToolTip } from '@elastic/eui'; +import { EuiButtonEmpty, EuiToolTip } from '@elastic/eui'; import { noop } from 'lodash/fp'; import React, { useEffect, useRef, useState, useCallback, useMemo } from 'react'; -import { connect, ConnectedProps } from 'react-redux'; import styled from 'styled-components'; import { BrowserFields } from '../../../common/containers/source'; -import { timelineActions } from '../../store/timeline'; import { ColumnHeaderOptions } from '../../../timelines/store/timeline/model'; import { DEFAULT_CATEGORY_NAME } from '../timeline/body/column_headers/default_headers'; import { FieldsBrowser } from './field_browser'; @@ -33,181 +31,160 @@ FieldsBrowserButtonContainer.displayName = 'FieldsBrowserButtonContainer'; /** * Manages the state of the field browser */ -export const StatefulFieldsBrowserComponent = React.memo( - ({ - columnHeaders, - browserFields, - height, - isEventViewer = false, - onFieldSelected, - onUpdateColumns, - timelineId, - toggleColumn, - width, - }) => { - /** tracks the latest timeout id from `setTimeout`*/ - const inputTimeoutId = useRef(0); - - /** all field names shown in the field browser must contain this string (when specified) */ - const [filterInput, setFilterInput] = useState(''); - /** all fields in this collection have field names that match the filterInput */ - const [filteredBrowserFields, setFilteredBrowserFields] = useState(null); - /** when true, show a spinner in the input to indicate the field browser is searching for matching field names */ - const [isSearching, setIsSearching] = useState(false); - /** this category will be displayed in the right-hand pane of the field browser */ - const [selectedCategoryId, setSelectedCategoryId] = useState(DEFAULT_CATEGORY_NAME); - /** show the field browser */ - const [show, setShow] = useState(false); - useEffect(() => { - return () => { - if (inputTimeoutId.current !== 0) { - // ⚠️ mutation: cancel any remaining timers and zero-out the timer id: - clearTimeout(inputTimeoutId.current); - inputTimeoutId.current = 0; - } - }; - }, []); - - /** Shows / hides the field browser */ - const toggleShow = useCallback(() => { - setShow(!show); - }, [show]); - - /** Invoked when the user types in the filter input */ - const updateFilter = useCallback( - (newFilterInput: string) => { - setFilterInput(newFilterInput); - setIsSearching(true); - if (inputTimeoutId.current !== 0) { - clearTimeout(inputTimeoutId.current); // ⚠️ mutation: cancel any previous timers - } - // ⚠️ mutation: schedule a new timer that will apply the filter when it fires: - inputTimeoutId.current = window.setTimeout(() => { - const newFilteredBrowserFields = filterBrowserFieldsByFieldName({ - browserFields: mergeBrowserFieldsWithDefaultCategory(browserFields), - substring: newFilterInput, - }); - setFilteredBrowserFields(newFilteredBrowserFields); - setIsSearching(false); - - const newSelectedCategoryId = - newFilterInput === '' || Object.keys(newFilteredBrowserFields).length === 0 - ? DEFAULT_CATEGORY_NAME - : Object.keys(newFilteredBrowserFields) - .sort() - .reduce( - (selected, category) => - newFilteredBrowserFields[category].fields != null && - newFilteredBrowserFields[selected].fields != null && - Object.keys(newFilteredBrowserFields[category].fields!).length > - Object.keys(newFilteredBrowserFields[selected].fields!).length - ? category - : selected, - Object.keys(newFilteredBrowserFields)[0] - ); - setSelectedCategoryId(newSelectedCategoryId); - }, INPUT_TIMEOUT); - }, - // eslint-disable-next-line react-hooks/exhaustive-deps - [browserFields, filterInput, inputTimeoutId.current] - ); - - /** - * Invoked when the user clicks a category name in the left-hand side of - * the field browser - */ - const updateSelectedCategoryId = useCallback((categoryId: string) => { - setSelectedCategoryId(categoryId); - }, []); - - /** - * Invoked when the user clicks on the context menu to view a category's - * columns in the timeline, this function dispatches the action that - * causes the timeline display those columns. - */ - const updateColumnsAndSelectCategoryId = useCallback((columns: ColumnHeaderOptions[]) => { - onUpdateColumns(columns); // show the category columns in the timeline - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - /** Invoked when the field browser should be hidden */ - const hideFieldBrowser = useCallback(() => { - setFilterInput(''); - setFilterInput(''); - setFilteredBrowserFields(null); - setIsSearching(false); - setSelectedCategoryId(DEFAULT_CATEGORY_NAME); - setShow(false); - }, []); - // only merge in the default category if the field browser is visible - const browserFieldsWithDefaultCategory = useMemo( - () => (show ? mergeBrowserFieldsWithDefaultCategory(browserFields) : {}), - [show, browserFields] - ); - - return ( - <> - - - {isEventViewer ? ( - - ) : ( - - {i18n.FIELDS} - - )} - - - {show && ( - - )} - - - ); - } -); - -const mapDispatchToProps = { - removeColumn: timelineActions.removeColumn, - upsertColumn: timelineActions.upsertColumn, +export const StatefulFieldsBrowserComponent: React.FC = ({ + columnHeaders, + browserFields, + height, + isEventViewer = false, + onFieldSelected, + onUpdateColumns, + timelineId, + toggleColumn, + width, +}) => { + /** tracks the latest timeout id from `setTimeout`*/ + const inputTimeoutId = useRef(0); + + /** all field names shown in the field browser must contain this string (when specified) */ + const [filterInput, setFilterInput] = useState(''); + /** all fields in this collection have field names that match the filterInput */ + const [filteredBrowserFields, setFilteredBrowserFields] = useState(null); + /** when true, show a spinner in the input to indicate the field browser is searching for matching field names */ + const [isSearching, setIsSearching] = useState(false); + /** this category will be displayed in the right-hand pane of the field browser */ + const [selectedCategoryId, setSelectedCategoryId] = useState(DEFAULT_CATEGORY_NAME); + /** show the field browser */ + const [show, setShow] = useState(false); + useEffect(() => { + return () => { + if (inputTimeoutId.current !== 0) { + // ⚠️ mutation: cancel any remaining timers and zero-out the timer id: + clearTimeout(inputTimeoutId.current); + inputTimeoutId.current = 0; + } + }; + }, []); + + /** Shows / hides the field browser */ + const toggleShow = useCallback(() => { + setShow(!show); + }, [show]); + + /** Invoked when the user types in the filter input */ + const updateFilter = useCallback( + (newFilterInput: string) => { + setFilterInput(newFilterInput); + setIsSearching(true); + if (inputTimeoutId.current !== 0) { + clearTimeout(inputTimeoutId.current); // ⚠️ mutation: cancel any previous timers + } + // ⚠️ mutation: schedule a new timer that will apply the filter when it fires: + inputTimeoutId.current = window.setTimeout(() => { + const newFilteredBrowserFields = filterBrowserFieldsByFieldName({ + browserFields: mergeBrowserFieldsWithDefaultCategory(browserFields), + substring: newFilterInput, + }); + setFilteredBrowserFields(newFilteredBrowserFields); + setIsSearching(false); + + const newSelectedCategoryId = + newFilterInput === '' || Object.keys(newFilteredBrowserFields).length === 0 + ? DEFAULT_CATEGORY_NAME + : Object.keys(newFilteredBrowserFields) + .sort() + .reduce( + (selected, category) => + newFilteredBrowserFields[category].fields != null && + newFilteredBrowserFields[selected].fields != null && + Object.keys(newFilteredBrowserFields[category].fields!).length > + Object.keys(newFilteredBrowserFields[selected].fields!).length + ? category + : selected, + Object.keys(newFilteredBrowserFields)[0] + ); + setSelectedCategoryId(newSelectedCategoryId); + }, INPUT_TIMEOUT); + }, + // eslint-disable-next-line react-hooks/exhaustive-deps + [browserFields, filterInput, inputTimeoutId.current] + ); + + /** + * Invoked when the user clicks a category name in the left-hand side of + * the field browser + */ + const updateSelectedCategoryId = useCallback((categoryId: string) => { + setSelectedCategoryId(categoryId); + }, []); + + /** + * Invoked when the user clicks on the context menu to view a category's + * columns in the timeline, this function dispatches the action that + * causes the timeline display those columns. + */ + const updateColumnsAndSelectCategoryId = useCallback((columns: ColumnHeaderOptions[]) => { + onUpdateColumns(columns); // show the category columns in the timeline + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + /** Invoked when the field browser should be hidden */ + const hideFieldBrowser = useCallback(() => { + setFilterInput(''); + setFilterInput(''); + setFilteredBrowserFields(null); + setIsSearching(false); + setSelectedCategoryId(DEFAULT_CATEGORY_NAME); + setShow(false); + }, []); + // only merge in the default category if the field browser is visible + const browserFieldsWithDefaultCategory = useMemo( + () => (show ? mergeBrowserFieldsWithDefaultCategory(browserFields) : {}), + [show, browserFields] + ); + + return ( + <> + + + + {i18n.FIELDS} + + + + {show && ( + + )} + + + ); }; -const connector = connect(null, mapDispatchToProps); - -type PropsFromRedux = ConnectedProps; - -export const StatefulFieldsBrowser = connector(React.memo(StatefulFieldsBrowserComponent)); +export const StatefulFieldsBrowser = React.memo(StatefulFieldsBrowserComponent); diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/types.ts b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/types.ts index e1515a3a79254..fdee59f993ad7 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/types.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/types.ts @@ -8,7 +8,7 @@ import { SetStateAction, Dispatch } from 'react'; import { AllTimelinesVariables } from '../../containers/all'; import { TimelineModel } from '../../store/timeline/model'; import { NoteResult } from '../../../graphql/types'; -import { TimelineTypeLiteral } from '../../../../common/types/timeline'; +import { TimelineTypeLiteral, RowRendererId } from '../../../../common/types/timeline'; /** The users who added a timeline to favorites */ export interface FavoriteTimelineResult { @@ -41,6 +41,7 @@ export interface OpenTimelineResult { created?: number | null; description?: string | null; eventIdToNoteIds?: Readonly> | null; + excludedRowRendererIds?: RowRendererId[] | null; favorite?: FavoriteTimelineResult[] | null; noteIds?: string[] | null; notes?: TimelineResultNote[] | null; diff --git a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/categories_pane.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/categories_pane.test.tsx new file mode 100644 index 0000000000000..42b44ba72438f --- /dev/null +++ b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/categories_pane.test.tsx @@ -0,0 +1,61 @@ +/* + * 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 { mount } from 'enzyme'; +import React from 'react'; +import { ThemeProvider } from 'styled-components'; +import euiDarkVars from '@elastic/eui/dist/eui_theme_dark.json'; + +import { mockBrowserFields } from '../../../common/containers/source/mock'; + +import { CATEGORY_PANE_WIDTH } from './helpers'; +import { CategoriesPane } from './categories_pane'; +import * as i18n from './translations'; + +const timelineId = 'test'; + +describe('CategoriesPane', () => { + const theme = () => ({ eui: euiDarkVars, darkMode: true }); + test('it renders the expected title', () => { + const wrapper = mount( + + + + ); + + expect(wrapper.find('[data-test-subj="categories-pane-title"]').first().text()).toEqual( + i18n.CATEGORIES + ); + }); + + test('it renders a "No fields match" message when filteredBrowserFields is empty', () => { + const wrapper = mount( + + + + ); + + expect(wrapper.find('[data-test-subj="categories-container"] tbody').first().text()).toEqual( + i18n.NO_FIELDS_MATCH + ); + }); +}); diff --git a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/categories_pane.tsx b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/categories_pane.tsx new file mode 100644 index 0000000000000..480070fda9594 --- /dev/null +++ b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/categories_pane.tsx @@ -0,0 +1,97 @@ +/* + * 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 { EuiInMemoryTable, EuiTitle } from '@elastic/eui'; +import React from 'react'; +import styled from 'styled-components'; + +import { BrowserFields } from '../../../common/containers/source'; + +import { FieldBrowserProps } from './types'; +import { getCategoryColumns } from './category_columns'; +import { TABLE_HEIGHT } from './helpers'; + +import * as i18n from './translations'; + +const CategoryNames = styled.div<{ height: number; width: number }>` + ${({ height }) => `height: ${height}px`}; + overflow: auto; + padding: 5px; + ${({ width }) => `width: ${width}px`}; + thead { + display: none; + } +`; + +CategoryNames.displayName = 'CategoryNames'; + +const Title = styled(EuiTitle)` + padding-left: 5px; +`; + +Title.displayName = 'Title'; + +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 + * the filter input (as a substring). + */ + filteredBrowserFields: BrowserFields; + /** + * Invoked when the user clicks on the name of a category in the left-hand + * side of the field browser + */ + onCategorySelected: (categoryId: string) => void; + /** The category selected on the left-hand side of the field browser */ + selectedCategoryId: string; + /** The width of the categories pane */ + width: number; +}; + +export const CategoriesPane = React.memo( + ({ + browserFields, + filteredBrowserFields, + onCategorySelected, + onUpdateColumns, + selectedCategoryId, + timelineId, + width, + }) => ( + <> + + <h5 data-test-subj="categories-pane-title">{i18n.CATEGORIES}</h5> + + + + ({ categoryId }))} + message={i18n.NO_FIELDS_MATCH} + pagination={false} + sorting={false} + /> + + + ) +); + +CategoriesPane.displayName = 'CategoriesPane'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/category.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/category.test.tsx new file mode 100644 index 0000000000000..16174e92b3c37 --- /dev/null +++ b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/category.test.tsx @@ -0,0 +1,106 @@ +/* + * 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 { mockBrowserFields } from '../../../common/containers/source/mock'; + +import { Category } from './category'; +import { getFieldItems } from './field_items'; +import { FIELDS_PANE_WIDTH } from './helpers'; +import { TestProviders } from '../../../common/mock'; +import { useMountAppended } from '../../../common/utils/use_mount_appended'; + +import * as i18n from './translations'; + +describe('Category', () => { + const timelineId = 'test'; + const selectedCategoryId = 'client'; + const mount = useMountAppended(); + + test('it renders the category id as the value of the title', () => { + const wrapper = mount( + + + + ); + + expect(wrapper.find('[data-test-subj="selected-category-title"]').first().text()).toEqual( + selectedCategoryId + ); + }); + + test('it renders the Field column header', () => { + const wrapper = mount( + + + + ); + + expect(wrapper.find('.euiTableCellContent__text').at(0).text()).toEqual(i18n.FIELD); + }); + + test('it renders the Description column header', () => { + const wrapper = mount( + + + + ); + + expect(wrapper.find('.euiTableCellContent__text').at(1).text()).toEqual(i18n.DESCRIPTION); + }); +}); diff --git a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/category.tsx b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/category.tsx new file mode 100644 index 0000000000000..fc91693039449 --- /dev/null +++ b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/category.tsx @@ -0,0 +1,61 @@ +/* + * 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 { EuiInMemoryTable } from '@elastic/eui'; +import React from 'react'; +import styled from 'styled-components'; + +import { BrowserFields } from '../../../common/containers/source'; + +import { CategoryTitle } from './category_title'; +import { FieldItem, getFieldColumns } from './field_items'; +import { TABLE_HEIGHT } from './helpers'; + +const TableContainer = styled.div<{ height: number; width: number }>` + ${({ height }) => `height: ${height}px`}; + overflow-x: hidden; + overflow-y: auto; + ${({ width }) => `width: ${width}px`}; +`; + +TableContainer.displayName = 'TableContainer'; + +interface Props { + categoryId: string; + fieldItems: FieldItem[]; + filteredBrowserFields: BrowserFields; + onCategorySelected: (categoryId: string) => void; + timelineId: string; + width: number; +} + +export const Category = React.memo( + ({ categoryId, filteredBrowserFields, fieldItems, timelineId, width }) => ( + <> + + + + + + + ) +); + +Category.displayName = 'Category'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/category_columns.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/category_columns.test.tsx new file mode 100644 index 0000000000000..fcd19d30c8e50 --- /dev/null +++ b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/category_columns.test.tsx @@ -0,0 +1,137 @@ +/* + * 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 { mount } from 'enzyme'; +import React from 'react'; + +import { mockBrowserFields } from '../../../common/containers/source/mock'; + +import { CATEGORY_PANE_WIDTH, getFieldCount } from './helpers'; +import { CategoriesPane } from './categories_pane'; +import { ThemeProvider } from 'styled-components'; +import euiDarkVars from '@elastic/eui/dist/eui_theme_dark.json'; + +const timelineId = 'test'; +const theme = () => ({ eui: euiDarkVars, darkMode: true }); + +describe('getCategoryColumns', () => { + Object.keys(mockBrowserFields).forEach((categoryId) => { + test(`it renders the ${categoryId} category name (from filteredBrowserFields)`, () => { + const wrapper = mount( + + + + ); + + expect( + wrapper.find(`.field-browser-category-pane-${categoryId}-${timelineId}`).first().text() + ).toEqual(categoryId); + }); + }); + + Object.keys(mockBrowserFields).forEach((categoryId) => { + test(`it renders the correct field count for the ${categoryId} category (from filteredBrowserFields)`, () => { + const wrapper = mount( + + + + ); + + expect( + wrapper.find(`[data-test-subj="${categoryId}-category-count"]`).first().text() + ).toEqual(`${getFieldCount(mockBrowserFields[categoryId])}`); + }); + }); + + test('it renders the selected category with bold text', () => { + const selectedCategoryId = 'auditd'; + + const wrapper = mount( + + + + ); + + expect( + wrapper.find(`.field-browser-category-pane-${selectedCategoryId}-${timelineId}`).at(1) + ).toHaveStyleRule('font-weight', 'bold', { modifier: '.euiText' }); + }); + + test('it does NOT render an un-selected category with bold text', () => { + const selectedCategoryId = 'auditd'; + const notTheSelectedCategoryId = 'base'; + + const wrapper = mount( + + + + ); + + expect( + wrapper.find(`.field-browser-category-pane-${notTheSelectedCategoryId}-${timelineId}`).at(1) + ).toHaveStyleRule('font-weight', 'normal', { modifier: '.euiText' }); + }); + + test('it invokes onCategorySelected when a user clicks a category', () => { + const selectedCategoryId = 'auditd'; + const notTheSelectedCategoryId = 'base'; + + const onCategorySelected = jest.fn(); + + const wrapper = mount( + + + + ); + + wrapper + .find(`.field-browser-category-pane-${notTheSelectedCategoryId}-${timelineId}`) + .first() + .simulate('click'); + + expect(onCategorySelected).toHaveBeenCalledWith(notTheSelectedCategoryId); + }); +}); diff --git a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/category_columns.tsx b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/category_columns.tsx new file mode 100644 index 0000000000000..14c17b7262724 --- /dev/null +++ b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/category_columns.tsx @@ -0,0 +1,144 @@ +/* + * 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. + */ + +/* eslint-disable react/display-name */ + +import { EuiIcon, EuiFlexGroup, EuiFlexItem, EuiLink, EuiText, EuiToolTip } from '@elastic/eui'; +import React, { useMemo } from 'react'; +import styled from 'styled-components'; + +import { BrowserFields } from '../../../common/containers/source'; +import { getColumnsWithTimestamp } from '../../../common/components/event_details/helpers'; +import { CountBadge } from '../../../common/components/page'; +import { OnUpdateColumns } from '../timeline/events'; +import { WithHoverActions } from '../../../common/components/with_hover_actions'; +import { LoadingSpinner, getCategoryPaneCategoryClassName, getFieldCount } from './helpers'; +import * as i18n from './translations'; +import { useManageTimeline } from '../manage_timeline'; + +const CategoryName = styled.span<{ bold: boolean }>` + .euiText { + font-weight: ${({ bold }) => (bold ? 'bold' : 'normal')}; + } +`; + +CategoryName.displayName = 'CategoryName'; + +const LinkContainer = styled.div` + width: 100%; + .euiLink { + width: 100%; + } +`; + +LinkContainer.displayName = 'LinkContainer'; + +export interface CategoryItem { + categoryId: string; +} + +interface ToolTipProps { + categoryId: string; + browserFields: BrowserFields; + onUpdateColumns: OnUpdateColumns; + timelineId: string; +} + +const ToolTip = React.memo( + ({ categoryId, browserFields, onUpdateColumns, timelineId }) => { + const { getManageTimelineById } = useManageTimeline(); + // eslint-disable-next-line react-hooks/exhaustive-deps + const { isLoading } = useMemo(() => getManageTimelineById(timelineId) ?? { isLoading: false }, [ + timelineId, + ]); + 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, + onCategorySelected, + onUpdateColumns, + selectedCategoryId, + timelineId, +}: { + browserFields: BrowserFields; + filteredBrowserFields: BrowserFields; + onCategorySelected: (categoryId: string) => void; + onUpdateColumns: OnUpdateColumns; + selectedCategoryId: string; + timelineId: string; +}) => [ + { + field: 'categoryId', + name: '', + sortable: true, + truncateText: false, + render: (categoryId: string, _: { categoryId: string }) => ( + + onCategorySelected(categoryId)}> + + + + } + render={() => ( + + {categoryId} + + )} + /> + + + + + {getFieldCount(filteredBrowserFields[categoryId])} + + + + + + ), + }, +]; diff --git a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/category_title.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/category_title.test.tsx new file mode 100644 index 0000000000000..ac7b2f7e67ae8 --- /dev/null +++ b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/category_title.test.tsx @@ -0,0 +1,62 @@ +/* + * 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 { mount } from 'enzyme'; +import React from 'react'; + +import { mockBrowserFields } from '../../../common/containers/source/mock'; + +import { CategoryTitle } from './category_title'; +import { getFieldCount } from './helpers'; + +describe('CategoryTitle', () => { + const timelineId = 'test'; + + test('it renders the category id as the value of the title', () => { + const categoryId = 'client'; + const wrapper = mount( + + ); + + expect(wrapper.find('[data-test-subj="selected-category-title"]').first().text()).toEqual( + categoryId + ); + }); + + test('when `categoryId` specifies a valid category in `filteredBrowserFields`, a count of the field is displayed in the badge', () => { + const validCategoryId = 'client'; + const wrapper = mount( + + ); + + expect(wrapper.find(`[data-test-subj="selected-category-count-badge"]`).first().text()).toEqual( + `${getFieldCount(mockBrowserFields[validCategoryId])}` + ); + }); + + test('when `categoryId` specifies an INVALID category in `filteredBrowserFields`, a count of zero is displayed in the badge', () => { + const invalidCategoryId = 'this.is.not.happening'; + const wrapper = mount( + + ); + + expect(wrapper.find(`[data-test-subj="selected-category-count-badge"]`).first().text()).toEqual( + '0' + ); + }); +}); diff --git a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/category_title.tsx b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/category_title.tsx new file mode 100644 index 0000000000000..c8d59f5c0dfa4 --- /dev/null +++ b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/category_title.tsx @@ -0,0 +1,59 @@ +/* + * 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 { EuiFlexGroup, EuiFlexItem, EuiTitle } from '@elastic/eui'; +import React from 'react'; +import styled from 'styled-components'; + +import { BrowserFields } from '../../../common/containers/source'; +import { getFieldBrowserCategoryTitleClassName, getFieldCount } from './helpers'; +import { CountBadge } from '../../../common/components/page'; + +const CountBadgeContainer = styled.div` + position: relative; + top: -3px; +`; + +CountBadgeContainer.displayName = 'CountBadgeContainer'; + +interface Props { + /** The title of the category */ + categoryId: string; + /** + * A map of categoryId -> metadata about the fields in that category, + * filtered such that the name of every field in the category includes + * the filter input (as a substring). + */ + filteredBrowserFields: BrowserFields; + /** The timeline associated with this field browser */ + timelineId: string; +} + +export const CategoryTitle = React.memo( + ({ filteredBrowserFields, categoryId, timelineId }) => ( + + + +
{categoryId}
+
+
+ + + + + {getFieldCount(filteredBrowserFields[categoryId])} + + + +
+ ) +); + +CategoryTitle.displayName = 'CategoryTitle'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/field_browser.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/field_browser.test.tsx new file mode 100644 index 0000000000000..7c4e3d435e1ed --- /dev/null +++ b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/field_browser.test.tsx @@ -0,0 +1,250 @@ +/* + * 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 { mount } from 'enzyme'; +import React from 'react'; + +import { mockBrowserFields } from '../../../common/containers/source/mock'; +import { TestProviders } from '../../../common/mock'; + +import { FieldsBrowser } from './field_browser'; +import { FIELD_BROWSER_HEIGHT, FIELD_BROWSER_WIDTH } from './helpers'; + +describe('FieldsBrowser', () => { + const timelineId = 'test'; + + // `enzyme` doesn't mount the components into the global jsdom `document` + // but that's where the click detector listener is, so for testing, we + // pass the top-level mounted component's click event on to document + const triggerDocumentMouseDown = () => { + const event = new Event('mousedown'); + document.dispatchEvent(event); + }; + + const triggerDocumentMouseUp = () => { + const event = new Event('mouseup'); + document.dispatchEvent(event); + }; + + test('it invokes onOutsideClick when onFieldSelected is undefined, and the user clicks outside the fields browser', () => { + const onOutsideClick = jest.fn(); + + const wrapper = mount( + +
+ +
+
+ ); + + wrapper.find('[data-test-subj="outside"]').simulate('mousedown'); + wrapper.find('[data-test-subj="outside"]').simulate('mouseup'); + + expect(onOutsideClick).toHaveBeenCalled(); + }); + + test('it does NOT invoke onOutsideClick when onFieldSelected is defined, and the user clicks outside the fields browser', () => { + const onOutsideClick = jest.fn(); + + const wrapper = mount( + +
+ +
+
+ ); + + wrapper.find('[data-test-subj="outside"]').simulate('mousedown'); + wrapper.find('[data-test-subj="outside"]').simulate('mouseup'); + + expect(onOutsideClick).not.toHaveBeenCalled(); + }); + + test('it renders the header', () => { + const wrapper = mount( + + + + ); + + expect(wrapper.find('[data-test-subj="header"]').exists()).toBe(true); + }); + + test('it renders the categories pane', () => { + const wrapper = mount( + + + + ); + + expect(wrapper.find('[data-test-subj="left-categories-pane"]').exists()).toBe(true); + }); + + test('it renders the fields pane', () => { + const wrapper = mount( + + + + ); + + expect(wrapper.find('[data-test-subj="fields-pane"]').exists()).toBe(true); + }); + + test('focuses the search input when the component mounts', () => { + const wrapper = mount( + + + + ); + + expect( + wrapper.find('[data-test-subj="field-search"]').first().getDOMNode().id === + document.activeElement!.id + ).toBe(true); + }); + + test('it invokes onSearchInputChange when the user types in the field search input', () => { + const onSearchInputChange = jest.fn(); + const inputText = 'event.category'; + + const wrapper = mount( + + + + ); + + const searchField = wrapper.find('[data-test-subj="field-search"]').first(); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const changeEvent: any = { target: { value: inputText } }; + const onChange = searchField.props().onChange; + + onChange!(changeEvent); + searchField.simulate('change').update(); + + expect(onSearchInputChange).toBeCalledWith(inputText); + }); +}); diff --git a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/field_items.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/field_items.test.tsx new file mode 100644 index 0000000000000..e4c9621c2f71c --- /dev/null +++ b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/field_items.test.tsx @@ -0,0 +1,281 @@ +/* + * 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 { omit } from 'lodash/fp'; +import React from 'react'; + +import { mockBrowserFields } from '../../../common/containers/source/mock'; +import { TestProviders } from '../../../common/mock'; +import { ColumnHeaderOptions } from '../../../timelines/store/timeline/model'; +import { defaultColumnHeaderType } from '../timeline/body/column_headers/default_headers'; +import { DEFAULT_DATE_COLUMN_MIN_WIDTH } from '../timeline/body/constants'; + +import { Category } from './category'; +import { getFieldColumns, getFieldItems } from './field_items'; +import { FIELDS_PANE_WIDTH } from './helpers'; +import { useMountAppended } from '../../../common/utils/use_mount_appended'; + +const selectedCategoryId = 'base'; +const selectedCategoryFields = mockBrowserFields[selectedCategoryId].fields; +const timestampFieldId = '@timestamp'; +const columnHeaders: ColumnHeaderOptions[] = [ + { + category: 'base', + columnHeaderType: defaultColumnHeaderType, + description: + 'Date/time when the event originated.\nFor log events this is the date/time when the event was generated, and not when it was read.\nRequired field for all events.', + example: '2016-05-23T08:05:34.853Z', + id: '@timestamp', + type: 'date', + aggregatable: true, + width: DEFAULT_DATE_COLUMN_MIN_WIDTH, + }, +]; + +describe('field_items', () => { + const timelineId = 'test'; + const mount = useMountAppended(); + + describe('getFieldItems', () => { + Object.keys(selectedCategoryFields!).forEach((fieldId) => { + test(`it renders the name of the ${fieldId} field`, () => { + const wrapper = mount( + + + + ); + + expect(wrapper.find(`[data-test-subj="field-name-${fieldId}"]`).first().text()).toEqual( + fieldId + ); + }); + }); + + Object.keys(selectedCategoryFields!).forEach((fieldId) => { + test(`it renders a checkbox for the ${fieldId} field`, () => { + const wrapper = mount( + + + + ); + + expect(wrapper.find(`[data-test-subj="field-${fieldId}-checkbox"]`).first().exists()).toBe( + true + ); + }); + }); + + test('it renders a checkbox in the checked state when the field is selected to be displayed as a column in the timeline', () => { + const wrapper = mount( + + + + ); + + expect( + wrapper.find(`[data-test-subj="field-${timestampFieldId}-checkbox"]`).first().props() + .checked + ).toBe(true); + }); + + test('it does NOT render a checkbox in the checked state when the field is NOT selected to be displayed as a column in the timeline', () => { + const wrapper = mount( + + header.id !== timestampFieldId), + highlight: '', + onUpdateColumns: jest.fn(), + timelineId, + toggleColumn: jest.fn(), + })} + width={FIELDS_PANE_WIDTH} + onCategorySelected={jest.fn()} + timelineId={timelineId} + /> + + ); + + expect( + wrapper.find(`[data-test-subj="field-${timestampFieldId}-checkbox"]`).first().props() + .checked + ).toBe(false); + }); + + test('it invokes `toggleColumn` when the user interacts with the checkbox', () => { + const toggleColumn = jest.fn(); + + const wrapper = mount( + + + + ); + + wrapper + .find('input[type="checkbox"]') + .first() + .simulate('change', { + target: { checked: true }, + }); + wrapper.update(); + + expect(toggleColumn).toBeCalledWith({ + columnHeaderType: 'not-filtered', + id: '@timestamp', + width: 180, + }); + }); + + test('it renders the expected icon for a field', () => { + const wrapper = mount( + + + + ); + + expect( + wrapper.find(`[data-test-subj="field-${timestampFieldId}-icon"]`).first().props().type + ).toEqual('clock'); + }); + + test('it renders the expected field description', () => { + const wrapper = mount( + + + + ); + + expect( + wrapper.find(`[data-test-subj="field-${timestampFieldId}-description"]`).first().text() + ).toEqual( + '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' + ); + }); + }); + + describe('getFieldColumns', () => { + test('it returns the expected column definitions', () => { + expect(getFieldColumns().map((column) => omit('render', column))).toEqual([ + { field: 'field', name: 'Field', sortable: true, width: '250px' }, + { + field: 'description', + name: 'Description', + sortable: true, + truncateText: true, + width: '400px', + }, + ]); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/field_items.tsx b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/field_items.tsx new file mode 100644 index 0000000000000..aaad9cf145ab7 --- /dev/null +++ b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/field_items.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. + */ + +/* eslint-disable react/display-name */ + +import { EuiCheckbox, EuiIcon, EuiToolTip, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { uniqBy } from 'lodash/fp'; +import React from 'react'; +import { Draggable } from 'react-beautiful-dnd'; +import styled from 'styled-components'; + +import { BrowserField, BrowserFields } from '../../../common/containers/source'; +import { ColumnHeaderOptions } from '../../../timelines/store/timeline/model'; +import { DragEffects } from '../../../common/components/drag_and_drop/draggable_wrapper'; +import { DroppableWrapper } from '../../../common/components/drag_and_drop/droppable_wrapper'; +import { + getDraggableFieldId, + getDroppableId, + DRAG_TYPE_FIELD, +} from '../../../common/components/drag_and_drop/helpers'; +import { DraggableFieldBadge } from '../../../common/components/draggables/field_badge'; +import { getEmptyValue } from '../../../common/components/empty_value'; +import { + getColumnsWithTimestamp, + getExampleText, + getIconFromType, +} from '../../../common/components/event_details/helpers'; +import { SelectableText } from '../../../common/components/selectable_text'; +import { defaultColumnHeaderType } from '../timeline/body/column_headers/default_headers'; +import { DEFAULT_COLUMN_MIN_WIDTH } from '../timeline/body/constants'; +import { OnUpdateColumns } from '../timeline/events'; +import { TruncatableText } from '../../../common/components/truncatable_text'; +import { FieldName } from './field_name'; +import * as i18n from './translations'; + +const TypeIcon = styled(EuiIcon)` + margin-left: 5px; + position: relative; + top: -1px; +`; + +TypeIcon.displayName = 'TypeIcon'; + +export const Description = styled.span` + user-select: text; + width: 150px; +`; + +Description.displayName = 'Description'; + +/** + * An item rendered in the table + */ +export interface FieldItem { + description: React.ReactNode; + field: React.ReactNode; + fieldId: string; +} + +/** + * Returns the draggable fields, values, and descriptions shown when a user expands an event + */ +export const getFieldItems = ({ + browserFields, + category, + categoryId, + columnHeaders, + highlight = '', + onUpdateColumns, + timelineId, + toggleColumn, +}: { + browserFields: BrowserFields; + category: Partial; + categoryId: string; + columnHeaders: ColumnHeaderOptions[]; + highlight?: string; + timelineId: string; + toggleColumn: (column: ColumnHeaderOptions) => void; + onUpdateColumns: OnUpdateColumns; +}): FieldItem[] => + uniqBy('name', [ + ...Object.values(category != null && category.fields != null ? category.fields : {}), + ]).map((field) => ({ + description: ( + + {`${field.description || getEmptyValue()} ${getExampleText(field.example)}`} + + ), + field: ( + ( +
+ + + +
+ )} + > + + {(provided) => ( +
+ + + + c.id === field.name) !== -1} + data-test-subj={`field-${field.name}-checkbox`} + id={field.name || ''} + onChange={() => + toggleColumn({ + columnHeaderType: defaultColumnHeaderType, + id: field.name || '', + width: DEFAULT_COLUMN_MIN_WIDTH, + }) + } + /> + + + + + + + + + + + + + +
+ )} +
+
+ ), + fieldId: field.name || '', + })); + +/** + * Returns a table column template provided to the `EuiInMemoryTable`'s + * `columns` prop + */ +export const getFieldColumns = () => [ + { + field: 'field', + name: i18n.FIELD, + sortable: true, + render: (field: React.ReactNode, _: FieldItem) => <>{field}, + width: '250px', + }, + { + field: 'description', + name: i18n.DESCRIPTION, + render: (description: string, _: FieldItem) => ( + + + <>{description} + + + ), + sortable: true, + truncateText: true, + width: '400px', + }, +]; diff --git a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/field_name.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/field_name.test.tsx new file mode 100644 index 0000000000000..da0cbb99b8671 --- /dev/null +++ b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/field_name.test.tsx @@ -0,0 +1,66 @@ +/* + * 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 { mount } from 'enzyme'; +import React from 'react'; + +import { mockBrowserFields } from '../../../common/containers/source/mock'; +import { TestProviders } from '../../../common/mock'; +import { getColumnsWithTimestamp } from '../../../common/components/event_details/helpers'; + +import { FieldName } from './field_name'; + +const categoryId = 'base'; +const timestampFieldId = '@timestamp'; + +const defaultProps = { + categoryId, + categoryColumns: getColumnsWithTimestamp({ + browserFields: mockBrowserFields, + category: categoryId, + }), + fieldId: timestampFieldId, + onUpdateColumns: jest.fn(), + timelineId: 'timeline-id', +}; + +describe('FieldName', () => { + test('it renders the field name', () => { + const wrapper = mount( + + + + ); + + expect( + wrapper.find(`[data-test-subj="field-name-${timestampFieldId}"]`).first().text() + ).toEqual(timestampFieldId); + }); + + test('it renders a copy to clipboard action menu item a user hovers over the name', () => { + const wrapper = mount( + + + + ); + + wrapper.simulate('mouseenter'); + wrapper.update(); + expect(wrapper.find('[data-test-subj="copy-to-clipboard"]').exists()).toBe(true); + }); + + test('it highlights the text specified by the `highlight` prop', () => { + const highlight = 'stamp'; + + const wrapper = mount( + + + + ); + + expect(wrapper.find('strong').first().text()).toEqual(highlight); + }); +}); diff --git a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/field_name.tsx b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/field_name.tsx new file mode 100644 index 0000000000000..985c8b35094ef --- /dev/null +++ b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/field_name.tsx @@ -0,0 +1,115 @@ +/* + * 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 { EuiHighlight, EuiText } from '@elastic/eui'; +import React, { useCallback, useState, useMemo } from 'react'; +import styled from 'styled-components'; + +import { ColumnHeaderOptions } from '../../../timelines/store/timeline/model'; +import { OnUpdateColumns } from '../timeline/events'; +import { WithHoverActions } from '../../../common/components/with_hover_actions'; +import { DraggableWrapperHoverContent } from '../../../common/components/drag_and_drop/draggable_wrapper_hover_content'; + +/** + * The name of a (draggable) field + */ +export const FieldNameContainer = styled.span` + border-radius: 4px; + padding: 0 4px 0 8px; + position: relative; + + &::before { + background-image: linear-gradient( + 135deg, + ${({ theme }) => theme.eui.euiColorMediumShade} 25%, + transparent 25% + ), + linear-gradient(-135deg, ${({ theme }) => theme.eui.euiColorMediumShade} 25%, transparent 25%), + linear-gradient(135deg, transparent 75%, ${({ theme }) => theme.eui.euiColorMediumShade} 75%), + linear-gradient(-135deg, transparent 75%, ${({ theme }) => theme.eui.euiColorMediumShade} 75%); + background-position: 0 0, 1px 0, 1px -1px, 0px 1px; + background-size: 2px 2px; + bottom: 2px; + content: ''; + display: block; + left: 2px; + position: absolute; + top: 2px; + width: 4px; + } + + &:hover, + &:focus { + transition: background-color 0.7s ease; + background-color: #000; + color: #fff; + + &::before { + background-image: linear-gradient(135deg, #fff 25%, transparent 25%), + linear-gradient( + -135deg, + ${({ theme }) => theme.eui.euiColorLightestShade} 25%, + transparent 25% + ), + linear-gradient( + 135deg, + transparent 75%, + ${({ theme }) => theme.eui.euiColorLightestShade} 75% + ), + linear-gradient( + -135deg, + transparent 75%, + ${({ theme }) => theme.eui.euiColorLightestShade} 75% + ); + } + } +`; + +FieldNameContainer.displayName = 'FieldNameContainer'; + +/** Renders a field name in it's non-dragging state */ +export const FieldName = React.memo<{ + categoryId: string; + categoryColumns: ColumnHeaderOptions[]; + fieldId: string; + highlight?: string; + onUpdateColumns: OnUpdateColumns; + timelineId: string; +}>(({ fieldId, highlight = '', timelineId }) => { + const [showTopN, setShowTopN] = useState(false); + const toggleTopN = useCallback(() => { + setShowTopN(!showTopN); + }, [setShowTopN, showTopN]); + + const hoverContent = useMemo( + () => ( + + ), + [fieldId, showTopN, toggleTopN, timelineId] + ); + + const render = useCallback( + () => ( + + + + {fieldId} + + + + ), + [fieldId, highlight] + ); + + return ; +}); + +FieldName.displayName = 'FieldName'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/fields_pane.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/fields_pane.test.tsx new file mode 100644 index 0000000000000..b55bbfc023774 --- /dev/null +++ b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/fields_pane.test.tsx @@ -0,0 +1,120 @@ +/* + * 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 { mockBrowserFields } from '../../../common/containers/source/mock'; +import { TestProviders } from '../../../common/mock'; +import { useMountAppended } from '../../../common/utils/use_mount_appended'; + +import { FIELDS_PANE_WIDTH } from './helpers'; +import { FieldsPane } from './fields_pane'; + +const timelineId = 'test'; + +describe('FieldsPane', () => { + const mount = useMountAppended(); + + test('it renders the selected category', () => { + const selectedCategory = 'auditd'; + + const wrapper = mount( + + + + ); + + expect(wrapper.find(`[data-test-subj="selected-category-title"]`).first().text()).toEqual( + selectedCategory + ); + }); + + test('it renders a unknown category that does not exist in filteredBrowserFields', () => { + const selectedCategory = 'unknown'; + + const wrapper = mount( + + + + ); + + expect(wrapper.find(`[data-test-subj="selected-category-title"]`).first().text()).toEqual( + selectedCategory + ); + }); + + test('it renders the expected message when `filteredBrowserFields` is empty and `searchInput` is empty', () => { + const searchInput = ''; + + const wrapper = mount( + + + + ); + + expect(wrapper.find(`[data-test-subj="no-fields-match"]`).first().text()).toEqual( + 'No fields match ' + ); + }); + + test('it renders the expected message when `filteredBrowserFields` is empty and `searchInput` is an unknown field name', () => { + const searchInput = 'thisFieldDoesNotExist'; + + const wrapper = mount( + + + + ); + + expect(wrapper.find(`[data-test-subj="no-fields-match"]`).first().text()).toEqual( + `No fields match ${searchInput}` + ); + }); +}); diff --git a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/fields_pane.tsx b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/fields_pane.tsx new file mode 100644 index 0000000000000..73ea739216857 --- /dev/null +++ b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/fields_pane.tsx @@ -0,0 +1,106 @@ +/* + * 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 { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import React from 'react'; +import styled from 'styled-components'; + +import { BrowserFields } from '../../../common/containers/source'; +import { ColumnHeaderOptions } from '../../../timelines/store/timeline/model'; + +import { Category } from './category'; +import { FieldBrowserProps } from './types'; +import { getFieldItems } from './field_items'; +import { FIELDS_PANE_WIDTH, TABLE_HEIGHT } from './helpers'; + +import * as i18n from './translations'; + +const NoFieldsPanel = styled.div` + background-color: ${(props) => props.theme.eui.euiColorLightestShade}; + width: ${FIELDS_PANE_WIDTH}px; + height: ${TABLE_HEIGHT}px; +`; + +NoFieldsPanel.displayName = 'NoFieldsPanel'; + +const NoFieldsFlexGroup = styled(EuiFlexGroup)` + height: 100%; +`; + +NoFieldsFlexGroup.displayName = 'NoFieldsFlexGroup'; + +type Props = Pick & { + columnHeaders: ColumnHeaderOptions[]; + /** + * A map of categoryId -> metadata about the fields in that category, + * filtered such that the name of every field in the category includes + * the filter input (as a substring). + */ + filteredBrowserFields: BrowserFields; + /** + * Invoked when the user clicks on the name of a category in the left-hand + * side of the field browser + */ + onCategorySelected: (categoryId: string) => void; + /** The text displayed in the search input */ + searchInput: string; + /** + * The category selected on the left-hand side of the field browser + */ + selectedCategoryId: string; + /** The width field browser */ + width: number; + /** + * Invoked to add or remove a column from the timeline + */ + toggleColumn: (column: ColumnHeaderOptions) => void; +}; +export const FieldsPane = React.memo( + ({ + columnHeaders, + filteredBrowserFields, + onCategorySelected, + onUpdateColumns, + searchInput, + selectedCategoryId, + timelineId, + toggleColumn, + width, + }) => ( + <> + {Object.keys(filteredBrowserFields).length > 0 ? ( + + ) : ( + + + +

{i18n.NO_FIELDS_MATCH_INPUT(searchInput)}

+
+
+
+ )} + + ) +); + +FieldsPane.displayName = 'FieldsPane'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/header.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/header.test.tsx new file mode 100644 index 0000000000000..bb33b36dd9a19 --- /dev/null +++ b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/header.test.tsx @@ -0,0 +1,258 @@ +/* + * 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 { mount } from 'enzyme'; +import React from 'react'; +import { mockBrowserFields } from '../../../common/containers/source/mock'; +import { TestProviders } from '../../../common/mock'; +import { defaultHeaders } from '../timeline/body/column_headers/default_headers'; +import { Header } from './header'; + +const timelineId = 'test'; + +describe('Header', () => { + test('it renders the field browser title', () => { + const wrapper = mount( + +
+ + ); + + expect(wrapper.find('[data-test-subj="field-browser-title"]').first().text()).toEqual( + 'Customize Columns' + ); + }); + + test('it renders the Reset Fields button', () => { + const wrapper = mount( + +
+ + ); + + expect(wrapper.find('[data-test-subj="reset-fields"]').first().text()).toEqual('Reset Fields'); + }); + + test('it invokes onUpdateColumns when the user clicks the Reset Fields button', () => { + const onUpdateColumns = jest.fn(); + + const wrapper = mount( + +
+ + ); + + wrapper.find('[data-test-subj="reset-fields"]').first().simulate('click'); + + expect(onUpdateColumns).toBeCalledWith(defaultHeaders); + }); + + test('it invokes onOutsideClick when the user clicks the Reset Fields button', () => { + const onOutsideClick = jest.fn(); + + const wrapper = mount( + +
+ + ); + + wrapper.find('[data-test-subj="reset-fields"]').first().simulate('click'); + + expect(onOutsideClick).toBeCalled(); + }); + + test('it renders the field search input with the expected placeholder text when the searchInput prop is empty', () => { + const wrapper = mount( + +
+ + ); + + expect(wrapper.find('[data-test-subj="field-search"]').first().props().placeholder).toEqual( + 'Field name' + ); + }); + + test('it renders the "current" search value in the input when searchInput is not empty', () => { + const searchInput = 'aFieldName'; + + const wrapper = mount( + +
+ + ); + + expect(wrapper.find('input').props().value).toEqual(searchInput); + }); + + test('it renders the field search input with a spinner when isSearching is true', () => { + const wrapper = mount( + +
+ + ); + + expect(wrapper.find('.euiLoadingSpinner').first().exists()).toBe(true); + }); + + test('it invokes onSearchInputChange when the user types in the search field', () => { + const onSearchInputChange = jest.fn(); + + const wrapper = mount( + +
+ + ); + + wrapper + .find('input') + .first() + .simulate('change', { target: { value: 'timestamp' } }); + wrapper.update(); + + expect(onSearchInputChange).toBeCalled(); + }); + + test('it returns the expected categories count when filteredBrowserFields is empty', () => { + const wrapper = mount( + +
+ + ); + + expect(wrapper.find('[data-test-subj="categories-count"]').first().text()).toEqual( + '0 categories' + ); + }); + + test('it returns the expected categories count when filteredBrowserFields is NOT empty', () => { + const wrapper = mount( + +
+ + ); + + expect(wrapper.find('[data-test-subj="categories-count"]').first().text()).toEqual( + '9 categories' + ); + }); + + test('it returns the expected fields count when filteredBrowserFields is empty', () => { + const wrapper = mount( + +
+ + ); + + expect(wrapper.find('[data-test-subj="fields-count"]').first().text()).toEqual('0 fields'); + }); + + test('it returns the expected fields count when filteredBrowserFields is NOT empty', () => { + const wrapper = mount( + +
+ + ); + + expect(wrapper.find('[data-test-subj="fields-count"]').first().text()).toEqual('25 fields'); + }); +}); diff --git a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/header.tsx b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/header.tsx new file mode 100644 index 0000000000000..2e822e1765ff5 --- /dev/null +++ b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/header.tsx @@ -0,0 +1,123 @@ +/* + * 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 { EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, EuiTitle } from '@elastic/eui'; +import React, { useCallback, useMemo } from 'react'; +import styled from 'styled-components'; + +import { BrowserFields } from '../../../common/containers/source'; +import { alertsHeaders } from '../../../alerts/components/alerts_table/default_config'; +import { alertsHeaders as externalAlertsHeaders } from '../../../common/components/alerts_viewer/default_headers'; +import { defaultHeaders as eventsDefaultHeaders } from '../../../common/components/events_viewer/default_headers'; +import { defaultHeaders } from '../timeline/body/column_headers/default_headers'; +import { OnUpdateColumns } from '../timeline/events'; + +import { SEARCH_INPUT_WIDTH } from './helpers'; + +import * as i18n from './translations'; +import { useManageTimeline } from '../manage_timeline'; + +// background-color: ${props => props.theme.eui.euiColorLightestShade}; +const HeaderContainer = styled.div` + padding: 16px; + margin-bottom: 8px; +`; + +HeaderContainer.displayName = 'HeaderContainer'; + +const SearchContainer = styled.div` + input { + max-width: ${SEARCH_INPUT_WIDTH}px; + width: ${SEARCH_INPUT_WIDTH}px; + } +`; + +SearchContainer.displayName = 'SearchContainer'; + +interface Props { + filteredBrowserFields: BrowserFields; + isEventViewer?: boolean; + isSearching: boolean; + onOutsideClick: () => void; + onSearchInputChange: (event: React.ChangeEvent) => void; + onUpdateColumns: OnUpdateColumns; + searchInput: string; + timelineId: string; +} + +const TitleRow = React.memo<{ + id: string; + isEventViewer?: boolean; + onOutsideClick: () => void; + onUpdateColumns: OnUpdateColumns; +}>(({ id, isEventViewer, onOutsideClick, onUpdateColumns }) => { + const { getManageTimelineById } = useManageTimeline(); + const documentType = useMemo(() => getManageTimelineById(id).documentType, [ + getManageTimelineById, + id, + ]); + const handleResetColumns = useCallback(() => { + let resetDefaultHeaders = defaultHeaders; + if (isEventViewer) { + if (documentType.toLocaleLowerCase() === 'externalAlerts') { + resetDefaultHeaders = externalAlertsHeaders; + } else if (documentType.toLocaleLowerCase() === 'alerts') { + resetDefaultHeaders = alertsHeaders; + } else { + resetDefaultHeaders = eventsDefaultHeaders; + } + } + onUpdateColumns(resetDefaultHeaders); + onOutsideClick(); + }, [isEventViewer, onOutsideClick, onUpdateColumns, documentType]); + + return ( + + + +

{i18n.CUSTOMIZE_COLUMNS}

+
+
+ + + + {i18n.RESET_FIELDS} + + +
+ ); +}); + +TitleRow.displayName = 'TitleRow'; + +export const Header = React.memo( + ({ + isEventViewer, + isSearching, + filteredBrowserFields, + onOutsideClick, + onSearchInputChange, + onUpdateColumns, + searchInput, + timelineId, + }) => ( + + + + ) +); + +Header.displayName = 'Header'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/helpers.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/helpers.test.tsx new file mode 100644 index 0000000000000..0e1b00dd9b864 --- /dev/null +++ b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/helpers.test.tsx @@ -0,0 +1,376 @@ +/* + * 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 { mockBrowserFields } from '../../../common/containers/source/mock'; + +import { + categoryHasFields, + createVirtualCategory, + getCategoryPaneCategoryClassName, + getFieldBrowserCategoryTitleClassName, + getFieldBrowserSearchInputClassName, + getFieldCount, + filterBrowserFieldsByFieldName, +} from './helpers'; +import { BrowserFields } from '../../../common/containers/source'; + +const timelineId = 'test'; + +describe('helpers', () => { + describe('getCategoryPaneCategoryClassName', () => { + test('it returns the expected class name', () => { + const categoryId = 'auditd'; + + expect(getCategoryPaneCategoryClassName({ categoryId, timelineId })).toEqual( + 'field-browser-category-pane-auditd-test' + ); + }); + }); + + describe('getFieldBrowserCategoryTitleClassName', () => { + test('it returns the expected class name', () => { + const categoryId = 'auditd'; + + expect(getFieldBrowserCategoryTitleClassName({ categoryId, timelineId })).toEqual( + 'field-browser-category-title-auditd-test' + ); + }); + }); + + describe('getFieldBrowserSearchInputClassName', () => { + test('it returns the expected class name', () => { + expect(getFieldBrowserSearchInputClassName(timelineId)).toEqual( + 'field-browser-search-input-test' + ); + }); + }); + + describe('categoryHasFields', () => { + test('it returns false if the category fields property is undefined', () => { + expect(categoryHasFields({})).toBe(false); + }); + + test('it returns false if the category fields property is empty', () => { + expect(categoryHasFields({ fields: {} })).toBe(false); + }); + + test('it returns true if the category has one field', () => { + expect( + categoryHasFields({ + fields: { + 'auditd.data.a0': { + aggregatable: true, + category: 'auditd', + description: null, + example: null, + format: '', + indexes: ['auditbeat'], + name: 'auditd.data.a0', + searchable: true, + type: 'string', + }, + }, + }) + ).toBe(true); + }); + + test('it returns true if the category has multiple fields', () => { + expect( + categoryHasFields({ + fields: { + 'agent.ephemeral_id': { + aggregatable: true, + category: 'agent', + description: + 'Ephemeral identifier of this agent (if one exists). This id normally changes across restarts, but `agent.id` does not.', + example: '8a4f500f', + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'agent.ephemeral_id', + searchable: true, + type: 'string', + }, + 'agent.hostname': { + aggregatable: true, + category: 'agent', + description: null, + example: null, + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'agent.hostname', + searchable: true, + type: 'string', + }, + }, + }) + ).toBe(true); + }); + }); + + describe('getFieldCount', () => { + test('it returns 0 if the category fields property is undefined', () => { + expect(getFieldCount({})).toEqual(0); + }); + + test('it returns 0 if the category fields property is empty', () => { + expect(getFieldCount({ fields: {} })).toEqual(0); + }); + + test('it returns 1 if the category has one field', () => { + expect( + getFieldCount({ + fields: { + 'auditd.data.a0': { + aggregatable: true, + category: 'auditd', + description: null, + example: null, + format: '', + indexes: ['auditbeat'], + name: 'auditd.data.a0', + searchable: true, + type: 'string', + }, + }, + }) + ).toEqual(1); + }); + + test('it returns the correct count when category has multiple fields', () => { + expect( + getFieldCount({ + fields: { + 'agent.ephemeral_id': { + aggregatable: true, + category: 'agent', + description: + 'Ephemeral identifier of this agent (if one exists). This id normally changes across restarts, but `agent.id` does not.', + example: '8a4f500f', + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'agent.ephemeral_id', + searchable: true, + type: 'string', + }, + 'agent.hostname': { + aggregatable: true, + category: 'agent', + description: null, + example: null, + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'agent.hostname', + searchable: true, + type: 'string', + }, + }, + }) + ).toEqual(2); + }); + }); + + describe('filterBrowserFieldsByFieldName', () => { + test('it returns an empty collection when browserFields is empty', () => { + expect(filterBrowserFieldsByFieldName({ browserFields: {}, substring: '' })).toEqual({}); + }); + + test('it returns an empty collection when browserFields is empty and substring is non empty', () => { + expect( + filterBrowserFieldsByFieldName({ browserFields: {}, substring: 'nothing to match' }) + ).toEqual({}); + }); + + test('it returns an empty collection when browserFields is NOT empty and substring does not match any fields', () => { + expect( + filterBrowserFieldsByFieldName({ + browserFields: mockBrowserFields, + substring: 'nothing to match', + }) + ).toEqual({}); + }); + + test('it returns the original collection when browserFields is NOT empty and substring is empty', () => { + expect( + filterBrowserFieldsByFieldName({ + browserFields: mockBrowserFields, + substring: '', + }) + ).toEqual(mockBrowserFields); + }); + + test('it returns (only) non-empty categories, where each category contains only the fields matching the substring', () => { + const filtered: BrowserFields = { + agent: { + fields: { + 'agent.ephemeral_id': { + aggregatable: true, + category: 'agent', + description: + 'Ephemeral identifier of this agent (if one exists). This id normally changes across restarts, but `agent.id` does not.', + example: '8a4f500f', + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'agent.ephemeral_id', + searchable: true, + type: 'string', + }, + 'agent.id': { + aggregatable: true, + category: 'agent', + description: + 'Unique identifier of this agent (if one exists). Example: For Beats this would be beat.id.', + example: '8a4f500d', + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'agent.id', + searchable: true, + type: 'string', + }, + }, + }, + cloud: { + fields: { + 'cloud.account.id': { + aggregatable: true, + category: 'cloud', + description: + 'The cloud account or organization id used to identify different entities in a multi-tenant environment. Examples: AWS account id, Google Cloud ORG Id, or other unique identifier.', + example: '666777888999', + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'cloud.account.id', + searchable: true, + type: 'string', + }, + }, + }, + container: { + fields: { + 'container.id': { + aggregatable: true, + category: 'container', + description: 'Unique container id.', + example: null, + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'container.id', + searchable: true, + type: 'string', + }, + }, + }, + }; + + expect( + filterBrowserFieldsByFieldName({ + browserFields: mockBrowserFields, + substring: 'id', + }) + ).toEqual(filtered); + }); + }); + + describe('createVirtualCategory', () => { + test('it combines the specified fields into a virtual category when the input ONLY contains field names that contain dots (e.g. agent.hostname)', () => { + const expectedMatchingFields = { + fields: { + 'agent.hostname': { + aggregatable: true, + category: 'agent', + description: null, + example: null, + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'agent.hostname', + searchable: true, + type: 'string', + }, + 'client.domain': { + aggregatable: true, + category: 'client', + description: 'Client domain.', + example: null, + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'client.domain', + searchable: true, + type: 'string', + }, + 'client.geo.country_iso_code': { + aggregatable: true, + category: 'client', + description: 'Country ISO code.', + example: 'CA', + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'client.geo.country_iso_code', + searchable: true, + type: 'string', + }, + }, + }; + + const fieldIds = ['agent.hostname', 'client.domain', 'client.geo.country_iso_code']; + + expect( + createVirtualCategory({ + browserFields: mockBrowserFields, + fieldIds, + }) + ).toEqual(expectedMatchingFields); + }); + + test('it combines the specified fields into a virtual category when the input includes field names from the base category that do NOT contain dots (e.g. @timestamp)', () => { + const expectedMatchingFields = { + fields: { + 'agent.hostname': { + aggregatable: true, + category: 'agent', + description: null, + example: null, + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'agent.hostname', + searchable: true, + type: 'string', + }, + '@timestamp': { + aggregatable: true, + category: 'base', + description: + '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', + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: '@timestamp', + searchable: true, + type: 'date', + }, + 'client.domain': { + aggregatable: true, + category: 'client', + description: 'Client domain.', + example: null, + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'client.domain', + searchable: true, + type: 'string', + }, + }, + }; + + const fieldIds = ['agent.hostname', '@timestamp', 'client.domain']; + + expect( + createVirtualCategory({ + browserFields: mockBrowserFields, + fieldIds, + }) + ).toEqual(expectedMatchingFields); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/helpers.tsx b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/helpers.tsx new file mode 100644 index 0000000000000..df4b106ab6bd4 --- /dev/null +++ b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/helpers.tsx @@ -0,0 +1,143 @@ +/* + * 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 { EuiLoadingSpinner } from '@elastic/eui'; +import { filter, get, pickBy } from 'lodash/fp'; +import styled from 'styled-components'; + +import { BrowserField, BrowserFields } from '../../../common/containers/source'; +import { + DEFAULT_CATEGORY_NAME, + defaultHeaders, +} from '../timeline/body/column_headers/default_headers'; + +export const LoadingSpinner = styled(EuiLoadingSpinner)` + cursor: pointer; + position: relative; + top: 3px; +`; + +LoadingSpinner.displayName = 'LoadingSpinner'; + +export const CATEGORY_PANE_WIDTH = 200; +export const DESCRIPTION_COLUMN_WIDTH = 300; +export const FIELD_COLUMN_WIDTH = 200; +export const FIELD_BROWSER_WIDTH = 900; +export const FIELD_BROWSER_HEIGHT = 300; +export const FIELDS_PANE_WIDTH = 670; +export const HEADER_HEIGHT = 40; +export const PANES_FLEX_GROUP_WIDTH = CATEGORY_PANE_WIDTH + FIELDS_PANE_WIDTH + 10; +export const SEARCH_INPUT_WIDTH = 850; +export const TABLE_HEIGHT = 260; +export const TYPE_COLUMN_WIDTH = 50; + +/** + * Returns the CSS class name for the title of a category shown in the left + * side field browser + */ +export const getCategoryPaneCategoryClassName = ({ + categoryId, + timelineId, +}: { + categoryId: string; + timelineId: string; +}): string => `field-browser-category-pane-${categoryId}-${timelineId}`; + +/** + * Returns the CSS class name for the title of a category shown in the right + * side of field browser + */ +export const getFieldBrowserCategoryTitleClassName = ({ + categoryId, + timelineId, +}: { + categoryId: string; + timelineId: string; +}): string => `field-browser-category-title-${categoryId}-${timelineId}`; + +/** Returns the class name for a field browser search input */ +export const getFieldBrowserSearchInputClassName = (timelineId: string): string => + `field-browser-search-input-${timelineId}`; + +/** Returns true if the specified category has at least one field */ +export const categoryHasFields = (category: Partial): boolean => + category.fields != null && Object.keys(category.fields).length > 0; + +/** Returns the count of fields in the specified category */ +export const getFieldCount = (category: Partial | undefined): number => + category != null && category.fields != null ? Object.keys(category.fields).length : 0; + +/** + * Filters the specified `BrowserFields` to return a new collection where every + * category contains at least one field name that matches the specified substring. + */ +export const filterBrowserFieldsByFieldName = ({ + browserFields, + substring, +}: { + browserFields: BrowserFields; + substring: string; +}): BrowserFields => { + const trimmedSubstring = substring.trim(); + + // filter each category such that it only contains fields with field names + // that contain the specified substring: + const filteredBrowserFields: BrowserFields = Object.keys(browserFields).reduce( + (filteredCategories, categoryId) => ({ + ...filteredCategories, + [categoryId]: { + ...browserFields[categoryId], + fields: filter( + (f) => f.name != null && f.name.includes(trimmedSubstring), + browserFields[categoryId].fields + ).reduce((filtered, field) => ({ ...filtered, [field.name!]: field }), {}), + }, + }), + {} + ); + + // only pick non-empty categories from the filtered browser fields + const nonEmptyCategories: BrowserFields = pickBy( + (category) => categoryHasFields(category), + filteredBrowserFields + ); + + return nonEmptyCategories; +}; + +/** + * Returns a "virtual" category (e.g. default ECS) from the specified fieldIds + */ +export const createVirtualCategory = ({ + browserFields, + fieldIds, +}: { + browserFields: BrowserFields; + fieldIds: string[]; +}): Partial => ({ + fields: fieldIds.reduce>>>((fields, fieldId) => { + const splitId = fieldId.split('.'); // source.geo.city_name -> [source, geo, city_name] + + return { + ...fields, + [fieldId]: { + ...get([splitId.length > 1 ? splitId[0] : 'base', 'fields', fieldId], browserFields), + name: fieldId, + }, + }; + }, {}), +}); + +/** Merges the specified browser fields with the default category (i.e. `default ECS`) */ +export const mergeBrowserFieldsWithDefaultCategory = ( + browserFields: BrowserFields +): BrowserFields => ({ + ...browserFields, + [DEFAULT_CATEGORY_NAME]: createVirtualCategory({ + browserFields, + fieldIds: defaultHeaders.map((header) => header.id), + }), +}); diff --git a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/index.test.tsx new file mode 100644 index 0000000000000..24dc806838d90 --- /dev/null +++ b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/index.test.tsx @@ -0,0 +1,243 @@ +/* + * 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 { mount } from 'enzyme'; +import React from 'react'; +import { ActionCreator } from 'typescript-fsa'; + +import { mockBrowserFields } from '../../../common/containers/source/mock'; +import { TestProviders } from '../../../common/mock'; +import { ColumnHeaderOptions } from '../../../timelines/store/timeline/model'; + +import { FIELD_BROWSER_HEIGHT, FIELD_BROWSER_WIDTH } from './helpers'; + +import { StatefulFieldsBrowserComponent } from '.'; + +// Suppress warnings about "react-beautiful-dnd" until we migrate to @testing-library/react +/* eslint-disable no-console */ +const originalError = console.error; +const originalWarn = console.warn; +beforeAll(() => { + console.warn = jest.fn(); + console.error = jest.fn(); +}); +afterAll(() => { + console.error = originalError; + console.warn = originalWarn; +}); + +const removeColumnMock = (jest.fn() as unknown) as ActionCreator<{ + id: string; + columnId: string; +}>; + +const upsertColumnMock = (jest.fn() as unknown) as ActionCreator<{ + column: ColumnHeaderOptions; + id: string; + index: number; +}>; + +describe('StatefulFieldsBrowser', () => { + const timelineId = 'test'; + + test('it renders the Fields button, which displays the fields browser on click', () => { + const wrapper = mount( + + + + ); + + expect(wrapper.find('[data-test-subj="show-field-browser"]').first().text()).toEqual('Columns'); + }); + + describe('toggleShow', () => { + test('it does NOT render the fields browser until the Fields button is clicked', () => { + const wrapper = mount( + + + + ); + + expect(wrapper.find('[data-test-subj="fields-browser-container"]').exists()).toBe(false); + }); + + test('it renders the fields browser when the Fields button is clicked', () => { + const wrapper = mount( + + + + ); + + wrapper.find('[data-test-subj="show-field-browser"]').first().simulate('click'); + + expect(wrapper.find('[data-test-subj="fields-browser-container"]').exists()).toBe(true); + }); + }); + + describe('updateSelectedCategoryId', () => { + beforeEach(() => { + jest.useFakeTimers(); + }); + test('it updates the selectedCategoryId state, which makes the category bold, when the user clicks a category name in the left hand side of the field browser', () => { + const wrapper = mount( + + + + ); + + wrapper.find('[data-test-subj="show-field-browser"]').first().simulate('click'); + + wrapper.find(`.field-browser-category-pane-auditd-${timelineId}`).first().simulate('click'); + + wrapper.update(); + expect( + wrapper.find(`.field-browser-category-pane-auditd-${timelineId}`).at(1) + ).toHaveStyleRule('font-weight', 'bold', { modifier: '.euiText' }); + }); + + test('it updates the selectedCategoryId state according to most fields returned', () => { + const wrapper = mount( + + + + ); + + wrapper.find('[data-test-subj="show-field-browser"]').first().simulate('click'); + expect( + wrapper.find(`.field-browser-category-pane-cloud-${timelineId}`).at(1) + ).toHaveStyleRule('font-weight', 'normal', { modifier: '.euiText' }); + wrapper + .find('[data-test-subj="field-search"]') + .last() + .simulate('change', { target: { value: 'cloud' } }); + + jest.runOnlyPendingTimers(); + wrapper.update(); + expect( + wrapper.find(`.field-browser-category-pane-cloud-${timelineId}`).at(1) + ).toHaveStyleRule('font-weight', 'bold', { modifier: '.euiText' }); + }); + }); + + test('it renders the Fields Browser button as a settings gear when the isEventViewer prop is true', () => { + const isEventViewer = true; + + const wrapper = mount( + + + + ); + + expect(wrapper.find('[data-test-subj="show-field-browser-gear"]').first().exists()).toBe(true); + }); + + test('it does NOT render the Fields Browser button as a settings gear when the isEventViewer prop is false', () => { + const isEventViewer = false; + + const wrapper = mount( + + + + ); + + expect(wrapper.find('[data-test-subj="show-field-browser-gear"]').first().exists()).toBe(false); + }); + + test('it does NOT render the default Fields Browser button when the isEventViewer prop is true', () => { + const isEventViewer = true; + + const wrapper = mount( + + + + ); + + expect(wrapper.find('[data-test-subj="show-field-browser"]').first().exists()).toBe(false); + }); +}); diff --git a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/index.tsx new file mode 100644 index 0000000000000..bdb0fa1daf9aa --- /dev/null +++ b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/index.tsx @@ -0,0 +1,208 @@ +/* + * 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 { EuiButtonEmpty, EuiToolTip } from '@elastic/eui'; +import { noop } from 'lodash/fp'; +import React, { useEffect, useRef, useState, useCallback, useMemo } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import styled from 'styled-components'; + +import { State } from '../../../common/store'; +import { BrowserFields } from '../../../common/containers/source'; +import { ColumnHeaderOptions } from '../../../timelines/store/timeline/model'; +import { setExcludedRowRendererIds as dispatchSetExcludedRowRendererIds } from '../../../timelines/store/timeline/actions'; +import { DEFAULT_CATEGORY_NAME } from '../timeline/body/column_headers/default_headers'; +import { RowRenderersBrowser } from './row_renderers_browser'; +import { filterBrowserFieldsByFieldName, mergeBrowserFieldsWithDefaultCategory } from './helpers'; +import * as i18n from './translations'; +import { FieldBrowserProps } from './types'; + +const fieldsButtonClassName = 'fields-button'; + +/** wait this many ms after the user completes typing before applying the filter input */ +export const INPUT_TIMEOUT = 250; + +const RowRenderersBrowserButtonContainer = styled.div` + position: relative; +`; + +RowRenderersBrowserButtonContainer.displayName = 'RowRenderersBrowserButtonContainer'; + +/** + * Manages the state of the field browser + */ +export const StatefulRowRenderersBrowserComponent: React.FC = ({ + columnHeaders, + browserFields, + height, + isEventViewer = false, + onFieldSelected, + onUpdateColumns, + timelineId, + toggleColumn, + width, +}) => { + const dispatch = useDispatch(); + const excludedRowRendererIds = useSelector( + (state: State) => state.timeline.timelineById[timelineId]?.excludedRowRendererIds || [] + ); + + const setExcludedRowRendererIds = useCallback( + (payload) => + dispatch( + dispatchSetExcludedRowRendererIds({ id: timelineId, excludedRowRendererIds: payload }) + ), + [dispatch, timelineId] + ); + + /** tracks the latest timeout id from `setTimeout`*/ + const inputTimeoutId = useRef(0); + + /** all field names shown in the field browser must contain this string (when specified) */ + const [filterInput, setFilterInput] = useState(''); + /** all fields in this collection have field names that match the filterInput */ + const [filteredBrowserFields, setFilteredBrowserFields] = useState(null); + /** when true, show a spinner in the input to indicate the field browser is searching for matching field names */ + const [isSearching, setIsSearching] = useState(false); + /** this category will be displayed in the right-hand pane of the field browser */ + const [selectedCategoryId, setSelectedCategoryId] = useState(DEFAULT_CATEGORY_NAME); + /** show the field browser */ + const [show, setShow] = useState(false); + useEffect(() => { + return () => { + if (inputTimeoutId.current !== 0) { + // ⚠️ mutation: cancel any remaining timers and zero-out the timer id: + clearTimeout(inputTimeoutId.current); + inputTimeoutId.current = 0; + } + }; + }, []); + + /** Shows / hides the field browser */ + const toggleShow = useCallback(() => { + setShow(!show); + }, [show]); + + /** Invoked when the user types in the filter input */ + const updateFilter = useCallback( + (newFilterInput: string) => { + setFilterInput(newFilterInput); + setIsSearching(true); + if (inputTimeoutId.current !== 0) { + clearTimeout(inputTimeoutId.current); // ⚠️ mutation: cancel any previous timers + } + // ⚠️ mutation: schedule a new timer that will apply the filter when it fires: + inputTimeoutId.current = window.setTimeout(() => { + const newFilteredBrowserFields = filterBrowserFieldsByFieldName({ + browserFields: mergeBrowserFieldsWithDefaultCategory(browserFields), + substring: newFilterInput, + }); + setFilteredBrowserFields(newFilteredBrowserFields); + setIsSearching(false); + + const newSelectedCategoryId = + newFilterInput === '' || Object.keys(newFilteredBrowserFields).length === 0 + ? DEFAULT_CATEGORY_NAME + : Object.keys(newFilteredBrowserFields) + .sort() + .reduce( + (selected, category) => + newFilteredBrowserFields[category].fields != null && + newFilteredBrowserFields[selected].fields != null && + Object.keys(newFilteredBrowserFields[category].fields!).length > + Object.keys(newFilteredBrowserFields[selected].fields!).length + ? category + : selected, + Object.keys(newFilteredBrowserFields)[0] + ); + setSelectedCategoryId(newSelectedCategoryId); + }, INPUT_TIMEOUT); + }, + // eslint-disable-next-line react-hooks/exhaustive-deps + [browserFields, filterInput, inputTimeoutId.current] + ); + + /** + * Invoked when the user clicks a category name in the left-hand side of + * the field browser + */ + const updateSelectedCategoryId = useCallback((categoryId: string) => { + setSelectedCategoryId(categoryId); + }, []); + + /** + * Invoked when the user clicks on the context menu to view a category's + * columns in the timeline, this function dispatches the action that + * causes the timeline display those columns. + */ + const updateColumnsAndSelectCategoryId = useCallback((columns: ColumnHeaderOptions[]) => { + onUpdateColumns(columns); // show the category columns in the timeline + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + /** Invoked when the field browser should be hidden */ + const hideFieldBrowser = useCallback(() => { + setFilterInput(''); + setFilterInput(''); + setFilteredBrowserFields(null); + setIsSearching(false); + setSelectedCategoryId(DEFAULT_CATEGORY_NAME); + setShow(false); + }, []); + // only merge in the default category if the field browser is visible + const browserFieldsWithDefaultCategory = useMemo( + () => (show ? mergeBrowserFieldsWithDefaultCategory(browserFields) : {}), + [show, browserFields] + ); + + return ( + <> + + + + {i18n.FIELDS} + + + + {show && ( + + )} + + + ); +}; + +export const StatefulRowRenderersBrowser = React.memo(StatefulRowRenderersBrowserComponent); diff --git a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/row_renderers_browser.tsx b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/row_renderers_browser.tsx new file mode 100644 index 0000000000000..3a2bad6969977 --- /dev/null +++ b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/row_renderers_browser.tsx @@ -0,0 +1,303 @@ +/* + * 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 { EuiFlexGroup, EuiOutsideClickDetector, EuiInMemoryTable } from '@elastic/eui'; +import React, { useCallback, useMemo, useState, useRef } from 'react'; +import { noop, xorBy } from 'lodash/fp'; +import styled from 'styled-components'; + +import { RowRendererId } from '../../../../common/types/timeline'; +import { rowRenderers } from '../timeline/body/renderers'; +import { BrowserFields } from '../../../common/containers/source'; +import { ColumnHeaderOptions } from '../../../timelines/store/timeline/model'; + +import { Header } from './header'; +import { PANES_FLEX_GROUP_WIDTH } from './helpers'; +import { FieldBrowserProps, OnHideFieldBrowser } from './types'; + +const FieldsBrowserContainer = styled.div<{ width: number }>` + background-color: ${({ theme }) => theme.eui.euiColorLightestShade}; + border: ${({ theme }) => theme.eui.euiBorderWidthThin} solid + ${({ theme }) => theme.eui.euiColorMediumShade}; + border-radius: ${({ theme }) => theme.eui.euiBorderRadius}; + left: 0; + padding: ${({ theme }) => theme.eui.paddingSizes.s} ${({ theme }) => theme.eui.paddingSizes.s} + ${({ theme }) => theme.eui.paddingSizes.m}; + position: absolute; + top: calc(100% + ${({ theme }) => theme.eui.euiSize}); + width: ${({ width }) => width}px; + z-index: 9990; +`; +FieldsBrowserContainer.displayName = 'FieldsBrowserContainer'; + +const PanesFlexGroup = styled(EuiFlexGroup)` + width: ${PANES_FLEX_GROUP_WIDTH}px; +`; +PanesFlexGroup.displayName = 'PanesFlexGroup'; + +interface RowRendererOption { + id: RowRendererId; + name: string; + description: string; + example: React.ReactNode; +} + +type Props = Pick< + FieldBrowserProps, + | 'browserFields' + | 'isEventViewer' + | 'height' + | 'onFieldSelected' + | 'onUpdateColumns' + | 'timelineId' + | 'width' +> & { + /** + * The current timeline column headers + */ + columnHeaders: ColumnHeaderOptions[]; + excludedRowRendererIds: RowRendererId[]; + /** + * A map of categoryId -> metadata about the fields in that category, + * filtered such that the name of every field in the category includes + * the filter input (as a substring). + */ + filteredBrowserFields: BrowserFields; + /** + * When true, a busy spinner will be shown to indicate the field browser + * is searching for fields that match the specified `searchInput` + */ + isSearching: boolean; + /** The text displayed in the search input */ + searchInput: string; + /** + * The category selected on the left-hand side of the field browser + */ + selectedCategoryId: string; + /** + * Invoked when the user clicks on the name of a category in the left-hand + * side of the field browser + */ + onCategorySelected: (categoryId: string) => void; + /** + * Hides the field browser when invoked + */ + onHideFieldBrowser: OnHideFieldBrowser; + /** + * Invoked when the user clicks outside of the field browser + */ + onOutsideClick: () => void; + /** + * Invoked when the user types in the search input + */ + onSearchInputChange: (newSearchInput: string) => void; + /** + * Invoked to add or remove a column from the timeline + */ + toggleColumn: (column: ColumnHeaderOptions) => void; + setExcludedRowRendererIds: (excludedRowRendererIds: RowRendererId[]) => void; +}; + +/** + * This component has no internal state, but it uses lifecycle methods to + * set focus to the search input, scroll to the selected category, etc + */ +const FieldsBrowserComponent: React.FC = ({ + excludedRowRendererIds = [], + filteredBrowserFields, + isEventViewer, + setExcludedRowRendererIds, + onFieldSelected, + onOutsideClick, + timelineId, + width, +}) => { + const columns = [ + { + field: 'name', + name: 'Name', + sortable: true, + truncateText: true, + }, + { + field: 'description', + name: 'Description', + truncateText: true, + }, + { + field: 'category', + name: 'Category', + truncateText: true, + }, + { + field: 'example', + name: 'Example', + render: () =>
EXAMPLE
, + }, + ]; + + const search = { + box: { + incremental: true, + schema: true, + }, + // filters: !filters + // ? undefined + // : [ + // { + // type: 'is', + // field: 'online', + // name: 'Online', + // negatedName: 'Offline', + // }, + // { + // type: 'field_value_selection', + // field: 'nationality', + // name: 'Nationality', + // multiSelect: false, + // options: [], + // }, + // ], + }; + + const renderers: RowRendererOption[] = [ + { + id: 'auditd', + name: 'Auditd', + description: 'Auditd Row Renderer', + example: () => <>, + }, + { + id: 'auditd_file', + name: 'Auditd File', + description: 'Auditd Row Renderer', + example: () => <>, + }, + { + id: 'system', + name: 'System', + description: 'System Row Renderer', + example: () => <>, + }, + + { + id: 'system_endgame_process', + name: 'System Endgame Process', + description: 'Endgame Process Row Renderer', + example: () => <>, + }, + + { + id: 'system_fin', + name: 'System FIM', + description: 'FIM Row Renderer', + example: () => <>, + }, + + { + id: 'system_file', + name: 'System File', + description: 'System File Row Renderer', + example: () => <>, + }, + + { + id: 'system_socket', + name: 'System Socket', + description: 'Auditd Row Renderer', + example: () => <>, + }, + + { + id: 'system_security_event', + name: 'System Security Event', + description: 'Auditd Row Renderer', + example: () => <>, + }, + + { + id: 'system_dns', + name: 'System DNS', + description: 'Auditd Row Renderer', + example: () => <>, + }, + { + id: 'suricata', + name: 'Suricata', + description: 'Auditd Row Renderer', + example: () => <>, + }, + { + id: 'zeek', + name: 'Zeek', + description: 'Auditd Row Renderer', + example: () => <>, + }, + { + id: 'netflow', + name: 'Netflow', + description: 'Auditd Row Renderer', + example: () => <>, + }, + ]; + + const notExcludedRowRenderers = useMemo(() => { + if (excludedRowRendererIds.includes('all')) return []; + + return renderers; + // return renderers.filter((renderer) => excludedRowRendererIds.includes(renderer.id)); + }, [excludedRowRendererIds, renderers]); + + const selectionValue = { + selectable: () => true, + selectableMessage: () => '', + onSelectionChange: (selection) => { + console.error('selection', selection); + + if (!selection || !selection.length) return setExcludedRowRendererIds(['all']); + + const excludedRowRenderers = xorBy('id', renderers, selection); + + setExcludedRowRendererIds(excludedRowRenderers.map((rowRenderer) => rowRenderer.id)); + }, + initialSelected: notExcludedRowRenderers, + }; + + console.error('rowRenderers', rowRenderers); + + const tableRef = useRef(); + + return ( + + +
+ + + + + ); +}; + +export const RowRenderersBrowser = React.memo(FieldsBrowserComponent); diff --git a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/translations.ts b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/translations.ts new file mode 100644 index 0000000000000..0d078d99075c7 --- /dev/null +++ b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/translations.ts @@ -0,0 +1,88 @@ +/* + * 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 { i18n } from '@kbn/i18n'; + +export const CATEGORY = i18n.translate('xpack.securitySolution.fieldBrowser.categoryLabel', { + defaultMessage: 'Category', +}); + +export const CATEGORIES = i18n.translate('xpack.securitySolution.fieldBrowser.categoriesTitle', { + defaultMessage: 'Categories', +}); + +export const COPY_TO_CLIPBOARD = i18n.translate( + 'xpack.securitySolution.fieldBrowser.copyToClipboard', + { + defaultMessage: 'Copy to Clipboard', + } +); + +export const CUSTOMIZE_COLUMNS = i18n.translate( + 'xpack.securitySolution.fieldBrowser.customizeColumnsTitle', + { + defaultMessage: 'Customize Columns', + } +); + +export const DESCRIPTION = i18n.translate('xpack.securitySolution.fieldBrowser.descriptionLabel', { + defaultMessage: 'Description', +}); + +export const FIELD = i18n.translate('xpack.securitySolution.fieldBrowser.fieldLabel', { + defaultMessage: 'Field', +}); + +export const FIELDS = i18n.translate('xpack.securitySolution.fieldBrowser.fieldsTitle', { + defaultMessage: 'Columns', +}); + +export const FIELDS_COUNT = (totalCount: number) => + i18n.translate('xpack.securitySolution.fieldBrowser.fieldsCountTitle', { + values: { totalCount }, + defaultMessage: '{totalCount} {totalCount, plural, =1 {field} other {fields}}', + }); + +export const FILTER_PLACEHOLDER = i18n.translate( + 'xpack.securitySolution.fieldBrowser.filterPlaceholder', + { + defaultMessage: 'Field name', + } +); + +export const NO_FIELDS_MATCH = i18n.translate( + 'xpack.securitySolution.fieldBrowser.noFieldsMatchLabel', + { + defaultMessage: 'No fields match', + } +); + +export const NO_FIELDS_MATCH_INPUT = (searchInput: string) => + i18n.translate('xpack.securitySolution.fieldBrowser.noFieldsMatchInputLabel', { + defaultMessage: 'No fields match {searchInput}', + values: { + searchInput, + }, + }); + +export const RESET_FIELDS = i18n.translate('xpack.securitySolution.fieldBrowser.resetFieldsLink', { + defaultMessage: 'Reset Fields', +}); + +export const TOGGLE_COLUMN_TOOLTIP = i18n.translate( + 'xpack.securitySolution.fieldBrowser.toggleColumnTooltip', + { + defaultMessage: 'Toggle column', + } +); + +export const VIEW_CATEGORY = (categoryId: string) => + i18n.translate('xpack.securitySolution.fieldBrowser.viewCategoryTooltip', { + defaultMessage: 'View all {categoryId} fields', + values: { + categoryId, + }, + }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/types.ts b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/types.ts new file mode 100644 index 0000000000000..2b9889ec13e79 --- /dev/null +++ b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/types.ts @@ -0,0 +1,37 @@ +/* + * 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 { BrowserFields } from '../../../common/containers/source'; +import { ColumnHeaderOptions } from '../../../timelines/store/timeline/model'; +import { OnUpdateColumns } from '../timeline/events'; + +export type OnFieldSelected = (fieldId: string) => void; +export type OnHideFieldBrowser = () => void; + +export interface FieldBrowserProps { + /** The timeline's current column headers */ + columnHeaders: ColumnHeaderOptions[]; + /** A map of categoryId -> metadata about the fields in that category */ + browserFields: BrowserFields; + /** The height of the field browser */ + height: number; + /** When true, this Fields Browser is being used as an "events viewer" */ + isEventViewer?: boolean; + /** + * Overrides the default behavior of the `FieldBrowser` to enable + * "selection" mode, where a field is selected by clicking a button + * instead of dragging it to the timeline + */ + onFieldSelected?: OnFieldSelected; + /** Invoked when a user chooses to view a new set of columns in the timeline */ + onUpdateColumns: OnUpdateColumns; + /** The timeline associated with this field browser */ + timelineId: string; + /** Adds or removes a column to / from the timeline */ + toggleColumn: (column: ColumnHeaderOptions) => void; + /** The width of the field browser */ + width: number; +} diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/index.test.tsx index 6a7734ce3161d..8ea0c6e734809 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/index.test.tsx @@ -10,7 +10,6 @@ import React from 'react'; import { DEFAULT_ACTIONS_COLUMN_WIDTH } from '../constants'; import { defaultHeaders } from './default_headers'; import { Direction } from '../../../../../graphql/types'; -import { mockBrowserFields } from '../../../../../common/containers/source/mock'; import { Sort } from '../sort'; import { TestProviders } from '../../../../../common/mock/test_providers'; import { useMountAppended } from '../../../../../common/utils/use_mount_appended'; @@ -30,19 +29,16 @@ describe('ColumnHeaders', () => { const wrapper = shallow( ); expect(wrapper).toMatchSnapshot(); @@ -53,19 +49,16 @@ describe('ColumnHeaders', () => { ); @@ -78,19 +71,16 @@ describe('ColumnHeaders', () => { ); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/index.tsx index 2bb78c0dcb0ad..7e36d019c2f69 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/index.tsx @@ -12,21 +12,17 @@ import deepEqual from 'fast-deep-equal'; import { DragEffects } from '../../../../../common/components/drag_and_drop/draggable_wrapper'; import { DraggableFieldBadge } from '../../../../../common/components/draggables/field_badge'; -import { BrowserFields } from '../../../../../common/containers/source'; import { ColumnHeaderOptions } from '../../../../../timelines/store/timeline/model'; import { DRAG_TYPE_FIELD, droppableTimelineColumnsPrefix, } from '../../../../../common/components/drag_and_drop/helpers'; -import { StatefulFieldsBrowser } from '../../../fields_browser'; -import { FIELD_BROWSER_HEIGHT, FIELD_BROWSER_WIDTH } from '../../../fields_browser/helpers'; import { OnColumnRemoved, OnColumnResized, OnColumnSorted, OnFilterChange, OnSelectAll, - OnUpdateColumns, } from '../../events'; import { EventsTh, @@ -42,7 +38,6 @@ import { ColumnHeader } from './column_header'; interface Props { actionsColumnWidth: number; - browserFields: BrowserFields; columnHeaders: ColumnHeaderOptions[]; isEventViewer?: boolean; isSelectAllChecked: boolean; @@ -51,12 +46,10 @@ interface Props { onColumnSorted: OnColumnSorted; onFilterChange?: OnFilterChange; onSelectAll: OnSelectAll; - onUpdateColumns: OnUpdateColumns; showEventsSelect: boolean; showSelectAllCheckbox: boolean; sort: Sort; timelineId: string; - toggleColumn: (column: ColumnHeaderOptions) => void; } interface DraggableContainerProps { @@ -82,7 +75,6 @@ DraggableContainer.displayName = 'DraggableContainer'; /** Renders the timeline header columns */ export const ColumnHeadersComponent = ({ actionsColumnWidth, - browserFields, columnHeaders, isEventViewer = false, isSelectAllChecked, @@ -90,13 +82,11 @@ export const ColumnHeadersComponent = ({ onColumnResized, onColumnSorted, onSelectAll, - onUpdateColumns, onFilterChange = noop, showEventsSelect, showSelectAllCheckbox, sort, timelineId, - toggleColumn, }: Props) => { const [draggingIndex, setDraggingIndex] = useState(null); @@ -172,19 +162,7 @@ export const ColumnHeadersComponent = ({ data-test-subj="actions-container" > - - - + {showEventsSelect && ( @@ -242,13 +220,10 @@ export const ColumnHeaders = React.memo( prevProps.onColumnResized === nextProps.onColumnResized && prevProps.onColumnSorted === nextProps.onColumnSorted && prevProps.onSelectAll === nextProps.onSelectAll && - prevProps.onUpdateColumns === nextProps.onUpdateColumns && prevProps.onFilterChange === nextProps.onFilterChange && prevProps.showEventsSelect === nextProps.showEventsSelect && prevProps.showSelectAllCheckbox === nextProps.showSelectAllCheckbox && prevProps.sort === nextProps.sort && prevProps.timelineId === nextProps.timelineId && - prevProps.toggleColumn === nextProps.toggleColumn && - deepEqual(prevProps.columnHeaders, nextProps.columnHeaders) && - deepEqual(prevProps.browserFields, nextProps.browserFields) + deepEqual(prevProps.columnHeaders, nextProps.columnHeaders) ); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.tsx index da8835d5903e1..aab4a4c3eb013 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.tsx @@ -22,6 +22,10 @@ import { OnUnPinEvent, OnUpdateColumns, } from '../events'; +import { StatefulFieldsBrowser } from '../../fields_browser'; +import { FIELD_BROWSER_HEIGHT, FIELD_BROWSER_WIDTH } from '../../fields_browser/helpers'; +import { StatefulRowRenderersBrowser } from '../../row_renderers_browser'; +// import { FIELD_BROWSER_HEIGHT, FIELD_BROWSER_WIDTH } from '../../row_renderers_browser/helpers'; import { EventsTable, TimelineBody, TimelineBodyGlobalStyle } from '../styles'; import { ColumnHeaders } from './column_headers'; import { getActionsColumnWidth } from './column_headers/helpers'; @@ -127,11 +131,34 @@ export const Body = React.memo( return ( <> +
+ + +
( onColumnSorted={onColumnSorted} onFilterChange={onFilterChange} onSelectAll={onSelectAll} - onUpdateColumns={onUpdateColumns} showEventsSelect={false} showSelectAllCheckbox={showCheckboxes} sort={sort} timelineId={id} - toggleColumn={toggleColumn} /> ({ + id: 'auditd', isInstance: (ecs) => { const module: string | null | undefined = get('event.module[0]', ecs); const action: string | null | undefined = get('event.action[0]', ecs); @@ -54,6 +55,7 @@ export const createGenericFileRowRenderer = ({ text: string; fileIcon?: IconType; }): RowRenderer => ({ + id: 'auditd_file', isInstance: (ecs) => { const module: string | null | undefined = get('event.module[0]', ecs); const action: string | null | undefined = get('event.action[0]', ecs); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/netflow/netflow_row_renderer.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/netflow/netflow_row_renderer.tsx index 91499fd9c30f5..9ab7bc83348c9 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/netflow/netflow_row_renderer.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/netflow/netflow_row_renderer.tsx @@ -84,6 +84,7 @@ export const eventActionMatches = (eventAction: string | object | undefined | nu }; export const netflowRowRenderer: RowRenderer = { + id: 'netflow', isInstance: (ecs) => eventCategoryMatches(get(EVENT_CATEGORY_FIELD, ecs)) || eventActionMatches(get(EVENT_ACTION_FIELD, ecs)), diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/plain_row_renderer.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/plain_row_renderer.tsx index e63f60226c707..e424fe193bf51 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/plain_row_renderer.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/plain_row_renderer.tsx @@ -4,13 +4,16 @@ * you may not use this file except in compliance with the Elastic License. */ -/* eslint-disable react/display-name */ - import React from 'react'; import { RowRenderer } from './row_renderer'; +const PlainRowRenderer = () => <>; + +PlainRowRenderer.displayName = 'PlainRowRenderer'; + export const plainRowRenderer: RowRenderer = { + id: 'plain', isInstance: (_) => true, - renderRow: () => <>, + renderRow: PlainRowRenderer, }; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/row_renderer.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/row_renderer.tsx index 5cee0a0118dd2..609e9dba1a46e 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/row_renderer.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/row_renderer.tsx @@ -7,6 +7,7 @@ import React from 'react'; import { BrowserFields } from '../../../../../common/containers/source'; +import type { RowRendererId } from '../../../../../../common/types/timeline'; import { Ecs } from '../../../../../graphql/types'; import { EventsTrSupplement } from '../../styles'; @@ -22,6 +23,7 @@ export const RowRendererContainer = React.memo(({ chi RowRendererContainer.displayName = 'RowRendererContainer'; export interface RowRenderer { + id: RowRendererId; isInstance: (data: Ecs) => boolean; renderRow: ({ browserFields, diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/suricata/suricata_row_renderer.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/suricata/suricata_row_renderer.tsx index 5012f321188d6..fe6c03fe8f5c0 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/suricata/suricata_row_renderer.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/suricata/suricata_row_renderer.tsx @@ -13,6 +13,7 @@ import { RowRenderer, RowRendererContainer } from '../row_renderer'; import { SuricataDetails } from './suricata_details'; export const suricataRowRenderer: RowRenderer = { + id: 'suricata', isInstance: (ecs) => { const module: string | null | undefined = get('event.module[0]', ecs); return module != null && module.toLowerCase() === 'suricata'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/system/generic_row_renderer.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/system/generic_row_renderer.tsx index e31fc26e4ae52..c677d32cb17a4 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/system/generic_row_renderer.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/system/generic_row_renderer.tsx @@ -25,6 +25,7 @@ export const createGenericSystemRowRenderer = ({ actionName: string; text: string; }): RowRenderer => ({ + id: 'system', isInstance: (ecs) => { const module: string | null | undefined = get('event.module[0]', ecs); const action: string | null | undefined = get('event.action[0]', ecs); @@ -55,6 +56,7 @@ export const createEndgameProcessRowRenderer = ({ actionName: string; text: string; }): RowRenderer => ({ + id: 'system_file', isInstance: (ecs) => { const action: string | null | undefined = get('event.action[0]', ecs); const category: string | null | undefined = get('event.category[0]', ecs); @@ -86,6 +88,7 @@ export const createFimRowRenderer = ({ actionName: string; text: string; }): RowRenderer => ({ + id: 'system_fin', isInstance: (ecs) => { const action: string | null | undefined = get('event.action[0]', ecs); const category: string | null | undefined = get('event.category[0]', ecs); @@ -117,6 +120,7 @@ export const createGenericFileRowRenderer = ({ actionName: string; text: string; }): RowRenderer => ({ + id: 'system_file', isInstance: (ecs) => { const module: string | null | undefined = get('event.module[0]', ecs); const action: string | null | undefined = get('event.action[0]', ecs); @@ -147,6 +151,7 @@ export const createSocketRowRenderer = ({ actionName: string; text: string; }): RowRenderer => ({ + id: 'system_socket', isInstance: (ecs) => { const action: string | null | undefined = get('event.action[0]', ecs); return action != null && action.toLowerCase() === actionName; @@ -169,6 +174,7 @@ export const createSecurityEventRowRenderer = ({ }: { actionName: string; }): RowRenderer => ({ + id: 'system_security_event', isInstance: (ecs) => { const category: string | null | undefined = get('event.category[0]', ecs); const action: string | null | undefined = get('event.action[0]', ecs); @@ -192,6 +198,7 @@ export const createSecurityEventRowRenderer = ({ }); export const createDnsRowRenderer = (): RowRenderer => ({ + id: 'system_dns', isInstance: (ecs) => { const dnsQuestionType: string | null | undefined = get('dns.question.type[0]', ecs); const dnsQuestionName: string | null | undefined = get('dns.question.name[0]', ecs); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/zeek/zeek_row_renderer.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/zeek/zeek_row_renderer.tsx index 25228b04bb50b..037ea9556a50e 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/zeek/zeek_row_renderer.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/zeek/zeek_row_renderer.tsx @@ -13,6 +13,7 @@ import { RowRenderer, RowRendererContainer } from '../row_renderer'; import { ZeekDetails } from './zeek_details'; export const zeekRowRenderer: RowRenderer = { + id: 'zeek', isInstance: (ecs) => { const module: string | null | undefined = get('event.module[0]', ecs); return module != null && module.toLowerCase() === 'zeek'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/stateful_body.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/stateful_body.tsx index 2d5e64fb09ffc..7b2dce0566697 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/stateful_body.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/stateful_body.tsx @@ -59,6 +59,7 @@ const StatefulBodyComponent = React.memo( columnHeaders, data, eventIdToNoteIds, + excludedRowRendererIds, height, id, isEventViewer = false, @@ -94,8 +95,7 @@ const StatefulBodyComponent = React.memo( const onAddNoteToEvent: AddNoteToEvent = useCallback( ({ eventId, noteId }: { eventId: string; noteId: string }) => addNoteToEvent!({ id, eventId, noteId }), - // eslint-disable-next-line react-hooks/exhaustive-deps - [id] + [id, addNoteToEvent] ); const onRowSelected: OnRowSelected = useCallback( @@ -132,35 +132,36 @@ const StatefulBodyComponent = React.memo( (sorted) => { updateSort!({ id, sort: sorted }); }, - // eslint-disable-next-line react-hooks/exhaustive-deps - [id] + [id, updateSort] ); const onColumnRemoved: OnColumnRemoved = useCallback( (columnId) => removeColumn!({ id, columnId }), - // eslint-disable-next-line react-hooks/exhaustive-deps - [id] + [id, removeColumn] ); const onColumnResized: OnColumnResized = useCallback( ({ columnId, delta }) => applyDeltaToColumnWidth!({ id, columnId, delta }), - // eslint-disable-next-line react-hooks/exhaustive-deps - [id] + [applyDeltaToColumnWidth, id] ); - // eslint-disable-next-line react-hooks/exhaustive-deps - const onPinEvent: OnPinEvent = useCallback((eventId) => pinEvent!({ id, eventId }), [id]); + const onPinEvent: OnPinEvent = useCallback((eventId) => pinEvent!({ id, eventId }), [ + id, + pinEvent, + ]); - // eslint-disable-next-line react-hooks/exhaustive-deps - const onUnPinEvent: OnUnPinEvent = useCallback((eventId) => unPinEvent!({ id, eventId }), [id]); + const onUnPinEvent: OnUnPinEvent = useCallback((eventId) => unPinEvent!({ id, eventId }), [ + id, + unPinEvent, + ]); - // eslint-disable-next-line react-hooks/exhaustive-deps - const onUpdateNote: UpdateNote = useCallback((note: Note) => updateNote!({ note }), []); + const onUpdateNote: UpdateNote = useCallback((note: Note) => updateNote!({ note }), [ + updateNote, + ]); const onUpdateColumns: OnUpdateColumns = useCallback( (columns) => updateColumns!({ id, columns }), - // eslint-disable-next-line react-hooks/exhaustive-deps - [id] + [id, updateColumns] ); // Sync to selectAll so parent components can select all events @@ -168,8 +169,22 @@ const StatefulBodyComponent = React.memo( if (selectAll) { onSelectAll({ isSelected: true }); } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [selectAll]); // onSelectAll dependency not necessary + }, [onSelectAll, selectAll]); + + const enabledRowRenderers = useMemo(() => { + if (!showRowRenderers || (excludedRowRendererIds && excludedRowRendererIds[0] === 'all')) + return [plainRowRenderer]; + + if (!excludedRowRendererIds) return rowRenderers; + + console.error('dupa', rowRenderers, excludedRowRendererIds); + console.error( + 'enabled', + rowRenderers.filter((rowRenderer) => !excludedRowRendererIds.includes(rowRenderer.id)) + ); + + return rowRenderers.filter((rowRenderer) => !excludedRowRendererIds.includes(rowRenderer.id)); + }, [excludedRowRendererIds, showRowRenderers]); return ( ( onUnPinEvent={onUnPinEvent} onUpdateColumns={onUpdateColumns} pinnedEventIds={pinnedEventIds} - rowRenderers={showRowRenderers ? rowRenderers : [plainRowRenderer]} + rowRenderers={enabledRowRenderers} selectedEventIds={selectedEventIds} showCheckboxes={showCheckboxes} sort={sort} @@ -208,6 +223,7 @@ const StatefulBodyComponent = React.memo( deepEqual(prevProps.browserFields, nextProps.browserFields) && deepEqual(prevProps.columnHeaders, nextProps.columnHeaders) && deepEqual(prevProps.data, nextProps.data) && + deepEqual(prevProps.excludedRowRendererIds, nextProps.excludedRowRendererIds) && prevProps.eventIdToNoteIds === nextProps.eventIdToNoteIds && deepEqual(prevProps.notesById, nextProps.notesById) && prevProps.height === nextProps.height && @@ -238,6 +254,7 @@ const makeMapStateToProps = () => { columns, eventIdToNoteIds, eventType, + excludedRowRendererIds, isSelectAllChecked, loadingEventIds, pinnedEventIds, @@ -250,6 +267,7 @@ const makeMapStateToProps = () => { columnHeaders: memoizedColumnHeaders(columns, browserFields), eventIdToNoteIds, eventType, + excludedRowRendererIds, isSelectAllChecked, loadingEventIds, notesById: getNotesByIds(state), diff --git a/x-pack/plugins/security_solution/public/timelines/containers/all/index.gql_query.ts b/x-pack/plugins/security_solution/public/timelines/containers/all/index.gql_query.ts index 60d000fe78184..dbc5e74b5d7a6 100644 --- a/x-pack/plugins/security_solution/public/timelines/containers/all/index.gql_query.ts +++ b/x-pack/plugins/security_solution/public/timelines/containers/all/index.gql_query.ts @@ -42,6 +42,7 @@ export const allTimelinesQuery = gql` updatedBy version } + excludedRowRendererIds notes { eventId note diff --git a/x-pack/plugins/security_solution/public/timelines/containers/all/index.tsx b/x-pack/plugins/security_solution/public/timelines/containers/all/index.tsx index f025cf15181c3..e1465550a4fb6 100644 --- a/x-pack/plugins/security_solution/public/timelines/containers/all/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/containers/all/index.tsx @@ -62,6 +62,7 @@ export const getAllTimeline = memoizeOne( return acc; }, {}) : null, + excludedRowRendererIds: timeline.excludedRowRendererIds, favorite: timeline.favorite, noteIds: timeline.noteIds, notes: diff --git a/x-pack/plugins/security_solution/public/timelines/containers/one/index.gql_query.ts b/x-pack/plugins/security_solution/public/timelines/containers/one/index.gql_query.ts index 47e80b005fb99..8ae1e8cf37956 100644 --- a/x-pack/plugins/security_solution/public/timelines/containers/one/index.gql_query.ts +++ b/x-pack/plugins/security_solution/public/timelines/containers/one/index.gql_query.ts @@ -68,6 +68,7 @@ export const oneTimelineQuery = gql` updatedBy version } + excludedRowRendererIds favorite { fullName userName diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/actions.ts b/x-pack/plugins/security_solution/public/timelines/store/timeline/actions.ts index c5df017604b0c..1a63fa36a5251 100644 --- a/x-pack/plugins/security_solution/public/timelines/store/timeline/actions.ts +++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/actions.ts @@ -16,7 +16,7 @@ import { KueryFilterQuery, SerializedFilterQuery } from '../../../common/store/t import { EventType, KqlMode, TimelineModel, ColumnHeaderOptions } from './model'; import { TimelineNonEcsData } from '../../../graphql/types'; -import { TimelineTypeLiteral } from '../../../../common/types/timeline'; +import { TimelineTypeLiteral, RowRendererId } from '../../../../common/types/timeline'; import { InsertTimeline } from './types'; const actionCreator = actionCreatorFactory('x-pack/security_solution/local/timeline'); @@ -252,3 +252,8 @@ export const clearEventsDeleted = actionCreator<{ export const updateEventType = actionCreator<{ id: string; eventType: EventType }>( 'UPDATE_EVENT_TYPE' ); + +export const setExcludedRowRendererIds = actionCreator<{ + id: string; + excludedRowRendererIds: RowRendererId[]; +}>('SET_TIMELINE_EXCLUDED_ROW_RENDERER_IDS'); diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/defaults.ts b/x-pack/plugins/security_solution/public/timelines/store/timeline/defaults.ts index 5290178092f3e..8db54bc289a69 100644 --- a/x-pack/plugins/security_solution/public/timelines/store/timeline/defaults.ts +++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/defaults.ts @@ -18,6 +18,7 @@ export const timelineDefaults: SubsetTimelineModel & Pick { description: '', eventIdToNoteIds: {}, eventType: 'all', + excludedRowRendererIds: [], highlightedDropAndProviderId: '', historyIds: [], filters: [ @@ -233,6 +234,7 @@ describe('Epic Timeline', () => { }, description: '', eventType: 'all', + excludedRowRendererIds: [], filters: [ { exists: null, diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/helpers.ts b/x-pack/plugins/security_solution/public/timelines/store/timeline/helpers.ts index 15f956fa79d3c..288aa8fed16aa 100644 --- a/x-pack/plugins/security_solution/public/timelines/store/timeline/helpers.ts +++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/helpers.ts @@ -19,7 +19,7 @@ import { } from '../../../timelines/components/timeline/data_providers/data_provider'; import { KueryFilterQuery, SerializedFilterQuery } from '../../../common/store/model'; import { TimelineNonEcsData } from '../../../graphql/types'; -import { TimelineTypeLiteral } from '../../../../common/types/timeline'; +import { TimelineTypeLiteral, RowRendererId } from '../../../../common/types/timeline'; import { timelineDefaults } from './defaults'; import { ColumnHeaderOptions, KqlMode, TimelineModel, EventType } from './model'; @@ -1318,3 +1318,26 @@ export const updateFilters = ({ id, filters, timelineById }: UpdateFiltersParams }, }; }; + +interface UpdateExcludedRowRenderersIds { + id: string; + excludedRowRendererIds: RowRendererId[]; + timelineById: TimelineById; +} + +export const updateExcludedRowRenderersIds = ({ + id, + excludedRowRendererIds, + timelineById, +}: UpdateExcludedRowRenderersIds): TimelineById => { + const timeline = timelineById[id]; + + return { + ...timelineById, + [id]: { + ...timeline, + excludedRowRendererIds, + showRowRenderers: excludedRowRendererIds[0] !== 'all', + }, + }; +}; diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/model.ts b/x-pack/plugins/security_solution/public/timelines/store/timeline/model.ts index caad70226365a..375808e9c4017 100644 --- a/x-pack/plugins/security_solution/public/timelines/store/timeline/model.ts +++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/model.ts @@ -15,6 +15,7 @@ import { TimelineStatus, } from '../../../graphql/types'; import { KueryFilterQuery, SerializedFilterQuery } from '../../../common/store/types'; +import type { RowRendererId } from '../../../../common/types/timeline'; export const DEFAULT_PAGE_COUNT = 2; // Eui Pager will not render unless this is a minimum of 2 pages export type KqlMode = 'filter' | 'search'; @@ -54,6 +55,8 @@ export interface TimelineModel { eventType?: EventType; /** A map of events in this timeline to the chronologically ordered notes (in this timeline) associated with the event */ eventIdToNoteIds: Record; + /** A list of Ids of excluded Row Renderers */ + excludedRowRendererIds: RowRendererId[]; filters?: Filter[]; /** The chronological history of actions related to this timeline */ historyIds: string[]; @@ -129,6 +132,7 @@ export type SubsetTimelineModel = Readonly< | 'description' | 'eventType' | 'eventIdToNoteIds' + | 'excludedRowRendererIds' | 'highlightedDropAndProviderId' | 'historyIds' | 'isFavorite' diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/reducer.test.ts b/x-pack/plugins/security_solution/public/timelines/store/timeline/reducer.test.ts index 3bdb16be79939..839fe6d334175 100644 --- a/x-pack/plugins/security_solution/public/timelines/store/timeline/reducer.test.ts +++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/reducer.test.ts @@ -68,6 +68,7 @@ const timelineByIdMock: TimelineById = { description: '', deletedEventIds: [], eventIdToNoteIds: {}, + excludedRowRendererIds: [], highlightedDropAndProviderId: '', historyIds: [], id: 'foo', @@ -1109,6 +1110,7 @@ describe('Timeline', () => { deletedEventIds: [], description: '', eventIdToNoteIds: {}, + excludedRowRendererIds: [], highlightedDropAndProviderId: '', historyIds: [], isFavorite: false, @@ -1205,6 +1207,7 @@ describe('Timeline', () => { description: '', deletedEventIds: [], eventIdToNoteIds: {}, + excludedRowRendererIds: [], highlightedDropAndProviderId: '', historyIds: [], isFavorite: false, @@ -1411,6 +1414,7 @@ describe('Timeline', () => { description: '', deletedEventIds: [], eventIdToNoteIds: {}, + excludedRowRendererIds: [], highlightedDropAndProviderId: '', historyIds: [], isFavorite: false, @@ -1507,6 +1511,7 @@ describe('Timeline', () => { description: '', deletedEventIds: [], eventIdToNoteIds: {}, + excludedRowRendererIds: [], highlightedDropAndProviderId: '', historyIds: [], isFavorite: false, @@ -1702,6 +1707,7 @@ describe('Timeline', () => { description: '', deletedEventIds: [], eventIdToNoteIds: {}, + excludedRowRendererIds: [], highlightedDropAndProviderId: '', historyIds: [], isFavorite: false, @@ -1780,6 +1786,7 @@ describe('Timeline', () => { description: '', deletedEventIds: [], eventIdToNoteIds: {}, + excludedRowRendererIds: [], highlightedDropAndProviderId: '', historyIds: [], isFavorite: false, @@ -1884,6 +1891,7 @@ describe('Timeline', () => { description: '', deletedEventIds: [], eventIdToNoteIds: {}, + excludedRowRendererIds: [], highlightedDropAndProviderId: '', historyIds: [], id: 'foo', diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/reducer.ts b/x-pack/plugins/security_solution/public/timelines/store/timeline/reducer.ts index 5e314f1597451..e4daecc12b767 100644 --- a/x-pack/plugins/security_solution/public/timelines/store/timeline/reducer.ts +++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/reducer.ts @@ -25,6 +25,7 @@ import { removeProvider, setEventsDeleted, setEventsLoading, + setExcludedRowRendererIds, setFilters, setInsertTimeline, setKqlFilterQueryDraft, @@ -73,6 +74,7 @@ import { setLoadingTimelineEvents, setSelectedTimelineEvents, unPinTimelineEvent, + updateExcludedRowRenderersIds, updateHighlightedDropAndProvider, updateKqlFilterQueryDraft, updateTimelineColumns, @@ -296,6 +298,14 @@ export const timelineReducer = reducerWithInitialState(initialTimelineState) }, }, })) + .case(setExcludedRowRendererIds, (state, { id, excludedRowRendererIds }) => ({ + ...state, + timelineById: updateExcludedRowRenderersIds({ + id, + excludedRowRendererIds, + timelineById: state.timelineById, + }), + })) .case(setSelected, (state, { id, eventIds, isSelected, isSelectAllChecked }) => ({ ...state, timelineById: setSelectedTimelineEvents({ diff --git a/x-pack/plugins/security_solution/server/graphql/timeline/schema.gql.ts b/x-pack/plugins/security_solution/server/graphql/timeline/schema.gql.ts index b9aa8534ab0e9..29e706175be23 100644 --- a/x-pack/plugins/security_solution/server/graphql/timeline/schema.gql.ts +++ b/x-pack/plugins/security_solution/server/graphql/timeline/schema.gql.ts @@ -135,6 +135,22 @@ export const timelineSchema = gql` draft } + enum RowRendererId { + all + auditd + auditd_file + netflow + suricata + system + system_dns + system_endgame_process + system_file + system_fin + system_security_event + system_socket + zeek + } + input TimelineInput { columns: [ColumnHeaderInput!] dataProviders: [DataProviderInput!] @@ -239,6 +255,7 @@ export const timelineSchema = gql` description: String eventIdToNoteIds: [NoteResult!] eventType: String + excludedRowRendererIds: [RowRendererId!] favorite: [FavoriteTimelineResult!] filters: [FilterTimelineResult!] kqlMode: String diff --git a/x-pack/plugins/security_solution/server/graphql/types.ts b/x-pack/plugins/security_solution/server/graphql/types.ts index 4a063647a183d..9b917f15f2703 100644 --- a/x-pack/plugins/security_solution/server/graphql/types.ts +++ b/x-pack/plugins/security_solution/server/graphql/types.ts @@ -344,6 +344,22 @@ export enum TlsFields { _id = '_id', } +export enum RowRendererId { + all = 'all', + auditd = 'auditd', + auditd_file = 'auditd_file', + netflow = 'netflow', + suricata = 'suricata', + system = 'system', + system_dns = 'system_dns', + system_endgame_process = 'system_endgame_process', + system_file = 'system_file', + system_fin = 'system_fin', + system_security_event = 'system_security_event', + system_socket = 'system_socket', + zeek = 'zeek', +} + export enum TimelineStatus { active = 'active', draft = 'draft', @@ -1940,6 +1956,8 @@ export interface TimelineResult { eventType?: Maybe; + excludedRowRendererIds?: Maybe; + favorite?: Maybe; filters?: Maybe; @@ -8025,6 +8043,12 @@ export namespace TimelineResultResolvers { eventType?: EventTypeResolver, TypeParent, TContext>; + excludedRowRendererIds?: ExcludedRowRendererIdsResolver< + Maybe, + TypeParent, + TContext + >; + favorite?: FavoriteResolver, TypeParent, TContext>; filters?: FiltersResolver, TypeParent, TContext>; @@ -8108,6 +8132,11 @@ export namespace TimelineResultResolvers { Parent = TimelineResult, TContext = SiemContext > = Resolver; + export type ExcludedRowRendererIdsResolver< + R = Maybe, + Parent = TimelineResult, + TContext = SiemContext + > = Resolver; export type FavoriteResolver< R = Maybe, Parent = TimelineResult, diff --git a/x-pack/plugins/security_solution/server/lib/timeline/pick_saved_timeline.ts b/x-pack/plugins/security_solution/server/lib/timeline/pick_saved_timeline.ts index 281726d488abe..40293e8b6bbff 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/pick_saved_timeline.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/pick_saved_timeline.ts @@ -49,5 +49,5 @@ export const pickSavedTimeline = ( savedTimeline.status = TimelineStatus.active; } - return savedTimeline; + if (!savedTimeline.showRowRe) return savedTimeline; }; diff --git a/x-pack/plugins/security_solution/server/lib/timeline/saved_object_mappings.ts b/x-pack/plugins/security_solution/server/lib/timeline/saved_object_mappings.ts index 51bff033b8791..61a3dd1e9923f 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/saved_object_mappings.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/saved_object_mappings.ts @@ -129,6 +129,9 @@ export const timelineSavedObjectMappings: SavedObjectsType['mappings'] = { eventType: { type: 'keyword', }, + excludedRowRendererIds: { + type: 'text', + }, favorite: { properties: { keySearch: { From 8dbdfbe744462f5b85bad72b37e33aeecc8fb9b0 Mon Sep 17 00:00:00 2001 From: Patryk Kopycinski Date: Tue, 23 Jun 2020 10:11:54 +0200 Subject: [PATCH 02/19] cleanup --- .../common/types/timeline/index.ts | 85 +++- .../common/components/events_viewer/index.tsx | 26 +- .../components/fields_browser/index.tsx | 79 ++- .../components/open_timeline/helpers.test.ts | 4 + .../categories_pane.test.tsx | 61 --- .../row_renderers_browser/categories_pane.tsx | 97 ---- .../row_renderers_browser/category.test.tsx | 106 ---- .../row_renderers_browser/category.tsx | 61 --- .../category_columns.test.tsx | 137 ----- .../category_columns.tsx | 144 ------ .../category_title.test.tsx | 62 --- .../row_renderers_browser/category_title.tsx | 59 --- .../row_renderers_browser/constants.ts | 17 + .../field_browser.test.tsx | 250 ---------- .../field_items.test.tsx | 281 ----------- .../row_renderers_browser/field_items.tsx | 193 ------- .../row_renderers_browser/field_name.test.tsx | 66 --- .../row_renderers_browser/field_name.tsx | 115 ----- .../fields_pane.test.tsx | 120 ----- .../row_renderers_browser/fields_pane.tsx | 106 ---- .../row_renderers_browser/header.test.tsx | 258 ---------- .../row_renderers_browser/header.tsx | 123 ----- .../row_renderers_browser/helpers.test.tsx | 376 -------------- .../row_renderers_browser/helpers.tsx | 143 ------ .../row_renderers_browser/index.test.tsx | 2 +- .../row_renderers_browser/index.tsx | 150 +----- .../row_renderers_browser.tsx | 216 +++----- .../__snapshots__/index.test.tsx.snap | 470 +----------------- .../components/timeline/body/index.tsx | 54 +- .../timeline/body/stateful_body.tsx | 11 +- .../timelines/store/timeline/actions.ts | 1 + .../store/timeline/epic_local_storage.ts | 2 + .../lib/timeline/pick_saved_timeline.ts | 4 +- 33 files changed, 267 insertions(+), 3612 deletions(-) delete mode 100644 x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/categories_pane.test.tsx delete mode 100644 x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/categories_pane.tsx delete mode 100644 x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/category.test.tsx delete mode 100644 x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/category.tsx delete mode 100644 x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/category_columns.test.tsx delete mode 100644 x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/category_columns.tsx delete mode 100644 x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/category_title.test.tsx delete mode 100644 x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/category_title.tsx create mode 100644 x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/constants.ts delete mode 100644 x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/field_browser.test.tsx delete mode 100644 x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/field_items.test.tsx delete mode 100644 x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/field_items.tsx delete mode 100644 x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/field_name.test.tsx delete mode 100644 x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/field_name.tsx delete mode 100644 x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/fields_pane.test.tsx delete mode 100644 x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/fields_pane.tsx delete mode 100644 x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/header.test.tsx delete mode 100644 x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/header.tsx delete mode 100644 x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/helpers.test.tsx delete mode 100644 x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/helpers.tsx diff --git a/x-pack/plugins/security_solution/common/types/timeline/index.ts b/x-pack/plugins/security_solution/common/types/timeline/index.ts index a13417384c2d2..371dbfeefa6c5 100644 --- a/x-pack/plugins/security_solution/common/types/timeline/index.ts +++ b/x-pack/plugins/security_solution/common/types/timeline/index.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -/* eslint-disable @typescript-eslint/no-empty-interface */ +/* eslint-disable @typescript-eslint/camelcase, @typescript-eslint/no-empty-interface */ import * as runtimeTypes from 'io-ts'; import { SavedObjectsClient } from 'kibana/server'; @@ -151,24 +151,71 @@ export type TimelineStatusLiteralWithNull = runtimeTypes.TypeOf< typeof TimelineStatusLiteralWithNullRt >; -export const RowRendererIdRuntimeType = runtimeTypes.keyof({ - auditd: null, - auditd_file: null, - netflow: null, - plain: null, - suricata: null, - system: null, - system_dns: null, - system_endgame_process: null, - system_file: null, - system_fin: null, - system_security_event: null, - system_socket: null, - zeek: null, - all: null, -}); +const stringEnum = (enumObj: T, enumName = 'enum') => + new runtimeTypes.Type( + enumName, + (u): u is T[keyof T] => Object.values(enumObj).includes(u), + (u, c) => + Object.values(enumObj).includes(u) + ? runtimeTypes.success(u as T[keyof T]) + : runtimeTypes.failure(u, c), + (a) => (a as unknown) as string + ); + +export enum RowRendererId { + auditd = 'auditd', + auditd_file = 'auditd_file', + netflow = 'netflow', + plain = 'plain', + suricata = 'suricata', + system = 'system', + system_dns = 'system_dns', + system_endgame_process = 'system_endgame_process', + system_file = 'system_file', + system_fin = 'system_fin', + system_security_event = 'system_security_event', + system_socket = 'system_socket', + zeek = 'zeek', + all = 'all', +} -export type RowRendererId = runtimeTypes.TypeOf; +export const RowRendererIdRuntimeType = stringEnum(RowRendererId, 'RowRendererId'); + +// export const RowRendererIdRuntimeType = stringEnum({ +// auditd: 'auditd', +// auditd_file: 'auditd_file', +// netflow: 'netflow', +// plain: 'plain', +// suricata: 'suricata', +// system: 'system', +// system_dns: 'system_dns', +// system_endgame_process: 'system_endgame_process', +// system_file: 'system_file', +// system_fin: 'system_fin', +// system_security_event: 'system_security_event', +// system_socket: 'system_socket', +// zeek: 'zeek', +// all: 'all', +// }); + +// export const RowRendererIdRuntimeType = runtimeTypes.keyof({ +// auditd: null, +// auditd_file: null, +// netflow: null, +// plain: null, +// suricata: null, +// system: null, +// system_dns: null, +// system_endgame_process: null, +// system_file: null, +// system_fin: null, +// system_security_event: null, +// system_socket: null, +// zeek: null, +// all: null, +// }); + +// export type RowRendererId = runtimeTypes.TypeOf; /* * Timeline Types @@ -194,7 +241,7 @@ export const SavedTimelineRuntimeType = runtimeTypes.partial({ dataProviders: unionWithNullType(runtimeTypes.array(SavedDataProviderRuntimeType)), description: unionWithNullType(runtimeTypes.string), eventType: unionWithNullType(runtimeTypes.string), - excludedRowRendererIds: unionWithNullType(RowRendererIdRuntimeType), + excludedRowRendererIds: unionWithNullType(runtimeTypes.array(RowRendererIdRuntimeType)), favorite: unionWithNullType(runtimeTypes.array(SavedFavoriteRuntimeType)), filters: unionWithNullType(runtimeTypes.array(SavedFilterRuntimeType)), kqlMode: unionWithNullType(runtimeTypes.string), diff --git a/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx b/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx index 1645db371802c..2f245b20ded7f 100644 --- a/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx @@ -38,7 +38,6 @@ export interface OwnProps { type Props = OwnProps & PropsFromRedux; const StatefulEventsViewerComponent: React.FC = ({ - createTimeline, columns, dataProviders, deletedEventIds, @@ -56,8 +55,6 @@ const StatefulEventsViewerComponent: React.FC = ({ query, removeColumn, start, - showCheckboxes, - showRowRenderers, sort, updateItemsPerPage, upsertColumn, @@ -67,15 +64,12 @@ const StatefulEventsViewerComponent: React.FC = ({ defaultIndices ?? useUiSetting(DEFAULT_INDEX_KEY) ); - useEffect(() => { - if (createTimeline != null) { - createTimeline({ id, columns, sort, itemsPerPage, showCheckboxes, showRowRenderers }); - } - return () => { + useEffect( + () => () => { deleteEventQuery({ id, inputId: 'global' }); - }; - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); + }, + [deleteEventQuery, id] + ); const onChangeItemsPerPage: OnChangeItemsPerPage = useCallback( (itemsChangedPerPage) => updateItemsPerPage({ id, itemsPerPage: itemsChangedPerPage }), @@ -145,18 +139,18 @@ const makeMapStateToProps = () => { columns, dataProviders, deletedEventIds, + excludedRowRendererIds, itemsPerPage, itemsPerPageOptions, kqlMode, sort, - showCheckboxes, - showRowRenderers, } = events; return { columns, dataProviders, deletedEventIds, + excludedRowRendererIds, filters: getGlobalFiltersQuerySelector(state), id, isLive: input.policy.kind === 'interval', @@ -165,15 +159,12 @@ const makeMapStateToProps = () => { kqlMode, query: getGlobalQuerySelector(state), sort, - showCheckboxes, - showRowRenderers, }; }; return mapStateToProps; }; const mapDispatchToProps = { - createTimeline: timelineActions.createTimeline, deleteEventQuery: inputsActions.deleteOneQuery, updateItemsPerPage: timelineActions.updateItemsPerPage, removeColumn: timelineActions.removeColumn, @@ -192,6 +183,7 @@ export const StatefulEventsViewer = connector( deepEqual(prevProps.columns, nextProps.columns) && deepEqual(prevProps.defaultIndices, nextProps.defaultIndices) && deepEqual(prevProps.dataProviders, nextProps.dataProviders) && + deepEqual(prevProps.excludedRowRendererIds, nextProps.excludedRowRendererIds) && prevProps.deletedEventIds === nextProps.deletedEventIds && prevProps.end === nextProps.end && deepEqual(prevProps.filters, nextProps.filters) && @@ -203,8 +195,6 @@ export const StatefulEventsViewer = connector( deepEqual(prevProps.sort, nextProps.sort) && prevProps.start === nextProps.start && deepEqual(prevProps.pageFilters, nextProps.pageFilters) && - prevProps.showCheckboxes === nextProps.showCheckboxes && - prevProps.showRowRenderers === nextProps.showRowRenderers && prevProps.start === nextProps.start && prevProps.utilityBar === nextProps.utilityBar ) diff --git a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/index.tsx index 355ba6d233669..63adde9f72c4a 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/index.tsx @@ -17,8 +17,6 @@ import { filterBrowserFieldsByFieldName, mergeBrowserFieldsWithDefaultCategory } import * as i18n from './translations'; import { FieldBrowserProps } from './types'; -const fieldsButtonClassName = 'fields-button'; - /** wait this many ms after the user completes typing before applying the filter input */ export const INPUT_TIMEOUT = 250; @@ -143,47 +141,42 @@ export const StatefulFieldsBrowserComponent: React.FC = ({ ); return ( - <> - - - - {i18n.FIELDS} - - - - {show && ( - - )} - - + + + + {i18n.FIELDS} + + + + {show && ( + + )} + ); }; diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.test.ts b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.test.ts index 31ac3240afb72..8b33251cedb03 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.test.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.test.ts @@ -270,6 +270,7 @@ describe('helpers', () => { deletedEventIds: [], eventIdToNoteIds: {}, eventType: 'all', + excludedRowRendererIds: [], filters: [], highlightedDropAndProviderId: '', historyIds: [], @@ -368,6 +369,7 @@ describe('helpers', () => { deletedEventIds: [], eventIdToNoteIds: {}, eventType: 'all', + excludedRowRendererIds: [], filters: [], highlightedDropAndProviderId: '', historyIds: [], @@ -502,6 +504,7 @@ describe('helpers', () => { deletedEventIds: [], eventIdToNoteIds: {}, eventType: 'all', + excludedRowRendererIds: [], filters: [], highlightedDropAndProviderId: '', historyIds: [], @@ -628,6 +631,7 @@ describe('helpers', () => { deletedEventIds: [], eventIdToNoteIds: {}, eventType: 'all', + excludedRowRendererIds: [], filters: [ { $state: { diff --git a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/categories_pane.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/categories_pane.test.tsx deleted file mode 100644 index 42b44ba72438f..0000000000000 --- a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/categories_pane.test.tsx +++ /dev/null @@ -1,61 +0,0 @@ -/* - * 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 { mount } from 'enzyme'; -import React from 'react'; -import { ThemeProvider } from 'styled-components'; -import euiDarkVars from '@elastic/eui/dist/eui_theme_dark.json'; - -import { mockBrowserFields } from '../../../common/containers/source/mock'; - -import { CATEGORY_PANE_WIDTH } from './helpers'; -import { CategoriesPane } from './categories_pane'; -import * as i18n from './translations'; - -const timelineId = 'test'; - -describe('CategoriesPane', () => { - const theme = () => ({ eui: euiDarkVars, darkMode: true }); - test('it renders the expected title', () => { - const wrapper = mount( - - - - ); - - expect(wrapper.find('[data-test-subj="categories-pane-title"]').first().text()).toEqual( - i18n.CATEGORIES - ); - }); - - test('it renders a "No fields match" message when filteredBrowserFields is empty', () => { - const wrapper = mount( - - - - ); - - expect(wrapper.find('[data-test-subj="categories-container"] tbody').first().text()).toEqual( - i18n.NO_FIELDS_MATCH - ); - }); -}); diff --git a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/categories_pane.tsx b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/categories_pane.tsx deleted file mode 100644 index 480070fda9594..0000000000000 --- a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/categories_pane.tsx +++ /dev/null @@ -1,97 +0,0 @@ -/* - * 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 { EuiInMemoryTable, EuiTitle } from '@elastic/eui'; -import React from 'react'; -import styled from 'styled-components'; - -import { BrowserFields } from '../../../common/containers/source'; - -import { FieldBrowserProps } from './types'; -import { getCategoryColumns } from './category_columns'; -import { TABLE_HEIGHT } from './helpers'; - -import * as i18n from './translations'; - -const CategoryNames = styled.div<{ height: number; width: number }>` - ${({ height }) => `height: ${height}px`}; - overflow: auto; - padding: 5px; - ${({ width }) => `width: ${width}px`}; - thead { - display: none; - } -`; - -CategoryNames.displayName = 'CategoryNames'; - -const Title = styled(EuiTitle)` - padding-left: 5px; -`; - -Title.displayName = 'Title'; - -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 - * the filter input (as a substring). - */ - filteredBrowserFields: BrowserFields; - /** - * Invoked when the user clicks on the name of a category in the left-hand - * side of the field browser - */ - onCategorySelected: (categoryId: string) => void; - /** The category selected on the left-hand side of the field browser */ - selectedCategoryId: string; - /** The width of the categories pane */ - width: number; -}; - -export const CategoriesPane = React.memo( - ({ - browserFields, - filteredBrowserFields, - onCategorySelected, - onUpdateColumns, - selectedCategoryId, - timelineId, - width, - }) => ( - <> - - <h5 data-test-subj="categories-pane-title">{i18n.CATEGORIES}</h5> - - - - ({ categoryId }))} - message={i18n.NO_FIELDS_MATCH} - pagination={false} - sorting={false} - /> - - - ) -); - -CategoriesPane.displayName = 'CategoriesPane'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/category.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/category.test.tsx deleted file mode 100644 index 16174e92b3c37..0000000000000 --- a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/category.test.tsx +++ /dev/null @@ -1,106 +0,0 @@ -/* - * 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 { mockBrowserFields } from '../../../common/containers/source/mock'; - -import { Category } from './category'; -import { getFieldItems } from './field_items'; -import { FIELDS_PANE_WIDTH } from './helpers'; -import { TestProviders } from '../../../common/mock'; -import { useMountAppended } from '../../../common/utils/use_mount_appended'; - -import * as i18n from './translations'; - -describe('Category', () => { - const timelineId = 'test'; - const selectedCategoryId = 'client'; - const mount = useMountAppended(); - - test('it renders the category id as the value of the title', () => { - const wrapper = mount( - - - - ); - - expect(wrapper.find('[data-test-subj="selected-category-title"]').first().text()).toEqual( - selectedCategoryId - ); - }); - - test('it renders the Field column header', () => { - const wrapper = mount( - - - - ); - - expect(wrapper.find('.euiTableCellContent__text').at(0).text()).toEqual(i18n.FIELD); - }); - - test('it renders the Description column header', () => { - const wrapper = mount( - - - - ); - - expect(wrapper.find('.euiTableCellContent__text').at(1).text()).toEqual(i18n.DESCRIPTION); - }); -}); diff --git a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/category.tsx b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/category.tsx deleted file mode 100644 index fc91693039449..0000000000000 --- a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/category.tsx +++ /dev/null @@ -1,61 +0,0 @@ -/* - * 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 { EuiInMemoryTable } from '@elastic/eui'; -import React from 'react'; -import styled from 'styled-components'; - -import { BrowserFields } from '../../../common/containers/source'; - -import { CategoryTitle } from './category_title'; -import { FieldItem, getFieldColumns } from './field_items'; -import { TABLE_HEIGHT } from './helpers'; - -const TableContainer = styled.div<{ height: number; width: number }>` - ${({ height }) => `height: ${height}px`}; - overflow-x: hidden; - overflow-y: auto; - ${({ width }) => `width: ${width}px`}; -`; - -TableContainer.displayName = 'TableContainer'; - -interface Props { - categoryId: string; - fieldItems: FieldItem[]; - filteredBrowserFields: BrowserFields; - onCategorySelected: (categoryId: string) => void; - timelineId: string; - width: number; -} - -export const Category = React.memo( - ({ categoryId, filteredBrowserFields, fieldItems, timelineId, width }) => ( - <> - - - - - - - ) -); - -Category.displayName = 'Category'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/category_columns.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/category_columns.test.tsx deleted file mode 100644 index fcd19d30c8e50..0000000000000 --- a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/category_columns.test.tsx +++ /dev/null @@ -1,137 +0,0 @@ -/* - * 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 { mount } from 'enzyme'; -import React from 'react'; - -import { mockBrowserFields } from '../../../common/containers/source/mock'; - -import { CATEGORY_PANE_WIDTH, getFieldCount } from './helpers'; -import { CategoriesPane } from './categories_pane'; -import { ThemeProvider } from 'styled-components'; -import euiDarkVars from '@elastic/eui/dist/eui_theme_dark.json'; - -const timelineId = 'test'; -const theme = () => ({ eui: euiDarkVars, darkMode: true }); - -describe('getCategoryColumns', () => { - Object.keys(mockBrowserFields).forEach((categoryId) => { - test(`it renders the ${categoryId} category name (from filteredBrowserFields)`, () => { - const wrapper = mount( - - - - ); - - expect( - wrapper.find(`.field-browser-category-pane-${categoryId}-${timelineId}`).first().text() - ).toEqual(categoryId); - }); - }); - - Object.keys(mockBrowserFields).forEach((categoryId) => { - test(`it renders the correct field count for the ${categoryId} category (from filteredBrowserFields)`, () => { - const wrapper = mount( - - - - ); - - expect( - wrapper.find(`[data-test-subj="${categoryId}-category-count"]`).first().text() - ).toEqual(`${getFieldCount(mockBrowserFields[categoryId])}`); - }); - }); - - test('it renders the selected category with bold text', () => { - const selectedCategoryId = 'auditd'; - - const wrapper = mount( - - - - ); - - expect( - wrapper.find(`.field-browser-category-pane-${selectedCategoryId}-${timelineId}`).at(1) - ).toHaveStyleRule('font-weight', 'bold', { modifier: '.euiText' }); - }); - - test('it does NOT render an un-selected category with bold text', () => { - const selectedCategoryId = 'auditd'; - const notTheSelectedCategoryId = 'base'; - - const wrapper = mount( - - - - ); - - expect( - wrapper.find(`.field-browser-category-pane-${notTheSelectedCategoryId}-${timelineId}`).at(1) - ).toHaveStyleRule('font-weight', 'normal', { modifier: '.euiText' }); - }); - - test('it invokes onCategorySelected when a user clicks a category', () => { - const selectedCategoryId = 'auditd'; - const notTheSelectedCategoryId = 'base'; - - const onCategorySelected = jest.fn(); - - const wrapper = mount( - - - - ); - - wrapper - .find(`.field-browser-category-pane-${notTheSelectedCategoryId}-${timelineId}`) - .first() - .simulate('click'); - - expect(onCategorySelected).toHaveBeenCalledWith(notTheSelectedCategoryId); - }); -}); diff --git a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/category_columns.tsx b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/category_columns.tsx deleted file mode 100644 index 14c17b7262724..0000000000000 --- a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/category_columns.tsx +++ /dev/null @@ -1,144 +0,0 @@ -/* - * 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. - */ - -/* eslint-disable react/display-name */ - -import { EuiIcon, EuiFlexGroup, EuiFlexItem, EuiLink, EuiText, EuiToolTip } from '@elastic/eui'; -import React, { useMemo } from 'react'; -import styled from 'styled-components'; - -import { BrowserFields } from '../../../common/containers/source'; -import { getColumnsWithTimestamp } from '../../../common/components/event_details/helpers'; -import { CountBadge } from '../../../common/components/page'; -import { OnUpdateColumns } from '../timeline/events'; -import { WithHoverActions } from '../../../common/components/with_hover_actions'; -import { LoadingSpinner, getCategoryPaneCategoryClassName, getFieldCount } from './helpers'; -import * as i18n from './translations'; -import { useManageTimeline } from '../manage_timeline'; - -const CategoryName = styled.span<{ bold: boolean }>` - .euiText { - font-weight: ${({ bold }) => (bold ? 'bold' : 'normal')}; - } -`; - -CategoryName.displayName = 'CategoryName'; - -const LinkContainer = styled.div` - width: 100%; - .euiLink { - width: 100%; - } -`; - -LinkContainer.displayName = 'LinkContainer'; - -export interface CategoryItem { - categoryId: string; -} - -interface ToolTipProps { - categoryId: string; - browserFields: BrowserFields; - onUpdateColumns: OnUpdateColumns; - timelineId: string; -} - -const ToolTip = React.memo( - ({ categoryId, browserFields, onUpdateColumns, timelineId }) => { - const { getManageTimelineById } = useManageTimeline(); - // eslint-disable-next-line react-hooks/exhaustive-deps - const { isLoading } = useMemo(() => getManageTimelineById(timelineId) ?? { isLoading: false }, [ - timelineId, - ]); - 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, - onCategorySelected, - onUpdateColumns, - selectedCategoryId, - timelineId, -}: { - browserFields: BrowserFields; - filteredBrowserFields: BrowserFields; - onCategorySelected: (categoryId: string) => void; - onUpdateColumns: OnUpdateColumns; - selectedCategoryId: string; - timelineId: string; -}) => [ - { - field: 'categoryId', - name: '', - sortable: true, - truncateText: false, - render: (categoryId: string, _: { categoryId: string }) => ( - - onCategorySelected(categoryId)}> - - - - } - render={() => ( - - {categoryId} - - )} - /> - - - - - {getFieldCount(filteredBrowserFields[categoryId])} - - - - - - ), - }, -]; diff --git a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/category_title.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/category_title.test.tsx deleted file mode 100644 index ac7b2f7e67ae8..0000000000000 --- a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/category_title.test.tsx +++ /dev/null @@ -1,62 +0,0 @@ -/* - * 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 { mount } from 'enzyme'; -import React from 'react'; - -import { mockBrowserFields } from '../../../common/containers/source/mock'; - -import { CategoryTitle } from './category_title'; -import { getFieldCount } from './helpers'; - -describe('CategoryTitle', () => { - const timelineId = 'test'; - - test('it renders the category id as the value of the title', () => { - const categoryId = 'client'; - const wrapper = mount( - - ); - - expect(wrapper.find('[data-test-subj="selected-category-title"]').first().text()).toEqual( - categoryId - ); - }); - - test('when `categoryId` specifies a valid category in `filteredBrowserFields`, a count of the field is displayed in the badge', () => { - const validCategoryId = 'client'; - const wrapper = mount( - - ); - - expect(wrapper.find(`[data-test-subj="selected-category-count-badge"]`).first().text()).toEqual( - `${getFieldCount(mockBrowserFields[validCategoryId])}` - ); - }); - - test('when `categoryId` specifies an INVALID category in `filteredBrowserFields`, a count of zero is displayed in the badge', () => { - const invalidCategoryId = 'this.is.not.happening'; - const wrapper = mount( - - ); - - expect(wrapper.find(`[data-test-subj="selected-category-count-badge"]`).first().text()).toEqual( - '0' - ); - }); -}); diff --git a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/category_title.tsx b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/category_title.tsx deleted file mode 100644 index c8d59f5c0dfa4..0000000000000 --- a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/category_title.tsx +++ /dev/null @@ -1,59 +0,0 @@ -/* - * 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 { EuiFlexGroup, EuiFlexItem, EuiTitle } from '@elastic/eui'; -import React from 'react'; -import styled from 'styled-components'; - -import { BrowserFields } from '../../../common/containers/source'; -import { getFieldBrowserCategoryTitleClassName, getFieldCount } from './helpers'; -import { CountBadge } from '../../../common/components/page'; - -const CountBadgeContainer = styled.div` - position: relative; - top: -3px; -`; - -CountBadgeContainer.displayName = 'CountBadgeContainer'; - -interface Props { - /** The title of the category */ - categoryId: string; - /** - * A map of categoryId -> metadata about the fields in that category, - * filtered such that the name of every field in the category includes - * the filter input (as a substring). - */ - filteredBrowserFields: BrowserFields; - /** The timeline associated with this field browser */ - timelineId: string; -} - -export const CategoryTitle = React.memo( - ({ filteredBrowserFields, categoryId, timelineId }) => ( - - - -
{categoryId}
-
-
- - - - - {getFieldCount(filteredBrowserFields[categoryId])} - - - -
- ) -); - -CategoryTitle.displayName = 'CategoryTitle'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/constants.ts b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/constants.ts new file mode 100644 index 0000000000000..fffd02f338bcd --- /dev/null +++ b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/constants.ts @@ -0,0 +1,17 @@ +/* + * 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. + */ + +export const CATEGORY_PANE_WIDTH = 200; +export const DESCRIPTION_COLUMN_WIDTH = 300; +export const FIELD_COLUMN_WIDTH = 200; +export const FIELD_BROWSER_WIDTH = 900; +export const FIELD_BROWSER_HEIGHT = 300; +export const FIELDS_PANE_WIDTH = 670; +export const HEADER_HEIGHT = 40; +export const PANES_FLEX_GROUP_WIDTH = CATEGORY_PANE_WIDTH + FIELDS_PANE_WIDTH + 10; +export const SEARCH_INPUT_WIDTH = 850; +export const TABLE_HEIGHT = 260; +export const TYPE_COLUMN_WIDTH = 50; diff --git a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/field_browser.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/field_browser.test.tsx deleted file mode 100644 index 7c4e3d435e1ed..0000000000000 --- a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/field_browser.test.tsx +++ /dev/null @@ -1,250 +0,0 @@ -/* - * 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 { mount } from 'enzyme'; -import React from 'react'; - -import { mockBrowserFields } from '../../../common/containers/source/mock'; -import { TestProviders } from '../../../common/mock'; - -import { FieldsBrowser } from './field_browser'; -import { FIELD_BROWSER_HEIGHT, FIELD_BROWSER_WIDTH } from './helpers'; - -describe('FieldsBrowser', () => { - const timelineId = 'test'; - - // `enzyme` doesn't mount the components into the global jsdom `document` - // but that's where the click detector listener is, so for testing, we - // pass the top-level mounted component's click event on to document - const triggerDocumentMouseDown = () => { - const event = new Event('mousedown'); - document.dispatchEvent(event); - }; - - const triggerDocumentMouseUp = () => { - const event = new Event('mouseup'); - document.dispatchEvent(event); - }; - - test('it invokes onOutsideClick when onFieldSelected is undefined, and the user clicks outside the fields browser', () => { - const onOutsideClick = jest.fn(); - - const wrapper = mount( - -
- -
-
- ); - - wrapper.find('[data-test-subj="outside"]').simulate('mousedown'); - wrapper.find('[data-test-subj="outside"]').simulate('mouseup'); - - expect(onOutsideClick).toHaveBeenCalled(); - }); - - test('it does NOT invoke onOutsideClick when onFieldSelected is defined, and the user clicks outside the fields browser', () => { - const onOutsideClick = jest.fn(); - - const wrapper = mount( - -
- -
-
- ); - - wrapper.find('[data-test-subj="outside"]').simulate('mousedown'); - wrapper.find('[data-test-subj="outside"]').simulate('mouseup'); - - expect(onOutsideClick).not.toHaveBeenCalled(); - }); - - test('it renders the header', () => { - const wrapper = mount( - - - - ); - - expect(wrapper.find('[data-test-subj="header"]').exists()).toBe(true); - }); - - test('it renders the categories pane', () => { - const wrapper = mount( - - - - ); - - expect(wrapper.find('[data-test-subj="left-categories-pane"]').exists()).toBe(true); - }); - - test('it renders the fields pane', () => { - const wrapper = mount( - - - - ); - - expect(wrapper.find('[data-test-subj="fields-pane"]').exists()).toBe(true); - }); - - test('focuses the search input when the component mounts', () => { - const wrapper = mount( - - - - ); - - expect( - wrapper.find('[data-test-subj="field-search"]').first().getDOMNode().id === - document.activeElement!.id - ).toBe(true); - }); - - test('it invokes onSearchInputChange when the user types in the field search input', () => { - const onSearchInputChange = jest.fn(); - const inputText = 'event.category'; - - const wrapper = mount( - - - - ); - - const searchField = wrapper.find('[data-test-subj="field-search"]').first(); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const changeEvent: any = { target: { value: inputText } }; - const onChange = searchField.props().onChange; - - onChange!(changeEvent); - searchField.simulate('change').update(); - - expect(onSearchInputChange).toBeCalledWith(inputText); - }); -}); diff --git a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/field_items.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/field_items.test.tsx deleted file mode 100644 index e4c9621c2f71c..0000000000000 --- a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/field_items.test.tsx +++ /dev/null @@ -1,281 +0,0 @@ -/* - * 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 { omit } from 'lodash/fp'; -import React from 'react'; - -import { mockBrowserFields } from '../../../common/containers/source/mock'; -import { TestProviders } from '../../../common/mock'; -import { ColumnHeaderOptions } from '../../../timelines/store/timeline/model'; -import { defaultColumnHeaderType } from '../timeline/body/column_headers/default_headers'; -import { DEFAULT_DATE_COLUMN_MIN_WIDTH } from '../timeline/body/constants'; - -import { Category } from './category'; -import { getFieldColumns, getFieldItems } from './field_items'; -import { FIELDS_PANE_WIDTH } from './helpers'; -import { useMountAppended } from '../../../common/utils/use_mount_appended'; - -const selectedCategoryId = 'base'; -const selectedCategoryFields = mockBrowserFields[selectedCategoryId].fields; -const timestampFieldId = '@timestamp'; -const columnHeaders: ColumnHeaderOptions[] = [ - { - category: 'base', - columnHeaderType: defaultColumnHeaderType, - description: - 'Date/time when the event originated.\nFor log events this is the date/time when the event was generated, and not when it was read.\nRequired field for all events.', - example: '2016-05-23T08:05:34.853Z', - id: '@timestamp', - type: 'date', - aggregatable: true, - width: DEFAULT_DATE_COLUMN_MIN_WIDTH, - }, -]; - -describe('field_items', () => { - const timelineId = 'test'; - const mount = useMountAppended(); - - describe('getFieldItems', () => { - Object.keys(selectedCategoryFields!).forEach((fieldId) => { - test(`it renders the name of the ${fieldId} field`, () => { - const wrapper = mount( - - - - ); - - expect(wrapper.find(`[data-test-subj="field-name-${fieldId}"]`).first().text()).toEqual( - fieldId - ); - }); - }); - - Object.keys(selectedCategoryFields!).forEach((fieldId) => { - test(`it renders a checkbox for the ${fieldId} field`, () => { - const wrapper = mount( - - - - ); - - expect(wrapper.find(`[data-test-subj="field-${fieldId}-checkbox"]`).first().exists()).toBe( - true - ); - }); - }); - - test('it renders a checkbox in the checked state when the field is selected to be displayed as a column in the timeline', () => { - const wrapper = mount( - - - - ); - - expect( - wrapper.find(`[data-test-subj="field-${timestampFieldId}-checkbox"]`).first().props() - .checked - ).toBe(true); - }); - - test('it does NOT render a checkbox in the checked state when the field is NOT selected to be displayed as a column in the timeline', () => { - const wrapper = mount( - - header.id !== timestampFieldId), - highlight: '', - onUpdateColumns: jest.fn(), - timelineId, - toggleColumn: jest.fn(), - })} - width={FIELDS_PANE_WIDTH} - onCategorySelected={jest.fn()} - timelineId={timelineId} - /> - - ); - - expect( - wrapper.find(`[data-test-subj="field-${timestampFieldId}-checkbox"]`).first().props() - .checked - ).toBe(false); - }); - - test('it invokes `toggleColumn` when the user interacts with the checkbox', () => { - const toggleColumn = jest.fn(); - - const wrapper = mount( - - - - ); - - wrapper - .find('input[type="checkbox"]') - .first() - .simulate('change', { - target: { checked: true }, - }); - wrapper.update(); - - expect(toggleColumn).toBeCalledWith({ - columnHeaderType: 'not-filtered', - id: '@timestamp', - width: 180, - }); - }); - - test('it renders the expected icon for a field', () => { - const wrapper = mount( - - - - ); - - expect( - wrapper.find(`[data-test-subj="field-${timestampFieldId}-icon"]`).first().props().type - ).toEqual('clock'); - }); - - test('it renders the expected field description', () => { - const wrapper = mount( - - - - ); - - expect( - wrapper.find(`[data-test-subj="field-${timestampFieldId}-description"]`).first().text() - ).toEqual( - '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' - ); - }); - }); - - describe('getFieldColumns', () => { - test('it returns the expected column definitions', () => { - expect(getFieldColumns().map((column) => omit('render', column))).toEqual([ - { field: 'field', name: 'Field', sortable: true, width: '250px' }, - { - field: 'description', - name: 'Description', - sortable: true, - truncateText: true, - width: '400px', - }, - ]); - }); - }); -}); diff --git a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/field_items.tsx b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/field_items.tsx deleted file mode 100644 index aaad9cf145ab7..0000000000000 --- a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/field_items.tsx +++ /dev/null @@ -1,193 +0,0 @@ -/* - * 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. - */ - -/* eslint-disable react/display-name */ - -import { EuiCheckbox, EuiIcon, EuiToolTip, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -import { uniqBy } from 'lodash/fp'; -import React from 'react'; -import { Draggable } from 'react-beautiful-dnd'; -import styled from 'styled-components'; - -import { BrowserField, BrowserFields } from '../../../common/containers/source'; -import { ColumnHeaderOptions } from '../../../timelines/store/timeline/model'; -import { DragEffects } from '../../../common/components/drag_and_drop/draggable_wrapper'; -import { DroppableWrapper } from '../../../common/components/drag_and_drop/droppable_wrapper'; -import { - getDraggableFieldId, - getDroppableId, - DRAG_TYPE_FIELD, -} from '../../../common/components/drag_and_drop/helpers'; -import { DraggableFieldBadge } from '../../../common/components/draggables/field_badge'; -import { getEmptyValue } from '../../../common/components/empty_value'; -import { - getColumnsWithTimestamp, - getExampleText, - getIconFromType, -} from '../../../common/components/event_details/helpers'; -import { SelectableText } from '../../../common/components/selectable_text'; -import { defaultColumnHeaderType } from '../timeline/body/column_headers/default_headers'; -import { DEFAULT_COLUMN_MIN_WIDTH } from '../timeline/body/constants'; -import { OnUpdateColumns } from '../timeline/events'; -import { TruncatableText } from '../../../common/components/truncatable_text'; -import { FieldName } from './field_name'; -import * as i18n from './translations'; - -const TypeIcon = styled(EuiIcon)` - margin-left: 5px; - position: relative; - top: -1px; -`; - -TypeIcon.displayName = 'TypeIcon'; - -export const Description = styled.span` - user-select: text; - width: 150px; -`; - -Description.displayName = 'Description'; - -/** - * An item rendered in the table - */ -export interface FieldItem { - description: React.ReactNode; - field: React.ReactNode; - fieldId: string; -} - -/** - * Returns the draggable fields, values, and descriptions shown when a user expands an event - */ -export const getFieldItems = ({ - browserFields, - category, - categoryId, - columnHeaders, - highlight = '', - onUpdateColumns, - timelineId, - toggleColumn, -}: { - browserFields: BrowserFields; - category: Partial; - categoryId: string; - columnHeaders: ColumnHeaderOptions[]; - highlight?: string; - timelineId: string; - toggleColumn: (column: ColumnHeaderOptions) => void; - onUpdateColumns: OnUpdateColumns; -}): FieldItem[] => - uniqBy('name', [ - ...Object.values(category != null && category.fields != null ? category.fields : {}), - ]).map((field) => ({ - description: ( - - {`${field.description || getEmptyValue()} ${getExampleText(field.example)}`} - - ), - field: ( - ( -
- - - -
- )} - > - - {(provided) => ( -
- - - - c.id === field.name) !== -1} - data-test-subj={`field-${field.name}-checkbox`} - id={field.name || ''} - onChange={() => - toggleColumn({ - columnHeaderType: defaultColumnHeaderType, - id: field.name || '', - width: DEFAULT_COLUMN_MIN_WIDTH, - }) - } - /> - - - - - - - - - - - - - -
- )} -
-
- ), - fieldId: field.name || '', - })); - -/** - * Returns a table column template provided to the `EuiInMemoryTable`'s - * `columns` prop - */ -export const getFieldColumns = () => [ - { - field: 'field', - name: i18n.FIELD, - sortable: true, - render: (field: React.ReactNode, _: FieldItem) => <>{field}, - width: '250px', - }, - { - field: 'description', - name: i18n.DESCRIPTION, - render: (description: string, _: FieldItem) => ( - - - <>{description} - - - ), - sortable: true, - truncateText: true, - width: '400px', - }, -]; diff --git a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/field_name.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/field_name.test.tsx deleted file mode 100644 index da0cbb99b8671..0000000000000 --- a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/field_name.test.tsx +++ /dev/null @@ -1,66 +0,0 @@ -/* - * 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 { mount } from 'enzyme'; -import React from 'react'; - -import { mockBrowserFields } from '../../../common/containers/source/mock'; -import { TestProviders } from '../../../common/mock'; -import { getColumnsWithTimestamp } from '../../../common/components/event_details/helpers'; - -import { FieldName } from './field_name'; - -const categoryId = 'base'; -const timestampFieldId = '@timestamp'; - -const defaultProps = { - categoryId, - categoryColumns: getColumnsWithTimestamp({ - browserFields: mockBrowserFields, - category: categoryId, - }), - fieldId: timestampFieldId, - onUpdateColumns: jest.fn(), - timelineId: 'timeline-id', -}; - -describe('FieldName', () => { - test('it renders the field name', () => { - const wrapper = mount( - - - - ); - - expect( - wrapper.find(`[data-test-subj="field-name-${timestampFieldId}"]`).first().text() - ).toEqual(timestampFieldId); - }); - - test('it renders a copy to clipboard action menu item a user hovers over the name', () => { - const wrapper = mount( - - - - ); - - wrapper.simulate('mouseenter'); - wrapper.update(); - expect(wrapper.find('[data-test-subj="copy-to-clipboard"]').exists()).toBe(true); - }); - - test('it highlights the text specified by the `highlight` prop', () => { - const highlight = 'stamp'; - - const wrapper = mount( - - - - ); - - expect(wrapper.find('strong').first().text()).toEqual(highlight); - }); -}); diff --git a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/field_name.tsx b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/field_name.tsx deleted file mode 100644 index 985c8b35094ef..0000000000000 --- a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/field_name.tsx +++ /dev/null @@ -1,115 +0,0 @@ -/* - * 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 { EuiHighlight, EuiText } from '@elastic/eui'; -import React, { useCallback, useState, useMemo } from 'react'; -import styled from 'styled-components'; - -import { ColumnHeaderOptions } from '../../../timelines/store/timeline/model'; -import { OnUpdateColumns } from '../timeline/events'; -import { WithHoverActions } from '../../../common/components/with_hover_actions'; -import { DraggableWrapperHoverContent } from '../../../common/components/drag_and_drop/draggable_wrapper_hover_content'; - -/** - * The name of a (draggable) field - */ -export const FieldNameContainer = styled.span` - border-radius: 4px; - padding: 0 4px 0 8px; - position: relative; - - &::before { - background-image: linear-gradient( - 135deg, - ${({ theme }) => theme.eui.euiColorMediumShade} 25%, - transparent 25% - ), - linear-gradient(-135deg, ${({ theme }) => theme.eui.euiColorMediumShade} 25%, transparent 25%), - linear-gradient(135deg, transparent 75%, ${({ theme }) => theme.eui.euiColorMediumShade} 75%), - linear-gradient(-135deg, transparent 75%, ${({ theme }) => theme.eui.euiColorMediumShade} 75%); - background-position: 0 0, 1px 0, 1px -1px, 0px 1px; - background-size: 2px 2px; - bottom: 2px; - content: ''; - display: block; - left: 2px; - position: absolute; - top: 2px; - width: 4px; - } - - &:hover, - &:focus { - transition: background-color 0.7s ease; - background-color: #000; - color: #fff; - - &::before { - background-image: linear-gradient(135deg, #fff 25%, transparent 25%), - linear-gradient( - -135deg, - ${({ theme }) => theme.eui.euiColorLightestShade} 25%, - transparent 25% - ), - linear-gradient( - 135deg, - transparent 75%, - ${({ theme }) => theme.eui.euiColorLightestShade} 75% - ), - linear-gradient( - -135deg, - transparent 75%, - ${({ theme }) => theme.eui.euiColorLightestShade} 75% - ); - } - } -`; - -FieldNameContainer.displayName = 'FieldNameContainer'; - -/** Renders a field name in it's non-dragging state */ -export const FieldName = React.memo<{ - categoryId: string; - categoryColumns: ColumnHeaderOptions[]; - fieldId: string; - highlight?: string; - onUpdateColumns: OnUpdateColumns; - timelineId: string; -}>(({ fieldId, highlight = '', timelineId }) => { - const [showTopN, setShowTopN] = useState(false); - const toggleTopN = useCallback(() => { - setShowTopN(!showTopN); - }, [setShowTopN, showTopN]); - - const hoverContent = useMemo( - () => ( - - ), - [fieldId, showTopN, toggleTopN, timelineId] - ); - - const render = useCallback( - () => ( - - - - {fieldId} - - - - ), - [fieldId, highlight] - ); - - return ; -}); - -FieldName.displayName = 'FieldName'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/fields_pane.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/fields_pane.test.tsx deleted file mode 100644 index b55bbfc023774..0000000000000 --- a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/fields_pane.test.tsx +++ /dev/null @@ -1,120 +0,0 @@ -/* - * 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 { mockBrowserFields } from '../../../common/containers/source/mock'; -import { TestProviders } from '../../../common/mock'; -import { useMountAppended } from '../../../common/utils/use_mount_appended'; - -import { FIELDS_PANE_WIDTH } from './helpers'; -import { FieldsPane } from './fields_pane'; - -const timelineId = 'test'; - -describe('FieldsPane', () => { - const mount = useMountAppended(); - - test('it renders the selected category', () => { - const selectedCategory = 'auditd'; - - const wrapper = mount( - - - - ); - - expect(wrapper.find(`[data-test-subj="selected-category-title"]`).first().text()).toEqual( - selectedCategory - ); - }); - - test('it renders a unknown category that does not exist in filteredBrowserFields', () => { - const selectedCategory = 'unknown'; - - const wrapper = mount( - - - - ); - - expect(wrapper.find(`[data-test-subj="selected-category-title"]`).first().text()).toEqual( - selectedCategory - ); - }); - - test('it renders the expected message when `filteredBrowserFields` is empty and `searchInput` is empty', () => { - const searchInput = ''; - - const wrapper = mount( - - - - ); - - expect(wrapper.find(`[data-test-subj="no-fields-match"]`).first().text()).toEqual( - 'No fields match ' - ); - }); - - test('it renders the expected message when `filteredBrowserFields` is empty and `searchInput` is an unknown field name', () => { - const searchInput = 'thisFieldDoesNotExist'; - - const wrapper = mount( - - - - ); - - expect(wrapper.find(`[data-test-subj="no-fields-match"]`).first().text()).toEqual( - `No fields match ${searchInput}` - ); - }); -}); diff --git a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/fields_pane.tsx b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/fields_pane.tsx deleted file mode 100644 index 73ea739216857..0000000000000 --- a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/fields_pane.tsx +++ /dev/null @@ -1,106 +0,0 @@ -/* - * 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 { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -import React from 'react'; -import styled from 'styled-components'; - -import { BrowserFields } from '../../../common/containers/source'; -import { ColumnHeaderOptions } from '../../../timelines/store/timeline/model'; - -import { Category } from './category'; -import { FieldBrowserProps } from './types'; -import { getFieldItems } from './field_items'; -import { FIELDS_PANE_WIDTH, TABLE_HEIGHT } from './helpers'; - -import * as i18n from './translations'; - -const NoFieldsPanel = styled.div` - background-color: ${(props) => props.theme.eui.euiColorLightestShade}; - width: ${FIELDS_PANE_WIDTH}px; - height: ${TABLE_HEIGHT}px; -`; - -NoFieldsPanel.displayName = 'NoFieldsPanel'; - -const NoFieldsFlexGroup = styled(EuiFlexGroup)` - height: 100%; -`; - -NoFieldsFlexGroup.displayName = 'NoFieldsFlexGroup'; - -type Props = Pick & { - columnHeaders: ColumnHeaderOptions[]; - /** - * A map of categoryId -> metadata about the fields in that category, - * filtered such that the name of every field in the category includes - * the filter input (as a substring). - */ - filteredBrowserFields: BrowserFields; - /** - * Invoked when the user clicks on the name of a category in the left-hand - * side of the field browser - */ - onCategorySelected: (categoryId: string) => void; - /** The text displayed in the search input */ - searchInput: string; - /** - * The category selected on the left-hand side of the field browser - */ - selectedCategoryId: string; - /** The width field browser */ - width: number; - /** - * Invoked to add or remove a column from the timeline - */ - toggleColumn: (column: ColumnHeaderOptions) => void; -}; -export const FieldsPane = React.memo( - ({ - columnHeaders, - filteredBrowserFields, - onCategorySelected, - onUpdateColumns, - searchInput, - selectedCategoryId, - timelineId, - toggleColumn, - width, - }) => ( - <> - {Object.keys(filteredBrowserFields).length > 0 ? ( - - ) : ( - - - -

{i18n.NO_FIELDS_MATCH_INPUT(searchInput)}

-
-
-
- )} - - ) -); - -FieldsPane.displayName = 'FieldsPane'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/header.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/header.test.tsx deleted file mode 100644 index bb33b36dd9a19..0000000000000 --- a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/header.test.tsx +++ /dev/null @@ -1,258 +0,0 @@ -/* - * 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 { mount } from 'enzyme'; -import React from 'react'; -import { mockBrowserFields } from '../../../common/containers/source/mock'; -import { TestProviders } from '../../../common/mock'; -import { defaultHeaders } from '../timeline/body/column_headers/default_headers'; -import { Header } from './header'; - -const timelineId = 'test'; - -describe('Header', () => { - test('it renders the field browser title', () => { - const wrapper = mount( - -
- - ); - - expect(wrapper.find('[data-test-subj="field-browser-title"]').first().text()).toEqual( - 'Customize Columns' - ); - }); - - test('it renders the Reset Fields button', () => { - const wrapper = mount( - -
- - ); - - expect(wrapper.find('[data-test-subj="reset-fields"]').first().text()).toEqual('Reset Fields'); - }); - - test('it invokes onUpdateColumns when the user clicks the Reset Fields button', () => { - const onUpdateColumns = jest.fn(); - - const wrapper = mount( - -
- - ); - - wrapper.find('[data-test-subj="reset-fields"]').first().simulate('click'); - - expect(onUpdateColumns).toBeCalledWith(defaultHeaders); - }); - - test('it invokes onOutsideClick when the user clicks the Reset Fields button', () => { - const onOutsideClick = jest.fn(); - - const wrapper = mount( - -
- - ); - - wrapper.find('[data-test-subj="reset-fields"]').first().simulate('click'); - - expect(onOutsideClick).toBeCalled(); - }); - - test('it renders the field search input with the expected placeholder text when the searchInput prop is empty', () => { - const wrapper = mount( - -
- - ); - - expect(wrapper.find('[data-test-subj="field-search"]').first().props().placeholder).toEqual( - 'Field name' - ); - }); - - test('it renders the "current" search value in the input when searchInput is not empty', () => { - const searchInput = 'aFieldName'; - - const wrapper = mount( - -
- - ); - - expect(wrapper.find('input').props().value).toEqual(searchInput); - }); - - test('it renders the field search input with a spinner when isSearching is true', () => { - const wrapper = mount( - -
- - ); - - expect(wrapper.find('.euiLoadingSpinner').first().exists()).toBe(true); - }); - - test('it invokes onSearchInputChange when the user types in the search field', () => { - const onSearchInputChange = jest.fn(); - - const wrapper = mount( - -
- - ); - - wrapper - .find('input') - .first() - .simulate('change', { target: { value: 'timestamp' } }); - wrapper.update(); - - expect(onSearchInputChange).toBeCalled(); - }); - - test('it returns the expected categories count when filteredBrowserFields is empty', () => { - const wrapper = mount( - -
- - ); - - expect(wrapper.find('[data-test-subj="categories-count"]').first().text()).toEqual( - '0 categories' - ); - }); - - test('it returns the expected categories count when filteredBrowserFields is NOT empty', () => { - const wrapper = mount( - -
- - ); - - expect(wrapper.find('[data-test-subj="categories-count"]').first().text()).toEqual( - '9 categories' - ); - }); - - test('it returns the expected fields count when filteredBrowserFields is empty', () => { - const wrapper = mount( - -
- - ); - - expect(wrapper.find('[data-test-subj="fields-count"]').first().text()).toEqual('0 fields'); - }); - - test('it returns the expected fields count when filteredBrowserFields is NOT empty', () => { - const wrapper = mount( - -
- - ); - - expect(wrapper.find('[data-test-subj="fields-count"]').first().text()).toEqual('25 fields'); - }); -}); diff --git a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/header.tsx b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/header.tsx deleted file mode 100644 index 2e822e1765ff5..0000000000000 --- a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/header.tsx +++ /dev/null @@ -1,123 +0,0 @@ -/* - * 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 { EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, EuiTitle } from '@elastic/eui'; -import React, { useCallback, useMemo } from 'react'; -import styled from 'styled-components'; - -import { BrowserFields } from '../../../common/containers/source'; -import { alertsHeaders } from '../../../alerts/components/alerts_table/default_config'; -import { alertsHeaders as externalAlertsHeaders } from '../../../common/components/alerts_viewer/default_headers'; -import { defaultHeaders as eventsDefaultHeaders } from '../../../common/components/events_viewer/default_headers'; -import { defaultHeaders } from '../timeline/body/column_headers/default_headers'; -import { OnUpdateColumns } from '../timeline/events'; - -import { SEARCH_INPUT_WIDTH } from './helpers'; - -import * as i18n from './translations'; -import { useManageTimeline } from '../manage_timeline'; - -// background-color: ${props => props.theme.eui.euiColorLightestShade}; -const HeaderContainer = styled.div` - padding: 16px; - margin-bottom: 8px; -`; - -HeaderContainer.displayName = 'HeaderContainer'; - -const SearchContainer = styled.div` - input { - max-width: ${SEARCH_INPUT_WIDTH}px; - width: ${SEARCH_INPUT_WIDTH}px; - } -`; - -SearchContainer.displayName = 'SearchContainer'; - -interface Props { - filteredBrowserFields: BrowserFields; - isEventViewer?: boolean; - isSearching: boolean; - onOutsideClick: () => void; - onSearchInputChange: (event: React.ChangeEvent) => void; - onUpdateColumns: OnUpdateColumns; - searchInput: string; - timelineId: string; -} - -const TitleRow = React.memo<{ - id: string; - isEventViewer?: boolean; - onOutsideClick: () => void; - onUpdateColumns: OnUpdateColumns; -}>(({ id, isEventViewer, onOutsideClick, onUpdateColumns }) => { - const { getManageTimelineById } = useManageTimeline(); - const documentType = useMemo(() => getManageTimelineById(id).documentType, [ - getManageTimelineById, - id, - ]); - const handleResetColumns = useCallback(() => { - let resetDefaultHeaders = defaultHeaders; - if (isEventViewer) { - if (documentType.toLocaleLowerCase() === 'externalAlerts') { - resetDefaultHeaders = externalAlertsHeaders; - } else if (documentType.toLocaleLowerCase() === 'alerts') { - resetDefaultHeaders = alertsHeaders; - } else { - resetDefaultHeaders = eventsDefaultHeaders; - } - } - onUpdateColumns(resetDefaultHeaders); - onOutsideClick(); - }, [isEventViewer, onOutsideClick, onUpdateColumns, documentType]); - - return ( - - - -

{i18n.CUSTOMIZE_COLUMNS}

-
-
- - - - {i18n.RESET_FIELDS} - - -
- ); -}); - -TitleRow.displayName = 'TitleRow'; - -export const Header = React.memo( - ({ - isEventViewer, - isSearching, - filteredBrowserFields, - onOutsideClick, - onSearchInputChange, - onUpdateColumns, - searchInput, - timelineId, - }) => ( - - - - ) -); - -Header.displayName = 'Header'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/helpers.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/helpers.test.tsx deleted file mode 100644 index 0e1b00dd9b864..0000000000000 --- a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/helpers.test.tsx +++ /dev/null @@ -1,376 +0,0 @@ -/* - * 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 { mockBrowserFields } from '../../../common/containers/source/mock'; - -import { - categoryHasFields, - createVirtualCategory, - getCategoryPaneCategoryClassName, - getFieldBrowserCategoryTitleClassName, - getFieldBrowserSearchInputClassName, - getFieldCount, - filterBrowserFieldsByFieldName, -} from './helpers'; -import { BrowserFields } from '../../../common/containers/source'; - -const timelineId = 'test'; - -describe('helpers', () => { - describe('getCategoryPaneCategoryClassName', () => { - test('it returns the expected class name', () => { - const categoryId = 'auditd'; - - expect(getCategoryPaneCategoryClassName({ categoryId, timelineId })).toEqual( - 'field-browser-category-pane-auditd-test' - ); - }); - }); - - describe('getFieldBrowserCategoryTitleClassName', () => { - test('it returns the expected class name', () => { - const categoryId = 'auditd'; - - expect(getFieldBrowserCategoryTitleClassName({ categoryId, timelineId })).toEqual( - 'field-browser-category-title-auditd-test' - ); - }); - }); - - describe('getFieldBrowserSearchInputClassName', () => { - test('it returns the expected class name', () => { - expect(getFieldBrowserSearchInputClassName(timelineId)).toEqual( - 'field-browser-search-input-test' - ); - }); - }); - - describe('categoryHasFields', () => { - test('it returns false if the category fields property is undefined', () => { - expect(categoryHasFields({})).toBe(false); - }); - - test('it returns false if the category fields property is empty', () => { - expect(categoryHasFields({ fields: {} })).toBe(false); - }); - - test('it returns true if the category has one field', () => { - expect( - categoryHasFields({ - fields: { - 'auditd.data.a0': { - aggregatable: true, - category: 'auditd', - description: null, - example: null, - format: '', - indexes: ['auditbeat'], - name: 'auditd.data.a0', - searchable: true, - type: 'string', - }, - }, - }) - ).toBe(true); - }); - - test('it returns true if the category has multiple fields', () => { - expect( - categoryHasFields({ - fields: { - 'agent.ephemeral_id': { - aggregatable: true, - category: 'agent', - description: - 'Ephemeral identifier of this agent (if one exists). This id normally changes across restarts, but `agent.id` does not.', - example: '8a4f500f', - format: '', - indexes: ['auditbeat', 'filebeat', 'packetbeat'], - name: 'agent.ephemeral_id', - searchable: true, - type: 'string', - }, - 'agent.hostname': { - aggregatable: true, - category: 'agent', - description: null, - example: null, - format: '', - indexes: ['auditbeat', 'filebeat', 'packetbeat'], - name: 'agent.hostname', - searchable: true, - type: 'string', - }, - }, - }) - ).toBe(true); - }); - }); - - describe('getFieldCount', () => { - test('it returns 0 if the category fields property is undefined', () => { - expect(getFieldCount({})).toEqual(0); - }); - - test('it returns 0 if the category fields property is empty', () => { - expect(getFieldCount({ fields: {} })).toEqual(0); - }); - - test('it returns 1 if the category has one field', () => { - expect( - getFieldCount({ - fields: { - 'auditd.data.a0': { - aggregatable: true, - category: 'auditd', - description: null, - example: null, - format: '', - indexes: ['auditbeat'], - name: 'auditd.data.a0', - searchable: true, - type: 'string', - }, - }, - }) - ).toEqual(1); - }); - - test('it returns the correct count when category has multiple fields', () => { - expect( - getFieldCount({ - fields: { - 'agent.ephemeral_id': { - aggregatable: true, - category: 'agent', - description: - 'Ephemeral identifier of this agent (if one exists). This id normally changes across restarts, but `agent.id` does not.', - example: '8a4f500f', - format: '', - indexes: ['auditbeat', 'filebeat', 'packetbeat'], - name: 'agent.ephemeral_id', - searchable: true, - type: 'string', - }, - 'agent.hostname': { - aggregatable: true, - category: 'agent', - description: null, - example: null, - format: '', - indexes: ['auditbeat', 'filebeat', 'packetbeat'], - name: 'agent.hostname', - searchable: true, - type: 'string', - }, - }, - }) - ).toEqual(2); - }); - }); - - describe('filterBrowserFieldsByFieldName', () => { - test('it returns an empty collection when browserFields is empty', () => { - expect(filterBrowserFieldsByFieldName({ browserFields: {}, substring: '' })).toEqual({}); - }); - - test('it returns an empty collection when browserFields is empty and substring is non empty', () => { - expect( - filterBrowserFieldsByFieldName({ browserFields: {}, substring: 'nothing to match' }) - ).toEqual({}); - }); - - test('it returns an empty collection when browserFields is NOT empty and substring does not match any fields', () => { - expect( - filterBrowserFieldsByFieldName({ - browserFields: mockBrowserFields, - substring: 'nothing to match', - }) - ).toEqual({}); - }); - - test('it returns the original collection when browserFields is NOT empty and substring is empty', () => { - expect( - filterBrowserFieldsByFieldName({ - browserFields: mockBrowserFields, - substring: '', - }) - ).toEqual(mockBrowserFields); - }); - - test('it returns (only) non-empty categories, where each category contains only the fields matching the substring', () => { - const filtered: BrowserFields = { - agent: { - fields: { - 'agent.ephemeral_id': { - aggregatable: true, - category: 'agent', - description: - 'Ephemeral identifier of this agent (if one exists). This id normally changes across restarts, but `agent.id` does not.', - example: '8a4f500f', - format: '', - indexes: ['auditbeat', 'filebeat', 'packetbeat'], - name: 'agent.ephemeral_id', - searchable: true, - type: 'string', - }, - 'agent.id': { - aggregatable: true, - category: 'agent', - description: - 'Unique identifier of this agent (if one exists). Example: For Beats this would be beat.id.', - example: '8a4f500d', - format: '', - indexes: ['auditbeat', 'filebeat', 'packetbeat'], - name: 'agent.id', - searchable: true, - type: 'string', - }, - }, - }, - cloud: { - fields: { - 'cloud.account.id': { - aggregatable: true, - category: 'cloud', - description: - 'The cloud account or organization id used to identify different entities in a multi-tenant environment. Examples: AWS account id, Google Cloud ORG Id, or other unique identifier.', - example: '666777888999', - format: '', - indexes: ['auditbeat', 'filebeat', 'packetbeat'], - name: 'cloud.account.id', - searchable: true, - type: 'string', - }, - }, - }, - container: { - fields: { - 'container.id': { - aggregatable: true, - category: 'container', - description: 'Unique container id.', - example: null, - format: '', - indexes: ['auditbeat', 'filebeat', 'packetbeat'], - name: 'container.id', - searchable: true, - type: 'string', - }, - }, - }, - }; - - expect( - filterBrowserFieldsByFieldName({ - browserFields: mockBrowserFields, - substring: 'id', - }) - ).toEqual(filtered); - }); - }); - - describe('createVirtualCategory', () => { - test('it combines the specified fields into a virtual category when the input ONLY contains field names that contain dots (e.g. agent.hostname)', () => { - const expectedMatchingFields = { - fields: { - 'agent.hostname': { - aggregatable: true, - category: 'agent', - description: null, - example: null, - format: '', - indexes: ['auditbeat', 'filebeat', 'packetbeat'], - name: 'agent.hostname', - searchable: true, - type: 'string', - }, - 'client.domain': { - aggregatable: true, - category: 'client', - description: 'Client domain.', - example: null, - format: '', - indexes: ['auditbeat', 'filebeat', 'packetbeat'], - name: 'client.domain', - searchable: true, - type: 'string', - }, - 'client.geo.country_iso_code': { - aggregatable: true, - category: 'client', - description: 'Country ISO code.', - example: 'CA', - format: '', - indexes: ['auditbeat', 'filebeat', 'packetbeat'], - name: 'client.geo.country_iso_code', - searchable: true, - type: 'string', - }, - }, - }; - - const fieldIds = ['agent.hostname', 'client.domain', 'client.geo.country_iso_code']; - - expect( - createVirtualCategory({ - browserFields: mockBrowserFields, - fieldIds, - }) - ).toEqual(expectedMatchingFields); - }); - - test('it combines the specified fields into a virtual category when the input includes field names from the base category that do NOT contain dots (e.g. @timestamp)', () => { - const expectedMatchingFields = { - fields: { - 'agent.hostname': { - aggregatable: true, - category: 'agent', - description: null, - example: null, - format: '', - indexes: ['auditbeat', 'filebeat', 'packetbeat'], - name: 'agent.hostname', - searchable: true, - type: 'string', - }, - '@timestamp': { - aggregatable: true, - category: 'base', - description: - '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', - format: '', - indexes: ['auditbeat', 'filebeat', 'packetbeat'], - name: '@timestamp', - searchable: true, - type: 'date', - }, - 'client.domain': { - aggregatable: true, - category: 'client', - description: 'Client domain.', - example: null, - format: '', - indexes: ['auditbeat', 'filebeat', 'packetbeat'], - name: 'client.domain', - searchable: true, - type: 'string', - }, - }, - }; - - const fieldIds = ['agent.hostname', '@timestamp', 'client.domain']; - - expect( - createVirtualCategory({ - browserFields: mockBrowserFields, - fieldIds, - }) - ).toEqual(expectedMatchingFields); - }); - }); -}); diff --git a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/helpers.tsx b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/helpers.tsx deleted file mode 100644 index df4b106ab6bd4..0000000000000 --- a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/helpers.tsx +++ /dev/null @@ -1,143 +0,0 @@ -/* - * 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 { EuiLoadingSpinner } from '@elastic/eui'; -import { filter, get, pickBy } from 'lodash/fp'; -import styled from 'styled-components'; - -import { BrowserField, BrowserFields } from '../../../common/containers/source'; -import { - DEFAULT_CATEGORY_NAME, - defaultHeaders, -} from '../timeline/body/column_headers/default_headers'; - -export const LoadingSpinner = styled(EuiLoadingSpinner)` - cursor: pointer; - position: relative; - top: 3px; -`; - -LoadingSpinner.displayName = 'LoadingSpinner'; - -export const CATEGORY_PANE_WIDTH = 200; -export const DESCRIPTION_COLUMN_WIDTH = 300; -export const FIELD_COLUMN_WIDTH = 200; -export const FIELD_BROWSER_WIDTH = 900; -export const FIELD_BROWSER_HEIGHT = 300; -export const FIELDS_PANE_WIDTH = 670; -export const HEADER_HEIGHT = 40; -export const PANES_FLEX_GROUP_WIDTH = CATEGORY_PANE_WIDTH + FIELDS_PANE_WIDTH + 10; -export const SEARCH_INPUT_WIDTH = 850; -export const TABLE_HEIGHT = 260; -export const TYPE_COLUMN_WIDTH = 50; - -/** - * Returns the CSS class name for the title of a category shown in the left - * side field browser - */ -export const getCategoryPaneCategoryClassName = ({ - categoryId, - timelineId, -}: { - categoryId: string; - timelineId: string; -}): string => `field-browser-category-pane-${categoryId}-${timelineId}`; - -/** - * Returns the CSS class name for the title of a category shown in the right - * side of field browser - */ -export const getFieldBrowserCategoryTitleClassName = ({ - categoryId, - timelineId, -}: { - categoryId: string; - timelineId: string; -}): string => `field-browser-category-title-${categoryId}-${timelineId}`; - -/** Returns the class name for a field browser search input */ -export const getFieldBrowserSearchInputClassName = (timelineId: string): string => - `field-browser-search-input-${timelineId}`; - -/** Returns true if the specified category has at least one field */ -export const categoryHasFields = (category: Partial): boolean => - category.fields != null && Object.keys(category.fields).length > 0; - -/** Returns the count of fields in the specified category */ -export const getFieldCount = (category: Partial | undefined): number => - category != null && category.fields != null ? Object.keys(category.fields).length : 0; - -/** - * Filters the specified `BrowserFields` to return a new collection where every - * category contains at least one field name that matches the specified substring. - */ -export const filterBrowserFieldsByFieldName = ({ - browserFields, - substring, -}: { - browserFields: BrowserFields; - substring: string; -}): BrowserFields => { - const trimmedSubstring = substring.trim(); - - // filter each category such that it only contains fields with field names - // that contain the specified substring: - const filteredBrowserFields: BrowserFields = Object.keys(browserFields).reduce( - (filteredCategories, categoryId) => ({ - ...filteredCategories, - [categoryId]: { - ...browserFields[categoryId], - fields: filter( - (f) => f.name != null && f.name.includes(trimmedSubstring), - browserFields[categoryId].fields - ).reduce((filtered, field) => ({ ...filtered, [field.name!]: field }), {}), - }, - }), - {} - ); - - // only pick non-empty categories from the filtered browser fields - const nonEmptyCategories: BrowserFields = pickBy( - (category) => categoryHasFields(category), - filteredBrowserFields - ); - - return nonEmptyCategories; -}; - -/** - * Returns a "virtual" category (e.g. default ECS) from the specified fieldIds - */ -export const createVirtualCategory = ({ - browserFields, - fieldIds, -}: { - browserFields: BrowserFields; - fieldIds: string[]; -}): Partial => ({ - fields: fieldIds.reduce>>>((fields, fieldId) => { - const splitId = fieldId.split('.'); // source.geo.city_name -> [source, geo, city_name] - - return { - ...fields, - [fieldId]: { - ...get([splitId.length > 1 ? splitId[0] : 'base', 'fields', fieldId], browserFields), - name: fieldId, - }, - }; - }, {}), -}); - -/** Merges the specified browser fields with the default category (i.e. `default ECS`) */ -export const mergeBrowserFieldsWithDefaultCategory = ( - browserFields: BrowserFields -): BrowserFields => ({ - ...browserFields, - [DEFAULT_CATEGORY_NAME]: createVirtualCategory({ - browserFields, - fieldIds: defaultHeaders.map((header) => header.id), - }), -}); diff --git a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/index.test.tsx index 24dc806838d90..ed33883e6328d 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/index.test.tsx @@ -12,7 +12,7 @@ import { mockBrowserFields } from '../../../common/containers/source/mock'; import { TestProviders } from '../../../common/mock'; import { ColumnHeaderOptions } from '../../../timelines/store/timeline/model'; -import { FIELD_BROWSER_HEIGHT, FIELD_BROWSER_WIDTH } from './helpers'; +import { FIELD_BROWSER_HEIGHT, FIELD_BROWSER_WIDTH } from './constants'; import { StatefulFieldsBrowserComponent } from '.'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/index.tsx index bdb0fa1daf9aa..6990129c7ba9b 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/index.tsx @@ -5,50 +5,34 @@ */ import { EuiButtonEmpty, EuiToolTip } from '@elastic/eui'; -import { noop } from 'lodash/fp'; -import React, { useEffect, useRef, useState, useCallback, useMemo } from 'react'; +import React, { useState, useCallback } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import styled from 'styled-components'; import { State } from '../../../common/store'; -import { BrowserFields } from '../../../common/containers/source'; -import { ColumnHeaderOptions } from '../../../timelines/store/timeline/model'; + import { setExcludedRowRendererIds as dispatchSetExcludedRowRendererIds } from '../../../timelines/store/timeline/actions'; -import { DEFAULT_CATEGORY_NAME } from '../timeline/body/column_headers/default_headers'; import { RowRenderersBrowser } from './row_renderers_browser'; -import { filterBrowserFieldsByFieldName, mergeBrowserFieldsWithDefaultCategory } from './helpers'; import * as i18n from './translations'; import { FieldBrowserProps } from './types'; -const fieldsButtonClassName = 'fields-button'; - -/** wait this many ms after the user completes typing before applying the filter input */ -export const INPUT_TIMEOUT = 250; - const RowRenderersBrowserButtonContainer = styled.div` position: relative; `; RowRenderersBrowserButtonContainer.displayName = 'RowRenderersBrowserButtonContainer'; -/** - * Manages the state of the field browser - */ export const StatefulRowRenderersBrowserComponent: React.FC = ({ - columnHeaders, - browserFields, height, - isEventViewer = false, - onFieldSelected, - onUpdateColumns, timelineId, - toggleColumn, width, }) => { + console.error('timelineId', timelineId); const dispatch = useDispatch(); const excludedRowRendererIds = useSelector( (state: State) => state.timeline.timelineById[timelineId]?.excludedRowRendererIds || [] ); + const [show, setShow] = useState(false); const setExcludedRowRendererIds = useCallback( (payload) => @@ -58,143 +42,29 @@ export const StatefulRowRenderersBrowserComponent: React.FC = [dispatch, timelineId] ); - /** tracks the latest timeout id from `setTimeout`*/ - const inputTimeoutId = useRef(0); - - /** all field names shown in the field browser must contain this string (when specified) */ - const [filterInput, setFilterInput] = useState(''); - /** all fields in this collection have field names that match the filterInput */ - const [filteredBrowserFields, setFilteredBrowserFields] = useState(null); - /** when true, show a spinner in the input to indicate the field browser is searching for matching field names */ - const [isSearching, setIsSearching] = useState(false); - /** this category will be displayed in the right-hand pane of the field browser */ - const [selectedCategoryId, setSelectedCategoryId] = useState(DEFAULT_CATEGORY_NAME); - /** show the field browser */ - const [show, setShow] = useState(false); - useEffect(() => { - return () => { - if (inputTimeoutId.current !== 0) { - // ⚠️ mutation: cancel any remaining timers and zero-out the timer id: - clearTimeout(inputTimeoutId.current); - inputTimeoutId.current = 0; - } - }; - }, []); - - /** Shows / hides the field browser */ - const toggleShow = useCallback(() => { - setShow(!show); - }, [show]); - - /** Invoked when the user types in the filter input */ - const updateFilter = useCallback( - (newFilterInput: string) => { - setFilterInput(newFilterInput); - setIsSearching(true); - if (inputTimeoutId.current !== 0) { - clearTimeout(inputTimeoutId.current); // ⚠️ mutation: cancel any previous timers - } - // ⚠️ mutation: schedule a new timer that will apply the filter when it fires: - inputTimeoutId.current = window.setTimeout(() => { - const newFilteredBrowserFields = filterBrowserFieldsByFieldName({ - browserFields: mergeBrowserFieldsWithDefaultCategory(browserFields), - substring: newFilterInput, - }); - setFilteredBrowserFields(newFilteredBrowserFields); - setIsSearching(false); - - const newSelectedCategoryId = - newFilterInput === '' || Object.keys(newFilteredBrowserFields).length === 0 - ? DEFAULT_CATEGORY_NAME - : Object.keys(newFilteredBrowserFields) - .sort() - .reduce( - (selected, category) => - newFilteredBrowserFields[category].fields != null && - newFilteredBrowserFields[selected].fields != null && - Object.keys(newFilteredBrowserFields[category].fields!).length > - Object.keys(newFilteredBrowserFields[selected].fields!).length - ? category - : selected, - Object.keys(newFilteredBrowserFields)[0] - ); - setSelectedCategoryId(newSelectedCategoryId); - }, INPUT_TIMEOUT); - }, - // eslint-disable-next-line react-hooks/exhaustive-deps - [browserFields, filterInput, inputTimeoutId.current] - ); - - /** - * Invoked when the user clicks a category name in the left-hand side of - * the field browser - */ - const updateSelectedCategoryId = useCallback((categoryId: string) => { - setSelectedCategoryId(categoryId); - }, []); - - /** - * Invoked when the user clicks on the context menu to view a category's - * columns in the timeline, this function dispatches the action that - * causes the timeline display those columns. - */ - const updateColumnsAndSelectCategoryId = useCallback((columns: ColumnHeaderOptions[]) => { - onUpdateColumns(columns); // show the category columns in the timeline - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); + const toggleShow = useCallback(() => setShow(!show), [show]); - /** Invoked when the field browser should be hidden */ - const hideFieldBrowser = useCallback(() => { - setFilterInput(''); - setFilterInput(''); - setFilteredBrowserFields(null); - setIsSearching(false); - setSelectedCategoryId(DEFAULT_CATEGORY_NAME); - setShow(false); - }, []); - // only merge in the default category if the field browser is visible - const browserFieldsWithDefaultCategory = useMemo( - () => (show ? mergeBrowserFieldsWithDefaultCategory(browserFields) : {}), - [show, browserFields] - ); + const hideFieldBrowser = useCallback(() => setShow(false), []); return ( <> - + - {i18n.FIELDS} + {'Event Renderers'} {show && ( ` background-color: ${({ theme }) => theme.eui.euiColorLightestShade}; @@ -24,8 +34,6 @@ const FieldsBrowserContainer = styled.div<{ width: number }>` ${({ theme }) => theme.eui.euiColorMediumShade}; border-radius: ${({ theme }) => theme.eui.euiBorderRadius}; left: 0; - padding: ${({ theme }) => theme.eui.paddingSizes.s} ${({ theme }) => theme.eui.paddingSizes.s} - ${({ theme }) => theme.eui.paddingSizes.m}; position: absolute; top: calc(100% + ${({ theme }) => theme.eui.euiSize}); width: ${({ width }) => width}px; @@ -33,71 +41,22 @@ const FieldsBrowserContainer = styled.div<{ width: number }>` `; FieldsBrowserContainer.displayName = 'FieldsBrowserContainer'; -const PanesFlexGroup = styled(EuiFlexGroup)` - width: ${PANES_FLEX_GROUP_WIDTH}px; +const CloseButtonIcon = styled(EuiButtonIcon)` + position: absolute; + right: 0; + top: 0; `; -PanesFlexGroup.displayName = 'PanesFlexGroup'; interface RowRendererOption { id: RowRendererId; name: string; description: string; - example: React.ReactNode; + example?: React.ReactNode; } -type Props = Pick< - FieldBrowserProps, - | 'browserFields' - | 'isEventViewer' - | 'height' - | 'onFieldSelected' - | 'onUpdateColumns' - | 'timelineId' - | 'width' -> & { - /** - * The current timeline column headers - */ - columnHeaders: ColumnHeaderOptions[]; +type Props = Pick & { excludedRowRendererIds: RowRendererId[]; - /** - * A map of categoryId -> metadata about the fields in that category, - * filtered such that the name of every field in the category includes - * the filter input (as a substring). - */ - filteredBrowserFields: BrowserFields; - /** - * When true, a busy spinner will be shown to indicate the field browser - * is searching for fields that match the specified `searchInput` - */ - isSearching: boolean; - /** The text displayed in the search input */ - searchInput: string; - /** - * The category selected on the left-hand side of the field browser - */ - selectedCategoryId: string; - /** - * Invoked when the user clicks on the name of a category in the left-hand - * side of the field browser - */ - onCategorySelected: (categoryId: string) => void; - /** - * Hides the field browser when invoked - */ - onHideFieldBrowser: OnHideFieldBrowser; - /** - * Invoked when the user clicks outside of the field browser - */ onOutsideClick: () => void; - /** - * Invoked when the user types in the search input - */ - onSearchInputChange: (newSearchInput: string) => void; - /** - * Invoked to add or remove a column from the timeline - */ - toggleColumn: (column: ColumnHeaderOptions) => void; setExcludedRowRendererIds: (excludedRowRendererIds: RowRendererId[]) => void; }; @@ -107,12 +66,8 @@ type Props = Pick< */ const FieldsBrowserComponent: React.FC = ({ excludedRowRendererIds = [], - filteredBrowserFields, - isEventViewer, setExcludedRowRendererIds, - onFieldSelected, onOutsideClick, - timelineId, width, }) => { const columns = [ @@ -127,16 +82,6 @@ const FieldsBrowserComponent: React.FC = ({ name: 'Description', truncateText: true, }, - { - field: 'category', - name: 'Category', - truncateText: true, - }, - { - field: 'example', - name: 'Example', - render: () =>
EXAMPLE
, - }, ]; const search = { @@ -144,23 +89,6 @@ const FieldsBrowserComponent: React.FC = ({ incremental: true, schema: true, }, - // filters: !filters - // ? undefined - // : [ - // { - // type: 'is', - // field: 'online', - // name: 'Online', - // negatedName: 'Offline', - // }, - // { - // type: 'field_value_selection', - // field: 'nationality', - // name: 'Nationality', - // multiSelect: false, - // options: [], - // }, - // ], }; const renderers: RowRendererOption[] = [ @@ -168,85 +96,75 @@ const FieldsBrowserComponent: React.FC = ({ id: 'auditd', name: 'Auditd', description: 'Auditd Row Renderer', - example: () => <>, }, { id: 'auditd_file', name: 'Auditd File', description: 'Auditd Row Renderer', - example: () => <>, }, { id: 'system', name: 'System', description: 'System Row Renderer', - example: () => <>, }, { id: 'system_endgame_process', name: 'System Endgame Process', description: 'Endgame Process Row Renderer', - example: () => <>, }, { id: 'system_fin', name: 'System FIM', description: 'FIM Row Renderer', - example: () => <>, }, { id: 'system_file', name: 'System File', description: 'System File Row Renderer', - example: () => <>, }, { id: 'system_socket', name: 'System Socket', description: 'Auditd Row Renderer', - example: () => <>, }, { id: 'system_security_event', name: 'System Security Event', description: 'Auditd Row Renderer', - example: () => <>, }, { id: 'system_dns', name: 'System DNS', description: 'Auditd Row Renderer', - example: () => <>, }, { id: 'suricata', name: 'Suricata', description: 'Auditd Row Renderer', - example: () => <>, }, { id: 'zeek', name: 'Zeek', description: 'Auditd Row Renderer', - example: () => <>, }, { id: 'netflow', name: 'Netflow', description: 'Auditd Row Renderer', - example: () => <>, }, ]; const notExcludedRowRenderers = useMemo(() => { if (excludedRowRendererIds.includes('all')) return []; + console.error('test', excludedRowRendererIds); + return renderers; // return renderers.filter((renderer) => excludedRowRendererIds.includes(renderer.id)); }, [excludedRowRendererIds, renderers]); @@ -254,9 +172,7 @@ const FieldsBrowserComponent: React.FC = ({ const selectionValue = { selectable: () => true, selectableMessage: () => '', - onSelectionChange: (selection) => { - console.error('selection', selection); - + onSelectionChange: (selection: RowRendererOption[]) => { if (!selection || !selection.length) return setExcludedRowRendererIds(['all']); const excludedRowRenderers = xorBy('id', renderers, selection); @@ -266,35 +182,67 @@ const FieldsBrowserComponent: React.FC = ({ initialSelected: notExcludedRowRenderers, }; - console.error('rowRenderers', rowRenderers); - - const tableRef = useRef(); + const handleDisableAll = useCallback(() => setExcludedRowRendererIds(['all']), [ + setExcludedRowRendererIds, + ]); + const handleEnableAll = useCallback(() => setExcludedRowRendererIds([]), [ + setExcludedRowRendererIds, + ]); return ( -
- - + + + + + + {'Customize Row Renderers'} + + + + + + {'Disable All'} + + + + + + {'Enable All'} + + + + + + + + + + + + + + + + ); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/__snapshots__/index.test.tsx.snap index 03e4f4b5f0f2b..2fdff5c7ef69a 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/__snapshots__/index.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/__snapshots__/index.test.tsx.snap @@ -11,475 +11,7 @@ exports[`ColumnHeaders rendering renders correctly against snapshot 1`] = ` justifyContent="space-between" > - - - + ( return ( <> -
- - -
+ + + + + + + + + ( if (!excludedRowRendererIds) return rowRenderers; - console.error('dupa', rowRenderers, excludedRowRendererIds); - console.error( - 'enabled', - rowRenderers.filter((rowRenderer) => !excludedRowRendererIds.includes(rowRenderer.id)) - ); + // console.error('dupa', rowRenderers, excludedRowRendererIds); + // console.error( + // 'enabled', + // rowRenderers.filter((rowRenderer) => !excludedRowRendererIds.includes(rowRenderer.id)) + // ); return rowRenderers.filter((rowRenderer) => !excludedRowRendererIds.includes(rowRenderer.id)); }, [excludedRowRendererIds, showRowRenderers]); @@ -250,6 +250,7 @@ const makeMapStateToProps = () => { const getNotesByIds = appSelectors.notesByIdsSelector(); const mapStateToProps = (state: State, { browserFields, id }: OwnProps) => { const timeline: TimelineModel = getTimeline(state, id) ?? timelineDefaults; + console.error('mapStateTimeline', timeline, id); const { columns, eventIdToNoteIds, diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/actions.ts b/x-pack/plugins/security_solution/public/timelines/store/timeline/actions.ts index 1a63fa36a5251..569939cf825da 100644 --- a/x-pack/plugins/security_solution/public/timelines/store/timeline/actions.ts +++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/actions.ts @@ -58,6 +58,7 @@ export const createTimeline = actionCreator<{ start: number; end: number; }; + excludedRowRendererIds?: RowRendererId[]; filters?: Filter[]; columns: ColumnHeaderOptions[]; itemsPerPage?: number; diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/epic_local_storage.ts b/x-pack/plugins/security_solution/public/timelines/store/timeline/epic_local_storage.ts index b3d1db23ffae8..632525750c8d8 100644 --- a/x-pack/plugins/security_solution/public/timelines/store/timeline/epic_local_storage.ts +++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/epic_local_storage.ts @@ -16,6 +16,7 @@ import { removeColumn, upsertColumn, applyDeltaToColumnWidth, + setExcludedRowRendererIds, updateColumns, updateItemsPerPage, updateSort, @@ -30,6 +31,7 @@ const timelineActionTypes = [ updateColumns.type, updateItemsPerPage.type, updateSort.type, + setExcludedRowRendererIds.type, ]; export const isPageTimeline = (timelineId: string | undefined): boolean => diff --git a/x-pack/plugins/security_solution/server/lib/timeline/pick_saved_timeline.ts b/x-pack/plugins/security_solution/server/lib/timeline/pick_saved_timeline.ts index 40293e8b6bbff..041ac81dd94db 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/pick_saved_timeline.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/pick_saved_timeline.ts @@ -49,5 +49,7 @@ export const pickSavedTimeline = ( savedTimeline.status = TimelineStatus.active; } - if (!savedTimeline.showRowRe) return savedTimeline; + savedTimeline.excludedRowRendererIds = savedTimeline.excludedRowRendererIds ?? []; + + return savedTimeline; }; From 3b4e91b161c8e1513c4306d9e4cf483b94dd6961 Mon Sep 17 00:00:00 2001 From: Patryk Kopycinski Date: Tue, 23 Jun 2020 16:47:04 +0200 Subject: [PATCH 03/19] cleanup --- .../common/types/timeline/index.ts | 49 +------------------ .../security_solution/common/utility_types.ts | 11 +++++ .../public/graphql/introspection.json | 1 + .../security_solution/public/graphql/types.ts | 1 + .../row_renderers_browser.tsx | 30 ++++++------ .../renderers/auditd/generic_row_renderer.tsx | 6 ++- .../netflow/netflow_row_renderer.tsx | 3 +- .../body/renderers/plain_row_renderer.tsx | 4 +- .../suricata/suricata_row_renderer.tsx | 4 +- .../renderers/system/generic_row_renderer.tsx | 16 +++--- .../body/renderers/zeek/zeek_row_renderer.tsx | 4 +- .../server/graphql/timeline/schema.gql.ts | 1 + .../security_solution/server/graphql/types.ts | 1 + 13 files changed, 55 insertions(+), 76 deletions(-) diff --git a/x-pack/plugins/security_solution/common/types/timeline/index.ts b/x-pack/plugins/security_solution/common/types/timeline/index.ts index 371dbfeefa6c5..8472ea1977d0c 100644 --- a/x-pack/plugins/security_solution/common/types/timeline/index.ts +++ b/x-pack/plugins/security_solution/common/types/timeline/index.ts @@ -9,7 +9,7 @@ import * as runtimeTypes from 'io-ts'; import { SavedObjectsClient } from 'kibana/server'; -import { unionWithNullType } from '../../utility_types'; +import { stringEnum, unionWithNullType } from '../../utility_types'; import { NoteSavedObject, NoteSavedObjectToReturnRuntimeType } from './note'; import { PinnedEventToReturnSavedObjectRuntimeType, PinnedEventSavedObject } from './pinned_event'; @@ -151,17 +151,6 @@ export type TimelineStatusLiteralWithNull = runtimeTypes.TypeOf< typeof TimelineStatusLiteralWithNullRt >; -const stringEnum = (enumObj: T, enumName = 'enum') => - new runtimeTypes.Type( - enumName, - (u): u is T[keyof T] => Object.values(enumObj).includes(u), - (u, c) => - Object.values(enumObj).includes(u) - ? runtimeTypes.success(u as T[keyof T]) - : runtimeTypes.failure(u, c), - (a) => (a as unknown) as string - ); - export enum RowRendererId { auditd = 'auditd', auditd_file = 'auditd_file', @@ -181,42 +170,6 @@ export enum RowRendererId { export const RowRendererIdRuntimeType = stringEnum(RowRendererId, 'RowRendererId'); -// export const RowRendererIdRuntimeType = stringEnum({ -// auditd: 'auditd', -// auditd_file: 'auditd_file', -// netflow: 'netflow', -// plain: 'plain', -// suricata: 'suricata', -// system: 'system', -// system_dns: 'system_dns', -// system_endgame_process: 'system_endgame_process', -// system_file: 'system_file', -// system_fin: 'system_fin', -// system_security_event: 'system_security_event', -// system_socket: 'system_socket', -// zeek: 'zeek', -// all: 'all', -// }); - -// export const RowRendererIdRuntimeType = runtimeTypes.keyof({ -// auditd: null, -// auditd_file: null, -// netflow: null, -// plain: null, -// suricata: null, -// system: null, -// system_dns: null, -// system_endgame_process: null, -// system_file: null, -// system_fin: null, -// system_security_event: null, -// system_socket: null, -// zeek: null, -// all: null, -// }); - -// export type RowRendererId = runtimeTypes.TypeOf; - /* * Timeline Types */ diff --git a/x-pack/plugins/security_solution/common/utility_types.ts b/x-pack/plugins/security_solution/common/utility_types.ts index a12dd926a9181..43271dc40ba12 100644 --- a/x-pack/plugins/security_solution/common/utility_types.ts +++ b/x-pack/plugins/security_solution/common/utility_types.ts @@ -15,3 +15,14 @@ export interface DescriptionList { export const unionWithNullType = (type: T) => runtimeTypes.union([type, runtimeTypes.null]); + +export const stringEnum = (enumObj: T, enumName = 'enum') => + new runtimeTypes.Type( + enumName, + (u): u is T[keyof T] => Object.values(enumObj).includes(u), + (u, c) => + Object.values(enumObj).includes(u) + ? runtimeTypes.success(u as T[keyof T]) + : runtimeTypes.failure(u, c), + (a) => (a as unknown) as string + ); diff --git a/x-pack/plugins/security_solution/public/graphql/introspection.json b/x-pack/plugins/security_solution/public/graphql/introspection.json index 399745d7dc385..d276c2cb0b7b4 100644 --- a/x-pack/plugins/security_solution/public/graphql/introspection.json +++ b/x-pack/plugins/security_solution/public/graphql/introspection.json @@ -10098,6 +10098,7 @@ "isDeprecated": false, "deprecationReason": null }, + { "name": "plain", "description": "", "isDeprecated": false, "deprecationReason": null }, { "name": "suricata", "description": "", diff --git a/x-pack/plugins/security_solution/public/graphql/types.ts b/x-pack/plugins/security_solution/public/graphql/types.ts index d566a46037261..e45ca73ce4265 100644 --- a/x-pack/plugins/security_solution/public/graphql/types.ts +++ b/x-pack/plugins/security_solution/public/graphql/types.ts @@ -347,6 +347,7 @@ export enum RowRendererId { auditd = 'auditd', auditd_file = 'auditd_file', netflow = 'netflow', + plain = 'plain', suricata = 'suricata', system = 'system', system_dns = 'system_dns', diff --git a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/row_renderers_browser.tsx b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/row_renderers_browser.tsx index 29300b06d471d..cdcbfac4176d5 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/row_renderers_browser.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/row_renderers_browser.tsx @@ -93,75 +93,75 @@ const FieldsBrowserComponent: React.FC = ({ const renderers: RowRendererOption[] = [ { - id: 'auditd', + id: RowRendererId.auditd, name: 'Auditd', description: 'Auditd Row Renderer', }, { - id: 'auditd_file', + id: RowRendererId.auditd_file, name: 'Auditd File', description: 'Auditd Row Renderer', }, { - id: 'system', + id: RowRendererId.system, name: 'System', description: 'System Row Renderer', }, { - id: 'system_endgame_process', + id: RowRendererId.system_endgame_process, name: 'System Endgame Process', description: 'Endgame Process Row Renderer', }, { - id: 'system_fin', + id: RowRendererId.system_fin, name: 'System FIM', description: 'FIM Row Renderer', }, { - id: 'system_file', + id: RowRendererId.system_file, name: 'System File', description: 'System File Row Renderer', }, { - id: 'system_socket', + id: RowRendererId.system_socket, name: 'System Socket', description: 'Auditd Row Renderer', }, { - id: 'system_security_event', + id: RowRendererId.system_security_event, name: 'System Security Event', description: 'Auditd Row Renderer', }, { - id: 'system_dns', + id: RowRendererId.system_dns, name: 'System DNS', description: 'Auditd Row Renderer', }, { - id: 'suricata', + id: RowRendererId.suricata, name: 'Suricata', description: 'Auditd Row Renderer', }, { - id: 'zeek', + id: RowRendererId.zeek, name: 'Zeek', description: 'Auditd Row Renderer', }, { - id: 'netflow', + id: RowRendererId.netflow, name: 'Netflow', description: 'Auditd Row Renderer', }, ]; const notExcludedRowRenderers = useMemo(() => { - if (excludedRowRendererIds.includes('all')) return []; + if (excludedRowRendererIds.includes(RowRendererId.all)) return []; console.error('test', excludedRowRendererIds); @@ -173,7 +173,7 @@ const FieldsBrowserComponent: React.FC = ({ selectable: () => true, selectableMessage: () => '', onSelectionChange: (selection: RowRendererOption[]) => { - if (!selection || !selection.length) return setExcludedRowRendererIds(['all']); + if (!selection || !selection.length) return setExcludedRowRendererIds([RowRendererId.all]); const excludedRowRenderers = xorBy('id', renderers, selection); @@ -182,7 +182,7 @@ const FieldsBrowserComponent: React.FC = ({ initialSelected: notExcludedRowRenderers, }; - const handleDisableAll = useCallback(() => setExcludedRowRendererIds(['all']), [ + const handleDisableAll = useCallback(() => setExcludedRowRendererIds([RowRendererId.all]), [ setExcludedRowRendererIds, ]); const handleEnableAll = useCallback(() => setExcludedRowRendererIds([]), [ diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/auditd/generic_row_renderer.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/auditd/generic_row_renderer.tsx index 455d654c46866..3e7520f641f4a 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/auditd/generic_row_renderer.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/auditd/generic_row_renderer.tsx @@ -10,6 +10,8 @@ import { IconType } from '@elastic/eui'; import { get } from 'lodash/fp'; import React from 'react'; +import { RowRendererId } from '../../../../../../../common/types/timeline'; + import { RowRenderer, RowRendererContainer } from '../row_renderer'; import { AuditdGenericDetails } from './generic_details'; import { AuditdGenericFileDetails } from './generic_file_details'; @@ -22,7 +24,7 @@ export const createGenericAuditRowRenderer = ({ actionName: string; text: string; }): RowRenderer => ({ - id: 'auditd', + id: RowRendererId.auditd, isInstance: (ecs) => { const module: string | null | undefined = get('event.module[0]', ecs); const action: string | null | undefined = get('event.action[0]', ecs); @@ -55,7 +57,7 @@ export const createGenericFileRowRenderer = ({ text: string; fileIcon?: IconType; }): RowRenderer => ({ - id: 'auditd_file', + id: RowRendererId.auditd_file, isInstance: (ecs) => { const module: string | null | undefined = get('event.module[0]', ecs); const action: string | null | undefined = get('event.action[0]', ecs); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/netflow/netflow_row_renderer.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/netflow/netflow_row_renderer.tsx index 9ab7bc83348c9..795c914c3c9a0 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/netflow/netflow_row_renderer.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/netflow/netflow_row_renderer.tsx @@ -10,6 +10,7 @@ import { get } from 'lodash/fp'; import React from 'react'; import styled from 'styled-components'; +import { RowRendererId } from '../../../../../../../common/types/timeline'; import { asArrayIfExists } from '../../../../../../common/lib/helpers'; import { TLS_CLIENT_CERTIFICATE_FINGERPRINT_SHA1_FIELD_NAME, @@ -84,7 +85,7 @@ export const eventActionMatches = (eventAction: string | object | undefined | nu }; export const netflowRowRenderer: RowRenderer = { - id: 'netflow', + id: RowRendererId.netflow, isInstance: (ecs) => eventCategoryMatches(get(EVENT_CATEGORY_FIELD, ecs)) || eventActionMatches(get(EVENT_ACTION_FIELD, ecs)), diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/plain_row_renderer.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/plain_row_renderer.tsx index e424fe193bf51..0b860491918df 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/plain_row_renderer.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/plain_row_renderer.tsx @@ -6,6 +6,8 @@ import React from 'react'; +import { RowRendererId } from '../../../../../../common/types/timeline'; + import { RowRenderer } from './row_renderer'; const PlainRowRenderer = () => <>; @@ -13,7 +15,7 @@ const PlainRowRenderer = () => <>; PlainRowRenderer.displayName = 'PlainRowRenderer'; export const plainRowRenderer: RowRenderer = { - id: 'plain', + id: RowRendererId.plain, isInstance: (_) => true, renderRow: PlainRowRenderer, }; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/suricata/suricata_row_renderer.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/suricata/suricata_row_renderer.tsx index fe6c03fe8f5c0..242f63611f2ff 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/suricata/suricata_row_renderer.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/suricata/suricata_row_renderer.tsx @@ -9,11 +9,13 @@ import { get } from 'lodash/fp'; import React from 'react'; +import { RowRendererId } from '../../../../../../../common/types/timeline'; + import { RowRenderer, RowRendererContainer } from '../row_renderer'; import { SuricataDetails } from './suricata_details'; export const suricataRowRenderer: RowRenderer = { - id: 'suricata', + id: RowRendererId.suricata, isInstance: (ecs) => { const module: string | null | undefined = get('event.module[0]', ecs); return module != null && module.toLowerCase() === 'suricata'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/system/generic_row_renderer.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/system/generic_row_renderer.tsx index c677d32cb17a4..6137c8e30442b 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/system/generic_row_renderer.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/system/generic_row_renderer.tsx @@ -9,6 +9,8 @@ import { get } from 'lodash/fp'; import React from 'react'; +import { RowRendererId } from '../../../../../../../common/types/timeline'; + import { DnsRequestEventDetails } from '../dns/dns_request_event_details'; import { EndgameSecurityEventDetails } from '../endgame/endgame_security_event_details'; import { isFileEvent, isNillEmptyOrNotFinite } from '../helpers'; @@ -25,7 +27,7 @@ export const createGenericSystemRowRenderer = ({ actionName: string; text: string; }): RowRenderer => ({ - id: 'system', + id: RowRendererId.system, isInstance: (ecs) => { const module: string | null | undefined = get('event.module[0]', ecs); const action: string | null | undefined = get('event.action[0]', ecs); @@ -56,7 +58,7 @@ export const createEndgameProcessRowRenderer = ({ actionName: string; text: string; }): RowRenderer => ({ - id: 'system_file', + id: RowRendererId.system_file, isInstance: (ecs) => { const action: string | null | undefined = get('event.action[0]', ecs); const category: string | null | undefined = get('event.category[0]', ecs); @@ -88,7 +90,7 @@ export const createFimRowRenderer = ({ actionName: string; text: string; }): RowRenderer => ({ - id: 'system_fin', + id: RowRendererId.system_fin, isInstance: (ecs) => { const action: string | null | undefined = get('event.action[0]', ecs); const category: string | null | undefined = get('event.category[0]', ecs); @@ -120,7 +122,7 @@ export const createGenericFileRowRenderer = ({ actionName: string; text: string; }): RowRenderer => ({ - id: 'system_file', + id: RowRendererId.system_file, isInstance: (ecs) => { const module: string | null | undefined = get('event.module[0]', ecs); const action: string | null | undefined = get('event.action[0]', ecs); @@ -151,7 +153,7 @@ export const createSocketRowRenderer = ({ actionName: string; text: string; }): RowRenderer => ({ - id: 'system_socket', + id: RowRendererId.system_socket, isInstance: (ecs) => { const action: string | null | undefined = get('event.action[0]', ecs); return action != null && action.toLowerCase() === actionName; @@ -174,7 +176,7 @@ export const createSecurityEventRowRenderer = ({ }: { actionName: string; }): RowRenderer => ({ - id: 'system_security_event', + id: RowRendererId.system_security_event, isInstance: (ecs) => { const category: string | null | undefined = get('event.category[0]', ecs); const action: string | null | undefined = get('event.action[0]', ecs); @@ -198,7 +200,7 @@ export const createSecurityEventRowRenderer = ({ }); export const createDnsRowRenderer = (): RowRenderer => ({ - id: 'system_dns', + id: RowRendererId.system_dns, isInstance: (ecs) => { const dnsQuestionType: string | null | undefined = get('dns.question.type[0]', ecs); const dnsQuestionName: string | null | undefined = get('dns.question.name[0]', ecs); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/zeek/zeek_row_renderer.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/zeek/zeek_row_renderer.tsx index 037ea9556a50e..9bbb7a4090dea 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/zeek/zeek_row_renderer.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/zeek/zeek_row_renderer.tsx @@ -9,11 +9,13 @@ import { get } from 'lodash/fp'; import React from 'react'; +import { RowRendererId } from '../../../../../../../common/types/timeline'; + import { RowRenderer, RowRendererContainer } from '../row_renderer'; import { ZeekDetails } from './zeek_details'; export const zeekRowRenderer: RowRenderer = { - id: 'zeek', + id: RowRendererId.zeek, isInstance: (ecs) => { const module: string | null | undefined = get('event.module[0]', ecs); return module != null && module.toLowerCase() === 'zeek'; diff --git a/x-pack/plugins/security_solution/server/graphql/timeline/schema.gql.ts b/x-pack/plugins/security_solution/server/graphql/timeline/schema.gql.ts index 29e706175be23..90ab71c0507d2 100644 --- a/x-pack/plugins/security_solution/server/graphql/timeline/schema.gql.ts +++ b/x-pack/plugins/security_solution/server/graphql/timeline/schema.gql.ts @@ -140,6 +140,7 @@ export const timelineSchema = gql` auditd auditd_file netflow + plain suricata system system_dns diff --git a/x-pack/plugins/security_solution/server/graphql/types.ts b/x-pack/plugins/security_solution/server/graphql/types.ts index 9b917f15f2703..6d2e90693d384 100644 --- a/x-pack/plugins/security_solution/server/graphql/types.ts +++ b/x-pack/plugins/security_solution/server/graphql/types.ts @@ -349,6 +349,7 @@ export enum RowRendererId { auditd = 'auditd', auditd_file = 'auditd_file', netflow = 'netflow', + plain = 'plain', suricata = 'suricata', system = 'system', system_dns = 'system_dns', From 811703f0e23b8328e5439399a5c17ea3a6a13b99 Mon Sep 17 00:00:00 2001 From: Patryk Kopycinski Date: Wed, 24 Jun 2020 09:04:51 +0200 Subject: [PATCH 04/19] WIP --- .../row_renderers_browser/row_renderers_browser.tsx | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/row_renderers_browser.tsx b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/row_renderers_browser.tsx index cdcbfac4176d5..d6fe26b5beaf1 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/row_renderers_browser.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/row_renderers_browser.tsx @@ -211,13 +211,13 @@ const FieldsBrowserComponent: React.FC = ({ - + {'Disable All'} - + {'Enable All'} @@ -226,8 +226,6 @@ const FieldsBrowserComponent: React.FC = ({ - - From 9b3b6d5a0b3d11419420eac4b1dd68c64e9e39d9 Mon Sep 17 00:00:00 2001 From: Patryk Kopycinski Date: Mon, 29 Jun 2020 10:18:10 +0200 Subject: [PATCH 05/19] WIP --- .../drag_and_drop/draggable_wrapper.tsx | 255 ++++++++++-------- .../common/components/draggables/index.tsx | 61 +++-- .../row_renderers_browser/index.tsx | 34 +-- .../row_renderers_browser.tsx | 33 ++- 4 files changed, 214 insertions(+), 169 deletions(-) diff --git a/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper.tsx b/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper.tsx index 22b95f0d0c0e9..be55604228b86 100644 --- a/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper.tsx +++ b/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper.tsx @@ -56,6 +56,14 @@ const Wrapper = styled.div` [data-rbd-placeholder-context-id] { display: none !important; } + + ${({ disabled }) => + disabled && + ` + [data-rbd-draggable-id]:hover { + cursor: default; + } + `} `; Wrapper.displayName = 'Wrapper'; @@ -63,6 +71,7 @@ Wrapper.displayName = 'Wrapper'; const ProviderContentWrapper = styled.span` > span.euiToolTipAnchor { display: block; /* allow EuiTooltip content to be truncatable */ + white-space: nowrap; } `; @@ -74,6 +83,7 @@ type RenderFunctionProp = ( interface Props { dataProvider: DataProvider; + disabled?: boolean; inline?: boolean; render: RenderFunctionProp; truncate?: boolean; @@ -99,124 +109,137 @@ export const getStyle = ( }; }; -export const DraggableWrapper = React.memo( - ({ dataProvider, onFilterAdded, render, truncate }) => { - const [showTopN, setShowTopN] = useState(false); - const toggleTopN = useCallback(() => { - setShowTopN(!showTopN); - }, [setShowTopN, showTopN]); - - const [providerRegistered, setProviderRegistered] = useState(false); - - const dispatch = useDispatch(); - - const registerProvider = useCallback(() => { - if (!providerRegistered) { - dispatch(dragAndDropActions.registerProvider({ provider: dataProvider })); - setProviderRegistered(true); - } - }, [dispatch, providerRegistered, dataProvider]); - - const unRegisterProvider = useCallback( - () => dispatch(dragAndDropActions.unRegisterProvider({ id: dataProvider.id })), - [dispatch, dataProvider] - ); - - useEffect( - () => () => { - unRegisterProvider(); - }, - // eslint-disable-next-line react-hooks/exhaustive-deps - [] - ); - - const hoverContent = useMemo( - () => ( - - ), - [dataProvider, onFilterAdded, showTopN, toggleTopN] - ); - - const renderContent = useCallback( - () => ( - - - ( - -
- - {render(dataProvider, provided, snapshot)} - -
-
- )} - > - {(droppableProvided) => ( -
- = ({ + dataProvider, + onFilterAdded, + render, + truncate, + disabled, +}) => { + const [showTopN, setShowTopN] = useState(false); + const toggleTopN = useCallback(() => { + setShowTopN(!showTopN); + }, [setShowTopN, showTopN]); + + const [providerRegistered, setProviderRegistered] = useState(false); + + const dispatch = useDispatch(); + + const registerProvider = useCallback(() => { + if (!providerRegistered) { + dispatch(dragAndDropActions.registerProvider({ provider: dataProvider })); + setProviderRegistered(true); + } + }, [dispatch, providerRegistered, dataProvider]); + + const unRegisterProvider = useCallback( + () => dispatch(dragAndDropActions.unRegisterProvider({ id: dataProvider.id })), + [dispatch, dataProvider] + ); + + useEffect( + () => () => { + unRegisterProvider(); + }, + // eslint-disable-next-line react-hooks/exhaustive-deps + [] + ); + + const hoverContent = useMemo( + () => ( + + ), + [dataProvider, onFilterAdded, showTopN, toggleTopN] + ); + + const renderContent = useCallback( + () => ( + + + ( + +
+ - {(provided, snapshot) => ( - - {truncate && !snapshot.isDragging ? ( - - {render(dataProvider, provided, snapshot)} - - ) : ( - - {render(dataProvider, provided, snapshot)} - - )} - - )} - - {droppableProvided.placeholder} + {render(dataProvider, provided, snapshot)} +
- )} -
-
-
- ), - [dataProvider, render, registerProvider, truncate] - ); - - return ( - - ); - }, + + )} + > + {(droppableProvided) => ( +
+ + {(provided, snapshot) => ( + + {truncate && !snapshot.isDragging ? ( + + {render(dataProvider, provided, snapshot)} + + ) : ( + + {render(dataProvider, provided, snapshot)} + + )} + + )} + + {droppableProvided.placeholder} +
+ )} + + + + ), + [dataProvider, render, registerProvider, truncate] + ); + + if (true || disabled) return <>{renderContent()}; + + return ( + + ); +}; + +DraggableWrapperComponent.displayName = 'DraggableWrapperComponent'; + +export const DraggableWrapper = React.memo( + DraggableWrapperComponent, (prevProps, nextProps) => deepEqual(prevProps.dataProvider, nextProps.dataProvider) && prevProps.render !== nextProps.render && diff --git a/x-pack/plugins/security_solution/public/common/components/draggables/index.tsx b/x-pack/plugins/security_solution/public/common/components/draggables/index.tsx index fcf007a4cf1ba..40c3631b1fa9c 100644 --- a/x-pack/plugins/security_solution/public/common/components/draggables/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/draggables/index.tsx @@ -73,6 +73,7 @@ Content.displayName = 'Content'; * Draggable text (or an arbitrary visualization specified by `children`) * that's only displayed when the specified value is non-`null`. * + * @param disabled - disallows to drag the badge, used in customize row renderer example column * @param id - a unique draggable id, which typically follows the format `${contextId}-${eventId}-${field}-${value}` * @param field - the name of the field, e.g. `network.transport` * @param value - value of the field e.g. `tcp` @@ -83,7 +84,7 @@ Content.displayName = 'Content'; * @param queryValue - defaults to `value`, this query overrides the `queryMatch.value` used by the `DataProvider` that represents the data */ export const DefaultDraggable = React.memo( - ({ id, field, value, name, children, tooltipContent, queryValue }) => + ({ id, field, value, name, children, tooltipContent, queryValue, disabled }) => value != null ? ( & { * prevent a tooltip from being displayed, or pass arbitrary content * @param queryValue - defaults to `value`, this query overrides the `queryMatch.value` used by the `DataProvider` that represents the data */ -export const DraggableBadge = React.memo( - ({ - contextId, - eventId, - field, - value, - iconType, - name, - color = 'hollow', - children, - tooltipContent, - queryValue, - }) => - value != null ? ( - - - {children ? children : value !== '' ? value : getEmptyStringTag()} - - - ) : null -); +const DraggableBadgeComponent: React.FC = ({ + contextId, + eventId, + field, + value, + iconType, + name, + color = 'hollow', + children, + tooltipContent, + queryValue, +}) => + value != null ? ( + + + {children ? children : value !== '' ? value : getEmptyStringTag()} + + + ) : null; + +DraggableBadgeComponent.displayName = 'DraggableBadgeComponent'; +export const DraggableBadge = React.memo(DraggableBadgeComponent); DraggableBadge.displayName = 'DraggableBadge'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/index.tsx index c0385663785e7..85a1d739db5be 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/index.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiButtonEmpty, EuiToolTip } from '@elastic/eui'; +import { EuiButtonEmpty, EuiToolTip, EuiPopover } from '@elastic/eui'; import React, { useState, useCallback } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import styled from 'styled-components'; @@ -45,21 +45,23 @@ export const StatefulRowRenderersBrowserComponent: React.FC = const hideFieldBrowser = useCallback(() => setShow(false), []); + const button = ( + + + {'Event Renderers'} + + + ); + return ( <> - - - - {'Event Renderers'} - - - - {show && ( + + = excludedRowRendererIds={excludedRowRendererIds} setExcludedRowRendererIds={setExcludedRowRendererIds} /> - )} - + + ); }; diff --git a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/row_renderers_browser.tsx b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/row_renderers_browser.tsx index d6fe26b5beaf1..4da22d69e6355 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/row_renderers_browser.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/row_renderers_browser.tsx @@ -23,21 +23,23 @@ import React, { useCallback, useMemo } from 'react'; import { xorBy } from 'lodash/fp'; import styled from 'styled-components'; +import { getMockNetflowData } from '../../../common/mock/netflow'; import { RowRendererId } from '../../../../common/types/timeline'; // import { rowRenderers } from '../timeline/body/renderers'; +import { netflowRowRenderer } from '../timeline/body/renderers/netflow/netflow_row_renderer'; import { FieldBrowserProps } from './types'; const FieldsBrowserContainer = styled.div<{ width: number }>` - background-color: ${({ theme }) => theme.eui.euiColorLightestShade}; - border: ${({ theme }) => theme.eui.euiBorderWidthThin} solid - ${({ theme }) => theme.eui.euiColorMediumShade}; - border-radius: ${({ theme }) => theme.eui.euiBorderRadius}; - left: 0; - position: absolute; - top: calc(100% + ${({ theme }) => theme.eui.euiSize}); + // background-color: ${({ theme }) => theme.eui.euiColorLightestShade}; + // border: ${({ theme }) => theme.eui.euiBorderWidthThin} solid + // ${({ theme }) => theme.eui.euiColorMediumShade}; + // border-radius: ${({ theme }) => theme.eui.euiBorderRadius}; + // left: 0; + // position: absolute; + // top: calc(100% + ${({ theme }) => theme.eui.euiSize}); width: ${({ width }) => width}px; - z-index: 9990; + // z-index: 9990; `; FieldsBrowserContainer.displayName = 'FieldsBrowserContainer'; @@ -82,6 +84,20 @@ const FieldsBrowserComponent: React.FC = ({ name: 'Description', truncateText: true, }, + { + field: 'example', + name: 'Example', + truncateText: true, + render: () => ( +
+ {netflowRowRenderer.renderRow({ + browserFields: {}, + data: getMockNetflowData(), + timelineId: 'row-renderer-example', + })} +
+ ), + }, ]; const search = { @@ -96,6 +112,7 @@ const FieldsBrowserComponent: React.FC = ({ id: RowRendererId.auditd, name: 'Auditd', description: 'Auditd Row Renderer', + example: () => {}, }, { id: RowRendererId.auditd_file, From 51a10e57e157209d53db865b570777be736d2227 Mon Sep 17 00:00:00 2001 From: Patryk Kopycinski Date: Thu, 2 Jul 2020 12:42:54 +0200 Subject: [PATCH 06/19] modal --- .../drag_and_drop/draggable_wrapper.tsx | 8 +- .../__snapshots__/index.test.tsx.snap | 1 + .../common/components/draggables/index.tsx | 3 +- .../components/fields_browser/index.tsx | 1 - .../row_renderers_browser/examples/auditd.tsx | 28 ++ .../examples/auditd_file.tsx | 28 ++ .../row_renderers_browser/examples/index.tsx | 18 + .../examples/netflow.tsx | 21 + .../examples/suricata.tsx | 21 + .../row_renderers_browser/examples/system.tsx | 28 ++ .../examples/system_dns.tsx | 25 ++ .../examples/system_endgame_process.tsx | 28 ++ .../examples/system_file.tsx | 28 ++ .../examples/system_fim.tsx | 28 ++ .../examples/system_security_event.tsx | 27 ++ .../examples/system_socket.tsx | 27 ++ .../row_renderers_browser/examples/zeek.tsx | 21 + .../row_renderers_browser/index.tsx | 152 +++++-- .../row_renderers_browser.tsx | 395 ++++++++---------- .../components/row_renderers_browser/types.ts | 2 - .../components/timeline/body/index.tsx | 172 ++++---- 21 files changed, 728 insertions(+), 334 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/auditd.tsx create mode 100644 x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/auditd_file.tsx create mode 100644 x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/index.tsx create mode 100644 x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/netflow.tsx create mode 100644 x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/suricata.tsx create mode 100644 x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/system.tsx create mode 100644 x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/system_dns.tsx create mode 100644 x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/system_endgame_process.tsx create mode 100644 x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/system_file.tsx create mode 100644 x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/system_fim.tsx create mode 100644 x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/system_security_event.tsx create mode 100644 x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/system_socket.tsx create mode 100644 x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/zeek.tsx diff --git a/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper.tsx b/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper.tsx index be55604228b86..d46d30fbc0d8b 100644 --- a/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper.tsx +++ b/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper.tsx @@ -49,7 +49,11 @@ class DragDropErrorBoundary extends React.PureComponent { } } -const Wrapper = styled.div` +interface WrapperProps { + disabled: boolean; +} + +const Wrapper = styled.div` display: inline-block; max-width: 100%; @@ -229,7 +233,7 @@ const DraggableWrapperComponent: React.FC = ({ [dataProvider, render, registerProvider, truncate] ); - if (true || disabled) return <>{renderContent()}; + if (disabled) return <>{renderContent()}; return ( diff --git a/x-pack/plugins/security_solution/public/common/components/draggables/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/common/components/draggables/__snapshots__/index.test.tsx.snap index 93608a181adff..15b67c330a932 100644 --- a/x-pack/plugins/security_solution/public/common/components/draggables/__snapshots__/index.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/common/components/draggables/__snapshots__/index.test.tsx.snap @@ -2,6 +2,7 @@ exports[`draggables rendering it renders the default Badge 1`] = ` ( - ({ id, field, value, name, children, tooltipContent, queryValue, disabled }) => + ({ id, field, value, name, children, tooltipContent, queryValue }) => value != null ? ( { + const auditdRowRenderer = createGenericAuditRowRenderer({ + actionName: 'connected-to', + text: 'some text', + }); + + return ( + <> + {auditdRowRenderer.renderRow({ + browserFields: {}, + data: mockTimelineData[26].ecs, + timelineId: 'row-renderer-example', + })} + + ); +}; +export const AuditdExample = React.memo(AuditdExampleComponent); diff --git a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/auditd_file.tsx b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/auditd_file.tsx new file mode 100644 index 0000000000000..48e3b310ebb7f --- /dev/null +++ b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/auditd_file.tsx @@ -0,0 +1,28 @@ +/* + * 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 { mockTimelineData } from '../../../../common/mock/mock_timeline_data'; +import { createGenericFileRowRenderer } from '../../timeline/body/renderers/auditd/generic_row_renderer'; + +const AuditdFileExampleComponent: React.FC = () => { + const auditdFileRowRenderer = createGenericFileRowRenderer({ + actionName: 'opened-file', + text: 'some text', + }); + + return ( + <> + {auditdFileRowRenderer.renderRow({ + browserFields: {}, + data: mockTimelineData[27].ecs, + timelineId: 'row-renderer-example', + })} + + ); +}; +export const AuditdFileExample = React.memo(AuditdFileExampleComponent); diff --git a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/index.tsx new file mode 100644 index 0000000000000..3cc39a3bf7050 --- /dev/null +++ b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/index.tsx @@ -0,0 +1,18 @@ +/* + * 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. + */ + +export * from './auditd'; +export * from './auditd_file'; +export * from './netflow'; +export * from './suricata'; +export * from './system'; +export * from './system_dns'; +export * from './system_endgame_process'; +export * from './system_file'; +export * from './system_fim'; +export * from './system_security_event'; +export * from './system_socket'; +export * from './zeek'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/netflow.tsx b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/netflow.tsx new file mode 100644 index 0000000000000..8605fbff1a84e --- /dev/null +++ b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/netflow.tsx @@ -0,0 +1,21 @@ +/* + * 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 { getMockNetflowData } from '../../../../common/mock/netflow'; + +import { netflowRowRenderer } from '../../timeline/body/renderers/netflow/netflow_row_renderer'; + +const NetflowExampleComponent: React.FC = () => ( + <> + {netflowRowRenderer.renderRow({ + browserFields: {}, + data: getMockNetflowData(), + timelineId: 'row-renderer-example', + })} + +); +export const NetflowExample = React.memo(NetflowExampleComponent); diff --git a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/suricata.tsx b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/suricata.tsx new file mode 100644 index 0000000000000..ecaa2797bbd43 --- /dev/null +++ b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/suricata.tsx @@ -0,0 +1,21 @@ +/* + * 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 { mockTimelineData } from '../../../../common/mock/mock_timeline_data'; +import { suricataRowRenderer } from '../../timeline/body/renderers/suricata/suricata_row_renderer'; + +const SuricataExampleComponent: React.FC = () => ( + <> + {suricataRowRenderer.renderRow({ + browserFields: {}, + data: mockTimelineData[2].ecs, + timelineId: 'row-renderer-example', + })} + +); +export const SuricataExample = React.memo(SuricataExampleComponent); diff --git a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/system.tsx b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/system.tsx new file mode 100644 index 0000000000000..343cfa1098183 --- /dev/null +++ b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/system.tsx @@ -0,0 +1,28 @@ +/* + * 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 { mockTimelineData } from '../../../../common/mock/mock_timeline_data'; +import { createGenericSystemRowRenderer } from '../../timeline/body/renderers/system/generic_row_renderer'; + +const SystemExampleComponent: React.FC = () => { + const systemRowRenderer = createGenericSystemRowRenderer({ + actionName: 'process_started', + text: 'some text', + }); + + return ( + <> + {systemRowRenderer.renderRow({ + browserFields: {}, + data: mockTimelineData[29].ecs, + timelineId: 'row-renderer-example', + })} + + ); +}; +export const SystemExample = React.memo(SystemExampleComponent); diff --git a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/system_dns.tsx b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/system_dns.tsx new file mode 100644 index 0000000000000..b989cd5816002 --- /dev/null +++ b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/system_dns.tsx @@ -0,0 +1,25 @@ +/* + * 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 { createDnsRowRenderer } from '../../timeline/body/renderers/system/generic_row_renderer'; +import { mockEndgameDnsRequest } from '../../../../common/mock/mock_endgame_ecs_data'; + +const SystemDnsExampleComponent: React.FC = () => { + const systemDnsRowRenderer = createDnsRowRenderer(); + + return ( + <> + {systemDnsRowRenderer.renderRow({ + browserFields: {}, + data: mockEndgameDnsRequest, + timelineId: 'row-renderer-example', + })} + + ); +}; +export const SystemDnsExample = React.memo(SystemDnsExampleComponent); diff --git a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/system_endgame_process.tsx b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/system_endgame_process.tsx new file mode 100644 index 0000000000000..22b523a3374b9 --- /dev/null +++ b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/system_endgame_process.tsx @@ -0,0 +1,28 @@ +/* + * 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 { createEndgameProcessRowRenderer } from '../../timeline/body/renderers/system/generic_row_renderer'; +import { mockEndgameCreationEvent } from '../../../../common/mock/mock_endgame_ecs_data'; + +const SystemEndgameProcessExampleComponent: React.FC = () => { + const systemEndgameProcessRowRenderer = createEndgameProcessRowRenderer({ + actionName: 'creation_event', + text: 'started process', + }); + + return ( + <> + {systemEndgameProcessRowRenderer.renderRow({ + browserFields: {}, + data: mockEndgameCreationEvent, + timelineId: 'row-renderer-example', + })} + + ); +}; +export const SystemEndgameProcessExample = React.memo(SystemEndgameProcessExampleComponent); diff --git a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/system_file.tsx b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/system_file.tsx new file mode 100644 index 0000000000000..5479b553b7172 --- /dev/null +++ b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/system_file.tsx @@ -0,0 +1,28 @@ +/* + * 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 { mockTimelineData } from '../../../../common/mock/mock_timeline_data'; +import { createGenericFileRowRenderer } from '../../timeline/body/renderers/system/generic_row_renderer'; + +const SystemFileExampleComponent: React.FC = () => { + const systemFileRowRenderer = createGenericFileRowRenderer({ + actionName: 'user_login', + text: 'some text', + }); + + return ( + <> + {systemFileRowRenderer.renderRow({ + browserFields: {}, + data: mockTimelineData[28].ecs, + timelineId: 'row-renderer-example', + })} + + ); +}; +export const SystemFileExample = React.memo(SystemFileExampleComponent); diff --git a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/system_fim.tsx b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/system_fim.tsx new file mode 100644 index 0000000000000..d40f1aad10820 --- /dev/null +++ b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/system_fim.tsx @@ -0,0 +1,28 @@ +/* + * 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 { mockEndgameFileCreateEvent } from '../../../../common/mock/mock_endgame_ecs_data'; +import { createFimRowRenderer } from '../../timeline/body/renderers/system/generic_row_renderer'; + +const SystemFimExampleComponent: React.FC = () => { + const systemFimRowRenderer = createFimRowRenderer({ + actionName: 'file_create_event', + text: 'created a file', + }); + + return ( + <> + {systemFimRowRenderer.renderRow({ + browserFields: {}, + data: mockEndgameFileCreateEvent, + timelineId: 'row-renderer-example', + })} + + ); +}; +export const SystemFimExample = React.memo(SystemFimExampleComponent); diff --git a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/system_security_event.tsx b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/system_security_event.tsx new file mode 100644 index 0000000000000..9075fb3ad41c4 --- /dev/null +++ b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/system_security_event.tsx @@ -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 from 'react'; + +import { createSecurityEventRowRenderer } from '../../timeline/body/renderers/system/generic_row_renderer'; +import { mockEndgameUserLogon } from '../../../../common/mock/mock_endgame_ecs_data'; + +const SystemSecurityEventExampleComponent: React.FC = () => { + const systemSecurityEventRowRenderer = createSecurityEventRowRenderer({ + actionName: 'user_logon', + }); + + return ( + <> + {systemSecurityEventRowRenderer.renderRow({ + browserFields: {}, + data: mockEndgameUserLogon, + timelineId: 'row-renderer-example', + })} + + ); +}; +export const SystemSecurityEventExample = React.memo(SystemSecurityEventExampleComponent); diff --git a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/system_socket.tsx b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/system_socket.tsx new file mode 100644 index 0000000000000..a592d32e93079 --- /dev/null +++ b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/system_socket.tsx @@ -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 from 'react'; + +import { createSocketRowRenderer } from '../../timeline/body/renderers/system/generic_row_renderer'; +import { mockEndgameIpv4ConnectionAcceptEvent } from '../../../../common/mock/mock_endgame_ecs_data'; + +const SystemSocketExampleComponent: React.FC = () => { + const systemSocketRowRenderer = createSocketRowRenderer({ + actionName: 'ipv4_connection_accept_event', + text: 'accepted a connection via', + }); + return ( + <> + {systemSocketRowRenderer.renderRow({ + browserFields: {}, + data: mockEndgameIpv4ConnectionAcceptEvent, + timelineId: 'row-renderer-example', + })} + + ); +}; +export const SystemSocketExample = React.memo(SystemSocketExampleComponent); diff --git a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/zeek.tsx b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/zeek.tsx new file mode 100644 index 0000000000000..980b2cd4d1a0d --- /dev/null +++ b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/zeek.tsx @@ -0,0 +1,21 @@ +/* + * 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 { mockTimelineData } from '../../../../common/mock/mock_timeline_data'; +import { zeekRowRenderer } from '../../timeline/body/renderers/zeek/zeek_row_renderer'; + +const ZeekExampleComponent: React.FC = () => ( + <> + {zeekRowRenderer.renderRow({ + browserFields: {}, + data: mockTimelineData[13].ecs, + timelineId: 'row-renderer-example', + })} + +); +export const ZeekExample = React.memo(ZeekExampleComponent); diff --git a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/index.tsx index 85a1d739db5be..8ca92ca94be1a 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/index.tsx @@ -4,28 +4,76 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiButtonEmpty, EuiToolTip, EuiPopover } from '@elastic/eui'; +import { + EuiButtonEmpty, + EuiToolTip, + EuiOverlayMask, + EuiModal, + EuiModalHeader, + EuiModalHeaderTitle, + EuiModalBody, + EuiButton, + EuiFlexGroup, + EuiFlexItem, + EuiButtonIcon, +} from '@elastic/eui'; import React, { useState, useCallback } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import styled from 'styled-components'; import { State } from '../../../common/store'; +import { RowRendererId } from '../../../../common/types/timeline'; import { setExcludedRowRendererIds as dispatchSetExcludedRowRendererIds } from '../../../timelines/store/timeline/actions'; import { RowRenderersBrowser } from './row_renderers_browser'; import * as i18n from './translations'; import { FieldBrowserProps } from './types'; +const CloseButtonIcon = styled(EuiButtonIcon)` + position: absolute; + right: 0; + top: 0; +`; + const RowRenderersBrowserButtonContainer = styled.div` position: relative; `; RowRenderersBrowserButtonContainer.displayName = 'RowRenderersBrowserButtonContainer'; -export const StatefulRowRenderersBrowserComponent: React.FC = ({ - height, +const StyledEuiModal = styled(EuiModal)` + margin: 0 auto; +`; + +const StyledEuiModalBody = styled(EuiModalBody)` + .euiModalBody__overflow { + display: flex; + align-items: stretch; + overflow: hidden; + + > div { + display: flex; + flex-direction: column; + flex: 1; + + > div:first-child { + flex: 0; + } + + .euiBasicTable { + flex: 1; + overflow: auto; + } + } + } +`; + +interface StatefulRowRenderersBrowserProps { + timelineId: string; +} + +export const StatefulRowRenderersBrowserComponent: React.FC = ({ timelineId, - width, }) => { const dispatch = useDispatch(); const excludedRowRendererIds = useSelector( @@ -36,7 +84,10 @@ export const StatefulRowRenderersBrowserComponent: React.FC = const setExcludedRowRendererIds = useCallback( (payload) => dispatch( - dispatchSetExcludedRowRendererIds({ id: timelineId, excludedRowRendererIds: payload }) + dispatchSetExcludedRowRendererIds({ + id: timelineId, + excludedRowRendererIds: payload, + }) ), [dispatch, timelineId] ); @@ -45,33 +96,76 @@ export const StatefulRowRenderersBrowserComponent: React.FC = const hideFieldBrowser = useCallback(() => setShow(false), []); - const button = ( - - - {'Event Renderers'} - - - ); + const handleDisableAll = useCallback(() => setExcludedRowRendererIds([RowRendererId.all]), [ + setExcludedRowRendererIds, + ]); + const handleEnableAll = useCallback(() => setExcludedRowRendererIds([]), [ + setExcludedRowRendererIds, + ]); return ( <> - - - - - + + + {'Event Renderers'} + + + + {show && ( + + + + + + + {'Customize Row Renderers'} + + + + + + {'Disable All'} + + + + + + {'Enable All'} + + + + + + + + + + + + + )} ); }; diff --git a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/row_renderers_browser.tsx b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/row_renderers_browser.tsx index 4da22d69e6355..4cb5d7a034d1a 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/row_renderers_browser.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/row_renderers_browser.tsx @@ -6,48 +6,27 @@ /* eslint-disable react/display-name */ -import { - EuiFlexGroup, - EuiFlexItem, - EuiButtonIcon, - EuiOutsideClickDetector, - EuiInMemoryTable, - EuiSpacer, - EuiModalHeader, - EuiModalHeaderTitle, - EuiModalBody, - EuiButton, - EuiButtonEmpty, -} from '@elastic/eui'; -import React, { useCallback, useMemo } from 'react'; +import { EuiFlexItem, EuiInMemoryTable } from '@elastic/eui'; +import React, { useMemo, useCallback } from 'react'; import { xorBy } from 'lodash/fp'; import styled from 'styled-components'; -import { getMockNetflowData } from '../../../common/mock/netflow'; import { RowRendererId } from '../../../../common/types/timeline'; -// import { rowRenderers } from '../timeline/body/renderers'; -import { netflowRowRenderer } from '../timeline/body/renderers/netflow/netflow_row_renderer'; - import { FieldBrowserProps } from './types'; - -const FieldsBrowserContainer = styled.div<{ width: number }>` - // background-color: ${({ theme }) => theme.eui.euiColorLightestShade}; - // border: ${({ theme }) => theme.eui.euiBorderWidthThin} solid - // ${({ theme }) => theme.eui.euiColorMediumShade}; - // border-radius: ${({ theme }) => theme.eui.euiBorderRadius}; - // left: 0; - // position: absolute; - // top: calc(100% + ${({ theme }) => theme.eui.euiSize}); - width: ${({ width }) => width}px; - // z-index: 9990; -`; -FieldsBrowserContainer.displayName = 'FieldsBrowserContainer'; - -const CloseButtonIcon = styled(EuiButtonIcon)` - position: absolute; - right: 0; - top: 0; -`; +import { + AuditdExample, + AuditdFileExample, + NetflowExample, + SuricataExample, + SystemExample, + SystemDnsExample, + SystemEndgameProcessExample, + SystemFileExample, + SystemFimExample, + SystemSecurityEventExample, + SystemSocketExample, + ZeekExample, +} from './examples'; interface RowRendererOption { id: RowRendererId; @@ -56,210 +35,198 @@ interface RowRendererOption { example?: React.ReactNode; } -type Props = Pick & { +type Props = Pick & { excludedRowRendererIds: RowRendererId[]; - onOutsideClick: () => void; setExcludedRowRendererIds: (excludedRowRendererIds: RowRendererId[]) => void; }; -/** - * This component has no internal state, but it uses lifecycle methods to - * set focus to the search input, scroll to the selected category, etc - */ -const FieldsBrowserComponent: React.FC = ({ - excludedRowRendererIds = [], - setExcludedRowRendererIds, - onOutsideClick, - width, -}) => { - const columns = [ - { - field: 'name', - name: 'Name', - sortable: true, - truncateText: true, - }, - { - field: 'description', - name: 'Description', - truncateText: true, - }, - { - field: 'example', - name: 'Example', - truncateText: true, - render: () => ( -
- {netflowRowRenderer.renderRow({ - browserFields: {}, - data: getMockNetflowData(), - timelineId: 'row-renderer-example', - })} -
- ), - }, - ]; +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const StyledEuiInMemoryTable = styled(EuiInMemoryTable as any)` + .euiTable { + width: auto; - const search = { - box: { - incremental: true, - schema: true, - }, - }; + .euiTableHeaderCellCheckbox > .euiTableCellContent { + display: none; // we don't want to display checkbox in the table + } + } +`; - const renderers: RowRendererOption[] = [ - { - id: RowRendererId.auditd, - name: 'Auditd', - description: 'Auditd Row Renderer', - example: () => {}, - }, - { - id: RowRendererId.auditd_file, - name: 'Auditd File', - description: 'Auditd Row Renderer', - }, - { - id: RowRendererId.system, - name: 'System', - description: 'System Row Renderer', - }, +const StyledEuiFlexItem = styled(EuiFlexItem)` + > div { + padding: 0; - { - id: RowRendererId.system_endgame_process, - name: 'System Endgame Process', - description: 'Endgame Process Row Renderer', - }, + > div { + margin: 0; + } + } +`; - { - id: RowRendererId.system_fin, - name: 'System FIM', - description: 'FIM Row Renderer', - }, +const ExampleWrapperComponent = (Example?: React.ReactElement) => { + if (!Example) return; - { - id: RowRendererId.system_file, - name: 'System File', - description: 'System File Row Renderer', - }, + return ( + + + + ); +}; - { - id: RowRendererId.system_socket, - name: 'System Socket', - description: 'Auditd Row Renderer', - }, +const search = { + box: { + incremental: true, + schema: true, + }, +}; - { - id: RowRendererId.system_security_event, - name: 'System Security Event', - description: 'Auditd Row Renderer', - }, +const renderers: RowRendererOption[] = [ + { + id: RowRendererId.auditd, + name: 'Auditd', + description: 'Auditd Row Renderer', + example: AuditdExample, + }, + { + id: RowRendererId.auditd_file, + name: 'Auditd File', + description: 'Auditd File Row Renderer', + example: AuditdFileExample, + }, + { + id: RowRendererId.system, + name: 'System', + description: 'System Row Renderer', + example: SystemExample, + }, + + { + id: RowRendererId.system_endgame_process, + name: 'System Endgame Process', + description: 'Endgame Process Row Renderer', + example: SystemEndgameProcessExample, + }, + + { + id: RowRendererId.system_fin, + name: 'System FIM', + description: 'FIM Row Renderer', + example: SystemFimExample, + }, + { + id: RowRendererId.system_file, + name: 'System File', + description: 'System File Row Renderer', + example: SystemFileExample, + }, + { + id: RowRendererId.system_socket, + name: 'System Socket', + description: 'System Socket Row Renderer', + example: SystemSocketExample, + }, + + { + id: RowRendererId.system_security_event, + name: 'System Security Event', + description: 'System Security Event Row Renderer', + example: SystemSecurityEventExample, + }, + + { + id: RowRendererId.system_dns, + name: 'System DNS', + description: 'System DNS Row Renderer', + example: SystemDnsExample, + }, + + { + id: RowRendererId.suricata, + name: 'Suricata', + description: 'Suricata Row Renderer', + example: SuricataExample, + }, + { + id: RowRendererId.zeek, + name: 'Zeek', + description: 'Zeek Row Renderer', + example: ZeekExample, + }, + { + id: RowRendererId.netflow, + name: 'Netflow', + description: 'Netflow Row Renderer', + example: NetflowExample, + }, +]; - { - id: RowRendererId.system_dns, - name: 'System DNS', - description: 'Auditd Row Renderer', - }, - { - id: RowRendererId.suricata, - name: 'Suricata', - description: 'Auditd Row Renderer', - }, - { - id: RowRendererId.zeek, - name: 'Zeek', - description: 'Auditd Row Renderer', - }, - { - id: RowRendererId.netflow, - name: 'Netflow', - description: 'Auditd Row Renderer', - }, - ]; +const FieldsBrowserComponent: React.FC = ({ + excludedRowRendererIds = [], + setExcludedRowRendererIds, +}) => { + const columns = useMemo( + () => [ + { + field: 'name', + name: 'Name', + sortable: true, + truncateText: true, + width: '15%', + }, + { + field: 'description', + name: 'Description', + truncateText: true, + width: '20%', + }, + { + field: 'example', + name: 'Example', + width: '65%', + render: ExampleWrapperComponent, + }, + ], + [] + ); const notExcludedRowRenderers = useMemo(() => { if (excludedRowRendererIds.includes(RowRendererId.all)) return []; - console.error('test', excludedRowRendererIds); + return renderers.filter((renderer) => !excludedRowRendererIds.includes(renderer.id)); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); - return renderers; - // return renderers.filter((renderer) => excludedRowRendererIds.includes(renderer.id)); - }, [excludedRowRendererIds, renderers]); + const handleSelectable = useCallback(() => true, []); - const selectionValue = { - selectable: () => true, - selectableMessage: () => '', - onSelectionChange: (selection: RowRendererOption[]) => { + const handleSelectionChange = useCallback( + (selection: RowRendererOption[]) => { if (!selection || !selection.length) return setExcludedRowRendererIds([RowRendererId.all]); const excludedRowRenderers = xorBy('id', renderers, selection); setExcludedRowRendererIds(excludedRowRenderers.map((rowRenderer) => rowRenderer.id)); }, - initialSelected: notExcludedRowRenderers, - }; + [setExcludedRowRendererIds] + ); - const handleDisableAll = useCallback(() => setExcludedRowRendererIds([RowRendererId.all]), [ - setExcludedRowRendererIds, - ]); - const handleEnableAll = useCallback(() => setExcludedRowRendererIds([]), [ - setExcludedRowRendererIds, - ]); + const selectionValue = useMemo( + () => ({ + selectable: handleSelectable, + onSelectionChange: handleSelectionChange, + initialSelected: notExcludedRowRenderers, + }), + // eslint-disable-next-line react-hooks/exhaustive-deps + [handleSelectable, handleSelectionChange] + ); return ( - - - - - - - - {'Customize Row Renderers'} - - - - - - {'Disable All'} - - - - - - {'Enable All'} - - - - - - - - - - - - - - - - + ); }; diff --git a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/types.ts b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/types.ts index 2b9889ec13e79..ae09779f5de14 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/types.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/types.ts @@ -32,6 +32,4 @@ export interface FieldBrowserProps { timelineId: string; /** Adds or removes a column to / from the timeline */ toggleColumn: (column: ColumnHeaderOptions) => void; - /** The width of the field browser */ - width: number; } diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.tsx index 6dca11779e421..b70e80e63d603 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.tsx @@ -138,91 +138,95 @@ export const Body = React.memo( ); return ( - <> - - - - - - - - - - {showGraphView(graphEventId) && ( - - )} - - - + + +
+ + + + + + + + +
+ + {showGraphView(graphEventId) && ( + + )} + + + - - - - - + +
+
+ + + ); } ); From a645f812a951c657d59cc99962e119ecea967be6 Mon Sep 17 00:00:00 2001 From: Patryk Kopycinski Date: Thu, 2 Jul 2020 21:59:17 +0200 Subject: [PATCH 07/19] tableref --- .../row_renderers_browser/index.tsx | 50 +++--- .../row_renderers_browser.tsx | 147 +++++++++--------- 2 files changed, 96 insertions(+), 101 deletions(-) diff --git a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/index.tsx index 8ca92ca94be1a..b12114686a7d6 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/index.tsx @@ -17,7 +17,7 @@ import { EuiFlexItem, EuiButtonIcon, } from '@elastic/eui'; -import React, { useState, useCallback } from 'react'; +import React, { useState, useCallback, useRef } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import styled from 'styled-components'; @@ -29,20 +29,13 @@ import { RowRenderersBrowser } from './row_renderers_browser'; import * as i18n from './translations'; import { FieldBrowserProps } from './types'; -const CloseButtonIcon = styled(EuiButtonIcon)` - position: absolute; - right: 0; - top: 0; -`; - -const RowRenderersBrowserButtonContainer = styled.div` - position: relative; -`; - -RowRenderersBrowserButtonContainer.displayName = 'RowRenderersBrowserButtonContainer'; - const StyledEuiModal = styled(EuiModal)` margin: 0 auto; + max-width: 95vw; + + > .euiModal__flex { + max-height: 95vh; + } `; const StyledEuiModalBody = styled(EuiModalBody)` @@ -68,13 +61,19 @@ const StyledEuiModalBody = styled(EuiModalBody)` } `; +const StyledEuiOverlayMask = styled(EuiOverlayMask)` + z-index: 8001; + padding-bottom: 0; +`; + interface StatefulRowRenderersBrowserProps { timelineId: string; } -export const StatefulRowRenderersBrowserComponent: React.FC = ({ +const StatefulRowRenderersBrowserComponent: React.FC = ({ timelineId, }) => { + const tableRef = useRef(); const dispatch = useDispatch(); const excludedRowRendererIds = useSelector( (state: State) => state.timeline.timelineById[timelineId]?.excludedRowRendererIds || [] @@ -96,12 +95,15 @@ export const StatefulRowRenderersBrowserComponent: React.FC setShow(false), []); - const handleDisableAll = useCallback(() => setExcludedRowRendererIds([RowRendererId.all]), [ - setExcludedRowRendererIds, - ]); - const handleEnableAll = useCallback(() => setExcludedRowRendererIds([]), [ - setExcludedRowRendererIds, - ]); + const handleDisableAll = useCallback(() => { + setExcludedRowRendererIds([RowRendererId.all]); + tableRef.current.setSelection([]); + }, [setExcludedRowRendererIds]); + + const handleEnableAll = useCallback(() => { + setExcludedRowRendererIds([]); + tableRef.current.setSelection([]); + }, [setExcludedRowRendererIds]); return ( <> @@ -117,9 +119,8 @@ export const StatefulRowRenderersBrowserComponent: React.FC {show && ( - - - + + - + )} ); diff --git a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/row_renderers_browser.tsx b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/row_renderers_browser.tsx index 4cb5d7a034d1a..17202869339ba 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/row_renderers_browser.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/row_renderers_browser.tsx @@ -7,7 +7,7 @@ /* eslint-disable react/display-name */ import { EuiFlexItem, EuiInMemoryTable } from '@elastic/eui'; -import React, { useMemo, useCallback } from 'react'; +import React, { useMemo, useCallback, useRef } from 'react'; import { xorBy } from 'lodash/fp'; import styled from 'styled-components'; @@ -78,7 +78,7 @@ const search = { }, }; -const renderers: RowRendererOption[] = [ +export const renderers: RowRendererOption[] = [ { id: RowRendererId.auditd, name: 'Auditd', @@ -97,14 +97,12 @@ const renderers: RowRendererOption[] = [ description: 'System Row Renderer', example: SystemExample, }, - { id: RowRendererId.system_endgame_process, name: 'System Endgame Process', description: 'Endgame Process Row Renderer', example: SystemEndgameProcessExample, }, - { id: RowRendererId.system_fin, name: 'System FIM', @@ -123,21 +121,18 @@ const renderers: RowRendererOption[] = [ description: 'System Socket Row Renderer', example: SystemSocketExample, }, - { id: RowRendererId.system_security_event, name: 'System Security Event', description: 'System Security Event Row Renderer', example: SystemSecurityEventExample, }, - { id: RowRendererId.system_dns, name: 'System DNS', description: 'System DNS Row Renderer', example: SystemDnsExample, }, - { id: RowRendererId.suricata, name: 'Suricata', @@ -158,76 +153,74 @@ const renderers: RowRendererOption[] = [ }, ]; -const FieldsBrowserComponent: React.FC = ({ - excludedRowRendererIds = [], - setExcludedRowRendererIds, -}) => { - const columns = useMemo( - () => [ - { - field: 'name', - name: 'Name', - sortable: true, - truncateText: true, - width: '15%', - }, - { - field: 'description', - name: 'Description', - truncateText: true, - width: '20%', +const FieldsBrowserComponent: React.FC = React.forwardRef( + ({ excludedRowRendererIds = [], setExcludedRowRendererIds }, ref) => { + const columns = useMemo( + () => [ + { + field: 'name', + name: 'Name', + sortable: true, + truncateText: true, + width: '15%', + }, + { + field: 'description', + name: 'Description', + truncateText: true, + width: '20%', + }, + { + field: 'example', + name: 'Example', + width: '65%', + render: ExampleWrapperComponent, + }, + ], + [] + ); + + const notExcludedRowRenderers = useMemo(() => { + if (excludedRowRendererIds.includes(RowRendererId.all)) return []; + + return renderers.filter((renderer) => !excludedRowRendererIds.includes(renderer.id)); + }, [excludedRowRendererIds]); + + const handleSelectable = useCallback(() => true, []); + + const handleSelectionChange = useCallback( + (selection: RowRendererOption[]) => { + if (!selection || !selection.length) return setExcludedRowRendererIds([RowRendererId.all]); + + const excludedRowRenderers = xorBy('id', renderers, selection); + + setExcludedRowRendererIds(excludedRowRenderers.map((rowRenderer) => rowRenderer.id)); }, - { - field: 'example', - name: 'Example', - width: '65%', - render: ExampleWrapperComponent, - }, - ], - [] - ); - - const notExcludedRowRenderers = useMemo(() => { - if (excludedRowRendererIds.includes(RowRendererId.all)) return []; - - return renderers.filter((renderer) => !excludedRowRendererIds.includes(renderer.id)); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - const handleSelectable = useCallback(() => true, []); - - const handleSelectionChange = useCallback( - (selection: RowRendererOption[]) => { - if (!selection || !selection.length) return setExcludedRowRendererIds([RowRendererId.all]); - - const excludedRowRenderers = xorBy('id', renderers, selection); - - setExcludedRowRendererIds(excludedRowRenderers.map((rowRenderer) => rowRenderer.id)); - }, - [setExcludedRowRendererIds] - ); - - const selectionValue = useMemo( - () => ({ - selectable: handleSelectable, - onSelectionChange: handleSelectionChange, - initialSelected: notExcludedRowRenderers, - }), - // eslint-disable-next-line react-hooks/exhaustive-deps - [handleSelectable, handleSelectionChange] - ); - - return ( - - ); -}; + [setExcludedRowRendererIds] + ); + + const selectionValue = useMemo( + () => ({ + selectable: handleSelectable, + onSelectionChange: handleSelectionChange, + initialSelected: notExcludedRowRenderers, + }), + [handleSelectable, handleSelectionChange, notExcludedRowRenderers] + ); + + return ( + + ); + } +); export const RowRenderersBrowser = React.memo(FieldsBrowserComponent); From deab0157451c230a047af34e0ad996c6d28372b5 Mon Sep 17 00:00:00 2001 From: Andrew Goldstein Date: Fri, 3 Jul 2020 16:04:24 -0600 Subject: [PATCH 08/19] * Added `i18n` descriptions and links to documentation * Added a subtitle: `Event Renderers automatically convey the most relevant details in an event to reveal its story` * Renamed some renderers --- .../row_renderers_browser/catalog/index.tsx | 199 ++++++++++++++++ .../catalog/translations.ts | 215 ++++++++++++++++++ .../row_renderers_browser/index.tsx | 6 +- .../row_renderers_browser.tsx | 139 ++++------- .../row_renderers_browser/translations.ts | 15 ++ 5 files changed, 471 insertions(+), 103 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/catalog/index.tsx create mode 100644 x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/catalog/translations.ts diff --git a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/catalog/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/catalog/index.tsx new file mode 100644 index 0000000000000..c59390155ba8e --- /dev/null +++ b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/catalog/index.tsx @@ -0,0 +1,199 @@ +/* + * 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 { EuiLink } from '@elastic/eui'; +import React from 'react'; +import { ExternalLinkIcon } from '../../../../common/components/external_link_icon'; + +import { RowRendererId } from '../../../../../common/types/timeline'; +import { + AuditdExample, + AuditdFileExample, + NetflowExample, + SuricataExample, + SystemExample, + SystemDnsExample, + SystemEndgameProcessExample, + SystemFileExample, + SystemFimExample, + SystemSecurityEventExample, + SystemSocketExample, + ZeekExample, +} from '../examples'; +import * as i18n from './translations'; + +const Link = ({ children, url }: { children: React.ReactNode; url: string }) => ( + + {children} + + +); + +export interface RowRendererOption { + id: RowRendererId; + name: string; + description: React.ReactNode; + searchableDescription: string; + example: React.ReactNode; +} + +export const renderers: RowRendererOption[] = [ + { + id: RowRendererId.auditd, + name: i18n.AUDITD_NAME, + description: ( + + + {i18n.AUDITD_NAME} + {' '} + {i18n.AUDITD_DESCRIPTION_PART1} + + ), + example: AuditdExample, // TODO: replace "some text" in this example + searchableDescription: `${i18n.AUDITD_NAME} ${i18n.AUDITD_DESCRIPTION_PART1}`, + }, + { + id: RowRendererId.auditd_file, + name: i18n.AUDITD_FILE_NAME, + description: ( + + + {i18n.AUDITD_NAME} + {' '} + {i18n.AUDITD_FILE_DESCRIPTION_PART1} + + ), + example: AuditdFileExample, // TODO: replace both the `unset` session ID and "some text" in this example + searchableDescription: `${i18n.AUDITD_FILE_NAME} ${i18n.AUDITD_FILE_DESCRIPTION_PART1}`, + }, + { + id: RowRendererId.system_security_event, + name: i18n.AUTHENTICATION_NAME, + description: ( +
+

{i18n.AUTHENTICATION_DESCRIPTION_PART1}

+
+

{i18n.AUTHENTICATION_DESCRIPTION_PART2}

+
+ ), + example: SystemSecurityEventExample, + searchableDescription: `${i18n.AUTHENTICATION_DESCRIPTION_PART1} ${i18n.AUTHENTICATION_DESCRIPTION_PART2}`, + }, + { + id: RowRendererId.system_dns, + name: i18n.DNS_NAME, + description: i18n.DNS_DESCRIPTION_PART1, + example: SystemDnsExample, + searchableDescription: i18n.DNS_DESCRIPTION_PART1, + }, + { + id: RowRendererId.netflow, + name: i18n.FLOW_NAME, + description: ( +
+

{i18n.FLOW_DESCRIPTION_PART1}

+
+

{i18n.FLOW_DESCRIPTION_PART2}

+
+ ), + example: NetflowExample, + searchableDescription: `${i18n.FLOW_DESCRIPTION_PART1} ${i18n.FLOW_DESCRIPTION_PART2}`, + }, + { + id: RowRendererId.system, + name: i18n.SYSTEM_NAME, + description: ( +
+

+ {i18n.SYSTEM_DESCRIPTION_PART1}{' '} + + {i18n.SYSTEM_NAME} + {' '} + {i18n.SYSTEM_DESCRIPTION_PART2} +

+
+

{i18n.SYSTEM_DESCRIPTION_PART3}

+
+ ), + example: SystemExample, // TODO: replace this example with data from `event.category: process and event.module: system` + searchableDescription: `${i18n.SYSTEM_DESCRIPTION_PART1} ${i18n.SYSTEM_NAME} ${i18n.SYSTEM_DESCRIPTION_PART2} ${i18n.SYSTEM_DESCRIPTION_PART3}`, + }, + { + id: RowRendererId.system_endgame_process, + name: i18n.PROCESS, + description: ( +
+

{i18n.PROCESS_DESCRIPTION_PART1}

+
+

{i18n.PROCESS_DESCRIPTION_PART2}

+
+ ), + example: SystemEndgameProcessExample, + searchableDescription: `${i18n.PROCESS_DESCRIPTION_PART1} ${i18n.PROCESS_DESCRIPTION_PART2}`, + }, + { + id: RowRendererId.system_fin, // TODO: this `id` should be "FIM" + name: i18n.FIM_NAME, + description: i18n.FIM_DESCRIPTION_PART1, + example: SystemFimExample, + searchableDescription: i18n.FIM_DESCRIPTION_PART1, + }, + { + id: RowRendererId.system_file, + name: i18n.FILE_NAME, + description: i18n.FILE_DESCRIPTION_PART1, + example: SystemFileExample, // TODO: replace this example with a real CRUD example, similar to FIM + searchableDescription: i18n.FILE_DESCRIPTION_PART1, + }, + { + id: RowRendererId.system_socket, + name: i18n.SOCKET_NAME, + description: ( +
+

{i18n.SOCKET_DESCRIPTION_PART1}

+
+

{i18n.SOCKET_DESCRIPTION_PART2}

+
+ ), + example: SystemSocketExample, + searchableDescription: `${i18n.SOCKET_DESCRIPTION_PART1} ${i18n.SOCKET_DESCRIPTION_PART2}`, + }, + { + id: RowRendererId.suricata, + name: 'Suricata', + description: ( +

+ {i18n.SURICATA_DESCRIPTION_PART1}{' '} + + {i18n.SURICATA_NAME} + {' '} + {i18n.SURICATA_DESCRIPTION_PART2} +

+ ), + example: SuricataExample, + searchableDescription: `${i18n.SURICATA_DESCRIPTION_PART1} ${i18n.SURICATA_NAME} ${i18n.SURICATA_DESCRIPTION_PART2}`, + }, + { + id: RowRendererId.zeek, + name: i18n.ZEEK_NAME, + description: ( +

+ {i18n.ZEEK_DESCRIPTION_PART1}{' '} + + {i18n.ZEEK_NAME} + {' '} + {i18n.ZEEK_DESCRIPTION_PART2} +

+ ), + example: ZeekExample, + searchableDescription: `${i18n.ZEEK_DESCRIPTION_PART1} ${i18n.ZEEK_NAME} ${i18n.ZEEK_DESCRIPTION_PART2}`, + }, +]; diff --git a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/catalog/translations.ts b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/catalog/translations.ts new file mode 100644 index 0000000000000..456e8c1c574ce --- /dev/null +++ b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/catalog/translations.ts @@ -0,0 +1,215 @@ +/* + * 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 { i18n } from '@kbn/i18n'; + +export const AUDITD_NAME = i18n.translate('xpack.securitySolution.eventRenderers.auditdName', { + defaultMessage: 'auditd', +}); + +export const AUDITD_DESCRIPTION_PART1 = i18n.translate( + 'xpack.securitySolution.eventRenderers.auditdDescriptionPart1', + { + defaultMessage: 'audit events convey security-relevant logs from the Linux Audit Framework.', + } +); + +export const AUDITD_FILE_NAME = i18n.translate( + 'xpack.securitySolution.eventRenderers.auditdFileName', + { + defaultMessage: 'auditd File', + } +); + +export const AUDITD_FILE_DESCRIPTION_PART1 = i18n.translate( + 'xpack.securitySolution.eventRenderers.auditdFileDescriptionPart1', + { + defaultMessage: + 'File events show users (and system accounts) performing CRUD operations on files via specific processes.', + } +); + +export const AUTHENTICATION_NAME = i18n.translate( + 'xpack.securitySolution.eventRenderers.authenticationName', + { + defaultMessage: 'Authentication', + } +); + +export const AUTHENTICATION_DESCRIPTION_PART1 = i18n.translate( + 'xpack.securitySolution.eventRenderers.authenticationDescriptionPart1', + { + defaultMessage: + 'Authentication events show users (and system accounts) successfully or unsuccessfully logging into hosts.', + } +); + +export const AUTHENTICATION_DESCRIPTION_PART2 = i18n.translate( + 'xpack.securitySolution.eventRenderers.authenticationDescriptionPart2', + { + defaultMessage: + 'Some authentication events may include additional details when users authenticate on behalf of other users.', + } +); + +export const DNS_NAME = i18n.translate('xpack.securitySolution.eventRenderers.dnsName', { + defaultMessage: 'Domain Name System (DNS)', +}); + +export const DNS_DESCRIPTION_PART1 = i18n.translate( + 'xpack.securitySolution.eventRenderers.dnsDescriptionPart1', + { + defaultMessage: + 'Domain Name System (DNS) events show users (and system accounts) making requests via specific processes to translate from host names to IP addresses.', + } +); + +export const FILE_NAME = i18n.translate('xpack.securitySolution.eventRenderers.fileName', { + defaultMessage: 'File', +}); + +export const FILE_DESCRIPTION_PART1 = i18n.translate( + 'xpack.securitySolution.eventRenderers.fileDescriptionPart1', + { + defaultMessage: + 'File events show users (and system accounts) performing CRUD operations on files via specific processes.', + } +); + +export const FIM_NAME = i18n.translate('xpack.securitySolution.eventRenderers.fimName', { + defaultMessage: 'File Integrity Module (FIM)', +}); + +export const FIM_DESCRIPTION_PART1 = i18n.translate( + 'xpack.securitySolution.eventRenderers.fimDescriptionPart1', + { + defaultMessage: + 'File Integrity Module (FIM) events show users (and system accounts) performing CRUD operations on files via specific processes.', + } +); + +export const FLOW_NAME = i18n.translate('xpack.securitySolution.eventRenderers.flowName', { + defaultMessage: 'Flow', +}); + +export const FLOW_DESCRIPTION_PART1 = i18n.translate( + 'xpack.securitySolution.eventRenderers.flowDescriptionPart1', + { + defaultMessage: + "The Flow renderer visualizes the flow of data between a source and destination. It's applicable to many types of events.", + } +); + +export const FLOW_DESCRIPTION_PART2 = i18n.translate( + 'xpack.securitySolution.eventRenderers.flowDescriptionPart2', + { + defaultMessage: + 'The hosts, ports, protocol, direction, duration, amount transferred, process, geographic location, and other details are visualized when available.', + } +); + +export const PROCESS = i18n.translate('xpack.securitySolution.eventRenderers.processName', { + defaultMessage: 'Process', +}); + +export const PROCESS_DESCRIPTION_PART1 = i18n.translate( + 'xpack.securitySolution.eventRenderers.processDescriptionPart1', + { + defaultMessage: + 'Process events show users (and system accounts) starting and stopping processes.', + } +); + +export const PROCESS_DESCRIPTION_PART2 = i18n.translate( + 'xpack.securitySolution.eventRenderers.processDescriptionPart2', + { + defaultMessage: + 'Details including the command line arguments, parent process, and if applicable, file hashes are displayed when available.', + } +); + +export const SOCKET_NAME = i18n.translate('xpack.securitySolution.eventRenderers.socketName', { + defaultMessage: 'Socket (Network)', +}); + +export const SOCKET_DESCRIPTION_PART1 = i18n.translate( + 'xpack.securitySolution.eventRenderers.socketDescriptionPart1', + { + defaultMessage: + 'Socket (Network) events show processes listening, accepting, and closing connections.', + } +); + +export const SOCKET_DESCRIPTION_PART2 = i18n.translate( + 'xpack.securitySolution.eventRenderers.socketDescriptionPart2', + { + defaultMessage: + 'Details including the protocol, ports, and a community ID for correlating all network events related to a single flow are displayed when available.', + } +); + +export const SURICATA_NAME = i18n.translate('xpack.securitySolution.eventRenderers.suricataName', { + defaultMessage: 'Suricata', +}); + +export const SURICATA_DESCRIPTION_PART1 = i18n.translate( + 'xpack.securitySolution.eventRenderers.suricataDescriptionPart1', + { + defaultMessage: 'Summarizes', + } +); + +export const SURICATA_DESCRIPTION_PART2 = i18n.translate( + 'xpack.securitySolution.eventRenderers.suricataDescriptionPart2', + { + defaultMessage: + 'intrusion detection (IDS), inline intrusion prevention (IPS), and network security monitoring (NSM) events', + } +); + +export const SYSTEM_NAME = i18n.translate('xpack.securitySolution.eventRenderers.systemName', { + defaultMessage: 'System', +}); + +export const SYSTEM_DESCRIPTION_PART1 = i18n.translate( + 'xpack.securitySolution.eventRenderers.systemDescriptionPart1', + { + defaultMessage: 'The Auditbeat', + } +); + +export const SYSTEM_DESCRIPTION_PART2 = i18n.translate( + 'xpack.securitySolution.eventRenderers.systemDescriptionPart2', + { + defaultMessage: 'module collects various security related information about a system.', + } +); + +export const SYSTEM_DESCRIPTION_PART3 = i18n.translate( + 'xpack.securitySolution.eventRenderers.systemDescriptionPart3', + { + defaultMessage: + 'All datasets send both periodic state information (e.g. all currently running processes) and real-time changes (e.g. when a new process starts or stops).', + } +); + +export const ZEEK_NAME = i18n.translate('xpack.securitySolution.eventRenderers.zeekName', { + defaultMessage: 'Zeek (formerly Bro)', +}); + +export const ZEEK_DESCRIPTION_PART1 = i18n.translate( + 'xpack.securitySolution.eventRenderers.zeekDescriptionPart1', + { + defaultMessage: 'Summarizes events from the', + } +); + +export const ZEEK_DESCRIPTION_PART2 = i18n.translate( + 'xpack.securitySolution.eventRenderers.zeekDescriptionPart2', + { + defaultMessage: 'Network Security Monitoring (NSM) tool', + } +); diff --git a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/index.tsx index b12114686a7d6..3f1aad4dc312e 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/index.tsx @@ -6,6 +6,7 @@ import { EuiButtonEmpty, + EuiText, EuiToolTip, EuiOverlayMask, EuiModal, @@ -15,7 +16,6 @@ import { EuiButton, EuiFlexGroup, EuiFlexItem, - EuiButtonIcon, } from '@elastic/eui'; import React, { useState, useCallback, useRef } from 'react'; import { useDispatch, useSelector } from 'react-redux'; @@ -27,7 +27,6 @@ import { RowRendererId } from '../../../../common/types/timeline'; import { setExcludedRowRendererIds as dispatchSetExcludedRowRendererIds } from '../../../timelines/store/timeline/actions'; import { RowRenderersBrowser } from './row_renderers_browser'; import * as i18n from './translations'; -import { FieldBrowserProps } from './types'; const StyledEuiModal = styled(EuiModal)` margin: 0 auto; @@ -129,7 +128,8 @@ const StatefulRowRenderersBrowserComponent: React.FC - {'Customize Row Renderers'} + {i18n.CUSTOMIZE_EVENT_RENDERERS_TITLE} + {i18n.CUSTOMIZE_EVENT_RENDERERS_DESCRIPTION} diff --git a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/row_renderers_browser.tsx b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/row_renderers_browser.tsx index 17202869339ba..c35b1bb42238b 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/row_renderers_browser.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/row_renderers_browser.tsx @@ -7,33 +7,14 @@ /* eslint-disable react/display-name */ import { EuiFlexItem, EuiInMemoryTable } from '@elastic/eui'; -import React, { useMemo, useCallback, useRef } from 'react'; +import React, { useMemo, useCallback, useState } from 'react'; import { xorBy } from 'lodash/fp'; import styled from 'styled-components'; import { RowRendererId } from '../../../../common/types/timeline'; +import { renderers, RowRendererOption } from './catalog'; import { FieldBrowserProps } from './types'; -import { - AuditdExample, - AuditdFileExample, - NetflowExample, - SuricataExample, - SystemExample, - SystemDnsExample, - SystemEndgameProcessExample, - SystemFileExample, - SystemFimExample, - SystemSecurityEventExample, - SystemSocketExample, - ZeekExample, -} from './examples'; - -interface RowRendererOption { - id: RowRendererId; - name: string; - description: string; - example?: React.ReactNode; -} +import { OnTableChangeParams } from '../open_timeline/types'; type Props = Pick & { excludedRowRendererIds: RowRendererId[]; @@ -78,83 +59,34 @@ const search = { }, }; -export const renderers: RowRendererOption[] = [ - { - id: RowRendererId.auditd, - name: 'Auditd', - description: 'Auditd Row Renderer', - example: AuditdExample, - }, - { - id: RowRendererId.auditd_file, - name: 'Auditd File', - description: 'Auditd File Row Renderer', - example: AuditdFileExample, - }, - { - id: RowRendererId.system, - name: 'System', - description: 'System Row Renderer', - example: SystemExample, - }, - { - id: RowRendererId.system_endgame_process, - name: 'System Endgame Process', - description: 'Endgame Process Row Renderer', - example: SystemEndgameProcessExample, - }, - { - id: RowRendererId.system_fin, - name: 'System FIM', - description: 'FIM Row Renderer', - example: SystemFimExample, - }, - { - id: RowRendererId.system_file, - name: 'System File', - description: 'System File Row Renderer', - example: SystemFileExample, - }, - { - id: RowRendererId.system_socket, - name: 'System Socket', - description: 'System Socket Row Renderer', - example: SystemSocketExample, - }, - { - id: RowRendererId.system_security_event, - name: 'System Security Event', - description: 'System Security Event Row Renderer', - example: SystemSecurityEventExample, - }, - { - id: RowRendererId.system_dns, - name: 'System DNS', - description: 'System DNS Row Renderer', - example: SystemDnsExample, - }, - { - id: RowRendererId.suricata, - name: 'Suricata', - description: 'Suricata Row Renderer', - example: SuricataExample, - }, - { - id: RowRendererId.zeek, - name: 'Zeek', - description: 'Zeek Row Renderer', - example: ZeekExample, - }, - { - id: RowRendererId.netflow, - name: 'Netflow', - description: 'Netflow Row Renderer', - example: NetflowExample, - }, -]; +/** + * Since `searchableDescription` contains raw text to power the Search bar, + * this "noop" function ensures it's not actually rendered + */ +const renderSearchableDescriptionNoop = () => null; const FieldsBrowserComponent: React.FC = React.forwardRef( ({ excludedRowRendererIds = [], setExcludedRowRendererIds }, ref) => { + const [sortField, setSortField] = useState('name'); + const [sortDirection, setSortDirection] = useState('asc'); + + const onTableChange = useCallback( + ({ page, sort }: OnTableChangeParams) => { + const { field, direction } = sort; + setSortDirection(direction); + setSortField(field); + }, + [setSortField, setSortDirection] + ); + + const sort = useMemo( + () => ({ + sortField, + sortDirection, + }), + [sortField, sortDirection] + ); + const columns = useMemo( () => [ { @@ -162,13 +94,13 @@ const FieldsBrowserComponent: React.FC = React.forwardRef( name: 'Name', sortable: true, truncateText: true, - width: '15%', + width: '10%', }, { field: 'description', name: 'Description', - truncateText: true, - width: '20%', + width: '25%', + render: (description: React.ReactNode) => description, }, { field: 'example', @@ -176,6 +108,12 @@ const FieldsBrowserComponent: React.FC = React.forwardRef( width: '65%', render: ExampleWrapperComponent, }, + { + field: 'searchableDescription', + sortable: false, + width: '0px', + render: renderSearchableDescriptionNoop, + }, ], [] ); @@ -215,9 +153,10 @@ const FieldsBrowserComponent: React.FC = React.forwardRef( itemId="id" columns={columns} search={search} - sorting={true} + sorting={sort} isSelectable={true} selection={selectionValue} + onTableChange={onTableChange} /> ); } diff --git a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/translations.ts b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/translations.ts index 0d078d99075c7..19798360e7890 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/translations.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/translations.ts @@ -28,6 +28,21 @@ export const CUSTOMIZE_COLUMNS = i18n.translate( } ); +export const CUSTOMIZE_EVENT_RENDERERS_TITLE = i18n.translate( + 'xpack.securitySolution.customizeEventRenderers.customizeEventRenderersTitle', + { + defaultMessage: 'Customize Event Renderers', + } +); + +export const CUSTOMIZE_EVENT_RENDERERS_DESCRIPTION = i18n.translate( + 'xpack.securitySolution.customizeEventRenderers.customizeEventRenderersDescription', + { + defaultMessage: + 'Event Renderers automatically convey the most relevant details in an event to reveal its story', + } +); + export const DESCRIPTION = i18n.translate('xpack.securitySolution.fieldBrowser.descriptionLabel', { defaultMessage: 'Description', }); From cc23fe4c1022601b828145792881379cdcf84125 Mon Sep 17 00:00:00 2001 From: Patryk Kopycinski Date: Tue, 7 Jul 2020 09:17:12 +0200 Subject: [PATCH 09/19] cleanup all --- .../common/types/timeline/index.ts | 1 - .../alerts_table/alerts_utility_bar/index.tsx | 8 +- .../alerts/components/alerts_table/index.tsx | 30 ++-- .../drag_and_drop/draggable_wrapper.tsx | 2 +- .../common/components/events_viewer/index.tsx | 2 +- .../public/graphql/introspection.json | 1 - .../security_solution/public/graphql/types.ts | 1 - .../catalog/translations.ts | 4 +- .../row_renderers_browser/index.tsx | 23 ++- .../row_renderers_browser.tsx | 20 ++- .../row_renderers_browser/translations.ts | 35 ++-- .../components/timeline/body/index.tsx | 167 ++++++++---------- .../timeline/body/stateful_body.tsx | 8 +- .../timelines/store/timeline/helpers.ts | 2 +- .../server/graphql/timeline/schema.gql.ts | 1 - .../security_solution/server/graphql/types.ts | 1 - 16 files changed, 148 insertions(+), 158 deletions(-) diff --git a/x-pack/plugins/security_solution/common/types/timeline/index.ts b/x-pack/plugins/security_solution/common/types/timeline/index.ts index d894f71ee8ca7..9d730130f7721 100644 --- a/x-pack/plugins/security_solution/common/types/timeline/index.ts +++ b/x-pack/plugins/security_solution/common/types/timeline/index.ts @@ -167,7 +167,6 @@ export enum RowRendererId { system_security_event = 'system_security_event', system_socket = 'system_socket', zeek = 'zeek', - all = 'all', } export const RowRendererIdRuntimeType = stringEnum(RowRendererId, 'RowRendererId'); diff --git a/x-pack/plugins/security_solution/public/alerts/components/alerts_table/alerts_utility_bar/index.tsx b/x-pack/plugins/security_solution/public/alerts/components/alerts_table/alerts_utility_bar/index.tsx index 0ceb2c87dd5ea..6533be1a9b09c 100644 --- a/x-pack/plugins/security_solution/public/alerts/components/alerts_table/alerts_utility_bar/index.tsx +++ b/x-pack/plugins/security_solution/public/alerts/components/alerts_table/alerts_utility_bar/index.tsx @@ -39,6 +39,10 @@ interface AlertsUtilityBarProps { updateAlertsStatus: UpdateAlertsStatus; } +const UtilityBarFlexGroup = styled(EuiFlexGroup)` + min-width: 175px; +`; + const AlertsUtilityBarComponent: React.FC = ({ canUserCRUD, hasIndexWrite, @@ -69,10 +73,6 @@ const AlertsUtilityBarComponent: React.FC = ({ defaultNumberFormat ); - const UtilityBarFlexGroup = styled(EuiFlexGroup)` - min-width: 175px; - `; - const UtilityBarPopoverContent = (closePopover: () => void) => ( {currentFilter !== FILTER_OPEN && ( diff --git a/x-pack/plugins/security_solution/public/alerts/components/alerts_table/index.tsx b/x-pack/plugins/security_solution/public/alerts/components/alerts_table/index.tsx index 98bb6434ddafd..cba383e1b88d2 100644 --- a/x-pack/plugins/security_solution/public/alerts/components/alerts_table/index.tsx +++ b/x-pack/plugins/security_solution/public/alerts/components/alerts_table/index.tsx @@ -258,22 +258,20 @@ export const AlertsTableComponent: React.FC = ({ // Callback for creating the AlertsUtilityBar which receives totalCount from EventsViewer component const utilityBarCallback = useCallback( - (refetchQuery: inputsModel.Refetch, totalCount: number) => { - return ( - 0} - clearSelection={clearSelectionCallback} - hasIndexWrite={hasIndexWrite} - currentFilter={filterGroup} - selectAll={selectAllCallback} - selectedEventIds={selectedEventIds} - showClearSelection={showClearSelectionAction} - totalCount={totalCount} - updateAlertsStatus={updateAlertsStatusCallback.bind(null, refetchQuery)} - /> - ); - }, + (refetchQuery: inputsModel.Refetch, totalCount: number) => ( + 0} + clearSelection={clearSelectionCallback} + hasIndexWrite={hasIndexWrite} + currentFilter={filterGroup} + selectAll={selectAllCallback} + selectedEventIds={selectedEventIds} + showClearSelection={showClearSelectionAction} + totalCount={totalCount} + updateAlertsStatus={updateAlertsStatusCallback.bind(null, refetchQuery)} + /> + ), [ canUserCRUD, hasIndexWrite, diff --git a/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper.tsx b/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper.tsx index ee54dbaefb65d..8732eb5ea8f82 100644 --- a/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper.tsx +++ b/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper.tsx @@ -198,7 +198,7 @@ const DraggableWrapperComponent: React.FC = ({ const renderContent = useCallback( () => ( - + = ({ onChangeItemsPerPage={onChangeItemsPerPage} query={query} start={start} - sort={sort!} + sort={sort} toggleColumn={toggleColumn} utilityBar={utilityBar} /> diff --git a/x-pack/plugins/security_solution/public/graphql/introspection.json b/x-pack/plugins/security_solution/public/graphql/introspection.json index efb26dfe17ba2..ea16c83d3652a 100644 --- a/x-pack/plugins/security_solution/public/graphql/introspection.json +++ b/x-pack/plugins/security_solution/public/graphql/introspection.json @@ -10131,7 +10131,6 @@ "inputFields": null, "interfaces": null, "enumValues": [ - { "name": "all", "description": "", "isDeprecated": false, "deprecationReason": null }, { "name": "auditd", "description": "", "isDeprecated": false, "deprecationReason": null }, { "name": "auditd_file", diff --git a/x-pack/plugins/security_solution/public/graphql/types.ts b/x-pack/plugins/security_solution/public/graphql/types.ts index a4c6dcb37ab34..7701b1672ec0d 100644 --- a/x-pack/plugins/security_solution/public/graphql/types.ts +++ b/x-pack/plugins/security_solution/public/graphql/types.ts @@ -343,7 +343,6 @@ export enum TlsFields { } export enum RowRendererId { - all = 'all', auditd = 'auditd', auditd_file = 'auditd_file', netflow = 'netflow', diff --git a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/catalog/translations.ts b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/catalog/translations.ts index 456e8c1c574ce..f4d473cdfd3d2 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/catalog/translations.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/catalog/translations.ts @@ -7,7 +7,7 @@ import { i18n } from '@kbn/i18n'; export const AUDITD_NAME = i18n.translate('xpack.securitySolution.eventRenderers.auditdName', { - defaultMessage: 'auditd', + defaultMessage: 'Auditd', }); export const AUDITD_DESCRIPTION_PART1 = i18n.translate( @@ -20,7 +20,7 @@ export const AUDITD_DESCRIPTION_PART1 = i18n.translate( export const AUDITD_FILE_NAME = i18n.translate( 'xpack.securitySolution.eventRenderers.auditdFileName', { - defaultMessage: 'auditd File', + defaultMessage: 'Auditd File', } ); diff --git a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/index.tsx index 3f1aad4dc312e..3903ad81cc019 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/index.tsx @@ -23,6 +23,7 @@ import styled from 'styled-components'; import { State } from '../../../common/store'; +import { renderers } from './catalog'; import { RowRendererId } from '../../../../common/types/timeline'; import { setExcludedRowRendererIds as dispatchSetExcludedRowRendererIds } from '../../../timelines/store/timeline/actions'; import { RowRenderersBrowser } from './row_renderers_browser'; @@ -63,6 +64,10 @@ const StyledEuiModalBody = styled(EuiModalBody)` const StyledEuiOverlayMask = styled(EuiOverlayMask)` z-index: 8001; padding-bottom: 0; + + > div { + width: 100%; + } `; interface StatefulRowRenderersBrowserProps { @@ -95,25 +100,25 @@ const StatefulRowRenderersBrowserComponent: React.FC setShow(false), []); const handleDisableAll = useCallback(() => { - setExcludedRowRendererIds([RowRendererId.all]); - tableRef.current.setSelection([]); - }, [setExcludedRowRendererIds]); + setExcludedRowRendererIds(Object.values(RowRendererId)); + tableRef?.current.setSelection([]); + }, [tableRef, setExcludedRowRendererIds]); const handleEnableAll = useCallback(() => { + tableRef?.current.setSelection(renderers); setExcludedRowRendererIds([]); - tableRef.current.setSelection([]); - }, [setExcludedRowRendererIds]); + }, [tableRef, setExcludedRowRendererIds]); return ( <> - + - {'Event Renderers'} + {i18n.EVENT_RENDERERS_TITLE} @@ -139,7 +144,7 @@ const StatefulRowRenderersBrowserComponent: React.FC - {'Disable All'} + {i18n.DISABLE_ALL} @@ -150,7 +155,7 @@ const StatefulRowRenderersBrowserComponent: React.FC - {'Enable All'} + {i18n.ENABLE_ALL} diff --git a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/row_renderers_browser.tsx b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/row_renderers_browser.tsx index c35b1bb42238b..ed52fb70d745a 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/row_renderers_browser.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/row_renderers_browser.tsx @@ -94,18 +94,18 @@ const FieldsBrowserComponent: React.FC = React.forwardRef( name: 'Name', sortable: true, truncateText: true, - width: '10%', + width: '8%', }, { field: 'description', name: 'Description', - width: '25%', + width: '32%', render: (description: React.ReactNode) => description, }, { field: 'example', name: 'Example', - width: '65%', + width: '60%', render: ExampleWrapperComponent, }, { @@ -119,7 +119,7 @@ const FieldsBrowserComponent: React.FC = React.forwardRef( ); const notExcludedRowRenderers = useMemo(() => { - if (excludedRowRendererIds.includes(RowRendererId.all)) return []; + if (excludedRowRendererIds.length === Object.keys(RowRendererId).length) return []; return renderers.filter((renderer) => !excludedRowRendererIds.includes(renderer.id)); }, [excludedRowRendererIds]); @@ -128,7 +128,8 @@ const FieldsBrowserComponent: React.FC = React.forwardRef( const handleSelectionChange = useCallback( (selection: RowRendererOption[]) => { - if (!selection || !selection.length) return setExcludedRowRendererIds([RowRendererId.all]); + if (!selection || !selection.length) + return setExcludedRowRendererIds(Object.values(RowRendererId)); const excludedRowRenderers = xorBy('id', renderers, selection); @@ -153,7 +154,12 @@ const FieldsBrowserComponent: React.FC = React.forwardRef( itemId="id" columns={columns} search={search} - sorting={sort} + sorting={{ + sort: { + field: 'id', + direction: 'desc', + }, + }} isSelectable={true} selection={selectionValue} onTableChange={onTableChange} @@ -162,4 +168,4 @@ const FieldsBrowserComponent: React.FC = React.forwardRef( } ); -export const RowRenderersBrowser = React.memo(FieldsBrowserComponent); +export const RowRenderersBrowser = FieldsBrowserComponent; diff --git a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/translations.ts b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/translations.ts index 19798360e7890..750e0c88c72ad 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/translations.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/translations.ts @@ -6,25 +6,10 @@ import { i18n } from '@kbn/i18n'; -export const CATEGORY = i18n.translate('xpack.securitySolution.fieldBrowser.categoryLabel', { - defaultMessage: 'Category', -}); - -export const CATEGORIES = i18n.translate('xpack.securitySolution.fieldBrowser.categoriesTitle', { - defaultMessage: 'Categories', -}); - -export const COPY_TO_CLIPBOARD = i18n.translate( - 'xpack.securitySolution.fieldBrowser.copyToClipboard', - { - defaultMessage: 'Copy to Clipboard', - } -); - -export const CUSTOMIZE_COLUMNS = i18n.translate( - 'xpack.securitySolution.fieldBrowser.customizeColumnsTitle', +export const EVENT_RENDERERS_TITLE = i18n.translate( + 'xpack.securitySolution.customizeEventRenderers.eventRenderersTitle', { - defaultMessage: 'Customize Columns', + defaultMessage: 'Event Renderers', } ); @@ -43,6 +28,20 @@ export const CUSTOMIZE_EVENT_RENDERERS_DESCRIPTION = i18n.translate( } ); +export const ENABLE_ALL = i18n.translate( + 'xpack.securitySolution.customizeEventRenderers.enableAllRenderersButtonLabel', + { + defaultMessage: 'Enable all', + } +); + +export const DISABLE_ALL = i18n.translate( + 'xpack.securitySolution.customizeEventRenderers.disableAllRenderersButtonLabel', + { + defaultMessage: 'Disable all', + } +); + export const DESCRIPTION = i18n.translate('xpack.securitySolution.fieldBrowser.descriptionLabel', { defaultMessage: 'Description', }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.tsx index 92e60bb3ed09e..a95def76522fb 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.tsx @@ -137,99 +137,82 @@ export const Body = React.memo( ); return ( - - -
- - - - - - - - -
- - {showGraphView(graphEventId) && ( - - )} - - - + <> + + + + + + + + + + {showGraphView(graphEventId) && ( + + )} + + + - - - - -
-
+ + + + + ); } ); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/stateful_body.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/stateful_body.tsx index 4763da7ddc03e..bba4bfb05bf7e 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/stateful_body.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/stateful_body.tsx @@ -10,7 +10,7 @@ import React, { useCallback, useEffect, useMemo } from 'react'; import { connect, ConnectedProps } from 'react-redux'; import deepEqual from 'fast-deep-equal'; -import { TimelineId } from '../../../../../common/types/timeline'; +import { RowRendererId, TimelineId } from '../../../../../common/types/timeline'; import { BrowserFields } from '../../../../common/containers/source'; import { TimelineItem } from '../../../../graphql/types'; import { Note } from '../../../../common/lib/note'; @@ -175,7 +175,11 @@ const StatefulBodyComponent = React.memo( }, [onSelectAll, selectAll]); const enabledRowRenderers = useMemo(() => { - if (!showRowRenderers || (excludedRowRendererIds && excludedRowRendererIds[0] === 'all')) + if ( + !showRowRenderers || + (excludedRowRendererIds && + excludedRowRendererIds.length === Object.keys(RowRendererId).length) + ) return [plainRowRenderer]; if (!excludedRowRendererIds) return rowRenderers; diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/helpers.ts b/x-pack/plugins/security_solution/public/timelines/store/timeline/helpers.ts index 11b58dbb4e519..9b5232de14e07 100644 --- a/x-pack/plugins/security_solution/public/timelines/store/timeline/helpers.ts +++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/helpers.ts @@ -1372,7 +1372,7 @@ export const updateExcludedRowRenderersIds = ({ [id]: { ...timeline, excludedRowRendererIds, - showRowRenderers: excludedRowRendererIds[0] !== 'all', + showRowRenderers: excludedRowRendererIds.length !== Object.keys(RowRendererId).length, }, }; }; diff --git a/x-pack/plugins/security_solution/server/graphql/timeline/schema.gql.ts b/x-pack/plugins/security_solution/server/graphql/timeline/schema.gql.ts index 72137303a58b6..f9aaa6dfc691a 100644 --- a/x-pack/plugins/security_solution/server/graphql/timeline/schema.gql.ts +++ b/x-pack/plugins/security_solution/server/graphql/timeline/schema.gql.ts @@ -142,7 +142,6 @@ export const timelineSchema = gql` } enum RowRendererId { - all auditd auditd_file netflow diff --git a/x-pack/plugins/security_solution/server/graphql/types.ts b/x-pack/plugins/security_solution/server/graphql/types.ts index 73ef9e03bcecb..5325a17180b8d 100644 --- a/x-pack/plugins/security_solution/server/graphql/types.ts +++ b/x-pack/plugins/security_solution/server/graphql/types.ts @@ -345,7 +345,6 @@ export enum TlsFields { } export enum RowRendererId { - all = 'all', auditd = 'auditd', auditd_file = 'auditd_file', netflow = 'netflow', From 905342d771cdc8047200619a61bdc82bcaf9ab13 Mon Sep 17 00:00:00 2001 From: Patryk Kopycinski Date: Tue, 7 Jul 2020 10:42:10 +0200 Subject: [PATCH 10/19] cleanup --- .../common/types/timeline/index.ts | 2 +- .../common/components/events_viewer/index.tsx | 24 +- .../public/common/mock/mock_timeline_data.ts | 2 +- .../public/graphql/introspection.json | 2 +- .../security_solution/public/graphql/types.ts | 2 +- .../row_renderers_browser/catalog/index.tsx | 6 +- .../row_renderers_browser/constants.ts | 17 -- .../row_renderers_browser/examples/auditd.tsx | 2 +- .../examples/auditd_file.tsx | 2 +- .../row_renderers_browser/index.test.tsx | 243 ------------------ .../row_renderers_browser.tsx | 58 ++--- .../row_renderers_browser/translations.ts | 59 ----- .../auditd/generic_row_renderer.test.tsx | 4 +- .../renderers/system/generic_row_renderer.tsx | 2 +- .../server/graphql/timeline/schema.gql.ts | 2 +- .../security_solution/server/graphql/types.ts | 2 +- 16 files changed, 57 insertions(+), 372 deletions(-) delete mode 100644 x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/constants.ts delete mode 100644 x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/index.test.tsx diff --git a/x-pack/plugins/security_solution/common/types/timeline/index.ts b/x-pack/plugins/security_solution/common/types/timeline/index.ts index 9d730130f7721..d39ff06f6a6de 100644 --- a/x-pack/plugins/security_solution/common/types/timeline/index.ts +++ b/x-pack/plugins/security_solution/common/types/timeline/index.ts @@ -163,7 +163,7 @@ export enum RowRendererId { system_dns = 'system_dns', system_endgame_process = 'system_endgame_process', system_file = 'system_file', - system_fin = 'system_fin', + system_fim = 'system_fim', system_security_event = 'system_security_event', system_socket = 'system_socket', zeek = 'zeek', diff --git a/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx b/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx index f7b2bc2d6e9e9..2a59153d320b1 100644 --- a/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx @@ -38,6 +38,7 @@ export interface OwnProps { type Props = OwnProps & PropsFromRedux; const StatefulEventsViewerComponent: React.FC = ({ + createTimeline, columns, dataProviders, deletedEventIds, @@ -55,6 +56,8 @@ const StatefulEventsViewerComponent: React.FC = ({ query, removeColumn, start, + showCheckboxes, + showRowRenderers, sort, updateItemsPerPage, upsertColumn, @@ -64,12 +67,16 @@ const StatefulEventsViewerComponent: React.FC = ({ defaultIndices ?? useUiSetting(DEFAULT_INDEX_KEY) ); - useEffect( - () => () => { + useEffect(() => { + if (createTimeline != null) { + createTimeline({ id, columns, sort, itemsPerPage, showCheckboxes, showRowRenderers }); + } + + return () => { deleteEventQuery({ id, inputId: 'global' }); - }, - [deleteEventQuery, id] - ); + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); const onChangeItemsPerPage: OnChangeItemsPerPage = useCallback( (itemsChangedPerPage) => updateItemsPerPage({ id, itemsPerPage: itemsChangedPerPage }), @@ -144,6 +151,8 @@ const makeMapStateToProps = () => { itemsPerPageOptions, kqlMode, sort, + showCheckboxes, + showRowRenderers, } = events; return { @@ -159,12 +168,15 @@ const makeMapStateToProps = () => { kqlMode, query: getGlobalQuerySelector(state), sort, + showCheckboxes, + showRowRenderers, }; }; return mapStateToProps; }; const mapDispatchToProps = { + createTimeline: timelineActions.createTimeline, deleteEventQuery: inputsActions.deleteOneQuery, updateItemsPerPage: timelineActions.updateItemsPerPage, removeColumn: timelineActions.removeColumn, @@ -193,6 +205,8 @@ export const StatefulEventsViewer = connector( prevProps.kqlMode === nextProps.kqlMode && deepEqual(prevProps.query, nextProps.query) && deepEqual(prevProps.sort, nextProps.sort) && + prevProps.showCheckboxes === nextProps.showCheckboxes && + prevProps.showRowRenderers === nextProps.showRowRenderers && prevProps.start === nextProps.start && deepEqual(prevProps.pageFilters, nextProps.pageFilters) && prevProps.start === nextProps.start && diff --git a/x-pack/plugins/security_solution/public/common/mock/mock_timeline_data.ts b/x-pack/plugins/security_solution/public/common/mock/mock_timeline_data.ts index 7503062300d2d..4d80c6de9922f 100644 --- a/x-pack/plugins/security_solution/public/common/mock/mock_timeline_data.ts +++ b/x-pack/plugins/security_solution/public/common/mock/mock_timeline_data.ts @@ -1174,7 +1174,7 @@ export const mockTimelineData: TimelineItem[] = [ }, auditd: { result: ['success'], - session: ['unset'], + session: ['242'], data: null, summary: { actor: { primary: ['unset'], secondary: ['root'] }, diff --git a/x-pack/plugins/security_solution/public/graphql/introspection.json b/x-pack/plugins/security_solution/public/graphql/introspection.json index ea16c83d3652a..4dd32145ee594 100644 --- a/x-pack/plugins/security_solution/public/graphql/introspection.json +++ b/x-pack/plugins/security_solution/public/graphql/introspection.json @@ -10171,7 +10171,7 @@ "deprecationReason": null }, { - "name": "system_fin", + "name": "system_fim", "description": "", "isDeprecated": false, "deprecationReason": null diff --git a/x-pack/plugins/security_solution/public/graphql/types.ts b/x-pack/plugins/security_solution/public/graphql/types.ts index 7701b1672ec0d..8ec52f59c9c75 100644 --- a/x-pack/plugins/security_solution/public/graphql/types.ts +++ b/x-pack/plugins/security_solution/public/graphql/types.ts @@ -352,7 +352,7 @@ export enum RowRendererId { system_dns = 'system_dns', system_endgame_process = 'system_endgame_process', system_file = 'system_file', - system_fin = 'system_fin', + system_fim = 'system_fim', system_security_event = 'system_security_event', system_socket = 'system_socket', zeek = 'zeek', diff --git a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/catalog/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/catalog/index.tsx index c59390155ba8e..1709476b123de 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/catalog/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/catalog/index.tsx @@ -57,7 +57,7 @@ export const renderers: RowRendererOption[] = [ {i18n.AUDITD_DESCRIPTION_PART1} ), - example: AuditdExample, // TODO: replace "some text" in this example + example: AuditdExample, searchableDescription: `${i18n.AUDITD_NAME} ${i18n.AUDITD_DESCRIPTION_PART1}`, }, { @@ -71,7 +71,7 @@ export const renderers: RowRendererOption[] = [ {i18n.AUDITD_FILE_DESCRIPTION_PART1} ), - example: AuditdFileExample, // TODO: replace both the `unset` session ID and "some text" in this example + example: AuditdFileExample, searchableDescription: `${i18n.AUDITD_FILE_NAME} ${i18n.AUDITD_FILE_DESCRIPTION_PART1}`, }, { @@ -140,7 +140,7 @@ export const renderers: RowRendererOption[] = [ searchableDescription: `${i18n.PROCESS_DESCRIPTION_PART1} ${i18n.PROCESS_DESCRIPTION_PART2}`, }, { - id: RowRendererId.system_fin, // TODO: this `id` should be "FIM" + id: RowRendererId.system_fim, name: i18n.FIM_NAME, description: i18n.FIM_DESCRIPTION_PART1, example: SystemFimExample, diff --git a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/constants.ts b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/constants.ts deleted file mode 100644 index fffd02f338bcd..0000000000000 --- a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/constants.ts +++ /dev/null @@ -1,17 +0,0 @@ -/* - * 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. - */ - -export const CATEGORY_PANE_WIDTH = 200; -export const DESCRIPTION_COLUMN_WIDTH = 300; -export const FIELD_COLUMN_WIDTH = 200; -export const FIELD_BROWSER_WIDTH = 900; -export const FIELD_BROWSER_HEIGHT = 300; -export const FIELDS_PANE_WIDTH = 670; -export const HEADER_HEIGHT = 40; -export const PANES_FLEX_GROUP_WIDTH = CATEGORY_PANE_WIDTH + FIELDS_PANE_WIDTH + 10; -export const SEARCH_INPUT_WIDTH = 850; -export const TABLE_HEIGHT = 260; -export const TYPE_COLUMN_WIDTH = 50; diff --git a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/auditd.tsx b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/auditd.tsx index 2bdd390f8b9f0..5e974e7fb3507 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/auditd.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/auditd.tsx @@ -12,7 +12,7 @@ import { createGenericAuditRowRenderer } from '../../timeline/body/renderers/aud const AuditdExampleComponent: React.FC = () => { const auditdRowRenderer = createGenericAuditRowRenderer({ actionName: 'connected-to', - text: 'some text', + text: 'connected using', }); return ( diff --git a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/auditd_file.tsx b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/auditd_file.tsx index 48e3b310ebb7f..5b0aae8e5762e 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/auditd_file.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/auditd_file.tsx @@ -12,7 +12,7 @@ import { createGenericFileRowRenderer } from '../../timeline/body/renderers/audi const AuditdFileExampleComponent: React.FC = () => { const auditdFileRowRenderer = createGenericFileRowRenderer({ actionName: 'opened-file', - text: 'some text', + text: 'opened file using', }); return ( diff --git a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/index.test.tsx deleted file mode 100644 index ed33883e6328d..0000000000000 --- a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/index.test.tsx +++ /dev/null @@ -1,243 +0,0 @@ -/* - * 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 { mount } from 'enzyme'; -import React from 'react'; -import { ActionCreator } from 'typescript-fsa'; - -import { mockBrowserFields } from '../../../common/containers/source/mock'; -import { TestProviders } from '../../../common/mock'; -import { ColumnHeaderOptions } from '../../../timelines/store/timeline/model'; - -import { FIELD_BROWSER_HEIGHT, FIELD_BROWSER_WIDTH } from './constants'; - -import { StatefulFieldsBrowserComponent } from '.'; - -// Suppress warnings about "react-beautiful-dnd" until we migrate to @testing-library/react -/* eslint-disable no-console */ -const originalError = console.error; -const originalWarn = console.warn; -beforeAll(() => { - console.warn = jest.fn(); - console.error = jest.fn(); -}); -afterAll(() => { - console.error = originalError; - console.warn = originalWarn; -}); - -const removeColumnMock = (jest.fn() as unknown) as ActionCreator<{ - id: string; - columnId: string; -}>; - -const upsertColumnMock = (jest.fn() as unknown) as ActionCreator<{ - column: ColumnHeaderOptions; - id: string; - index: number; -}>; - -describe('StatefulFieldsBrowser', () => { - const timelineId = 'test'; - - test('it renders the Fields button, which displays the fields browser on click', () => { - const wrapper = mount( - - - - ); - - expect(wrapper.find('[data-test-subj="show-field-browser"]').first().text()).toEqual('Columns'); - }); - - describe('toggleShow', () => { - test('it does NOT render the fields browser until the Fields button is clicked', () => { - const wrapper = mount( - - - - ); - - expect(wrapper.find('[data-test-subj="fields-browser-container"]').exists()).toBe(false); - }); - - test('it renders the fields browser when the Fields button is clicked', () => { - const wrapper = mount( - - - - ); - - wrapper.find('[data-test-subj="show-field-browser"]').first().simulate('click'); - - expect(wrapper.find('[data-test-subj="fields-browser-container"]').exists()).toBe(true); - }); - }); - - describe('updateSelectedCategoryId', () => { - beforeEach(() => { - jest.useFakeTimers(); - }); - test('it updates the selectedCategoryId state, which makes the category bold, when the user clicks a category name in the left hand side of the field browser', () => { - const wrapper = mount( - - - - ); - - wrapper.find('[data-test-subj="show-field-browser"]').first().simulate('click'); - - wrapper.find(`.field-browser-category-pane-auditd-${timelineId}`).first().simulate('click'); - - wrapper.update(); - expect( - wrapper.find(`.field-browser-category-pane-auditd-${timelineId}`).at(1) - ).toHaveStyleRule('font-weight', 'bold', { modifier: '.euiText' }); - }); - - test('it updates the selectedCategoryId state according to most fields returned', () => { - const wrapper = mount( - - - - ); - - wrapper.find('[data-test-subj="show-field-browser"]').first().simulate('click'); - expect( - wrapper.find(`.field-browser-category-pane-cloud-${timelineId}`).at(1) - ).toHaveStyleRule('font-weight', 'normal', { modifier: '.euiText' }); - wrapper - .find('[data-test-subj="field-search"]') - .last() - .simulate('change', { target: { value: 'cloud' } }); - - jest.runOnlyPendingTimers(); - wrapper.update(); - expect( - wrapper.find(`.field-browser-category-pane-cloud-${timelineId}`).at(1) - ).toHaveStyleRule('font-weight', 'bold', { modifier: '.euiText' }); - }); - }); - - test('it renders the Fields Browser button as a settings gear when the isEventViewer prop is true', () => { - const isEventViewer = true; - - const wrapper = mount( - - - - ); - - expect(wrapper.find('[data-test-subj="show-field-browser-gear"]').first().exists()).toBe(true); - }); - - test('it does NOT render the Fields Browser button as a settings gear when the isEventViewer prop is false', () => { - const isEventViewer = false; - - const wrapper = mount( - - - - ); - - expect(wrapper.find('[data-test-subj="show-field-browser-gear"]').first().exists()).toBe(false); - }); - - test('it does NOT render the default Fields Browser button when the isEventViewer prop is true', () => { - const isEventViewer = true; - - const wrapper = mount( - - - - ); - - expect(wrapper.find('[data-test-subj="show-field-browser"]').first().exists()).toBe(false); - }); -}); diff --git a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/row_renderers_browser.tsx b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/row_renderers_browser.tsx index ed52fb70d745a..c98c736b6eddf 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/row_renderers_browser.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/row_renderers_browser.tsx @@ -4,17 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ -/* eslint-disable react/display-name */ - import { EuiFlexItem, EuiInMemoryTable } from '@elastic/eui'; -import React, { useMemo, useCallback, useState } from 'react'; +import React, { useMemo, useCallback } from 'react'; import { xorBy } from 'lodash/fp'; import styled from 'styled-components'; import { RowRendererId } from '../../../../common/types/timeline'; import { renderers, RowRendererOption } from './catalog'; import { FieldBrowserProps } from './types'; -import { OnTableChangeParams } from '../open_timeline/types'; type Props = Pick & { excludedRowRendererIds: RowRendererId[]; @@ -24,7 +21,9 @@ type Props = Pick & { // eslint-disable-next-line @typescript-eslint/no-explicit-any const StyledEuiInMemoryTable = styled(EuiInMemoryTable as any)` .euiTable { - width: auto; + tr > *:last-child { + display: none; + } .euiTableHeaderCellCheckbox > .euiTableCellContent { display: none; // we don't want to display checkbox in the table @@ -33,6 +32,8 @@ const StyledEuiInMemoryTable = styled(EuiInMemoryTable as any)` `; const StyledEuiFlexItem = styled(EuiFlexItem)` + overflow: auto; + > div { padding: 0; @@ -42,7 +43,7 @@ const StyledEuiFlexItem = styled(EuiFlexItem)` } `; -const ExampleWrapperComponent = (Example?: React.ReactElement) => { +const ExampleWrapperComponent = (Example?: React.ElementType) => { if (!Example) return; return ( @@ -63,28 +64,18 @@ const search = { * Since `searchableDescription` contains raw text to power the Search bar, * this "noop" function ensures it's not actually rendered */ -const renderSearchableDescriptionNoop = () => null; +const renderSearchableDescriptionNoop = () => <>; -const FieldsBrowserComponent: React.FC = React.forwardRef( +const RowRenderersBrowserComponent: React.FC = React.forwardRef( ({ excludedRowRendererIds = [], setExcludedRowRendererIds }, ref) => { - const [sortField, setSortField] = useState('name'); - const [sortDirection, setSortDirection] = useState('asc'); - - const onTableChange = useCallback( - ({ page, sort }: OnTableChangeParams) => { - const { field, direction } = sort; - setSortDirection(direction); - setSortField(field); - }, - [setSortField, setSortDirection] - ); - const sort = useMemo( () => ({ - sortField, - sortDirection, + sort: { + field: 'name', + direction: 'asc', + }, }), - [sortField, sortDirection] + [] ); const columns = useMemo( @@ -94,22 +85,23 @@ const FieldsBrowserComponent: React.FC = React.forwardRef( name: 'Name', sortable: true, truncateText: true, - width: '8%', + width: '10%', }, { field: 'description', name: 'Description', - width: '32%', + width: '25%', render: (description: React.ReactNode) => description, }, { field: 'example', name: 'Example', - width: '60%', + width: '65%', render: ExampleWrapperComponent, }, { field: 'searchableDescription', + name: 'Searchable Description', sortable: false, width: '0px', render: renderSearchableDescriptionNoop, @@ -154,18 +146,16 @@ const FieldsBrowserComponent: React.FC = React.forwardRef( itemId="id" columns={columns} search={search} - sorting={{ - sort: { - field: 'id', - direction: 'desc', - }, - }} + sorting={sort} isSelectable={true} selection={selectionValue} - onTableChange={onTableChange} /> ); } ); -export const RowRenderersBrowser = FieldsBrowserComponent; +RowRenderersBrowserComponent.displayName = 'RowRenderersBrowserComponent'; + +export const RowRenderersBrowser = RowRenderersBrowserComponent; + +RowRenderersBrowser.displayName = 'RowRenderersBrowser'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/translations.ts b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/translations.ts index 750e0c88c72ad..93874ff3240bf 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/translations.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/translations.ts @@ -41,62 +41,3 @@ export const DISABLE_ALL = i18n.translate( defaultMessage: 'Disable all', } ); - -export const DESCRIPTION = i18n.translate('xpack.securitySolution.fieldBrowser.descriptionLabel', { - defaultMessage: 'Description', -}); - -export const FIELD = i18n.translate('xpack.securitySolution.fieldBrowser.fieldLabel', { - defaultMessage: 'Field', -}); - -export const FIELDS = i18n.translate('xpack.securitySolution.fieldBrowser.fieldsTitle', { - defaultMessage: 'Columns', -}); - -export const FIELDS_COUNT = (totalCount: number) => - i18n.translate('xpack.securitySolution.fieldBrowser.fieldsCountTitle', { - values: { totalCount }, - defaultMessage: '{totalCount} {totalCount, plural, =1 {field} other {fields}}', - }); - -export const FILTER_PLACEHOLDER = i18n.translate( - 'xpack.securitySolution.fieldBrowser.filterPlaceholder', - { - defaultMessage: 'Field name', - } -); - -export const NO_FIELDS_MATCH = i18n.translate( - 'xpack.securitySolution.fieldBrowser.noFieldsMatchLabel', - { - defaultMessage: 'No fields match', - } -); - -export const NO_FIELDS_MATCH_INPUT = (searchInput: string) => - i18n.translate('xpack.securitySolution.fieldBrowser.noFieldsMatchInputLabel', { - defaultMessage: 'No fields match {searchInput}', - values: { - searchInput, - }, - }); - -export const RESET_FIELDS = i18n.translate('xpack.securitySolution.fieldBrowser.resetFieldsLink', { - defaultMessage: 'Reset Fields', -}); - -export const TOGGLE_COLUMN_TOOLTIP = i18n.translate( - 'xpack.securitySolution.fieldBrowser.toggleColumnTooltip', - { - defaultMessage: 'Toggle column', - } -); - -export const VIEW_CATEGORY = (categoryId: string) => - i18n.translate('xpack.securitySolution.fieldBrowser.viewCategoryTooltip', { - defaultMessage: 'View all {categoryId} fields', - values: { - categoryId, - }, - }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/auditd/generic_row_renderer.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/auditd/generic_row_renderer.test.tsx index aec463f531448..295b8663d3e7d 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/auditd/generic_row_renderer.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/auditd/generic_row_renderer.test.tsx @@ -34,7 +34,7 @@ describe('GenericRowRenderer', () => { auditd = cloneDeep(mockTimelineData[26].ecs); connectedToRenderer = createGenericAuditRowRenderer({ actionName: 'connected-to', - text: 'some text', + text: 'connected using', }); }); test('renders correctly against snapshot', () => { @@ -95,7 +95,7 @@ describe('GenericRowRenderer', () => { auditdFile = cloneDeep(mockTimelineData[27].ecs); fileToRenderer = createGenericFileRowRenderer({ actionName: 'opened-file', - text: 'some text', + text: 'opened file using', }); }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/system/generic_row_renderer.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/system/generic_row_renderer.tsx index 6137c8e30442b..67e050160805e 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/system/generic_row_renderer.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/system/generic_row_renderer.tsx @@ -90,7 +90,7 @@ export const createFimRowRenderer = ({ actionName: string; text: string; }): RowRenderer => ({ - id: RowRendererId.system_fin, + id: RowRendererId.system_fim, isInstance: (ecs) => { const action: string | null | undefined = get('event.action[0]', ecs); const category: string | null | undefined = get('event.category[0]', ecs); diff --git a/x-pack/plugins/security_solution/server/graphql/timeline/schema.gql.ts b/x-pack/plugins/security_solution/server/graphql/timeline/schema.gql.ts index f9aaa6dfc691a..cc2e5620f60b4 100644 --- a/x-pack/plugins/security_solution/server/graphql/timeline/schema.gql.ts +++ b/x-pack/plugins/security_solution/server/graphql/timeline/schema.gql.ts @@ -151,7 +151,7 @@ export const timelineSchema = gql` system_dns system_endgame_process system_file - system_fin + system_fim system_security_event system_socket zeek diff --git a/x-pack/plugins/security_solution/server/graphql/types.ts b/x-pack/plugins/security_solution/server/graphql/types.ts index 5325a17180b8d..8394e1fba5f90 100644 --- a/x-pack/plugins/security_solution/server/graphql/types.ts +++ b/x-pack/plugins/security_solution/server/graphql/types.ts @@ -354,7 +354,7 @@ export enum RowRendererId { system_dns = 'system_dns', system_endgame_process = 'system_endgame_process', system_file = 'system_file', - system_fin = 'system_fin', + system_fim = 'system_fim', system_security_event = 'system_security_event', system_socket = 'system_socket', zeek = 'zeek', From 0497cff1f71ff2009ec28b72768019e7eb15aef5 Mon Sep 17 00:00:00 2001 From: Patryk Kopycinski Date: Wed, 8 Jul 2020 00:11:38 +0200 Subject: [PATCH 11/19] cleanup --- .../common/types/timeline/index.ts | 1 - .../cypress/screens/timeline.ts | 2 +- .../components/alerts_table/actions.test.tsx | 1 + .../alerts/components/alerts_table/index.tsx | 30 +- .../common/components/events_viewer/index.tsx | 5 +- .../public/graphql/introspection.json | 14 + .../security_solution/public/graphql/types.ts | 2 + .../components/fields_browser/index.test.tsx | 12 +- .../components/fields_browser/index.tsx | 14 +- .../row_renderers_browser/catalog/index.tsx | 4 +- .../row_renderers_browser/examples/system.tsx | 8 +- .../examples/system_file.tsx | 8 +- .../row_renderers_browser/index.tsx | 16 +- .../row_renderers_browser.tsx | 7 +- .../components/row_renderers_browser/types.ts | 35 - .../__snapshots__/index.test.tsx.snap | 904 +++++++++--------- .../body/column_headers/index.test.tsx | 10 + .../timeline/body/column_headers/index.tsx | 33 +- .../components/timeline/body/index.tsx | 26 +- .../generic_file_details.test.tsx.snap | 4 +- .../generic_row_renderer.test.tsx.snap | 6 +- .../auditd/generic_row_renderer.test.tsx | 4 +- .../suricata_signature.test.tsx.snap | 1 - .../renderers/suricata/suricata_signature.tsx | 2 - .../body/renderers/zeek/zeek_signature.tsx | 3 - .../timelines/components/timeline/styles.tsx | 23 +- .../public/timelines/store/timeline/epic.ts | 1 + .../server/graphql/timeline/schema.gql.ts | 1 + .../security_solution/server/graphql/types.ts | 2 + 29 files changed, 599 insertions(+), 580 deletions(-) delete mode 100644 x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/types.ts diff --git a/x-pack/plugins/security_solution/common/types/timeline/index.ts b/x-pack/plugins/security_solution/common/types/timeline/index.ts index d39ff06f6a6de..467e49209e685 100644 --- a/x-pack/plugins/security_solution/common/types/timeline/index.ts +++ b/x-pack/plugins/security_solution/common/types/timeline/index.ts @@ -290,7 +290,6 @@ export const TimelineSavedToReturnObjectRuntimeType = runtimeTypes.intersection( }), runtimeTypes.partial({ eventIdToNoteIds: runtimeTypes.array(NoteSavedObjectToReturnRuntimeType), - excludedRowRendererIds: runtimeTypes.array(RowRendererIdRuntimeType), noteIds: runtimeTypes.array(runtimeTypes.string), notes: runtimeTypes.array(NoteSavedObjectToReturnRuntimeType), pinnedEventIds: runtimeTypes.array(runtimeTypes.string), diff --git a/x-pack/plugins/security_solution/cypress/screens/timeline.ts b/x-pack/plugins/security_solution/cypress/screens/timeline.ts index c673cf34b6dae..f41e499f70101 100644 --- a/x-pack/plugins/security_solution/cypress/screens/timeline.ts +++ b/x-pack/plugins/security_solution/cypress/screens/timeline.ts @@ -47,7 +47,7 @@ export const TIMELINE_DESCRIPTION = '[data-test-subj="timeline-description"]'; export const TIMELINE_DROPPED_DATA_PROVIDERS = '[data-test-subj="providerContainer"]'; export const TIMELINE_FIELDS_BUTTON = - '[data-test-subj="timeline"] [data-test-subj="show-field-browser"]'; + '[data-test-subj="timeline"] [data-test-subj="show-field-browser-gear"]'; export const TIMELINE_FLYOUT_HEADER = '[data-test-subj="eui-flyout-header"]'; diff --git a/x-pack/plugins/security_solution/public/alerts/components/alerts_table/actions.test.tsx b/x-pack/plugins/security_solution/public/alerts/components/alerts_table/actions.test.tsx index bd62b79a3c54e..9658125b60557 100644 --- a/x-pack/plugins/security_solution/public/alerts/components/alerts_table/actions.test.tsx +++ b/x-pack/plugins/security_solution/public/alerts/components/alerts_table/actions.test.tsx @@ -158,6 +158,7 @@ describe('alert actions', () => { description: 'This is a sample rule description', eventIdToNoteIds: {}, eventType: 'all', + excludedRowRendererIds: [], filters: [ { $state: { diff --git a/x-pack/plugins/security_solution/public/alerts/components/alerts_table/index.tsx b/x-pack/plugins/security_solution/public/alerts/components/alerts_table/index.tsx index cba383e1b88d2..98bb6434ddafd 100644 --- a/x-pack/plugins/security_solution/public/alerts/components/alerts_table/index.tsx +++ b/x-pack/plugins/security_solution/public/alerts/components/alerts_table/index.tsx @@ -258,20 +258,22 @@ export const AlertsTableComponent: React.FC = ({ // Callback for creating the AlertsUtilityBar which receives totalCount from EventsViewer component const utilityBarCallback = useCallback( - (refetchQuery: inputsModel.Refetch, totalCount: number) => ( - 0} - clearSelection={clearSelectionCallback} - hasIndexWrite={hasIndexWrite} - currentFilter={filterGroup} - selectAll={selectAllCallback} - selectedEventIds={selectedEventIds} - showClearSelection={showClearSelectionAction} - totalCount={totalCount} - updateAlertsStatus={updateAlertsStatusCallback.bind(null, refetchQuery)} - /> - ), + (refetchQuery: inputsModel.Refetch, totalCount: number) => { + return ( + 0} + clearSelection={clearSelectionCallback} + hasIndexWrite={hasIndexWrite} + currentFilter={filterGroup} + selectAll={selectAllCallback} + selectedEventIds={selectedEventIds} + showClearSelection={showClearSelectionAction} + totalCount={totalCount} + updateAlertsStatus={updateAlertsStatusCallback.bind(null, refetchQuery)} + /> + ); + }, [ canUserCRUD, hasIndexWrite, diff --git a/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx b/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx index 2a59153d320b1..a88fad3051e0b 100644 --- a/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx @@ -71,7 +71,6 @@ const StatefulEventsViewerComponent: React.FC = ({ if (createTimeline != null) { createTimeline({ id, columns, sort, itemsPerPage, showCheckboxes, showRowRenderers }); } - return () => { deleteEventQuery({ id, inputId: 'global' }); }; @@ -205,10 +204,10 @@ export const StatefulEventsViewer = connector( prevProps.kqlMode === nextProps.kqlMode && deepEqual(prevProps.query, nextProps.query) && deepEqual(prevProps.sort, nextProps.sort) && - prevProps.showCheckboxes === nextProps.showCheckboxes && - prevProps.showRowRenderers === nextProps.showRowRenderers && prevProps.start === nextProps.start && deepEqual(prevProps.pageFilters, nextProps.pageFilters) && + prevProps.showCheckboxes === nextProps.showCheckboxes && + prevProps.showRowRenderers === nextProps.showRowRenderers && prevProps.start === nextProps.start && prevProps.utilityBar === nextProps.utilityBar ) diff --git a/x-pack/plugins/security_solution/public/graphql/introspection.json b/x-pack/plugins/security_solution/public/graphql/introspection.json index 4dd32145ee594..4d16664b9f420 100644 --- a/x-pack/plugins/security_solution/public/graphql/introspection.json +++ b/x-pack/plugins/security_solution/public/graphql/introspection.json @@ -11107,6 +11107,20 @@ "type": { "kind": "SCALAR", "name": "String", "ofType": null }, "defaultValue": null }, + { + "name": "excludedRowRendererIds", + "description": "", + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "ENUM", "name": "RowRendererId", "ofType": null } + } + }, + "defaultValue": null + }, { "name": "filters", "description": "", diff --git a/x-pack/plugins/security_solution/public/graphql/types.ts b/x-pack/plugins/security_solution/public/graphql/types.ts index 8ec52f59c9c75..917a10043dcbf 100644 --- a/x-pack/plugins/security_solution/public/graphql/types.ts +++ b/x-pack/plugins/security_solution/public/graphql/types.ts @@ -124,6 +124,8 @@ export interface TimelineInput { eventType?: Maybe; + excludedRowRendererIds?: Maybe; + filters?: Maybe; kqlMode?: Maybe; diff --git a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/index.test.tsx index 03b670190f263..cbdca0c60f96b 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/index.test.tsx @@ -45,7 +45,7 @@ describe('StatefulFieldsBrowser', () => { ); - expect(wrapper.find('[data-test-subj="show-field-browser"]').first().text()).toEqual('Columns'); + expect(wrapper.find('[data-test-subj="show-field-browser-gear"]').exists()).toBe(true); }); describe('toggleShow', () => { @@ -82,7 +82,7 @@ describe('StatefulFieldsBrowser', () => { ); - wrapper.find('[data-test-subj="show-field-browser"]').first().simulate('click'); + wrapper.find('[data-test-subj="show-field-browser-gear"]').first().simulate('click'); expect(wrapper.find('[data-test-subj="fields-browser-container"]').exists()).toBe(true); }); @@ -107,7 +107,7 @@ describe('StatefulFieldsBrowser', () => { ); - wrapper.find('[data-test-subj="show-field-browser"]').first().simulate('click'); + wrapper.find('[data-test-subj="show-field-browser-gear"]').first().simulate('click'); wrapper.find(`.field-browser-category-pane-auditd-${timelineId}`).first().simulate('click'); @@ -132,7 +132,7 @@ describe('StatefulFieldsBrowser', () => { ); - wrapper.find('[data-test-subj="show-field-browser"]').first().simulate('click'); + wrapper.find('[data-test-subj="show-field-browser-gear"]').first().simulate('click'); expect( wrapper.find(`.field-browser-category-pane-cloud-${timelineId}`).at(1) ).toHaveStyleRule('font-weight', 'normal', { modifier: '.euiText' }); @@ -170,7 +170,7 @@ describe('StatefulFieldsBrowser', () => { expect(wrapper.find('[data-test-subj="show-field-browser-gear"]').first().exists()).toBe(true); }); - test('it does NOT render the Fields Browser button as a settings gear when the isEventViewer prop is false', () => { + test('it does NOT render the default Fields Browser button when the isEventViewer prop is false', () => { const isEventViewer = false; const wrapper = mount( @@ -188,7 +188,7 @@ describe('StatefulFieldsBrowser', () => { ); - expect(wrapper.find('[data-test-subj="show-field-browser-gear"]').first().exists()).toBe(false); + expect(wrapper.find('[data-test-subj="show-field-browser"]').first().exists()).toBe(false); }); test('it does NOT render the default Fields Browser button when the isEventViewer prop is true', () => { diff --git a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/index.tsx index 63adde9f72c4a..9582a2c25d88f 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/index.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiButtonEmpty, EuiToolTip } from '@elastic/eui'; +import { EuiButtonIcon, EuiToolTip } from '@elastic/eui'; import { noop } from 'lodash/fp'; import React, { useEffect, useRef, useState, useCallback, useMemo } from 'react'; import styled from 'styled-components'; @@ -17,11 +17,14 @@ import { filterBrowserFieldsByFieldName, mergeBrowserFieldsWithDefaultCategory } import * as i18n from './translations'; import { FieldBrowserProps } from './types'; +const fieldsButtonClassName = 'fields-button'; + /** wait this many ms after the user completes typing before applying the filter input */ export const INPUT_TIMEOUT = 250; const FieldsBrowserButtonContainer = styled.div` position: relative; + width: 24px; `; FieldsBrowserButtonContainer.displayName = 'FieldsBrowserButtonContainer'; @@ -143,14 +146,15 @@ export const StatefulFieldsBrowserComponent: React.FC = ({ return ( - {i18n.FIELDS} - + {show && ( diff --git a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/catalog/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/catalog/index.tsx index 1709476b123de..55d1694297e2e 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/catalog/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/catalog/index.tsx @@ -123,7 +123,7 @@ export const renderers: RowRendererOption[] = [

{i18n.SYSTEM_DESCRIPTION_PART3}

), - example: SystemExample, // TODO: replace this example with data from `event.category: process and event.module: system` + example: SystemExample, searchableDescription: `${i18n.SYSTEM_DESCRIPTION_PART1} ${i18n.SYSTEM_NAME} ${i18n.SYSTEM_DESCRIPTION_PART2} ${i18n.SYSTEM_DESCRIPTION_PART3}`, }, { @@ -150,7 +150,7 @@ export const renderers: RowRendererOption[] = [ id: RowRendererId.system_file, name: i18n.FILE_NAME, description: i18n.FILE_DESCRIPTION_PART1, - example: SystemFileExample, // TODO: replace this example with a real CRUD example, similar to FIM + example: SystemFileExample, searchableDescription: i18n.FILE_DESCRIPTION_PART1, }, { diff --git a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/system.tsx b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/system.tsx index 343cfa1098183..cc11f2393db1b 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/system.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/system.tsx @@ -6,20 +6,20 @@ import React from 'react'; -import { mockTimelineData } from '../../../../common/mock/mock_timeline_data'; import { createGenericSystemRowRenderer } from '../../timeline/body/renderers/system/generic_row_renderer'; +import { mockEndgameTerminationEvent } from '../../../../common/mock/mock_endgame_ecs_data'; const SystemExampleComponent: React.FC = () => { const systemRowRenderer = createGenericSystemRowRenderer({ - actionName: 'process_started', - text: 'some text', + actionName: 'termination_event', + text: 'terminated process', }); return ( <> {systemRowRenderer.renderRow({ browserFields: {}, - data: mockTimelineData[29].ecs, + data: mockEndgameTerminationEvent, timelineId: 'row-renderer-example', })} diff --git a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/system_file.tsx b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/system_file.tsx index 5479b553b7172..6f69e51723676 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/system_file.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/system_file.tsx @@ -6,20 +6,20 @@ import React from 'react'; -import { mockTimelineData } from '../../../../common/mock/mock_timeline_data'; +import { mockEndgameFileDeleteEvent } from '../../../../common/mock/mock_endgame_ecs_data'; import { createGenericFileRowRenderer } from '../../timeline/body/renderers/system/generic_row_renderer'; const SystemFileExampleComponent: React.FC = () => { const systemFileRowRenderer = createGenericFileRowRenderer({ - actionName: 'user_login', - text: 'some text', + actionName: 'file_delete_event', + text: 'deleted a file', }); return ( <> {systemFileRowRenderer.renderRow({ browserFields: {}, - data: mockTimelineData[28].ecs, + data: mockEndgameFileDeleteEvent, timelineId: 'row-renderer-example', })} diff --git a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/index.tsx index 3903ad81cc019..7f50e2e297238 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/index.tsx @@ -6,6 +6,7 @@ import { EuiButtonEmpty, + EuiButtonIcon, EuiText, EuiToolTip, EuiOverlayMask, @@ -32,6 +33,7 @@ import * as i18n from './translations'; const StyledEuiModal = styled(EuiModal)` margin: 0 auto; max-width: 95vw; + min-height: 95vh; > .euiModal__flex { max-height: 95vh; @@ -101,25 +103,27 @@ const StatefulRowRenderersBrowserComponent: React.FC { setExcludedRowRendererIds(Object.values(RowRendererId)); - tableRef?.current.setSelection([]); + // eslint-disable-next-line no-unused-expressions + tableRef?.current?.setSelection([]); }, [tableRef, setExcludedRowRendererIds]); const handleEnableAll = useCallback(() => { - tableRef?.current.setSelection(renderers); setExcludedRowRendererIds([]); + // eslint-disable-next-line no-unused-expressions + tableRef?.current?.setSelection(renderers); }, [tableRef, setExcludedRowRendererIds]); return ( <> - {i18n.EVENT_RENDERERS_TITLE} - + {show && ( diff --git a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/row_renderers_browser.tsx b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/row_renderers_browser.tsx index c98c736b6eddf..cc7e31e519bb6 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/row_renderers_browser.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/row_renderers_browser.tsx @@ -11,12 +11,11 @@ import styled from 'styled-components'; import { RowRendererId } from '../../../../common/types/timeline'; import { renderers, RowRendererOption } from './catalog'; -import { FieldBrowserProps } from './types'; -type Props = Pick & { +interface RowRenderersBrowserProps { excludedRowRendererIds: RowRendererId[]; setExcludedRowRendererIds: (excludedRowRendererIds: RowRendererId[]) => void; -}; +} // eslint-disable-next-line @typescript-eslint/no-explicit-any const StyledEuiInMemoryTable = styled(EuiInMemoryTable as any)` @@ -66,7 +65,7 @@ const search = { */ const renderSearchableDescriptionNoop = () => <>; -const RowRenderersBrowserComponent: React.FC = React.forwardRef( +const RowRenderersBrowserComponent: React.FC = React.forwardRef( ({ excludedRowRendererIds = [], setExcludedRowRendererIds }, ref) => { const sort = useMemo( () => ({ diff --git a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/types.ts b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/types.ts deleted file mode 100644 index ae09779f5de14..0000000000000 --- a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/types.ts +++ /dev/null @@ -1,35 +0,0 @@ -/* - * 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 { BrowserFields } from '../../../common/containers/source'; -import { ColumnHeaderOptions } from '../../../timelines/store/timeline/model'; -import { OnUpdateColumns } from '../timeline/events'; - -export type OnFieldSelected = (fieldId: string) => void; -export type OnHideFieldBrowser = () => void; - -export interface FieldBrowserProps { - /** The timeline's current column headers */ - columnHeaders: ColumnHeaderOptions[]; - /** A map of categoryId -> metadata about the fields in that category */ - browserFields: BrowserFields; - /** The height of the field browser */ - height: number; - /** When true, this Fields Browser is being used as an "events viewer" */ - isEventViewer?: boolean; - /** - * Overrides the default behavior of the `FieldBrowser` to enable - * "selection" mode, where a field is selected by clicking a button - * instead of dragging it to the timeline - */ - onFieldSelected?: OnFieldSelected; - /** Invoked when a user chooses to view a new set of columns in the timeline */ - onUpdateColumns: OnUpdateColumns; - /** The timeline associated with this field browser */ - timelineId: string; - /** Adds or removes a column to / from the timeline */ - toggleColumn: (column: ColumnHeaderOptions) => void; -} diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/__snapshots__/index.test.tsx.snap index efd99e781d827..eee94f4b58d4e 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/__snapshots__/index.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/__snapshots__/index.test.tsx.snap @@ -8,479 +8,479 @@ exports[`ColumnHeaders rendering renders correctly against snapshot 1`] = ` - - - + } + columnHeaders={ + Array [ + Object { + "columnHeaderType": "not-filtered", + "id": "@timestamp", + "width": 190, + }, + Object { + "columnHeaderType": "not-filtered", + "id": "message", + "width": 180, + }, + Object { + "columnHeaderType": "not-filtered", + "id": "event.category", + "width": 180, + }, + Object { + "columnHeaderType": "not-filtered", + "id": "event.action", + "width": 180, + }, + Object { + "columnHeaderType": "not-filtered", + "id": "host.name", + "width": 180, + }, + Object { + "columnHeaderType": "not-filtered", + "id": "source.ip", + "width": 180, + }, + Object { + "columnHeaderType": "not-filtered", + "id": "destination.ip", + "width": 180, + }, + Object { + "columnHeaderType": "not-filtered", + "id": "user.name", + "width": 180, + }, + ] + } + data-test-subj="field-browser" + height={300} + isEventViewer={false} + onUpdateColumns={[MockFunction]} + timelineId="test" + toggleColumn={[MockFunction]} + width={900} + /> + { const wrapper = shallow( ); expect(wrapper).toMatchSnapshot(); @@ -49,16 +53,19 @@ describe('ColumnHeaders', () => { ); @@ -71,16 +78,19 @@ describe('ColumnHeaders', () => { ); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/index.tsx index 39e5b17524710..bbba6e7cf6330 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/index.tsx @@ -12,6 +12,7 @@ import deepEqual from 'fast-deep-equal'; import { DragEffects } from '../../../../../common/components/drag_and_drop/draggable_wrapper'; import { DraggableFieldBadge } from '../../../../../common/components/draggables/field_badge'; +import { BrowserFields } from '../../../../../common/containers/source'; import { ColumnHeaderOptions } from '../../../../../timelines/store/timeline/model'; import { DRAG_TYPE_FIELD, @@ -23,8 +24,12 @@ import { OnColumnSorted, OnFilterChange, OnSelectAll, + OnUpdateColumns, } from '../../events'; import { DEFAULT_ICON_BUTTON_WIDTH } from '../../helpers'; +import { StatefulFieldsBrowser } from '../../../fields_browser'; +import { StatefulRowRenderersBrowser } from '../../../row_renderers_browser'; +import { FIELD_BROWSER_HEIGHT, FIELD_BROWSER_WIDTH } from '../../../fields_browser/helpers'; import { EventsTh, EventsThContent, @@ -39,6 +44,7 @@ import { ColumnHeader } from './column_header'; interface Props { actionsColumnWidth: number; + browserFields: BrowserFields; columnHeaders: ColumnHeaderOptions[]; isEventViewer?: boolean; isSelectAllChecked: boolean; @@ -47,10 +53,12 @@ interface Props { onColumnSorted: OnColumnSorted; onFilterChange?: OnFilterChange; onSelectAll: OnSelectAll; + onUpdateColumns: OnUpdateColumns; showEventsSelect: boolean; showSelectAllCheckbox: boolean; sort: Sort; timelineId: string; + toggleColumn: (column: ColumnHeaderOptions) => void; } interface DraggableContainerProps { @@ -76,6 +84,7 @@ DraggableContainer.displayName = 'DraggableContainer'; /** Renders the timeline header columns */ export const ColumnHeadersComponent = ({ actionsColumnWidth, + browserFields, columnHeaders, isEventViewer = false, isSelectAllChecked, @@ -83,11 +92,13 @@ export const ColumnHeadersComponent = ({ onColumnResized, onColumnSorted, onSelectAll, + onUpdateColumns, onFilterChange = noop, showEventsSelect, showSelectAllCheckbox, sort, timelineId, + toggleColumn, }: Props) => { const [draggingIndex, setDraggingIndex] = useState(null); @@ -160,6 +171,7 @@ export const ColumnHeadersComponent = ({ {showSelectAllCheckbox && ( @@ -175,7 +187,21 @@ export const ColumnHeadersComponent = ({ )} - + + {showEventsSelect && ( @@ -222,10 +248,13 @@ export const ColumnHeaders = React.memo( prevProps.onColumnResized === nextProps.onColumnResized && prevProps.onColumnSorted === nextProps.onColumnSorted && prevProps.onSelectAll === nextProps.onSelectAll && + prevProps.onUpdateColumns === nextProps.onUpdateColumns && prevProps.onFilterChange === nextProps.onFilterChange && prevProps.showEventsSelect === nextProps.showEventsSelect && prevProps.showSelectAllCheckbox === nextProps.showSelectAllCheckbox && prevProps.sort === nextProps.sort && prevProps.timelineId === nextProps.timelineId && - deepEqual(prevProps.columnHeaders, nextProps.columnHeaders) + prevProps.toggleColumn === nextProps.toggleColumn && + deepEqual(prevProps.columnHeaders, nextProps.columnHeaders) && + deepEqual(prevProps.browserFields, nextProps.browserFields) ); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.tsx index a95def76522fb..6a296170fffde 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.tsx @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiFlexGroup, EuiFlexItem, EuiHorizontalRule } from '@elastic/eui'; import React, { useMemo, useRef } from 'react'; import { BrowserFields } from '../../../../common/containers/source'; @@ -23,9 +22,6 @@ import { OnUnPinEvent, OnUpdateColumns, } from '../events'; -import { StatefulFieldsBrowser } from '../../fields_browser'; -import { FIELD_BROWSER_HEIGHT, FIELD_BROWSER_WIDTH } from '../../fields_browser/helpers'; -import { StatefulRowRenderersBrowser } from '../../row_renderers_browser'; import { EventsTable, TimelineBody, TimelineBodyGlobalStyle } from '../styles'; import { ColumnHeaders } from './column_headers'; import { getActionsColumnWidth } from './column_headers/helpers'; @@ -138,25 +134,6 @@ export const Body = React.memo( return ( <> - - - - - - - - - {showGraphView(graphEventId) && ( )} @@ -170,6 +147,7 @@ export const Body = React.memo( ( onColumnSorted={onColumnSorted} onFilterChange={onFilterChange} onSelectAll={onSelectAll} + onUpdateColumns={onUpdateColumns} showEventsSelect={false} showSelectAllCheckbox={showCheckboxes} sort={sort} timelineId={id} + toggleColumn={toggleColumn} /> @@ -135,7 +135,7 @@ exports[`GenericRowRenderer #createGenericFileRowRenderer renders correctly agai "success", ], "session": Array [ - "unset", + "242", ], "summary": Object { "actor": Object { @@ -259,7 +259,7 @@ exports[`GenericRowRenderer #createGenericFileRowRenderer renders correctly agai } } fileIcon="document" - text="some text" + text="opened file using" timelineId="test" /> diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/auditd/generic_row_renderer.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/auditd/generic_row_renderer.test.tsx index 295b8663d3e7d..ce9e3d3d5c749 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/auditd/generic_row_renderer.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/auditd/generic_row_renderer.test.tsx @@ -80,7 +80,7 @@ describe('GenericRowRenderer', () => { ); expect(wrapper.text()).toContain( - 'Session246alice@zeek-londonsome textwget(1490)wget www.example.comwith resultsuccessDestination93.184.216.34:80' + 'Session246alice@zeek-londonconnected usingwget(1490)wget www.example.comwith resultsuccessDestination93.184.216.34:80' ); }); }); @@ -142,7 +142,7 @@ describe('GenericRowRenderer', () => { ); expect(wrapper.text()).toContain( - 'Sessionunsetroot@zeek-londonin/some text/proc/15990/attr/currentusingsystemd-journal(27244)/lib/systemd/systemd-journaldwith resultsuccess' + 'Session242root@zeek-londonin/opened file using/proc/15990/attr/currentusingsystemd-journal(27244)/lib/systemd/systemd-journaldwith resultsuccess' ); }); }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/suricata/__snapshots__/suricata_signature.test.tsx.snap b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/suricata/__snapshots__/suricata_signature.test.tsx.snap index f766befaf47e4..e55465cfd8895 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/suricata/__snapshots__/suricata_signature.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/suricata/__snapshots__/suricata_signature.test.tsx.snap @@ -34,7 +34,6 @@ exports[`SuricataSignature rendering it renders the default SuricataSignature 1` > Hello - diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/suricata/suricata_signature.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/suricata/suricata_signature.tsx index db0ddd857238f..1cd78178d017f 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/suricata/suricata_signature.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/suricata/suricata_signature.tsx @@ -13,7 +13,6 @@ import { DraggableWrapper, } from '../../../../../../common/components/drag_and_drop/draggable_wrapper'; import { escapeDataProviderId } from '../../../../../../common/components/drag_and_drop/helpers'; -import { ExternalLinkIcon } from '../../../../../../common/components/external_link_icon'; import { GoogleLink } from '../../../../../../common/components/links'; import { Provider } from '../../../data_providers/provider'; @@ -122,7 +121,6 @@ export const SuricataSignature = React.memo<{ {signature.split(' ').splice(tokens.length).join(' ')} - diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/zeek/zeek_signature.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/zeek/zeek_signature.tsx index cdf4a8cba68ab..74f75a0a73386 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/zeek/zeek_signature.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/zeek/zeek_signature.tsx @@ -15,7 +15,6 @@ import { DraggableWrapper, } from '../../../../../../common/components/drag_and_drop/draggable_wrapper'; import { escapeDataProviderId } from '../../../../../../common/components/drag_and_drop/helpers'; -import { ExternalLinkIcon } from '../../../../../../common/components/external_link_icon'; import { GoogleLink, ReputationLink } from '../../../../../../common/components/links'; import { Provider } from '../../../data_providers/provider'; import { IS_OPERATOR } from '../../../data_providers/data_provider'; @@ -120,7 +119,6 @@ export const Link = React.memo(({ value, link }) => {
{value} -
); @@ -129,7 +127,6 @@ export const Link = React.memo(({ value, link }) => {
-
); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/styles.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/styles.tsx index 47d848021ba43..ff38cfe2fbbdc 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/styles.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/styles.tsx @@ -89,12 +89,16 @@ export const EventsTrHeader = styled.div.attrs(({ className }) => ({ display: flex; `; -export const EventsThGroupActions = styled.div.attrs(({ className = '' }) => ({ +export const EventsThGroupActions = styled.div.attrs(({ className = '', isEventViewer }) => ({ className: `siemEventsTable__thGroupActions ${className}`, -}))<{ actionsColumnWidth: number }>` +}))<{ actionsColumnWidth: number; isEventViewer: boolean }>` display: flex; - flex: 0 0 ${({ actionsColumnWidth }) => `${actionsColumnWidth}px`}; + flex: 0 0 + ${({ actionsColumnWidth, isEventViewer }) => + `${!isEventViewer ? actionsColumnWidth + 4 : actionsColumnWidth}px`}; min-width: 0; + padding-left: ${({ isEventViewer }) => + !isEventViewer ? '4px;' : '0;'}; // match timeline event border `; export const EventsThGroupData = styled.div.attrs(({ className = '' }) => ({ @@ -151,6 +155,11 @@ export const EventsThContent = styled.div.attrs(({ className = '' }) => ({ width != null ? `${width}px` : '100%'}; /* Using width: 100% instead of flex: 1 and max-width: 100% for IE11 */ + + > button.euiButtonIcon, + > .euiToolTipAnchor > button.euiButtonIcon { + margin-left: ${({ theme }) => `-${theme.eui.paddingSizes.xs}`}; + } `; /* EVENTS BODY */ @@ -198,8 +207,7 @@ export const EventsTrSupplement = styled.div.attrs(({ className = '' }) => ({ }))<{ className: string }>` font-size: ${({ theme }) => theme.eui.euiFontSizeXS}; line-height: ${({ theme }) => theme.eui.euiLineHeight}; - padding: 0 ${({ theme }) => theme.eui.paddingSizes.xs} 0 - ${({ theme }) => theme.eui.paddingSizes.xl}; + padding: 0 ${({ theme }) => theme.eui.paddingSizes.xs} 0 52px; `; export const EventsTdGroupActions = styled.div.attrs(({ className = '' }) => ({ @@ -249,6 +257,11 @@ export const EventsTdContent = styled.div.attrs(({ className }) => ({ width != null ? `${width}px` : '100%'}; /* Using width: 100% instead of flex: 1 and max-width: 100% for IE11 */ + + > button.euiButtonIcon, + > .euiToolTipAnchor > button.euiButtonIcon { + margin-left: ${({ theme }) => `-${theme.eui.paddingSizes.xs}`}; + } `; /** diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/epic.ts b/x-pack/plugins/security_solution/public/timelines/store/timeline/epic.ts index 94acb9d92075b..bd93b6cc9a585 100644 --- a/x-pack/plugins/security_solution/public/timelines/store/timeline/epic.ts +++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/epic.ts @@ -329,6 +329,7 @@ const timelineInput: TimelineInput = { dataProviders: null, description: null, eventType: null, + excludedRowRendererIds: null, filters: null, kqlMode: null, kqlQuery: null, diff --git a/x-pack/plugins/security_solution/server/graphql/timeline/schema.gql.ts b/x-pack/plugins/security_solution/server/graphql/timeline/schema.gql.ts index cc2e5620f60b4..bc8d92fdda6c4 100644 --- a/x-pack/plugins/security_solution/server/graphql/timeline/schema.gql.ts +++ b/x-pack/plugins/security_solution/server/graphql/timeline/schema.gql.ts @@ -162,6 +162,7 @@ export const timelineSchema = gql` dataProviders: [DataProviderInput!] description: String eventType: String + excludedRowRendererIds: [RowRendererId!] filters: [FilterTimelineInput!] kqlMode: String kqlQuery: SerializedFilterQueryInput diff --git a/x-pack/plugins/security_solution/server/graphql/types.ts b/x-pack/plugins/security_solution/server/graphql/types.ts index 8394e1fba5f90..cddea3a4a66e9 100644 --- a/x-pack/plugins/security_solution/server/graphql/types.ts +++ b/x-pack/plugins/security_solution/server/graphql/types.ts @@ -126,6 +126,8 @@ export interface TimelineInput { eventType?: Maybe; + excludedRowRendererIds?: Maybe; + filters?: Maybe; kqlMode?: Maybe; From ded219dac942e273a94f0250ff3bdcd657db4805 Mon Sep 17 00:00:00 2001 From: Patryk Kopycinski Date: Wed, 8 Jul 2020 00:18:03 +0200 Subject: [PATCH 12/19] cleanup --- .../public/timelines/components/row_renderers_browser/index.tsx | 2 -- .../public/timelines/components/timeline/styles.tsx | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/index.tsx index 7f50e2e297238..42e76fdeb3112 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/index.tsx @@ -103,13 +103,11 @@ const StatefulRowRenderersBrowserComponent: React.FC { setExcludedRowRendererIds(Object.values(RowRendererId)); - // eslint-disable-next-line no-unused-expressions tableRef?.current?.setSelection([]); }, [tableRef, setExcludedRowRendererIds]); const handleEnableAll = useCallback(() => { setExcludedRowRendererIds([]); - // eslint-disable-next-line no-unused-expressions tableRef?.current?.setSelection(renderers); }, [tableRef, setExcludedRowRendererIds]); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/styles.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/styles.tsx index ff38cfe2fbbdc..f87b0ce286cdf 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/styles.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/styles.tsx @@ -89,7 +89,7 @@ export const EventsTrHeader = styled.div.attrs(({ className }) => ({ display: flex; `; -export const EventsThGroupActions = styled.div.attrs(({ className = '', isEventViewer }) => ({ +export const EventsThGroupActions = styled.div.attrs(({ className = '' }) => ({ className: `siemEventsTable__thGroupActions ${className}`, }))<{ actionsColumnWidth: number; isEventViewer: boolean }>` display: flex; From e5c7f1ab50fd15e650aff3a2b500da760e83bf79 Mon Sep 17 00:00:00 2001 From: Patryk Kopycinski Date: Wed, 8 Jul 2020 14:57:39 +0200 Subject: [PATCH 13/19] PR comments --- .../drag_and_drop/draggable_wrapper.tsx | 34 ++++----- .../__snapshots__/index.test.tsx.snap | 1 - .../common/components/draggables/index.tsx | 74 +++++++++++-------- .../row_renderers_browser/constants.ts | 7 ++ .../row_renderers_browser/examples/auditd.tsx | 3 +- .../examples/auditd_file.tsx | 3 +- .../examples/netflow.tsx | 5 +- .../examples/suricata.tsx | 3 +- .../row_renderers_browser/examples/system.tsx | 3 +- .../examples/system_dns.tsx | 3 +- .../examples/system_endgame_process.tsx | 3 +- .../examples/system_file.tsx | 3 +- .../examples/system_fim.tsx | 3 +- .../examples/system_security_event.tsx | 3 +- .../examples/system_socket.tsx | 3 +- .../row_renderers_browser/examples/zeek.tsx | 3 +- .../row_renderers_browser/index.tsx | 12 +-- .../row_renderers_browser.tsx | 7 +- 18 files changed, 101 insertions(+), 72 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/constants.ts diff --git a/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper.tsx b/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper.tsx index 8732eb5ea8f82..2274887b92e14 100644 --- a/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper.tsx +++ b/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper.tsx @@ -15,13 +15,13 @@ import { } from 'react-beautiful-dnd'; import { useDispatch } from 'react-redux'; import styled from 'styled-components'; -import deepEqual from 'fast-deep-equal'; import { dragAndDropActions } from '../../store/drag_and_drop'; import { DataProvider } from '../../../timelines/components/timeline/data_providers/data_provider'; +import { ROW_RENDERER_BROWSER_EXAMPLE_TIMELINE_ID } from '../../../timelines/components/row_renderers_browser/constants'; + import { TruncatableText } from '../truncatable_text'; import { WithHoverActions } from '../with_hover_actions'; - import { DraggableWrapperHoverContent, useGetTimelineId } from './draggable_wrapper_hover_content'; import { getDraggableId, getDroppableId } from './helpers'; import { ProviderContainer } from './provider_container'; @@ -64,7 +64,9 @@ const Wrapper = styled.div` ${({ disabled }) => disabled && ` - [data-rbd-draggable-id]:hover { + [data-rbd-draggable-id]:hover, + .euiBadge:hover, + .euiBadge__text:hover { cursor: default; } `} @@ -116,7 +118,6 @@ export const getStyle = ( const DraggableWrapperComponent: React.FC = ({ dataProvider, - disabled, onFilterAdded, render, timelineId, @@ -128,6 +129,7 @@ const DraggableWrapperComponent: React.FC = ({ const [goGetTimelineId, setGoGetTimelineId] = useState(false); const timelineIdFind = useGetTimelineId(draggableRef, goGetTimelineId); const [providerRegistered, setProviderRegistered] = useState(false); + const isDisabled = dataProvider.id.includes(`-${ROW_RENDERER_BROWSER_EXAMPLE_TIMELINE_ID}-`); const dispatch = useDispatch(); @@ -147,11 +149,11 @@ const DraggableWrapperComponent: React.FC = ({ }, [handleClosePopOverTrigger]); const registerProvider = useCallback(() => { - if (!disabled && !providerRegistered) { + if (!isDisabled && !providerRegistered) { dispatch(dragAndDropActions.registerProvider({ provider: dataProvider })); setProviderRegistered(true); } - }, [disabled, providerRegistered, dispatch, dataProvider]); + }, [isDisabled, providerRegistered, dispatch, dataProvider]); const unRegisterProvider = useCallback( () => dispatch(dragAndDropActions.unRegisterProvider({ id: dataProvider.id })), @@ -160,11 +162,11 @@ const DraggableWrapperComponent: React.FC = ({ useEffect( () => () => { - if (!disabled) { + if (!isDisabled) { unRegisterProvider(); } }, - [disabled, unRegisterProvider] + [isDisabled, unRegisterProvider] ); const hoverContent = useMemo( @@ -198,7 +200,7 @@ const DraggableWrapperComponent: React.FC = ({ const renderContent = useCallback( () => ( - + = ({ draggableId={getDraggableId(dataProvider.id)} index={0} key={getDraggableId(dataProvider.id)} - isDragDisabled={disabled} + isDragDisabled={isDisabled} > {(provided, snapshot) => ( = ({ ), - [dataProvider, registerProvider, render, disabled, truncate] + [dataProvider, registerProvider, render, isDisabled, truncate] ); - if (disabled) return <>{renderContent()}; + if (isDisabled) return <>{renderContent()}; return ( = ({ ); }; -export const DraggableWrapper = React.memo( - DraggableWrapperComponent, - (prevProps, nextProps) => - deepEqual(prevProps.dataProvider, nextProps.dataProvider) && - prevProps.render !== nextProps.render && - prevProps.truncate === nextProps.truncate -); +export const DraggableWrapper = React.memo(DraggableWrapperComponent); DraggableWrapper.displayName = 'DraggableWrapper'; diff --git a/x-pack/plugins/security_solution/public/common/components/draggables/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/common/components/draggables/__snapshots__/index.test.tsx.snap index 15b67c330a932..93608a181adff 100644 --- a/x-pack/plugins/security_solution/public/common/components/draggables/__snapshots__/index.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/common/components/draggables/__snapshots__/index.test.tsx.snap @@ -2,7 +2,6 @@ exports[`draggables rendering it renders the default Badge 1`] = ` ( - ({ id, field, value, name, children, timelineId, tooltipContent, queryValue }) => - value != null ? ( + ({ id, field, value, name, children, timelineId, tooltipContent, queryValue }) => { + const dataProviderProp: DataProvider = useMemo( + () => ({ + and: [], + enabled: true, + id: escapeDataProviderId(id), + name: name ? name : value ?? '', + excluded: false, + kqlQuery: '', + queryMatch: { + field, + value: queryValue ? queryValue : value ?? '', + operator: IS_OPERATOR, + }, + }), + [field, id, name, queryValue, value] + ); + + const renderCallback = useCallback( + (dataProvider, _, snapshot) => + snapshot.isDragging ? ( + + + + ) : ( + + {children} + + ), + [children, field, tooltipContent, value] + ); + + if (value == null) return null; + + return ( - snapshot.isDragging ? ( - - - - ) : ( - - {children} - - ) - } + dataProvider={dataProviderProp} + render={renderCallback} timelineId={timelineId} /> - ) : null + ); + } ); DefaultDraggable.displayName = 'DefaultDraggable'; @@ -166,7 +181,6 @@ const DraggableBadgeComponent: React.FC = ({ value={value} tooltipContent={tooltipContent} queryValue={queryValue} - disabled={contextId.includes('-row-renderer-example-')} > {children ? children : value !== '' ? value : getEmptyStringTag()} diff --git a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/constants.ts b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/constants.ts new file mode 100644 index 0000000000000..4749afda9570a --- /dev/null +++ b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/constants.ts @@ -0,0 +1,7 @@ +/* + * 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. + */ + +export const ROW_RENDERER_BROWSER_EXAMPLE_TIMELINE_ID = 'row-renderer-example'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/auditd.tsx b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/auditd.tsx index 5e974e7fb3507..b62faebff9901 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/auditd.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/auditd.tsx @@ -8,6 +8,7 @@ import React from 'react'; import { mockTimelineData } from '../../../../common/mock/mock_timeline_data'; import { createGenericAuditRowRenderer } from '../../timeline/body/renderers/auditd/generic_row_renderer'; +import { ROW_RENDERER_BROWSER_EXAMPLE_TIMELINE_ID } from '../constants'; const AuditdExampleComponent: React.FC = () => { const auditdRowRenderer = createGenericAuditRowRenderer({ @@ -20,7 +21,7 @@ const AuditdExampleComponent: React.FC = () => { {auditdRowRenderer.renderRow({ browserFields: {}, data: mockTimelineData[26].ecs, - timelineId: 'row-renderer-example', + timelineId: ROW_RENDERER_BROWSER_EXAMPLE_TIMELINE_ID, })} ); diff --git a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/auditd_file.tsx b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/auditd_file.tsx index 5b0aae8e5762e..e0fe66f54300e 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/auditd_file.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/auditd_file.tsx @@ -8,6 +8,7 @@ import React from 'react'; import { mockTimelineData } from '../../../../common/mock/mock_timeline_data'; import { createGenericFileRowRenderer } from '../../timeline/body/renderers/auditd/generic_row_renderer'; +import { ROW_RENDERER_BROWSER_EXAMPLE_TIMELINE_ID } from '../constants'; const AuditdFileExampleComponent: React.FC = () => { const auditdFileRowRenderer = createGenericFileRowRenderer({ @@ -20,7 +21,7 @@ const AuditdFileExampleComponent: React.FC = () => { {auditdFileRowRenderer.renderRow({ browserFields: {}, data: mockTimelineData[27].ecs, - timelineId: 'row-renderer-example', + timelineId: ROW_RENDERER_BROWSER_EXAMPLE_TIMELINE_ID, })} ); diff --git a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/netflow.tsx b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/netflow.tsx index 8605fbff1a84e..a276bafb65c60 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/netflow.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/netflow.tsx @@ -5,16 +5,17 @@ */ import React from 'react'; -import { getMockNetflowData } from '../../../../common/mock/netflow'; +import { getMockNetflowData } from '../../../../common/mock/netflow'; import { netflowRowRenderer } from '../../timeline/body/renderers/netflow/netflow_row_renderer'; +import { ROW_RENDERER_BROWSER_EXAMPLE_TIMELINE_ID } from '../constants'; const NetflowExampleComponent: React.FC = () => ( <> {netflowRowRenderer.renderRow({ browserFields: {}, data: getMockNetflowData(), - timelineId: 'row-renderer-example', + timelineId: ROW_RENDERER_BROWSER_EXAMPLE_TIMELINE_ID, })} ); diff --git a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/suricata.tsx b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/suricata.tsx index ecaa2797bbd43..318f427b81f28 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/suricata.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/suricata.tsx @@ -8,13 +8,14 @@ import React from 'react'; import { mockTimelineData } from '../../../../common/mock/mock_timeline_data'; import { suricataRowRenderer } from '../../timeline/body/renderers/suricata/suricata_row_renderer'; +import { ROW_RENDERER_BROWSER_EXAMPLE_TIMELINE_ID } from '../constants'; const SuricataExampleComponent: React.FC = () => ( <> {suricataRowRenderer.renderRow({ browserFields: {}, data: mockTimelineData[2].ecs, - timelineId: 'row-renderer-example', + timelineId: ROW_RENDERER_BROWSER_EXAMPLE_TIMELINE_ID, })} ); diff --git a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/system.tsx b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/system.tsx index cc11f2393db1b..828d9588103bb 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/system.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/system.tsx @@ -8,6 +8,7 @@ import React from 'react'; import { createGenericSystemRowRenderer } from '../../timeline/body/renderers/system/generic_row_renderer'; import { mockEndgameTerminationEvent } from '../../../../common/mock/mock_endgame_ecs_data'; +import { ROW_RENDERER_BROWSER_EXAMPLE_TIMELINE_ID } from '../constants'; const SystemExampleComponent: React.FC = () => { const systemRowRenderer = createGenericSystemRowRenderer({ @@ -20,7 +21,7 @@ const SystemExampleComponent: React.FC = () => { {systemRowRenderer.renderRow({ browserFields: {}, data: mockEndgameTerminationEvent, - timelineId: 'row-renderer-example', + timelineId: ROW_RENDERER_BROWSER_EXAMPLE_TIMELINE_ID, })} ); diff --git a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/system_dns.tsx b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/system_dns.tsx index b989cd5816002..4937b0f05ce7c 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/system_dns.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/system_dns.tsx @@ -8,6 +8,7 @@ import React from 'react'; import { createDnsRowRenderer } from '../../timeline/body/renderers/system/generic_row_renderer'; import { mockEndgameDnsRequest } from '../../../../common/mock/mock_endgame_ecs_data'; +import { ROW_RENDERER_BROWSER_EXAMPLE_TIMELINE_ID } from '../constants'; const SystemDnsExampleComponent: React.FC = () => { const systemDnsRowRenderer = createDnsRowRenderer(); @@ -17,7 +18,7 @@ const SystemDnsExampleComponent: React.FC = () => { {systemDnsRowRenderer.renderRow({ browserFields: {}, data: mockEndgameDnsRequest, - timelineId: 'row-renderer-example', + timelineId: ROW_RENDERER_BROWSER_EXAMPLE_TIMELINE_ID, })} ); diff --git a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/system_endgame_process.tsx b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/system_endgame_process.tsx index 22b523a3374b9..fd4a745407b27 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/system_endgame_process.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/system_endgame_process.tsx @@ -8,6 +8,7 @@ import React from 'react'; import { createEndgameProcessRowRenderer } from '../../timeline/body/renderers/system/generic_row_renderer'; import { mockEndgameCreationEvent } from '../../../../common/mock/mock_endgame_ecs_data'; +import { ROW_RENDERER_BROWSER_EXAMPLE_TIMELINE_ID } from '../constants'; const SystemEndgameProcessExampleComponent: React.FC = () => { const systemEndgameProcessRowRenderer = createEndgameProcessRowRenderer({ @@ -20,7 +21,7 @@ const SystemEndgameProcessExampleComponent: React.FC = () => { {systemEndgameProcessRowRenderer.renderRow({ browserFields: {}, data: mockEndgameCreationEvent, - timelineId: 'row-renderer-example', + timelineId: ROW_RENDERER_BROWSER_EXAMPLE_TIMELINE_ID, })} ); diff --git a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/system_file.tsx b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/system_file.tsx index 6f69e51723676..94822afe185bc 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/system_file.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/system_file.tsx @@ -8,6 +8,7 @@ import React from 'react'; import { mockEndgameFileDeleteEvent } from '../../../../common/mock/mock_endgame_ecs_data'; import { createGenericFileRowRenderer } from '../../timeline/body/renderers/system/generic_row_renderer'; +import { ROW_RENDERER_BROWSER_EXAMPLE_TIMELINE_ID } from '../constants'; const SystemFileExampleComponent: React.FC = () => { const systemFileRowRenderer = createGenericFileRowRenderer({ @@ -20,7 +21,7 @@ const SystemFileExampleComponent: React.FC = () => { {systemFileRowRenderer.renderRow({ browserFields: {}, data: mockEndgameFileDeleteEvent, - timelineId: 'row-renderer-example', + timelineId: ROW_RENDERER_BROWSER_EXAMPLE_TIMELINE_ID, })} ); diff --git a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/system_fim.tsx b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/system_fim.tsx index d40f1aad10820..a2a2091f5d08b 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/system_fim.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/system_fim.tsx @@ -8,6 +8,7 @@ import React from 'react'; import { mockEndgameFileCreateEvent } from '../../../../common/mock/mock_endgame_ecs_data'; import { createFimRowRenderer } from '../../timeline/body/renderers/system/generic_row_renderer'; +import { ROW_RENDERER_BROWSER_EXAMPLE_TIMELINE_ID } from '../constants'; const SystemFimExampleComponent: React.FC = () => { const systemFimRowRenderer = createFimRowRenderer({ @@ -20,7 +21,7 @@ const SystemFimExampleComponent: React.FC = () => { {systemFimRowRenderer.renderRow({ browserFields: {}, data: mockEndgameFileCreateEvent, - timelineId: 'row-renderer-example', + timelineId: ROW_RENDERER_BROWSER_EXAMPLE_TIMELINE_ID, })} ); diff --git a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/system_security_event.tsx b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/system_security_event.tsx index 9075fb3ad41c4..bc577771cc90c 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/system_security_event.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/system_security_event.tsx @@ -8,6 +8,7 @@ import React from 'react'; import { createSecurityEventRowRenderer } from '../../timeline/body/renderers/system/generic_row_renderer'; import { mockEndgameUserLogon } from '../../../../common/mock/mock_endgame_ecs_data'; +import { ROW_RENDERER_BROWSER_EXAMPLE_TIMELINE_ID } from '../constants'; const SystemSecurityEventExampleComponent: React.FC = () => { const systemSecurityEventRowRenderer = createSecurityEventRowRenderer({ @@ -19,7 +20,7 @@ const SystemSecurityEventExampleComponent: React.FC = () => { {systemSecurityEventRowRenderer.renderRow({ browserFields: {}, data: mockEndgameUserLogon, - timelineId: 'row-renderer-example', + timelineId: ROW_RENDERER_BROWSER_EXAMPLE_TIMELINE_ID, })} ); diff --git a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/system_socket.tsx b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/system_socket.tsx index a592d32e93079..ac698ecc4b597 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/system_socket.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/system_socket.tsx @@ -8,6 +8,7 @@ import React from 'react'; import { createSocketRowRenderer } from '../../timeline/body/renderers/system/generic_row_renderer'; import { mockEndgameIpv4ConnectionAcceptEvent } from '../../../../common/mock/mock_endgame_ecs_data'; +import { ROW_RENDERER_BROWSER_EXAMPLE_TIMELINE_ID } from '../constants'; const SystemSocketExampleComponent: React.FC = () => { const systemSocketRowRenderer = createSocketRowRenderer({ @@ -19,7 +20,7 @@ const SystemSocketExampleComponent: React.FC = () => { {systemSocketRowRenderer.renderRow({ browserFields: {}, data: mockEndgameIpv4ConnectionAcceptEvent, - timelineId: 'row-renderer-example', + timelineId: ROW_RENDERER_BROWSER_EXAMPLE_TIMELINE_ID, })} ); diff --git a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/zeek.tsx b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/zeek.tsx index 980b2cd4d1a0d..56f0d207fbc6d 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/zeek.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/zeek.tsx @@ -8,13 +8,14 @@ import React from 'react'; import { mockTimelineData } from '../../../../common/mock/mock_timeline_data'; import { zeekRowRenderer } from '../../timeline/body/renderers/zeek/zeek_row_renderer'; +import { ROW_RENDERER_BROWSER_EXAMPLE_TIMELINE_ID } from '../constants'; const ZeekExampleComponent: React.FC = () => ( <> {zeekRowRenderer.renderRow({ browserFields: {}, data: mockTimelineData[13].ecs, - timelineId: 'row-renderer-example', + timelineId: ROW_RENDERER_BROWSER_EXAMPLE_TIMELINE_ID, })} ); diff --git a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/index.tsx index 42e76fdeb3112..2792b264ba7e2 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/index.tsx @@ -17,6 +17,7 @@ import { EuiButton, EuiFlexGroup, EuiFlexItem, + EuiInMemoryTable, } from '@elastic/eui'; import React, { useState, useCallback, useRef } from 'react'; import { useDispatch, useSelector } from 'react-redux'; @@ -25,7 +26,6 @@ import styled from 'styled-components'; import { State } from '../../../common/store'; import { renderers } from './catalog'; -import { RowRendererId } from '../../../../common/types/timeline'; import { setExcludedRowRendererIds as dispatchSetExcludedRowRendererIds } from '../../../timelines/store/timeline/actions'; import { RowRenderersBrowser } from './row_renderers_browser'; import * as i18n from './translations'; @@ -79,7 +79,7 @@ interface StatefulRowRenderersBrowserProps { const StatefulRowRenderersBrowserComponent: React.FC = ({ timelineId, }) => { - const tableRef = useRef(); + const tableRef = useRef>(); const dispatch = useDispatch(); const excludedRowRendererIds = useSelector( (state: State) => state.timeline.timelineById[timelineId]?.excludedRowRendererIds || [] @@ -102,14 +102,14 @@ const StatefulRowRenderersBrowserComponent: React.FC setShow(false), []); const handleDisableAll = useCallback(() => { - setExcludedRowRendererIds(Object.values(RowRendererId)); + // eslint-disable-next-line no-unused-expressions tableRef?.current?.setSelection([]); - }, [tableRef, setExcludedRowRendererIds]); + }, [tableRef]); const handleEnableAll = useCallback(() => { - setExcludedRowRendererIds([]); + // eslint-disable-next-line no-unused-expressions tableRef?.current?.setSelection(renderers); - }, [tableRef, setExcludedRowRendererIds]); + }, [tableRef]); return ( <> diff --git a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/row_renderers_browser.tsx b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/row_renderers_browser.tsx index cc7e31e519bb6..8e5018350e41d 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/row_renderers_browser.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/row_renderers_browser.tsx @@ -13,6 +13,7 @@ import { RowRendererId } from '../../../../common/types/timeline'; import { renderers, RowRendererOption } from './catalog'; interface RowRenderersBrowserProps { + // ref?: React.Ref>; excludedRowRendererIds: RowRendererId[]; setExcludedRowRendererIds: (excludedRowRendererIds: RowRendererId[]) => void; } @@ -65,8 +66,8 @@ const search = { */ const renderSearchableDescriptionNoop = () => <>; -const RowRenderersBrowserComponent: React.FC = React.forwardRef( - ({ excludedRowRendererIds = [], setExcludedRowRendererIds }, ref) => { +const RowRenderersBrowserComponent = React.forwardRef( + ({ excludedRowRendererIds = [], setExcludedRowRendererIds }: RowRenderersBrowserProps, ref) => { const sort = useMemo( () => ({ sort: { @@ -155,6 +156,6 @@ const RowRenderersBrowserComponent: React.FC = React.f RowRenderersBrowserComponent.displayName = 'RowRenderersBrowserComponent'; -export const RowRenderersBrowser = RowRenderersBrowserComponent; +export const RowRenderersBrowser = React.memo(RowRenderersBrowserComponent); RowRenderersBrowser.displayName = 'RowRenderersBrowser'; From 60152d7b53629bf035e1ab5853e2f11171109ce7 Mon Sep 17 00:00:00 2001 From: Patryk Kopycinski Date: Thu, 9 Jul 2020 13:33:34 +0200 Subject: [PATCH 14/19] PR comments --- .../cypress/screens/hosts/events.ts | 2 +- .../cypress/screens/timeline.ts | 2 +- .../public/app/home/index.tsx | 2 +- .../alerts_viewer/default_headers.ts | 3 +- .../events_viewer/events_viewer.test.tsx | 2 +- .../common/components/events_viewer/index.tsx | 14 ++++-- .../public/common/mock/global_state.ts | 1 - .../public/common/mock/mock_timeline_data.ts | 26 +++++----- .../public/common/mock/timeline_results.ts | 2 - .../components/alerts_table/actions.test.tsx | 1 - .../alerts_table/default_config.tsx | 3 +- .../public/detections/index.ts | 4 +- .../components/fields_browser/index.test.tsx | 35 +++---------- .../components/fields_browser/index.tsx | 2 +- .../components/open_timeline/helpers.test.ts | 4 -- .../row_renderers_browser/examples/auditd.tsx | 3 +- .../examples/auditd_file.tsx | 3 +- .../row_renderers_browser/examples/system.tsx | 3 +- .../examples/system_endgame_process.tsx | 3 +- .../examples/system_file.tsx | 3 +- .../examples/system_fim.tsx | 3 +- .../examples/system_socket.tsx | 3 +- .../row_renderers_browser.tsx | 50 ++++++++++++------- .../body/column_headers/helpers.test.ts | 3 +- .../timeline/body/column_headers/helpers.ts | 15 ++++-- .../components/timeline/body/constants.ts | 3 ++ .../generic_row_renderer.test.tsx.snap | 4 +- .../auditd/generic_row_renderer.test.tsx | 2 +- .../timeline/body/stateful_body.tsx | 11 ++-- .../timelines/store/timeline/actions.ts | 1 - .../timelines/store/timeline/defaults.ts | 1 - .../timelines/store/timeline/epic.test.ts | 1 - .../timelines/store/timeline/helpers.ts | 7 ++- .../public/timelines/store/timeline/model.ts | 3 -- .../timelines/store/timeline/reducer.test.ts | 8 --- .../timelines/store/timeline/reducer.ts | 4 +- .../timeline/routes/utils/export_timelines.ts | 2 +- 37 files changed, 113 insertions(+), 126 deletions(-) diff --git a/x-pack/plugins/security_solution/cypress/screens/hosts/events.ts b/x-pack/plugins/security_solution/cypress/screens/hosts/events.ts index a946fefe273e1..4b1ca19bd96fe 100644 --- a/x-pack/plugins/security_solution/cypress/screens/hosts/events.ts +++ b/x-pack/plugins/security_solution/cypress/screens/hosts/events.ts @@ -7,7 +7,7 @@ export const CLOSE_MODAL = '[data-test-subj="modal-inspect-close"]'; export const EVENTS_VIEWER_FIELDS_BUTTON = - '[data-test-subj="events-viewer-panel"] [data-test-subj="show-field-browser-gear"]'; + '[data-test-subj="events-viewer-panel"] [data-test-subj="show-field-browser"]'; export const EVENTS_VIEWER_PANEL = '[data-test-subj="events-viewer-panel"]'; diff --git a/x-pack/plugins/security_solution/cypress/screens/timeline.ts b/x-pack/plugins/security_solution/cypress/screens/timeline.ts index f41e499f70101..c673cf34b6dae 100644 --- a/x-pack/plugins/security_solution/cypress/screens/timeline.ts +++ b/x-pack/plugins/security_solution/cypress/screens/timeline.ts @@ -47,7 +47,7 @@ export const TIMELINE_DESCRIPTION = '[data-test-subj="timeline-description"]'; export const TIMELINE_DROPPED_DATA_PROVIDERS = '[data-test-subj="providerContainer"]'; export const TIMELINE_FIELDS_BUTTON = - '[data-test-subj="timeline"] [data-test-subj="show-field-browser-gear"]'; + '[data-test-subj="timeline"] [data-test-subj="show-field-browser"]'; export const TIMELINE_FLYOUT_HEADER = '[data-test-subj="eui-flyout-header"]'; diff --git a/x-pack/plugins/security_solution/public/app/home/index.tsx b/x-pack/plugins/security_solution/public/app/home/index.tsx index 909a1804456c2..8f03945df437c 100644 --- a/x-pack/plugins/security_solution/public/app/home/index.tsx +++ b/x-pack/plugins/security_solution/public/app/home/index.tsx @@ -17,7 +17,7 @@ import { UseUrlState } from '../../common/components/url_state'; import { useWithSource } from '../../common/containers/source'; import { useShowTimeline } from '../../common/utils/timeline/use_show_timeline'; import { navTabs } from './home_navigations'; -import { useSignalIndex } from '../../alerts/containers/detection_engine/alerts/use_signal_index'; +import { useSignalIndex } from '../../detections/containers/detection_engine/alerts/use_signal_index'; const WrappedByAutoSizer = styled.div` height: 100%; diff --git a/x-pack/plugins/security_solution/public/common/components/alerts_viewer/default_headers.ts b/x-pack/plugins/security_solution/public/common/components/alerts_viewer/default_headers.ts index cf5b565b99f67..ba4ecf9a33eee 100644 --- a/x-pack/plugins/security_solution/public/common/components/alerts_viewer/default_headers.ts +++ b/x-pack/plugins/security_solution/public/common/components/alerts_viewer/default_headers.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { RowRendererId } from '../../../../common/types/timeline'; import { defaultColumnHeaderType } from '../../../timelines/components/timeline/body/column_headers/default_headers'; import { DEFAULT_COLUMN_MIN_WIDTH, @@ -69,5 +70,5 @@ export const alertsHeaders: ColumnHeaderOptions[] = [ export const alertsDefaultModel: SubsetTimelineModel = { ...timelineDefaults, columns: alertsHeaders, - showRowRenderers: false, + excludedRowRendererIds: Object.values(RowRendererId), }; diff --git a/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.test.tsx b/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.test.tsx index 2a079ce015f0d..38ca1176d1700 100644 --- a/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.test.tsx @@ -77,7 +77,7 @@ describe('EventsViewer', () => { await wait(); wrapper.update(); - expect(wrapper.find(`[data-test-subj="show-field-browser-gear"]`).first().exists()).toBe(true); + expect(wrapper.find(`[data-test-subj="show-field-browser"]`).first().exists()).toBe(true); }); test('it renders the footer containing the Load More button', async () => { diff --git a/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx b/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx index 8e79481e6410d..b89d2b8c08625 100644 --- a/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx @@ -45,6 +45,7 @@ const StatefulEventsViewerComponent: React.FC = ({ defaultIndices, deleteEventQuery, end, + excludedRowRendererIds, filters, headerFilterGroup, id, @@ -57,7 +58,6 @@ const StatefulEventsViewerComponent: React.FC = ({ removeColumn, start, showCheckboxes, - showRowRenderers, sort, updateItemsPerPage, upsertColumn, @@ -69,7 +69,14 @@ const StatefulEventsViewerComponent: React.FC = ({ useEffect(() => { if (createTimeline != null) { - createTimeline({ id, columns, sort, itemsPerPage, showCheckboxes, showRowRenderers }); + createTimeline({ + id, + columns, + excludedRowRendererIds, + sort, + itemsPerPage, + showCheckboxes, + }); } return () => { deleteEventQuery({ id, inputId: 'global' }); @@ -151,7 +158,6 @@ const makeMapStateToProps = () => { kqlMode, sort, showCheckboxes, - showRowRenderers, } = events; return { @@ -168,7 +174,6 @@ const makeMapStateToProps = () => { query: getGlobalQuerySelector(state), sort, showCheckboxes, - showRowRenderers, }; }; return mapStateToProps; @@ -207,7 +212,6 @@ export const StatefulEventsViewer = connector( prevProps.start === nextProps.start && deepEqual(prevProps.pageFilters, nextProps.pageFilters) && prevProps.showCheckboxes === nextProps.showCheckboxes && - prevProps.showRowRenderers === nextProps.showRowRenderers && prevProps.start === nextProps.start && prevProps.utilityBar === nextProps.utilityBar ) diff --git a/x-pack/plugins/security_solution/public/common/mock/global_state.ts b/x-pack/plugins/security_solution/public/common/mock/global_state.ts index c8b9d4f7e720b..89f100992e1b9 100644 --- a/x-pack/plugins/security_solution/public/common/mock/global_state.ts +++ b/x-pack/plugins/security_solution/public/common/mock/global_state.ts @@ -216,7 +216,6 @@ export const mockGlobalState: State = { }, selectedEventIds: {}, show: false, - showRowRenderers: true, showCheckboxes: false, pinnedEventIds: {}, pinnedEventsSaveObject: {}, diff --git a/x-pack/plugins/security_solution/public/common/mock/mock_timeline_data.ts b/x-pack/plugins/security_solution/public/common/mock/mock_timeline_data.ts index 4d80c6de9922f..9974842bff474 100644 --- a/x-pack/plugins/security_solution/public/common/mock/mock_timeline_data.ts +++ b/x-pack/plugins/security_solution/public/common/mock/mock_timeline_data.ts @@ -418,8 +418,8 @@ export const mockTimelineData: TimelineItem[] = [ data: [ { field: '@timestamp', value: ['2019-03-07T05:06:51.000Z'] }, { field: 'host.name', value: ['zeek-franfurt'] }, - { field: 'source.ip', value: ['185.176.26.101'] }, - { field: 'destination.ip', value: ['207.154.238.205'] }, + { field: 'source.ip', value: ['192.168.26.101'] }, + { field: 'destination.ip', value: ['192.168.238.205'] }, ], ecs: { _id: '14', @@ -466,8 +466,8 @@ export const mockTimelineData: TimelineItem[] = [ data: [ { field: '@timestamp', value: ['2019-03-07T00:51:28.000Z'] }, { field: 'host.name', value: ['suricata-zeek-singapore'] }, - { field: 'source.ip', value: ['206.189.35.240'] }, - { field: 'destination.ip', value: ['67.207.67.3'] }, + { field: 'source.ip', value: ['192.168.35.240'] }, + { field: 'destination.ip', value: ['192.168.67.3'] }, ], ecs: { _id: '15', @@ -520,8 +520,8 @@ export const mockTimelineData: TimelineItem[] = [ data: [ { field: '@timestamp', value: ['2019-03-05T07:00:20.000Z'] }, { field: 'host.name', value: ['suricata-zeek-singapore'] }, - { field: 'source.ip', value: ['206.189.35.240'] }, - { field: 'destination.ip', value: ['192.241.164.26'] }, + { field: 'source.ip', value: ['192.168.35.240'] }, + { field: 'destination.ip', value: ['192.168.164.26'] }, ], ecs: { _id: '16', @@ -572,7 +572,7 @@ export const mockTimelineData: TimelineItem[] = [ data: [ { field: '@timestamp', value: ['2019-02-28T22:36:28.000Z'] }, { field: 'host.name', value: ['zeek-franfurt'] }, - { field: 'source.ip', value: ['8.42.77.171'] }, + { field: 'source.ip', value: ['192.168.77.171'] }, ], ecs: { _id: '17', @@ -621,8 +621,8 @@ export const mockTimelineData: TimelineItem[] = [ data: [ { field: '@timestamp', value: ['2019-02-22T21:12:13.000Z'] }, { field: 'host.name', value: ['zeek-sensor-amsterdam'] }, - { field: 'source.ip', value: ['188.166.66.184'] }, - { field: 'destination.ip', value: ['91.189.95.15'] }, + { field: 'source.ip', value: ['192.168.66.184'] }, + { field: 'destination.ip', value: ['192.168.95.15'] }, ], ecs: { _id: '18', @@ -767,7 +767,7 @@ export const mockTimelineData: TimelineItem[] = [ { field: '@timestamp', value: ['2019-03-14T22:30:25.527Z'] }, { field: 'event.category', value: ['user-login'] }, { field: 'host.name', value: ['zeek-london'] }, - { field: 'source.ip', value: ['8.42.77.171'] }, + { field: 'source.ip', value: ['192.168.77.171'] }, { field: 'user.name', value: ['root'] }, ], ecs: { @@ -1101,7 +1101,7 @@ export const mockTimelineData: TimelineItem[] = [ { field: 'event.action', value: ['connected-to'] }, { field: 'event.category', value: ['audit-rule'] }, { field: 'host.name', value: ['zeek-london'] }, - { field: 'destination.ip', value: ['93.184.216.34'] }, + { field: 'destination.ip', value: ['192.168.216.34'] }, { field: 'user.name', value: ['alice'] }, ], ecs: { @@ -1121,7 +1121,7 @@ export const mockTimelineData: TimelineItem[] = [ data: null, summary: { actor: { primary: ['alice'], secondary: ['alice'] }, - object: { primary: ['93.184.216.34'], secondary: ['80'], type: ['socket'] }, + object: { primary: ['192.168.216.34'], secondary: ['80'], type: ['socket'] }, how: ['/usr/bin/wget'], message_type: null, sequence: null, @@ -1133,7 +1133,7 @@ export const mockTimelineData: TimelineItem[] = [ ip: ['46.101.3.136', '10.16.0.5', 'fe80::4066:42ff:fe19:b3b9'], }, source: null, - destination: { ip: ['93.184.216.34'], port: [80] }, + destination: { ip: ['192.168.216.34'], port: [80] }, geo: null, suricata: null, network: null, diff --git a/x-pack/plugins/security_solution/public/common/mock/timeline_results.ts b/x-pack/plugins/security_solution/public/common/mock/timeline_results.ts index abd84edea63f1..b1df41a19aebe 100644 --- a/x-pack/plugins/security_solution/public/common/mock/timeline_results.ts +++ b/x-pack/plugins/security_solution/public/common/mock/timeline_results.ts @@ -2138,7 +2138,6 @@ export const mockTimelineModel: TimelineModel = { selectedEventIds: {}, show: false, showCheckboxes: false, - showRowRenderers: true, sort: { columnId: '@timestamp', sortDirection: Direction.desc, @@ -2243,7 +2242,6 @@ export const defaultTimelineProps: CreateTimelineProps = { selectedEventIds: {}, show: false, showCheckboxes: false, - showRowRenderers: true, sort: { columnId: '@timestamp', sortDirection: Direction.desc }, status: TimelineStatus.draft, title: '', diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.test.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.test.tsx index 9658125b60557..1e68ba476f7bb 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.test.tsx @@ -211,7 +211,6 @@ describe('alert actions', () => { selectedEventIds: {}, show: true, showCheckboxes: false, - showRowRenderers: true, sort: { columnId: '@timestamp', sortDirection: 'desc', diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/default_config.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/default_config.tsx index 697dff4012982..2765f6a3d37af 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/default_config.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/default_config.tsx @@ -11,6 +11,7 @@ import ApolloClient from 'apollo-client'; import { Dispatch } from 'redux'; import { EuiText } from '@elastic/eui'; +import { RowRendererId } from '../../../../common/types/timeline'; import { Status } from '../../../../common/detection_engine/schemas/common/schemas'; import { Filter } from '../../../../../../../src/plugins/data/common/es_query'; import { @@ -162,7 +163,7 @@ export const alertsDefaultModel: SubsetTimelineModel = { ...timelineDefaults, columns: alertsHeaders, showCheckboxes: true, - showRowRenderers: false, + excludedRowRendererIds: Object.values(RowRendererId), }; export const requiredFieldsForActions = [ diff --git a/x-pack/plugins/security_solution/public/detections/index.ts b/x-pack/plugins/security_solution/public/detections/index.ts index d043127a3098b..30d1e30417583 100644 --- a/x-pack/plugins/security_solution/public/detections/index.ts +++ b/x-pack/plugins/security_solution/public/detections/index.ts @@ -10,7 +10,7 @@ import { TimelineIdLiteral, TimelineId } from '../../common/types/timeline'; import { AlertsRoutes } from './routes'; import { SecuritySubPlugin } from '../app/types'; -const ALERTS_TIMELINE_IDS: TimelineIdLiteral[] = [ +const DETECTIONS_TIMELINE_IDS: TimelineIdLiteral[] = [ TimelineId.detectionsRulesDetailsPage, TimelineId.detectionsPage, ]; @@ -22,7 +22,7 @@ export class Detections { return { SubPluginRoutes: AlertsRoutes, storageTimelines: { - timelineById: getTimelinesInStorageByIds(storage, ALERTS_TIMELINE_IDS), + timelineById: getTimelinesInStorageByIds(storage, DETECTIONS_TIMELINE_IDS), }, }; } diff --git a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/index.test.tsx index cbdca0c60f96b..ed3f957ad11a8 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/index.test.tsx @@ -45,7 +45,7 @@ describe('StatefulFieldsBrowser', () => { ); - expect(wrapper.find('[data-test-subj="show-field-browser-gear"]').exists()).toBe(true); + expect(wrapper.find('[data-test-subj="show-field-browser"]').exists()).toBe(true); }); describe('toggleShow', () => { @@ -82,7 +82,7 @@ describe('StatefulFieldsBrowser', () => { ); - wrapper.find('[data-test-subj="show-field-browser-gear"]').first().simulate('click'); + wrapper.find('[data-test-subj="show-field-browser"]').first().simulate('click'); expect(wrapper.find('[data-test-subj="fields-browser-container"]').exists()).toBe(true); }); @@ -107,7 +107,7 @@ describe('StatefulFieldsBrowser', () => { ); - wrapper.find('[data-test-subj="show-field-browser-gear"]').first().simulate('click'); + wrapper.find('[data-test-subj="show-field-browser"]').first().simulate('click'); wrapper.find(`.field-browser-category-pane-auditd-${timelineId}`).first().simulate('click'); @@ -132,7 +132,7 @@ describe('StatefulFieldsBrowser', () => { ); - wrapper.find('[data-test-subj="show-field-browser-gear"]').first().simulate('click'); + wrapper.find('[data-test-subj="show-field-browser"]').first().simulate('click'); expect( wrapper.find(`.field-browser-category-pane-cloud-${timelineId}`).at(1) ).toHaveStyleRule('font-weight', 'normal', { modifier: '.euiText' }); @@ -167,31 +167,10 @@ describe('StatefulFieldsBrowser', () => { ); - expect(wrapper.find('[data-test-subj="show-field-browser-gear"]').first().exists()).toBe(true); + expect(wrapper.find('[data-test-subj="show-field-browser"]').first().exists()).toBe(true); }); - test('it does NOT render the default Fields Browser button when the isEventViewer prop is false', () => { - const isEventViewer = false; - - const wrapper = mount( - - - - ); - - expect(wrapper.find('[data-test-subj="show-field-browser"]').first().exists()).toBe(false); - }); - - test('it does NOT render the default Fields Browser button when the isEventViewer prop is true', () => { + test('it renders the Fields Browser button as a settings gear when the isEventViewer prop is false', () => { const isEventViewer = true; const wrapper = mount( @@ -209,6 +188,6 @@ describe('StatefulFieldsBrowser', () => { ); - expect(wrapper.find('[data-test-subj="show-field-browser"]').first().exists()).toBe(false); + expect(wrapper.find('[data-test-subj="show-field-browser"]').first().exists()).toBe(true); }); }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/index.tsx index 9582a2c25d88f..7b843b4f69447 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/index.tsx @@ -149,7 +149,7 @@ export const StatefulFieldsBrowserComponent: React.FC = ({ diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.test.ts b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.test.ts index 8b33251cedb03..89a35fb838a96 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.test.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.test.ts @@ -295,7 +295,6 @@ describe('helpers', () => { selectedEventIds: {}, show: false, showCheckboxes: false, - showRowRenderers: true, sort: { columnId: '@timestamp', sortDirection: 'desc', @@ -394,7 +393,6 @@ describe('helpers', () => { selectedEventIds: {}, show: false, showCheckboxes: false, - showRowRenderers: true, sort: { columnId: '@timestamp', sortDirection: 'desc', @@ -535,7 +533,6 @@ describe('helpers', () => { selectedEventIds: {}, show: false, showCheckboxes: false, - showRowRenderers: true, sort: { columnId: '@timestamp', sortDirection: 'desc', @@ -705,7 +702,6 @@ describe('helpers', () => { selectedEventIds: {}, show: false, showCheckboxes: false, - showRowRenderers: true, sort: { columnId: '@timestamp', sortDirection: 'desc', diff --git a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/auditd.tsx b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/auditd.tsx index b62faebff9901..d90d0fdfa558b 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/auditd.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/auditd.tsx @@ -8,12 +8,13 @@ import React from 'react'; import { mockTimelineData } from '../../../../common/mock/mock_timeline_data'; import { createGenericAuditRowRenderer } from '../../timeline/body/renderers/auditd/generic_row_renderer'; +import { CONNECTED_USING } from '../../timeline/body/renderers/auditd/translations'; import { ROW_RENDERER_BROWSER_EXAMPLE_TIMELINE_ID } from '../constants'; const AuditdExampleComponent: React.FC = () => { const auditdRowRenderer = createGenericAuditRowRenderer({ actionName: 'connected-to', - text: 'connected using', + text: CONNECTED_USING, }); return ( diff --git a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/auditd_file.tsx b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/auditd_file.tsx index e0fe66f54300e..fc8e51864f50a 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/auditd_file.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/auditd_file.tsx @@ -8,12 +8,13 @@ import React from 'react'; import { mockTimelineData } from '../../../../common/mock/mock_timeline_data'; import { createGenericFileRowRenderer } from '../../timeline/body/renderers/auditd/generic_row_renderer'; +import { OPENED_FILE, USING } from '../../timeline/body/renderers/auditd/translations'; import { ROW_RENDERER_BROWSER_EXAMPLE_TIMELINE_ID } from '../constants'; const AuditdFileExampleComponent: React.FC = () => { const auditdFileRowRenderer = createGenericFileRowRenderer({ actionName: 'opened-file', - text: 'opened file using', + text: `${OPENED_FILE} ${USING}`, }); return ( diff --git a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/system.tsx b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/system.tsx index 828d9588103bb..c8c3b48ac366a 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/system.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/system.tsx @@ -6,6 +6,7 @@ import React from 'react'; +import { TERMINATED_PROCESS } from '../../timeline/body/renderers/system/translations'; import { createGenericSystemRowRenderer } from '../../timeline/body/renderers/system/generic_row_renderer'; import { mockEndgameTerminationEvent } from '../../../../common/mock/mock_endgame_ecs_data'; import { ROW_RENDERER_BROWSER_EXAMPLE_TIMELINE_ID } from '../constants'; @@ -13,7 +14,7 @@ import { ROW_RENDERER_BROWSER_EXAMPLE_TIMELINE_ID } from '../constants'; const SystemExampleComponent: React.FC = () => { const systemRowRenderer = createGenericSystemRowRenderer({ actionName: 'termination_event', - text: 'terminated process', + text: TERMINATED_PROCESS, }); return ( diff --git a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/system_endgame_process.tsx b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/system_endgame_process.tsx index fd4a745407b27..675bc172ab6f7 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/system_endgame_process.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/system_endgame_process.tsx @@ -8,12 +8,13 @@ import React from 'react'; import { createEndgameProcessRowRenderer } from '../../timeline/body/renderers/system/generic_row_renderer'; import { mockEndgameCreationEvent } from '../../../../common/mock/mock_endgame_ecs_data'; +import { PROCESS_STARTED } from '../../timeline/body/renderers/system/translations'; import { ROW_RENDERER_BROWSER_EXAMPLE_TIMELINE_ID } from '../constants'; const SystemEndgameProcessExampleComponent: React.FC = () => { const systemEndgameProcessRowRenderer = createEndgameProcessRowRenderer({ actionName: 'creation_event', - text: 'started process', + text: PROCESS_STARTED, }); return ( diff --git a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/system_file.tsx b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/system_file.tsx index 94822afe185bc..62c243a7e8502 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/system_file.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/system_file.tsx @@ -8,12 +8,13 @@ import React from 'react'; import { mockEndgameFileDeleteEvent } from '../../../../common/mock/mock_endgame_ecs_data'; import { createGenericFileRowRenderer } from '../../timeline/body/renderers/system/generic_row_renderer'; +import { DELETED_FILE } from '../../timeline/body/renderers/system/translations'; import { ROW_RENDERER_BROWSER_EXAMPLE_TIMELINE_ID } from '../constants'; const SystemFileExampleComponent: React.FC = () => { const systemFileRowRenderer = createGenericFileRowRenderer({ actionName: 'file_delete_event', - text: 'deleted a file', + text: DELETED_FILE, }); return ( diff --git a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/system_fim.tsx b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/system_fim.tsx index a2a2091f5d08b..ad3eeb7f797ff 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/system_fim.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/system_fim.tsx @@ -8,12 +8,13 @@ import React from 'react'; import { mockEndgameFileCreateEvent } from '../../../../common/mock/mock_endgame_ecs_data'; import { createFimRowRenderer } from '../../timeline/body/renderers/system/generic_row_renderer'; +import { CREATED_FILE } from '../../timeline/body/renderers/system/translations'; import { ROW_RENDERER_BROWSER_EXAMPLE_TIMELINE_ID } from '../constants'; const SystemFimExampleComponent: React.FC = () => { const systemFimRowRenderer = createFimRowRenderer({ actionName: 'file_create_event', - text: 'created a file', + text: CREATED_FILE, }); return ( diff --git a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/system_socket.tsx b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/system_socket.tsx index ac698ecc4b597..dd119d1b60f39 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/system_socket.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/examples/system_socket.tsx @@ -6,6 +6,7 @@ import React from 'react'; +import { ACCEPTED_A_CONNECTION_VIA } from '../../timeline/body/renderers/system/translations'; import { createSocketRowRenderer } from '../../timeline/body/renderers/system/generic_row_renderer'; import { mockEndgameIpv4ConnectionAcceptEvent } from '../../../../common/mock/mock_endgame_ecs_data'; import { ROW_RENDERER_BROWSER_EXAMPLE_TIMELINE_ID } from '../constants'; @@ -13,7 +14,7 @@ import { ROW_RENDERER_BROWSER_EXAMPLE_TIMELINE_ID } from '../constants'; const SystemSocketExampleComponent: React.FC = () => { const systemSocketRowRenderer = createSocketRowRenderer({ actionName: 'ipv4_connection_accept_event', - text: 'accepted a connection via', + text: ACCEPTED_A_CONNECTION_VIA, }); return ( <> diff --git a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/row_renderers_browser.tsx b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/row_renderers_browser.tsx index 8e5018350e41d..1310308bbc456 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/row_renderers_browser.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/row_renderers_browser.tsx @@ -6,14 +6,13 @@ import { EuiFlexItem, EuiInMemoryTable } from '@elastic/eui'; import React, { useMemo, useCallback } from 'react'; -import { xorBy } from 'lodash/fp'; +import { xor, xorBy } from 'lodash/fp'; import styled from 'styled-components'; import { RowRendererId } from '../../../../common/types/timeline'; import { renderers, RowRendererOption } from './catalog'; interface RowRenderersBrowserProps { - // ref?: React.Ref>; excludedRowRendererIds: RowRendererId[]; setExcludedRowRendererIds: (excludedRowRendererIds: RowRendererId[]) => void; } @@ -66,16 +65,35 @@ const search = { */ const renderSearchableDescriptionNoop = () => <>; +const initialSorting = { + sort: { + field: 'name', + direction: 'asc', + }, +}; + +const StyledNameButton = styled.button``; + const RowRenderersBrowserComponent = React.forwardRef( ({ excludedRowRendererIds = [], setExcludedRowRendererIds }: RowRenderersBrowserProps, ref) => { - const sort = useMemo( - () => ({ - sort: { - field: 'name', - direction: 'asc', - }, - }), - [] + const notExcludedRowRenderers = useMemo(() => { + if (excludedRowRendererIds.length === Object.keys(RowRendererId).length) return []; + + return renderers.filter((renderer) => !excludedRowRendererIds.includes(renderer.id)); + }, [excludedRowRendererIds]); + + const handleNameClick = useCallback( + (item: RowRendererOption) => () => { + const newSelection = xor([item], notExcludedRowRenderers); + // @ts-ignore + ref?.current?.setSelection(newSelection); // eslint-disable-line no-unused-expressions + }, + [notExcludedRowRenderers, ref] + ); + + const nameColumnRenderCallback = useCallback( + (value, item) => {value}, + [handleNameClick] ); const columns = useMemo( @@ -84,8 +102,8 @@ const RowRenderersBrowserComponent = React.forwardRef( field: 'name', name: 'Name', sortable: true, - truncateText: true, width: '10%', + render: nameColumnRenderCallback, }, { field: 'description', @@ -107,15 +125,9 @@ const RowRenderersBrowserComponent = React.forwardRef( render: renderSearchableDescriptionNoop, }, ], - [] + [nameColumnRenderCallback] ); - const notExcludedRowRenderers = useMemo(() => { - if (excludedRowRendererIds.length === Object.keys(RowRendererId).length) return []; - - return renderers.filter((renderer) => !excludedRowRendererIds.includes(renderer.id)); - }, [excludedRowRendererIds]); - const handleSelectable = useCallback(() => true, []); const handleSelectionChange = useCallback( @@ -146,7 +158,7 @@ const RowRenderersBrowserComponent = React.forwardRef( itemId="id" columns={columns} search={search} - sorting={sort} + sorting={initialSorting} isSelectable={true} selection={selectionValue} /> diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/helpers.test.ts b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/helpers.test.ts index d66b75538ef6f..07cd8d128b153 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/helpers.test.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/helpers.test.ts @@ -11,6 +11,7 @@ import { DEFAULT_ACTIONS_COLUMN_WIDTH, SHOW_CHECK_BOXES_COLUMN_WIDTH, EVENTS_VIEWER_ACTIONS_COLUMN_WIDTH, + MINIMUM_ACTIONS_COLUMN_WIDTH, } from '../constants'; describe('helpers', () => { @@ -36,7 +37,7 @@ describe('helpers', () => { }); test('returns the events viewer actions column width when isEventViewer is true', () => { - expect(getActionsColumnWidth(true)).toEqual(EVENTS_VIEWER_ACTIONS_COLUMN_WIDTH); + expect(getActionsColumnWidth(true)).toEqual(MINIMUM_ACTIONS_COLUMN_WIDTH); }); test('returns the events viewer actions column width + checkbox width when isEventViewer is true and showCheckboxes is true', () => { diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/helpers.ts b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/helpers.ts index c538457431fef..e6ace4f5e593a 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/helpers.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/helpers.ts @@ -14,6 +14,7 @@ import { SHOW_CHECK_BOXES_COLUMN_WIDTH, EVENTS_VIEWER_ACTIONS_COLUMN_WIDTH, DEFAULT_ACTIONS_COLUMN_WIDTH, + MINIMUM_ACTIONS_COLUMN_WIDTH, } from '../constants'; /** Enriches the column headers with field details from the specified browserFields */ @@ -42,7 +43,13 @@ export const getActionsColumnWidth = ( isEventViewer: boolean, showCheckboxes = false, additionalActionWidth = 0 -): number => - (showCheckboxes ? SHOW_CHECK_BOXES_COLUMN_WIDTH : 0) + - (isEventViewer ? EVENTS_VIEWER_ACTIONS_COLUMN_WIDTH : DEFAULT_ACTIONS_COLUMN_WIDTH) + - additionalActionWidth; +): number => { + const actionsColumnWidth = + (showCheckboxes ? SHOW_CHECK_BOXES_COLUMN_WIDTH : 0) + + (isEventViewer ? EVENTS_VIEWER_ACTIONS_COLUMN_WIDTH : DEFAULT_ACTIONS_COLUMN_WIDTH) + + additionalActionWidth; + + return actionsColumnWidth > MINIMUM_ACTIONS_COLUMN_WIDTH + ? actionsColumnWidth + : MINIMUM_ACTIONS_COLUMN_WIDTH; +}; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/constants.ts b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/constants.ts index 5f3fb4fa5113c..6b6ae3c3467b5 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/constants.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/constants.ts @@ -4,6 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ +/** The minimum (fixed) width of the Actions column */ +export const MINIMUM_ACTIONS_COLUMN_WIDTH = 50; // px; + /** The (fixed) width of the Actions column */ export const DEFAULT_ACTIONS_COLUMN_WIDTH = 76; // px; /** diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/auditd/__snapshots__/generic_row_renderer.test.tsx.snap b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/auditd/__snapshots__/generic_row_renderer.test.tsx.snap index f0b15df005911..784924e896673 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/auditd/__snapshots__/generic_row_renderer.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/auditd/__snapshots__/generic_row_renderer.test.tsx.snap @@ -32,7 +32,7 @@ exports[`GenericRowRenderer #createGenericAuditRowRenderer renders correctly aga "message_type": null, "object": Object { "primary": Array [ - "93.184.216.34", + "192.168.216.34", ], "secondary": Array [ "80", @@ -46,7 +46,7 @@ exports[`GenericRowRenderer #createGenericAuditRowRenderer renders correctly aga }, "destination": Object { "ip": Array [ - "93.184.216.34", + "192.168.216.34", ], "port": Array [ 80, diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/auditd/generic_row_renderer.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/auditd/generic_row_renderer.test.tsx index ce9e3d3d5c749..1e314c0ebd281 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/auditd/generic_row_renderer.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/auditd/generic_row_renderer.test.tsx @@ -80,7 +80,7 @@ describe('GenericRowRenderer', () => { ); expect(wrapper.text()).toContain( - 'Session246alice@zeek-londonconnected usingwget(1490)wget www.example.comwith resultsuccessDestination93.184.216.34:80' + 'Session246alice@zeek-londonconnected usingwget(1490)wget www.example.comwith resultsuccessDestination192.168.216.34:80' ); }); }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/stateful_body.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/stateful_body.tsx index bba4bfb05bf7e..141534f1dcb6f 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/stateful_body.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/stateful_body.tsx @@ -75,7 +75,6 @@ const StatefulBodyComponent = React.memo( clearSelected, show, showCheckboxes, - showRowRenderers, graphEventId, sort, toggleColumn, @@ -176,16 +175,15 @@ const StatefulBodyComponent = React.memo( const enabledRowRenderers = useMemo(() => { if ( - !showRowRenderers || - (excludedRowRendererIds && - excludedRowRendererIds.length === Object.keys(RowRendererId).length) + excludedRowRendererIds && + excludedRowRendererIds.length === Object.keys(RowRendererId).length ) return [plainRowRenderer]; if (!excludedRowRendererIds) return rowRenderers; return rowRenderers.filter((rowRenderer) => !excludedRowRendererIds.includes(rowRenderer.id)); - }, [excludedRowRendererIds, showRowRenderers]); + }, [excludedRowRendererIds]); return ( ( prevProps.show === nextProps.show && prevProps.selectedEventIds === nextProps.selectedEventIds && prevProps.showCheckboxes === nextProps.showCheckboxes && - prevProps.showRowRenderers === nextProps.showRowRenderers && prevProps.sort === nextProps.sort ); @@ -267,7 +264,6 @@ const makeMapStateToProps = () => { selectedEventIds, show, showCheckboxes, - showRowRenderers, } = timeline; return { @@ -284,7 +280,6 @@ const makeMapStateToProps = () => { selectedEventIds, show, showCheckboxes, - showRowRenderers, }; }; return mapStateToProps; diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/actions.ts b/x-pack/plugins/security_solution/public/timelines/store/timeline/actions.ts index 41837a3df1098..b6885ef53328f 100644 --- a/x-pack/plugins/security_solution/public/timelines/store/timeline/actions.ts +++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/actions.ts @@ -69,7 +69,6 @@ export const createTimeline = actionCreator<{ show?: boolean; sort?: Sort; showCheckboxes?: boolean; - showRowRenderers?: boolean; timelineType?: TimelineTypeLiteral; templateTimelineId?: string; templateTimelineVersion?: number; diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/defaults.ts b/x-pack/plugins/security_solution/public/timelines/store/timeline/defaults.ts index 8db54bc289a69..f4c4085715af9 100644 --- a/x-pack/plugins/security_solution/public/timelines/store/timeline/defaults.ts +++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/defaults.ts @@ -50,7 +50,6 @@ export const timelineDefaults: SubsetTimelineModel & Pick { selectedEventIds: {}, show: true, showCheckboxes: false, - showRowRenderers: true, sort: { columnId: '@timestamp', sortDirection: Direction.desc }, status: TimelineStatus.active, width: 1100, diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/helpers.ts b/x-pack/plugins/security_solution/public/timelines/store/timeline/helpers.ts index 9b5232de14e07..bec5a2e430019 100644 --- a/x-pack/plugins/security_solution/public/timelines/store/timeline/helpers.ts +++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/helpers.ts @@ -133,6 +133,7 @@ interface AddNewTimelineParams { start: number; end: number; }; + excludedRowRendererIds?: RowRendererId[]; filters?: Filter[]; id: string; itemsPerPage?: number; @@ -143,7 +144,6 @@ interface AddNewTimelineParams { show?: boolean; sort?: Sort; showCheckboxes?: boolean; - showRowRenderers?: boolean; timelineById: TimelineById; timelineType: TimelineTypeLiteral; } @@ -153,6 +153,7 @@ export const addNewTimeline = ({ columns, dataProviders = [], dateRange = { start: 0, end: 0 }, + excludedRowRendererIds = [], filters = timelineDefaults.filters, id, itemsPerPage = timelineDefaults.itemsPerPage, @@ -160,7 +161,6 @@ export const addNewTimeline = ({ sort = timelineDefaults.sort, show = false, showCheckboxes = false, - showRowRenderers = true, timelineById, timelineType, }: AddNewTimelineParams): TimelineById => { @@ -179,6 +179,7 @@ export const addNewTimeline = ({ columns, dataProviders, dateRange, + excludedRowRendererIds, filters, itemsPerPage, kqlQuery, @@ -189,7 +190,6 @@ export const addNewTimeline = ({ isSaving: false, isLoading: false, showCheckboxes, - showRowRenderers, timelineType: !disableTemplate ? timelineType : timelineDefaults.timelineType, ...templateTimelineInfo, }, @@ -1372,7 +1372,6 @@ export const updateExcludedRowRenderersIds = ({ [id]: { ...timeline, excludedRowRendererIds, - showRowRenderers: excludedRowRendererIds.length !== Object.keys(RowRendererId).length, }, }; }; diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/model.ts b/x-pack/plugins/security_solution/public/timelines/store/timeline/model.ts index a353d242afffa..db933ab11af3f 100644 --- a/x-pack/plugins/security_solution/public/timelines/store/timeline/model.ts +++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/model.ts @@ -111,8 +111,6 @@ export interface TimelineModel { show: boolean; /** When true, shows checkboxes enabling selection. Selected events store in selectedEventIds **/ showCheckboxes: boolean; - /** When true, shows additional rowRenderers below the PlainRowRenderer **/ - showRowRenderers: boolean; /** Specifies which column the timeline is sorted on, and the direction (ascending / descending) */ sort: Sort; /** status: active | draft */ @@ -157,7 +155,6 @@ export type SubsetTimelineModel = Readonly< | 'selectedEventIds' | 'show' | 'showCheckboxes' - | 'showRowRenderers' | 'sort' | 'width' | 'isSaving' diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/reducer.test.ts b/x-pack/plugins/security_solution/public/timelines/store/timeline/reducer.test.ts index 6956b6ab0bf21..4d7372059b858 100644 --- a/x-pack/plugins/security_solution/public/timelines/store/timeline/reducer.test.ts +++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/reducer.test.ts @@ -96,7 +96,6 @@ const timelineByIdMock: TimelineById = { selectedEventIds: {}, show: true, showCheckboxes: false, - showRowRenderers: true, sort: { columnId: '@timestamp', sortDirection: Direction.desc, @@ -1131,7 +1130,6 @@ describe('Timeline', () => { }, selectedEventIds: {}, show: true, - showRowRenderers: true, showCheckboxes: false, sort: { columnId: '@timestamp', @@ -1228,7 +1226,6 @@ describe('Timeline', () => { }, selectedEventIds: {}, show: true, - showRowRenderers: true, showCheckboxes: false, sort: { columnId: '@timestamp', @@ -1435,7 +1432,6 @@ describe('Timeline', () => { }, selectedEventIds: {}, show: true, - showRowRenderers: true, showCheckboxes: false, sort: { columnId: '@timestamp', @@ -1532,7 +1528,6 @@ describe('Timeline', () => { }, selectedEventIds: {}, show: true, - showRowRenderers: true, showCheckboxes: false, sort: { columnId: '@timestamp', @@ -1728,7 +1723,6 @@ describe('Timeline', () => { }, selectedEventIds: {}, show: true, - showRowRenderers: true, showCheckboxes: false, sort: { columnId: '@timestamp', @@ -1795,7 +1789,6 @@ describe('Timeline', () => { isLoading: false, id: 'foo', savedObjectId: null, - showRowRenderers: true, kqlMode: 'filter', kqlQuery: { filterQuery: null, filterQueryDraft: null }, loadingEventIds: [], @@ -1914,7 +1907,6 @@ describe('Timeline', () => { }, selectedEventIds: {}, show: true, - showRowRenderers: true, showCheckboxes: false, sort: { columnId: '@timestamp', diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/reducer.ts b/x-pack/plugins/security_solution/public/timelines/store/timeline/reducer.ts index bdd303e9c76bb..3c7f990c98146 100644 --- a/x-pack/plugins/security_solution/public/timelines/store/timeline/reducer.ts +++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/reducer.ts @@ -129,13 +129,13 @@ export const timelineReducer = reducerWithInitialState(initialTimelineState) id, dataProviders, dateRange, + excludedRowRendererIds, show, columns, itemsPerPage, kqlQuery, sort, showCheckboxes, - showRowRenderers, timelineType = TimelineType.default, filters, } @@ -146,6 +146,7 @@ export const timelineReducer = reducerWithInitialState(initialTimelineState) columns, dataProviders, dateRange, + excludedRowRendererIds, filters, id, itemsPerPage, @@ -153,7 +154,6 @@ export const timelineReducer = reducerWithInitialState(initialTimelineState) sort, show, showCheckboxes, - showRowRenderers, timelineById: state.timelineById, timelineType, }), diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/export_timelines.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/export_timelines.ts index 23090bfc6f0bd..f4b97ac3510cc 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/export_timelines.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/export_timelines.ts @@ -181,7 +181,7 @@ const getTimelinesFromObjects = async ( if (myTimeline != null) { const timelineNotes = myNotes.filter((n) => n.timelineId === timelineId); const timelinePinnedEventIds = myPinnedEventIds.filter((p) => p.timelineId === timelineId); - const exportedTimeline = omit('status', myTimeline); + const exportedTimeline = omit(['status', 'excludedRowRendererIds'], myTimeline); return [ ...acc, { From fd18049529c80b9ba227904a20ac00b35300c2fd Mon Sep 17 00:00:00 2001 From: Patryk Kopycinski Date: Thu, 9 Jul 2020 13:46:10 +0200 Subject: [PATCH 15/19] Fix styling --- .../components/timeline/body/column_headers/helpers.ts | 7 ++++--- .../components/timeline/body/column_headers/index.tsx | 2 ++ 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/helpers.ts b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/helpers.ts index e6ace4f5e593a..903b17c4e8f15 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/helpers.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/helpers.ts @@ -44,12 +44,13 @@ export const getActionsColumnWidth = ( showCheckboxes = false, additionalActionWidth = 0 ): number => { + const checkboxesWidth = showCheckboxes ? SHOW_CHECK_BOXES_COLUMN_WIDTH : 0; const actionsColumnWidth = - (showCheckboxes ? SHOW_CHECK_BOXES_COLUMN_WIDTH : 0) + + checkboxesWidth + (isEventViewer ? EVENTS_VIEWER_ACTIONS_COLUMN_WIDTH : DEFAULT_ACTIONS_COLUMN_WIDTH) + additionalActionWidth; - return actionsColumnWidth > MINIMUM_ACTIONS_COLUMN_WIDTH + return actionsColumnWidth > MINIMUM_ACTIONS_COLUMN_WIDTH + checkboxesWidth ? actionsColumnWidth - : MINIMUM_ACTIONS_COLUMN_WIDTH; + : MINIMUM_ACTIONS_COLUMN_WIDTH + checkboxesWidth; }; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/index.tsx index bbba6e7cf6330..b139aa1a7a9a6 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/index.tsx @@ -198,6 +198,8 @@ export const ColumnHeadersComponent = ({ toggleColumn={toggleColumn} width={FIELD_BROWSER_WIDTH} /> +
+ Date: Thu, 9 Jul 2020 15:40:43 +0200 Subject: [PATCH 16/19] fix styling --- .../__snapshots__/index.test.tsx.snap | 2 + .../body/column_headers/helpers.test.ts | 3 +- .../body/events/event_column_view.tsx | 70 ++++++++++--------- 3 files changed, 40 insertions(+), 35 deletions(-) diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/__snapshots__/index.test.tsx.snap index eee94f4b58d4e..2436e71a89b86 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/__snapshots__/index.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/__snapshots__/index.test.tsx.snap @@ -477,6 +477,8 @@ exports[`ColumnHeaders rendering renders correctly against snapshot 1`] = ` toggleColumn={[MockFunction]} width={900} /> + + { test('returns the events viewer actions column width + checkbox width when isEventViewer is true and showCheckboxes is true', () => { expect(getActionsColumnWidth(true, true)).toEqual( - EVENTS_VIEWER_ACTIONS_COLUMN_WIDTH + SHOW_CHECK_BOXES_COLUMN_WIDTH + MINIMUM_ACTIONS_COLUMN_WIDTH + SHOW_CHECK_BOXES_COLUMN_WIDTH ); }); }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/event_column_view.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/event_column_view.tsx index a450d082cb85d..bd3b2db6af049 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/event_column_view.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/event_column_view.tsx @@ -21,7 +21,7 @@ import { Note } from '../../../../../common/lib/note'; import { ColumnHeaderOptions } from '../../../../../timelines/store/timeline/model'; import { AssociateNote, UpdateNote } from '../../../notes/helpers'; import { OnColumnResized, OnPinEvent, OnRowSelected, OnUnPinEvent } from '../../events'; -import { EventsTdContent, EventsTrData } from '../../styles'; +import { EventsTd, EventsTdContent, EventsTrData } from '../../styles'; import { Actions } from '../actions'; import { DataDrivenColumns } from '../data_driven_columns'; import { eventHasNotes, getPinOnClick } from '../helpers'; @@ -133,22 +133,24 @@ export const EventColumnView = React.memo( ...acc, icon: [ ...acc.icon, - - - action.onClick({ eventId: id, ecsData, data })} - /> - - , + + + + action.onClick({ eventId: id, ecsData, data })} + /> + + + , ], }; } @@ -176,23 +178,25 @@ export const EventColumnView = React.memo( return grouped.contextMenu.length > 0 ? [ ...grouped.icon, - - + - - - , + + + + + , ] : grouped.icon; }, [button, closePopover, id, onClickCb, data, ecsData, timelineActions, isPopoverOpen]); From ee8a2c38ee0a61375a16f5b302369827d918c244 Mon Sep 17 00:00:00 2001 From: Patryk Kopycinski Date: Thu, 9 Jul 2020 21:18:03 +0200 Subject: [PATCH 17/19] Fix merge --- .../drag_and_drop/draggable_wrapper.tsx | 17 ++++++++--------- .../data_providers/provider_item_badge.tsx | 2 +- .../timelines/store/timeline/reducer.test.ts | 4 ++-- 3 files changed, 11 insertions(+), 12 deletions(-) diff --git a/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper.tsx b/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper.tsx index 2274887b92e14..3cbb905de93c1 100644 --- a/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper.tsx +++ b/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper.tsx @@ -130,7 +130,6 @@ const DraggableWrapperComponent: React.FC = ({ const timelineIdFind = useGetTimelineId(draggableRef, goGetTimelineId); const [providerRegistered, setProviderRegistered] = useState(false); const isDisabled = dataProvider.id.includes(`-${ROW_RENDERER_BROWSER_EXAMPLE_TIMELINE_ID}-`); - const dispatch = useDispatch(); const handleClosePopOverTrigger = useCallback( @@ -149,24 +148,24 @@ const DraggableWrapperComponent: React.FC = ({ }, [handleClosePopOverTrigger]); const registerProvider = useCallback(() => { - if (!isDisabled && !providerRegistered) { + if (!isDisabled) { dispatch(dragAndDropActions.registerProvider({ provider: dataProvider })); setProviderRegistered(true); } - }, [isDisabled, providerRegistered, dispatch, dataProvider]); + }, [isDisabled, dispatch, dataProvider]); const unRegisterProvider = useCallback( - () => dispatch(dragAndDropActions.unRegisterProvider({ id: dataProvider.id })), - [dispatch, dataProvider] + () => + providerRegistered && + dispatch(dragAndDropActions.unRegisterProvider({ id: dataProvider.id })), + [providerRegistered, dispatch, dataProvider.id] ); useEffect( () => () => { - if (!isDisabled) { - unRegisterProvider(); - } + unRegisterProvider(); }, - [isDisabled, unRegisterProvider] + [unRegisterProvider] ); const hoverContent = useMemo( diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/provider_item_badge.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/provider_item_badge.tsx index bc7c313553f1e..ece23d7a10886 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/provider_item_badge.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/provider_item_badge.tsx @@ -97,7 +97,7 @@ export const ProviderItemBadge = React.memo( useEffect(() => { // optionally register the provider if provided - if (!providerRegistered && register != null) { + if (register != null) { dispatch(dragAndDropActions.registerProvider({ provider: { ...register, and: [] } })); setProviderRegistered(true); } diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/reducer.test.ts b/x-pack/plugins/security_solution/public/timelines/store/timeline/reducer.test.ts index d89b10db2363e..4cfc20eb81705 100644 --- a/x-pack/plugins/security_solution/public/timelines/store/timeline/reducer.test.ts +++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/reducer.test.ts @@ -1619,6 +1619,7 @@ describe('Timeline', () => { description: '', deletedEventIds: [], eventIdToNoteIds: {}, + excludedRowRendererIds: [], highlightedDropAndProviderId: '', historyIds: [], isFavorite: false, @@ -1639,7 +1640,6 @@ describe('Timeline', () => { }, selectedEventIds: {}, show: true, - showRowRenderers: true, showCheckboxes: false, sort: { columnId: '@timestamp', @@ -1722,6 +1722,7 @@ describe('Timeline', () => { description: '', deletedEventIds: [], eventIdToNoteIds: {}, + excludedRowRendererIds: [], highlightedDropAndProviderId: '', historyIds: [], isFavorite: false, @@ -1742,7 +1743,6 @@ describe('Timeline', () => { }, selectedEventIds: {}, show: true, - showRowRenderers: true, showCheckboxes: false, sort: { columnId: '@timestamp', From c65bc8871a2fbbaad64bed0dd7d91aacd456c12e Mon Sep 17 00:00:00 2001 From: Patryk Kopycinski Date: Fri, 10 Jul 2020 10:20:53 +0200 Subject: [PATCH 18/19] Fix styling --- .../components/drag_and_drop/draggable_wrapper.tsx | 1 - .../source_destination/source_destination_arrows.tsx | 4 ++++ .../row_renderers_browser/row_renderers_browser.tsx | 10 ++++++++-- .../components/timeline/body/actions/index.tsx | 6 +++--- .../public/timelines/components/timeline/styles.tsx | 3 +-- 5 files changed, 16 insertions(+), 8 deletions(-) diff --git a/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper.tsx b/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper.tsx index 3cbb905de93c1..64f6699d21dac 100644 --- a/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper.tsx +++ b/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper.tsx @@ -77,7 +77,6 @@ Wrapper.displayName = 'Wrapper'; const ProviderContentWrapper = styled.span` > span.euiToolTipAnchor { display: block; /* allow EuiTooltip content to be truncatable */ - white-space: nowrap; } `; diff --git a/x-pack/plugins/security_solution/public/network/components/source_destination/source_destination_arrows.tsx b/x-pack/plugins/security_solution/public/network/components/source_destination/source_destination_arrows.tsx index 95cc76a349c17..73c5c1e37da0f 100644 --- a/x-pack/plugins/security_solution/public/network/components/source_destination/source_destination_arrows.tsx +++ b/x-pack/plugins/security_solution/public/network/components/source_destination/source_destination_arrows.tsx @@ -35,6 +35,10 @@ Percent.displayName = 'Percent'; const SourceDestinationArrowsContainer = styled(EuiFlexGroup)` margin: 0 2px; + + .euiToolTipAnchor { + white-space: nowrap; + } `; SourceDestinationArrowsContainer.displayName = 'SourceDestinationArrowsContainer'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/row_renderers_browser.tsx b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/row_renderers_browser.tsx index 1310308bbc456..d2b0ad788fdb5 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/row_renderers_browser.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/row_renderers_browser.tsx @@ -72,7 +72,9 @@ const initialSorting = { }, }; -const StyledNameButton = styled.button``; +const StyledNameButton = styled.button` + text-align: left; +`; const RowRenderersBrowserComponent = React.forwardRef( ({ excludedRowRendererIds = [], setExcludedRowRendererIds }: RowRenderersBrowserProps, ref) => { @@ -92,7 +94,11 @@ const RowRenderersBrowserComponent = React.forwardRef( ); const nameColumnRenderCallback = useCallback( - (value, item) => {value}, + (value, item) => ( + + {value} + + ), [handleNameClick] ); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/index.tsx index 2039307691321..daf1984092269 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/index.tsx @@ -118,9 +118,9 @@ export const Actions = React.memo( - {loading && } - - {!loading && ( + {loading ? ( + + ) : ( ({ */ export const EventsLoading = styled(EuiLoadingSpinner)` - margin: ${({ theme }) => theme.eui.euiSizeXS}; - vertical-align: top; + vertical-align: middle; `; From 73059696c689ffe4d72b808165fbc6231acb090b Mon Sep 17 00:00:00 2001 From: Patryk Kopycinski Date: Fri, 10 Jul 2020 12:33:17 +0200 Subject: [PATCH 19/19] fix flyout --- .../public/timelines/components/flyout/pane/index.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/x-pack/plugins/security_solution/public/timelines/components/flyout/pane/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/flyout/pane/index.tsx index fbe3c475c9fe6..8c03d82aafafb 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/flyout/pane/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/flyout/pane/index.tsx @@ -28,6 +28,7 @@ interface FlyoutPaneComponentProps { const EuiFlyoutContainer = styled.div` .timeline-flyout { + z-index: 4001; min-width: 150px; width: auto; }