diff --git a/x-pack/plugins/security_solution/public/common/components/accessibility/helpers.test.ts b/x-pack/plugins/security_solution/public/common/components/accessibility/helpers.test.ts deleted file mode 100644 index c099a1413a88f7..00000000000000 --- a/x-pack/plugins/security_solution/public/common/components/accessibility/helpers.test.ts +++ /dev/null @@ -1,21 +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 { ariaIndexToArrayIndex, arrayIndexToAriaIndex } from './helpers'; - -describe('helpers', () => { - describe('ariaIndexToArrayIndex', () => { - it('returns the expected array index', () => { - expect(ariaIndexToArrayIndex(1)).toEqual(0); - }); - }); - - describe('arrayIndexToAriaIndex', () => { - it('returns the expected aria index', () => { - expect(arrayIndexToAriaIndex(0)).toEqual(1); - }); - }); -}); diff --git a/x-pack/plugins/security_solution/public/common/components/accessibility/helpers.test.tsx b/x-pack/plugins/security_solution/public/common/components/accessibility/helpers.test.tsx new file mode 100644 index 00000000000000..48db4b1f261b69 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/accessibility/helpers.test.tsx @@ -0,0 +1,70 @@ +/* + * 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 { + ariaIndexToArrayIndex, + arrayIndexToAriaIndex, + getNotesContainerClassName, + getRowRendererClassName, + isArrowRight, +} from './helpers'; + +describe('helpers', () => { + describe('ariaIndexToArrayIndex', () => { + test('it returns the expected array index', () => { + expect(ariaIndexToArrayIndex(1)).toEqual(0); + }); + }); + + describe('arrayIndexToAriaIndex', () => { + test('it returns the expected aria index', () => { + expect(arrayIndexToAriaIndex(0)).toEqual(1); + }); + }); + + describe('isArrowRight', () => { + test('it returns true if the right arrow key was pressed', () => { + let result = false; + const onKeyDown = (keyboardEvent: React.KeyboardEvent) => { + result = isArrowRight(keyboardEvent); + }; + + const wrapper = mount(
); + wrapper.find('div').simulate('keydown', { key: 'ArrowRight' }); + wrapper.update(); + + expect(result).toBe(true); + }); + + test('it returns false if another key was pressed', () => { + let result = false; + const onKeyDown = (keyboardEvent: React.KeyboardEvent) => { + result = isArrowRight(keyboardEvent); + }; + + const wrapper = mount(
); + wrapper.find('div').simulate('keydown', { key: 'Enter' }); + wrapper.update(); + + expect(result).toBe(false); + }); + }); + + describe('getRowRendererClassName', () => { + test('it returns the expected class name', () => { + expect(getRowRendererClassName(2)).toBe('row-renderer-2'); + }); + }); + + describe('getNotesContainerClassName', () => { + test('it returns the expected class name', () => { + expect(getNotesContainerClassName(2)).toBe('notes-container-2'); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/common/components/accessibility/helpers.ts b/x-pack/plugins/security_solution/public/common/components/accessibility/helpers.ts index d8603c9d02fcb7..8fc535c680b26f 100644 --- a/x-pack/plugins/security_solution/public/common/components/accessibility/helpers.ts +++ b/x-pack/plugins/security_solution/public/common/components/accessibility/helpers.ts @@ -5,6 +5,11 @@ */ import { DRAGGABLE_KEYBOARD_WRAPPER_CLASS_NAME } from '../drag_and_drop/helpers'; +import { + NOTES_CONTAINER_CLASS_NAME, + NOTE_CONTENT_CLASS_NAME, + ROW_RENDERER_CLASS_NAME, +} from '../../../timelines/components/timeline/body/helpers'; import { HOVER_ACTIONS_ALWAYS_SHOW_CLASS_NAME } from '../with_hover_actions'; /** @@ -63,6 +68,9 @@ export const isArrowDownOrArrowUp = (event: React.KeyboardEvent): boolean => export const isArrowKey = (event: React.KeyboardEvent): boolean => isArrowRightOrArrowLeft(event) || isArrowDownOrArrowUp(event); +/** Returns `true` if the right arrow key was pressed */ +export const isArrowRight = (event: React.KeyboardEvent): boolean => event.key === 'ArrowRight'; + /** Returns `true` if the escape key was pressed */ export const isEscape = (event: React.KeyboardEvent): boolean => event.key === 'Escape'; @@ -284,6 +292,12 @@ export type OnColumnFocused = ({ newFocusedColumnAriaColindex: number | null; }) => void; +export const getRowRendererClassName = (ariaRowindex: number) => + `${ROW_RENDERER_CLASS_NAME}-${ariaRowindex}`; + +export const getNotesContainerClassName = (ariaRowindex: number) => + `${NOTES_CONTAINER_CLASS_NAME}-${ariaRowindex}`; + /** * This function implements arrow key support for the `onKeyDownFocusHandler`. * @@ -312,6 +326,28 @@ export const onArrowKeyDown = ({ onColumnFocused?: OnColumnFocused; rowindexAttribute: string; }) => { + if (isArrowDown(event) && event.shiftKey) { + const firstRowRendererDraggable = containerElement?.querySelector( + `.${getRowRendererClassName(focusedAriaRowindex)} .${DRAGGABLE_KEYBOARD_WRAPPER_CLASS_NAME}` + ); + + if (firstRowRendererDraggable) { + firstRowRendererDraggable.focus(); + return; + } + } + + if (isArrowRight(event) && event.shiftKey) { + const firstNoteContent = containerElement?.querySelector( + `.${getNotesContainerClassName(focusedAriaRowindex)} .${NOTE_CONTENT_CLASS_NAME}` + ); + + if (firstNoteContent) { + firstNoteContent.focus(); + return; + } + } + const ariaColindex = isArrowRightOrArrowLeft(event) ? getNewAriaColindex({ focusedAriaColindex, diff --git a/x-pack/plugins/security_solution/public/common/components/accessibility/tooltip_with_keyboard_shortcut/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/accessibility/tooltip_with_keyboard_shortcut/index.test.tsx new file mode 100644 index 00000000000000..773fc3eeff4836 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/accessibility/tooltip_with_keyboard_shortcut/index.test.tsx @@ -0,0 +1,40 @@ +/* + * 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 { TooltipWithKeyboardShortcut } from '.'; + +const props = { + content:
{'To pay respect'}
, + shortcut: 'F', + showShortcut: true, +}; + +describe('TooltipWithKeyboardShortcut', () => { + test('it renders the provided content', () => { + const wrapper = mount(); + + expect(wrapper.find('[data-test-subj="content"]').text()).toBe('To pay respect'); + }); + + test('it renders the additionalScreenReaderOnlyContext', () => { + const wrapper = mount( + + ); + + expect(wrapper.find('[data-test-subj="additionalScreenReaderOnlyContext"]').text()).toBe( + 'field.name' + ); + }); + + test('it renders the expected shortcut', () => { + const wrapper = mount(); + + expect(wrapper.find('[data-test-subj="shortcut"]').first().text()).toBe('Press\u00a0F'); + }); +}); diff --git a/x-pack/plugins/security_solution/public/common/components/accessibility/tooltip_with_keyboard_shortcut/index.tsx b/x-pack/plugins/security_solution/public/common/components/accessibility/tooltip_with_keyboard_shortcut/index.tsx index 807953c51a42c3..ab6f90c8fec816 100644 --- a/x-pack/plugins/security_solution/public/common/components/accessibility/tooltip_with_keyboard_shortcut/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/accessibility/tooltip_with_keyboard_shortcut/index.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiScreenReaderOnly, EuiText } from '@elastic/eui'; +import { EuiText, EuiScreenReaderOnly } from '@elastic/eui'; import React from 'react'; import * as i18n from './translations'; @@ -23,14 +23,14 @@ const TooltipWithKeyboardShortcutComponent = ({ showShortcut, }: Props) => ( <> -
{content}
+
{content}
{additionalScreenReaderOnlyContext !== '' && ( - +

{additionalScreenReaderOnlyContext}

)} {showShortcut && ( - + {i18n.PRESS} {'\u00a0'} {shortcut} diff --git a/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper_hover_content.test.tsx b/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper_hover_content.test.tsx index 1cf03225cec033..9ce5778fb72e5c 100644 --- a/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper_hover_content.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper_hover_content.test.tsx @@ -14,7 +14,6 @@ import '../../mock/match_media'; import { useKibana } from '../../lib/kibana'; import { TestProviders } from '../../mock'; import { FilterManager } from '../../../../../../../src/plugins/data/public'; -import { useAddToTimeline } from '../../hooks/use_add_to_timeline'; import { useSourcererScope } from '../../containers/sourcerer'; import { DraggableWrapperHoverContent } from './draggable_wrapper_hover_content'; import { @@ -41,8 +40,14 @@ jest.mock('uuid', () => { v4: jest.fn(() => 'uuid.v4()'), }; }); - -jest.mock('../../hooks/use_add_to_timeline'); +const mockStartDragToTimeline = jest.fn(); +jest.mock('../../hooks/use_add_to_timeline', () => { + const original = jest.requireActual('../../hooks/use_add_to_timeline'); + return { + ...original, + useAddToTimeline: () => ({ startDragToTimeline: mockStartDragToTimeline }), + }; +}); const mockAddFilters = jest.fn(); const mockGetTimelineFilterManager = jest.fn().mockReturnValue({ addFilters: mockAddFilters, @@ -78,8 +83,7 @@ const defaultProps = { describe('DraggableWrapperHoverContent', () => { beforeAll(() => { - // our mock implementation of the useAddToTimeline hook returns a mock startDragToTimeline function: - (useAddToTimeline as jest.Mock).mockReturnValue({ startDragToTimeline: jest.fn() }); + mockStartDragToTimeline.mockReset(); (useSourcererScope as jest.Mock).mockReturnValue({ browserFields: mockBrowserFields, selectedPatterns: [], @@ -376,7 +380,7 @@ describe('DraggableWrapperHoverContent', () => { }); }); - test('when clicked, it invokes the `startDragToTimeline` function returned by the `useAddToTimeline` hook', () => { + test('when clicked, it invokes the `startDragToTimeline` function returned by the `useAddToTimeline` hook', async () => { const wrapper = mount( { ); - // The following "startDragToTimeline" function returned by our mock - // useAddToTimeline hook is called when the user clicks the - // Add to timeline investigation action: - const { startDragToTimeline } = useAddToTimeline({ - draggableId, - fieldName: aggregatableStringField, - }); - wrapper.find('[data-test-subj="add-to-timeline"]').first().simulate('click'); - wrapper.update(); - waitFor(() => { - expect(startDragToTimeline).toHaveBeenCalled(); + await waitFor(() => { + wrapper.update(); + expect(mockStartDragToTimeline).toHaveBeenCalled(); }); }); }); describe('Top N', () => { - test(`it renders the 'Show top field' button when showTopN is false and an aggregatable string field is provided`, async () => { + test(`it renders the 'Show top field' button when showTopN is false and an aggregatable string field is provided`, () => { const aggregatableStringField = 'cloud.account.id'; const wrapper = mount( @@ -425,7 +421,7 @@ describe('DraggableWrapperHoverContent', () => { expect(wrapper.find('[data-test-subj="show-top-field"]').first().exists()).toBe(true); }); - test(`it renders the 'Show top field' button when showTopN is false and a allowlisted signal field is provided`, async () => { + test(`it renders the 'Show top field' button when showTopN is false and a allowlisted signal field is provided`, () => { const allowlistedField = 'signal.rule.name'; const wrapper = mount( @@ -443,7 +439,7 @@ describe('DraggableWrapperHoverContent', () => { expect(wrapper.find('[data-test-subj="show-top-field"]').first().exists()).toBe(true); }); - test(`it does NOT render the 'Show top field' button when showTopN is false and a field not known to BrowserFields is provided`, async () => { + test(`it does NOT render the 'Show top field' button when showTopN is false and a field not known to BrowserFields is provided`, () => { const notKnownToBrowserFields = 'unknown.field'; const wrapper = mount( @@ -461,7 +457,7 @@ describe('DraggableWrapperHoverContent', () => { expect(wrapper.find('[data-test-subj="show-top-field"]').first().exists()).toBe(false); }); - test(`it should invokes goGetTimelineId when user is over the 'Show top field' button`, () => { + test(`it should invokes goGetTimelineId when user is over the 'Show top field' button`, async () => { const allowlistedField = 'signal.rule.name'; const wrapper = mount( @@ -476,12 +472,12 @@ describe('DraggableWrapperHoverContent', () => { ); const button = wrapper.find(`[data-test-subj="show-top-field"]`).first(); button.simulate('mouseenter'); - waitFor(() => { + await waitFor(() => { expect(goGetTimelineId).toHaveBeenCalledWith(true); }); }); - test(`invokes the toggleTopN function when the 'Show top field' button is clicked`, async () => { + test(`invokes the toggleTopN function when the 'Show top field' button is clicked`, () => { const allowlistedField = 'signal.rule.name'; const wrapper = mount( @@ -502,7 +498,7 @@ describe('DraggableWrapperHoverContent', () => { expect(toggleTopN).toBeCalled(); }); - test(`it does NOT render the Top N histogram when when showTopN is false`, async () => { + test(`it does NOT render the Top N histogram when when showTopN is false`, () => { const allowlistedField = 'signal.rule.name'; const wrapper = mount( @@ -522,7 +518,7 @@ describe('DraggableWrapperHoverContent', () => { ); }); - test(`it does NOT render the 'Show top field' button when showTopN is true`, async () => { + test(`it does NOT render the 'Show top field' button when showTopN is true`, () => { const allowlistedField = 'signal.rule.name'; const wrapper = mount( @@ -541,7 +537,7 @@ describe('DraggableWrapperHoverContent', () => { expect(wrapper.find('[data-test-subj="show-top-field"]').first().exists()).toBe(false); }); - test(`it renders the Top N histogram when when showTopN is true`, async () => { + test(`it renders the Top N histogram when when showTopN is true`, () => { const allowlistedField = 'signal.rule.name'; const wrapper = mount( diff --git a/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper_hover_content.tsx b/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper_hover_content.tsx index 2d3fdb9cb94291..adbb38f20c0280 100644 --- a/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper_hover_content.tsx +++ b/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper_hover_content.tsx @@ -324,6 +324,7 @@ const DraggableWrapperHoverContentComponent: React.FC = ({ color="text" data-test-subj="add-to-timeline" iconType="timeline" + onClick={handleStartDragToTimeline} /> )} diff --git a/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.tsx b/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.tsx index 515758965d6d17..7d38e3b732fc09 100644 --- a/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.tsx +++ b/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.tsx @@ -16,7 +16,11 @@ import { BrowserFields, DocValueFields } from '../../containers/source'; import { useTimelineEvents } from '../../../timelines/containers'; import { timelineActions } from '../../../timelines/store/timeline'; import { useKibana } from '../../lib/kibana'; -import { ColumnHeaderOptions, KqlMode } from '../../../timelines/store/timeline/model'; +import { + ColumnHeaderOptions, + KqlMode, + TimelineTabs, +} from '../../../timelines/store/timeline/model'; import { HeaderSection } from '../header_section'; import { defaultHeaders } from '../../../timelines/components/timeline/body/column_headers/default_headers'; import { Sort } from '../../../timelines/components/timeline/body/sort'; @@ -334,6 +338,7 @@ const EventsViewerComponent: React.FC = ({ onRuleChange={onRuleChange} refetch={refetch} sort={sort} + tabType={TimelineTabs.query} totalPages={calculateTotalPages({ itemsCount: totalCountMinusDeleted, itemsPerPage, diff --git a/x-pack/plugins/security_solution/public/common/components/page/index.tsx b/x-pack/plugins/security_solution/public/common/components/page/index.tsx index 37cc8f4ac3b93a..30a7685a193b2e 100644 --- a/x-pack/plugins/security_solution/public/common/components/page/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/page/index.tsx @@ -89,8 +89,14 @@ export const AppGlobalStyle = createGlobalStyle<{ theme: { eui: { euiColorPrimar to zero. */ .euiScreenReaderOnly { - height: 0px; - width: 0px; + clip: rect(1px, 1px, 1px, 1px); + clip-path: inset(50%); + height: 1px; + margin: -1px; + overflow: hidden; + padding: 0; + position: absolute; + width: 1px; } `; diff --git a/x-pack/plugins/security_solution/public/timelines/components/notes/note_cards/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/notes/note_cards/index.test.tsx index afec2055140d34..febbbb23db1efa 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/notes/note_cards/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/notes/note_cards/index.test.tsx @@ -11,6 +11,7 @@ import '../../../../common/mock/formatted_relative'; import { NoteCards } from '.'; import { TimelineStatus } from '../../../../../common/types/timeline'; import { TestProviders } from '../../../../common/mock'; +import { TimelineResultNote } from '../../open_timeline/types'; const getNotesByIds = () => ({ abc: { @@ -38,35 +39,42 @@ jest.mock('../../../../common/hooks/use_selector', () => ({ })); describe('NoteCards', () => { - const noteIds = ['abc', 'def']; + const notes: TimelineResultNote[] = Object.entries(getNotesByIds()).map( + ([_, { created, id, note, saveObjectId, user }]) => ({ + saveObjectId, + note, + noteId: id, + updated: created.getTime(), + updatedBy: user, + }) + ); const props = { associateNote: jest.fn(), ariaRowindex: 2, getNotesByIds, getNewNoteId: jest.fn(), - noteIds, + notes: [], showAddNote: true, status: TimelineStatus.active, toggleShowAddNote: jest.fn(), updateNote: jest.fn(), }; - test('it renders the notes column when noteIds are specified', () => { + test('it renders the notes column when notes are specified', () => { const wrapper = mount( - + ); expect(wrapper.find('[data-test-subj="notes"]').exists()).toEqual(true); }); - test('it does NOT render the notes column when noteIds are NOT specified', () => { - const testProps = { ...props, noteIds: [] }; + test('it does NOT render the notes column when notes are NOT specified', () => { const wrapper = mount( - + ); @@ -76,7 +84,7 @@ describe('NoteCards', () => { test('renders note cards', () => { const wrapper = mount( - + ); @@ -85,6 +93,18 @@ describe('NoteCards', () => { ); }); + test('renders the expected screenreader only text', () => { + const wrapper = mount( + + + + ); + + expect(wrapper.find('[data-test-subj="screenReaderOnly"]').first().text()).toEqual( + 'You are viewing notes for the event in row 2. Press the up arrow key when finished to return to the event.' + ); + }); + test('it shows controls for adding notes when showAddNote is true', () => { const wrapper = mount( diff --git a/x-pack/plugins/security_solution/public/timelines/components/notes/note_cards/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/notes/note_cards/index.tsx index 99cf8740809dae..9b307690cf12c8 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/notes/note_cards/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/notes/note_cards/index.tsx @@ -5,11 +5,10 @@ */ import { EuiFlexGroup, EuiPanel, EuiScreenReaderOnly } from '@elastic/eui'; -import React, { useState, useCallback, useMemo } from 'react'; +import React, { useState, useCallback } from 'react'; import styled from 'styled-components'; -import { appSelectors } from '../../../../common/store'; -import { useDeepEqualSelector } from '../../../../common/hooks/use_selector'; +import { getNotesContainerClassName } from '../../../../common/components/accessibility/helpers'; import { AddNote } from '../add_note'; import { AssociateNote } from '../helpers'; import { NotePreviews, NotePreviewsContainer } from '../../open_timeline/note_previews'; @@ -44,16 +43,14 @@ NotesContainer.displayName = 'NotesContainer'; interface Props { ariaRowindex: number; associateNote: AssociateNote; - noteIds: string[]; + notes: TimelineResultNote[]; showAddNote: boolean; toggleShowAddNote: () => void; } /** A view for entering and reviewing notes */ export const NoteCards = React.memo( - ({ ariaRowindex, associateNote, noteIds, showAddNote, toggleShowAddNote }) => { - const getNotesByIds = useMemo(() => appSelectors.notesByIdsSelector(), []); - const notesById = useDeepEqualSelector(getNotesByIds); + ({ ariaRowindex, associateNote, notes, showAddNote, toggleShowAddNote }) => { const [newNote, setNewNote] = useState(''); const associateNoteAndToggleShow = useCallback( @@ -64,23 +61,16 @@ export const NoteCards = React.memo( [associateNote, toggleShowAddNote] ); - const notes: TimelineResultNote[] = useMemo( - () => - appSelectors.getNotes(notesById, noteIds).map((note) => ({ - savedObjectId: note.saveObjectId, - note: note.note, - noteId: note.id, - updated: (note.lastEdit ?? note.created).getTime(), - updatedBy: note.user, - })), - [notesById, noteIds] - ); - return ( {notes.length ? ( - +

{i18n.YOU_ARE_VIEWING_NOTES(ariaRowindex)}

diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/note_previews/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/note_previews/index.tsx index 2a1d0d2ad11cf2..fc05e61442e83d 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/note_previews/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/note_previews/index.tsx @@ -5,7 +5,7 @@ */ import { uniqBy } from 'lodash/fp'; -import { EuiAvatar, EuiButtonIcon, EuiCommentList } from '@elastic/eui'; +import { EuiAvatar, EuiButtonIcon, EuiCommentList, EuiScreenReaderOnly } from '@elastic/eui'; import { FormattedRelative } from '@kbn/i18n/react'; import React, { useCallback, useMemo } from 'react'; import styled from 'styled-components'; @@ -15,6 +15,7 @@ import { TimelineResultNote } from '../types'; import { getEmptyValue, defaultToEmptyTag } from '../../../../common/components/empty_value'; import { MarkdownRenderer } from '../../../../common/components/markdown_editor'; import { timelineActions } from '../../../store/timeline'; +import { NOTE_CONTENT_CLASS_NAME } from '../../timeline/body/helpers'; import * as i18n from './translations'; export const NotePreviewsContainer = styled.section` @@ -89,7 +90,14 @@ export const NotePreviews = React.memo( ) : ( getEmptyValue() ), - children: {note.note ?? ''}, + children: ( +
+ +

{i18n.USER_ADDED_A_NOTE(note.updatedBy ?? i18n.AN_UNKNOWN_USER)}

+
+ {note.note ?? ''} +
+ ), actions: eventId && timelineId ? ( diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/note_previews/translations.ts b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/note_previews/translations.ts index 9857e55e365700..d38dee8a415045 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/note_previews/translations.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/note_previews/translations.ts @@ -12,3 +12,16 @@ export const TOGGLE_EXPAND_EVENT_DETAILS = i18n.translate( defaultMessage: 'Expand event details', } ); + +export const USER_ADDED_A_NOTE = (user: string) => + i18n.translate('xpack.securitySolution.timeline.userAddedANoteScreenReaderOnly', { + values: { user }, + defaultMessage: '{user} added a note', + }); + +export const AN_UNKNOWN_USER = i18n.translate( + 'xpack.securitySolution.timeline.anUnknownUserScreenReaderOnly', + { + defaultMessage: 'an unknown user', + } +); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/data_driven_columns/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/data_driven_columns/__snapshots__/index.test.tsx.snap index 8f514ca49e8480..d112a665d77c00 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/data_driven_columns/__snapshots__/index.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/data_driven_columns/__snapshots__/index.test.tsx.snap @@ -44,6 +44,7 @@ exports[`Columns it renders the expected columns 1`] = ` truncate={true} /> + 0 + 0 + 0 + 0 + 0 + 0 + 0 `; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/data_driven_columns/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/data_driven_columns/index.test.tsx index 00b3a10bba5386..d7931b563c7779 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/data_driven_columns/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/data_driven_columns/index.test.tsx @@ -26,6 +26,8 @@ describe('Columns', () => { columnRenderers={columnRenderers} data={mockTimelineData[0].data} ecsData={mockTimelineData[0].ecs} + hasRowRenderers={false} + notesCount={0} timelineId="test" /> ); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/data_driven_columns/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/data_driven_columns/index.tsx index 6dad9851e5adba..c497d4f459f000 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/data_driven_columns/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/data_driven_columns/index.tsx @@ -21,12 +21,14 @@ import * as i18n from './translations'; interface Props { _id: string; - activeTab?: TimelineTabs; ariaRowindex: number; columnHeaders: ColumnHeaderOptions[]; columnRenderers: ColumnRenderer[]; data: TimelineNonEcsData[]; ecsData: Ecs; + hasRowRenderers: boolean; + notesCount: number; + tabType?: TimelineTabs; timelineId: string; } @@ -74,12 +76,23 @@ export const onKeyDown = (keyboardEvent: React.KeyboardEvent) => { }; export const DataDrivenColumns = React.memo( - ({ _id, activeTab, ariaRowindex, columnHeaders, columnRenderers, data, ecsData, timelineId }) => ( + ({ + _id, + ariaRowindex, + columnHeaders, + columnRenderers, + data, + ecsData, + hasRowRenderers, + notesCount, + tabType, + timelineId, + }) => ( {columnHeaders.map((header, i) => ( ( eventId: _id, field: header, linkValues: getOr([], header.linkField ?? '', ecsData), - timelineId: activeTab != null ? `${timelineId}-${activeTab}` : timelineId, + timelineId: tabType != null ? `${timelineId}-${tabType}` : timelineId, truncate: true, values: getMappedNonEcsValue({ data, @@ -104,6 +117,17 @@ export const DataDrivenColumns = React.memo( })} + {hasRowRenderers && ( + +

{i18n.EVENT_HAS_AN_EVENT_RENDERER(ariaRowindex)}

+
+ )} + + {notesCount && ( + +

{i18n.EVENT_HAS_NOTES({ row: ariaRowindex, notesCount })}

+
+ )}
))}
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/data_driven_columns/translations.ts b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/data_driven_columns/translations.ts index 80199e0026ac32..63086d56d07530 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/data_driven_columns/translations.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/data_driven_columns/translations.ts @@ -11,3 +11,17 @@ export const YOU_ARE_IN_A_TABLE_CELL = ({ column, row }: { column: number; row: values: { column, row }, defaultMessage: 'You are in a table cell. row: {row}, column: {column}', }); + +export const EVENT_HAS_AN_EVENT_RENDERER = (row: number) => + i18n.translate('xpack.securitySolution.timeline.eventHasEventRendererScreenReaderOnly', { + values: { row }, + defaultMessage: + 'The event in row {row} has an event renderer. Press shift + down arrow to focus it.', + }); + +export const EVENT_HAS_NOTES = ({ notesCount, row }: { notesCount: number; row: number }) => + i18n.translate('xpack.securitySolution.timeline.eventHasNotesScreenReaderOnly', { + values: { notesCount, row }, + defaultMessage: + 'The event in row {row} has {notesCount, plural, =1 {a note} other {{notesCount} notes}}. Press shift + right arrow to focus notes.', + }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/event_column_view.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/event_column_view.test.tsx index 9bb8a695454d74..0525767e616bef 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/event_column_view.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/event_column_view.test.tsx @@ -36,8 +36,10 @@ describe('EventColumnView', () => { }, eventIdToNoteIds: {}, expanded: false, + hasRowRenderers: false, loading: false, loadingEventIds: [], + notesCount: 0, onEventToggled: jest.fn(), onPinEvent: jest.fn(), onRowSelected: jest.fn(), 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 6aee6f9d4fdfab..ae8d2a47c7dc78 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 @@ -35,7 +35,6 @@ import * as i18n from '../translations'; interface Props { id: string; actionsColumnWidth: number; - activeTab?: TimelineTabs; ariaRowindex: number; columnHeaders: ColumnHeaderOptions[]; columnRenderers: ColumnRenderer[]; @@ -46,15 +45,18 @@ interface Props { isEventPinned: boolean; isEventViewer?: boolean; loadingEventIds: Readonly; + notesCount: number; onEventToggled: () => void; onPinEvent: OnPinEvent; onRowSelected: OnRowSelected; onUnPinEvent: OnUnPinEvent; refetch: inputsModel.Refetch; onRuleChange?: () => void; + hasRowRenderers: boolean; selectedEventIds: Readonly>; showCheckboxes: boolean; showNotes: boolean; + tabType?: TimelineTabs; timelineId: string; toggleShowNotes: () => void; } @@ -65,7 +67,6 @@ export const EventColumnView = React.memo( ({ id, actionsColumnWidth, - activeTab, ariaRowindex, columnHeaders, columnRenderers, @@ -76,15 +77,18 @@ export const EventColumnView = React.memo( isEventPinned = false, isEventViewer = false, loadingEventIds, + notesCount, onEventToggled, onPinEvent, onRowSelected, onUnPinEvent, refetch, + hasRowRenderers, onRuleChange, selectedEventIds, showCheckboxes, showNotes, + tabType, timelineId, toggleShowNotes, }) => { @@ -225,12 +229,14 @@ export const EventColumnView = React.memo( diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/index.tsx index bce5f1293e66b8..92ae01b185f7a1 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/index.tsx @@ -24,7 +24,6 @@ import { eventIsPinned } from '../helpers'; const ARIA_ROW_INDEX_OFFSET = 2; interface Props { - activeTab?: TimelineTabs; actionsColumnWidth: number; browserFields: BrowserFields; columnHeaders: ColumnHeaderOptions[]; @@ -43,11 +42,11 @@ interface Props { rowRenderers: RowRenderer[]; selectedEventIds: Readonly>; showCheckboxes: boolean; + tabType?: TimelineTabs; } const EventsComponent: React.FC = ({ actionsColumnWidth, - activeTab, browserFields, columnHeaders, columnRenderers, @@ -65,11 +64,11 @@ const EventsComponent: React.FC = ({ rowRenderers, selectedEventIds, showCheckboxes, + tabType, }) => ( {data.map((event, i) => ( = ({ eventIdToNoteIds={eventIdToNoteIds} isEventPinned={eventIsPinned({ eventId: event._id, pinnedEventIds })} isEventViewer={isEventViewer} - key={`${id}_${activeTab}_${event._id}_${event._index}`} + key={`${id}_${tabType}_${event._id}_${event._index}`} lastFocusedAriaColindex={lastFocusedAriaColindex} loadingEventIds={loadingEventIds} onRowSelected={onRowSelected} @@ -89,6 +88,7 @@ const EventsComponent: React.FC = ({ onRuleChange={onRuleChange} selectedEventIds={selectedEventIds} showCheckboxes={showCheckboxes} + tabType={tabType} timelineId={id} /> ))} diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/stateful_event.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/stateful_event.tsx index 9802e4532b05bf..e3f5a744e8b7d7 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/stateful_event.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/stateful_event.tsx @@ -19,22 +19,22 @@ import { OnPinEvent, OnRowSelected } from '../../events'; import { STATEFUL_EVENT_CSS_CLASS_NAME } from '../../helpers'; import { EventsTrGroup, EventsTrSupplement, EventsTrSupplementContainer } from '../../styles'; import { ColumnRenderer } from '../renderers/column_renderer'; - import { RowRenderer } from '../renderers/row_renderer'; import { isEventBuildingBlockType, getEventType } from '../helpers'; import { NoteCards } from '../../../notes/note_cards'; import { useEventDetailsWidthContext } from '../../../../../common/components/events_viewer/event_details_width_context'; import { EventColumnView } from './event_column_view'; -import { inputsModel } from '../../../../../common/store'; +import { appSelectors, inputsModel } from '../../../../../common/store'; import { timelineActions, timelineSelectors } from '../../../../store/timeline'; import { activeTimeline } from '../../../../containers/active_timeline_context'; +import { TimelineResultNote } from '../../../open_timeline/types'; +import { getRowRenderer } from '../renderers/get_row_renderer'; import { StatefulRowRenderer } from './stateful_row_renderer'; import { NOTES_BUTTON_CLASS_NAME } from '../../properties/helpers'; import { timelineDefaults } from '../../../../store/timeline/defaults'; interface Props { actionsColumnWidth: number; - activeTab?: TimelineTabs; containerRef: React.MutableRefObject; browserFields: BrowserFields; columnHeaders: ColumnHeaderOptions[]; @@ -52,6 +52,7 @@ interface Props { rowRenderers: RowRenderer[]; selectedEventIds: Readonly>; showCheckboxes: boolean; + tabType?: TimelineTabs; timelineId: string; } @@ -66,7 +67,6 @@ EventsTrSupplementContainerWrapper.displayName = 'EventsTrSupplementContainerWra const StatefulEventComponent: React.FC = ({ actionsColumnWidth, - activeTab, browserFields, containerRef, columnHeaders, @@ -84,6 +84,7 @@ const StatefulEventComponent: React.FC = ({ ariaRowindex, selectedEventIds, showCheckboxes, + tabType, timelineId, }) => { const trGroupRef = useRef(null); @@ -93,12 +94,31 @@ const StatefulEventComponent: React.FC = ({ const expandedEvent = useDeepEqualSelector( (state) => (getTimeline(state, timelineId) ?? timelineDefaults).expandedEvent ); - + const getNotesByIds = useMemo(() => appSelectors.notesByIdsSelector(), []); + const notesById = useDeepEqualSelector(getNotesByIds); + const noteIds: string[] = eventIdToNoteIds[event._id] || emptyNotes; const isExpanded = useMemo(() => expandedEvent && expandedEvent.eventId === event._id, [ event._id, expandedEvent, ]); + const notes: TimelineResultNote[] = useMemo( + () => + appSelectors.getNotes(notesById, noteIds).map((note) => ({ + savedObjectId: note.saveObjectId, + note: note.note, + noteId: note.id, + updated: (note.lastEdit ?? note.created).getTime(), + updatedBy: note.user, + })), + [notesById, noteIds] + ); + + const hasRowRenderers: boolean = useMemo(() => getRowRenderer(event.ecs, rowRenderers) != null, [ + event.ecs, + rowRenderers, + ]); + const onToggleShowNotes = useCallback(() => { const eventId = event._id; @@ -195,7 +215,6 @@ const StatefulEventComponent: React.FC = ({ = ({ ecsData={event.ecs} eventIdToNoteIds={eventIdToNoteIds} expanded={isExpanded} + hasRowRenderers={hasRowRenderers} isEventPinned={isEventPinned} isEventViewer={isEventViewer} loadingEventIds={loadingEventIds} + notesCount={notes.length} onEventToggled={handleOnEventToggled} onPinEvent={onPinEvent} onRowSelected={onRowSelected} @@ -215,6 +236,7 @@ const StatefulEventComponent: React.FC = ({ selectedEventIds={selectedEventIds} showCheckboxes={showCheckboxes} showNotes={!!showNotes[event._id]} + tabType={tabType} timelineId={timelineId} toggleShowNotes={onToggleShowNotes} /> @@ -228,7 +250,7 @@ const StatefulEventComponent: React.FC = ({ ariaRowindex={ariaRowindex} associateNote={associateNote} data-test-subj="note-cards" - noteIds={eventIdToNoteIds[event._id] || emptyNotes} + notes={notes} showAddNote={!!showNotes[event._id]} toggleShowAddNote={onToggleShowNotes} /> diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/stateful_row_renderer/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/stateful_row_renderer/index.tsx index 1628824b46a08d..4000ebcfd767a8 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/stateful_row_renderer/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/stateful_row_renderer/index.tsx @@ -12,6 +12,7 @@ import { BrowserFields } from '../../../../../../common/containers/source'; import { ARIA_COLINDEX_ATTRIBUTE, ARIA_ROWINDEX_ATTRIBUTE, + getRowRendererClassName, } from '../../../../../../common/components/accessibility/helpers'; import { TimelineItem } from '../../../../../../../common/search_strategy/timeline'; import { getRowRenderer } from '../../renderers/get_row_renderer'; @@ -59,28 +60,44 @@ export const StatefulRowRenderer = ({ rowindexAttribute: ARIA_ROWINDEX_ATTRIBUTE, }); + const rowRenderer = useMemo(() => getRowRenderer(event.ecs, rowRenderers), [ + event.ecs, + rowRenderers, + ]); + const content = useMemo( - () => ( - - -

{i18n.YOU_ARE_IN_AN_EVENT_RENDERER(ariaRowindex)}

-
-
- {getRowRenderer(event.ecs, rowRenderers).renderRow({ - browserFields, - data: event.ecs, - timelineId, - })} + () => + rowRenderer && ( + // eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions +
+ + + +

{i18n.YOU_ARE_IN_AN_EVENT_RENDERER(ariaRowindex)}

+
+
+ {rowRenderer.renderRow({ + browserFields, + data: event.ecs, + timelineId, + })} +
+
+
- - ), - [ariaRowindex, browserFields, event.ecs, focusOwnership, onKeyDown, rowRenderers, timelineId] + ), + [ + ariaRowindex, + browserFields, + event.ecs, + focusOwnership, + onFocus, + onKeyDown, + onOutsideClick, + rowRenderer, + timelineId, + ] ); - return ( - // eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions -
- {content} -
- ); + return content; }; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/helpers.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/helpers.tsx index 3470dba636aa8c..0295d44b646d77 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/helpers.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/helpers.tsx @@ -160,3 +160,9 @@ const InvestigateInResolverActionComponent: React.FC { setSelected: (jest.fn() as unknown) as StatefulBodyProps['setSelected'], sort: mockSort, showCheckboxes: false, - activeTab: TimelineTabs.query, + tabType: TimelineTabs.query, totalPages: 1, }; 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 f6190b39214e90..4a33d0d3af33e9 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 @@ -21,7 +21,7 @@ import { BrowserFields } from '../../../../common/containers/source'; import { TimelineItem } from '../../../../../common/search_strategy/timeline'; import { inputsModel, State } from '../../../../common/store'; import { useManageTimeline } from '../../manage_timeline'; -import { ColumnHeaderOptions, TimelineModel } from '../../../store/timeline/model'; +import { ColumnHeaderOptions, TimelineModel, TimelineTabs } from '../../../store/timeline/model'; import { timelineDefaults } from '../../../store/timeline/defaults'; import { timelineActions, timelineSelectors } from '../../../store/timeline'; import { OnRowSelected, OnSelectAll } from '../events'; @@ -43,6 +43,7 @@ interface OwnProps { isEventViewer?: boolean; sort: Sort[]; refetch: inputsModel.Refetch; + tabType: TimelineTabs; totalPages: number; onRuleChange?: () => void; } @@ -60,7 +61,6 @@ export type StatefulBodyProps = OwnProps & PropsFromRedux; export const BodyComponent = React.memo( ({ - activeTab, activePage, browserFields, columnHeaders, @@ -79,6 +79,7 @@ export const BodyComponent = React.memo( showCheckboxes, refetch, sort, + tabType, totalPages, }) => { const containerRef = useRef(null); @@ -200,7 +201,6 @@ export const BodyComponent = React.memo( ( onRuleChange={onRuleChange} selectedEventIds={selectedEventIds} showCheckboxes={showCheckboxes} + tabType={tabType} /> @@ -225,7 +226,6 @@ export const BodyComponent = React.memo( ); }, (prevProps, nextProps) => - prevProps.activeTab === nextProps.activeTab && deepEqual(prevProps.browserFields, nextProps.browserFields) && deepEqual(prevProps.columnHeaders, nextProps.columnHeaders) && deepEqual(prevProps.data, nextProps.data) && @@ -238,7 +238,8 @@ export const BodyComponent = React.memo( prevProps.id === nextProps.id && prevProps.isEventViewer === nextProps.isEventViewer && prevProps.isSelectAllChecked === nextProps.isSelectAllChecked && - prevProps.showCheckboxes === nextProps.showCheckboxes + prevProps.showCheckboxes === nextProps.showCheckboxes && + prevProps.tabType === nextProps.tabType ); BodyComponent.displayName = 'BodyComponent'; @@ -253,7 +254,6 @@ const makeMapStateToProps = () => { const mapStateToProps = (state: State, { browserFields, id }: OwnProps) => { const timeline: TimelineModel = getTimeline(state, id) ?? timelineDefaults; const { - activeTab, columns, eventIdToNoteIds, excludedRowRendererIds, @@ -265,7 +265,6 @@ const makeMapStateToProps = () => { } = timeline; return { - activeTab: id === TimelineId.active ? activeTab : undefined, columnHeaders: memoizedColumnHeaders(columns, browserFields), eventIdToNoteIds, excludedRowRendererIds, diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/get_row_renderer.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/get_row_renderer.test.tsx index b4fdc427d9db3e..f3a914ff4be29c 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/get_row_renderer.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/get_row_renderer.test.tsx @@ -48,7 +48,7 @@ describe('get_column_renderer', () => { test('renders correctly against snapshot', () => { const rowRenderer = getRowRenderer(nonSuricata, rowRenderers); - const row = rowRenderer.renderRow({ + const row = rowRenderer?.renderRow({ browserFields: mockBrowserFields, data: nonSuricata, timelineId: 'test', @@ -60,7 +60,7 @@ describe('get_column_renderer', () => { test('should render plain row data when it is a non suricata row', () => { const rowRenderer = getRowRenderer(nonSuricata, rowRenderers); - const row = rowRenderer.renderRow({ + const row = rowRenderer?.renderRow({ browserFields: mockBrowserFields, data: nonSuricata, timelineId: 'test', @@ -75,7 +75,7 @@ describe('get_column_renderer', () => { test('should render a suricata row data when it is a suricata row', () => { const rowRenderer = getRowRenderer(suricata, rowRenderers); - const row = rowRenderer.renderRow({ + const row = rowRenderer?.renderRow({ browserFields: mockBrowserFields, data: suricata, timelineId: 'test', @@ -93,7 +93,7 @@ describe('get_column_renderer', () => { test('should render a suricata row data if event.category is network_traffic', () => { suricata.event = { ...suricata.event, ...{ category: ['network_traffic'] } }; const rowRenderer = getRowRenderer(suricata, rowRenderers); - const row = rowRenderer.renderRow({ + const row = rowRenderer?.renderRow({ browserFields: mockBrowserFields, data: suricata, timelineId: 'test', @@ -111,7 +111,7 @@ describe('get_column_renderer', () => { test('should render a zeek row data if event.category is network_traffic', () => { zeek.event = { ...zeek.event, ...{ category: ['network_traffic'] } }; const rowRenderer = getRowRenderer(zeek, rowRenderers); - const row = rowRenderer.renderRow({ + const row = rowRenderer?.renderRow({ browserFields: mockBrowserFields, data: zeek, timelineId: 'test', @@ -129,7 +129,7 @@ describe('get_column_renderer', () => { test('should render a system row data if event.category is network_traffic', () => { system.event = { ...system.event, ...{ category: ['network_traffic'] } }; const rowRenderer = getRowRenderer(system, rowRenderers); - const row = rowRenderer.renderRow({ + const row = rowRenderer?.renderRow({ browserFields: mockBrowserFields, data: system, timelineId: 'test', @@ -147,7 +147,7 @@ describe('get_column_renderer', () => { test('should render a auditd row data if event.category is network_traffic', () => { auditd.event = { ...auditd.event, ...{ category: ['network_traffic'] } }; const rowRenderer = getRowRenderer(auditd, rowRenderers); - const row = rowRenderer.renderRow({ + const row = rowRenderer?.renderRow({ browserFields: mockBrowserFields, data: auditd, timelineId: 'test', diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/get_row_renderer.ts b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/get_row_renderer.ts index 779d54216e26c8..1662cf4037cac3 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/get_row_renderer.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/get_row_renderer.ts @@ -7,15 +7,5 @@ import { Ecs } from '../../../../../../common/ecs'; import { RowRenderer } from './row_renderer'; -const unhandledRowRenderer = (): never => { - throw new Error('Unhandled Row Renderer'); -}; - -export const getRowRenderer = (ecs: Ecs, rowRenderers: RowRenderer[]): RowRenderer => { - const renderer = rowRenderers.find((rowRenderer) => rowRenderer.isInstance(ecs)); - if (renderer == null) { - return unhandledRowRenderer(); - } else { - return renderer; - } -}; +export const getRowRenderer = (ecs: Ecs, rowRenderers: RowRenderer[]): RowRenderer | null => + rowRenderers.find((rowRenderer) => rowRenderer.isInstance(ecs)) ?? null; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/index.ts b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/index.ts index 8e95fc3ad238a6..f4498b10e4c8d8 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/index.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/index.ts @@ -9,7 +9,6 @@ import { ColumnRenderer } from './column_renderer'; import { emptyColumnRenderer } from './empty_column_renderer'; import { netflowRowRenderer } from './netflow/netflow_row_renderer'; import { plainColumnRenderer } from './plain_column_renderer'; -import { plainRowRenderer } from './plain_row_renderer'; import { RowRenderer } from './row_renderer'; import { suricataRowRenderer } from './suricata/suricata_row_renderer'; import { unknownColumnRenderer } from './unknown_column_renderer'; @@ -29,7 +28,6 @@ export const rowRenderers: RowRenderer[] = [ suricataRowRenderer, zeekRowRenderer, netflowRowRenderer, - plainRowRenderer, // falls-back to the plain row renderer ]; export const columnRenderers: ColumnRenderer[] = [ diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/pinned_tab_content/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/pinned_tab_content/index.tsx index 45c190c42605c2..a0d2ca57f90b36 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/pinned_tab_content/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/pinned_tab_content/index.tsx @@ -23,7 +23,7 @@ import { EventDetailsWidthProvider } from '../../../../common/components/events_ import { SourcererScopeName } from '../../../../common/store/sourcerer/model'; import { timelineDefaults } from '../../../store/timeline/defaults'; import { useSourcererScope } from '../../../../common/containers/sourcerer'; -import { TimelineModel } from '../../../store/timeline/model'; +import { TimelineModel, TimelineTabs } from '../../../store/timeline/model'; import { EventDetails } from '../event_details'; import { ToggleExpandedEvent } from '../../../store/timeline/actions'; import { State } from '../../../../common/store'; @@ -183,6 +183,7 @@ export const PinnedTabContentComponent: React.FC = ({ id={timelineId} refetch={refetch} sort={sort} + tabType={TimelineTabs.pinned} totalPages={calculateTotalPages({ itemsCount: totalCount, itemsPerPage, diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/query_tab_content/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/query_tab_content/index.tsx index f6d6654d7fecee..c0840d58174b32 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/query_tab_content/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/query_tab_content/index.tsx @@ -330,6 +330,7 @@ export const QueryTabContentComponent: React.FC = ({ id={timelineId} refetch={refetch} sort={sort} + tabType={TimelineTabs.query} totalPages={calculateTotalPages({ itemsCount: totalCount, itemsPerPage, diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs_content/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs_content/index.tsx index 7f0809cf9b9d8f..c97571fbbd6f35 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs_content/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs_content/index.tsx @@ -199,7 +199,7 @@ const TabsContentComponent: React.FC = ({ timelineId, graphEve disabled={!graphEventId} key={TimelineTabs.graph} > - {i18n.GRAPH_TAB} + {i18n.ANALYZER_TAB}