mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[Security Solution][Notes] - add note block to alert defaitls flyout header (#193373)
This commit is contained in:
parent
ef5aee1cc2
commit
258adf5335
14 changed files with 465 additions and 101 deletions
|
@ -0,0 +1,27 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { render } from '@testing-library/react';
|
||||
import { AlertHeaderBlock } from './alert_header_block';
|
||||
|
||||
const title = <div>{'title'}</div>;
|
||||
const children = <div data-test-subj={'CHILDREN_TEST_ID'}>{'children'}</div>;
|
||||
const dataTestSubj = 'TITLE_TEST_ID';
|
||||
|
||||
describe('<AlertHeaderBlock />', () => {
|
||||
it('should render component', () => {
|
||||
const { getByTestId } = render(
|
||||
<AlertHeaderBlock title={title} data-test-subj={dataTestSubj}>
|
||||
{children}
|
||||
</AlertHeaderBlock>
|
||||
);
|
||||
|
||||
expect(getByTestId('TITLE_TEST_ID')).toHaveTextContent('title');
|
||||
expect(getByTestId('CHILDREN_TEST_ID')).toBeInTheDocument();
|
||||
});
|
||||
});
|
|
@ -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
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { ReactElement, ReactNode, VFC } from 'react';
|
||||
import React, { memo } from 'react';
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiPanel, EuiTitle } from '@elastic/eui';
|
||||
|
||||
export interface AlertHeaderBlockProps {
|
||||
/**
|
||||
* React component to render as the title
|
||||
*/
|
||||
title: ReactElement;
|
||||
/**
|
||||
* React component to render as the value
|
||||
*/
|
||||
children: ReactNode;
|
||||
/**
|
||||
* data-test-subj to use for the title
|
||||
*/
|
||||
['data-test-subj']?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reusable component for rendering a block with rounded edges, show a title and value below one another
|
||||
*/
|
||||
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}>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiTitle size="xxs" data-test-subj={dataTestSubj}>
|
||||
<h3>{title}</h3>
|
||||
</EuiTitle>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>{children}</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiPanel>
|
||||
)
|
||||
);
|
||||
|
||||
AlertHeaderBlock.displayName = 'AlertHeaderBlock';
|
|
@ -13,8 +13,10 @@ import {
|
|||
SEVERITY_VALUE_TEST_ID,
|
||||
FLYOUT_ALERT_HEADER_TITLE_TEST_ID,
|
||||
STATUS_BUTTON_TEST_ID,
|
||||
ASSIGNEES_HEADER_TEST_ID,
|
||||
ALERT_SUMMARY_PANEL_TEST_ID,
|
||||
ASSIGNEES_TEST_ID,
|
||||
ASSIGNEES_EMPTY_TEST_ID,
|
||||
NOTES_TITLE_TEST_ID,
|
||||
} from './test_ids';
|
||||
import { AlertHeaderTitle } from './alert_header_title';
|
||||
import moment from 'moment-timezone';
|
||||
|
@ -22,8 +24,10 @@ import { useDateFormat, useTimeZone } from '../../../../common/lib/kibana';
|
|||
import { mockGetFieldsData } from '../../shared/mocks/mock_get_fields_data';
|
||||
import { mockDataFormattedForFieldBrowser } from '../../shared/mocks/mock_data_formatted_for_field_browser';
|
||||
import { TestProvidersComponent } from '../../../../common/mock';
|
||||
import { useIsExperimentalFeatureEnabled } from '../../../../common/hooks/use_experimental_features';
|
||||
|
||||
jest.mock('../../../../common/lib/kibana');
|
||||
jest.mock('../../../../common/hooks/use_experimental_features');
|
||||
|
||||
moment.suppressDeprecationWarnings = true;
|
||||
moment.tz.setDefault('UTC');
|
||||
|
@ -51,7 +55,7 @@ describe('<AlertHeaderTitle />', () => {
|
|||
});
|
||||
|
||||
it('should render component', () => {
|
||||
const { getByTestId } = renderHeader(mockContextValue);
|
||||
const { getByTestId, queryByTestId } = renderHeader(mockContextValue);
|
||||
|
||||
expect(getByTestId(HEADER_TEXT_TEST_ID)).toHaveTextContent('rule-name');
|
||||
expect(getByTestId(SEVERITY_VALUE_TEST_ID)).toBeInTheDocument();
|
||||
|
@ -59,7 +63,8 @@ describe('<AlertHeaderTitle />', () => {
|
|||
|
||||
expect(getByTestId(RISK_SCORE_VALUE_TEST_ID)).toBeInTheDocument();
|
||||
expect(getByTestId(STATUS_BUTTON_TEST_ID)).toBeInTheDocument();
|
||||
expect(getByTestId(ASSIGNEES_HEADER_TEST_ID)).toBeInTheDocument();
|
||||
expect(getByTestId(ASSIGNEES_TEST_ID)).toBeInTheDocument();
|
||||
expect(queryByTestId(ASSIGNEES_EMPTY_TEST_ID)).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render title correctly if flyout is in preview', () => {
|
||||
|
@ -68,7 +73,15 @@ describe('<AlertHeaderTitle />', () => {
|
|||
|
||||
expect(getByTestId(RISK_SCORE_VALUE_TEST_ID)).toBeInTheDocument();
|
||||
expect(queryByTestId(STATUS_BUTTON_TEST_ID)).not.toBeInTheDocument();
|
||||
expect(getByTestId(ASSIGNEES_HEADER_TEST_ID)).toHaveTextContent('Assignees—');
|
||||
expect(getByTestId(ASSIGNEES_EMPTY_TEST_ID)).toBeInTheDocument();
|
||||
expect(queryByTestId(ASSIGNEES_TEST_ID)).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render notes section if experimental flag is enabled', () => {
|
||||
(useIsExperimentalFeatureEnabled as jest.Mock).mockReturnValue(true);
|
||||
|
||||
const { getByTestId } = renderHeader(mockContextValue);
|
||||
expect(getByTestId(NOTES_TITLE_TEST_ID)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render fall back values if document is not alert', () => {
|
||||
|
|
|
@ -6,11 +6,12 @@
|
|||
*/
|
||||
|
||||
import React, { memo, useCallback, useMemo } from 'react';
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiPanel, useEuiTheme, EuiLink } from '@elastic/eui';
|
||||
import { css } from '@emotion/css';
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiLink } from '@elastic/eui';
|
||||
import { ALERT_WORKFLOW_ASSIGNEE_IDS } from '@kbn/rule-data-utils';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FlyoutTitle } from '@kbn/security-solution-common';
|
||||
import { useIsExperimentalFeatureEnabled } from '../../../../common/hooks/use_experimental_features';
|
||||
import { Notes } from './notes';
|
||||
import { useRuleDetailsLink } from '../../shared/hooks/use_rule_details_link';
|
||||
import { DocumentStatus } from './status';
|
||||
import { DocumentSeverity } from './severity';
|
||||
|
@ -22,6 +23,11 @@ import { PreferenceFormattedDate } from '../../../../common/components/formatted
|
|||
import { FLYOUT_ALERT_HEADER_TITLE_TEST_ID, ALERT_SUMMARY_PANEL_TEST_ID } from './test_ids';
|
||||
import { Assignees } from './assignees';
|
||||
|
||||
// minWidth for each block, allows to switch for a 1 row 4 blocks to 2 rows with 2 block each
|
||||
const blockStyles = {
|
||||
minWidth: 280,
|
||||
};
|
||||
|
||||
/**
|
||||
* Alert details flyout right section header
|
||||
*/
|
||||
|
@ -34,10 +40,13 @@ export const AlertHeaderTitle = memo(() => {
|
|||
refetchFlyoutData,
|
||||
getFieldsData,
|
||||
} = useDocumentDetailsContext();
|
||||
const securitySolutionNotesEnabled = useIsExperimentalFeatureEnabled(
|
||||
'securitySolutionNotesEnabled'
|
||||
);
|
||||
|
||||
const { isAlert, ruleName, timestamp, ruleId } = useBasicDataFromDetailsData(
|
||||
dataFormattedForFieldBrowser
|
||||
);
|
||||
const { euiTheme } = useEuiTheme();
|
||||
|
||||
const href = useRuleDetailsLink({ ruleId: !isPreview ? ruleId : null });
|
||||
const ruleTitle = useMemo(
|
||||
|
@ -89,27 +98,52 @@ export const AlertHeaderTitle = memo(() => {
|
|||
/>
|
||||
)}
|
||||
<EuiSpacer size="m" />
|
||||
<EuiPanel
|
||||
hasShadow={false}
|
||||
hasBorder
|
||||
css={css`
|
||||
padding: ${euiTheme.size.m} ${euiTheme.size.s};
|
||||
`}
|
||||
data-test-subj={ALERT_SUMMARY_PANEL_TEST_ID}
|
||||
>
|
||||
<EuiFlexGroup direction="row" gutterSize="m" responsive={false}>
|
||||
<EuiFlexItem
|
||||
css={css`
|
||||
border-right: ${euiTheme.border.thin};
|
||||
`}
|
||||
>
|
||||
{securitySolutionNotesEnabled ? (
|
||||
<EuiFlexGroup
|
||||
direction="row"
|
||||
gutterSize="s"
|
||||
responsive={false}
|
||||
wrap
|
||||
data-test-subj={ALERT_SUMMARY_PANEL_TEST_ID}
|
||||
>
|
||||
<EuiFlexItem style={blockStyles}>
|
||||
<EuiFlexGroup direction="row" gutterSize="s" responsive={false}>
|
||||
<EuiFlexItem>
|
||||
<DocumentStatus />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<RiskScore />
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem style={blockStyles}>
|
||||
<EuiFlexGroup direction="row" gutterSize="s" responsive={false}>
|
||||
<EuiFlexItem>
|
||||
<Assignees
|
||||
eventId={eventId}
|
||||
assignedUserIds={alertAssignees}
|
||||
onAssigneesUpdated={onAssigneesUpdated}
|
||||
isPreview={isPreview}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<Notes />
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
) : (
|
||||
<EuiFlexGroup
|
||||
direction="row"
|
||||
gutterSize="s"
|
||||
responsive={false}
|
||||
wrap
|
||||
data-test-subj={ALERT_SUMMARY_PANEL_TEST_ID}
|
||||
>
|
||||
<EuiFlexItem>
|
||||
<DocumentStatus />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem
|
||||
css={css`
|
||||
border-right: ${euiTheme.border.thin};
|
||||
`}
|
||||
>
|
||||
<EuiFlexItem>
|
||||
<RiskScore />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
|
@ -121,7 +155,7 @@ export const AlertHeaderTitle = memo(() => {
|
|||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiPanel>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
|
|
@ -10,8 +10,8 @@ import { render } from '@testing-library/react';
|
|||
|
||||
import {
|
||||
ASSIGNEES_ADD_BUTTON_TEST_ID,
|
||||
ASSIGNEES_EMPTY_TEST_ID,
|
||||
ASSIGNEES_TITLE_TEST_ID,
|
||||
ASSIGNEES_HEADER_TEST_ID,
|
||||
} from './test_ids';
|
||||
import { Assignees } from './assignees';
|
||||
|
||||
|
@ -180,6 +180,6 @@ describe('<Assignees />', () => {
|
|||
);
|
||||
|
||||
expect(queryByTestId(ASSIGNEES_ADD_BUTTON_TEST_ID)).not.toBeInTheDocument();
|
||||
expect(getByTestId(ASSIGNEES_HEADER_TEST_ID)).toHaveTextContent('Assignees—');
|
||||
expect(getByTestId(ASSIGNEES_EMPTY_TEST_ID)).toHaveTextContent('—');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -14,12 +14,12 @@ import {
|
|||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiPopover,
|
||||
EuiTitle,
|
||||
EuiToolTip,
|
||||
useGeneratedHtmlId,
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { AlertHeaderBlock } from './alert_header_block';
|
||||
import { useSetAlertAssignees } from '../../../../common/components/toolbar/bulk_actions/use_set_alert_assignees';
|
||||
import { getEmptyTagValue } from '../../../../common/components/empty_value';
|
||||
import { ASSIGNEES_PANEL_WIDTH } from '../../../../common/components/assignees/constants';
|
||||
|
@ -32,7 +32,8 @@ import { useBulkGetUserProfiles } from '../../../../common/components/user_profi
|
|||
import { UsersAvatarsPanel } from '../../../../common/components/user_profiles/users_avatars_panel';
|
||||
import {
|
||||
ASSIGNEES_ADD_BUTTON_TEST_ID,
|
||||
ASSIGNEES_HEADER_TEST_ID,
|
||||
ASSIGNEES_EMPTY_TEST_ID,
|
||||
ASSIGNEES_TEST_ID,
|
||||
ASSIGNEES_TITLE_TEST_ID,
|
||||
} from './test_ids';
|
||||
|
||||
|
@ -158,26 +159,19 @@ export const Assignees: FC<AssigneesProps> = memo(
|
|||
]);
|
||||
|
||||
return (
|
||||
<EuiFlexGroup
|
||||
data-test-subj={ASSIGNEES_HEADER_TEST_ID}
|
||||
direction="column"
|
||||
gutterSize="none"
|
||||
responsive={false}
|
||||
<AlertHeaderBlock
|
||||
title={
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.flyout.right.header.assignedTitle"
|
||||
defaultMessage="Assignees"
|
||||
/>
|
||||
}
|
||||
data-test-subj={ASSIGNEES_TITLE_TEST_ID}
|
||||
>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiTitle size="xxs" data-test-subj={ASSIGNEES_TITLE_TEST_ID}>
|
||||
<h3>
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.flyout.right.header.assignedTitle"
|
||||
defaultMessage="Assignees"
|
||||
/>
|
||||
</h3>
|
||||
</EuiTitle>
|
||||
</EuiFlexItem>
|
||||
{isPreview ? (
|
||||
getEmptyTagValue()
|
||||
<div data-test-subj={ASSIGNEES_EMPTY_TEST_ID}>{getEmptyTagValue()}</div>
|
||||
) : (
|
||||
<EuiFlexGroup gutterSize="none" responsive={false}>
|
||||
<EuiFlexGroup gutterSize="none" responsive={false} data-test-subj={ASSIGNEES_TEST_ID}>
|
||||
{assignedUsers && (
|
||||
<EuiFlexItem grow={false}>
|
||||
<UsersAvatarsPanel userProfiles={assignedUsers} maxVisibleAvatars={2} />
|
||||
|
@ -186,7 +180,7 @@ export const Assignees: FC<AssigneesProps> = memo(
|
|||
<EuiFlexItem grow={false}>{updateAssigneesPopover}</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
)}
|
||||
</EuiFlexGroup>
|
||||
</AlertHeaderBlock>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
|
|
@ -0,0 +1,154 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
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 { 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';
|
||||
|
||||
const mockAddError = jest.fn();
|
||||
jest.mock('../../../../common/hooks/use_app_toasts', () => ({
|
||||
useAppToasts: () => ({
|
||||
addError: mockAddError,
|
||||
}),
|
||||
}));
|
||||
|
||||
const mockDispatch = jest.fn();
|
||||
jest.mock('react-redux', () => {
|
||||
const original = jest.requireActual('react-redux');
|
||||
return {
|
||||
...original,
|
||||
useDispatch: () => mockDispatch,
|
||||
};
|
||||
});
|
||||
|
||||
describe('<Notes />', () => {
|
||||
it('should render loading spinner', () => {
|
||||
const store = createMockStore({
|
||||
...mockGlobalState,
|
||||
notes: {
|
||||
...mockGlobalState.notes,
|
||||
status: {
|
||||
...mockGlobalState.notes.status,
|
||||
fetchNotesByDocumentIds: ReqStatus.Loading,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const { getByTestId } = render(
|
||||
<TestProviders store={store}>
|
||||
<DocumentDetailsContext.Provider value={mockContextValue}>
|
||||
<Notes />
|
||||
</DocumentDetailsContext.Provider>
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
expect(mockDispatch).toHaveBeenCalled();
|
||||
expect(getByTestId(NOTES_TITLE_TEST_ID)).toBeInTheDocument();
|
||||
expect(getByTestId(NOTES_TITLE_TEST_ID)).toHaveTextContent('Notes');
|
||||
expect(getByTestId(NOTES_LOADING_TEST_ID)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render number of notes', () => {
|
||||
const contextValue = {
|
||||
...mockContextValue,
|
||||
eventId: '1',
|
||||
};
|
||||
|
||||
const { getByTestId } = render(
|
||||
<TestProviders>
|
||||
<DocumentDetailsContext.Provider value={contextValue}>
|
||||
<Notes />
|
||||
</DocumentDetailsContext.Provider>
|
||||
</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');
|
||||
});
|
||||
|
||||
it('should render number of notes in scientific notation for big numbers', () => {
|
||||
const createMockNote = (noteId: string): Note => ({
|
||||
eventId: '1', // should be a valid id based on mockTimelineData
|
||||
noteId,
|
||||
note: 'note-1',
|
||||
timelineId: 'timeline-1',
|
||||
created: 1663882629000,
|
||||
createdBy: 'elastic',
|
||||
updated: 1663882629000,
|
||||
updatedBy: 'elastic',
|
||||
version: 'version',
|
||||
});
|
||||
const mockEntities = [...Array(1000).keys()]
|
||||
.map((i: number) => createMockNote(i.toString()))
|
||||
.reduce((acc, entity) => {
|
||||
// @ts-ignore
|
||||
acc[entity.noteId] = entity;
|
||||
return acc;
|
||||
}, {});
|
||||
const mockIds = Object.keys(mockEntities);
|
||||
|
||||
const store = createMockStore({
|
||||
...mockGlobalState,
|
||||
notes: {
|
||||
...mockGlobalState.notes,
|
||||
entities: mockEntities,
|
||||
ids: mockIds,
|
||||
},
|
||||
});
|
||||
const contextValue = {
|
||||
...mockContextValue,
|
||||
eventId: '1',
|
||||
};
|
||||
|
||||
const { getByTestId } = render(
|
||||
<TestProviders store={store}>
|
||||
<DocumentDetailsContext.Provider value={contextValue}>
|
||||
<Notes />
|
||||
</DocumentDetailsContext.Provider>
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
expect(getByTestId(NOTES_COUNT_TEST_ID)).toHaveTextContent('1k');
|
||||
});
|
||||
|
||||
it('should render toast error', () => {
|
||||
const store = createMockStore({
|
||||
...mockGlobalState,
|
||||
notes: {
|
||||
...mockGlobalState.notes,
|
||||
status: {
|
||||
...mockGlobalState.notes.status,
|
||||
fetchNotesByDocumentIds: ReqStatus.Failed,
|
||||
},
|
||||
error: {
|
||||
...mockGlobalState.notes.error,
|
||||
fetchNotesByDocumentIds: { type: 'http', status: 500 },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
render(
|
||||
<TestProviders store={store}>
|
||||
<DocumentDetailsContext.Provider value={mockContextValue}>
|
||||
<Notes />
|
||||
</DocumentDetailsContext.Provider>
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
expect(mockAddError).toHaveBeenCalledWith(null, {
|
||||
title: FETCH_NOTES_ERROR,
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,87 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { memo, 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 { 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 type { State } from '../../../../common/store';
|
||||
import type { Note } from '../../../../../common/api/timeline';
|
||||
import {
|
||||
fetchNotesByDocumentIds,
|
||||
ReqStatus,
|
||||
selectFetchNotesByDocumentIdsError,
|
||||
selectFetchNotesByDocumentIdsStatus,
|
||||
selectSortedNotesByDocumentId,
|
||||
} from '../../../../notes/store/notes.slice';
|
||||
import { useAppToasts } from '../../../../common/hooks/use_app_toasts';
|
||||
import { AlertHeaderBlock } from './alert_header_block';
|
||||
|
||||
export const FETCH_NOTES_ERROR = i18n.translate(
|
||||
'xpack.securitySolution.notes.fetchNotesErrorLabel',
|
||||
{
|
||||
defaultMessage: 'Error fetching notes',
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Renders a block with the number of notes for the event
|
||||
*/
|
||||
export const Notes = memo(() => {
|
||||
const dispatch = useDispatch();
|
||||
const { eventId } = useDocumentDetailsContext();
|
||||
const { addError: addErrorToast } = useAppToasts();
|
||||
|
||||
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' },
|
||||
})
|
||||
);
|
||||
|
||||
// show a toast if the fetch notes call fails
|
||||
useEffect(() => {
|
||||
if (fetchStatus === ReqStatus.Failed && fetchError) {
|
||||
addErrorToast(null, {
|
||||
title: FETCH_NOTES_ERROR,
|
||||
});
|
||||
}
|
||||
}, [addErrorToast, fetchError, fetchStatus]);
|
||||
|
||||
return (
|
||||
<AlertHeaderBlock
|
||||
title={
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.flyout.right.header.notesTitle"
|
||||
defaultMessage="Notes"
|
||||
/>
|
||||
}
|
||||
data-test-subj={NOTES_TITLE_TEST_ID}
|
||||
>
|
||||
{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>
|
||||
)}
|
||||
</AlertHeaderBlock>
|
||||
);
|
||||
});
|
||||
|
||||
Notes.displayName = 'Notes';
|
|
@ -6,9 +6,9 @@
|
|||
*/
|
||||
|
||||
import React, { memo } from 'react';
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiTitle } from '@elastic/eui';
|
||||
import { ALERT_RISK_SCORE } from '@kbn/rule-data-utils';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { AlertHeaderBlock } from './alert_header_block';
|
||||
import { RISK_SCORE_TITLE_TEST_ID, RISK_SCORE_VALUE_TEST_ID } from './test_ids';
|
||||
import { useDocumentDetailsContext } from '../../shared/context';
|
||||
|
||||
|
@ -33,21 +33,17 @@ export const RiskScore = memo(() => {
|
|||
}
|
||||
|
||||
return (
|
||||
<EuiFlexGroup direction="column" gutterSize="xs" responsive={false}>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiTitle size="xxs" data-test-subj={RISK_SCORE_TITLE_TEST_ID}>
|
||||
<h3>
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.flyout.right.header.riskScoreTitle"
|
||||
defaultMessage="Risk score"
|
||||
/>
|
||||
</h3>
|
||||
</EuiTitle>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<span data-test-subj={RISK_SCORE_VALUE_TEST_ID}>{alertRiskScore}</span>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<AlertHeaderBlock
|
||||
title={
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.flyout.right.header.riskScoreTitle"
|
||||
defaultMessage="Risk score"
|
||||
/>
|
||||
}
|
||||
data-test-subj={RISK_SCORE_TITLE_TEST_ID}
|
||||
>
|
||||
<span data-test-subj={RISK_SCORE_VALUE_TEST_ID}>{alertRiskScore}</span>
|
||||
</AlertHeaderBlock>
|
||||
);
|
||||
});
|
||||
|
||||
|
|
|
@ -9,7 +9,7 @@ import type { FC } from 'react';
|
|||
import React, { useMemo } from 'react';
|
||||
import { find } from 'lodash/fp';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiTitle } from '@elastic/eui';
|
||||
import { AlertHeaderBlock } from './alert_header_block';
|
||||
import { getEmptyTagValue } from '../../../../common/components/empty_value';
|
||||
import { SIGNAL_STATUS_FIELD_NAME } from '../../../../timelines/components/timeline/body/renderers/constants';
|
||||
import { StatusPopoverButton } from './status_popover_button';
|
||||
|
@ -51,32 +51,28 @@ export const DocumentStatus: FC = () => {
|
|||
}, [browserFields, dataFormattedForFieldBrowser, eventId, scopeId]);
|
||||
|
||||
return (
|
||||
<EuiFlexGroup direction="column" gutterSize="xs" responsive={false}>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiTitle size="xxs" data-test-subj={STATUS_TITLE_TEST_ID}>
|
||||
<h3>
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.flyout.right.header.statusTitle"
|
||||
defaultMessage="Status"
|
||||
/>
|
||||
</h3>
|
||||
</EuiTitle>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
{!statusData || !hasData(statusData) || isPreview ? (
|
||||
getEmptyTagValue()
|
||||
) : (
|
||||
<CellActions field={SIGNAL_STATUS_FIELD_NAME} value={statusData.values[0]}>
|
||||
<StatusPopoverButton
|
||||
eventId={eventId}
|
||||
contextId={scopeId}
|
||||
enrichedFieldInfo={statusData}
|
||||
scopeId={scopeId}
|
||||
/>
|
||||
</CellActions>
|
||||
)}
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<AlertHeaderBlock
|
||||
title={
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.flyout.right.header.statusTitle"
|
||||
defaultMessage="Status"
|
||||
/>
|
||||
}
|
||||
data-test-subj={STATUS_TITLE_TEST_ID}
|
||||
>
|
||||
{!statusData || !hasData(statusData) || isPreview ? (
|
||||
getEmptyTagValue()
|
||||
) : (
|
||||
<CellActions field={SIGNAL_STATUS_FIELD_NAME} value={statusData.values[0]}>
|
||||
<StatusPopoverButton
|
||||
eventId={eventId}
|
||||
contextId={scopeId}
|
||||
enrichedFieldInfo={statusData}
|
||||
scopeId={scopeId}
|
||||
/>
|
||||
</CellActions>
|
||||
)}
|
||||
</AlertHeaderBlock>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -33,7 +33,12 @@ export const RISK_SCORE_VALUE_TEST_ID = `${FLYOUT_HEADER_TEST_ID}RiskScoreValue`
|
|||
export const SHARE_BUTTON_TEST_ID = `${FLYOUT_HEADER_TEST_ID}ShareButton` as const;
|
||||
export const CHAT_BUTTON_TEST_ID = 'newChatByTitle' as const;
|
||||
|
||||
export const ASSIGNEES_HEADER_TEST_ID = `${FLYOUT_HEADER_TEST_ID}AssigneesHeader` as const;
|
||||
export const NOTES_TITLE_TEST_ID = `${FLYOUT_HEADER_TEST_ID}NotesTitle` 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;
|
||||
|
||||
export const ASSIGNEES_EMPTY_TEST_ID = `${FLYOUT_HEADER_TEST_ID}AssigneesEmpty` as const;
|
||||
export const ASSIGNEES_TEST_ID = `${FLYOUT_HEADER_TEST_ID}Assignees` as const;
|
||||
export const ASSIGNEES_TITLE_TEST_ID = `${FLYOUT_HEADER_TEST_ID}AssigneesTitle` as const;
|
||||
export const ASSIGNEES_ADD_BUTTON_TEST_ID = `${FLYOUT_HEADER_TEST_ID}AssigneesAddButton` as const;
|
||||
|
||||
|
|
|
@ -34,7 +34,6 @@ import {
|
|||
DOCUMENT_DETAILS_FLYOUT_HEADER_LINK_ICON,
|
||||
DOCUMENT_DETAILS_FLYOUT_HEADER_RISK_SCORE,
|
||||
DOCUMENT_DETAILS_FLYOUT_HEADER_RISK_SCORE_VALUE,
|
||||
DOCUMENT_DETAILS_FLYOUT_HEADER_ASSIGNEES,
|
||||
DOCUMENT_DETAILS_FLYOUT_HEADER_SEVERITY_VALUE,
|
||||
DOCUMENT_DETAILS_FLYOUT_HEADER_STATUS,
|
||||
DOCUMENT_DETAILS_FLYOUT_HEADER_TITLE,
|
||||
|
@ -42,6 +41,7 @@ import {
|
|||
DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB,
|
||||
DOCUMENT_DETAILS_FLYOUT_TABLE_TAB,
|
||||
DOCUMENT_DETAILS_FLYOUT_FOOTER_ISOLATE_HOST,
|
||||
DOCUMENT_DETAILS_FLYOUT_HEADER_ASSIGNEES_TITLE,
|
||||
} from '../../../../screens/expandable_flyout/alert_details_right_panel';
|
||||
import {
|
||||
closeFlyout,
|
||||
|
@ -93,7 +93,11 @@ describe('Alert details expandable flyout right panel', { tags: ['@ess', '@serve
|
|||
.should('be.visible')
|
||||
.and('have.text', rule.risk_score);
|
||||
|
||||
cy.get(DOCUMENT_DETAILS_FLYOUT_HEADER_ASSIGNEES).should('have.text', 'Assignees');
|
||||
cy.get(DOCUMENT_DETAILS_FLYOUT_HEADER_ASSIGNEES_TITLE).should('have.text', 'Assignees');
|
||||
|
||||
// TODO uncomment when the securitySolutionNotesEnabled feature flag is removed
|
||||
// cy.get(DOCUMENT_DETAILS_FLYOUT_HEADER_NOTES_TITLE).should('have.text', 'Notes');
|
||||
// cy.get(DOCUMENT_DETAILS_FLYOUT_HEADER_NOTES_VALUE).should('have.text', '0');
|
||||
|
||||
cy.get(DOCUMENT_DETAILS_FLYOUT_HEADER_SEVERITY_VALUE)
|
||||
.should('be.visible')
|
||||
|
|
|
@ -48,8 +48,17 @@ export const DOCUMENT_DETAILS_FLYOUT_HEADER_RISK_SCORE_VALUE = getDataTestSubjec
|
|||
'securitySolutionFlyoutHeaderRiskScoreValue'
|
||||
);
|
||||
export const DOCUMENT_DETAILS_FLYOUT_HEADER_SEVERITY_VALUE = getDataTestSubjectSelector('severity');
|
||||
export const DOCUMENT_DETAILS_FLYOUT_HEADER_ASSIGNEES = getDataTestSubjectSelector(
|
||||
'securitySolutionFlyoutHeaderAssigneesHeader'
|
||||
export const DOCUMENT_DETAILS_FLYOUT_HEADER_ASSIGNEES_TITLE = getDataTestSubjectSelector(
|
||||
'securitySolutionFlyoutHeaderAssigneesTitle'
|
||||
);
|
||||
export const DOCUMENT_DETAILS_FLYOUT_HEADER_ASSIGNEES_VALUE = getDataTestSubjectSelector(
|
||||
'securitySolutionFlyoutHeaderAssignees'
|
||||
);
|
||||
export const DOCUMENT_DETAILS_FLYOUT_HEADER_NOTES_TITLE = getDataTestSubjectSelector(
|
||||
'securitySolutionFlyoutHeaderAssigneesTitle'
|
||||
);
|
||||
export const DOCUMENT_DETAILS_FLYOUT_HEADER_NOTES_VALUE = getDataTestSubjectSelector(
|
||||
'securitySolutionFlyoutHeaderAssigneesValue'
|
||||
);
|
||||
|
||||
/* Footer */
|
||||
|
|
|
@ -27,7 +27,7 @@ import {
|
|||
ALERT_ASSIGNEES_SELECTABLE_OPTIONS,
|
||||
} from '../screens/alerts';
|
||||
import { PAGE_TITLE } from '../screens/common/page';
|
||||
import { DOCUMENT_DETAILS_FLYOUT_HEADER_ASSIGNEES } from '../screens/expandable_flyout/alert_details_right_panel';
|
||||
import { DOCUMENT_DETAILS_FLYOUT_HEADER_ASSIGNEES_VALUE } from '../screens/expandable_flyout/alert_details_right_panel';
|
||||
import { expandFirstAlertActions, selectFirstPageAlerts } from './alerts';
|
||||
import { login } from './login';
|
||||
import { visitWithTimeRange } from './navigation';
|
||||
|
@ -85,7 +85,7 @@ export const checkEmptyAssigneesStateInAlertsTable = () => {
|
|||
};
|
||||
|
||||
export const checkEmptyAssigneesStateInAlertDetailsFlyout = () => {
|
||||
cy.get(DOCUMENT_DETAILS_FLYOUT_HEADER_ASSIGNEES).within(() => {
|
||||
cy.get(DOCUMENT_DETAILS_FLYOUT_HEADER_ASSIGNEES_VALUE).within(() => {
|
||||
cy.get(ALERT_AVATARS_PANEL).children().should('have.length', 0);
|
||||
});
|
||||
};
|
||||
|
@ -137,13 +137,13 @@ export const alertsTableShowsAssigneesBadgeForFirstAlert = (users: string[]) =>
|
|||
};
|
||||
|
||||
export const alertDetailsFlyoutShowsAssignees = (users: string[]) => {
|
||||
cy.get(DOCUMENT_DETAILS_FLYOUT_HEADER_ASSIGNEES).within(() => {
|
||||
cy.get(DOCUMENT_DETAILS_FLYOUT_HEADER_ASSIGNEES_VALUE).within(() => {
|
||||
users.forEach((user) => cy.get(`.euiAvatar${ALERT_USER_AVATAR(user)}`).should('exist'));
|
||||
});
|
||||
};
|
||||
|
||||
export const alertDetailsFlyoutShowsAssigneesBadge = (users: string[]) => {
|
||||
cy.get(DOCUMENT_DETAILS_FLYOUT_HEADER_ASSIGNEES).within(() => {
|
||||
cy.get(DOCUMENT_DETAILS_FLYOUT_HEADER_ASSIGNEES_VALUE).within(() => {
|
||||
cy.get(ALERT_ASSIGNEES_COUNT_BADGE).contains(users.length);
|
||||
users.forEach((user) => cy.get(`.euiAvatar${ALERT_USER_AVATAR(user)}`).should('not.exist'));
|
||||
});
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue