From 21a0e8887c6d8359979e869a46a49e2a1a76c739 Mon Sep 17 00:00:00 2001 From: PhilippeOberti Date: Tue, 24 Sep 2024 11:49:06 -0500 Subject: [PATCH] [Security Solution][Notes] - add button to add note from flyout note header block --- .../left/components/add_note.tsx | 23 +++-- .../left/components/notes_list.tsx | 24 +++-- .../right/components/alert_header_block.tsx | 2 +- .../right/components/notes.test.tsx | 65 ++++++++++++- .../right/components/notes.tsx | 91 ++++++++++++++++--- .../right/components/test_ids.ts | 3 + .../translations/translations/fr-FR.json | 10 -- .../translations/translations/ja-JP.json | 10 -- .../translations/translations/zh-CN.json | 10 -- 9 files changed, 170 insertions(+), 68 deletions(-) diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/left/components/add_note.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/left/components/add_note.tsx index 88c77b5d09160c..5e4e390ac50776 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/left/components/add_note.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/left/components/add_note.tsx @@ -16,6 +16,7 @@ import { EuiIcon, EuiSpacer, EuiToolTip, + useEuiTheme, } from '@elastic/eui'; import { css } from '@emotion/react'; import { useDispatch, useSelector } from 'react-redux'; @@ -40,31 +41,34 @@ import { } from '../../../../notes/store/notes.slice'; import { MarkdownEditor } from '../../../../common/components/markdown_editor'; -const timelineCheckBoxId = 'xpack.securitySolution.notes.attachToTimelineCheckboxId'; +const timelineCheckBoxId = 'xpack.securitySolution.flyout.left.notes.attachToTimelineCheckboxId'; export const MARKDOWN_ARIA_LABEL = i18n.translate( - 'xpack.securitySolution.notes.markdownAriaLabel', + 'xpack.securitySolution.flyout.left.notes.markdownAriaLabel', { defaultMessage: 'Note', } ); -export const ADD_NOTE_BUTTON = i18n.translate('xpack.securitySolution.notes.addNoteBtnLabel', { - defaultMessage: 'Add note', -}); +export const ADD_NOTE_BUTTON = i18n.translate( + 'xpack.securitySolution.flyout.left.notes.addNoteBtnLabel', + { + defaultMessage: 'Add note', + } +); export const CREATE_NOTE_ERROR = i18n.translate( - 'xpack.securitySolution.notes.createNoteErrorLabel', + 'xpack.securitySolution.flyout.left.notes.createNoteErrorLabel', { defaultMessage: 'Error create note', } ); export const ATTACH_TO_TIMELINE_CHECKBOX = i18n.translate( - 'xpack.securitySolution.notes.attachToTimelineCheckboxLabel', + 'xpack.securitySolution.flyout.left.notes.attachToTimelineCheckboxLabel', { defaultMessage: 'Attach to active timeline', } ); export const ATTACH_TO_TIMELINE_INFO = i18n.translate( - 'xpack.securitySolution.notes.attachToTimelineInfoLabel', + 'xpack.securitySolution.flyout.left.notes.attachToTimelineInfoLabel', { defaultMessage: 'The active timeline must be saved before a note can be associated with it', } @@ -83,6 +87,7 @@ export interface AddNewNoteProps { */ export const AddNote = memo(({ eventId }: AddNewNoteProps) => { const { telemetry } = useKibana().services; + const { euiTheme } = useEuiTheme(); const dispatch = useDispatch(); const { addError: addErrorToast } = useAppToasts(); const [editorValue, setEditorValue] = useState(''); @@ -171,7 +176,7 @@ export const AddNote = memo(({ eventId }: AddNewNoteProps) => { diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/left/components/notes_list.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/left/components/notes_list.tsx index 6fb9241973bac0..088dc9cdad3239 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/left/components/notes_list.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/left/components/notes_list.tsx @@ -42,23 +42,29 @@ import { import { useAppToasts } from '../../../../common/hooks/use_app_toasts'; import { useUserPrivileges } from '../../../../common/components/user_privileges'; -export const ADDED_A_NOTE = i18n.translate('xpack.securitySolution.notes.addedANoteLabel', { - defaultMessage: 'added a note', -}); +export const ADDED_A_NOTE = i18n.translate( + 'xpack.securitySolution.flyout.left.notes.addedANoteLabel', + { + defaultMessage: 'added a note', + } +); export const FETCH_NOTES_ERROR = i18n.translate( - 'xpack.securitySolution.notes.fetchNotesErrorLabel', + 'xpack.securitySolution.flyout.left.notes.fetchNotesErrorLabel', { defaultMessage: 'Error fetching notes', } ); -export const NO_NOTES = i18n.translate('xpack.securitySolution.notes.noNotesLabel', { +export const NO_NOTES = i18n.translate('xpack.securitySolution.flyout.left.notes.noNotesLabel', { defaultMessage: 'No notes have been created for this document', }); -export const DELETE_NOTE = i18n.translate('xpack.securitySolution.notes.deleteNoteLabel', { - defaultMessage: 'Delete note', -}); +export const DELETE_NOTE = i18n.translate( + 'xpack.securitySolution.flyout.left.notes.deleteNoteLabel', + { + defaultMessage: 'Delete note', + } +); export const DELETE_NOTE_ERROR = i18n.translate( - 'xpack.securitySolution.notes.deleteNoteErrorLabel', + 'xpack.securitySolution.flyout.left.notes.deleteNoteErrorLabel', { defaultMessage: 'Error deleting note', } diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/alert_header_block.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/alert_header_block.tsx index 84259ffcf0eb62..fac083f90de5f6 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/alert_header_block.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/alert_header_block.tsx @@ -30,7 +30,7 @@ export interface AlertHeaderBlockProps { export const AlertHeaderBlock: VFC = memo( ({ title, children, 'data-test-subj': dataTestSubj }) => ( - +

{title}

diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/notes.test.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/notes.test.tsx index 437fb5d9e21b24..f70cd2aae3e8de 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/notes.test.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/notes.test.tsx @@ -8,12 +8,23 @@ import React from 'react'; import { render } from '@testing-library/react'; import { DocumentDetailsContext } from '../../shared/context'; -import { NOTES_COUNT_TEST_ID, NOTES_LOADING_TEST_ID, NOTES_TITLE_TEST_ID } from './test_ids'; +import { + NOTES_ADD_NOTE_BUTTON_TEST_ID, + NOTES_ADD_NOTE_ICON_BUTTON_TEST_ID, + NOTES_COUNT_TEST_ID, + NOTES_LOADING_TEST_ID, + NOTES_TITLE_TEST_ID, +} from './test_ids'; import { FETCH_NOTES_ERROR, Notes } from './notes'; import { mockContextValue } from '../../shared/mocks/mock_context'; import { createMockStore, mockGlobalState, TestProviders } from '../../../../common/mock'; import { ReqStatus } from '../../../../notes'; import type { Note } from '../../../../../common/api/timeline'; +import { useExpandableFlyoutApi } from '@kbn/expandable-flyout'; +import { DocumentDetailsLeftPanelKey } from '../../shared/constants/panel_keys'; +import { LeftPanelNotesTab } from '../../left'; + +jest.mock('@kbn/expandable-flyout'); const mockAddError = jest.fn(); jest.mock('../../../../common/hooks/use_app_toasts', () => ({ @@ -33,6 +44,8 @@ jest.mock('react-redux', () => { describe('', () => { it('should render loading spinner', () => { + (useExpandableFlyoutApi as jest.Mock).mockReturnValue({ openLeftPanel: jest.fn() }); + const store = createMockStore({ ...mockGlobalState, notes: { @@ -58,7 +71,38 @@ describe('', () => { expect(getByTestId(NOTES_LOADING_TEST_ID)).toBeInTheDocument(); }); - it('should render number of notes', () => { + it('should render Add note button if no notes are present', () => { + const mockOpenLeftPanel = jest.fn(); + (useExpandableFlyoutApi as jest.Mock).mockReturnValue({ openLeftPanel: mockOpenLeftPanel }); + + const { getByTestId } = render( + + + + + + ); + + const button = getByTestId(NOTES_ADD_NOTE_BUTTON_TEST_ID); + expect(button).toBeInTheDocument(); + + button.click(); + + expect(mockOpenLeftPanel).toHaveBeenCalledWith({ + id: DocumentDetailsLeftPanelKey, + path: { tab: LeftPanelNotesTab }, + params: { + id: mockContextValue.eventId, + indexName: mockContextValue.indexName, + scopeId: mockContextValue.scopeId, + }, + }); + }); + + it('should render number of notes and plus button', () => { + const mockOpenLeftPanel = jest.fn(); + (useExpandableFlyoutApi as jest.Mock).mockReturnValue({ openLeftPanel: mockOpenLeftPanel }); + const contextValue = { ...mockContextValue, eventId: '1', @@ -72,10 +116,23 @@ describe('', () => { ); - expect(getByTestId(NOTES_TITLE_TEST_ID)).toBeInTheDocument(); - expect(getByTestId(NOTES_TITLE_TEST_ID)).toHaveTextContent('Notes'); expect(getByTestId(NOTES_COUNT_TEST_ID)).toBeInTheDocument(); expect(getByTestId(NOTES_COUNT_TEST_ID)).toHaveTextContent('1'); + + const button = getByTestId(NOTES_ADD_NOTE_ICON_BUTTON_TEST_ID); + + expect(button).toBeInTheDocument(); + button.click(); + + expect(mockOpenLeftPanel).toHaveBeenCalledWith({ + id: DocumentDetailsLeftPanelKey, + path: { tab: LeftPanelNotesTab }, + params: { + id: contextValue.eventId, + indexName: mockContextValue.indexName, + scopeId: mockContextValue.scopeId, + }, + }); }); it('should render number of notes in scientific notation for big numbers', () => { diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/notes.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/notes.tsx index 41f96c90f89ec4..a981a16117a680 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/notes.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/notes.tsx @@ -5,14 +5,30 @@ * 2.0. */ -import React, { memo, useEffect } from 'react'; +import React, { memo, useCallback, useEffect } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { FormattedMessage } from '@kbn/i18n-react'; import { i18n } from '@kbn/i18n'; -import { EuiLoadingSpinner } from '@elastic/eui'; +import { + EuiButtonEmpty, + EuiButtonIcon, + EuiFlexGroup, + EuiFlexItem, + EuiLoadingSpinner, + useEuiTheme, +} from '@elastic/eui'; +import { css } from '@emotion/react'; +import { useExpandableFlyoutApi } from '@kbn/expandable-flyout'; +import { DocumentDetailsLeftPanelKey } from '../../shared/constants/panel_keys'; import { FormattedCount } from '../../../../common/components/formatted_number'; import { useDocumentDetailsContext } from '../../shared/context'; -import { NOTES_COUNT_TEST_ID, NOTES_LOADING_TEST_ID, NOTES_TITLE_TEST_ID } from './test_ids'; +import { + NOTES_ADD_NOTE_BUTTON_TEST_ID, + NOTES_ADD_NOTE_ICON_BUTTON_TEST_ID, + NOTES_COUNT_TEST_ID, + NOTES_LOADING_TEST_ID, + NOTES_TITLE_TEST_ID, +} from './test_ids'; import type { State } from '../../../../common/store'; import type { Note } from '../../../../../common/api/timeline'; import { @@ -20,26 +36,49 @@ import { ReqStatus, selectFetchNotesByDocumentIdsError, selectFetchNotesByDocumentIdsStatus, - selectSortedNotesByDocumentId, + selectNotesByDocumentId, } from '../../../../notes/store/notes.slice'; import { useAppToasts } from '../../../../common/hooks/use_app_toasts'; import { AlertHeaderBlock } from './alert_header_block'; +import { LeftPanelNotesTab } from '../../left'; export const FETCH_NOTES_ERROR = i18n.translate( - 'xpack.securitySolution.notes.fetchNotesErrorLabel', + 'xpack.securitySolution.flyout.right.notes.fetchNotesErrorLabel', { defaultMessage: 'Error fetching notes', } ); +export const ADD_NOTE_BUTTON = i18n.translate( + 'xpack.securitySolution.flyout.right.notes.addNoteButtonLabel', + { + defaultMessage: 'Add note', + } +); /** * Renders a block with the number of notes for the event */ export const Notes = memo(() => { + const { euiTheme } = useEuiTheme(); const dispatch = useDispatch(); - const { eventId } = useDocumentDetailsContext(); + const { eventId, indexName, scopeId } = useDocumentDetailsContext(); const { addError: addErrorToast } = useAppToasts(); + const { openLeftPanel } = useExpandableFlyoutApi(); + const openExpandedFlyoutNotesTab = useCallback( + () => + openLeftPanel({ + id: DocumentDetailsLeftPanelKey, + path: { tab: LeftPanelNotesTab }, + params: { + id: eventId, + indexName, + scopeId, + }, + }), + [eventId, indexName, openLeftPanel, scopeId] + ); + useEffect(() => { dispatch(fetchNotesByDocumentIds({ documentIds: [eventId] })); }, [dispatch, eventId]); @@ -47,12 +86,7 @@ export const Notes = memo(() => { const fetchStatus = useSelector((state: State) => selectFetchNotesByDocumentIdsStatus(state)); const fetchError = useSelector((state: State) => selectFetchNotesByDocumentIdsError(state)); - const notes: Note[] = useSelector((state: State) => - selectSortedNotesByDocumentId(state, { - documentId: eventId, - sort: { field: 'created', direction: 'desc' }, - }) - ); + const notes: Note[] = useSelector((state: State) => selectNotesByDocumentId(state, eventId)); // show a toast if the fetch notes call fails useEffect(() => { @@ -76,9 +110,36 @@ export const Notes = memo(() => { {fetchStatus === ReqStatus.Loading ? ( ) : ( -
- -
+ <> + {notes.length === 0 ? ( + + {ADD_NOTE_BUTTON} + + ) : ( + + + + + + + + + )} + )} ); diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/test_ids.ts b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/test_ids.ts index 83f833a4b24ace..40670ddc7110a1 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/test_ids.ts +++ b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/test_ids.ts @@ -34,6 +34,9 @@ export const SHARE_BUTTON_TEST_ID = `${FLYOUT_HEADER_TEST_ID}ShareButton` as con export const CHAT_BUTTON_TEST_ID = 'newChatByTitle' as const; export const NOTES_TITLE_TEST_ID = `${FLYOUT_HEADER_TEST_ID}NotesTitle` as const; +export const NOTES_ADD_NOTE_BUTTON_TEST_ID = `${FLYOUT_HEADER_TEST_ID}NotesAddNoteButton` as const; +export const NOTES_ADD_NOTE_ICON_BUTTON_TEST_ID = + `${FLYOUT_HEADER_TEST_ID}NotesAddNoteIconButton` as const; export const NOTES_COUNT_TEST_ID = `${FLYOUT_HEADER_TEST_ID}NotesCount` as const; export const NOTES_LOADING_TEST_ID = `${FLYOUT_HEADER_TEST_ID}NotesLoading` as const; diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index a14f2ce6d398dd..0a9ddb6c6b7535 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -39698,17 +39698,9 @@ "xpack.securitySolution.noPermissionsTitle": "Privilèges requis", "xpack.securitySolution.noPrivilegesDefaultMessage": "Pour afficher cette page, vous devez mettre à jour les privilèges. Pour en savoir plus, contactez votre administrateur Kibana.", "xpack.securitySolution.noPrivilegesPerPageMessage": "Pour afficher {pageName}, vous devez mettre à jour les privilèges. Pour en savoir plus, contactez votre administrateur Kibana.", - "xpack.securitySolution.notes.addedANoteLabel": "a ajouté une note", - "xpack.securitySolution.notes.addNoteBtnLabel": "Ajouter la note", "xpack.securitySolution.notes.addNoteButtonLabel": "Ajouter la note", - "xpack.securitySolution.notes.attachToTimelineCheckboxLabel": "Attacher à la chronologie active", - "xpack.securitySolution.notes.attachToTimelineInfoLabel": "La chronologie active doit être enregistrée avant qu'une note puisse lui être associée", "xpack.securitySolution.notes.cancelButtonLabel": "Annuler", "xpack.securitySolution.notes.createdByLabel": "Créé par", - "xpack.securitySolution.notes.createNoteErrorLabel": "Erreur lors de la création de la note", - "xpack.securitySolution.notes.deleteNoteErrorLabel": "Erreur lors de la suppression de la note", - "xpack.securitySolution.notes.deleteNoteLabel": "Supprimer la note", - "xpack.securitySolution.notes.fetchNotesErrorLabel": "Erreur lors de la récupération des notes", "xpack.securitySolution.notes.management.batchActionsTitle": "Actions groupées", "xpack.securitySolution.notes.management.createdByColumnTitle": "Créé par", "xpack.securitySolution.notes.management.createdColumnTitle": "Créé", @@ -39725,8 +39717,6 @@ "xpack.securitySolution.notes.management.tableError": "Impossible de charger les notes", "xpack.securitySolution.notes.management.timelineColumnTitle": "Chronologie", "xpack.securitySolution.notes.management.viewEventInTimeline": "Afficher l'événement dans la chronologie", - "xpack.securitySolution.notes.markdownAriaLabel": "Note", - "xpack.securitySolution.notes.noNotesLabel": "Aucune note n'a été créée pour ce document", "xpack.securitySolution.notes.noteLabel": "Note", "xpack.securitySolution.notes.notesTitle": "Notes", "xpack.securitySolution.notes.search.FilterByUserOrNotePlaceholder": "Filtre par utilisateur ou note", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 5184bf3718ef6d..6e3754bb7046b4 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -39441,17 +39441,9 @@ "xpack.securitySolution.noPermissionsTitle": "権限が必要です", "xpack.securitySolution.noPrivilegesDefaultMessage": "このページを表示するには、権限を更新する必要があります。詳細については、Kibana管理者に連絡してください。", "xpack.securitySolution.noPrivilegesPerPageMessage": "{pageName}を表示するには、権限を更新する必要があります。詳細については、Kibana管理者に連絡してください。", - "xpack.securitySolution.notes.addedANoteLabel": "メモを追加しました", - "xpack.securitySolution.notes.addNoteBtnLabel": "メモを追加", "xpack.securitySolution.notes.addNoteButtonLabel": "メモを追加", - "xpack.securitySolution.notes.attachToTimelineCheckboxLabel": "アクティブなタイムラインに関連付ける", - "xpack.securitySolution.notes.attachToTimelineInfoLabel": "メモを関連付けるには、アクティブなタイムラインを保存する必要があります", "xpack.securitySolution.notes.cancelButtonLabel": "キャンセル", "xpack.securitySolution.notes.createdByLabel": "作成者", - "xpack.securitySolution.notes.createNoteErrorLabel": "作成エラー", - "xpack.securitySolution.notes.deleteNoteErrorLabel": "メモの削除エラー", - "xpack.securitySolution.notes.deleteNoteLabel": "メモを削除", - "xpack.securitySolution.notes.fetchNotesErrorLabel": "メモの取得エラー", "xpack.securitySolution.notes.management.batchActionsTitle": "一斉アクション", "xpack.securitySolution.notes.management.createdByColumnTitle": "作成者", "xpack.securitySolution.notes.management.createdColumnTitle": "作成済み", @@ -39468,8 +39460,6 @@ "xpack.securitySolution.notes.management.tableError": "メモを読み込めません", "xpack.securitySolution.notes.management.timelineColumnTitle": "Timeline", "xpack.securitySolution.notes.management.viewEventInTimeline": "タイムラインでイベントを表示", - "xpack.securitySolution.notes.markdownAriaLabel": "注", - "xpack.securitySolution.notes.noNotesLabel": "このドキュメントに作成されたメモはありません", "xpack.securitySolution.notes.noteLabel": "注", "xpack.securitySolution.notes.notesTitle": "メモ", "xpack.securitySolution.notes.search.FilterByUserOrNotePlaceholder": "ユーザーまたはメモでフィルター", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index dcc0af6a008c34..67d9fa5e067a0f 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -39487,17 +39487,9 @@ "xpack.securitySolution.noPermissionsTitle": "需要权限", "xpack.securitySolution.noPrivilegesDefaultMessage": "要查看此页面,必须更新权限。有关详细信息,请联系您的 Kibana 管理员。", "xpack.securitySolution.noPrivilegesPerPageMessage": "要查看 {pageName},必须更新权限。有关详细信息,请联系您的 Kibana 管理员。", - "xpack.securitySolution.notes.addedANoteLabel": "已添加备注", - "xpack.securitySolution.notes.addNoteBtnLabel": "添加备注", "xpack.securitySolution.notes.addNoteButtonLabel": "添加备注", - "xpack.securitySolution.notes.attachToTimelineCheckboxLabel": "附加到活动时间线", - "xpack.securitySolution.notes.attachToTimelineInfoLabel": "必须先保存活动时间线,然后才能将备注与其关联起来", "xpack.securitySolution.notes.cancelButtonLabel": "取消", "xpack.securitySolution.notes.createdByLabel": "创建者", - "xpack.securitySolution.notes.createNoteErrorLabel": "创建备注时出错", - "xpack.securitySolution.notes.deleteNoteErrorLabel": "删除备注时出错", - "xpack.securitySolution.notes.deleteNoteLabel": "删除备注", - "xpack.securitySolution.notes.fetchNotesErrorLabel": "提取备注时出错", "xpack.securitySolution.notes.management.batchActionsTitle": "批处理操作", "xpack.securitySolution.notes.management.createdByColumnTitle": "创建者", "xpack.securitySolution.notes.management.createdColumnTitle": "创建时间", @@ -39514,8 +39506,6 @@ "xpack.securitySolution.notes.management.tableError": "无法加载备注", "xpack.securitySolution.notes.management.timelineColumnTitle": "时间线", "xpack.securitySolution.notes.management.viewEventInTimeline": "在时间线中查看事件", - "xpack.securitySolution.notes.markdownAriaLabel": "备注", - "xpack.securitySolution.notes.noNotesLabel": "尚未为此文档创建备注", "xpack.securitySolution.notes.noteLabel": "备注", "xpack.securitySolution.notes.notesTitle": "备注", "xpack.securitySolution.notes.search.FilterByUserOrNotePlaceholder": "按用户或备注筛选",