diff --git a/x-pack/legacy/plugins/siem/cypress/integration/lib/hosts/selectors.ts b/x-pack/legacy/plugins/siem/cypress/integration/lib/hosts/selectors.ts index f1a762cfab79b..30949abfe315f 100644 --- a/x-pack/legacy/plugins/siem/cypress/integration/lib/hosts/selectors.ts +++ b/x-pack/legacy/plugins/siem/cypress/integration/lib/hosts/selectors.ts @@ -15,3 +15,7 @@ export const ALL_HOSTS_WIDGET_DRAGGABLE_HOSTS = `${ALL_HOSTS_WIDGET} ${ALL_HOSTS /** Clicking this button displays the `Events` tab */ export const EVENTS_TAB_BUTTON = '[data-test-subj="navigation-events"]'; + +export const NAVIGATION_HOSTS_ALL_HOSTS = '[data-test-subj="navigation-link-allHosts"]'; + +export const NAVIGATION_HOSTS_ANOMALIES = '[data-test-subj="navigation-link-anomalies"]'; diff --git a/x-pack/legacy/plugins/siem/cypress/integration/lib/url_state/index.ts b/x-pack/legacy/plugins/siem/cypress/integration/lib/url_state/index.ts index 77f8e4786b2ed..2d89eabda20ff 100644 --- a/x-pack/legacy/plugins/siem/cypress/integration/lib/url_state/index.ts +++ b/x-pack/legacy/plugins/siem/cypress/integration/lib/url_state/index.ts @@ -37,11 +37,13 @@ export const ABSOLUTE_DATE_RANGE = { '/app/siem#/network/?kqlQuery=(filterQuery:!n,queryLocation:network.page)&timerange=(global:(linkTo:!(),timerange:(from:1564689809186,kind:absolute,to:1564691609186)),timeline:(linkTo:!(),timerange:(from:1564776209186,kind:absolute,to:1564779809186)))', urlKqlNetworkNetwork: `/app/siem#/network/?_g=()&kqlQuery=(filterQuery:(expression:'source.ip:%20"10.142.0.9"',kind:kuery),queryLocation:network.page)&timerange=(global:(linkTo:!(timeline),timerange:(from:1564689809186,kind:absolute,to:1564691609186)),timeline:(linkTo:!(global),timerange:(from:1564689809186,kind:absolute,to:1564691609186)))`, urlKqlNetworkHosts: `/app/siem#/network/?_g=()&kqlQuery=(filterQuery:(expression:'source.ip:%20"10.142.0.9"',kind:kuery),queryLocation:hosts.page)&timerange=(global:(linkTo:!(timeline),timerange:(from:1564689809186,kind:absolute,to:1564691609186)),timeline:(linkTo:!(global),timerange:(from:1564689809186,kind:absolute,to:1564691609186)))`, - urlKqlHostsNetwork: `/app/siem#/hosts/?_g=()&kqlQuery=(filterQuery:(expression:'source.ip:%20"10.142.0.9"',kind:kuery),queryLocation:network.page)&timerange=(global:(linkTo:!(timeline),timerange:(from:1564689809186,kind:absolute,to:1564691609186)),timeline:(linkTo:!(global),timerange:(from:1564689809186,kind:absolute,to:1564691609186)))`, - urlKqlHostsHosts: `/app/siem#/hosts/?_g=()&kqlQuery=(filterQuery:(expression:'source.ip:%20"10.142.0.9"',kind:kuery),queryLocation:hosts.page)&timerange=(global:(linkTo:!(timeline),timerange:(from:1564689809186,kind:absolute,to:1564691609186)),timeline:(linkTo:!(global),timerange:(from:1564689809186,kind:absolute,to:1564691609186)))`, + urlKqlHostsNetwork: `/app/siem#/hosts/allHosts?_g=()&kqlQuery=(filterQuery:(expression:'source.ip:%20"10.142.0.9"',kind:kuery),queryLocation:network.page)&timerange=(global:(linkTo:!(timeline),timerange:(from:1564689809186,kind:absolute,to:1564691609186)),timeline:(linkTo:!(global),timerange:(from:1564689809186,kind:absolute,to:1564691609186)))`, + urlKqlHostsHosts: `/app/siem#/hosts/allHosts?_g=()&kqlQuery=(filterQuery:(expression:'source.ip:%20"10.142.0.9"',kind:kuery),queryLocation:hosts.page)&timerange=(global:(linkTo:!(timeline),timerange:(from:1564689809186,kind:absolute,to:1564691609186)),timeline:(linkTo:!(global),timerange:(from:1564689809186,kind:absolute,to:1564691609186)))`, + urlHost: + '/app/siem#/hosts/authentications?timerange=(global:(linkTo:!(timeline),timerange:(from:1564689809186,kind:absolute,to:1564691609186)),timeline:(linkTo:!(global),timerange:(from:1564689809186,kind:absolute,to:1564691609186)))', }; export const DATE_PICKER_START_DATE_POPOVER_BUTTON = - '[data-test-subj="globalDatePicker"] [data-test-subj="superDatePickerstartDatePopoverButton"]'; + 'div[data-test-subj="globalDatePicker"] button[data-test-subj="superDatePickerstartDatePopoverButton"]'; export const DATE_PICKER_END_DATE_POPOVER_BUTTON = '[data-test-subj="globalDatePicker"] [data-test-subj="superDatePickerendDatePopoverButton"]'; export const DATE_PICKER_START_DATE_POPOVER_BUTTON_TIMELINE = @@ -56,3 +58,6 @@ export const DATE_PICKER_APPLY_BUTTON_TIMELINE = export const DATE_PICKER_ABSOLUTE_INPUT = '[data-test-subj="superDatePickerAbsoluteDateInput"]'; export const KQL_INPUT = '[data-test-subj="kqlInput"]'; export const TIMELINE_TITLE = '[data-test-subj="timeline-title"]'; + +export const HOST_DETAIL_SIEM_KIBANA = '[data-test-subj="all-hosts"] a.euiLink'; +export const BREADCRUMBS = '[data-test-subj="breadcrumbs"] a'; diff --git a/x-pack/legacy/plugins/siem/cypress/integration/lib/urls/index.ts b/x-pack/legacy/plugins/siem/cypress/integration/lib/urls/index.ts index 25cff28836a7c..1d07d2693cfa8 100644 --- a/x-pack/legacy/plugins/siem/cypress/integration/lib/urls/index.ts +++ b/x-pack/legacy/plugins/siem/cypress/integration/lib/urls/index.ts @@ -5,7 +5,7 @@ */ /** The SIEM app's Hosts page */ -export const HOSTS_PAGE = '/app/siem#/hosts'; +export const HOSTS_PAGE = '/app/siem#/hosts/allHosts'; /** Kibana's login page */ export const LOGIN_PAGE = '/login'; diff --git a/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/url_state/url_state.spec.ts b/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/url_state/url_state.spec.ts index 5d5feca99457a..41beb9c762c83 100644 --- a/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/url_state/url_state.spec.ts +++ b/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/url_state/url_state.spec.ts @@ -17,6 +17,8 @@ import { DATE_PICKER_START_DATE_POPOVER_BUTTON_TIMELINE, KQL_INPUT, TIMELINE_TITLE, + HOST_DETAIL_SIEM_KIBANA, + BREADCRUMBS, } from '../../lib/url_state'; import { DEFAULT_TIMEOUT, loginAndWaitForPage } from '../../lib/util/helpers'; import { @@ -25,8 +27,10 @@ import { hostExistsQuery, toggleTimelineVisibility, } from '../../lib/timeline/helpers'; -import { NAVIGATION_NETWORK } from '../../lib/navigation/selectors'; +import { NAVIGATION_NETWORK, NAVIGATION_HOSTS } from '../../lib/navigation/selectors'; import { HOSTS_PAGE } from '../../lib/urls'; +import { waitForAllHostsWidget } from '../../lib/hosts/helpers'; +import { NAVIGATION_HOSTS_ALL_HOSTS, NAVIGATION_HOSTS_ANOMALIES } from '../../lib/hosts/selectors'; describe('url state', () => { afterEach(() => { @@ -190,6 +194,65 @@ describe('url state', () => { ); }); + it('sets the url state when kql is set and check if href reflect this change', () => { + loginAndWaitForPage(ABSOLUTE_DATE_RANGE.url); + cy.get(KQL_INPUT, { timeout: 5000 }).type('source.ip: "10.142.0.9" {enter}'); + cy.get(NAVIGATION_HOSTS) + .first() + .click({ force: true }); + cy.get(NAVIGATION_NETWORK).should( + 'have.attr', + 'href', + "#/link-to/network?kqlQuery=(filterQuery:(expression:'source.ip:%20%2210.142.0.9%22%20',kind:kuery),queryLocation:network.page)&timerange=(global:(linkTo:!(timeline),timerange:(from:1564689809186,kind:absolute,to:1564691609186)),timeline:(linkTo:!(global),timerange:(from:1564689809186,kind:absolute,to:1564691609186)))" + ); + }); + + it('sets KQL in host page and detail page and check if href match on breadcrumb, tabs and subTabs', () => { + loginAndWaitForPage(ABSOLUTE_DATE_RANGE.urlHost); + cy.get(KQL_INPUT, { timeout: 5000 }).type('host.name: "siem-kibana" {enter}'); + cy.get(NAVIGATION_HOSTS_ALL_HOSTS) + .first() + .click({ force: true }); + waitForAllHostsWidget(); + cy.get(HOST_DETAIL_SIEM_KIBANA, { timeout: 5000 }) + .first() + .invoke('text') + .should('eq', 'siem-kibana'); + cy.get(HOST_DETAIL_SIEM_KIBANA) + .first() + .click({ force: true }); + cy.get(KQL_INPUT, { timeout: 5000 }).type('agent.type: "auditbeat" {enter}'); + cy.get(NAVIGATION_HOSTS).should( + 'have.attr', + 'href', + "#/link-to/hosts?kqlQuery=(filterQuery:(expression:'host.name:%20%22siem-kibana%22%20',kind:kuery),queryLocation:hosts.page)&timerange=(global:(linkTo:!(timeline),timerange:(from:1564689809186,kind:absolute,to:1564691609186)),timeline:(linkTo:!(global),timerange:(from:1564689809186,kind:absolute,to:1564691609186)))" + ); + cy.get(NAVIGATION_NETWORK).should( + 'have.attr', + 'href', + '#/link-to/network?timerange=(global:(linkTo:!(timeline),timerange:(from:1564689809186,kind:absolute,to:1564691609186)),timeline:(linkTo:!(global),timerange:(from:1564689809186,kind:absolute,to:1564691609186)))' + ); + cy.get(NAVIGATION_HOSTS_ANOMALIES).should( + 'have.attr', + 'href', + "#/hosts/siem-kibana/anomalies?kqlQuery=(filterQuery:(expression:'agent.type:%20%22auditbeat%22%20',kind:kuery),queryLocation:hosts.details)&timerange=(global:(linkTo:!(timeline),timerange:(from:1564689809186,kind:absolute,to:1564691609186)),timeline:(linkTo:!(global),timerange:(from:1564689809186,kind:absolute,to:1564691609186)))" + ); + cy.get(BREADCRUMBS) + .eq(1) + .should( + 'have.attr', + 'href', + "#/link-to/hosts?kqlQuery=(filterQuery:(expression:'host.name:%20%22siem-kibana%22%20',kind:kuery),queryLocation:hosts.page)&timerange=(global:(linkTo:!(timeline),timerange:(from:1564689809186,kind:absolute,to:1564691609186)),timeline:(linkTo:!(global),timerange:(from:1564689809186,kind:absolute,to:1564691609186)))" + ); + cy.get(BREADCRUMBS) + .eq(2) + .should( + 'have.attr', + 'href', + "#/link-to/hosts/siem-kibana?kqlQuery=(filterQuery:(expression:'agent.type:%20%22auditbeat%22%20',kind:kuery),queryLocation:hosts.details)&timerange=(global:(linkTo:!(timeline),timerange:(from:1564689809186,kind:absolute,to:1564691609186)),timeline:(linkTo:!(global),timerange:(from:1564689809186,kind:absolute,to:1564691609186)))" + ); + }); + it('clears kql when navigating to a new page', () => { loginAndWaitForPage(ABSOLUTE_DATE_RANGE.urlKqlHostsHosts); cy.get(NAVIGATION_NETWORK).click({ force: true }); @@ -202,17 +265,10 @@ describe('url state', () => { executeKQL(hostExistsQuery); assertAtLeastOneEventMatchesSearch(); const bestTimelineName = 'The Best Timeline'; - cy.get(TIMELINE_TITLE).type(bestTimelineName); - cy.hash().then(hash => { - const matched = hash.match(/(?<=timelineId=\').+?(?=\')/g); - const newTimelineId = matched && matched.length > 0 ? matched[0] : 'null'; - expect(matched).to.have.lengthOf(1); - cy.log('hash', hash); - cy.log('matched', matched); - cy.log('newTimelineId', newTimelineId); - cy.visit( - `/app/siem#/timelines?timelineId='${newTimelineId}'&timerange=(global:(linkTo:!(),timerange:(from:1565274377369,kind:absolute,to:1565360777369)),timeline:(linkTo:!(),timerange:(from:1565274377369,kind:absolute,to:1565360777369)))` - ).then(() => cy.get(TIMELINE_TITLE).should('have.attr', 'value', bestTimelineName)); - }); + cy.get(TIMELINE_TITLE, { timeout: 5000 }).type(bestTimelineName); + cy.url().should('include', 'timelineId='); + cy.visit( + `/app/siem#/timelines?timerange=(global:(linkTo:!(),timerange:(from:1565274377369,kind:absolute,to:1565360777369)),timeline:(linkTo:!(),timerange:(from:1565274377369,kind:absolute,to:1565360777369)))` + ).then(() => cy.get(TIMELINE_TITLE).should('have.attr', 'value', bestTimelineName)); }); }); diff --git a/x-pack/legacy/plugins/siem/public/components/link_to/link_to.tsx b/x-pack/legacy/plugins/siem/public/components/link_to/link_to.tsx index 9268badd81234..d0857e6ff8b48 100644 --- a/x-pack/legacy/plugins/siem/public/components/link_to/link_to.tsx +++ b/x-pack/legacy/plugins/siem/public/components/link_to/link_to.tsx @@ -20,21 +20,27 @@ interface LinkToPageProps { export const LinkToPage = pure(({ match }) => ( - - + + + - - - - + + + )); diff --git a/x-pack/legacy/plugins/siem/public/components/link_to/redirect_to_hosts.tsx b/x-pack/legacy/plugins/siem/public/components/link_to/redirect_to_hosts.tsx index 9b1be534af9f5..ee4ff75595c66 100644 --- a/x-pack/legacy/plugins/siem/public/components/link_to/redirect_to_hosts.tsx +++ b/x-pack/legacy/plugins/siem/public/components/link_to/redirect_to_hosts.tsx @@ -11,7 +11,7 @@ import { RedirectWrapper } from './redirect_wrapper'; import { HostsTableType } from '../../store/hosts/model'; export type HostComponentProps = RouteComponentProps<{ - hostName: string; + detailName: string; tabName: HostsTableType; search: string; }>; @@ -31,13 +31,13 @@ export const RedirectToHostsPage = ({ export const RedirectToHostDetailsPage = ({ match: { - params: { hostName, tabName }, + params: { detailName, tabName }, }, location: { search }, }: HostComponentProps) => { const defaultSelectedTab = HostsTableType.authentications; const selectedTab = tabName ? tabName : defaultSelectedTab; - const to = `/hosts/${hostName}/${selectedTab}${search}`; + const to = `/hosts/${detailName}/${selectedTab}${search}`; return ; }; @@ -45,8 +45,8 @@ export const getHostsUrl = () => '#/link-to/hosts'; export const getTabsOnHostsUrl = (tabName: HostsTableType) => `#/link-to/hosts/${tabName}`; -export const getHostDetailsUrl = (hostName: string) => `#/link-to/hosts/${hostName}`; +export const getHostDetailsUrl = (detailName: string) => `#/link-to/hosts/${detailName}`; -export const getTabsOnHostDetailsUrl = (hostName: string, tabName: HostsTableType) => { - return `#/link-to/hosts/${hostName}/${tabName}`; +export const getTabsOnHostDetailsUrl = (detailName: string, tabName: HostsTableType) => { + return `#/link-to/hosts/${detailName}/${tabName}`; }; diff --git a/x-pack/legacy/plugins/siem/public/components/link_to/redirect_to_network.tsx b/x-pack/legacy/plugins/siem/public/components/link_to/redirect_to_network.tsx index dba3f825362ba..50b23486d42a2 100644 --- a/x-pack/legacy/plugins/siem/public/components/link_to/redirect_to_network.tsx +++ b/x-pack/legacy/plugins/siem/public/components/link_to/redirect_to_network.tsx @@ -10,17 +10,17 @@ import { RouteComponentProps } from 'react-router-dom'; import { RedirectWrapper } from './redirect_wrapper'; export type NetworkComponentProps = RouteComponentProps<{ - ip: string; + detailName: string; search: string; }>; export const RedirectToNetworkPage = ({ match: { - params: { ip }, + params: { detailName }, }, location: { search }, }: NetworkComponentProps) => ( - + ); export const getNetworkUrl = () => '#/link-to/network'; diff --git a/x-pack/legacy/plugins/siem/public/components/navigation/breadcrumbs/index.test.ts b/x-pack/legacy/plugins/siem/public/components/navigation/breadcrumbs/index.test.ts index 088c65b1da08b..39a4c8efc4001 100644 --- a/x-pack/legacy/plugins/siem/public/components/navigation/breadcrumbs/index.test.ts +++ b/x-pack/legacy/plugins/siem/public/components/navigation/breadcrumbs/index.test.ts @@ -6,13 +6,11 @@ import chrome from 'ui/chrome'; import '../../../mock/match_media'; import { encodeIpv6 } from '../../../lib/helpers'; -import { getBreadcrumbs as getHostDetailsBreadcrumbs } from '../../../pages/hosts/host_details'; -import { getBreadcrumbs as getIPDetailsBreadcrumbs } from '../../../pages/network/ip_details'; -import { TIMELINES_PAGE_NAME } from '../../link_to/redirect_to_timelines'; -import { getBreadcrumbsForRoute, rootBreadcrumbs, setBreadcrumbs } from '.'; +import { getBreadcrumbsForRoute, setBreadcrumbs } from '.'; import { HostsTableType } from '../../../store/hosts/model'; -import { SiemPageName } from '../../../pages/home/home_navigations'; +import { RouteSpyState } from '../../../utils/route/types'; +import { TabNavigationProps } from '../tab_navigation/types'; jest.mock('ui/chrome', () => ({ getBasePath: () => { @@ -26,96 +24,184 @@ jest.mock('ui/chrome', () => ({ }), })); +const getMockObject = ( + pageName: string, + pathName: string, + detailName: string | undefined +): RouteSpyState & TabNavigationProps => ({ + detailName, + hostDetails: { filterQuery: null, queryLocation: null }, + hosts: { filterQuery: null, queryLocation: null }, + navTabs: { + hosts: { + disabled: false, + href: '#/link-to/hosts', + id: 'hosts', + name: 'Hosts', + urlKey: 'host', + }, + network: { + disabled: false, + href: '#/link-to/network', + id: 'network', + name: 'Network', + urlKey: 'network', + }, + overview: { + disabled: false, + href: '#/link-to/overview', + id: 'overview', + name: 'Overview', + urlKey: 'overview', + }, + timelines: { + disabled: false, + href: '#/link-to/timelines', + id: 'timelines', + name: 'Timelines', + urlKey: 'timeline', + }, + }, + network: { filterQuery: null, queryLocation: null }, + pageName, + pathName, + search: '', + tabName: HostsTableType.authentications, + timelineId: '', + timerange: { + global: { + linkTo: ['timeline'], + timerange: { + from: 1558048243696, + fromStr: 'now-24h', + kind: 'relative', + to: 1558134643697, + toStr: 'now', + }, + }, + timeline: { + linkTo: ['global'], + timerange: { + from: 1558048243696, + fromStr: 'now-24h', + kind: 'relative', + to: 1558134643697, + toStr: 'now', + }, + }, + }, +}); + describe('Navigation Breadcrumbs', () => { const hostName = 'siem-kibana'; - const hostDetailsParams = { - pageName: SiemPageName.hosts, - hostName, - tabName: HostsTableType.authentications, - }; - const hostBreadcrumbs = [ - ...rootBreadcrumbs.overview, - ...getHostDetailsBreadcrumbs(hostDetailsParams), - ]; + const ipv4 = '192.0.2.255'; - const ipv4Breadcrumbs = [...rootBreadcrumbs.overview, ...getIPDetailsBreadcrumbs(ipv4)]; const ipv6 = '2001:db8:ffff:ffff:ffff:ffff:ffff:ffff'; const ipv6Encoded = encodeIpv6(ipv6); - const ipv6Breadcrumbs = [...rootBreadcrumbs.overview, ...getIPDetailsBreadcrumbs(ipv6Encoded)]; - describe('getBreadcrumbsForRoute', () => { - test('should return Host breadcrumbs when supplied link-to host pathname', () => { - const pathname = '/link-to/hosts'; - const breadcrumbs = getBreadcrumbsForRoute(pathname); - expect(breadcrumbs).toEqual(rootBreadcrumbs.hosts); - }); + describe('getBreadcrumbsForRoute', () => { test('should return Host breadcrumbs when supplied host pathname', () => { - const pathname = '/hosts'; - const breadcrumbs = getBreadcrumbsForRoute(pathname); - expect(breadcrumbs).toEqual(rootBreadcrumbs.hosts); - }); - - test('should return Host breadcrumbs when supplied host pathname with trailing slash', () => { - const pathname = '/hosts/'; - const breadcrumbs = getBreadcrumbsForRoute(pathname); - expect(breadcrumbs).toEqual(rootBreadcrumbs.hosts); + const breadcrumbs = getBreadcrumbsForRoute(getMockObject('hosts', '/hosts', undefined)); + expect(breadcrumbs).toEqual([ + { + href: '#/link-to/overview', + text: 'SIEM', + }, + { + href: + '#/link-to/hosts?timerange=(global:(linkTo:!(timeline),timerange:(from:1558048243696,fromStr:now-24h,kind:relative,to:1558134643697,toStr:now)),timeline:(linkTo:!(global),timerange:(from:1558048243696,fromStr:now-24h,kind:relative,to:1558134643697,toStr:now)))', + text: 'Hosts', + }, + { + href: '', + text: 'Authentications', + }, + ]); }); test('should return Network breadcrumbs when supplied network pathname', () => { - const pathname = '/network'; - const breadcrumbs = getBreadcrumbsForRoute(pathname); - expect(breadcrumbs).toEqual(rootBreadcrumbs.network); - }); - - test('should return Timelines breadcrumbs when supplied link-to timelines pathname', () => { - const pathname = `/link-to/${TIMELINES_PAGE_NAME}`; - const breadcrumbs = getBreadcrumbsForRoute(pathname); - expect(breadcrumbs).toEqual(rootBreadcrumbs.timelines); + const breadcrumbs = getBreadcrumbsForRoute(getMockObject('network', '/network', undefined)); + expect(breadcrumbs).toEqual([ + { text: 'SIEM', href: '#/link-to/overview' }, + { + text: 'Network', + href: + '#/link-to/network?timerange=(global:(linkTo:!(timeline),timerange:(from:1558048243696,fromStr:now-24h,kind:relative,to:1558134643697,toStr:now)),timeline:(linkTo:!(global),timerange:(from:1558048243696,fromStr:now-24h,kind:relative,to:1558134643697,toStr:now)))', + }, + ]); }); test('should return Timelines breadcrumbs when supplied timelines pathname', () => { - const pathname = '/timelines'; - const breadcrumbs = getBreadcrumbsForRoute(pathname); - expect(breadcrumbs).toEqual(rootBreadcrumbs.timelines); - }); - - test('should return Host Details breadcrumbs when supplied link-to pathname with hostName', () => { - const pathname = `/link-to/hosts/${hostName}`; - - const breadcrumbs = getBreadcrumbsForRoute(pathname, hostDetailsParams); - expect(breadcrumbs).toEqual(hostBreadcrumbs); + const breadcrumbs = getBreadcrumbsForRoute( + getMockObject('timelines', '/timelines', undefined) + ); + expect(breadcrumbs).toEqual([ + { text: 'SIEM', href: '#/link-to/overview' }, + { text: 'Timelines', href: '' }, + ]); }); test('should return Host Details breadcrumbs when supplied a pathname with hostName', () => { - const pathname = `/hosts/${hostName}`; - - const breadcrumbs = getBreadcrumbsForRoute(pathname, hostDetailsParams); - expect(breadcrumbs).toEqual(hostBreadcrumbs); - }); - - test('should return IP Details breadcrumbs when supplied link-to pathname with ipv4', () => { - const pathname = `link-to/network/ip/${ipv4}`; - const breadcrumbs = getBreadcrumbsForRoute(pathname); - expect(breadcrumbs).toEqual(ipv4Breadcrumbs); + const breadcrumbs = getBreadcrumbsForRoute(getMockObject('hosts', '/hosts', hostName)); + expect(breadcrumbs).toEqual([ + { text: 'SIEM', href: '#/link-to/overview' }, + { + text: 'Hosts', + href: + '#/link-to/hosts?timerange=(global:(linkTo:!(timeline),timerange:(from:1558048243696,fromStr:now-24h,kind:relative,to:1558134643697,toStr:now)),timeline:(linkTo:!(global),timerange:(from:1558048243696,fromStr:now-24h,kind:relative,to:1558134643697,toStr:now)))', + }, + { + text: 'siem-kibana', + href: + '#/link-to/hosts/siem-kibana?timerange=(global:(linkTo:!(timeline),timerange:(from:1558048243696,fromStr:now-24h,kind:relative,to:1558134643697,toStr:now)),timeline:(linkTo:!(global),timerange:(from:1558048243696,fromStr:now-24h,kind:relative,to:1558134643697,toStr:now)))', + }, + { text: 'Authentications', href: '' }, + ]); }); test('should return IP Details breadcrumbs when supplied pathname with ipv4', () => { - const pathname = `/network/ip/${ipv4}`; - const breadcrumbs = getBreadcrumbsForRoute(pathname); - expect(breadcrumbs).toEqual(ipv4Breadcrumbs); + const breadcrumbs = getBreadcrumbsForRoute(getMockObject('network', '/network', ipv4)); + expect(breadcrumbs).toEqual([ + { text: 'SIEM', href: '#/link-to/overview' }, + { + text: 'Network', + href: + '#/link-to/network?timerange=(global:(linkTo:!(timeline),timerange:(from:1558048243696,fromStr:now-24h,kind:relative,to:1558134643697,toStr:now)),timeline:(linkTo:!(global),timerange:(from:1558048243696,fromStr:now-24h,kind:relative,to:1558134643697,toStr:now)))', + }, + { text: '192.0.2.255', href: '' }, + ]); }); test('should return IP Details breadcrumbs when supplied pathname with ipv6', () => { - const pathname = `/network/ip/${ipv6Encoded}`; - const breadcrumbs = getBreadcrumbsForRoute(pathname); - expect(breadcrumbs).toEqual(ipv6Breadcrumbs); + const breadcrumbs = getBreadcrumbsForRoute(getMockObject('network', '/network', ipv6Encoded)); + expect(breadcrumbs).toEqual([ + { text: 'SIEM', href: '#/link-to/overview' }, + { + text: 'Network', + href: + '#/link-to/network?timerange=(global:(linkTo:!(timeline),timerange:(from:1558048243696,fromStr:now-24h,kind:relative,to:1558134643697,toStr:now)),timeline:(linkTo:!(global),timerange:(from:1558048243696,fromStr:now-24h,kind:relative,to:1558134643697,toStr:now)))', + }, + { text: '2001:db8:ffff:ffff:ffff:ffff:ffff:ffff', href: '' }, + ]); }); }); describe('setBreadcrumbs()', () => { test('should call chrome breadcrumb service with correct breadcrumbs', () => { - const pathname = `/hosts/${hostName}`; - setBreadcrumbs(pathname, hostDetailsParams); - expect(chrome.breadcrumbs.set).toBeCalledWith(hostBreadcrumbs); + setBreadcrumbs(getMockObject('hosts', '/hosts', hostName)); + expect(chrome.breadcrumbs.set).toBeCalledWith([ + { text: 'SIEM', href: '#/link-to/overview' }, + { + text: 'Hosts', + href: + '#/link-to/hosts?timerange=(global:(linkTo:!(timeline),timerange:(from:1558048243696,fromStr:now-24h,kind:relative,to:1558134643697,toStr:now)),timeline:(linkTo:!(global),timerange:(from:1558048243696,fromStr:now-24h,kind:relative,to:1558134643697,toStr:now)))', + }, + { + text: 'siem-kibana', + href: + '#/link-to/hosts/siem-kibana?timerange=(global:(linkTo:!(timeline),timerange:(from:1558048243696,fromStr:now-24h,kind:relative,to:1558134643697,toStr:now)),timeline:(linkTo:!(global),timerange:(from:1558048243696,fromStr:now-24h,kind:relative,to:1558134643697,toStr:now)))', + }, + { text: 'Authentications', href: '' }, + ]); }); }); }); diff --git a/x-pack/legacy/plugins/siem/public/components/navigation/breadcrumbs/index.ts b/x-pack/legacy/plugins/siem/public/components/navigation/breadcrumbs/index.ts index 7037d3cc2e869..3e6eb4e51685b 100644 --- a/x-pack/legacy/plugins/siem/public/components/navigation/breadcrumbs/index.ts +++ b/x-pack/legacy/plugins/siem/public/components/navigation/breadcrumbs/index.ts @@ -6,23 +6,20 @@ import chrome, { Breadcrumb } from 'ui/chrome'; +import { getOr } from 'lodash/fp'; import { APP_NAME } from '../../../../common/constants'; -import { getBreadcrumbs as getHostDetailsBreadcrumbs } from '../../../pages/hosts/host_details'; +import { getBreadcrumbs as getHostDetailsBreadcrumbs } from '../../../pages/hosts/details/utils'; import { getBreadcrumbs as getIPDetailsBreadcrumbs } from '../../../pages/network/ip_details'; -import { getNetworkUrl, getOverviewUrl, getTimelinesUrl } from '../../link_to'; -import * as i18n from '../translations'; -import { getHostsUrl } from '../../link_to/redirect_to_hosts'; -import { HostsTableType } from '../../../store/hosts/model'; import { SiemPageName } from '../../../pages/home/home_navigations'; +import { RouteSpyState } from '../../../utils/route/types'; +import { getOverviewUrl } from '../../link_to'; -export interface NavigationParams { - pageName?: SiemPageName; - hostName?: string; - tabName?: HostsTableType; -} +import { TabNavigationProps } from '../tab_navigation/types'; +import { getSearch } from '../helpers'; +import { SearchNavTab } from '../types'; -export const setBreadcrumbs = (pathname: string, params?: NavigationParams) => { - const breadcrumbs = getBreadcrumbsForRoute(pathname, params); +export const setBreadcrumbs = (object: RouteSpyState & TabNavigationProps) => { + const breadcrumbs = getBreadcrumbsForRoute(object); if (breadcrumbs) { chrome.breadcrumbs.set(breadcrumbs); } @@ -35,48 +32,49 @@ export const siemRootBreadcrumb: Breadcrumb[] = [ }, ]; -export const rootBreadcrumbs: { [name: string]: Breadcrumb[] } = { - overview: siemRootBreadcrumb, - hosts: [ - ...siemRootBreadcrumb, - { - text: i18n.HOSTS, - href: getHostsUrl(), - }, - ], - network: [ - ...siemRootBreadcrumb, - { - text: i18n.NETWORK, - href: getNetworkUrl(), - }, - ], - timelines: [ - ...siemRootBreadcrumb, - { - text: i18n.TIMELINES, - href: getTimelinesUrl(), - }, - ], -}; - export const getBreadcrumbsForRoute = ( - pathname: string, - params?: NavigationParams + object: RouteSpyState & TabNavigationProps ): Breadcrumb[] | null => { - const removeSlash = pathname.replace(/\/$/, ''); - const trailingPath = removeSlash.match(/([^\/]+$)/); - - if (trailingPath !== null) { - if (params != null && params.pageName === SiemPageName.hosts) { - return [...siemRootBreadcrumb, ...getHostDetailsBreadcrumbs(params)]; - } - if (Object.keys(rootBreadcrumbs).includes(trailingPath[0])) { - return rootBreadcrumbs[trailingPath[0]]; - } - if (pathname.match(/network\/ip\/.*?/)) { - return [...siemRootBreadcrumb, ...getIPDetailsBreadcrumbs(trailingPath[0])]; + if (object != null && object.navTabs && object.pageName === SiemPageName.hosts) { + const tempNav: SearchNavTab = { urlKey: 'host', isDetailPage: false }; + let urlStateKeys = [getOr(tempNav, object.pageName, object.navTabs)]; + if (object.tabName != null) { + urlStateKeys = [...urlStateKeys, getOr(tempNav, object.tabName, object.navTabs)]; } + return [ + ...siemRootBreadcrumb, + ...getHostDetailsBreadcrumbs( + object, + urlStateKeys.reduce((acc: string[], item: SearchNavTab) => { + acc = [...acc, getSearch(item, object)]; + return acc; + }, []) + ), + ]; + } + if (object != null && object.navTabs && object.pageName === SiemPageName.network) { + const tempNav: SearchNavTab = { urlKey: 'network', isDetailPage: false }; + const urlStateKeys = [getOr(tempNav, object.pageName, object.navTabs)]; + return [ + ...siemRootBreadcrumb, + ...getIPDetailsBreadcrumbs( + object.detailName, + urlStateKeys.reduce((acc: string[], item) => { + acc = [...acc, getSearch(item, object)]; + return acc; + }, []) + ), + ]; } + if (object != null && object.navTabs && object.pageName && object.navTabs[object.pageName]) { + return [ + ...siemRootBreadcrumb, + { + text: object.navTabs[object.pageName].name, + href: '', + }, + ]; + } + return null; }; diff --git a/x-pack/legacy/plugins/siem/public/components/navigation/helpers.ts b/x-pack/legacy/plugins/siem/public/components/navigation/helpers.ts new file mode 100644 index 0000000000000..466a200e662e3 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/components/navigation/helpers.ts @@ -0,0 +1,50 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Location } from 'history'; + +import { UrlInputsModel } from '../../store/inputs/model'; +import { CONSTANTS } from '../url_state/constants'; +import { KqlQuery, URL_STATE_KEYS, KeyUrlState } from '../url_state/types'; +import { + replaceQueryStringInLocation, + replaceStateKeyInQueryString, + getQueryStringFromLocation, +} from '../url_state/helpers'; + +import { TabNavigationProps } from './tab_navigation/types'; +import { SearchNavTab } from './types'; + +export const getSearch = (tab: SearchNavTab, urlState: TabNavigationProps): string => { + if (tab && tab.urlKey != null && URL_STATE_KEYS[tab.urlKey] != null) { + return URL_STATE_KEYS[tab.urlKey].reduce( + (myLocation: Location, urlKey: KeyUrlState) => { + let urlStateToReplace: UrlInputsModel | KqlQuery | string = urlState[CONSTANTS.timelineId]; + if (urlKey === CONSTANTS.kqlQuery && tab.urlKey === 'host') { + urlStateToReplace = tab.isDetailPage ? urlState.hostDetails : urlState.hosts; + } else if (urlKey === CONSTANTS.kqlQuery && tab.urlKey === 'network') { + urlStateToReplace = urlState.network; + } else if (urlKey === CONSTANTS.timerange) { + urlStateToReplace = urlState[CONSTANTS.timerange]; + } + myLocation = replaceQueryStringInLocation( + myLocation, + replaceStateKeyInQueryString(urlKey, urlStateToReplace)( + getQueryStringFromLocation(myLocation) + ) + ); + return myLocation; + }, + { + pathname: urlState.pathName, + hash: '', + search: '', + state: '', + } + ).search; + } + return ''; +}; diff --git a/x-pack/legacy/plugins/siem/public/components/navigation/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/navigation/index.test.tsx index e15646d80ffb5..25ebb8ad89ecd 100644 --- a/x-pack/legacy/plugins/siem/public/components/navigation/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/navigation/index.test.tsx @@ -6,51 +6,27 @@ import { shallow } from 'enzyme'; import * as React from 'react'; -import { RouteComponentProps } from 'react-router'; import { CONSTANTS } from '../url_state/constants'; import { SiemNavigationComponent } from './'; import { setBreadcrumbs } from './breadcrumbs'; import { navTabs } from '../../pages/home/home_navigations'; -import { TabNavigationProps } from './type'; +import { TabNavigationProps } from './tab_navigation/types'; +import { HostsTableType } from '../../store/hosts/model'; +import { RouteSpyState } from '../../utils/route/types'; jest.mock('./breadcrumbs', () => ({ setBreadcrumbs: jest.fn(), })); -type Action = 'PUSH' | 'POP' | 'REPLACE'; -type Props = RouteComponentProps & TabNavigationProps; -const pop: Action = 'POP'; describe('SIEM Navigation', () => { - const location = { - pathname: '/hosts', + const mockProps: TabNavigationProps & RouteSpyState = { + pageName: 'hosts', + pathName: '/hosts', + detailName: undefined, search: '', - state: '', - hash: '', - }; - - const mockProps: Props = { - location, - match: { - isExact: true, - params: {}, - path: '', - url: '', - }, + tabName: HostsTableType.authentications, navTabs, - history: { - length: 2, - location, - action: pop, - push: jest.fn(), - replace: jest.fn(), - go: jest.fn(), - goBack: jest.fn(), - goForward: jest.fn(), - block: jest.fn(), - createHref: jest.fn(), - listen: jest.fn(), - }, [CONSTANTS.timerange]: { global: { [CONSTANTS.timerange]: { @@ -87,13 +63,141 @@ describe('SIEM Navigation', () => { }, [CONSTANTS.timelineId]: '', }; - const wrapper = shallow(); + const wrapper = shallow(); test('it calls setBreadcrumbs with correct path on mount', () => { - expect(setBreadcrumbs).toHaveBeenNthCalledWith(1, '/hosts', {}); + expect(setBreadcrumbs).toHaveBeenNthCalledWith(1, { + detailName: undefined, + hostDetails: { filterQuery: null, queryLocation: null }, + hosts: { filterQuery: null, queryLocation: null }, + navTabs: { + hosts: { + disabled: false, + href: '#/link-to/hosts', + id: 'hosts', + name: 'Hosts', + urlKey: 'host', + }, + network: { + disabled: false, + href: '#/link-to/network', + id: 'network', + name: 'Network', + urlKey: 'network', + }, + overview: { + disabled: false, + href: '#/link-to/overview', + id: 'overview', + name: 'Overview', + urlKey: 'overview', + }, + timelines: { + disabled: false, + href: '#/link-to/timelines', + id: 'timelines', + name: 'Timelines', + urlKey: 'timeline', + }, + }, + network: { filterQuery: null, queryLocation: null }, + pageName: 'hosts', + pathName: '/hosts', + search: '', + tabName: 'authentications', + timelineId: '', + timerange: { + global: { + linkTo: ['timeline'], + timerange: { + from: 1558048243696, + fromStr: 'now-24h', + kind: 'relative', + to: 1558134643697, + toStr: 'now', + }, + }, + timeline: { + linkTo: ['global'], + timerange: { + from: 1558048243696, + fromStr: 'now-24h', + kind: 'relative', + to: 1558134643697, + toStr: 'now', + }, + }, + }, + }); }); test('it calls setBreadcrumbs with correct path on update', () => { - wrapper.setProps({ location: { pathname: '/network' } }); + wrapper.setProps({ + pageName: 'network', + pathName: '/network', + tabName: undefined, + }); wrapper.update(); - expect(setBreadcrumbs).toHaveBeenNthCalledWith(2, '/network', {}); + expect(setBreadcrumbs).toHaveBeenNthCalledWith(2, { + detailName: undefined, + hostDetails: { filterQuery: null, queryLocation: null }, + hosts: { filterQuery: null, queryLocation: null }, + navTabs: { + hosts: { + disabled: false, + href: '#/link-to/hosts', + id: 'hosts', + name: 'Hosts', + urlKey: 'host', + }, + network: { + disabled: false, + href: '#/link-to/network', + id: 'network', + name: 'Network', + urlKey: 'network', + }, + overview: { + disabled: false, + href: '#/link-to/overview', + id: 'overview', + name: 'Overview', + urlKey: 'overview', + }, + timelines: { + disabled: false, + href: '#/link-to/timelines', + id: 'timelines', + name: 'Timelines', + urlKey: 'timeline', + }, + }, + network: { filterQuery: null, queryLocation: null }, + pageName: 'network', + pathName: '/network', + search: '', + tabName: undefined, + timelineId: '', + timerange: { + global: { + linkTo: ['timeline'], + timerange: { + from: 1558048243696, + fromStr: 'now-24h', + kind: 'relative', + to: 1558134643697, + toStr: 'now', + }, + }, + timeline: { + linkTo: ['global'], + timerange: { + from: 1558048243696, + fromStr: 'now-24h', + kind: 'relative', + to: 1558134643697, + toStr: 'now', + }, + }, + }, + }); }); }); diff --git a/x-pack/legacy/plugins/siem/public/components/navigation/index.tsx b/x-pack/legacy/plugins/siem/public/components/navigation/index.tsx index cbfb53f13779f..d53895606f9ee 100644 --- a/x-pack/legacy/plugins/siem/public/components/navigation/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/navigation/index.tsx @@ -4,14 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ +import { isEqual } from 'lodash/fp'; import React from 'react'; import { compose } from 'redux'; import { connect } from 'react-redux'; -import { RouteComponentProps, withRouter } from 'react-router-dom'; -import { setBreadcrumbs } from './breadcrumbs'; -import { TabNavigation } from './tab_navigation'; -import { TabNavigationProps, SiemNavigationComponentProps } from './type'; +import { RouteSpyState } from '../../utils/route/types'; +import { useRouteSpy } from '../../utils/route/use_route_spy'; +import { CONSTANTS } from '../url_state/constants'; import { inputsSelectors, hostsSelectors, @@ -21,15 +21,23 @@ import { hostsModel, networkModel, } from '../../store'; -import { CONSTANTS } from '../url_state/constants'; -export class SiemNavigationComponent extends React.Component< - RouteComponentProps & TabNavigationProps -> { - public shouldComponentUpdate(nextProps: Readonly): boolean { +import { setBreadcrumbs } from './breadcrumbs'; +import { TabNavigation } from './tab_navigation'; +import { TabNavigationProps } from './tab_navigation/types'; +import { SiemNavigationComponentProps } from './types'; + +export class SiemNavigationComponent extends React.Component { + public shouldComponentUpdate(nextProps: Readonly): boolean { if ( - this.props.location.pathname === nextProps.location.pathname && - this.props.location.search === nextProps.location.search + this.props.pathName === nextProps.pathName && + this.props.search === nextProps.search && + isEqual(this.props.hosts, nextProps.hosts) && + isEqual(this.props.hostDetails, nextProps.hostDetails) && + isEqual(this.props.network, nextProps.network) && + isEqual(this.props.navTabs, nextProps.navTabs) && + isEqual(this.props.timerange, nextProps.timerange) && + isEqual(this.props.timelineId, nextProps.timelineId) ) { return false; } @@ -38,45 +46,104 @@ export class SiemNavigationComponent extends React.Component< public componentWillMount(): void { const { - location, - match: { params }, + detailName, + hosts, + hostDetails, + navTabs, + network, + pageName, + pathName, + search, + tabName, + timerange, + timelineId, } = this.props; - if (location.pathname) { - setBreadcrumbs(location.pathname, params); + if (pathName) { + setBreadcrumbs({ + detailName, + hosts, + hostDetails, + navTabs, + network, + pageName, + pathName, + search, + tabName, + timerange, + timelineId, + }); } } - public componentWillReceiveProps(nextProps: Readonly): void { - if (this.props.location.pathname !== nextProps.location.pathname) { - setBreadcrumbs(nextProps.location.pathname, nextProps.match.params); + public componentWillReceiveProps(nextProps: Readonly): void { + if ( + this.props.pathName !== nextProps.pathName || + this.props.search !== nextProps.search || + !isEqual(this.props.hosts, nextProps.hosts) || + !isEqual(this.props.hostDetails, nextProps.hostDetails) || + !isEqual(this.props.network, nextProps.network) || + !isEqual(this.props.navTabs, nextProps.navTabs) || + !isEqual(this.props.timerange, nextProps.timerange) || + !isEqual(this.props.timelineId, nextProps.timelineId) + ) { + const { + detailName, + hosts, + hostDetails, + navTabs, + network, + pageName, + pathName, + search, + tabName, + timelineId, + timerange, + } = nextProps; + if (pathName) { + setBreadcrumbs({ + detailName, + hosts, + hostDetails, + navTabs, + network, + pageName, + pathName, + search, + tabName, + timerange, + timelineId, + }); + } } } public render() { const { display, - location, - hosts, hostDetails, - match, + hosts, navTabs, network, + pageName, + pathName, showBorder, - timerange, + tabName, timelineId, + timerange, } = this.props; return ( ); } @@ -132,7 +199,15 @@ const makeMapStateToProps = () => { return mapStateToProps; }; -export const SiemNavigation = compose>( - withRouter, - connect(makeMapStateToProps) -)(SiemNavigationComponent); +export const SiemNavigationRedux = compose< + React.ComponentClass +>(connect(makeMapStateToProps))(SiemNavigationComponent); + +export const SiemNavigation = React.memo(props => { + const [routeProps] = useRouteSpy(); + const stateNavReduxProps: RouteSpyState & SiemNavigationComponentProps = { + ...routeProps, + ...props, + }; + return ; +}); diff --git a/x-pack/legacy/plugins/siem/public/components/navigation/tab_navigation/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/navigation/tab_navigation/index.test.tsx index 7216d825f9c3d..1493ab8ac9ce8 100644 --- a/x-pack/legacy/plugins/siem/public/components/navigation/tab_navigation/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/navigation/tab_navigation/index.test.tsx @@ -8,32 +8,26 @@ import { shallow } from 'enzyme'; import * as React from 'react'; import { TabNavigation } from './'; -import { TabNavigationProps } from '../type'; +import { TabNavigationProps } from './types'; import { navTabs, SiemPageName } from '../../../pages/home/home_navigations'; import { HostsTableType } from '../../../store/hosts/model'; import { navTabsHostDetails } from '../../../pages/hosts/hosts_navigations'; import { CONSTANTS } from '../../url_state/constants'; +import { RouteSpyState } from '../../../utils/route/types'; describe('Tab Navigation', () => { const pageName = SiemPageName.hosts; const hostName = 'siem-window'; const tabName = HostsTableType.authentications; const pathName = `/${pageName}/${hostName}/${tabName}`; - const mockMatch = { - params: { + + describe('Page Navigation', () => { + const mockProps: TabNavigationProps & RouteSpyState = { pageName, - hostName, + pathName, + detailName: undefined, + search: '', tabName, - }, - }; - describe('Page Navigation', () => { - const mockProps: TabNavigationProps = { - location: { - pathname: pathName, - search: '', - state: '', - hash: '', - }, navTabs, [CONSTANTS.timerange]: { global: { @@ -77,7 +71,6 @@ describe('Tab Navigation', () => { test('it mounts with correct tab highlighted', () => { const wrapper = shallow(); const hostsTab = wrapper.find('[data-test-subj="navigation-hosts"]'); - expect(hostsTab.prop('isSelected')).toBeTruthy(); }); test('it changes active tab when nav changes by props', () => { @@ -85,12 +78,9 @@ describe('Tab Navigation', () => { const networkTab = () => wrapper.find('[data-test-subj="navigation-network"]'); expect(networkTab().prop('isSelected')).toBeFalsy(); wrapper.setProps({ - location: { - pathname: '/network', - search: '', - state: '', - hash: '', - }, + pageName: 'network', + pathName: '/network', + tabName: undefined, }); wrapper.update(); expect(networkTab().prop('isSelected')).toBeTruthy(); @@ -105,15 +95,13 @@ describe('Tab Navigation', () => { }); describe('Table Navigation', () => { - const mockProps: TabNavigationProps = { - location: { - pathname: pathName, - search: '', - state: '', - hash: '', - }, + const mockProps: TabNavigationProps & RouteSpyState = { + pageName: 'hosts', + pathName: '/hosts', + detailName: undefined, + search: '', + tabName: HostsTableType.authentications, navTabs: navTabsHostDetails(hostName), - match: mockMatch, [CONSTANTS.timerange]: { global: { [CONSTANTS.timerange]: { @@ -162,18 +150,15 @@ describe('Tab Navigation', () => { expect(tableNavigationTab.prop('isSelected')).toBeTruthy(); }); test('it changes active tab when nav changes by props', () => { - const newMatch = { - params: { - pageName: SiemPageName.hosts, - hostName, - tabName: HostsTableType.events, - }, - }; const wrapper = shallow(); const tableNavigationTab = () => wrapper.find(`[data-test-subj="navigation-${HostsTableType.events}"]`); expect(tableNavigationTab().prop('isSelected')).toBeFalsy(); - wrapper.setProps({ location: `/${SiemPageName.hosts}`, match: newMatch }); + wrapper.setProps({ + pageName: SiemPageName.hosts, + pathName: `/${SiemPageName.hosts}`, + tabName: HostsTableType.events, + }); wrapper.update(); expect(tableNavigationTab().prop('isSelected')).toBeTruthy(); }); diff --git a/x-pack/legacy/plugins/siem/public/components/navigation/tab_navigation/index.tsx b/x-pack/legacy/plugins/siem/public/components/navigation/tab_navigation/index.tsx index 5c831bf51e23d..98357901e4273 100644 --- a/x-pack/legacy/plugins/siem/public/components/navigation/tab_navigation/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/navigation/tab_navigation/index.tsx @@ -5,23 +5,15 @@ */ import { EuiTab, EuiTabs, EuiLink } from '@elastic/eui'; import { get, getOr } from 'lodash/fp'; -import { Location } from 'history'; + import * as React from 'react'; import styled from 'styled-components'; import classnames from 'classnames'; import { trackUiAction as track, METRIC_TYPE, TELEMETRY_EVENT } from '../../../lib/track_usage'; import { HostsTableType } from '../../../store/hosts/model'; -import { UrlInputsModel } from '../../../store/inputs/model'; -import { CONSTANTS } from '../../url_state/constants'; -import { KqlQuery, URL_STATE_KEYS, KeyUrlState } from '../../url_state/types'; -import { NavTab, NavMatchParams, TabNavigationProps } from '../type'; - -import { - replaceQueryStringInLocation, - replaceStateKeyInQueryString, - getQueryStringFromLocation, -} from '../../url_state/helpers'; +import { getSearch } from '../helpers'; +import { TabNavigationProps } from './types'; const TabContainer = styled.div` .euiLink { @@ -42,15 +34,11 @@ interface TabNavigationState { export class TabNavigation extends React.PureComponent { constructor(props: TabNavigationProps) { super(props); - const pathname = props.location.pathname; - const match = props.match; - const selectedTabId = this.mapLocationToTab(pathname, match); + const selectedTabId = this.mapLocationToTab(props.pageName, props.tabName); this.state = { selectedTabId }; } public componentWillReceiveProps(nextProps: TabNavigationProps): void { - const pathname = nextProps.location.pathname; - const match = nextProps.match; - const selectedTabId = this.mapLocationToTab(pathname, match); + const selectedTabId = this.mapLocationToTab(nextProps.pageName, nextProps.tabName); if (this.state.selectedTabId !== selectedTabId) { this.setState(prevState => ({ @@ -68,13 +56,13 @@ export class TabNavigation extends React.PureComponent { + public mapLocationToTab = (pageName: string, tabName?: HostsTableType): string => { const { navTabs } = this.props; - const tabName: HostsTableType | undefined = get('params.tabName', match); - const myNavTab = Object.keys(navTabs) - .map(tab => get(tab, navTabs)) - .filter((item: NavTab) => (tabName || pathname).includes(item.id))[0]; - return getOr('', 'id', myNavTab); + return getOr( + '', + 'id', + Object.values(navTabs).find(item => tabName === item.id || pageName === item.id) + ); }; private renderTabs = (): JSX.Element[] => { @@ -88,7 +76,7 @@ export class TabNavigation extends React.PureComponent { - return URL_STATE_KEYS[tab.urlKey].reduce( - (myLocation: Location, urlKey: KeyUrlState) => { - let urlStateToReplace: UrlInputsModel | KqlQuery | string = this.props[ - CONSTANTS.timelineId - ]; - if (urlKey === CONSTANTS.kqlQuery && tab.urlKey === 'host') { - urlStateToReplace = tab.isDetailPage ? this.props.hostDetails : this.props.hosts; - } else if (urlKey === CONSTANTS.kqlQuery && tab.urlKey === 'network') { - urlStateToReplace = this.props.network; - } else if (urlKey === CONSTANTS.timerange) { - urlStateToReplace = this.props[CONSTANTS.timerange]; - } - myLocation = replaceQueryStringInLocation( - myLocation, - replaceStateKeyInQueryString(urlKey, urlStateToReplace)( - getQueryStringFromLocation(myLocation) - ) - ); - return myLocation; - }, - { - ...this.props.location, - search: '', - } - ).search; - }; } diff --git a/x-pack/legacy/plugins/siem/public/components/navigation/tab_navigation/types.ts b/x-pack/legacy/plugins/siem/public/components/navigation/tab_navigation/types.ts new file mode 100644 index 0000000000000..38970e31332cd --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/components/navigation/tab_navigation/types.ts @@ -0,0 +1,23 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { UrlInputsModel } from '../../../store/inputs/model'; +import { CONSTANTS } from '../../url_state/constants'; +import { KqlQuery } from '../../url_state/types'; +import { HostsTableType } from '../../../store/hosts/model'; + +import { SiemNavigationComponentProps } from '../types'; + +export interface TabNavigationProps extends SiemNavigationComponentProps { + pathName: string; + pageName: string; + tabName: HostsTableType | undefined; + hosts: KqlQuery; + hostDetails: KqlQuery; + network: KqlQuery; + [CONSTANTS.timerange]: UrlInputsModel; + [CONSTANTS.timelineId]: string; +} diff --git a/x-pack/legacy/plugins/siem/public/components/navigation/type.ts b/x-pack/legacy/plugins/siem/public/components/navigation/type.ts deleted file mode 100644 index ff96f28ed815e..0000000000000 --- a/x-pack/legacy/plugins/siem/public/components/navigation/type.ts +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ -import { Location } from 'history'; - -import { UrlInputsModel } from '../../store/inputs/model'; -import { CONSTANTS } from '../url_state/constants'; -import { KqlQuery, UrlStateType } from '../url_state/types'; -import { NavigationParams } from './breadcrumbs'; - -export interface NavTab { - id: string; - name: string; - href: string; - disabled: boolean; - urlKey: UrlStateType; - isDetailPage?: boolean; -} - -export interface NavMatchParams { - params: NavigationParams; -} - -export interface SiemNavigationComponentProps { - display?: 'default' | 'condensed'; - navTabs: Record; - showBorder?: boolean; -} - -export interface TabNavigationProps extends SiemNavigationComponentProps { - location: Location; - hosts: KqlQuery; - hostDetails: KqlQuery; - network: KqlQuery; - [CONSTANTS.timerange]: UrlInputsModel; - [CONSTANTS.timelineId]: string; - match?: NavMatchParams; -} diff --git a/x-pack/legacy/plugins/siem/public/components/navigation/types.ts b/x-pack/legacy/plugins/siem/public/components/navigation/types.ts new file mode 100644 index 0000000000000..2918a19df52fd --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/components/navigation/types.ts @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { UrlStateType } from '../url_state/constants'; + +export interface SiemNavigationComponentProps { + display?: 'default' | 'condensed'; + navTabs: Record; + showBorder?: boolean; +} + +export type SearchNavTab = NavTab | { urlKey: UrlStateType; isDetailPage: boolean }; + +export interface NavTab { + id: string; + name: string; + href: string; + disabled: boolean; + urlKey: UrlStateType; + isDetailPage?: boolean; +} diff --git a/x-pack/legacy/plugins/siem/public/components/page/add_to_kql/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/page/add_to_kql/index.test.tsx index f2f37c78807a0..c08b877076cbe 100644 --- a/x-pack/legacy/plugins/siem/public/components/page/add_to_kql/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/page/add_to_kql/index.test.tsx @@ -90,7 +90,7 @@ describe('AddToKql Component', () => { activePage: 0, limit: 10, }, - hosts: { + allHosts: { activePage: 0, limit: 10, direction: 'desc', diff --git a/x-pack/legacy/plugins/siem/public/components/page/hosts/hosts_table/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/page/hosts/hosts_table/index.test.tsx index a4399a16dbf05..700340b8c9dd2 100644 --- a/x-pack/legacy/plugins/siem/public/components/page/hosts/hosts_table/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/page/hosts/hosts_table/index.test.tsx @@ -21,6 +21,7 @@ import { createStore, hostsModel, State } from '../../../../store'; import { HostsTable } from './index'; import { mockData } from './mock'; +import { HostsTableType } from '../../../../store/hosts/model'; describe('Hosts Table', () => { const loadPage = jest.fn(); @@ -100,7 +101,7 @@ describe('Hosts Table', () => { ); }); test('Initial value of the store', () => { - expect(store.getState().hosts.page.queries.hosts).toEqual({ + expect(store.getState().hosts.page.queries[HostsTableType.hosts]).toEqual({ activePage: 0, direction: 'desc', sortField: 'lastSeen', @@ -128,7 +129,7 @@ describe('Hosts Table', () => { wrapper.update(); - expect(store.getState().hosts.page.queries.hosts).toEqual({ + expect(store.getState().hosts.page.queries[HostsTableType.hosts]).toEqual({ activePage: 0, direction: 'asc', sortField: 'hostName', diff --git a/x-pack/legacy/plugins/siem/public/components/url_state/__snapshots__/index.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/url_state/__snapshots__/index.test.tsx.snap index e3195baf20fd3..47b07bbd09f5a 100644 --- a/x-pack/legacy/plugins/siem/public/components/url_state/__snapshots__/index.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/url_state/__snapshots__/index.test.tsx.snap @@ -333,28 +333,11 @@ exports[`UrlStateContainer mounts and renders 1`] = ` "state": "", }, "push": [MockFunction], - "replace": [MockFunction] { - "calls": Array [ - Array [ - Object { - "hash": "", - "pathname": "/network", - "search": "?timerange=(global:(linkTo:!(timeline),timerange:(from:0,fromStr:now-24h,kind:relative,to:1,toStr:now)),timeline:(linkTo:!(global),timerange:(from:0,fromStr:now-24h,kind:relative,to:1,toStr:now)))", - "state": "", - }, - ], - ], - "results": Array [ - Object { - "type": "return", - "value": undefined, - }, - ], - }, + "replace": [MockFunction], } } > - - - + - - - - + } + /> + + diff --git a/x-pack/legacy/plugins/siem/public/components/url_state/constants.ts b/x-pack/legacy/plugins/siem/public/components/url_state/constants.ts index 66f79d53c77f0..e0ecfc1640bbe 100644 --- a/x-pack/legacy/plugins/siem/public/components/url_state/constants.ts +++ b/x-pack/legacy/plugins/siem/public/components/url_state/constants.ts @@ -16,3 +16,5 @@ export enum CONSTANTS { timelineId = 'timelineId', unknown = 'unknown', } + +export type UrlStateType = 'host' | 'network' | 'overview' | 'timeline'; diff --git a/x-pack/legacy/plugins/siem/public/components/url_state/helpers.test.ts b/x-pack/legacy/plugins/siem/public/components/url_state/helpers.test.ts index 7a0f1402d765a..69e3d10fff8e9 100644 --- a/x-pack/legacy/plugins/siem/public/components/url_state/helpers.test.ts +++ b/x-pack/legacy/plugins/siem/public/components/url_state/helpers.test.ts @@ -3,45 +3,69 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ - -import { isKqlForRoute } from './helpers'; +import { navTabs, SiemPageName } from '../../pages/home/home_navigations'; +import { isKqlForRoute, getTitle } from './helpers'; import { CONSTANTS } from './constants'; -describe('isKqlForRoute', () => { - test('host page and host page kuery', () => { - const result = isKqlForRoute('/hosts', CONSTANTS.hostsPage); - expect(result).toBeTruthy(); - }); - test('host page and host details kuery', () => { - const result = isKqlForRoute('/hosts', CONSTANTS.hostsDetails); - expect(result).toBeFalsy(); - }); - test('works when there is a trailing slash', () => { - const result = isKqlForRoute('/hosts/', CONSTANTS.hostsPage); - expect(result).toBeTruthy(); - }); - test('host details and host details kuery', () => { - const result = isKqlForRoute('/hosts/siem-kibana', CONSTANTS.hostsDetails); - expect(result).toBeTruthy(); - }); - test('host details and host page kuery', () => { - const result = isKqlForRoute('/hosts/siem-kibana', CONSTANTS.hostsPage); - expect(result).toBeFalsy(); - }); - test('network page and network page kuery', () => { - const result = isKqlForRoute('/network', CONSTANTS.networkPage); - expect(result).toBeTruthy(); - }); - test('network page and network details kuery', () => { - const result = isKqlForRoute('/network', CONSTANTS.networkDetails); - expect(result).toBeFalsy(); - }); - test('network details and network details kuery', () => { - const result = isKqlForRoute('/network/ip/10.100.7.198', CONSTANTS.networkDetails); - expect(result).toBeTruthy(); +describe('Helpers Url_State', () => { + describe('isKqlForRoute', () => { + test('host page and host page kuery', () => { + const result = isKqlForRoute(SiemPageName.hosts, undefined, CONSTANTS.hostsPage); + expect(result).toBeTruthy(); + }); + test('host page and host details kuery', () => { + const result = isKqlForRoute(SiemPageName.hosts, undefined, CONSTANTS.hostsDetails); + expect(result).toBeFalsy(); + }); + test('host details and host details kuery', () => { + const result = isKqlForRoute(SiemPageName.hosts, 'siem-kibana', CONSTANTS.hostsDetails); + expect(result).toBeTruthy(); + }); + test('host details and host page kuery', () => { + const result = isKqlForRoute(SiemPageName.hosts, 'siem-kibana', CONSTANTS.hostsPage); + expect(result).toBeFalsy(); + }); + test('network page and network page kuery', () => { + const result = isKqlForRoute(SiemPageName.network, undefined, CONSTANTS.networkPage); + expect(result).toBeTruthy(); + }); + test('network page and network details kuery', () => { + const result = isKqlForRoute(SiemPageName.network, undefined, CONSTANTS.networkDetails); + expect(result).toBeFalsy(); + }); + test('network details and network details kuery', () => { + const result = isKqlForRoute(SiemPageName.network, '10.100.7.198', CONSTANTS.networkDetails); + expect(result).toBeTruthy(); + }); + test('network details and network page kuery', () => { + const result = isKqlForRoute(SiemPageName.network, '123.234.34', CONSTANTS.networkPage); + expect(result).toBeFalsy(); + }); }); - test('network details and network page kuery', () => { - const result = isKqlForRoute('/network/ip/123.234.34', CONSTANTS.networkPage); - expect(result).toBeFalsy(); + describe('getTitle', () => { + test('host page name', () => { + const result = getTitle('hosts', undefined, navTabs); + expect(result).toEqual('Hosts'); + }); + test('network page name', () => { + const result = getTitle('network', undefined, navTabs); + expect(result).toEqual('Network'); + }); + test('overview page name', () => { + const result = getTitle('overview', undefined, navTabs); + expect(result).toEqual('Overview'); + }); + test('timelines page name', () => { + const result = getTitle('timelines', undefined, navTabs); + expect(result).toEqual('Timelines'); + }); + test('details page name', () => { + const result = getTitle('hosts', 'details', navTabs); + expect(result).toEqual('details'); + }); + test('Not existing', () => { + const result = getTitle('IamHereButNotReally', undefined, navTabs); + expect(result).toEqual(''); + }); }); }); diff --git a/x-pack/legacy/plugins/siem/public/components/url_state/helpers.ts b/x-pack/legacy/plugins/siem/public/components/url_state/helpers.ts index cda63bd3a76bd..64a53a8402a57 100644 --- a/x-pack/legacy/plugins/siem/public/components/url_state/helpers.ts +++ b/x-pack/legacy/plugins/siem/public/components/url_state/helpers.ts @@ -7,8 +7,11 @@ import { decode, encode, RisonValue } from 'rison-node'; import { Location } from 'history'; import { QueryString } from 'ui/utils/query_string'; -import { CONSTANTS } from './constants'; -import { LocationTypes, UrlStateType } from './types'; + +import { SiemPageName } from '../../pages/home/home_navigations'; +import { NavTab } from '../navigation/types'; +import { CONSTANTS, UrlStateType } from './constants'; +import { LocationTypes } from './types'; // eslint-disable-next-line @typescript-eslint/no-explicit-any export const decodeRisonUrlState = (value: string | undefined): RisonValue | any | undefined => { @@ -71,50 +74,56 @@ export const replaceQueryStringInLocation = (location: Location, queryString: st } }; -export const getUrlType = (pathname: string): UrlStateType => { - const removeSlash = pathname.replace(/\/$/, ''); - const trailingPath = removeSlash.match(/([^\/]+$)/); - if (trailingPath !== null) { - if (trailingPath[0] === 'hosts' || pathname.match(/^\/hosts\/.+$/) != null) { - return 'host'; - } else if (trailingPath[0] === 'network' || pathname.match(/^\/network\/.+$/) != null) { - return 'network'; - } else if (trailingPath[0] === 'overview') { - return 'overview'; - } else if (trailingPath[0] === 'timelines') { - return 'timeline'; - } +export const getUrlType = (pageName: string): UrlStateType => { + if (pageName === SiemPageName.hosts) { + return 'host'; + } else if (pageName === SiemPageName.network) { + return 'network'; + } else if (pageName === SiemPageName.overview) { + return 'overview'; + } else if (pageName === SiemPageName.timelines) { + return 'timeline'; } return 'overview'; }; -export const getCurrentLocation = (pathname: string): LocationTypes => { - const removeSlash = pathname.replace(/\/$/, ''); - const trailingPath = removeSlash.match(/([^\/]+$)/); - if (trailingPath !== null) { - if (trailingPath[0] === 'hosts') { - return CONSTANTS.hostsPage; - } else if (pathname.match(/^\/hosts\/.+$/) != null) { +export const getTitle = ( + pageName: string, + detailName: string | undefined, + navTabs: Record +): string => { + if (detailName != null) return detailName; + return navTabs[pageName] != null ? navTabs[pageName].name : ''; +}; + +export const getCurrentLocation = ( + pageName: string, + detailName: string | undefined +): LocationTypes => { + if (pageName === SiemPageName.hosts) { + if (detailName != null) { return CONSTANTS.hostsDetails; - } else if (trailingPath[0] === 'network') { - return CONSTANTS.networkPage; - } else if (pathname.match(/^\/network\/.+$/) != null) { + } + return CONSTANTS.hostsPage; + } else if (pageName === SiemPageName.network) { + if (detailName != null) { return CONSTANTS.networkDetails; - } else if (trailingPath[0] === 'overview') { - return CONSTANTS.overviewPage; - } else if (trailingPath[0] === 'timelines') { - return CONSTANTS.timelinePage; } + return CONSTANTS.networkPage; + } else if (pageName === SiemPageName.overview) { + return CONSTANTS.overviewPage; + } else if (pageName === SiemPageName.timelines) { + return CONSTANTS.timelinePage; } return CONSTANTS.unknown; - // throw new Error(`'Unknown pathName in else if statement': ${pathname}`); }; export const isKqlForRoute = ( - pathname: string, + pageName: string, + detailName: string | undefined, queryLocation: LocationTypes | null = null ): boolean => { - const currentLocation = getCurrentLocation(pathname); + const currentLocation = getCurrentLocation(pageName, detailName); if ( (currentLocation === CONSTANTS.hostsPage && queryLocation === CONSTANTS.hostsPage) || (currentLocation === CONSTANTS.networkPage && queryLocation === CONSTANTS.networkPage) || diff --git a/x-pack/legacy/plugins/siem/public/components/url_state/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/url_state/index.test.tsx index 6957ed515b7c5..dd8e8909a0921 100644 --- a/x-pack/legacy/plugins/siem/public/components/url_state/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/url_state/index.test.tsx @@ -3,7 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { Location } from 'history'; + import { mount } from 'enzyme'; import toJson from 'enzyme-to-json'; import * as React from 'react'; @@ -11,20 +11,16 @@ import { Router } from 'react-router-dom'; import { MockedProvider } from 'react-apollo/test-utils'; import { StaticIndexPattern } from 'ui/index_patterns'; -import { - apolloClientObservable, - globalNode, - HookWrapper, - mockGlobalState, - TestProviders, -} from '../../mock'; +import { apolloClientObservable, HookWrapper, mockGlobalState, TestProviders } from '../../mock'; import { createStore, State } from '../../store'; import { UseUrlState } from './'; import { defaultProps, getMockPropsObj, mockHistory, testCases } from './test_dependencies'; import { UrlStateContainerPropTypes } from './types'; -import { useUrlStateHooks, initializeLocation } from './use_url_state'; +import { useUrlStateHooks } from './use_url_state'; import { CONSTANTS } from './constants'; +import { RouteSpyState } from '../../utils/route/types'; +import { navTabs, SiemPageName } from '../../pages/home/home_navigations'; let mockProps: UrlStateContainerPropTypes; @@ -39,6 +35,19 @@ const indexPattern: StaticIndexPattern = { }, ], }; + +// const mockUseRouteSpy: jest.Mock = useRouteSpy as jest.Mock; +const mockRouteSpy: RouteSpyState = { + pageName: SiemPageName.network, + detailName: undefined, + tabName: undefined, + search: '', + pathName: '/network', +}; +jest.mock('../../utils/route/use_route_spy', () => ({ + useRouteSpy: () => [mockRouteSpy], +})); + describe('UrlStateContainer', () => { const state: State = mockGlobalState; @@ -55,7 +64,7 @@ describe('UrlStateContainer', () => { - + @@ -68,53 +77,64 @@ describe('UrlStateContainer', () => { describe('handleInitialize', () => { describe('URL state updates redux', () => { describe('relative timerange actions are called with correct data on component mount', () => { - test.each(testCases)('%o', (page, namespaceLower, namespaceUpper, examplePath) => { - mockProps = getMockPropsObj({ page, examplePath, namespaceLower }).relativeTimeSearch - .undefinedQuery; - mount( useUrlStateHooks(args)} />); - - // @ts-ignore property mock does not exists - expect(defaultProps.setRelativeTimerange.mock.calls[1][0]).toEqual({ - from: 1558591200000, - fromStr: 'now-1d/d', - kind: 'relative', - to: 1558677599999, - toStr: 'now-1d/d', - id: 'global', - }); - // @ts-ignore property mock does not exists - expect(defaultProps.setRelativeTimerange.mock.calls[0][0]).toEqual({ - from: 1558732849370, - fromStr: 'now-15m', - kind: 'relative', - to: 1558733749370, - toStr: 'now', - id: 'timeline', - }); - }); + test.each(testCases)( + '%o', + (page, namespaceLower, namespaceUpper, examplePath, type, pageName, detailName) => { + mockProps = getMockPropsObj({ + page, + examplePath, + namespaceLower, + pageName, + detailName, + }).relativeTimeSearch.undefinedQuery; + mount( useUrlStateHooks(args)} />); + + // @ts-ignore property mock does not exists + expect(defaultProps.setRelativeTimerange.mock.calls[1][0]).toEqual({ + from: 1558591200000, + fromStr: 'now-1d/d', + kind: 'relative', + to: 1558677599999, + toStr: 'now-1d/d', + id: 'global', + }); + // @ts-ignore property mock does not exists + expect(defaultProps.setRelativeTimerange.mock.calls[0][0]).toEqual({ + from: 1558732849370, + fromStr: 'now-15m', + kind: 'relative', + to: 1558733749370, + toStr: 'now', + id: 'timeline', + }); + } + ); }); describe('absolute timerange actions are called with correct data on component mount', () => { - test.each(testCases)('%o', (page, namespaceLower, namespaceUpper, examplePath) => { - mockProps = getMockPropsObj({ page, examplePath, namespaceLower }).absoluteTimeSearch - .undefinedQuery; - mount( useUrlStateHooks(args)} />); - - // @ts-ignore property mock does not exists - expect(defaultProps.setAbsoluteTimerange.mock.calls[1][0]).toEqual({ - from: 1556736012685, - kind: 'absolute', - to: 1556822416082, - id: 'global', - }); - // @ts-ignore property mock does not exists - expect(defaultProps.setAbsoluteTimerange.mock.calls[0][0]).toEqual({ - from: 1556736012685, - kind: 'absolute', - to: 1556822416082, - id: 'timeline', - }); - }); + test.each(testCases)( + '%o', + (page, namespaceLower, namespaceUpper, examplePath, type, pageName, detailName) => { + mockProps = getMockPropsObj({ page, examplePath, namespaceLower, pageName, detailName }) + .absoluteTimeSearch.undefinedQuery; + mount( useUrlStateHooks(args)} />); + + // @ts-ignore property mock does not exists + expect(defaultProps.setAbsoluteTimerange.mock.calls[1][0]).toEqual({ + from: 1556736012685, + kind: 'absolute', + to: 1556822416082, + id: 'global', + }); + // @ts-ignore property mock does not exists + expect(defaultProps.setAbsoluteTimerange.mock.calls[0][0]).toEqual({ + from: 1556736012685, + kind: 'absolute', + to: 1556822416082, + id: 'timeline', + }); + } + ); }); describe('kqlQuery action is called with correct data on component mount', () => { @@ -128,9 +148,9 @@ describe('UrlStateContainer', () => { }; test.each(testCases.slice(0, 4))( ' %o', - (page, namespaceLower, namespaceUpper, examplePath, type) => { - mockProps = getMockPropsObj({ page, examplePath, namespaceLower }).relativeTimeSearch - .undefinedQuery; + (page, namespaceLower, namespaceUpper, examplePath, type, pageName, detailName) => { + mockProps = getMockPropsObj({ page, examplePath, namespaceLower, pageName, detailName }) + .relativeTimeSearch.undefinedQuery; mount( useUrlStateHooks(args)} />); const functionName = namespaceUpper === 'Network' ? defaultProps.setNetworkKql : defaultProps.setHostsKql; @@ -144,111 +164,54 @@ describe('UrlStateContainer', () => { }); describe('kqlQuery action is not called called when the queryLocation does not match the router location', () => { - test.each(testCases)('%o', (page, namespaceLower, namespaceUpper, examplePath) => { - mockProps = getMockPropsObj({ page, examplePath, namespaceLower }) - .oppositeQueryLocationSearch.undefinedQuery; - mount( useUrlStateHooks(args)} />); - const functionName = - namespaceUpper === 'Network' ? defaultProps.setNetworkKql : defaultProps.setHostsKql; - // @ts-ignore property mock does not exists - expect(functionName.mock.calls.length).toEqual(0); - }); + test.each(testCases)( + '%o', + (page, namespaceLower, namespaceUpper, examplePath, type, pageName, detailName) => { + mockProps = getMockPropsObj({ + page, + examplePath, + namespaceLower, + pageName, + detailName, + }).oppositeQueryLocationSearch.undefinedQuery; + mount( useUrlStateHooks(args)} />); + const functionName = + namespaceUpper === 'Network' ? defaultProps.setNetworkKql : defaultProps.setHostsKql; + // @ts-ignore property mock does not exists + expect(functionName.mock.calls.length).toEqual(0); + } + ); }); }); describe('Redux updates URL state', () => { describe('kqlQuery url state is set from redux data on component mount', () => { - test.each(testCases)('%o', (page, namespaceLower, namespaceUpper, examplePath) => { - mockProps = getMockPropsObj({ page, examplePath, namespaceLower }).noSearch.definedQuery; - mount( useUrlStateHooks(args)} />); - - // @ts-ignore property mock does not exists - expect( - mockHistory.replace.mock.calls[mockHistory.replace.mock.calls.length - 1][0] - ).toEqual({ - hash: '', - pathname: examplePath, - search: [CONSTANTS.overviewPage, CONSTANTS.timelinePage].includes(page) - ? '?_g=()&timerange=(global:(linkTo:!(timeline),timerange:(from:1558048243696,fromStr:now-24h,kind:relative,to:1558134643697,toStr:now)),timeline:(linkTo:!(global),timerange:(from:1558048243696,fromStr:now-24h,kind:relative,to:1558134643697,toStr:now)))' - : `?_g=()&kqlQuery=(filterQuery:(expression:'host.name:%22siem-es%22',kind:kuery),queryLocation:${page})&timerange=(global:(linkTo:!(timeline),timerange:(from:1558048243696,fromStr:now-24h,kind:relative,to:1558134643697,toStr:now)),timeline:(linkTo:!(global),timerange:(from:1558048243696,fromStr:now-24h,kind:relative,to:1558134643697,toStr:now)))`, - state: '', - }); - }); - }); - }); - }); - - describe('initializeLocation', () => { - test('basic functionality with no pathname', () => { - Object.defineProperty(globalNode.window, 'location', { - value: { - href: 'http://localhost:5601/app/siem#/overview', - hash: '#/overview', - }, - writable: true, - }); - const location: Location = { - hash: '', - pathname: '/', - search: '', - state: null, - }; - expect(initializeLocation(location).search).toEqual(''); - }); - test('basic functionality with no search', () => { - Object.defineProperty(globalNode.window, 'location', { - value: { - href: 'http://localhost:5601/app/siem#/hosts?_g=()', - }, - writable: true, - }); - const location: Location = { - hash: '', - pathname: '/hosts', - search: '?_g=()', - state: null, - }; - expect(initializeLocation(location).search).toEqual('?_g=()'); - }); - - test('basic functionality with search', () => { - Object.defineProperty(globalNode.window, 'location', { - value: { - href: - "http://localhost:5601/app/siem#/hosts?_g=()&kqlQuery=(filterQuery:(expression:'%20host.name:%20%22beats-ci-immutable-ubuntu-1604-1560801145745062645%22%20and%20process.name:*',kind:kuery),queryLocation:hosts.page,type:page)&timerange=(global:(linkTo:!(timeline),timerange:(from:1560714985274,fromStr:now-24h,kind:relative,to:1560801385274,toStr:now)),timeline:(linkTo:!(global),timerange:(from:1560714985274,fromStr:now-24h,kind:relative,to:1560801385274,toStr:now)))", - }, - writable: true, - }); - const location: Location = { - hash: '', - pathname: '/hosts', - search: - "?_g=()&kqlQuery=(filterQuery:(expression:'%2Bhost.name:%2B%22beats-ci-immutable-ubuntu-1604-1560801145745062645%22%2Band%2Bprocess.name:*',kind:kuery),queryLocation:hosts.page,type:page)&timerange=(global:(linkTo:!(timeline),timerange:(from:1560714985274,fromStr:now-24h,kind:relative,to:1560801385274,toStr:now)),timeline:(linkTo:!(global),timerange:(from:1560714985274,fromStr:now-24h,kind:relative,to:1560801385274,toStr:now)))", - state: null, - }; - expect(initializeLocation(location).search).toEqual( - "?_g=()&kqlQuery=(filterQuery:(expression:'%20host.name:%20%22beats-ci-immutable-ubuntu-1604-1560801145745062645%22%20and%20process.name:*',kind:kuery),queryLocation:hosts.page,type:page)&timerange=(global:(linkTo:!(timeline),timerange:(from:1560714985274,fromStr:now-24h,kind:relative,to:1560801385274,toStr:now)),timeline:(linkTo:!(global),timerange:(from:1560714985274,fromStr:now-24h,kind:relative,to:1560801385274,toStr:now)))" - ); - }); + test.each(testCases)( + '%o', + (page, namespaceLower, namespaceUpper, examplePath, type, pageName, detailName) => { + mockProps = getMockPropsObj({ + page, + examplePath, + namespaceLower, + pageName, + detailName, + }).noSearch.definedQuery; + mount( useUrlStateHooks(args)} />); - test('If hash and pathname do not match href from the hash, do not do anything', () => { - Object.defineProperty(globalNode.window, 'location', { - value: { - href: - "http://localhost:5601/app/siem#/hosts?_g=()&kqlQuery=(filterQuery:(expression:'%20host.name:%20%22beats-ci-immutable-ubuntu-1604-1560801145745062645%22%20and%20process.name:*',kind:kuery),queryLocation:hosts.page,type:page)&timerange=(global:(linkTo:!(timeline),timerange:(from:1560714985274,fromStr:now-24h,kind:relative,to:1560801385274,toStr:now)),timeline:(linkTo:!(global),timerange:(from:1560714985274,fromStr:now-24h,kind:relative,to:1560801385274,toStr:now)))", - }, - writable: true, + // @ts-ignore property mock does not exists + expect( + mockHistory.replace.mock.calls[mockHistory.replace.mock.calls.length - 1][0] + ).toEqual({ + hash: '', + pathname: examplePath, + search: [CONSTANTS.overviewPage, CONSTANTS.timelinePage].includes(page) + ? '?_g=()&timerange=(global:(linkTo:!(timeline),timerange:(from:1558048243696,fromStr:now-24h,kind:relative,to:1558134643697,toStr:now)),timeline:(linkTo:!(global),timerange:(from:1558048243696,fromStr:now-24h,kind:relative,to:1558134643697,toStr:now)))' + : `?_g=()&kqlQuery=(filterQuery:(expression:'host.name:%22siem-es%22',kind:kuery),queryLocation:${page})&timerange=(global:(linkTo:!(timeline),timerange:(from:1558048243696,fromStr:now-24h,kind:relative,to:1558134643697,toStr:now)),timeline:(linkTo:!(global),timerange:(from:1558048243696,fromStr:now-24h,kind:relative,to:1558134643697,toStr:now)))`, + state: '', + }); + } + ); }); - const location: Location = { - hash: '', - pathname: '/network', - search: - "?_g=()&kqlQuery=(filterQuery:(expression:'%2Bhost.name:%2B%22beats-ci-immutable-ubuntu-1604-1560801145745062645%22%2Band%2Bprocess.name:*',kind:kuery),queryLocation:hosts.page,type:page)&timerange=(global:(linkTo:!(timeline),timerange:(from:1560714985274,fromStr:now-24h,kind:relative,to:1560801385274,toStr:now)),timeline:(linkTo:!(global),timerange:(from:1560714985274,fromStr:now-24h,kind:relative,to:1560801385274,toStr:now)))", - state: null, - }; - expect(initializeLocation(location).search).toEqual( - "?_g=()&kqlQuery=(filterQuery:(expression:'%2Bhost.name:%2B%22beats-ci-immutable-ubuntu-1604-1560801145745062645%22%2Band%2Bprocess.name:*',kind:kuery),queryLocation:hosts.page,type:page)&timerange=(global:(linkTo:!(timeline),timerange:(from:1560714985274,fromStr:now-24h,kind:relative,to:1560801385274,toStr:now)),timeline:(linkTo:!(global),timerange:(from:1560714985274,fromStr:now-24h,kind:relative,to:1560801385274,toStr:now)))" - ); }); }); }); diff --git a/x-pack/legacy/plugins/siem/public/components/url_state/index.tsx b/x-pack/legacy/plugins/siem/public/components/url_state/index.tsx index 700dc9f9ff440..e53cb55c9b792 100644 --- a/x-pack/legacy/plugins/siem/public/components/url_state/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/url_state/index.tsx @@ -7,7 +7,6 @@ import React from 'react'; import { compose, Dispatch } from 'redux'; import { connect } from 'react-redux'; -import { withRouter } from 'react-router-dom'; import { isEqual } from 'lodash/fp'; import { @@ -20,6 +19,8 @@ import { timelineSelectors, } from '../../store'; import { hostsActions, inputsActions, networkActions, timelineActions } from '../../store/actions'; +import { RouteSpyState } from '../../utils/route/types'; +import { useRouteSpy } from '../../utils/route/use_route_spy'; import { CONSTANTS } from './constants'; import { UrlStateContainerPropTypes, UrlStateProps, KqlQuery, LocationTypes } from './types'; @@ -28,13 +29,12 @@ import { dispatchUpdateTimeline } from '../open_timeline/helpers'; import { getCurrentLocation } from './helpers'; export const UrlStateContainer = React.memo( - props => { + (props: UrlStateContainerPropTypes) => { useUrlStateHooks(props); return null; }, (prevProps, nextProps) => - prevProps.location.pathname === nextProps.location.pathname && - isEqual(prevProps.urlState, nextProps.urlState) + prevProps.pathName === nextProps.pathName && isEqual(prevProps.urlState, nextProps.urlState) ); UrlStateContainer.displayName = 'UrlStateContainer'; @@ -44,12 +44,12 @@ const makeMapStateToProps = () => { const getHostsFilterQueryAsKuery = hostsSelectors.hostsFilterQueryAsKuery(); const getNetworkFilterQueryAsKuery = networkSelectors.networkFilterQueryAsKuery(); const getTimelines = timelineSelectors.getTimelines(); - const mapStateToProps = (state: State, { location }: UrlStateContainerPropTypes) => { + const mapStateToProps = (state: State, { pageName, detailName }: UrlStateContainerPropTypes) => { const inputState = getInputsSelector(state); const { linkTo: globalLinkTo, timerange: globalTimerange } = inputState.global; const { linkTo: timelineLinkTo, timerange: timelineTimerange } = inputState.timeline; - const page: LocationTypes | null = getCurrentLocation(location.pathname); + const page: LocationTypes | null = getCurrentLocation(pageName, detailName); const kqlQueryInitialState: KqlQuery = { filterQuery: null, queryLocation: page, @@ -121,10 +121,15 @@ const mapDispatchToProps = (dispatch: Dispatch) => ({ dispatch, }); -export const UseUrlState = compose>( - withRouter, +export const UrlStateRedux = compose>( connect( makeMapStateToProps, mapDispatchToProps ) )(UrlStateContainer); + +export const UseUrlState = React.memo(props => { + const [routeProps] = useRouteSpy(); + const urlStateReduxProps: RouteSpyState & UrlStateProps = { ...routeProps, ...props }; + return ; +}); diff --git a/x-pack/legacy/plugins/siem/public/components/url_state/index_mocked.test.tsx b/x-pack/legacy/plugins/siem/public/components/url_state/index_mocked.test.tsx index f194e4cc6f425..6598bd491f73b 100644 --- a/x-pack/legacy/plugins/siem/public/components/url_state/index_mocked.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/url_state/index_mocked.test.tsx @@ -9,6 +9,7 @@ import { difference } from 'lodash/fp'; import * as React from 'react'; import { HookWrapper } from '../../mock/hook_wrapper'; +import { SiemPageName } from '../../pages/home/home_navigations'; import { CONSTANTS } from './constants'; import { getFilterQuery, getMockPropsObj, mockHistory, testCases } from './test_dependencies'; @@ -36,6 +37,8 @@ describe('UrlStateContainer - lodash.throttle mocked to test update url', () => page: CONSTANTS.networkPage, examplePath: '/network', namespaceLower: 'network', + pageName: SiemPageName.network, + detailName: undefined, }).noSearch.definedQuery; const wrapper = mount( useUrlStateHooks(args)} /> @@ -85,6 +88,8 @@ describe('UrlStateContainer - lodash.throttle mocked to test update url', () => page: CONSTANTS.networkPage, examplePath: '/network', namespaceLower: 'network', + pageName: SiemPageName.network, + detailName: undefined, }).noSearch.undefinedQuery; const wrapper = mount( useUrlStateHooks(args)} /> @@ -117,6 +122,8 @@ describe('UrlStateContainer - lodash.throttle mocked to test update url', () => page: CONSTANTS.networkPage, examplePath: '/network', namespaceLower: 'network', + pageName: SiemPageName.network, + detailName: undefined, }).noSearch.undefinedQuery; const wrapper = mount( useUrlStateHooks(args)} /> @@ -145,39 +152,46 @@ describe('UrlStateContainer - lodash.throttle mocked to test update url', () => describe('handleInitialize', () => { describe('Redux updates URL state', () => { describe('Timerange url state is set when not defined on component mount', () => { - test.each(testCases)('%o', (page, namespaceLower, namespaceUpper, examplePath) => { - mockProps = getMockPropsObj({ page, examplePath, namespaceLower }).noSearch - .undefinedQuery; - mount( useUrlStateHooks(args)} />); - - expect(mockHistory.replace.mock.calls[0][0]).toEqual({ - hash: '', - pathname: examplePath, - search: '?_g=()', - state: '', - }); - - expect( - mockHistory.replace.mock.calls[mockHistory.replace.mock.calls.length - 1][0] - ).toEqual({ - hash: '', - pathname: examplePath, - search: - '?_g=()&timerange=(global:(linkTo:!(timeline),timerange:(from:1558048243696,fromStr:now-24h,kind:relative,to:1558134643697,toStr:now)),timeline:(linkTo:!(global),timerange:(from:1558048243696,fromStr:now-24h,kind:relative,to:1558134643697,toStr:now)))', - state: '', - }); - }); + test.each(testCases)( + '%o', + (page, namespaceLower, namespaceUpper, examplePath, type, pageName, detailName) => { + mockProps = getMockPropsObj({ page, examplePath, namespaceLower, pageName, detailName }) + .noSearch.undefinedQuery; + mount( useUrlStateHooks(args)} />); + + expect(mockHistory.replace.mock.calls[0][0]).toEqual({ + hash: '', + pathname: examplePath, + search: '?_g=()', + state: '', + }); + + expect( + mockHistory.replace.mock.calls[mockHistory.replace.mock.calls.length - 1][0] + ).toEqual({ + hash: '', + pathname: examplePath, + search: + '?_g=()&timerange=(global:(linkTo:!(timeline),timerange:(from:1558048243696,fromStr:now-24h,kind:relative,to:1558134643697,toStr:now)),timeline:(linkTo:!(global),timerange:(from:1558048243696,fromStr:now-24h,kind:relative,to:1558134643697,toStr:now)))', + state: '', + }); + } + ); test('url state is set from redux data when location updates and initialization', () => { mockProps = getMockPropsObj({ page: CONSTANTS.hostsPage, examplePath: '/hosts', namespaceLower: 'hosts', + pageName: SiemPageName.hosts, + detailName: undefined, }).noSearch.undefinedQuery; const updatedProps = getMockPropsObj({ page: CONSTANTS.networkPage, examplePath: '/network', namespaceLower: 'network', + pageName: SiemPageName.network, + detailName: undefined, }).noSearch.definedQuery; const wrapper = mount( useUrlStateHooks(args)} /> diff --git a/x-pack/legacy/plugins/siem/public/components/url_state/test_dependencies.ts b/x-pack/legacy/plugins/siem/public/components/url_state/test_dependencies.ts index a520d54a6d493..1915b2c524525 100644 --- a/x-pack/legacy/plugins/siem/public/components/url_state/test_dependencies.ts +++ b/x-pack/legacy/plugins/siem/public/components/url_state/test_dependencies.ts @@ -10,6 +10,8 @@ import { UrlStateContainerPropTypes, LocationTypes, KqlQuery } from './types'; import { CONSTANTS } from './constants'; import { InputsModelId } from '../../store/inputs/constants'; import { DispatchUpdateTimeline } from '../open_timeline/types'; +import { navTabs, SiemPageName } from '../../pages/home/home_navigations'; +import { HostsTableType } from '../../store/hosts/model'; type Action = 'PUSH' | 'POP' | 'REPLACE'; const pop: Action = 'POP'; @@ -44,13 +46,12 @@ export const mockHistory = { }; export const defaultProps: UrlStateContainerPropTypes = { - match: { - isExact: true, - params: '', - path: '', - url: '', - }, - isInitializing: true, + pageName: SiemPageName.network, + detailName: undefined, + tabName: HostsTableType.authentications, + search: '', + pathName: '/network', + navTabs, indexPattern: { fields: [ { @@ -127,13 +128,14 @@ export const defaultProps: UrlStateContainerPropTypes = { ...mockHistory, location: defaultLocation, }, - location: defaultLocation, }; export const getMockProps = ( location = defaultLocation, kqlQueryKey = CONSTANTS.networkPage, - kqlQueryValue: KqlQuery | null + kqlQueryValue: KqlQuery | null, + pageName: string, + detailName: string | undefined ): UrlStateContainerPropTypes => ({ ...defaultProps, urlState: { @@ -144,16 +146,27 @@ export const getMockProps = ( ...mockHistory, location, }, - location, + detailName, + pageName, + pathName: location.pathname, + search: location.search, }); interface GetMockPropsObj { examplePath: string; namespaceLower: string; page: LocationTypes; + pageName: string; + detailName: string | undefined; } -export const getMockPropsObj = ({ page, examplePath, namespaceLower }: GetMockPropsObj) => ({ +export const getMockPropsObj = ({ + page, + examplePath, + namespaceLower, + pageName, + detailName, +}: GetMockPropsObj) => ({ noSearch: { undefinedQuery: getMockProps( { @@ -163,7 +176,9 @@ export const getMockPropsObj = ({ page, examplePath, namespaceLower }: GetMockPr state: '', }, page, - null + null, + pageName, + detailName ), definedQuery: getMockProps( { @@ -173,7 +188,9 @@ export const getMockPropsObj = ({ page, examplePath, namespaceLower }: GetMockPr state: '', }, page, - getFilterQuery(page) + getFilterQuery(page), + pageName, + detailName ), }, relativeTimeSearch: { @@ -185,7 +202,9 @@ export const getMockPropsObj = ({ page, examplePath, namespaceLower }: GetMockPr state: '', }, page, - null + null, + pageName, + detailName ), definedQuery: getMockProps( { @@ -195,7 +214,9 @@ export const getMockPropsObj = ({ page, examplePath, namespaceLower }: GetMockPr state: '', }, page, - getFilterQuery(page) + getFilterQuery(page), + pageName, + detailName ), }, absoluteTimeSearch: { @@ -208,7 +229,9 @@ export const getMockPropsObj = ({ page, examplePath, namespaceLower }: GetMockPr state: '', }, page, - null + null, + pageName, + detailName ), definedQuery: getMockProps( { @@ -219,7 +242,9 @@ export const getMockPropsObj = ({ page, examplePath, namespaceLower }: GetMockPr state: '', }, page, - getFilterQuery(page) + getFilterQuery(page), + pageName, + detailName ), }, oppositeQueryLocationSearch: { @@ -233,7 +258,9 @@ export const getMockPropsObj = ({ page, examplePath, namespaceLower }: GetMockPr state: '', }, page, - null + null, + pageName, + detailName ), }, }); @@ -245,40 +272,54 @@ export const testCases = [ /* page */ CONSTANTS.networkPage, /* namespaceLower */ 'network', /* namespaceUpper */ 'Network', - /* examplePath */ '/network', + /* pathName */ '/network', /* type */ networkModel.NetworkType.page, + /* pageName */ SiemPageName.network, + /* detailName */ undefined, ], [ /* page */ CONSTANTS.hostsPage, /* namespaceLower */ 'hosts', /* namespaceUpper */ 'Hosts', - /* examplePath */ '/hosts', + /* pathName */ '/hosts', /* type */ hostsModel.HostsType.page, + /* pageName */ SiemPageName.hosts, + /* detailName */ undefined, ], [ /* page */ CONSTANTS.hostsDetails, /* namespaceLower */ 'hosts', /* namespaceUpper */ 'Hosts', - /* examplePath */ '/hosts/siem-es', + /* pathName */ '/hosts/siem-es', /* type */ hostsModel.HostsType.details, + /* pageName */ SiemPageName.hosts, + /* detailName */ 'host-test', ], [ /* page */ CONSTANTS.networkDetails, /* namespaceLower */ 'network', /* namespaceUpper */ 'Network', - /* examplePath */ '/network/ip/100.90.80', + /* pathName */ '/network/ip/100.90.80', /* type */ networkModel.NetworkType.details, + /* pageName */ SiemPageName.network, + /* detailName */ '100.90.80', ], [ /* page */ CONSTANTS.overviewPage, /* namespaceLower */ 'overview', /* namespaceUpper */ 'Overview', - /* examplePath */ '/overview', + /* pathName */ '/overview', + /* type */ null, + /* pageName */ SiemPageName.overview, + /* detailName */ undefined, ], [ /* page */ CONSTANTS.timelinePage, /* namespaceLower */ 'timeline', /* namespaceUpper */ 'Timeline', - /* examplePath */ '/timeline', + /* pathName */ '/timeline', + /* type */ null, + /* pageName */ SiemPageName.timelines, + /* detailName */ undefined, ], ]; diff --git a/x-pack/legacy/plugins/siem/public/components/url_state/types.ts b/x-pack/legacy/plugins/siem/public/components/url_state/types.ts index ea3ea40481179..8ff54e0f9d433 100644 --- a/x-pack/legacy/plugins/siem/public/components/url_state/types.ts +++ b/x-pack/legacy/plugins/siem/public/components/url_state/types.ts @@ -4,20 +4,18 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Location } from 'history'; -import { RouteComponentProps } from 'react-router'; import { ActionCreator } from 'typescript-fsa'; import { StaticIndexPattern } from 'ui/index_patterns'; - import { Dispatch } from 'redux'; + import { hostsModel, KueryFilterQuery, networkModel, SerializedFilterQuery } from '../../store'; import { UrlInputsModel } from '../../store/inputs/model'; import { InputsModelId } from '../../store/inputs/constants'; - -import { CONSTANTS } from './constants'; +import { RouteSpyState } from '../../utils/route/types'; import { DispatchUpdateTimeline } from '../open_timeline/types'; +import { NavTab } from '../navigation/types'; -export type UrlStateType = 'host' | 'network' | 'overview' | 'timeline'; +import { CONSTANTS, UrlStateType } from './constants'; export const ALL_URL_STATE_KEYS: KeyUrlState[] = [ CONSTANTS.kqlQuery, @@ -53,8 +51,8 @@ export interface UrlState { } export type KeyUrlState = keyof UrlState; -export interface UrlStateProps { - isInitializing: boolean; +export interface UrlStateProps { + navTabs: Record; indexPattern?: StaticIndexPattern; mapToUrlState?: (value: string) => UrlState; onChange?: (urlState: UrlState, previousUrlState: UrlState) => void; @@ -100,12 +98,12 @@ export interface UrlStateDispatchToPropsType { }>; } -export type UrlStateContainerPropTypes = RouteComponentProps & +export type UrlStateContainerPropTypes = RouteSpyState & UrlStateStateToPropsType & UrlStateDispatchToPropsType & UrlStateProps; export interface PreviousLocationUrlState { - location: Location; + pathName: string | undefined; urlState: UrlState; } diff --git a/x-pack/legacy/plugins/siem/public/components/url_state/use_url_state.tsx b/x-pack/legacy/plugins/siem/public/components/url_state/use_url_state.tsx index 441f828f03417..f444b0105100c 100644 --- a/x-pack/legacy/plugins/siem/public/components/url_state/use_url_state.tsx +++ b/x-pack/legacy/plugins/siem/public/components/url_state/use_url_state.tsx @@ -6,7 +6,7 @@ import { Location } from 'history'; import { get, isEqual, difference, isEmpty } from 'lodash/fp'; -import { useEffect, useRef } from 'react'; +import { useEffect, useRef, useState } from 'react'; import { convertKueryToElasticSearchQuery } from '../../lib/keury'; import { InputsModelId, TimeRangeKinds } from '../../store/inputs/constants'; @@ -16,8 +16,12 @@ import { RelativeTimeRange, UrlInputsModel, } from '../../store/inputs/model'; +import { useApolloClient } from '../../utils/apollo_context'; +import { queryTimelineById } from '../open_timeline/helpers'; +import { HostsType } from '../../store/hosts/model'; +import { NetworkType } from '../../store/network/model'; -import { CONSTANTS } from './constants'; +import { CONSTANTS, UrlStateType } from './constants'; import { replaceQueryStringInLocation, getQueryStringFromLocation, @@ -27,6 +31,7 @@ import { isKqlForRoute, getCurrentLocation, getUrlType, + getTitle, } from './helpers'; import { normalizeTimeRange } from './normalize_time_range'; import { @@ -36,12 +41,7 @@ import { KeyUrlState, KqlQuery, ALL_URL_STATE_KEYS, - UrlStateType, } from './types'; -import { useApolloClient } from '../../utils/apollo_context'; -import { queryTimelineById } from '../open_timeline/helpers'; -import { HostsType } from '../../store/hosts/model'; -import { NetworkType } from '../../store/network/model'; function usePrevious(value: PreviousLocationUrlState) { const ref = useRef(value); @@ -54,37 +54,52 @@ function usePrevious(value: PreviousLocationUrlState) { export const useUrlStateHooks = ({ addGlobalLinkTo, addTimelineLinkTo, + detailName, dispatch, - location, indexPattern, - isInitializing, history, + navTabs, + pageName, + pathName, removeGlobalLinkTo, removeTimelineLinkTo, + search, setAbsoluteTimerange, setHostsKql, setNetworkKql, setRelativeTimerange, + tabName, updateTimeline, updateTimelineIsLoading, urlState, }: UrlStateContainerPropTypes) => { + const [isInitializing, setIsInitializing] = useState(true); const apolloClient = useApolloClient(); - const prevProps = usePrevious({ location, urlState }); + const prevProps = usePrevious({ pathName, urlState }); const replaceStateInLocation = ( urlStateToReplace: UrlInputsModel | KqlQuery | string, urlStateKey: string, - latestLocation: Location = location + latestLocation: Location = { + hash: '', + pathname: pathName, + search, + state: '', + } ) => { const newLocation = replaceQueryStringInLocation( - location, + { + hash: '', + pathname: pathName, + search, + state: '', + }, replaceStateKeyInQueryString(urlStateKey, urlStateToReplace)( getQueryStringFromLocation(latestLocation) ) ); - if (!isEqual(newLocation.search, latestLocation.search)) { + if (history && !isEqual(newLocation.search, latestLocation.search)) { history.replace(newLocation); } return newLocation; @@ -101,7 +116,7 @@ export const useUrlStateHooks = ({ const kqlQueryStateData: KqlQuery = decodeRisonUrlState(newUrlStateString); if ( urlKey === CONSTANTS.kqlQuery && - !isKqlForRoute(location.pathname, kqlQueryStateData.queryLocation) && + !isKqlForRoute(pageName, detailName, kqlQueryStateData.queryLocation) && urlState[urlKey].queryLocation === kqlQueryStateData.queryLocation ) { myLocation = replaceStateInLocation( @@ -201,7 +216,7 @@ export const useUrlStateHooks = ({ } if (urlKey === CONSTANTS.kqlQuery && indexPattern != null) { const kqlQueryStateData: KqlQuery = decodeRisonUrlState(newUrlStateString); - if (isKqlForRoute(location.pathname, kqlQueryStateData.queryLocation)) { + if (isKqlForRoute(pageName, detailName, kqlQueryStateData.queryLocation)) { const filterQuery = { kuery: kqlQueryStateData.filterQuery, serializedQuery: convertKueryToElasticSearchQuery( @@ -209,7 +224,7 @@ export const useUrlStateHooks = ({ indexPattern ), }; - const page = getCurrentLocation(location.pathname); + const page = getCurrentLocation(pageName, detailName); if ([CONSTANTS.hostsPage, CONSTANTS.hostsDetails].includes(page)) { dispatch( setHostsKql({ @@ -243,38 +258,30 @@ export const useUrlStateHooks = ({ }; useEffect(() => { - const type: UrlStateType = getUrlType(location.pathname); - if (isInitializing) { - handleInitialize(initializeLocation(location), type); + const type: UrlStateType = getUrlType(pageName); + const location: Location = { + hash: '', + pathname: pathName, + search, + state: '', + }; + + if (isInitializing && pageName != null && pageName !== '') { + handleInitialize(location, type); + setIsInitializing(false); } else if (!isEqual(urlState, prevProps.urlState)) { let newLocation: Location = location; URL_STATE_KEYS[type].forEach((urlKey: KeyUrlState) => { newLocation = replaceStateInLocation(urlState[urlKey], urlKey, newLocation); }); - } else if (location.pathname !== prevProps.location.pathname) { + } else if (pathName !== prevProps.pathName) { handleInitialize(location, type); } }); - return null; -}; + useEffect(() => { + document.title = `${getTitle(pageName, detailName, navTabs)} - Kibana`; + }, [pageName]); -/* - * Why are we doing that, it is because angular-ui router is encoding the `+` back to `2%B` after - * that react router is getting the data with the `+` and convert to `2%B` - * so we need to get back the value from the window location at initialization to avoid - * to bring back the `+` in the kql - */ -export const initializeLocation = (location: Location): Location => { - if (location.pathname === '/') { - location.pathname = window.location.hash.substring(1); - } - const substringIndex = - window.location.href.indexOf(`#${location.pathname}`) >= 0 - ? window.location.href.indexOf(`#${location.pathname}`) + location.pathname.length + 1 - : -1; - if (substringIndex >= 0 && location.pathname !== '/') { - location.search = window.location.href.substring(substringIndex); - } - return location; + return null; }; diff --git a/x-pack/legacy/plugins/siem/public/containers/global_time/index.tsx b/x-pack/legacy/plugins/siem/public/containers/global_time/index.tsx index 8280f7cb32c08..aa90cb8025dd0 100644 --- a/x-pack/legacy/plugins/siem/public/containers/global_time/index.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/global_time/index.tsx @@ -22,7 +22,7 @@ interface GlobalQuery extends SetQuery { inputId: InputsModelId; } -interface GlobalTimeArgs { +export interface GlobalTimeArgs { from: number; to: number; setQuery: ({ id, inspect, loading, refetch }: SetQuery) => void; @@ -55,7 +55,7 @@ export const GlobalTimeComponent = React.memo( return () => { deleteAllQuery({ id: 'global' }); }; - }); + }, []); return ( <> diff --git a/x-pack/legacy/plugins/siem/public/mock/global_state.ts b/x-pack/legacy/plugins/siem/public/mock/global_state.ts index 7a19c65ec9f34..83fa30c97145f 100644 --- a/x-pack/legacy/plugins/siem/public/mock/global_state.ts +++ b/x-pack/legacy/plugins/siem/public/mock/global_state.ts @@ -38,7 +38,7 @@ export const mockGlobalState: State = { page: { queries: { authentications: { activePage: 0, limit: 10 }, - hosts: { + allHosts: { activePage: 0, limit: 10, direction: Direction.desc, @@ -54,7 +54,7 @@ export const mockGlobalState: State = { details: { queries: { authentications: { activePage: 0, limit: 10 }, - hosts: { + allHosts: { activePage: 0, limit: 10, direction: Direction.desc, diff --git a/x-pack/legacy/plugins/siem/public/pages/home/home_navigations.tsx b/x-pack/legacy/plugins/siem/public/pages/home/home_navigations.tsx index edbcad74a9cda..329547fe202e0 100644 --- a/x-pack/legacy/plugins/siem/public/pages/home/home_navigations.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/home/home_navigations.tsx @@ -3,14 +3,14 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import * as i18n from '../../components/navigation/translations'; -import { NavTab } from '../../components/navigation/type'; +import * as i18n from './translations'; import { getOverviewUrl, getNetworkUrl, getTimelinesUrl, getHostsUrl, } from '../../components/link_to'; +import { NavTab } from '../../components/navigation/types'; export enum SiemPageName { overview = 'overview', diff --git a/x-pack/legacy/plugins/siem/public/pages/home/index.tsx b/x-pack/legacy/plugins/siem/public/pages/home/index.tsx index 6bfbdc48e63f5..f5c22e6acbf64 100644 --- a/x-pack/legacy/plugins/siem/public/pages/home/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/home/index.tsx @@ -12,7 +12,6 @@ import { pure } from 'recompose'; import styled from 'styled-components'; import chrome from 'ui/chrome'; -import { i18n } from '@kbn/i18n'; import { AutoSizer } from '../../components/auto_sizer'; import { DragDropContextWrapper } from '../../components/drag_and_drop/drag_drop_context_wrapper'; import { Flyout, flyoutHeaderHeight } from '../../components/flyout'; @@ -25,7 +24,6 @@ import { NotFoundPage } from '../404'; import { HostsContainer } from '../hosts'; import { NetworkContainer } from '../network'; import { Overview } from '../overview'; -import { PageRoute } from '../../components/page_route/pageroute'; import { Timelines } from '../timelines'; import { WithSource } from '../../containers/source'; import { MlPopover } from '../../components/ml_popover/ml_popover'; @@ -33,7 +31,7 @@ import { MlHostConditionalContainer } from '../../components/ml/conditional_link import { MlNetworkConditionalContainer } from '../../components/ml/conditional_links/ml_network_conditional_container'; import { navTabs } from './home_navigations'; import { UseUrlState } from '../../components/url_state'; -import { useGlobalLoading } from '../../utils/use_global_loading'; +import { SpyRoute } from '../../utils/route/spy_routes'; const WrappedByAutoSizer = styled.div` height: 100%; @@ -77,119 +75,100 @@ const calculateFlyoutHeight = ({ windowHeight: number; }): number => Math.max(0, windowHeight - globalHeaderSize); -export const HomePage = pure(() => { - const isGlobalInitializing = useGlobalLoading(); - return ( - - {({ measureRef, windowMeasurement: { height: windowHeight = 0 } }) => ( - - - - - {({ browserFields, indexPattern }) => ( - - - - ( + + {({ measureRef, windowMeasurement: { height: windowHeight = 0 } }) => ( + + + + + {({ browserFields, indexPattern }) => ( + + + + + - + + + + + + + + + + + + + + + + + + + + + + + + + + + } /> + ( + + )} /> - - - - - - - - - - - - - - - - - - - - - - - - - - - - ( - - )} - /> - - - ( - - )} - /> - - - - - - - - )} - - - - )} - - ); -}); + ( + + )} + /> + } /> + + + + + + + + )} + + + + + )} + +)); HomePage.displayName = 'HomePage'; diff --git a/x-pack/legacy/plugins/siem/public/components/navigation/translations.ts b/x-pack/legacy/plugins/siem/public/pages/home/translations.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/navigation/translations.ts rename to x-pack/legacy/plugins/siem/public/pages/home/translations.ts diff --git a/x-pack/legacy/plugins/siem/public/pages/hosts/details/body.tsx b/x-pack/legacy/plugins/siem/public/pages/hosts/details/body.tsx new file mode 100644 index 0000000000000..ad2c38cb0af12 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/hosts/details/body.tsx @@ -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; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { connect } from 'react-redux'; + +import { indicesExistOrDataTemporarilyUnavailable, WithSource } from '../../../containers/source'; +import { setAbsoluteRangeDatePicker as dispatchAbsoluteRangeDatePicker } from '../../../store/inputs/actions'; +import { scoreIntervalToDateTime } from '../../../components/ml/score/score_interval_to_datetime'; +import { Anomaly } from '../../../components/ml/types'; +import { getHostDetailsEventsKqlQueryExpression } from '../helpers'; + +import { HostDetailsBodyComponentProps } from './type'; +import { getFilterQuery, type, makeMapStateToProps } from './utils'; + +const HostDetailsBodyComponent = React.memo( + ({ + children, + filterQueryExpression, + from, + isInitializing, + detailName, + setAbsoluteRangeDatePicker, + setQuery, + to, + }) => { + return ( + + {({ indicesExist, indexPattern }) => + indicesExistOrDataTemporarilyUnavailable(indicesExist) ? ( + <> + {children({ + endDate: to, + filterQuery: getFilterQuery(detailName, filterQueryExpression, indexPattern), + kqlQueryExpression: getHostDetailsEventsKqlQueryExpression({ + filterQueryExpression, + hostName: detailName, + }), + skip: isInitializing, + setQuery, + startDate: from, + type, + indexPattern, + narrowDateRange: (score: Anomaly, interval: string) => { + const fromTo = scoreIntervalToDateTime(score, interval); + setAbsoluteRangeDatePicker({ + id: 'global', + from: fromTo.from, + to: fromTo.to, + }); + }, + })} + + ) : null + } + + ); + } +); + +HostDetailsBodyComponent.displayName = 'HostDetailsBodyComponent'; + +export const HostDetailsBody = connect( + makeMapStateToProps, + { + setAbsoluteRangeDatePicker: dispatchAbsoluteRangeDatePicker, + } +)(HostDetailsBodyComponent); diff --git a/x-pack/legacy/plugins/siem/public/pages/hosts/details/index.tsx b/x-pack/legacy/plugins/siem/public/pages/hosts/details/index.tsx new file mode 100644 index 0000000000000..ebcfeb30c6e0c --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/hosts/details/index.tsx @@ -0,0 +1,175 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiHorizontalRule, EuiSpacer } from '@elastic/eui'; +import React from 'react'; +import { compose } from 'redux'; +import { connect } from 'react-redux'; +import { StickyContainer } from 'react-sticky'; + +import { FiltersGlobal } from '../../../components/filters_global'; +import { HeaderPage } from '../../../components/header_page'; +import { LastEventTime } from '../../../components/last_event_time'; + +import { HostOverviewByNameQuery } from '../../../containers/hosts/overview'; +import { indicesExistOrDataTemporarilyUnavailable, WithSource } from '../../../containers/source'; +import { LastEventIndexKey } from '../../../graphql/types'; + +import { HostsEmptyPage } from '../hosts_empty_page'; +import { HostsKql } from '../kql'; +import { setAbsoluteRangeDatePicker as dispatchAbsoluteRangeDatePicker } from '../../../store/inputs/actions'; +import { scoreIntervalToDateTime } from '../../../components/ml/score/score_interval_to_datetime'; +import { KpiHostDetailsQuery } from '../../../containers/kpi_host_details'; +import { hostToCriteria } from '../../../components/ml/criteria/host_to_criteria'; +import { navTabsHostDetails } from '../hosts_navigations'; +import { SiemNavigation } from '../../../components/navigation'; +import { HostsQueryProps } from '../hosts'; +import { SpyRoute } from '../../../utils/route/spy_routes'; +import { AnomalyTableProvider } from '../../../components/ml/anomaly/anomaly_table_provider'; +import { manageQuery } from '../../../components/page/manage_query'; +import { HostOverview } from '../../../components/page/hosts/host_overview'; +import { KpiHostsComponent } from '../../../components/page/hosts'; + +import { HostDetailsComponentProps } from './type'; +import { getFilterQuery, type, makeMapStateToProps } from './utils'; + +export { HostDetailsBody } from './body'; + +const HostOverviewManage = manageQuery(HostOverview); +const KpiHostDetailsManage = manageQuery(KpiHostsComponent); + +const HostDetailsComponent = React.memo( + ({ + isInitializing, + filterQueryExpression, + from, + detailName, + setQuery, + setAbsoluteRangeDatePicker, + to, + }) => { + return ( + <> + + {({ indicesExist, indexPattern }) => + indicesExistOrDataTemporarilyUnavailable(indicesExist) ? ( + + + + + + + } + title={detailName} + /> + + {({ hostOverview, loading, id, inspect, refetch }) => ( + + {({ isLoadingAnomaliesData, anomaliesData }) => ( + { + const fromTo = scoreIntervalToDateTime(score, interval); + setAbsoluteRangeDatePicker({ + id: 'global', + from: fromTo.from, + to: fromTo.to, + }); + }} + /> + )} + + )} + + + + + + {({ kpiHostDetails, id, inspect, loading, refetch }) => ( + { + /** + * Using setTimeout here because of this issue: + * https://github.com/elastic/elastic-charts/issues/360 + * Need to remove the setTimeout here after this issue is fixed. + * */ + setTimeout(() => { + setAbsoluteRangeDatePicker({ id: 'global', from: min, to: max }); + }, 500); + }} + /> + )} + + + + + + + ) : ( + <> + + + + + ) + } + + + + ); + } +); + +HostDetailsComponent.displayName = 'HostDetailsComponent'; + +export const HostDetails = compose>( + connect( + makeMapStateToProps, + { + setAbsoluteRangeDatePicker: dispatchAbsoluteRangeDatePicker, + } + ) +)(HostDetailsComponent); diff --git a/x-pack/legacy/plugins/siem/public/pages/hosts/details/type.ts b/x-pack/legacy/plugins/siem/public/pages/hosts/details/type.ts new file mode 100644 index 0000000000000..a057e791ab1d4 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/hosts/details/type.ts @@ -0,0 +1,37 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { ActionCreator } from 'typescript-fsa'; + +import { InputsModelId } from '../../../store/inputs/constants'; +import { CommonChildren, AnonamaliesChildren, HostsQueryProps } from '../hosts'; +import { HostComponentProps } from '../../../components/link_to/redirect_to_hosts'; + +interface HostDetailsComponentReduxProps { + filterQueryExpression: string; +} + +interface HostDetailsComponentDispatchProps { + setAbsoluteRangeDatePicker: ActionCreator<{ + id: InputsModelId; + from: number; + to: number; + }>; + detailName: string; +} + +export interface HostDetailsBodyProps extends HostsQueryProps { + children: CommonChildren | AnonamaliesChildren; +} + +export type HostDetailsComponentProps = HostDetailsComponentReduxProps & + HostDetailsComponentDispatchProps & + HostComponentProps & + HostsQueryProps; + +export type HostDetailsBodyComponentProps = HostDetailsComponentReduxProps & + HostDetailsComponentDispatchProps & + HostDetailsBodyProps; diff --git a/x-pack/legacy/plugins/siem/public/pages/hosts/details/utils.ts b/x-pack/legacy/plugins/siem/public/pages/hosts/details/utils.ts new file mode 100644 index 0000000000000..cd4239f00cac7 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/hosts/details/utils.ts @@ -0,0 +1,81 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { isEmpty } from 'lodash/fp'; +import { Breadcrumb } from 'ui/chrome'; +import { StaticIndexPattern } from 'ui/index_patterns'; + +import { ESTermQuery } from '../../../../common/typed_json'; + +import { hostsModel, hostsSelectors } from '../../../store/hosts'; +import { HostsTableType } from '../../../store/hosts/model'; +import { State } from '../../../store'; +import { getHostsUrl, getHostDetailsUrl } from '../../../components/link_to/redirect_to_hosts'; + +import * as i18n from '../translations'; +import { convertKueryToElasticSearchQuery, escapeQueryValue } from '../../../lib/keury'; +import { RouteSpyState } from '../../../utils/route/types'; + +export const type = hostsModel.HostsType.details; + +export const makeMapStateToProps = () => { + const getHostsFilterQuery = hostsSelectors.hostsFilterQueryExpression(); + return (state: State) => ({ + filterQueryExpression: getHostsFilterQuery(state, type) || '', + }); +}; + +const TabNameMappedToI18nKey = { + [HostsTableType.hosts]: i18n.NAVIGATION_ALL_HOSTS_TITLE, + [HostsTableType.authentications]: i18n.NAVIGATION_AUTHENTICATIONS_TITLE, + [HostsTableType.uncommonProcesses]: i18n.NAVIGATION_UNCOMMON_PROCESSES_TITLE, + [HostsTableType.anomalies]: i18n.NAVIGATION_ANOMALIES_TITLE, + [HostsTableType.events]: i18n.NAVIGATION_EVENTS_TITLE, +}; + +export const getBreadcrumbs = (params: RouteSpyState, search: string[]): Breadcrumb[] => { + let breadcrumb = [ + { + text: i18n.PAGE_TITLE, + href: `${getHostsUrl()}${search && search[0] ? search[0] : ''}`, + }, + ]; + if (params.detailName != null) { + breadcrumb = [ + ...breadcrumb, + { + text: params.detailName, + href: `${getHostDetailsUrl(params.detailName)}${search && search[1] ? search[1] : ''}`, + }, + ]; + } + if (params.tabName != null) { + breadcrumb = [ + ...breadcrumb, + { + text: TabNameMappedToI18nKey[params.tabName], + href: '', + }, + ]; + } + return breadcrumb; +}; + +export const getFilterQuery = ( + hostName: string | null, + filterQueryExpression: string, + indexPattern: StaticIndexPattern +): ESTermQuery | string => + isEmpty(filterQueryExpression) + ? hostName + ? { term: { 'host.name': hostName } } + : '' + : convertKueryToElasticSearchQuery( + `${filterQueryExpression} ${ + hostName ? `and host.name: "${escapeQueryValue(hostName)}"` : '' + }`, + indexPattern + ); diff --git a/x-pack/legacy/plugins/siem/public/pages/hosts/host_details.tsx b/x-pack/legacy/plugins/siem/public/pages/hosts/host_details.tsx deleted file mode 100644 index 972aef1327750..0000000000000 --- a/x-pack/legacy/plugins/siem/public/pages/hosts/host_details.tsx +++ /dev/null @@ -1,319 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { EuiHorizontalRule, EuiSpacer } from '@elastic/eui'; -import { isEmpty, get } from 'lodash/fp'; -import React from 'react'; -import { connect } from 'react-redux'; -import { StickyContainer } from 'react-sticky'; -import { Breadcrumb } from 'ui/chrome'; -import { StaticIndexPattern } from 'ui/index_patterns'; - -import { ActionCreator } from 'typescript-fsa'; -import { ESTermQuery } from '../../../common/typed_json'; -import { FiltersGlobal } from '../../components/filters_global'; -import { HeaderPage } from '../../components/header_page'; -import { LastEventTime } from '../../components/last_event_time'; -import { - getHostsUrl, - HostComponentProps, - getHostDetailsUrl, -} from '../../components/link_to/redirect_to_hosts'; -import { KpiHostsComponent } from '../../components/page/hosts'; - -import { HostOverview } from '../../components/page/hosts/host_overview'; -import { manageQuery } from '../../components/page/manage_query'; -import { GlobalTime } from '../../containers/global_time'; -import { HostOverviewByNameQuery } from '../../containers/hosts/overview'; -import { indicesExistOrDataTemporarilyUnavailable, WithSource } from '../../containers/source'; -import { LastEventIndexKey } from '../../graphql/types'; -import { convertKueryToElasticSearchQuery, escapeQueryValue } from '../../lib/keury'; -import { hostsModel, hostsSelectors, State } from '../../store'; - -import { HostsEmptyPage } from './hosts_empty_page'; -import { HostsKql } from './kql'; -import * as i18n from './translations'; -import { AnomalyTableProvider } from '../../components/ml/anomaly/anomaly_table_provider'; -import { setAbsoluteRangeDatePicker as dispatchAbsoluteRangeDatePicker } from '../../store/inputs/actions'; -import { InputsModelId } from '../../store/inputs/constants'; -import { scoreIntervalToDateTime } from '../../components/ml/score/score_interval_to_datetime'; -import { KpiHostDetailsQuery } from '../../containers/kpi_host_details'; -import { hostToCriteria } from '../../components/ml/criteria/host_to_criteria'; -import { navTabsHostDetails } from './hosts_navigations'; -import { SiemNavigation } from '../../components/navigation'; -import { Anomaly } from '../../components/ml/types'; -import { NavigationParams } from '../../components/navigation/breadcrumbs'; -import { HostsTableType } from '../../store/hosts/model'; -import { HostsQueryProps } from './hosts'; -import { getHostDetailsEventsKqlQueryExpression } from './helpers'; - -const type = hostsModel.HostsType.details; - -const HostOverviewManage = manageQuery(HostOverview); -const KpiHostDetailsManage = manageQuery(KpiHostsComponent); - -interface HostDetailsComponentReduxProps { - filterQueryExpression: string; - setAbsoluteRangeDatePicker: ActionCreator<{ - id: InputsModelId; - from: number; - to: number; - }>; -} - -type HostDetailsComponentProps = HostDetailsComponentReduxProps & - HostComponentProps & - HostsQueryProps; - -const HostDetailsComponent = React.memo( - ({ - match: { - params: { hostName, tabName }, - }, - filterQueryExpression, - setAbsoluteRangeDatePicker, - }) => { - return ( - - {({ indicesExist, indexPattern }) => - indicesExistOrDataTemporarilyUnavailable(indicesExist) ? ( - - - - - - - } - title={hostName} - /> - - - {({ to, from, setQuery, isInitializing }) => ( - <> - - {({ hostOverview, loading, id, inspect, refetch }) => ( - - {({ isLoadingAnomaliesData, anomaliesData }) => ( - { - const fromTo = scoreIntervalToDateTime(score, interval); - setAbsoluteRangeDatePicker({ - id: 'global', - from: fromTo.from, - to: fromTo.to, - }); - }} - /> - )} - - )} - - - - - - {({ kpiHostDetails, id, inspect, loading, refetch }) => ( - { - /** - * Using setTimeout here because of this issue: - * https://github.com/elastic/elastic-charts/issues/360 - * Need to remove the setTimeout here after this issue is fixed. - * */ - setTimeout(() => { - setAbsoluteRangeDatePicker({ id: 'global', from: min, to: max }); - }, 500); - }} - /> - )} - - - - - - - )} - - - ) : ( - <> - - - - - ) - } - - ); - } -); - -HostDetailsComponent.displayName = 'HostDetailsComponent'; - -const HostDetailsBodyComponent = React.memo( - ({ - match: { - params: { hostName, tabName }, - }, - filterQueryExpression, - setAbsoluteRangeDatePicker, - children, - }) => { - return ( - - {({ indicesExist, indexPattern }) => - indicesExistOrDataTemporarilyUnavailable(indicesExist) ? ( - - {({ to, from, setQuery, isInitializing }) => ( - <> - {children({ - endDate: to, - filterQuery: getFilterQuery(hostName, filterQueryExpression, indexPattern), - kqlQueryExpression: getHostDetailsEventsKqlQueryExpression({ - filterQueryExpression, - hostName, - }), - skip: isInitializing, - setQuery, - startDate: from, - type, - indexPattern, - narrowDateRange: (score: Anomaly, interval: string) => { - const fromTo = scoreIntervalToDateTime(score, interval); - setAbsoluteRangeDatePicker({ - id: 'global', - from: fromTo.from, - to: fromTo.to, - }); - }, - })} - - )} - - ) : null - } - - ); - } -); - -HostDetailsBodyComponent.displayName = 'HostDetailsBodyComponent'; - -const makeMapStateToProps = () => { - const getHostsFilterQuery = hostsSelectors.hostsFilterQueryExpression(); - return (state: State) => ({ - filterQueryExpression: getHostsFilterQuery(state, type) || '', - }); -}; - -export const HostDetails = connect( - makeMapStateToProps, - { - setAbsoluteRangeDatePicker: dispatchAbsoluteRangeDatePicker, - } -)(HostDetailsComponent); - -export const HostDetailsBody = connect( - makeMapStateToProps, - { - setAbsoluteRangeDatePicker: dispatchAbsoluteRangeDatePicker, - } -)(HostDetailsBodyComponent); - -const TabNameMappedToI18nKey = { - [HostsTableType.hosts]: i18n.NAVIGATION_ALL_HOSTS_TITLE, - [HostsTableType.authentications]: i18n.NAVIGATION_AUTHENTICATIONS_TITLE, - [HostsTableType.uncommonProcesses]: i18n.NAVIGATION_UNCOMMON_PROCESSES_TITLE, - [HostsTableType.anomalies]: i18n.NAVIGATION_ANOMALIES_TITLE, - [HostsTableType.events]: i18n.NAVIGATION_EVENTS_TITLE, -}; - -export const getBreadcrumbs = (params: NavigationParams): Breadcrumb[] => { - const hostName = get('hostName', params); - const tabName = get('tabName', params); - let breadcrumb = [ - { - text: i18n.PAGE_TITLE, - href: getHostsUrl(), - }, - ]; - if (hostName) { - breadcrumb = [ - ...breadcrumb, - { - text: hostName, - href: getHostDetailsUrl(hostName), - }, - ]; - } - if (tabName) { - breadcrumb = [ - ...breadcrumb, - { - text: TabNameMappedToI18nKey[tabName], - href: '', - }, - ]; - } - return breadcrumb; -}; - -const getFilterQuery = ( - hostName: string | null, - filterQueryExpression: string, - indexPattern: StaticIndexPattern -): ESTermQuery | string => - isEmpty(filterQueryExpression) - ? hostName - ? { term: { 'host.name': hostName } } - : '' - : convertKueryToElasticSearchQuery( - `${filterQueryExpression} ${ - hostName ? `and host.name: "${escapeQueryValue(hostName)}"` : '' - }`, - indexPattern - ); diff --git a/x-pack/legacy/plugins/siem/public/pages/hosts/hosts.test.tsx b/x-pack/legacy/plugins/siem/public/pages/hosts/hosts.test.tsx index fbb522772d4a4..98698d50da096 100644 --- a/x-pack/legacy/plugins/siem/public/pages/hosts/hosts.test.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/hosts/hosts.test.tsx @@ -7,10 +7,11 @@ import { mount } from 'enzyme'; import * as React from 'react'; import { Router } from 'react-router-dom'; +import { ActionCreator } from 'typescript-fsa'; import '../../mock/match_media'; import '../../mock/ui_settings'; -import { Hosts, AnonamaliesChildren, HostsComponentProps } from './hosts'; +import { Hosts, HostsComponentProps } from './hosts'; import { mocksSource } from '../../containers/source/mock'; import { TestProviders } from '../../mock'; @@ -19,6 +20,8 @@ import { cloneDeep } from 'lodash/fp'; import { SiemNavigation } from '../../components/navigation'; import { wait } from '../../lib/helpers'; +import { InputsModelId } from '../../store/inputs/constants'; + jest.mock('../../lib/settings/use_kibana_ui_setting'); jest.mock('ui/documentation_links', () => ({ @@ -62,22 +65,26 @@ const mockHistory = { listen: jest.fn(), }; -const mockMatch = { - isExact: false, - url: '/', - path: '/', -}; -const mockChildren: AnonamaliesChildren = () =>
; - // Suppress warnings about "act" until async/await syntax is supported: https://github.com/facebook/react/issues/14769 /* eslint-disable no-console */ const originalError = console.error; +const to = new Date('2018-03-23T18:49:23.132Z').valueOf(); +const from = new Date('2018-03-24T03:33:52.253Z').valueOf(); + describe('Hosts - rendering', () => { - const hostProps = { - match: mockMatch, - children: mockChildren, - } as HostsComponentProps; + const hostProps: HostsComponentProps = { + from, + to, + setQuery: jest.fn(), + isInitializing: false, + setAbsoluteRangeDatePicker: (jest.fn() as unknown) as ActionCreator<{ + from: number; + id: InputsModelId; + to: number; + }>, + filterQuery: '', + }; beforeAll(() => { console.error = jest.fn(); diff --git a/x-pack/legacy/plugins/siem/public/pages/hosts/hosts.tsx b/x-pack/legacy/plugins/siem/public/pages/hosts/hosts.tsx index 63a822b90d0ee..6ada127c9f650 100644 --- a/x-pack/legacy/plugins/siem/public/pages/hosts/hosts.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/hosts/hosts.tsx @@ -6,18 +6,16 @@ import { EuiSpacer } from '@elastic/eui'; import React from 'react'; +import { compose } from 'redux'; import { connect } from 'react-redux'; import { StickyContainer } from 'react-sticky'; -import { pure } from 'recompose'; import { ActionCreator } from 'typescript-fsa'; -import { RouteComponentProps } from 'react-router-dom'; -import { FiltersGlobal } from '../../components/filters_global'; import { HeaderPage } from '../../components/header_page'; import { LastEventTime } from '../../components/last_event_time'; import { KpiHostsComponent } from '../../components/page/hosts'; import { manageQuery } from '../../components/page/manage_query'; -import { GlobalTime } from '../../containers/global_time'; +import { GlobalTimeArgs } from '../../containers/global_time'; import { KpiHostsQuery } from '../../containers/kpi_hosts'; import { indicesExistOrDataTemporarilyUnavailable, WithSource } from '../../containers/source'; import { LastEventIndexKey } from '../../graphql/types'; @@ -28,6 +26,8 @@ import { HostsKql } from './kql'; import { setAbsoluteRangeDatePicker as dispatchSetAbsoluteRangeDatePicker } from '../../store/inputs/actions'; import { InputsModelId } from '../../store/inputs/constants'; import { SiemNavigation } from '../../components/navigation'; +import { SpyRoute } from '../../utils/route/spy_routes'; +import { FiltersGlobal } from '../../components/filters_global'; import * as i18n from './translations'; import { @@ -40,7 +40,9 @@ const KpiHostsComponentManage = manageQuery(KpiHostsComponent); interface HostsComponentReduxProps { filterQuery: string; - kqlQueryExpression: string; +} + +interface HostsComponentDispatchProps { setAbsoluteRangeDatePicker: ActionCreator<{ id: InputsModelId; from: number; @@ -48,32 +50,31 @@ interface HostsComponentReduxProps { }>; } -type CommonChildren = (args: HostsComponentsQueryProps) => JSX.Element; +export type CommonChildren = (args: HostsComponentsQueryProps) => JSX.Element; export type AnonamaliesChildren = (args: AnomaliesQueryTabBodyProps) => JSX.Element; -export interface HostsQueryProps { - children: CommonChildren | AnonamaliesChildren; -} - -export type HostsComponentProps = RouteComponentProps & HostsComponentReduxProps & HostsQueryProps; +export type HostsQueryProps = GlobalTimeArgs; -const HostsComponent = pure(({ filterQuery, setAbsoluteRangeDatePicker }) => { - return ( - - {({ indicesExist, indexPattern }) => - indicesExistOrDataTemporarilyUnavailable(indicesExist) ? ( - - - - +export type HostsComponentProps = HostsComponentReduxProps & + HostsComponentDispatchProps & + HostsQueryProps; - } - title={i18n.PAGE_TITLE} - /> +const HostsComponent = React.memo( + ({ isInitializing, filterQuery, from, setAbsoluteRangeDatePicker, setQuery, to }) => { + return ( + <> + + {({ indicesExist, indexPattern }) => + indicesExistOrDataTemporarilyUnavailable(indicesExist) ? ( + + + + - - {({ to, from, setQuery, isInitializing }) => ( + } + title={i18n.PAGE_TITLE} + /> <> (({ filterQuery, setAbsoluteRang - )} - - - ) : ( - <> - - - - ) - } - - ); -}); + + ) : ( + <> + + + + ) + } + + + + ); + } +); HostsComponent.displayName = 'HostsComponent'; const makeMapStateToProps = () => { const getHostsFilterQueryAsJson = hostsSelectors.hostsFilterQueryAsJson(); - const mapStateToProps = (state: State) => ({ + const mapStateToProps = (state: State): HostsComponentReduxProps => ({ filterQuery: getHostsFilterQueryAsJson(state, hostsModel.HostsType.page) || '', }); return mapStateToProps; }; -export const Hosts = connect( - makeMapStateToProps, - { - setAbsoluteRangeDatePicker: dispatchSetAbsoluteRangeDatePicker, - } +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export const Hosts = compose>( + connect( + makeMapStateToProps, + { + setAbsoluteRangeDatePicker: dispatchSetAbsoluteRangeDatePicker, + } + ) )(HostsComponent); diff --git a/x-pack/legacy/plugins/siem/public/pages/hosts/hosts_body.tsx b/x-pack/legacy/plugins/siem/public/pages/hosts/hosts_body.tsx index 4b8fa3b734587..898bdec5b281c 100644 --- a/x-pack/legacy/plugins/siem/public/pages/hosts/hosts_body.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/hosts/hosts_body.tsx @@ -14,12 +14,17 @@ import { indicesExistOrDataTemporarilyUnavailable, WithSource } from '../../cont import { hostsModel, hostsSelectors, State } from '../../store'; -import { HostsComponentProps } from './hosts'; +import { HostsComponentProps, CommonChildren, AnonamaliesChildren } from './hosts'; import { scoreIntervalToDateTime } from '../../components/ml/score/score_interval_to_datetime'; import { setAbsoluteRangeDatePicker as dispatchSetAbsoluteRangeDatePicker } from '../../store/inputs/actions'; import { Anomaly } from '../../components/ml/types'; -const HostsBodyComponent = pure( +interface HostsBodyComponentProps extends HostsComponentProps { + kqlQueryExpression: string; + children: CommonChildren | AnonamaliesChildren; +} + +const HostsBodyComponent = pure( ({ filterQuery, kqlQueryExpression, setAbsoluteRangeDatePicker, children }) => { return ( diff --git a/x-pack/legacy/plugins/siem/public/pages/hosts/hosts_navigations.tsx b/x-pack/legacy/plugins/siem/public/pages/hosts/hosts_navigations.tsx index 52416bf6997a8..1f496e4d85b6e 100644 --- a/x-pack/legacy/plugins/siem/public/pages/hosts/hosts_navigations.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/hosts/hosts_navigations.tsx @@ -9,7 +9,6 @@ import { getOr } from 'lodash/fp'; import React from 'react'; import * as i18n from './translations'; -import { NavTab } from '../../components/navigation/type'; import { HostsTable, UncommonProcessTable } from '../../components/page/hosts'; import { HostsQuery } from '../../containers/hosts'; @@ -24,6 +23,7 @@ import { AuthenticationsQuery } from '../../containers/authentications'; import { ESTermQuery } from '../../../common/typed_json'; import { HostsTableType } from '../../store/hosts/model'; import { StatefulEventsViewer } from '../../components/events_viewer'; +import { NavTab } from '../../components/navigation/types'; const getTabsOnHostsUrl = (tabName: HostsTableType) => `#/hosts/${tabName}`; const getTabsOnHostDetailsUrl = (hostName: string, tabName: HostsTableType) => { diff --git a/x-pack/legacy/plugins/siem/public/pages/hosts/index.tsx b/x-pack/legacy/plugins/siem/public/pages/hosts/index.tsx index 415848337d9e9..36d9826c5d6a6 100644 --- a/x-pack/legacy/plugins/siem/public/pages/hosts/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/hosts/index.tsx @@ -5,15 +5,9 @@ */ import React from 'react'; -import { Redirect, Route, Switch } from 'react-router-dom'; -import { pure } from 'recompose'; -import { i18n } from '@kbn/i18n'; -import { PageRoute } from '../../components/page_route/pageroute'; +import { Redirect, Route, Switch, RouteComponentProps } from 'react-router-dom'; -import { HostComponentProps } from '../../components/link_to/redirect_to_hosts'; - -import { HostDetails, HostDetailsBody } from './host_details'; -import { Hosts } from './hosts'; +import { HostDetailsBody, HostDetails } from './details'; import { HostsQueryTabBody, AuthenticationsQueryTabBody, @@ -23,6 +17,8 @@ import { } from './hosts_navigations'; import { HostsBody } from './hosts_body'; import { HostsTableType } from '../../store/hosts/model'; +import { GlobalTime } from '../../containers/global_time'; +import { Hosts } from './hosts'; const hostsPagePath = `/:pageName(hosts)`; @@ -35,114 +31,199 @@ const getHostsTabPath = (pagePath: string) => `${HostsTableType.events})`; const getHostDetailsTabPath = (pagePath: string) => - `${pagePath}/:hostName/:tabName(` + + `${pagePath}/:detailName/:tabName(` + `${HostsTableType.authentications}|` + `${HostsTableType.uncommonProcesses}|` + `${HostsTableType.anomalies}|` + `${HostsTableType.events})`; -export const HostsContainer = pure(({ match }) => ( - <> - - ( - <> - +type Props = Partial> & { url: string }; + +export const HostsContainer = React.memo(({ url }) => ( + + {({ to, from, setQuery, isInitializing }) => ( + + ( } - /> - - )} - /> - ( - <> - - } - /> - } - /> - } - /> - } - /> - } - /> - - )} - /> - ( - <> - - } - /> - } - /> - } - /> - } - /> - } + render={() => ( + <> + + + + )} /> - - )} - /> - - - - + )} + /> + ( + <> + + ( + + )} + /> + ( + + )} + /> + ( + + )} + /> + ( + + )} + /> + ( + + )} + /> + + )} + /> + ( + <> + + ( + + )} + /> + ( + + )} + /> + ( + + )} + /> + ( + + )} + /> + ( + + )} + /> + + )} + /> + + + + )} + )); HostsContainer.displayName = 'HostsContainer'; diff --git a/x-pack/legacy/plugins/siem/public/pages/network/__snapshots__/ip_details.test.tsx.snap b/x-pack/legacy/plugins/siem/public/pages/network/__snapshots__/ip_details.test.tsx.snap index 633b68e9359be..71d799bbf7063 100644 --- a/x-pack/legacy/plugins/siem/public/pages/network/__snapshots__/ip_details.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/pages/network/__snapshots__/ip_details.test.tsx.snap @@ -2,6 +2,7 @@ exports[`Ip Details it matches the snapshot 1`] = ` (({ match }) => ( - <> - - ( - - )} - /> - ( - - )} - /> - - - + +type Props = Partial> & { url: string }; + +export const NetworkContainer = React.memo(() => ( + + } /> + } + /> + + )); NetworkContainer.displayName = 'NetworkContainer'; diff --git a/x-pack/legacy/plugins/siem/public/pages/network/ip_details.test.tsx b/x-pack/legacy/plugins/siem/public/pages/network/ip_details.test.tsx index edf042d7c7384..6f91d24207064 100644 --- a/x-pack/legacy/plugins/siem/public/pages/network/ip_details.test.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/network/ip_details.test.tsx @@ -70,7 +70,8 @@ const getMockProps = (ip: string) => ({ state: '', hash: '', }, - match: { params: { ip, search: '' }, isExact: true, path: '', url: '' }, + detailName: ip, + match: { params: { detailName: ip, search: '' }, isExact: true, path: '', url: '' }, setAbsoluteRangeDatePicker: (jest.fn() as unknown) as ActionCreator<{ id: InputsModelId; from: number; diff --git a/x-pack/legacy/plugins/siem/public/pages/network/ip_details.tsx b/x-pack/legacy/plugins/siem/public/pages/network/ip_details.tsx index dafa6f57b6b7a..6e723c1c83a78 100644 --- a/x-pack/legacy/plugins/siem/public/pages/network/ip_details.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/network/ip_details.tsx @@ -16,7 +16,7 @@ import { ActionCreator } from 'typescript-fsa'; import { FiltersGlobal } from '../../components/filters_global'; import { HeaderPage } from '../../components/header_page'; import { LastEventTime } from '../../components/last_event_time'; -import { getNetworkUrl, NetworkComponentProps } from '../../components/link_to/redirect_to_network'; +import { getNetworkUrl } from '../../components/link_to/redirect_to_network'; import { manageQuery } from '../../components/page/manage_query'; import { DomainsTable } from '../../components/page/network/domains_table'; import { FlowTargetSelectConnected } from '../../components/page/network/flow_target_select_connected'; @@ -42,6 +42,7 @@ import { InputsModelId } from '../../store/inputs/constants'; import { scoreIntervalToDateTime } from '../../components/ml/score/score_interval_to_datetime'; import { AnomaliesNetworkTable } from '../../components/ml/tables/anomalies_network_table'; import { networkToCriteria } from '../../components/ml/criteria/network_to_criteria'; +import { SpyRoute } from '../../utils/route/spy_routes'; const DomainsTableManage = manageQuery(DomainsTable); const TlsTableManage = manageQuery(TlsTable); @@ -58,212 +59,216 @@ interface IPDetailsComponentReduxProps { }>; } -export type IPDetailsComponentProps = IPDetailsComponentReduxProps & NetworkComponentProps; +export type IPDetailsComponentProps = IPDetailsComponentReduxProps & { detailName: string }; export const IPDetailsComponent = pure( - ({ - match: { - params: { ip }, - }, - filterQuery, - flowTarget, - setAbsoluteRangeDatePicker, - }) => ( - - {({ indicesExist, indexPattern }) => - indicesExistOrDataTemporarilyUnavailable(indicesExist) ? ( - - - - + ({ detailName, filterQuery, flowTarget, setAbsoluteRangeDatePicker }) => ( + <> + + {({ indicesExist, indexPattern }) => { + const ip = decodeIpv6(detailName); + return indicesExistOrDataTemporarilyUnavailable(indicesExist) ? ( + + + + - - } - title={decodeIpv6(ip)} - > - - + } + title={ip} + > + + - - {({ to, from, setQuery, isInitializing }) => ( - <> - - {({ id, inspect, ipOverviewData, loading, refetch }) => ( - - {({ isLoadingAnomaliesData, anomaliesData }) => ( - { - const fromTo = scoreIntervalToDateTime(score, interval); - setAbsoluteRangeDatePicker({ - id: 'global', - from: fromTo.from, - to: fromTo.to, - }); - }} - /> - )} - - )} - + + {({ to, from, setQuery, isInitializing }) => ( + <> + + {({ id, inspect, ipOverviewData, loading, refetch }) => ( + + {({ isLoadingAnomaliesData, anomaliesData }) => ( + { + const fromTo = scoreIntervalToDateTime(score, interval); + setAbsoluteRangeDatePicker({ + id: 'global', + from: fromTo.from, + to: fromTo.to, + }); + }} + /> + )} + + )} + - + - - {({ - id, - inspect, - domains, - totalCount, - pageInfo, - loading, - loadPage, - refetch, - }) => ( - - )} - + + {({ + id, + inspect, + domains, + totalCount, + pageInfo, + loading, + loadPage, + refetch, + }) => ( + + )} + - + - - {({ id, inspect, users, totalCount, pageInfo, loading, loadPage, refetch }) => ( - - )} - + + {({ + id, + inspect, + users, + totalCount, + pageInfo, + loading, + loadPage, + refetch, + }) => ( + + )} + - + - - {({ id, inspect, tls, totalCount, pageInfo, loading, loadPage, refetch }) => ( - - )} - + + {({ id, inspect, tls, totalCount, pageInfo, loading, loadPage, refetch }) => ( + + )} + - + - { - const fromTo = scoreIntervalToDateTime(score, interval); - setAbsoluteRangeDatePicker({ - id: 'global', - from: fromTo.from, - to: fromTo.to, - }); - }} - /> - - )} - - - ) : ( - <> - + { + const fromTo = scoreIntervalToDateTime(score, interval); + setAbsoluteRangeDatePicker({ + id: 'global', + from: fromTo.from, + to: fromTo.to, + }); + }} + /> + + )} + + + ) : ( + <> + - - - ) - } - + + + ); + }} + + + ) ); @@ -285,11 +290,11 @@ export const IPDetails = connect( } )(IPDetailsComponent); -export const getBreadcrumbs = (ip: string): Breadcrumb[] => { +export const getBreadcrumbs = (ip: string | undefined, search: string[]): Breadcrumb[] => { const breadcrumbs = [ { text: i18n.PAGE_TITLE, - href: getNetworkUrl(), + href: `${getNetworkUrl()}${search && search[0] ? search[0] : ''}`, }, ]; if (ip) { diff --git a/x-pack/legacy/plugins/siem/public/pages/network/network.tsx b/x-pack/legacy/plugins/siem/public/pages/network/network.tsx index 28f395e5cf303..061b1145cf5cd 100644 --- a/x-pack/legacy/plugins/siem/public/pages/network/network.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/network/network.tsx @@ -12,6 +12,7 @@ import { connect } from 'react-redux'; import { StickyContainer } from 'react-sticky'; import { ActionCreator } from 'typescript-fsa'; +import { RouteComponentProps } from 'react-router-dom'; import { FiltersGlobal } from '../../components/filters_global'; import { HeaderPage } from '../../components/header_page'; import { LastEventTime } from '../../components/last_event_time'; @@ -35,6 +36,7 @@ import { setAbsoluteRangeDatePicker as dispatchSetAbsoluteRangeDatePicker } from import { InputsModelId } from '../../store/inputs/constants'; import { EmbeddedMap } from '../../components/embeddables/embedded_map'; import { NetworkFilter } from '../../containers/network'; +import { SpyRoute } from '../../utils/route/spy_routes'; const NetworkTopNFlowTableManage = manageQuery(NetworkTopNFlowTable); const NetworkDnsTableManage = manageQuery(NetworkDnsTable); @@ -49,7 +51,7 @@ interface NetworkComponentReduxProps { }>; } -type NetworkComponentProps = NetworkComponentReduxProps; +type NetworkComponentProps = NetworkComponentReduxProps & Partial>; const mediaMatch = window.matchMedia( 'screen and (min-width: ' + euiLightVars.euiBreakpoints.xl + ')' ); @@ -73,8 +75,8 @@ export const getFlexDirection = () => { }; const NetworkComponent = React.memo( - ({ filterQuery, queryExpression, setAbsoluteRangeDatePicker }) => { - return ( + ({ filterQuery, queryExpression, setAbsoluteRangeDatePicker }) => ( + <> {({ indicesExist, indexPattern }) => indicesExistOrDataTemporarilyUnavailable(indicesExist) ? ( @@ -283,8 +285,9 @@ const NetworkComponent = React.memo( ) } - ); - } + + + ) ); NetworkComponent.displayName = 'NetworkComponent'; diff --git a/x-pack/legacy/plugins/siem/public/pages/overview/index.tsx b/x-pack/legacy/plugins/siem/public/pages/overview/index.tsx index 82f4d3e32b117..e0af54acde310 100644 --- a/x-pack/legacy/plugins/siem/public/pages/overview/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/overview/index.tsx @@ -4,11 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; -import { pure } from 'recompose'; +import React, { memo } from 'react'; import { OverviewComponent } from './overview'; -export const Overview = pure(() => ); +export const Overview = memo(() => ); Overview.displayName = 'Overview'; diff --git a/x-pack/legacy/plugins/siem/public/pages/overview/overview.test.tsx b/x-pack/legacy/plugins/siem/public/pages/overview/overview.test.tsx index 160379567133f..833030e0dc8a1 100644 --- a/x-pack/legacy/plugins/siem/public/pages/overview/overview.test.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/overview/overview.test.tsx @@ -5,15 +5,16 @@ */ import { mount } from 'enzyme'; +import { cloneDeep } from 'lodash/fp'; import * as React from 'react'; +import { MockedProvider } from 'react-apollo/test-utils'; +import { MemoryRouter } from 'react-router-dom'; import { Overview } from './index'; import '../../mock/ui_settings'; import { mocksSource } from '../../containers/source/mock'; import { TestProviders } from '../../mock'; -import { MockedProvider } from 'react-apollo/test-utils'; -import { cloneDeep } from 'lodash/fp'; jest.mock('ui/documentation_links', () => ({ documentationLinks: { @@ -45,7 +46,9 @@ describe('Overview', () => { const wrapper = mount( - + + + ); @@ -60,7 +63,9 @@ describe('Overview', () => { const wrapper = mount( - + + + ); diff --git a/x-pack/legacy/plugins/siem/public/pages/overview/overview.tsx b/x-pack/legacy/plugins/siem/public/pages/overview/overview.tsx index f13b48532dc14..d8965f4d49491 100644 --- a/x-pack/legacy/plugins/siem/public/pages/overview/overview.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/overview/overview.tsx @@ -19,6 +19,7 @@ import { GlobalTime } from '../../containers/global_time'; import { Summary } from './summary'; import { EmptyPage } from '../../components/empty_page'; import { WithSource, indicesExistOrDataTemporarilyUnavailable } from '../../containers/source'; +import { SpyRoute } from '../../utils/route/spy_routes'; import * as i18n from './translations'; @@ -65,6 +66,7 @@ export const OverviewComponent = pure(() => { ) } + ); }); diff --git a/x-pack/legacy/plugins/siem/public/pages/timelines/timelines_page.tsx b/x-pack/legacy/plugins/siem/public/pages/timelines/timelines_page.tsx index 942c5730e2222..adc5471cc37a7 100644 --- a/x-pack/legacy/plugins/siem/public/pages/timelines/timelines_page.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/timelines/timelines_page.tsx @@ -10,6 +10,7 @@ import styled from 'styled-components'; import { HeaderPage } from '../../components/header_page'; import { StatefulOpenTimeline } from '../../components/open_timeline'; +import { SpyRoute } from '../../utils/route/spy_routes'; import * as i18n from './translations'; @@ -41,6 +42,7 @@ export class TimelinesPage extends React.PureComponent { title={i18n.ALL_TIMELINES_PANEL_TITLE} /> + ); } diff --git a/x-pack/legacy/plugins/siem/public/routes.tsx b/x-pack/legacy/plugins/siem/public/routes.tsx index 37e56f4f067f7..9a132eb8d4fac 100644 --- a/x-pack/legacy/plugins/siem/public/routes.tsx +++ b/x-pack/legacy/plugins/siem/public/routes.tsx @@ -10,16 +10,19 @@ import { Route, Router, Switch } from 'react-router-dom'; import { NotFoundPage } from './pages/404'; import { HomePage } from './pages/home'; +import { ManageRoutesSpy } from './utils/route/manage_spy_routes'; interface RouterProps { history: History; } export const PageRouter: FC = memo(({ history }) => ( - - - - - - + + + + + + + + )); diff --git a/x-pack/legacy/plugins/siem/public/store/hosts/model.ts b/x-pack/legacy/plugins/siem/public/store/hosts/model.ts index 29469c129c23f..69efa404d2eee 100644 --- a/x-pack/legacy/plugins/siem/public/store/hosts/model.ts +++ b/x-pack/legacy/plugins/siem/public/store/hosts/model.ts @@ -14,7 +14,7 @@ export enum HostsType { export enum HostsTableType { authentications = 'authentications', - hosts = 'hosts', + hosts = 'allHosts', events = 'events', uncommonProcesses = 'uncommonProcesses', anomalies = 'anomalies', diff --git a/x-pack/legacy/plugins/siem/public/store/hosts/reducer.ts b/x-pack/legacy/plugins/siem/public/store/hosts/reducer.ts index 80d7876902203..a597386942cb1 100644 --- a/x-pack/legacy/plugins/siem/public/store/hosts/reducer.ts +++ b/x-pack/legacy/plugins/siem/public/store/hosts/reducer.ts @@ -106,8 +106,8 @@ export const hostsReducer = reducerWithInitialState(initialHostsState) ...state[hostsType], queries: { ...state[hostsType].queries, - hosts: { - ...state[hostsType].queries.hosts, + [HostsTableType.hosts]: { + ...state[hostsType].queries[HostsTableType.hosts], direction: sort.direction, sortField: sort.field, }, diff --git a/x-pack/legacy/plugins/siem/public/store/hosts/selectors.ts b/x-pack/legacy/plugins/siem/public/store/hosts/selectors.ts index dcd6cd8e67006..a4cf0715ef6da 100644 --- a/x-pack/legacy/plugins/siem/public/store/hosts/selectors.ts +++ b/x-pack/legacy/plugins/siem/public/store/hosts/selectors.ts @@ -10,7 +10,7 @@ import { createSelector } from 'reselect'; import { isFromKueryExpressionValid } from '../../lib/keury'; import { State } from '../reducer'; -import { GenericHostsModel, HostsType } from './model'; +import { GenericHostsModel, HostsType, HostsTableType } from './model'; const selectHosts = (state: State, hostsType: HostsType): GenericHostsModel => get(hostsType, state.hosts); @@ -24,7 +24,7 @@ export const authenticationsSelector = () => export const hostsSelector = () => createSelector( selectHosts, - hosts => hosts.queries.hosts + hosts => hosts.queries[HostsTableType.hosts] ); export const eventsSelector = () => diff --git a/x-pack/legacy/plugins/siem/public/utils/route/helpers.ts b/x-pack/legacy/plugins/siem/public/utils/route/helpers.ts new file mode 100644 index 0000000000000..188ae9c6c1866 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/utils/route/helpers.ts @@ -0,0 +1,23 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { noop } from 'lodash/fp'; +import { createContext, Dispatch } from 'react'; + +import { RouteSpyState, RouteSpyAction } from './types'; + +export const initRouteSpy: RouteSpyState = { + pageName: '', + detailName: undefined, + tabName: undefined, + search: '', + pathName: '/', +}; + +export const RouterSpyStateContext = createContext<[RouteSpyState, Dispatch]>([ + initRouteSpy, + () => noop, +]); diff --git a/x-pack/legacy/plugins/siem/public/utils/route/index.test.tsx b/x-pack/legacy/plugins/siem/public/utils/route/index.test.tsx new file mode 100644 index 0000000000000..bcc256d50d960 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/utils/route/index.test.tsx @@ -0,0 +1,202 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { mount } from 'enzyme'; +import React from 'react'; + +import { HostsTableType } from '../../store/hosts/model'; +import { RouteSpyState } from './types'; +import { ManageRoutesSpy } from './manage_spy_routes'; +import { SpyRouteComponent } from './spy_routes'; +import { useRouteSpy } from './use_route_spy'; + +type Action = 'PUSH' | 'POP' | 'REPLACE'; +const pop: Action = 'POP'; + +const defaultLocation = { + hash: '', + pathname: '/hosts', + search: '', + state: '', +}; + +export const mockHistory = { + action: pop, + block: jest.fn(), + createHref: jest.fn(), + go: jest.fn(), + goBack: jest.fn(), + goForward: jest.fn(), + length: 2, + listen: jest.fn(), + location: defaultLocation, + push: jest.fn(), + replace: jest.fn(), +}; + +const dispatchMock = jest.fn(); +const mockRoutes: RouteSpyState = { + pageName: '', + detailName: undefined, + tabName: undefined, + search: '', + pathName: '/', + history: mockHistory, +}; + +const mockUseRouteSpy: jest.Mock = useRouteSpy as jest.Mock; +jest.mock('./use_route_spy', () => ({ + useRouteSpy: jest.fn(), +})); + +describe('Spy Routes', () => { + describe('At Initialization of the app', () => { + beforeEach(() => { + dispatchMock.mockReset(); + dispatchMock.mockClear(); + }); + test('Make sure we update search state first', () => { + const pathname = '/'; + mockUseRouteSpy.mockImplementation(() => [mockRoutes, dispatchMock]); + mount( + + + + ); + + expect(dispatchMock.mock.calls[0]).toEqual([ + { + type: 'updateSearch', + search: '?importantQueryString="really"', + }, + ]); + }); + + test('Make sure we update search state first and then update the route but keeping the initial search', () => { + const pathname = '/hosts/allHosts'; + mockUseRouteSpy.mockImplementation(() => [mockRoutes, dispatchMock]); + mount( + + + + ); + + expect(dispatchMock.mock.calls[0]).toEqual([ + { + type: 'updateSearch', + search: '?importantQueryString="really"', + }, + ]); + + expect(dispatchMock.mock.calls[1]).toEqual([ + { + route: { + detailName: undefined, + history: mockHistory, + pageName: 'hosts', + pathName: pathname, + tabName: HostsTableType.hosts, + }, + type: 'updateRouteWithOutSearch', + }, + ]); + }); + }); + + describe('When app is running', () => { + beforeEach(() => { + dispatchMock.mockReset(); + dispatchMock.mockClear(); + }); + test('Update route should be updated when there is changed detected', () => { + const pathname = '/hosts/allHosts'; + const newPathname = `hosts/${HostsTableType.authentications}`; + mockUseRouteSpy.mockImplementation(() => [mockRoutes, dispatchMock]); + const wrapper = mount( + + ); + + dispatchMock.mockReset(); + dispatchMock.mockClear(); + + wrapper.setProps({ + location: { + hash: '', + pathname: newPathname, + search: '?updated="true"', + state: '', + }, + match: { + isExact: false, + path: newPathname, + url: newPathname, + params: { + pageName: 'hosts', + detailName: undefined, + tabName: HostsTableType.authentications, + search: '', + }, + }, + }); + wrapper.update(); + expect(dispatchMock.mock.calls[0]).toEqual([ + { + route: { + detailName: undefined, + history: mockHistory, + pageName: 'hosts', + pathName: newPathname, + tabName: HostsTableType.authentications, + search: '?updated="true"', + }, + type: 'updateRoute', + }, + ]); + }); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/utils/route/manage_spy_routes.tsx b/x-pack/legacy/plugins/siem/public/utils/route/manage_spy_routes.tsx new file mode 100644 index 0000000000000..87b40c565c758 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/utils/route/manage_spy_routes.tsx @@ -0,0 +1,31 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { memo, useReducer } from 'react'; + +import { ManageRoutesSpyProps, RouteSpyState, RouteSpyAction } from './types'; +import { RouterSpyStateContext, initRouteSpy } from './helpers'; + +export const ManageRoutesSpy = memo(({ children }: ManageRoutesSpyProps) => { + const reducerSpyRoute = (state: RouteSpyState, action: RouteSpyAction) => { + switch (action.type) { + case 'updateRoute': + return action.route; + case 'updateRouteWithOutSearch': + return { ...state, ...action.route }; + case 'updateSearch': + return { ...state, search: action.search }; + default: + return state; + } + }; + + return ( + + {children} + + ); +}); diff --git a/x-pack/legacy/plugins/siem/public/utils/route/spy_routes.tsx b/x-pack/legacy/plugins/siem/public/utils/route/spy_routes.tsx new file mode 100644 index 0000000000000..3a02d81272344 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/utils/route/spy_routes.tsx @@ -0,0 +1,68 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import * as H from 'history'; +import { isEqual } from 'lodash/fp'; +import { memo, useEffect, useState } from 'react'; +import { withRouter } from 'react-router-dom'; + +import { SpyRouteProps } from './types'; +import { useRouteSpy } from './use_route_spy'; + +export const SpyRouteComponent = memo( + ({ + location: { pathname, search }, + history, + match: { + params: { pageName, detailName, tabName }, + }, + }) => { + const [isInitializing, setIsInitializing] = useState(true); + const [route, dispatch] = useRouteSpy(); + + useEffect(() => { + if (isInitializing && search !== '') { + dispatch({ + type: 'updateSearch', + search, + }); + setIsInitializing(false); + } + }, [search]); + useEffect(() => { + if (pageName && !isEqual(route.pathName, pathname)) { + if (isInitializing && detailName == null) { + dispatch({ + type: 'updateRouteWithOutSearch', + route: { + pageName, + detailName, + tabName, + pathName: pathname, + history, + }, + }); + setIsInitializing(false); + } else { + dispatch({ + type: 'updateRoute', + route: { + pageName, + detailName, + tabName, + search, + pathName: pathname, + history, + }, + }); + } + } + }, [pathname, search, pageName, detailName, tabName]); + return null; + } +); + +export const SpyRoute = withRouter(SpyRouteComponent); diff --git a/x-pack/legacy/plugins/siem/public/utils/route/types.ts b/x-pack/legacy/plugins/siem/public/utils/route/types.ts new file mode 100644 index 0000000000000..62f6b67df245f --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/utils/route/types.ts @@ -0,0 +1,45 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import * as H from 'history'; +import React from 'react'; +import { RouteComponentProps } from 'react-router-dom'; + +import { HostsTableType } from '../../store/hosts/model'; + +export interface RouteSpyState { + pageName: string; + detailName: string | undefined; + tabName: HostsTableType | undefined; + search: string; + pathName: string; + history?: H.History; +} + +export type RouteSpyAction = + | { + type: 'updateSearch'; + search: string; + } + | { + type: 'updateRouteWithOutSearch'; + route: Pick; + } + | { + type: 'updateRoute'; + route: RouteSpyState; + }; + +export interface ManageRoutesSpyProps { + children: React.ReactNode; +} + +export type SpyRouteProps = RouteComponentProps<{ + pageName: string | undefined; + detailName: string | undefined; + tabName: HostsTableType | undefined; + search: string; +}>; diff --git a/x-pack/legacy/plugins/siem/public/utils/route/use_route_spy.tsx b/x-pack/legacy/plugins/siem/public/utils/route/use_route_spy.tsx new file mode 100644 index 0000000000000..ce988df1c9d2f --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/utils/route/use_route_spy.tsx @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { useContext } from 'react'; +import { RouterSpyStateContext } from './helpers'; + +export const useRouteSpy = () => useContext(RouterSpyStateContext);