[Security Solution][Notes] - add note block to alert defaitls flyout header (#193373)

This commit is contained in:
Philippe Oberti 2024-09-20 02:39:28 +02:00 committed by GitHub
parent ef5aee1cc2
commit 258adf5335
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 465 additions and 101 deletions

View file

@ -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();
});
});

View file

@ -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';

View file

@ -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', () => {

View file

@ -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>
)}
</>
);
});

View file

@ -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('—');
});
});

View file

@ -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>
);
}
);

View file

@ -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,
});
});
});

View file

@ -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';

View file

@ -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>
);
});

View file

@ -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>
);
};

View file

@ -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;

View file

@ -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')

View file

@ -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 */

View file

@ -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'));
});