Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Security Solution][Notes] - add button to add note from flyout note header block #193903

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
EuiIcon,
EuiSpacer,
EuiToolTip,
useEuiTheme,
} from '@elastic/eui';
import { css } from '@emotion/react';
import { useDispatch, useSelector } from 'react-redux';
Expand All @@ -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',
}
Expand All @@ -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('');
Expand Down Expand Up @@ -171,7 +176,7 @@ export const AddNote = memo(({ eventId }: AddNewNoteProps) => {
<EuiIcon
type="iInCircle"
css={css`
margin-left: 4px;
margin-left: ${euiTheme.size.s};
`}
/>
</EuiToolTip>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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',
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ export interface AlertHeaderBlockProps {
export const AlertHeaderBlock: VFC<AlertHeaderBlockProps> = memo(
({ title, children, 'data-test-subj': dataTestSubj }) => (
<EuiPanel hasShadow={false} hasBorder paddingSize="s">
<EuiFlexGroup direction="column" gutterSize="xs" responsive={false}>
<EuiFlexGroup direction="column" gutterSize="xs" responsive={false} alignItems="flexStart">
<EuiFlexItem grow={false}>
<EuiTitle size="xxs" data-test-subj={dataTestSubj}>
<h3>{title}</h3>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => ({
Expand All @@ -33,6 +44,8 @@ jest.mock('react-redux', () => {

describe('<Notes />', () => {
it('should render loading spinner', () => {
(useExpandableFlyoutApi as jest.Mock).mockReturnValue({ openLeftPanel: jest.fn() });

const store = createMockStore({
...mockGlobalState,
notes: {
Expand All @@ -58,7 +71,38 @@ describe('<Notes />', () => {
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(
<TestProviders>
<DocumentDetailsContext.Provider value={mockContextValue}>
<Notes />
</DocumentDetailsContext.Provider>
</TestProviders>
);

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',
Expand All @@ -72,10 +116,23 @@ describe('<Notes />', () => {
</TestProviders>
);

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', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,54 +5,88 @@
* 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 {
fetchNotesByDocumentIds,
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]);

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(() => {
Expand All @@ -76,9 +110,36 @@ export const Notes = memo(() => {
{fetchStatus === ReqStatus.Loading ? (
<EuiLoadingSpinner data-test-subj={NOTES_LOADING_TEST_ID} size="m" />
) : (
<div data-test-subj={NOTES_COUNT_TEST_ID}>
<FormattedCount count={notes.length} />
</div>
<>
{notes.length === 0 ? (
<EuiButtonEmpty
iconType="plusInCircle"
onClick={openExpandedFlyoutNotesTab}
size="s"
aria-label={ADD_NOTE_BUTTON}
data-test-subj={NOTES_ADD_NOTE_BUTTON_TEST_ID}
>
{ADD_NOTE_BUTTON}
</EuiButtonEmpty>
) : (
<EuiFlexGroup responsive={false} alignItems="center" gutterSize="none">
<EuiFlexItem data-test-subj={NOTES_COUNT_TEST_ID}>
<FormattedCount count={notes.length} />
</EuiFlexItem>
<EuiFlexItem>
<EuiButtonIcon
onClick={openExpandedFlyoutNotesTab}
iconType="plusInCircle"
css={css`
margin-left: ${euiTheme.size.xs};
`}
aria-label={ADD_NOTE_BUTTON}
data-test-subj={NOTES_ADD_NOTE_ICON_BUTTON_TEST_ID}
/>
</EuiFlexItem>
</EuiFlexGroup>
)}
</>
)}
</AlertHeaderBlock>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
10 changes: 0 additions & 10 deletions x-pack/plugins/translations/translations/fr-FR.json
Original file line number Diff line number Diff line change
Expand Up @@ -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éé",
Expand All @@ -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",
Expand Down
Loading