diff --git a/x-pack/plugins/security_solution/public/common/components/links/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/links/index.test.tsx index 965167f2c945e..0c077aaea81a8 100644 --- a/x-pack/plugins/security_solution/public/common/components/links/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/links/index.test.tsx @@ -46,6 +46,7 @@ jest.mock('../../lib/kibana', () => { describe('Custom Links', () => { const hostName = 'Host Name'; const ipv4 = '192.0.2.255'; + const ipv4a = '192.0.2.266'; const ipv6 = '2001:db8:ffff:ffff:ffff:ffff:ffff:ffff'; const ipv6Encoded = encodeIpv6(ipv6); @@ -64,6 +65,16 @@ describe('Custom Links', () => { }); describe('NetworkDetailsLink', () => { + test('can handle array of ips', () => { + const wrapper = mount(); + expect(wrapper.find('EuiLink').first().prop('href')).toEqual( + `/ip/${encodeURIComponent(ipv4)}/source` + ); + expect(wrapper.text()).toEqual(`${ipv4}${ipv4a}`); + expect(wrapper.find('EuiLink').last().prop('href')).toEqual( + `/ip/${encodeURIComponent(ipv4a)}/source` + ); + }); test('should render valid link to IP Details with ipv4 as the display text', () => { const wrapper = mount(); expect(wrapper.find('EuiLink').prop('href')).toEqual( diff --git a/x-pack/plugins/security_solution/public/common/components/links/index.tsx b/x-pack/plugins/security_solution/public/common/components/links/index.tsx index c74791b8b3aa7..a488b019b123a 100644 --- a/x-pack/plugins/security_solution/public/common/components/links/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/links/index.tsx @@ -14,7 +14,7 @@ import { EuiToolTip, } from '@elastic/eui'; import React, { useMemo, useCallback, SyntheticEvent } from 'react'; -import { isNil } from 'lodash/fp'; +import { isArray, isNil } from 'lodash/fp'; import { IP_REPUTATION_LINKS_SETTING, APP_ID } from '../../../../common/constants'; import { @@ -172,7 +172,7 @@ 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; + ip: string | string[]; flowTarget?: FlowTarget | FlowTargetSourceDest; isButton?: boolean; onClick?: (e: SyntheticEvent) => void | undefined; @@ -181,39 +181,46 @@ const NetworkDetailsLinkComponent: React.FC<{ const { formatUrl, search } = useFormatUrl(SecurityPageName.network); const { navigateToApp } = useKibana().services.application; const goToNetworkDetails = useCallback( - (ev) => { + (ev, cIp: string) => { ev.preventDefault(); navigateToApp(APP_ID, { deepLinkId: SecurityPageName.network, - path: getNetworkDetailsUrl(encodeURIComponent(encodeIpv6(ip)), flowTarget, search), + path: getNetworkDetailsUrl(encodeURIComponent(encodeIpv6(cIp)), flowTarget, search), }); }, - [flowTarget, ip, navigateToApp, search] + [flowTarget, navigateToApp, search] ); - const href = useMemo( - () => formatUrl(getNetworkDetailsUrl(encodeURIComponent(encodeIpv6(ip)))), - [formatUrl, ip] + const getHref = useCallback( + (cIp: string) => formatUrl(getNetworkDetailsUrl(encodeURIComponent(encodeIpv6(cIp)))), + [formatUrl] ); - return isButton ? ( - - {children} - - ) : ( - - {children ? children : ip} - + const getLink = useCallback( + (cIp: string, i: number) => + isButton ? ( + goToNetworkDetails(e, cIp))} + href={getHref(cIp)} + title={title ?? cIp} + > + {children} + + ) : ( + goToNetworkDetails(e, cIp))} + href={getHref(cIp)} + data-test-subj="network-details" + > + {children ? children : cIp} + + ), + [Component, children, getHref, goToNetworkDetails, isButton, onClick, title] ); + return isArray(ip) ? <>{ip.map(getLink)} : getLink(ip, 0); }; export const NetworkDetailsLink = React.memo(NetworkDetailsLinkComponent);