mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[Security Solution] tgrid cellActions enhancement (#113419)
* Alerts cellAction enhancement * styling * fix types * expandable topN * fix types * styling for filters * styling * rm getDefaultCellActions * styling * globalFilters for topN * rm unused i18n keys * unit test * add i18n * rename component * fix types * update i18n keys * unit tests * styling for reason row renderer * rename file * fix Circular Dependencies * update wording/icons for show top N * cell value text overflow * reason in grid-view * unit test * default selected option for topN * lint error * configurable paddingSize and showLegend for topN * update snapshot * rename reason title * fix cypress * fix cypress * fix unit tests * fix default cell actions * fix page crashing * unit test * add unit tests * code review * fix missing props * fix expand ip button Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
cdce98c8a3
commit
56a2e788ca
55 changed files with 1556 additions and 493 deletions
|
@ -210,7 +210,7 @@ exports[`AlertSummaryView Behavior event code renders additional summary rows 1`
|
|||
class="eventFieldsTable__fieldValue"
|
||||
>
|
||||
<span
|
||||
class="euiToolTipAnchor"
|
||||
class="euiToolTipAnchor eui-textTruncate eui-alignMiddle"
|
||||
>
|
||||
Nov 25, 2020 @ 15:42:39.417
|
||||
</span>
|
||||
|
@ -872,7 +872,7 @@ exports[`AlertSummaryView Memory event code renders additional summary rows 1`]
|
|||
class="eventFieldsTable__fieldValue"
|
||||
>
|
||||
<span
|
||||
class="euiToolTipAnchor"
|
||||
class="euiToolTipAnchor eui-textTruncate eui-alignMiddle"
|
||||
>
|
||||
Nov 25, 2020 @ 15:42:39.417
|
||||
</span>
|
||||
|
|
|
@ -25,7 +25,7 @@ import {
|
|||
} from '../../../../timelines/components/timeline/body/renderers/constants';
|
||||
import { BYTES_FORMAT } from '../../../../timelines/components/timeline/body/renderers/bytes';
|
||||
import { EVENT_DURATION_FIELD_NAME } from '../../../../timelines/components/duration';
|
||||
import { PORT_NAMES } from '../../../../network/components/port';
|
||||
import { PORT_NAMES } from '../../../../network/components/port/helpers';
|
||||
import { INDICATOR_REFERENCE } from '../../../../../common/cti/constants';
|
||||
import { BrowserField } from '../../../containers/source';
|
||||
import { DataProvider, IS_OPERATOR } from '../../../../../common/types';
|
||||
|
|
|
@ -92,23 +92,27 @@ PreferenceFormattedP1DTDate.displayName = 'PreferenceFormattedP1DTDate';
|
|||
* - a long representation of the date that includes the day of the week (e.g. Thursday, March 21, 2019 6:47pm)
|
||||
* - the raw date value (e.g. 2019-03-22T00:47:46Z)
|
||||
*/
|
||||
export const FormattedDate = React.memo<{
|
||||
|
||||
interface FormattedDateProps {
|
||||
className?: string;
|
||||
fieldName: string;
|
||||
value?: string | number | null;
|
||||
className?: string;
|
||||
}>(({ value, fieldName, className = '' }): JSX.Element => {
|
||||
if (value == null) {
|
||||
return getOrEmptyTagFromValue(value);
|
||||
}
|
||||
export const FormattedDate = React.memo<FormattedDateProps>(
|
||||
({ value, fieldName, className = '' }): JSX.Element => {
|
||||
if (value == null) {
|
||||
return getOrEmptyTagFromValue(value);
|
||||
}
|
||||
const maybeDate = getMaybeDate(value);
|
||||
return maybeDate.isValid() ? (
|
||||
<LocalizedDateTooltip date={maybeDate.toDate()} fieldName={fieldName} className={className}>
|
||||
<PreferenceFormattedDate value={maybeDate.toDate()} />
|
||||
</LocalizedDateTooltip>
|
||||
) : (
|
||||
getOrEmptyTagFromValue(value)
|
||||
);
|
||||
}
|
||||
const maybeDate = getMaybeDate(value);
|
||||
return maybeDate.isValid() ? (
|
||||
<LocalizedDateTooltip date={maybeDate.toDate()} fieldName={fieldName} className={className}>
|
||||
<PreferenceFormattedDate value={maybeDate.toDate()} />
|
||||
</LocalizedDateTooltip>
|
||||
) : (
|
||||
getOrEmptyTagFromValue(value)
|
||||
);
|
||||
});
|
||||
);
|
||||
|
||||
FormattedDate.displayName = 'FormattedDate';
|
||||
|
||||
|
|
|
@ -6,23 +6,25 @@ exports[`HeaderSection it renders 1`] = `
|
|||
>
|
||||
<EuiFlexGroup
|
||||
alignItems="center"
|
||||
gutterSize="none"
|
||||
>
|
||||
<EuiFlexItem
|
||||
grow={true}
|
||||
>
|
||||
<EuiFlexGroup
|
||||
alignItems="center"
|
||||
gutterSize="none"
|
||||
responsive={false}
|
||||
>
|
||||
<EuiFlexItem>
|
||||
<EuiTitle
|
||||
size="m"
|
||||
>
|
||||
<h2
|
||||
<h4
|
||||
data-test-subj="header-section-title"
|
||||
>
|
||||
Test title
|
||||
</h2>
|
||||
</h4>
|
||||
</EuiTitle>
|
||||
<Subtitle
|
||||
data-test-subj="header-section-subtitle"
|
||||
|
|
|
@ -68,12 +68,12 @@ const HeaderSectionComponent: React.FC<HeaderSectionProps> = ({
|
|||
hideSubtitle = false,
|
||||
}) => (
|
||||
<Header data-test-subj="header-section" border={border} height={height}>
|
||||
<EuiFlexGroup alignItems="center">
|
||||
<EuiFlexGroup alignItems="center" gutterSize="none">
|
||||
<EuiFlexItem grow={growLeftSplit}>
|
||||
<EuiFlexGroup alignItems="center" responsive={false}>
|
||||
<EuiFlexGroup alignItems="center" responsive={false} gutterSize="none">
|
||||
<EuiFlexItem>
|
||||
<EuiTitle size={titleSize}>
|
||||
<h2 data-test-subj="header-section-title">
|
||||
<h4 data-test-subj="header-section-title">
|
||||
{title}
|
||||
{tooltip && (
|
||||
<>
|
||||
|
@ -81,7 +81,7 @@ const HeaderSectionComponent: React.FC<HeaderSectionProps> = ({
|
|||
<EuiIconTip color="subdued" content={tooltip} size="l" type="iInCircle" />
|
||||
</>
|
||||
)}
|
||||
</h2>
|
||||
</h4>
|
||||
</EuiTitle>
|
||||
|
||||
{!hideSubtitle && (
|
||||
|
|
|
@ -30,33 +30,49 @@ const SHOW_TOP = (fieldName: string) =>
|
|||
});
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
/** When `Component` is used with `EuiDataGrid`; the grid keeps a reference to `Component` for show / hide functionality.
|
||||
* When `Component` is used with `EuiContextMenu`, we pass EuiContextMenuItem to render the right style.
|
||||
*/
|
||||
Component?: typeof EuiButtonEmpty | typeof EuiButtonIcon | typeof EuiContextMenuItem;
|
||||
enablePopOver?: boolean;
|
||||
field: string;
|
||||
flush?: 'left' | 'right' | 'both';
|
||||
globalFilters?: Filter[];
|
||||
iconSide?: 'left' | 'right';
|
||||
iconType?: string;
|
||||
isExpandable?: boolean;
|
||||
onClick: () => void;
|
||||
onFilterAdded?: () => void;
|
||||
ownFocus: boolean;
|
||||
paddingSize?: 's' | 'm' | 'l' | 'none';
|
||||
showTooltip?: boolean;
|
||||
showTopN: boolean;
|
||||
showLegend?: boolean;
|
||||
timelineId?: string | null;
|
||||
title?: string;
|
||||
value?: string[] | string | null;
|
||||
}
|
||||
|
||||
export const ShowTopNButton: React.FC<Props> = React.memo(
|
||||
({
|
||||
className,
|
||||
Component,
|
||||
enablePopOver,
|
||||
field,
|
||||
flush,
|
||||
iconSide,
|
||||
iconType,
|
||||
isExpandable,
|
||||
onClick,
|
||||
onFilterAdded,
|
||||
ownFocus,
|
||||
paddingSize,
|
||||
showLegend,
|
||||
showTooltip = true,
|
||||
showTopN,
|
||||
timelineId,
|
||||
title,
|
||||
value,
|
||||
globalFilters,
|
||||
}) => {
|
||||
|
@ -70,31 +86,36 @@ export const ShowTopNButton: React.FC<Props> = React.memo(
|
|||
? SourcererScopeName.detections
|
||||
: SourcererScopeName.default;
|
||||
const { browserFields, indexPattern } = useSourcererScope(activeScope);
|
||||
|
||||
const icon = iconType ?? 'visBarVertical';
|
||||
const side = iconSide ?? 'left';
|
||||
const buttonTitle = title ?? SHOW_TOP(field);
|
||||
const basicButton = useMemo(
|
||||
() =>
|
||||
Component ? (
|
||||
<Component
|
||||
aria-label={SHOW_TOP(field)}
|
||||
aria-label={buttonTitle}
|
||||
className={className}
|
||||
data-test-subj="show-top-field"
|
||||
icon="visBarVertical"
|
||||
iconType="visBarVertical"
|
||||
icon={icon}
|
||||
iconType={icon}
|
||||
iconSide={side}
|
||||
flush={flush}
|
||||
onClick={onClick}
|
||||
title={SHOW_TOP(field)}
|
||||
title={buttonTitle}
|
||||
>
|
||||
{SHOW_TOP(field)}
|
||||
{buttonTitle}
|
||||
</Component>
|
||||
) : (
|
||||
<EuiButtonIcon
|
||||
aria-label={SHOW_TOP(field)}
|
||||
aria-label={buttonTitle}
|
||||
className="securitySolution__hoverActionButton"
|
||||
data-test-subj="show-top-field"
|
||||
iconSize="s"
|
||||
iconType="visBarVertical"
|
||||
iconType={icon}
|
||||
onClick={onClick}
|
||||
/>
|
||||
),
|
||||
[Component, field, onClick]
|
||||
[Component, buttonTitle, className, flush, icon, onClick, side]
|
||||
);
|
||||
|
||||
const button = useMemo(
|
||||
|
@ -107,7 +128,7 @@ export const ShowTopNButton: React.FC<Props> = React.memo(
|
|||
field,
|
||||
value,
|
||||
})}
|
||||
content={SHOW_TOP(field)}
|
||||
content={buttonTitle}
|
||||
shortcut={SHOW_TOP_N_KEYBOARD_SHORTCUT}
|
||||
showShortcut={ownFocus}
|
||||
/>
|
||||
|
@ -118,7 +139,7 @@ export const ShowTopNButton: React.FC<Props> = React.memo(
|
|||
) : (
|
||||
basicButton
|
||||
),
|
||||
[basicButton, field, ownFocus, showTooltip, showTopN, value]
|
||||
[basicButton, buttonTitle, field, ownFocus, showTooltip, showTopN, value]
|
||||
);
|
||||
|
||||
const topNPannel = useMemo(
|
||||
|
@ -128,15 +149,37 @@ export const ShowTopNButton: React.FC<Props> = React.memo(
|
|||
field={field}
|
||||
indexPattern={indexPattern}
|
||||
onFilterAdded={onFilterAdded}
|
||||
paddingSize={paddingSize}
|
||||
showLegend={showLegend}
|
||||
timelineId={timelineId ?? undefined}
|
||||
toggleTopN={onClick}
|
||||
value={value}
|
||||
globalFilters={globalFilters}
|
||||
/>
|
||||
),
|
||||
[browserFields, field, indexPattern, onClick, onFilterAdded, timelineId, value, globalFilters]
|
||||
[
|
||||
browserFields,
|
||||
field,
|
||||
indexPattern,
|
||||
onFilterAdded,
|
||||
paddingSize,
|
||||
showLegend,
|
||||
timelineId,
|
||||
onClick,
|
||||
value,
|
||||
globalFilters,
|
||||
]
|
||||
);
|
||||
|
||||
if (isExpandable) {
|
||||
return (
|
||||
<>
|
||||
{basicButton}
|
||||
{showTopN && topNPannel}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return showTopN ? (
|
||||
enablePopOver ? (
|
||||
<EuiPopover
|
||||
|
|
|
@ -0,0 +1,71 @@
|
|||
/*
|
||||
* 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, { SyntheticEvent } from 'react';
|
||||
import {
|
||||
EuiButton,
|
||||
EuiButtonEmpty,
|
||||
EuiButtonIcon,
|
||||
EuiButtonProps,
|
||||
EuiLink,
|
||||
EuiLinkProps,
|
||||
PropsForAnchor,
|
||||
PropsForButton,
|
||||
} from '@elastic/eui';
|
||||
import styled from 'styled-components';
|
||||
|
||||
export const LinkButton: React.FC<PropsForButton<EuiButtonProps> | PropsForAnchor<EuiButtonProps>> =
|
||||
({ children, ...props }) => <EuiButton {...props}>{children}</EuiButton>;
|
||||
|
||||
export const LinkAnchor: React.FC<EuiLinkProps> = ({ children, ...props }) => (
|
||||
<EuiLink {...props}>{children}</EuiLink>
|
||||
);
|
||||
|
||||
export const Comma = styled('span')`
|
||||
margin-right: 5px;
|
||||
margin-left: 5px;
|
||||
&::after {
|
||||
content: ' ,';
|
||||
}
|
||||
`;
|
||||
|
||||
Comma.displayName = 'Comma';
|
||||
|
||||
const GenericLinkButtonComponent: React.FC<{
|
||||
children?: React.ReactNode;
|
||||
/** `Component` is only used with `EuiDataGrid`; the grid keeps a reference to `Component` for show / hide functionality */
|
||||
Component?: typeof EuiButtonEmpty | typeof EuiButtonIcon;
|
||||
dataTestSubj?: string;
|
||||
href: string;
|
||||
onClick?: (e: SyntheticEvent) => void;
|
||||
title?: string;
|
||||
iconType?: string;
|
||||
}> = ({ children, Component, dataTestSubj, href, onClick, title, iconType = 'expand' }) => {
|
||||
return Component ? (
|
||||
<Component
|
||||
data-test-subj={dataTestSubj}
|
||||
href={href}
|
||||
iconType={iconType}
|
||||
onClick={onClick}
|
||||
title={title}
|
||||
>
|
||||
{title ?? children}
|
||||
</Component>
|
||||
) : (
|
||||
<LinkButton data-test-subj={dataTestSubj} href={href} onClick={onClick}>
|
||||
{title ?? children}
|
||||
</LinkButton>
|
||||
);
|
||||
};
|
||||
|
||||
export const GenericLinkButton = React.memo(GenericLinkButtonComponent);
|
||||
|
||||
export const PortContainer = styled.div`
|
||||
& svg {
|
||||
position: relative;
|
||||
top: -1px;
|
||||
}
|
||||
`;
|
|
@ -6,19 +6,15 @@
|
|||
*/
|
||||
|
||||
import {
|
||||
EuiButton,
|
||||
EuiButtonProps,
|
||||
EuiLink,
|
||||
EuiLinkProps,
|
||||
EuiToolTip,
|
||||
EuiButtonEmpty,
|
||||
EuiButtonIcon,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
PropsForAnchor,
|
||||
PropsForButton,
|
||||
EuiLink,
|
||||
EuiToolTip,
|
||||
} from '@elastic/eui';
|
||||
import React, { useMemo, useCallback, SyntheticEvent } from 'react';
|
||||
import { isNil } from 'lodash/fp';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { IP_REPUTATION_LINKS_SETTING, APP_ID } from '../../../../common/constants';
|
||||
import {
|
||||
|
@ -43,23 +39,12 @@ import { isUrlInvalid } from '../../utils/validators';
|
|||
import * as i18n from './translations';
|
||||
import { SecurityPageName } from '../../../app/types';
|
||||
import { getUebaDetailsUrl } from '../link_to/redirect_to_ueba';
|
||||
import { LinkButton, LinkAnchor, GenericLinkButton, PortContainer, Comma } from './helpers';
|
||||
|
||||
export { LinkButton, LinkAnchor } from './helpers';
|
||||
|
||||
export const DEFAULT_NUMBER_OF_LINK = 5;
|
||||
|
||||
export const LinkButton: React.FC<PropsForButton<EuiButtonProps> | PropsForAnchor<EuiButtonProps>> =
|
||||
({ children, ...props }) => <EuiButton {...props}>{children}</EuiButton>;
|
||||
|
||||
export const LinkAnchor: React.FC<EuiLinkProps> = ({ children, ...props }) => (
|
||||
<EuiLink {...props}>{children}</EuiLink>
|
||||
);
|
||||
|
||||
export const PortContainer = styled.div`
|
||||
& svg {
|
||||
position: relative;
|
||||
top: -1px;
|
||||
}
|
||||
`;
|
||||
|
||||
// Internal Links
|
||||
const UebaDetailsLinkComponent: React.FC<{
|
||||
children?: React.ReactNode;
|
||||
|
@ -102,10 +87,13 @@ export const UebaDetailsLink = React.memo(UebaDetailsLinkComponent);
|
|||
|
||||
const HostDetailsLinkComponent: React.FC<{
|
||||
children?: React.ReactNode;
|
||||
/** `Component` is only used with `EuiDataGrid`; the grid keeps a reference to `Component` for show / hide functionality */
|
||||
Component?: typeof EuiButtonEmpty | typeof EuiButtonIcon;
|
||||
hostName: string;
|
||||
isButton?: boolean;
|
||||
onClick?: (e: SyntheticEvent) => void;
|
||||
}> = ({ children, hostName, isButton, onClick }) => {
|
||||
title?: string;
|
||||
}> = ({ children, Component, hostName, isButton, onClick, title }) => {
|
||||
const { formatUrl, search } = useFormatUrl(SecurityPageName.hosts);
|
||||
const { navigateToApp } = useKibana().services.application;
|
||||
const goToHostDetails = useCallback(
|
||||
|
@ -118,19 +106,25 @@ const HostDetailsLinkComponent: React.FC<{
|
|||
},
|
||||
[hostName, navigateToApp, search]
|
||||
);
|
||||
|
||||
const href = useMemo(
|
||||
() => formatUrl(getHostDetailsUrl(encodeURIComponent(hostName))),
|
||||
[formatUrl, hostName]
|
||||
);
|
||||
return isButton ? (
|
||||
<LinkButton
|
||||
<GenericLinkButton
|
||||
Component={Component}
|
||||
dataTestSubj="data-grid-host-details"
|
||||
href={href}
|
||||
iconType="expand"
|
||||
onClick={onClick ?? goToHostDetails}
|
||||
href={formatUrl(getHostDetailsUrl(encodeURIComponent(hostName)))}
|
||||
data-test-subj="host-details-button"
|
||||
title={title ?? hostName}
|
||||
>
|
||||
{children ? children : hostName}
|
||||
</LinkButton>
|
||||
{children}
|
||||
</GenericLinkButton>
|
||||
) : (
|
||||
<LinkAnchor
|
||||
onClick={onClick ?? goToHostDetails}
|
||||
href={formatUrl(getHostDetailsUrl(encodeURIComponent(hostName)))}
|
||||
href={href}
|
||||
data-test-subj="host-details-button"
|
||||
>
|
||||
{children ? children : hostName}
|
||||
|
@ -176,11 +170,14 @@ ExternalLink.displayName = 'ExternalLink';
|
|||
|
||||
const NetworkDetailsLinkComponent: React.FC<{
|
||||
children?: React.ReactNode;
|
||||
/** `Component` is only used with `EuiDataGrid`; the grid keeps a reference to `Component` for show / hide functionality */
|
||||
Component?: typeof EuiButtonEmpty | typeof EuiButtonIcon;
|
||||
ip: string;
|
||||
flowTarget?: FlowTarget | FlowTargetSourceDest;
|
||||
isButton?: boolean;
|
||||
onClick?: (e: SyntheticEvent) => void | undefined;
|
||||
}> = ({ children, ip, flowTarget = FlowTarget.source, isButton, onClick }) => {
|
||||
title?: string;
|
||||
}> = ({ Component, children, ip, flowTarget = FlowTarget.source, isButton, onClick, title }) => {
|
||||
const { formatUrl, search } = useFormatUrl(SecurityPageName.network);
|
||||
const { navigateToApp } = useKibana().services.application;
|
||||
const goToNetworkDetails = useCallback(
|
||||
|
@ -193,19 +190,25 @@ const NetworkDetailsLinkComponent: React.FC<{
|
|||
},
|
||||
[flowTarget, ip, navigateToApp, search]
|
||||
);
|
||||
const href = useMemo(
|
||||
() => formatUrl(getNetworkDetailsUrl(encodeURIComponent(encodeIpv6(ip)))),
|
||||
[formatUrl, ip]
|
||||
);
|
||||
|
||||
return isButton ? (
|
||||
<LinkButton
|
||||
href={formatUrl(getNetworkDetailsUrl(encodeURIComponent(encodeIpv6(ip))))}
|
||||
<GenericLinkButton
|
||||
Component={Component}
|
||||
dataTestSubj="data-grid-network-details"
|
||||
onClick={onClick ?? goToNetworkDetails}
|
||||
data-test-subj="network-details"
|
||||
href={href}
|
||||
title={title ?? ip}
|
||||
>
|
||||
{children ? children : ip}
|
||||
</LinkButton>
|
||||
{children}
|
||||
</GenericLinkButton>
|
||||
) : (
|
||||
<LinkAnchor
|
||||
onClick={onClick ?? goToNetworkDetails}
|
||||
href={formatUrl(getNetworkDetailsUrl(encodeURIComponent(encodeIpv6(ip))))}
|
||||
href={href}
|
||||
data-test-subj="network-details"
|
||||
>
|
||||
{children ? children : ip}
|
||||
|
@ -272,63 +275,84 @@ CreateCaseLink.displayName = 'CreateCaseLink';
|
|||
|
||||
// External Links
|
||||
export const GoogleLink = React.memo<{ children?: React.ReactNode; link: string }>(
|
||||
({ children, link }) => (
|
||||
<ExternalLink url={`https://www.google.com/search?q=${encodeURIComponent(link)}`}>
|
||||
{children ? children : link}
|
||||
</ExternalLink>
|
||||
)
|
||||
({ children, link }) => {
|
||||
const url = useMemo(
|
||||
() => `https://www.google.com/search?q=${encodeURIComponent(link)}`,
|
||||
[link]
|
||||
);
|
||||
return <ExternalLink url={url}>{children ? children : link}</ExternalLink>;
|
||||
}
|
||||
);
|
||||
|
||||
GoogleLink.displayName = 'GoogleLink';
|
||||
|
||||
export const PortOrServiceNameLink = React.memo<{
|
||||
children?: React.ReactNode;
|
||||
/** `Component` is only used with `EuiDataGrid`; the grid keeps a reference to `Component` for show / hide functionality */
|
||||
Component?: typeof EuiButtonEmpty | typeof EuiButtonIcon;
|
||||
portOrServiceName: number | string;
|
||||
}>(({ children, portOrServiceName }) => (
|
||||
<PortContainer>
|
||||
<EuiLink
|
||||
data-test-subj="port-or-service-name-link"
|
||||
href={`https://www.iana.org/assignments/service-names-port-numbers/service-names-port-numbers.xhtml?search=${encodeURIComponent(
|
||||
onClick?: (e: SyntheticEvent) => void | undefined;
|
||||
title?: string;
|
||||
}>(({ Component, title, children, portOrServiceName }) => {
|
||||
const href = useMemo(
|
||||
() =>
|
||||
`https://www.iana.org/assignments/service-names-port-numbers/service-names-port-numbers.xhtml?search=${encodeURIComponent(
|
||||
String(portOrServiceName)
|
||||
)}`}
|
||||
target="_blank"
|
||||
)}`,
|
||||
[portOrServiceName]
|
||||
);
|
||||
return Component ? (
|
||||
<Component
|
||||
href={href}
|
||||
data-test-subj="data-grid-port-or-service-name-link"
|
||||
title={title}
|
||||
iconType="link"
|
||||
>
|
||||
{children ? children : portOrServiceName}
|
||||
</EuiLink>
|
||||
</PortContainer>
|
||||
));
|
||||
{title ?? children ?? portOrServiceName}
|
||||
</Component>
|
||||
) : (
|
||||
<PortContainer>
|
||||
<EuiLink data-test-subj="port-or-service-name-link" href={href} target="_blank">
|
||||
{children ? children : portOrServiceName}
|
||||
</EuiLink>
|
||||
</PortContainer>
|
||||
);
|
||||
});
|
||||
|
||||
PortOrServiceNameLink.displayName = 'PortOrServiceNameLink';
|
||||
|
||||
export const Ja3FingerprintLink = React.memo<{
|
||||
children?: React.ReactNode;
|
||||
ja3Fingerprint: string;
|
||||
}>(({ children, ja3Fingerprint }) => (
|
||||
<EuiLink
|
||||
data-test-subj="ja3-fingerprint-link"
|
||||
href={`https://sslbl.abuse.ch/ja3-fingerprints/${encodeURIComponent(ja3Fingerprint)}`}
|
||||
target="_blank"
|
||||
>
|
||||
{children ? children : ja3Fingerprint}
|
||||
</EuiLink>
|
||||
));
|
||||
}>(({ children, ja3Fingerprint }) => {
|
||||
const href = useMemo(
|
||||
() => `https://sslbl.abuse.ch/ja3-fingerprints/${encodeURIComponent(ja3Fingerprint)}`,
|
||||
[ja3Fingerprint]
|
||||
);
|
||||
return (
|
||||
<EuiLink data-test-subj="ja3-fingerprint-link" href={href} target="_blank">
|
||||
{children ? children : ja3Fingerprint}
|
||||
</EuiLink>
|
||||
);
|
||||
});
|
||||
|
||||
Ja3FingerprintLink.displayName = 'Ja3FingerprintLink';
|
||||
|
||||
export const CertificateFingerprintLink = React.memo<{
|
||||
children?: React.ReactNode;
|
||||
certificateFingerprint: string;
|
||||
}>(({ children, certificateFingerprint }) => (
|
||||
<EuiLink
|
||||
data-test-subj="certificate-fingerprint-link"
|
||||
href={`https://sslbl.abuse.ch/ssl-certificates/sha1/${encodeURIComponent(
|
||||
certificateFingerprint
|
||||
)}`}
|
||||
target="_blank"
|
||||
>
|
||||
{children ? children : certificateFingerprint}
|
||||
</EuiLink>
|
||||
));
|
||||
}>(({ children, certificateFingerprint }) => {
|
||||
const href = useMemo(
|
||||
() =>
|
||||
`https://sslbl.abuse.ch/ssl-certificates/sha1/${encodeURIComponent(certificateFingerprint)}`,
|
||||
[certificateFingerprint]
|
||||
);
|
||||
return (
|
||||
<EuiLink data-test-subj="certificate-fingerprint-link" href={href} target="_blank">
|
||||
{children ? children : certificateFingerprint}
|
||||
</EuiLink>
|
||||
);
|
||||
});
|
||||
|
||||
CertificateFingerprintLink.displayName = 'CertificateFingerprintLink';
|
||||
|
||||
|
@ -354,16 +378,6 @@ const isReputationLink = (
|
|||
(rowItem as ReputationLinkSetting).url_template !== undefined &&
|
||||
(rowItem as ReputationLinkSetting).name !== undefined;
|
||||
|
||||
export const Comma = styled('span')`
|
||||
margin-right: 5px;
|
||||
margin-left: 5px;
|
||||
&::after {
|
||||
content: ' ,';
|
||||
}
|
||||
`;
|
||||
|
||||
Comma.displayName = 'Comma';
|
||||
|
||||
const defaultNameMapping: Record<DefaultReputationLink, string> = {
|
||||
[DefaultReputationLink['virustotal.com']]: i18n.VIEW_VIRUS_TOTAL,
|
||||
[DefaultReputationLink['talosIntelligence.com']]: i18n.VIEW_TALOS_INTELLIGENCE,
|
||||
|
@ -463,11 +477,13 @@ ReputationLinkComponent.displayName = 'ReputationLinkComponent';
|
|||
export const ReputationLink = React.memo(ReputationLinkComponent);
|
||||
|
||||
export const WhoIsLink = React.memo<{ children?: React.ReactNode; domain: string }>(
|
||||
({ children, domain }) => (
|
||||
<ExternalLink url={`https://www.iana.org/whois?q=${encodeURIComponent(domain)}`}>
|
||||
{children ? children : domain}
|
||||
</ExternalLink>
|
||||
)
|
||||
({ children, domain }) => {
|
||||
const url = useMemo(
|
||||
() => `https://www.iana.org/whois?q=${encodeURIComponent(domain)}`,
|
||||
[domain]
|
||||
);
|
||||
return <ExternalLink url={url}>{children ? children : domain}</ExternalLink>;
|
||||
}
|
||||
);
|
||||
|
||||
WhoIsLink.displayName = 'WhoIsLink';
|
||||
|
|
|
@ -79,6 +79,7 @@ export const MatrixHistogramComponent: React.FC<MatrixHistogramComponentProps> =
|
|||
legendPosition,
|
||||
mapping,
|
||||
onError,
|
||||
paddingSize = 'm',
|
||||
panelHeight = DEFAULT_PANEL_HEIGHT,
|
||||
setAbsoluteRangeDatePickerTarget = 'global',
|
||||
setQuery,
|
||||
|
@ -200,7 +201,11 @@ export const MatrixHistogramComponent: React.FC<MatrixHistogramComponentProps> =
|
|||
return (
|
||||
<>
|
||||
<InspectButtonContainer show={!isInitialLoading}>
|
||||
<HistogramPanel data-test-subj={`${id}Panel`} height={panelHeight}>
|
||||
<HistogramPanel
|
||||
data-test-subj={`${id}Panel`}
|
||||
height={panelHeight}
|
||||
paddingSize={paddingSize}
|
||||
>
|
||||
{loading && !isInitialLoading && (
|
||||
<EuiProgress
|
||||
data-test-subj="initialLoadingPanelMatrixOverTime"
|
||||
|
|
|
@ -52,6 +52,7 @@ interface MatrixHistogramBasicProps {
|
|||
legendPosition?: Position;
|
||||
mapping?: MatrixHistogramMappingTypes;
|
||||
panelHeight?: number;
|
||||
paddingSize?: 's' | 'm' | 'l' | 'none';
|
||||
setQuery: GlobalTimeArgs['setQuery'];
|
||||
startDate: GlobalTimeArgs['from'];
|
||||
stackByOptions: MatrixHistogramOption[];
|
||||
|
|
|
@ -31,13 +31,58 @@ export const AppGlobalStyle = createGlobalStyle<{ theme: { eui: { euiColorPrimar
|
|||
z-index: 9950 !important;
|
||||
}
|
||||
|
||||
.euiDataGridRowCell__expandButton .euiDataGridRowCell__actionButtonIcon {
|
||||
display: none;
|
||||
|
||||
&:first-child,
|
||||
&:nth-child(2),
|
||||
&:nth-child(3) {
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/*
|
||||
overrides the default styling of EuiDataGrid expand popover footer to
|
||||
make it a column of actions instead of the default actions row
|
||||
*/
|
||||
.euiDataGridRowCell__popover .euiPopoverFooter .euiFlexGroup {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
|
||||
.euiDataGridRowCell__popover {
|
||||
|
||||
max-width: 815px !important;
|
||||
max-height: none !important;
|
||||
overflow: hidden;
|
||||
|
||||
|
||||
.expandable-top-value-button {
|
||||
&.euiButtonEmpty--primary:enabled:focus,
|
||||
.euiButtonEmpty--primary:focus {
|
||||
background-color: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
&.euiPopover__panel.euiPopover__panel-isOpen {
|
||||
padding: 8px 0;
|
||||
min-width: 65px;
|
||||
}
|
||||
|
||||
|
||||
.euiPopoverFooter {
|
||||
border: 0;
|
||||
.euiFlexGroup {
|
||||
flex-direction: column;
|
||||
|
||||
.euiButtonEmpty .euiButtonContent {
|
||||
justify-content: left;
|
||||
}
|
||||
|
||||
.euiFlexItem:first-child,
|
||||
.euiFlexItem:nth-child(2) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
|
@ -54,10 +99,14 @@ export const AppGlobalStyle = createGlobalStyle<{ theme: { eui: { euiColorPrimar
|
|||
}
|
||||
|
||||
/* hide open draggable popovers when a modal is being displayed to prevent them from covering the modal */
|
||||
body.euiBody-hasOverlayMask .withHoverActions__popover.euiPopover__panel-isOpen{
|
||||
visibility: hidden !important;
|
||||
body.euiBody-hasOverlayMask {
|
||||
.euiDataGridRowCell__popover.euiPopover__panel-isOpen,
|
||||
.withHoverActions__popover.euiPopover__panel-isOpen {
|
||||
visibility: hidden !important;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* ensure elastic charts tooltips appear above open euiPopovers */
|
||||
.echTooltip {
|
||||
z-index: 9950;
|
||||
|
|
|
@ -81,6 +81,8 @@ export interface OwnProps {
|
|||
timelineId?: string;
|
||||
toggleTopN: () => void;
|
||||
onFilterAdded?: () => void;
|
||||
paddingSize?: 's' | 'm' | 'l' | 'none';
|
||||
showLegend?: boolean;
|
||||
value?: string[] | string | null;
|
||||
globalFilters?: Filter[];
|
||||
}
|
||||
|
@ -101,6 +103,8 @@ const StatefulTopNComponent: React.FC<Props> = ({
|
|||
globalQuery = EMPTY_QUERY,
|
||||
kqlMode,
|
||||
onFilterAdded,
|
||||
paddingSize,
|
||||
showLegend,
|
||||
timelineId,
|
||||
toggleTopN,
|
||||
value,
|
||||
|
@ -160,7 +164,9 @@ const StatefulTopNComponent: React.FC<Props> = ({
|
|||
from={timelineId === TimelineId.active ? activeTimelineFrom : from}
|
||||
indexPattern={indexPattern}
|
||||
options={options}
|
||||
paddingSize={paddingSize}
|
||||
query={timelineId === TimelineId.active ? EMPTY_QUERY : globalQuery}
|
||||
showLegend={showLegend}
|
||||
setAbsoluteRangeDatePickerTarget={timelineId === TimelineId.active ? 'timeline' : 'global'}
|
||||
setQuery={setQuery}
|
||||
timelineId={timelineId}
|
||||
|
|
|
@ -55,8 +55,10 @@ export interface Props extends Pick<GlobalTimeArgs, 'from' | 'to' | 'deleteQuery
|
|||
filters: Filter[];
|
||||
indexPattern: IIndexPattern;
|
||||
options: TopNOption[];
|
||||
paddingSize?: 's' | 'm' | 'l' | 'none';
|
||||
query: Query;
|
||||
setAbsoluteRangeDatePickerTarget: InputsModelId;
|
||||
showLegend?: boolean;
|
||||
timelineId?: string;
|
||||
toggleTopN: () => void;
|
||||
onFilterAdded?: () => void;
|
||||
|
@ -72,7 +74,9 @@ const TopNComponent: React.FC<Props> = ({
|
|||
from,
|
||||
indexPattern,
|
||||
options,
|
||||
paddingSize,
|
||||
query,
|
||||
showLegend,
|
||||
setAbsoluteRangeDatePickerTarget,
|
||||
setQuery,
|
||||
timelineId,
|
||||
|
@ -127,7 +131,9 @@ const TopNComponent: React.FC<Props> = ({
|
|||
indexPattern={indexPattern}
|
||||
indexNames={view === 'raw' ? rawIndices : allIndices}
|
||||
onlyField={field}
|
||||
paddingSize={paddingSize}
|
||||
query={query}
|
||||
showLegend={showLegend}
|
||||
setAbsoluteRangeDatePickerTarget={setAbsoluteRangeDatePickerTarget}
|
||||
setQuery={setQuery}
|
||||
showSpacer={false}
|
||||
|
@ -141,7 +147,9 @@ const TopNComponent: React.FC<Props> = ({
|
|||
filters={filters}
|
||||
headerChildren={headerChildren}
|
||||
onlyField={field}
|
||||
paddingSize={paddingSize}
|
||||
query={query}
|
||||
showLegend={showLegend}
|
||||
setAbsoluteRangeDatePickerTarget={setAbsoluteRangeDatePickerTarget}
|
||||
timelineId={timelineId}
|
||||
/>
|
||||
|
|
|
@ -0,0 +1,92 @@
|
|||
/*
|
||||
* 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 { EuiDataGridColumn } from '@elastic/eui';
|
||||
import type {
|
||||
BrowserFields,
|
||||
TimelineNonEcsData,
|
||||
} from '../../../../../timelines/common/search_strategy';
|
||||
import { TGridCellAction } from '../../../../../timelines/common/types';
|
||||
import { Ecs } from '../../../../common/ecs';
|
||||
import { ColumnHeaderType } from '../../../timelines/store/timeline/model';
|
||||
|
||||
import { defaultCellActions, EmptyComponent } from './default_cell_actions';
|
||||
import { COLUMNS_WITH_LINKS } from './helpers';
|
||||
|
||||
describe('default cell actions', () => {
|
||||
const browserFields: BrowserFields = {};
|
||||
const data: TimelineNonEcsData[][] = [[]];
|
||||
const ecsData: Ecs[] = [];
|
||||
const timelineId = 'mockTimelineId';
|
||||
const pageSize = 10;
|
||||
|
||||
test('columns without any link action (e.g.: signal.status) should return an empty component (not null or data grid would crash)', () => {
|
||||
const columnHeaders = [
|
||||
{
|
||||
category: 'signal',
|
||||
columnHeaderType: 'no-filtered' as ColumnHeaderType,
|
||||
id: 'signal.status',
|
||||
type: 'string',
|
||||
aggregatable: true,
|
||||
initialWidth: 105,
|
||||
},
|
||||
];
|
||||
|
||||
const columnsWithCellActions: EuiDataGridColumn[] = columnHeaders.map((header) => {
|
||||
const buildAction = (tGridCellAction: TGridCellAction) =>
|
||||
tGridCellAction({
|
||||
browserFields,
|
||||
data,
|
||||
ecsData,
|
||||
header: columnHeaders.find((h) => h.id === header.id),
|
||||
pageSize,
|
||||
timelineId,
|
||||
});
|
||||
|
||||
return {
|
||||
...header,
|
||||
cellActions: defaultCellActions?.map(buildAction),
|
||||
};
|
||||
});
|
||||
|
||||
expect(columnsWithCellActions[0]?.cellActions?.length).toEqual(5);
|
||||
expect(columnsWithCellActions[0]?.cellActions![4]).toEqual(EmptyComponent);
|
||||
});
|
||||
|
||||
const columnHeadersToTest = COLUMNS_WITH_LINKS.map((c) => [
|
||||
{
|
||||
category: 'signal',
|
||||
columnHeaderType: 'no-filtered' as ColumnHeaderType,
|
||||
id: c.columnId,
|
||||
type: c.fieldType,
|
||||
aggregatable: true,
|
||||
initialWidth: 105,
|
||||
},
|
||||
]);
|
||||
describe.each(columnHeadersToTest)('columns with a link action', (columnHeaders) => {
|
||||
test(`${columnHeaders.id ?? columnHeaders.type}`, () => {
|
||||
const columnsWithCellActions: EuiDataGridColumn[] = [columnHeaders].map((header) => {
|
||||
const buildAction = (tGridCellAction: TGridCellAction) =>
|
||||
tGridCellAction({
|
||||
browserFields,
|
||||
data,
|
||||
ecsData,
|
||||
header: [columnHeaders].find((h) => h.id === header.id),
|
||||
pageSize,
|
||||
timelineId,
|
||||
});
|
||||
|
||||
return {
|
||||
...header,
|
||||
cellActions: defaultCellActions?.map(buildAction),
|
||||
};
|
||||
});
|
||||
|
||||
expect(columnsWithCellActions[0]?.cellActions?.length).toEqual(5);
|
||||
expect(columnsWithCellActions[0]?.cellActions![4]).not.toEqual(EmptyComponent);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -5,21 +5,28 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { useCallback, useState, useMemo } from 'react';
|
||||
import { Filter } from '../../../../../../../src/plugins/data/public';
|
||||
import { EuiDataGridColumnCellActionProps } from '@elastic/eui';
|
||||
import { head, getOr, get, isEmpty } from 'lodash/fp';
|
||||
import React, { useMemo } from 'react';
|
||||
|
||||
import type {
|
||||
BrowserFields,
|
||||
TimelineNonEcsData,
|
||||
} from '../../../../../timelines/common/search_strategy';
|
||||
import { DataProvider, TGridCellAction } from '../../../../../timelines/common/types';
|
||||
import {
|
||||
ColumnHeaderOptions,
|
||||
DataProvider,
|
||||
TGridCellAction,
|
||||
} from '../../../../../timelines/common/types';
|
||||
import { getPageRowIndex } from '../../../../../timelines/public';
|
||||
import { Ecs } from '../../../../common/ecs';
|
||||
import { getMappedNonEcsValue } from '../../../timelines/components/timeline/body/data_driven_columns';
|
||||
import { FormattedFieldValue } from '../../../timelines/components/timeline/body/renderers/formatted_field';
|
||||
import { parseValue } from '../../../timelines/components/timeline/body/renderers/parse_value';
|
||||
import { IS_OPERATOR } from '../../../timelines/components/timeline/data_providers/data_provider';
|
||||
import { allowTopN, escapeDataProviderId } from '../../components/drag_and_drop/helpers';
|
||||
import { ShowTopNButton } from '../../components/hover_actions/actions/show_top_n';
|
||||
import { getAllFieldsByName } from '../../containers/source';
|
||||
import { escapeDataProviderId } from '../../components/drag_and_drop/helpers';
|
||||
import { useKibana } from '../kibana';
|
||||
import { getLink } from './helpers';
|
||||
|
||||
/** a noop required by the filter in / out buttons */
|
||||
const onFilterAdded = () => {};
|
||||
|
@ -36,15 +43,77 @@ const useKibanaServices = () => {
|
|||
return { timelines, filterManager };
|
||||
};
|
||||
|
||||
/** the default actions shown in `EuiDataGrid` cells */
|
||||
export const defaultCellActions: TGridCellAction[] = [
|
||||
export const EmptyComponent = () => <></>;
|
||||
|
||||
const cellActionLink = [
|
||||
({
|
||||
browserFields,
|
||||
data,
|
||||
ecsData,
|
||||
header,
|
||||
timelineId,
|
||||
pageSize,
|
||||
}: {
|
||||
browserFields: BrowserFields;
|
||||
data: TimelineNonEcsData[][];
|
||||
ecsData: Ecs[];
|
||||
header?: ColumnHeaderOptions;
|
||||
timelineId: string;
|
||||
pageSize: number;
|
||||
}) =>
|
||||
getLink(header?.id, header?.type, header?.linkField)
|
||||
? ({ rowIndex, columnId, Component, closePopover }: EuiDataGridColumnCellActionProps) => {
|
||||
const pageRowIndex = getPageRowIndex(rowIndex, pageSize);
|
||||
const ecs = pageRowIndex < ecsData.length ? ecsData[pageRowIndex] : null;
|
||||
const linkValues = header && getOr([], header.linkField ?? '', ecs);
|
||||
const eventId = header && get('_id' ?? '', ecs);
|
||||
|
||||
if (pageRowIndex >= data.length) {
|
||||
// data grid expects each cell action always return an element, it crashes if returns null
|
||||
return <></>;
|
||||
}
|
||||
|
||||
const values = getMappedNonEcsValue({
|
||||
data: data[pageRowIndex],
|
||||
fieldName: columnId,
|
||||
});
|
||||
|
||||
const link = getLink(columnId, header?.type, header?.linkField);
|
||||
const value = parseValue(head(values));
|
||||
|
||||
return link && eventId && values && !isEmpty(value) ? (
|
||||
<FormattedFieldValue
|
||||
Component={Component}
|
||||
contextId={`expanded-value-${columnId}-row-${pageRowIndex}-${timelineId}`}
|
||||
eventId={eventId}
|
||||
fieldFormat={header?.format || ''}
|
||||
fieldName={columnId}
|
||||
fieldType={header?.type || ''}
|
||||
isButton={true}
|
||||
isDraggable={false}
|
||||
value={value}
|
||||
truncate={false}
|
||||
title={values.length > 1 ? `${link?.label}: ${value}` : link?.label}
|
||||
linkValue={head(linkValues)}
|
||||
onClick={closePopover}
|
||||
/>
|
||||
) : (
|
||||
// data grid expects each cell action always return an element, it crashes if returns null
|
||||
<></>
|
||||
);
|
||||
}
|
||||
: EmptyComponent,
|
||||
];
|
||||
|
||||
export const cellActions: TGridCellAction[] = [
|
||||
({ data, pageSize }: { data: TimelineNonEcsData[][]; pageSize: number }) =>
|
||||
({ rowIndex, columnId, Component }) => {
|
||||
({ rowIndex, columnId, Component }: EuiDataGridColumnCellActionProps) => {
|
||||
const { timelines, filterManager } = useKibanaServices();
|
||||
|
||||
const pageRowIndex = getPageRowIndex(rowIndex, pageSize);
|
||||
if (pageRowIndex >= data.length) {
|
||||
return null;
|
||||
// data grid expects each cell action always return an element, it crashes if returns null
|
||||
return <></>;
|
||||
}
|
||||
|
||||
const value = getMappedNonEcsValue({
|
||||
|
@ -72,7 +141,8 @@ export const defaultCellActions: TGridCellAction[] = [
|
|||
|
||||
const pageRowIndex = getPageRowIndex(rowIndex, pageSize);
|
||||
if (pageRowIndex >= data.length) {
|
||||
return null;
|
||||
// data grid expects each cell action always return an element, it crashes if returns null
|
||||
return <></>;
|
||||
}
|
||||
|
||||
const value = getMappedNonEcsValue({
|
||||
|
@ -100,34 +170,8 @@ export const defaultCellActions: TGridCellAction[] = [
|
|||
|
||||
const pageRowIndex = getPageRowIndex(rowIndex, pageSize);
|
||||
if (pageRowIndex >= data.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const value = getMappedNonEcsValue({
|
||||
data: data[pageRowIndex],
|
||||
fieldName: columnId,
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
{timelines.getHoverActions().getCopyButton({
|
||||
Component,
|
||||
field: columnId,
|
||||
isHoverAction: false,
|
||||
ownFocus: false,
|
||||
showTooltip: false,
|
||||
value,
|
||||
})}
|
||||
</>
|
||||
);
|
||||
},
|
||||
({ data, pageSize }: { data: TimelineNonEcsData[][]; pageSize: number }) =>
|
||||
({ rowIndex, columnId, Component }) => {
|
||||
const { timelines } = useKibanaServices();
|
||||
|
||||
const pageRowIndex = getPageRowIndex(rowIndex, pageSize);
|
||||
if (pageRowIndex >= data.length) {
|
||||
return null;
|
||||
// data grid expects each cell action always return an element, it crashes if returns null
|
||||
return <></>;
|
||||
}
|
||||
|
||||
const value = getMappedNonEcsValue({
|
||||
|
@ -165,26 +209,14 @@ export const defaultCellActions: TGridCellAction[] = [
|
|||
</>
|
||||
);
|
||||
},
|
||||
({
|
||||
browserFields,
|
||||
data,
|
||||
globalFilters,
|
||||
timelineId,
|
||||
pageSize,
|
||||
}: {
|
||||
browserFields: BrowserFields;
|
||||
data: TimelineNonEcsData[][];
|
||||
globalFilters?: Filter[];
|
||||
timelineId: string;
|
||||
pageSize: number;
|
||||
}) =>
|
||||
({ data, pageSize }: { data: TimelineNonEcsData[][]; pageSize: number }) =>
|
||||
({ rowIndex, columnId, Component }) => {
|
||||
const [showTopN, setShowTopN] = useState(false);
|
||||
const onClick = useCallback(() => setShowTopN(!showTopN), [showTopN]);
|
||||
const { timelines } = useKibanaServices();
|
||||
|
||||
const pageRowIndex = getPageRowIndex(rowIndex, pageSize);
|
||||
if (pageRowIndex >= data.length) {
|
||||
return null;
|
||||
// data grid expects each cell action always return an element, it crashes if returns null
|
||||
return <></>;
|
||||
}
|
||||
|
||||
const value = getMappedNonEcsValue({
|
||||
|
@ -192,31 +224,20 @@ export const defaultCellActions: TGridCellAction[] = [
|
|||
fieldName: columnId,
|
||||
});
|
||||
|
||||
const showButton = useMemo(
|
||||
() =>
|
||||
allowTopN({
|
||||
browserField: getAllFieldsByName(browserFields)[columnId],
|
||||
fieldName: columnId,
|
||||
hideTopN: false,
|
||||
}),
|
||||
[columnId]
|
||||
return (
|
||||
<>
|
||||
{timelines.getHoverActions().getCopyButton({
|
||||
Component,
|
||||
field: columnId,
|
||||
isHoverAction: false,
|
||||
ownFocus: false,
|
||||
showTooltip: false,
|
||||
value,
|
||||
})}
|
||||
</>
|
||||
);
|
||||
|
||||
return showButton ? (
|
||||
<ShowTopNButton
|
||||
Component={Component}
|
||||
enablePopOver
|
||||
data-test-subj="hover-actions-show-top-n"
|
||||
field={columnId}
|
||||
globalFilters={globalFilters}
|
||||
onClick={onClick}
|
||||
onFilterAdded={onFilterAdded}
|
||||
ownFocus={false}
|
||||
showTopN={showTopN}
|
||||
showTooltip={false}
|
||||
timelineId={timelineId}
|
||||
value={value}
|
||||
/>
|
||||
) : null;
|
||||
},
|
||||
];
|
||||
|
||||
/** the default actions shown in `EuiDataGrid` cells */
|
||||
export const defaultCellActions = [...cellActions, ...cellActionLink];
|
||||
|
|
|
@ -0,0 +1,49 @@
|
|||
/*
|
||||
* 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 { shallow } from 'enzyme';
|
||||
import React from 'react';
|
||||
import { ExpandedCellValueActions } from './expanded_cell_value_actions';
|
||||
|
||||
jest.mock('../kibana');
|
||||
|
||||
describe('ExpandedCellValueActions', () => {
|
||||
const props = {
|
||||
browserFields: {
|
||||
host: {
|
||||
fields: {
|
||||
'host.name': {
|
||||
aggregatable: true,
|
||||
category: 'host',
|
||||
description:
|
||||
'Name of the host. It can contain what `hostname` returns on Unix systems, the fully qualified domain name, or a name specified by the user. The sender decides which value to use.',
|
||||
type: 'string',
|
||||
name: 'host.name',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
field: 'host.name',
|
||||
globalFilters: [],
|
||||
onFilterAdded: () => {},
|
||||
timelineId: 'mockTimelineId',
|
||||
value: ['mock value'],
|
||||
};
|
||||
const wrapper = shallow(<ExpandedCellValueActions {...props} />);
|
||||
|
||||
test('renders show topN button', () => {
|
||||
expect(wrapper.find('[data-test-subj="data-grid-expanded-show-top-n"]').exists()).toBeTruthy();
|
||||
});
|
||||
|
||||
test('renders filter in button', () => {
|
||||
expect(wrapper.find('EuiFlexItem').first().html()).toContain('Filter button');
|
||||
});
|
||||
|
||||
test('renders filter out button', () => {
|
||||
expect(wrapper.find('EuiFlexItem').last().html()).toContain('Filter out button');
|
||||
});
|
||||
});
|
|
@ -0,0 +1,125 @@
|
|||
/*
|
||||
* 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 { EuiButtonEmpty, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
||||
import { noop } from 'lodash/fp';
|
||||
import React, { useMemo, useState, useCallback } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { Filter } from '../../../../../../../src/plugins/data/public';
|
||||
import { BrowserFields } from '../../../../../timelines/common/search_strategy';
|
||||
import { allowTopN } from '../../components/drag_and_drop/helpers';
|
||||
import { ShowTopNButton } from '../../components/hover_actions/actions/show_top_n';
|
||||
import { getAllFieldsByName } from '../../containers/source';
|
||||
import { useKibana } from '../kibana';
|
||||
import { SHOW_TOP_VALUES, HIDE_TOP_VALUES } from './translations';
|
||||
|
||||
interface Props {
|
||||
browserFields: BrowserFields;
|
||||
field: string;
|
||||
globalFilters?: Filter[];
|
||||
timelineId: string;
|
||||
value: string[] | undefined;
|
||||
onFilterAdded?: () => void;
|
||||
}
|
||||
|
||||
const StyledFlexGroup = styled(EuiFlexGroup)`
|
||||
border-top: 1px solid #d3dae6;
|
||||
border-bottom: 1px solid #d3dae6;
|
||||
margin-top: 2px;
|
||||
`;
|
||||
|
||||
export const StyledContent = styled.div<{ $isDetails: boolean }>`
|
||||
padding: ${({ $isDetails }) => ($isDetails ? '0 8px' : undefined)};
|
||||
`;
|
||||
|
||||
const ExpandedCellValueActionsComponent: React.FC<Props> = ({
|
||||
browserFields,
|
||||
field,
|
||||
globalFilters,
|
||||
onFilterAdded,
|
||||
timelineId,
|
||||
value,
|
||||
}) => {
|
||||
const {
|
||||
timelines,
|
||||
data: {
|
||||
query: { filterManager },
|
||||
},
|
||||
} = useKibana().services;
|
||||
const showButton = useMemo(
|
||||
() =>
|
||||
allowTopN({
|
||||
browserField: getAllFieldsByName(browserFields)[field],
|
||||
fieldName: field,
|
||||
hideTopN: false,
|
||||
}),
|
||||
[browserFields, field]
|
||||
);
|
||||
|
||||
const [showTopN, setShowTopN] = useState(false);
|
||||
const onClick = useCallback(() => setShowTopN(!showTopN), [showTopN]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<StyledContent $isDetails data-test-subj="data-grid-expanded-cell-value-actions">
|
||||
{showButton ? (
|
||||
<ShowTopNButton
|
||||
className="eui-displayBlock expandable-top-value-button"
|
||||
Component={EuiButtonEmpty}
|
||||
data-test-subj="data-grid-expanded-show-top-n"
|
||||
field={field}
|
||||
flush="both"
|
||||
globalFilters={globalFilters}
|
||||
iconSide="right"
|
||||
iconType={showTopN ? 'arrowUp' : 'arrowDown'}
|
||||
isExpandable
|
||||
onClick={onClick}
|
||||
onFilterAdded={onFilterAdded ?? noop}
|
||||
ownFocus={false}
|
||||
paddingSize="none"
|
||||
showLegend
|
||||
showTopN={showTopN}
|
||||
showTooltip={false}
|
||||
timelineId={timelineId}
|
||||
title={showTopN ? HIDE_TOP_VALUES : SHOW_TOP_VALUES}
|
||||
value={value}
|
||||
/>
|
||||
) : null}
|
||||
</StyledContent>
|
||||
<StyledFlexGroup gutterSize="s">
|
||||
<EuiFlexItem>
|
||||
{timelines.getHoverActions().getFilterForValueButton({
|
||||
Component: EuiButtonEmpty,
|
||||
field,
|
||||
filterManager,
|
||||
onFilterAdded,
|
||||
ownFocus: false,
|
||||
size: 's',
|
||||
showTooltip: false,
|
||||
value,
|
||||
})}
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
{timelines.getHoverActions().getFilterOutValueButton({
|
||||
Component: EuiButtonEmpty,
|
||||
field,
|
||||
filterManager,
|
||||
onFilterAdded,
|
||||
ownFocus: false,
|
||||
size: 's',
|
||||
showTooltip: false,
|
||||
value,
|
||||
})}
|
||||
</EuiFlexItem>
|
||||
</StyledFlexGroup>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
ExpandedCellValueActionsComponent.displayName = 'ExpandedCellValueActionsComponent';
|
||||
|
||||
export const ExpandedCellValueActions = React.memo(ExpandedCellValueActionsComponent);
|
|
@ -0,0 +1,65 @@
|
|||
/*
|
||||
* 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 * as i18n from './translations';
|
||||
import {
|
||||
EVENT_URL_FIELD_NAME,
|
||||
HOST_NAME_FIELD_NAME,
|
||||
REFERENCE_URL_FIELD_NAME,
|
||||
RULE_REFERENCE_FIELD_NAME,
|
||||
SIGNAL_RULE_NAME_FIELD_NAME,
|
||||
} from '../../../timelines/components/timeline/body/renderers/constants';
|
||||
import { INDICATOR_REFERENCE } from '../../../../common/cti/constants';
|
||||
import { IP_FIELD_TYPE } from '../../../network/components/ip';
|
||||
import { PORT_NAMES } from '../../../network/components/port/helpers';
|
||||
|
||||
export const COLUMNS_WITH_LINKS = [
|
||||
{
|
||||
columnId: HOST_NAME_FIELD_NAME,
|
||||
label: i18n.VIEW_HOST_SUMMARY,
|
||||
},
|
||||
{
|
||||
columnId: 'source.ip',
|
||||
fieldType: IP_FIELD_TYPE,
|
||||
label: i18n.EXPAND_IP_DETAILS,
|
||||
},
|
||||
{
|
||||
columnId: 'destination.ip',
|
||||
fieldType: IP_FIELD_TYPE,
|
||||
label: i18n.EXPAND_IP_DETAILS,
|
||||
},
|
||||
{
|
||||
columnId: SIGNAL_RULE_NAME_FIELD_NAME,
|
||||
label: i18n.VIEW_RULE_DETAILS,
|
||||
},
|
||||
...PORT_NAMES.map((p) => ({
|
||||
columnId: p,
|
||||
label: i18n.VIEW_PORT_DETAILS,
|
||||
})),
|
||||
{
|
||||
columnId: RULE_REFERENCE_FIELD_NAME,
|
||||
label: i18n.VIEW_RULE_REFERENCE,
|
||||
},
|
||||
{
|
||||
columnId: REFERENCE_URL_FIELD_NAME,
|
||||
label: i18n.VIEW_RULE_REFERENCE,
|
||||
},
|
||||
{
|
||||
columnId: EVENT_URL_FIELD_NAME,
|
||||
label: i18n.VIEW_EVENT_REFERENCE,
|
||||
},
|
||||
{
|
||||
columnId: INDICATOR_REFERENCE,
|
||||
label: i18n.VIEW_INDICATOR_REFERENCE,
|
||||
},
|
||||
];
|
||||
|
||||
export const getLink = (cId?: string, fieldType?: string, linkField?: string) =>
|
||||
cId &&
|
||||
COLUMNS_WITH_LINKS.find(
|
||||
(c) => c.columnId === cId || (c.fieldType && fieldType === c.fieldType && linkField != null)
|
||||
);
|
|
@ -0,0 +1,71 @@
|
|||
/*
|
||||
* 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 { i18n } from '@kbn/i18n';
|
||||
|
||||
export const SHOW_TOP_VALUES = i18n.translate(
|
||||
'xpack.securitySolution.expandedValue.showTopN.showTopValues',
|
||||
{
|
||||
defaultMessage: 'Show top values',
|
||||
}
|
||||
);
|
||||
|
||||
export const HIDE_TOP_VALUES = i18n.translate(
|
||||
'xpack.securitySolution.expandedValue.hideTopValues.HideTopValues',
|
||||
{
|
||||
defaultMessage: 'Hide top values',
|
||||
}
|
||||
);
|
||||
|
||||
export const VIEW_HOST_SUMMARY = i18n.translate(
|
||||
'xpack.securitySolution.expandedValue.links.viewHostSummary',
|
||||
{
|
||||
defaultMessage: 'View host summary',
|
||||
}
|
||||
);
|
||||
|
||||
export const EXPAND_IP_DETAILS = i18n.translate(
|
||||
'xpack.securitySolution.expandedValue.links.expandIpDetails',
|
||||
{
|
||||
defaultMessage: 'Expand ip details',
|
||||
}
|
||||
);
|
||||
|
||||
export const VIEW_RULE_DETAILS = i18n.translate(
|
||||
'xpack.securitySolution.expandedValue.links.viewRuleDetails',
|
||||
{
|
||||
defaultMessage: 'View rule details',
|
||||
}
|
||||
);
|
||||
|
||||
export const VIEW_PORT_DETAILS = i18n.translate(
|
||||
'xpack.securitySolution.expandedValue.links.viewPortDetails',
|
||||
{
|
||||
defaultMessage: 'View port details',
|
||||
}
|
||||
);
|
||||
|
||||
export const VIEW_RULE_REFERENCE = i18n.translate(
|
||||
'xpack.securitySolution.expandedValue.links.viewRuleReference',
|
||||
{
|
||||
defaultMessage: 'View rule reference',
|
||||
}
|
||||
);
|
||||
|
||||
export const VIEW_EVENT_REFERENCE = i18n.translate(
|
||||
'xpack.securitySolution.expandedValue.links.viewEventReference',
|
||||
{
|
||||
defaultMessage: 'View event reference',
|
||||
}
|
||||
);
|
||||
|
||||
export const VIEW_INDICATOR_REFERENCE = i18n.translate(
|
||||
'xpack.securitySolution.expandedValue.links.viewIndicatorReference',
|
||||
{
|
||||
defaultMessage: 'View indicator reference',
|
||||
}
|
||||
);
|
|
@ -31,6 +31,7 @@ interface AlertsHistogramProps {
|
|||
legendItems: LegendItem[];
|
||||
legendPosition?: Position;
|
||||
loading: boolean;
|
||||
showLegend?: boolean;
|
||||
to: string;
|
||||
data: HistogramData[];
|
||||
updateDateRange: UpdateDateRange;
|
||||
|
@ -43,6 +44,7 @@ export const AlertsHistogram = React.memo<AlertsHistogramProps>(
|
|||
legendItems,
|
||||
legendPosition = 'right',
|
||||
loading,
|
||||
showLegend,
|
||||
to,
|
||||
updateDateRange,
|
||||
}) => {
|
||||
|
@ -73,8 +75,9 @@ export const AlertsHistogram = React.memo<AlertsHistogramProps>(
|
|||
<Settings
|
||||
legendPosition={legendPosition}
|
||||
onBrushEnd={updateDateRange}
|
||||
showLegend={legendItems.length === 0}
|
||||
showLegendExtra
|
||||
// showLegend controls the default legend coming from Elastic chart, we show them when our customised legend items doesn't exist (but we still want to show legend).
|
||||
showLegend={showLegend && legendItems.length === 0}
|
||||
showLegendExtra={showLegend}
|
||||
theme={theme}
|
||||
/>
|
||||
|
||||
|
|
|
@ -63,10 +63,12 @@ interface AlertsHistogramPanelProps {
|
|||
headerChildren?: React.ReactNode;
|
||||
/** Override all defaults, and only display this field */
|
||||
onlyField?: AlertsStackByField;
|
||||
paddingSize?: 's' | 'm' | 'l' | 'none';
|
||||
titleSize?: EuiTitleSize;
|
||||
query?: Query;
|
||||
legendPosition?: Position;
|
||||
signalIndexName: string | null;
|
||||
showLegend?: boolean;
|
||||
showLinkToAlerts?: boolean;
|
||||
showTotalAlertsCount?: boolean;
|
||||
showStackBy?: boolean;
|
||||
|
@ -85,9 +87,11 @@ export const AlertsHistogramPanel = memo<AlertsHistogramPanelProps>(
|
|||
filters,
|
||||
headerChildren,
|
||||
onlyField,
|
||||
paddingSize = 'm',
|
||||
query,
|
||||
legendPosition = 'right',
|
||||
signalIndexName,
|
||||
showLegend = true,
|
||||
showLinkToAlerts = false,
|
||||
showTotalAlertsCount = false,
|
||||
showStackBy = true,
|
||||
|
@ -154,7 +158,7 @@ export const AlertsHistogramPanel = memo<AlertsHistogramPanelProps>(
|
|||
|
||||
const legendItems: LegendItem[] = useMemo(
|
||||
() =>
|
||||
alertsData?.aggregations?.alertsByGrouping?.buckets != null
|
||||
showLegend && alertsData?.aggregations?.alertsByGrouping?.buckets != null
|
||||
? alertsData.aggregations.alertsByGrouping.buckets.map((bucket, i) => ({
|
||||
color: i < defaultLegendColors.length ? defaultLegendColors[i] : undefined,
|
||||
dataProviderId: escapeDataProviderId(
|
||||
|
@ -165,7 +169,12 @@ export const AlertsHistogramPanel = memo<AlertsHistogramPanelProps>(
|
|||
value: bucket.key,
|
||||
}))
|
||||
: NO_LEGEND_DATA,
|
||||
[alertsData, selectedStackByOption, timelineId]
|
||||
[
|
||||
alertsData?.aggregations?.alertsByGrouping.buckets,
|
||||
selectedStackByOption,
|
||||
showLegend,
|
||||
timelineId,
|
||||
]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -254,7 +263,7 @@ export const AlertsHistogramPanel = memo<AlertsHistogramPanelProps>(
|
|||
|
||||
return (
|
||||
<InspectButtonContainer data-test-subj="alerts-histogram-panel" show={!isInitialLoading}>
|
||||
<KpiPanel height={PANEL_HEIGHT} hasBorder>
|
||||
<KpiPanel height={PANEL_HEIGHT} hasBorder paddingSize={paddingSize}>
|
||||
<HeaderSection
|
||||
id={uniqueQueryId}
|
||||
title={titleText}
|
||||
|
@ -288,6 +297,7 @@ export const AlertsHistogramPanel = memo<AlertsHistogramPanelProps>(
|
|||
legendPosition={legendPosition}
|
||||
loading={isLoadingAlerts}
|
||||
to={to}
|
||||
showLegend={showLegend}
|
||||
updateDateRange={updateDateRange}
|
||||
/>
|
||||
)}
|
||||
|
|
|
@ -375,21 +375,21 @@ export const AlertsTableComponent: React.FC<AlertsTableComponentProps> = ({
|
|||
|
||||
return (
|
||||
<StatefulEventsViewer
|
||||
pageFilters={defaultFiltersMemo}
|
||||
additionalFilters={additionalFiltersComponent}
|
||||
currentFilter={filterGroup}
|
||||
defaultCellActions={defaultCellActions}
|
||||
defaultModel={defaultTimelineModel}
|
||||
entityType="events"
|
||||
end={to}
|
||||
currentFilter={filterGroup}
|
||||
entityType="events"
|
||||
hasAlertsCrud={hasIndexWrite && hasIndexMaintenance}
|
||||
id={timelineId}
|
||||
onRuleChange={onRuleChange}
|
||||
pageFilters={defaultFiltersMemo}
|
||||
renderCellValue={RenderCellValue}
|
||||
rowRenderers={defaultRowRenderers}
|
||||
scopeId={SourcererScopeName.detections}
|
||||
start={from}
|
||||
utilityBar={utilityBarCallback}
|
||||
additionalFilters={additionalFiltersComponent}
|
||||
hasAlertsCrud={hasIndexWrite && hasIndexMaintenance}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -18,37 +18,41 @@ import { DefaultCellRenderer } from '../../../timelines/components/timeline/cell
|
|||
*/
|
||||
export const RenderCellValue: React.FC<EuiDataGridCellValueElementProps & CellValueElementProps> =
|
||||
({
|
||||
browserFields,
|
||||
columnId,
|
||||
data,
|
||||
ecsData,
|
||||
eventId,
|
||||
isDraggable,
|
||||
globalFilters,
|
||||
header,
|
||||
isDetails,
|
||||
isDraggable,
|
||||
isExpandable,
|
||||
isExpanded,
|
||||
linkValues,
|
||||
rowIndex,
|
||||
rowRenderers,
|
||||
setCellProps,
|
||||
timelineId,
|
||||
ecsData,
|
||||
rowRenderers,
|
||||
browserFields,
|
||||
truncate,
|
||||
}) => (
|
||||
<DefaultCellRenderer
|
||||
browserFields={browserFields}
|
||||
columnId={columnId}
|
||||
data={data}
|
||||
ecsData={ecsData}
|
||||
eventId={eventId}
|
||||
isDraggable={isDraggable}
|
||||
globalFilters={globalFilters}
|
||||
header={header}
|
||||
isDetails={isDetails}
|
||||
isDraggable={isDraggable}
|
||||
isExpandable={isExpandable}
|
||||
isExpanded={isExpanded}
|
||||
linkValues={linkValues}
|
||||
rowIndex={rowIndex}
|
||||
rowRenderers={rowRenderers}
|
||||
setCellProps={setCellProps}
|
||||
timelineId={timelineId}
|
||||
ecsData={ecsData}
|
||||
rowRenderers={rowRenderers}
|
||||
browserFields={browserFields}
|
||||
truncate={truncate}
|
||||
/>
|
||||
);
|
||||
|
|
|
@ -115,11 +115,11 @@ const EventsQueryTabBodyComponent: React.FC<HostsComponentsQueryProps> = ({
|
|||
end={endDate}
|
||||
entityType="events"
|
||||
id={TimelineId.hostsPageEvents}
|
||||
pageFilters={pageFilters}
|
||||
renderCellValue={DefaultCellRenderer}
|
||||
rowRenderers={defaultRowRenderers}
|
||||
scopeId={SourcererScopeName.default}
|
||||
start={startDate}
|
||||
pageFilters={pageFilters}
|
||||
unit={unit}
|
||||
/>
|
||||
</>
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
export const CLIENT_PORT_FIELD_NAME = 'client.port';
|
||||
export const DESTINATION_PORT_FIELD_NAME = 'destination.port';
|
||||
export const SERVER_PORT_FIELD_NAME = 'server.port';
|
||||
export const SOURCE_PORT_FIELD_NAME = 'source.port';
|
||||
export const URL_PORT_FIELD_NAME = 'url.port';
|
||||
|
||||
export const PORT_NAMES = [
|
||||
CLIENT_PORT_FIELD_NAME,
|
||||
DESTINATION_PORT_FIELD_NAME,
|
||||
SERVER_PORT_FIELD_NAME,
|
||||
SOURCE_PORT_FIELD_NAME,
|
||||
URL_PORT_FIELD_NAME,
|
||||
];
|
|
@ -5,33 +5,22 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { EuiButtonEmpty, EuiButtonIcon } from '@elastic/eui';
|
||||
import React from 'react';
|
||||
|
||||
import { DefaultDraggable } from '../../../common/components/draggables';
|
||||
import { getEmptyValue } from '../../../common/components/empty_value';
|
||||
import { PortOrServiceNameLink } from '../../../common/components/links';
|
||||
|
||||
export const CLIENT_PORT_FIELD_NAME = 'client.port';
|
||||
export const DESTINATION_PORT_FIELD_NAME = 'destination.port';
|
||||
export const SERVER_PORT_FIELD_NAME = 'server.port';
|
||||
export const SOURCE_PORT_FIELD_NAME = 'source.port';
|
||||
export const URL_PORT_FIELD_NAME = 'url.port';
|
||||
|
||||
export const PORT_NAMES = [
|
||||
CLIENT_PORT_FIELD_NAME,
|
||||
DESTINATION_PORT_FIELD_NAME,
|
||||
SERVER_PORT_FIELD_NAME,
|
||||
SOURCE_PORT_FIELD_NAME,
|
||||
URL_PORT_FIELD_NAME,
|
||||
];
|
||||
|
||||
export const Port = React.memo<{
|
||||
contextId: string;
|
||||
Component?: typeof EuiButtonEmpty | typeof EuiButtonIcon;
|
||||
eventId: string;
|
||||
fieldName: string;
|
||||
isDraggable?: boolean;
|
||||
title?: string;
|
||||
value: string | undefined | null;
|
||||
}>(({ contextId, eventId, fieldName, isDraggable, value }) =>
|
||||
}>(({ Component, contextId, eventId, fieldName, isDraggable, title, value }) =>
|
||||
isDraggable ? (
|
||||
<DefaultDraggable
|
||||
data-test-subj="port"
|
||||
|
@ -41,10 +30,18 @@ export const Port = React.memo<{
|
|||
tooltipContent={fieldName}
|
||||
value={value}
|
||||
>
|
||||
<PortOrServiceNameLink portOrServiceName={value || getEmptyValue()} />
|
||||
<PortOrServiceNameLink
|
||||
portOrServiceName={value || getEmptyValue()}
|
||||
Component={Component}
|
||||
title={title}
|
||||
/>
|
||||
</DefaultDraggable>
|
||||
) : (
|
||||
<PortOrServiceNameLink portOrServiceName={value || getEmptyValue()} />
|
||||
<PortOrServiceNameLink
|
||||
portOrServiceName={value || getEmptyValue()}
|
||||
Component={Component}
|
||||
title={title}
|
||||
/>
|
||||
)
|
||||
);
|
||||
|
||||
|
|
|
@ -18,7 +18,7 @@ import { TestProviders } from '../../../common/mock/test_providers';
|
|||
import { ID_FIELD_NAME } from '../../../common/components/event_details/event_id';
|
||||
import { useMountAppended } from '../../../common/utils/use_mount_appended';
|
||||
import { DESTINATION_IP_FIELD_NAME, SOURCE_IP_FIELD_NAME } from '../ip';
|
||||
import { DESTINATION_PORT_FIELD_NAME, SOURCE_PORT_FIELD_NAME } from '../port';
|
||||
import { DESTINATION_PORT_FIELD_NAME, SOURCE_PORT_FIELD_NAME } from '../port/helpers';
|
||||
import {
|
||||
DESTINATION_BYTES_FIELD_NAME,
|
||||
DESTINATION_PACKETS_FIELD_NAME,
|
||||
|
|
|
@ -15,7 +15,7 @@ import '../../../common/mock/match_media';
|
|||
import { TestProviders } from '../../../common/mock/test_providers';
|
||||
import { ID_FIELD_NAME } from '../../../common/components/event_details/event_id';
|
||||
import { DESTINATION_IP_FIELD_NAME, SOURCE_IP_FIELD_NAME } from '../ip';
|
||||
import { DESTINATION_PORT_FIELD_NAME, SOURCE_PORT_FIELD_NAME } from '../port';
|
||||
import { DESTINATION_PORT_FIELD_NAME, SOURCE_PORT_FIELD_NAME } from '../port/helpers';
|
||||
import * as i18n from '../../../timelines/components/timeline/body/renderers/translations';
|
||||
import { useMountAppended } from '../../../common/utils/use_mount_appended';
|
||||
|
||||
|
|
|
@ -11,7 +11,8 @@ import React from 'react';
|
|||
import deepEqual from 'fast-deep-equal';
|
||||
|
||||
import { DESTINATION_IP_FIELD_NAME, SOURCE_IP_FIELD_NAME } from '../ip';
|
||||
import { DESTINATION_PORT_FIELD_NAME, SOURCE_PORT_FIELD_NAME, Port } from '../port';
|
||||
import { Port } from '../port';
|
||||
import { DESTINATION_PORT_FIELD_NAME, SOURCE_PORT_FIELD_NAME } from '../port/helpers';
|
||||
import * as i18n from '../../../timelines/components/timeline/body/renderers/translations';
|
||||
|
||||
import { GeoFields } from './geo_fields';
|
||||
|
|
|
@ -49,8 +49,10 @@ interface Props extends Pick<GlobalTimeArgs, 'from' | 'to' | 'deleteQuery' | 'se
|
|||
indexPattern: IIndexPattern;
|
||||
indexNames: string[];
|
||||
onlyField?: string;
|
||||
paddingSize?: 's' | 'm' | 'l' | 'none';
|
||||
query: Query;
|
||||
setAbsoluteRangeDatePickerTarget?: InputsModelId;
|
||||
showLegend?: boolean;
|
||||
showSpacer?: boolean;
|
||||
timelineId?: string;
|
||||
toggleTopN?: () => void;
|
||||
|
@ -70,9 +72,11 @@ const EventsByDatasetComponent: React.FC<Props> = ({
|
|||
indexPattern,
|
||||
indexNames,
|
||||
onlyField,
|
||||
paddingSize,
|
||||
query,
|
||||
setAbsoluteRangeDatePickerTarget,
|
||||
setQuery,
|
||||
showLegend,
|
||||
showSpacer = true,
|
||||
timelineId,
|
||||
to,
|
||||
|
@ -177,9 +181,11 @@ const EventsByDatasetComponent: React.FC<Props> = ({
|
|||
id={uniqueQueryId}
|
||||
indexNames={indexNames}
|
||||
onError={toggleTopN}
|
||||
paddingSize={paddingSize}
|
||||
setAbsoluteRangeDatePickerTarget={setAbsoluteRangeDatePickerTarget}
|
||||
setQuery={setQuery}
|
||||
showSpacer={showSpacer}
|
||||
showLegend={showLegend}
|
||||
skip={filterQuery === undefined}
|
||||
startDate={from}
|
||||
timelineId={timelineId}
|
||||
|
|
|
@ -23,8 +23,10 @@ interface Props {
|
|||
headerChildren?: React.ReactNode;
|
||||
/** Override all defaults, and only display this field */
|
||||
onlyField?: AlertsStackByField;
|
||||
paddingSize?: 's' | 'm' | 'l' | 'none';
|
||||
query?: Query;
|
||||
setAbsoluteRangeDatePickerTarget?: InputsModelId;
|
||||
showLegend?: boolean;
|
||||
timelineId?: string;
|
||||
}
|
||||
|
||||
|
@ -33,7 +35,9 @@ const SignalsByCategoryComponent: React.FC<Props> = ({
|
|||
filters,
|
||||
headerChildren,
|
||||
onlyField,
|
||||
paddingSize,
|
||||
query,
|
||||
showLegend,
|
||||
setAbsoluteRangeDatePickerTarget = 'global',
|
||||
timelineId,
|
||||
}) => {
|
||||
|
@ -61,16 +65,18 @@ const SignalsByCategoryComponent: React.FC<Props> = ({
|
|||
combinedQueries={combinedQueries}
|
||||
filters={filters}
|
||||
headerChildren={headerChildren}
|
||||
legendPosition={'right'}
|
||||
onlyField={onlyField}
|
||||
titleSize={onlyField == null ? 'm' : 's'}
|
||||
paddingSize={paddingSize}
|
||||
query={query}
|
||||
signalIndexName={signalIndexName}
|
||||
showTotalAlertsCount={true}
|
||||
showLegend={showLegend}
|
||||
showLinkToAlerts={onlyField == null ? true : false}
|
||||
showStackBy={onlyField == null}
|
||||
legendPosition={'right'}
|
||||
showTotalAlertsCount={true}
|
||||
signalIndexName={signalIndexName}
|
||||
timelineId={timelineId}
|
||||
title={i18n.ALERT_COUNT}
|
||||
titleSize={onlyField == null ? 'm' : 's'}
|
||||
updateDateRange={updateDateRangeCallback}
|
||||
/>
|
||||
);
|
||||
|
|
|
@ -10,6 +10,7 @@ import React, { useCallback, useMemo, useContext } from 'react';
|
|||
import { useDispatch } from 'react-redux';
|
||||
import deepEqual from 'fast-deep-equal';
|
||||
|
||||
import { EuiButtonEmpty, EuiButtonIcon } from '@elastic/eui';
|
||||
import { FlowTarget } from '../../../../common/search_strategy/security_solution/network';
|
||||
import {
|
||||
DragEffects,
|
||||
|
@ -145,11 +146,15 @@ interface AddressLinksItemProps extends Omit<AddressLinksProps, 'addresses'> {
|
|||
|
||||
const AddressLinksItemComponent: React.FC<AddressLinksItemProps> = ({
|
||||
address,
|
||||
Component,
|
||||
contextId,
|
||||
eventId,
|
||||
fieldName,
|
||||
isButton,
|
||||
isDraggable,
|
||||
onClick,
|
||||
truncate,
|
||||
title,
|
||||
}) => {
|
||||
const key = `address-links-draggable-wrapper-${getUniqueId({
|
||||
contextId,
|
||||
|
@ -171,6 +176,10 @@ const AddressLinksItemComponent: React.FC<AddressLinksItemProps> = ({
|
|||
const openNetworkDetailsSidePanel = useCallback(
|
||||
(e) => {
|
||||
e.preventDefault();
|
||||
if (onClick) {
|
||||
onClick();
|
||||
}
|
||||
|
||||
if (eventContext && isInTimelineContext) {
|
||||
const { tabType, timelineID } = eventContext;
|
||||
const updatedExpandedDetail: TimelineExpandedDetailType = {
|
||||
|
@ -196,22 +205,41 @@ const AddressLinksItemComponent: React.FC<AddressLinksItemProps> = ({
|
|||
}
|
||||
}
|
||||
},
|
||||
[eventContext, isInTimelineContext, address, fieldName, dispatch]
|
||||
[onClick, eventContext, isInTimelineContext, address, fieldName, dispatch]
|
||||
);
|
||||
|
||||
// The below is explicitly defined this way as the onClick takes precedence when it and the href are both defined
|
||||
// When this component is used outside of timeline/alerts table (i.e. in the flyout) we would still like it to link to the IP Overview page
|
||||
const content = useMemo(
|
||||
() => (
|
||||
<Content field={fieldName} tooltipContent={fieldName}>
|
||||
() =>
|
||||
Component ? (
|
||||
<NetworkDetailsLink
|
||||
Component={Component}
|
||||
ip={address}
|
||||
isButton={false}
|
||||
isButton={isButton}
|
||||
onClick={isInTimelineContext ? openNetworkDetailsSidePanel : undefined}
|
||||
title={title}
|
||||
/>
|
||||
</Content>
|
||||
),
|
||||
[address, fieldName, isInTimelineContext, openNetworkDetailsSidePanel]
|
||||
) : (
|
||||
<Content field={fieldName} tooltipContent={fieldName}>
|
||||
<NetworkDetailsLink
|
||||
Component={Component}
|
||||
ip={address}
|
||||
isButton={isButton}
|
||||
onClick={isInTimelineContext ? openNetworkDetailsSidePanel : undefined}
|
||||
title={title}
|
||||
/>
|
||||
</Content>
|
||||
),
|
||||
[
|
||||
Component,
|
||||
address,
|
||||
fieldName,
|
||||
isButton,
|
||||
isInTimelineContext,
|
||||
openNetworkDetailsSidePanel,
|
||||
title,
|
||||
]
|
||||
);
|
||||
|
||||
const render = useCallback(
|
||||
|
@ -245,20 +273,28 @@ const AddressLinksItem = React.memo(AddressLinksItemComponent);
|
|||
|
||||
interface AddressLinksProps {
|
||||
addresses: string[];
|
||||
Component?: typeof EuiButtonEmpty | typeof EuiButtonIcon;
|
||||
contextId: string;
|
||||
eventId: string;
|
||||
fieldName: string;
|
||||
isButton?: boolean;
|
||||
isDraggable: boolean;
|
||||
onClick?: () => void;
|
||||
truncate?: boolean;
|
||||
title?: string;
|
||||
}
|
||||
|
||||
const AddressLinksComponent: React.FC<AddressLinksProps> = ({
|
||||
addresses,
|
||||
Component,
|
||||
contextId,
|
||||
eventId,
|
||||
fieldName,
|
||||
isButton,
|
||||
isDraggable,
|
||||
onClick,
|
||||
truncate,
|
||||
title,
|
||||
}) => {
|
||||
const uniqAddresses = useMemo(() => uniq(addresses), [addresses]);
|
||||
|
||||
|
@ -268,14 +304,29 @@ const AddressLinksComponent: React.FC<AddressLinksProps> = ({
|
|||
<AddressLinksItem
|
||||
key={address}
|
||||
address={address}
|
||||
Component={Component}
|
||||
contextId={contextId}
|
||||
eventId={eventId}
|
||||
fieldName={fieldName}
|
||||
isButton={isButton}
|
||||
isDraggable={isDraggable}
|
||||
onClick={onClick}
|
||||
truncate={truncate}
|
||||
title={title}
|
||||
/>
|
||||
)),
|
||||
[contextId, eventId, fieldName, isDraggable, truncate, uniqAddresses]
|
||||
[
|
||||
Component,
|
||||
contextId,
|
||||
eventId,
|
||||
fieldName,
|
||||
isButton,
|
||||
isDraggable,
|
||||
onClick,
|
||||
title,
|
||||
truncate,
|
||||
uniqAddresses,
|
||||
]
|
||||
);
|
||||
|
||||
return <>{content}</>;
|
||||
|
@ -293,13 +344,28 @@ const AddressLinks = React.memo(
|
|||
);
|
||||
|
||||
const FormattedIpComponent: React.FC<{
|
||||
Component?: typeof EuiButtonEmpty | typeof EuiButtonIcon;
|
||||
contextId: string;
|
||||
eventId: string;
|
||||
fieldName: string;
|
||||
isButton?: boolean;
|
||||
isDraggable: boolean;
|
||||
onClick?: () => void;
|
||||
title?: string;
|
||||
truncate?: boolean;
|
||||
value: string | object | null | undefined;
|
||||
}> = ({ contextId, eventId, fieldName, isDraggable, truncate, value }) => {
|
||||
}> = ({
|
||||
Component,
|
||||
contextId,
|
||||
eventId,
|
||||
fieldName,
|
||||
isDraggable,
|
||||
isButton,
|
||||
onClick,
|
||||
title,
|
||||
truncate,
|
||||
value,
|
||||
}) => {
|
||||
if (isString(value) && !isEmpty(value)) {
|
||||
try {
|
||||
const addresses = JSON.parse(value);
|
||||
|
@ -307,10 +373,14 @@ const FormattedIpComponent: React.FC<{
|
|||
return (
|
||||
<AddressLinks
|
||||
addresses={addresses}
|
||||
Component={Component}
|
||||
contextId={contextId}
|
||||
eventId={eventId}
|
||||
fieldName={fieldName}
|
||||
isButton={isButton}
|
||||
isDraggable={isDraggable}
|
||||
onClick={onClick}
|
||||
title={title}
|
||||
truncate={truncate}
|
||||
/>
|
||||
);
|
||||
|
@ -323,11 +393,15 @@ const FormattedIpComponent: React.FC<{
|
|||
return (
|
||||
<AddressLinks
|
||||
addresses={[value]}
|
||||
Component={Component}
|
||||
contextId={contextId}
|
||||
eventId={eventId}
|
||||
isButton={isButton}
|
||||
isDraggable={isDraggable}
|
||||
onClick={onClick}
|
||||
fieldName={fieldName}
|
||||
truncate={truncate}
|
||||
title={title}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
|
|
|
@ -25,7 +25,7 @@ import { JA3_HASH_FIELD_NAME } from '../ja3_fingerprint';
|
|||
import {
|
||||
DESTINATION_PORT_FIELD_NAME,
|
||||
SOURCE_PORT_FIELD_NAME,
|
||||
} from '../../../network/components/port';
|
||||
} from '../../../network/components/port/helpers';
|
||||
import {
|
||||
DESTINATION_GEO_CITY_NAME_FIELD_NAME,
|
||||
DESTINATION_GEO_CONTINENT_NAME_FIELD_NAME,
|
||||
|
|
|
@ -6,33 +6,44 @@
|
|||
*/
|
||||
|
||||
import type React from 'react';
|
||||
import { Filter } from '../../../../../../../../../src/plugins/data/public';
|
||||
import { BrowserFields } from '../../../../../../../timelines/common/search_strategy';
|
||||
|
||||
import { BrowserFields, ColumnHeaderOptions, RowRenderer } from '../../../../../../common';
|
||||
import { ColumnHeaderOptions, RowRenderer } from '../../../../../../common';
|
||||
import { Ecs } from '../../../../../../common/ecs';
|
||||
import { TimelineNonEcsData } from '../../../../../../common/search_strategy/timeline';
|
||||
|
||||
export interface ColumnRenderer {
|
||||
isInstance: (columnName: string, data: TimelineNonEcsData[]) => boolean;
|
||||
renderColumn: ({
|
||||
browserFields,
|
||||
className,
|
||||
columnName,
|
||||
eventId,
|
||||
field,
|
||||
globalFilters,
|
||||
isDetails,
|
||||
isDraggable,
|
||||
linkValues,
|
||||
rowRenderers,
|
||||
timelineId,
|
||||
truncate,
|
||||
values,
|
||||
linkValues,
|
||||
}: {
|
||||
asPlainText?: boolean;
|
||||
browserFields?: BrowserFields;
|
||||
className?: string;
|
||||
columnName: string;
|
||||
ecsData?: Ecs;
|
||||
eventId: string;
|
||||
field: ColumnHeaderOptions;
|
||||
globalFilters?: Filter[];
|
||||
isDetails?: boolean;
|
||||
isDraggable?: boolean;
|
||||
linkValues?: string[] | null | undefined;
|
||||
rowRenderers?: RowRenderer[];
|
||||
timelineId: string;
|
||||
truncate?: boolean;
|
||||
values: string[] | null | undefined;
|
||||
linkValues?: string[] | null | undefined;
|
||||
ecsData?: Ecs;
|
||||
rowRenderers?: RowRenderer[];
|
||||
browserFields?: BrowserFields;
|
||||
}) => React.ReactNode;
|
||||
}
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
|
||||
/* eslint-disable complexity */
|
||||
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiToolTip } from '@elastic/eui';
|
||||
import { EuiButtonEmpty, EuiButtonIcon, EuiFlexGroup, EuiFlexItem, EuiToolTip } from '@elastic/eui';
|
||||
import { isNumber, isEmpty } from 'lodash/fp';
|
||||
import React from 'react';
|
||||
|
||||
|
@ -19,7 +19,8 @@ import { getOrEmptyTagFromValue } from '../../../../../common/components/empty_v
|
|||
import { FormattedDate } from '../../../../../common/components/formatted_date';
|
||||
import { FormattedIp } from '../../../../components/formatted_ip';
|
||||
|
||||
import { Port, PORT_NAMES } from '../../../../../network/components/port';
|
||||
import { Port } from '../../../../../network/components/port';
|
||||
import { PORT_NAMES } from '../../../../../network/components/port/helpers';
|
||||
import { TruncatableText } from '../../../../../common/components/truncatable_text';
|
||||
import {
|
||||
DATE_FIELD_TYPE,
|
||||
|
@ -44,44 +45,60 @@ import { AgentStatuses } from './agent_statuses';
|
|||
const columnNamesNotDraggable = [MESSAGE_FIELD_NAME];
|
||||
|
||||
const FormattedFieldValueComponent: React.FC<{
|
||||
asPlainText?: boolean;
|
||||
/** `Component` is only used with `EuiDataGrid`; the grid keeps a reference to `Component` for show / hide functionality */
|
||||
Component?: typeof EuiButtonEmpty | typeof EuiButtonIcon;
|
||||
contextId: string;
|
||||
eventId: string;
|
||||
isObjectArray?: boolean;
|
||||
fieldFormat?: string;
|
||||
fieldName: string;
|
||||
fieldType?: string;
|
||||
isButton?: boolean;
|
||||
isDraggable?: boolean;
|
||||
onClick?: () => void;
|
||||
title?: string;
|
||||
truncate?: boolean;
|
||||
value: string | number | undefined | null;
|
||||
linkValue?: string | null | undefined;
|
||||
}> = ({
|
||||
asPlainText,
|
||||
Component,
|
||||
contextId,
|
||||
eventId,
|
||||
fieldFormat,
|
||||
fieldName,
|
||||
fieldType,
|
||||
isButton,
|
||||
isObjectArray = false,
|
||||
isDraggable = true,
|
||||
truncate,
|
||||
onClick,
|
||||
title,
|
||||
truncate = true,
|
||||
value,
|
||||
linkValue,
|
||||
}) => {
|
||||
if (isObjectArray) {
|
||||
return <>{value}</>;
|
||||
if (isObjectArray || asPlainText) {
|
||||
return <span data-test-subj={`formatted-field-${fieldName}`}>{value}</span>;
|
||||
} else if (fieldType === IP_FIELD_TYPE) {
|
||||
return (
|
||||
<FormattedIp
|
||||
Component={Component}
|
||||
eventId={eventId}
|
||||
contextId={contextId}
|
||||
fieldName={fieldName}
|
||||
isButton={isButton}
|
||||
isDraggable={isDraggable}
|
||||
value={!isNumber(value) ? value : String(value)}
|
||||
onClick={onClick}
|
||||
title={title}
|
||||
truncate={truncate}
|
||||
/>
|
||||
);
|
||||
} else if (fieldType === GEO_FIELD_TYPE) {
|
||||
return <>{value}</>;
|
||||
} else if (fieldType === DATE_FIELD_TYPE) {
|
||||
const classNames = truncate ? 'eui-textTruncate eui-alignMiddle' : undefined;
|
||||
return isDraggable ? (
|
||||
<DefaultDraggable
|
||||
field={fieldName}
|
||||
|
@ -90,18 +107,20 @@ const FormattedFieldValueComponent: React.FC<{
|
|||
tooltipContent={null}
|
||||
value={`${value}`}
|
||||
>
|
||||
<FormattedDate fieldName={fieldName} value={value} />
|
||||
<FormattedDate className={classNames} fieldName={fieldName} value={value} />
|
||||
</DefaultDraggable>
|
||||
) : (
|
||||
<FormattedDate fieldName={fieldName} value={value} />
|
||||
<FormattedDate className={classNames} fieldName={fieldName} value={value} />
|
||||
);
|
||||
} else if (PORT_NAMES.some((portName) => fieldName === portName)) {
|
||||
return (
|
||||
<Port
|
||||
Component={Component}
|
||||
contextId={contextId}
|
||||
eventId={eventId}
|
||||
fieldName={fieldName}
|
||||
isDraggable={isDraggable}
|
||||
title={title}
|
||||
value={`${value}`}
|
||||
/>
|
||||
);
|
||||
|
@ -118,10 +137,14 @@ const FormattedFieldValueComponent: React.FC<{
|
|||
} else if (fieldName === HOST_NAME_FIELD_NAME) {
|
||||
return (
|
||||
<HostName
|
||||
Component={Component}
|
||||
contextId={contextId}
|
||||
eventId={eventId}
|
||||
fieldName={fieldName}
|
||||
isDraggable={isDraggable}
|
||||
isButton={isButton}
|
||||
onClick={onClick}
|
||||
title={title}
|
||||
value={value}
|
||||
/>
|
||||
);
|
||||
|
@ -138,11 +161,13 @@ const FormattedFieldValueComponent: React.FC<{
|
|||
} else if (fieldName === SIGNAL_RULE_NAME_FIELD_NAME) {
|
||||
return (
|
||||
<RenderRuleName
|
||||
Component={Component}
|
||||
contextId={contextId}
|
||||
eventId={eventId}
|
||||
fieldName={fieldName}
|
||||
isDraggable={isDraggable}
|
||||
linkValue={linkValue}
|
||||
title={title}
|
||||
truncate={truncate}
|
||||
value={value}
|
||||
/>
|
||||
|
@ -185,7 +210,17 @@ const FormattedFieldValueComponent: React.FC<{
|
|||
INDICATOR_REFERENCE,
|
||||
].includes(fieldName)
|
||||
) {
|
||||
return renderUrl({ contextId, eventId, fieldName, linkValue, isDraggable, truncate, value });
|
||||
return renderUrl({
|
||||
contextId,
|
||||
Component,
|
||||
eventId,
|
||||
fieldName,
|
||||
linkValue,
|
||||
isDraggable,
|
||||
truncate,
|
||||
title,
|
||||
value,
|
||||
});
|
||||
} else if (columnNamesNotDraggable.includes(fieldName) || !isDraggable) {
|
||||
return truncate && !isEmpty(value) ? (
|
||||
<TruncatableText data-test-subj="truncatable-message">
|
||||
|
|
|
@ -5,9 +5,17 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { EuiLink, EuiFlexGroup, EuiFlexItem, EuiIcon, EuiToolTip } from '@elastic/eui';
|
||||
import {
|
||||
EuiButtonEmpty,
|
||||
EuiButtonIcon,
|
||||
EuiLink,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiIcon,
|
||||
EuiToolTip,
|
||||
} from '@elastic/eui';
|
||||
import { isString, isEmpty } from 'lodash/fp';
|
||||
import React, { useCallback } from 'react';
|
||||
import React, { SyntheticEvent, useCallback, useMemo } from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { DefaultDraggable } from '../../../../../common/components/draggables';
|
||||
|
@ -30,22 +38,26 @@ const EventModuleFlexItem = styled(EuiFlexItem)`
|
|||
`;
|
||||
|
||||
interface RenderRuleNameProps {
|
||||
Component?: typeof EuiButtonEmpty | typeof EuiButtonIcon;
|
||||
contextId: string;
|
||||
eventId: string;
|
||||
fieldName: string;
|
||||
isDraggable: boolean;
|
||||
linkValue: string | null | undefined;
|
||||
truncate?: boolean;
|
||||
title?: string;
|
||||
value: string | number | null | undefined;
|
||||
}
|
||||
|
||||
export const RenderRuleName: React.FC<RenderRuleNameProps> = ({
|
||||
Component,
|
||||
contextId,
|
||||
eventId,
|
||||
fieldName,
|
||||
isDraggable,
|
||||
linkValue,
|
||||
truncate,
|
||||
title,
|
||||
value,
|
||||
}) => {
|
||||
const ruleName = `${value}`;
|
||||
|
@ -69,15 +81,29 @@ export const RenderRuleName: React.FC<RenderRuleNameProps> = ({
|
|||
[navigateToApp, ruleId, search]
|
||||
);
|
||||
|
||||
const href = useMemo(
|
||||
() =>
|
||||
getUrlForApp(APP_ID, {
|
||||
deepLinkId: SecurityPageName.rules,
|
||||
path: getRuleDetailsUrl(ruleId ?? '', search),
|
||||
}),
|
||||
[getUrlForApp, ruleId, search]
|
||||
);
|
||||
const id = `event-details-value-default-draggable-${contextId}-${eventId}-${fieldName}-${value}-${ruleId}`;
|
||||
|
||||
if (isString(value) && ruleName.length > 0 && ruleId != null) {
|
||||
const link = (
|
||||
<LinkAnchor
|
||||
const link = Component ? (
|
||||
<Component
|
||||
aria-label={title}
|
||||
data-test-subj={`view-${fieldName}`}
|
||||
iconType="link"
|
||||
onClick={goToRuleDetails}
|
||||
href={getUrlForApp(APP_ID, {
|
||||
deepLinkId: SecurityPageName.rules,
|
||||
path: getRuleDetailsUrl(ruleId, search),
|
||||
})}
|
||||
title={title}
|
||||
>
|
||||
{title ?? value}
|
||||
</Component>
|
||||
) : (
|
||||
<LinkAnchor onClick={goToRuleDetails} href={href}>
|
||||
{content}
|
||||
</LinkAnchor>
|
||||
);
|
||||
|
@ -85,7 +111,7 @@ export const RenderRuleName: React.FC<RenderRuleNameProps> = ({
|
|||
return isDraggable ? (
|
||||
<DefaultDraggable
|
||||
field={fieldName}
|
||||
id={`event-details-value-default-draggable-${contextId}-${eventId}-${fieldName}-${value}-${ruleId}`}
|
||||
id={id}
|
||||
isDraggable={isDraggable}
|
||||
tooltipContent={value}
|
||||
value={value}
|
||||
|
@ -99,7 +125,7 @@ export const RenderRuleName: React.FC<RenderRuleNameProps> = ({
|
|||
return isDraggable ? (
|
||||
<DefaultDraggable
|
||||
field={fieldName}
|
||||
id={`event-details-value-default-draggable-${contextId}-${eventId}-${fieldName}-${value}-${ruleId}`}
|
||||
id={id}
|
||||
isDraggable={isDraggable}
|
||||
tooltipContent={value}
|
||||
value={`${value}`}
|
||||
|
@ -189,35 +215,71 @@ export const renderEventModule = ({
|
|||
);
|
||||
};
|
||||
|
||||
const GenericLinkComponent: React.FC<{
|
||||
children?: React.ReactNode;
|
||||
/** `Component` is only used with `EuiDataGrid`; the grid keeps a reference to `Component` for show / hide functionality */
|
||||
Component?: typeof EuiButtonEmpty | typeof EuiButtonIcon;
|
||||
dataTestSubj?: string;
|
||||
href: string;
|
||||
onClick?: (e: SyntheticEvent) => void;
|
||||
title?: string;
|
||||
iconType?: string;
|
||||
}> = ({ children, Component, dataTestSubj, href, onClick, title, iconType = 'link' }) => {
|
||||
return Component ? (
|
||||
<Component
|
||||
data-test-subj={dataTestSubj}
|
||||
href={href}
|
||||
iconType={iconType}
|
||||
onClick={onClick}
|
||||
title={title}
|
||||
>
|
||||
{title ?? children}
|
||||
</Component>
|
||||
) : (
|
||||
<EuiLink data-test-subj={dataTestSubj} target="_blank" href={href}>
|
||||
{title ?? children}
|
||||
</EuiLink>
|
||||
);
|
||||
};
|
||||
|
||||
const GenericLink = React.memo(GenericLinkComponent);
|
||||
|
||||
export const renderUrl = ({
|
||||
contextId,
|
||||
Component,
|
||||
eventId,
|
||||
fieldName,
|
||||
isDraggable,
|
||||
linkValue,
|
||||
truncate,
|
||||
title,
|
||||
value,
|
||||
}: {
|
||||
contextId: string;
|
||||
/** `Component` is only used with `EuiDataGrid`; the grid keeps a reference to `Component` for show / hide functionality */
|
||||
Component?: typeof EuiButtonEmpty | typeof EuiButtonIcon;
|
||||
eventId: string;
|
||||
fieldName: string;
|
||||
isDraggable: boolean;
|
||||
linkValue: string | null | undefined;
|
||||
truncate?: boolean;
|
||||
title?: string;
|
||||
value: string | number | null | undefined;
|
||||
}) => {
|
||||
const urlName = `${value}`;
|
||||
const isUrlValid = !isUrlInvalid(urlName);
|
||||
|
||||
const formattedValue = truncate ? <TruncatableText>{value}</TruncatableText> : value;
|
||||
const content = (
|
||||
<>
|
||||
{!isUrlInvalid(urlName) && (
|
||||
<EuiLink target="_blank" href={urlName}>
|
||||
{formattedValue}
|
||||
</EuiLink>
|
||||
)}
|
||||
{isUrlInvalid(urlName) && <>{formattedValue}</>}
|
||||
</>
|
||||
const content = isUrlValid ? (
|
||||
<GenericLink
|
||||
Component={Component}
|
||||
href={urlName}
|
||||
dataTestSubj="ata-grid-url"
|
||||
title={title}
|
||||
iconType="link"
|
||||
/>
|
||||
) : (
|
||||
<>{formattedValue}</>
|
||||
);
|
||||
|
||||
return isString(value) && urlName.length > 0 ? (
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
*/
|
||||
|
||||
import React, { useCallback, useContext, useMemo } from 'react';
|
||||
import { EuiButtonEmpty, EuiButtonIcon } from '@elastic/eui';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { isString } from 'lodash/fp';
|
||||
import { HostDetailsLink } from '../../../../../common/components/links';
|
||||
|
@ -23,17 +24,25 @@ import { StatefulEventContext } from '../../../../../../../timelines/public';
|
|||
|
||||
interface Props {
|
||||
contextId: string;
|
||||
Component?: typeof EuiButtonEmpty | typeof EuiButtonIcon;
|
||||
eventId: string;
|
||||
fieldName: string;
|
||||
isDraggable: boolean;
|
||||
isButton?: boolean;
|
||||
onClick?: () => void;
|
||||
value: string | number | undefined | null;
|
||||
title?: string;
|
||||
}
|
||||
|
||||
const HostNameComponent: React.FC<Props> = ({
|
||||
fieldName,
|
||||
Component,
|
||||
contextId,
|
||||
eventId,
|
||||
isDraggable,
|
||||
isButton,
|
||||
onClick,
|
||||
title,
|
||||
value,
|
||||
}) => {
|
||||
const dispatch = useDispatch();
|
||||
|
@ -44,6 +53,10 @@ const HostNameComponent: React.FC<Props> = ({
|
|||
const openHostDetailsSidePanel = useCallback(
|
||||
(e) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (onClick) {
|
||||
onClick();
|
||||
}
|
||||
if (eventContext && isInTimelineContext) {
|
||||
const { timelineID, tabType } = eventContext;
|
||||
const updatedExpandedDetail: TimelineExpandedDetailType = {
|
||||
|
@ -66,7 +79,7 @@ const HostNameComponent: React.FC<Props> = ({
|
|||
}
|
||||
}
|
||||
},
|
||||
[dispatch, eventContext, isInTimelineContext, hostName]
|
||||
[onClick, eventContext, isInTimelineContext, hostName, dispatch]
|
||||
);
|
||||
|
||||
// The below is explicitly defined this way as the onClick takes precedence when it and the href are both defined
|
||||
|
@ -74,14 +87,16 @@ const HostNameComponent: React.FC<Props> = ({
|
|||
const content = useMemo(
|
||||
() => (
|
||||
<HostDetailsLink
|
||||
Component={Component}
|
||||
hostName={hostName}
|
||||
isButton={false}
|
||||
isButton={isButton}
|
||||
onClick={isInTimelineContext ? openHostDetailsSidePanel : undefined}
|
||||
title={title}
|
||||
>
|
||||
<TruncatableText data-test-subj="draggable-truncatable-content">{hostName}</TruncatableText>
|
||||
</HostDetailsLink>
|
||||
),
|
||||
[hostName, isInTimelineContext, openHostDetailsSidePanel]
|
||||
[Component, hostName, isButton, isInTimelineContext, openHostDetailsSidePanel, title]
|
||||
);
|
||||
|
||||
return isString(value) && hostName.length > 0 ? (
|
||||
|
|
|
@ -29,7 +29,7 @@ import {
|
|||
import {
|
||||
DESTINATION_PORT_FIELD_NAME,
|
||||
SOURCE_PORT_FIELD_NAME,
|
||||
} from '../../../../../network/components/port';
|
||||
} from '../../../../../network/components/port/helpers';
|
||||
import {
|
||||
DESTINATION_GEO_CITY_NAME_FIELD_NAME,
|
||||
DESTINATION_GEO_CONTINENT_NAME_FIELD_NAME,
|
||||
|
|
|
@ -34,7 +34,7 @@ import {
|
|||
import {
|
||||
DESTINATION_PORT_FIELD_NAME,
|
||||
SOURCE_PORT_FIELD_NAME,
|
||||
} from '../../../../../../network/components/port';
|
||||
} from '../../../../../../network/components/port/helpers';
|
||||
import {
|
||||
NETWORK_BYTES_FIELD_NAME,
|
||||
NETWORK_COMMUNITY_ID_FIELD_NAME,
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
|
||||
import { head } from 'lodash/fp';
|
||||
import React from 'react';
|
||||
import { Filter } from '../../../../../../../../../src/plugins/data/public';
|
||||
|
||||
import { ColumnHeaderOptions } from '../../../../../../common';
|
||||
import { TimelineNonEcsData } from '../../../../../../common/search_strategy/timeline';
|
||||
|
@ -21,8 +22,8 @@ export const dataExistsAtColumn = (columnName: string, data: TimelineNonEcsData[
|
|||
export const plainColumnRenderer: ColumnRenderer = {
|
||||
isInstance: (columnName: string, data: TimelineNonEcsData[]) =>
|
||||
dataExistsAtColumn(columnName, data),
|
||||
|
||||
renderColumn: ({
|
||||
asPlainText,
|
||||
columnName,
|
||||
eventId,
|
||||
field,
|
||||
|
@ -32,9 +33,11 @@ export const plainColumnRenderer: ColumnRenderer = {
|
|||
values,
|
||||
linkValues,
|
||||
}: {
|
||||
asPlainText?: boolean;
|
||||
columnName: string;
|
||||
eventId: string;
|
||||
field: ColumnHeaderOptions;
|
||||
globalFilters?: Filter[];
|
||||
isDraggable?: boolean;
|
||||
timelineId: string;
|
||||
truncate?: boolean;
|
||||
|
@ -44,16 +47,17 @@ export const plainColumnRenderer: ColumnRenderer = {
|
|||
values != null
|
||||
? values.map((value, i) => (
|
||||
<FormattedFieldValue
|
||||
key={`plain-column-renderer-formatted-field-value-${timelineId}-${columnName}-${eventId}-${field.id}-${value}-${i}`}
|
||||
asPlainText={asPlainText}
|
||||
contextId={`plain-column-renderer-formatted-field-value-${timelineId}`}
|
||||
eventId={eventId}
|
||||
fieldFormat={field.format || ''}
|
||||
fieldName={columnName}
|
||||
fieldType={field.type || ''}
|
||||
isDraggable={isDraggable}
|
||||
value={parseValue(value)}
|
||||
truncate={truncate}
|
||||
key={`plain-column-renderer-formatted-field-value-${timelineId}-${columnName}-${eventId}-${field.id}-${value}-${i}`}
|
||||
linkValue={head(linkValues)}
|
||||
truncate={truncate}
|
||||
value={parseValue(value)}
|
||||
/>
|
||||
))
|
||||
: getEmptyTagValue(),
|
||||
|
|
|
@ -19,7 +19,7 @@ import {
|
|||
RowRenderer,
|
||||
RowRendererId,
|
||||
} from '../../../../../../common';
|
||||
import { fireEvent, render } from '@testing-library/react';
|
||||
import { render } from '@testing-library/react';
|
||||
import { TestProviders } from '../../../../../../../timelines/public/mock';
|
||||
import { useDraggableKeyboardWrapper as mockUseDraggableKeyboardWrapper } from '../../../../../../../timelines/public/components';
|
||||
import { cloneDeep } from 'lodash';
|
||||
|
@ -92,9 +92,10 @@ describe('reasonColumnRenderer', () => {
|
|||
expect(plainColumnRenderer.renderColumn).toBeCalledTimes(1);
|
||||
});
|
||||
|
||||
it("doesn't call `plainColumnRenderer.renderColumn` when ecsData, rowRenderers or browserFields fields are not empty", () => {
|
||||
it("doesn't call `plainColumnRenderer.renderColumn` in expanded value when ecsData, rowRenderers or browserFields fields are not empty", () => {
|
||||
reasonColumnRenderer.renderColumn({
|
||||
...defaultProps,
|
||||
isDetails: true,
|
||||
ecsData: invalidEcs,
|
||||
rowRenderers,
|
||||
browserFields,
|
||||
|
@ -103,9 +104,22 @@ describe('reasonColumnRenderer', () => {
|
|||
expect(plainColumnRenderer.renderColumn).toBeCalledTimes(0);
|
||||
});
|
||||
|
||||
it("doesn't render popover button when getRowRenderer doesn't find a rowRenderer", () => {
|
||||
it('call `plainColumnRenderer.renderColumn` in cell value', () => {
|
||||
reasonColumnRenderer.renderColumn({
|
||||
...defaultProps,
|
||||
isDetails: false,
|
||||
ecsData: invalidEcs,
|
||||
rowRenderers,
|
||||
browserFields,
|
||||
});
|
||||
|
||||
expect(plainColumnRenderer.renderColumn).toBeCalledTimes(1);
|
||||
});
|
||||
|
||||
it("doesn't render reason renderers button when getRowRenderer doesn't find a rowRenderer", () => {
|
||||
const renderedColumn = reasonColumnRenderer.renderColumn({
|
||||
...defaultProps,
|
||||
isDetails: true,
|
||||
ecsData: invalidEcs,
|
||||
rowRenderers,
|
||||
browserFields,
|
||||
|
@ -116,9 +130,10 @@ describe('reasonColumnRenderer', () => {
|
|||
expect(wrapper.queryByTestId('reason-cell-button')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('render popover button when getRowRenderer finds a rowRenderer', () => {
|
||||
it('render reason renderers when getRowRenderer finds a rowRenderer', () => {
|
||||
const renderedColumn = reasonColumnRenderer.renderColumn({
|
||||
...defaultProps,
|
||||
isDetails: true,
|
||||
ecsData: validEcs,
|
||||
rowRenderers,
|
||||
browserFields,
|
||||
|
@ -126,39 +141,7 @@ describe('reasonColumnRenderer', () => {
|
|||
|
||||
const wrapper = render(<TestProviders>{renderedColumn}</TestProviders>);
|
||||
|
||||
expect(wrapper.queryByTestId('reason-cell-button')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('render rowRender inside a popover when reson field button is clicked', () => {
|
||||
const renderedColumn = reasonColumnRenderer.renderColumn({
|
||||
...defaultProps,
|
||||
ecsData: validEcs,
|
||||
rowRenderers,
|
||||
browserFields,
|
||||
});
|
||||
|
||||
const wrapper = render(<TestProviders>{renderedColumn}</TestProviders>);
|
||||
|
||||
fireEvent.click(wrapper.getByTestId('reason-cell-button'));
|
||||
|
||||
expect(wrapper.queryByTestId('test-row-render')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('the popover always contains a class that hides it when an overlay (e.g. the inspect modal) is displayed', () => {
|
||||
const renderedColumn = reasonColumnRenderer.renderColumn({
|
||||
...defaultProps,
|
||||
ecsData: validEcs,
|
||||
rowRenderers,
|
||||
browserFields,
|
||||
});
|
||||
|
||||
const wrapper = render(<TestProviders>{renderedColumn}</TestProviders>);
|
||||
|
||||
fireEvent.click(wrapper.getByTestId('reason-cell-button'));
|
||||
|
||||
expect(wrapper.getByRole('dialog')).toHaveClass(
|
||||
'euiPanel euiPanel--paddingMedium euiPanel--borderRadiusMedium euiPanel--plain euiPanel--noShadow euiPopover__panel euiPopover__panel--right withHoverActions__popover'
|
||||
);
|
||||
expect(wrapper.queryByTestId('reason-cell-renderer')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -5,11 +5,10 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { EuiButtonEmpty, EuiPopover, EuiPopoverTitle } from '@elastic/eui';
|
||||
import { EuiSpacer, EuiPanel } from '@elastic/eui';
|
||||
import { isEqual } from 'lodash/fp';
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import React, { useMemo } from 'react';
|
||||
|
||||
import styled from 'styled-components';
|
||||
import { BrowserFields, ColumnHeaderOptions, RowRenderer } from '../../../../../../common';
|
||||
import { Ecs } from '../../../../../../common/ecs';
|
||||
import { eventRendererNames } from '../../../row_renderers_browser/catalog/constants';
|
||||
|
@ -23,58 +22,59 @@ export const reasonColumnRenderer: ColumnRenderer = {
|
|||
isInstance: isEqual(REASON_FIELD_NAME),
|
||||
|
||||
renderColumn: ({
|
||||
browserFields,
|
||||
columnName,
|
||||
ecsData,
|
||||
eventId,
|
||||
field,
|
||||
isDetails,
|
||||
isDraggable = true,
|
||||
linkValues,
|
||||
rowRenderers = [],
|
||||
timelineId,
|
||||
truncate,
|
||||
values,
|
||||
linkValues,
|
||||
ecsData,
|
||||
rowRenderers = [],
|
||||
browserFields,
|
||||
}: {
|
||||
browserFields?: BrowserFields;
|
||||
columnName: string;
|
||||
ecsData?: Ecs;
|
||||
eventId: string;
|
||||
field: ColumnHeaderOptions;
|
||||
isDetails?: boolean;
|
||||
isDraggable?: boolean;
|
||||
linkValues?: string[] | null | undefined;
|
||||
rowRenderers?: RowRenderer[];
|
||||
timelineId: string;
|
||||
truncate?: boolean;
|
||||
values: string[] | undefined | null;
|
||||
linkValues?: string[] | null | undefined;
|
||||
|
||||
ecsData?: Ecs;
|
||||
rowRenderers?: RowRenderer[];
|
||||
browserFields?: BrowserFields;
|
||||
}) =>
|
||||
values != null && ecsData && rowRenderers?.length > 0 && browserFields
|
||||
? values.map((value, i) => (
|
||||
<ReasonCell
|
||||
key={`reason-column-renderer-value-${timelineId}-${columnName}-${eventId}-${field.id}-${value}-${i}`}
|
||||
timelineId={timelineId}
|
||||
value={value}
|
||||
ecsData={ecsData}
|
||||
rowRenderers={rowRenderers}
|
||||
browserFields={browserFields}
|
||||
/>
|
||||
))
|
||||
: plainColumnRenderer.renderColumn({
|
||||
columnName,
|
||||
eventId,
|
||||
field,
|
||||
isDraggable,
|
||||
timelineId,
|
||||
truncate,
|
||||
values,
|
||||
linkValues,
|
||||
}),
|
||||
}) => {
|
||||
if (isDetails && values && ecsData && rowRenderers && browserFields) {
|
||||
return values.map((value, i) => (
|
||||
<ReasonCell
|
||||
browserFields={browserFields}
|
||||
ecsData={ecsData}
|
||||
key={`reason-column-renderer-value-${timelineId}-${columnName}-${eventId}-${field.id}-${value}-${i}`}
|
||||
rowRenderers={rowRenderers}
|
||||
timelineId={timelineId}
|
||||
value={value}
|
||||
/>
|
||||
));
|
||||
} else {
|
||||
return plainColumnRenderer.renderColumn({
|
||||
columnName,
|
||||
eventId,
|
||||
field,
|
||||
isDetails,
|
||||
isDraggable,
|
||||
linkValues,
|
||||
timelineId,
|
||||
truncate,
|
||||
values,
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
const StyledEuiButtonEmpty = styled(EuiButtonEmpty)`
|
||||
font-weight: ${(props) => props.theme.eui.euiFontWeightRegular};
|
||||
`;
|
||||
|
||||
const ReasonCell: React.FC<{
|
||||
value: string | number | undefined | null;
|
||||
timelineId: string;
|
||||
|
@ -82,8 +82,6 @@ const ReasonCell: React.FC<{
|
|||
rowRenderers: RowRenderer[];
|
||||
browserFields: BrowserFields;
|
||||
}> = ({ ecsData, rowRenderers, browserFields, timelineId, value }) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
const rowRenderer = useMemo(() => getRowRenderer(ecsData, rowRenderers), [ecsData, rowRenderers]);
|
||||
|
||||
const rowRender = useMemo(() => {
|
||||
|
@ -98,38 +96,17 @@ const ReasonCell: React.FC<{
|
|||
);
|
||||
}, [rowRenderer, browserFields, ecsData, timelineId]);
|
||||
|
||||
const handleTogglePopOver = useCallback(() => setIsOpen(!isOpen), [setIsOpen, isOpen]);
|
||||
const handleClosePopOver = useCallback(() => setIsOpen(false), [setIsOpen]);
|
||||
|
||||
const button = useMemo(
|
||||
() => (
|
||||
<StyledEuiButtonEmpty
|
||||
data-test-subj="reason-cell-button"
|
||||
size="xs"
|
||||
flush="left"
|
||||
onClick={handleTogglePopOver}
|
||||
>
|
||||
{value}
|
||||
</StyledEuiButtonEmpty>
|
||||
),
|
||||
[value, handleTogglePopOver]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{rowRenderer && rowRender ? (
|
||||
<EuiPopover
|
||||
isOpen={isOpen}
|
||||
anchorPosition="rightCenter"
|
||||
closePopover={handleClosePopOver}
|
||||
panelClassName="withHoverActions__popover"
|
||||
button={button}
|
||||
>
|
||||
<EuiPopoverTitle paddingSize="s">
|
||||
{i18n.EVENT_RENDERER_POPOVER_TITLE(eventRendererNames[rowRenderer.id] ?? '')}
|
||||
</EuiPopoverTitle>
|
||||
{rowRender}
|
||||
</EuiPopover>
|
||||
<>
|
||||
{value}
|
||||
<h4>{i18n.REASON_RENDERER_TITLE(eventRendererNames[rowRenderer.id] ?? '')}</h4>
|
||||
<EuiSpacer size="xs" />
|
||||
<EuiPanel color="subdued" className="eui-xScroll" data-test-subj="reason-cell-renderer">
|
||||
<div className="eui-displayInlineBlock">{rowRender}</div>
|
||||
</EuiPanel>
|
||||
</>
|
||||
) : (
|
||||
value
|
||||
)}
|
||||
|
|
|
@ -52,8 +52,8 @@ export const EMPTY_STATUS = i18n.translate(
|
|||
}
|
||||
);
|
||||
|
||||
export const EVENT_RENDERER_POPOVER_TITLE = (eventRendererName: string) =>
|
||||
i18n.translate('xpack.securitySolution.event.reason.eventRenderPopoverTitle', {
|
||||
export const REASON_RENDERER_TITLE = (eventRendererName: string) =>
|
||||
i18n.translate('xpack.securitySolution.event.reason.reasonRendererTitle', {
|
||||
values: { eventRendererName },
|
||||
defaultMessage: 'Event renderer: {eventRendererName} ',
|
||||
});
|
||||
|
|
|
@ -16,6 +16,8 @@ import { DroppableWrapper } from '../../../../common/components/drag_and_drop/dr
|
|||
import { mockBrowserFields } from '../../../../common/containers/source/mock';
|
||||
import { defaultHeaders, mockTimelineData, TestProviders } from '../../../../common/mock';
|
||||
import { DefaultCellRenderer } from './default_cell_renderer';
|
||||
import { BrowserFields } from '../../../../../../timelines/common/search_strategy';
|
||||
import { Ecs } from '../../../../../common/ecs';
|
||||
|
||||
jest.mock('../../../../common/lib/kibana');
|
||||
|
||||
|
@ -26,15 +28,16 @@ const mockImplementation = {
|
|||
};
|
||||
|
||||
describe('DefaultCellRenderer', () => {
|
||||
const columnId = 'signal.rule.risk_score';
|
||||
const columnId = '@timestamp';
|
||||
const eventId = '_id-123';
|
||||
const isDetails = true;
|
||||
const isExpandable = true;
|
||||
const isExpanded = true;
|
||||
const linkValues = ['foo', 'bar', '@baz'];
|
||||
const rowIndex = 3;
|
||||
const setCellProps = jest.fn();
|
||||
const timelineId = 'test';
|
||||
const ecsData = {} as Ecs;
|
||||
const browserFields = {} as BrowserFields;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
|
@ -44,14 +47,17 @@ describe('DefaultCellRenderer', () => {
|
|||
test('it invokes `getColumnRenderer` with the expected arguments', () => {
|
||||
const data = cloneDeep(mockTimelineData[0].data);
|
||||
const header = cloneDeep(defaultHeaders[0]);
|
||||
const isDetails = true;
|
||||
|
||||
mount(
|
||||
<TestProviders>
|
||||
<DragDropContextWrapper browserFields={mockBrowserFields}>
|
||||
<DroppableWrapper droppableId="testing">
|
||||
<DefaultCellRenderer
|
||||
browserFields={browserFields}
|
||||
columnId={columnId}
|
||||
data={data}
|
||||
ecsData={ecsData}
|
||||
eventId={eventId}
|
||||
header={header}
|
||||
isDetails={isDetails}
|
||||
|
@ -71,17 +77,69 @@ describe('DefaultCellRenderer', () => {
|
|||
expect(getColumnRenderer).toBeCalledWith(header.id, columnRenderers, data);
|
||||
});
|
||||
|
||||
test('it invokes `renderColumn` with the expected arguments', () => {
|
||||
test('if in tgrid expanded value, it invokes `renderColumn` with the expected arguments', () => {
|
||||
const data = cloneDeep(mockTimelineData[0].data);
|
||||
const header = cloneDeep(defaultHeaders[0]);
|
||||
const isDetails = true;
|
||||
const truncate = isDetails ? false : true;
|
||||
|
||||
mount(
|
||||
<TestProviders>
|
||||
<DragDropContextWrapper browserFields={mockBrowserFields}>
|
||||
<DroppableWrapper droppableId="testing">
|
||||
<DefaultCellRenderer
|
||||
browserFields={browserFields}
|
||||
columnId={columnId}
|
||||
data={data}
|
||||
ecsData={ecsData}
|
||||
eventId={eventId}
|
||||
header={header}
|
||||
isDetails={isDetails}
|
||||
isDraggable={true}
|
||||
isExpandable={isExpandable}
|
||||
isExpanded={isExpanded}
|
||||
linkValues={linkValues}
|
||||
rowIndex={rowIndex}
|
||||
setCellProps={setCellProps}
|
||||
timelineId={timelineId}
|
||||
truncate={truncate}
|
||||
/>
|
||||
</DroppableWrapper>
|
||||
</DragDropContextWrapper>
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
expect(mockImplementation.renderColumn).toBeCalledWith({
|
||||
asPlainText: false,
|
||||
browserFields,
|
||||
columnName: header.id,
|
||||
ecsData,
|
||||
eventId,
|
||||
field: header,
|
||||
isDetails,
|
||||
isDraggable: true,
|
||||
linkValues,
|
||||
rowRenderers: undefined,
|
||||
timelineId,
|
||||
truncate,
|
||||
values: ['2018-11-05T19:03:25.937Z'],
|
||||
});
|
||||
});
|
||||
|
||||
test('if in tgrid expanded value, it renders ExpandedCellValueActions', () => {
|
||||
const data = cloneDeep(mockTimelineData[0].data);
|
||||
const header = cloneDeep(defaultHeaders[1]);
|
||||
const isDetails = true;
|
||||
const id = 'event.severity';
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<DragDropContextWrapper browserFields={mockBrowserFields}>
|
||||
<DroppableWrapper droppableId="testing">
|
||||
<DefaultCellRenderer
|
||||
browserFields={browserFields}
|
||||
columnId={id}
|
||||
ecsData={ecsData}
|
||||
data={data}
|
||||
eventId={eventId}
|
||||
header={header}
|
||||
isDetails={isDetails}
|
||||
|
@ -98,15 +156,8 @@ describe('DefaultCellRenderer', () => {
|
|||
</TestProviders>
|
||||
);
|
||||
|
||||
expect(mockImplementation.renderColumn).toBeCalledWith({
|
||||
columnName: header.id,
|
||||
eventId,
|
||||
field: header,
|
||||
isDraggable: true,
|
||||
linkValues,
|
||||
timelineId,
|
||||
truncate: true,
|
||||
values: ['2018-11-05T19:03:25.937Z'],
|
||||
});
|
||||
expect(
|
||||
wrapper.find('[data-test-subj="data-grid-expanded-cell-value-actions"]').exists()
|
||||
).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -12,36 +12,70 @@ import { columnRenderers } from '../body/renderers';
|
|||
import { getColumnRenderer } from '../body/renderers/get_column_renderer';
|
||||
|
||||
import { CellValueElementProps } from '.';
|
||||
import { getLink } from '../../../../common/lib/cell_actions/helpers';
|
||||
import {
|
||||
ExpandedCellValueActions,
|
||||
StyledContent,
|
||||
} from '../../../../common/lib/cell_actions/expanded_cell_value_actions';
|
||||
|
||||
const FIELDS_WITHOUT_CELL_ACTIONS = ['@timestamp', 'signal.rule.risk_score', 'signal.reason'];
|
||||
const hasCellActions = (columnId?: string) => {
|
||||
return columnId && FIELDS_WITHOUT_CELL_ACTIONS.indexOf(columnId) < 0;
|
||||
};
|
||||
|
||||
export const DefaultCellRenderer: React.FC<CellValueElementProps> = ({
|
||||
columnId,
|
||||
browserFields,
|
||||
data,
|
||||
ecsData,
|
||||
eventId,
|
||||
globalFilters,
|
||||
header,
|
||||
isDetails,
|
||||
isDraggable,
|
||||
linkValues,
|
||||
rowRenderers,
|
||||
setCellProps,
|
||||
timelineId,
|
||||
rowRenderers,
|
||||
browserFields,
|
||||
ecsData,
|
||||
}) => (
|
||||
<>
|
||||
{getColumnRenderer(header.id, columnRenderers, data).renderColumn({
|
||||
columnName: header.id,
|
||||
eventId,
|
||||
field: header,
|
||||
isDraggable,
|
||||
linkValues,
|
||||
timelineId,
|
||||
truncate: true,
|
||||
values: getMappedNonEcsValue({
|
||||
data,
|
||||
fieldName: header.id,
|
||||
}),
|
||||
rowRenderers,
|
||||
browserFields,
|
||||
ecsData,
|
||||
})}
|
||||
</>
|
||||
);
|
||||
truncate,
|
||||
}) => {
|
||||
const values = getMappedNonEcsValue({
|
||||
data,
|
||||
fieldName: header.id,
|
||||
});
|
||||
const styledContentClassName = isDetails
|
||||
? 'eui-textBreakWord'
|
||||
: 'eui-displayInlineBlock eui-textTruncate';
|
||||
return (
|
||||
<>
|
||||
<StyledContent className={styledContentClassName} $isDetails={isDetails}>
|
||||
{getColumnRenderer(header.id, columnRenderers, data).renderColumn({
|
||||
asPlainText: !!getLink(header.id, header.type), // we want to render value with links as plain text but keep other formatters like badge.
|
||||
browserFields,
|
||||
columnName: header.id,
|
||||
ecsData,
|
||||
eventId,
|
||||
field: header,
|
||||
isDetails,
|
||||
isDraggable,
|
||||
linkValues,
|
||||
rowRenderers,
|
||||
timelineId,
|
||||
truncate,
|
||||
values: getMappedNonEcsValue({
|
||||
data,
|
||||
fieldName: header.id,
|
||||
}),
|
||||
})}
|
||||
</StyledContent>
|
||||
{isDetails && browserFields && hasCellActions(header.id) && (
|
||||
<ExpandedCellValueActions
|
||||
browserFields={browserFields}
|
||||
field={header.id}
|
||||
globalFilters={globalFilters}
|
||||
timelineId={timelineId}
|
||||
value={values}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -7,21 +7,26 @@
|
|||
|
||||
import { EuiDataGridCellValueElementProps } from '@elastic/eui';
|
||||
import { RowRenderer } from '../../..';
|
||||
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
|
||||
import { Filter } from '../../../../../../../src/plugins/data/public';
|
||||
import { Ecs } from '../../../ecs';
|
||||
import { BrowserFields, TimelineNonEcsData } from '../../../search_strategy';
|
||||
import { ColumnHeaderOptions } from '../columns';
|
||||
|
||||
/** The following props are provided to the function called by `renderCellValue` */
|
||||
export type CellValueElementProps = EuiDataGridCellValueElementProps & {
|
||||
asPlainText?: boolean;
|
||||
browserFields?: BrowserFields;
|
||||
data: TimelineNonEcsData[];
|
||||
ecsData?: Ecs;
|
||||
eventId: string; // _id
|
||||
globalFilters?: Filter[];
|
||||
header: ColumnHeaderOptions;
|
||||
isDraggable: boolean;
|
||||
linkValues: string[] | undefined;
|
||||
timelineId: string;
|
||||
rowRenderers?: RowRenderer[];
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
setFlyoutAlert?: (data: any) => void;
|
||||
ecsData?: Ecs;
|
||||
rowRenderers?: RowRenderer[];
|
||||
browserFields?: BrowserFields;
|
||||
timelineId: string;
|
||||
truncate?: boolean;
|
||||
};
|
||||
|
|
|
@ -8,9 +8,10 @@
|
|||
import { ReactNode } from 'react';
|
||||
|
||||
import { EuiDataGridColumn, EuiDataGridColumnCellActionProps } from '@elastic/eui';
|
||||
import { Filter, IFieldSubType } from '../../../../../../../src/plugins/data/common';
|
||||
import { IFieldSubType } from '../../../../../../../src/plugins/data/common';
|
||||
import { BrowserFields } from '../../../search_strategy/index_fields';
|
||||
import { TimelineNonEcsData } from '../../../search_strategy/timeline';
|
||||
import { Ecs } from '../../../ecs';
|
||||
|
||||
export type ColumnHeaderType = 'not-filtered' | 'text-filter';
|
||||
|
||||
|
@ -45,14 +46,16 @@ export type ColumnId = string;
|
|||
export type TGridCellAction = ({
|
||||
browserFields,
|
||||
data,
|
||||
globalFilters,
|
||||
ecsData,
|
||||
header,
|
||||
pageSize,
|
||||
timelineId,
|
||||
}: {
|
||||
browserFields: BrowserFields;
|
||||
/** each row of data is represented as one TimelineNonEcsData[] */
|
||||
data: TimelineNonEcsData[][];
|
||||
globalFilters?: Filter[];
|
||||
ecsData: Ecs[];
|
||||
header?: ColumnHeaderOptions;
|
||||
pageSize: number;
|
||||
timelineId: string;
|
||||
}) => (props: EuiDataGridColumnCellActionProps) => ReactNode;
|
||||
|
|
|
@ -14,8 +14,8 @@ import { TooltipWithKeyboardShortcut } from '../../tooltip_with_keyboard_shortcu
|
|||
import { createFilter, getAdditionalScreenReaderOnlyContext } from '../utils';
|
||||
import { HoverActionComponentProps, FilterValueFnArgs } from './types';
|
||||
|
||||
export const FILTER_FOR_VALUE = i18n.translate('xpack.timelines.hoverActions.filterForValue', {
|
||||
defaultMessage: 'Filter for value',
|
||||
export const FILTER_FOR_VALUE = i18n.translate('xpack.timelines.hoverActions.filterIn', {
|
||||
defaultMessage: 'Filter In',
|
||||
});
|
||||
export const FILTER_FOR_VALUE_KEYBOARD_SHORTCUT = 'f';
|
||||
|
||||
|
@ -31,6 +31,7 @@ const FilterForValueButton: React.FC<FilterForValueProps> = React.memo(
|
|||
onFilterAdded,
|
||||
ownFocus,
|
||||
onClick,
|
||||
size,
|
||||
showTooltip = false,
|
||||
value,
|
||||
}) => {
|
||||
|
@ -74,6 +75,7 @@ const FilterForValueButton: React.FC<FilterForValueProps> = React.memo(
|
|||
data-test-subj="filter-for-value"
|
||||
iconType="plusInCircle"
|
||||
onClick={filterForValueFn}
|
||||
size={size}
|
||||
title={FILTER_FOR_VALUE}
|
||||
>
|
||||
{FILTER_FOR_VALUE}
|
||||
|
@ -87,9 +89,10 @@ const FilterForValueButton: React.FC<FilterForValueProps> = React.memo(
|
|||
iconSize="s"
|
||||
iconType="plusInCircle"
|
||||
onClick={filterForValueFn}
|
||||
size={size}
|
||||
/>
|
||||
),
|
||||
[Component, defaultFocusedButtonRef, filterForValueFn]
|
||||
[Component, defaultFocusedButtonRef, filterForValueFn, size]
|
||||
);
|
||||
|
||||
return showTooltip ? (
|
||||
|
|
|
@ -14,8 +14,8 @@ import { TooltipWithKeyboardShortcut } from '../../tooltip_with_keyboard_shortcu
|
|||
import { createFilter, getAdditionalScreenReaderOnlyContext } from '../utils';
|
||||
import { HoverActionComponentProps, FilterValueFnArgs } from './types';
|
||||
|
||||
export const FILTER_OUT_VALUE = i18n.translate('xpack.timelines.hoverActions.filterOutValue', {
|
||||
defaultMessage: 'Filter out value',
|
||||
export const FILTER_OUT_VALUE = i18n.translate('xpack.timelines.hoverActions.filterOut', {
|
||||
defaultMessage: 'Filter Out',
|
||||
});
|
||||
|
||||
export const FILTER_OUT_VALUE_KEYBOARD_SHORTCUT = 'o';
|
||||
|
@ -30,6 +30,7 @@ const FilterOutValueButton: React.FC<HoverActionComponentProps & FilterValueFnAr
|
|||
onFilterAdded,
|
||||
ownFocus,
|
||||
onClick,
|
||||
size,
|
||||
showTooltip = false,
|
||||
value,
|
||||
}) => {
|
||||
|
@ -74,6 +75,7 @@ const FilterOutValueButton: React.FC<HoverActionComponentProps & FilterValueFnAr
|
|||
data-test-subj="filter-out-value"
|
||||
iconType="minusInCircle"
|
||||
onClick={filterOutValueFn}
|
||||
size={size}
|
||||
title={FILTER_OUT_VALUE}
|
||||
>
|
||||
{FILTER_OUT_VALUE}
|
||||
|
@ -87,9 +89,10 @@ const FilterOutValueButton: React.FC<HoverActionComponentProps & FilterValueFnAr
|
|||
iconSize="s"
|
||||
iconType="minusInCircle"
|
||||
onClick={filterOutValueFn}
|
||||
size={size}
|
||||
/>
|
||||
),
|
||||
[Component, defaultFocusedButtonRef, filterOutValueFn]
|
||||
[Component, defaultFocusedButtonRef, filterOutValueFn, size]
|
||||
);
|
||||
|
||||
return showTooltip ? (
|
||||
|
|
|
@ -18,11 +18,13 @@ export interface FilterValueFnArgs {
|
|||
}
|
||||
|
||||
export interface HoverActionComponentProps {
|
||||
className?: string;
|
||||
defaultFocusedButtonRef?: EuiButtonIconPropsForButton['buttonRef'];
|
||||
field: string;
|
||||
keyboardEvent?: React.KeyboardEvent;
|
||||
ownFocus: boolean;
|
||||
onClick?: () => void;
|
||||
size?: 'xs' | 's' | 'm';
|
||||
showTooltip?: boolean;
|
||||
value?: string[] | string | null;
|
||||
}
|
||||
|
|
|
@ -15,6 +15,8 @@ import { defaultHeaders, mockBrowserFields, mockTimelineData, TestProviders } fr
|
|||
import { TimelineTabs } from '../../../../common/types/timeline';
|
||||
import { TestCellRenderer } from '../../../mock/cell_renderer';
|
||||
import { mockGlobalState } from '../../../mock/global_state';
|
||||
import { EuiDataGridColumn } from '@elastic/eui';
|
||||
import { defaultColumnHeaderType } from '../../../store/t_grid/defaults';
|
||||
|
||||
const mockSort: Sort[] = [
|
||||
{
|
||||
|
@ -151,5 +153,124 @@ describe('Body', () => {
|
|||
.text()
|
||||
).toEqual(mockTimelineData[0].ecs.timestamp);
|
||||
});
|
||||
|
||||
test("timestamp column doesn't render cell actions", () => {
|
||||
const headersJustTimestamp = defaultHeaders.filter((h) => h.id === '@timestamp');
|
||||
const testProps = {
|
||||
...props,
|
||||
columnHeaders: headersJustTimestamp,
|
||||
data: mockTimelineData.slice(0, 1),
|
||||
};
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<BodyComponent {...testProps} />
|
||||
</TestProviders>
|
||||
);
|
||||
wrapper.update();
|
||||
|
||||
expect(
|
||||
wrapper
|
||||
.find('[data-test-subj="body-data-grid"]')
|
||||
.first()
|
||||
.prop<EuiDataGridColumn[]>('columns')
|
||||
.find((c) => c.id === '@timestamp')?.cellActions
|
||||
).toBeUndefined();
|
||||
});
|
||||
|
||||
test("signal.rule.risk_score column doesn't render cell actions", () => {
|
||||
const columnHeaders = [
|
||||
{
|
||||
category: 'signal',
|
||||
columnHeaderType: defaultColumnHeaderType,
|
||||
id: 'signal.rule.risk_score',
|
||||
type: 'number',
|
||||
aggregatable: true,
|
||||
initialWidth: 105,
|
||||
},
|
||||
];
|
||||
const testProps = {
|
||||
...props,
|
||||
columnHeaders,
|
||||
data: mockTimelineData.slice(0, 1),
|
||||
};
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<BodyComponent {...testProps} />
|
||||
</TestProviders>
|
||||
);
|
||||
wrapper.update();
|
||||
|
||||
expect(
|
||||
wrapper
|
||||
.find('[data-test-subj="body-data-grid"]')
|
||||
.first()
|
||||
.prop<EuiDataGridColumn[]>('columns')
|
||||
.find((c) => c.id === 'signal.rule.risk_score')?.cellActions
|
||||
).toBeUndefined();
|
||||
});
|
||||
|
||||
test("signal.reason column doesn't render cell actions", () => {
|
||||
const columnHeaders = [
|
||||
{
|
||||
category: 'signal',
|
||||
columnHeaderType: defaultColumnHeaderType,
|
||||
id: 'signal.reason',
|
||||
type: 'string',
|
||||
aggregatable: true,
|
||||
initialWidth: 450,
|
||||
},
|
||||
];
|
||||
const testProps = {
|
||||
...props,
|
||||
columnHeaders,
|
||||
data: mockTimelineData.slice(0, 1),
|
||||
};
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<BodyComponent {...testProps} />
|
||||
</TestProviders>
|
||||
);
|
||||
wrapper.update();
|
||||
|
||||
expect(
|
||||
wrapper
|
||||
.find('[data-test-subj="body-data-grid"]')
|
||||
.first()
|
||||
.prop<EuiDataGridColumn[]>('columns')
|
||||
.find((c) => c.id === 'signal.reason')?.cellActions
|
||||
).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
test("signal.rule.risk_score column doesn't render cell actions", () => {
|
||||
const columnHeaders = [
|
||||
{
|
||||
category: 'signal',
|
||||
columnHeaderType: defaultColumnHeaderType,
|
||||
id: 'signal.rule.risk_score',
|
||||
type: 'number',
|
||||
aggregatable: true,
|
||||
initialWidth: 105,
|
||||
},
|
||||
];
|
||||
const testProps = {
|
||||
...props,
|
||||
columnHeaders,
|
||||
data: mockTimelineData.slice(0, 1),
|
||||
};
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<BodyComponent {...testProps} />
|
||||
</TestProviders>
|
||||
);
|
||||
wrapper.update();
|
||||
|
||||
expect(
|
||||
wrapper
|
||||
.find('[data-test-subj="body-data-grid"]')
|
||||
.first()
|
||||
.prop<EuiDataGridColumn[]>('columns')
|
||||
.find((c) => c.id === 'signal.rule.risk_score')?.cellActions
|
||||
).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -145,6 +145,9 @@ const EuiDataGridContainer = styled.div<{ hideLastPage: boolean }>`
|
|||
}
|
||||
`;
|
||||
|
||||
const FIELDS_WITHOUT_CELL_ACTIONS = ['@timestamp', 'signal.rule.risk_score', 'signal.reason'];
|
||||
const hasCellActions = (columnId?: string) =>
|
||||
columnId && FIELDS_WITHOUT_CELL_ACTIONS.indexOf(columnId) < 0;
|
||||
const transformControlColumns = ({
|
||||
actionColumnsWidth,
|
||||
columnHeaders,
|
||||
|
@ -636,7 +639,6 @@ export const BodyComponent = React.memo<StatefulBodyProps>(
|
|||
setEventsDeleted,
|
||||
hasAlertsCrudPermissions,
|
||||
]);
|
||||
|
||||
const columnsWithCellActions: EuiDataGridColumn[] = useMemo(
|
||||
() =>
|
||||
columnHeaders.map((header) => {
|
||||
|
@ -644,18 +646,24 @@ export const BodyComponent = React.memo<StatefulBodyProps>(
|
|||
tGridCellAction({
|
||||
browserFields,
|
||||
data: data.map((row) => row.data),
|
||||
globalFilters: filters,
|
||||
ecsData: data.map((row) => row.ecs),
|
||||
header: columnHeaders.find((h) => h.id === header.id),
|
||||
pageSize,
|
||||
timelineId: id,
|
||||
});
|
||||
|
||||
return {
|
||||
...header,
|
||||
cellActions:
|
||||
header.tGridCellActions?.map(buildAction) ?? defaultCellActions?.map(buildAction),
|
||||
...(hasCellActions(header.id)
|
||||
? {
|
||||
cellActions:
|
||||
header.tGridCellActions?.map(buildAction) ??
|
||||
defaultCellActions?.map(buildAction),
|
||||
}
|
||||
: {}),
|
||||
};
|
||||
}),
|
||||
[browserFields, columnHeaders, data, defaultCellActions, id, pageSize, filters]
|
||||
[columnHeaders, defaultCellActions, browserFields, data, pageSize, id]
|
||||
);
|
||||
|
||||
const renderTGridCellValue = useMemo(() => {
|
||||
|
@ -663,9 +671,9 @@ export const BodyComponent = React.memo<StatefulBodyProps>(
|
|||
columnId,
|
||||
rowIndex,
|
||||
setCellProps,
|
||||
isDetails,
|
||||
}): React.ReactElement | null => {
|
||||
const pageRowIndex = getPageRowIndex(rowIndex, pageSize);
|
||||
|
||||
const rowData = pageRowIndex < data.length ? data[pageRowIndex].data : null;
|
||||
const header = columnHeaders.find((h) => h.id === columnId);
|
||||
const eventId = pageRowIndex < data.length ? data[pageRowIndex]._id : null;
|
||||
|
@ -687,34 +695,36 @@ export const BodyComponent = React.memo<StatefulBodyProps>(
|
|||
}
|
||||
|
||||
return renderCellValue({
|
||||
browserFields,
|
||||
columnId: header.id,
|
||||
eventId,
|
||||
data: rowData,
|
||||
ecsData: ecs,
|
||||
eventId,
|
||||
globalFilters: filters,
|
||||
header,
|
||||
isDetails,
|
||||
isDraggable: false,
|
||||
isExpandable: true,
|
||||
isExpanded: false,
|
||||
isDetails: false,
|
||||
linkValues: getOr([], header.linkField ?? '', ecs),
|
||||
rowIndex,
|
||||
setCellProps,
|
||||
timelineId: tabType != null ? `${id}-${tabType}` : id,
|
||||
ecsData: ecs,
|
||||
browserFields,
|
||||
rowRenderers,
|
||||
setCellProps,
|
||||
timelineId: id,
|
||||
truncate: isDetails ? false : true,
|
||||
}) as React.ReactElement;
|
||||
};
|
||||
return Cell;
|
||||
}, [
|
||||
browserFields,
|
||||
columnHeaders,
|
||||
data,
|
||||
filters,
|
||||
id,
|
||||
renderCellValue,
|
||||
tabType,
|
||||
theme,
|
||||
browserFields,
|
||||
rowRenderers,
|
||||
pageSize,
|
||||
renderCellValue,
|
||||
rowRenderers,
|
||||
theme,
|
||||
]);
|
||||
|
||||
const onChangeItemsPerPage = useCallback(
|
||||
|
|
|
@ -180,19 +180,21 @@ const EventRenderedViewComponent = ({
|
|||
rowRenderer.isInstance(ecsData)
|
||||
);
|
||||
return (
|
||||
<EuiFlexGroup gutterSize="none" direction="column">
|
||||
<EuiFlexGroup gutterSize="none" direction="column" className="eui-fullWidth">
|
||||
{reason && <EuiFlexItem>{reason}</EuiFlexItem>}
|
||||
{rowRenderersValid.length > 0 &&
|
||||
rowRenderersValid.map((rowRenderer) => (
|
||||
<>
|
||||
<EuiHorizontalRule size="half" margin="xs" />
|
||||
<EventRenderedFlexItem>
|
||||
{rowRenderer.renderRow({
|
||||
browserFields,
|
||||
data: ecsData,
|
||||
isDraggable: false,
|
||||
timelineId: 'NONE',
|
||||
})}
|
||||
<EventRenderedFlexItem className="eui-xScroll">
|
||||
<div className="eui-displayInlineBlock">
|
||||
{rowRenderer.renderRow({
|
||||
browserFields,
|
||||
data: ecsData,
|
||||
isDraggable: false,
|
||||
timelineId: 'NONE',
|
||||
})}
|
||||
</div>
|
||||
</EventRenderedFlexItem>
|
||||
</>
|
||||
))}
|
||||
|
|
|
@ -22164,7 +22164,6 @@
|
|||
"xpack.securitySolution.enpdoint.resolver.panelutils.invaliddate": "無効な日付",
|
||||
"xpack.securitySolution.enpdoint.resolver.panelutils.noTimestampRetrieved": "タイムスタンプが取得されていません",
|
||||
"xpack.securitySolution.event.module.linkToElasticEndpointSecurityDescription": "Endpoint Securityで開く",
|
||||
"xpack.securitySolution.event.reason.eventRenderPopoverTitle": "イベントレンダラー:{eventRendererName} ",
|
||||
"xpack.securitySolution.eventDetails.blank": " ",
|
||||
"xpack.securitySolution.eventDetails.copyToClipboard": "クリップボードにコピー",
|
||||
"xpack.securitySolution.eventDetails.copyToClipboardTooltip": "クリップボードにコピー",
|
||||
|
@ -24427,8 +24426,6 @@
|
|||
"xpack.timelines.hoverActions.addToTimeline.addedFieldMessage": "{fieldOrValue}をタイムラインに追加しました",
|
||||
"xpack.timelines.hoverActions.columnToggleLabel": "表の{field}列を切り替える",
|
||||
"xpack.timelines.hoverActions.fieldLabel": "フィールド",
|
||||
"xpack.timelines.hoverActions.filterForValue": "値でフィルター",
|
||||
"xpack.timelines.hoverActions.filterOutValue": "値を除外",
|
||||
"xpack.timelines.hoverActions.moreActions": "さらにアクションを表示",
|
||||
"xpack.timelines.hoverActions.nestedColumnToggleLabel": "{field}フィールドはオブジェクトであり、列として追加できるネストされたフィールドに分解されます",
|
||||
"xpack.timelines.hoverActions.tooltipWithKeyboardShortcut.pressTooltipLabel": "プレス",
|
||||
|
|
|
@ -22512,7 +22512,6 @@
|
|||
"xpack.securitySolution.enpdoint.resolver.panelutils.invaliddate": "日期无效",
|
||||
"xpack.securitySolution.enpdoint.resolver.panelutils.noTimestampRetrieved": "未检索时间戳",
|
||||
"xpack.securitySolution.event.module.linkToElasticEndpointSecurityDescription": "在 Endpoint Security 中打开",
|
||||
"xpack.securitySolution.event.reason.eventRenderPopoverTitle": "事件渲染器:{eventRendererName} ",
|
||||
"xpack.securitySolution.eventDetails.blank": " ",
|
||||
"xpack.securitySolution.eventDetails.copyToClipboard": "复制到剪贴板",
|
||||
"xpack.securitySolution.eventDetails.copyToClipboardTooltip": "复制到剪贴板",
|
||||
|
@ -24840,8 +24839,6 @@
|
|||
"xpack.timelines.hoverActions.addToTimeline.addedFieldMessage": "已将 {fieldOrValue} 添加到时间线",
|
||||
"xpack.timelines.hoverActions.columnToggleLabel": "在表中切换 {field} 列",
|
||||
"xpack.timelines.hoverActions.fieldLabel": "字段",
|
||||
"xpack.timelines.hoverActions.filterForValue": "筛留值",
|
||||
"xpack.timelines.hoverActions.filterOutValue": "筛除值",
|
||||
"xpack.timelines.hoverActions.moreActions": "更多操作",
|
||||
"xpack.timelines.hoverActions.nestedColumnToggleLabel": "{field} 字段是对象,并分解为可以添加为列的嵌套字段",
|
||||
"xpack.timelines.hoverActions.tooltipWithKeyboardShortcut.pressTooltipLabel": "按",
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue