[Security Solution] Expandable flyout - clean up loading states and no data messages (#166939)

## Summary

This PR cleaned up the loading and empty state of most components in the
new expandable alerts flyout:

1) Refactor `FlyoutLoading` component to be used across components
2) Updated unit tests to explicitly check no data messages
3) Changed loading spinner in smaller components to be skeleton text
4) Moved error/no data message to table
5) Added loading and/or error messages for:
1) Right panel -> about -> Rule description, note that rule preview is
disabled if no rule is found

![image](11cb7926-ff86-4deb-a9d1-993463fad466)

2) Right panel -> about -> Alert reason, note that the alert reason
preview is disabled if no reason available

![image](9674d81a-ca8b-4fea-981a-23c2cf3f3bfc)

   3) Right panel -> Investigation -> Highlighted fields

![image](d349251a-a027-409d-b94d-2792abe6543c)

   4) Right panel -> Insights -> host and user preview

![image](161340aa-4ca1-4205-9628-87206743f776)

5) Right panel -> Visualization -> analyzer preview (to match error in
analyzer graph)

![image](73b0018d-1f97-4fa3-be4d-b55afeb047b7)

6) Right panel -> About -> show rule summary & show alert reason when
there is an error
- Very uncommon because the button should be disabled if data is not
available, adding as fall back

![image](5088e9f8-bdd1-4e61-8878-dba756b44c32)



### Checklist

- [x] Any text added follows [EUI's writing
guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses
sentence case text and includes [i18n
support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md)
- [x] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios
This commit is contained in:
christineweng 2023-09-27 15:19:25 -05:00 committed by GitHub
parent bb5439f4e1
commit 7b4e6a6775
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
51 changed files with 700 additions and 621 deletions

View file

@ -20,7 +20,6 @@ import {
CORRELATIONS_DETAILS_BY_SESSION_SECTION_TABLE_TEST_ID,
CORRELATIONS_DETAILS_BY_SOURCE_SECTION_TABLE_TEST_ID,
CORRELATIONS_DETAILS_CASES_SECTION_TABLE_TEST_ID,
CORRELATIONS_DETAILS_NO_DATA_TEST_ID,
CORRELATIONS_DETAILS_SUPPRESSED_ALERTS_SECTION_TEST_ID,
} from './test_ids';
import { useFetchRelatedAlertsBySession } from '../../shared/hooks/use_fetch_related_alerts_by_session';
@ -53,12 +52,13 @@ const renderCorrelationDetails = () => {
</TestProviders>
);
};
const CORRELATIONS_DETAILS_SUPPRESSED_ALERTS_TITLE_TEST_ID =
EXPANDABLE_PANEL_HEADER_TITLE_TEXT_TEST_ID(
CORRELATIONS_DETAILS_SUPPRESSED_ALERTS_SECTION_TEST_ID
);
const NO_DATA_MESSAGE = 'No correlations data available.';
describe('CorrelationsDetails', () => {
beforeEach(() => {
jest.clearAllMocks();
@ -102,14 +102,14 @@ describe('CorrelationsDetails', () => {
dataCount: 1,
});
const { getByTestId, queryByTestId } = renderCorrelationDetails();
const { getByTestId, queryByText } = renderCorrelationDetails();
expect(getByTestId(CORRELATIONS_DETAILS_BY_ANCESTRY_SECTION_TABLE_TEST_ID)).toBeInTheDocument();
expect(getByTestId(CORRELATIONS_DETAILS_BY_SOURCE_SECTION_TABLE_TEST_ID)).toBeInTheDocument();
expect(getByTestId(CORRELATIONS_DETAILS_BY_SESSION_SECTION_TABLE_TEST_ID)).toBeInTheDocument();
expect(getByTestId(CORRELATIONS_DETAILS_CASES_SECTION_TABLE_TEST_ID)).toBeInTheDocument();
expect(getByTestId(CORRELATIONS_DETAILS_SUPPRESSED_ALERTS_TITLE_TEST_ID)).toBeInTheDocument();
expect(queryByTestId(CORRELATIONS_DETAILS_NO_DATA_TEST_ID)).not.toBeInTheDocument();
expect(queryByText(NO_DATA_MESSAGE)).not.toBeInTheDocument();
});
it('should render no section and show error message if show values are false', () => {
@ -125,7 +125,7 @@ describe('CorrelationsDetails', () => {
jest.mocked(useShowRelatedCases).mockReturnValue(false);
jest.mocked(useShowSuppressedAlerts).mockReturnValue({ show: false, alertSuppressionCount: 0 });
const { getByTestId, queryByTestId } = renderCorrelationDetails();
const { getByText, queryByTestId } = renderCorrelationDetails();
expect(
queryByTestId(CORRELATIONS_DETAILS_BY_ANCESTRY_SECTION_TABLE_TEST_ID)
@ -140,10 +140,7 @@ describe('CorrelationsDetails', () => {
expect(
queryByTestId(CORRELATIONS_DETAILS_SUPPRESSED_ALERTS_TITLE_TEST_ID)
).not.toBeInTheDocument();
expect(getByTestId(CORRELATIONS_DETAILS_NO_DATA_TEST_ID)).toBeInTheDocument();
expect(getByTestId(CORRELATIONS_DETAILS_NO_DATA_TEST_ID)).toHaveTextContent(
'No correlations data available.'
);
expect(getByText(NO_DATA_MESSAGE)).toBeInTheDocument();
});
it('should render no section if values are null', () => {

View file

@ -6,9 +6,9 @@
*/
import React from 'react';
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import { EuiPanel, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import { CORRELATIONS_DETAILS_NO_DATA_TEST_ID } from './test_ids';
import { CORRELATIONS_DETAILS_TEST_ID } from './test_ids';
import { RelatedAlertsBySession } from './related_alerts_by_session';
import { RelatedAlertsBySameSourceEvent } from './related_alerts_by_same_source_event';
import { RelatedCases } from './related_cases';
@ -56,7 +56,7 @@ export const CorrelationsDetails: React.FC = () => {
showSuppressedAlerts;
return (
<>
<EuiPanel paddingSize="none" data-test-subj={CORRELATIONS_DETAILS_TEST_ID} color="transparent">
{canShowAtLeastOneInsight ? (
<EuiFlexGroup gutterSize="l" direction="column">
{showSuppressedAlerts && (
@ -98,14 +98,12 @@ export const CorrelationsDetails: React.FC = () => {
)}
</EuiFlexGroup>
) : (
<p data-test-subj={CORRELATIONS_DETAILS_NO_DATA_TEST_ID}>
<FormattedMessage
id="xpack.securitySolution.flyout.left.insights.correlations.noDataDescription"
defaultMessage="No correlations data available."
/>
</p>
<FormattedMessage
id="xpack.securitySolution.flyout.left.insights.correlations.noDataDescription"
defaultMessage="No correlations data available."
/>
)}
</>
</EuiPanel>
);
};

View file

@ -11,12 +11,7 @@ import '@testing-library/jest-dom';
import { LeftPanelContext } from '../context';
import { TestProviders } from '../../../common/mock';
import { EntitiesDetails } from './entities_details';
import {
ENTITIES_DETAILS_NO_DATA_TEST_ID,
ENTITIES_DETAILS_TEST_ID,
HOST_DETAILS_TEST_ID,
USER_DETAILS_TEST_ID,
} from './test_ids';
import { ENTITIES_DETAILS_TEST_ID, HOST_DETAILS_TEST_ID, USER_DETAILS_TEST_ID } from './test_ids';
import { mockContextValue } from '../mocks/mock_context';
import { EXPANDABLE_PANEL_CONTENT_TEST_ID } from '../../shared/components/test_ids';
@ -40,6 +35,8 @@ jest.mock('react-redux', () => {
const USER_TEST_ID = EXPANDABLE_PANEL_CONTENT_TEST_ID(USER_DETAILS_TEST_ID);
const HOST_TEST_ID = EXPANDABLE_PANEL_CONTENT_TEST_ID(HOST_DETAILS_TEST_ID);
const NO_DATA_MESSAGE = 'Host and user information are unavailable for this alert.';
const renderEntitiesDetails = (contextValue: LeftPanelContext) =>
render(
<TestProviders>
@ -51,11 +48,11 @@ const renderEntitiesDetails = (contextValue: LeftPanelContext) =>
describe('<EntitiesDetails />', () => {
it('renders entities details correctly', () => {
const { getByTestId, queryByTestId } = renderEntitiesDetails(mockContextValue);
const { getByTestId, queryByText } = renderEntitiesDetails(mockContextValue);
expect(getByTestId(ENTITIES_DETAILS_TEST_ID)).toBeInTheDocument();
expect(getByTestId(USER_TEST_ID)).toBeInTheDocument();
expect(getByTestId(HOST_TEST_ID)).toBeInTheDocument();
expect(queryByTestId(ENTITIES_DETAILS_NO_DATA_TEST_ID)).not.toBeInTheDocument();
expect(queryByText(NO_DATA_MESSAGE)).not.toBeInTheDocument();
});
it('should render no data message if user name and host name are not available', () => {
@ -64,11 +61,8 @@ describe('<EntitiesDetails />', () => {
getFieldsData: (fieldName: string) =>
fieldName === '@timestamp' ? ['2022-07-25T08:20:18.966Z'] : [],
};
const { getByTestId, queryByTestId } = renderEntitiesDetails(contextValue);
expect(getByTestId(ENTITIES_DETAILS_NO_DATA_TEST_ID)).toBeInTheDocument();
expect(getByTestId(ENTITIES_DETAILS_NO_DATA_TEST_ID)).toHaveTextContent(
'Host and user information are unavailable for this alert.'
);
const { getByText, queryByTestId } = renderEntitiesDetails(contextValue);
expect(getByText(NO_DATA_MESSAGE)).toBeInTheDocument();
expect(queryByTestId(USER_TEST_ID)).not.toBeInTheDocument();
expect(queryByTestId(HOST_TEST_ID)).not.toBeInTheDocument();
});
@ -87,11 +81,8 @@ describe('<EntitiesDetails />', () => {
}
},
};
const { getByTestId, queryByTestId } = renderEntitiesDetails(contextValue);
expect(getByTestId(ENTITIES_DETAILS_NO_DATA_TEST_ID)).toBeInTheDocument();
expect(getByTestId(ENTITIES_DETAILS_NO_DATA_TEST_ID)).toHaveTextContent(
'Host and user information are unavailable for this alert.'
);
const { getByText, queryByTestId } = renderEntitiesDetails(contextValue);
expect(getByText(NO_DATA_MESSAGE)).toBeInTheDocument();
expect(queryByTestId(USER_TEST_ID)).not.toBeInTheDocument();
expect(queryByTestId(HOST_TEST_ID)).not.toBeInTheDocument();
});

View file

@ -12,7 +12,7 @@ import { useLeftPanelContext } from '../context';
import { getField } from '../../shared/utils';
import { UserDetails } from './user_details';
import { HostDetails } from './host_details';
import { ENTITIES_DETAILS_NO_DATA_TEST_ID, ENTITIES_DETAILS_TEST_ID } from './test_ids';
import { ENTITIES_DETAILS_TEST_ID } from './test_ids';
export const ENTITIES_TAB_ID = 'entities-details';
@ -45,12 +45,10 @@ export const EntitiesDetails: React.FC = () => {
)}
</EuiFlexGroup>
) : (
<p data-test-subj={ENTITIES_DETAILS_NO_DATA_TEST_ID}>
<FormattedMessage
id="xpack.securitySolution.flyout.left.insights.entities.noDataDescription"
defaultMessage="Host and user information are unavailable for this alert."
/>
</p>
<FormattedMessage
id="xpack.securitySolution.flyout.left.insights.entities.noDataDescription"
defaultMessage="Host and user information are unavailable for this alert."
/>
)}
</>
);

View file

@ -10,15 +10,15 @@ import { render } from '@testing-library/react';
import { InvestigationGuide } from './investigation_guide';
import { LeftPanelContext } from '../context';
import { TestProviders } from '../../../common/mock';
import {
INVESTIGATION_GUIDE_LOADING_TEST_ID,
INVESTIGATION_GUIDE_NO_DATA_TEST_ID,
} from './test_ids';
import { INVESTIGATION_GUIDE_TEST_ID, INVESTIGATION_GUIDE_LOADING_TEST_ID } from './test_ids';
import { mockContextValue } from '../mocks/mock_context';
import { useInvestigationGuide } from '../../shared/hooks/use_investigation_guide';
jest.mock('../../shared/hooks/use_investigation_guide');
const NO_DATA_TEXT =
"There's no investigation guide for this rule. Edit the rule's settingsExternal link(opens in a new tab or window) to add one.";
const renderInvestigationGuide = (context: LeftPanelContext = mockContextValue) => (
<TestProviders>
<LeftPanelContext.Provider value={context}>
@ -35,8 +35,10 @@ describe('<InvestigationGuide />', () => {
basicAlertData: { ruleId: 'ruleId' },
ruleNote: 'test note',
});
const { queryByTestId } = render(renderInvestigationGuide());
expect(queryByTestId(INVESTIGATION_GUIDE_NO_DATA_TEST_ID)).not.toBeInTheDocument();
const { queryByTestId, getByText, queryByText } = render(renderInvestigationGuide());
expect(getByText('test note')).toBeInTheDocument();
expect(queryByText(NO_DATA_TEXT)).not.toBeInTheDocument();
expect(queryByTestId(INVESTIGATION_GUIDE_LOADING_TEST_ID)).not.toBeInTheDocument();
});
@ -54,10 +56,7 @@ describe('<InvestigationGuide />', () => {
ruleNote: 'test note',
});
const { getByTestId } = render(renderInvestigationGuide());
expect(getByTestId(INVESTIGATION_GUIDE_NO_DATA_TEST_ID)).toBeInTheDocument();
expect(getByTestId(INVESTIGATION_GUIDE_NO_DATA_TEST_ID)).toHaveTextContent(
`Theres no investigation guide for this rule. Edit the rule's settingsExternal link(opens in a new tab or window) to add one.`
);
expect(getByTestId(INVESTIGATION_GUIDE_TEST_ID)).toHaveTextContent(NO_DATA_TEXT);
});
it('should render no data message when there is no rule note', () => {
@ -66,10 +65,7 @@ describe('<InvestigationGuide />', () => {
ruleNote: undefined,
});
const { getByTestId } = render(renderInvestigationGuide());
expect(getByTestId(INVESTIGATION_GUIDE_NO_DATA_TEST_ID)).toBeInTheDocument();
expect(getByTestId(INVESTIGATION_GUIDE_NO_DATA_TEST_ID)).toHaveTextContent(
`Theres no investigation guide for this rule. Edit the rule's settingsExternal link(opens in a new tab or window) to add one.`
);
expect(getByTestId(INVESTIGATION_GUIDE_TEST_ID)).toHaveTextContent(NO_DATA_TEXT);
});
it('should render no data message when useInvestigationGuide errors out', () => {
@ -78,6 +74,6 @@ describe('<InvestigationGuide />', () => {
error: true,
});
const { getByTestId } = render(renderInvestigationGuide());
expect(getByTestId(INVESTIGATION_GUIDE_NO_DATA_TEST_ID)).toBeInTheDocument();
expect(getByTestId(INVESTIGATION_GUIDE_TEST_ID)).toHaveTextContent(NO_DATA_TEXT);
});
});

View file

@ -5,15 +5,13 @@
* 2.0.
*/
import React from 'react';
import { EuiFlexGroup, EuiFlexItem, EuiLink, EuiLoadingSpinner } from '@elastic/eui';
import { EuiLink } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import { useInvestigationGuide } from '../../shared/hooks/use_investigation_guide';
import { useLeftPanelContext } from '../context';
import {
INVESTIGATION_GUIDE_LOADING_TEST_ID,
INVESTIGATION_GUIDE_NO_DATA_TEST_ID,
} from './test_ids';
import { INVESTIGATION_GUIDE_TEST_ID, INVESTIGATION_GUIDE_LOADING_TEST_ID } from './test_ids';
import { InvestigationGuideView } from '../../../common/components/event_details/investigation_guide_view';
import { FlyoutLoading } from '../../shared/components/flyout_loading';
/**
* Investigation guide displayed in the left panel.
@ -26,22 +24,11 @@ export const InvestigationGuide: React.FC = () => {
dataFormattedForFieldBrowser,
});
if (loading) {
return (
<EuiFlexGroup
justifyContent="spaceAround"
data-test-subj={INVESTIGATION_GUIDE_LOADING_TEST_ID}
>
<EuiFlexItem grow={false}>
<EuiLoadingSpinner size="m" />
</EuiFlexItem>
</EuiFlexGroup>
);
}
return (
<>
{!error && basicAlertData.ruleId && ruleNote ? (
<div data-test-subj={INVESTIGATION_GUIDE_TEST_ID}>
{loading ? (
<FlyoutLoading data-test-subj={INVESTIGATION_GUIDE_LOADING_TEST_ID} />
) : !error && basicAlertData.ruleId && ruleNote ? (
<InvestigationGuideView
basicData={basicAlertData}
ruleNote={ruleNote}
@ -49,27 +36,25 @@ export const InvestigationGuide: React.FC = () => {
showFullView={true}
/>
) : (
<p data-test-subj={INVESTIGATION_GUIDE_NO_DATA_TEST_ID}>
<FormattedMessage
id="xpack.securitySolution.flyout.left.investigation.noDataDescription"
defaultMessage="Theres no investigation guide for this rule. {documentation} to add one."
values={{
documentation: (
<EuiLink
href="https://www.elastic.co/guide/en/security/current/rules-ui-management.html#edit-rules-settings"
target="_blank"
>
<FormattedMessage
id="xpack.securitySolution.flyout.left.investigation.noDataLinkText"
defaultMessage="Edit the rule's settings"
/>
</EuiLink>
),
}}
/>
</p>
<FormattedMessage
id="xpack.securitySolution.flyout.left.investigation.noDataDescription"
defaultMessage="There's no investigation guide for this rule. {documentation} to add one."
values={{
documentation: (
<EuiLink
href="https://www.elastic.co/guide/en/security/current/rules-ui-management.html#edit-rules-settings"
target="_blank"
>
<FormattedMessage
id="xpack.securitySolution.flyout.left.investigation.noDataLinkText"
defaultMessage="Edit the rule's settings"
/>
</EuiLink>
),
}}
/>
)}
</>
</div>
);
};

View file

@ -10,12 +10,10 @@ import React from 'react';
import { LeftPanelContext } from '../context';
import { PrevalenceDetails } from './prevalence_details';
import {
PREVALENCE_DETAILS_LOADING_TEST_ID,
PREVALENCE_DETAILS_TABLE_ALERT_COUNT_CELL_TEST_ID,
PREVALENCE_DETAILS_TABLE_DOC_COUNT_CELL_TEST_ID,
PREVALENCE_DETAILS_TABLE_FIELD_CELL_TEST_ID,
PREVALENCE_DETAILS_TABLE_HOST_PREVALENCE_CELL_TEST_ID,
PREVALENCE_DETAILS_NO_DATA_TEST_ID,
PREVALENCE_DETAILS_TABLE_TEST_ID,
PREVALENCE_DETAILS_UPSELL_TEST_ID,
PREVALENCE_DETAILS_TABLE_USER_PREVALENCE_CELL_TEST_ID,
@ -47,6 +45,8 @@ jest.mock('../../../common/hooks/use_license', () => {
};
});
const NO_DATA_MESSAGE = 'No prevalence data available.';
const panelContextValue = {
eventId: 'event id',
indexName: 'indexName',
@ -96,7 +96,7 @@ describe('PrevalenceDetails', () => {
],
});
const { getByTestId, getAllByTestId, queryByTestId } = renderPrevalenceDetails();
const { getByTestId, getAllByTestId, queryByTestId, queryByText } = renderPrevalenceDetails();
expect(getByTestId(PREVALENCE_DETAILS_TABLE_TEST_ID)).toBeInTheDocument();
expect(getAllByTestId(PREVALENCE_DETAILS_TABLE_FIELD_CELL_TEST_ID).length).toBeGreaterThan(1);
@ -114,7 +114,7 @@ describe('PrevalenceDetails', () => {
getAllByTestId(PREVALENCE_DETAILS_TABLE_USER_PREVALENCE_CELL_TEST_ID).length
).toBeGreaterThan(1);
expect(queryByTestId(PREVALENCE_DETAILS_UPSELL_TEST_ID)).not.toBeInTheDocument();
expect(queryByTestId(PREVALENCE_DETAILS_NO_DATA_TEST_ID)).not.toBeInTheDocument();
expect(queryByText(NO_DATA_MESSAGE)).not.toBeInTheDocument();
});
it('should render formatted numbers for the alert and document count columns', () => {
@ -201,20 +201,6 @@ describe('PrevalenceDetails', () => {
expect(getByTestId(PREVALENCE_DETAILS_UPSELL_TEST_ID)).toBeInTheDocument();
});
it('should render loading', () => {
(usePrevalence as jest.Mock).mockReturnValue({
loading: true,
error: false,
data: [],
});
const { getByTestId, queryByTestId } = renderPrevalenceDetails();
expect(getByTestId(PREVALENCE_DETAILS_LOADING_TEST_ID)).toBeInTheDocument();
expect(queryByTestId(PREVALENCE_DETAILS_UPSELL_TEST_ID)).not.toBeInTheDocument();
expect(queryByTestId(PREVALENCE_DETAILS_NO_DATA_TEST_ID)).not.toBeInTheDocument();
});
it('should render no data message if call errors out', () => {
(usePrevalence as jest.Mock).mockReturnValue({
loading: false,
@ -222,13 +208,8 @@ describe('PrevalenceDetails', () => {
data: [],
});
const { getByTestId, queryByTestId } = renderPrevalenceDetails();
expect(getByTestId(PREVALENCE_DETAILS_NO_DATA_TEST_ID)).toBeInTheDocument();
expect(getByTestId(PREVALENCE_DETAILS_NO_DATA_TEST_ID)).toHaveTextContent(
'No prevalence data available.'
);
expect(queryByTestId(PREVALENCE_DETAILS_LOADING_TEST_ID)).not.toBeInTheDocument();
const { getByText } = renderPrevalenceDetails();
expect(getByText(NO_DATA_MESSAGE)).toBeInTheDocument();
});
it('should render no data message if no data', () => {
@ -238,12 +219,7 @@ describe('PrevalenceDetails', () => {
data: [],
});
const { getByTestId, queryByTestId } = renderPrevalenceDetails();
expect(getByTestId(PREVALENCE_DETAILS_NO_DATA_TEST_ID)).toBeInTheDocument();
expect(getByTestId(PREVALENCE_DETAILS_NO_DATA_TEST_ID)).toHaveTextContent(
'No prevalence data available.'
);
expect(queryByTestId(PREVALENCE_DETAILS_LOADING_TEST_ID)).not.toBeInTheDocument();
const { getByText } = renderPrevalenceDetails();
expect(getByText(NO_DATA_MESSAGE)).toBeInTheDocument();
});
});

View file

@ -14,7 +14,6 @@ import {
EuiFlexItem,
EuiInMemoryTable,
EuiLink,
EuiLoadingSpinner,
EuiPanel,
EuiSpacer,
EuiSuperDatePicker,
@ -28,14 +27,12 @@ import { InvestigateInTimelineButton } from '../../../common/components/event_de
import type { PrevalenceData } from '../../shared/hooks/use_prevalence';
import { usePrevalence } from '../../shared/hooks/use_prevalence';
import {
PREVALENCE_DETAILS_LOADING_TEST_ID,
PREVALENCE_DETAILS_TABLE_ALERT_COUNT_CELL_TEST_ID,
PREVALENCE_DETAILS_TABLE_DOC_COUNT_CELL_TEST_ID,
PREVALENCE_DETAILS_TABLE_HOST_PREVALENCE_CELL_TEST_ID,
PREVALENCE_DETAILS_TABLE_VALUE_CELL_TEST_ID,
PREVALENCE_DETAILS_TABLE_FIELD_CELL_TEST_ID,
PREVALENCE_DETAILS_TABLE_USER_PREVALENCE_CELL_TEST_ID,
PREVALENCE_DETAILS_NO_DATA_TEST_ID,
PREVALENCE_DETAILS_DATE_PICKER_TEST_ID,
PREVALENCE_DETAILS_TABLE_TEST_ID,
PREVALENCE_DETAILS_UPSELL_TEST_ID,
@ -319,19 +316,6 @@ export const PrevalenceDetails: React.FC = () => {
[data, absoluteStart, absoluteEnd]
);
if (loading) {
return (
<EuiFlexGroup
justifyContent="spaceAround"
data-test-subj={PREVALENCE_DETAILS_LOADING_TEST_ID}
>
<EuiFlexItem grow={false}>
<EuiLoadingSpinner size="m" />
</EuiFlexItem>
</EuiFlexGroup>
);
}
const upsell = (
<>
<EuiCallOut data-test-subj={PREVALENCE_DETAILS_UPSELL_TEST_ID}>
@ -366,20 +350,18 @@ export const PrevalenceDetails: React.FC = () => {
width="full"
/>
<EuiSpacer size="m" />
{data.length > 0 ? (
<EuiInMemoryTable
items={items}
columns={columns}
data-test-subj={PREVALENCE_DETAILS_TABLE_TEST_ID}
/>
) : (
<p data-test-subj={PREVALENCE_DETAILS_NO_DATA_TEST_ID}>
<EuiInMemoryTable
items={error ? [] : items}
columns={columns}
loading={loading}
data-test-subj={PREVALENCE_DETAILS_TABLE_TEST_ID}
message={
<FormattedMessage
id="xpack.securitySolution.flyout.left.insights.prevalence.noDataDescription"
defaultMessage="No prevalence data available."
/>
</p>
)}
}
/>
</EuiPanel>
</>
);

View file

@ -7,10 +7,9 @@
import React from 'react';
import type { EuiBasicTableColumn } from '@elastic/eui';
import { EuiInMemoryTable, EuiSkeletonText } from '@elastic/eui';
import { EuiInMemoryTable } from '@elastic/eui';
import type { RelatedCase } from '@kbn/cases-plugin/common';
import { FormattedMessage } from '@kbn/i18n-react';
import { i18n } from '@kbn/i18n';
import { CellTooltipWrapper } from '../../shared/components/cell_tooltip_wrapper';
import { CaseDetailsLink } from '../../../common/components/links';
import {
@ -65,22 +64,6 @@ export interface RelatedCasesProps {
export const RelatedCases: React.VFC<RelatedCasesProps> = ({ eventId }) => {
const { loading, error, data, dataCount } = useFetchRelatedCases({ eventId });
if (loading) {
return (
<EuiSkeletonText
lines={1}
size="m"
isLoading={loading}
contentAriaLabel={i18n.translate(
'xpack.securitySolution.flyout.left.insights.correlations.relatedCasesLoadingAriaLabel',
{
defaultMessage: 'Related cases is loading',
}
)}
/>
);
}
if (error) {
return null;
}

View file

@ -10,7 +10,7 @@ import { render } from '@testing-library/react';
import '@testing-library/jest-dom';
import { LeftPanelContext } from '../context';
import { rawEventData, TestProviders } from '../../../common/mock';
import { RESPONSE_DETAILS_TEST_ID, RESPONSE_NO_DATA_TEST_ID } from './test_ids';
import { RESPONSE_DETAILS_TEST_ID } from './test_ids';
import { ResponseDetails } from './response_details';
import { useIsExperimentalFeatureEnabled } from '../../../common/hooks/use_experimental_features';
@ -59,6 +59,9 @@ jest.mock('../../../common/lib/kibana', () => {
};
});
const NO_DATA_MESSAGE =
"There are no response actions defined for this event. To add some, edit the rule's settings and set up response actionsExternal link(opens in a new tab or window).";
const defaultContextValue = {
dataAsNestedObject: {
_id: 'test',
@ -114,7 +117,7 @@ describe('<ResponseDetails />', () => {
expect(wrapper.getByTestId('responseActionsViewWrapper')).toBeInTheDocument();
expect(wrapper.queryByTestId('osqueryViewWrapper')).not.toBeInTheDocument();
expect(wrapper.queryByTestId(RESPONSE_NO_DATA_TEST_ID)).not.toBeInTheDocument();
expect(wrapper.getByTestId(RESPONSE_DETAILS_TEST_ID)).not.toHaveTextContent(NO_DATA_MESSAGE);
});
it('should render the view with osquery only', () => {
@ -135,9 +138,6 @@ describe('<ResponseDetails />', () => {
expect(wrapper.queryByTestId('responseActionsViewWrapper')).not.toBeInTheDocument();
expect(wrapper.queryByTestId('osqueryViewWrapper')).not.toBeInTheDocument();
expect(wrapper.getByTestId(RESPONSE_NO_DATA_TEST_ID)).toBeInTheDocument();
expect(wrapper.getByTestId(RESPONSE_NO_DATA_TEST_ID)).toHaveTextContent(
'There are no response actions defined for this event. To add some, edit the rules settings and set up response actionsExternal link(opens in a new tab or window).'
);
expect(wrapper.getByTestId(RESPONSE_DETAILS_TEST_ID)).toHaveTextContent(NO_DATA_MESSAGE);
});
});

View file

@ -69,7 +69,7 @@ export const ResponseDetails: React.FC = () => {
<InlineBlock data-test-subj={RESPONSE_NO_DATA_TEST_ID}>
<FormattedMessage
id="xpack.securitySolution.flyout.left.response.noDataDescription"
defaultMessage="There are no response actions defined for this event. To add some, edit the rules settings and set up {link}."
defaultMessage="There are no response actions defined for this event. To add some, edit the rule's settings and set up {link}."
values={{
link: (
<EuiLink

View file

@ -20,8 +20,6 @@ export const SESSION_VIEW_ERROR_TEST_ID = `${PREFIX}SessionViewError` as const;
const PREVALENCE_DETAILS_TEST_ID = `${PREFIX}PrevalenceDetails` as const;
export const PREVALENCE_DETAILS_DATE_PICKER_TEST_ID =
`${PREVALENCE_DETAILS_TEST_ID}DatePicker` as const;
export const PREVALENCE_DETAILS_LOADING_TEST_ID = `${PREVALENCE_DETAILS_TEST_ID}Loading` as const;
export const PREVALENCE_DETAILS_NO_DATA_TEST_ID = `${PREVALENCE_DETAILS_TEST_ID}NoData` as const;
export const PREVALENCE_DETAILS_UPSELL_TEST_ID = `${PREVALENCE_DETAILS_TEST_ID}Upsell` as const;
export const PREVALENCE_DETAILS_TABLE_TEST_ID = `${PREVALENCE_DETAILS_TEST_ID}Table` as const;
export const PREVALENCE_DETAILS_TABLE_FIELD_CELL_TEST_ID =
@ -40,7 +38,6 @@ export const PREVALENCE_DETAILS_TABLE_USER_PREVALENCE_CELL_TEST_ID =
/* Entities */
export const ENTITIES_DETAILS_TEST_ID = `${PREFIX}EntitiesDetails` as const;
export const ENTITIES_DETAILS_NO_DATA_TEST_ID = `${ENTITIES_DETAILS_TEST_ID}NoData` as const;
export const USER_DETAILS_TEST_ID = `${PREFIX}UsersDetails` as const;
export const USER_DETAILS_RELATED_HOSTS_TABLE_TEST_ID =
`${USER_DETAILS_TEST_ID}RelatedHostsTable` as const;
@ -58,9 +55,7 @@ export const THREAT_INTELLIGENCE_DETAILS_LOADING_TEST_ID =
/* Correlations */
const CORRELATIONS_DETAILS_TEST_ID = `${PREFIX}CorrelationsDetails` as const;
export const CORRELATIONS_DETAILS_NO_DATA_TEST_ID =
`${CORRELATIONS_DETAILS_TEST_ID}NoData` as const;
export const CORRELATIONS_DETAILS_TEST_ID = `${PREFIX}CorrelationsDetails` as const;
export const CORRELATIONS_DETAILS_BY_ANCESTRY_SECTION_TEST_ID =
`${CORRELATIONS_DETAILS_TEST_ID}AlertsByAncestrySection` as const;
@ -91,5 +86,5 @@ export const RESPONSE_NO_DATA_TEST_ID = `${RESPONSE_TEST_ID}NoData` as const;
/* Investigation */
export const INVESTIGATION_GUIDE_LOADING_TEST_ID = `${PREFIX}InvestigationGuideLoading` as const;
export const INVESTIGATION_GUIDE_NO_DATA_TEST_ID = `${PREFIX}NoData` as const;
export const INVESTIGATION_GUIDE_TEST_ID = `${PREFIX}InvestigationGuide` as const;
export const INVESTIGATION_GUIDE_LOADING_TEST_ID = `${INVESTIGATION_GUIDE_TEST_ID}Loading` as const;

View file

@ -6,12 +6,13 @@
*/
import React from 'react';
import { EuiFlexGroup, EuiFlexItem, EuiLoadingSpinner, EuiSpacer } from '@elastic/eui';
import { EuiSpacer } from '@elastic/eui';
import isEmpty from 'lodash/isEmpty';
import { EnrichmentRangePicker } from '../../../common/components/event_details/cti_details/enrichment_range_picker';
import { ThreatDetailsView } from '../../../common/components/event_details/cti_details/threat_details_view';
import { useThreatIntelligenceDetails } from '../hooks/use_threat_intelligence_details';
import { THREAT_INTELLIGENCE_DETAILS_LOADING_TEST_ID } from './test_ids';
import { FlyoutLoading } from '../../shared/components/flyout_loading';
export const THREAT_INTELLIGENCE_TAB_ID = 'threat-intelligence-details';
@ -29,33 +30,20 @@ export const ThreatIntelligenceDetails: React.FC = () => {
setRange,
} = useThreatIntelligenceDetails();
if (isEventDataLoading) {
return (
<EuiFlexGroup
justifyContent="spaceAround"
data-test-subj={THREAT_INTELLIGENCE_DETAILS_LOADING_TEST_ID}
>
<EuiFlexItem grow={false}>
<EuiLoadingSpinner size="m" />
</EuiFlexItem>
</EuiFlexGroup>
);
}
return (
<>
<ThreatDetailsView
before={null}
loading={isLoading}
enrichments={enrichments}
showInvestigationTimeEnrichments={!isEmpty(eventFields)}
>
<>
<EnrichmentRangePicker setRange={setRange} loading={isEnrichmentsLoading} range={range} />
<EuiSpacer size="m" />
</>
</ThreatDetailsView>
</>
return isEventDataLoading ? (
<FlyoutLoading data-test-subj={THREAT_INTELLIGENCE_DETAILS_LOADING_TEST_ID} />
) : (
<ThreatDetailsView
before={null}
loading={isLoading}
enrichments={enrichments}
showInvestigationTimeEnrichments={!isEmpty(eventFields)}
>
<>
<EnrichmentRangePicker setRange={setRange} loading={isEnrichmentsLoading} range={range} />
<EuiSpacer size="m" />
</>
</ThreatDetailsView>
);
};

View file

@ -21,6 +21,8 @@ const panelContextValue = {
...mockContextValue,
};
const NO_DATA_MESSAGE = 'There was an error displaying data.';
describe('<AlertReasonPreview />', () => {
it('should render alert reason preview', () => {
const { getByTestId } = render(
@ -35,4 +37,17 @@ describe('<AlertReasonPreview />', () => {
expect(getByTestId(ALERT_REASON_PREVIEW_BODY_TEST_ID)).toBeInTheDocument();
expect(getByTestId(ALERT_REASON_PREVIEW_BODY_TEST_ID)).toHaveTextContent('Alert reason');
});
it('should render no data message if alert reason is not available', () => {
const { getByText } = render(
<IntlProvider locale="en">
<PreviewPanelContext.Provider value={{} as unknown as PreviewPanelContext}>
<ThemeProvider theme={mockTheme}>
<AlertReasonPreview />
</ThemeProvider>
</PreviewPanelContext.Provider>
</IntlProvider>
);
expect(getByText(NO_DATA_MESSAGE)).toBeInTheDocument();
});
});

View file

@ -14,6 +14,7 @@ import { ALERT_REASON_PREVIEW_BODY_TEST_ID } from './test_ids';
import { usePreviewPanelContext } from '../context';
import { getRowRenderer } from '../../../timelines/components/timeline/body/renderers/get_row_renderer';
import { defaultRowRenderers } from '../../../timelines/components/timeline/body/renderers';
import { FlyoutError } from '../../shared/components/flyout_error';
const ReasonPreviewContainerWrapper = styled.div`
overflow-x: auto;
@ -47,7 +48,11 @@ export const AlertReasonPreview: React.FC = () => {
);
if (!renderer) {
return null;
return (
<EuiPanel hasShadow={false} hasBorder={false}>
<FlyoutError />
</EuiPanel>
);
}
return (

View file

@ -67,11 +67,9 @@ const renderRulePreview = () =>
</TestProviders>
);
describe('<RulePreview />', () => {
beforeEach(() => {
// (useAppToasts as jest.Mock).mockReturnValue(useAppToastsValueMock);
});
const NO_DATA_MESSAGE = 'There was an error displaying data.';
describe('<RulePreview />', () => {
afterEach(() => {
jest.clearAllMocks();
});
@ -134,9 +132,10 @@ describe('<RulePreview />', () => {
it('should not render rule preview when rule is null', async () => {
mockUseRuleWithFallback.mockReturnValue({});
const { queryByTestId } = renderRulePreview();
const { queryByTestId, getByText } = renderRulePreview();
await act(async () => {
expect(queryByTestId(RULE_PREVIEW_BODY_TEST_ID)).not.toBeInTheDocument();
expect(getByText(NO_DATA_MESSAGE)).toBeInTheDocument();
});
});
});

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import React, { memo, useState, useEffect } from 'react';
import { EuiText, EuiHorizontalRule, EuiSpacer, EuiPanel, EuiLoadingSpinner } from '@elastic/eui';
import { EuiText, EuiHorizontalRule, EuiSpacer, EuiPanel } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import { useKibana } from '../../../common/lib/kibana';
import { useGetSavedQuery } from '../../../detections/pages/detection_engine/rules/use_get_saved_query';
@ -19,6 +19,8 @@ import { StepAboutRuleReadOnly } from '../../../detections/components/rules/step
import { StepDefineRuleReadOnly } from '../../../detections/components/rules/step_define_rule';
import { StepScheduleRuleReadOnly } from '../../../detections/components/rules/step_schedule_rule';
import { StepRuleActionsReadOnly } from '../../../detections/components/rules/step_rule_actions';
import { FlyoutLoading } from '../../shared/components/flyout_loading';
import { FlyoutError } from '../../shared/components/flyout_error';
import {
RULE_PREVIEW_BODY_TEST_ID,
RULE_PREVIEW_ABOUT_TEST_ID,
@ -79,7 +81,9 @@ export const RulePreview: React.FC = memo(() => {
const hasResponseActions = Boolean(ruleActionsData?.responseActions?.length);
const hasActions = ruleActionsData != null && (hasNotificationActions || hasResponseActions);
return rule ? (
return ruleLoading ? (
<FlyoutLoading data-test-subj={RULE_PREVIEW_LOADING_TEST_ID} />
) : rule ? (
<EuiPanel hasShadow={false} data-test-subj={RULE_PREVIEW_BODY_TEST_ID} className="eui-yScroll">
<RulePreviewTitle rule={rule} isSuppressed={!isExistingRule} />
<EuiHorizontalRule margin="s" />
@ -169,9 +173,11 @@ export const RulePreview: React.FC = memo(() => {
</ExpandableSection>
)}
</EuiPanel>
) : ruleLoading ? (
<EuiLoadingSpinner size="l" data-test-subj={RULE_PREVIEW_LOADING_TEST_ID} />
) : null;
) : (
<EuiPanel hasBorder={false} hasShadow={false}>
<FlyoutError />
</EuiPanel>
);
});
RulePreview.displayName = 'RulePreview';

View file

@ -30,6 +30,8 @@ const renderAnalyzerPreview = (contextValue: RightPanelContext) =>
</TestProviders>
);
const NO_DATA_MESSAGE = 'An error is preventing this alert from being analyzed.';
describe('<AnalyzerPreview />', () => {
beforeEach(() => {
jest.resetAllMocks();
@ -57,7 +59,7 @@ describe('<AnalyzerPreview />', () => {
expect(wrapper.getByTestId(ANALYZER_PREVIEW_TEST_ID)).toBeInTheDocument();
});
it('does not show analyzer preview when documentId and index are not present', () => {
it('shows error message when documentid and index are not present', () => {
mockUseAlertPrevalenceFromProcessTree.mockReturnValue({
loading: false,
error: false,
@ -76,13 +78,25 @@ describe('<AnalyzerPreview />', () => {
},
],
};
const { queryByTestId } = renderAnalyzerPreview(contextValue);
const { getByText } = renderAnalyzerPreview(contextValue);
expect(mockUseAlertPrevalenceFromProcessTree).toHaveBeenCalledWith({
isActiveTimeline: false,
documentId: '',
indices: [],
});
expect(queryByTestId(ANALYZER_PREVIEW_TEST_ID)).not.toBeInTheDocument();
expect(getByText(NO_DATA_MESSAGE)).toBeInTheDocument();
});
it('shows error message when there is an error', () => {
mockUseAlertPrevalenceFromProcessTree.mockReturnValue({
loading: false,
error: true,
alertIds: undefined,
statsNodes: undefined,
});
const { getByText } = renderAnalyzerPreview(mockContextValue);
expect(getByText(NO_DATA_MESSAGE)).toBeInTheDocument();
});
});

View file

@ -6,9 +6,10 @@
*/
import React, { useEffect, useMemo, useState } from 'react';
import { find } from 'lodash/fp';
import { EuiTreeView } from '@elastic/eui';
import { EuiTreeView, EuiSkeletonText } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { ANALYZER_PREVIEW_TEST_ID } from './test_ids';
import { FormattedMessage } from '@kbn/i18n-react';
import { ANALYZER_PREVIEW_TEST_ID, ANALYZER_PREVIEW_LOADING_TEST_ID } from './test_ids';
import { getTreeNodes } from '../utils/analyzer_helpers';
import { ANCESTOR_ID, RULE_INDICES } from '../../shared/constants/field_names';
import { useRightPanelContext } from '../context';
@ -41,7 +42,7 @@ export const AnalyzerPreview: React.FC = () => {
const index = find({ category: 'kibana', field: RULE_INDICES }, data);
const indices = index?.values ?? [];
const { statsNodes } = useAlertPrevalenceFromProcessTree({
const { statsNodes, loading, error } = useAlertPrevalenceFromProcessTree({
isActiveTimeline: isActiveTimeline(scopeId),
documentId: processDocumentId,
indices,
@ -58,24 +59,36 @@ export const AnalyzerPreview: React.FC = () => {
[cache.statsNodes]
);
if (!documentId || !index || !items || items.length === 0) {
return null;
}
const showAnalyzerTree = documentId && index && items && items.length > 0 && !error;
return (
<div data-test-subj={ANALYZER_PREVIEW_TEST_ID}>
<EuiTreeView
items={items}
display="compressed"
aria-label={i18n.translate(
'xpack.securitySolution.flyout.right.visualizations.analyzerPreview.treeViewAriaLabel',
{
defaultMessage: 'Analyzer preview',
}
)}
showExpansionArrows
/>
</div>
return loading ? (
<EuiSkeletonText
data-test-subj={ANALYZER_PREVIEW_LOADING_TEST_ID}
contentAriaLabel={i18n.translate(
'xpack.securitySolution.flyout.right.visualizations.analyzerPreview.loadingAriaLabel',
{
defaultMessage: 'analyzer preview',
}
)}
/>
) : showAnalyzerTree ? (
<EuiTreeView
items={items}
display="compressed"
aria-label={i18n.translate(
'xpack.securitySolution.flyout.right.visualizations.analyzerPreview.treeViewAriaLabel',
{
defaultMessage: 'Analyzer preview',
}
)}
showExpansionArrows
data-test-subj={ANALYZER_PREVIEW_TEST_ID}
/>
) : (
<FormattedMessage
id="xpack.securitySolution.flyout.right.visualizations.analyzerPreview.errorDescription"
defaultMessage="An error is preventing this alert from being analyzed."
/>
);
};

View file

@ -9,9 +9,10 @@ import { render, screen } from '@testing-library/react';
import { TestProviders } from '../../../common/mock';
import React from 'react';
import { RightPanelContext } from '../context';
import { mockContextValue } from '../mocks/mock_context';
import { AnalyzerPreviewContainer } from './analyzer_preview_container';
import { isInvestigateInResolverActionEnabled } from '../../../detections/components/alerts_table/timeline_actions/investigate_in_resolver';
import { ANALYZER_PREVIEW_NO_DATA_TEST_ID, ANALYZER_PREVIEW_TEST_ID } from './test_ids';
import { ANALYZER_PREVIEW_TEST_ID } from './test_ids';
import { useAlertPrevalenceFromProcessTree } from '../../../common/containers/alerts/use_alert_prevalence_from_process_tree';
import * as mock from '../mocks/mock_analyzer_data';
import {
@ -42,9 +43,13 @@ jest.mock('react-redux', () => {
};
});
const NO_ANALYZER_MESSAGE =
'You can only visualize events triggered by hosts configured with the Elastic Defend integration or any sysmon data from winlogbeat. Refer to Visual event analyzerExternal link(opens in a new tab or window) for more information.';
const panelContextValue = {
...mockContextValue,
dataFormattedForFieldBrowser: mockDataFormattedForFieldBrowser,
} as unknown as RightPanelContext;
};
const renderAnalyzerPreview = () =>
render(
@ -72,10 +77,9 @@ describe('AnalyzerPreviewContainer', () => {
investigateInTimelineAlertClick: jest.fn(),
});
const { getByTestId, queryByTestId } = renderAnalyzerPreview();
const { getByTestId } = renderAnalyzerPreview();
expect(getByTestId(ANALYZER_PREVIEW_TEST_ID)).toBeInTheDocument();
expect(queryByTestId(ANALYZER_PREVIEW_NO_DATA_TEST_ID)).not.toBeInTheDocument();
expect(
getByTestId(EXPANDABLE_PANEL_HEADER_TITLE_LINK_TEST_ID(ANALYZER_PREVIEW_TEST_ID))
).toBeInTheDocument();
@ -91,6 +95,9 @@ describe('AnalyzerPreviewContainer', () => {
expect(
screen.queryByTestId(EXPANDABLE_PANEL_HEADER_TITLE_TEXT_TEST_ID(ANALYZER_PREVIEW_TEST_ID))
).not.toBeInTheDocument();
expect(
screen.getByTestId(EXPANDABLE_PANEL_CONTENT_TEST_ID(ANALYZER_PREVIEW_TEST_ID))
).not.toHaveTextContent(NO_ANALYZER_MESSAGE);
});
it('should render error message and text in header', () => {
@ -99,16 +106,13 @@ describe('AnalyzerPreviewContainer', () => {
investigateInTimelineAlertClick: jest.fn(),
});
const { getByTestId, queryByTestId } = renderAnalyzerPreview();
expect(queryByTestId(ANALYZER_PREVIEW_TEST_ID)).not.toBeInTheDocument();
expect(getByTestId(ANALYZER_PREVIEW_NO_DATA_TEST_ID)).toBeInTheDocument();
expect(getByTestId(ANALYZER_PREVIEW_NO_DATA_TEST_ID)).toHaveTextContent(
'You can only visualize events triggered by hosts configured with the Elastic Defend integration or any sysmon data from winlogbeat. Refer to Visual event analyzerExternal link(opens in a new tab or window) for more information.'
);
const { getByTestId } = renderAnalyzerPreview();
expect(
getByTestId(EXPANDABLE_PANEL_HEADER_TITLE_TEXT_TEST_ID(ANALYZER_PREVIEW_TEST_ID))
).toBeInTheDocument();
expect(
getByTestId(EXPANDABLE_PANEL_CONTENT_TEST_ID(ANALYZER_PREVIEW_TEST_ID))
).toHaveTextContent(NO_ANALYZER_MESSAGE);
});
it('should navigate to left section Visualize tab when clicking on title', () => {

View file

@ -18,7 +18,7 @@ import { setActiveTabTimeline } from '../../../timelines/store/timeline/actions'
import { useRightPanelContext } from '../context';
import { isInvestigateInResolverActionEnabled } from '../../../detections/components/alerts_table/timeline_actions/investigate_in_resolver';
import { AnalyzerPreview } from './analyzer_preview';
import { ANALYZER_PREVIEW_NO_DATA_TEST_ID, ANALYZER_PREVIEW_TEST_ID } from './test_ids';
import { ANALYZER_PREVIEW_TEST_ID } from './test_ids';
import { ExpandablePanel } from '../../shared/components/expandable_panel';
const timelineId = 'timeline-1';
@ -81,27 +81,25 @@ export const AnalyzerPreviewContainer: React.FC = () => {
{isEnabled ? (
<AnalyzerPreview />
) : (
<p data-test-subj={ANALYZER_PREVIEW_NO_DATA_TEST_ID}>
<FormattedMessage
id="xpack.securitySolution.flyout.right.visualizations.analyzerPreview.noDataDescription"
defaultMessage="You can only visualize events triggered by hosts configured with the Elastic Defend integration or any {sysmon} data from {winlogbeat}. Refer to {link} for more information."
values={{
sysmon: <EuiMark>{'sysmon'}</EuiMark>,
winlogbeat: <EuiMark>{'winlogbeat'}</EuiMark>,
link: (
<EuiLink
href="https://www.elastic.co/guide/en/security/current/visual-event-analyzer.html"
target="_blank"
>
<FormattedMessage
id="xpack.securitySolution.flyout.right.visualizations.analyzerPreview.noDataLinkText"
defaultMessage="Visual event analyzer"
/>
</EuiLink>
),
}}
/>
</p>
<FormattedMessage
id="xpack.securitySolution.flyout.right.visualizations.analyzerPreview.noDataDescription"
defaultMessage="You can only visualize events triggered by hosts configured with the Elastic Defend integration or any {sysmon} data from {winlogbeat}. Refer to {link} for more information."
values={{
sysmon: <EuiMark>{'sysmon'}</EuiMark>,
winlogbeat: <EuiMark>{'winlogbeat'}</EuiMark>,
link: (
<EuiLink
href="https://www.elastic.co/guide/en/security/current/visual-event-analyzer.html"
target="_blank"
>
<FormattedMessage
id="xpack.securitySolution.flyout.right.visualizations.analyzerPreview.noDataLinkText"
defaultMessage="Visual event analyzer"
/>
</EuiLink>
),
}}
/>
)}
</ExpandablePanel>
);

View file

@ -14,7 +14,6 @@ import { CorrelationsOverview } from './correlations_overview';
import { CORRELATIONS_TAB_ID } from '../../left/components/correlations_details';
import { LeftPanelInsightsTab, LeftPanelKey } from '../../left';
import {
CORRELATIONS_NO_DATA_TEST_ID,
CORRELATIONS_RELATED_ALERTS_BY_ANCESTRY_TEST_ID,
CORRELATIONS_RELATED_ALERTS_BY_SAME_SOURCE_EVENT_TEST_ID,
CORRELATIONS_RELATED_ALERTS_BY_SESSION_TEST_ID,
@ -82,6 +81,8 @@ const renderCorrelationsOverview = (contextValue: RightPanelContext) => (
</TestProviders>
);
const NO_DATA_MESSAGE = 'No correlations data available.';
describe('<CorrelationsOverview />', () => {
it('should render wrapper component', () => {
jest.mocked(useShowRelatedAlertsByAncestry).mockReturnValue({ show: false });
@ -131,13 +132,13 @@ describe('<CorrelationsOverview />', () => {
dataCount: 1,
});
const { getByTestId, queryByTestId } = render(renderCorrelationsOverview(panelContextValue));
const { getByTestId, queryByText } = render(renderCorrelationsOverview(panelContextValue));
expect(getByTestId(RELATED_ALERTS_BY_ANCESTRY_TEST_ID)).toBeInTheDocument();
expect(getByTestId(RELATED_ALERTS_BY_SAME_SOURCE_EVENT_TEST_ID)).toBeInTheDocument();
expect(getByTestId(RELATED_ALERTS_BY_SESSION_TEST_ID)).toBeInTheDocument();
expect(getByTestId(RELATED_CASES_TEST_ID)).toBeInTheDocument();
expect(getByTestId(SUPPRESSED_ALERTS_TEST_ID)).toBeInTheDocument();
expect(queryByTestId(CORRELATIONS_NO_DATA_TEST_ID)).not.toBeInTheDocument();
expect(queryByText(NO_DATA_MESSAGE)).not.toBeInTheDocument();
});
it('should hide rows and show error message if show values are false', () => {
@ -153,16 +154,13 @@ describe('<CorrelationsOverview />', () => {
jest.mocked(useShowRelatedCases).mockReturnValue(false);
jest.mocked(useShowSuppressedAlerts).mockReturnValue({ show: false, alertSuppressionCount: 0 });
const { getByTestId, queryByTestId } = render(renderCorrelationsOverview(panelContextValue));
const { getByText, queryByTestId } = render(renderCorrelationsOverview(panelContextValue));
expect(queryByTestId(RELATED_ALERTS_BY_ANCESTRY_TEST_ID)).not.toBeInTheDocument();
expect(queryByTestId(RELATED_ALERTS_BY_SAME_SOURCE_EVENT_TEST_ID)).not.toBeInTheDocument();
expect(queryByTestId(RELATED_ALERTS_BY_SESSION_TEST_ID)).not.toBeInTheDocument();
expect(queryByTestId(RELATED_CASES_TEST_ID)).not.toBeInTheDocument();
expect(queryByTestId(SUPPRESSED_ALERTS_TEST_ID)).not.toBeInTheDocument();
expect(getByTestId(CORRELATIONS_NO_DATA_TEST_ID)).toBeInTheDocument();
expect(getByTestId(CORRELATIONS_NO_DATA_TEST_ID)).toHaveTextContent(
'No correlations data available.'
);
expect(getByText(NO_DATA_MESSAGE)).toBeInTheDocument();
});
it('should hide rows if values are null', () => {
@ -172,13 +170,13 @@ describe('<CorrelationsOverview />', () => {
jest.mocked(useShowRelatedCases).mockReturnValue(false);
jest.mocked(useShowSuppressedAlerts).mockReturnValue({ show: false, alertSuppressionCount: 0 });
const { queryByTestId } = render(renderCorrelationsOverview(panelContextValue));
const { queryByTestId, queryByText } = render(renderCorrelationsOverview(panelContextValue));
expect(queryByTestId(RELATED_ALERTS_BY_ANCESTRY_TEST_ID)).not.toBeInTheDocument();
expect(queryByTestId(RELATED_ALERTS_BY_SAME_SOURCE_EVENT_TEST_ID)).not.toBeInTheDocument();
expect(queryByTestId(RELATED_ALERTS_BY_SESSION_TEST_ID)).not.toBeInTheDocument();
expect(queryByTestId(RELATED_CASES_TEST_ID)).not.toBeInTheDocument();
expect(queryByTestId(SUPPRESSED_ALERTS_TEST_ID)).not.toBeInTheDocument();
expect(queryByTestId(CORRELATIONS_NO_DATA_TEST_ID)).not.toBeInTheDocument();
expect(queryByText(NO_DATA_MESSAGE)).not.toBeInTheDocument();
});
it('should navigate to the left section Insights tab when clicking on button', () => {

View file

@ -20,7 +20,7 @@ import { SuppressedAlerts } from './suppressed_alerts';
import { useShowSuppressedAlerts } from '../../shared/hooks/use_show_suppressed_alerts';
import { RelatedCases } from './related_cases';
import { useShowRelatedCases } from '../../shared/hooks/use_show_related_cases';
import { CORRELATIONS_NO_DATA_TEST_ID, CORRELATIONS_TEST_ID } from './test_ids';
import { CORRELATIONS_TEST_ID } from './test_ids';
import { useRightPanelContext } from '../context';
import { LeftPanelKey, LeftPanelInsightsTab } from '../../left';
import { CORRELATIONS_TAB_ID } from '../../left/components/correlations_details';
@ -120,12 +120,10 @@ export const CorrelationsOverview: React.FC = () => {
)}
</EuiFlexGroup>
) : (
<p data-test-subj={CORRELATIONS_NO_DATA_TEST_ID}>
<FormattedMessage
id="xpack.securitySolution.flyout.right.insights.correlations.noDataDescription"
defaultMessage="No correlations data available."
/>
</p>
<FormattedMessage
id="xpack.securitySolution.flyout.right.insights.correlations.noDataDescription"
defaultMessage="No correlations data available."
/>
)}
</ExpandablePanel>
);

View file

@ -8,7 +8,11 @@
import React from 'react';
import { FormattedMessage, __IntlProvider as IntlProvider } from '@kbn/i18n-react';
import { render } from '@testing-library/react';
import { DESCRIPTION_TITLE_TEST_ID, RULE_SUMMARY_BUTTON_TEST_ID } from './test_ids';
import {
DESCRIPTION_TITLE_TEST_ID,
RULE_SUMMARY_BUTTON_TEST_ID,
DESCRIPTION_DETAILS_TEST_ID,
} from './test_ids';
import { Description } from './description';
import { RightPanelContext } from '../context';
import { mockGetFieldsData } from '../../shared/mocks/mock_get_fields_data';
@ -64,6 +68,8 @@ const renderDescription = (panelContext: RightPanelContext) =>
</IntlProvider>
);
const NO_DATA_MESSAGE = "There's no description for this rule.";
describe('<Description />', () => {
it('should render the component', () => {
const { getByTestId } = renderDescription(
@ -72,17 +78,15 @@ describe('<Description />', () => {
expect(getByTestId(DESCRIPTION_TITLE_TEST_ID)).toBeInTheDocument();
expect(getByTestId(DESCRIPTION_TITLE_TEST_ID)).toHaveTextContent('Rule description');
expect(getByTestId(DESCRIPTION_DETAILS_TEST_ID)).toHaveTextContent(ruleDescription.values[0]);
expect(getByTestId(RULE_SUMMARY_BUTTON_TEST_ID)).toBeInTheDocument();
});
it('should not render rule preview button if rule name is not available', () => {
const { getByTestId, queryByTestId } = renderDescription(
panelContextValue([ruleUuid, ruleDescription])
);
it('should render no data message if rule description is not available', () => {
const { getByTestId, getByText } = renderDescription(panelContextValue([ruleUuid]));
expect(getByTestId(DESCRIPTION_TITLE_TEST_ID)).toBeInTheDocument();
expect(getByTestId(DESCRIPTION_TITLE_TEST_ID)).toHaveTextContent('Rule description');
expect(queryByTestId(RULE_SUMMARY_BUTTON_TEST_ID)).not.toBeInTheDocument();
expect(getByTestId(DESCRIPTION_DETAILS_TEST_ID)).toBeInTheDocument();
expect(getByText(NO_DATA_MESSAGE)).toBeInTheDocument();
});
it('should render document title if document is not an alert', () => {
@ -92,32 +96,48 @@ describe('<Description />', () => {
expect(getByTestId(DESCRIPTION_TITLE_TEST_ID)).toHaveTextContent('Document description');
});
it('should open preview panel when clicking on button', () => {
const panelContext = panelContextValue([ruleUuid, ruleDescription, ruleName]);
describe('rule preview', () => {
it('should render rule preview button as disabled if rule name is not available', () => {
const { getByTestId } = renderDescription(panelContextValue([ruleUuid, ruleDescription]));
expect(getByTestId(RULE_SUMMARY_BUTTON_TEST_ID)).toBeInTheDocument();
expect(getByTestId(RULE_SUMMARY_BUTTON_TEST_ID)).toHaveAttribute('disabled');
});
const { getByTestId } = renderDescription(panelContext);
it('should render rule preview button as disabled if rule id is not available', () => {
const { getByTestId } = renderDescription(
panelContextValue([{ ...ruleUuid, values: [] }, ruleName, ruleDescription])
);
expect(getByTestId(RULE_SUMMARY_BUTTON_TEST_ID)).toBeInTheDocument();
expect(getByTestId(RULE_SUMMARY_BUTTON_TEST_ID)).toHaveAttribute('disabled');
});
getByTestId(RULE_SUMMARY_BUTTON_TEST_ID).click();
it('should open preview panel when clicking on button', () => {
const panelContext = panelContextValue([ruleUuid, ruleDescription, ruleName]);
expect(flyoutContextValue.openPreviewPanel).toHaveBeenCalledWith({
id: PreviewPanelKey,
path: { tab: 'rule-preview' },
params: {
id: panelContext.eventId,
indexName: panelContext.indexName,
scopeId: panelContext.scopeId,
banner: {
title: (
<FormattedMessage
id="xpack.securitySolution.flyout.right.about.description.rulePreviewTitle"
defaultMessage="Preview rule details"
/>
),
backgroundColor: 'warning',
textColor: 'warning',
const { getByTestId } = renderDescription(panelContext);
getByTestId(RULE_SUMMARY_BUTTON_TEST_ID).click();
expect(flyoutContextValue.openPreviewPanel).toHaveBeenCalledWith({
id: PreviewPanelKey,
path: { tab: 'rule-preview' },
params: {
id: panelContext.eventId,
indexName: panelContext.indexName,
scopeId: panelContext.scopeId,
banner: {
title: (
<FormattedMessage
id="xpack.securitySolution.flyout.right.about.description.rulePreviewTitle"
defaultMessage="Preview rule details"
/>
),
backgroundColor: 'warning',
textColor: 'warning',
},
ruleId: ruleUuid.values[0],
},
ruleId: ruleUuid.values[0],
},
});
});
});
});

View file

@ -56,34 +56,41 @@ export const Description: FC = () => {
}, [eventId, openPreviewPanel, indexName, scopeId, ruleId]);
const viewRule = useMemo(
() =>
!isEmpty(ruleName) &&
!isEmpty(ruleId) && (
<EuiFlexItem grow={false}>
<EuiButtonEmpty
size="s"
iconType="expand"
onClick={openRulePreview}
iconSide="right"
data-test-subj={RULE_SUMMARY_BUTTON_TEST_ID}
aria-label={i18n.translate(
'xpack.securitySolution.flyout.right.about.description.ruleSummaryButtonAriaLabel',
{
defaultMessage: 'Show rule summary',
}
)}
>
<FormattedMessage
id="xpack.securitySolution.flyout.right.about.description.ruleSummaryButtonLabel"
defaultMessage="Show rule summary"
/>
</EuiButtonEmpty>
</EuiFlexItem>
),
() => (
<EuiFlexItem grow={false}>
<EuiButtonEmpty
size="s"
iconType="expand"
onClick={openRulePreview}
iconSide="right"
data-test-subj={RULE_SUMMARY_BUTTON_TEST_ID}
aria-label={i18n.translate(
'xpack.securitySolution.flyout.right.about.description.ruleSummaryButtonAriaLabel',
{
defaultMessage: 'Show rule summary',
}
)}
disabled={isEmpty(ruleName) || isEmpty(ruleId)}
>
<FormattedMessage
id="xpack.securitySolution.flyout.right.about.description.ruleSummaryButtonLabel"
defaultMessage="Show rule summary"
/>
</EuiButtonEmpty>
</EuiFlexItem>
),
[ruleName, openRulePreview, ruleId]
);
const hasRuleDescription = ruleDescription && ruleDescription.length > 0;
const alertRuleDescription =
ruleDescription?.length > 0 ? (
ruleDescription
) : (
<FormattedMessage
id="xpack.securitySolution.flyout.right.about.description.noRuleDescription"
defaultMessage="There's no description for this rule."
/>
);
return (
<EuiFlexGroup direction="column" gutterSize="s">
@ -112,7 +119,7 @@ export const Description: FC = () => {
</EuiTitle>
</EuiFlexItem>
<EuiFlexItem data-test-subj={DESCRIPTION_DETAILS_TEST_ID}>
{hasRuleDescription ? ruleDescription : '-'}
{isAlert ? alertRuleDescription : '-'}
</EuiFlexItem>
</EuiFlexGroup>
);

View file

@ -11,7 +11,6 @@ import { RightPanelContext } from '../context';
import {
ENTITIES_HOST_OVERVIEW_TEST_ID,
ENTITIES_USER_OVERVIEW_TEST_ID,
INSIGHTS_ENTITIES_NO_DATA_TEST_ID,
INSIGHTS_ENTITIES_TEST_ID,
} from './test_ids';
import { EntitiesOverview } from './entities_overview';
@ -45,6 +44,8 @@ const renderEntitiesOverview = (contextValue: RightPanelContext) =>
</TestProviders>
);
const NO_DATA_MESSAGE = 'Host and user information are unavailable for this alert.';
describe('<EntitiesOverview />', () => {
it('should render wrapper component', () => {
const { getByTestId, queryByTestId } = renderEntitiesOverview(mockContextValue);
@ -57,10 +58,10 @@ describe('<EntitiesOverview />', () => {
});
it('should render user and host', () => {
const { getByTestId, queryByTestId } = renderEntitiesOverview(mockContextValue);
const { getByTestId, queryByText } = renderEntitiesOverview(mockContextValue);
expect(getByTestId(ENTITIES_USER_OVERVIEW_TEST_ID)).toBeInTheDocument();
expect(getByTestId(ENTITIES_HOST_OVERVIEW_TEST_ID)).toBeInTheDocument();
expect(queryByTestId(INSIGHTS_ENTITIES_NO_DATA_TEST_ID)).not.toBeInTheDocument();
expect(queryByText(NO_DATA_MESSAGE)).not.toBeInTheDocument();
});
it('should only render user when host name is null', () => {
@ -69,11 +70,11 @@ describe('<EntitiesOverview />', () => {
getFieldsData: (field: string) => (field === 'user.name' ? 'user1' : null),
} as unknown as RightPanelContext;
const { queryByTestId, getByTestId } = renderEntitiesOverview(contextValue);
const { queryByTestId, getByTestId, queryByText } = renderEntitiesOverview(contextValue);
expect(getByTestId(ENTITIES_USER_OVERVIEW_TEST_ID)).toBeInTheDocument();
expect(queryByTestId(ENTITIES_HOST_OVERVIEW_TEST_ID)).not.toBeInTheDocument();
expect(queryByTestId(INSIGHTS_ENTITIES_NO_DATA_TEST_ID)).not.toBeInTheDocument();
expect(queryByText(NO_DATA_MESSAGE)).not.toBeInTheDocument();
});
it('should only render host when user name is null', () => {
@ -82,11 +83,11 @@ describe('<EntitiesOverview />', () => {
getFieldsData: (field: string) => (field === 'host.name' ? 'host1' : null),
} as unknown as RightPanelContext;
const { queryByTestId, getByTestId } = renderEntitiesOverview(contextValue);
const { queryByTestId, getByTestId, queryByText } = renderEntitiesOverview(contextValue);
expect(getByTestId(ENTITIES_HOST_OVERVIEW_TEST_ID)).toBeInTheDocument();
expect(queryByTestId(ENTITIES_USER_OVERVIEW_TEST_ID)).not.toBeInTheDocument();
expect(queryByTestId(INSIGHTS_ENTITIES_NO_DATA_TEST_ID)).not.toBeInTheDocument();
expect(queryByText(NO_DATA_MESSAGE)).not.toBeInTheDocument();
});
it('should render no data message if both host name and user name are null/blank', () => {
@ -95,11 +96,7 @@ describe('<EntitiesOverview />', () => {
getFieldsData: (field: string) => {},
} as unknown as RightPanelContext;
const { queryByTestId } = renderEntitiesOverview(contextValue);
expect(queryByTestId(INSIGHTS_ENTITIES_NO_DATA_TEST_ID)).toBeInTheDocument();
expect(queryByTestId(INSIGHTS_ENTITIES_NO_DATA_TEST_ID)).toHaveTextContent(
'Host and user information are unavailable for this alert.'
);
const { getByText } = renderEntitiesOverview(contextValue);
expect(getByText(NO_DATA_MESSAGE)).toBeInTheDocument();
});
});

View file

@ -9,7 +9,7 @@ import React, { useCallback } from 'react';
import { EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui';
import { useExpandableFlyoutContext } from '@kbn/expandable-flyout';
import { FormattedMessage } from '@kbn/i18n-react';
import { INSIGHTS_ENTITIES_NO_DATA_TEST_ID, INSIGHTS_ENTITIES_TEST_ID } from './test_ids';
import { INSIGHTS_ENTITIES_TEST_ID } from './test_ids';
import { ExpandablePanel } from '../../shared/components/expandable_panel';
import { useRightPanelContext } from '../context';
import { getField } from '../../shared/utils';
@ -80,12 +80,10 @@ export const EntitiesOverview: React.FC = () => {
)}
</EuiFlexGroup>
) : (
<p data-test-subj={INSIGHTS_ENTITIES_NO_DATA_TEST_ID}>
<FormattedMessage
id="xpack.securitySolution.flyout.right.insights.entities.noDataDescription"
defaultMessage="Host and user information are unavailable for this alert."
/>
</p>
<FormattedMessage
id="xpack.securitySolution.flyout.right.insights.entities.noDataDescription"
defaultMessage="Host and user information are unavailable for this alert."
/>
)}
</ExpandablePanel>
</>

View file

@ -27,6 +27,8 @@ const renderHighlightedFields = (contextValue: RightPanelContext) =>
</TestProviders>
);
const NO_DATA_MESSAGE = "There's no highlighted fields for this alert.";
describe('<HighlightedFields />', () => {
beforeEach(() => {
(useRuleWithFallback as jest.Mock).mockReturnValue({ investigation_fields: undefined });
@ -49,15 +51,14 @@ describe('<HighlightedFields />', () => {
expect(getByTestId(HIGHLIGHTED_FIELDS_DETAILS_TEST_ID)).toBeInTheDocument();
});
it(`should render empty component if there aren't any highlighted fields`, () => {
it(`should render no data message if there aren't any highlighted fields`, () => {
const contextValue = {
dataFormattedForFieldBrowser: mockDataFormattedForFieldBrowser,
scopeId: 'scopeId',
} as unknown as RightPanelContext;
(useHighlightedFields as jest.Mock).mockReturnValue({});
const { container } = renderHighlightedFields(contextValue);
expect(container).toBeEmptyDOMElement();
const { getByText } = renderHighlightedFields(contextValue);
expect(getByText(NO_DATA_MESSAGE)).toBeInTheDocument();
});
});

View file

@ -95,7 +95,7 @@ const columns: Array<EuiBasicTableColumn<HighlightedFieldsTableRow>> = [
export const HighlightedFields: FC = () => {
const { dataFormattedForFieldBrowser, scopeId } = useRightPanelContext();
const { ruleId } = useBasicDataFromDetailsData(dataFormattedForFieldBrowser);
const { rule: maybeRule } = useRuleWithFallback(ruleId);
const { loading, error, rule: maybeRule } = useRuleWithFallback(ruleId);
const highlightedFields = useHighlightedFields({
dataFormattedForFieldBrowser,
@ -106,10 +106,6 @@ export const HighlightedFields: FC = () => {
[highlightedFields, scopeId]
);
if (items.length === 0) {
return null;
}
return (
<EuiFlexGroup direction="column" gutterSize="s">
<EuiFlexItem data-test-subj={HIGHLIGHTED_FIELDS_TITLE_TEST_ID}>
@ -124,7 +120,18 @@ export const HighlightedFields: FC = () => {
</EuiFlexItem>
<EuiFlexItem data-test-subj={HIGHLIGHTED_FIELDS_DETAILS_TEST_ID}>
<EuiPanel hasBorder hasShadow={false}>
<EuiInMemoryTable items={items} columns={columns} compressed />
<EuiInMemoryTable
items={error ? [] : items}
columns={columns}
compressed
loading={loading}
message={
<FormattedMessage
id="xpack.securitySolution.flyout.right.investigation.highlightedFields.noDataDescription"
defaultMessage="There's no highlighted fields for this alert."
/>
}
/>
</EuiPanel>
</EuiFlexItem>
</EuiFlexGroup>

View file

@ -16,6 +16,7 @@ import {
ENTITIES_HOST_OVERVIEW_LAST_SEEN_TEST_ID,
ENTITIES_HOST_OVERVIEW_LINK_TEST_ID,
ENTITIES_HOST_OVERVIEW_RISK_LEVEL_TEST_ID,
ENTITIES_HOST_OVERVIEW_LOADING_TEST_ID,
} from './test_ids';
import { RightPanelContext } from '../context';
import { mockContextValue } from '../mocks/mock_context';
@ -100,6 +101,33 @@ describe('<HostEntityContent />', () => {
});
});
it('should render loading if loading for host details is true', () => {
mockUseHostDetails.mockReturnValue([true, { hostDetails: null }]);
mockUseRiskScore.mockReturnValue({ data: null, isAuthorized: true });
const { getByTestId } = render(
<TestProviders>
<RightPanelContext.Provider value={panelContextValue}>
<HostEntityOverview hostName={hostName} />
</RightPanelContext.Provider>
</TestProviders>
);
expect(getByTestId(ENTITIES_HOST_OVERVIEW_LOADING_TEST_ID)).toBeInTheDocument();
});
it('should render loading if loading for risk score is true', () => {
mockUseHostDetails.mockReturnValue([false, { hostDetails: null }]);
mockUseRiskScore.mockReturnValue({ data: null, isAuthorized: true, loading: true });
const { getByTestId } = render(
<TestProviders>
<RightPanelContext.Provider value={panelContextValue}>
<HostEntityOverview hostName={hostName} />
</RightPanelContext.Provider>
</TestProviders>
);
expect(getByTestId(ENTITIES_HOST_OVERVIEW_LOADING_TEST_ID)).toBeInTheDocument();
});
describe('license is not valid', () => {
it('should render os family and last seen', () => {
mockUseHostDetails.mockReturnValue([false, { hostDetails: hostData }]);

View file

@ -14,9 +14,11 @@ import {
useEuiTheme,
useEuiFontSize,
EuiIconTip,
EuiSkeletonText,
} from '@elastic/eui';
import { css } from '@emotion/css';
import { getOr } from 'lodash/fp';
import { i18n } from '@kbn/i18n';
import { useExpandableFlyoutContext } from '@kbn/expandable-flyout';
import { FormattedMessage } from '@kbn/i18n-react';
import { useRightPanelContext } from '../context';
@ -35,7 +37,11 @@ import { useSourcererDataView } from '../../../common/containers/sourcerer';
import { useGlobalTime } from '../../../common/containers/use_global_time';
import { useRiskScore } from '../../../explore/containers/risk_score';
import { useHostDetails } from '../../../explore/hosts/containers/hosts/details';
import * as i18n from '../../../overview/components/host_overview/translations';
import {
FAMILY,
LAST_SEEN,
HOST_RISK_CLASSIFICATION,
} from '../../../overview/components/host_overview/translations';
import { ENTITIES_TAB_ID } from '../../left/components/entities_details';
import {
ENTITIES_HOST_OVERVIEW_TEST_ID,
@ -43,6 +49,7 @@ import {
ENTITIES_HOST_OVERVIEW_LAST_SEEN_TEST_ID,
ENTITIES_HOST_OVERVIEW_RISK_LEVEL_TEST_ID,
ENTITIES_HOST_OVERVIEW_LINK_TEST_ID,
ENTITIES_HOST_OVERVIEW_LOADING_TEST_ID,
TECHNICAL_PREVIEW_ICON_TEST_ID,
} from './test_ids';
import { LeftPanelInsightsTab, LeftPanelKey } from '../../left';
@ -91,14 +98,18 @@ export const HostEntityOverview: React.FC<HostEntityOverviewProps> = ({ hostName
[hostName]
);
const { data: hostRisk, isAuthorized } = useRiskScore({
const {
data: hostRisk,
isAuthorized,
loading: isRiskScoreLoading,
} = useRiskScore({
filterQuery,
riskEntity: RiskScoreEntity.host,
skip: hostName == null,
timerange,
});
const [_, { hostDetails }] = useHostDetails({
const [isHostDetailsLoading, { hostDetails }] = useHostDetails({
hostName,
indexNames: selectedPatterns,
startDate: from,
@ -108,7 +119,7 @@ export const HostEntityOverview: React.FC<HostEntityOverviewProps> = ({ hostName
const hostOSFamily: DescriptionList[] = useMemo(
() => [
{
title: i18n.FAMILY,
title: FAMILY,
description: (
<DefaultFieldRenderer
rowItems={getOr([], 'host.os.family', hostDetails)}
@ -125,7 +136,7 @@ export const HostEntityOverview: React.FC<HostEntityOverviewProps> = ({ hostName
const hostLastSeen: DescriptionList[] = useMemo(
() => [
{
title: i18n.LAST_SEEN,
title: LAST_SEEN,
description: (
<FirstLastSeen
indexPatterns={selectedPatterns}
@ -148,7 +159,7 @@ export const HostEntityOverview: React.FC<HostEntityOverviewProps> = ({ hostName
{
title: (
<>
{i18n.HOST_RISK_CLASSIFICATION}
{HOST_RISK_CLASSIFICATION}
<EuiIconTip
title={
<FormattedMessage
@ -206,29 +217,39 @@ export const HostEntityOverview: React.FC<HostEntityOverviewProps> = ({ hostName
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
<EuiFlexItem>
<EuiFlexGroup>
<EuiFlexItem>
<OverviewDescriptionList
dataTestSubj={ENTITIES_HOST_OVERVIEW_OS_FAMILY_TEST_ID}
descriptionList={hostOSFamily}
/>
</EuiFlexItem>
<EuiFlexItem>
{isAuthorized ? (
<DescriptionListStyled
data-test-subj={ENTITIES_HOST_OVERVIEW_RISK_LEVEL_TEST_ID}
listItems={[hostRiskLevel]}
/>
) : (
{isRiskScoreLoading || isHostDetailsLoading ? (
<EuiSkeletonText
data-test-subj={ENTITIES_HOST_OVERVIEW_LOADING_TEST_ID}
contentAriaLabel={i18n.translate(
'xpack.securitySolution.flyout.right.insights.entities.hostLoadingAriaLabel',
{ defaultMessage: 'host overview' }
)}
/>
) : (
<EuiFlexItem>
<EuiFlexGroup>
<EuiFlexItem>
<OverviewDescriptionList
dataTestSubj={ENTITIES_HOST_OVERVIEW_LAST_SEEN_TEST_ID}
descriptionList={hostLastSeen}
dataTestSubj={ENTITIES_HOST_OVERVIEW_OS_FAMILY_TEST_ID}
descriptionList={hostOSFamily}
/>
)}
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
</EuiFlexItem>
<EuiFlexItem>
{isAuthorized ? (
<DescriptionListStyled
data-test-subj={ENTITIES_HOST_OVERVIEW_RISK_LEVEL_TEST_ID}
listItems={[hostRiskLevel]}
/>
) : (
<OverviewDescriptionList
dataTestSubj={ENTITIES_HOST_OVERVIEW_LAST_SEEN_TEST_ID}
descriptionList={hostLastSeen}
/>
)}
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
)}
</EuiFlexGroup>
);
};

View file

@ -13,7 +13,6 @@ import { RightPanelContext } from '../context';
import {
INVESTIGATION_GUIDE_BUTTON_TEST_ID,
INVESTIGATION_GUIDE_LOADING_TEST_ID,
INVESTIGATION_GUIDE_NO_DATA_TEST_ID,
INVESTIGATION_GUIDE_TEST_ID,
} from './test_ids';
import { mockContextValue } from '../mocks/mock_context';
@ -24,6 +23,8 @@ import { LeftPanelInvestigationTab, LeftPanelKey } from '../../left';
jest.mock('../../shared/hooks/use_investigation_guide');
const NO_DATA_MESSAGE = 'Investigation guideTheres no investigation guide for this rule.';
const renderInvestigationGuide = () =>
render(
<IntlProvider locale="en">
@ -51,7 +52,7 @@ describe('<InvestigationGuide />', () => {
'Show investigation guide'
);
expect(queryByTestId(INVESTIGATION_GUIDE_LOADING_TEST_ID)).not.toBeInTheDocument();
expect(queryByTestId(INVESTIGATION_GUIDE_NO_DATA_TEST_ID)).not.toBeInTheDocument();
expect(getByTestId(INVESTIGATION_GUIDE_TEST_ID)).not.toHaveTextContent(NO_DATA_MESSAGE);
});
it('should render loading', () => {
@ -59,10 +60,10 @@ describe('<InvestigationGuide />', () => {
loading: true,
});
const { getByTestId, queryByTestId } = renderInvestigationGuide();
expect(getByTestId(INVESTIGATION_GUIDE_TEST_ID)).toBeInTheDocument();
expect(getByTestId(INVESTIGATION_GUIDE_LOADING_TEST_ID)).toBeInTheDocument();
expect(queryByTestId(INVESTIGATION_GUIDE_TEST_ID)).not.toBeInTheDocument();
expect(queryByTestId(INVESTIGATION_GUIDE_BUTTON_TEST_ID)).not.toBeInTheDocument();
expect(queryByTestId(INVESTIGATION_GUIDE_NO_DATA_TEST_ID)).not.toBeInTheDocument();
expect(getByTestId(INVESTIGATION_GUIDE_TEST_ID)).not.toHaveTextContent(NO_DATA_MESSAGE);
});
it('should render no data message when there is no ruleId', () => {
@ -70,12 +71,9 @@ describe('<InvestigationGuide />', () => {
basicAlertData: {},
ruleNote: 'test note',
});
const { getByTestId, queryByTestId } = renderInvestigationGuide();
const { queryByTestId, getByTestId } = renderInvestigationGuide();
expect(queryByTestId(INVESTIGATION_GUIDE_BUTTON_TEST_ID)).not.toBeInTheDocument();
expect(getByTestId(INVESTIGATION_GUIDE_NO_DATA_TEST_ID)).toBeInTheDocument();
expect(getByTestId(INVESTIGATION_GUIDE_NO_DATA_TEST_ID)).toHaveTextContent(
'Theres no investigation guide for this rule.'
);
expect(getByTestId(INVESTIGATION_GUIDE_TEST_ID)).toHaveTextContent(NO_DATA_MESSAGE);
});
it('should render no data message when there is no rule note', () => {
@ -85,10 +83,7 @@ describe('<InvestigationGuide />', () => {
});
const { getByTestId, queryByTestId } = renderInvestigationGuide();
expect(queryByTestId(INVESTIGATION_GUIDE_BUTTON_TEST_ID)).not.toBeInTheDocument();
expect(getByTestId(INVESTIGATION_GUIDE_NO_DATA_TEST_ID)).toBeInTheDocument();
expect(getByTestId(INVESTIGATION_GUIDE_NO_DATA_TEST_ID)).toHaveTextContent(
'Theres no investigation guide for this rule.'
);
expect(getByTestId(INVESTIGATION_GUIDE_TEST_ID)).toHaveTextContent(NO_DATA_MESSAGE);
});
it('should render no data message when useInvestigationGuide errors out', () => {
@ -97,8 +92,9 @@ describe('<InvestigationGuide />', () => {
error: true,
});
const { getByTestId } = renderInvestigationGuide();
expect(getByTestId(INVESTIGATION_GUIDE_NO_DATA_TEST_ID)).toBeInTheDocument();
const { queryByTestId, getByTestId } = renderInvestigationGuide();
expect(queryByTestId(INVESTIGATION_GUIDE_BUTTON_TEST_ID)).not.toBeInTheDocument();
expect(getByTestId(INVESTIGATION_GUIDE_TEST_ID)).toHaveTextContent(NO_DATA_MESSAGE);
});
it('should navigate to investigation guide when clicking on button', () => {

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import React, { useCallback } from 'react';
import { EuiButton, EuiFlexGroup, EuiFlexItem, EuiLoadingSpinner, EuiTitle } from '@elastic/eui';
import { EuiButton, EuiFlexGroup, EuiFlexItem, EuiTitle, EuiSkeletonText } from '@elastic/eui';
import { useExpandableFlyoutContext } from '@kbn/expandable-flyout';
import { FormattedMessage } from '@kbn/i18n-react';
import { i18n } from '@kbn/i18n';
@ -15,7 +15,6 @@ import { LeftPanelKey, LeftPanelInvestigationTab } from '../../left';
import {
INVESTIGATION_GUIDE_BUTTON_TEST_ID,
INVESTIGATION_GUIDE_LOADING_TEST_ID,
INVESTIGATION_GUIDE_NO_DATA_TEST_ID,
INVESTIGATION_GUIDE_TEST_ID,
} from './test_ids';
@ -45,22 +44,9 @@ export const InvestigationGuide: React.FC = () => {
});
}, [eventId, indexName, openLeftPanel, scopeId]);
if (loading) {
return (
<EuiFlexGroup
justifyContent="spaceAround"
data-test-subj={INVESTIGATION_GUIDE_LOADING_TEST_ID}
>
<EuiFlexItem grow={false}>
<EuiLoadingSpinner size="m" />
</EuiFlexItem>
</EuiFlexGroup>
);
}
return (
<EuiFlexGroup direction="column" gutterSize="s">
<EuiFlexItem data-test-subj={INVESTIGATION_GUIDE_TEST_ID}>
<EuiFlexGroup direction="column" gutterSize="s" data-test-subj={INVESTIGATION_GUIDE_TEST_ID}>
<EuiFlexItem>
<EuiTitle size="xxs">
<h5>
<FormattedMessage
@ -70,8 +56,16 @@ export const InvestigationGuide: React.FC = () => {
</h5>
</EuiTitle>
</EuiFlexItem>
<EuiFlexItem>
{!error && basicAlertData.ruleId && ruleNote ? (
{loading ? (
<EuiSkeletonText
data-test-subj={INVESTIGATION_GUIDE_LOADING_TEST_ID}
contentAriaLabel={i18n.translate(
'xpack.securitySolution.flyout.right.investigation.investigationGuide.investigationGuideLoadingAriaLabel',
{ defaultMessage: 'investigation guide' }
)}
/>
) : !error && basicAlertData.ruleId && ruleNote ? (
<EuiFlexItem>
<EuiButton
onClick={goToInvestigationsTab}
iconType="documentation"
@ -88,15 +82,13 @@ export const InvestigationGuide: React.FC = () => {
defaultMessage="Show investigation guide"
/>
</EuiButton>
) : (
<p data-test-subj={INVESTIGATION_GUIDE_NO_DATA_TEST_ID}>
<FormattedMessage
id="xpack.securitySolution.flyout.right.investigation.investigationGuide.noDataDescription"
defaultMessage="Theres no investigation guide for this rule."
/>
</p>
)}
</EuiFlexItem>
</EuiFlexItem>
) : (
<FormattedMessage
id="xpack.securitySolution.flyout.right.investigation.investigationGuide.noDataDescription"
defaultMessage="Theres no investigation guide for this rule."
/>
)}
</EuiFlexGroup>
);
};

View file

@ -17,6 +17,7 @@ export const MitreAttack: FC = () => {
const threatDetails = useMemo(() => getMitreComponentParts(searchHit), [searchHit]);
if (!threatDetails || !threatDetails[0]) {
// Do not render empty message on MITRE attack because other frameworks could be used
return null;
}

View file

@ -9,7 +9,7 @@ import { ExpandableFlyoutContext } from '@kbn/expandable-flyout/src/context';
import { render } from '@testing-library/react';
import { TestProviders } from '../../../common/mock';
import { RightPanelContext } from '../context';
import { PREVALENCE_NO_DATA_TEST_ID, PREVALENCE_TEST_ID } from './test_ids';
import { PREVALENCE_TEST_ID } from './test_ids';
import { LeftPanelInsightsTab, LeftPanelKey } from '../../left';
import React from 'react';
import { PrevalenceOverview } from './prevalence_overview';
@ -31,6 +31,8 @@ const TITLE_LINK_TEST_ID = EXPANDABLE_PANEL_HEADER_TITLE_LINK_TEST_ID(PREVALENCE
const TITLE_ICON_TEST_ID = EXPANDABLE_PANEL_HEADER_TITLE_ICON_TEST_ID(PREVALENCE_TEST_ID);
const TITLE_TEXT_TEST_ID = EXPANDABLE_PANEL_HEADER_TITLE_TEXT_TEST_ID(PREVALENCE_TEST_ID);
const NO_DATA_MESSAGE = 'No prevalence data available.';
const flyoutContextValue = {
openLeftPanel: jest.fn(),
} as unknown as ExpandableFlyoutContext;
@ -69,10 +71,10 @@ describe('<PrevalenceOverview />', () => {
data: [],
});
const { getByTestId, queryByTestId } = renderPrevalenceOverview();
const { getByTestId, queryByText } = renderPrevalenceOverview();
expect(getByTestId(EXPANDABLE_PANEL_LOADING_TEST_ID(PREVALENCE_TEST_ID))).toBeInTheDocument();
expect(queryByTestId(PREVALENCE_NO_DATA_TEST_ID)).not.toBeInTheDocument();
expect(queryByText(NO_DATA_MESSAGE)).not.toBeInTheDocument();
});
it('should render no-data message', () => {
@ -82,12 +84,8 @@ describe('<PrevalenceOverview />', () => {
data: [],
});
const { getByTestId } = renderPrevalenceOverview();
expect(getByTestId(PREVALENCE_NO_DATA_TEST_ID)).toBeInTheDocument();
expect(getByTestId(PREVALENCE_NO_DATA_TEST_ID)).toHaveTextContent(
'No prevalence data available.'
);
const { getByText } = renderPrevalenceOverview();
expect(getByText(NO_DATA_MESSAGE)).toBeInTheDocument();
});
it('should render only data with prevalence less than 10%', () => {
@ -116,7 +114,7 @@ describe('<PrevalenceOverview />', () => {
],
});
const { queryByTestId, getByTestId } = renderPrevalenceOverview();
const { queryByTestId, getByTestId, queryByText } = renderPrevalenceOverview();
expect(getByTestId(TITLE_LINK_TEST_ID)).toHaveTextContent('Prevalence');
@ -131,7 +129,7 @@ describe('<PrevalenceOverview />', () => {
expect(queryByTestId(iconDataTestSubj2)).not.toBeInTheDocument();
expect(queryByTestId(valueDataTestSubj2)).not.toBeInTheDocument();
expect(queryByTestId(PREVALENCE_NO_DATA_TEST_ID)).not.toBeInTheDocument();
expect(queryByText(NO_DATA_MESSAGE)).not.toBeInTheDocument();
});
it('should navigate to left section Insights tab when clicking on button', () => {

View file

@ -12,7 +12,7 @@ import { useExpandableFlyoutContext } from '@kbn/expandable-flyout';
import { FormattedMessage } from '@kbn/i18n-react';
import { ExpandablePanel } from '../../shared/components/expandable_panel';
import { usePrevalence } from '../../shared/hooks/use_prevalence';
import { PREVALENCE_NO_DATA_TEST_ID, PREVALENCE_TEST_ID } from './test_ids';
import { PREVALENCE_TEST_ID } from './test_ids';
import { useRightPanelContext } from '../context';
import { LeftPanelKey, LeftPanelInsightsTab } from '../../left';
import { PREVALENCE_TAB_ID } from '../../left/components/prevalence_details';
@ -107,12 +107,10 @@ export const PrevalenceOverview: FC = () => {
/>
))
) : (
<p data-test-subj={PREVALENCE_NO_DATA_TEST_ID}>
<FormattedMessage
id="xpack.securitySolution.flyout.right.insights.prevalence.noDataDescription"
defaultMessage="No prevalence data available."
/>
</p>
<FormattedMessage
id="xpack.securitySolution.flyout.right.insights.prevalence.noDataDescription"
defaultMessage="No prevalence data available."
/>
)}
</EuiFlexGroup>
</ExpandablePanel>

View file

@ -8,11 +8,7 @@
import React from 'react';
import { render } from '@testing-library/react';
import { FormattedMessage, __IntlProvider as IntlProvider } from '@kbn/i18n-react';
import {
REASON_DETAILS_PREVIEW_BUTTON_TEST_ID,
REASON_DETAILS_TEST_ID,
REASON_TITLE_TEST_ID,
} from './test_ids';
import { REASON_DETAILS_PREVIEW_BUTTON_TEST_ID, REASON_TITLE_TEST_ID } from './test_ids';
import { Reason } from './reason';
import { RightPanelContext } from '../context';
import { mockGetFieldsData } from '../../shared/mocks/mock_get_fields_data';
@ -43,6 +39,8 @@ const renderReason = (panelContext: RightPanelContext = panelContextValue) =>
</IntlProvider>
);
const NO_DATA_MESSAGE = "There's no source event information for this alert.";
describe('<Reason />', () => {
it('should render the component for alert', () => {
const { getByTestId } = renderReason();
@ -73,9 +71,9 @@ describe('<Reason />', () => {
getFieldsData: () => {},
} as unknown as RightPanelContext;
const { getByTestId } = renderReason(panelContext);
const { getByText } = renderReason(panelContext);
expect(getByTestId(REASON_DETAILS_TEST_ID)).toBeEmptyDOMElement();
expect(getByText(NO_DATA_MESSAGE)).toBeInTheDocument();
});
it('should open preview panel when clicking on button', () => {

View file

@ -69,6 +69,7 @@ export const Reason: FC = () => {
defaultMessage: 'Show full reason',
}
)}
disabled={!alertReason}
>
<FormattedMessage
id="xpack.securitySolution.flyout.right.about.reason.alertReasonButtonLabel"
@ -77,7 +78,16 @@ export const Reason: FC = () => {
</EuiButtonEmpty>
</EuiFlexItem>
),
[openRulePreview]
[alertReason, openRulePreview]
);
const alertReasonText = alertReason ? (
alertReason
) : (
<FormattedMessage
id="xpack.securitySolution.flyout.right.about.reason.noReasonDescription"
defaultMessage="There's no source event information for this alert."
/>
);
return (
@ -108,7 +118,9 @@ export const Reason: FC = () => {
</h5>
</EuiTitle>
</EuiFlexItem>
<EuiFlexItem data-test-subj={REASON_DETAILS_TEST_ID}>{alertReason}</EuiFlexItem>
<EuiFlexItem data-test-subj={REASON_DETAILS_TEST_ID}>
{isAlert ? alertReasonText : '-'}
</EuiFlexItem>
</EuiFlexGroup>
);
};

View file

@ -12,11 +12,7 @@ import { RightPanelContext } from '../context';
import { SessionPreviewContainer } from './session_preview_container';
import { useSessionPreview } from '../hooks/use_session_preview';
import { useLicense } from '../../../common/hooks/use_license';
import {
SESSION_PREVIEW_NO_DATA_TEST_ID,
SESSION_PREVIEW_TEST_ID,
SESSION_PREVIEW_UPSELL_TEST_ID,
} from './test_ids';
import { SESSION_PREVIEW_TEST_ID } from './test_ids';
import {
EXPANDABLE_PANEL_CONTENT_TEST_ID,
EXPANDABLE_PANEL_HEADER_TITLE_ICON_TEST_ID,
@ -29,6 +25,11 @@ import { mockGetFieldsData } from '../../shared/mocks/mock_get_fields_data';
jest.mock('../hooks/use_session_preview');
jest.mock('../../../common/hooks/use_license');
const NO_DATA_MESSAGE =
'You can only view Linux session details if youve enabled the Include session data setting in your Elastic Defend integration policy. Refer to Enable Session View dataExternal link(opens in a new tab or window) for more information.';
const UPSELL_TEXT = 'This feature requires an Enterprise subscription';
const panelContextValue = {
getFieldsData: mockGetFieldsData,
} as unknown as RightPanelContext;
@ -57,14 +58,13 @@ describe('SessionPreviewContainer', () => {
(useSessionPreview as jest.Mock).mockReturnValue(sessionViewConfig);
(useLicense as jest.Mock).mockReturnValue({ isEnterprise: () => true });
const { getByTestId, queryByTestId } = renderSessionPreview();
const { getByTestId } = renderSessionPreview();
expect(getByTestId(SESSION_PREVIEW_TEST_ID)).toBeInTheDocument();
expect(queryByTestId(SESSION_PREVIEW_NO_DATA_TEST_ID)).not.toBeInTheDocument();
expect(queryByTestId(SESSION_PREVIEW_UPSELL_TEST_ID)).not.toBeInTheDocument();
expect(
getByTestId(EXPANDABLE_PANEL_HEADER_TITLE_LINK_TEST_ID(SESSION_PREVIEW_TEST_ID))
).toBeInTheDocument();
expect(
screen.queryByTestId(EXPANDABLE_PANEL_TOGGLE_ICON_TEST_ID(SESSION_PREVIEW_TEST_ID))
).not.toBeInTheDocument();
@ -77,6 +77,12 @@ describe('SessionPreviewContainer', () => {
expect(
screen.queryByTestId(EXPANDABLE_PANEL_HEADER_TITLE_TEXT_TEST_ID(SESSION_PREVIEW_TEST_ID))
).not.toBeInTheDocument();
expect(
screen.queryByTestId(EXPANDABLE_PANEL_CONTENT_TEST_ID(SESSION_PREVIEW_TEST_ID))
).not.toHaveTextContent(NO_DATA_MESSAGE);
expect(
screen.queryByTestId(EXPANDABLE_PANEL_CONTENT_TEST_ID(SESSION_PREVIEW_TEST_ID))
).not.toHaveTextContent(UPSELL_TEXT);
});
it('should render error message and text in header if no sessionConfig', () => {
@ -84,32 +90,35 @@ describe('SessionPreviewContainer', () => {
(useLicense as jest.Mock).mockReturnValue({ isEnterprise: () => true });
const { getByTestId, queryByTestId } = renderSessionPreview();
expect(queryByTestId(SESSION_PREVIEW_TEST_ID)).not.toBeInTheDocument();
expect(getByTestId(SESSION_PREVIEW_NO_DATA_TEST_ID)).toBeInTheDocument();
expect(getByTestId(SESSION_PREVIEW_NO_DATA_TEST_ID)).toHaveTextContent(
'You can only view Linux session details if youve enabled the Include session data setting in your Elastic Defend integration policy. Refer to Enable Session View dataExternal link(opens in a new tab or window) for more information.'
);
expect(queryByTestId(SESSION_PREVIEW_UPSELL_TEST_ID)).not.toBeInTheDocument();
expect(
getByTestId(EXPANDABLE_PANEL_HEADER_TITLE_TEXT_TEST_ID(SESSION_PREVIEW_TEST_ID))
).toBeInTheDocument();
expect(
screen.getByTestId(EXPANDABLE_PANEL_CONTENT_TEST_ID(SESSION_PREVIEW_TEST_ID))
).toHaveTextContent(NO_DATA_MESSAGE);
expect(
screen.queryByTestId(EXPANDABLE_PANEL_CONTENT_TEST_ID(SESSION_PREVIEW_TEST_ID))
).not.toHaveTextContent(UPSELL_TEXT);
expect(queryByTestId(SESSION_PREVIEW_TEST_ID)).not.toBeInTheDocument();
});
it('should render error message and text in header if no correct license', () => {
it('should render upsell message in header if no correct license', () => {
(useSessionPreview as jest.Mock).mockReturnValue(sessionViewConfig);
(useLicense as jest.Mock).mockReturnValue({ isEnterprise: () => false });
const { getByTestId, queryByTestId } = renderSessionPreview();
expect(queryByTestId(SESSION_PREVIEW_TEST_ID)).not.toBeInTheDocument();
expect(queryByTestId(SESSION_PREVIEW_NO_DATA_TEST_ID)).not.toBeInTheDocument();
expect(getByTestId(SESSION_PREVIEW_UPSELL_TEST_ID)).toBeInTheDocument();
expect(getByTestId(SESSION_PREVIEW_UPSELL_TEST_ID)).toHaveTextContent(
'This feature requires an Enterprise subscription'
);
expect(
getByTestId(EXPANDABLE_PANEL_HEADER_TITLE_TEXT_TEST_ID(SESSION_PREVIEW_TEST_ID))
).toBeInTheDocument();
expect(
screen.getByTestId(EXPANDABLE_PANEL_CONTENT_TEST_ID(SESSION_PREVIEW_TEST_ID))
).toHaveTextContent(UPSELL_TEXT);
expect(
screen.queryByTestId(EXPANDABLE_PANEL_CONTENT_TEST_ID(SESSION_PREVIEW_TEST_ID))
).not.toHaveTextContent(NO_DATA_MESSAGE);
expect(queryByTestId(SESSION_PREVIEW_TEST_ID)).not.toBeInTheDocument();
});
});

View file

@ -18,11 +18,7 @@ import { useInvestigateInTimeline } from '../../../detections/components/alerts_
import { useRightPanelContext } from '../context';
import { ALERTS_ACTIONS } from '../../../common/lib/apm/user_actions';
import { ExpandablePanel } from '../../shared/components/expandable_panel';
import {
SESSION_PREVIEW_NO_DATA_TEST_ID,
SESSION_PREVIEW_TEST_ID,
SESSION_PREVIEW_UPSELL_TEST_ID,
} from './test_ids';
import { SESSION_PREVIEW_TEST_ID } from './test_ids';
import { useStartTransaction } from '../../../common/lib/apm/use_start_transaction';
import { setActiveTabTimeline } from '../../../timelines/store/timeline/actions';
import { getScopedActions } from '../../../helpers';
@ -70,54 +66,50 @@ export const SessionPreviewContainer: FC = () => {
const { euiTheme } = useEuiTheme();
const noSessionMessage = !isEnterprisePlus ? (
<div data-test-subj={SESSION_PREVIEW_UPSELL_TEST_ID}>
<FormattedMessage
id="xpack.securitySolution.flyout.right.visualizations.sessionPreview.upsellDescription"
defaultMessage="This feature requires an {subscription}"
values={{
subscription: (
<EuiLink href="https://www.elastic.co/pricing/" target="_blank">
<FormattedMessage
id="xpack.securitySolution.flyout.right.visualizations.sessionPreview.upsellLinkText"
defaultMessage="Enterprise subscription"
/>
</EuiLink>
),
}}
/>
</div>
<FormattedMessage
id="xpack.securitySolution.flyout.right.visualizations.sessionPreview.upsellDescription"
defaultMessage="This feature requires an {subscription}"
values={{
subscription: (
<EuiLink href="https://www.elastic.co/pricing/" target="_blank">
<FormattedMessage
id="xpack.securitySolution.flyout.right.visualizations.sessionPreview.upsellLinkText"
defaultMessage="Enterprise subscription"
/>
</EuiLink>
),
}}
/>
) : !sessionViewConfig ? (
<div data-test-subj={SESSION_PREVIEW_NO_DATA_TEST_ID}>
<FormattedMessage
id="xpack.securitySolution.flyout.right.visualizations.sessionPreview.noDataDescription"
defaultMessage="You can only view Linux session details if youve enabled the {setting} setting in your Elastic Defend integration policy. Refer to {link} for more information."
values={{
setting: (
<span
css={css`
font-weight: ${euiTheme.font.weight.bold};
`}
>
<FormattedMessage
id="xpack.securitySolution.flyout.right.visualizations.sessionPreview.noDataSettingDescription"
defaultMessage="Include session data"
/>
</span>
),
link: (
<EuiLink
href="https://www.elastic.co/guide/en/security/current/session-view.html#enable-session-view"
target="_blank"
>
<FormattedMessage
id="xpack.securitySolution.flyout.right.visualizations.sessionPreview.noDataLinkText"
defaultMessage="Enable Session View data"
/>
</EuiLink>
),
}}
/>
</div>
<FormattedMessage
id="xpack.securitySolution.flyout.right.visualizations.sessionPreview.noDataDescription"
defaultMessage="You can only view Linux session details if youve enabled the {setting} setting in your Elastic Defend integration policy. Refer to {link} for more information."
values={{
setting: (
<span
css={css`
font-weight: ${euiTheme.font.weight.bold};
`}
>
<FormattedMessage
id="xpack.securitySolution.flyout.right.visualizations.sessionPreview.noDataSettingDescription"
defaultMessage="Include session data"
/>
</span>
),
link: (
<EuiLink
href="https://www.elastic.co/guide/en/security/current/session-view.html#enable-session-view"
target="_blank"
>
<FormattedMessage
id="xpack.securitySolution.flyout.right.visualizations.sessionPreview.noDataLinkText"
defaultMessage="Enable Session View data"
/>
</EuiLink>
),
}}
/>
) : null;
return (

View file

@ -28,14 +28,17 @@ export const CHAT_BUTTON_TEST_ID = 'newChatById' as const;
export const ABOUT_SECTION_TEST_ID = `${PREFIX}AboutSection` as const;
export const ABOUT_SECTION_HEADER_TEST_ID = ABOUT_SECTION_TEST_ID + HEADER_TEST_ID;
export const ABOUT_SECTION_CONTENT_TEST_ID = ABOUT_SECTION_TEST_ID + CONTENT_TEST_ID;
export const RULE_SUMMARY_BUTTON_TEST_ID = `${PREFIX}RuleSummaryButton` as const;
const DESCRIPTION_TEST_ID = `${PREFIX}Description` as const;
export const DESCRIPTION_TITLE_TEST_ID = `${DESCRIPTION_TEST_ID}Title` as const;
export const DESCRIPTION_DETAILS_TEST_ID = `${DESCRIPTION_TEST_ID}Details` as const;
const REASON_TEST_ID = `${PREFIX}Reason` as const;
export const REASON_TITLE_TEST_ID = `${REASON_TEST_ID}Title` as const;
export const REASON_DETAILS_TEST_ID = `${REASON_TEST_ID}Details` as const;
export const REASON_DETAILS_PREVIEW_BUTTON_TEST_ID = `${REASON_TEST_ID}PreviewButton` as const;
const MITRE_ATTACK_TEST_ID = `${PREFIX}MitreAttack` as const;
export const MITRE_ATTACK_TITLE_TEST_ID = `${MITRE_ATTACK_TEST_ID}Title` as const;
export const MITRE_ATTACK_DETAILS_TEST_ID = `${MITRE_ATTACK_TEST_ID}Details` as const;
@ -46,10 +49,11 @@ export const INVESTIGATION_SECTION_TEST_ID = `${PREFIX}InvestigationSection` as
export const INVESTIGATION_SECTION_HEADER_TEST_ID = INVESTIGATION_SECTION_TEST_ID + HEADER_TEST_ID;
export const INVESTIGATION_SECTION_CONTENT_TEST_ID =
INVESTIGATION_SECTION_TEST_ID + CONTENT_TEST_ID;
export const INVESTIGATION_GUIDE_TEST_ID = `${PREFIX}InvestigationGuide` as const;
export const INVESTIGATION_GUIDE_BUTTON_TEST_ID = `${INVESTIGATION_GUIDE_TEST_ID}Button` as const;
export const INVESTIGATION_GUIDE_LOADING_TEST_ID = `${INVESTIGATION_GUIDE_TEST_ID}Loading` as const;
export const INVESTIGATION_GUIDE_NO_DATA_TEST_ID = `${INVESTIGATION_GUIDE_TEST_ID}NoData` as const;
const HIGHLIGHTED_FIELDS_TEST_ID = `${PREFIX}HighlightedFields` as const;
export const HIGHLIGHTED_FIELDS_TITLE_TEST_ID = `${HIGHLIGHTED_FIELDS_TEST_ID}Title` as const;
export const HIGHLIGHTED_FIELDS_DETAILS_TEST_ID = `${HIGHLIGHTED_FIELDS_TEST_ID}Details` as const;
@ -75,8 +79,10 @@ export const SUMMARY_ROW_VALUE_TEST_ID = (dataTestSubj: string) => `${dataTestSu
/* Entities */
export const INSIGHTS_ENTITIES_TEST_ID = `${PREFIX}InsightsEntities` as const;
export const INSIGHTS_ENTITIES_NO_DATA_TEST_ID = `${INSIGHTS_ENTITIES_TEST_ID}NoData` as const;
export const ENTITIES_USER_OVERVIEW_TEST_ID = `${INSIGHTS_ENTITIES_TEST_ID}UserOverview` as const;
export const ENTITIES_USER_OVERVIEW_LOADING_TEST_ID =
`${ENTITIES_USER_OVERVIEW_TEST_ID}Loading` as const;
export const ENTITIES_USER_OVERVIEW_LINK_TEST_ID = `${ENTITIES_USER_OVERVIEW_TEST_ID}Link` as const;
export const ENTITIES_USER_OVERVIEW_DOMAIN_TEST_ID =
`${ENTITIES_USER_OVERVIEW_TEST_ID}Domain` as const;
@ -84,7 +90,10 @@ export const ENTITIES_USER_OVERVIEW_LAST_SEEN_TEST_ID =
`${ENTITIES_USER_OVERVIEW_TEST_ID}LastSeen` as const;
export const ENTITIES_USER_OVERVIEW_RISK_LEVEL_TEST_ID =
`${ENTITIES_USER_OVERVIEW_TEST_ID}RiskLevel` as const;
export const ENTITIES_HOST_OVERVIEW_TEST_ID = `${INSIGHTS_ENTITIES_TEST_ID}HostOverview` as const;
export const ENTITIES_HOST_OVERVIEW_LOADING_TEST_ID =
`${ENTITIES_HOST_OVERVIEW_TEST_ID}Loading` as const;
export const ENTITIES_HOST_OVERVIEW_LINK_TEST_ID = `${ENTITIES_HOST_OVERVIEW_TEST_ID}Link` as const;
export const ENTITIES_HOST_OVERVIEW_OS_FAMILY_TEST_ID =
`${ENTITIES_HOST_OVERVIEW_TEST_ID}OsFamily` as const;
@ -102,7 +111,6 @@ export const INSIGHTS_THREAT_INTELLIGENCE_TEST_ID = `${PREFIX}InsightsThreatInte
/* Correlations */
export const CORRELATIONS_TEST_ID = `${PREFIX}Correlations` as const;
export const CORRELATIONS_NO_DATA_TEST_ID = `${CORRELATIONS_TEST_ID}NoData` as const;
export const CORRELATIONS_SUPPRESSED_ALERTS_TEST_ID =
`${CORRELATIONS_TEST_ID}SuppressedAlerts` as const;
export const CORRELATIONS_SUPPRESSED_ALERTS_TECHNICAL_PREVIEW_TEST_ID =
@ -118,7 +126,6 @@ export const CORRELATIONS_RELATED_ALERTS_BY_ANCESTRY_TEST_ID =
/* Insights Prevalence */
export const PREVALENCE_TEST_ID = `${PREFIX}InsightsPrevalence` as const;
export const PREVALENCE_NO_DATA_TEST_ID = `${PREVALENCE_TEST_ID}NoData` as const;
/* Visualizations section */
@ -127,10 +134,10 @@ export const VISUALIZATIONS_SECTION_TEST_ID = `${VISUALIZATIONS_TEST_ID}Title` a
export const VISUALIZATIONS_SECTION_HEADER_TEST_ID =
`${VISUALIZATIONS_TEST_ID}TitleHeader` as const;
export const ANALYZER_PREVIEW_TEST_ID = `${PREFIX}AnalyzerPreview` as const;
export const ANALYZER_PREVIEW_NO_DATA_TEST_ID = `${ANALYZER_PREVIEW_TEST_ID}NoData` as const;
export const ANALYZER_PREVIEW_LOADING_TEST_ID = `${ANALYZER_PREVIEW_TEST_ID}Loading` as const;
export const SESSION_PREVIEW_TEST_ID = `${PREFIX}SessionPreview` as const;
export const SESSION_PREVIEW_UPSELL_TEST_ID = `${SESSION_PREVIEW_TEST_ID}UpSell` as const;
export const SESSION_PREVIEW_NO_DATA_TEST_ID = `${SESSION_PREVIEW_TEST_ID}NoData` as const;
/* Response section */

View file

@ -134,9 +134,9 @@ describe('<ThreatIntelligenceOverview />', () => {
loading: true,
});
const { getAllByTestId } = render(renderThreatIntelligenceOverview(panelContextValue));
const { getByTestId } = render(renderThreatIntelligenceOverview(panelContextValue));
expect(getAllByTestId(LOADING_TEST_ID)).toHaveLength(2);
expect(getByTestId(LOADING_TEST_ID)).toBeInTheDocument();
});
it('should navigate to left section Insights tab when clicking on button', () => {

View file

@ -67,6 +67,7 @@ export const ThreatIntelligenceOverview: FC = () => {
iconType: 'arrowStart',
}}
data-test-subj={INSIGHTS_THREAT_INTELLIGENCE_TEST_ID}
content={{ loading }}
>
<EuiFlexGroup
direction="column"
@ -74,7 +75,6 @@ export const ThreatIntelligenceOverview: FC = () => {
data-test-subj={`${INSIGHTS_THREAT_INTELLIGENCE_TEST_ID}Container`}
>
<InsightsSummaryRow
loading={loading}
icon={'warning'}
value={threatMatchesCount}
text={
@ -87,7 +87,6 @@ export const ThreatIntelligenceOverview: FC = () => {
data-test-subj={INSIGHTS_THREAT_INTELLIGENCE_TEST_ID}
/>
<InsightsSummaryRow
loading={loading}
icon={'warning'}
value={threatEnrichmentsCount}
text={

View file

@ -15,6 +15,7 @@ import {
ENTITIES_USER_OVERVIEW_LAST_SEEN_TEST_ID,
ENTITIES_USER_OVERVIEW_LINK_TEST_ID,
ENTITIES_USER_OVERVIEW_RISK_LEVEL_TEST_ID,
ENTITIES_USER_OVERVIEW_LOADING_TEST_ID,
} from './test_ids';
import { useObservedUserDetails } from '../../../explore/users/containers/users/observed_details';
import { mockContextValue } from '../mocks/mock_context';
@ -122,6 +123,36 @@ describe('<UserEntityOverview />', () => {
expect(getByTestId(ENTITIES_USER_OVERVIEW_LAST_SEEN_TEST_ID)).toHaveTextContent('—');
});
it('should render loading if user details returns loading as true', () => {
mockUseUserDetails.mockReturnValue([true, { userDetails: null }]);
mockUseRiskScore.mockReturnValue({ data: null, isAuthorized: true });
const { getByTestId, queryByTestId } = render(
<TestProviders>
<RightPanelContext.Provider value={panelContextValue}>
<UserEntityOverview userName={userName} />
</RightPanelContext.Provider>
</TestProviders>
);
expect(getByTestId(ENTITIES_USER_OVERVIEW_LOADING_TEST_ID)).toBeInTheDocument();
expect(queryByTestId(ENTITIES_USER_OVERVIEW_DOMAIN_TEST_ID)).not.toBeInTheDocument();
});
it('should render loading if risk score returns loading as true', () => {
mockUseUserDetails.mockReturnValue([false, { userDetails: null }]);
mockUseRiskScore.mockReturnValue({ data: null, isAuthorized: true, loading: true });
const { getByTestId, queryByTestId } = render(
<TestProviders>
<RightPanelContext.Provider value={panelContextValue}>
<UserEntityOverview userName={userName} />
</RightPanelContext.Provider>
</TestProviders>
);
expect(getByTestId(ENTITIES_USER_OVERVIEW_LOADING_TEST_ID)).toBeInTheDocument();
expect(queryByTestId(ENTITIES_USER_OVERVIEW_DOMAIN_TEST_ID)).not.toBeInTheDocument();
});
it('should navigate to left panel entities tab when clicking on title', () => {
mockUseUserDetails.mockReturnValue([false, { userDetails: userData }]);
mockUseRiskScore.mockReturnValue({ data: riskLevel, isAuthorized: true });

View file

@ -14,9 +14,11 @@ import {
useEuiTheme,
useEuiFontSize,
EuiIconTip,
EuiSkeletonText,
} from '@elastic/eui';
import { css } from '@emotion/css';
import { getOr } from 'lodash/fp';
import { i18n } from '@kbn/i18n';
import { useExpandableFlyoutContext } from '@kbn/expandable-flyout';
import { FormattedMessage } from '@kbn/i18n-react';
import { LeftPanelInsightsTab, LeftPanelKey } from '../../left';
@ -36,7 +38,11 @@ import { RiskScore } from '../../../explore/components/risk_score/severity/commo
import { useSourcererDataView } from '../../../common/containers/sourcerer';
import { useGlobalTime } from '../../../common/containers/use_global_time';
import { useRiskScore } from '../../../explore/containers/risk_score';
import * as i18n from '../../../overview/components/user_overview/translations';
import {
USER_DOMAIN,
LAST_SEEN,
USER_RISK_CLASSIFICATION,
} from '../../../overview/components/user_overview/translations';
import {
ENTITIES_USER_OVERVIEW_TEST_ID,
ENTITIES_USER_OVERVIEW_DOMAIN_TEST_ID,
@ -44,6 +50,7 @@ import {
ENTITIES_USER_OVERVIEW_RISK_LEVEL_TEST_ID,
ENTITIES_USER_OVERVIEW_LINK_TEST_ID,
TECHNICAL_PREVIEW_ICON_TEST_ID,
ENTITIES_USER_OVERVIEW_LOADING_TEST_ID,
} from './test_ids';
import { useObservedUserDetails } from '../../../explore/users/containers/users/observed_details';
@ -90,14 +97,18 @@ export const UserEntityOverview: React.FC<UserEntityOverviewProps> = ({ userName
() => (userName ? buildUserNamesFilter([userName]) : undefined),
[userName]
);
const [_, { userDetails }] = useObservedUserDetails({
const [isUserDetailsLoading, { userDetails }] = useObservedUserDetails({
endDate: to,
userName,
indexNames: selectedPatterns,
startDate: from,
});
const { data: userRisk, isAuthorized } = useRiskScore({
const {
data: userRisk,
isAuthorized,
loading: isRiskScoreLoading,
} = useRiskScore({
filterQuery,
riskEntity: RiskScoreEntity.user,
timerange,
@ -106,7 +117,7 @@ export const UserEntityOverview: React.FC<UserEntityOverviewProps> = ({ userName
const userDomain: DescriptionList[] = useMemo(
() => [
{
title: i18n.USER_DOMAIN,
title: USER_DOMAIN,
description: (
<DefaultFieldRenderer
rowItems={getOr([], 'user.domain', userDetails)}
@ -123,7 +134,7 @@ export const UserEntityOverview: React.FC<UserEntityOverviewProps> = ({ userName
const userLastSeen: DescriptionList[] = useMemo(
() => [
{
title: i18n.LAST_SEEN,
title: LAST_SEEN,
description: (
<FirstLastSeen
indexPatterns={selectedPatterns}
@ -147,7 +158,7 @@ export const UserEntityOverview: React.FC<UserEntityOverviewProps> = ({ userName
{
title: (
<>
{i18n.USER_RISK_CLASSIFICATION}
{USER_RISK_CLASSIFICATION}
<EuiIconTip
title={
<FormattedMessage
@ -206,27 +217,37 @@ export const UserEntityOverview: React.FC<UserEntityOverviewProps> = ({ userName
</EuiFlexGroup>
</EuiFlexItem>
<EuiFlexItem>
<EuiFlexGroup>
<EuiFlexItem>
<OverviewDescriptionList
dataTestSubj={ENTITIES_USER_OVERVIEW_DOMAIN_TEST_ID}
descriptionList={userDomain}
/>
</EuiFlexItem>
<EuiFlexItem>
{isAuthorized ? (
<DescriptionListStyled
data-test-subj={ENTITIES_USER_OVERVIEW_RISK_LEVEL_TEST_ID}
listItems={[userRiskLevel]}
/>
) : (
<OverviewDescriptionList
dataTestSubj={ENTITIES_USER_OVERVIEW_LAST_SEEN_TEST_ID}
descriptionList={userLastSeen}
/>
{isUserDetailsLoading || isRiskScoreLoading ? (
<EuiSkeletonText
contentAriaLabel={i18n.translate(
'xpack.securitySolution.flyout.right.insights.entities.userLoadingAriaLabel',
{ defaultMessage: 'user overview' }
)}
</EuiFlexItem>
</EuiFlexGroup>
data-test-subj={ENTITIES_USER_OVERVIEW_LOADING_TEST_ID}
/>
) : (
<EuiFlexGroup>
<EuiFlexItem>
<OverviewDescriptionList
dataTestSubj={ENTITIES_USER_OVERVIEW_DOMAIN_TEST_ID}
descriptionList={userDomain}
/>
</EuiFlexItem>
<EuiFlexItem>
{isAuthorized ? (
<DescriptionListStyled
data-test-subj={ENTITIES_USER_OVERVIEW_RISK_LEVEL_TEST_ID}
listItems={[userRiskLevel]}
/>
) : (
<OverviewDescriptionList
dataTestSubj={ENTITIES_USER_OVERVIEW_LAST_SEEN_TEST_ID}
descriptionList={userLastSeen}
/>
)}
</EuiFlexItem>
</EuiFlexGroup>
)}
</EuiFlexItem>
</EuiFlexGroup>
);

View file

@ -16,13 +16,13 @@ import {
EuiLink,
EuiTitle,
EuiText,
EuiLoadingSpinner,
useEuiTheme,
EuiToolTip,
EuiSkeletonText,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import type { IconType } from '@elastic/eui';
import { css } from '@emotion/react';
import { i18n } from '@kbn/i18n';
export interface ExpandablePanelPanelProps {
header: {
@ -197,11 +197,13 @@ export const ExpandablePanel: React.FC<ExpandablePanelPanelProps> = ({
}, [children, expandable, toggleStatus]);
const content = loading ? (
<EuiFlexGroup justifyContent="center">
<EuiFlexItem grow={false}>
<EuiLoadingSpinner data-test-subj={`${dataTestSubj}Loading`} />
</EuiFlexItem>
</EuiFlexGroup>
<EuiSkeletonText
data-test-subj={`${dataTestSubj}Loading`}
contentAriaLabel={i18n.translate(
'xpack.securitySolution.flyout.shared.expandablePanelLoadingAriaLabel',
{ defaultMessage: 'expandable panel' }
)}
/>
) : error ? null : (
children
);

View file

@ -15,4 +15,10 @@ describe('<FlyoutLoading />', () => {
const { getByTestId } = render(<FlyoutLoading />);
expect(getByTestId(FLYOUT_LOADING_TEST_ID)).toBeInTheDocument();
});
it('should render loading when data test subject is passed', () => {
const { getByTestId, queryByTestId } = render(<FlyoutLoading data-test-subj="test-id" />);
expect(getByTestId('test-id')).toBeInTheDocument();
expect(queryByTestId(FLYOUT_LOADING_TEST_ID)).not.toBeInTheDocument();
});
});

View file

@ -10,18 +10,25 @@ import { EuiFlexItem, EuiLoadingSpinner } from '@elastic/eui';
import { css } from '@emotion/react';
import { FLYOUT_LOADING_TEST_ID } from '../test_ids';
interface FlyoutLoadingProps {
/**
Data test subject string for testing
*/
['data-test-subj']?: string;
}
/**
* Use this when you need to show a loading state in the flyout
*/
export const FlyoutLoading: React.VFC = () => (
export const FlyoutLoading: React.FC<FlyoutLoadingProps> = ({
'data-test-subj': dataTestSubj = FLYOUT_LOADING_TEST_ID,
}) => (
<EuiFlexItem
css={css`
align-items: center;
justify-content: center;
`}
>
<EuiLoadingSpinner size="xxl" data-test-subj={FLYOUT_LOADING_TEST_ID} />
<EuiLoadingSpinner size="xxl" data-test-subj={dataTestSubj} />
</EuiFlexItem>
);
FlyoutLoading.displayName = 'FlyoutLoading';

View file

@ -5,10 +5,7 @@
* 2.0.
*/
import {
DOCUMENT_DETAILS_FLYOUT_VISUALIZE_TAB_SESSION_VIEW_BUTTON,
DOCUMENT_DETAILS_FLYOUT_VISUALIZE_TAB_SESSION_VIEW_ERROR,
} from '../../../../screens/expandable_flyout/alert_details_left_panel_session_view_tab';
import { DOCUMENT_DETAILS_FLYOUT_VISUALIZE_TAB_SESSION_VIEW_BUTTON } from '../../../../screens/expandable_flyout/alert_details_left_panel_session_view_tab';
import {
DOCUMENT_DETAILS_FLYOUT_VISUALIZE_TAB,
DOCUMENT_DETAILS_FLYOUT_VISUALIZE_TAB_BUTTON_GROUP,
@ -48,12 +45,6 @@ describe.skip(
cy.get(DOCUMENT_DETAILS_FLYOUT_VISUALIZE_TAB_SESSION_VIEW_BUTTON)
.should('be.visible')
.and('have.text', 'Session View');
// TODO ideally we would have a test for the session view component instead
cy.get(DOCUMENT_DETAILS_FLYOUT_VISUALIZE_TAB_SESSION_VIEW_ERROR)
.should('be.visible')
.and('contain.text', 'Unable to display session view')
.and('contain.text', 'There was an error displaying session view');
});
}
);

View file

@ -6,10 +6,15 @@
*/
import { RESPONSE_TAB_TEST_ID } from '@kbn/security-solution-plugin/public/flyout/left/test_ids';
import { RESPONSE_NO_DATA_TEST_ID } from '@kbn/security-solution-plugin/public/flyout/left/components/test_ids';
import {
RESPONSE_DETAILS_TEST_ID,
RESPONSE_NO_DATA_TEST_ID,
} from '@kbn/security-solution-plugin/public/flyout/left/components/test_ids';
import { getDataTestSubjectSelector } from '../../helpers/common';
export const DOCUMENT_DETAILS_FLYOUT_RESPONSE_TAB =
getDataTestSubjectSelector(RESPONSE_TAB_TEST_ID);
export const DOCUMENT_DETAILS_FLYOUT_RESPONSE_DETAILS =
getDataTestSubjectSelector(RESPONSE_DETAILS_TEST_ID);
export const DOCUMENT_DETAILS_FLYOUT_RESPONSE_EMPTY =
getDataTestSubjectSelector(RESPONSE_NO_DATA_TEST_ID);

View file

@ -6,12 +6,8 @@
*/
import { VISUALIZE_TAB_SESSION_VIEW_BUTTON_TEST_ID } from '@kbn/security-solution-plugin/public/flyout/left/tabs/test_ids';
import { SESSION_VIEW_ERROR_TEST_ID } from '@kbn/security-solution-plugin/public/flyout/left/components/test_ids';
import { getDataTestSubjectSelector } from '../../helpers/common';
export const DOCUMENT_DETAILS_FLYOUT_VISUALIZE_TAB_SESSION_VIEW_BUTTON = getDataTestSubjectSelector(
VISUALIZE_TAB_SESSION_VIEW_BUTTON_TEST_ID
);
export const DOCUMENT_DETAILS_FLYOUT_VISUALIZE_TAB_SESSION_VIEW_ERROR = getDataTestSubjectSelector(
SESSION_VIEW_ERROR_TEST_ID
);