mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[Security Solution] Add host/user name hover action to Entity analytics page (#144819)
issue: https://github.com/elastic/kibana/issues/144501 ## Summary Add host/user name hover action to Entity Analytics page. https://user-images.githubusercontent.com/1490444/200599973-415049c3-f2bb-4ae4-9052-c58db87d32ea.mov Why? To Improve triage by allowing users to investigate a Host. *** The UX is aligned with the event view UI. When the user hovers a table row it displays all hover actions. ### Extras * Extract duplicated topN logic to a hook for reusability. * Improve Entity analytics page header and risk table for small screens **Before** <img width="400px" src="https://user-images.githubusercontent.com/1490444/200850153-c81a0d73-1f59-4384-b721-37cbebbbd35d.png"> **After** <img width="250px" src="https://user-images.githubusercontent.com/1490444/200850149-d8a3aaa4-db04-4b13-a266-71eedb1ffa97.png">
This commit is contained in:
parent
b1d2211f9f
commit
cbc7fe10f6
12 changed files with 217 additions and 58 deletions
|
@ -155,7 +155,7 @@ const DraggableOnWrapperComponent: React.FC<Props> = ({
|
|||
openPopover,
|
||||
onFocus,
|
||||
setContainerRef,
|
||||
showTopN,
|
||||
isShowingTopN,
|
||||
} = useHoverActions({
|
||||
dataProvider,
|
||||
hideTopN,
|
||||
|
@ -307,7 +307,7 @@ const DraggableOnWrapperComponent: React.FC<Props> = ({
|
|||
|
||||
return (
|
||||
<WithHoverActions
|
||||
alwaysShow={showTopN || hoverActionsOwnFocus}
|
||||
alwaysShow={isShowingTopN || hoverActionsOwnFocus}
|
||||
closePopOverTrigger={closePopOverTrigger}
|
||||
hoverContent={hoverContent}
|
||||
onCloseRequested={onCloseRequested}
|
||||
|
@ -333,7 +333,7 @@ const DraggableWrapperComponent: React.FC<Props> = ({
|
|||
hoverContent,
|
||||
onCloseRequested,
|
||||
setContainerRef,
|
||||
showTopN,
|
||||
isShowingTopN,
|
||||
} = useHoverActions({
|
||||
dataProvider,
|
||||
hideTopN,
|
||||
|
@ -372,7 +372,7 @@ const DraggableWrapperComponent: React.FC<Props> = ({
|
|||
if (!isDraggable) {
|
||||
return (
|
||||
<WithHoverActions
|
||||
alwaysShow={showTopN || hoverActionsOwnFocus}
|
||||
alwaysShow={isShowingTopN || hoverActionsOwnFocus}
|
||||
closePopOverTrigger={closePopOverTrigger}
|
||||
hoverContent={disableHoverActions(scopeId) ? undefined : hoverContent}
|
||||
onCloseRequested={onCloseRequested}
|
||||
|
|
|
@ -5,12 +5,13 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { useCallback, useState, useContext } from 'react';
|
||||
import React, { useContext } from 'react';
|
||||
import { TimelineContext } from '../../../../timelines/components/timeline';
|
||||
import { HoverActions } from '../../hover_actions';
|
||||
import { useActionCellDataProvider } from './use_action_cell_data_provider';
|
||||
import type { EnrichedFieldInfo } from '../types';
|
||||
import type { ColumnHeaderOptions } from '../../../../../common/types/timeline';
|
||||
import { useTopNPopOver } from '../../hover_actions/utils';
|
||||
|
||||
interface Props extends EnrichedFieldInfo {
|
||||
contextId: string;
|
||||
|
@ -51,22 +52,9 @@ export const ActionCell: React.FC<Props> = React.memo(
|
|||
values,
|
||||
});
|
||||
|
||||
const { closeTopN, toggleTopN, isShowingTopN } = useTopNPopOver(setIsPopoverVisible);
|
||||
const { aggregatable, type } = fieldFromBrowserField || { aggregatable: false, type: '' };
|
||||
|
||||
const [showTopN, setShowTopN] = useState<boolean>(false);
|
||||
const { timelineId: timelineIdFind } = useContext(TimelineContext);
|
||||
const [hoverActionsOwnFocus] = useState<boolean>(false);
|
||||
const toggleTopN = useCallback(() => {
|
||||
setShowTopN((prevShowTopN) => {
|
||||
const newShowTopN = !prevShowTopN;
|
||||
if (setIsPopoverVisible) setIsPopoverVisible(newShowTopN);
|
||||
return newShowTopN;
|
||||
});
|
||||
}, [setIsPopoverVisible]);
|
||||
|
||||
const closeTopN = useCallback(() => {
|
||||
setShowTopN(false);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<HoverActions
|
||||
|
@ -81,8 +69,8 @@ export const ActionCell: React.FC<Props> = React.memo(
|
|||
hideAddToTimeline={hideAddToTimeline}
|
||||
isObjectArray={data.isObjectArray}
|
||||
onFilterAdded={onFilterAdded}
|
||||
ownFocus={hoverActionsOwnFocus}
|
||||
showTopN={showTopN}
|
||||
ownFocus={false}
|
||||
showTopN={isShowingTopN}
|
||||
scopeId={scopeId ?? timelineIdFind}
|
||||
toggleColumn={toggleColumn}
|
||||
toggleTopN={toggleTopN}
|
||||
|
|
|
@ -16,6 +16,7 @@ import type { ColumnHeaderOptions, DataProvider } from '../../../../common/types
|
|||
import { TimelineId } from '../../../../common/types/timeline';
|
||||
import { SHOW_TOP_N_KEYBOARD_SHORTCUT } from './keyboard_shortcut_constants';
|
||||
import { useHoverActionItems } from './use_hover_action_items';
|
||||
import { SecurityPageName } from '../../../app/types';
|
||||
|
||||
export const YOU_ARE_IN_A_DIALOG_CONTAINING_OPTIONS = (fieldName: string) =>
|
||||
i18n.translate(
|
||||
|
@ -217,10 +218,12 @@ export const HoverActions: React.FC<Props> = React.memo(
|
|||
const isCaseView = scopeId === TimelineId.casePage;
|
||||
const isTimelineView = scopeId === TimelineId.active;
|
||||
const isAlertDetailsView = scopeId === TimelineId.detectionsAlertDetailsPage;
|
||||
// TODO Provide a list of disabled/enabled actions as props
|
||||
const isEntityAnalyticsPage = scopeId === SecurityPageName.entityAnalytics;
|
||||
|
||||
const hideFilters = useMemo(
|
||||
() => isAlertDetailsView && !isTimelineView,
|
||||
[isTimelineView, isAlertDetailsView]
|
||||
() => (isAlertDetailsView || isEntityAnalyticsPage) && !isTimelineView,
|
||||
[isTimelineView, isAlertDetailsView, isEntityAnalyticsPage]
|
||||
);
|
||||
|
||||
const hiddenActionsCount = useMemo(() => {
|
||||
|
|
|
@ -14,6 +14,7 @@ import { HoverActions } from '.';
|
|||
import type { DataProvider } from '../../../../common/types';
|
||||
import { ProviderContentWrapper } from '../drag_and_drop/draggable_wrapper';
|
||||
import { getDraggableId } from '../drag_and_drop/helpers';
|
||||
import { useTopNPopOver } from './utils';
|
||||
|
||||
const draggableContainsLinks = (draggableElement: HTMLDivElement | null) => {
|
||||
const links = draggableElement?.querySelectorAll('.euiLink') ?? [];
|
||||
|
@ -55,7 +56,6 @@ export const useHoverActions = ({
|
|||
const containerRef = useRef<HTMLDivElement | null>(null);
|
||||
const keyboardHandlerRef = useRef<HTMLDivElement | null>(null);
|
||||
const [closePopOverTrigger, setClosePopOverTrigger] = useState(false);
|
||||
const [showTopN, setShowTopN] = useState<boolean>(false);
|
||||
const [hoverActionsOwnFocus, setHoverActionsOwnFocus] = useState<boolean>(false);
|
||||
const id = useMemo(
|
||||
() => (!scopeId ? timelineIdFind ?? tableIdFind : scopeId),
|
||||
|
@ -78,25 +78,13 @@ export const useHoverActions = ({
|
|||
}, 0); // invoked on the next tick, because we want to restore focus first
|
||||
}, [keyboardHandlerRef]);
|
||||
|
||||
const toggleTopN = useCallback(() => {
|
||||
setShowTopN((prevShowTopN) => {
|
||||
const newShowTopN = !prevShowTopN;
|
||||
if (newShowTopN === false) {
|
||||
handleClosePopOverTrigger();
|
||||
}
|
||||
return newShowTopN;
|
||||
});
|
||||
}, [handleClosePopOverTrigger]);
|
||||
|
||||
const closeTopN = useCallback(() => {
|
||||
setShowTopN(false);
|
||||
}, []);
|
||||
const { closeTopN, toggleTopN, isShowingTopN } = useTopNPopOver(handleClosePopOverTrigger);
|
||||
|
||||
const hoverContent = useMemo(() => {
|
||||
// display links as additional content in the hover menu to enable keyboard
|
||||
// navigation of links (when the draggable contains them):
|
||||
const additionalContent =
|
||||
hoverActionsOwnFocus && !showTopN && draggableContainsLinks(containerRef.current) ? (
|
||||
hoverActionsOwnFocus && !isShowingTopN && draggableContainsLinks(containerRef.current) ? (
|
||||
<ProviderContentWrapper
|
||||
data-test-subj={`draggable-link-content-${dataProvider.queryMatch.field}`}
|
||||
>
|
||||
|
@ -119,7 +107,7 @@ export const useHoverActions = ({
|
|||
onFilterAdded={onFilterAdded}
|
||||
ownFocus={hoverActionsOwnFocus}
|
||||
showOwnFocus={false}
|
||||
showTopN={showTopN}
|
||||
showTopN={isShowingTopN}
|
||||
scopeId={id}
|
||||
toggleTopN={toggleTopN}
|
||||
values={
|
||||
|
@ -131,7 +119,7 @@ export const useHoverActions = ({
|
|||
);
|
||||
}, [
|
||||
hoverActionsOwnFocus,
|
||||
showTopN,
|
||||
isShowingTopN,
|
||||
dataProvider,
|
||||
render,
|
||||
closeTopN,
|
||||
|
@ -156,7 +144,7 @@ export const useHoverActions = ({
|
|||
}, [hoverActionsOwnFocus, keyboardHandlerRef]);
|
||||
|
||||
const onCloseRequested = useCallback(() => {
|
||||
setShowTopN(false);
|
||||
closeTopN();
|
||||
|
||||
if (hoverActionsOwnFocus) {
|
||||
setHoverActionsOwnFocus(false);
|
||||
|
@ -165,7 +153,7 @@ export const useHoverActions = ({
|
|||
onFocus(); // return focus to this draggable on the next tick, because we owned focus
|
||||
}, 0);
|
||||
}
|
||||
}, [onFocus, hoverActionsOwnFocus]);
|
||||
}, [onFocus, hoverActionsOwnFocus, closeTopN]);
|
||||
|
||||
const openPopover = useCallback(() => {
|
||||
setHoverActionsOwnFocus(true);
|
||||
|
@ -182,7 +170,7 @@ export const useHoverActions = ({
|
|||
onFocus,
|
||||
openPopover,
|
||||
setContainerRef,
|
||||
showTopN,
|
||||
isShowingTopN,
|
||||
}),
|
||||
[
|
||||
closePopOverTrigger,
|
||||
|
@ -193,7 +181,7 @@ export const useHoverActions = ({
|
|||
onFocus,
|
||||
openPopover,
|
||||
setContainerRef,
|
||||
showTopN,
|
||||
isShowingTopN,
|
||||
]
|
||||
);
|
||||
};
|
||||
|
|
|
@ -5,6 +5,8 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { useCallback, useState } from 'react';
|
||||
|
||||
export const getAdditionalScreenReaderOnlyContext = ({
|
||||
field,
|
||||
value,
|
||||
|
@ -18,3 +20,20 @@ export const getAdditionalScreenReaderOnlyContext = ({
|
|||
|
||||
return Array.isArray(value) ? `${field} ${value.join(' ')}` : `${field} ${value}`;
|
||||
};
|
||||
|
||||
export const useTopNPopOver = (setIsPopoverVisible?: (isVisible: boolean) => void) => {
|
||||
const [isShowingTopN, setShowTopN] = useState<boolean>(false);
|
||||
const toggleTopN = useCallback(() => {
|
||||
setShowTopN((prevShowTopN) => {
|
||||
const newShowTopN = !prevShowTopN;
|
||||
if (setIsPopoverVisible) setIsPopoverVisible(newShowTopN);
|
||||
return newShowTopN;
|
||||
});
|
||||
}, [setIsPopoverVisible]);
|
||||
|
||||
const closeTopN = useCallback(() => {
|
||||
setShowTopN(false);
|
||||
}, []);
|
||||
|
||||
return { closeTopN, toggleTopN, isShowingTopN };
|
||||
};
|
||||
|
|
|
@ -0,0 +1,38 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { render } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import { useKibana as mockUseKibana } from '../../../../common/lib/kibana/__mocks__';
|
||||
import { TestProviders } from '../../../../common/mock';
|
||||
import { EntityAnalyticsHoverActions } from './entity_hover_actions';
|
||||
|
||||
const mockedUseKibana = mockUseKibana();
|
||||
|
||||
jest.mock('../../../../common/lib/kibana', () => {
|
||||
const original = jest.requireActual('../../../../common/lib/kibana');
|
||||
return {
|
||||
...original,
|
||||
useKibana: () => mockedUseKibana,
|
||||
};
|
||||
});
|
||||
|
||||
describe('EntityAnalyticsHoverActions', () => {
|
||||
it('it renders "add to timeline" and "copy" hover action', () => {
|
||||
const { getByTestId } = render(
|
||||
<EntityAnalyticsHoverActions
|
||||
idPrefix={`my-test-field`}
|
||||
fieldName={'test.field'}
|
||||
fieldValue={'testValue'}
|
||||
/>,
|
||||
{ wrapper: TestProviders }
|
||||
);
|
||||
|
||||
expect(getByTestId('test-add-to-timeline')).toBeInTheDocument();
|
||||
expect(getByTestId('test-copy-button')).toBeInTheDocument();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,72 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { useMemo } from 'react';
|
||||
import { noop } from 'lodash/fp';
|
||||
import type { DataProvider } from '../../../../../common/types';
|
||||
import { IS_OPERATOR } from '../../../../../common/types';
|
||||
import { escapeDataProviderId } from '../../../../common/components/drag_and_drop/helpers';
|
||||
import { SecurityPageName } from '../../../../app/types';
|
||||
import { HoverActions } from '../../../../common/components/hover_actions';
|
||||
|
||||
interface Props {
|
||||
onFilterAdded?: () => void;
|
||||
fieldName: string;
|
||||
fieldValue: string;
|
||||
idPrefix: string;
|
||||
}
|
||||
|
||||
export const EntityAnalyticsHoverActions: React.FC<Props> = ({
|
||||
fieldName,
|
||||
fieldValue,
|
||||
idPrefix,
|
||||
onFilterAdded,
|
||||
}) => {
|
||||
const id = useMemo(
|
||||
() => escapeDataProviderId(`${idPrefix}-${fieldName}-${fieldValue}`),
|
||||
[idPrefix, fieldName, fieldValue]
|
||||
);
|
||||
const dataProvider: DataProvider = useMemo(
|
||||
() => ({
|
||||
and: [],
|
||||
enabled: true,
|
||||
id,
|
||||
name: fieldValue,
|
||||
excluded: false,
|
||||
kqlQuery: '',
|
||||
queryMatch: {
|
||||
field: fieldName,
|
||||
value: fieldValue,
|
||||
displayValue: fieldValue,
|
||||
operator: IS_OPERATOR,
|
||||
},
|
||||
}),
|
||||
[fieldName, fieldValue, id]
|
||||
);
|
||||
|
||||
return (
|
||||
<HoverActions
|
||||
applyWidthAndPadding
|
||||
closeTopN={noop}
|
||||
dataProvider={dataProvider}
|
||||
dataType={'string'}
|
||||
field={fieldName}
|
||||
fieldType={'keyword'}
|
||||
hideTopN={true}
|
||||
isAggregatable
|
||||
isObjectArray={false}
|
||||
onFilterAdded={onFilterAdded}
|
||||
ownFocus={false}
|
||||
scopeId={SecurityPageName.entityAnalytics}
|
||||
showTopN={false}
|
||||
toggleTopN={noop}
|
||||
values={[fieldValue]}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
EntityAnalyticsHoverActions.displayName = 'EntityAnalyticsHoverActions';
|
|
@ -8,10 +8,30 @@ import styled from 'styled-components';
|
|||
import { EuiBasicTable } from '@elastic/eui';
|
||||
|
||||
// @ts-expect-error TS2769
|
||||
export const BasicTableWithoutBorderBottom = styled(EuiBasicTable)`
|
||||
export const StyledBasicTable = styled(EuiBasicTable)`
|
||||
.euiTableRow {
|
||||
.euiTableRowCell {
|
||||
border-bottom: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
.timelines__hoverActionButton {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.EntityAnalyticsTableHoverActions {
|
||||
.hoverActions-active {
|
||||
.timelines__hoverActionButton,
|
||||
.securitySolution__hoverActionButton {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.timelines__hoverActionButton,
|
||||
.securitySolution__hoverActionButton {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
|
@ -31,6 +31,7 @@ import { useMlCapabilities } from '../../../../common/components/ml/hooks/use_ml
|
|||
import { useQueryInspector } from '../../../../common/components/page/manage_query';
|
||||
import { ENTITY_ANALYTICS_ANOMALIES_PANEL } from '../anomalies';
|
||||
import { isJobStarted } from '../../../../../common/machine_learning/helpers';
|
||||
import { FormattedCount } from '../../../../common/components/formatted_number';
|
||||
|
||||
const StyledEuiTitle = styled(EuiTitle)`
|
||||
color: ${({ theme: { eui } }) => eui.euiColorDanger};
|
||||
|
@ -150,7 +151,7 @@ export const EntityAnalyticsHeader = () => {
|
|||
);
|
||||
|
||||
const totalAnomalies = useMemo(
|
||||
() => (areJobsEnabled ? sumBy('count', data) : '-'),
|
||||
() => (areJobsEnabled ? <FormattedCount count={sumBy('count', data)} /> : '-'),
|
||||
[data, areJobsEnabled]
|
||||
);
|
||||
|
||||
|
@ -165,14 +166,18 @@ export const EntityAnalyticsHeader = () => {
|
|||
|
||||
return (
|
||||
<EuiPanel hasBorder paddingSize="l">
|
||||
<EuiFlexGroup justifyContent="spaceAround">
|
||||
<EuiFlexGroup justifyContent="spaceAround" responsive={false}>
|
||||
{isPlatinumOrTrialLicense && (
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiFlexGroup direction="column" gutterSize="s">
|
||||
<EuiFlexGroup direction="column" gutterSize="s" responsive={false}>
|
||||
<EuiFlexItem className="eui-textCenter">
|
||||
<StyledEuiTitle data-test-subj="critical_hosts_quantity" size="l">
|
||||
<span>
|
||||
{hostsSeverityCount ? hostsSeverityCount[RiskSeverity.critical] : '-'}
|
||||
{hostsSeverityCount ? (
|
||||
<FormattedCount count={hostsSeverityCount[RiskSeverity.critical]} />
|
||||
) : (
|
||||
'-'
|
||||
)}
|
||||
</span>
|
||||
</StyledEuiTitle>
|
||||
</EuiFlexItem>
|
||||
|
@ -190,11 +195,15 @@ export const EntityAnalyticsHeader = () => {
|
|||
)}
|
||||
{isPlatinumOrTrialLicense && (
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiFlexGroup direction="column" gutterSize="s">
|
||||
<EuiFlexGroup direction="column" gutterSize="s" responsive={false}>
|
||||
<EuiFlexItem className="eui-textCenter">
|
||||
<StyledEuiTitle data-test-subj="critical_users_quantity" size="l">
|
||||
<span>
|
||||
{usersSeverityCount ? usersSeverityCount[RiskSeverity.critical] : '-'}
|
||||
{usersSeverityCount ? (
|
||||
<FormattedCount count={usersSeverityCount[RiskSeverity.critical]} />
|
||||
) : (
|
||||
'-'
|
||||
)}
|
||||
</span>
|
||||
</StyledEuiTitle>
|
||||
</EuiFlexItem>
|
||||
|
@ -212,7 +221,7 @@ export const EntityAnalyticsHeader = () => {
|
|||
)}
|
||||
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiFlexGroup direction="column" gutterSize="s">
|
||||
<EuiFlexGroup direction="column" gutterSize="s" responsive={false}>
|
||||
<EuiFlexItem className="eui-textCenter">
|
||||
<EuiTitle data-test-subj="anomalies_quantity" size="l">
|
||||
<span>{totalAnomalies}</span>
|
||||
|
|
|
@ -22,6 +22,7 @@ import type {
|
|||
import { RiskScoreEntity, RiskScoreFields } from '../../../../../common/search_strategy';
|
||||
import * as i18n from './translations';
|
||||
import { FormattedCount } from '../../../../common/components/formatted_number';
|
||||
import { EntityAnalyticsHoverActions } from '../common/entity_hover_actions';
|
||||
|
||||
type HostRiskScoreColumns = Array<EuiBasicTableColumn<HostRiskScore & UserRiskScore>>;
|
||||
|
||||
|
@ -37,9 +38,23 @@ export const getRiskScoreColumns = (
|
|||
render: (entityName: string) => {
|
||||
if (entityName != null && entityName.length > 0) {
|
||||
return riskEntity === RiskScoreEntity.host ? (
|
||||
<HostDetailsLink hostName={entityName} hostTab={HostsTableType.risk} />
|
||||
<>
|
||||
<HostDetailsLink hostName={entityName} hostTab={HostsTableType.risk} />
|
||||
<EntityAnalyticsHoverActions
|
||||
idPrefix={`hosts-risk-table-${entityName}`}
|
||||
fieldName={'host.name'}
|
||||
fieldValue={entityName}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<UserDetailsLink userName={entityName} userTab={UsersTableType.risk} />
|
||||
<>
|
||||
<UserDetailsLink userName={entityName} userTab={UsersTableType.risk} />
|
||||
<EntityAnalyticsHoverActions
|
||||
idPrefix={`users-risk-table-${entityName}`}
|
||||
fieldName={'user.name'}
|
||||
fieldValue={entityName}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
return getEmptyTagValue();
|
||||
|
@ -50,6 +65,7 @@ export const getRiskScoreColumns = (
|
|||
riskEntity === RiskScoreEntity.host
|
||||
? RiskScoreFields.hostRiskScore
|
||||
: RiskScoreFields.userRiskScore,
|
||||
width: '15%',
|
||||
name: i18n.RISK_SCORE_TITLE(riskEntity),
|
||||
truncateText: true,
|
||||
mobileOptions: { show: true },
|
||||
|
@ -57,7 +73,7 @@ export const getRiskScoreColumns = (
|
|||
if (riskScore != null) {
|
||||
return (
|
||||
<span data-test-subj="risk-score-truncate" title={`${riskScore}`}>
|
||||
{riskScore.toFixed(2)}
|
||||
{Math.round(riskScore)}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
@ -67,6 +83,7 @@ export const getRiskScoreColumns = (
|
|||
{
|
||||
field:
|
||||
riskEntity === RiskScoreEntity.host ? RiskScoreFields.hostRisk : RiskScoreFields.userRisk,
|
||||
width: '30%',
|
||||
name: (
|
||||
<EuiToolTip content={i18n.ENTITY_RISK_TOOLTIP(riskEntity)}>
|
||||
<>
|
||||
|
@ -86,7 +103,7 @@ export const getRiskScoreColumns = (
|
|||
},
|
||||
{
|
||||
field: RiskScoreFields.alertsCount,
|
||||
width: '15%',
|
||||
width: '10%',
|
||||
name: i18n.ALERTS,
|
||||
truncateText: false,
|
||||
mobileOptions: { show: true },
|
||||
|
|
|
@ -52,6 +52,8 @@ jest.mock('../../detection_response/hooks/use_navigate_to_timeline', () => {
|
|||
};
|
||||
});
|
||||
|
||||
jest.mock('../../../../common/components/hover_actions', () => ({ HoverActions: () => null }));
|
||||
|
||||
describe.each([RiskScoreEntity.host, RiskScoreEntity.user])(
|
||||
'EntityAnalyticsRiskScores entityType: %s',
|
||||
(riskEntity) => {
|
||||
|
|
|
@ -31,7 +31,7 @@ import { InspectButtonContainer } from '../../../../common/components/inspect';
|
|||
import { useQueryToggle } from '../../../../common/containers/query_toggle';
|
||||
import { hostsActions } from '../../../../hosts/store';
|
||||
import { RiskScoreDonutChart } from '../common/risk_score_donut_chart';
|
||||
import { BasicTableWithoutBorderBottom } from '../common/basic_table_without_border_bottom';
|
||||
import { StyledBasicTable } from '../common/styled_basic_table';
|
||||
import { RISKY_HOSTS_DOC_LINK, RISKY_USERS_DOC_LINK } from '../../../../../common/constants';
|
||||
import { RiskScoreHeaderTitle } from '../../../../risk_score/components/risk_score_onboarding/risk_score_header_title';
|
||||
import { RiskScoresNoDataDetected } from '../../../../risk_score/components/risk_score_onboarding/risk_score_no_data_detected';
|
||||
|
@ -273,12 +273,15 @@ const EntityAnalyticsRiskScoresComponent = ({ riskEntity }: { riskEntity: RiskSc
|
|||
<RiskScoreDonutChart severityCount={severityCount ?? EMPTY_SEVERITY_COUNT} />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<BasicTableWithoutBorderBottom
|
||||
<StyledBasicTable
|
||||
responsive={false}
|
||||
items={data ?? []}
|
||||
columns={columns}
|
||||
loading={isTableLoading}
|
||||
id={entity.tableQueryId}
|
||||
rowProps={{
|
||||
className: 'EntityAnalyticsTableHoverActions',
|
||||
}}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue