diff --git a/x-pack/legacy/plugins/siem/public/components/events_viewer/default_model.tsx b/x-pack/legacy/plugins/siem/public/components/events_viewer/default_model.tsx new file mode 100644 index 00000000000000..ac385057406e23 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/components/events_viewer/default_model.tsx @@ -0,0 +1,13 @@ +/* + * 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 { defaultHeaders } from './default_headers'; +import { SubsetTimelineModel, timelineDefaults } from '../../store/timeline/model'; + +export const eventsDefaultModel: SubsetTimelineModel = { + ...timelineDefaults, + columns: defaultHeaders, +}; diff --git a/x-pack/legacy/plugins/siem/public/components/events_viewer/events_viewer.test.tsx b/x-pack/legacy/plugins/siem/public/components/events_viewer/events_viewer.test.tsx index 25b2427d34d6ab..01276b94f1c849 100644 --- a/x-pack/legacy/plugins/siem/public/components/events_viewer/events_viewer.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/events_viewer/events_viewer.test.tsx @@ -8,7 +8,7 @@ import { mount } from 'enzyme'; import React from 'react'; import { MockedProvider } from 'react-apollo/test-utils'; -import { TestProviders } from '../../mock'; +import { mockIndexPattern, TestProviders } from '../../mock'; import { mockUiSettings } from '../../mock/ui_settings'; import { wait } from '../../lib/helpers'; @@ -16,6 +16,9 @@ import { mockEventViewerResponse } from './mock'; import { StatefulEventsViewer } from '.'; import { defaultHeaders } from './default_headers'; import { useKibanaCore } from '../../lib/compose/kibana_core'; +import { useFetchIndexPatterns } from '../../containers/detection_engine/rules/fetch_index_patterns'; +import { mockBrowserFields } from '../../containers/source/mock'; +import { eventsDefaultModel } from './default_model'; jest.mock('../../lib/settings/use_kibana_ui_setting'); @@ -25,6 +28,15 @@ mockUseKibanaCore.mockImplementation(() => ({ uiSettings: mockUiSettings, })); +const mockUseFetchIndexPatterns: jest.Mock = useFetchIndexPatterns as jest.Mock; +jest.mock('../../containers/detection_engine/rules/fetch_index_patterns'); +mockUseFetchIndexPatterns.mockImplementation(() => [ + { + browserFields: mockBrowserFields, + indexPatterns: mockIndexPattern, + }, +]); + const from = 1566943856794; const to = 1566857456791; @@ -33,7 +45,12 @@ describe('EventsViewer', () => { const wrapper = mount( - + ); @@ -53,7 +70,12 @@ describe('EventsViewer', () => { const wrapper = mount( - + ); @@ -73,7 +95,12 @@ describe('EventsViewer', () => { const wrapper = mount( - + ); @@ -94,7 +121,12 @@ describe('EventsViewer', () => { const wrapper = mount( - + ); diff --git a/x-pack/legacy/plugins/siem/public/components/events_viewer/events_viewer.tsx b/x-pack/legacy/plugins/siem/public/components/events_viewer/events_viewer.tsx index ee7853d092784f..9878194a178260 100644 --- a/x-pack/legacy/plugins/siem/public/components/events_viewer/events_viewer.tsx +++ b/x-pack/legacy/plugins/siem/public/components/events_viewer/events_viewer.tsx @@ -26,13 +26,13 @@ import { Footer, footerHeight } from '../timeline/footer'; import { combineQueries } from '../timeline/helpers'; import { TimelineRefetch } from '../timeline/refetch_timeline'; import { isCompactFooter } from '../timeline/timeline'; -import { ManageTimelineContext } from '../timeline/timeline_context'; +import { ManageTimelineContext, TimelineTypeContextProps } from '../timeline/timeline_context'; import * as i18n from './translations'; import { - IIndexPattern, - Query, esFilters, esQuery, + IIndexPattern, + Query, } from '../../../../../../../src/plugins/data/public'; const DEFAULT_EVENTS_VIEWER_HEIGHT = 500; @@ -48,6 +48,7 @@ interface Props { dataProviders: DataProvider[]; end: number; filters: esFilters.Filter[]; + headerFilterGroup?: React.ReactNode; height?: number; id: string; indexPattern: IIndexPattern; @@ -60,7 +61,9 @@ interface Props { showInspect: boolean; start: number; sort: Sort; + timelineTypeContext: TimelineTypeContextProps; toggleColumn: (column: ColumnHeader) => void; + utilityBar?: (totalCount: number) => React.ReactNode; } export const EventsViewer = React.memo( @@ -70,6 +73,7 @@ export const EventsViewer = React.memo( dataProviders, end, filters, + headerFilterGroup, height = DEFAULT_EVENTS_VIEWER_HEIGHT, id, indexPattern, @@ -82,7 +86,9 @@ export const EventsViewer = React.memo( showInspect, start, sort, + timelineTypeContext, toggleColumn, + utilityBar, }) => { const columnsHeader = isEmpty(columns) ? defaultHeaders : columns; const core = useKibanaCore(); @@ -116,6 +122,7 @@ export const EventsViewer = React.memo( fields={columnsHeader.map(c => c.id)} filterQuery={combinedQueries.filterQuery} id={id} + indexPattern={indexPattern} limit={itemsPerPage} sortField={{ sortFieldId: sort.columnId, @@ -137,17 +144,29 @@ export const EventsViewer = React.memo( + subtitle={ + utilityBar + ? undefined + : `${i18n.SHOWING}: ${totalCount.toLocaleString()} ${i18n.UNIT( + totalCount + )}` + } + title={timelineTypeContext?.title ?? i18n.EVENTS} + > + {headerFilterGroup} + + + {utilityBar?.(totalCount)}
- + ({ uiSettings: mockUiSettings, })); +const mockUseFetchIndexPatterns: jest.Mock = useFetchIndexPatterns as jest.Mock; +jest.mock('../../containers/detection_engine/rules/fetch_index_patterns'); +mockUseFetchIndexPatterns.mockImplementation(() => [ + { + browserFields: mockBrowserFields, + indexPatterns: mockIndexPattern, + }, +]); + const from = 1566943856794; const to = 1566857456791; @@ -32,7 +44,12 @@ describe('StatefulEventsViewer', () => { const wrapper = mount( - + ); @@ -52,7 +69,12 @@ describe('StatefulEventsViewer', () => { const wrapper = mount( - + ); @@ -72,7 +94,12 @@ describe('StatefulEventsViewer', () => { const wrapper = mount( - + ); diff --git a/x-pack/legacy/plugins/siem/public/components/events_viewer/index.tsx b/x-pack/legacy/plugins/siem/public/components/events_viewer/index.tsx index 3514fda4efe29e..613861a4c905c3 100644 --- a/x-pack/legacy/plugins/siem/public/components/events_viewer/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/events_viewer/index.tsx @@ -5,26 +5,35 @@ */ import { isEqual } from 'lodash/fp'; -import React, { useEffect, useState, useCallback } from 'react'; +import React, { useCallback, useEffect, useState } from 'react'; import { connect } from 'react-redux'; import { ActionCreator } from 'typescript-fsa'; -import { WithSource } from '../../containers/source'; +import chrome from 'ui/chrome'; import { inputsModel, inputsSelectors, State, timelineSelectors } from '../../store'; -import { timelineActions, inputsActions } from '../../store/actions'; -import { KqlMode, TimelineModel } from '../../store/timeline/model'; +import { inputsActions, timelineActions } from '../../store/actions'; +import { KqlMode, SubsetTimelineModel, TimelineModel } from '../../store/timeline/model'; import { ColumnHeader } from '../timeline/body/column_headers/column_header'; import { DataProvider } from '../timeline/data_providers/data_provider'; import { Sort } from '../timeline/body/sort'; import { OnChangeItemsPerPage } from '../timeline/events'; -import { Query, esFilters } from '../../../../../../../src/plugins/data/public'; +import { esFilters, Query } from '../../../../../../../src/plugins/data/public'; import { EventsViewer } from './events_viewer'; import { InputsModelId } from '../../store/inputs/constants'; +import { useFetchIndexPatterns } from '../../containers/detection_engine/rules/fetch_index_patterns'; +import { TimelineTypeContextProps } from '../timeline/timeline_context'; +import { DEFAULT_INDEX_KEY } from '../../../common/constants'; export interface OwnProps { + defaultIndices?: string[]; + defaultFilters?: esFilters.Filter[]; + defaultModel: SubsetTimelineModel; end: number; id: string; start: number; + headerFilterGroup?: React.ReactNode; + timelineTypeContext?: TimelineTypeContextProps; + utilityBar?: (totalCount: number) => React.ReactNode; } interface StateReduxProps { @@ -74,9 +83,12 @@ const StatefulEventsViewerComponent = React.memo( createTimeline, columns, dataProviders, + defaultModel, + defaultIndices, deleteEventQuery, end, filters, + headerFilterGroup, id, isLive, itemsPerPage, @@ -86,10 +98,18 @@ const StatefulEventsViewerComponent = React.memo( removeColumn, start, sort, + timelineTypeContext = { + showCheckboxes: false, + showRowRenderers: true, + }, updateItemsPerPage, upsertColumn, + utilityBar, }) => { const [showInspect, setShowInspect] = useState(false); + const [{ browserFields, indexPatterns }] = useFetchIndexPatterns( + defaultIndices ?? chrome.getUiSettingsClient().get(DEFAULT_INDEX_KEY) + ); useEffect(() => { if (createTimeline != null) { @@ -131,31 +151,30 @@ const StatefulEventsViewerComponent = React.memo( const handleOnMouseLeave = useCallback(() => setShowInspect(false), []); return ( - - {({ indexPattern, browserFields }) => ( -
- -
- )} -
+
+ +
); }, (prevProps, nextProps) => @@ -182,9 +201,9 @@ const makeMapStateToProps = () => { const getGlobalQuerySelector = inputsSelectors.globalQuerySelector(); const getGlobalFiltersQuerySelector = inputsSelectors.globalFiltersQuerySelector(); const getEvents = timelineSelectors.getEventsByIdSelector(); - const mapStateToProps = (state: State, { id }: OwnProps) => { + const mapStateToProps = (state: State, { id, defaultModel }: OwnProps) => { const input: inputsModel.InputsRange = getInputsTimeline(state); - const events: TimelineModel = getEvents(state, id); + const events: TimelineModel = getEvents(state, id) ?? defaultModel; const { columns, dataProviders, itemsPerPage, itemsPerPageOptions, kqlMode, sort } = events; return { diff --git a/x-pack/legacy/plugins/siem/public/components/events_viewer/mock.ts b/x-pack/legacy/plugins/siem/public/components/events_viewer/mock.ts index 8512d9535cc69c..352b0b95c6dd4d 100644 --- a/x-pack/legacy/plugins/siem/public/components/events_viewer/mock.ts +++ b/x-pack/legacy/plugins/siem/public/components/events_viewer/mock.ts @@ -5,7 +5,6 @@ */ import { noop } from 'lodash/fp'; -import { defaultIndexPattern } from '../../../default_index_pattern'; import { timelineQuery } from '../../containers/timeline/index.gql_query'; export const mockEventViewerResponse = [ @@ -31,7 +30,7 @@ export const mockEventViewerResponse = [ sourceId: 'default', pagination: { limit: 25, cursor: null, tiebreaker: null }, sortField: { sortFieldId: '@timestamp', direction: 'desc' }, - defaultIndex: defaultIndexPattern, + defaultIndex: ['filebeat-*', 'auditbeat-*', 'packetbeat-*'], inspect: false, }, }, diff --git a/x-pack/legacy/plugins/siem/public/components/flyout/header/index.tsx b/x-pack/legacy/plugins/siem/public/components/flyout/header/index.tsx index 22c57c8c8a00ec..e1075c89ca3505 100644 --- a/x-pack/legacy/plugins/siem/public/components/flyout/header/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/flyout/header/index.tsx @@ -25,7 +25,7 @@ import { Properties } from '../../timeline/properties'; import { appActions, appModel } from '../../../store/app'; import { inputsActions } from '../../../store/inputs'; import { timelineActions } from '../../../store/actions'; -import { TimelineModel } from '../../../store/timeline/model'; +import { timelineDefaults, TimelineModel } from '../../../store/timeline/model'; import { DEFAULT_TIMELINE_WIDTH } from '../../timeline/body/helpers'; import { InputsModelId } from '../../../store/inputs/constants'; @@ -129,7 +129,7 @@ const makeMapStateToProps = () => { const getNotesByIds = appSelectors.notesByIdsSelector(); const getGlobalInput = inputsSelectors.globalSelector(); const mapStateToProps = (state: State, { timelineId }: OwnProps) => { - const timeline: TimelineModel = getTimeline(state, timelineId); + const timeline: TimelineModel = getTimeline(state, timelineId) ?? timelineDefaults; const globalInput: inputsModel.InputsRange = getGlobalInput(state); const { dataProviders, diff --git a/x-pack/legacy/plugins/siem/public/components/open_timeline/index.tsx b/x-pack/legacy/plugins/siem/public/components/open_timeline/index.tsx index ba99a92f66b493..c22c5fdbcfbc59 100644 --- a/x-pack/legacy/plugins/siem/public/components/open_timeline/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/open_timeline/index.tsx @@ -42,6 +42,7 @@ import { } from './types'; import { DEFAULT_SORT_FIELD, DEFAULT_SORT_DIRECTION } from './constants'; import { ColumnHeader } from '../timeline/body/column_headers/column_header'; +import { timelineDefaults } from '../../store/timeline/model'; interface OwnProps { apolloClient: ApolloClient; @@ -315,7 +316,7 @@ export const StatefulOpenTimelineComponent = React.memo( const makeMapStateToProps = () => { const getTimeline = timelineSelectors.getTimelineByIdSelector(); const mapStateToProps = (state: State) => { - const timeline = getTimeline(state, 'timeline-1'); + const timeline = getTimeline(state, 'timeline-1') ?? timelineDefaults; return { timeline, diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/column_header.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/column_header.tsx index 1de227a9a675b3..15911f522032a3 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/column_header.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/column_header.tsx @@ -17,6 +17,7 @@ export interface ColumnHeader { example?: string; format?: string; id: ColumnId; + label?: string; placeholder?: string; type?: string; width: number; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/header/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/header/index.test.tsx index 35a4f4a74ae200..bfcf3cd6397999 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/header/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/header/index.test.tsx @@ -67,6 +67,31 @@ describe('Header', () => { ).toEqual(columnHeader.id); }); + test('it renders the header text alias when label is provided', () => { + const label = 'Timestamp'; + const headerWithLabel = { ...columnHeader, label }; + const wrapper = mount( + + + + ); + + expect( + wrapper + .find(`[data-test-subj="header-text-${columnHeader.id}"]`) + .first() + .text() + ).toEqual(label); + }); + test('it renders a sort indicator', () => { const headerSortable = { ...columnHeader, aggregatable: true }; const wrapper = mount( diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/header/index.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/header/index.tsx index 474de010204970..311b4bfda60fe6 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/header/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/header/index.tsx @@ -50,7 +50,7 @@ const HeaderComp = React.memo( data-test-subj="header-tooltip" content={} > - <>{header.id} + <>{header.label ?? header.id} @@ -66,7 +66,7 @@ const HeaderComp = React.memo( data-test-subj="header-tooltip" content={} > - <>{header.id} + <>{header.label ?? header.id} diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/events/event_column_view.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/events/event_column_view.tsx index 96cb9f754f525b..72f45bdb27faec 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/events/event_column_view.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/events/event_column_view.tsx @@ -17,6 +17,7 @@ import { ColumnHeader } from '../column_headers/column_header'; import { DataDrivenColumns } from '../data_driven_columns'; import { eventHasNotes, getPinOnClick } from '../helpers'; import { ColumnRenderer } from '../renderers/column_renderer'; +import { useTimelineTypeContext } from '../../timeline_context'; interface Props { id: string; @@ -67,44 +68,48 @@ export const EventColumnView = React.memo( timelineId, toggleShowNotes, updateNote, - }) => ( - - + }) => { + const timelineTypeContext = useTimelineTypeContext(); - - - ), + return ( + + + + + + ); + }, (prevProps, nextProps) => { return ( prevProps.id === nextProps.id && diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/helpers.test.ts b/x-pack/legacy/plugins/siem/public/components/timeline/body/helpers.test.ts index 21a2e54aa19495..83099c68e4ca9b 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/helpers.test.ts +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/helpers.test.ts @@ -17,6 +17,7 @@ import { getColumnWidthFromType, getPinTooltip, stringifyEvent, + SHOW_CHECK_BOXES_COLUMN_WIDTH, } from './helpers'; describe('helpers', () => { @@ -258,8 +259,20 @@ describe('helpers', () => { expect(getActionsColumnWidth(false)).toEqual(DEFAULT_ACTIONS_COLUMN_WIDTH); }); + test('returns the default actions column width + checkbox width when isEventViewer is false and showCheckboxes is true', () => { + expect(getActionsColumnWidth(false, true)).toEqual( + DEFAULT_ACTIONS_COLUMN_WIDTH + SHOW_CHECK_BOXES_COLUMN_WIDTH + ); + }); + test('returns the events viewer actions column width when isEventViewer is true', () => { expect(getActionsColumnWidth(true)).toEqual(EVENTS_VIEWER_ACTIONS_COLUMN_WIDTH); }); + + 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 + ); + }); }); }); diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/helpers.ts b/x-pack/legacy/plugins/siem/public/components/timeline/body/helpers.ts index 0b1d21b2371eeb..5fdc2fddfbfb14 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/helpers.ts +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/helpers.ts @@ -18,6 +18,8 @@ export const DEFAULT_ACTIONS_COLUMN_WIDTH = 115; // px; * an events viewer, which has fewer actions than a regular events viewer */ export const EVENTS_VIEWER_ACTIONS_COLUMN_WIDTH = 32; // px; +/** Additional column width to include when checkboxes are shown **/ +export const SHOW_CHECK_BOXES_COLUMN_WIDTH = 32; // px; /** The default minimum width of a column (when a width for the column type is not specified) */ export const DEFAULT_COLUMN_MIN_WIDTH = 180; // px /** The default minimum width of a column of type `date` */ @@ -93,5 +95,6 @@ export const getColumnHeaders = ( }; /** Returns the (fixed) width of the Actions column */ -export const getActionsColumnWidth = (isEventViewer: boolean): number => - isEventViewer ? EVENTS_VIEWER_ACTIONS_COLUMN_WIDTH : DEFAULT_ACTIONS_COLUMN_WIDTH; +export const getActionsColumnWidth = (isEventViewer: boolean, showCheckboxes = false): number => + (showCheckboxes ? SHOW_CHECK_BOXES_COLUMN_WIDTH : 0) + + (isEventViewer ? EVENTS_VIEWER_ACTIONS_COLUMN_WIDTH : DEFAULT_ACTIONS_COLUMN_WIDTH); diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/index.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/index.tsx index 07e37346ac968d..0aed68a0e4ad79 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/index.tsx @@ -27,6 +27,7 @@ import { getActionsColumnWidth } from './helpers'; import { ColumnRenderer } from './renderers/column_renderer'; import { RowRenderer } from './renderers/row_renderer'; import { Sort } from './sort'; +import { useTimelineTypeContext } from '../timeline_context'; export interface BodyProps { addNoteToEvent: AddNoteToEvent; @@ -80,9 +81,11 @@ export const Body = React.memo( toggleColumn, updateNote, }) => { + const timelineTypeContext = useTimelineTypeContext(); + const columnWidths = columnHeaders.reduce( (totalWidth, header) => totalWidth + header.width, - getActionsColumnWidth(isEventViewer) + getActionsColumnWidth(isEventViewer, timelineTypeContext.showCheckboxes) ); return ( @@ -94,7 +97,10 @@ export const Body = React.memo( style={{ minWidth: columnWidths + 'px' }} > ( /> ( updateNote, updateSort, }) => { + const timelineTypeContext = useTimelineTypeContext(); + const getNotesByIds = useCallback( (noteIds: string[]): Note[] => appSelectors.getNotes(notesById, noteIds), [notesById] @@ -167,7 +171,7 @@ const StatefulBodyComponent = React.memo( onUpdateColumns={onUpdateColumns} pinnedEventIds={pinnedEventIds} range={range!} - rowRenderers={rowRenderers} + rowRenderers={timelineTypeContext.showRowRenderers ? rowRenderers : [plainRowRenderer]} sort={sort} toggleColumn={toggleColumn} updateNote={onUpdateNote} @@ -202,7 +206,7 @@ const makeMapStateToProps = () => { const getTimeline = timelineSelectors.getTimelineByIdSelector(); const getNotesByIds = appSelectors.notesByIdsSelector(); const mapStateToProps = (state: State, { browserFields, id }: OwnProps) => { - const timeline: TimelineModel = getTimeline(state, id); + const timeline: TimelineModel = getTimeline(state, id) ?? timelineDefaults; const { columns, eventIdToNoteIds, pinnedEventIds } = timeline; return { diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/footer/index.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/footer/index.tsx index ce74f26b8db82f..5cef75ed395148 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/footer/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/footer/index.tsx @@ -27,6 +27,7 @@ import { OnChangeItemsPerPage, OnLoadMore } from '../events'; import { LastUpdatedAt } from './last_updated'; import * as i18n from './translations'; +import { useTimelineTypeContext } from '../timeline_context'; const FixedWidthLastUpdated = styled.div<{ compact: boolean }>` width: ${({ compact }) => (!compact ? 200 : 25)}px; @@ -110,43 +111,49 @@ export const EventsCountComponent = ({ itemsCount: number; onClick: () => void; serverSideEventCount: number; -}) => ( -
- - - {itemsCount} - - - {` ${i18n.OF} `} - - } - isOpen={isOpen} - closePopover={closePopover} - panelPaddingSize="none" - > - - - - - - {serverSideEventCount} - {' '} - {i18n.EVENTS} - - -
-); +}) => { + const timelineTypeContext = useTimelineTypeContext(); + return ( +
+ + + {itemsCount} + + + {` ${i18n.OF} `} + + } + isOpen={isOpen} + closePopover={closePopover} + panelPaddingSize="none" + > + + + + + + {serverSideEventCount} + {' '} + {timelineTypeContext.documentType ?? i18n.EVENTS} + + +
+ ); +}; EventsCountComponent.displayName = 'EventsCountComponent'; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/index.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/index.tsx index 8c911b4ab06cb7..4156c67e9b8419 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/index.tsx @@ -14,7 +14,7 @@ import { esFilters } from '../../../../../../../src/plugins/data/public'; import { WithSource } from '../../containers/source'; import { inputsModel, inputsSelectors, State, timelineSelectors } from '../../store'; import { timelineActions } from '../../store/actions'; -import { KqlMode, TimelineModel } from '../../store/timeline/model'; +import { KqlMode, timelineDefaults, TimelineModel } from '../../store/timeline/model'; import { ColumnHeader } from './body/column_headers/column_header'; import { DataProvider, QueryOperator } from './data_providers/data_provider'; @@ -315,7 +315,7 @@ const makeMapStateToProps = () => { const getKqlQueryTimeline = timelineSelectors.getKqlFilterQuerySelector(); const getInputsTimeline = inputsSelectors.getTimelineSelector(); const mapStateToProps = (state: State, { id }: OwnProps) => { - const timeline: TimelineModel = getTimeline(state, id); + const timeline: TimelineModel = getTimeline(state, id) ?? timelineDefaults; const input: inputsModel.InputsRange = getInputsTimeline(state); const { columns, diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/search_or_filter/index.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/search_or_filter/index.tsx index 31d2b7a2d85f2a..31d1002f161792 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/search_or_filter/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/search_or_filter/index.tsx @@ -21,7 +21,7 @@ import { inputsSelectors, } from '../../../store'; import { timelineActions } from '../../../store/actions'; -import { KqlMode, TimelineModel } from '../../../store/timeline/model'; +import { KqlMode, timelineDefaults, TimelineModel } from '../../../store/timeline/model'; import { DispatchUpdateReduxTime, dispatchUpdateReduxTime } from '../../super_date_picker'; import { DataProvider } from '../data_providers/data_provider'; import { SearchOrFilter } from './search_or_filter'; @@ -195,7 +195,7 @@ const makeMapStateToProps = () => { const getInputsTimeline = inputsSelectors.getTimelineSelector(); const getInputsPolicy = inputsSelectors.getTimelinePolicySelector(); const mapStateToProps = (state: State, { timelineId }: OwnProps) => { - const timeline: TimelineModel = getTimeline(state, timelineId); + const timeline: TimelineModel = getTimeline(state, timelineId) ?? timelineDefaults; const input: inputsModel.InputsRange = getInputsTimeline(state); const policy: inputsModel.Policy = getInputsPolicy(state); return { diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/timeline_context.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/timeline_context.tsx index b6006dea8b8acb..584fe03d2149b0 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/timeline_context.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/timeline_context.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { createContext, useContext, useEffect, memo, useState } from 'react'; +import React, { createContext, memo, useContext, useEffect, useState } from 'react'; const initTimelineContext = false; export const TimelineContext = createContext(initTimelineContext); @@ -14,30 +14,55 @@ const initTimelineWidth = 0; export const TimelineWidthContext = createContext(initTimelineWidth); export const useTimelineWidthContext = () => useContext(TimelineWidthContext); +export interface TimelineTypeContextProps { + documentType?: string; + footerText?: string; + showCheckboxes: boolean; + showRowRenderers: boolean; + title?: string; +} +const initTimelineType: TimelineTypeContextProps = { + documentType: undefined, + footerText: undefined, + showCheckboxes: false, + showRowRenderers: true, + title: undefined, +}; +export const TimelineTypeContext = createContext(initTimelineType); +export const useTimelineTypeContext = () => useContext(TimelineTypeContext); + interface ManageTimelineContextProps { children: React.ReactNode; loading: boolean; width: number; + type?: TimelineTypeContextProps; } // todo we need to refactor this as more complex context/reducer with useReducer // to avoid so many Context, at least the separation of code is there now export const ManageTimelineContext = memo( - ({ children, loading, width }) => { + ({ children, loading, width, type = initTimelineType }) => { const [myLoading, setLoading] = useState(initTimelineContext); const [myWidth, setWidth] = useState(initTimelineWidth); + const [myType, setType] = useState(initTimelineType); useEffect(() => { setLoading(loading); }, [loading]); + useEffect(() => { + setType(type); + }, [type]); + useEffect(() => { setWidth(width); }, [width]); return ( - {children} + + {children} + ); } diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/api.ts b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/api.ts index 798cf91612a856..a26f376f962d71 100644 --- a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/api.ts +++ b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/api.ts @@ -16,15 +16,17 @@ import { Rule, } from './types'; import { throwIfNotOk } from '../../../hooks/api/api'; +import { DETECTION_ENGINE_RULES_URL } from '../../../../common/constants'; /** * Add provided Rule * * @param rule to add * @param kbnVersion current Kibana Version to use for headers + * @param signal to cancel request */ export const addRule = async ({ rule, kbnVersion, signal }: AddRulesProps): Promise => { - const response = await fetch(`${chrome.getBasePath()}/api/detection_engine/rules`, { + const response = await fetch(`${chrome.getBasePath()}${DETECTION_ENGINE_RULES_URL}`, { method: 'POST', credentials: 'same-origin', headers: { @@ -47,6 +49,7 @@ export const addRule = async ({ rule, kbnVersion, signal }: AddRulesProps): Prom * @param pagination desired pagination options (e.g. page/perPage) * @param id if specified, will return specific rule if exists * @param kbnVersion current Kibana Version to use for headers + * @param signal to cancel request */ export const fetchRules = async ({ filterOptions = { @@ -75,8 +78,8 @@ export const fetchRules = async ({ const endpoint = id != null - ? `${chrome.getBasePath()}/api/detection_engine/rules?id="${id}"` - : `${chrome.getBasePath()}/api/detection_engine/rules/_find?${queryParams.join('&')}`; + ? `${chrome.getBasePath()}${DETECTION_ENGINE_RULES_URL}?id="${id}"` + : `${chrome.getBasePath()}${DETECTION_ENGINE_RULES_URL}/_find?${queryParams.join('&')}`; const response = await fetch(endpoint, { method: 'GET', @@ -106,7 +109,7 @@ export const enableRules = async ({ kbnVersion, }: EnableRulesProps): Promise => { const requests = ids.map(id => - fetch(`${chrome.getBasePath()}/api/detection_engine/rules`, { + fetch(`${chrome.getBasePath()}${DETECTION_ENGINE_RULES_URL}`, { method: 'PUT', credentials: 'same-origin', headers: { @@ -134,7 +137,7 @@ export const enableRules = async ({ export const deleteRules = async ({ ids, kbnVersion }: DeleteRulesProps): Promise => { // TODO: Don't delete if immutable! const requests = ids.map(id => - fetch(`${chrome.getBasePath()}/api/detection_engine/rules?id=${id}`, { + fetch(`${chrome.getBasePath()}${DETECTION_ENGINE_RULES_URL}?id=${id}`, { method: 'DELETE', credentials: 'same-origin', headers: { @@ -163,7 +166,7 @@ export const duplicateRules = async ({ kbnVersion, }: DuplicateRulesProps): Promise => { const requests = rules.map(rule => - fetch(`${chrome.getBasePath()}/api/detection_engine/rules`, { + fetch(`${chrome.getBasePath()}${DETECTION_ENGINE_RULES_URL}`, { method: 'POST', credentials: 'same-origin', headers: { diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/fetch_index_patterns.tsx b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/fetch_index_patterns.tsx index 8cbda9cd5afe98..dd744f4d7ecd58 100644 --- a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/fetch_index_patterns.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/fetch_index_patterns.tsx @@ -8,7 +8,12 @@ import { isEmpty, get } from 'lodash/fp'; import { useEffect, useState, Dispatch, SetStateAction } from 'react'; import { IIndexPattern } from 'src/plugins/data/public'; -import { getIndexFields, sourceQuery } from '../../../containers/source'; +import { + BrowserFields, + getBrowserFields, + getIndexFields, + sourceQuery, +} from '../../../containers/source'; import { useStateToaster } from '../../../components/toasters'; import { errorToToaster } from '../../../components/ml/api/error_to_toaster'; import { SourceQuery } from '../../../graphql/types'; @@ -16,20 +21,22 @@ import { useApolloClient } from '../../../utils/apollo_context'; import * as i18n from './translations'; -interface FetchIndexPattern { +interface FetchIndexPatternReturn { + browserFields: BrowserFields | null; isLoading: boolean; indices: string[]; indicesExists: boolean; indexPatterns: IIndexPattern | null; } -type Return = [FetchIndexPattern, Dispatch>]; +type Return = [FetchIndexPatternReturn, Dispatch>]; -export const useFetchIndexPatterns = (): Return => { +export const useFetchIndexPatterns = (defaultIndices: string[] = []): Return => { const apolloClient = useApolloClient(); - const [indices, setIndices] = useState([]); + const [indices, setIndices] = useState(defaultIndices); const [indicesExists, setIndicesExists] = useState(false); const [indexPatterns, setIndexPatterns] = useState(null); + const [browserFields, setBrowserFields] = useState(null); const [isLoading, setIsLoading] = useState(false); const [, dispatchToaster] = useStateToaster(); @@ -62,6 +69,7 @@ export const useFetchIndexPatterns = (): Return => { setIndexPatterns( getIndexFields(indices.join(), get('data.source.status.indexFields', result)) ); + setBrowserFields(getBrowserFields(get('data.source.status.indexFields', result))); } }, error => { @@ -80,5 +88,5 @@ export const useFetchIndexPatterns = (): Return => { }; }, [indices]); - return [{ isLoading, indices, indicesExists, indexPatterns }, setIndices]; + return [{ browserFields, isLoading, indices, indicesExists, indexPatterns }, setIndices]; }; diff --git a/x-pack/legacy/plugins/siem/public/containers/source/index.tsx b/x-pack/legacy/plugins/siem/public/containers/source/index.tsx index 653d5543c0923b..8bddbd14a367c1 100644 --- a/x-pack/legacy/plugins/siem/public/containers/source/index.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/source/index.tsx @@ -70,7 +70,7 @@ export const getIndexFields = memoizeOne( : { fields: [], title } ); -const getBrowserFields = memoizeOne( +export const getBrowserFields = memoizeOne( (fields: IndexField[]): BrowserFields => fields && fields.length > 0 ? fields.reduce( diff --git a/x-pack/legacy/plugins/siem/public/containers/timeline/index.tsx b/x-pack/legacy/plugins/siem/public/containers/timeline/index.tsx index 21ef4ffdd8e377..40ed3b3747c105 100644 --- a/x-pack/legacy/plugins/siem/public/containers/timeline/index.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/timeline/index.tsx @@ -19,11 +19,12 @@ import { TimelineEdges, TimelineItem, } from '../../graphql/types'; -import { inputsModel, State, inputsSelectors } from '../../store'; +import { inputsModel, inputsSelectors, State } from '../../store'; import { createFilter } from '../helpers'; import { QueryTemplate, QueryTemplateProps } from '../query_template'; import { timelineQuery } from './index.gql_query'; +import { IIndexPattern } from '../../../../../../../src/plugins/data/common/index_patterns'; export interface TimelineArgs { events: TimelineItem[]; @@ -44,6 +45,7 @@ export interface TimelineQueryReduxProps { export interface OwnProps extends QueryTemplateProps { children?: (args: TimelineArgs) => React.ReactNode; id: string; + indexPattern?: IIndexPattern; limit: number; sortField: SortField; fields: string[]; @@ -67,6 +69,7 @@ class TimelineQueryComponent extends QueryTemplate< const { children, id, + indexPattern, isInspected, limit, fields, @@ -80,7 +83,8 @@ class TimelineQueryComponent extends QueryTemplate< sourceId, pagination: { limit, cursor: null, tiebreaker: null }, sortField, - defaultIndex: chrome.getUiSettingsClient().get(DEFAULT_INDEX_KEY), + defaultIndex: + indexPattern?.title.split(',') ?? chrome.getUiSettingsClient().get(DEFAULT_INDEX_KEY), inspect: isInspected, }; return ( diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/detection_engine.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/detection_engine.tsx index f02e80ebfaf665..e1373c8b18bb7a 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/detection_engine.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/detection_engine.tsx @@ -4,15 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { - EuiButton, - EuiFilterButton, - EuiFilterGroup, - EuiPanel, - EuiSelect, - EuiSpacer, -} from '@elastic/eui'; -import React, { useState } from 'react'; +import { EuiButton, EuiPanel, EuiSelect, EuiSpacer } from '@elastic/eui'; +import React from 'react'; import { StickyContainer } from 'react-sticky'; import { FiltersGlobal } from '../../components/filters_global'; @@ -20,100 +13,12 @@ import { HeaderPage } from '../../components/header_page'; import { HeaderSection } from '../../components/header_section'; import { HistogramSignals } from '../../components/page/detection_engine/histogram_signals'; import { SiemSearchBar } from '../../components/search_bar'; -import { - UtilityBar, - UtilityBarAction, - UtilityBarGroup, - UtilityBarSection, - UtilityBarText, -} from '../../components/detection_engine/utility_bar'; import { WrapperPage } from '../../components/wrapper_page'; import { indicesExistOrDataTemporarilyUnavailable, WithSource } from '../../containers/source'; import { SpyRoute } from '../../utils/route/spy_routes'; import { DetectionEngineEmptyPage } from './detection_engine_empty_page'; import * as i18n from './translations'; - -const OpenSignals = React.memo(() => { - return ( - <> - - - - {`${i18n.PANEL_SUBTITLE_SHOWING}: 7,712 signals`} - - - - {'Selected: 20 signals'} - -

{'Batch actions context menu here.'}

} - > - {'Batch actions'} -
- - - {'Select all signals on all pages'} - -
- - - {'Clear 7 filters'} - - {'Clear aggregation'} - -
- - - -

{'Customize columns context menu here.'}

} - > - {'Customize columns'} -
- - {'Aggregate data'} -
-
-
- - {/* Michael: Open signals datagrid here. Talk to Chandler Prall about possibility of early access. If not possible, use basic table. */} - - ); -}); - -const ClosedSignals = React.memo(() => { - return ( - <> - - - - {`${i18n.PANEL_SUBTITLE_SHOWING}: 7,712 signals`} - - - - - -

{'Customize columns context menu here.'}

} - > - {'Customize columns'} -
- - {'Aggregate data'} -
-
-
- - {/* Michael: Closed signals datagrid here. Talk to Chandler Prall about possibility of early access. If not possible, use basic table. */} - - ); -}); +import { SignalsTable } from './signals'; export const DetectionEngineComponent = React.memo(() => { const sampleChartOptions = [ @@ -129,9 +34,6 @@ export const DetectionEngineComponent = React.memo(() => { { text: 'Top users', value: 'users' }, ]; - const filterGroupOptions = ['open', 'closed']; - const [filterGroupState, setFilterGroupState] = useState(filterGroupOptions[0]); - return ( <> @@ -164,28 +66,7 @@ export const DetectionEngineComponent = React.memo(() => { - - - - setFilterGroupState(filterGroupOptions[0])} - withNext - > - {'Open signals'} - - - setFilterGroupState(filterGroupOptions[1])} - > - {'Closed signals'} - - - - - {filterGroupState === filterGroupOptions[0] ? : } - + ) : ( diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/signals/components/closed_signals/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/signals/components/closed_signals/index.tsx new file mode 100644 index 00000000000000..7ccc95a1d70833 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/signals/components/closed_signals/index.tsx @@ -0,0 +1,45 @@ +/* + * 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 { + UtilityBar, + UtilityBarAction, + UtilityBarGroup, + UtilityBarSection, + UtilityBarText, +} from '../../../../../components/detection_engine/utility_bar'; +import * as i18n from '../../translations'; + +export const ClosedSignals = React.memo<{ totalCount: number }>(({ totalCount }) => { + return ( + <> + + + + {`${i18n.PANEL_SUBTITLE_SHOWING}: ${totalCount} signals`} + + + + + +

{'Customize columns context menu here.'}

} + > + {'Customize columns'} +
+ + {'Aggregate data'} +
+
+
+ + {/* Michael: Closed signals datagrid here. Talk to Chandler Prall about possibility of early access. If not possible, use basic table. */} + + ); +}); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/signals/components/open_signals/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/signals/components/open_signals/index.tsx new file mode 100644 index 00000000000000..f65f511ab33a9a --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/signals/components/open_signals/index.tsx @@ -0,0 +1,47 @@ +/* + * 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 { + UtilityBar, + UtilityBarAction, + UtilityBarGroup, + UtilityBarSection, + UtilityBarText, +} from '../../../../../components/detection_engine/utility_bar'; +import * as i18n from '../../translations'; + +export const OpenSignals = React.memo<{ totalCount: number }>(({ totalCount }) => { + return ( + <> + + + + {`${i18n.PANEL_SUBTITLE_SHOWING}: ${totalCount} signals`} + + + + {'Selected: 20 signals'} + +

{'Batch actions context menu here.'}

} + > + {'Batch actions'} +
+ + + {'Select all signals on all pages'} + +
+
+
+ + {/* Michael: Open signals datagrid here. Talk to Chandler Prall about possibility of early access. If not possible, use basic table. */} + + ); +}); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/signals/default_headers.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/signals/default_headers.tsx new file mode 100644 index 00000000000000..d6bfcd80b99566 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/signals/default_headers.tsx @@ -0,0 +1,79 @@ +/* + * 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 { ColumnHeader } from '../../../components/timeline/body/column_headers/column_header'; +import { defaultColumnHeaderType } from '../../../components/timeline/body/column_headers/default_headers'; +import { + DEFAULT_COLUMN_MIN_WIDTH, + DEFAULT_DATE_COLUMN_MIN_WIDTH, +} from '../../../components/timeline/body/helpers'; + +import * as i18n from './translations'; + +export const signalsHeaders: ColumnHeader[] = [ + { + columnHeaderType: defaultColumnHeaderType, + id: 'signal.rule.name', + label: i18n.SIGNALS_HEADERS_RULE, + width: DEFAULT_COLUMN_MIN_WIDTH, + }, + { + columnHeaderType: defaultColumnHeaderType, + id: 'signal.rule.type', + label: i18n.SIGNALS_HEADERS_METHOD, + width: 80, + }, + { + columnHeaderType: defaultColumnHeaderType, + id: 'signal.rule.severity', + label: i18n.SIGNALS_HEADERS_SEVERITY, + width: 80, + }, + { + columnHeaderType: defaultColumnHeaderType, + id: 'signal.rule.risk_score', + label: i18n.SIGNALS_HEADERS_RISK_SCORE, + width: 120, + }, + { + category: 'event', + columnHeaderType: defaultColumnHeaderType, + id: 'event.action', + type: 'string', + aggregatable: true, + width: 140, + }, + { + columnHeaderType: defaultColumnHeaderType, + id: 'event.category', + width: 150, + }, + { + columnHeaderType: defaultColumnHeaderType, + id: 'host.name', + width: 120, + }, + { + columnHeaderType: defaultColumnHeaderType, + id: 'user.name', + width: 120, + }, + { + columnHeaderType: defaultColumnHeaderType, + id: 'source.ip', + width: 120, + }, + { + columnHeaderType: defaultColumnHeaderType, + id: 'destination.ip', + width: 120, + }, + { + columnHeaderType: defaultColumnHeaderType, + id: '@timestamp', + width: DEFAULT_DATE_COLUMN_MIN_WIDTH, + }, +]; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/signals/default_model.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/signals/default_model.tsx new file mode 100644 index 00000000000000..bb1f806d67c038 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/signals/default_model.tsx @@ -0,0 +1,13 @@ +/* + * 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 { signalsHeaders } from './default_headers'; +import { SubsetTimelineModel, timelineDefaults } from '../../../store/timeline/model'; + +export const signalsDefaultModel: SubsetTimelineModel = { + ...timelineDefaults, + columns: signalsHeaders, +}; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/signals/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/signals/index.tsx new file mode 100644 index 00000000000000..edc7ed133d10ce --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/signals/index.tsx @@ -0,0 +1,93 @@ +/* + * 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 { EuiFilterButton, EuiFilterGroup } from '@elastic/eui'; +import React, { useCallback, useState } from 'react'; +import { OpenSignals } from './components/open_signals'; +import { ClosedSignals } from './components/closed_signals'; +import { GlobalTime } from '../../../containers/global_time'; +import { StatefulEventsViewer } from '../../../components/events_viewer'; +import * as i18n from './translations'; +import { DEFAULT_SIGNALS_INDEX } from '../../../../common/constants'; +import { signalsDefaultModel } from './default_model'; + +const SIGNALS_PAGE_TIMELINE_ID = 'signals-page'; +const FILTER_OPEN = 'open'; +const FILTER_CLOSED = 'closed'; + +export const SignalsTableFilterGroup = React.memo( + ({ onFilterGroupChanged }: { onFilterGroupChanged: (filterGroup: string) => void }) => { + const [filterGroup, setFilterGroup] = useState(FILTER_OPEN); + + return ( + + { + setFilterGroup(FILTER_OPEN); + onFilterGroupChanged(FILTER_OPEN); + }} + withNext + > + {'Open signals'} + + + setFilterGroup(FILTER_CLOSED)} + > + {'Closed signals'} + + + ); + } +); + +export const SignalsTable = React.memo(() => { + const [filterGroup, setFilterGroup] = useState(FILTER_OPEN); + + const onFilterGroupChangedCallback = useCallback( + (newFilterGroup: string) => { + setFilterGroup(newFilterGroup); + }, + [setFilterGroup] + ); + + return ( + <> + + {({ to, from, setQuery, deleteQuery, isInitializing }) => ( + + } + id={SIGNALS_PAGE_TIMELINE_ID} + start={from} + timelineTypeContext={{ + documentType: i18n.SIGNALS_DOCUMENT_TYPE, + footerText: i18n.TOTAL_COUNT_OF_SIGNALS, + showCheckboxes: true, + showRowRenderers: false, + title: i18n.SIGNALS_TABLE_TITLE, + }} + utilityBar={(totalCount: number) => + filterGroup === FILTER_OPEN ? ( + + ) : ( + + ) + } + /> + )} + + + ); +}); + +SignalsTable.displayName = 'SignalsTable'; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/signals/translations.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/signals/translations.ts new file mode 100644 index 00000000000000..1806ba85f8b554 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/signals/translations.ts @@ -0,0 +1,64 @@ +/* + * 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 PAGE_TITLE = i18n.translate('xpack.siem.detectionEngine.pageTitle', { + defaultMessage: 'Detection engine', +}); + +export const PANEL_SUBTITLE_SHOWING = i18n.translate( + 'xpack.siem.detectionEngine.panelSubtitleShowing', + { + defaultMessage: 'Showing', + } +); + +export const SIGNALS_TABLE_TITLE = i18n.translate('xpack.siem.detectionEngine.signals.tableTitle', { + defaultMessage: 'All signals', +}); + +export const SIGNALS_DOCUMENT_TYPE = i18n.translate( + 'xpack.siem.detectionEngine.signals.documentTypeTitle', + { + defaultMessage: 'Signals', + } +); + +export const TOTAL_COUNT_OF_SIGNALS = i18n.translate( + 'xpack.siem.detectionEngine.signals.totalCountOfSignalsTitle', + { + defaultMessage: 'signals match the search criteria', + } +); + +export const SIGNALS_HEADERS_RULE = i18n.translate( + 'xpack.siem.eventsViewer.signals.defaultHeaders.ruleTitle', + { + defaultMessage: 'Rule', + } +); + +export const SIGNALS_HEADERS_METHOD = i18n.translate( + 'xpack.siem.eventsViewer.signals.defaultHeaders.methodTitle', + { + defaultMessage: 'Method', + } +); + +export const SIGNALS_HEADERS_SEVERITY = i18n.translate( + 'xpack.siem.eventsViewer.signals.defaultHeaders.severityTitle', + { + defaultMessage: 'Severity', + } +); + +export const SIGNALS_HEADERS_RISK_SCORE = i18n.translate( + 'xpack.siem.eventsViewer.signals.defaultHeaders.riskScoreTitle', + { + defaultMessage: 'Risk Score', + } +); diff --git a/x-pack/legacy/plugins/siem/public/pages/hosts/navigation/events_query_tab_body.tsx b/x-pack/legacy/plugins/siem/public/pages/hosts/navigation/events_query_tab_body.tsx index 808565cadfa461..223538c07d5df9 100644 --- a/x-pack/legacy/plugins/siem/public/pages/hosts/navigation/events_query_tab_body.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/hosts/navigation/events_query_tab_body.tsx @@ -12,6 +12,7 @@ import { manageQuery } from '../../../components/page/manage_query'; import { EventsOverTimeHistogram } from '../../../components/page/hosts/events_over_time'; import { EventsOverTimeQuery } from '../../../containers/events/events_over_time'; import { hostsModel } from '../../../store/hosts'; +import { eventsDefaultModel } from '../../../components/events_viewer/default_model'; const HOSTS_PAGE_TIMELINE_ID = 'hosts-page'; const EventsOverTimeManage = manageQuery(EventsOverTimeHistogram); @@ -48,7 +49,12 @@ export const EventsQueryTabBody = ({ )} - + ); }; diff --git a/x-pack/legacy/plugins/siem/public/store/timeline/model.ts b/x-pack/legacy/plugins/siem/public/store/timeline/model.ts index 405564a4b5b0d6..401edb73a0de89 100644 --- a/x-pack/legacy/plugins/siem/public/store/timeline/model.ts +++ b/x-pack/legacy/plugins/siem/public/store/timeline/model.ts @@ -9,7 +9,6 @@ import { ColumnHeader } from '../../components/timeline/body/column_headers/colu import { DataProvider } from '../../components/timeline/data_providers/data_provider'; import { DEFAULT_TIMELINE_WIDTH } from '../../components/timeline/body/helpers'; import { defaultHeaders } from '../../components/timeline/body/column_headers/default_headers'; -import { defaultHeaders as eventsDefaultHeaders } from '../../components/events_viewer/default_headers'; import { Sort } from '../../components/timeline/body/sort'; import { Direction, PinnedEvent } from '../../graphql/types'; import { KueryFilterQuery, SerializedFilterQuery } from '../model'; @@ -74,34 +73,37 @@ export interface TimelineModel { version: string | null; } -export const timelineDefaults: Readonly> = { +export type SubsetTimelineModel = Readonly< + Pick< + TimelineModel, + | 'columns' + | 'dataProviders' + | 'description' + | 'eventIdToNoteIds' + | 'highlightedDropAndProviderId' + | 'historyIds' + | 'isFavorite' + | 'isLive' + | 'itemsPerPage' + | 'itemsPerPageOptions' + | 'kqlMode' + | 'kqlQuery' + | 'title' + | 'noteIds' + | 'pinnedEventIds' + | 'pinnedEventsSaveObject' + | 'dateRange' + | 'show' + | 'sort' + | 'width' + | 'isSaving' + | 'isLoading' + | 'savedObjectId' + | 'version' + > +>; + +export const timelineDefaults: SubsetTimelineModel & Pick = { columns: defaultHeaders, dataProviders: [], description: '', @@ -137,31 +139,3 @@ export const timelineDefaults: Readonly> = { ...timelineDefaults, columns: eventsDefaultHeaders }; diff --git a/x-pack/legacy/plugins/siem/public/store/timeline/selectors.ts b/x-pack/legacy/plugins/siem/public/store/timeline/selectors.ts index c248387e6b3fd2..780145ebfa54c6 100644 --- a/x-pack/legacy/plugins/siem/public/store/timeline/selectors.ts +++ b/x-pack/legacy/plugins/siem/public/store/timeline/selectors.ts @@ -9,8 +9,8 @@ import { createSelector } from 'reselect'; import { isFromKueryExpressionValid } from '../../lib/keury'; import { State } from '../reducer'; -import { eventsDefaults, timelineDefaults, TimelineModel } from './model'; -import { TimelineById, AutoSavedWarningMsg } from './types'; +import { TimelineModel } from './model'; +import { AutoSavedWarningMsg, TimelineById } from './types'; const selectTimelineById = (state: State): TimelineById => state.timeline.timelineById; @@ -37,11 +37,9 @@ export const getShowCallOutUnauthorizedMsg = () => export const getTimelines = () => timelineByIdSelector; -export const getTimelineByIdSelector = () => - createSelector(selectTimeline, timeline => timeline || timelineDefaults); +export const getTimelineByIdSelector = () => createSelector(selectTimeline, timeline => timeline); -export const getEventsByIdSelector = () => - createSelector(selectTimeline, timeline => timeline || eventsDefaults); +export const getEventsByIdSelector = () => createSelector(selectTimeline, timeline => timeline); export const getKqlFilterQuerySelector = () => createSelector(selectTimeline, timeline =>